ya-agent-stream-protocol 0.86.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.
@@ -0,0 +1,3 @@
1
+ from ya_agent_stream_protocol.json_types import JsonArray, JsonObject, JsonValue
2
+
3
+ __all__ = ["JsonArray", "JsonObject", "JsonValue"]
@@ -0,0 +1,37 @@
1
+ from ya_agent_stream_protocol.agui.events import dump_agui_event
2
+ from ya_agent_stream_protocol.agui.replay import (
3
+ AguiReplayBuffer,
4
+ AguiReplayConfig,
5
+ compact_agui_events,
6
+ is_subagent_detail_event,
7
+ is_subagent_event,
8
+ )
9
+ from ya_agent_stream_protocol.agui.sse import (
10
+ BufferedStreamEvent,
11
+ build_buffered_stream_event,
12
+ format_sse_event,
13
+ resolve_event_cursor,
14
+ )
15
+ from ya_agent_stream_protocol.agui.validation import (
16
+ parse_message_events,
17
+ parse_required_message_events,
18
+ validate_agui_events,
19
+ validate_display_events,
20
+ )
21
+
22
+ __all__ = [
23
+ "AguiReplayBuffer",
24
+ "AguiReplayConfig",
25
+ "BufferedStreamEvent",
26
+ "build_buffered_stream_event",
27
+ "compact_agui_events",
28
+ "dump_agui_event",
29
+ "format_sse_event",
30
+ "is_subagent_detail_event",
31
+ "is_subagent_event",
32
+ "parse_message_events",
33
+ "parse_required_message_events",
34
+ "resolve_event_cursor",
35
+ "validate_agui_events",
36
+ "validate_display_events",
37
+ ]
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from ya_agent_stream_protocol.json_types import JsonObject
8
+
9
+
10
+ def dump_agui_event(event: BaseModel) -> JsonObject:
11
+ payload = event.model_dump(mode="json", exclude_none=True, by_alias=True)
12
+ payload.setdefault("timestamp", int(datetime.now(UTC).timestamp() * 1000))
13
+ return payload # type: ignore[return-value]
@@ -0,0 +1,236 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ from ya_agent_stream_protocol.json_types import JsonObject, JsonValue
7
+
8
+ _REPLAY_DROP_EVENT_TYPES = frozenset({
9
+ "TEXT_MESSAGE_START",
10
+ "TEXT_MESSAGE_END",
11
+ "REASONING_MESSAGE_START",
12
+ "REASONING_MESSAGE_END",
13
+ "TOOL_CALL_START",
14
+ "TOOL_CALL_END",
15
+ })
16
+ _SUBAGENT_DETAIL_EVENT_TYPES = frozenset({
17
+ "TEXT_MESSAGE_START",
18
+ "TEXT_MESSAGE_CHUNK",
19
+ "TEXT_MESSAGE_END",
20
+ "REASONING_MESSAGE_START",
21
+ "REASONING_MESSAGE_CHUNK",
22
+ "REASONING_MESSAGE_END",
23
+ "TOOL_CALL_START",
24
+ "TOOL_CALL_CHUNK",
25
+ "TOOL_CALL_END",
26
+ "TOOL_CALL_RESULT",
27
+ })
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class AguiReplayConfig:
32
+ agent_id_field: str | None = None
33
+ main_agent_id: str = "main"
34
+ drop_subagent_detail_events: bool = False
35
+
36
+
37
+ @dataclass(slots=True)
38
+ class AguiReplayBuffer:
39
+ config: AguiReplayConfig = field(default_factory=AguiReplayConfig)
40
+ events: list[JsonObject] = field(default_factory=list)
41
+ _text_chunk_index: dict[str, int] = field(default_factory=dict)
42
+ _reasoning_chunk_index: dict[str, int] = field(default_factory=dict)
43
+ _tool_chunk_index: dict[str, int] = field(default_factory=dict)
44
+ _chunk_fragments: dict[int, list[str]] = field(default_factory=dict)
45
+
46
+ def append(self, event: dict[str, Any]) -> None:
47
+ event_type = str(event.get("type", "")).strip()
48
+ if event_type == "":
49
+ return
50
+ if self.is_subagent_detail_event(event):
51
+ return
52
+ if event_type in _REPLAY_DROP_EVENT_TYPES:
53
+ return
54
+ if event_type == "TEXT_MESSAGE_CHUNK":
55
+ self._merge_text_chunk(event)
56
+ return
57
+ if event_type == "REASONING_MESSAGE_CHUNK":
58
+ self._merge_reasoning_chunk(event)
59
+ return
60
+ if event_type == "TOOL_CALL_CHUNK":
61
+ self._merge_tool_call_chunk(event)
62
+ return
63
+ self._append_passthrough_event(event)
64
+
65
+ def extend_snapshot(self, events: list[dict[str, Any]]) -> None:
66
+ self.clear()
67
+ for event in events:
68
+ self.append(event)
69
+
70
+ def snapshot(self) -> list[JsonObject]:
71
+ snapshot: list[JsonObject] = []
72
+ for index, event in enumerate(self.events):
73
+ event_copy = dict(event)
74
+ fragments = self._chunk_fragments.get(index)
75
+ if fragments:
76
+ event_copy["delta"] = "".join(fragments)
77
+ snapshot.append(event_copy)
78
+ return snapshot
79
+
80
+ def clear(self) -> None:
81
+ self.events.clear()
82
+ self._text_chunk_index.clear()
83
+ self._reasoning_chunk_index.clear()
84
+ self._tool_chunk_index.clear()
85
+ self._chunk_fragments.clear()
86
+
87
+ def is_subagent_event(self, event: dict[str, Any]) -> bool:
88
+ agent_id_field = self.config.agent_id_field
89
+ if agent_id_field is None:
90
+ return False
91
+ agent_id = _normalized_identifier(_event_field(event, agent_id_field, _camel_to_snake(agent_id_field)))
92
+ return agent_id is not None and agent_id != self.config.main_agent_id
93
+
94
+ def is_subagent_detail_event(self, event: dict[str, Any]) -> bool:
95
+ if not self.config.drop_subagent_detail_events:
96
+ return False
97
+ event_type = str(event.get("type", "")).strip()
98
+ return event_type in _SUBAGENT_DETAIL_EVENT_TYPES and self.is_subagent_event(event)
99
+
100
+ def _merge_text_chunk(self, event: dict[str, Any]) -> None:
101
+ message_id = _normalized_identifier(_event_field(event, "messageId", "message_id"))
102
+ if message_id is None:
103
+ self._append_passthrough_event(event)
104
+ return
105
+ existing_index = self._text_chunk_index.get(message_id)
106
+ if existing_index is None:
107
+ self._text_chunk_index[message_id] = self._append_chunk_event(event)
108
+ return
109
+ self._append_delta_fragment(existing_index, event.get("delta"))
110
+ existing = self.events[existing_index]
111
+ if existing.get("role") is None and event.get("role") is not None:
112
+ existing["role"] = event.get("role")
113
+ if existing.get("name") is None and event.get("name") is not None:
114
+ existing["name"] = event.get("name")
115
+
116
+ def _merge_reasoning_chunk(self, event: dict[str, Any]) -> None:
117
+ message_id = _normalized_identifier(_event_field(event, "messageId", "message_id"))
118
+ if message_id is None:
119
+ self._append_passthrough_event(event)
120
+ return
121
+ existing_index = self._reasoning_chunk_index.get(message_id)
122
+ if existing_index is None:
123
+ self._reasoning_chunk_index[message_id] = self._append_chunk_event(event)
124
+ return
125
+ self._append_delta_fragment(existing_index, event.get("delta"))
126
+
127
+ def _merge_tool_call_chunk(self, event: dict[str, Any]) -> None:
128
+ tool_call_id = _normalized_identifier(_event_field(event, "toolCallId", "tool_call_id"))
129
+ if tool_call_id is None:
130
+ self._append_passthrough_event(event)
131
+ return
132
+ existing_index = self._tool_chunk_index.get(tool_call_id)
133
+ if existing_index is None:
134
+ self._tool_chunk_index[tool_call_id] = self._append_chunk_event(event)
135
+ return
136
+ self._append_delta_fragment(existing_index, event.get("delta"))
137
+ existing = self.events[existing_index]
138
+ tool_call_name = _event_field(event, "toolCallName", "tool_call_name")
139
+ if existing.get("toolCallName") is None and tool_call_name is not None:
140
+ existing["toolCallName"] = tool_call_name
141
+ parent_message_id = _event_field(event, "parentMessageId", "parent_message_id")
142
+ if existing.get("parentMessageId") is None and parent_message_id is not None:
143
+ existing["parentMessageId"] = parent_message_id
144
+
145
+ def _append_passthrough_event(self, event: dict[str, Any]) -> None:
146
+ self._forget_previous_run_state_if_needed(event)
147
+ self.events.append(_coerce_json_object(event))
148
+
149
+ def _append_chunk_event(self, event: dict[str, Any]) -> int:
150
+ self._forget_previous_run_state_if_needed(event)
151
+ event_copy = _coerce_json_object(event)
152
+ fragment = _delta_fragment(event_copy.get("delta"))
153
+ if fragment is not None:
154
+ event_copy["delta"] = ""
155
+ index = len(self.events)
156
+ self.events.append(event_copy)
157
+ if fragment is not None:
158
+ self._chunk_fragments[index] = [fragment]
159
+ return index
160
+
161
+ def _append_delta_fragment(self, index: int, value: object) -> None:
162
+ fragment = _delta_fragment(value)
163
+ if fragment is None:
164
+ return
165
+ self._chunk_fragments.setdefault(index, []).append(fragment)
166
+
167
+ def _forget_previous_run_state_if_needed(self, event: dict[str, Any]) -> None:
168
+ if event.get("type") != "RUN_STARTED":
169
+ return
170
+ self._text_chunk_index.clear()
171
+ self._reasoning_chunk_index.clear()
172
+ self._tool_chunk_index.clear()
173
+ self._chunk_fragments = {
174
+ index: fragments for index, fragments in self._chunk_fragments.items() if index < len(self.events)
175
+ }
176
+
177
+
178
+ def compact_agui_events(events: list[dict[str, Any]], *, config: AguiReplayConfig | None = None) -> list[JsonObject]:
179
+ replay = AguiReplayBuffer(config=config or AguiReplayConfig())
180
+ for event in events:
181
+ replay.append(event)
182
+ return replay.snapshot()
183
+
184
+
185
+ def is_subagent_detail_event(
186
+ event: dict[str, Any], *, agent_id_field: str = "yaacliAgentId", main_agent_id: str = "main"
187
+ ) -> bool:
188
+ replay = AguiReplayBuffer(
189
+ config=AguiReplayConfig(
190
+ agent_id_field=agent_id_field,
191
+ main_agent_id=main_agent_id,
192
+ drop_subagent_detail_events=True,
193
+ )
194
+ )
195
+ return replay.is_subagent_detail_event(event)
196
+
197
+
198
+ def is_subagent_event(
199
+ event: dict[str, Any], *, agent_id_field: str = "yaacliAgentId", main_agent_id: str = "main"
200
+ ) -> bool:
201
+ replay = AguiReplayBuffer(config=AguiReplayConfig(agent_id_field=agent_id_field, main_agent_id=main_agent_id))
202
+ return replay.is_subagent_event(event)
203
+
204
+
205
+ def _coerce_json_object(event: dict[str, Any]) -> JsonObject:
206
+ return dict(event) # type: ignore[return-value]
207
+
208
+
209
+ def _event_field(event: dict[str, Any], camel_name: str, snake_name: str) -> JsonValue:
210
+ if camel_name in event:
211
+ return event[camel_name] # type: ignore[return-value]
212
+ return event.get(snake_name) # type: ignore[return-value]
213
+
214
+
215
+ def _normalized_identifier(value: object) -> str | None:
216
+ if not isinstance(value, str):
217
+ return None
218
+ normalized = value.strip()
219
+ return normalized or None
220
+
221
+
222
+ def _delta_fragment(value: object) -> str | None:
223
+ if value is None:
224
+ return None
225
+ if isinstance(value, str):
226
+ return value
227
+ return str(value)
228
+
229
+
230
+ def _camel_to_snake(value: str) -> str:
231
+ output: list[str] = []
232
+ for index, character in enumerate(value):
233
+ if character.isupper() and index > 0:
234
+ output.append("_")
235
+ output.append(character.lower())
236
+ return "".join(output)
@@ -0,0 +1,37 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from dataclasses import dataclass
5
+ from typing import Any
6
+
7
+ from ya_agent_stream_protocol.json_types import JsonObject
8
+
9
+
10
+ @dataclass(slots=True)
11
+ class BufferedStreamEvent:
12
+ id: str
13
+ payload: JsonObject
14
+ terminal: bool = False
15
+
16
+
17
+ def format_sse_event(event: BufferedStreamEvent) -> dict[str, str]:
18
+ return {
19
+ "id": event.id,
20
+ "event": str(event.payload.get("type", "message")),
21
+ "data": json.dumps(event.payload, ensure_ascii=False),
22
+ }
23
+
24
+
25
+ def resolve_event_cursor(last_event_id: str | None) -> int:
26
+ if last_event_id is None:
27
+ return 0
28
+ try:
29
+ return max(int(last_event_id), 0)
30
+ except ValueError:
31
+ return 0
32
+
33
+
34
+ def build_buffered_stream_event(
35
+ event_id: int | str, payload: dict[str, Any], *, terminal: bool = False
36
+ ) -> BufferedStreamEvent:
37
+ return BufferedStreamEvent(id=str(event_id), payload=dict(payload), terminal=terminal) # type: ignore[arg-type]
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from ya_agent_stream_protocol.json_types import JsonObject, JsonValue
6
+
7
+
8
+ def validate_agui_events(
9
+ raw_payload: object,
10
+ *,
11
+ payload_name: str = "AGUI events payload",
12
+ allow_none: bool = False,
13
+ ) -> list[JsonObject] | None:
14
+ if raw_payload is None and allow_none:
15
+ return None
16
+ if not isinstance(raw_payload, list):
17
+ raise TypeError(f"{payload_name} must be a top-level JSON array of AGUI event objects")
18
+ parsed_events: list[JsonObject] = [event for event in raw_payload if isinstance(event, dict)]
19
+ if len(parsed_events) != len(raw_payload):
20
+ raise TypeError(f"{payload_name} must contain only AGUI event objects")
21
+ return parsed_events
22
+
23
+
24
+ def parse_message_events(raw_message_payload: JsonValue) -> list[JsonObject] | None:
25
+ return validate_agui_events(raw_message_payload, payload_name="message payload", allow_none=True)
26
+
27
+
28
+ def parse_required_message_events(
29
+ raw_message_payload: JsonValue, *, payload_name: str = "message payload"
30
+ ) -> list[JsonObject]:
31
+ parsed = validate_agui_events(raw_message_payload, payload_name=payload_name, allow_none=False)
32
+ if parsed is None:
33
+ raise TypeError(f"{payload_name} must be a top-level JSON array of AGUI event objects")
34
+ return parsed
35
+
36
+
37
+ def validate_display_events(raw_payload: object) -> list[JsonObject]:
38
+ parsed = validate_agui_events(raw_payload, payload_name="display_messages payload", allow_none=False)
39
+ if parsed is None:
40
+ raise TypeError("display_messages payload must be a top-level JSON array of event objects")
41
+ return parsed
42
+
43
+
44
+ def coerce_json_object(value: dict[str, Any]) -> JsonObject:
45
+ return dict(value) # type: ignore[return-value]
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ JsonValue = None | bool | int | float | str | list["JsonValue"] | dict[str, "JsonValue"]
4
+ JsonArray = list[JsonValue]
5
+ JsonObject = dict[str, JsonValue]
@@ -0,0 +1,3 @@
1
+ from ya_agent_stream_protocol.sdk.agui_adapter import AguiAdapterConfig, AguiEventAdapter
2
+
3
+ __all__ = ["AguiAdapterConfig", "AguiEventAdapter"]
@@ -0,0 +1,450 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from dataclasses import asdict, dataclass, field, is_dataclass
6
+ from datetime import UTC, datetime
7
+ from pathlib import Path
8
+ from typing import cast
9
+
10
+ from ag_ui.core.events import (
11
+ CustomEvent,
12
+ ReasoningMessageChunkEvent,
13
+ ReasoningMessageEndEvent,
14
+ ReasoningMessageStartEvent,
15
+ RunErrorEvent,
16
+ RunFinishedEvent,
17
+ RunStartedEvent,
18
+ TextMessageChunkEvent,
19
+ TextMessageEndEvent,
20
+ TextMessageStartEvent,
21
+ ToolCallChunkEvent,
22
+ ToolCallEndEvent,
23
+ ToolCallResultEvent,
24
+ ToolCallStartEvent,
25
+ )
26
+ from pydantic import BaseModel
27
+ from pydantic_ai import (
28
+ FinalResultEvent,
29
+ FunctionToolResultEvent,
30
+ OutputToolResultEvent,
31
+ PartDeltaEvent,
32
+ PartEndEvent,
33
+ PartStartEvent,
34
+ TextPartDelta,
35
+ ThinkingPartDelta,
36
+ ToolCallPartDelta,
37
+ )
38
+ from pydantic_ai.messages import RetryPromptPart, TextPart, ThinkingPart, ToolCallPart, ToolReturnPart
39
+ from ya_agent_sdk.context.agent import StreamEvent
40
+ from ya_agent_sdk.events import MessageReceivedEvent, ModelRequestStartEvent, UsageSnapshotEvent
41
+
42
+ from ya_agent_stream_protocol.agui.events import dump_agui_event
43
+ from ya_agent_stream_protocol.json_types import JsonObject, JsonValue
44
+
45
+
46
+ @dataclass(slots=True)
47
+ class AguiAdapterConfig:
48
+ run_event_prefix: str
49
+ agent_event_prefix: str = "ya_agent"
50
+ stream_metadata_prefix: str | None = None
51
+
52
+
53
+ @dataclass(slots=True)
54
+ class PartCursor:
55
+ kind: str
56
+ part_id: str
57
+ role: str | None = None
58
+ tool_call_name: str | None = None
59
+ emitted_chunk: bool = False
60
+
61
+
62
+ @dataclass(slots=True)
63
+ class AgentCursor:
64
+ loop_index: int = 0
65
+ parts: dict[int, PartCursor] = field(default_factory=dict)
66
+
67
+
68
+ class AguiEventAdapter:
69
+ def __init__(self, *, session_id: str, run_id: str, config: AguiAdapterConfig) -> None:
70
+ self._session_id = session_id
71
+ self._run_id = run_id
72
+ self._config = config
73
+ self._agents: dict[str, AgentCursor] = {}
74
+
75
+ def build_run_started_event(self, *, input_parts: list[JsonObject] | None = None) -> JsonObject:
76
+ _ = input_parts
77
+ return dump_agui_event(RunStartedEvent(thread_id=self._session_id, run_id=self._run_id))
78
+
79
+ def build_run_finished_event(self, result: JsonValue = None) -> JsonObject:
80
+ return dump_agui_event(RunFinishedEvent(thread_id=self._session_id, run_id=self._run_id, result=result))
81
+
82
+ def build_run_error_event(self, *, message: str, code: str | None = None) -> JsonObject:
83
+ return dump_agui_event(RunErrorEvent(message=message, code=code))
84
+
85
+ def build_run_custom_event(self, event_name: str, payload: object) -> JsonObject:
86
+ return dump_agui_event(
87
+ CustomEvent(
88
+ name=f"{self._config.run_event_prefix}.{event_name}",
89
+ value=_serialize_value(payload),
90
+ )
91
+ )
92
+
93
+ def adapt_stream_event(self, stream_event: StreamEvent) -> list[JsonObject]:
94
+ cursor = self._agents.setdefault(stream_event.agent_id, AgentCursor())
95
+ event = stream_event.event
96
+
97
+ if isinstance(event, ModelRequestStartEvent):
98
+ cursor.loop_index = event.loop_index
99
+
100
+ if isinstance(event, PartStartEvent):
101
+ return self._with_stream_metadata(stream_event, self._adapt_part_start(stream_event, cursor))
102
+ if isinstance(event, PartDeltaEvent):
103
+ return self._with_stream_metadata(stream_event, self._adapt_part_delta(stream_event, cursor))
104
+ if isinstance(event, PartEndEvent):
105
+ return self._with_stream_metadata(stream_event, self._adapt_part_end(stream_event, cursor))
106
+ if isinstance(event, FunctionToolResultEvent | OutputToolResultEvent):
107
+ return self._with_stream_metadata(stream_event, self._adapt_function_tool_result(stream_event))
108
+ if isinstance(event, FinalResultEvent):
109
+ return [
110
+ self._custom_agent_event(
111
+ event_name="final_result",
112
+ stream_event=stream_event,
113
+ payload={"tool_name": event.tool_name, "tool_call_id": event.tool_call_id},
114
+ )
115
+ ]
116
+ if isinstance(event, UsageSnapshotEvent):
117
+ return [
118
+ self._custom_agent_event(
119
+ event_name="usage_snapshot",
120
+ stream_event=stream_event,
121
+ payload=_serialize_value(event.snapshot) if event.snapshot is not None else None,
122
+ )
123
+ ]
124
+ if isinstance(event, MessageReceivedEvent):
125
+ return [
126
+ self._custom_agent_event(
127
+ event_name="message_received",
128
+ stream_event=stream_event,
129
+ payload={"messages": _serialize_value(event.messages)},
130
+ )
131
+ ]
132
+ return [
133
+ self._custom_agent_event(
134
+ event_name=_camel_to_snake(type(event).__name__),
135
+ stream_event=stream_event,
136
+ payload=_serialize_value(event),
137
+ )
138
+ ]
139
+
140
+ def _adapt_part_start(self, stream_event: StreamEvent, cursor: AgentCursor) -> list[JsonObject]:
141
+ event = cast(PartStartEvent, stream_event.event)
142
+ part = event.part
143
+ if isinstance(part, TextPart):
144
+ message_id = part.id or self._part_id(stream_event.agent_id, cursor.loop_index, event.index, "text")
145
+ cursor.parts[event.index] = PartCursor(kind="text", part_id=message_id, role="assistant")
146
+ events = [
147
+ dump_agui_event(
148
+ TextMessageStartEvent(message_id=message_id, role="assistant", name=stream_event.agent_name)
149
+ )
150
+ ]
151
+ if part.content:
152
+ events.append(
153
+ dump_agui_event(
154
+ TextMessageChunkEvent(
155
+ message_id=message_id,
156
+ role="assistant",
157
+ name=stream_event.agent_name,
158
+ delta=part.content,
159
+ )
160
+ )
161
+ )
162
+ cursor.parts[event.index].emitted_chunk = True
163
+ return events
164
+ if isinstance(part, ThinkingPart):
165
+ message_id = part.id or self._part_id(stream_event.agent_id, cursor.loop_index, event.index, "reasoning")
166
+ cursor.parts[event.index] = PartCursor(kind="reasoning", part_id=message_id, role="reasoning")
167
+ events = [dump_agui_event(ReasoningMessageStartEvent(message_id=message_id, role="reasoning"))]
168
+ if part.content:
169
+ events.append(dump_agui_event(ReasoningMessageChunkEvent(message_id=message_id, delta=part.content)))
170
+ cursor.parts[event.index].emitted_chunk = True
171
+ return events
172
+ if isinstance(part, ToolCallPart):
173
+ tool_call_id = part.tool_call_id
174
+ cursor.parts[event.index] = PartCursor(
175
+ kind="tool_call",
176
+ part_id=tool_call_id,
177
+ tool_call_name=part.tool_name,
178
+ )
179
+ events = [dump_agui_event(ToolCallStartEvent(tool_call_id=tool_call_id, tool_call_name=part.tool_name))]
180
+ chunk_delta = _stringify_tool_call_args(part.args)
181
+ if chunk_delta is not None or part.tool_name:
182
+ events.append(
183
+ dump_agui_event(
184
+ ToolCallChunkEvent(
185
+ tool_call_id=tool_call_id,
186
+ tool_call_name=part.tool_name,
187
+ delta=chunk_delta,
188
+ )
189
+ )
190
+ )
191
+ cursor.parts[event.index].emitted_chunk = True
192
+ return events
193
+ if isinstance(part, ToolReturnPart):
194
+ return [self._tool_result_event(part)]
195
+ if isinstance(part, RetryPromptPart):
196
+ return [
197
+ self._custom_agent_event("retry_prompt_part", stream_event=stream_event, payload=_serialize_value(part))
198
+ ]
199
+ return [self._custom_agent_event("part_start", stream_event=stream_event, payload=_serialize_value(event))]
200
+
201
+ def _adapt_part_delta(self, stream_event: StreamEvent, cursor: AgentCursor) -> list[JsonObject]:
202
+ event = cast(PartDeltaEvent, stream_event.event)
203
+ delta = event.delta
204
+ if isinstance(delta, TextPartDelta):
205
+ part_cursor = self._ensure_text_cursor(stream_event.agent_id, cursor, event.index)
206
+ part_cursor.emitted_chunk = True
207
+ return [
208
+ dump_agui_event(
209
+ TextMessageChunkEvent(
210
+ message_id=part_cursor.part_id,
211
+ role="assistant",
212
+ name=stream_event.agent_name,
213
+ delta=delta.content_delta,
214
+ )
215
+ )
216
+ ]
217
+ if isinstance(delta, ThinkingPartDelta):
218
+ part_cursor = self._ensure_reasoning_cursor(stream_event.agent_id, cursor, event.index)
219
+ events: list[JsonObject] = []
220
+ if delta.content_delta:
221
+ part_cursor.emitted_chunk = True
222
+ events.append(
223
+ dump_agui_event(
224
+ ReasoningMessageChunkEvent(message_id=part_cursor.part_id, delta=delta.content_delta)
225
+ )
226
+ )
227
+ if getattr(delta, "signature_delta", None):
228
+ events.append(
229
+ self._custom_agent_event(
230
+ "reasoning_signature_delta",
231
+ stream_event=stream_event,
232
+ payload={"message_id": part_cursor.part_id, "signature_delta": delta.signature_delta},
233
+ )
234
+ )
235
+ return events
236
+ if isinstance(delta, ToolCallPartDelta):
237
+ part_cursor = self._ensure_tool_call_cursor(stream_event.agent_id, cursor, event.index, delta.tool_call_id)
238
+ if delta.tool_name_delta:
239
+ part_cursor.tool_call_name = f"{part_cursor.tool_call_name or ''}{delta.tool_name_delta}" or None
240
+ part_cursor.emitted_chunk = True
241
+ return [
242
+ dump_agui_event(
243
+ ToolCallChunkEvent(
244
+ tool_call_id=part_cursor.part_id,
245
+ tool_call_name=part_cursor.tool_call_name,
246
+ delta=_stringify_tool_call_args(delta.args_delta),
247
+ )
248
+ )
249
+ ]
250
+ return [self._custom_agent_event("part_delta", stream_event=stream_event, payload=_serialize_value(event))]
251
+
252
+ def _adapt_part_end(self, stream_event: StreamEvent, cursor: AgentCursor) -> list[JsonObject]:
253
+ event = cast(PartEndEvent, stream_event.event)
254
+ part = event.part
255
+ part_cursor = cursor.parts.pop(event.index, None)
256
+ if isinstance(part, TextPart):
257
+ message_id = (
258
+ part_cursor.part_id
259
+ if part_cursor is not None
260
+ else part.id or self._part_id(stream_event.agent_id, cursor.loop_index, event.index, "text")
261
+ )
262
+ events: list[JsonObject] = []
263
+ emitted_chunk = part_cursor.emitted_chunk if part_cursor is not None else False
264
+ if part.content and not emitted_chunk:
265
+ events.append(
266
+ dump_agui_event(
267
+ TextMessageChunkEvent(
268
+ message_id=message_id,
269
+ role="assistant",
270
+ name=stream_event.agent_name,
271
+ delta=part.content,
272
+ )
273
+ )
274
+ )
275
+ events.append(dump_agui_event(TextMessageEndEvent(message_id=message_id)))
276
+ return events
277
+ if isinstance(part, ThinkingPart):
278
+ message_id = (
279
+ part_cursor.part_id
280
+ if part_cursor is not None
281
+ else part.id or self._part_id(stream_event.agent_id, cursor.loop_index, event.index, "reasoning")
282
+ )
283
+ events = []
284
+ emitted_chunk = part_cursor.emitted_chunk if part_cursor is not None else False
285
+ if part.content and not emitted_chunk:
286
+ events.append(dump_agui_event(ReasoningMessageChunkEvent(message_id=message_id, delta=part.content)))
287
+ events.append(dump_agui_event(ReasoningMessageEndEvent(message_id=message_id)))
288
+ return events
289
+ if isinstance(part, ToolCallPart):
290
+ tool_call_id = part.tool_call_id if part_cursor is None else part_cursor.part_id
291
+ events = []
292
+ emitted_chunk = part_cursor.emitted_chunk if part_cursor is not None else False
293
+ if not emitted_chunk:
294
+ events.append(
295
+ dump_agui_event(
296
+ ToolCallChunkEvent(
297
+ tool_call_id=tool_call_id,
298
+ tool_call_name=part.tool_name,
299
+ delta=_stringify_tool_call_args(part.args),
300
+ )
301
+ )
302
+ )
303
+ events.append(dump_agui_event(ToolCallEndEvent(tool_call_id=tool_call_id)))
304
+ return events
305
+ if isinstance(part, ToolReturnPart):
306
+ return [self._tool_result_event(part)]
307
+ if isinstance(part, RetryPromptPart):
308
+ return [
309
+ self._custom_agent_event("retry_prompt_part", stream_event=stream_event, payload=_serialize_value(part))
310
+ ]
311
+ return [self._custom_agent_event("part_end", stream_event=stream_event, payload=_serialize_value(event))]
312
+
313
+ def _adapt_function_tool_result(self, stream_event: StreamEvent) -> list[JsonObject]:
314
+ event = cast(FunctionToolResultEvent | OutputToolResultEvent, stream_event.event)
315
+ part = event.part
316
+ content = event.content if isinstance(event, FunctionToolResultEvent) else None
317
+ if isinstance(part, ToolReturnPart):
318
+ return [self._tool_result_event(part, content=content)]
319
+ if isinstance(part, RetryPromptPart):
320
+ return [
321
+ self._custom_agent_event(
322
+ "retry_prompt_part",
323
+ stream_event=stream_event,
324
+ payload={"part": _serialize_value(part), "content": _serialize_value(content)},
325
+ )
326
+ ]
327
+ return [
328
+ self._custom_agent_event("function_tool_result", stream_event=stream_event, payload=_serialize_value(event))
329
+ ]
330
+
331
+ def _tool_result_event(self, part: ToolReturnPart, *, content: object = None) -> JsonObject:
332
+ tool_call_id = part.tool_call_id
333
+ return dump_agui_event(
334
+ ToolCallResultEvent(
335
+ message_id=f"{tool_call_id}:result",
336
+ tool_call_id=tool_call_id,
337
+ content=_stringify_tool_result(content if content is not None else part.content),
338
+ role="tool",
339
+ )
340
+ )
341
+
342
+ def _ensure_text_cursor(self, agent_id: str, cursor: AgentCursor, index: int) -> PartCursor:
343
+ existing = cursor.parts.get(index)
344
+ if existing is not None:
345
+ return existing
346
+ part_cursor = PartCursor(
347
+ kind="text",
348
+ part_id=self._part_id(agent_id, cursor.loop_index, index, "text"),
349
+ role="assistant",
350
+ )
351
+ cursor.parts[index] = part_cursor
352
+ return part_cursor
353
+
354
+ def _ensure_reasoning_cursor(self, agent_id: str, cursor: AgentCursor, index: int) -> PartCursor:
355
+ existing = cursor.parts.get(index)
356
+ if existing is not None:
357
+ return existing
358
+ part_cursor = PartCursor(
359
+ kind="reasoning",
360
+ part_id=self._part_id(agent_id, cursor.loop_index, index, "reasoning"),
361
+ role="reasoning",
362
+ )
363
+ cursor.parts[index] = part_cursor
364
+ return part_cursor
365
+
366
+ def _ensure_tool_call_cursor(
367
+ self,
368
+ agent_id: str,
369
+ cursor: AgentCursor,
370
+ index: int,
371
+ tool_call_id: str | None,
372
+ ) -> PartCursor:
373
+ existing = cursor.parts.get(index)
374
+ if existing is not None:
375
+ if tool_call_id:
376
+ existing.part_id = tool_call_id
377
+ return existing
378
+ part_cursor = PartCursor(
379
+ kind="tool_call",
380
+ part_id=tool_call_id or self._part_id(agent_id, cursor.loop_index, index, "tool_call"),
381
+ )
382
+ cursor.parts[index] = part_cursor
383
+ return part_cursor
384
+
385
+ def _part_id(self, agent_id: str, loop_index: int, part_index: int, kind: str) -> str:
386
+ return f"{self._run_id}:{agent_id}:{loop_index}:{kind}:{part_index}"
387
+
388
+ def _with_stream_metadata(self, stream_event: StreamEvent, events: list[JsonObject]) -> list[JsonObject]:
389
+ prefix = self._config.stream_metadata_prefix
390
+ if prefix is None:
391
+ return events
392
+ agent_id_key = f"{prefix}AgentId"
393
+ agent_name_key = f"{prefix}AgentName"
394
+ for event in events:
395
+ event[agent_id_key] = stream_event.agent_id
396
+ event[agent_name_key] = stream_event.agent_name
397
+ return events
398
+
399
+ def _custom_agent_event(self, event_name: str, *, stream_event: StreamEvent, payload: object) -> JsonObject:
400
+ return dump_agui_event(
401
+ CustomEvent(
402
+ name=f"{self._config.agent_event_prefix}.{event_name}",
403
+ value={
404
+ "run_id": self._run_id,
405
+ "session_id": self._session_id,
406
+ "agent_id": stream_event.agent_id,
407
+ "agent_name": stream_event.agent_name,
408
+ "payload": _serialize_value(payload),
409
+ },
410
+ )
411
+ )
412
+
413
+
414
+ def _stringify_tool_call_args(value: object) -> str | None:
415
+ if value is None:
416
+ return None
417
+ if isinstance(value, str):
418
+ return value
419
+ return json.dumps(_serialize_value(value), ensure_ascii=False, separators=(",", ":"))
420
+
421
+
422
+ def _stringify_tool_result(value: object) -> str:
423
+ if isinstance(value, str):
424
+ return value
425
+ return json.dumps(_serialize_value(value), ensure_ascii=False)
426
+
427
+
428
+ def _camel_to_snake(value: str) -> str:
429
+ snake = re.sub(r"(?<!^)(?=[A-Z])", "_", value).lower()
430
+ return snake.removesuffix("_event")
431
+
432
+
433
+ def _serialize_value(value: object) -> JsonValue:
434
+ if value is None or isinstance(value, (str, int, float, bool)):
435
+ return value
436
+ if isinstance(value, datetime):
437
+ return value.astimezone(UTC).isoformat()
438
+ if isinstance(value, Path):
439
+ return str(value)
440
+ if isinstance(value, bytes):
441
+ return value.decode("utf-8", errors="replace")
442
+ if isinstance(value, BaseModel):
443
+ return value.model_dump(mode="json") # type: ignore[return-value]
444
+ if is_dataclass(value) and not isinstance(value, type):
445
+ return _serialize_value(asdict(value))
446
+ if isinstance(value, dict):
447
+ return {str(key): _serialize_value(item) for key, item in value.items()}
448
+ if isinstance(value, (list, tuple, set)):
449
+ return [_serialize_value(item) for item in value]
450
+ return str(value)
@@ -0,0 +1,26 @@
1
+ Metadata-Version: 2.4
2
+ Name: ya-agent-stream-protocol
3
+ Version: 0.86.0
4
+ Summary: Shared stream protocol adapters between ya-agent-sdk and applications
5
+ Project-URL: Repository, https://github.com/wh1isper/ya-mono
6
+ Author-email: wh1isper <jizhongsheng957@gmail.com>
7
+ Keywords: agent,agui,protocol,python,stream
8
+ Classifier: Intended Audience :: Developers
9
+ Classifier: Programming Language :: Python
10
+ Classifier: Programming Language :: Python :: 3
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Requires-Python: <3.14,>=3.11
16
+ Requires-Dist: ag-ui-protocol>=0.1.18
17
+ Requires-Dist: pydantic-ai<2,>=1.100.0
18
+ Requires-Dist: pydantic>=2.0.0
19
+ Requires-Dist: ya-agent-sdk==0.86.0
20
+ Description-Content-Type: text/markdown
21
+
22
+ # ya-agent-stream-protocol
23
+
24
+ Shared stream protocol adapters between `ya-agent-sdk` and applications.
25
+
26
+ The package provides AGUI event adaptation, compact replay buffers, message artifact validation, and SSE framing helpers used by YAACLI, YA Claw, and future applications.
@@ -0,0 +1,12 @@
1
+ ya_agent_stream_protocol/__init__.py,sha256=5FEUSelMihbYDj-cPCSvkHdWlppokC-Kd8_kcNi8KKU,133
2
+ ya_agent_stream_protocol/json_types.py,sha256=V8Ifr3FusOMIYBG596Q-4GCBNvQkO56ooh8HRoPkuME,187
3
+ ya_agent_stream_protocol/agui/__init__.py,sha256=7ZbIUV7pWd4nDqBAmIPLDx5SicKarDpULLxySg3R6Is,977
4
+ ya_agent_stream_protocol/agui/events.py,sha256=znWu8OtpjnKUP8J3dsefUQPW3AuxxisqiV-pMWU-L_Y,424
5
+ ya_agent_stream_protocol/agui/replay.py,sha256=IkNzfYG2XG8RWvrJ5iRX3cY9bJPxxqQXBOHifJhKgKM,9046
6
+ ya_agent_stream_protocol/agui/sse.py,sha256=8f8pIvx0QwiMM3b2tMytGeZDYJ09qn5F23otwwEHU34,970
7
+ ya_agent_stream_protocol/agui/validation.py,sha256=lRwQVMpKLKliA5oYuCLQRgIiHx7L6A8g3hQQzUpzqjc,1746
8
+ ya_agent_stream_protocol/sdk/__init__.py,sha256=9_BnDWw-A9MKTSIs8SuQwPAMZ6fbPiO8tO6brTIM__U,143
9
+ ya_agent_stream_protocol/sdk/agui_adapter.py,sha256=8I7yd3B_1GIVPPI2y7OCyaCpFlXZ9rprZyGTSYiGnC0,19084
10
+ ya_agent_stream_protocol-0.86.0.dist-info/METADATA,sha256=w3zRnntriWcz9YV8fry_6fUoqFwMaIA_Am_Gcy_L8qE,1153
11
+ ya_agent_stream_protocol-0.86.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
12
+ ya_agent_stream_protocol-0.86.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any