databricks-agentbricks 0.2.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.
Files changed (159) hide show
  1. databricks_agentbricks/__init__.py +1 -0
  2. databricks_agentbricks/_group.py +217 -0
  3. databricks_agentbricks/agent_project.py +657 -0
  4. databricks_agentbricks/app_resources.py +139 -0
  5. databricks_agentbricks/cli/__init__.py +12 -0
  6. databricks_agentbricks/cli/app.py +108 -0
  7. databricks_agentbricks/cli/app_auth.py +198 -0
  8. databricks_agentbricks/cli/auth.py +142 -0
  9. databricks_agentbricks/cli/deploy.py +954 -0
  10. databricks_agentbricks/cli/dev.py +310 -0
  11. databricks_agentbricks/cli/endpoint.py +145 -0
  12. databricks_agentbricks/cli/endpoint_examples.py +33 -0
  13. databricks_agentbricks/cli/endpoint_output.py +53 -0
  14. databricks_agentbricks/cli/endpoint_request.py +71 -0
  15. databricks_agentbricks/cli/endpoint_transport.py +148 -0
  16. databricks_agentbricks/cli/help.py +467 -0
  17. databricks_agentbricks/cli/init.py +496 -0
  18. databricks_agentbricks/cli/mcp.py +75 -0
  19. databricks_agentbricks/cli/memory.py +520 -0
  20. databricks_agentbricks/cli/sandbox.py +823 -0
  21. databricks_agentbricks/cli/sessions.py +588 -0
  22. databricks_agentbricks/cli/tools.py +456 -0
  23. databricks_agentbricks/cli/tracing.py +724 -0
  24. databricks_agentbricks/databricks_cli.py +38 -0
  25. databricks_agentbricks/errors.py +114 -0
  26. databricks_agentbricks/lakebase_runtime_store.py +107 -0
  27. databricks_agentbricks/legacy_lakebase_runtime_store.py +74 -0
  28. databricks_agentbricks/project_config.py +157 -0
  29. databricks_agentbricks/project_types.py +55 -0
  30. databricks_agentbricks/py.typed +0 -0
  31. databricks_agentbricks/render.py +349 -0
  32. databricks_agentbricks/templates/agent-bricks-migrate/SKILL.md +101 -0
  33. databricks_agentbricks/templates/agent-langgraph/.env.example +36 -0
  34. databricks_agentbricks/templates/agent-langgraph/.gitignore +199 -0
  35. databricks_agentbricks/templates/agent-langgraph/AGENTKIT_CONTRACT.md +95 -0
  36. databricks_agentbricks/templates/agent-langgraph/AGENTS.md +31 -0
  37. databricks_agentbricks/templates/agent-langgraph/CLAUDE.md +1 -0
  38. databricks_agentbricks/templates/agent-langgraph/README.md +141 -0
  39. databricks_agentbricks/templates/agent-langgraph/agent/__init__.py +0 -0
  40. databricks_agentbricks/templates/agent-langgraph/agent/agent.py +177 -0
  41. databricks_agentbricks/templates/agent-langgraph/agent/mcps.py +27 -0
  42. databricks_agentbricks/templates/agent-langgraph/agent/tools/__init__.py +24 -0
  43. databricks_agentbricks/templates/agent-langgraph/agent/tools/sample_tool.py +15 -0
  44. databricks_agentbricks/templates/agent-langgraph/agent/tools/send_message.py +17 -0
  45. databricks_agentbricks/templates/agent-langgraph/app.yaml +15 -0
  46. databricks_agentbricks/templates/agent-langgraph/pyproject.toml +42 -0
  47. databricks_agentbricks/templates/agent-langgraph/runtime/__init__.py +0 -0
  48. databricks_agentbricks/templates/agent-langgraph/runtime/adapter.py +144 -0
  49. databricks_agentbricks/templates/agent-langgraph/runtime/main.py +25 -0
  50. databricks_agentbricks/templates/agent-langgraph/tests/__init__.py +0 -0
  51. databricks_agentbricks/templates/agent-langgraph/tests/test_agent.py +211 -0
  52. databricks_agentbricks/templates/agent-langgraph/tests/test_runtime.py +19 -0
  53. databricks_agentbricks/templates/agent-langgraph/tests/test_workspace.py +28 -0
  54. databricks_agentbricks/templates/agent-openai/.env.example +37 -0
  55. databricks_agentbricks/templates/agent-openai/.gitignore +199 -0
  56. databricks_agentbricks/templates/agent-openai/AGENTKIT_CONTRACT.md +107 -0
  57. databricks_agentbricks/templates/agent-openai/AGENTS.md +76 -0
  58. databricks_agentbricks/templates/agent-openai/CLAUDE.md +1 -0
  59. databricks_agentbricks/templates/agent-openai/README.md +143 -0
  60. databricks_agentbricks/templates/agent-openai/agent/__init__.py +0 -0
  61. databricks_agentbricks/templates/agent-openai/agent/agent.py +205 -0
  62. databricks_agentbricks/templates/agent-openai/agent/mcps.py +28 -0
  63. databricks_agentbricks/templates/agent-openai/agent/tools/__init__.py +24 -0
  64. databricks_agentbricks/templates/agent-openai/agent/tools/sample_tool.py +15 -0
  65. databricks_agentbricks/templates/agent-openai/agent/tools/send_message.py +18 -0
  66. databricks_agentbricks/templates/agent-openai/app.yaml +15 -0
  67. databricks_agentbricks/templates/agent-openai/pyproject.toml +42 -0
  68. databricks_agentbricks/templates/agent-openai/runtime/__init__.py +0 -0
  69. databricks_agentbricks/templates/agent-openai/runtime/adapter.py +188 -0
  70. databricks_agentbricks/templates/agent-openai/runtime/main.py +25 -0
  71. databricks_agentbricks/templates/agent-openai/tests/__init__.py +0 -0
  72. databricks_agentbricks/templates/agent-openai/tests/test_agent.py +326 -0
  73. databricks_agentbricks/templates/agent-openai/tests/test_runtime.py +19 -0
  74. databricks_agentbricks/templates/agent-openai/tests/test_workspace.py +28 -0
  75. databricks_agentbricks/templates/custom-agent-langgraph/.env.example +1 -0
  76. databricks_agentbricks/templates/custom-agent-langgraph/.gitignore +11 -0
  77. databricks_agentbricks/templates/custom-agent-langgraph/README.md +22 -0
  78. databricks_agentbricks/templates/custom-agent-langgraph/agent/__init__.py +0 -0
  79. databricks_agentbricks/templates/custom-agent-langgraph/agent/agent.py +27 -0
  80. databricks_agentbricks/templates/custom-agent-langgraph/app.yaml +1 -0
  81. databricks_agentbricks/templates/custom-agent-langgraph/pyproject.toml +38 -0
  82. databricks_agentbricks/templates/custom-agent-langgraph/runtime/__init__.py +0 -0
  83. databricks_agentbricks/templates/custom-agent-langgraph/runtime/main.py +31 -0
  84. databricks_agentbricks/templates/custom-agent-langgraph/tests/test_runtime.py +29 -0
  85. databricks_agentbricks/templates/custom-agent-openai/.env.example +1 -0
  86. databricks_agentbricks/templates/custom-agent-openai/.gitignore +11 -0
  87. databricks_agentbricks/templates/custom-agent-openai/README.md +22 -0
  88. databricks_agentbricks/templates/custom-agent-openai/agent/__init__.py +0 -0
  89. databricks_agentbricks/templates/custom-agent-openai/agent/agent.py +29 -0
  90. databricks_agentbricks/templates/custom-agent-openai/app.yaml +1 -0
  91. databricks_agentbricks/templates/custom-agent-openai/pyproject.toml +38 -0
  92. databricks_agentbricks/templates/custom-agent-openai/runtime/__init__.py +0 -0
  93. databricks_agentbricks/templates/custom-agent-openai/runtime/main.py +31 -0
  94. databricks_agentbricks/templates/custom-agent-openai/tests/test_runtime.py +29 -0
  95. databricks_agentbricks/templates/sandbox_mcp.py +39 -0
  96. databricks_agentbricks/templates/sandbox_mcp_langgraph.py +39 -0
  97. databricks_agentbricks/templates/ui/agent-langgraph/CHAT_APP.md +49 -0
  98. databricks_agentbricks/templates/ui/agent-langgraph/runtime/main.py +26 -0
  99. databricks_agentbricks/templates/ui/agent-langgraph/runtime/ui.py +528 -0
  100. databricks_agentbricks/templates/ui/agent-langgraph/tests/test_demo_ui.py +542 -0
  101. databricks_agentbricks/templates/ui/agent-langgraph/ui/agentbricks-mark.png +0 -0
  102. databricks_agentbricks/templates/ui/agent-langgraph/ui/app.js +1464 -0
  103. databricks_agentbricks/templates/ui/agent-langgraph/ui/index.html +281 -0
  104. databricks_agentbricks/templates/ui/agent-langgraph/ui/styles.css +1913 -0
  105. databricks_agentbricks/templates/ui/agent-openai/CHAT_APP.md +54 -0
  106. databricks_agentbricks/templates/ui/agent-openai/runtime/main.py +26 -0
  107. databricks_agentbricks/templates/ui/agent-openai/runtime/ui.py +521 -0
  108. databricks_agentbricks/templates/ui/agent-openai/tests/test_demo_ui.py +512 -0
  109. databricks_agentbricks/templates/ui/agent-openai/ui/agentbricks-mark.png +0 -0
  110. databricks_agentbricks/templates/ui/agent-openai/ui/app.js +1428 -0
  111. databricks_agentbricks/templates/ui/agent-openai/ui/index.html +281 -0
  112. databricks_agentbricks/templates/ui/agent-openai/ui/styles.css +1913 -0
  113. databricks_agentbricks/theme.py +72 -0
  114. databricks_agentbricks-0.2.0.dist-info/METADATA +897 -0
  115. databricks_agentbricks-0.2.0.dist-info/RECORD +159 -0
  116. databricks_agentbricks-0.2.0.dist-info/WHEEL +4 -0
  117. databricks_agentbricks-0.2.0.dist-info/entry_points.txt +2 -0
  118. databricks_agentbricks-0.2.0.dist-info/licenses/NOTICE +39 -0
  119. databricks_agentkit/__init__.py +67 -0
  120. databricks_agentkit/_api_client.py +673 -0
  121. databricks_agentkit/_pagination.py +25 -0
  122. databricks_agentkit/client.py +31 -0
  123. databricks_agentkit/langgraph/__init__.py +98 -0
  124. databricks_agentkit/langgraph/genie.py +50 -0
  125. databricks_agentkit/langgraph/mcp.py +216 -0
  126. databricks_agentkit/langgraph/memory.py +67 -0
  127. databricks_agentkit/langgraph/session_store.py +485 -0
  128. databricks_agentkit/memory_store.py +371 -0
  129. databricks_agentkit/models.py +330 -0
  130. databricks_agentkit/openai/__init__.py +93 -0
  131. databricks_agentkit/openai/genie.py +50 -0
  132. databricks_agentkit/openai/mcp.py +189 -0
  133. databricks_agentkit/openai/memory.py +67 -0
  134. databricks_agentkit/openai/sessions.py +157 -0
  135. databricks_agentkit/py.typed +0 -0
  136. databricks_agentkit/runtime/ARCHITECTURE.md +64 -0
  137. databricks_agentkit/runtime/README.md +184 -0
  138. databricks_agentkit/runtime/__init__.py +69 -0
  139. databricks_agentkit/runtime/app.py +316 -0
  140. databricks_agentkit/runtime/auth.py +149 -0
  141. databricks_agentkit/runtime/durability/__init__.py +1 -0
  142. databricks_agentkit/runtime/durability/execution.py +133 -0
  143. databricks_agentkit/runtime/durability/heartbeat.py +64 -0
  144. databricks_agentkit/runtime/durability/lakebase_runtime_store.py +648 -0
  145. databricks_agentkit/runtime/durability/recovery.py +70 -0
  146. databricks_agentkit/runtime/durability/store.py +33 -0
  147. databricks_agentkit/runtime/execution.py +160 -0
  148. databricks_agentkit/runtime/genie.py +224 -0
  149. databricks_agentkit/runtime/mcp_auth.py +114 -0
  150. databricks_agentkit/runtime/model_services.py +90 -0
  151. databricks_agentkit/runtime/runtime.py +273 -0
  152. databricks_agentkit/runtime/session_store_client.py +120 -0
  153. databricks_agentkit/runtime/store.py +287 -0
  154. databricks_agentkit/runtime/tool_manifest.py +223 -0
  155. databricks_agentkit/runtime/tracing.py +71 -0
  156. databricks_agentkit/runtime/types.py +118 -0
  157. databricks_agentkit/runtime/workspace.py +27 -0
  158. databricks_agentkit/session_store.py +434 -0
  159. databricks_agentkit/timefmt.py +85 -0
@@ -0,0 +1,520 @@
1
+ """`ab memory` — manage workspace-scoped managed memory stores and entries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pathlib
6
+ import sys
7
+ from typing import Any
8
+
9
+ import click
10
+
11
+ from databricks_agentbricks import render
12
+ from databricks_agentbricks.errors import AgentCliError
13
+ from databricks_agentbricks.render import field
14
+ from databricks_agentkit import timefmt
15
+
16
+ _BREADCRUMB = "Agent Memory"
17
+
18
+ # Friendly aliases for the memory-entry source type, mapped to the API enum values.
19
+ _SOURCE_TYPES = {
20
+ "agent": "MANAGED_MEMORY_ENTRY_SOURCE_TYPE_AGENT",
21
+ "unspecified": "MANAGED_MEMORY_ENTRY_SOURCE_TYPE_UNSPECIFIED",
22
+ }
23
+
24
+
25
+ def _normalize_source_type(value):
26
+ """Accept a friendly alias ('agent'/'unspecified') or the full enum; None passes through."""
27
+ if value is None:
28
+ return None
29
+ key = value.strip().lower()
30
+ if key in _SOURCE_TYPES:
31
+ return _SOURCE_TYPES[key]
32
+ if value in _SOURCE_TYPES.values():
33
+ return value
34
+ raise AgentCliError(f"Invalid --source-type {value!r}. Choose one of: agent, unspecified.")
35
+
36
+
37
+ def _require_entry_store(store, entry) -> None:
38
+ """A store is needed unless the entry is a full `memory-stores/.../entries/...` name."""
39
+ if not store and not str(entry).strip().startswith("memory-stores/"):
40
+ raise AgentCliError(
41
+ "Provide --store, or pass the full entry resource name "
42
+ "(memory-stores/<store>/entries/<id>)."
43
+ )
44
+
45
+
46
+ def _store_id(store: dict) -> str:
47
+ name = field(store, "name") or ""
48
+ return name.split("/")[-1] if name else "—"
49
+
50
+
51
+ def _truncate(value: Any, length: int = 60) -> str:
52
+ text = "" if value is None else str(value)
53
+ return text if len(text) <= length else text[: length - 1] + "…"
54
+
55
+
56
+ # --- group ------------------------------------------------------------------
57
+
58
+
59
+ @click.group()
60
+ def memory() -> None:
61
+ """Manage an agent's long-term memory: memory stores and their entries.
62
+
63
+ Memory is what an agent remembers across separate conversations — durable facts and
64
+ preferences (for example "prefers concise answers", or a saved profile detail), as opposed to
65
+ the turn-by-turn history of a single conversation (that is `ab sessions`).
66
+
67
+ A memory store is the managed store that holds this memory; each entry is a small document (a
68
+ path plus its content) partitioned by actor, so one store keeps every user's memories separate.
69
+ """
70
+
71
+
72
+ @memory.group()
73
+ def stores() -> None:
74
+ """Workspace-scoped managed memory stores."""
75
+
76
+
77
+ @memory.group()
78
+ def entries() -> None:
79
+ """Memory entries within a store, partitioned by actor."""
80
+
81
+
82
+ # --- bind a store to an agent project (agent.toml) --------------------------
83
+
84
+
85
+ def _source_option(function):
86
+ return click.option(
87
+ "--source",
88
+ type=click.Path(exists=True, file_okay=False, path_type=pathlib.Path),
89
+ default=pathlib.Path("."),
90
+ show_default=True,
91
+ help="Agent Bricks project containing agent.toml.",
92
+ )(function)
93
+
94
+
95
+ @memory.command("bind")
96
+ @click.argument("store")
97
+ @_source_option
98
+ @click.pass_obj
99
+ def memory_bind(obj, store: str, source: pathlib.Path) -> None:
100
+ """Bind memory STORE to the agent by declaring it in agent.toml.
101
+
102
+ This only edits agent.toml — it does not create the store. `ab deploy` creates any declared
103
+ store that doesn't exist yet and grants the deployed app's service principal access to it.
104
+ """
105
+ from databricks_agentbricks.agent_project import AgentProject
106
+
107
+ project = AgentProject.load(source)
108
+ project.bind_memory_store(store)
109
+ project.write()
110
+ if obj.output == "json":
111
+ render.emit_json({"memory_store": store, "manifest": str(project.path)})
112
+ return
113
+ render.success(
114
+ f"Bound memory store '{store}'",
115
+ fields={"agent.toml": str(project.path)},
116
+ next_steps=[
117
+ (
118
+ f"ab memory stores create --name {store}",
119
+ "Create the store now without deploying",
120
+ ),
121
+ ("ab dev", "Re-run to pick up the store locally"),
122
+ ("ab deploy <name>", "Create it if missing and grant the app access"),
123
+ ],
124
+ )
125
+
126
+
127
+ @memory.command("unbind")
128
+ @_source_option
129
+ @click.pass_obj
130
+ def memory_unbind(obj, source: pathlib.Path) -> None:
131
+ """Remove the memory store binding from the agent's agent.toml.
132
+
133
+ Only edits agent.toml; the managed store itself is untouched (delete it with
134
+ `ab memory stores delete`).
135
+ """
136
+ from databricks_agentbricks.agent_project import AgentProject
137
+
138
+ project = AgentProject.load(source)
139
+ if project.unbind_memory_store():
140
+ project.write()
141
+ render.success("Removed memory store binding", fields={"agent.toml": str(project.path)})
142
+ else:
143
+ click.echo(f"No memory store binding in {project.path}.")
144
+
145
+
146
+ # --- stores -----------------------------------------------------------------
147
+
148
+
149
+ def _store_starter_code(obj, store: dict) -> list[tuple[str, str, str]]:
150
+ store_id = _store_id(store)
151
+ name = field(store, "name") or f"memory-stores/{store_id}"
152
+ return [
153
+ (
154
+ "curl",
155
+ "bash",
156
+ f"""
157
+ curl -X POST "{obj.client().host}/api/2.0/agents/{name}/entries" \\
158
+ -H "Authorization: Bearer $DATABRICKS_TOKEN" -H "Content-Type: application/json" \\
159
+ -d '{{"actor_id": "alice", "path": "/preferences/style.md", "content": "Terse, code first."}}'
160
+ """,
161
+ ),
162
+ (
163
+ "agentbricks",
164
+ "bash",
165
+ f"""
166
+ ab memory entries create --store {store_id} \\
167
+ --actor-id alice --path /preferences/style.md --content "Terse, code first."
168
+ ab memory entries search --store {store_id} --actor-id alice --query "style"
169
+ """,
170
+ ),
171
+ ]
172
+
173
+
174
+ def _store_created(store: dict):
175
+ # The API returns RFC 3339 `create_time`; older responses used epoch-millis
176
+ # `created_at`. Read the current field, falling back to the legacy one.
177
+ return field(store, "create_time") or field(store, "created_at")
178
+
179
+
180
+ def _store_updated(store: dict):
181
+ return field(store, "update_time") or field(store, "updated_at")
182
+
183
+
184
+ def _render_store_detail(obj, store: dict) -> None:
185
+ render.detail(
186
+ _BREADCRUMB,
187
+ field(store, "display_name") or _store_id(store),
188
+ {
189
+ "Name": field(store, "display_name"),
190
+ "Resource name": field(store, "name"),
191
+ "Workspace": field(store, "workspace_id"),
192
+ "Creator": field(store, "owner_user_id"),
193
+ "Storage": render.field(field(store, "storage_backend") or {}, "backend_id"),
194
+ "Description": field(store, "description"),
195
+ "Created": timefmt.absolute(_store_created(store)),
196
+ "Updated": timefmt.absolute(_store_updated(store)),
197
+ },
198
+ status="ACTIVE",
199
+ snippets=_store_starter_code(obj, store),
200
+ )
201
+
202
+
203
+ @stores.command("create")
204
+ @click.option(
205
+ "--display-name",
206
+ "--name",
207
+ "display_name",
208
+ required=True,
209
+ help="Workspace-unique display name (--name is accepted as an alias).",
210
+ )
211
+ @click.option("--description", default=None, help="Optional human-readable description.")
212
+ @click.pass_obj
213
+ def stores_create(obj, display_name, description) -> None:
214
+ """Create a memory store."""
215
+ with render.status(f"Creating memory store '{display_name}'…"):
216
+ data = obj.client().create_memory_store(display_name, description)
217
+ if obj.output == "json":
218
+ render.emit_json(data)
219
+ return
220
+ store_id = _store_id(data)
221
+ render.success(
222
+ f"Created memory store '{display_name}'",
223
+ fields={"Store ID": store_id, "Name": field(data, "name")},
224
+ next_steps=[
225
+ (
226
+ f"ab memory entries create --store {store_id} --actor-id <id> --path </p>",
227
+ "Add a memory entry for an actor",
228
+ ),
229
+ (f"ab memory stores get {store_id}", "View this store's details"),
230
+ (
231
+ f"ab memory bind {display_name}",
232
+ "Bind this store to the agent (wired in on dev/deploy)",
233
+ ),
234
+ ],
235
+ )
236
+
237
+
238
+ @stores.command("list")
239
+ @click.option("--page-size", type=int, default=25, show_default=True)
240
+ @click.option("--page-token", default=None)
241
+ @click.pass_obj
242
+ def stores_list(obj, page_size, page_token) -> None:
243
+ """List memory stores in the workspace (25 per page; paginates interactively on a terminal)."""
244
+ client = obj.client()
245
+ if obj.output == "json":
246
+ render.emit_json(client.list_memory_stores(page_size, page_token))
247
+ return
248
+ # On a terminal, offer to fetch the next page instead of only printing the --page-token hint.
249
+ interactive = sys.stdin.isatty() and sys.stdout.isatty()
250
+ while True:
251
+ data = client.list_memory_stores(page_size, page_token)
252
+ items = field(data, "managed_memory_stores") or []
253
+ rows = [
254
+ [
255
+ field(s, "display_name"),
256
+ _store_id(s),
257
+ timefmt.relative(_store_created(s)),
258
+ timefmt.relative(_store_updated(s)),
259
+ _truncate(field(s, "description"), 40),
260
+ ]
261
+ for s in items
262
+ ]
263
+ token = field(data, "next_page_token")
264
+ render.resource_table(
265
+ "Managed Memory Stores",
266
+ [
267
+ ("Name", "left"),
268
+ ("Resource name", "left"),
269
+ ("Created", "left"),
270
+ ("Updated", "left"),
271
+ ("Description", "left"),
272
+ ],
273
+ rows,
274
+ # In an interactive session the prompt below replaces the token hint.
275
+ subtitle=None if (interactive and token) else _page_note(data),
276
+ no_wrap=[1], # keep the resource name full-width; other columns narrow to fit
277
+ )
278
+ if not token or not interactive:
279
+ break
280
+ if not click.confirm("Show next page?", default=False):
281
+ break
282
+ page_token = token
283
+
284
+
285
+ @stores.command("get")
286
+ @click.argument("name")
287
+ @click.pass_obj
288
+ def stores_get(obj, name) -> None:
289
+ """Get a memory store by id or resource name."""
290
+ data = obj.client().get_memory_store(name)
291
+ if obj.output == "json":
292
+ render.emit_json(data)
293
+ return
294
+ _render_store_detail(obj, data)
295
+
296
+
297
+ @stores.command("update")
298
+ @click.argument("name")
299
+ @click.option("--display-name", default=None)
300
+ @click.option("--description", default=None)
301
+ @click.pass_obj
302
+ def stores_update(obj, name, display_name, description) -> None:
303
+ """Update a store's display name and/or description."""
304
+ data = obj.client().update_memory_store(name, display_name, description)
305
+ if obj.output == "json":
306
+ render.emit_json(data)
307
+ return
308
+ _render_store_detail(obj, data)
309
+
310
+
311
+ @stores.command("delete")
312
+ @click.argument("name")
313
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
314
+ @click.pass_obj
315
+ def stores_delete(obj, name, yes) -> None:
316
+ """Delete (soft-delete) a memory store."""
317
+ render.confirm_destroy(f"memory store '{name}'", assume_yes=yes)
318
+ obj.client().delete_memory_store(name)
319
+ if obj.output == "json":
320
+ render.emit_json({"deleted": name})
321
+ return
322
+ render.success(f"Deleted memory store '{name}'")
323
+
324
+
325
+ # --- entries ----------------------------------------------------------------
326
+
327
+
328
+ def _render_entry_detail(entry: dict) -> None:
329
+ render.detail(
330
+ f"{_BREADCRUMB} Entry",
331
+ field(entry, "path") or "—",
332
+ {
333
+ "Name": field(entry, "name"),
334
+ "Actor": field(entry, "actor_id"),
335
+ "Session": field(entry, "session_id"),
336
+ "Path": field(entry, "path"),
337
+ "Source": field(entry, "source_type"),
338
+ "Description": field(entry, "description"),
339
+ "Content": field(entry, "content"),
340
+ "Created": timefmt.absolute(field(entry, "create_time")),
341
+ "Updated": timefmt.absolute(field(entry, "update_time")),
342
+ },
343
+ status="ACTIVE",
344
+ )
345
+
346
+
347
+ @entries.command("create")
348
+ @click.option("--store", required=True, help="Store id or resource name.")
349
+ @click.option("--actor-id", required=True, help="Actor (partition) this entry belongs to.")
350
+ @click.option("--path", required=True, help="Absolute path, e.g. /preferences/style.md.")
351
+ @click.option(
352
+ "--content", default=None, help="Entry content (inline). Use --content-file for large content."
353
+ )
354
+ @click.option(
355
+ "--content-file",
356
+ "content_file",
357
+ type=click.Path(exists=True, dir_okay=False),
358
+ default=None,
359
+ help="Read entry content from a file (avoids shell arg-length limits on large content).",
360
+ )
361
+ @click.option("--description", default=None, help="Optional human-readable description.")
362
+ @click.option("--session-id", default=None, help="Optional session id to associate the entry with.")
363
+ @click.option(
364
+ "--source-type",
365
+ default=None,
366
+ help="Origin of the entry: 'agent' or 'unspecified'.",
367
+ )
368
+ @click.pass_obj
369
+ def entries_create(
370
+ obj, store, actor_id, path, content, content_file, description, session_id, source_type
371
+ ) -> None:
372
+ """Create a memory entry."""
373
+ if content is not None and content_file is not None:
374
+ raise AgentCliError("Pass either --content or --content-file, not both.")
375
+ if content_file is not None:
376
+ content = pathlib.Path(content_file).read_text()
377
+ data = obj.client().create_memory_entry(
378
+ store, actor_id, path, content, description, session_id, _normalize_source_type(source_type)
379
+ )
380
+ if obj.output == "json":
381
+ render.emit_json(data)
382
+ return
383
+ render.success(f"Created memory entry '{path}'", fields={"Name": field(data, "name")})
384
+
385
+
386
+ @entries.command("get")
387
+ @click.option(
388
+ "--store", default=None, help="Store id/name (optional if ENTRY is a full resource name)."
389
+ )
390
+ @click.argument("entry")
391
+ @click.pass_obj
392
+ def entries_get(obj, store, entry) -> None:
393
+ """Get an entry by id or resource name (includes content)."""
394
+ _require_entry_store(store, entry)
395
+ data = obj.client().get_memory_entry(store, entry)
396
+ if obj.output == "json":
397
+ render.emit_json(data)
398
+ return
399
+ _render_entry_detail(data)
400
+
401
+
402
+ @entries.command("list")
403
+ @click.option("--store", required=True)
404
+ @click.option("--actor-id", required=True, help="Required partition key.")
405
+ @click.option("--path-prefix", default=None)
406
+ @click.option("--session-id", default=None)
407
+ @click.option("--page-size", type=int, default=None)
408
+ @click.option("--page-token", default=None)
409
+ @click.pass_obj
410
+ def entries_list(obj, store, actor_id, path_prefix, session_id, page_size, page_token) -> None:
411
+ """List entries for an actor. The text view omits content; `-o json` includes it."""
412
+ data = obj.client().list_memory_entries(
413
+ store, actor_id, path_prefix, session_id, page_size, page_token
414
+ )
415
+ if obj.output == "json":
416
+ render.emit_json(data)
417
+ return
418
+ items = field(data, "managed_memory_entries") or []
419
+ rows = [
420
+ [
421
+ field(e, "path"),
422
+ field(e, "actor_id"),
423
+ field(e, "session_id"),
424
+ _truncate(field(e, "description"), 40),
425
+ timefmt.relative(field(e, "update_time")),
426
+ ]
427
+ for e in items
428
+ ]
429
+ render.resource_table(
430
+ f"Memory Entries · actor {actor_id}",
431
+ [
432
+ ("Path", "left"),
433
+ ("Actor", "left"),
434
+ ("Session", "left"),
435
+ ("Description", "left"),
436
+ ("Updated", "left"),
437
+ ],
438
+ rows,
439
+ subtitle=_page_note(data),
440
+ )
441
+
442
+
443
+ @entries.command("search")
444
+ @click.option("--store", required=True)
445
+ @click.option("--actor-id", required=True)
446
+ @click.option("--query", required=True)
447
+ @click.option("--page-size", type=int, default=None)
448
+ @click.pass_obj
449
+ def entries_search(obj, store, actor_id, query, page_size) -> None:
450
+ """Full-text search an actor's entries, ranked (includes content)."""
451
+ data = obj.client().search_memory_entries(
452
+ store,
453
+ actor_id,
454
+ query,
455
+ page_size=page_size,
456
+ )
457
+ if obj.output == "json":
458
+ render.emit_json(data)
459
+ return
460
+ results = field(data, "results")
461
+ items = (
462
+ [field(result, "managed_memory_entry") for result in results]
463
+ if results is not None
464
+ else field(data, "managed_memory_entries") or []
465
+ )
466
+ rows = [
467
+ [
468
+ field(e, "path"),
469
+ field(e, "actor_id"),
470
+ _truncate(field(e, "content"), 50),
471
+ timefmt.relative(field(e, "update_time")),
472
+ ]
473
+ for e in items
474
+ ]
475
+ render.resource_table(
476
+ f"Memory Search · '{query}'",
477
+ [("Path", "left"), ("Actor", "left"), ("Content", "left"), ("Updated", "left")],
478
+ rows,
479
+ )
480
+
481
+
482
+ @entries.command("update")
483
+ @click.option(
484
+ "--store", default=None, help="Store id/name (optional if ENTRY is a full resource name)."
485
+ )
486
+ @click.argument("entry")
487
+ @click.option("--content", default=None, help="New entry content.")
488
+ @click.option("--description", default=None, help="New description.")
489
+ @click.pass_obj
490
+ def entries_update(obj, store, entry, content, description) -> None:
491
+ """Update an entry's content and/or description."""
492
+ _require_entry_store(store, entry)
493
+ data = obj.client().update_memory_entry(store, entry, content, description)
494
+ if obj.output == "json":
495
+ render.emit_json(data)
496
+ return
497
+ _render_entry_detail(data)
498
+
499
+
500
+ @entries.command("delete")
501
+ @click.option(
502
+ "--store", default=None, help="Store id/name (optional if ENTRY is a full resource name)."
503
+ )
504
+ @click.argument("entry")
505
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
506
+ @click.pass_obj
507
+ def entries_delete(obj, store, entry, yes) -> None:
508
+ """Delete a memory entry."""
509
+ _require_entry_store(store, entry)
510
+ render.confirm_destroy(f"memory entry '{entry}'", assume_yes=yes)
511
+ obj.client().delete_memory_entry(store, entry)
512
+ if obj.output == "json":
513
+ render.emit_json({"deleted": entry})
514
+ return
515
+ render.success(f"Deleted memory entry '{entry}'")
516
+
517
+
518
+ def _page_note(data: dict) -> str | None:
519
+ token = field(data, "next_page_token")
520
+ return f"More results available — pass --page-token {token}" if token else None