create-windy 0.2.34 → 0.3.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 (237) hide show
  1. package/dist/cli.js +167 -17
  2. package/package.json +1 -1
  3. package/template/.windy-template.json +2 -2
  4. package/template/AGENTS.md +5 -0
  5. package/template/README.md +3 -1
  6. package/template/apps/agent-server/Dockerfile +1 -0
  7. package/template/apps/agent-server/README.md +18 -5
  8. package/template/apps/agent-server/adk_web_agents/__init__.py +1 -0
  9. package/template/apps/agent-server/adk_web_agents/safe_debug/__init__.py +3 -0
  10. package/template/apps/agent-server/adk_web_agents/safe_debug/agent.py +40 -0
  11. package/template/apps/agent-server/pyproject.toml +1 -1
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +191 -16
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_failure.py +13 -0
  14. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +225 -0
  15. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +76 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/app.py +1 -2
  17. package/template/apps/agent-server/tests/test_adk_adapter.py +305 -0
  18. package/template/apps/agent-server/tests/test_adk_web_agent.py +12 -0
  19. package/template/apps/server/package.json +4 -1
  20. package/template/apps/server/src/agent/run-facade.ts +2 -7
  21. package/template/apps/server/src/agent/tool-registry.ts +2 -6
  22. package/template/apps/server/src/application-services.ts +50 -0
  23. package/template/apps/server/src/audit/drizzle-log-store.ts +25 -0
  24. package/template/apps/server/src/audit/search-summary.ts +2 -6
  25. package/template/apps/server/src/auth/credential-restriction.ts +1 -0
  26. package/template/apps/server/src/auth/password.ts +30 -21
  27. package/template/apps/server/src/auth/routes.test.ts +44 -3
  28. package/template/apps/server/src/auth/routes.ts +6 -1
  29. package/template/apps/server/src/auth/runtime.ts +4 -1
  30. package/template/apps/server/src/auth/service.ts +25 -7
  31. package/template/apps/server/src/configuration/bootstrap.ts +20 -1
  32. package/template/apps/server/src/configuration/definition.test.ts +71 -0
  33. package/template/apps/server/src/configuration/definition.ts +242 -12
  34. package/template/apps/server/src/configuration/drizzle-repository.ts +36 -2
  35. package/template/apps/server/src/configuration/repository.ts +27 -2
  36. package/template/apps/server/src/configuration/routes.ts +4 -1
  37. package/template/apps/server/src/configuration/service.test.ts +100 -0
  38. package/template/apps/server/src/configuration/service.ts +50 -10
  39. package/template/apps/server/src/data-access/scoped-repository.integration.test.ts +3 -2
  40. package/template/apps/server/src/data-governance/export/service.ts +2 -6
  41. package/template/apps/server/src/example-modules.ts +4 -1
  42. package/template/apps/server/src/guards.ts +18 -5
  43. package/template/apps/server/src/http/request-source.test.ts +85 -0
  44. package/template/apps/server/src/http/request-source.ts +90 -0
  45. package/template/apps/server/src/index.ts +23 -6
  46. package/template/apps/server/src/license/routes.ts +2 -0
  47. package/template/apps/server/src/license/runtime-service.ts +10 -11
  48. package/template/apps/server/src/module-bootstrap.ts +2 -2
  49. package/template/apps/server/src/module-host/request-context.ts +6 -6
  50. package/template/apps/server/src/module-host/storage-scope.ts +2 -9
  51. package/template/apps/server/src/module-host/user-directory-port.ts +2 -6
  52. package/template/apps/server/src/notification/channel-adapter.ts +46 -0
  53. package/template/apps/server/src/notification/delivery-policy.test.ts +59 -0
  54. package/template/apps/server/src/notification/delivery-policy.ts +37 -0
  55. package/template/apps/server/src/notification/delivery-repository.ts +198 -0
  56. package/template/apps/server/src/notification/delivery-worker.test.ts +87 -0
  57. package/template/apps/server/src/notification/delivery-worker.ts +88 -0
  58. package/template/apps/server/src/notification/drizzle-management.ts +180 -0
  59. package/template/apps/server/src/notification/drizzle-query.ts +160 -0
  60. package/template/apps/server/src/notification/drizzle-repository.integration.test.ts +301 -0
  61. package/template/apps/server/src/notification/drizzle-repository.ts +156 -90
  62. package/template/apps/server/src/notification/extension-routes.test.ts +119 -0
  63. package/template/apps/server/src/notification/extension-routes.ts +111 -0
  64. package/template/apps/server/src/notification/memory-delivery-repository.ts +137 -0
  65. package/template/apps/server/src/notification/memory-support.ts +72 -0
  66. package/template/apps/server/src/notification/preference-repository.ts +95 -0
  67. package/template/apps/server/src/notification/recipient-directory.ts +104 -0
  68. package/template/apps/server/src/notification/repository.test.ts +171 -0
  69. package/template/apps/server/src/notification/repository.ts +179 -32
  70. package/template/apps/server/src/notification/revision-routes.test.ts +186 -0
  71. package/template/apps/server/src/notification/route-support.ts +225 -0
  72. package/template/apps/server/src/notification/routes.test.ts +182 -0
  73. package/template/apps/server/src/notification/routes.ts +225 -132
  74. package/template/apps/server/src/notification/service.test.ts +104 -0
  75. package/template/apps/server/src/notification/service.ts +140 -0
  76. package/template/apps/server/src/notification/smtp-adapter.test.ts +65 -0
  77. package/template/apps/server/src/notification/smtp-adapter.ts +68 -0
  78. package/template/apps/server/src/persistence.integration.test.ts +41 -2
  79. package/template/apps/server/src/route-guards.ts +1 -0
  80. package/template/apps/server/src/runtime-audit-list.test.ts +39 -0
  81. package/template/apps/server/src/runtime-development.ts +1 -0
  82. package/template/apps/server/src/runtime.test.ts +23 -9
  83. package/template/apps/server/src/runtime.ts +39 -2
  84. package/template/apps/server/src/scheduler/audit.ts +2 -11
  85. package/template/apps/server/src/scheduler/definitions.test.ts +6 -0
  86. package/template/apps/server/src/scheduler/definitions.ts +7 -0
  87. package/template/apps/server/src/scheduler/recovery-service.ts +2 -11
  88. package/template/apps/server/src/scheduler/routes.ts +2 -7
  89. package/template/apps/server/src/search/opensearch-client.test.ts +45 -0
  90. package/template/apps/server/src/search/opensearch-client.ts +145 -0
  91. package/template/apps/server/src/search/service.ts +2 -6
  92. package/template/apps/server/src/settings/routes.ts +15 -3
  93. package/template/apps/server/src/settings/runtime.ts +10 -1
  94. package/template/apps/server/src/storage/runtime.ts +13 -0
  95. package/template/apps/server/src/system/audit-query.ts +33 -4
  96. package/template/apps/server/src/system/audit-routes.test.ts +58 -4
  97. package/template/apps/server/src/system/audit-routes.ts +6 -5
  98. package/template/apps/server/src/system/department-organization.ts +320 -0
  99. package/template/apps/server/src/system/department-routes.test.ts +207 -48
  100. package/template/apps/server/src/system/department-routes.ts +114 -33
  101. package/template/apps/server/src/system/drizzle-repository.ts +15 -7
  102. package/template/apps/server/src/system/drizzle-scoped-repository.ts +5 -3
  103. package/template/apps/server/src/system/drizzle-system.ts +37 -9
  104. package/template/apps/server/src/system/drizzle-transaction-context.ts +14 -0
  105. package/template/apps/server/src/system/identity-route-config.ts +7 -2
  106. package/template/apps/server/src/system/mutation-runner.ts +23 -0
  107. package/template/apps/server/src/system/organization-postgres.integration.test.ts +176 -0
  108. package/template/apps/server/src/system/resource-access-response.ts +12 -6
  109. package/template/apps/server/src/system/role-department-scope-policy.ts +55 -0
  110. package/template/apps/server/src/system/route-helpers.ts +2 -5
  111. package/template/apps/server/src/system/route-types.ts +12 -3
  112. package/template/apps/server/src/system/routes-validation.test.ts +26 -0
  113. package/template/apps/server/src/system/routes.test.ts +10 -2
  114. package/template/apps/server/src/system/routes.ts +70 -36
  115. package/template/apps/server/src/system/seed.ts +7 -0
  116. package/template/apps/server/src/work-order/data-scope.integration.test.ts +9 -2
  117. package/template/apps/server/src/work-order/opensearch-index.test.ts +97 -0
  118. package/template/apps/server/src/work-order/opensearch-index.ts +313 -0
  119. package/template/apps/server/src/work-order/search-provider.ts +60 -1
  120. package/template/apps/server/src/work-order/service.ts +42 -10
  121. package/template/apps/web/src/app/error-pages.ts +9 -0
  122. package/template/apps/web/src/components/auth/PasswordChangeForm.vue +16 -11
  123. package/template/apps/web/src/components/auth/PasswordChangeForm.webtest.ts +14 -0
  124. package/template/apps/web/src/components/common/FormDialog.vue +16 -2
  125. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +20 -1
  126. package/template/apps/web/src/composables/useGlobalSearch.ts +68 -0
  127. package/template/apps/web/src/composables/usePlatformSettings.ts +8 -7
  128. package/template/apps/web/src/layout/GlobalSearchResults.vue +58 -0
  129. package/template/apps/web/src/layout/NavigationSearchDialog.vue +42 -6
  130. package/template/apps/web/src/layout/NavigationSearchDialog.webtest.ts +71 -0
  131. package/template/apps/web/src/layout/NotificationMenu.vue +73 -31
  132. package/template/apps/web/src/layout/NotificationMenu.webtest.ts +44 -1
  133. package/template/apps/web/src/layout/NotificationMenuItem.vue +61 -0
  134. package/template/apps/web/src/layout/NotificationPreferencesDialog.vue +181 -0
  135. package/template/apps/web/src/layout/NotificationPreferencesDialog.webtest.ts +80 -0
  136. package/template/apps/web/src/lib/date-time.ts +22 -10
  137. package/template/apps/web/src/lib/date-time.webtest.ts +9 -1
  138. package/template/apps/web/src/main.ts +14 -1
  139. package/template/apps/web/src/pages/auth/ChangePasswordPage.vue +16 -1
  140. package/template/apps/web/src/pages/configuration/components/ModuleConfigVersionList.vue +1 -0
  141. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationEditor.vue +10 -0
  142. package/template/apps/web/src/pages/configuration/components/ModuleConfigurationWorkbench.vue +37 -6
  143. package/template/apps/web/src/pages/errors/PlatformErrorPage.vue +115 -0
  144. package/template/apps/web/src/pages/errors/PlatformErrorPage.webtest.ts +44 -0
  145. package/template/apps/web/src/pages/errors/error-page-extension.ts +26 -0
  146. package/template/apps/web/src/pages/system/SystemNotificationsPage.vue +136 -68
  147. package/template/apps/web/src/pages/system/SystemNotificationsPage.webtest.ts +224 -12
  148. package/template/apps/web/src/pages/system/audit-log-presenter.ts +86 -0
  149. package/template/apps/web/src/pages/system/audit-time-range.ts +28 -0
  150. package/template/apps/web/src/pages/system/components/AuditActionCell.vue +10 -69
  151. package/template/apps/web/src/pages/system/components/AuditActionCell.webtest.ts +20 -27
  152. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.vue +134 -0
  153. package/template/apps/web/src/pages/system/components/AuditLogDetailDialog.webtest.ts +76 -0
  154. package/template/apps/web/src/pages/system/components/AuditLogFilters.vue +116 -52
  155. package/template/apps/web/src/pages/system/components/AuditLogFilters.webtest.ts +52 -15
  156. package/template/apps/web/src/pages/system/components/NotificationHistoryDialog.vue +129 -0
  157. package/template/apps/web/src/pages/system/components/NotificationManagementCard.vue +91 -0
  158. package/template/apps/web/src/pages/system/components/NotificationPublishDialog.vue +121 -0
  159. package/template/apps/web/src/pages/system/components/NotificationPublishForm.vue +162 -1
  160. package/template/apps/web/src/pages/system/components/SystemResourceActions.vue +68 -0
  161. package/template/apps/web/src/pages/system/components/SystemResourceEditor.vue +74 -0
  162. package/template/apps/web/src/pages/system/components/SystemResourceFilters.vue +3 -4
  163. package/template/apps/web/src/pages/system/components/SystemResourcePage.vue +91 -98
  164. package/template/apps/web/src/pages/system/components/SystemResourcePage.webtest.ts +68 -0
  165. package/template/apps/web/src/pages/system/components/SystemResourceTable.vue +20 -1
  166. package/template/apps/web/src/pages/system/components/SystemResourceTable.webtest.ts +28 -7
  167. package/template/apps/web/src/pages/system/components/department/DepartmentOrganizationForm.vue +210 -0
  168. package/template/apps/web/src/pages/system/components/department/DepartmentTransitionDialog.vue +112 -0
  169. package/template/apps/web/src/pages/system/components/user-account/UserAccountForm.vue +17 -4
  170. package/template/apps/web/src/pages/system/composables/useDepartmentTransitions.ts +79 -0
  171. package/template/apps/web/src/pages/system/composables/useNotificationPermissions.ts +14 -0
  172. package/template/apps/web/src/pages/system/composables/useSystemResource.ts +2 -0
  173. package/template/apps/web/src/pages/system/composables/useSystemResource.webtest.ts +15 -2
  174. package/template/apps/web/src/pages/system/resource-config.ts +12 -4
  175. package/template/apps/web/src/router/error-navigation.ts +50 -0
  176. package/template/apps/web/src/router/error-navigation.webtest.ts +106 -0
  177. package/template/apps/web/src/router/index.ts +71 -0
  178. package/template/apps/web/src/router/index.webtest.ts +29 -0
  179. package/template/apps/web/src/router/manifest-routes.webtest.ts +4 -0
  180. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  181. package/template/apps/web/src/services/auth-api.ts +5 -0
  182. package/template/apps/web/src/services/http.ts +43 -6
  183. package/template/apps/web/src/services/http.webtest.ts +70 -0
  184. package/template/apps/web/src/services/notification-api.ts +85 -2
  185. package/template/apps/web/src/services/notification-api.webtest.ts +110 -0
  186. package/template/apps/web/src/services/search-api.ts +39 -0
  187. package/template/apps/web/src/services/system-api.ts +18 -0
  188. package/template/apps/web/vitest.config.ts +1 -0
  189. package/template/docker-compose.yml +66 -0
  190. package/template/docs/architecture/ai-runtime.md +14 -5
  191. package/template/docs/architecture/search-provider-protocol.md +38 -2
  192. package/template/docs/development/custom-error-pages.md +78 -0
  193. package/template/docs/development/custom-login-page.md +3 -3
  194. package/template/docs/platform/agent-runtime.md +32 -6
  195. package/template/docs/platform/audit-reliability.md +46 -2
  196. package/template/docs/platform/module-configuration.md +13 -4
  197. package/template/docs/platform/notifications.md +67 -11
  198. package/template/docs/platform/organization-membership.md +17 -3
  199. package/template/docs/platform/system-crud-api.md +16 -0
  200. package/template/docs/platform/web-system-crud.md +6 -2
  201. package/template/package.json +4 -0
  202. package/template/packages/config/index.test.ts +45 -0
  203. package/template/packages/config/package.json +1 -1
  204. package/template/packages/config/src/platform.ts +169 -48
  205. package/template/packages/database/drizzle/0035_material_nightshade.sql +10 -0
  206. package/template/packages/database/drizzle/0036_hesitant_speed.sql +35 -0
  207. package/template/packages/database/drizzle/0037_absent_nick_fury.sql +2 -0
  208. package/template/packages/database/drizzle/0038_uneven_wasp.sql +38 -0
  209. package/template/packages/database/drizzle/meta/0035_snapshot.json +7331 -0
  210. package/template/packages/database/drizzle/meta/0036_snapshot.json +7436 -0
  211. package/template/packages/database/drizzle/meta/0037_snapshot.json +7457 -0
  212. package/template/packages/database/drizzle/meta/0038_snapshot.json +7749 -0
  213. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  214. package/template/packages/database/package.json +1 -1
  215. package/template/packages/database/src/schema/identity.ts +7 -0
  216. package/template/packages/database/src/schema/notifications.ts +135 -0
  217. package/template/packages/database/src/schema-notification-delivery.test.ts +54 -0
  218. package/template/packages/database/src/schema-workflow-notification.test.ts +40 -1
  219. package/template/packages/modules/package.json +1 -1
  220. package/template/packages/modules/src/system-api-permissions.ts +54 -0
  221. package/template/packages/modules/src/system-audit-actions.ts +10 -0
  222. package/template/packages/modules/src/system-features.ts +1 -1
  223. package/template/packages/modules/src/system-module-catalog.ts +2 -1
  224. package/template/packages/modules/src/system-modules.test.ts +13 -1
  225. package/template/packages/modules/src/system-modules.ts +12 -0
  226. package/template/packages/modules/src/system-permissions.ts +9 -5
  227. package/template/packages/server-sdk/package.json +1 -1
  228. package/template/packages/shared/package.json +1 -1
  229. package/template/packages/shared/src/audit.ts +25 -0
  230. package/template/packages/shared/src/module-configuration.ts +5 -0
  231. package/template/packages/shared/src/notification.ts +92 -1
  232. package/template/packages/shared/src/password.ts +7 -2
  233. package/template/packages/shared/src/platform-settings.ts +2 -0
  234. package/template/packages/storage/package.json +1 -1
  235. package/template/packages/storage/src/upload-service.test.ts +16 -0
  236. package/template/packages/storage/src/upload-service.ts +5 -1
  237. package/template/scripts/notification-postgres-regression.ts +27 -0
@@ -1,39 +1,214 @@
1
1
  from __future__ import annotations
2
2
 
3
- from collections.abc import AsyncIterator, Mapping
3
+ import asyncio
4
+ from collections.abc import AsyncIterator
4
5
 
6
+ from google.adk.agents import LlmAgent
7
+ from google.adk.agents.run_config import RunConfig, StreamingMode
8
+ from google.adk.events import Event
9
+ from google.adk.runners import Runner
10
+ from google.adk.sessions import InMemorySessionService
11
+ from google.genai import types
12
+
13
+ from .adk_failure import AdkFailureState
14
+ from .adk_model import GovernedOpenAiLlm
15
+ from .adk_tools import GovernedTool
16
+ from .contracts import JsonValue
5
17
  from .engine import (
6
18
  AgentEngineAdapter,
19
+ EngineCompleted,
7
20
  EngineRunRequest,
8
21
  EngineSignal,
22
+ OutputDelta,
9
23
  SafeEngineError,
24
+ ToolCompleted,
25
+ ToolRequested,
26
+ UsageReported,
10
27
  )
28
+ from .openai_engine import cancelled, validate_runtime_request
29
+ from .openai_provider import OpenAiCompatibleProvider
30
+ from .provider_governance import ProviderRequestGovernance
31
+ from .tool_host import ToolHostClient
11
32
 
12
33
 
13
34
  class AdkEngineAdapter(AgentEngineAdapter):
14
- """Google ADK implementation skeleton.
15
-
16
- Agent objects remain opaque and internal. A later slice will create ADK Runners,
17
- consume ADK Events, and translate them into EngineSignal values at this seam.
18
- """
35
+ """Google ADK Runner implementation behind the stable engine seam."""
19
36
 
20
- def __init__(self, agents: Mapping[str, object] | None = None) -> None:
21
- self._agents = dict(agents or {})
37
+ def __init__(
38
+ self,
39
+ provider: OpenAiCompatibleProvider | None = None,
40
+ tool_host: ToolHostClient | None = None,
41
+ governance: ProviderRequestGovernance | None = None,
42
+ ) -> None:
43
+ self._provider = provider
44
+ self._tool_host = tool_host
45
+ self._governance = governance
46
+ self._cancellations: dict[str, asyncio.Event] = {}
22
47
 
23
48
  def supports(self, operation_key: str) -> bool:
24
- return operation_key in self._agents
49
+ return bool(operation_key.strip())
25
50
 
26
51
  async def execute(
27
52
  self,
28
53
  request: EngineRunRequest,
29
54
  ) -> AsyncIterator[EngineSignal]:
30
- del request
31
- raise SafeEngineError(
32
- "RUNTIME_UNAVAILABLE",
33
- "Agent Runtime 尚未配置",
34
- retryable=True,
55
+ provider, tool_host, governance = self._configured()
56
+ validate_runtime_request(request, provider.config.deployment_key)
57
+ cancellation = self._cancellations.setdefault(
58
+ request.run_id,
59
+ asyncio.Event(),
35
60
  )
36
- yield # pragma: no cover - keeps this method an async generator
61
+ output = ""
62
+ output_bytes = 0
63
+ failures = AdkFailureState()
64
+ event_failure: SafeEngineError | None = None
65
+ try:
66
+ session_service = InMemorySessionService() # type: ignore[no-untyped-call]
67
+ await session_service.create_session(
68
+ app_name="southwind_runtime",
69
+ user_id="platform",
70
+ session_id=request.run_id,
71
+ )
72
+ runner = Runner(
73
+ app_name="southwind_runtime",
74
+ agent=LlmAgent(
75
+ name="southwind_operation",
76
+ model=GovernedOpenAiLlm(
77
+ request,
78
+ provider,
79
+ governance,
80
+ cancellation,
81
+ failures,
82
+ ),
83
+ instruction=request.instructions,
84
+ tools=[
85
+ GovernedTool(
86
+ tool,
87
+ request,
88
+ tool_host,
89
+ cancellation,
90
+ failures,
91
+ )
92
+ for tool in request.allowed_tools
93
+ ],
94
+ ),
95
+ session_service=session_service,
96
+ )
97
+ async for event in runner.run_async(
98
+ user_id="platform",
99
+ session_id=request.run_id,
100
+ new_message=types.Content(
101
+ role="user",
102
+ parts=[types.Part(text=request.user_prompt)],
103
+ ),
104
+ run_config=RunConfig(
105
+ streaming_mode=StreamingMode.SSE,
106
+ max_llm_calls=provider.config.max_tool_rounds,
107
+ ),
108
+ ):
109
+ cancelled(cancellation)
110
+ if event.error_code:
111
+ event_failure = failures.failure or SafeEngineError(
112
+ "PROVIDER_UNAVAILABLE",
113
+ "模型服务暂不可用",
114
+ retryable=True,
115
+ )
116
+ continue
117
+ async for signal in signals_for_event(event):
118
+ if isinstance(signal, OutputDelta):
119
+ output_bytes += len(signal.text.encode())
120
+ if output_bytes > request.limits.max_output_bytes:
121
+ raise SafeEngineError(
122
+ "BUDGET_EXCEEDED",
123
+ "Agent 输出超过 Operation 上限",
124
+ retryable=False,
125
+ )
126
+ output += signal.text
127
+ yield signal
128
+ if event_failure is not None:
129
+ raise event_failure
130
+ cancelled(cancellation)
131
+ yield EngineCompleted({"text": output})
132
+ except SafeEngineError:
133
+ raise
134
+ except asyncio.CancelledError:
135
+ raise SafeEngineError(
136
+ "RUN_CANCELLED",
137
+ "Agent Run 已取消",
138
+ retryable=False,
139
+ ) from None
140
+ except Exception:
141
+ raise SafeEngineError(
142
+ "RUNTIME_UNAVAILABLE",
143
+ "Agent Runtime 执行失败",
144
+ retryable=True,
145
+ ) from None
146
+ finally:
147
+ self._cancellations.pop(request.run_id, None)
37
148
 
38
149
  async def cancel(self, run_id: str) -> None:
39
- del run_id
150
+ cancellation = self._cancellations.get(run_id)
151
+ if cancellation is not None:
152
+ cancellation.set()
153
+
154
+ async def health(self) -> None:
155
+ provider, _, _ = self._configured()
156
+ await provider.health()
157
+
158
+ def governance_health(self) -> dict[str, int | float | str | None]:
159
+ _, _, governance = self._configured()
160
+ return governance.health()
161
+
162
+ def _configured(
163
+ self,
164
+ ) -> tuple[
165
+ OpenAiCompatibleProvider,
166
+ ToolHostClient,
167
+ ProviderRequestGovernance,
168
+ ]:
169
+ if not self._provider or not self._tool_host or not self._governance:
170
+ raise SafeEngineError(
171
+ "RUNTIME_UNAVAILABLE",
172
+ "Agent Runtime 尚未配置",
173
+ retryable=True,
174
+ )
175
+ return self._provider, self._tool_host, self._governance
176
+
177
+
178
+ async def signals_for_event(event: Event) -> AsyncIterator[EngineSignal]:
179
+ for call in event.get_function_calls():
180
+ if not call.id or not call.name:
181
+ raise SafeEngineError(
182
+ "TOOL_DENIED",
183
+ "模型请求了无效 Tool",
184
+ retryable=False,
185
+ )
186
+ yield ToolRequested(call.id, call.name)
187
+ for response in event.get_function_responses():
188
+ if not response.id:
189
+ raise SafeEngineError(
190
+ "TOOL_DENIED",
191
+ "Agent Tool 响应缺少稳定标识",
192
+ retryable=False,
193
+ )
194
+ yield ToolCompleted(response.id)
195
+ if event.partial and event.content:
196
+ for part in event.content.parts or []:
197
+ if part.text and not part.thought:
198
+ yield OutputDelta(part.text)
199
+ usage = southwind_usage(event)
200
+ if usage is not None:
201
+ yield UsageReported(usage)
202
+
203
+
204
+ def southwind_usage(event: Event) -> dict[str, JsonValue] | None:
205
+ metadata = event.custom_metadata or {}
206
+ value = metadata.get("southwindUsage")
207
+ if not isinstance(value, dict):
208
+ return None
209
+ return {
210
+ key: item
211
+ for key, item in value.items()
212
+ if isinstance(key, str)
213
+ and (item is None or isinstance(item, str | int | float | bool | list | dict))
214
+ }
@@ -0,0 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from .engine import SafeEngineError
6
+
7
+
8
+ @dataclass
9
+ class AdkFailureState:
10
+ failure: SafeEngineError | None = None
11
+
12
+ def capture(self, error: SafeEngineError) -> None:
13
+ self.failure = error
@@ -0,0 +1,225 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import json
5
+ from collections.abc import AsyncGenerator
6
+
7
+ from google.adk.models.base_llm import BaseLlm
8
+ from google.adk.models.llm_request import LlmRequest
9
+ from google.adk.models.llm_response import LlmResponse
10
+ from google.genai import types
11
+ from pydantic import PrivateAttr
12
+
13
+ from .adk_failure import AdkFailureState
14
+ from .contracts import JsonValue
15
+ from .engine import EngineRunRequest, SafeEngineError
16
+ from .openai_engine import cancelled, parse_arguments
17
+ from .openai_provider import OpenAiCompatibleProvider, ProviderTurn
18
+ from .provider_governance import ProviderRequestGovernance
19
+
20
+
21
+ class GovernedOpenAiLlm(BaseLlm):
22
+ """ADK model bridge that preserves Southwind's provider governance."""
23
+
24
+ _request: EngineRunRequest = PrivateAttr()
25
+ _provider: OpenAiCompatibleProvider = PrivateAttr()
26
+ _governance: ProviderRequestGovernance = PrivateAttr()
27
+ _cancellation: asyncio.Event = PrivateAttr()
28
+ _failures: AdkFailureState = PrivateAttr()
29
+ _attempt: int = PrivateAttr(default=0)
30
+
31
+ def __init__(
32
+ self,
33
+ request: EngineRunRequest,
34
+ provider: OpenAiCompatibleProvider,
35
+ governance: ProviderRequestGovernance,
36
+ cancellation: asyncio.Event,
37
+ failures: AdkFailureState,
38
+ ) -> None:
39
+ super().__init__(model=provider.config.model)
40
+ self._request = request
41
+ self._provider = provider
42
+ self._governance = governance
43
+ self._cancellation = cancellation
44
+ self._failures = failures
45
+
46
+ async def generate_content_async(
47
+ self,
48
+ llm_request: LlmRequest,
49
+ stream: bool = False,
50
+ ) -> AsyncGenerator[LlmResponse, None]:
51
+ del stream
52
+ try:
53
+ cancelled(self._cancellation)
54
+ self._attempt += 1
55
+ if self._attempt > self._provider.config.max_tool_rounds:
56
+ raise SafeEngineError(
57
+ "BUDGET_EXCEEDED",
58
+ "Agent Tool 调用轮次超过上限",
59
+ retryable=False,
60
+ )
61
+ messages = adk_messages(llm_request, self._request.instructions)
62
+ governed = self._governance.authorize(
63
+ deployment_key=self._provider.config.deployment_key,
64
+ operation_key=self._request.operation_key,
65
+ run_id=self._request.run_id,
66
+ attempt=self._attempt,
67
+ messages=messages,
68
+ tools=self._request.allowed_tools,
69
+ policy=self._request.provider_data_policy,
70
+ input_value=self._request.input,
71
+ measure=lambda values, tools: len(
72
+ self._provider.request_body(values, tools)
73
+ ),
74
+ )
75
+ turn = await self._provider.complete_turn(
76
+ governed.messages,
77
+ governed.tools,
78
+ )
79
+ cancelled(self._cancellation)
80
+ validate_tool_calls(
81
+ turn,
82
+ {tool.key for tool in self._request.allowed_tools},
83
+ )
84
+ async for response in responses_for_turn(turn):
85
+ yield response
86
+ except SafeEngineError as error:
87
+ self._failures.capture(error)
88
+ raise
89
+
90
+
91
+ def adk_messages(
92
+ request: LlmRequest,
93
+ instructions: str,
94
+ ) -> list[dict[str, JsonValue]]:
95
+ result: list[dict[str, JsonValue]] = [{"role": "system", "content": instructions}]
96
+ for content in request.contents:
97
+ parts = content.parts or []
98
+ calls = [part.function_call for part in parts if part.function_call]
99
+ responses = [part.function_response for part in parts if part.function_response]
100
+ text = "".join(
101
+ part.text or "" for part in parts if not part.thought and part.text
102
+ )
103
+ if calls:
104
+ result.append(
105
+ {
106
+ "role": "assistant",
107
+ "content": text or None,
108
+ "tool_calls": [
109
+ {
110
+ "id": call.id or "",
111
+ "type": "function",
112
+ "function": {
113
+ "name": call.name or "",
114
+ "arguments": json.dumps(
115
+ call.args or {},
116
+ ensure_ascii=False,
117
+ separators=(",", ":"),
118
+ ),
119
+ },
120
+ }
121
+ for call in calls
122
+ ],
123
+ }
124
+ )
125
+ elif responses:
126
+ result.extend(tool_response_messages(responses))
127
+ elif text:
128
+ result.append(
129
+ {
130
+ "role": "assistant" if content.role == "model" else "user",
131
+ "content": text,
132
+ }
133
+ )
134
+ return result
135
+
136
+
137
+ def tool_response_messages(
138
+ responses: list[types.FunctionResponse],
139
+ ) -> list[dict[str, JsonValue]]:
140
+ return [
141
+ {
142
+ "role": "tool",
143
+ "tool_call_id": response.id or "",
144
+ "content": json.dumps(
145
+ response.response or {},
146
+ ensure_ascii=False,
147
+ separators=(",", ":"),
148
+ ),
149
+ }
150
+ for response in responses
151
+ ]
152
+
153
+
154
+ def validate_tool_calls(turn: ProviderTurn, allowed: set[str]) -> None:
155
+ for call in turn.tool_calls:
156
+ if not call.call_id or not call.name or call.name not in allowed:
157
+ raise SafeEngineError(
158
+ "TOOL_DENIED",
159
+ "模型请求了未授权 Tool",
160
+ retryable=False,
161
+ )
162
+ parse_arguments(call.arguments)
163
+
164
+
165
+ async def responses_for_turn(
166
+ turn: ProviderTurn,
167
+ ) -> AsyncGenerator[LlmResponse, None]:
168
+ usage = turn.usage or {"source": "unknown"}
169
+ metadata = {"southwindUsage": usage}
170
+ if turn.tool_calls:
171
+ yield LlmResponse(
172
+ content=types.Content(
173
+ role="model",
174
+ parts=[
175
+ types.Part(
176
+ function_call=types.FunctionCall(
177
+ id=call.call_id,
178
+ name=call.name,
179
+ args=parse_arguments(call.arguments),
180
+ )
181
+ )
182
+ for call in turn.tool_calls
183
+ ],
184
+ ),
185
+ partial=False,
186
+ custom_metadata=metadata,
187
+ usage_metadata=adk_usage(usage),
188
+ )
189
+ return
190
+ for delta in turn.text_deltas:
191
+ yield LlmResponse(
192
+ content=types.Content(
193
+ role="model",
194
+ parts=[types.Part(text=delta)],
195
+ ),
196
+ partial=True,
197
+ )
198
+ yield LlmResponse(
199
+ content=types.Content(
200
+ role="model",
201
+ parts=[types.Part(text=turn.text)],
202
+ ),
203
+ partial=False,
204
+ custom_metadata=metadata,
205
+ usage_metadata=adk_usage(usage),
206
+ )
207
+
208
+
209
+ def adk_usage(
210
+ usage: dict[str, JsonValue],
211
+ ) -> types.GenerateContentResponseUsageMetadata | None:
212
+ input_tokens = usage.get("inputTokens")
213
+ output_tokens = usage.get("outputTokens")
214
+ total_tokens = usage.get("totalTokens")
215
+ if not any(
216
+ isinstance(value, int) for value in (input_tokens, output_tokens, total_tokens)
217
+ ):
218
+ return None
219
+ return types.GenerateContentResponseUsageMetadata(
220
+ prompt_token_count=input_tokens if isinstance(input_tokens, int) else None,
221
+ candidates_token_count=(
222
+ output_tokens if isinstance(output_tokens, int) else None
223
+ ),
224
+ total_token_count=total_tokens if isinstance(total_tokens, int) else None,
225
+ )
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, cast
5
+
6
+ from google.adk.tools.base_tool import BaseTool
7
+ from google.adk.tools.tool_context import ToolContext
8
+ from google.genai import types
9
+
10
+ from .adk_failure import AdkFailureState
11
+ from .contracts import JsonValue, RuntimeToolDefinition
12
+ from .engine import EngineRunRequest, SafeEngineError
13
+ from .openai_engine import cancelled
14
+ from .tool_host import ToolHostClient, ToolInvocation
15
+
16
+
17
+ class GovernedTool(BaseTool):
18
+ """ADK declaration whose only execution path is the governed Tool Host."""
19
+
20
+ def __init__(
21
+ self,
22
+ definition: RuntimeToolDefinition,
23
+ request: EngineRunRequest,
24
+ tool_host: ToolHostClient,
25
+ cancellation: asyncio.Event,
26
+ failures: AdkFailureState,
27
+ ) -> None:
28
+ super().__init__(
29
+ name=definition.key,
30
+ description=definition.description,
31
+ )
32
+ self._request = request
33
+ self._tool_host = tool_host
34
+ self._cancellation = cancellation
35
+ self._failures = failures
36
+
37
+ def _get_declaration(self) -> types.FunctionDeclaration:
38
+ return types.FunctionDeclaration(
39
+ name=self.name,
40
+ description=self.description,
41
+ parameters_json_schema={
42
+ "type": "object",
43
+ "additionalProperties": True,
44
+ },
45
+ )
46
+
47
+ async def run_async(
48
+ self,
49
+ *,
50
+ args: dict[str, Any],
51
+ tool_context: ToolContext,
52
+ ) -> JsonValue:
53
+ try:
54
+ cancelled(self._cancellation)
55
+ call_id = tool_context.function_call_id
56
+ if not call_id:
57
+ raise SafeEngineError(
58
+ "TOOL_DENIED",
59
+ "Agent Tool Call 缺少稳定标识",
60
+ retryable=False,
61
+ )
62
+ result = await self._tool_host.execute(
63
+ self._request.tool_host_url,
64
+ ToolInvocation(
65
+ self._request.run_id,
66
+ call_id,
67
+ self.name,
68
+ cast(dict[str, JsonValue], args),
69
+ self._request.execution_grant,
70
+ ),
71
+ )
72
+ cancelled(self._cancellation)
73
+ return result
74
+ except SafeEngineError as error:
75
+ self._failures.capture(error)
76
+ raise
@@ -21,7 +21,6 @@ from .contracts import (
21
21
  StartRunResponse,
22
22
  )
23
23
  from .engine import AgentEngineAdapter
24
- from .openai_engine import OpenAiCompatibleEngineAdapter
25
24
  from .openai_provider import OpenAiCompatibleProvider
26
25
  from .provider_audit import ProviderAuditStore
27
26
  from .provider_governance import ProviderRequestGovernance
@@ -229,7 +228,7 @@ def default_engine() -> AgentEngineAdapter:
229
228
  internal_token = os.environ.get("AGENT_SERVER_INTERNAL_TOKEN", "").strip()
230
229
  if not internal_token:
231
230
  raise ValueError("Agent Server Provider 已配置但内部 Token 缺失")
232
- return OpenAiCompatibleEngineAdapter(
231
+ return AdkEngineAdapter(
233
232
  OpenAiCompatibleProvider(
234
233
  config,
235
234
  EnvironmentSecretResolver(os.environ),