create-windy 0.3.18 → 0.3.19
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.
- package/dist/cli.js +5 -0
- package/package.json +1 -1
- package/template/.windy-template.json +2 -2
- package/template/apps/agent-server/src/southwind_agent_server/adk_debug.py +38 -5
- package/template/apps/agent-server/tests/test_adk_debug_result_polling.py +139 -0
- package/template/docs/platform/agent-runtime.md +2 -0
package/dist/cli.js
CHANGED
|
@@ -7368,6 +7368,11 @@ var recipes = [
|
|
|
7368
7368
|
id: "starter-0.3.17-to-0.3.18",
|
|
7369
7369
|
from: "0.3.17",
|
|
7370
7370
|
to: "0.3.18"
|
|
7371
|
+
},
|
|
7372
|
+
{
|
|
7373
|
+
id: "starter-0.3.18-to-0.3.19",
|
|
7374
|
+
from: "0.3.18",
|
|
7375
|
+
to: "0.3.19"
|
|
7371
7376
|
}
|
|
7372
7377
|
];
|
|
7373
7378
|
function resolveRecipeChain(sourceVersion, targetVersion) {
|
package/package.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import uuid
|
|
@@ -15,6 +16,8 @@ from .config import normalize_base_url
|
|
|
15
16
|
MAX_QUESTION_LENGTH = 4_000
|
|
16
17
|
MAX_VISIBLE_EVENTS = 64
|
|
17
18
|
MAX_EVENT_FRAME_BYTES = 64 * 1024
|
|
19
|
+
DEFAULT_RESULT_POLL_TIMEOUT_SECONDS = 60.0
|
|
20
|
+
DEFAULT_RESULT_POLL_INTERVAL_SECONDS = 0.5
|
|
18
21
|
|
|
19
22
|
|
|
20
23
|
@dataclass(frozen=True)
|
|
@@ -99,11 +102,19 @@ class CanonicalRunDebugClient:
|
|
|
99
102
|
bearer_token: str,
|
|
100
103
|
*,
|
|
101
104
|
transport: httpx.AsyncBaseTransport | None = None,
|
|
105
|
+
result_poll_timeout_seconds: float = DEFAULT_RESULT_POLL_TIMEOUT_SECONDS,
|
|
106
|
+
result_poll_interval_seconds: float = DEFAULT_RESULT_POLL_INTERVAL_SECONDS,
|
|
102
107
|
) -> None:
|
|
103
108
|
self._operation_key = operation_key
|
|
104
109
|
self._base_url = normalize_base_url(base_url)
|
|
105
110
|
self._bearer_token = bearer_token.strip()
|
|
106
111
|
self._transport = transport
|
|
112
|
+
if not 0 <= result_poll_timeout_seconds <= 120:
|
|
113
|
+
raise ValueError("result_poll_timeout_seconds 必须在 0 到 120 秒之间")
|
|
114
|
+
if not 0 < result_poll_interval_seconds <= 1:
|
|
115
|
+
raise ValueError("result_poll_interval_seconds 必须大于 0 且不超过 1 秒")
|
|
116
|
+
self._result_poll_timeout_seconds = result_poll_timeout_seconds
|
|
117
|
+
self._result_poll_interval_seconds = result_poll_interval_seconds
|
|
107
118
|
if not self._bearer_token:
|
|
108
119
|
raise RuntimeError("ADK_DEBUG_PLATFORM_TOKEN 未配置,拒绝启动真实调试 Run")
|
|
109
120
|
|
|
@@ -142,13 +153,35 @@ class CanonicalRunDebugClient:
|
|
|
142
153
|
)
|
|
143
154
|
run_id = read_run_id(started)
|
|
144
155
|
events = await collect_compact_events(self._events(client, run_id))
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
156
|
+
try:
|
|
157
|
+
async with asyncio.timeout(self._result_poll_timeout_seconds):
|
|
158
|
+
while True:
|
|
159
|
+
result = await client.get(
|
|
160
|
+
f"api/agent/runs/{run_id}/result",
|
|
161
|
+
params={"operationKey": self._operation_key},
|
|
162
|
+
)
|
|
163
|
+
if result.is_success:
|
|
164
|
+
break
|
|
165
|
+
try:
|
|
166
|
+
error = result.json()
|
|
167
|
+
except json.JSONDecodeError:
|
|
168
|
+
error = None
|
|
169
|
+
if (
|
|
170
|
+
result.status_code != 409
|
|
171
|
+
or not isinstance(error, dict)
|
|
172
|
+
or error.get("code") != "RUN_CONFLICT"
|
|
173
|
+
or error.get("retryable") is not True
|
|
174
|
+
):
|
|
175
|
+
raise RuntimeError("平台 Agent Run 永久失败或结果读取失败")
|
|
176
|
+
await asyncio.sleep(self._result_poll_interval_seconds)
|
|
177
|
+
except TimeoutError as error:
|
|
178
|
+
raise RuntimeError("平台 Agent Run 结果等待超时") from error
|
|
179
|
+
answer = read_text_output(result, run_id)
|
|
180
|
+
if not any(event.get("type") == "run.completed" for event in events):
|
|
181
|
+
events = [*events[: MAX_VISIBLE_EVENTS - 1], {"type": "run.completed"}]
|
|
149
182
|
return {
|
|
150
183
|
"runId": run_id,
|
|
151
|
-
"answer":
|
|
184
|
+
"answer": answer,
|
|
152
185
|
"events": events,
|
|
153
186
|
}
|
|
154
187
|
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from southwind_agent_server.adk_debug import CanonicalRunDebugClient
|
|
9
|
+
|
|
10
|
+
OPERATION_KEY = "customer.copilot.global"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async def test_client_waits_for_result_after_stream_closes() -> None:
|
|
14
|
+
result_attempts = 0
|
|
15
|
+
started_runs = 0
|
|
16
|
+
|
|
17
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
18
|
+
nonlocal result_attempts, started_runs
|
|
19
|
+
if request.url.path == "/api/agent/runs":
|
|
20
|
+
started_runs += 1
|
|
21
|
+
return httpx.Response(200, json={"runId": "run_race"})
|
|
22
|
+
if request.url.path.endswith("/events"):
|
|
23
|
+
return httpx.Response(
|
|
24
|
+
200,
|
|
25
|
+
content=(
|
|
26
|
+
b'event: run.started\ndata: {"sequence":1,"type":"run.started"}\n\n'
|
|
27
|
+
),
|
|
28
|
+
)
|
|
29
|
+
if request.url.path.endswith("/result"):
|
|
30
|
+
result_attempts += 1
|
|
31
|
+
if result_attempts < 3:
|
|
32
|
+
return httpx.Response(
|
|
33
|
+
409,
|
|
34
|
+
json={"code": "RUN_CONFLICT", "retryable": True},
|
|
35
|
+
)
|
|
36
|
+
return httpx.Response(
|
|
37
|
+
200,
|
|
38
|
+
json={
|
|
39
|
+
"runId": "run_race",
|
|
40
|
+
"status": "completed",
|
|
41
|
+
"output": {"text": "稍后可读"},
|
|
42
|
+
},
|
|
43
|
+
)
|
|
44
|
+
raise AssertionError(request.url)
|
|
45
|
+
|
|
46
|
+
client = CanonicalRunDebugClient(
|
|
47
|
+
OPERATION_KEY,
|
|
48
|
+
"https://platform.test",
|
|
49
|
+
"developer-token",
|
|
50
|
+
transport=httpx.MockTransport(handler),
|
|
51
|
+
result_poll_interval_seconds=0.001,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
result = await client.run("验证竞态")
|
|
55
|
+
|
|
56
|
+
assert result["answer"] == "稍后可读"
|
|
57
|
+
events = result["events"]
|
|
58
|
+
assert isinstance(events, list) and {"type": "run.completed"} in events
|
|
59
|
+
assert started_runs == 1
|
|
60
|
+
assert result_attempts == 3
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.parametrize(
|
|
64
|
+
("payload", "timeout_seconds", "delay_seconds", "message"),
|
|
65
|
+
[
|
|
66
|
+
(
|
|
67
|
+
{
|
|
68
|
+
"code": "RUN_CANCELLED",
|
|
69
|
+
"message": "secret-upstream-body",
|
|
70
|
+
"retryable": False,
|
|
71
|
+
},
|
|
72
|
+
5,
|
|
73
|
+
0,
|
|
74
|
+
"永久失败",
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
{
|
|
78
|
+
"code": "RUN_CONFLICT",
|
|
79
|
+
"message": "Agent Run 尚未完成",
|
|
80
|
+
"retryable": True,
|
|
81
|
+
},
|
|
82
|
+
0.01,
|
|
83
|
+
0,
|
|
84
|
+
"等待超时",
|
|
85
|
+
),
|
|
86
|
+
(
|
|
87
|
+
{
|
|
88
|
+
"code": "RUN_CONFLICT",
|
|
89
|
+
"message": "slow-secret-upstream-body",
|
|
90
|
+
"retryable": True,
|
|
91
|
+
},
|
|
92
|
+
0.01,
|
|
93
|
+
0.05,
|
|
94
|
+
"等待超时",
|
|
95
|
+
),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
async def test_client_bounds_result_polling_and_hides_error_content(
|
|
99
|
+
payload: dict[str, object],
|
|
100
|
+
timeout_seconds: float,
|
|
101
|
+
delay_seconds: float,
|
|
102
|
+
message: str,
|
|
103
|
+
) -> None:
|
|
104
|
+
requests: list[httpx.Request] = []
|
|
105
|
+
|
|
106
|
+
async def handler(request: httpx.Request) -> httpx.Response:
|
|
107
|
+
requests.append(request)
|
|
108
|
+
if request.url.path == "/api/agent/runs":
|
|
109
|
+
return httpx.Response(200, json={"runId": "run_terminal"})
|
|
110
|
+
if request.url.path.endswith("/events"):
|
|
111
|
+
return httpx.Response(
|
|
112
|
+
200,
|
|
113
|
+
content=(
|
|
114
|
+
b"event: run.completed\ndata: "
|
|
115
|
+
b'{"sequence":1,"type":"run.completed"}\n\n'
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
if request.url.path.endswith("/result"):
|
|
119
|
+
await asyncio.sleep(delay_seconds)
|
|
120
|
+
return httpx.Response(409, json=payload)
|
|
121
|
+
raise AssertionError(request.url)
|
|
122
|
+
|
|
123
|
+
client = CanonicalRunDebugClient(
|
|
124
|
+
OPERATION_KEY,
|
|
125
|
+
"https://platform.test",
|
|
126
|
+
"developer-token",
|
|
127
|
+
transport=httpx.MockTransport(handler),
|
|
128
|
+
result_poll_timeout_seconds=timeout_seconds,
|
|
129
|
+
result_poll_interval_seconds=0.001,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
with pytest.raises(RuntimeError, match=message) as failure:
|
|
133
|
+
await client.run("验证结果失败")
|
|
134
|
+
|
|
135
|
+
error_message = str(failure.value)
|
|
136
|
+
assert "secret-upstream-body" not in error_message
|
|
137
|
+
assert "slow-secret-upstream-body" not in error_message
|
|
138
|
+
assert sum(request.url.path == "/api/agent/runs" for request in requests) == 1
|
|
139
|
+
assert sum(request.url.path.endswith("/result") for request in requests) >= 1
|
|
@@ -240,6 +240,8 @@ Operation Key 与真实开发数据用户注入 Windy-owned `docker-compose.agen
|
|
|
240
240
|
不连接业务 Repository 或 Tool Host;
|
|
241
241
|
- 正式 Server 仍签发 Actor、Scope 和短时 Execution Grant,Agent Server 仍按
|
|
242
242
|
`allowedToolKeys` 执行并审计;
|
|
243
|
+
- SSE 先于结果持久化结束时,Adapter 最多再等待 60 秒并每 500ms 读取同一个 Run
|
|
244
|
+
的结果;只重试明确标记为可重试的 `RUN_CONFLICT`,不会重新创建 Run;
|
|
243
245
|
- 事件摘要最多 64 条,单个 SSE frame 最大 64 KiB;流式 `output.delta` 只显示计数,
|
|
244
246
|
客户端边读边压缩,不先把完整事件流保存在内存。
|
|
245
247
|
|