create-windy 0.4.0 → 0.5.0

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 (194) hide show
  1. package/dist/cli.js +263 -121
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/apps/agent-server/adk_web_agents/canonical_debug/agent.py +29 -9
  5. package/template/apps/agent-server/adk_web_agents/services.py +25 -0
  6. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +40 -11
  7. package/template/apps/agent-server/src/southwind_agent_server/adk_debug_sessions.py +325 -0
  8. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +70 -6
  9. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +1 -1
  10. package/template/apps/agent-server/src/southwind_agent_server/app.py +16 -4
  11. package/template/apps/agent-server/src/southwind_agent_server/approval_resolution.py +30 -0
  12. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +9 -0
  13. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +2 -1
  14. package/template/apps/agent-server/src/southwind_agent_server/engine.py +3 -1
  15. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +95 -130
  16. package/template/apps/agent-server/src/southwind_agent_server/openai_engine_validation.py +133 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +89 -20
  18. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_insert.py +34 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_queries.py +234 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_replay.py +44 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_status.py +83 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_store.py +331 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/postgres_report_summary.py +304 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/report_lifecycle.py +217 -0
  25. package/template/apps/agent-server/src/southwind_agent_server/report_metrics.py +149 -0
  26. package/template/apps/agent-server/src/southwind_agent_server/report_presenter.py +280 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/report_routes.py +126 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/report_store.py +344 -0
  29. package/template/apps/agent-server/src/southwind_agent_server/report_types.py +111 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +31 -2
  31. package/template/apps/agent-server/src/southwind_agent_server/service.py +56 -51
  32. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +18 -1
  33. package/template/apps/agent-server/tests/test_adk_adapter.py +7 -9
  34. package/template/apps/agent-server/tests/test_adk_debug_sessions.py +215 -0
  35. package/template/apps/agent-server/tests/test_adk_web_canonical_debug.py +48 -1
  36. package/template/apps/agent-server/tests/test_openai_provider.py +58 -6
  37. package/template/apps/agent-server/tests/test_postgres_report_status.py +59 -0
  38. package/template/apps/agent-server/tests/test_postgres_report_summary.py +298 -0
  39. package/template/apps/agent-server/tests/test_provider_attempt_reporting.py +152 -0
  40. package/template/apps/agent-server/tests/test_report_attempt_lifecycle.py +126 -0
  41. package/template/apps/agent-server/tests/test_report_resilience.py +181 -0
  42. package/template/apps/agent-server/tests/test_report_store.py +286 -0
  43. package/template/apps/agent-server/tests/test_report_summary_parity.py +115 -0
  44. package/template/apps/agent-server/tests/test_report_terminal_sync.py +177 -0
  45. package/template/apps/server/src/agent/debug-session-facade-provider-failure.test.ts +269 -0
  46. package/template/apps/server/src/agent/debug-session-facade.test.ts +335 -0
  47. package/template/apps/server/src/agent/debug-session-facade.ts +302 -0
  48. package/template/apps/server/src/agent/debug-session-id.test.ts +37 -0
  49. package/template/apps/server/src/agent/debug-session-id.ts +37 -0
  50. package/template/apps/server/src/agent/debug-session-provider-reader.ts +54 -0
  51. package/template/apps/server/src/agent/debug-session-routes.test.ts +64 -0
  52. package/template/apps/server/src/agent/debug-session-routes.ts +73 -0
  53. package/template/apps/server/src/agent/debug-session-validation.ts +106 -0
  54. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +2 -0
  55. package/template/apps/server/src/agent/provider-input-governance.test.ts +18 -0
  56. package/template/apps/server/src/agent/provider-input-governance.ts +37 -1
  57. package/template/apps/server/src/agent/remote-runtime.test.ts +7 -0
  58. package/template/apps/server/src/agent/remote-runtime.ts +1 -0
  59. package/template/apps/server/src/agent/run-authorization.test.ts +98 -0
  60. package/template/apps/server/src/agent/run-authorization.ts +32 -1
  61. package/template/apps/server/src/agent/run-binding-repository.ts +1 -0
  62. package/template/apps/server/src/agent/run-contracts.ts +5 -0
  63. package/template/apps/server/src/agent/run-facade-adk-debug.test.ts +39 -0
  64. package/template/apps/server/src/agent/run-facade.test.ts +78 -0
  65. package/template/apps/server/src/agent/run-facade.ts +33 -3
  66. package/template/apps/server/src/agent/run-input-commitment.ts +50 -0
  67. package/template/apps/server/src/agent/run-input-verification.ts +41 -0
  68. package/template/apps/server/src/agent/runtime-bootstrap-report.test.ts +136 -0
  69. package/template/apps/server/src/agent/runtime-bootstrap.ts +66 -1
  70. package/template/apps/server/src/agent/tool-host.test.ts +1 -0
  71. package/template/apps/server/src/agent/unavailable-routes.ts +4 -0
  72. package/template/apps/server/src/ai-runtime-report/actor-directory.ts +94 -0
  73. package/template/apps/server/src/ai-runtime-report/actor-handle.test.ts +27 -0
  74. package/template/apps/server/src/ai-runtime-report/actor-handle.ts +81 -0
  75. package/template/apps/server/src/ai-runtime-report/contracts.ts +91 -0
  76. package/template/apps/server/src/ai-runtime-report/remote-parser.ts +212 -0
  77. package/template/apps/server/src/ai-runtime-report/remote-port.ts +138 -0
  78. package/template/apps/server/src/ai-runtime-report/routes.ts +172 -0
  79. package/template/apps/server/src/ai-runtime-report/scope.ts +63 -0
  80. package/template/apps/server/src/ai-runtime-report/service.test.ts +271 -0
  81. package/template/apps/server/src/ai-runtime-report/service.ts +206 -0
  82. package/template/apps/server/src/license/runtime-audit-reliability.test.ts +2 -0
  83. package/template/apps/server/src/license/runtime-service.test.ts +6 -0
  84. package/template/apps/server/src/module-host/ai-operation-port.test.ts +63 -0
  85. package/template/apps/server/src/module-host/ai-operation-port.ts +30 -5
  86. package/template/apps/server/src/module-host/debug-session-provider-registration.test.ts +162 -0
  87. package/template/apps/server/src/module-host/debug-session-provider-registration.ts +84 -0
  88. package/template/apps/server/src/module-host/initialize-contracts.ts +7 -0
  89. package/template/apps/server/src/module-host/initialize.ts +6 -197
  90. package/template/apps/server/src/module-host/request-context.ts +1 -0
  91. package/template/apps/server/src/module-host/scoped-host.ts +214 -0
  92. package/template/apps/server/src/module-host.ts +1 -0
  93. package/template/apps/server/src/runtime-adk-debug.test.ts +25 -1
  94. package/template/apps/server/src/runtime-adk-debug.ts +8 -2
  95. package/template/apps/server/src/settings/drizzle-watermark-repository.integration.test.ts +4 -0
  96. package/template/apps/server/src/settings/drizzle-watermark-repository.ts +11 -3
  97. package/template/apps/server/src/settings/watermark-routes.test.ts +54 -1
  98. package/template/apps/server/src/settings/watermark-routes.ts +29 -1
  99. package/template/apps/server/src/system/operations.test.ts +54 -0
  100. package/template/apps/server/src/system/operations.ts +23 -1
  101. package/template/apps/server/src/system/seed.test.ts +4 -0
  102. package/template/apps/server/src/system/system-resource-monitoring.test.ts +82 -0
  103. package/template/apps/server/src/system/system-resource-monitoring.ts +300 -0
  104. package/template/apps/web/src/composables/useWatermarkSettings.webtest.ts +14 -0
  105. package/template/apps/web/src/composables/watermark-settings.ts +40 -11
  106. package/template/apps/web/src/composables/watermark-settings.webtest.ts +19 -6
  107. package/template/apps/web/src/layout/AppSidebar.vue +6 -1
  108. package/template/apps/web/src/layout/AppSidebar.webtest.ts +2 -0
  109. package/template/apps/web/src/layout/GlobalWatermark.layering.webtest.ts +2 -0
  110. package/template/apps/web/src/layout/GlobalWatermark.vue +4 -8
  111. package/template/apps/web/src/layout/GlobalWatermark.webtest.ts +12 -5
  112. package/template/apps/web/src/pages/auth/LoginPage.vue +6 -1
  113. package/template/apps/web/src/pages/settings/BrandingSettingsSection.vue +20 -3
  114. package/template/apps/web/src/pages/settings/BrandingSettingsSection.webtest.ts +80 -0
  115. package/template/apps/web/src/pages/settings/WatermarkPreview.vue +72 -0
  116. package/template/apps/web/src/pages/settings/WatermarkPreview.webtest.ts +55 -0
  117. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.vue +80 -24
  118. package/template/apps/web/src/pages/settings/WatermarkSettingsSection.webtest.ts +115 -0
  119. package/template/apps/web/src/pages/system/AiRuntimePage.vue +129 -0
  120. package/template/apps/web/src/pages/system/SystemOperationsPage.vue +2 -1
  121. package/template/apps/web/src/pages/system/ai-runtime-presenter.ts +60 -0
  122. package/template/apps/web/src/pages/system/ai-runtime-presenter.webtest.ts +48 -0
  123. package/template/apps/web/src/pages/system/audit-time-range.ts +34 -2
  124. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +4 -3
  125. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +26 -1
  126. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.vue +98 -0
  127. package/template/apps/web/src/pages/system/components/OperationGpuMonitor.webtest.ts +61 -0
  128. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.vue +53 -0
  129. package/template/apps/web/src/pages/system/components/OperationServiceMonitor.webtest.ts +20 -1
  130. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +14 -1
  131. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeActivityChart.vue +85 -0
  132. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeCharts.webtest.ts +82 -0
  133. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.vue +98 -0
  134. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeFilters.webtest.ts +27 -0
  135. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunDetailDialog.vue +168 -0
  136. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeRunTable.vue +130 -0
  137. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.vue +82 -0
  138. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeSummaryCards.webtest.ts +102 -0
  139. package/template/apps/web/src/pages/system/components/ai-runtime/AiRuntimeTokenByModelChart.vue +101 -0
  140. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.ts +185 -0
  141. package/template/apps/web/src/pages/system/composables/useAiRuntimeReport.webtest.ts +158 -0
  142. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +21 -7
  143. package/template/apps/web/src/router/error-navigation.ts +22 -0
  144. package/template/apps/web/src/router/index.ts +12 -23
  145. package/template/apps/web/src/router/platform-admin-route-bindings.ts +4 -0
  146. package/template/apps/web/src/services/ai-runtime-api.ts +164 -0
  147. package/template/apps/web/src/services/ai-runtime-api.webtest.ts +111 -0
  148. package/template/apps/web/src/services/operations-api.ts +28 -0
  149. package/template/docker-compose.agent-debug.yml +1 -1
  150. package/template/docs/architecture/ai-runtime.md +2 -1
  151. package/template/docs/architecture/module-host.md +3 -0
  152. package/template/docs/platform/agent-run-attestation.md +65 -0
  153. package/template/docs/platform/agent-runtime-observability.md +135 -0
  154. package/template/docs/platform/agent-runtime.md +47 -3
  155. package/template/docs/platform/operations-overview.md +13 -0
  156. package/template/docs/platform/platform-authentication.md +4 -3
  157. package/template/docs/platform/platform-shell-settings.md +15 -10
  158. package/template/docs/platform/web-system-crud.md +1 -1
  159. package/template/packages/config/package.json +1 -1
  160. package/template/packages/database/drizzle/0044_sweet_tarantula.sql +6 -0
  161. package/template/packages/database/drizzle/0045_tiny_cassandra_nova.sql +65 -0
  162. package/template/packages/database/drizzle/0046_tranquil_living_mummy.sql +6 -0
  163. package/template/packages/database/drizzle/0047_oval_luke_cage.sql +18 -0
  164. package/template/packages/database/drizzle/meta/0044_snapshot.json +8349 -0
  165. package/template/packages/database/drizzle/meta/0045_snapshot.json +8814 -0
  166. package/template/packages/database/drizzle/meta/0046_snapshot.json +8820 -0
  167. package/template/packages/database/drizzle/meta/0047_snapshot.json +8835 -0
  168. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  169. package/template/packages/database/package.json +1 -1
  170. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  171. package/template/packages/database/src/schema/agent-runtime.ts +114 -0
  172. package/template/packages/database/src/schema/platform-settings.ts +3 -0
  173. package/template/packages/jobs/package.json +1 -1
  174. package/template/packages/modules/package.json +1 -1
  175. package/template/packages/modules/src/system-admin-routes.ts +10 -0
  176. package/template/packages/modules/src/system-admin-ui-catalog.ts +1 -1
  177. package/template/packages/modules/src/system-api-control-plane.ts +17 -1
  178. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  179. package/template/packages/modules/src/system-features.ts +8 -0
  180. package/template/packages/modules/src/system-module-catalog.test.ts +35 -0
  181. package/template/packages/modules/src/system-module-catalog.ts +15 -4
  182. package/template/packages/modules/src/system-modules-access.test.ts +69 -0
  183. package/template/packages/modules/src/system-modules.ts +13 -0
  184. package/template/packages/modules/src/system-permissions.ts +2 -0
  185. package/template/packages/server-sdk/index.ts +11 -0
  186. package/template/packages/server-sdk/package.json +2 -1
  187. package/template/packages/server-sdk/src/ai-operation-port.ts +5 -0
  188. package/template/packages/server-sdk/src/debug-session-provider.ts +63 -0
  189. package/template/packages/server-sdk/src/module-host.ts +5 -0
  190. package/template/packages/shared/index.ts +3 -0
  191. package/template/packages/shared/package.json +1 -1
  192. package/template/packages/shared/src/ai-runtime-report.ts +134 -0
  193. package/template/packages/shared/src/platform-settings.ts +17 -0
  194. package/template/scripts/lib/postgres-regression.ts +201 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-windy",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
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": "50d32b9470dbf55110aba320338f5e54ed896c77",
4
- "generatorVersion": "0.4.0",
3
+ "templateCommit": "647dac095fa843d7c6eaa3e734d3fbf0d80d81d8",
4
+ "generatorVersion": "0.5.0",
5
5
  "modules": [
6
6
  {
7
7
  "name": "system",
@@ -8,6 +8,7 @@ from google.adk.agents import LlmAgent
8
8
  from google.adk.models.base_llm import BaseLlm
9
9
  from google.adk.models.llm_request import LlmRequest
10
10
  from google.adk.models.llm_response import LlmResponse
11
+ from google.adk.tools.tool_context import ToolContext
11
12
  from google.genai import types
12
13
 
13
14
  from southwind_agent_server.adk_debug import (
@@ -15,13 +16,20 @@ from southwind_agent_server.adk_debug import (
15
16
  TextOperationDebugConfig,
16
17
  load_text_operation_debug_config,
17
18
  )
19
+ from southwind_agent_server.adk_debug_sessions import (
20
+ IMPORTED_SESSION_STATE_KEY,
21
+ ImportedDebugSessionReadOnlyError,
22
+ )
18
23
 
19
24
  TOOL_NAME = "run_canonical_operation"
20
25
  PROJECT_OPERATION = load_text_operation_debug_config()
21
26
 
22
27
 
23
- async def run_canonical_operation(question: str) -> dict[str, object]:
28
+ async def run_canonical_operation(
29
+ question: str, tool_context: ToolContext
30
+ ) -> dict[str, object]:
24
31
  """Run the project-selected text Operation through canonical platform APIs."""
32
+ ensure_debug_session_writable(tool_context.state.get(IMPORTED_SESSION_STATE_KEY))
25
33
  if PROJECT_OPERATION is None:
26
34
  raise RuntimeError("项目尚未选择可调试的文本 Operation")
27
35
  return await CanonicalRunDebugClient.from_environment(
@@ -29,6 +37,13 @@ async def run_canonical_operation(question: str) -> dict[str, object]:
29
37
  ).run(question)
30
38
 
31
39
 
40
+ def ensure_debug_session_writable(imported_marker: object) -> None:
41
+ if imported_marker is True:
42
+ raise ImportedDebugSessionReadOnlyError(
43
+ "导入的业务会话是实时只读投影;请新建本地会话继续提问"
44
+ )
45
+
46
+
32
47
  class CanonicalDebugLlm(BaseLlm):
33
48
  """ADK dispatcher; real model and Tool execution stay in canonical Run."""
34
49
 
@@ -42,7 +57,8 @@ class CanonicalDebugLlm(BaseLlm):
42
57
  stream: bool = False,
43
58
  ) -> AsyncGenerator[LlmResponse, None]:
44
59
  del stream
45
- tool_response = latest_tool_response(llm_request)
60
+ question, user_turn_index = latest_user_turn(llm_request)
61
+ tool_response = latest_tool_response(llm_request, user_turn_index)
46
62
  if tool_response is not None:
47
63
  yield text_response(render_result(tool_response))
48
64
  return
@@ -52,7 +68,6 @@ class CanonicalDebugLlm(BaseLlm):
52
68
  "选择真实文本 Operation。"
53
69
  )
54
70
  return
55
- question = latest_user_text(llm_request)
56
71
  if not question:
57
72
  yield text_response("请输入要交给真实 Agent Runtime 调试的业务问题。")
58
73
  return
@@ -72,18 +87,23 @@ class CanonicalDebugLlm(BaseLlm):
72
87
  )
73
88
 
74
89
 
75
- def latest_user_text(request: LlmRequest) -> str:
76
- for content in reversed(request.contents):
90
+ def latest_user_turn(request: LlmRequest) -> tuple[str, int]:
91
+ for index in range(len(request.contents) - 1, -1, -1):
92
+ content = request.contents[index]
77
93
  if content.role != "user":
78
94
  continue
79
95
  text = "".join(part.text or "" for part in content.parts or [])
80
96
  if text.strip():
81
- return text.strip()
82
- return ""
97
+ return text.strip(), index
98
+ return "", -1
83
99
 
84
100
 
85
- def latest_tool_response(request: LlmRequest) -> dict[str, object] | None:
86
- for content in reversed(request.contents):
101
+ def latest_tool_response(
102
+ request: LlmRequest, user_turn_index: int
103
+ ) -> dict[str, object] | None:
104
+ if user_turn_index < 0:
105
+ return None
106
+ for content in reversed(request.contents[user_turn_index + 1 :]):
87
107
  for part in content.parts or []:
88
108
  response = part.function_response
89
109
  if response and response.name == TOOL_NAME:
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+ from google.adk.cli.service_registry import get_service_registry
6
+
7
+ from southwind_agent_server.adk_debug_sessions import (
8
+ WindyDebugSessionClientV1,
9
+ WindyDebugSessionServiceV1,
10
+ )
11
+
12
+ SESSION_SERVICE_URI = "windy-debug://v1"
13
+
14
+
15
+ def create_windy_debug_session_service(
16
+ uri: str, **_kwargs: Any
17
+ ) -> WindyDebugSessionServiceV1:
18
+ if uri != SESSION_SERVICE_URI:
19
+ raise ValueError("Windy ADK debug session URI 版本不受支持")
20
+ return WindyDebugSessionServiceV1(WindyDebugSessionClientV1.from_environment())
21
+
22
+
23
+ get_service_registry().register_session_service(
24
+ "windy-debug", create_windy_debug_session_service
25
+ )
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  from collections.abc import AsyncIterator
5
+ from typing import cast
5
6
 
6
7
  from google.adk.agents import LlmAgent
7
8
  from google.adk.agents.run_config import RunConfig, StreamingMode
@@ -13,7 +14,6 @@ from google.genai import types
13
14
  from .adk_failure import AdkFailureState
14
15
  from .adk_model import GovernedOpenAiLlm
15
16
  from .adk_tools import ApprovalSignalState, GovernedTool
16
- from .contracts import JsonValue
17
17
  from .engine import (
18
18
  AgentEngineAdapter,
19
19
  EngineCompleted,
@@ -26,9 +26,11 @@ from .engine import (
26
26
  UsageReported,
27
27
  )
28
28
  from .multimodal import final_output
29
- from .openai_engine import cancelled, validate_runtime_request
29
+ from .openai_engine_validation import cancelled, validate_runtime_request
30
30
  from .openai_provider import OpenAiCompatibleProvider
31
31
  from .provider_governance import ProviderRequestGovernance
32
+ from .report_store import AgentRuntimeReportStore
33
+ from .report_types import CacheStatus, UsageFacts, UsageSource
32
34
  from .tool_host import ToolHostClient, canonical_tool_call_id
33
35
 
34
36
 
@@ -40,10 +42,12 @@ class AdkEngineAdapter(AgentEngineAdapter):
40
42
  provider: OpenAiCompatibleProvider | None = None,
41
43
  tool_host: ToolHostClient | None = None,
42
44
  governance: ProviderRequestGovernance | None = None,
45
+ report_store: AgentRuntimeReportStore | None = None,
43
46
  ) -> None:
44
47
  self._provider = provider
45
48
  self._tool_host = tool_host
46
49
  self._governance = governance
50
+ self._report_store = report_store
47
51
  self._cancellations: dict[str, asyncio.Event] = {}
48
52
 
49
53
  @property
@@ -95,6 +99,7 @@ class AdkEngineAdapter(AgentEngineAdapter):
95
99
  governance,
96
100
  cancellation,
97
101
  failures,
102
+ self._report_store,
98
103
  ),
99
104
  instruction=request.instructions,
100
105
  tools=[
@@ -241,21 +246,45 @@ async def signals_for_event(
241
246
  if part.text and not part.thought:
242
247
  yield OutputDelta(part.text)
243
248
  usage = southwind_usage(event)
244
- if usage is not None:
245
- yield UsageReported(usage)
249
+ attempt_sequence = southwind_attempt_sequence(event)
250
+ if usage is not None and attempt_sequence is not None:
251
+ yield UsageReported(attempt_sequence, usage)
246
252
 
247
253
 
248
- def southwind_usage(event: Event) -> dict[str, JsonValue] | None:
254
+ def southwind_usage(event: Event) -> UsageFacts | None:
249
255
  metadata = event.custom_metadata or {}
250
256
  value = metadata.get("southwindUsage")
251
257
  if not isinstance(value, dict):
252
258
  return None
253
- return {
254
- key: item
255
- for key, item in value.items()
256
- if isinstance(key, str)
257
- and (item is None or isinstance(item, str | int | float | bool | list | dict))
258
- }
259
+ source = value.get("source")
260
+ cache_status = value.get("cacheStatus")
261
+ if source not in {"provider-reported", "estimated", "unknown"}:
262
+ return None
263
+ if cache_status not in {"hit", "miss", "unknown"}:
264
+ return None
265
+ return UsageFacts(
266
+ source=cast(UsageSource, source),
267
+ cache_status=cast(CacheStatus, cache_status),
268
+ input_tokens=measurement(value.get("inputTokens")),
269
+ output_tokens=measurement(value.get("outputTokens")),
270
+ cached_input_tokens=measurement(value.get("cachedInputTokens")),
271
+ reasoning_tokens=measurement(value.get("reasoningTokens")),
272
+ total_tokens=measurement(value.get("totalTokens")),
273
+ first_token_latency_ms=measurement(value.get("firstTokenLatencyMs")),
274
+ )
275
+
276
+
277
+ def southwind_attempt_sequence(event: Event) -> int | None:
278
+ value = (event.custom_metadata or {}).get("southwindAttemptSequence")
279
+ return value if isinstance(value, int) and value > 0 else None
280
+
281
+
282
+ def measurement(value: object) -> int | None:
283
+ return (
284
+ value
285
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0
286
+ else None
287
+ )
259
288
 
260
289
 
261
290
  async def merge_adk_events(
@@ -0,0 +1,325 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import re
6
+ from datetime import datetime
7
+ from typing import Literal, Protocol, cast, override
8
+
9
+ import httpx
10
+ from google.adk.events.event import Event
11
+ from google.adk.sessions.base_session_service import (
12
+ BaseSessionService,
13
+ GetSessionConfig,
14
+ ListSessionsResponse,
15
+ )
16
+ from google.adk.sessions.in_memory_session_service import InMemorySessionService
17
+ from google.adk.sessions.session import Session
18
+ from google.genai import types
19
+ from pydantic import BaseModel, ConfigDict, Field, ValidationError
20
+
21
+ from .config import normalize_base_url
22
+
23
+ CANONICAL_DEBUG_APP = "canonical_debug"
24
+ CANONICAL_DEBUG_USER = "user"
25
+ IMPORTED_SESSION_ID_PREFIX = "wdbg1_"
26
+ IMPORTED_SESSION_ID_PATTERN = re.compile(r"^wdbg1_[A-Za-z0-9_-]{43}$")
27
+ IMPORTED_SESSION_STATE_KEY = "__windy_debug_session_v1__"
28
+ MAX_MESSAGES = 64
29
+ MAX_TOTAL_MESSAGE_CHARS = 128_000
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ class ImportedDebugSessionReadOnlyError(RuntimeError):
34
+ pass
35
+
36
+
37
+ class _StrictModel(BaseModel):
38
+ model_config = ConfigDict(extra="forbid", strict=True)
39
+
40
+
41
+ class DebugSessionSummaryV1(_StrictModel):
42
+ id: str = Field(pattern=r"^wdbg1_[A-Za-z0-9_-]{43}$")
43
+ title: str = Field(min_length=1, max_length=200)
44
+ scopeLabel: str | None = Field(default=None, min_length=1, max_length=120)
45
+ updatedAt: str = Field(min_length=1, max_length=40)
46
+ revision: str = Field(min_length=1, max_length=200)
47
+
48
+
49
+ class DebugSessionMessageV1(_StrictModel):
50
+ sequence: int = Field(ge=0)
51
+ role: Literal["user", "assistant"]
52
+ content: str = Field(min_length=1, max_length=16_000)
53
+ createdAt: str = Field(min_length=1, max_length=40)
54
+
55
+
56
+ class DebugSessionDetailV1(DebugSessionSummaryV1):
57
+ messages: list[DebugSessionMessageV1] = Field(max_length=MAX_MESSAGES)
58
+
59
+
60
+ class DebugSessionListEnvelopeV1(_StrictModel):
61
+ schemaVersion: Literal[1]
62
+ sessions: list[DebugSessionSummaryV1]
63
+ truncated: bool
64
+
65
+
66
+ class DebugSessionDetailEnvelopeV1(_StrictModel):
67
+ schemaVersion: Literal[1]
68
+ session: DebugSessionDetailV1
69
+
70
+
71
+ class DebugSessionClientPortV1(Protocol):
72
+ async def list(self) -> DebugSessionListEnvelopeV1: ...
73
+
74
+ async def get(self, session_id: str) -> DebugSessionDetailV1 | None: ...
75
+
76
+
77
+ class WindyDebugSessionClientV1:
78
+ def __init__(
79
+ self,
80
+ base_url: str,
81
+ bearer_token: str,
82
+ *,
83
+ transport: httpx.AsyncBaseTransport | None = None,
84
+ ) -> None:
85
+ self._base_url = normalize_base_url(base_url)
86
+ self._bearer_token = bearer_token.strip()
87
+ self._transport = transport
88
+ if not self._bearer_token:
89
+ raise RuntimeError("ADK_DEBUG_PLATFORM_TOKEN 未配置,拒绝读取调试会话")
90
+
91
+ @classmethod
92
+ def from_environment(cls) -> WindyDebugSessionClientV1:
93
+ return cls(
94
+ os.environ.get("ADK_DEBUG_PLATFORM_URL", "http://server:9747"),
95
+ os.environ.get("ADK_DEBUG_PLATFORM_TOKEN", ""),
96
+ )
97
+
98
+ async def list(self) -> DebugSessionListEnvelopeV1:
99
+ value = await self._get("api/agent/debug/sessions/v1")
100
+ try:
101
+ envelope = DebugSessionListEnvelopeV1.model_validate(value)
102
+ for session in envelope.sessions:
103
+ parse_timestamp(session.updatedAt)
104
+ return envelope
105
+ except (ValidationError, ValueError) as error:
106
+ raise RuntimeError("平台调试会话列表响应无效") from error
107
+
108
+ async def get(self, session_id: str) -> DebugSessionDetailV1 | None:
109
+ if not IMPORTED_SESSION_ID_PATTERN.fullmatch(session_id):
110
+ raise RuntimeError("平台调试会话 ID 无效")
111
+ value = await self._get(
112
+ f"api/agent/debug/sessions/v1/{session_id}",
113
+ allow_not_found=True,
114
+ )
115
+ if value is None:
116
+ return None
117
+ try:
118
+ session = DebugSessionDetailEnvelopeV1.model_validate(value).session
119
+ validate_detail(session)
120
+ return session
121
+ except (ValidationError, ValueError) as error:
122
+ raise RuntimeError("平台调试会话详情响应无效") from error
123
+
124
+ async def _get(self, path: str, *, allow_not_found: bool = False) -> object | None:
125
+ headers = {
126
+ "authorization": f"Bearer {self._bearer_token}",
127
+ "user-agent": "Windy-ADK-Web-Dev",
128
+ }
129
+ async with httpx.AsyncClient(
130
+ base_url=f"{self._base_url}/",
131
+ headers=headers,
132
+ timeout=httpx.Timeout(15),
133
+ transport=self._transport,
134
+ ) as client:
135
+ response = await client.get(path)
136
+ if allow_not_found and response.status_code == 404:
137
+ return None
138
+ if not response.is_success:
139
+ raise RuntimeError("平台调试会话读取失败")
140
+ try:
141
+ return cast(object, response.json())
142
+ except ValueError as error:
143
+ raise RuntimeError("平台调试会话响应不是有效 JSON") from error
144
+
145
+
146
+ class WindyDebugSessionServiceV1(BaseSessionService):
147
+ """Keep normal ADK sessions local and overlay live read-only projections."""
148
+
149
+ def __init__(
150
+ self,
151
+ client: DebugSessionClientPortV1,
152
+ local: InMemorySessionService | None = None,
153
+ ) -> None:
154
+ self._client = client
155
+ self._local = (
156
+ local if local is not None else InMemorySessionService() # type: ignore[no-untyped-call]
157
+ )
158
+ self._deleted_imports: set[str] = set()
159
+
160
+ @override
161
+ async def create_session(
162
+ self,
163
+ *,
164
+ app_name: str,
165
+ user_id: str,
166
+ state: dict[str, object] | None = None,
167
+ session_id: str | None = None,
168
+ ) -> Session:
169
+ if session_id and session_id.startswith(IMPORTED_SESSION_ID_PREFIX):
170
+ raise ImportedDebugSessionReadOnlyError("导入的业务会话 ID 为只读保留值")
171
+ return await self._local.create_session(
172
+ app_name=app_name,
173
+ user_id=user_id,
174
+ state=state,
175
+ session_id=session_id,
176
+ )
177
+
178
+ @override
179
+ async def get_session(
180
+ self,
181
+ *,
182
+ app_name: str,
183
+ user_id: str,
184
+ session_id: str,
185
+ config: GetSessionConfig | None = None,
186
+ ) -> Session | None:
187
+ if not self._is_imported_request(app_name, user_id, session_id):
188
+ return await self._local.get_session(
189
+ app_name=app_name,
190
+ user_id=user_id,
191
+ session_id=session_id,
192
+ config=config,
193
+ )
194
+ if session_id in self._deleted_imports:
195
+ return None
196
+ detail = await self._client.get(session_id)
197
+ return project_detail(detail, config) if detail else None
198
+
199
+ @override
200
+ async def list_sessions(
201
+ self, *, app_name: str, user_id: str | None = None
202
+ ) -> ListSessionsResponse:
203
+ local = await self._local.list_sessions(app_name=app_name, user_id=user_id)
204
+ if app_name != CANONICAL_DEBUG_APP or user_id != CANONICAL_DEBUG_USER:
205
+ return local
206
+ try:
207
+ remote = await self._client.list()
208
+ except Exception:
209
+ # Discovery is best-effort; cancellation and other BaseException values
210
+ # still propagate instead of being mistaken for an offline platform.
211
+ logger.warning("平台调试会话不可用,仅返回本地 ADK 会话")
212
+ return local
213
+ imported = [
214
+ project_summary(session)
215
+ for session in remote.sessions
216
+ if session.id not in self._deleted_imports
217
+ ]
218
+ return ListSessionsResponse(sessions=[*local.sessions, *imported])
219
+
220
+ @override
221
+ async def delete_session(
222
+ self, *, app_name: str, user_id: str, session_id: str
223
+ ) -> None:
224
+ if self._is_imported_request(app_name, user_id, session_id):
225
+ self._deleted_imports.add(session_id)
226
+ return
227
+ await self._local.delete_session(
228
+ app_name=app_name, user_id=user_id, session_id=session_id
229
+ )
230
+
231
+ @override
232
+ async def append_event(self, session: Session, event: Event) -> Event:
233
+ if session.state.get(IMPORTED_SESSION_STATE_KEY) is True:
234
+ raise ImportedDebugSessionReadOnlyError(
235
+ "导入的业务会话是实时只读投影;请新建本地会话继续提问"
236
+ )
237
+ return await self._local.append_event(session=session, event=event)
238
+
239
+ @override
240
+ async def get_user_state(self, *, app_name: str, user_id: str) -> dict[str, object]:
241
+ return await self._local.get_user_state(app_name=app_name, user_id=user_id)
242
+
243
+ @staticmethod
244
+ def _is_imported_request(app_name: str, user_id: str, session_id: str) -> bool:
245
+ return (
246
+ app_name == CANONICAL_DEBUG_APP
247
+ and user_id == CANONICAL_DEBUG_USER
248
+ and IMPORTED_SESSION_ID_PATTERN.fullmatch(session_id) is not None
249
+ )
250
+
251
+
252
+ def project_summary(value: DebugSessionSummaryV1) -> Session:
253
+ return Session(
254
+ id=value.id,
255
+ app_name=CANONICAL_DEBUG_APP,
256
+ user_id=CANONICAL_DEBUG_USER,
257
+ state=imported_state(value),
258
+ events=[],
259
+ last_update_time=parse_timestamp(value.updatedAt),
260
+ )
261
+
262
+
263
+ def project_detail(
264
+ value: DebugSessionDetailV1,
265
+ config: GetSessionConfig | None,
266
+ ) -> Session:
267
+ events = [project_message(value.id, message) for message in value.messages]
268
+ if config and config.after_timestamp is not None:
269
+ events = [
270
+ event for event in events if event.timestamp >= config.after_timestamp
271
+ ]
272
+ if config and config.num_recent_events is not None:
273
+ events = (
274
+ [] if config.num_recent_events == 0 else events[-config.num_recent_events :]
275
+ )
276
+ return Session(
277
+ id=value.id,
278
+ app_name=CANONICAL_DEBUG_APP,
279
+ user_id=CANONICAL_DEBUG_USER,
280
+ state=imported_state(value),
281
+ events=events,
282
+ last_update_time=parse_timestamp(value.updatedAt),
283
+ )
284
+
285
+
286
+ def project_message(session_id: str, value: DebugSessionMessageV1) -> Event:
287
+ role = "user" if value.role == "user" else "model"
288
+ return Event(
289
+ id=f"{session_id}-{value.sequence}",
290
+ invocation_id=f"{session_id}-projection",
291
+ author="user" if value.role == "user" else CANONICAL_DEBUG_APP,
292
+ content=types.Content(role=role, parts=[types.Part(text=value.content)]),
293
+ timestamp=parse_timestamp(value.createdAt),
294
+ )
295
+
296
+
297
+ def imported_state(value: DebugSessionSummaryV1) -> dict[str, object]:
298
+ display_name = (
299
+ f"{value.title} · {value.scopeLabel}" if value.scopeLabel else value.title
300
+ )
301
+ return {
302
+ IMPORTED_SESSION_STATE_KEY: True,
303
+ "__session_metadata__": {"displayName": display_name},
304
+ }
305
+
306
+
307
+ def validate_detail(value: DebugSessionDetailV1) -> None:
308
+ parse_timestamp(value.updatedAt)
309
+ total_chars = 0
310
+ previous_sequence = -1
311
+ for message in value.messages:
312
+ parse_timestamp(message.createdAt)
313
+ if message.sequence <= previous_sequence:
314
+ raise ValueError("调试会话消息顺序无效")
315
+ previous_sequence = message.sequence
316
+ total_chars += len(message.content)
317
+ if total_chars > MAX_TOTAL_MESSAGE_CHARS:
318
+ raise ValueError("调试会话消息总量超过上限")
319
+
320
+
321
+ def parse_timestamp(value: str) -> float:
322
+ parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
323
+ if parsed.tzinfo is None:
324
+ raise ValueError("调试会话时间必须包含时区")
325
+ return parsed.timestamp()