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 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,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.3.18",
3
+ "version": "0.3.19",
4
4
  "description": "创建单组织、单租户、私有部署优先的 Windy 企业 Dashboard Starter",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "templateCommit": "f1470acc1588deb087d3144a76b0729affed0988",
4
- "generatorVersion": "0.3.18",
3
+ "templateCommit": "45fdbe8e2a55dff118933af58a60976e9bfe318c",
4
+ "generatorVersion": "0.3.19",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -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
- result = await client.get(
146
- f"api/agent/runs/{run_id}/result",
147
- params={"operationKey": self._operation_key},
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": read_text_output(result, run_id),
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