agentgraph-server 0.8.0__tar.gz → 0.9.0__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.
Files changed (117) hide show
  1. agentgraph_server-0.9.0/.agents/skills/agentgraph/SKILL.md +54 -0
  2. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/.agents/skills/agentgraph/references/commands.md +5 -0
  3. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/.agents/skills/agentgraph/references/operations.md +28 -14
  4. {agentgraph_server-0.8.0/agentgraph_server.egg-info → agentgraph_server-0.9.0}/PKG-INFO +14 -14
  5. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/README.md +1 -1
  6. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/backend.py +99 -31
  7. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/cli.py +11 -4
  8. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/cli_query.py +16 -1
  9. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/connectors/base.py +2 -0
  10. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/connectors/command_effects.py +10 -0
  11. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/connectors/feed.py +12 -1
  12. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/connectors/status.py +1 -1
  13. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/core/storage.py +26 -0
  14. agentgraph_server-0.9.0/agentgraph/graph/delete.py +102 -0
  15. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/fetch.py +3 -1
  16. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/upsert.py +7 -2
  17. agentgraph_server-0.9.0/agentgraph/mcp/models.py +131 -0
  18. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/mcp/server.py +264 -177
  19. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/query_client.py +14 -1
  20. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/graph_api.py +10 -1
  21. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/meta_api.py +2 -1
  22. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/static/viewer.html +175 -22
  23. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0/agentgraph_server.egg-info}/PKG-INFO +14 -14
  24. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/SOURCES.txt +1 -0
  25. agentgraph_server-0.9.0/agentgraph_server.egg-info/requires.txt +38 -0
  26. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/pyproject.toml +13 -13
  27. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_auth.py +8 -9
  28. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_browse.py +21 -0
  29. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_cli.py +69 -21
  30. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_connector_discovery.py +16 -3
  31. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_docs_build.py +15 -6
  32. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_expiration.py +36 -0
  33. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_fetch.py +17 -2
  34. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_mcp_transport.py +17 -4
  35. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_observed_since.py +10 -1
  36. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_query.py +246 -89
  37. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_query_client.py +2 -2
  38. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_release_metadata.py +2 -2
  39. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_route_resolution.py +1 -0
  40. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_rss_connector.py +197 -230
  41. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_schema.py +51 -0
  42. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_upsert.py +44 -0
  43. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_viewer_layout_contract.py +75 -0
  44. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_web_connector.py +23 -129
  45. agentgraph_server-0.8.0/.agents/skills/agentgraph/SKILL.md +0 -113
  46. agentgraph_server-0.8.0/agentgraph/graph/delete.py +0 -40
  47. agentgraph_server-0.8.0/agentgraph_server.egg-info/requires.txt +0 -38
  48. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/.agents/skills/agentgraph/references/data-model.md +0 -0
  49. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/.agents/skills/slack-auth/SKILL.md +0 -0
  50. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/LICENSE +0 -0
  51. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/__init__.py +0 -0
  52. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/auth/__init__.py +0 -0
  53. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/auth/credentials.py +0 -0
  54. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/backends/__init__.py +0 -0
  55. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/__init__.py +0 -0
  56. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/schema.sql +0 -0
  57. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/backends/sqlite/vector.py +0 -0
  58. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/cli_sync.py +0 -0
  59. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/config.py +0 -0
  60. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/connectors/__init__.py +0 -0
  61. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/connectors/match_patterns.py +0 -0
  62. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/connectors/registry.py +0 -0
  63. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/core/__init__.py +0 -0
  64. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/core/context.py +0 -0
  65. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/core/runtime.py +0 -0
  66. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/demo.py +0 -0
  67. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/demo_fixtures/reliable-webhooks.md +0 -0
  68. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/demo_fixtures/retry-guidance.md +0 -0
  69. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/__init__.py +0 -0
  70. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/bookmark.py +0 -0
  71. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/download.py +0 -0
  72. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/embeddings.py +0 -0
  73. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/expiration.py +0 -0
  74. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/link.py +0 -0
  75. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/operations.py +0 -0
  76. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/person.py +0 -0
  77. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/graph/query.py +0 -0
  78. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/logging.py +0 -0
  79. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/mcp/__init__.py +0 -0
  80. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/perf.py +0 -0
  81. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/__init__.py +0 -0
  82. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/app.py +0 -0
  83. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/browse_api.py +0 -0
  84. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/observation.py +0 -0
  85. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/presentation.py +0 -0
  86. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/router.py +0 -0
  87. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/sync.py +0 -0
  88. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/sync_api.py +0 -0
  89. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/server/uds.py +0 -0
  90. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph/skills.py +0 -0
  91. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/dependency_links.txt +0 -0
  92. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/entry_points.txt +0 -0
  93. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/agentgraph_server.egg-info/top_level.txt +0 -0
  94. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/setup.cfg +0 -0
  95. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_auth_status_methods.py +0 -0
  96. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_benchmarks.py +0 -0
  97. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_command_effects.py +0 -0
  98. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_config.py +0 -0
  99. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_connectors.py +0 -0
  100. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_discord_attachments.py +0 -0
  101. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_embeddings.py +0 -0
  102. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_entity_types.py +0 -0
  103. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_feed_connectors.py +0 -0
  104. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_graph_operations.py +0 -0
  105. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_launch_demo.py +0 -0
  106. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_logging.py +0 -0
  107. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_match_patterns.py +0 -0
  108. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_observation.py +0 -0
  109. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_query_parity.py +0 -0
  110. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_registry.py +0 -0
  111. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_router.py +0 -0
  112. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_server_imports.py +0 -0
  113. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_server_lifespan.py +0 -0
  114. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_server_uds.py +0 -0
  115. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_slack_auth_artifacts.py +0 -0
  116. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_slack_oauth.py +0 -0
  117. {agentgraph_server-0.8.0 → agentgraph_server-0.9.0}/tests/test_sync.py +0 -0
@@ -0,0 +1,54 @@
1
+ ---
2
+ name: agentgraph
3
+ description: Use AgentGraph through its CLI or MCP tools to search and traverse selected local context, retrieve full source-backed evidence, fetch missing or stale resources, and troubleshoot connector availability.
4
+ ---
5
+
6
+ # AgentGraph skill
7
+
8
+ AgentGraph is a local graph of selected messages, documents, people, feeds, pages,
9
+ and relationships. Use its CLI or connected MCP tools to investigate that context;
10
+ do not query its database or connector internals directly.
11
+
12
+ Prefer the connected MCP tools when AgentGraph is available through MCP. Use the
13
+ `agentgraph` CLI when MCP is unavailable or when a terminal script is the natural
14
+ interface. Do not query AgentGraph's SQLite database or connector internals directly.
15
+
16
+ ## Investigate
17
+
18
+ 1. Start with `search` for discovery. If the user supplied a graph ID, platform
19
+ reference, or indexed URL, use `get` directly instead.
20
+ 2. Read the full entity before making a source-backed claim. Search results and graph
21
+ traversals contain bounded snippets.
22
+ 3. Inspect direct edges or traverse only when relationships help answer the question.
23
+ 4. Resolve a stub or fetch a known resource when missing or stale content blocks the
24
+ answer, then repeat the read.
25
+ 5. Compare source dates and contents. Cite the source URL or entity ID and separate
26
+ source facts from conclusions drawn across sources.
27
+
28
+ Use search filters when the type, source, author, time window, observation, or
29
+ attachments are already known. A search without a query lists matching entities by
30
+ date; a query ranks them by relevance.
31
+
32
+ ## Guardrails
33
+
34
+ - A stub has neither a title nor content.
35
+ - Use `source_created_at` and `source_updated_at` for source chronology; local
36
+ `created_at` and `updated_at` describe the graph record.
37
+ - Browser observation alone updates `observed_at`; fetch, poll, and ingest do not
38
+ indicate that the user viewed a resource.
39
+ - Check connector or authentication state only when availability, freshness, or a
40
+ graph operation calls it into question.
41
+ - Confirm Person merges, deletion, credential removal, and removal of bookmark
42
+ protection with the user.
43
+ - A multi-target deletion resolves every target before changing the graph; it either
44
+ deletes the resolved set or leaves the graph unchanged.
45
+
46
+ ## References
47
+
48
+ - Read [references/commands.md](references/commands.md) for command syntax, MCP
49
+ mappings, filters, targets, stubs, and result limits.
50
+ - Read [references/data-model.md](references/data-model.md) for entity types,
51
+ relationships, attachments, timestamps, and retention.
52
+ - Read [references/operations.md](references/operations.md) for connector discovery,
53
+ authentication, fetching, downloads, polling, connector-owned commands, and server
54
+ transport.
@@ -43,6 +43,11 @@ MCP search defaults to `refresh=false`; set it only when fresh connector-owned
43
43
  presentation metadata is needed. It does not replace a targeted source fetch for
44
44
  stale content.
45
45
 
46
+ MCP tools return structured content: successes use `status="ok"` with a `data`
47
+ object; expected failures use `status="error"`, an error `code`, and `message`, with
48
+ the MCP error flag set. Search `data` includes `has_more`; entity payloads include
49
+ `is_stub` and, when known, `source_url`.
50
+
46
51
  ## Stubs
47
52
 
48
53
  An entity is a stub when it has neither a title nor content. With the CLI, pass
@@ -16,6 +16,11 @@ receive the full installed entity type catalog in the `search_entities_tool`
16
16
  description during tool discovery. Use `--verify` only when a live provider check is
17
17
  needed.
18
18
 
19
+ Connector `last_synced_at` values come from successful poll timestamps in `sync_state`,
20
+ aggregated across account-scoped cursors. A connector command may reset its own cursor
21
+ through the generic command-effects path; after a reset, its status is `never` until the
22
+ next successful poll.
23
+
19
24
  MCP equivalents are `list_connectors_tool`, `list_auth_providers_tool`,
20
25
  `authenticate_provider_tool`, and `remove_auth_provider_tool`.
21
26
 
@@ -70,13 +75,15 @@ forms are `run_connector_command_tool("web", ["observe", "<url-or-prefix>"])` an
70
75
  ```bash
71
76
  agentgraph download <entity-id|platform/ref> [--output <file-or-dir>] [--json]
72
77
  agentgraph bookmark <entity-id|platform/ref|url> [--remove] [--json]
73
- agentgraph delete <entity-id|platform/ref|url> [--json]
78
+ agentgraph delete <entity-id|platform/ref|url>... [--json]
74
79
  agentgraph unify-persons <primary-person-id> <duplicate-person-id>... [--json]
75
80
  ```
76
81
 
77
- Use `download_entity_tool`, `bookmark_entity_tool`, `delete_entity_tool`, and
78
- `unify_persons_tool` through MCP. Confirm identity before person unification. The
79
- first Person is canonical and keeps its ID. Deletion removes connected edges.
82
+ Use `download_entity_tool`, `bookmark_entity_tool`, `delete_entity_tool`,
83
+ `delete_entities_tool`, and `unify_persons_tool` through MCP. Confirm identity before
84
+ person unification. The first Person is canonical and keeps its ID. Multi-target
85
+ deletion resolves every target before deleting any of them, removes connected edges,
86
+ and returns compact references for the deleted entities.
80
87
 
81
88
  ## Server and skill setup
82
89
 
@@ -88,12 +95,17 @@ The server listens on a Unix socket (`AGENTGRAPH_SERVER_UDS_PATH`, default
88
95
  `~/.agentgraph/agentgraph.sock`) as well as TCP, because most agent sandboxes deny
89
96
  loopback TCP while permitting an allowlisted socket.
90
97
 
91
- `agentgraph poll` and connector or authentication commands that queue a poll or ingest
92
- always contact that service and have no local fallback; if sandboxed execution blocks
93
- one, request permission to contact the server or ask for the socket to be allowlisted,
94
- then retry. Reads and `fetch`/`download` follow `AGENTGRAPH_QUERY_TRANSPORT`
95
- (`auto` by default: socket, then TCP, then in-process), so they keep working when the
96
- server is unreachable.
98
+ CLI polling and connector or authentication commands that queue a poll or ingest
99
+ contact the local server. Reads and `fetch`/`download` follow
100
+ `AGENTGRAPH_QUERY_TRANSPORT` (`auto` by default: socket, then TCP, then in-process),
101
+ so they keep working when the server is unreachable. The MCP server can run polling
102
+ and ingest locally when it cannot queue them on the local server.
103
+
104
+ Run ordinary reads with the default transport. To diagnose connectivity, use
105
+ `AGENTGRAPH_QUERY_TRANSPORT=server agentgraph search x --limit 1`; retry an affected
106
+ read with `AGENTGRAPH_QUERY_TRANSPORT=in-process` only after an actual sandbox
107
+ connection failure. See the Coding agent sandboxes section of the configuration docs
108
+ for Unix-socket allowlist settings.
97
109
 
98
110
  ```bash
99
111
  agentgraph mcp-config
@@ -101,9 +113,11 @@ agentgraph mcp-serve
101
113
  agentgraph install-skill [agentgraph] [--target user|project] [--no-claude] [--force] [--json]
102
114
  ```
103
115
 
104
- `agentgraph mcp-config` prints local stdio setup for ChatGPT Desktop Work Mode and
105
- Claude Desktop. In ChatGPT Desktop, enter the printed executable in **Command to
106
- launch** and `mcp-serve` in **Arguments**; for Claude Desktop, add the printed JSON
107
- to its MCP configuration file.
116
+ `agentgraph mcp-config` prints local stdio setup for ChatGPT Desktop Work Mode, Codex,
117
+ Claude Desktop, and Claude Code. Prefer these MCP connections when they are available.
118
+ In ChatGPT Desktop, enter the printed executable in **Command to launch** and
119
+ `mcp-serve` in **Arguments**; for Claude Desktop, add the printed JSON to its MCP
120
+ configuration file. Claude Code uses the printed user-scoped command so the server is
121
+ available across projects.
108
122
 
109
123
  The skill installer is available only through the `agentgraph install-skill` CLI command.
@@ -1,18 +1,18 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentgraph-server
3
- Version: 0.8.0
3
+ Version: 0.9.0
4
4
  Summary: A local-first CLI and MCP server that turns selected digital sources into a searchable graph for the AI agent you already use.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: aiosqlite>=0.20.0
9
- Requires-Dist: agentgraph-connector-web<0.7,>=0.6.0
9
+ Requires-Dist: agentgraph-connector-web<0.8,>=0.7.0
10
10
  Requires-Dist: apscheduler>=3.11.2
11
11
  Requires-Dist: click<8.4
12
12
  Requires-Dist: fastapi>=0.135.2
13
13
  Requires-Dist: fastembed>=0.8.0
14
14
  Requires-Dist: httpx>=0.28.1
15
- Requires-Dist: mcp>=1.26.0
15
+ Requires-Dist: mcp<2,>=1.26.0
16
16
  Requires-Dist: numpy>=2.4.6
17
17
  Requires-Dist: pydantic>=2.12.5
18
18
  Requires-Dist: pydantic-settings>=2.13.1
@@ -22,21 +22,21 @@ Requires-Dist: sqlite-vec>=0.1.9
22
22
  Requires-Dist: typer>=0.24.1
23
23
  Requires-Dist: uvicorn>=0.42.0
24
24
  Provides-Extra: google
25
- Requires-Dist: agentgraph-connector-google<0.7,>=0.6.0; extra == "google"
25
+ Requires-Dist: agentgraph-connector-google<0.7,>=0.6.1; extra == "google"
26
26
  Provides-Extra: slack
27
- Requires-Dist: agentgraph-connector-slack<0.7,>=0.6.0; extra == "slack"
27
+ Requires-Dist: agentgraph-connector-slack<0.7,>=0.6.1; extra == "slack"
28
28
  Provides-Extra: discord
29
- Requires-Dist: agentgraph-connector-discord<0.7,>=0.6.0; extra == "discord"
29
+ Requires-Dist: agentgraph-connector-discord<0.7,>=0.6.1; extra == "discord"
30
30
  Provides-Extra: rss
31
- Requires-Dist: agentgraph-connector-rss<0.7,>=0.6.0; extra == "rss"
31
+ Requires-Dist: agentgraph-connector-rss<0.8,>=0.7.0; extra == "rss"
32
32
  Provides-Extra: web
33
- Requires-Dist: agentgraph-connector-web<0.7,>=0.6.0; extra == "web"
33
+ Requires-Dist: agentgraph-connector-web<0.8,>=0.7.0; extra == "web"
34
34
  Provides-Extra: all
35
- Requires-Dist: agentgraph-connector-web<0.7,>=0.6.0; extra == "all"
36
- Requires-Dist: agentgraph-connector-google<0.7,>=0.6.0; extra == "all"
37
- Requires-Dist: agentgraph-connector-slack<0.7,>=0.6.0; extra == "all"
38
- Requires-Dist: agentgraph-connector-discord<0.7,>=0.6.0; extra == "all"
39
- Requires-Dist: agentgraph-connector-rss<0.7,>=0.6.0; extra == "all"
35
+ Requires-Dist: agentgraph-connector-web<0.8,>=0.7.0; extra == "all"
36
+ Requires-Dist: agentgraph-connector-google<0.7,>=0.6.1; extra == "all"
37
+ Requires-Dist: agentgraph-connector-slack<0.7,>=0.6.1; extra == "all"
38
+ Requires-Dist: agentgraph-connector-discord<0.7,>=0.6.1; extra == "all"
39
+ Requires-Dist: agentgraph-connector-rss<0.8,>=0.7.0; extra == "all"
40
40
  Dynamic: license-file
41
41
 
42
42
  # AgentGraph
@@ -122,7 +122,7 @@ Connect the agent you already use:
122
122
  agentgraph mcp-config
123
123
  ```
124
124
 
125
- Use the printed instructions to connect ChatGPT Desktop Work Mode or Claude Desktop to the local stdio server. See [Install](docs-src/install.md) for the complete setup path.
125
+ Use the printed instructions to connect ChatGPT Desktop Work Mode, Codex, Claude Desktop, or Claude Code to the local stdio server. MCP is the preferred connection; the installed skill provides CLI fallback workflows. See [Install](docs-src/install.md) for the complete setup path.
126
126
 
127
127
  ## What AgentGraph is and is not
128
128
 
@@ -81,7 +81,7 @@ Connect the agent you already use:
81
81
  agentgraph mcp-config
82
82
  ```
83
83
 
84
- Use the printed instructions to connect ChatGPT Desktop Work Mode or Claude Desktop to the local stdio server. See [Install](docs-src/install.md) for the complete setup path.
84
+ Use the printed instructions to connect ChatGPT Desktop Work Mode, Codex, Claude Desktop, or Claude Code to the local stdio server. MCP is the preferred connection; the installed skill provides CLI fallback workflows. See [Install](docs-src/install.md) for the complete setup path.
85
85
 
86
86
  ## What AgentGraph is and is not
87
87
 
@@ -68,6 +68,7 @@ _LIST_PAGE_ORDER_BY = {
68
68
  }
69
69
  _COLUMN_FILTERS = {"platform", "platform_entity_id", "entity_type"}
70
70
  _FTS_DELETE_CHUNK_SIZE = 500
71
+ _PLATFORM_ENTITY_ID_LOOKUP_CHUNK_SIZE = 900
71
72
  _BUSY_TIMEOUT_MS = 5_000
72
73
  _SCHEMA_VERSION = 6
73
74
 
@@ -869,11 +870,14 @@ class SQLiteBackend(StorageBackend):
869
870
  str(existing_row["content"]) if existing_row and existing_row["content"] else ""
870
871
  )
871
872
  fts_title = e.title if e.title is not None else existing_title
872
- fts_content = e.content if e.content is not None else existing_content
873
+ fts_content = (
874
+ e.content if e.content_searchable and e.content is not None else ""
875
+ )
873
876
  rewrite_fts = (
874
877
  existing_row is None
875
878
  or fts_title != existing_title
876
- or fts_content != existing_content
879
+ or (e.content_searchable and fts_content != existing_content)
880
+ or not e.content_searchable
877
881
  )
878
882
  embedding = embeddings.get(e.platform_entity_id)
879
883
  emb_blob = pack_embedding(embedding) if embedding else None
@@ -892,7 +896,10 @@ class SQLiteBackend(StorageBackend):
892
896
  target_title = e.title if e.title is not None else existing_row["title"]
893
897
  target_content = e.content if e.content is not None else existing_row["content"]
894
898
  target_embedding = (
895
- emb_blob if emb_blob is not None else existing_row["content_embedding"]
899
+ emb_blob if e.content_searchable and emb_blob is not None
900
+ else existing_row["content_embedding"]
901
+ if e.content_searchable
902
+ else None
896
903
  )
897
904
  target_source_created = (
898
905
  source_created
@@ -927,7 +934,7 @@ class SQLiteBackend(StorageBackend):
927
934
  entity_type = CASE WHEN entities.entity_type = 'Document' THEN EXCLUDED.entity_type ELSE entities.entity_type END,
928
935
  title = COALESCE(EXCLUDED.title, entities.title),
929
936
  content = COALESCE(EXCLUDED.content, entities.content),
930
- content_embedding = COALESCE(EXCLUDED.content_embedding, entities.content_embedding),
937
+ content_embedding = CASE WHEN ? THEN COALESCE(EXCLUDED.content_embedding, entities.content_embedding) ELSE NULL END,
931
938
  metadata = EXCLUDED.metadata,
932
939
  source_created_at = COALESCE(EXCLUDED.source_created_at, entities.source_created_at),
933
940
  source_updated_at = COALESCE(EXCLUDED.source_updated_at, entities.source_updated_at),
@@ -953,6 +960,7 @@ class SQLiteBackend(StorageBackend):
953
960
  local_updated,
954
961
  e.retention_policy,
955
962
  parent_id,
963
+ e.content_searchable,
956
964
  local_updated,
957
965
  ],
958
966
  )
@@ -1220,42 +1228,55 @@ class SQLiteBackend(StorageBackend):
1220
1228
 
1221
1229
  async def delete_entity(self, entity_id: str) -> EntityResult:
1222
1230
  """Delete one entity by internal ID. Edges cascade; FTS rows are removed explicitly."""
1231
+ deleted = await self.delete_entities([entity_id])
1232
+ return deleted[0]
1233
+
1234
+ async def delete_entities(self, entity_ids: list[str]) -> list[EntityResult]:
1235
+ """Atomically delete entities, their edges, and their FTS rows."""
1236
+ unique_ids = list(dict.fromkeys(entity_ids))
1237
+ if not unique_ids:
1238
+ raise ValueError("At least one entity ID is required")
1223
1239
  assert self._write_lock is not None
1224
1240
  async with self._write_lock:
1225
1241
  conn = self._conn_or_raise()
1226
1242
  await conn.execute("BEGIN IMMEDIATE")
1227
1243
  try:
1228
- await conn.execute(
1229
- """
1230
- UPDATE entities
1231
- SET retention_parent_id = NULL, updated_at = ?
1232
- WHERE retention_parent_id = ? AND bookmarked = 1
1233
- """,
1234
- [_now(), entity_id],
1235
- )
1236
- cursor = await conn.execute(
1237
- """
1238
- DELETE FROM entities
1239
- WHERE id = ?
1240
- RETURNING id, entity_type, platform, platform_entity_id,
1241
- title, content, metadata, created_at, updated_at,
1242
- source_created_at, source_updated_at, synced_at, observed_at,
1243
- retention_policy, retention_parent_id,
1244
- cumulative_observation_duration_ms, bookmarked
1245
- """,
1246
- [entity_id],
1247
- )
1248
- row = await cursor.fetchone()
1249
- if row is None:
1250
- raise ValueError(f"Entity {entity_id!r} not found")
1251
- await conn.execute(
1252
- "DELETE FROM entities_fts WHERE id NOT IN (SELECT id FROM entities)"
1253
- )
1244
+ rows: list[aiosqlite.Row] = []
1245
+ for start in range(0, len(unique_ids), _FTS_DELETE_CHUNK_SIZE):
1246
+ ids = unique_ids[start : start + _FTS_DELETE_CHUNK_SIZE]
1247
+ placeholders = ",".join("?" * len(ids))
1248
+ await conn.execute(
1249
+ f"""
1250
+ UPDATE entities
1251
+ SET retention_parent_id = NULL, updated_at = ?
1252
+ WHERE retention_parent_id IN ({placeholders}) AND bookmarked = 1
1253
+ """,
1254
+ [_now(), *ids],
1255
+ )
1256
+ cursor = await conn.execute(
1257
+ f"""
1258
+ DELETE FROM entities
1259
+ WHERE id IN ({placeholders})
1260
+ RETURNING id, entity_type, platform, platform_entity_id,
1261
+ title, content, metadata, created_at, updated_at,
1262
+ source_created_at, source_updated_at, synced_at, observed_at,
1263
+ retention_policy, retention_parent_id,
1264
+ cumulative_observation_duration_ms, bookmarked
1265
+ """,
1266
+ ids,
1267
+ )
1268
+ rows.extend(await cursor.fetchall())
1269
+ await conn.execute(
1270
+ f"DELETE FROM entities_fts WHERE id IN ({placeholders})", ids
1271
+ )
1272
+ if len(rows) != len(unique_ids):
1273
+ raise ValueError("One or more entities were not found")
1254
1274
  await conn.execute("COMMIT")
1255
1275
  except Exception:
1256
1276
  await conn.execute("ROLLBACK")
1257
1277
  raise
1258
- return _row_to_entity(row)
1278
+ deleted_by_id = {str(row["id"]): _row_to_entity(row) for row in rows}
1279
+ return [deleted_by_id[entity_id] for entity_id in unique_ids]
1259
1280
 
1260
1281
  # --- Read: entities ---
1261
1282
 
@@ -1546,6 +1567,30 @@ class SQLiteBackend(StorageBackend):
1546
1567
  )
1547
1568
  return _row_to_entity(row) if row else None
1548
1569
 
1570
+ async def get_existing_platform_entity_ids(
1571
+ self,
1572
+ platform: str,
1573
+ platform_entity_ids: list[str],
1574
+ ) -> set[str]:
1575
+ if not platform_entity_ids:
1576
+ return set()
1577
+
1578
+ existing: set[str] = set()
1579
+ for offset in range(0, len(platform_entity_ids), _PLATFORM_ENTITY_ID_LOOKUP_CHUNK_SIZE):
1580
+ chunk = platform_entity_ids[
1581
+ offset : offset + _PLATFORM_ENTITY_ID_LOOKUP_CHUNK_SIZE
1582
+ ]
1583
+ placeholders = ",".join("?" for _ in chunk)
1584
+ rows = await self._fetchall(
1585
+ f"""
1586
+ SELECT platform_entity_id FROM entities
1587
+ WHERE platform = ? AND platform_entity_id IN ({placeholders})
1588
+ """,
1589
+ [platform, *chunk],
1590
+ )
1591
+ existing.update(str(row["platform_entity_id"]) for row in rows)
1592
+ return existing
1593
+
1549
1594
  async def list_entities(
1550
1595
  self,
1551
1596
  entity_types: list[str] | None,
@@ -1992,6 +2037,9 @@ class SQLiteBackend(StorageBackend):
1992
2037
  [source, json.dumps(cursor), _now()],
1993
2038
  )
1994
2039
 
2040
+ async def clear_cursor(self, source: str) -> None:
2041
+ await self._execute("DELETE FROM sync_state WHERE source = ?", [source])
2042
+
1995
2043
  # --- Connector support ---
1996
2044
 
1997
2045
  async def increment_observation_duration(
@@ -2129,6 +2177,26 @@ class SQLiteBackend(StorageBackend):
2129
2177
  result[row["platform"]] = datetime.fromisoformat(val) if val else None
2130
2178
  return result
2131
2179
 
2180
+ async def get_sources_last_synced_at(
2181
+ self, sources: list[str]
2182
+ ) -> dict[str, datetime | None]:
2183
+ if not sources:
2184
+ return {}
2185
+ rows = await self._fetchall(
2186
+ "SELECT source, updated_at FROM sync_state"
2187
+ )
2188
+ result: dict[str, datetime | None] = dict.fromkeys(sources, None)
2189
+ for source in sources:
2190
+ matching = [
2191
+ row["updated_at"]
2192
+ for row in rows
2193
+ if row["source"] == source or row["source"].startswith(f"{source}:")
2194
+ ]
2195
+ values = [datetime.fromisoformat(value) for value in matching if value]
2196
+ if values:
2197
+ result[source] = max(values)
2198
+ return result
2199
+
2132
2200
  async def reset_synced_at(self, platform: str, platform_entity_id: str) -> None:
2133
2201
  await self._execute(
2134
2202
  "UPDATE entities SET synced_at = NULL WHERE platform = ? AND platform_entity_id = ?",
@@ -437,6 +437,13 @@ def connector_command(
437
437
  try:
438
438
  result = type(connector).run_cli_command(command_args)
439
439
  effects = type(connector).command_effects(command_args, result)
440
+ if effects.reset_cursors:
441
+ from agentgraph.cli_query import run_graph_operation
442
+ from agentgraph.connectors.command_effects import execute_cursor_resets
443
+
444
+ result["reset_cursors"] = run_graph_operation(
445
+ lambda: execute_cursor_resets(effects)
446
+ )
440
447
  if effects.delete_entities:
441
448
  from agentgraph.cli_query import run_graph_operation
442
449
  from agentgraph.connectors.command_effects import execute_deletions
@@ -737,13 +744,13 @@ def bookmark(
737
744
 
738
745
  @app.command("delete")
739
746
  def delete_cmd(
740
- target: str = typer.Argument(..., help="Entity ID, UUID prefix, platform ref, or URL"),
747
+ targets: list[str] = typer.Argument(..., help="Entity IDs, UUID prefixes, platform refs, or URLs"),
741
748
  json: bool = typer.Option(False, "--json", help="Output as JSON"),
742
749
  ) -> None:
743
- """Delete an entity from the graph."""
750
+ """Delete one or more entities from the graph."""
744
751
  from agentgraph.cli_query import cmd_delete
745
752
 
746
- cmd_delete(target=target, as_json=json)
753
+ cmd_delete(targets=targets, as_json=json)
747
754
 
748
755
 
749
756
  @app.command("unify-persons")
@@ -842,7 +849,7 @@ def mcp_config() -> None:
842
849
  typer.echo()
843
850
  typer.echo("Claude Code:")
844
851
  typer.echo(" Register the server from your terminal:\n")
845
- typer.echo(f" claude mcp add agentgraph -- {binary} mcp-serve")
852
+ typer.echo(f" claude mcp add --transport stdio --scope user agentgraph -- {binary} mcp-serve")
846
853
  typer.echo()
847
854
  typer.echo("Every client above runs the same server. Reads follow")
848
855
  typer.echo("AGENTGRAPH_QUERY_TRANSPORT: by default the local server when one is")
@@ -342,7 +342,22 @@ def cmd_bookmark(target: str, bookmarked: bool, as_json: bool) -> None:
342
342
  console.print(f"[green]{action}:[/green] {label} [{result['id'][:8]}]")
343
343
 
344
344
 
345
- def cmd_delete(target: str, as_json: bool) -> None:
345
+ def cmd_delete(targets: list[str], as_json: bool) -> None:
346
+ if len(targets) == 1:
347
+ _cmd_delete_one(targets[0], as_json)
348
+ return
349
+
350
+ async def operation(client: QueryClient) -> dict[str, Any]:
351
+ return await client.delete_many(targets)
352
+
353
+ result = _run_with_client(operation)
354
+ if as_json:
355
+ console.print_json(json.dumps(result, default=str))
356
+ return
357
+ console.print(f"[green]Deleted {result['deleted_count']} entities.[/green]")
358
+
359
+
360
+ def _cmd_delete_one(target: str, as_json: bool) -> None:
346
361
  async def operation(client: QueryClient) -> dict[str, Any]:
347
362
  return await client.delete(target)
348
363
 
@@ -105,6 +105,7 @@ class ConnectorCommandEffects:
105
105
  ingest_account_id: str | None = None
106
106
  delete_entities: tuple[EntityReference, ...] = ()
107
107
  fetch_references: tuple[SourceReference, ...] = ()
108
+ reset_cursors: tuple[str, ...] = ()
108
109
 
109
110
 
110
111
  class PersonRecord(BaseModel):
@@ -122,6 +123,7 @@ class EntityRecord(BaseModel):
122
123
  platform_entity_id: str
123
124
  title: str | None = None
124
125
  content: str | None = None
126
+ content_searchable: bool = True
125
127
  source_created_at: datetime | None = None
126
128
  source_updated_at: datetime | None = None
127
129
  metadata: dict[str, str | int | float | bool | None] = {}
@@ -39,6 +39,16 @@ async def execute_deletions(effects: ConnectorCommandEffects) -> list[dict[str,
39
39
  return deleted
40
40
 
41
41
 
42
+ async def execute_cursor_resets(effects: ConnectorCommandEffects) -> list[str]:
43
+ """Clear connector-requested cursors and return the sources that were reset."""
44
+ from agentgraph.core.context import get_backend
45
+
46
+ backend = get_backend()
47
+ for source in effects.reset_cursors:
48
+ await backend.clear_cursor(source)
49
+ return list(effects.reset_cursors)
50
+
51
+
42
52
  async def execute_fetches(effects: ConnectorCommandEffects) -> list[dict[str, Any]]:
43
53
  """Fetch connector-owned references and persist their returned batches."""
44
54
  from agentgraph.connectors.registry import get_connector
@@ -92,6 +92,13 @@ class TombstoneMutation(BaseModel):
92
92
  target: MutationTarget
93
93
 
94
94
 
95
+ class TombstoneBatchMutation(BaseModel):
96
+ event_id: UUID = Field(default_factory=uuid4)
97
+ kind: Literal["tombstone_batch"] = "tombstone_batch"
98
+ occurred_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
99
+ targets: list[MutationTarget] = Field(min_length=1)
100
+
101
+
95
102
  class EntityUpsertMutation(BaseModel):
96
103
  event_id: UUID = Field(default_factory=uuid4)
97
104
  kind: Literal["upsert"] = "upsert"
@@ -102,7 +109,11 @@ class EntityUpsertMutation(BaseModel):
102
109
 
103
110
 
104
111
  type MutationEvent = (
105
- ObservationMutation | BookmarkMutation | TombstoneMutation | EntityUpsertMutation
112
+ ObservationMutation
113
+ | BookmarkMutation
114
+ | TombstoneMutation
115
+ | TombstoneBatchMutation
116
+ | EntityUpsertMutation
106
117
  )
107
118
 
108
119
 
@@ -174,7 +174,7 @@ async def connector_status_items(
174
174
  for delegated_source in type(connector).poll_delegates:
175
175
  poll_delegators.setdefault(delegated_source, []).append(connector.source)
176
176
 
177
- last_synced_by_platform = await backend.get_platforms_last_synced_at(
177
+ last_synced_by_platform = await backend.get_sources_last_synced_at(
178
178
  [connector.source for connector in connectors]
179
179
  )
180
180
 
@@ -76,6 +76,11 @@ class StorageBackend(ABC):
76
76
  """Delete an entity and return the deleted entity."""
77
77
  ...
78
78
 
79
+ @abstractmethod
80
+ async def delete_entities(self, entity_ids: list[str]) -> list[EntityResult]:
81
+ """Atomically delete entities and return their deleted snapshots."""
82
+ ...
83
+
79
84
  # --- Read: entities ---
80
85
 
81
86
  @abstractmethod
@@ -127,6 +132,15 @@ class StorageBackend(ABC):
127
132
  content_limit: int | None = None,
128
133
  ) -> EntityResult | None: ...
129
134
 
135
+ @abstractmethod
136
+ async def get_existing_platform_entity_ids(
137
+ self,
138
+ platform: str,
139
+ platform_entity_ids: list[str],
140
+ ) -> set[str]:
141
+ """Return the supplied platform entity IDs that are already stored."""
142
+ ...
143
+
130
144
  @abstractmethod
131
145
  async def list_entities(
132
146
  self,
@@ -234,6 +248,11 @@ class StorageBackend(ABC):
234
248
  @abstractmethod
235
249
  async def save_cursor(self, source: str, cursor: dict[str, Any]) -> None: ...
236
250
 
251
+ @abstractmethod
252
+ async def clear_cursor(self, source: str) -> None:
253
+ """Remove a source cursor so its next poll starts from an empty state."""
254
+ ...
255
+
237
256
  # --- Connector support ---
238
257
 
239
258
  @abstractmethod
@@ -284,6 +303,13 @@ class StorageBackend(ABC):
284
303
  for platform in platforms
285
304
  }
286
305
 
306
+ @abstractmethod
307
+ async def get_sources_last_synced_at(
308
+ self, sources: list[str]
309
+ ) -> dict[str, datetime | None]:
310
+ """Return the latest successful poll timestamp for each connector source."""
311
+ ...
312
+
287
313
  @abstractmethod
288
314
  async def reset_synced_at(
289
315
  self, platform: str, platform_entity_id: str