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,588 @@
1
+ """`ab sessions` — manage session stores, sessions, and session items."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import pathlib
7
+ import sys
8
+ from typing import Any, Optional
9
+
10
+ import click
11
+
12
+ from databricks_agentbricks import render
13
+ from databricks_agentbricks.errors import AgentCliError
14
+ from databricks_agentbricks.render import field
15
+ from databricks_agentkit import timefmt
16
+
17
+ _BREADCRUMB = "Agent Session"
18
+
19
+
20
+ def _truncate(value: Any, length: int = 60) -> str:
21
+ text = "" if value is None else (value if isinstance(value, str) else json.dumps(value))
22
+ return text if len(text) <= length else text[: length - 1] + "…"
23
+
24
+
25
+ # --- group ------------------------------------------------------------------
26
+
27
+
28
+ @click.group()
29
+ def sessions() -> None:
30
+ """Manage an agent's conversations: session stores, the sessions in them, and their items.
31
+
32
+ A session is a single conversation between an actor (a user) and the agent. It holds that
33
+ conversation's ordered transcript of items — the messages, tool calls, and results that make up
34
+ its running state. A session store is the managed store that holds an agent's sessions and their
35
+ items, giving it durable conversation history it can list, resume, fork, or delete.
36
+
37
+ This is the short-term, per-conversation counterpart to the cross-conversation memory in
38
+ `ab memory`.
39
+ """
40
+
41
+
42
+ @sessions.group()
43
+ def stores() -> None:
44
+ """Workspace-scoped session stores."""
45
+
46
+
47
+ @sessions.group()
48
+ def items() -> None:
49
+ """Transcript items within a session."""
50
+
51
+
52
+ # --- bind a store to an agent project (agent.toml) --------------------------
53
+
54
+
55
+ def _source_option(function):
56
+ return click.option(
57
+ "--source",
58
+ type=click.Path(exists=True, file_okay=False, path_type=pathlib.Path),
59
+ default=pathlib.Path("."),
60
+ show_default=True,
61
+ help="Agent Bricks project containing agent.toml.",
62
+ )(function)
63
+
64
+
65
+ @sessions.command("bind")
66
+ @click.argument("store")
67
+ @_source_option
68
+ @click.pass_obj
69
+ def sessions_bind(obj, store: str, source: pathlib.Path) -> None:
70
+ """Bind session STORE to the agent by declaring it in agent.toml.
71
+
72
+ This only edits agent.toml — it does not create the store. `ab deploy` creates any declared
73
+ store that doesn't exist yet and grants the deployed app's service principal access to it.
74
+ """
75
+ from databricks_agentbricks.agent_project import AgentProject
76
+
77
+ project = AgentProject.load(source)
78
+ project.bind_session_store(store)
79
+ project.write()
80
+ if obj.output == "json":
81
+ render.emit_json({"session_store": store, "manifest": str(project.path)})
82
+ return
83
+ render.success(
84
+ f"Bound session store '{store}'",
85
+ fields={"agent.toml": str(project.path)},
86
+ next_steps=[
87
+ (
88
+ f"ab sessions stores create --name {store}",
89
+ "Create the store now without deploying",
90
+ ),
91
+ ("ab dev", "Re-run to pick up the store locally"),
92
+ ("ab deploy <name>", "Create it if missing and grant the app access"),
93
+ ],
94
+ )
95
+
96
+
97
+ @sessions.command("unbind")
98
+ @_source_option
99
+ @click.pass_obj
100
+ def sessions_unbind(obj, source: pathlib.Path) -> None:
101
+ """Remove the session store binding from the agent's agent.toml.
102
+
103
+ Only edits agent.toml; the managed store itself is untouched (delete it with
104
+ `ab sessions stores delete`).
105
+ """
106
+ from databricks_agentbricks.agent_project import AgentProject
107
+
108
+ project = AgentProject.load(source)
109
+ if project.unbind_session_store():
110
+ project.write()
111
+ render.success("Removed session store binding", fields={"agent.toml": str(project.path)})
112
+ else:
113
+ click.echo(f"No session store binding in {project.path}.")
114
+
115
+
116
+ # --- session stores ---------------------------------------------------------
117
+
118
+
119
+ def _render_store_detail(store: dict) -> None:
120
+ name = field(store, "session_store_name")
121
+ render.detail(
122
+ f"{_BREADCRUMB} Store",
123
+ name or "—",
124
+ {
125
+ "Name": name,
126
+ "Resource name": f"session-stores/{name}" if name else None,
127
+ "Store ID": field(store, "session_store_id"),
128
+ "Creator": field(store, "creator_user_id"),
129
+ "Storage": render.field(field(store, "storage_backend") or {}, "backend_id"),
130
+ "Description": field(store, "description"),
131
+ "Created": timefmt.absolute(field(store, "create_time")),
132
+ "Updated": timefmt.absolute(field(store, "update_time")),
133
+ },
134
+ status="ACTIVE",
135
+ )
136
+
137
+
138
+ @stores.command("create")
139
+ @click.option(
140
+ "--name",
141
+ "--display-name",
142
+ "name",
143
+ required=True,
144
+ help="Workspace-unique store name, 3-63 chars (--display-name is accepted as an alias).",
145
+ )
146
+ @click.option("--description", default=None)
147
+ @click.option("--metadata", default=None, help="JSON object of string labels.")
148
+ @click.pass_obj
149
+ def stores_create(obj, name, description, metadata) -> None:
150
+ """Create a session store."""
151
+ with render.status(f"Creating session store '{name}'…"):
152
+ data = obj.client().create_session_store(name, description, _parse_metadata(metadata))
153
+ if obj.output == "json":
154
+ render.emit_json(data)
155
+ return
156
+ render.success(
157
+ f"Created session store '{name}'",
158
+ fields={"Store ID": field(data, "session_store_id")},
159
+ next_steps=[
160
+ (
161
+ f"ab sessions create --store {name} --actor-id <id>",
162
+ "Start a session for an actor",
163
+ ),
164
+ (f"ab sessions stores get {name}", "View this store's details"),
165
+ (
166
+ f"ab sessions bind {name}",
167
+ "Bind this store to the agent (wired in on dev/deploy)",
168
+ ),
169
+ ],
170
+ )
171
+
172
+
173
+ @stores.command("list")
174
+ @click.option("--page-size", type=int, default=25, show_default=True)
175
+ @click.option("--page-token", default=None)
176
+ @click.pass_obj
177
+ def stores_list(obj, page_size, page_token) -> None:
178
+ """List session stores in the workspace (25 per page; paginates interactively on a terminal)."""
179
+ client = obj.client()
180
+ if obj.output == "json":
181
+ render.emit_json(client.list_session_stores(page_size, page_token))
182
+ return
183
+ # On a terminal, offer to fetch the next page instead of only printing the --page-token hint.
184
+ interactive = sys.stdin.isatty() and sys.stdout.isatty()
185
+ while True:
186
+ data = client.list_session_stores(page_size, page_token)
187
+ items_ = field(data, "session_stores") or []
188
+ rows = [
189
+ [
190
+ field(s, "session_store_name"),
191
+ timefmt.relative(field(s, "create_time")),
192
+ timefmt.relative(field(s, "update_time")),
193
+ _truncate(field(s, "description"), 40),
194
+ ]
195
+ for s in items_
196
+ ]
197
+ token = field(data, "next_page_token")
198
+ render.resource_table(
199
+ "Session Stores",
200
+ [
201
+ # A session store's resource name is its workspace-unique, human-readable name.
202
+ ("Resource name", "left"),
203
+ ("Created", "left"),
204
+ ("Updated", "left"),
205
+ ("Description", "left"),
206
+ ],
207
+ rows,
208
+ # In an interactive session the prompt below replaces the token hint.
209
+ subtitle=None if (interactive and token) else _page_note(data),
210
+ no_wrap=[0], # keep the resource name full-width; other columns narrow to fit
211
+ )
212
+ if not token or not interactive:
213
+ break
214
+ if not click.confirm("Show next page?", default=False):
215
+ break
216
+ page_token = token
217
+
218
+
219
+ @stores.command("get")
220
+ @click.argument("name")
221
+ @click.pass_obj
222
+ def stores_get(obj, name) -> None:
223
+ """Get a session store by name."""
224
+ data = obj.client().get_session_store(name)
225
+ if obj.output == "json":
226
+ render.emit_json(data)
227
+ return
228
+ _render_store_detail(data)
229
+
230
+
231
+ @stores.command("update")
232
+ @click.argument("name")
233
+ @click.option("--description", default=None)
234
+ @click.option("--metadata", default=None, help="JSON object of string labels.")
235
+ @click.pass_obj
236
+ def stores_update(obj, name, description, metadata) -> None:
237
+ """Update a store's description and/or metadata."""
238
+ data = obj.client().update_session_store(name, description, _parse_metadata(metadata))
239
+ if obj.output == "json":
240
+ render.emit_json(data)
241
+ return
242
+ _render_store_detail(data)
243
+
244
+
245
+ @stores.command("delete")
246
+ @click.argument("name")
247
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
248
+ @click.pass_obj
249
+ def stores_delete(obj, name, yes) -> None:
250
+ """Delete a session store."""
251
+ render.confirm_destroy(f"session store '{name}'", assume_yes=yes)
252
+ obj.client().delete_session_store(name)
253
+ if obj.output == "json":
254
+ render.emit_json({"deleted": name})
255
+ return
256
+ render.success(f"Deleted session store '{name}'")
257
+
258
+
259
+ # --- sessions ---------------------------------------------------------------
260
+
261
+
262
+ def _session_starter_code(obj, store: str, session_id: str) -> list[tuple[str, str, str]]:
263
+ return [
264
+ (
265
+ "curl",
266
+ "bash",
267
+ f"""
268
+ curl -X POST "{obj.client().host}/api/2.0/agents/session-stores/{store}/sessions/{session_id}/items:append" \\
269
+ -H "Authorization: Bearer $DATABRICKS_TOKEN" -H "Content-Type: application/json" \\
270
+ -d '{{"items": [{{"data": {{"role": "user", "content": "Hello"}}}}]}}'
271
+ """,
272
+ ),
273
+ (
274
+ "agentbricks",
275
+ "bash",
276
+ f"""
277
+ ab sessions items append --store {store} --session-id {session_id} \\
278
+ --data '{{"role": "user", "content": "Hello"}}'
279
+ ab sessions items list --store {store} --session-id {session_id}
280
+ """,
281
+ ),
282
+ ]
283
+
284
+
285
+ def _render_session_detail(obj, session: dict, store: Optional[str]) -> None:
286
+ store = store or field(session, "session_store_name")
287
+ session_id = field(session, "session_id")
288
+ render.detail(
289
+ _BREADCRUMB,
290
+ session_id or "—",
291
+ {
292
+ "Session ID": session_id,
293
+ "Store": field(session, "session_store_name") or store,
294
+ "Actor": field(session, "actor_id"),
295
+ "Parent": field(session, "parent_session_id"),
296
+ "Root": field(session, "root_session_id"),
297
+ "Created": timefmt.absolute(field(session, "create_time")),
298
+ "Last activity": timefmt.absolute(field(session, "last_activity_time")),
299
+ },
300
+ status="ACTIVE",
301
+ snippets=_session_starter_code(obj, store, session_id) if store and session_id else None,
302
+ )
303
+
304
+
305
+ @sessions.command("create")
306
+ @click.option("--store", required=True)
307
+ @click.option("--actor-id", required=True, help="Application actor id (child must match parent).")
308
+ @click.option("--session-id", default=None, help="Optional caller-chosen id.")
309
+ @click.option("--parent-session-id", default=None)
310
+ @click.option("--metadata", default=None, help="JSON object of string labels.")
311
+ @click.pass_obj
312
+ def sessions_create(obj, store, actor_id, session_id, parent_session_id, metadata) -> None:
313
+ """Create a session in a store."""
314
+ data = obj.client().create_session(
315
+ store, actor_id, session_id, parent_session_id, _parse_metadata(metadata)
316
+ )
317
+ if obj.output == "json":
318
+ render.emit_json(data)
319
+ return
320
+ render.success(
321
+ f"Created session '{field(data, 'session_id')}'",
322
+ fields={"Actor": actor_id, "Store": store},
323
+ next_steps=[
324
+ (
325
+ f"ab sessions items append --store {store} "
326
+ f"--session-id {field(data, 'session_id')} --data '{{...}}'",
327
+ "Append an item to this session",
328
+ ),
329
+ ],
330
+ )
331
+
332
+
333
+ @sessions.command("list")
334
+ @click.option("--store", required=True)
335
+ @click.option("--filter", "filter_", default=None, help='e.g. actor_id = "support-123".')
336
+ @click.option("--order-by", default=None, help="e.g. 'last_activity_time desc'.")
337
+ @click.option("--page-size", type=int, default=None)
338
+ @click.option("--page-token", default=None)
339
+ @click.pass_obj
340
+ def sessions_list(obj, store, filter_, order_by, page_size, page_token) -> None:
341
+ """List sessions in a store."""
342
+ data = obj.client().list_sessions(store, filter_, order_by, page_size, page_token)
343
+ if obj.output == "json":
344
+ render.emit_json(data)
345
+ return
346
+ items_ = field(data, "sessions") or []
347
+ rows = [
348
+ [
349
+ field(s, "session_id"),
350
+ field(s, "actor_id"),
351
+ field(s, "root_session_id"),
352
+ timefmt.relative(field(s, "create_time")),
353
+ timefmt.relative(field(s, "last_activity_time")),
354
+ ]
355
+ for s in items_
356
+ ]
357
+ render.resource_table(
358
+ f"Sessions · {store}",
359
+ [
360
+ ("Session ID", "left"),
361
+ ("Actor", "left"),
362
+ ("Root", "left"),
363
+ ("Created", "left"),
364
+ ("Last Activity", "left"),
365
+ ],
366
+ rows,
367
+ subtitle=_page_note(data),
368
+ )
369
+
370
+
371
+ @sessions.command("get")
372
+ @click.argument("session_id")
373
+ @click.option("--store", default=None, help="Session store name (required in this preview).")
374
+ @click.pass_obj
375
+ def sessions_get(obj, session_id, store) -> None:
376
+ """Get a session by id."""
377
+ if not store:
378
+ raise AgentCliError(
379
+ "Provide --store. Resolving a session by id alone is not supported in this preview."
380
+ )
381
+ data = obj.client().get_session(session_id, store)
382
+ if obj.output == "json":
383
+ render.emit_json(data)
384
+ return
385
+ _render_session_detail(obj, data, store)
386
+
387
+
388
+ @sessions.command("update")
389
+ @click.argument("session_id")
390
+ @click.option("--store", required=True)
391
+ @click.option(
392
+ "--metadata", required=True, help="JSON object of string labels (only mutable field)."
393
+ )
394
+ @click.pass_obj
395
+ def sessions_update(obj, session_id, store, metadata) -> None:
396
+ """Update a session's metadata."""
397
+ data = obj.client().update_session(store, session_id, _parse_metadata(metadata) or {})
398
+ if obj.output == "json":
399
+ render.emit_json(data)
400
+ return
401
+ _render_session_detail(obj, data, store)
402
+
403
+
404
+ @sessions.command("delete")
405
+ @click.argument("session_id")
406
+ @click.option("--store", required=True)
407
+ @click.option("--force", is_flag=True, help="Cascade-delete descendant sessions.")
408
+ @click.option("--yes", "-y", is_flag=True, help="Skip the confirmation prompt.")
409
+ @click.pass_obj
410
+ def sessions_delete(obj, session_id, store, force, yes) -> None:
411
+ """Delete a session."""
412
+ render.confirm_destroy(f"session '{session_id}'", assume_yes=yes)
413
+ obj.client().delete_session(store, session_id, force)
414
+ if obj.output == "json":
415
+ render.emit_json({"deleted": session_id})
416
+ return
417
+ render.success(f"Deleted session '{session_id}'")
418
+
419
+
420
+ @sessions.command("fork")
421
+ @click.argument("source_session_id_arg", required=False, metavar="[SOURCE_SESSION_ID]")
422
+ @click.option("--store", required=True)
423
+ @click.option(
424
+ "--source-session-id",
425
+ "source_session_id_opt",
426
+ default=None,
427
+ help="Source session to fork (or pass it as the positional argument).",
428
+ )
429
+ @click.option("--actor-id", required=True)
430
+ @click.option("--up-to-item-id", default=None, help="Copy through this item id inclusively.")
431
+ @click.option("--session-id", default=None, help="Optional id for the fork.")
432
+ @click.option("--metadata", default=None)
433
+ @click.pass_obj
434
+ def sessions_fork(
435
+ obj,
436
+ source_session_id_arg,
437
+ store,
438
+ source_session_id_opt,
439
+ actor_id,
440
+ up_to_item_id,
441
+ session_id,
442
+ metadata,
443
+ ) -> None:
444
+ """Fork a session into a new independent top-level session."""
445
+ source_session_id = source_session_id_arg or source_session_id_opt
446
+ if not source_session_id:
447
+ raise AgentCliError(
448
+ "Provide the source session id (as the positional argument or --source-session-id)."
449
+ )
450
+ if source_session_id_arg and source_session_id_opt:
451
+ raise AgentCliError(
452
+ "Pass the source session id once — either positionally or via --source-session-id."
453
+ )
454
+ data = obj.client().fork_session(
455
+ store, source_session_id, actor_id, up_to_item_id, session_id, _parse_metadata(metadata)
456
+ )
457
+ if obj.output == "json":
458
+ render.emit_json(data)
459
+ return
460
+ _render_session_detail(obj, field(data, "session") or {}, store)
461
+
462
+
463
+ # --- session items ----------------------------------------------------------
464
+
465
+
466
+ @items.command("list")
467
+ @click.option("--store", required=True)
468
+ @click.option("--session-id", required=True)
469
+ @click.option("--order-by", default=None, help="'create_time asc' or 'create_time desc'.")
470
+ @click.option("--page-size", type=int, default=None)
471
+ @click.option("--page-token", default=None)
472
+ @click.pass_obj
473
+ def items_list(obj, store, session_id, order_by, page_size, page_token) -> None:
474
+ """List transcript items in a session."""
475
+ data = obj.client().list_session_items(store, session_id, order_by, page_size, page_token)
476
+ if obj.output == "json":
477
+ render.emit_json(data)
478
+ return
479
+ items_ = field(data, "session_items") or []
480
+ rows = [
481
+ [
482
+ field(it, "item_id"),
483
+ timefmt.relative(field(it, "create_time")),
484
+ _truncate(field(it, "data"), 70),
485
+ ]
486
+ for it in items_
487
+ ]
488
+ render.resource_table(
489
+ f"Session Items · {session_id}",
490
+ [("Item ID", "left"), ("Created", "left"), ("Data", "left")],
491
+ rows,
492
+ subtitle=_page_note(data),
493
+ )
494
+
495
+
496
+ @items.command("append")
497
+ @click.option("--store", required=True)
498
+ @click.option("--session-id", required=True)
499
+ @click.option("--data", "data_", multiple=True, help="One item's JSON data (repeatable).")
500
+ @click.option(
501
+ "--file", "file_", type=click.File("r"), default=None, help="JSON array of item data values."
502
+ )
503
+ @click.pass_obj
504
+ def items_append(obj, store, session_id, data_, file_) -> None:
505
+ """Append one or more items to a session (atomic, in order)."""
506
+ payload = _load_items(data_, file_)
507
+ result = obj.client().append_session_items(store, session_id, payload)
508
+ if obj.output == "json":
509
+ render.emit_json(result)
510
+ return
511
+ appended = field(result, "session_items") or []
512
+ render.success(f"Appended {len(appended)} item(s) to session '{session_id}'")
513
+
514
+
515
+ @items.command("pop")
516
+ @click.option("--store", required=True)
517
+ @click.option("--session-id", required=True)
518
+ @click.pass_obj
519
+ def items_pop(obj, store, session_id) -> None:
520
+ """Remove and return the most recent item."""
521
+ data = obj.client().pop_session_item(store, session_id)
522
+ if obj.output == "json":
523
+ render.emit_json(data)
524
+ return
525
+ item = field(data, "item")
526
+ if not item:
527
+ render.success("Session was already empty")
528
+ return
529
+ render.success(
530
+ "Popped last item",
531
+ fields={
532
+ "Item ID": field(item, "item_id"),
533
+ "Data": _truncate(field(item, "data"), 70),
534
+ },
535
+ )
536
+
537
+
538
+ @items.command("clear")
539
+ @click.option("--store", required=True)
540
+ @click.option("--session-id", required=True)
541
+ @click.pass_obj
542
+ def items_clear(obj, store, session_id) -> None:
543
+ """Remove all items from a session."""
544
+ obj.client().clear_session_items(store, session_id)
545
+ if obj.output == "json":
546
+ render.emit_json({"cleared": session_id})
547
+ return
548
+ render.success(f"Cleared items from session '{session_id}'")
549
+
550
+
551
+ # --- shared helpers ---------------------------------------------------------
552
+
553
+
554
+ def _parse_metadata(value: Optional[str]) -> Optional[dict]:
555
+ if value is None:
556
+ return None
557
+ try:
558
+ parsed = json.loads(value)
559
+ except json.JSONDecodeError as exc:
560
+ raise AgentCliError(f"--metadata must be valid JSON: {exc}") from exc
561
+ if not isinstance(parsed, dict):
562
+ raise AgentCliError("--metadata must be a JSON object of string labels.")
563
+ return parsed
564
+
565
+
566
+ def _load_items(data_: tuple[str, ...], file_) -> list[Any]:
567
+ payload: list[Any] = []
568
+ if file_ is not None:
569
+ try:
570
+ loaded = json.load(file_)
571
+ except json.JSONDecodeError as exc:
572
+ raise AgentCliError(f"--file must contain a JSON array: {exc}") from exc
573
+ if not isinstance(loaded, list):
574
+ raise AgentCliError("--file must contain a JSON array of item data values.")
575
+ payload.extend(loaded)
576
+ for raw in data_:
577
+ try:
578
+ payload.append(json.loads(raw))
579
+ except json.JSONDecodeError as exc:
580
+ raise AgentCliError(f"--data must be valid JSON: {exc}") from exc
581
+ if not payload:
582
+ raise AgentCliError("Provide at least one --data item or a --file.")
583
+ return payload
584
+
585
+
586
+ def _page_note(data: dict) -> str | None:
587
+ token = field(data, "next_page_token")
588
+ return f"More results available — pass --page-token {token}" if token else None