hostanagent-client 0.2.4__tar.gz

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,16 @@
1
+ .venv/
2
+ .venv-release/
3
+ __pycache__/
4
+ *.py[cod]
5
+ .pytest_cache/
6
+ .ruff_cache/
7
+ *.egg-info/
8
+ dist/
9
+ build/
10
+ .env
11
+ .env.*
12
+ !.env.example
13
+ data/
14
+ *.sqlite*
15
+ .coverage
16
+ htmlcov/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 GeekArt Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,67 @@
1
+ Metadata-Version: 2.4
2
+ Name: hostanagent-client
3
+ Version: 0.2.4
4
+ Summary: Thin async client for the HostAnAgent public protocol
5
+ Project-URL: Repository, https://github.com/ffskyfan/HostAnAgentBackend
6
+ Project-URL: Documentation, https://pypi.org/project/hostanagent-client/
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Python: >=3.10
10
+ Requires-Dist: httpx<1,>=0.28
11
+ Description-Content-Type: text/markdown
12
+
13
+ # hostanagent-client 0.2.4
14
+
15
+ 用于产品后端的独立异步 SDK,Python >=3.10,仅依赖 httpx;不安装 HostAnAgent 服务、Worker 或 aisuite。协议兼容 1.0。0.2.4 为 MIT 许可准备版本,提供 wheel 和源码包,尚未公开发布到 PyPI。本 SDK 及随包文档采用 [MIT](https://opensource.org/license/mit),版权归 GeekArt Inc.;这项授权覆盖 SDK 目录,服务端另行管理。正式发布后,本 README 会随 PyPI 项目页公开;当前元数据中的 PyPI 文档地址尚未上线。
16
+
17
+ ```bash
18
+ python -m pip install ./hostanagent_client-0.2.4-py3-none-any.whl
19
+ ```
20
+
21
+ 在自己的控制台发布 Agent,用服务端应用 Key 连接。平台管理模型供应商凭证。当前代码已整合 D01:创建限制到目标 Agent 的 `runtime:read`、`runtime:write` Key;工具执行额外需要 `tools:execute`。运行发现使用 `await client.bootstrap()`,不需要配置管理权限。部署状态以文档仓库实施记录为准。
22
+
23
+ ```python
24
+ import os
25
+ from hostanagent import AgentClient, HostingError, idempotency_key
26
+
27
+ async def chat(trusted_product_user_id):
28
+ async with AgentClient("https://api.hostanagent.com/v1", os.environ["HAA_APPLICATION_KEY"],
29
+ subject=trusted_product_user_id) as client:
30
+ # Subject 必须来自产品验证过的登录态,不能直接取浏览器提交的 user_id。
31
+ models = await client.models() # reasoning_options 决定可用五档思考强度
32
+ session = await client.create_session()
33
+ key = idempotency_key() # 产品保存本请求的 key、正文和参数,未知结果时原样重试
34
+ try:
35
+ accepted = await client.run(session["id"], os.environ["HAA_AGENT_VERSION"], "整理我的任务",
36
+ idempotency_key=key, reasoning_effort="high", execution_mode="propose",
37
+ limits={"max_cost_usd": "1.000000000000"})
38
+ except HostingError as error:
39
+ if error.code == "INSUFFICIENT_BALANCE":
40
+ # 没有模型调用;当前人工测试入账,在线充值尚未开放。
41
+ return
42
+ raise
43
+ async for event in client.events(accepted["run_id"]):
44
+ print(event["sequence"], event["type"])
45
+ ```
46
+
47
+ `endpoint` 可使用主机根路径或 `/v1`。`run` 支持规范所有现有参数;`create_run(body, idempotency_key=...)` 直接发送完整 RunCreate。
48
+ `bootstrap()` 读取受限运行目录;`agent_runtime(agent_id, version_id=...)` 读取可启动状态和模型元数据;`capabilities/models` 读取能力;`sessions/session/messages/delete_session` 管理授权历史;`snapshot/cancel/add_input/approve/claim/receipt/events` 沿用协议。`create_session` 暴露 history_mode/runtime_mode,但 unsupported 模式仍由服务器拒绝,不能据此承诺 external/ephemeral 已实现。
49
+
50
+ 网络与恢复:
51
+
52
+ - SDK 不自动重试 POST,也不生成隐式新请求。`httpx` 连接异常意味着结果可能未知,应原样重试原 key 或查询已接受 Run;不能换 key 重启付费 Run。
53
+ - `events` 是单次观察订阅,按 UTF-8、CRLF、多行 data 解析,不调用工具 handler。网络断开后先 `snapshot(run_id)` 替换自己的投影,再 `events(run_id, after=snapshot["resume_cursor"])`。用事件 sequence 去重;缺口重新取快照。终态结束重连。401/403 停止恢复并重新核对权限。
54
+ - `cancel(run_id)` 才是明确的停止请求。停止读取 events 或 close 客户端不取消 Agent/业务任务。
55
+ - `add_input` 用同一幂等键重试。claim 的 replayed=true 要读取已有持久回执或由产品核对;绝不再次执行副作用。`receipt_id` 持久保存到确认完成。
56
+ - `HostingError` 提供 code、status_code、request_id、details;不要将内部异常或完整请求正文写入浏览器或日志。USD 原样保持十进制字符串。
57
+ - 恢复已有 session 保持其 agent_version;升级使用新会话。停用/归档由 D05 服务端最终拒绝,不自动换版本或提权。
58
+
59
+ 构建:安装 hatchling 后从干净提交执行 `python scripts/build_sdk.py`;生成 `dist/sdk/0.2.4/` 的 wheel、源码包、来源提交、固定契约摘要、构建工具版本与 SHA-256 清单。目录存在则拒绝覆盖。开发使用 `--candidate`。运行 `python scripts/check_sdk_release.py <产物目录>` 核对文件和包内容;加 `--publish-ready` 会额外拒绝许可证缺失、private 标记和未提交来源。`python scripts/test_sdk_install.py <wheel 或 tar.gz>` 在系统临时目录新建 venv、实际安装并调用,不依赖源码路径。离线依赖可通过 `HAA_WHEELHOUSE` 指定本地 wheelhouse。
60
+
61
+ 0.2.2 已整合 D01/D02/D03/D04/D05,保留固定版本调用方式;新增 `run(session_id, content="文本", agent_id="逻辑 Agent ID", idempotency_key=key)`。`agent_id` 和 `agent_version` 必须二选一。使用逻辑 ID 时由服务端在首次接受 Run 时固定版本,已有 Session 不跟随新发布;停用后原请求仍可按原 key 核对。0.2.2 快照与事件均拒绝非 1.0 协议。旧版 0.2.0/0.2.1/0.2.2 保留,不覆盖原文件。已用本机真实 API 签发的受限测试 Key 验证运行发现、文本 Run、SSE、跨 Agent 拒绝和撤销;模型及邮件均使用 mock。D02 执行器接口可经通用 `request` 调用,参考 `examples/README-executor.md`。
62
+
63
+ 0.2.3 保留上述 SDK API,增加源码包、发布检查和安装矩阵;采用包含 D07 与 durable 执行补齐的协议基线。
64
+
65
+ 0.2.4 补充客户端 MIT 许可;0.2.3 及更早产物保持不变。
66
+
67
+ httpx 及传递依赖由 pip 独立安装,不打入本 wheel。第三方许可见 THIRD_PARTY_NOTICES.md。MIT 许可见 LICENSE;公开发布和真实产品/生产联合验收另行安排。
@@ -0,0 +1,55 @@
1
+ # hostanagent-client 0.2.4
2
+
3
+ 用于产品后端的独立异步 SDK,Python >=3.10,仅依赖 httpx;不安装 HostAnAgent 服务、Worker 或 aisuite。协议兼容 1.0。0.2.4 为 MIT 许可准备版本,提供 wheel 和源码包,尚未公开发布到 PyPI。本 SDK 及随包文档采用 [MIT](https://opensource.org/license/mit),版权归 GeekArt Inc.;这项授权覆盖 SDK 目录,服务端另行管理。正式发布后,本 README 会随 PyPI 项目页公开;当前元数据中的 PyPI 文档地址尚未上线。
4
+
5
+ ```bash
6
+ python -m pip install ./hostanagent_client-0.2.4-py3-none-any.whl
7
+ ```
8
+
9
+ 在自己的控制台发布 Agent,用服务端应用 Key 连接。平台管理模型供应商凭证。当前代码已整合 D01:创建限制到目标 Agent 的 `runtime:read`、`runtime:write` Key;工具执行额外需要 `tools:execute`。运行发现使用 `await client.bootstrap()`,不需要配置管理权限。部署状态以文档仓库实施记录为准。
10
+
11
+ ```python
12
+ import os
13
+ from hostanagent import AgentClient, HostingError, idempotency_key
14
+
15
+ async def chat(trusted_product_user_id):
16
+ async with AgentClient("https://api.hostanagent.com/v1", os.environ["HAA_APPLICATION_KEY"],
17
+ subject=trusted_product_user_id) as client:
18
+ # Subject 必须来自产品验证过的登录态,不能直接取浏览器提交的 user_id。
19
+ models = await client.models() # reasoning_options 决定可用五档思考强度
20
+ session = await client.create_session()
21
+ key = idempotency_key() # 产品保存本请求的 key、正文和参数,未知结果时原样重试
22
+ try:
23
+ accepted = await client.run(session["id"], os.environ["HAA_AGENT_VERSION"], "整理我的任务",
24
+ idempotency_key=key, reasoning_effort="high", execution_mode="propose",
25
+ limits={"max_cost_usd": "1.000000000000"})
26
+ except HostingError as error:
27
+ if error.code == "INSUFFICIENT_BALANCE":
28
+ # 没有模型调用;当前人工测试入账,在线充值尚未开放。
29
+ return
30
+ raise
31
+ async for event in client.events(accepted["run_id"]):
32
+ print(event["sequence"], event["type"])
33
+ ```
34
+
35
+ `endpoint` 可使用主机根路径或 `/v1`。`run` 支持规范所有现有参数;`create_run(body, idempotency_key=...)` 直接发送完整 RunCreate。
36
+ `bootstrap()` 读取受限运行目录;`agent_runtime(agent_id, version_id=...)` 读取可启动状态和模型元数据;`capabilities/models` 读取能力;`sessions/session/messages/delete_session` 管理授权历史;`snapshot/cancel/add_input/approve/claim/receipt/events` 沿用协议。`create_session` 暴露 history_mode/runtime_mode,但 unsupported 模式仍由服务器拒绝,不能据此承诺 external/ephemeral 已实现。
37
+
38
+ 网络与恢复:
39
+
40
+ - SDK 不自动重试 POST,也不生成隐式新请求。`httpx` 连接异常意味着结果可能未知,应原样重试原 key 或查询已接受 Run;不能换 key 重启付费 Run。
41
+ - `events` 是单次观察订阅,按 UTF-8、CRLF、多行 data 解析,不调用工具 handler。网络断开后先 `snapshot(run_id)` 替换自己的投影,再 `events(run_id, after=snapshot["resume_cursor"])`。用事件 sequence 去重;缺口重新取快照。终态结束重连。401/403 停止恢复并重新核对权限。
42
+ - `cancel(run_id)` 才是明确的停止请求。停止读取 events 或 close 客户端不取消 Agent/业务任务。
43
+ - `add_input` 用同一幂等键重试。claim 的 replayed=true 要读取已有持久回执或由产品核对;绝不再次执行副作用。`receipt_id` 持久保存到确认完成。
44
+ - `HostingError` 提供 code、status_code、request_id、details;不要将内部异常或完整请求正文写入浏览器或日志。USD 原样保持十进制字符串。
45
+ - 恢复已有 session 保持其 agent_version;升级使用新会话。停用/归档由 D05 服务端最终拒绝,不自动换版本或提权。
46
+
47
+ 构建:安装 hatchling 后从干净提交执行 `python scripts/build_sdk.py`;生成 `dist/sdk/0.2.4/` 的 wheel、源码包、来源提交、固定契约摘要、构建工具版本与 SHA-256 清单。目录存在则拒绝覆盖。开发使用 `--candidate`。运行 `python scripts/check_sdk_release.py <产物目录>` 核对文件和包内容;加 `--publish-ready` 会额外拒绝许可证缺失、private 标记和未提交来源。`python scripts/test_sdk_install.py <wheel 或 tar.gz>` 在系统临时目录新建 venv、实际安装并调用,不依赖源码路径。离线依赖可通过 `HAA_WHEELHOUSE` 指定本地 wheelhouse。
48
+
49
+ 0.2.2 已整合 D01/D02/D03/D04/D05,保留固定版本调用方式;新增 `run(session_id, content="文本", agent_id="逻辑 Agent ID", idempotency_key=key)`。`agent_id` 和 `agent_version` 必须二选一。使用逻辑 ID 时由服务端在首次接受 Run 时固定版本,已有 Session 不跟随新发布;停用后原请求仍可按原 key 核对。0.2.2 快照与事件均拒绝非 1.0 协议。旧版 0.2.0/0.2.1/0.2.2 保留,不覆盖原文件。已用本机真实 API 签发的受限测试 Key 验证运行发现、文本 Run、SSE、跨 Agent 拒绝和撤销;模型及邮件均使用 mock。D02 执行器接口可经通用 `request` 调用,参考 `examples/README-executor.md`。
50
+
51
+ 0.2.3 保留上述 SDK API,增加源码包、发布检查和安装矩阵;采用包含 D07 与 durable 执行补齐的协议基线。
52
+
53
+ 0.2.4 补充客户端 MIT 许可;0.2.3 及更早产物保持不变。
54
+
55
+ httpx 及传递依赖由 pip 独立安装,不打入本 wheel。第三方许可见 THIRD_PARTY_NOTICES.md。MIT 许可见 LICENSE;公开发布和真实产品/生产联合验收另行安排。
@@ -0,0 +1,7 @@
1
+ # 许可与第三方依赖
2
+
3
+ 本 SDK 及随包文档采用 MIT,Copyright (c) 2026 GeekArt Inc.,完整文本见 LICENSE。第三方依赖继续适用各自的许可证。
4
+
5
+ 本 wheel 只含 HostAnAgent SDK 代码。运行依赖 httpx(BSD-3-Clause),由 pip 独立安装,未内嵌。传递依赖包括 httpcore(BSD-3-Clause)、anyio(MIT)、certifi(MPL-2.0)、idna(BSD-3-Clause)、h11(MIT)以及按环境需要的 typing_extensions(PSF-2.0)。以实际安装版本的 dist-info/licenses 和元数据为准,测试脚本记录实际依赖列表;若打包整个环境,需一并保留这些许可。
6
+
7
+ 构建使用 hatchling(MIT),不属于发布 wheel 的运行依赖。
@@ -0,0 +1,28 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.27,<2"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hostanagent-client"
7
+ version = "0.2.4"
8
+ description = "Thin async client for the HostAnAgent public protocol"
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ dependencies = ["httpx>=0.28,<1"]
12
+ license = "MIT"
13
+ license-files = ["LICENSE"]
14
+
15
+ [project.urls]
16
+ Repository = "https://github.com/ffskyfan/HostAnAgentBackend"
17
+ Documentation = "https://pypi.org/project/hostanagent-client/"
18
+
19
+ [tool.hatch.build.targets.wheel]
20
+ packages = ["src/hostanagent"]
21
+ core-metadata-version = "2.4"
22
+
23
+ [tool.hatch.build.targets.wheel.force-include]
24
+ "THIRD_PARTY_NOTICES.md" = "hostanagent/THIRD_PARTY_NOTICES.md"
25
+
26
+ [tool.hatch.build.targets.sdist]
27
+ core-metadata-version = "2.4"
28
+ include = ["/src/hostanagent", "/pyproject.toml", "/README.md", "/THIRD_PARTY_NOTICES.md", "/LICENSE"]
@@ -0,0 +1,170 @@
1
+ import json
2
+ import uuid
3
+ from urllib.parse import quote
4
+
5
+ import httpx
6
+
7
+
8
+ class HostingError(Exception):
9
+ def __init__(self, code, message, status_code, request_id=None, details=None, retryable=False):
10
+ self.code, self.status_code, self.request_id = code, status_code, request_id
11
+ self.details = details or {}
12
+ self.retryable = retryable
13
+ super().__init__(message)
14
+
15
+
16
+ class AgentClient:
17
+ """Server-side only. The application authenticates its user before setting subject."""
18
+
19
+ def __init__(self, endpoint, application_key, subject, *, transport=None):
20
+ base = endpoint.rstrip("/")
21
+ if not base.endswith("/v1"):
22
+ base += "/v1"
23
+ self.http = httpx.AsyncClient(base_url=base + "/", timeout=30,
24
+ transport=transport, headers={"Authorization": f"Bearer {application_key}", "X-Subject": subject})
25
+
26
+ async def __aenter__(self):
27
+ return self
28
+
29
+ async def __aexit__(self, *_):
30
+ await self.close()
31
+
32
+ async def close(self):
33
+ await self.http.aclose()
34
+
35
+ @staticmethod
36
+ def check(response):
37
+ if response.is_error:
38
+ try:
39
+ error = response.json()
40
+ except ValueError:
41
+ error = {}
42
+ if not isinstance(error, dict):
43
+ error = {}
44
+ raise HostingError(error.get("code", "HTTP_ERROR"), error.get("message", "Hosting request failed"),
45
+ response.status_code,
46
+ error.get("request_id") or response.headers.get("x-request-id"),
47
+ error.get("details"), error.get("retryable", False))
48
+
49
+ async def request(self, method, path, *, body=None, headers=None):
50
+ response = await self.http.request(method, path, json=body, headers=headers)
51
+ self.check(response)
52
+ return response.json() if response.content else None
53
+
54
+ async def capabilities(self):
55
+ return await self.request("GET", "capabilities")
56
+
57
+ async def models(self):
58
+ return await self.request("GET", "models")
59
+
60
+ async def bootstrap(self):
61
+ """Discover only the Agents allowed by this runtime credential."""
62
+ return await self.request("GET", "runtime/bootstrap")
63
+
64
+ async def agent_runtime(self, agent_id, *, version_id=None):
65
+ """Read lifecycle/model metadata without configuration permissions."""
66
+ path = f"agents/{quote(agent_id, safe='')}/runtime"
67
+ if version_id is not None:
68
+ path += f"?version_id={quote(version_id, safe='')}"
69
+ return await self.request("GET", path)
70
+
71
+ async def create_session(self, title="新会话", *, history_mode="managed", runtime_mode="durable"):
72
+ return await self.request("POST", "sessions", body={
73
+ "title": title, "history_mode": history_mode, "runtime_mode": runtime_mode,
74
+ })
75
+
76
+ async def sessions(self):
77
+ return await self.request("GET", "sessions")
78
+
79
+ async def session(self, session_id):
80
+ return await self.request("GET", f"sessions/{quote(session_id, safe='')}")
81
+
82
+ async def messages(self, session_id, *, before=None, limit=50):
83
+ path = f"sessions/{quote(session_id, safe='')}/messages?limit={int(limit)}"
84
+ if before is not None:
85
+ path += f"&before={int(before)}"
86
+ return await self.request("GET", path)
87
+
88
+ async def delete_session(self, session_id):
89
+ return await self.request("DELETE", f"sessions/{quote(session_id, safe='')}")
90
+
91
+ async def create_run(self, body, *, idempotency_key):
92
+ """Send the complete protocol RunCreate. No implicit retries or key generation."""
93
+ return await self.request("POST", "runs", body=body,
94
+ headers={"Idempotency-Key": idempotency_key})
95
+
96
+ async def run(self, session_id, agent_version=None, content=None, *, agent_id=None,
97
+ idempotency_key, context_refs=None, execution_mode="propose", limits=None,
98
+ reasoning_effort=None):
99
+ """Select a fixed version or let Hosting pin agent_id on the first accepted Run."""
100
+ if bool(agent_version) == bool(agent_id):
101
+ raise ValueError("Provide exactly one of agent_version or agent_id")
102
+ if content is None:
103
+ raise ValueError("Message content is required")
104
+ selection = {"agent_version": agent_version} if agent_version else {"agent_id": agent_id}
105
+ body = {"session_id": session_id, **selection,
106
+ "input": {"type": "message", "content": content}, "context_refs": context_refs or [],
107
+ "execution_mode": execution_mode}
108
+ if limits is not None:
109
+ body["limits"] = limits
110
+ if reasoning_effort is not None:
111
+ body["reasoning_effort"] = reasoning_effort
112
+ return await self.create_run(body, idempotency_key=idempotency_key)
113
+
114
+ async def snapshot(self, run_id):
115
+ snapshot = await self.request("GET", f"runs/{quote(run_id, safe='')}")
116
+ if not isinstance(snapshot, dict) or snapshot.get("protocol_version") != "1.0":
117
+ raise HostingError("PROTOCOL_UNSUPPORTED", "Unsupported snapshot protocol", 0)
118
+ return snapshot
119
+
120
+ async def cancel(self, run_id):
121
+ return await self.request("POST", f"runs/{quote(run_id, safe='')}/cancel")
122
+
123
+ async def add_input(self, run_id, content, *, idempotency_key):
124
+ return await self.request("POST", f"runs/{quote(run_id, safe='')}/inputs", body={"content": content},
125
+ headers={"Idempotency-Key": idempotency_key})
126
+
127
+ async def approve(self, run_id, call, decision):
128
+ return await self.request("POST", f"runs/{quote(run_id, safe='')}/approvals", body={
129
+ "approval_id": call["approval_id"], "tool_call_id": call["id"],
130
+ "arguments_digest": call["arguments_digest"], "decision": decision,
131
+ })
132
+
133
+ async def claim(self, run_id, call_id, executor_id):
134
+ return await self.request("POST", f"runs/{quote(run_id, safe='')}/tool-calls/{quote(call_id, safe='')}/claim",
135
+ body={"executor_id": executor_id})
136
+
137
+ async def receipt(self, run_id, call_id, grant, *, receipt_id, kind, **value):
138
+ return await self.request("POST", f"runs/{quote(run_id, safe='')}/tool-calls/{quote(call_id, safe='')}/receipts",
139
+ body={"receipt_id": receipt_id, "kind": kind, **value}, headers={"X-Tool-Grant": grant})
140
+
141
+ async def events(self, run_id, *, after=0):
142
+ """One SSE subscription. On reconnect fetch snapshot, then pass its resume_cursor."""
143
+ async with self.http.stream("GET", f"runs/{quote(run_id, safe='')}/events",
144
+ params={"after": after}, headers={"Accept": "text/event-stream"},
145
+ timeout=None) as response:
146
+ if response.is_error:
147
+ await response.aread()
148
+ self.check(response)
149
+ data = []
150
+ size = 0
151
+ async for line in response.aiter_lines():
152
+ size += len(line)
153
+ if size > 1048576:
154
+ raise HostingError("EVENT_TOO_LARGE", "Event exceeds frame limit", 0)
155
+ if not line:
156
+ if data:
157
+ event = json.loads("\n".join(data))
158
+ if not isinstance(event, dict) or event.get("protocol_version") != "1.0":
159
+ raise HostingError("PROTOCOL_UNSUPPORTED", "Unsupported event protocol", 0)
160
+ yield event
161
+ data, size = [], 0
162
+ elif line.startswith("data:"):
163
+ data.append(line[5:].lstrip())
164
+
165
+
166
+ def idempotency_key():
167
+ return str(uuid.uuid4())
168
+
169
+
170
+ __all__ = ["AgentClient", "HostingError", "idempotency_key"]