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,118 @@
1
+ """Contracts shared by Agent Bricks Runtime, stores, and agent applications.
2
+
3
+ The runtime returns :class:`Invocation` snapshots and ordered :class:`InvocationEvent` records.
4
+ An executor receives an :class:`InvocationAttemptContext` for attempt fencing and event emission.
5
+ ``DurableAgentServer`` adapts that lower-level context into :class:`InvocationContext` for functions
6
+ registered with ``@app.invoke`` and ``@app.recover``.
7
+
8
+ All request, response, and event payloads use the recursive ``JsonValue`` / ``JsonObject`` aliases,
9
+ so Runtime Store values can be stored consistently by in-memory and Lakebase implementations.
10
+ """
11
+
12
+ from collections.abc import Awaitable, Callable
13
+ from dataclasses import dataclass, field
14
+ from enum import Enum
15
+ from typing import TYPE_CHECKING, TypeAlias
16
+
17
+ if TYPE_CHECKING:
18
+ from databricks_agentkit.runtime.auth import RequestAuthContext
19
+
20
+ JsonValue: TypeAlias = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"]
21
+ JsonObject = dict[str, JsonValue]
22
+ InvocationEventEmitter = Callable[[JsonObject], Awaitable[int]]
23
+
24
+
25
+ class InvocationStatus(str, Enum):
26
+ """Lifecycle states stored by Agent Bricks Runtime."""
27
+
28
+ QUEUED = "QUEUED"
29
+ ACTIVE = "ACTIVE"
30
+ COMPLETED = "COMPLETED"
31
+ FAILED = "FAILED"
32
+
33
+
34
+ @dataclass(frozen=True)
35
+ class Invocation:
36
+ """A Runtime Store snapshot for one idempotent invocation and its owning attempt."""
37
+
38
+ invocation_id: str
39
+ status: InvocationStatus
40
+ attempt: int
41
+ request: JsonValue
42
+ response: JsonValue
43
+
44
+ @property
45
+ def is_terminal(self) -> bool:
46
+ """Whether this invocation can no longer transition or emit events."""
47
+ return self.status in {
48
+ InvocationStatus.COMPLETED,
49
+ InvocationStatus.FAILED,
50
+ }
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class InvocationEvent:
55
+ """One persisted event emitted by an invocation attempt."""
56
+
57
+ sequence_number: int
58
+ invocation_id: str
59
+ attempt: int
60
+ event: JsonObject
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class InvocationAttemptContext:
65
+ """Attempt metadata and event emission passed to the runtime's executor function."""
66
+
67
+ invocation_id: str
68
+ attempt: int
69
+ _emit: InvocationEventEmitter | None = field(default=None, repr=False, compare=False)
70
+
71
+ @property
72
+ def is_recovery(self) -> bool:
73
+ """Whether this is a replacement attempt after an earlier worker stopped heartbeating."""
74
+ return self.attempt > 1
75
+
76
+ async def emit(self, event: JsonObject) -> int:
77
+ """Persist an ordered event and return its replay cursor."""
78
+ if self._emit is None:
79
+ raise RuntimeError("event emission is not available for this invocation context")
80
+ return await self._emit(event)
81
+
82
+
83
+ InvocationExecutorFn = Callable[[JsonValue, InvocationAttemptContext], Awaitable[JsonValue]]
84
+
85
+
86
+ @dataclass(frozen=True)
87
+ class InvocationContext:
88
+ """Invocation/session metadata and event emission for a decorated agent function."""
89
+
90
+ invocation_id: str
91
+ session_id: str
92
+ attempt: int
93
+ _attempt_context: InvocationAttemptContext = field(repr=False, compare=False)
94
+ request_auth: "RequestAuthContext | None" = field(default=None, repr=False, compare=False)
95
+
96
+ @property
97
+ def is_recovery(self) -> bool:
98
+ """Whether ``@app.recover`` is handling a replacement attempt."""
99
+ return self.attempt > 1
100
+
101
+ async def emit(self, event: JsonObject) -> int:
102
+ """Persist an ordered application event and return its replay cursor."""
103
+ return await self._attempt_context.emit(event)
104
+
105
+
106
+ InvocationHook = Callable[[JsonValue, InvocationContext], Awaitable[JsonValue]]
107
+
108
+
109
+ class InvocationConflictError(ValueError):
110
+ """Raised when an invocation ID is reused with a different request."""
111
+
112
+
113
+ class InvocationNotFoundError(LookupError):
114
+ """Raised when waiting for an unknown invocation ID."""
115
+
116
+
117
+ class InvocationFailedError(RuntimeError):
118
+ """Raised when an invocation reaches the failed state."""
@@ -0,0 +1,27 @@
1
+ """Construct Databricks SDK clients with workspace routing when required."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from databricks.sdk import WorkspaceClient
8
+
9
+
10
+ def workspace_headers() -> dict[str, str]:
11
+ """Return headers required to route an account-host request to its workspace."""
12
+ workspace_id = os.getenv("DATABRICKS_WORKSPACE_ID", "").strip()
13
+ return {"X-Databricks-Org-Id": workspace_id} if workspace_id else {}
14
+
15
+
16
+ def workspace_client() -> WorkspaceClient:
17
+ """Return the environment-authenticated client for the active workspace.
18
+
19
+ ``databricks apps run-local`` can authenticate through an account-level vanity host while
20
+ exposing the target workspace through ``DATABRICKS_WORKSPACE_ID``. The SDK needs the same
21
+ routing header as the Databricks CLI for those profiles. Ordinary workspace hosts and deployed
22
+ Apps continue to use the SDK's default authentication chain.
23
+ """
24
+ headers = workspace_headers()
25
+ if not headers:
26
+ return WorkspaceClient()
27
+ return WorkspaceClient(custom_headers=headers)
@@ -0,0 +1,434 @@
1
+ """Typed session-store resources and operations."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterator, Sequence
6
+ from dataclasses import dataclass, field
7
+ from datetime import datetime
8
+ from typing import TYPE_CHECKING, Any, List, Optional
9
+
10
+ from databricks_agentkit._pagination import validate_page_size
11
+ from databricks_agentkit.timefmt import parse_timestamp
12
+
13
+ if TYPE_CHECKING:
14
+ from databricks_agentkit._api_client import _AgentBricksApiClient
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class SessionItem:
19
+ item_id: str
20
+ data: Any
21
+ create_time: Optional[datetime] = None
22
+
23
+
24
+ @dataclass(frozen=True, kw_only=True)
25
+ class ExtractedMemory:
26
+ """A memory entry produced by `Session.extract_memories` (a read-only result view)."""
27
+
28
+ name: str
29
+ actor_id: Optional[str] = None
30
+ path: Optional[str] = None
31
+ content: Optional[str] = None
32
+ description: Optional[str] = None
33
+ session_id: Optional[str] = None
34
+ source_type: Optional[str] = None
35
+ create_time: Optional[datetime] = None
36
+ update_time: Optional[datetime] = None
37
+
38
+
39
+ @dataclass(frozen=True, kw_only=True)
40
+ class Session:
41
+ store_name: str
42
+ session_id: str
43
+ actor_id: str
44
+ parent_session_id: Optional[str] = None
45
+ root_session_id: Optional[str] = None
46
+ metadata: dict[str, str] = field(default_factory=dict)
47
+ create_time: Optional[datetime] = None
48
+ update_time: Optional[datetime] = None
49
+ last_activity_time: Optional[datetime] = None
50
+ _client: SessionStores = field(repr=False, compare=False)
51
+
52
+ def update(self, *, metadata: dict[str, str]) -> Session:
53
+ return self._client._update_session(self, metadata=metadata)
54
+
55
+ def delete(self, *, force: bool = False) -> None:
56
+ self._client._delete_session(self, force=force)
57
+
58
+ def fork(
59
+ self,
60
+ *,
61
+ actor_id: str,
62
+ up_to_item_id: Optional[str] = None,
63
+ session_id: Optional[str] = None,
64
+ metadata: Optional[dict[str, str]] = None,
65
+ ) -> Session:
66
+ return self._client._fork_session(
67
+ self,
68
+ actor_id=actor_id,
69
+ up_to_item_id=up_to_item_id,
70
+ session_id=session_id,
71
+ metadata=metadata,
72
+ )
73
+
74
+ def list_items(
75
+ self,
76
+ *,
77
+ page_size: Optional[int] = None,
78
+ order_by: Optional[str] = None,
79
+ ) -> Iterator[SessionItem]:
80
+ return self._client._list_items(
81
+ self,
82
+ page_size=page_size,
83
+ order_by=order_by,
84
+ )
85
+
86
+ def append_items(self, items: Sequence[Any]) -> List[SessionItem]:
87
+ return self._client._append_items(self, items=items)
88
+
89
+ def pop_item(self) -> Optional[SessionItem]:
90
+ return self._client._pop_item(self)
91
+
92
+ def clear_items(self) -> None:
93
+ self._client._clear_items(self)
94
+
95
+ def extract_memories(
96
+ self,
97
+ *,
98
+ memory_store: str,
99
+ instructions: Optional[str] = None,
100
+ dry_run: bool = False,
101
+ ) -> List[ExtractedMemory]:
102
+ """Synchronously distill this session's transcript into memory entries.
103
+
104
+ Writes the entries to `memory_store` and returns them; pass `dry_run=True` to return the
105
+ extracted entries without persisting them.
106
+ """
107
+ return self._client._extract_memories(
108
+ self, memory_store=memory_store, instructions=instructions, dry_run=dry_run
109
+ )
110
+
111
+
112
+ @dataclass(frozen=True, kw_only=True)
113
+ class SessionStore:
114
+ name: str
115
+ session_store_id: Optional[str] = None
116
+ creator_user_id: Optional[str] = None
117
+ create_time: Optional[datetime] = None
118
+ update_time: Optional[datetime] = None
119
+ description: Optional[str] = None
120
+ metadata: dict[str, str] = field(default_factory=dict)
121
+ _client: SessionStores = field(repr=False, compare=False)
122
+
123
+ def update(
124
+ self,
125
+ *,
126
+ description: Optional[str] = None,
127
+ metadata: Optional[dict[str, str]] = None,
128
+ ) -> SessionStore:
129
+ return self._client._update_store(
130
+ self,
131
+ description=description,
132
+ metadata=metadata,
133
+ )
134
+
135
+ def delete(self) -> None:
136
+ self._client._delete_store(self)
137
+
138
+ def add(
139
+ self,
140
+ *,
141
+ actor_id: str,
142
+ session_id: Optional[str] = None,
143
+ parent_session_id: Optional[str] = None,
144
+ metadata: Optional[dict[str, str]] = None,
145
+ ) -> Session:
146
+ return self._client._add_session(
147
+ self,
148
+ actor_id=actor_id,
149
+ session_id=session_id,
150
+ parent_session_id=parent_session_id,
151
+ metadata=metadata,
152
+ )
153
+
154
+ def list(
155
+ self,
156
+ *,
157
+ page_size: Optional[int] = None,
158
+ filter: Optional[str] = None,
159
+ order_by: Optional[str] = None,
160
+ ) -> Iterator[Session]:
161
+ return self._client._list_sessions(
162
+ self,
163
+ page_size=page_size,
164
+ filter=filter,
165
+ order_by=order_by,
166
+ )
167
+
168
+ def get(self, session_id: str) -> Session:
169
+ return self._client._get_session(self, session_id=session_id)
170
+
171
+ def grant_permission(self, principal_id: str, *, permission: str = "WRITE") -> None:
172
+ """Grant a service principal READ/WRITE access to this session store.
173
+
174
+ ``principal_id`` is the service principal's application (client) id. The grant is applied
175
+ server-side, so the caller needs neither store ownership nor Lakebase MANAGE.
176
+ """
177
+ self._client._grant_permission(self, principal_id, permission=permission)
178
+
179
+
180
+ class SessionStores:
181
+ def __init__(self, api: _AgentBricksApiClient):
182
+ self._api = api
183
+
184
+ def create(
185
+ self,
186
+ name: str,
187
+ *,
188
+ description: Optional[str] = None,
189
+ metadata: Optional[dict[str, str]] = None,
190
+ ) -> SessionStore:
191
+ response = self._api.create_session_store(
192
+ name,
193
+ description,
194
+ metadata,
195
+ )
196
+ return self._store_from_response(response)
197
+
198
+ def list(self, *, page_size: Optional[int] = None) -> Iterator[SessionStore]:
199
+ validate_page_size(page_size)
200
+ page_token = None
201
+ while True:
202
+ response = self._api.list_session_stores(
203
+ page_size=page_size,
204
+ page_token=page_token,
205
+ )
206
+ for store in response.get("session_stores", []):
207
+ yield self._store_from_response(store)
208
+ page_token = response.get("next_page_token")
209
+ if not page_token:
210
+ return
211
+
212
+ def get(self, name: str) -> SessionStore:
213
+ return self._store_from_response(self._api.get_session_store(name))
214
+
215
+ def _update_store(
216
+ self,
217
+ store: SessionStore,
218
+ *,
219
+ description: Optional[str] = None,
220
+ metadata: Optional[dict[str, str]] = None,
221
+ ) -> SessionStore:
222
+ if description is None and metadata is None:
223
+ raise ValueError("at least one of description and metadata is required")
224
+ response = self._api.update_session_store(
225
+ store.name,
226
+ description=description,
227
+ metadata=metadata,
228
+ )
229
+ return self._store_from_response(response)
230
+
231
+ def _delete_store(self, store: SessionStore) -> None:
232
+ self._api.delete_session_store(store.name)
233
+
234
+ def _grant_permission(self, store: SessionStore, principal_id: str, *, permission: str) -> None:
235
+ self._api.grant_session_store_permission(store.name, principal_id, permission=permission)
236
+
237
+ def _add_session(
238
+ self,
239
+ store: SessionStore,
240
+ *,
241
+ actor_id: str,
242
+ session_id: Optional[str] = None,
243
+ parent_session_id: Optional[str] = None,
244
+ metadata: Optional[dict[str, str]] = None,
245
+ ) -> Session:
246
+ response = self._api.create_session(
247
+ store.name,
248
+ actor_id,
249
+ session_id=session_id,
250
+ parent_session_id=parent_session_id,
251
+ metadata=metadata,
252
+ )
253
+ return self._session_from_response(response, store.name)
254
+
255
+ def _list_sessions(
256
+ self,
257
+ store: SessionStore,
258
+ *,
259
+ page_size: Optional[int] = None,
260
+ filter: Optional[str] = None,
261
+ order_by: Optional[str] = None,
262
+ ) -> Iterator[Session]:
263
+ validate_page_size(page_size)
264
+ if order_by is None:
265
+ order_by = "create_time desc"
266
+ page_token = None
267
+ while True:
268
+ response = self._api.list_sessions(
269
+ store.name,
270
+ filter=filter,
271
+ order_by=order_by,
272
+ page_size=page_size,
273
+ page_token=page_token,
274
+ )
275
+ for session in response.get("sessions", []):
276
+ yield self._session_from_response(session, store.name)
277
+ page_token = response.get("next_page_token")
278
+ if not page_token:
279
+ return
280
+
281
+ def _get_session(
282
+ self,
283
+ store: SessionStore,
284
+ *,
285
+ session_id: str,
286
+ ) -> Session:
287
+ response = self._api.get_session(session_id, store.name)
288
+ return self._session_from_response(response, store.name)
289
+
290
+ def _update_session(self, session: Session, *, metadata: dict[str, str]) -> Session:
291
+ response = self._api.update_session(
292
+ session.store_name,
293
+ session.session_id,
294
+ metadata,
295
+ )
296
+ return self._session_from_response(response, session.store_name)
297
+
298
+ def _delete_session(self, session: Session, *, force: bool = False) -> None:
299
+ self._api.delete_session(session.store_name, session.session_id, force=force)
300
+
301
+ def _fork_session(
302
+ self,
303
+ session: Session,
304
+ *,
305
+ actor_id: str,
306
+ up_to_item_id: Optional[str] = None,
307
+ session_id: Optional[str] = None,
308
+ metadata: Optional[dict[str, str]] = None,
309
+ ) -> Session:
310
+ response = self._api.fork_session(
311
+ session.store_name,
312
+ session.session_id,
313
+ actor_id,
314
+ up_to_item_id=up_to_item_id,
315
+ session_id=session_id,
316
+ metadata=metadata,
317
+ )
318
+ return self._session_from_response(
319
+ response.get("session", response),
320
+ session.store_name,
321
+ )
322
+
323
+ def _list_items(
324
+ self,
325
+ session: Session,
326
+ *,
327
+ page_size: Optional[int] = None,
328
+ order_by: Optional[str] = None,
329
+ ) -> Iterator[SessionItem]:
330
+ validate_page_size(page_size)
331
+ page_token = None
332
+ while True:
333
+ response = self._api.list_session_items(
334
+ session.store_name,
335
+ session.session_id,
336
+ order_by=order_by,
337
+ page_size=page_size,
338
+ page_token=page_token,
339
+ )
340
+ for item in response.get("session_items", []):
341
+ yield self._item_from_response(item)
342
+ page_token = response.get("next_page_token")
343
+ if not page_token:
344
+ return
345
+
346
+ def _append_items(self, session: Session, *, items: Sequence[Any]) -> List[SessionItem]:
347
+ if not items:
348
+ raise ValueError("at least one item is required")
349
+ response = self._api.append_session_items(
350
+ session.store_name,
351
+ session.session_id,
352
+ list(items),
353
+ )
354
+ return [self._item_from_response(item) for item in response.get("session_items", [])]
355
+
356
+ def _pop_item(self, session: Session) -> Optional[SessionItem]:
357
+ response = self._api.pop_session_item(session.store_name, session.session_id)
358
+ item = response.get("item")
359
+ return self._item_from_response(item) if item is not None else None
360
+
361
+ def _clear_items(self, session: Session) -> None:
362
+ self._api.clear_session_items(session.store_name, session.session_id)
363
+
364
+ def _extract_memories(
365
+ self,
366
+ session: Session,
367
+ *,
368
+ memory_store: str,
369
+ instructions: Optional[str],
370
+ dry_run: bool,
371
+ ) -> List[ExtractedMemory]:
372
+ response = self._api.extract_memories(
373
+ session.store_name,
374
+ session.session_id,
375
+ memory_store,
376
+ instructions=instructions,
377
+ dry_run=dry_run,
378
+ )
379
+ return [self._extracted_from_response(e) for e in response.get("entries") or []]
380
+
381
+ @staticmethod
382
+ def _extracted_from_response(response: dict[str, Any]) -> ExtractedMemory:
383
+ return ExtractedMemory(
384
+ name=response["name"],
385
+ actor_id=response.get("actor_id"),
386
+ path=response.get("path"),
387
+ content=response.get("content"),
388
+ description=response.get("description"),
389
+ session_id=response.get("session_id"),
390
+ source_type=response.get("source_type"),
391
+ create_time=parse_timestamp(response.get("create_time")),
392
+ update_time=parse_timestamp(response.get("update_time")),
393
+ )
394
+
395
+ def _store_from_response(self, response: dict[str, Any]) -> SessionStore:
396
+ name = response.get("session_store_name")
397
+ if name is None:
398
+ name = response["name"].removeprefix("session-stores/")
399
+ return SessionStore(
400
+ name=name,
401
+ session_store_id=response.get("session_store_id"),
402
+ creator_user_id=response.get("creator_user_id"),
403
+ create_time=parse_timestamp(response.get("create_time")),
404
+ update_time=parse_timestamp(response.get("update_time")),
405
+ description=response.get("description"),
406
+ metadata=dict(response.get("metadata", {})),
407
+ _client=self,
408
+ )
409
+
410
+ def _session_from_response(
411
+ self,
412
+ response: dict[str, Any],
413
+ session_store_name: str,
414
+ ) -> Session:
415
+ return Session(
416
+ store_name=response.get("session_store_name", session_store_name),
417
+ session_id=response["session_id"],
418
+ actor_id=response["actor_id"],
419
+ parent_session_id=response.get("parent_session_id"),
420
+ root_session_id=response.get("root_session_id"),
421
+ metadata=dict(response.get("metadata", {})),
422
+ create_time=parse_timestamp(response.get("create_time")),
423
+ update_time=parse_timestamp(response.get("update_time")),
424
+ last_activity_time=parse_timestamp(response.get("last_activity_time")),
425
+ _client=self,
426
+ )
427
+
428
+ @staticmethod
429
+ def _item_from_response(response: dict[str, Any]) -> SessionItem:
430
+ return SessionItem(
431
+ item_id=response["item_id"],
432
+ data=response["data"],
433
+ create_time=parse_timestamp(response.get("create_time")),
434
+ )
@@ -0,0 +1,85 @@
1
+ """Timestamp parsing and humanization for the Agent Bricks CLI.
2
+
3
+ The 2.0 agents APIs return timestamps as `google.protobuf.Timestamp`, which
4
+ serializes to an RFC 3339 string (`2026-08-15T01:29:00Z`) under the fields
5
+ `create_time`/`update_time`. Older memory-store responses used epoch-millis
6
+ int64 (`created_at`/`updated_at`); `parse_timestamp` accepts either form.
7
+
8
+ `relative` returns concise phrases such as "13 days ago" and "An hour ago".
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from datetime import datetime, timezone
14
+ from typing import Optional, Union
15
+
16
+ TimestampValue = Union[int, float, str, datetime, None]
17
+
18
+
19
+ def parse_timestamp(value: TimestampValue) -> Optional[datetime]:
20
+ """Parse a datetime, an epoch-millis number, a numeric string, or an RFC 3339 string.
21
+
22
+ Returns a timezone-aware UTC datetime, or None if the value is empty/unparseable.
23
+ """
24
+ if value is None or value == "":
25
+ return None
26
+ if isinstance(value, datetime):
27
+ return value if value.tzinfo else value.replace(tzinfo=timezone.utc)
28
+ if isinstance(value, (int, float)):
29
+ return datetime.fromtimestamp(value / 1000.0, tz=timezone.utc)
30
+ if isinstance(value, str):
31
+ if value.isdigit():
32
+ return datetime.fromtimestamp(int(value) / 1000.0, tz=timezone.utc)
33
+ iso = value.replace("Z", "+00:00")
34
+ try:
35
+ dt = datetime.fromisoformat(iso)
36
+ except ValueError:
37
+ return None
38
+ return dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)
39
+ return None
40
+
41
+
42
+ def relative(value: TimestampValue, *, now: Optional[datetime] = None) -> str:
43
+ """Humanize a timestamp as an "N units ago" string."""
44
+ dt = parse_timestamp(value)
45
+ if dt is None:
46
+ return "—"
47
+ now = now or datetime.now(tz=timezone.utc)
48
+ seconds = (now - dt).total_seconds()
49
+ if seconds < 0:
50
+ return "just now"
51
+
52
+ minutes = seconds / 60
53
+ hours = minutes / 60
54
+ days = hours / 24
55
+
56
+ if seconds < 45:
57
+ return "just now"
58
+ if minutes < 2:
59
+ return "A minute ago"
60
+ if minutes < 60:
61
+ return f"{round(minutes)} minutes ago"
62
+ if hours < 2:
63
+ return "An hour ago"
64
+ if hours < 24:
65
+ return f"{round(hours)} hours ago"
66
+ if days < 2:
67
+ return "A day ago"
68
+ if days < 30:
69
+ return f"{round(days)} days ago"
70
+ if days < 60:
71
+ return "A month ago"
72
+ if days < 365:
73
+ return f"{round(days / 30)} months ago"
74
+ if days < 730:
75
+ return "A year ago"
76
+ return f"{round(days / 365)} years ago"
77
+
78
+
79
+ def absolute(value: TimestampValue) -> str:
80
+ """Format a timestamp like the mock's detail rail: "May 19, 2026, 05:00 PM"."""
81
+ dt = parse_timestamp(value)
82
+ if dt is None:
83
+ return "—"
84
+ local = dt.astimezone()
85
+ return f"{local.strftime('%b')} {local.day}, {local.strftime('%Y, %I:%M %p')}"