deepagents-graph-memory 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,193 @@
1
+ # - Gives the agent callable tools to find context and record what happened during its work.
2
+ # - Can also offer tools for adding individual graph items and connections.
3
+ # - Tests: test_tools.py checks which tools are available and that writes work;
4
+ # test_trace.py and test_recall.py check the history and lookup tools.
5
+
6
+ """Safe graph memory tools."""
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ from collections.abc import Sequence
12
+ from typing import Any, Literal
13
+
14
+ from langchain.tools import ToolRuntime
15
+ from langchain_core.tools import tool
16
+
17
+ from deepagents_graph_memory.backend import GraphMemoryBackend
18
+ from deepagents_graph_memory.errors import GraphMemoryError, GraphMemoryValidationError
19
+ from deepagents_graph_memory.paths import validate_subject
20
+ from deepagents_graph_memory.recall import RecallMode
21
+
22
+
23
+ def graph_memory_tools(graph_backend: GraphMemoryBackend, *, include_low_level_writes: bool = False, bound_subject: str | None = None) -> list[Any]:
24
+ """Create safe graph memory recall and write tools.
25
+
26
+ Args:
27
+ graph_backend: Graph memory backend to mutate.
28
+ include_low_level_writes: When true, expose generic node, edge, and
29
+ graph-document write tools. Keep this false for unconstrained agent
30
+ use; prefer domain-specific tools or `record_graph_trace`.
31
+ bound_subject: Optional subject all trace writes from these tools must use.
32
+
33
+ Returns:
34
+ LangChain tool objects for controlled graph writes.
35
+ """
36
+ if bound_subject is not None:
37
+ bound_subject = validate_subject(bound_subject)
38
+
39
+ @tool
40
+ def recall_graph_memory(
41
+ query: str,
42
+ anchors: list[str] | None = None,
43
+ mode: RecallMode = "auto",
44
+ token_budget: int = 2000,
45
+ max_depth: int = 3,
46
+ max_nodes: int = 50,
47
+ max_edges: int = 100,
48
+ ) -> str:
49
+ """Recall relevant graph memory: entities, relationships, prior reasoning traces, and connected context.
50
+
51
+ When reporting state changes from retrieved traces, distinguish what actually changed from what the agent did in response.
52
+ A switch in tool or action is not the same as the underlying condition changing.
53
+ If the same outcome recurs across multiple steps, the state changed once when it first appeared and stayed unchanged afterward.
54
+ """
55
+ try:
56
+ return graph_backend.recall_graph_memory(
57
+ query,
58
+ anchors=anchors,
59
+ mode=mode,
60
+ token_budget=token_budget,
61
+ max_depth=max_depth,
62
+ max_nodes=max_nodes,
63
+ max_edges=max_edges,
64
+ )
65
+ except GraphMemoryError as exc:
66
+ return f"Error: {exc}"
67
+
68
+ @tool
69
+ def add_graph_node(label: str, node_id: str, properties: dict[str, Any] | None = None) -> str:
70
+ """Add or update a graph node.
71
+
72
+ Use this for entity memory such as any named thing in the workflow's domain.
73
+ """
74
+ try:
75
+ graph_backend.add_graph_node(label, node_id, properties, source="graph_memory_tool")
76
+ except GraphMemoryError as exc:
77
+ return f"Error: {exc}"
78
+ return f"Added graph node {label}/{node_id}."
79
+
80
+ @tool
81
+ def add_graph_edge(
82
+ source_label: str,
83
+ source_id: str,
84
+ relationship: str,
85
+ target_label: str,
86
+ target_id: str,
87
+ properties: dict[str, Any] | None = None,
88
+ ) -> str:
89
+ """Add or update a directed graph relationship.
90
+
91
+ Use this for relationship memory such as any directed connection between two entities.
92
+ """
93
+ try:
94
+ graph_backend.add_graph_edge(
95
+ source_label,
96
+ source_id,
97
+ relationship,
98
+ target_label,
99
+ target_id,
100
+ properties,
101
+ source="graph_memory_tool",
102
+ )
103
+ except GraphMemoryError as exc:
104
+ return f"Error: {exc}"
105
+ return f"Added graph edge {source_label}/{source_id} -[{relationship}]-> {target_label}/{target_id}."
106
+
107
+ @tool
108
+ def add_graph_documents(documents: list[Any]) -> str:
109
+ """Add LangChain graph documents to graph memory."""
110
+ try:
111
+ graph_backend.add_graph_documents(cast_documents(documents))
112
+ except GraphMemoryError as exc:
113
+ return f"Error: {exc}"
114
+ return f"Added {len(documents)} graph document(s)."
115
+
116
+ @tool
117
+ def record_graph_trace(
118
+ situation: str,
119
+ rationale: str,
120
+ action: str,
121
+ outcome: str,
122
+ artifacts: list[str] | None = None,
123
+ evidence: list[str] | None = None,
124
+ evidence_refs: list[dict[str, str]] | None = None,
125
+ run_id: str | None = None,
126
+ agent_id: str | None = None,
127
+ subagent_id: str | None = None,
128
+ task_id: str | None = None,
129
+ subject: str | None = None,
130
+ observed_at: str | None = None,
131
+ supersedes: list[str] | None = None,
132
+ resolves: list[str] | None = None,
133
+ depends_on: list[str] | None = None,
134
+ finding_type: Literal["state", "interpretation"] = "interpretation",
135
+ operation_id: str | None = None,
136
+ runtime: ToolRuntime = None, # This installed ToolNode injects ToolRuntime, but not ToolRuntime | None.
137
+ ) -> str:
138
+ """Record a Situation/Rationale/Action/Outcome trace for long-running agent work."""
139
+ try:
140
+ if bound_subject is not None:
141
+ if subject is not None and validate_subject(subject) != bound_subject:
142
+ raise GraphMemoryValidationError("subject differs from the bound subject.")
143
+ subject = bound_subject
144
+ if operation_id is None and runtime is not None and runtime.tool_call_id:
145
+ thread_id = runtime.config.get("configurable", {}).get("thread_id")
146
+ operation_id = json.dumps(["tool-call", thread_id, runtime.tool_call_id], separators=(",", ":"))
147
+ trace_id = graph_backend.record_graph_trace(
148
+ situation=situation,
149
+ rationale=rationale,
150
+ action=action,
151
+ outcome=outcome,
152
+ artifacts=artifacts,
153
+ evidence=evidence,
154
+ evidence_refs=evidence_refs,
155
+ run_id=run_id,
156
+ agent_id=agent_id,
157
+ subagent_id=subagent_id,
158
+ task_id=task_id,
159
+ subject=subject,
160
+ observed_at=observed_at,
161
+ supersedes=supersedes,
162
+ resolves=resolves,
163
+ depends_on=depends_on,
164
+ finding_type=finding_type,
165
+ operation_id=operation_id,
166
+ source="graph_trace_tool",
167
+ )
168
+ except GraphMemoryError as exc:
169
+ return f"Error: {exc}"
170
+ return f"Recorded graph trace {trace_id}."
171
+
172
+ tools = [recall_graph_memory, record_graph_trace]
173
+ if include_low_level_writes:
174
+ tools.extend([add_graph_node, add_graph_edge, add_graph_documents])
175
+ return tools
176
+
177
+
178
+ def cast_documents(documents: Sequence[Any]) -> Sequence[Any]:
179
+ """Validate that a graph document payload is list-like.
180
+
181
+ Args:
182
+ documents: Graph documents.
183
+
184
+ Returns:
185
+ The original graph document sequence.
186
+
187
+ Raises:
188
+ GraphMemoryError: If the payload is not list-like.
189
+ """
190
+ if not isinstance(documents, list):
191
+ msg = "documents must be a list."
192
+ raise GraphMemoryError(msg)
193
+ return documents
@@ -0,0 +1,210 @@
1
+ # - Sets up Deep Agents to work through graph tools and explains how to use that context.
2
+ # - Adds graph guidance while preserving application-supplied instructions.
3
+ # - Tests: test_vgs.py and test_combined_context.py check prompt and tool behavior.
4
+
5
+ """Graph context guidance for Deep Agents."""
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Awaitable, Callable
10
+ from typing import Any, cast
11
+
12
+ from deepagents import HarnessProfile, register_harness_profile
13
+ from deepagents.middleware import filesystem
14
+ from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
15
+ from langchain_core.messages import AIMessage, SystemMessage
16
+
17
+ VFS_TOOL_NAMES = frozenset({"ls", "read_file", "write_file", "edit_file", "delete", "glob", "grep"})
18
+ """Deep Agents default virtual-filesystem tool names."""
19
+
20
+ _GRAPH_CONTEXT_INTRO = """## Virtual Graph System (VGS)
21
+
22
+ Graph context is enabled. The graph is the source of truth for recorded structured workflow context:
23
+ situations, rationales, actions, outcomes, artifacts, failures, evidence, decisions,
24
+ dependencies, and provenance. Its contents are recorded claims and evidence, not verified truth.
25
+ """
26
+
27
+ _COMMON_GRAPH_GUIDANCE = """## Reading Graph Context
28
+
29
+ - Use `recall_graph_memory` as the primary read path for graph context.
30
+ - Ask targeted questions. Include anchors such as graph source paths, file paths, run ids,
31
+ task ids, artifact ids, or subagent ids when you have them.
32
+ - Start with the default `auto` mode and modest budgets. Use `local` for one known entity
33
+ and `deep` only when the task clearly needs multi-hop context.
34
+ - Stop querying when recall returns enough connected context to act, returns no seeds,
35
+ repeats the same facts, reports that the next hop was not relevant, or reaches a node,
36
+ edge, depth, or token budget.
37
+ - Do not chase the graph to completeness. Use recalled facts to guide the next action,
38
+ then verify current external state with the available primary tools when the answer
39
+ depends on live files, tests, services, or command output.
40
+ - Compare findings linked to the same subject before a decision depends on them. Check
41
+ revisions, inputs, environments, and observation times to distinguish state changes
42
+ from competing explanations. If material disagreement remains, pause that dependent
43
+ decision and verify the disputed point with a targeted independent check.
44
+ - Truncated related findings are not evidence that the visible claims agree. Fetch or
45
+ narrow context before treating them as a resolved answer.
46
+ - A conclusion marked `needs recheck` relied on a changed premise. Inspect the
47
+ premise and its update or resolution, then verify current external state before
48
+ relying on that conclusion. `Dependency status unknown` also requires a narrower
49
+ recall or direct source check; it does not mean the conclusion is current.
50
+
51
+ ## Writing Graph Context
52
+
53
+ - Use `record_graph_trace` for durable workflow events, especially meaningful observations,
54
+ decisions, actions, failures, outcomes, artifacts, and evidence.
55
+ - Do not write every thought. Prefer facts that will help resume work, avoid repeated
56
+ failed attempts, explain a decision, or connect evidence to an outcome.
57
+ - Record failures and dead ends with their outcomes so future work can avoid repeating them.
58
+ - When citing captured output, pass `evidence_refs` with a source ID assigned at collection,
59
+ a locator, and optional revision, observed_at, or your summary. Reuse the ID for the
60
+ same captured source across workers; give a separate execution a new ID even if its
61
+ output text matches. These are citations, not votes or proof of independent checks.
62
+ Plain `evidence` strings remain useful but do not identify a shared source.
63
+ - For related findings, supply a stable, narrow `subject` within the project namespace.
64
+ Reuse the exact subject key supplied by the parent or application for the same
65
+ question. When a write tool has a bound subject, omit `subject` in the call. Do
66
+ not invent a new key for another report of that question. Keep changing revisions
67
+ in trace context or evidence, and use separate subjects for environments whose
68
+ states should not be compared as one question.
69
+ Supply `observed_at` only from known evidence or tool output; do not guess from the
70
+ recording clock. Use `finding_type="state"` for mutable observed state and
71
+ `"interpretation"` for explanations. Use `supersedes` only for an evidenced newer
72
+ mutable state. Arrival order and elapsed time alone never supersede a finding;
73
+ preserve parallel contenders until evidence resolves them.
74
+ - After checking competing claims, record an evidenced resolution with `resolves`
75
+ pointing to at least two same-subject traces. Explain the review in the rationale
76
+ and evidence. This records a judgment; it does not make the graph verify truth.
77
+ - When a decision relies on recorded findings, pass their Trace IDs in `depends_on`.
78
+ Rechecking creates a new trace that cites the findings actually used; keep the
79
+ original decision as history.
80
+ - Do not store ordinary user preferences, profile facts, or unrelated notes in the graph.
81
+ - Generated `/graph/...` markdown paths are read-only views over graph data, not storage locations to edit.
82
+ - If low-level graph write tools are exposed, use them only with clear labels, relationship
83
+ names, scope/provenance metadata, and schema discipline. Do not create arbitrary node or
84
+ edge types just because a fact could be represented.
85
+ """
86
+
87
+ VGS_SYSTEM_PROMPT_SUFFIX = (
88
+ _GRAPH_CONTEXT_INTRO + "\nIn VGS mode, do not assume the default Deep Agents filesystem tools are available.\n\n" + _COMMON_GRAPH_GUIDANCE
89
+ )
90
+ """Default VGS system prompt."""
91
+
92
+ GRAPH_CONTEXT_SYSTEM_PROMPT_SUFFIX = (
93
+ _GRAPH_CONTEXT_INTRO
94
+ + """
95
+ The virtual filesystem holds raw artifacts, logs, large tool results, normal memory,
96
+ skills, and preferences. Keep selective linked findings and decisions in the graph;
97
+ do not copy every file, read, or edit into it. Graph persistence does not preserve
98
+ the source files cited by a trace.
99
+
100
+ Choose the first tool for the task: open a current file for direct file work; recall
101
+ history or dependencies for context questions. Read the actual source and current
102
+ revision when needed. Save captured evidence before citing its source ID, locator,
103
+ revision, or observation time. If a source is missing, inaccessible, or changed,
104
+ the recorded finding remains historical and unverified against the current source.
105
+
106
+ File writes and graph writes are separate. If trace recording fails, retry with the
107
+ same operation identity without repeating a successful external action.
108
+
109
+ """
110
+ + _COMMON_GRAPH_GUIDANCE
111
+ )
112
+
113
+
114
+ class _VGSSystemPromptMiddleware(AgentMiddleware[Any, Any, Any]):
115
+ """Add graph guidance while preserving application-supplied instructions."""
116
+
117
+ def __init__(self, system_prompt: str, *, strip_filesystem_guidance: bool = True) -> None:
118
+ self.system_prompt = system_prompt
119
+ self.strip_filesystem_guidance = strip_filesystem_guidance
120
+
121
+ def wrap_model_call(
122
+ self,
123
+ request: ModelRequest[Any],
124
+ handler: Callable[[ModelRequest[Any]], ModelResponse[Any]],
125
+ ) -> ModelResponse[Any] | AIMessage:
126
+ """Apply VGS prompt guidance before the model call."""
127
+ return handler(
128
+ request.override(system_message=_apply_vgs_system_text(request.system_message, self.system_prompt, self.strip_filesystem_guidance)),
129
+ )
130
+
131
+ async def awrap_model_call(
132
+ self,
133
+ request: ModelRequest[Any],
134
+ handler: Callable[[ModelRequest[Any]], Awaitable[ModelResponse[Any]]],
135
+ ) -> ModelResponse[Any] | AIMessage:
136
+ """Async variant of `wrap_model_call`."""
137
+ return await handler(
138
+ request.override(system_message=_apply_vgs_system_text(request.system_message, self.system_prompt, self.strip_filesystem_guidance)),
139
+ )
140
+
141
+
142
+ def graph_context_middleware() -> AgentMiddleware[Any, Any, Any]:
143
+ """Add agent-local guidance for using graph context with Deep Agents files."""
144
+ return _VGSSystemPromptMiddleware(GRAPH_CONTEXT_SYSTEM_PROMPT_SUFFIX, strip_filesystem_guidance=False)
145
+
146
+
147
+ def vgs_harness_profile(*, system_prompt_suffix: str | None = VGS_SYSTEM_PROMPT_SUFFIX) -> HarnessProfile:
148
+ """Create a Deep Agents harness profile for VGS mode.
149
+
150
+ Args:
151
+ system_prompt_suffix: Optional VGS prompt text appended through middleware.
152
+
153
+ Returns:
154
+ Harness profile that excludes Deep Agents filesystem tools.
155
+ """
156
+ extra_middleware = () if system_prompt_suffix is None else (_VGSSystemPromptMiddleware(system_prompt_suffix),)
157
+ return HarnessProfile(excluded_tools=VFS_TOOL_NAMES, extra_middleware=extra_middleware)
158
+
159
+
160
+ def register_vgs_harness_profile(model: str, *, system_prompt_suffix: str | None = VGS_SYSTEM_PROMPT_SUFFIX) -> None:
161
+ """Register VGS mode for a Deep Agents model key.
162
+
163
+ Args:
164
+ model: Model key passed to `create_deep_agent`.
165
+ system_prompt_suffix: Optional VGS prompt text.
166
+ """
167
+ register_harness_profile(model, vgs_harness_profile(system_prompt_suffix=system_prompt_suffix))
168
+
169
+
170
+ def _apply_vgs_system_text(system_message: SystemMessage | None, text: str, strip_filesystem_guidance: bool) -> SystemMessage:
171
+ if system_message is None:
172
+ content_blocks = []
173
+ elif isinstance(system_message.content, str):
174
+ content_blocks = [{"type": "text", "text": system_message.content}]
175
+ else:
176
+ content_blocks = list(system_message.content)
177
+ if strip_filesystem_guidance:
178
+ content_blocks = _remove_legacy_filesystem_guidance(content_blocks)
179
+ if content_blocks:
180
+ text = f"\n\n{text}"
181
+ content_blocks.append({"type": "text", "text": text})
182
+ content = cast("list[str | dict[str, str]]", content_blocks)
183
+ return system_message.model_copy(update={"content": content}) if system_message else SystemMessage(content=content)
184
+
185
+
186
+ def _remove_legacy_filesystem_guidance(content_blocks: list[Any]) -> list[Any]:
187
+ # Deep Agents 0.7 puts this guidance in tool descriptions, not system prompts.
188
+ if not getattr(filesystem, "FILESYSTEM_SYSTEM_PROMPT", None):
189
+ return content_blocks
190
+ execution_prompt = getattr(filesystem, "EXECUTION_SYSTEM_PROMPT", "")
191
+ result = []
192
+ for block in content_blocks:
193
+ text = block.get("text") if isinstance(block, dict) and block.get("type") == "text" else None
194
+ if not isinstance(text, str):
195
+ result.append(block)
196
+ continue
197
+ stripped = text.strip()
198
+ _, separator, execution = stripped.partition("## Execute Tool `execute`")
199
+ execution = separator + execution
200
+ if not (
201
+ stripped.startswith("## Following Conventions")
202
+ and "## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`" in stripped
203
+ and "## Large Tool Results" in stripped
204
+ and "Offloaded tool results are stored under " in stripped
205
+ and (not execution or execution == execution_prompt)
206
+ ):
207
+ result.append(block)
208
+ elif execution:
209
+ result.append({**block, "text": text[: len(text) - len(text.lstrip())] + execution})
210
+ return result