aury-agent 0.0.4__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.
- aury/__init__.py +2 -0
- aury/agents/__init__.py +55 -0
- aury/agents/a2a/__init__.py +168 -0
- aury/agents/backends/__init__.py +196 -0
- aury/agents/backends/artifact/__init__.py +9 -0
- aury/agents/backends/artifact/memory.py +130 -0
- aury/agents/backends/artifact/types.py +133 -0
- aury/agents/backends/code/__init__.py +65 -0
- aury/agents/backends/file/__init__.py +11 -0
- aury/agents/backends/file/local.py +66 -0
- aury/agents/backends/file/types.py +40 -0
- aury/agents/backends/invocation/__init__.py +8 -0
- aury/agents/backends/invocation/memory.py +81 -0
- aury/agents/backends/invocation/types.py +110 -0
- aury/agents/backends/memory/__init__.py +8 -0
- aury/agents/backends/memory/memory.py +179 -0
- aury/agents/backends/memory/types.py +136 -0
- aury/agents/backends/message/__init__.py +9 -0
- aury/agents/backends/message/memory.py +122 -0
- aury/agents/backends/message/types.py +124 -0
- aury/agents/backends/sandbox.py +275 -0
- aury/agents/backends/session/__init__.py +8 -0
- aury/agents/backends/session/memory.py +93 -0
- aury/agents/backends/session/types.py +124 -0
- aury/agents/backends/shell/__init__.py +11 -0
- aury/agents/backends/shell/local.py +110 -0
- aury/agents/backends/shell/types.py +55 -0
- aury/agents/backends/shell.py +209 -0
- aury/agents/backends/snapshot/__init__.py +19 -0
- aury/agents/backends/snapshot/git.py +95 -0
- aury/agents/backends/snapshot/hybrid.py +125 -0
- aury/agents/backends/snapshot/memory.py +86 -0
- aury/agents/backends/snapshot/types.py +59 -0
- aury/agents/backends/state/__init__.py +29 -0
- aury/agents/backends/state/composite.py +49 -0
- aury/agents/backends/state/file.py +57 -0
- aury/agents/backends/state/memory.py +52 -0
- aury/agents/backends/state/sqlite.py +262 -0
- aury/agents/backends/state/types.py +178 -0
- aury/agents/backends/subagent/__init__.py +165 -0
- aury/agents/cli/__init__.py +41 -0
- aury/agents/cli/chat.py +239 -0
- aury/agents/cli/config.py +236 -0
- aury/agents/cli/extensions.py +460 -0
- aury/agents/cli/main.py +189 -0
- aury/agents/cli/session.py +337 -0
- aury/agents/cli/workflow.py +276 -0
- aury/agents/context_providers/__init__.py +66 -0
- aury/agents/context_providers/artifact.py +299 -0
- aury/agents/context_providers/base.py +177 -0
- aury/agents/context_providers/memory.py +70 -0
- aury/agents/context_providers/message.py +130 -0
- aury/agents/context_providers/skill.py +50 -0
- aury/agents/context_providers/subagent.py +46 -0
- aury/agents/context_providers/tool.py +68 -0
- aury/agents/core/__init__.py +83 -0
- aury/agents/core/base.py +573 -0
- aury/agents/core/context.py +797 -0
- aury/agents/core/context_builder.py +303 -0
- aury/agents/core/event_bus/__init__.py +15 -0
- aury/agents/core/event_bus/bus.py +203 -0
- aury/agents/core/factory.py +169 -0
- aury/agents/core/isolator.py +97 -0
- aury/agents/core/logging.py +95 -0
- aury/agents/core/parallel.py +194 -0
- aury/agents/core/runner.py +139 -0
- aury/agents/core/services/__init__.py +5 -0
- aury/agents/core/services/file_session.py +144 -0
- aury/agents/core/services/message.py +53 -0
- aury/agents/core/services/session.py +53 -0
- aury/agents/core/signals.py +109 -0
- aury/agents/core/state.py +363 -0
- aury/agents/core/types/__init__.py +107 -0
- aury/agents/core/types/action.py +176 -0
- aury/agents/core/types/artifact.py +135 -0
- aury/agents/core/types/block.py +736 -0
- aury/agents/core/types/message.py +350 -0
- aury/agents/core/types/recall.py +144 -0
- aury/agents/core/types/session.py +257 -0
- aury/agents/core/types/subagent.py +154 -0
- aury/agents/core/types/tool.py +205 -0
- aury/agents/eval/__init__.py +331 -0
- aury/agents/hitl/__init__.py +57 -0
- aury/agents/hitl/ask_user.py +242 -0
- aury/agents/hitl/compaction.py +230 -0
- aury/agents/hitl/exceptions.py +87 -0
- aury/agents/hitl/permission.py +617 -0
- aury/agents/hitl/revert.py +216 -0
- aury/agents/llm/__init__.py +31 -0
- aury/agents/llm/adapter.py +367 -0
- aury/agents/llm/openai.py +294 -0
- aury/agents/llm/provider.py +476 -0
- aury/agents/mcp/__init__.py +153 -0
- aury/agents/memory/__init__.py +46 -0
- aury/agents/memory/compaction.py +394 -0
- aury/agents/memory/manager.py +465 -0
- aury/agents/memory/processor.py +177 -0
- aury/agents/memory/store.py +187 -0
- aury/agents/memory/types.py +137 -0
- aury/agents/messages/__init__.py +40 -0
- aury/agents/messages/config.py +47 -0
- aury/agents/messages/raw_store.py +224 -0
- aury/agents/messages/store.py +118 -0
- aury/agents/messages/types.py +88 -0
- aury/agents/middleware/__init__.py +31 -0
- aury/agents/middleware/base.py +341 -0
- aury/agents/middleware/chain.py +342 -0
- aury/agents/middleware/message.py +129 -0
- aury/agents/middleware/message_container.py +126 -0
- aury/agents/middleware/raw_message.py +153 -0
- aury/agents/middleware/truncation.py +139 -0
- aury/agents/middleware/types.py +81 -0
- aury/agents/plugin.py +162 -0
- aury/agents/react/__init__.py +4 -0
- aury/agents/react/agent.py +1923 -0
- aury/agents/sandbox/__init__.py +23 -0
- aury/agents/sandbox/local.py +239 -0
- aury/agents/sandbox/remote.py +200 -0
- aury/agents/sandbox/types.py +115 -0
- aury/agents/skill/__init__.py +16 -0
- aury/agents/skill/loader.py +180 -0
- aury/agents/skill/types.py +83 -0
- aury/agents/tool/__init__.py +39 -0
- aury/agents/tool/builtin/__init__.py +23 -0
- aury/agents/tool/builtin/ask_user.py +155 -0
- aury/agents/tool/builtin/bash.py +107 -0
- aury/agents/tool/builtin/delegate.py +726 -0
- aury/agents/tool/builtin/edit.py +121 -0
- aury/agents/tool/builtin/plan.py +277 -0
- aury/agents/tool/builtin/read.py +91 -0
- aury/agents/tool/builtin/thinking.py +111 -0
- aury/agents/tool/builtin/yield_result.py +130 -0
- aury/agents/tool/decorator.py +252 -0
- aury/agents/tool/set.py +204 -0
- aury/agents/usage/__init__.py +12 -0
- aury/agents/usage/tracker.py +236 -0
- aury/agents/workflow/__init__.py +85 -0
- aury/agents/workflow/adapter.py +268 -0
- aury/agents/workflow/dag.py +116 -0
- aury/agents/workflow/dsl.py +575 -0
- aury/agents/workflow/executor.py +659 -0
- aury/agents/workflow/expression.py +136 -0
- aury/agents/workflow/parser.py +182 -0
- aury/agents/workflow/state.py +145 -0
- aury/agents/workflow/types.py +86 -0
- aury_agent-0.0.4.dist-info/METADATA +90 -0
- aury_agent-0.0.4.dist-info/RECORD +149 -0
- aury_agent-0.0.4.dist-info/WHEEL +4 -0
- aury_agent-0.0.4.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,736 @@
|
|
|
1
|
+
"""Block and BlockEvent data structures for streaming protocol."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import TYPE_CHECKING, Any, Literal
|
|
8
|
+
|
|
9
|
+
from .session import generate_id
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from ..context import InvocationContext
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class BlockKind(str, Enum):
|
|
16
|
+
"""Framework built-in block types.
|
|
17
|
+
|
|
18
|
+
These are the default kinds used by the framework internally.
|
|
19
|
+
Developers can use any custom string as block kind -
|
|
20
|
+
this enum is not exhaustive.
|
|
21
|
+
|
|
22
|
+
Example custom kinds: "code", "chart", "table", "image", etc.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# === Content ===
|
|
26
|
+
TEXT = "text" # Plain text / markdown
|
|
27
|
+
THINKING = "thinking" # LLM reasoning (collapsible)
|
|
28
|
+
|
|
29
|
+
# === Tool Execution ===
|
|
30
|
+
TOOL_USE = "tool_use" # Tool call
|
|
31
|
+
TOOL_RESULT = "tool_result" # Tool result
|
|
32
|
+
|
|
33
|
+
# === Agent ===
|
|
34
|
+
SUB_AGENT = "sub_agent" # Sub-agent delegation
|
|
35
|
+
PLAN = "plan" # Execution plan with checklist
|
|
36
|
+
|
|
37
|
+
# === Workflow ===
|
|
38
|
+
START = "start" # Workflow start
|
|
39
|
+
END = "end" # Workflow end
|
|
40
|
+
NODE = "node" # Workflow node execution block
|
|
41
|
+
|
|
42
|
+
# === HITL ===
|
|
43
|
+
HITL_REQUEST = "hitl_request" # HITL request (any format - choice, input, confirm, etc.)
|
|
44
|
+
|
|
45
|
+
# === Control Flow ===
|
|
46
|
+
YIELD = "yield" # Return control to parent
|
|
47
|
+
|
|
48
|
+
# === Output ===
|
|
49
|
+
ARTIFACT = "artifact" # Generated artifact (file, document, etc.)
|
|
50
|
+
ERROR = "error" # Error message
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class BlockOp(str, Enum):
|
|
54
|
+
"""Block operations.
|
|
55
|
+
|
|
56
|
+
Blocks have no lifecycle - they exist once created and can be
|
|
57
|
+
operated on at any time via their id.
|
|
58
|
+
"""
|
|
59
|
+
APPLY = "apply" # Complete data (create or replace)
|
|
60
|
+
DELTA = "delta" # Incremental append
|
|
61
|
+
PATCH = "patch" # Partial modification
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class Persistence(str, Enum):
|
|
65
|
+
"""Framework built-in persistence types.
|
|
66
|
+
|
|
67
|
+
Developers can use custom strings for specialized persistence behaviors.
|
|
68
|
+
"""
|
|
69
|
+
PERSISTENT = "persistent" # Stored to backend
|
|
70
|
+
TRANSIENT = "transient" # Not stored (progress bars, spinners, etc.)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ActorInfo:
|
|
75
|
+
"""Actor information."""
|
|
76
|
+
id: str
|
|
77
|
+
role: Literal["user", "assistant", "system"]
|
|
78
|
+
name: str | None = None
|
|
79
|
+
meta: dict[str, Any] | None = None
|
|
80
|
+
|
|
81
|
+
def to_dict(self) -> dict[str, Any]:
|
|
82
|
+
return {
|
|
83
|
+
"id": self.id,
|
|
84
|
+
"role": self.role,
|
|
85
|
+
"name": self.name,
|
|
86
|
+
"meta": self.meta,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
@classmethod
|
|
90
|
+
def from_dict(cls, data: dict[str, Any]) -> ActorInfo:
|
|
91
|
+
return cls(
|
|
92
|
+
id=data["id"],
|
|
93
|
+
role=data["role"],
|
|
94
|
+
name=data.get("name"),
|
|
95
|
+
meta=data.get("meta"),
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclass
|
|
100
|
+
class BlockEvent:
|
|
101
|
+
"""Streaming block event.
|
|
102
|
+
|
|
103
|
+
Used for real-time streaming to frontend.
|
|
104
|
+
Operations: APPLY (create/replace), DELTA (append), PATCH (partial update)
|
|
105
|
+
"""
|
|
106
|
+
block_id: str = field(default_factory=lambda: generate_id("blk"))
|
|
107
|
+
parent_id: str | None = None # For nesting
|
|
108
|
+
kind: BlockKind | str = BlockKind.TEXT
|
|
109
|
+
persistence: Persistence = Persistence.PERSISTENT
|
|
110
|
+
op: BlockOp = BlockOp.APPLY
|
|
111
|
+
data: dict[str, Any] | None = None
|
|
112
|
+
branch: str | None = None # For sub-agent isolation (e.g. "agent1.agent2")
|
|
113
|
+
|
|
114
|
+
# Protocol envelope
|
|
115
|
+
protocol_version: str = "1.0"
|
|
116
|
+
event_id: str = field(default_factory=lambda: generate_id("evt"))
|
|
117
|
+
timestamp: int = field(default_factory=lambda: int(datetime.now().timestamp() * 1000))
|
|
118
|
+
invocation_id: str = ""
|
|
119
|
+
session_id: str = ""
|
|
120
|
+
actor: ActorInfo | None = None
|
|
121
|
+
|
|
122
|
+
def to_dict(self) -> dict[str, Any]:
|
|
123
|
+
"""Convert to dictionary for serialization."""
|
|
124
|
+
kind_value = self.kind.value if isinstance(self.kind, BlockKind) else self.kind
|
|
125
|
+
return {
|
|
126
|
+
"block_id": self.block_id,
|
|
127
|
+
"parent_id": self.parent_id,
|
|
128
|
+
"kind": kind_value,
|
|
129
|
+
"persistence": self.persistence.value,
|
|
130
|
+
"op": self.op.value,
|
|
131
|
+
"data": self.data,
|
|
132
|
+
"branch": self.branch,
|
|
133
|
+
"protocol_version": self.protocol_version,
|
|
134
|
+
"event_id": self.event_id,
|
|
135
|
+
"timestamp": self.timestamp,
|
|
136
|
+
"invocation_id": self.invocation_id,
|
|
137
|
+
"session_id": self.session_id,
|
|
138
|
+
"actor": self.actor.to_dict() if self.actor else None,
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@classmethod
|
|
142
|
+
def from_dict(cls, data: dict[str, Any]) -> BlockEvent:
|
|
143
|
+
"""Create from dictionary."""
|
|
144
|
+
kind_str = data["kind"]
|
|
145
|
+
try:
|
|
146
|
+
kind = BlockKind(kind_str)
|
|
147
|
+
except ValueError:
|
|
148
|
+
kind = kind_str # Custom kind string
|
|
149
|
+
return cls(
|
|
150
|
+
block_id=data["block_id"],
|
|
151
|
+
parent_id=data.get("parent_id"),
|
|
152
|
+
kind=kind,
|
|
153
|
+
persistence=Persistence(data["persistence"]),
|
|
154
|
+
op=BlockOp(data["op"]),
|
|
155
|
+
data=data.get("data"),
|
|
156
|
+
branch=data.get("branch"),
|
|
157
|
+
protocol_version=data.get("protocol_version", "1.0"),
|
|
158
|
+
event_id=data.get("event_id", ""),
|
|
159
|
+
timestamp=data.get("timestamp", 0),
|
|
160
|
+
invocation_id=data.get("invocation_id", ""),
|
|
161
|
+
session_id=data.get("session_id", ""),
|
|
162
|
+
actor=ActorInfo.from_dict(data["actor"]) if data.get("actor") else None,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# ============================================================
|
|
167
|
+
# BlockMerger - Merge BlockEvents by kind
|
|
168
|
+
# ============================================================
|
|
169
|
+
|
|
170
|
+
class BlockMerger:
|
|
171
|
+
"""Base class for block mergers.
|
|
172
|
+
|
|
173
|
+
Mergers define how to combine multiple BlockEvents into final data.
|
|
174
|
+
Register custom mergers for custom block kinds.
|
|
175
|
+
|
|
176
|
+
Override apply/delta/patch methods to customize specific operations.
|
|
177
|
+
"""
|
|
178
|
+
|
|
179
|
+
def merge(self, current: dict[str, Any] | None, event: "BlockEvent") -> dict[str, Any]:
|
|
180
|
+
"""Merge event into current data. Dispatches to apply/delta/patch."""
|
|
181
|
+
if event.op == BlockOp.APPLY:
|
|
182
|
+
return self.apply(current, event)
|
|
183
|
+
elif event.op == BlockOp.DELTA:
|
|
184
|
+
return self.delta(current, event)
|
|
185
|
+
elif event.op == BlockOp.PATCH:
|
|
186
|
+
return self.patch(current, event)
|
|
187
|
+
return current or {}
|
|
188
|
+
|
|
189
|
+
def apply(self, current: dict[str, Any] | None, event: "BlockEvent") -> dict[str, Any]:
|
|
190
|
+
"""Handle APPLY: replace entirely with new data."""
|
|
191
|
+
return event.data or {}
|
|
192
|
+
|
|
193
|
+
def delta(self, current: dict[str, Any] | None, event: "BlockEvent") -> dict[str, Any]:
|
|
194
|
+
"""Handle DELTA: append/accumulate data.
|
|
195
|
+
|
|
196
|
+
Default: string concatenation, list extension.
|
|
197
|
+
"""
|
|
198
|
+
data = dict(current) if current else {}
|
|
199
|
+
if event.data:
|
|
200
|
+
for key, value in event.data.items():
|
|
201
|
+
if isinstance(value, str) and isinstance(data.get(key, ""), str):
|
|
202
|
+
data[key] = data.get(key, "") + value
|
|
203
|
+
elif isinstance(value, list) and isinstance(data.get(key), list):
|
|
204
|
+
data[key] = data.get(key, []) + value
|
|
205
|
+
else:
|
|
206
|
+
data[key] = value
|
|
207
|
+
return data
|
|
208
|
+
|
|
209
|
+
def patch(self, current: dict[str, Any] | None, event: "BlockEvent") -> dict[str, Any]:
|
|
210
|
+
"""Handle PATCH: partial update with JSON Path syntax.
|
|
211
|
+
|
|
212
|
+
Default: supports nested path like "a.b.c" or "items[0].name".
|
|
213
|
+
"""
|
|
214
|
+
data = dict(current) if current else {}
|
|
215
|
+
if event.data:
|
|
216
|
+
for path, value in event.data.items():
|
|
217
|
+
self._set_path(data, path, value)
|
|
218
|
+
return data
|
|
219
|
+
|
|
220
|
+
def _set_path(self, data: dict, path: str, value: Any) -> None:
|
|
221
|
+
"""Set value at path (supports nested paths).
|
|
222
|
+
|
|
223
|
+
Path syntax:
|
|
224
|
+
- "key" -> data["key"]
|
|
225
|
+
- "a.b.c" -> data["a"]["b"]["c"]
|
|
226
|
+
- "items[0]" -> data["items"][0]
|
|
227
|
+
- "items[0].name" -> data["items"][0]["name"]
|
|
228
|
+
"""
|
|
229
|
+
import re
|
|
230
|
+
|
|
231
|
+
# Parse path into parts
|
|
232
|
+
parts = []
|
|
233
|
+
for segment in path.replace("]", "").split("."):
|
|
234
|
+
if "[" in segment:
|
|
235
|
+
key, idx = segment.split("[")
|
|
236
|
+
if key:
|
|
237
|
+
parts.append(key)
|
|
238
|
+
parts.append(int(idx))
|
|
239
|
+
else:
|
|
240
|
+
parts.append(segment)
|
|
241
|
+
|
|
242
|
+
# Navigate to parent
|
|
243
|
+
current = data
|
|
244
|
+
for part in parts[:-1]:
|
|
245
|
+
if isinstance(part, int):
|
|
246
|
+
# Ensure list exists and is long enough
|
|
247
|
+
if not isinstance(current, list):
|
|
248
|
+
break
|
|
249
|
+
while len(current) <= part:
|
|
250
|
+
current.append({})
|
|
251
|
+
current = current[part]
|
|
252
|
+
else:
|
|
253
|
+
if part not in current:
|
|
254
|
+
current[part] = {}
|
|
255
|
+
current = current[part]
|
|
256
|
+
|
|
257
|
+
# Set final value
|
|
258
|
+
final_key = parts[-1]
|
|
259
|
+
if isinstance(final_key, int) and isinstance(current, list):
|
|
260
|
+
while len(current) <= final_key:
|
|
261
|
+
current.append(None)
|
|
262
|
+
current[final_key] = value
|
|
263
|
+
elif isinstance(current, dict):
|
|
264
|
+
current[final_key] = value
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# Merger registry
|
|
268
|
+
_block_mergers: dict[str, BlockMerger] = {}
|
|
269
|
+
_default_merger = BlockMerger()
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def register_merger(kind: str, merger: BlockMerger) -> None:
|
|
273
|
+
"""Register a custom merger for a block kind.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
kind: Block kind (e.g. "plan", "my_custom_type")
|
|
277
|
+
merger: Merger instance
|
|
278
|
+
"""
|
|
279
|
+
_block_mergers[kind] = merger
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_merger(kind: str | BlockKind) -> BlockMerger:
|
|
283
|
+
"""Get merger for a block kind.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
kind: Block kind
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Registered merger or default merger
|
|
290
|
+
"""
|
|
291
|
+
kind_str = kind.value if isinstance(kind, BlockKind) else kind
|
|
292
|
+
return _block_mergers.get(kind_str, _default_merger)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ============================================================
|
|
296
|
+
# PersistedBlock
|
|
297
|
+
# ============================================================
|
|
298
|
+
|
|
299
|
+
@dataclass
|
|
300
|
+
class PersistedBlock:
|
|
301
|
+
"""Persisted block (final state, no op).
|
|
302
|
+
|
|
303
|
+
This is what gets stored after BlockEvent stream is complete.
|
|
304
|
+
"""
|
|
305
|
+
block_id: str
|
|
306
|
+
parent_id: str | None = None
|
|
307
|
+
kind: BlockKind | str = BlockKind.TEXT
|
|
308
|
+
data: dict[str, Any] | None = None
|
|
309
|
+
|
|
310
|
+
# From protocol envelope
|
|
311
|
+
session_id: str = ""
|
|
312
|
+
invocation_id: str = ""
|
|
313
|
+
actor_id: str = ""
|
|
314
|
+
actor_role: str = "assistant"
|
|
315
|
+
|
|
316
|
+
# Branch for sub-agent isolation
|
|
317
|
+
branch: str | None = None
|
|
318
|
+
|
|
319
|
+
# Timestamps
|
|
320
|
+
created_at: datetime = field(default_factory=datetime.now)
|
|
321
|
+
updated_at: datetime | None = None
|
|
322
|
+
|
|
323
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
324
|
+
|
|
325
|
+
def to_dict(self) -> dict[str, Any]:
|
|
326
|
+
"""Convert to dictionary for serialization."""
|
|
327
|
+
kind_value = self.kind.value if isinstance(self.kind, BlockKind) else self.kind
|
|
328
|
+
return {
|
|
329
|
+
"block_id": self.block_id,
|
|
330
|
+
"parent_id": self.parent_id,
|
|
331
|
+
"kind": kind_value,
|
|
332
|
+
"data": self.data,
|
|
333
|
+
"session_id": self.session_id,
|
|
334
|
+
"invocation_id": self.invocation_id,
|
|
335
|
+
"actor_id": self.actor_id,
|
|
336
|
+
"actor_role": self.actor_role,
|
|
337
|
+
"branch": self.branch,
|
|
338
|
+
"created_at": self.created_at.isoformat(),
|
|
339
|
+
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
|
|
340
|
+
"metadata": self.metadata,
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
@classmethod
|
|
344
|
+
def from_dict(cls, data: dict[str, Any]) -> PersistedBlock:
|
|
345
|
+
"""Create from dictionary."""
|
|
346
|
+
kind_str = data["kind"]
|
|
347
|
+
try:
|
|
348
|
+
kind = BlockKind(kind_str)
|
|
349
|
+
except ValueError:
|
|
350
|
+
kind = kind_str
|
|
351
|
+
return cls(
|
|
352
|
+
block_id=data["block_id"],
|
|
353
|
+
parent_id=data.get("parent_id"),
|
|
354
|
+
kind=kind,
|
|
355
|
+
data=data.get("data"),
|
|
356
|
+
session_id=data.get("session_id", ""),
|
|
357
|
+
invocation_id=data.get("invocation_id", ""),
|
|
358
|
+
actor_id=data.get("actor_id", ""),
|
|
359
|
+
actor_role=data.get("actor_role", "assistant"),
|
|
360
|
+
branch=data.get("branch"),
|
|
361
|
+
created_at=datetime.fromisoformat(data["created_at"]),
|
|
362
|
+
updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else None,
|
|
363
|
+
metadata=data.get("metadata", {}),
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
@classmethod
|
|
367
|
+
def from_events(cls, events: list[BlockEvent]) -> "PersistedBlock":
|
|
368
|
+
"""Aggregate BlockEvents for a SINGLE block into a PersistedBlock.
|
|
369
|
+
|
|
370
|
+
All events must have the same block_id.
|
|
371
|
+
Uses registered merger for the block kind to combine events.
|
|
372
|
+
"""
|
|
373
|
+
if not events:
|
|
374
|
+
raise ValueError("Cannot create PersistedBlock from empty events")
|
|
375
|
+
|
|
376
|
+
first = events[0]
|
|
377
|
+
kind = first.kind
|
|
378
|
+
|
|
379
|
+
# Get merger for this kind
|
|
380
|
+
merger = get_merger(kind)
|
|
381
|
+
|
|
382
|
+
# Merge all events
|
|
383
|
+
data: dict[str, Any] | None = None
|
|
384
|
+
last_timestamp = first.timestamp
|
|
385
|
+
|
|
386
|
+
for event in events:
|
|
387
|
+
data = merger.merge(data, event)
|
|
388
|
+
last_timestamp = event.timestamp
|
|
389
|
+
|
|
390
|
+
return cls(
|
|
391
|
+
block_id=first.block_id,
|
|
392
|
+
parent_id=first.parent_id,
|
|
393
|
+
kind=kind,
|
|
394
|
+
data=data,
|
|
395
|
+
branch=first.branch,
|
|
396
|
+
session_id=first.session_id,
|
|
397
|
+
invocation_id=first.invocation_id,
|
|
398
|
+
actor_id=first.actor.id if first.actor else "",
|
|
399
|
+
actor_role=first.actor.role if first.actor else "assistant",
|
|
400
|
+
updated_at=datetime.fromtimestamp(last_timestamp / 1000) if len(events) > 1 else None,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
@classmethod
|
|
404
|
+
def from_event_stream(cls, events: list[BlockEvent]) -> list["PersistedBlock"]:
|
|
405
|
+
"""Aggregate BlockEvents into multiple PersistedBlocks.
|
|
406
|
+
|
|
407
|
+
Groups events by block_id, then merges each group.
|
|
408
|
+
Returns blocks in order of first appearance.
|
|
409
|
+
|
|
410
|
+
Args:
|
|
411
|
+
events: List of BlockEvents (can have multiple block_ids)
|
|
412
|
+
|
|
413
|
+
Returns:
|
|
414
|
+
List of PersistedBlocks, one per unique block_id
|
|
415
|
+
"""
|
|
416
|
+
if not events:
|
|
417
|
+
return []
|
|
418
|
+
|
|
419
|
+
# Group by block_id, preserving order
|
|
420
|
+
from collections import OrderedDict
|
|
421
|
+
grouped: OrderedDict[str, list[BlockEvent]] = OrderedDict()
|
|
422
|
+
for event in events:
|
|
423
|
+
if event.block_id not in grouped:
|
|
424
|
+
grouped[event.block_id] = []
|
|
425
|
+
grouped[event.block_id].append(event)
|
|
426
|
+
|
|
427
|
+
# Create PersistedBlock for each group
|
|
428
|
+
return [cls.from_events(group) for group in grouped.values()]
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ============================================================
|
|
432
|
+
# BlockAggregator - Aggregate events into blocks
|
|
433
|
+
# ============================================================
|
|
434
|
+
|
|
435
|
+
class BlockAggregator:
|
|
436
|
+
"""Aggregates BlockEvents into PersistedBlocks incrementally.
|
|
437
|
+
|
|
438
|
+
Processes events one by one, updating blocks as they arrive.
|
|
439
|
+
|
|
440
|
+
Example:
|
|
441
|
+
aggregator = BlockAggregator()
|
|
442
|
+
async for event in event_stream:
|
|
443
|
+
aggregator.process(event)
|
|
444
|
+
blocks = aggregator.blocks # Final result
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
def __init__(self):
|
|
448
|
+
self._blocks: dict[str, PersistedBlock] = {}
|
|
449
|
+
self._order: list[str] = [] # Track insertion order
|
|
450
|
+
|
|
451
|
+
def process(self, event: BlockEvent) -> PersistedBlock:
|
|
452
|
+
"""Process a single event, updating the corresponding block.
|
|
453
|
+
|
|
454
|
+
Returns the updated PersistedBlock.
|
|
455
|
+
"""
|
|
456
|
+
block_id = event.block_id
|
|
457
|
+
merger = get_merger(event.kind)
|
|
458
|
+
|
|
459
|
+
if block_id in self._blocks:
|
|
460
|
+
# Update existing block
|
|
461
|
+
block = self._blocks[block_id]
|
|
462
|
+
block.data = merger.merge(block.data, event)
|
|
463
|
+
block.updated_at = datetime.fromtimestamp(event.timestamp / 1000)
|
|
464
|
+
else:
|
|
465
|
+
# Create new block
|
|
466
|
+
block = PersistedBlock(
|
|
467
|
+
block_id=block_id,
|
|
468
|
+
parent_id=event.parent_id,
|
|
469
|
+
kind=event.kind,
|
|
470
|
+
data=merger.merge(None, event),
|
|
471
|
+
branch=event.branch,
|
|
472
|
+
session_id=event.session_id,
|
|
473
|
+
invocation_id=event.invocation_id,
|
|
474
|
+
actor_id=event.actor.id if event.actor else "",
|
|
475
|
+
actor_role=event.actor.role if event.actor else "assistant",
|
|
476
|
+
)
|
|
477
|
+
self._blocks[block_id] = block
|
|
478
|
+
self._order.append(block_id)
|
|
479
|
+
|
|
480
|
+
return block
|
|
481
|
+
|
|
482
|
+
def get(self, block_id: str) -> PersistedBlock | None:
|
|
483
|
+
"""Get a block by ID."""
|
|
484
|
+
return self._blocks.get(block_id)
|
|
485
|
+
|
|
486
|
+
@property
|
|
487
|
+
def blocks(self) -> list[PersistedBlock]:
|
|
488
|
+
"""Get all blocks in order of first appearance."""
|
|
489
|
+
return [self._blocks[bid] for bid in self._order]
|
|
490
|
+
|
|
491
|
+
def clear(self) -> None:
|
|
492
|
+
"""Clear all blocks."""
|
|
493
|
+
self._blocks.clear()
|
|
494
|
+
self._order.clear()
|
|
495
|
+
|
|
496
|
+
|
|
497
|
+
# ============================================================
|
|
498
|
+
# BlockHandle - Manage Block lifecycle
|
|
499
|
+
# ============================================================
|
|
500
|
+
|
|
501
|
+
class BlockHandle:
|
|
502
|
+
"""Block handle - manages a single Block's lifecycle.
|
|
503
|
+
|
|
504
|
+
Use this to manage a Block that needs multiple operations over time:
|
|
505
|
+
- Streaming text output (APPLY -> DELTA -> DELTA -> ...)
|
|
506
|
+
- Plan with progress updates (APPLY -> PATCH -> PATCH -> ...)
|
|
507
|
+
- Any Block that needs cross-content modification
|
|
508
|
+
|
|
509
|
+
Example:
|
|
510
|
+
# Streaming text
|
|
511
|
+
text = BlockHandle(ctx, kind="text")
|
|
512
|
+
await text.apply({"content": ""})
|
|
513
|
+
await text.delta({"delta": "Hello "})
|
|
514
|
+
await text.delta({"delta": "World"})
|
|
515
|
+
|
|
516
|
+
# Plan with updates
|
|
517
|
+
plan = BlockHandle(ctx, kind="plan")
|
|
518
|
+
await plan.apply({"steps": [...], "current": 0})
|
|
519
|
+
await plan.patch({"current": 1, "steps[0].status": "done"})
|
|
520
|
+
|
|
521
|
+
# Nested blocks
|
|
522
|
+
tool = BlockHandle(ctx, kind="tool_use")
|
|
523
|
+
await tool.apply({"name": "bash", "args": {...}})
|
|
524
|
+
output = BlockHandle(ctx, kind="text", parent=tool)
|
|
525
|
+
await output.apply({"content": "result..."})
|
|
526
|
+
"""
|
|
527
|
+
|
|
528
|
+
def __init__(
|
|
529
|
+
self,
|
|
530
|
+
ctx: "InvocationContext",
|
|
531
|
+
kind: BlockKind | str,
|
|
532
|
+
block_id: str | None = None,
|
|
533
|
+
parent: "BlockHandle | str | None" = None,
|
|
534
|
+
persistence: Persistence = Persistence.PERSISTENT,
|
|
535
|
+
branch: str | None = None,
|
|
536
|
+
):
|
|
537
|
+
self.ctx = ctx
|
|
538
|
+
self.kind = kind
|
|
539
|
+
self.block_id = block_id or generate_id("blk")
|
|
540
|
+
self.parent_id = parent.block_id if isinstance(parent, BlockHandle) else parent
|
|
541
|
+
self.persistence = persistence
|
|
542
|
+
# Use provided branch or get from context
|
|
543
|
+
self.branch = branch or getattr(ctx, 'branch', None)
|
|
544
|
+
|
|
545
|
+
async def apply(self, data: dict[str, Any], **kwargs: Any) -> None:
|
|
546
|
+
"""Create or completely replace the Block."""
|
|
547
|
+
await self.ctx.emit(BlockEvent(
|
|
548
|
+
block_id=self.block_id,
|
|
549
|
+
parent_id=self.parent_id,
|
|
550
|
+
kind=self.kind,
|
|
551
|
+
persistence=self.persistence,
|
|
552
|
+
op=BlockOp.APPLY,
|
|
553
|
+
data=data,
|
|
554
|
+
branch=self.branch,
|
|
555
|
+
**kwargs,
|
|
556
|
+
))
|
|
557
|
+
|
|
558
|
+
async def delta(self, data: dict[str, Any]) -> None:
|
|
559
|
+
"""Append incremental data to the Block."""
|
|
560
|
+
await self.ctx.emit(BlockEvent(
|
|
561
|
+
block_id=self.block_id,
|
|
562
|
+
parent_id=self.parent_id,
|
|
563
|
+
kind=self.kind,
|
|
564
|
+
persistence=self.persistence,
|
|
565
|
+
op=BlockOp.DELTA,
|
|
566
|
+
data=data,
|
|
567
|
+
branch=self.branch,
|
|
568
|
+
))
|
|
569
|
+
|
|
570
|
+
async def patch(self, data: dict[str, Any]) -> None:
|
|
571
|
+
"""Partially update the Block."""
|
|
572
|
+
await self.ctx.emit(BlockEvent(
|
|
573
|
+
block_id=self.block_id,
|
|
574
|
+
parent_id=self.parent_id,
|
|
575
|
+
kind=self.kind,
|
|
576
|
+
persistence=self.persistence,
|
|
577
|
+
op=BlockOp.PATCH,
|
|
578
|
+
data=data,
|
|
579
|
+
branch=self.branch,
|
|
580
|
+
))
|
|
581
|
+
|
|
582
|
+
def child(
|
|
583
|
+
self,
|
|
584
|
+
kind: BlockKind | str,
|
|
585
|
+
block_id: str | None = None,
|
|
586
|
+
persistence: Persistence = Persistence.PERSISTENT,
|
|
587
|
+
) -> "BlockHandle":
|
|
588
|
+
"""Create a child BlockHandle nested under this Block."""
|
|
589
|
+
return BlockHandle(
|
|
590
|
+
ctx=self.ctx,
|
|
591
|
+
kind=kind,
|
|
592
|
+
block_id=block_id,
|
|
593
|
+
parent=self,
|
|
594
|
+
persistence=persistence,
|
|
595
|
+
branch=self.branch, # Inherit branch from parent
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
# ============================================================
|
|
600
|
+
# Helper functions for creating BlockEvents (low-level API)
|
|
601
|
+
# ============================================================
|
|
602
|
+
|
|
603
|
+
def text_block(
|
|
604
|
+
block_id: str | None = None,
|
|
605
|
+
content: str = "",
|
|
606
|
+
parent_id: str | None = None,
|
|
607
|
+
session_id: str = "",
|
|
608
|
+
invocation_id: str = "",
|
|
609
|
+
actor: ActorInfo | None = None,
|
|
610
|
+
) -> BlockEvent:
|
|
611
|
+
"""Create a text block."""
|
|
612
|
+
return BlockEvent(
|
|
613
|
+
block_id=block_id or generate_id("blk"),
|
|
614
|
+
parent_id=parent_id,
|
|
615
|
+
kind=BlockKind.TEXT,
|
|
616
|
+
op=BlockOp.APPLY,
|
|
617
|
+
data={"content": content} if content else None,
|
|
618
|
+
session_id=session_id,
|
|
619
|
+
invocation_id=invocation_id,
|
|
620
|
+
actor=actor,
|
|
621
|
+
)
|
|
622
|
+
|
|
623
|
+
|
|
624
|
+
def text_delta(block_id: str, delta: str) -> BlockEvent:
|
|
625
|
+
"""Create a text block delta event."""
|
|
626
|
+
return BlockEvent(
|
|
627
|
+
block_id=block_id,
|
|
628
|
+
kind=BlockKind.TEXT,
|
|
629
|
+
op=BlockOp.DELTA,
|
|
630
|
+
data={"content": delta},
|
|
631
|
+
)
|
|
632
|
+
|
|
633
|
+
|
|
634
|
+
def thinking_block(
|
|
635
|
+
block_id: str | None = None,
|
|
636
|
+
content: str = "",
|
|
637
|
+
parent_id: str | None = None,
|
|
638
|
+
session_id: str = "",
|
|
639
|
+
invocation_id: str = "",
|
|
640
|
+
) -> BlockEvent:
|
|
641
|
+
"""Create a thinking block."""
|
|
642
|
+
return BlockEvent(
|
|
643
|
+
block_id=block_id or generate_id("blk"),
|
|
644
|
+
parent_id=parent_id,
|
|
645
|
+
kind=BlockKind.THINKING,
|
|
646
|
+
op=BlockOp.APPLY,
|
|
647
|
+
data={"content": content} if content else None,
|
|
648
|
+
session_id=session_id,
|
|
649
|
+
invocation_id=invocation_id,
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def thinking_delta(block_id: str, delta: str) -> BlockEvent:
|
|
654
|
+
"""Create a thinking block delta event."""
|
|
655
|
+
return BlockEvent(
|
|
656
|
+
block_id=block_id,
|
|
657
|
+
kind=BlockKind.THINKING,
|
|
658
|
+
op=BlockOp.DELTA,
|
|
659
|
+
data={"content": delta},
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def tool_use_block(
|
|
664
|
+
block_id: str,
|
|
665
|
+
name: str,
|
|
666
|
+
call_id: str,
|
|
667
|
+
args: dict[str, Any] | None = None,
|
|
668
|
+
parent_id: str | None = None,
|
|
669
|
+
session_id: str = "",
|
|
670
|
+
invocation_id: str = "",
|
|
671
|
+
) -> BlockEvent:
|
|
672
|
+
"""Create a tool use block."""
|
|
673
|
+
data = {"name": name, "call_id": call_id}
|
|
674
|
+
if args:
|
|
675
|
+
data["arguments"] = args
|
|
676
|
+
return BlockEvent(
|
|
677
|
+
block_id=block_id,
|
|
678
|
+
parent_id=parent_id,
|
|
679
|
+
kind=BlockKind.TOOL_USE,
|
|
680
|
+
op=BlockOp.APPLY,
|
|
681
|
+
data=data,
|
|
682
|
+
session_id=session_id,
|
|
683
|
+
invocation_id=invocation_id,
|
|
684
|
+
)
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def tool_use_patch(block_id: str, args: dict[str, Any]) -> BlockEvent:
|
|
688
|
+
"""Patch a tool use block with arguments."""
|
|
689
|
+
return BlockEvent(
|
|
690
|
+
block_id=block_id,
|
|
691
|
+
kind=BlockKind.TOOL_USE,
|
|
692
|
+
op=BlockOp.PATCH,
|
|
693
|
+
data={"arguments": args},
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def tool_result_block(
|
|
698
|
+
block_id: str,
|
|
699
|
+
tool_use_id: str,
|
|
700
|
+
content: str,
|
|
701
|
+
is_error: bool = False,
|
|
702
|
+
parent_id: str | None = None,
|
|
703
|
+
) -> BlockEvent:
|
|
704
|
+
"""Create a tool result block."""
|
|
705
|
+
return BlockEvent(
|
|
706
|
+
block_id=block_id,
|
|
707
|
+
parent_id=parent_id,
|
|
708
|
+
kind=BlockKind.TOOL_RESULT,
|
|
709
|
+
op=BlockOp.APPLY,
|
|
710
|
+
data={
|
|
711
|
+
"tool_use_id": tool_use_id,
|
|
712
|
+
"content": content,
|
|
713
|
+
"is_error": is_error,
|
|
714
|
+
},
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
|
|
718
|
+
def error_block(
|
|
719
|
+
block_id: str,
|
|
720
|
+
code: str,
|
|
721
|
+
message: str,
|
|
722
|
+
recoverable: bool = True,
|
|
723
|
+
parent_id: str | None = None,
|
|
724
|
+
) -> BlockEvent:
|
|
725
|
+
"""Create an error block."""
|
|
726
|
+
return BlockEvent(
|
|
727
|
+
block_id=block_id,
|
|
728
|
+
parent_id=parent_id,
|
|
729
|
+
kind=BlockKind.ERROR,
|
|
730
|
+
op=BlockOp.APPLY,
|
|
731
|
+
data={
|
|
732
|
+
"code": code,
|
|
733
|
+
"message": message,
|
|
734
|
+
"recoverable": recoverable,
|
|
735
|
+
},
|
|
736
|
+
)
|