capability-runtime 0.1.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 (52) hide show
  1. capability_runtime/__init__.py +90 -0
  2. capability_runtime/adapters/__init__.py +13 -0
  3. capability_runtime/adapters/agent_adapter.py +439 -0
  4. capability_runtime/adapters/agently_backend.py +423 -0
  5. capability_runtime/adapters/triggerflow_workflow_engine.py +865 -0
  6. capability_runtime/adapters/workflow_engine.py +43 -0
  7. capability_runtime/config.py +172 -0
  8. capability_runtime/errors.py +20 -0
  9. capability_runtime/guards.py +150 -0
  10. capability_runtime/host_protocol.py +400 -0
  11. capability_runtime/host_toolkit/__init__.py +55 -0
  12. capability_runtime/host_toolkit/approvals_profiles.py +94 -0
  13. capability_runtime/host_toolkit/evidence_hooks.py +65 -0
  14. capability_runtime/host_toolkit/history.py +74 -0
  15. capability_runtime/host_toolkit/invoke_capability.py +409 -0
  16. capability_runtime/host_toolkit/resume.py +317 -0
  17. capability_runtime/host_toolkit/system_prompt.py +132 -0
  18. capability_runtime/host_toolkit/turn_delta.py +128 -0
  19. capability_runtime/logging_utils.py +94 -0
  20. capability_runtime/manifest.py +173 -0
  21. capability_runtime/output_validator.py +139 -0
  22. capability_runtime/protocol/__init__.py +43 -0
  23. capability_runtime/protocol/agent.py +62 -0
  24. capability_runtime/protocol/capability.py +98 -0
  25. capability_runtime/protocol/chat_backend.py +38 -0
  26. capability_runtime/protocol/context.py +244 -0
  27. capability_runtime/protocol/workflow.py +119 -0
  28. capability_runtime/registry.py +287 -0
  29. capability_runtime/reporting/__init__.py +2 -0
  30. capability_runtime/reporting/node_report.py +497 -0
  31. capability_runtime/runtime.py +930 -0
  32. capability_runtime/runtime_ui_events_mixin.py +310 -0
  33. capability_runtime/sdk_lifecycle.py +982 -0
  34. capability_runtime/service_facade.py +418 -0
  35. capability_runtime/services.py +181 -0
  36. capability_runtime/structured_output.py +208 -0
  37. capability_runtime/structured_stream.py +38 -0
  38. capability_runtime/types.py +103 -0
  39. capability_runtime/ui_events/__init__.py +19 -0
  40. capability_runtime/ui_events/projector.py +617 -0
  41. capability_runtime/ui_events/session.py +292 -0
  42. capability_runtime/ui_events/store.py +127 -0
  43. capability_runtime/ui_events/transport.py +33 -0
  44. capability_runtime/ui_events/v1.py +76 -0
  45. capability_runtime/upstream_compat.py +182 -0
  46. capability_runtime/utils/__init__.py +1 -0
  47. capability_runtime/utils/usage.py +65 -0
  48. capability_runtime/workflow_runtime.py +218 -0
  49. capability_runtime-0.1.0.dist-info/METADATA +232 -0
  50. capability_runtime-0.1.0.dist-info/RECORD +52 -0
  51. capability_runtime-0.1.0.dist-info/WHEEL +5 -0
  52. capability_runtime-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,292 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from dataclasses import dataclass
5
+ from typing import Any, AsyncIterator, Dict, Optional, Set
6
+
7
+ from skills_runtime.core.contracts import AgentEvent
8
+
9
+ from ..logging_utils import log_suppressed_exception
10
+ from ..protocol.capability import CapabilityResult, CapabilityStatus
11
+ from ..protocol.context import ExecutionContext
12
+ from .projector import RuntimeUIEventProjector, _AgentCtx
13
+ from .store import AfterIdExpiredError, RuntimeEventStore
14
+ from .v1 import RuntimeEvent, StreamLevel
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ResumeErrorInfo:
19
+ after_id: str
20
+ known_min_id: Optional[str]
21
+ known_max_id: Optional[str]
22
+
23
+
24
+ class RuntimeUIEventsSession:
25
+ """
26
+ UI events 会话:一次 run 的事件投影与多订阅者消费。
27
+
28
+ 目标:
29
+ - 不绑定任何 Web 框架;
30
+ - 支持 `rid/after_id` 断线续传(exclusive);
31
+ - heartbeat 保活;
32
+ - terminal status 收敛:确保订阅侧能收到终态事件。
33
+ """
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ runtime: Any,
39
+ capability_id: str,
40
+ input: Dict[str, Any],
41
+ context: ExecutionContext,
42
+ level: StreamLevel,
43
+ store: RuntimeEventStore,
44
+ heartbeat_interval_s: float,
45
+ input_queue_maxsize: int = 4096,
46
+ subscriber_queue_maxsize: int = 1024,
47
+ ) -> None:
48
+ self._runtime = runtime
49
+ self._capability_id = str(capability_id)
50
+ self._input = dict(input or {})
51
+ self._context = context
52
+ self._level = level
53
+ self._store = store
54
+ self._heartbeat_interval_s = float(heartbeat_interval_s)
55
+ if int(input_queue_maxsize) <= 0:
56
+ raise ValueError("input_queue_maxsize must be > 0")
57
+ if int(subscriber_queue_maxsize) <= 0:
58
+ raise ValueError("subscriber_queue_maxsize must be > 0")
59
+ self._input_queue_maxsize = int(input_queue_maxsize)
60
+ self._subscriber_queue_maxsize = int(subscriber_queue_maxsize)
61
+
62
+ self._projector = RuntimeUIEventProjector(run_id=self._context.run_id, level=self._level)
63
+ self._in_q: asyncio.Queue = asyncio.Queue(maxsize=self._input_queue_maxsize)
64
+ self._subs: Set[asyncio.Queue] = set()
65
+ self._started = False
66
+ self._done = asyncio.Event()
67
+ self._runner_task: asyncio.Task[None] | None = None
68
+ self._session_task: asyncio.Task[None] | None = None
69
+ self._background_error: BaseException | None = None
70
+
71
+ @property
72
+ def run_id(self) -> str:
73
+ return self._context.run_id
74
+
75
+ @property
76
+ def store(self) -> RuntimeEventStore:
77
+ return self._store
78
+
79
+ def _emit_subscriber_lagged(self, *, q: asyncio.Queue) -> RuntimeEvent:
80
+ err = self._projector.error(
81
+ kind="subscriber_lagged",
82
+ message="subscriber queue overflow; policy=disconnect",
83
+ data={"queue_maxsize": int(getattr(q, "maxsize", 0) or 0), "policy": "disconnect"},
84
+ )
85
+ # 该错误是“订阅者局部诊断信号”,不进入 store;避免客户端误用其 rid 作为 after_id 续传游标。
86
+ return err.model_copy(update={"rid": None})
87
+
88
+ def _cut_off_subscriber(self, *, q: asyncio.Queue) -> None:
89
+ self._subs.discard(q)
90
+ try:
91
+ while True:
92
+ q.get_nowait()
93
+ except asyncio.QueueEmpty:
94
+ pass
95
+ try:
96
+ q.put_nowait(self._emit_subscriber_lagged(q=q))
97
+ except Exception as exc:
98
+ # fail-open:断开慢订阅者必须不影响主事件流
99
+ log_suppressed_exception(
100
+ context="cut_off_subscriber_emit_lagged",
101
+ exc=exc,
102
+ run_id=self._context.run_id if self._context else None,
103
+ )
104
+
105
+ def _publish_nowait(self, ev: RuntimeEvent) -> None:
106
+ self._store.append(ev)
107
+ for q in list(self._subs):
108
+ try:
109
+ q.put_nowait(ev)
110
+ except asyncio.QueueFull:
111
+ self._cut_off_subscriber(q=q)
112
+ except Exception as exc:
113
+ # fail-open:单个订阅者的异常不得影响主事件流
114
+ log_suppressed_exception(
115
+ context="publish_to_subscriber",
116
+ exc=exc,
117
+ extra={"event_type": getattr(ev, "type", None)},
118
+ )
119
+
120
+ def _emit_resume_error(self, *, info: ResumeErrorInfo) -> RuntimeEvent:
121
+ msg = (
122
+ f"after_id expired or not found: {info.after_id!r} "
123
+ f"(available: {info.known_min_id!r}..{info.known_max_id!r})"
124
+ )
125
+ return self._projector.error(
126
+ kind="after_id_expired",
127
+ message=msg,
128
+ data={
129
+ "after_id": info.after_id,
130
+ "known_min_id": info.known_min_id,
131
+ "known_max_id": info.known_max_id,
132
+ },
133
+ )
134
+
135
+ def _remember_task_failure(self, task: asyncio.Task[Any]) -> None:
136
+ """提取后台任务异常,避免 fire-and-forget 把错误静默丢掉。"""
137
+
138
+ try:
139
+ exc = task.exception()
140
+ except asyncio.CancelledError:
141
+ return
142
+ if exc is not None and self._background_error is None:
143
+ self._background_error = exc
144
+
145
+ async def ensure_started(self) -> None:
146
+ if self._started:
147
+ return
148
+ self._started = True
149
+
150
+ def _tap(agent_ev: AgentEvent, tap_ctx: Dict[str, Any]) -> None:
151
+ try:
152
+ expected_run_id = self._context.run_id
153
+ ctx_run_id = tap_ctx.get("run_id")
154
+ if isinstance(ctx_run_id, str) and ctx_run_id != expected_run_id:
155
+ return
156
+ ev_run_id = getattr(agent_ev, "run_id", None)
157
+ if isinstance(ev_run_id, str) and ev_run_id != expected_run_id:
158
+ return
159
+ self._in_q.put_nowait(("agent_event", agent_ev, tap_ctx))
160
+ except asyncio.QueueFull:
161
+ # fail-open:旁路 tap 不得影响主流程;输入队列背压时丢弃旁路事件
162
+ pass
163
+ except Exception as exc:
164
+ # fail-open:旁路 tap 过滤/入队异常不得影响主流程
165
+ log_suppressed_exception(
166
+ context="ui_events_tap",
167
+ exc=exc,
168
+ run_id=self._context.run_id if self._context else None,
169
+ extra={"event_type": getattr(agent_ev, "type", None)},
170
+ )
171
+
172
+ self._runtime._register_agent_event_tap(_tap)
173
+
174
+ async def _runner() -> None:
175
+ try:
176
+ async for item in self._runtime.run_stream(self._capability_id, input=self._input, context=self._context):
177
+ if isinstance(item, dict):
178
+ await self._in_q.put(("workflow_event", item))
179
+ elif isinstance(item, CapabilityResult):
180
+ await self._in_q.put(("terminal", item))
181
+ return
182
+ else:
183
+ continue
184
+ await self._in_q.put(("error", RuntimeError("run_stream ended without terminal CapabilityResult")))
185
+ except Exception as exc:
186
+ await self._in_q.put(("error", exc))
187
+
188
+ task = asyncio.create_task(_runner())
189
+ task.add_done_callback(self._remember_task_failure)
190
+ self._runner_task = task
191
+
192
+ async def _loop() -> None:
193
+ try:
194
+ for ev in self._projector.start():
195
+ self._publish_nowait(ev)
196
+
197
+ done = False
198
+ while not done:
199
+ try:
200
+ item = await asyncio.wait_for(self._in_q.get(), timeout=self._heartbeat_interval_s)
201
+ except asyncio.TimeoutError:
202
+ if self._level != StreamLevel.LITE:
203
+ self._publish_nowait(self._projector.heartbeat())
204
+ continue
205
+
206
+ kind = item[0]
207
+ if kind == "agent_event":
208
+ agent_ev, tap_ctx = item[1], item[2]
209
+ agent_ctx = _AgentCtx(
210
+ run_id=str(tap_ctx.get("run_id") or self._context.run_id),
211
+ capability_id=str(tap_ctx.get("capability_id") or ""),
212
+ workflow_id=str(tap_ctx.get("workflow_id")) if isinstance(tap_ctx.get("workflow_id"), str) else None,
213
+ workflow_instance_id=str(tap_ctx.get("workflow_instance_id"))
214
+ if isinstance(tap_ctx.get("workflow_instance_id"), str)
215
+ else None,
216
+ step_id=str(tap_ctx.get("step_id")) if isinstance(tap_ctx.get("step_id"), str) else None,
217
+ branch_id=str(tap_ctx.get("branch_id")) if isinstance(tap_ctx.get("branch_id"), str) else None,
218
+ wf_frames=list(tap_ctx.get("wf_frames")) if isinstance(tap_ctx.get("wf_frames"), list) else None,
219
+ )
220
+ for out_ev in self._projector.on_agent_event(agent_ev, ctx=agent_ctx):
221
+ self._publish_nowait(out_ev)
222
+ elif kind == "workflow_event":
223
+ wf_ev = item[1]
224
+ for out_ev in self._projector.on_workflow_event(wf_ev):
225
+ self._publish_nowait(out_ev)
226
+ elif kind == "terminal":
227
+ terminal = item[1]
228
+ for out_ev in self._projector.on_terminal(terminal):
229
+ self._publish_nowait(out_ev)
230
+ done = True
231
+ elif kind == "error":
232
+ exc = item[1]
233
+ self._publish_nowait(self._projector.error(kind="runner_error", message=str(exc)))
234
+ for out_ev in self._projector.on_terminal(CapabilityResult(status=CapabilityStatus.FAILED, error=str(exc))):
235
+ self._publish_nowait(out_ev)
236
+ done = True
237
+ finally:
238
+ self._done.set()
239
+ await task
240
+ self._runtime._unregister_agent_event_tap(_tap)
241
+
242
+ session_task = asyncio.create_task(_loop())
243
+ session_task.add_done_callback(self._remember_task_failure)
244
+ self._session_task = session_task
245
+
246
+ async def wait_done(self) -> None:
247
+ await self._done.wait()
248
+ if self._session_task is not None:
249
+ await self._session_task
250
+ if self._background_error is not None:
251
+ raise self._background_error
252
+
253
+ async def subscribe(self, *, after_id: Optional[str]) -> AsyncIterator[RuntimeEvent]:
254
+ await self.ensure_started()
255
+ q: asyncio.Queue = asyncio.Queue(maxsize=self._subscriber_queue_maxsize)
256
+ self._subs.add(q)
257
+ try:
258
+ try:
259
+ replay = list(self._store.read_after(after_id=after_id))
260
+ except AfterIdExpiredError as exc:
261
+ err = self._emit_resume_error(
262
+ info=ResumeErrorInfo(after_id=exc.after_id, known_min_id=exc.min_rid, known_max_id=exc.max_rid)
263
+ )
264
+ yield err
265
+ return
266
+
267
+ last_seq = replay[-1].seq if replay else 0
268
+ for ev in replay:
269
+ yield ev
270
+ if ev.type == "run.status" and ev.data.get("status") != "running":
271
+ return
272
+
273
+ while True:
274
+ if self._done.is_set() and q.empty():
275
+ return
276
+ ev = await q.get()
277
+ if ev.type == "error" and ev.data.get("kind") == "subscriber_lagged":
278
+ yield ev
279
+ return
280
+ if ev.seq <= last_seq:
281
+ continue
282
+ last_seq = ev.seq
283
+ yield ev
284
+ if ev.type == "run.status" and ev.data.get("status") != "running":
285
+ return
286
+ finally:
287
+ self._subs.discard(q)
288
+
289
+ async def _ensure_started(self) -> None:
290
+ """兼容旧调用方;新代码应使用公开的 `ensure_started()`。"""
291
+
292
+ await self.ensure_started()
@@ -0,0 +1,127 @@
1
+ """
2
+ Runtime UI Events:断线续传(rid/after_id)的最小 in-memory store。
3
+
4
+ 定位:
5
+ - 提供最小可复用能力:append + read_after(exclusive)+ 过期诊断;
6
+ - 不绑定任何 HTTP 框架或持久化实现;
7
+ - run 级别隔离由调用方保证(通常一个 store 对应一个 run)。
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections import deque
13
+ from dataclasses import dataclass
14
+ import threading
15
+ from typing import Deque, Dict, Iterable, Optional, Protocol
16
+
17
+ from .v1 import RuntimeEvent
18
+
19
+
20
+ class RuntimeEventStore(Protocol):
21
+ """
22
+ RuntimeEventStore:用于断线续传的最小 store 接口(可插拔)。
23
+
24
+ 约束:
25
+ - `read_after` 为排他语义(exclusive:strictly after)
26
+ - after_id 过期/不在窗口内时,建议抛出 `AfterIdExpiredError`(可诊断)
27
+ """
28
+
29
+ @property
30
+ def min_rid(self) -> Optional[str]: ...
31
+
32
+ @property
33
+ def max_rid(self) -> Optional[str]: ...
34
+
35
+ def append(self, ev: RuntimeEvent) -> None: ...
36
+
37
+ def read_after(self, *, after_id: Optional[str]) -> Iterable[RuntimeEvent]: ...
38
+
39
+
40
+ @dataclass(frozen=True)
41
+ class AfterIdExpiredError(Exception):
42
+ """
43
+ after_id 不在可用窗口内(可能是裁剪过期,也可能是未知/非法游标)。
44
+
45
+ 字段:
46
+ - after_id:请求的游标
47
+ - min_rid/max_rid:当前窗口内的可用范围(用于诊断与提示客户端策略)
48
+ """
49
+
50
+ after_id: str
51
+ min_rid: Optional[str]
52
+ max_rid: Optional[str]
53
+
54
+ def __str__(self) -> str:
55
+ return f"after_id expired or not found: {self.after_id!r} (available: {self.min_rid!r}..{self.max_rid!r})"
56
+
57
+
58
+ class InMemoryRuntimeEventStore(RuntimeEventStore):
59
+ """
60
+ in-memory 事件存储(ring buffer)。
61
+
62
+ 约束:
63
+ - `read_after` 为排他语义(exclusive:strictly after)
64
+ - 当 after_id 不在窗口内时抛出 AfterIdExpiredError(可诊断)
65
+ """
66
+
67
+ def __init__(self, *, max_events: int = 10_000) -> None:
68
+ if max_events <= 0:
69
+ raise ValueError("max_events must be > 0")
70
+ self._max_events = int(max_events)
71
+ self._events: Deque[RuntimeEvent] = deque(maxlen=self._max_events)
72
+ self._rid_to_pos: Dict[str, int] = {}
73
+ self._start_pos = 0
74
+ self._lock = threading.Lock()
75
+
76
+ @property
77
+ def min_rid(self) -> Optional[str]:
78
+ with self._lock:
79
+ if not self._events:
80
+ return None
81
+ return self._events[0].rid
82
+
83
+ @property
84
+ def max_rid(self) -> Optional[str]:
85
+ with self._lock:
86
+ if not self._events:
87
+ return None
88
+ return self._events[-1].rid
89
+
90
+ def append(self, ev: RuntimeEvent) -> None:
91
+ if ev.rid is None:
92
+ raise ValueError("RuntimeEvent.rid is required for resume store")
93
+ with self._lock:
94
+ if len(self._events) == self._max_events:
95
+ evicted = self._events.popleft()
96
+ if evicted.rid is not None:
97
+ self._rid_to_pos.pop(evicted.rid, None)
98
+ self._start_pos += 1
99
+ pos = self._start_pos + len(self._events)
100
+ self._events.append(ev)
101
+ self._rid_to_pos[ev.rid] = pos
102
+
103
+ def read_after(self, *, after_id: Optional[str]) -> Iterable[RuntimeEvent]:
104
+ """
105
+ 读取 after_id 之后的事件(exclusive)。
106
+
107
+ 参数:
108
+ - after_id:续传游标;None 表示从头读取当前窗口
109
+ """
110
+
111
+ with self._lock:
112
+ snapshot = list(self._events)
113
+ min_rid = snapshot[0].rid if snapshot else None
114
+ max_rid = snapshot[-1].rid if snapshot else None
115
+ if after_id is None:
116
+ return snapshot
117
+ if not snapshot:
118
+ return []
119
+
120
+ pos = self._rid_to_pos.get(str(after_id))
121
+ if pos is None:
122
+ raise AfterIdExpiredError(after_id=str(after_id), min_rid=min_rid, max_rid=max_rid)
123
+
124
+ idx = pos - self._start_pos
125
+ if idx < 0 or idx >= len(snapshot):
126
+ raise AfterIdExpiredError(after_id=str(after_id), min_rid=min_rid, max_rid=max_rid)
127
+ return snapshot[idx + 1 :]
@@ -0,0 +1,33 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Runtime UI Events:通用传输适配(JSON Lines / SSE 子集)。
5
+
6
+ 定位:
7
+ - 不绑定任何 Web 框架;
8
+ - 只负责 framing(把 RuntimeEvent 转成可流式发送的文本 chunk)。
9
+ """
10
+
11
+ import json
12
+
13
+ from .v1 import RuntimeEvent
14
+
15
+
16
+ def encode_json_line(ev: RuntimeEvent, *, prefix_data: bool = False) -> str:
17
+ """
18
+ 将单条 RuntimeEvent 编码为可流式发送的文本。
19
+
20
+ 参数:
21
+ - ev:RuntimeEvent
22
+ - prefix_data:是否使用 `data: ` 前缀(SSE 子集兼容)
23
+
24
+ 返回:
25
+ - JSONL:`<json>\\n`
26
+ - SSE 子集:`data: <json>\\n\\n`
27
+ """
28
+
29
+ payload = json.dumps(ev.model_dump(by_alias=True), ensure_ascii=False, separators=(",", ":"))
30
+ if prefix_data:
31
+ return f"data: {payload}\n\n"
32
+ return payload + "\n"
33
+
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ """
4
+ Runtime UI Events v1(协议模型)。
5
+
6
+ 定位:
7
+ - UI events 是“可观测/产品化投影层”,不是审计真相源;
8
+ - 真相源仍为 WAL/events/tool evidence → NodeReport;
9
+ - UI events 将(上游 `AgentEvent`、workflow 轻量事件、终态 `CapabilityResult`)投影为统一的 RuntimeEvent 信封;
10
+ - 本模块仅定义 v1 的稳定信封字段与最小结构约束。
11
+ """
12
+
13
+ from enum import Enum
14
+ from typing import Any, Dict, List, Optional
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field
17
+
18
+
19
+ class StreamLevel(str, Enum):
20
+ """事件输出层级。"""
21
+
22
+ LITE = "lite"
23
+ UI = "ui"
24
+ RAW = "raw"
25
+
26
+
27
+ class PathSegment(BaseModel):
28
+ """PathSegment(图投影路径段)。"""
29
+
30
+ model_config = ConfigDict(extra="forbid")
31
+
32
+ kind: str = Field(min_length=1)
33
+ id: str = Field(min_length=1)
34
+ # v1 加法演进:用于多实例/嵌套消歧与展示(不要求每条事件都提供)
35
+ instance_id: Optional[str] = None
36
+ # `ref` 用于携带“逻辑身份”(例如 workflow spec id / capability id / tool name),避免 id 变为 opaque 后 UI 无法展示。
37
+ # 约束:保持通用性,只提供 kind/id 两个维度。
38
+ ref: Optional[Dict[str, str]] = None
39
+
40
+
41
+ class Evidence(BaseModel):
42
+ """证据指针:用于回到 WAL/NodeReport/tool evidence 真相源。"""
43
+
44
+ model_config = ConfigDict(extra="forbid")
45
+
46
+ events_path: Optional[str] = None
47
+ call_id: Optional[str] = None
48
+ artifact_path: Optional[str] = None
49
+ node_report_schema: Optional[str] = None
50
+
51
+
52
+ class RuntimeEvent(BaseModel):
53
+ """
54
+ RuntimeEvent v1:统一事件信封(Envelope)。
55
+
56
+ 约束:
57
+ - `schema` 固定为 `capability-runtime.runtime_event.v1`
58
+ - `seq` 在单 run 内单调递增
59
+ - `rid` 为传输层游标(续传用),可与 `seq` 等价但语义不同
60
+ """
61
+
62
+ model_config = ConfigDict(extra="forbid", populate_by_name=True)
63
+
64
+ schema_id: str = Field(min_length=1, alias="schema")
65
+ type: str = Field(min_length=1)
66
+ run_id: str = Field(min_length=1)
67
+
68
+ seq: int = Field(ge=0)
69
+ ts_ms: int = Field(ge=0)
70
+ level: StreamLevel
71
+
72
+ path: List[PathSegment] = Field(default_factory=list)
73
+ data: Dict[str, Any] = Field(default_factory=dict)
74
+
75
+ rid: Optional[str] = None
76
+ evidence: Optional[Evidence] = None