create-windy 0.3.2 → 0.3.5

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 (343) hide show
  1. package/README.md +27 -9
  2. package/dist/cli.js +825 -117
  3. package/package.json +1 -1
  4. package/template/.agents/skills/windy-business-module/SKILL.md +16 -3
  5. package/template/.windy-template.json +2 -2
  6. package/template/AGENTS.md +4 -0
  7. package/template/README.md +18 -11
  8. package/template/_gitignore +1 -0
  9. package/template/apps/agent-server/README.md +2 -0
  10. package/template/apps/agent-server/pyproject.toml +8 -0
  11. package/template/apps/agent-server/src/southwind_agent_server/adk_adapter.py +87 -10
  12. package/template/apps/agent-server/src/southwind_agent_server/adk_model.py +18 -3
  13. package/template/apps/agent-server/src/southwind_agent_server/adk_tools.py +78 -11
  14. package/template/apps/agent-server/src/southwind_agent_server/app.py +120 -115
  15. package/template/apps/agent-server/src/southwind_agent_server/artifact_input.py +153 -0
  16. package/template/apps/agent-server/src/southwind_agent_server/config.py +29 -0
  17. package/template/apps/agent-server/src/southwind_agent_server/contracts.py +81 -1
  18. package/template/apps/agent-server/src/southwind_agent_server/deterministic.py +8 -0
  19. package/template/apps/agent-server/src/southwind_agent_server/engine.py +28 -0
  20. package/template/apps/agent-server/src/southwind_agent_server/fingerprint.py +41 -0
  21. package/template/apps/agent-server/src/southwind_agent_server/http_helpers.py +62 -0
  22. package/template/apps/agent-server/src/southwind_agent_server/lease.py +15 -0
  23. package/template/apps/agent-server/src/southwind_agent_server/multimodal.py +76 -0
  24. package/template/apps/agent-server/src/southwind_agent_server/openai_engine.py +112 -20
  25. package/template/apps/agent-server/src/southwind_agent_server/openai_provider.py +14 -1
  26. package/template/apps/agent-server/src/southwind_agent_server/postgres_store.py +329 -0
  27. package/template/apps/agent-server/src/southwind_agent_server/postgres_store_backend.py +332 -0
  28. package/template/apps/agent-server/src/southwind_agent_server/provider_governance.py +64 -3
  29. package/template/apps/agent-server/src/southwind_agent_server/runtime_factory.py +68 -0
  30. package/template/apps/agent-server/src/southwind_agent_server/service.py +218 -56
  31. package/template/apps/agent-server/src/southwind_agent_server/signal_events.py +36 -0
  32. package/template/apps/agent-server/src/southwind_agent_server/sqlite_catalog.py +107 -0
  33. package/template/apps/agent-server/src/southwind_agent_server/sqlite_schema.py +65 -0
  34. package/template/apps/agent-server/src/southwind_agent_server/sqlite_serialization.py +16 -0
  35. package/template/apps/agent-server/src/southwind_agent_server/sqlite_store.py +89 -57
  36. package/template/apps/agent-server/src/southwind_agent_server/store.py +105 -2
  37. package/template/apps/agent-server/src/southwind_agent_server/store_cipher.py +40 -0
  38. package/template/apps/agent-server/src/southwind_agent_server/telemetry.py +89 -0
  39. package/template/apps/agent-server/src/southwind_agent_server/tool_host.py +72 -0
  40. package/template/apps/agent-server/tests/test_adk_adapter.py +6 -2
  41. package/template/apps/agent-server/tests/test_api.py +24 -1
  42. package/template/apps/agent-server/tests/test_artifact_input.py +110 -0
  43. package/template/apps/agent-server/tests/test_config.py +4 -0
  44. package/template/apps/agent-server/tests/test_fingerprint.py +10 -0
  45. package/template/apps/agent-server/tests/test_multimodal.py +51 -0
  46. package/template/apps/agent-server/tests/test_openai_provider.py +6 -2
  47. package/template/apps/agent-server/tests/test_postgres_store.py +131 -0
  48. package/template/apps/agent-server/tests/test_provider_governance.py +61 -0
  49. package/template/apps/agent-server/tests/test_sqlite_store.py +95 -10
  50. package/template/apps/agent-server/tests/test_telemetry.py +25 -0
  51. package/template/apps/agent-server/tests/test_tool_approval.py +28 -0
  52. package/template/apps/agent-server/uv.lock +165 -0
  53. package/template/apps/document-server/Dockerfile +19 -0
  54. package/template/apps/document-server/pyproject.toml +43 -0
  55. package/template/apps/document-server/src/southwind_document_server/__init__.py +1 -0
  56. package/template/apps/document-server/src/southwind_document_server/adapter.py +337 -0
  57. package/template/apps/document-server/src/southwind_document_server/app.py +151 -0
  58. package/template/apps/document-server/src/southwind_document_server/config.py +94 -0
  59. package/template/apps/document-server/src/southwind_document_server/contracts.py +103 -0
  60. package/template/apps/document-server/src/southwind_document_server/service.py +149 -0
  61. package/template/apps/document-server/src/southwind_document_server/store.py +185 -0
  62. package/template/apps/document-server/tests/__init__.py +1 -0
  63. package/template/apps/document-server/tests/test_config.py +42 -0
  64. package/template/apps/document-server/tests/test_service.py +140 -0
  65. package/template/apps/document-server/uv.lock +430 -0
  66. package/template/apps/embedding-server/Dockerfile +17 -0
  67. package/template/apps/embedding-server/pyproject.toml +44 -0
  68. package/template/apps/embedding-server/src/southwind_embedding_server/__init__.py +1 -0
  69. package/template/apps/embedding-server/src/southwind_embedding_server/adapter.py +254 -0
  70. package/template/apps/embedding-server/src/southwind_embedding_server/app.py +119 -0
  71. package/template/apps/embedding-server/src/southwind_embedding_server/config.py +120 -0
  72. package/template/apps/embedding-server/src/southwind_embedding_server/contracts.py +106 -0
  73. package/template/apps/embedding-server/src/southwind_embedding_server/error.py +6 -0
  74. package/template/apps/embedding-server/src/southwind_embedding_server/image_input.py +87 -0
  75. package/template/apps/embedding-server/tests/__init__.py +1 -0
  76. package/template/apps/embedding-server/tests/test_adapter.py +92 -0
  77. package/template/apps/embedding-server/tests/test_config.py +38 -0
  78. package/template/apps/embedding-server/uv.lock +449 -0
  79. package/template/apps/server/src/agent/background-operation-port.ts +37 -0
  80. package/template/apps/server/src/agent/background-operation.test.ts +231 -0
  81. package/template/apps/server/src/agent/background-operation.ts +252 -0
  82. package/template/apps/server/src/agent/drizzle-run-binding-repository.ts +114 -0
  83. package/template/apps/server/src/agent/execution-grants.ts +7 -1
  84. package/template/apps/server/src/agent/fake-runtime.test-support.ts +43 -0
  85. package/template/apps/server/src/agent/idempotency-key.test.ts +28 -0
  86. package/template/apps/server/src/agent/idempotency-key.ts +11 -0
  87. package/template/apps/server/src/agent/remote-runtime.test.ts +115 -1
  88. package/template/apps/server/src/agent/remote-runtime.ts +173 -5
  89. package/template/apps/server/src/agent/run-authorization.test-support.ts +9 -0
  90. package/template/apps/server/src/agent/run-authorization.test.ts +126 -0
  91. package/template/apps/server/src/agent/run-authorization.ts +180 -0
  92. package/template/apps/server/src/agent/run-binding-repository.ts +93 -0
  93. package/template/apps/server/src/agent/run-contracts.ts +88 -1
  94. package/template/apps/server/src/agent/run-definition-support.ts +58 -0
  95. package/template/apps/server/src/agent/run-errors.ts +25 -0
  96. package/template/apps/server/src/agent/run-facade.test.ts +57 -46
  97. package/template/apps/server/src/agent/run-facade.ts +185 -65
  98. package/template/apps/server/src/agent/run-release.ts +78 -0
  99. package/template/apps/server/src/agent/run-route-input.ts +169 -0
  100. package/template/apps/server/src/agent/run-routes.ts +63 -76
  101. package/template/apps/server/src/agent/runtime-bootstrap.ts +169 -13
  102. package/template/apps/server/src/agent/sandbox-client.ts +200 -0
  103. package/template/apps/server/src/agent/sandbox-protocol.ts +76 -0
  104. package/template/apps/server/src/agent/sandbox-registry.ts +205 -0
  105. package/template/apps/server/src/agent/sandbox-runtime.test.ts +137 -0
  106. package/template/apps/server/src/agent/sandbox-worker-server.ts +195 -0
  107. package/template/apps/server/src/agent/sandbox-worker.ts +12 -0
  108. package/template/apps/server/src/agent/tool-approval.test.ts +75 -0
  109. package/template/apps/server/src/agent/tool-approval.ts +115 -0
  110. package/template/apps/server/src/agent/tool-audit.ts +62 -0
  111. package/template/apps/server/src/agent/tool-host.test.ts +39 -11
  112. package/template/apps/server/src/agent/tool-host.ts +184 -17
  113. package/template/apps/server/src/agent/tool-registry.ts +52 -47
  114. package/template/apps/server/src/agent/vision-facade.test.ts +256 -0
  115. package/template/apps/server/src/agent/vision-input.ts +120 -0
  116. package/template/apps/server/src/ai-artifact/codec.test.ts +44 -0
  117. package/template/apps/server/src/ai-artifact/codec.ts +137 -0
  118. package/template/apps/server/src/ai-artifact/routes.ts +37 -0
  119. package/template/apps/server/src/ai-artifact/runtime.ts +205 -0
  120. package/template/apps/server/src/application-services.ts +62 -2
  121. package/template/apps/server/src/auth/reauthentication.test.ts +40 -0
  122. package/template/apps/server/src/auth/reauthentication.ts +19 -0
  123. package/template/apps/server/src/auth/runtime.ts +9 -1
  124. package/template/apps/server/src/data-access/context.ts +6 -4
  125. package/template/apps/server/src/data-access/scope-snapshot.ts +27 -0
  126. package/template/apps/server/src/data-governance/export/approval-port.ts +6 -3
  127. package/template/apps/server/src/data-governance/export/authorization-port.test.ts +23 -4
  128. package/template/apps/server/src/data-governance/export/authorization-port.ts +89 -25
  129. package/template/apps/server/src/data-governance/export/drizzle-repository.integration.test.ts +2 -0
  130. package/template/apps/server/src/data-governance/export/drizzle-repository.ts +1 -0
  131. package/template/apps/server/src/data-governance/export/job-runtime.test.ts +15 -4
  132. package/template/apps/server/src/data-governance/export/job-runtime.ts +68 -15
  133. package/template/apps/server/src/data-governance/export/ndjson-stream.ts +25 -6
  134. package/template/apps/server/src/data-governance/export/postgres-recovery.integration.test.ts +4 -2
  135. package/template/apps/server/src/data-governance/export/renderer-job-runtime.test.ts +235 -0
  136. package/template/apps/server/src/data-governance/export/renderer-runtime.ts +293 -0
  137. package/template/apps/server/src/data-governance/export/renderer-selection.test.ts +67 -0
  138. package/template/apps/server/src/data-governance/export/repository.test.ts +2 -0
  139. package/template/apps/server/src/data-governance/export/request-fingerprint.test.ts +13 -0
  140. package/template/apps/server/src/data-governance/export/request-fingerprint.ts +2 -0
  141. package/template/apps/server/src/data-governance/export/routes.ts +11 -1
  142. package/template/apps/server/src/data-governance/export/scope-snapshot.ts +6 -18
  143. package/template/apps/server/src/data-governance/export/service-support.ts +188 -0
  144. package/template/apps/server/src/data-governance/export/service.ts +90 -113
  145. package/template/apps/server/src/data-governance/export/source.ts +101 -0
  146. package/template/apps/server/src/data-governance/export/types.ts +27 -0
  147. package/template/apps/server/src/document/contracts.ts +76 -0
  148. package/template/apps/server/src/document/facade.ts +339 -0
  149. package/template/apps/server/src/document/remote-runtime.test.ts +96 -0
  150. package/template/apps/server/src/document/remote-runtime.ts +223 -0
  151. package/template/apps/server/src/document/routes.ts +193 -0
  152. package/template/apps/server/src/embedding/contracts.ts +83 -0
  153. package/template/apps/server/src/embedding/error.ts +10 -0
  154. package/template/apps/server/src/embedding/facade.ts +217 -0
  155. package/template/apps/server/src/embedding/input.ts +172 -0
  156. package/template/apps/server/src/embedding/remote-runtime.test.ts +66 -0
  157. package/template/apps/server/src/embedding/remote-runtime.ts +218 -0
  158. package/template/apps/server/src/embedding/routes.ts +152 -0
  159. package/template/apps/server/src/index.ts +40 -43
  160. package/template/apps/server/src/jobs/artifact-service.test.ts +14 -2
  161. package/template/apps/server/src/jobs/artifact-service.ts +10 -0
  162. package/template/apps/server/src/jobs/drizzle-execution-repository.ts +17 -18
  163. package/template/apps/server/src/jobs/drizzle-execution.integration.test.ts +19 -1
  164. package/template/apps/server/src/jobs/drizzle-mappers.ts +4 -1
  165. package/template/apps/server/src/jobs/drizzle-recovery-repository.ts +49 -2
  166. package/template/apps/server/src/jobs/drizzle-repository.ts +1 -0
  167. package/template/apps/server/src/jobs/execution-repository.ts +17 -0
  168. package/template/apps/server/src/jobs/execution.test.ts +34 -0
  169. package/template/apps/server/src/jobs/in-memory-control.ts +135 -0
  170. package/template/apps/server/src/jobs/in-memory-execution.ts +34 -65
  171. package/template/apps/server/src/jobs/legacy-service-runtime.ts +5 -2
  172. package/template/apps/server/src/jobs/repository.ts +3 -0
  173. package/template/apps/server/src/jobs/worker.ts +80 -41
  174. package/template/apps/server/src/module-host/ai-operation-port.ts +170 -0
  175. package/template/apps/server/src/module-host/durable-context.test.ts +312 -0
  176. package/template/apps/server/src/module-host/durable-context.ts +124 -0
  177. package/template/apps/server/src/module-host/durable-handler-registration.ts +72 -0
  178. package/template/apps/server/src/module-host/governed-export-route-support.ts +171 -0
  179. package/template/apps/server/src/module-host/governed-export-routes.test.ts +327 -0
  180. package/template/apps/server/src/module-host/governed-export-routes.ts +238 -0
  181. package/template/apps/server/src/module-host/governed-renderer-registration.test.ts +247 -0
  182. package/template/apps/server/src/module-host/governed-resource-routes.ts +331 -0
  183. package/template/apps/server/src/module-host/governed-resources.test.ts +344 -0
  184. package/template/apps/server/src/module-host/governed-resources.ts +203 -0
  185. package/template/apps/server/src/module-host/http-runtime.ts +95 -0
  186. package/template/apps/server/src/module-host/initialize-contracts.ts +14 -0
  187. package/template/apps/server/src/module-host/initialize.test.ts +49 -1
  188. package/template/apps/server/src/module-host/initialize.ts +39 -16
  189. package/template/apps/server/src/module-host/job-port.test.ts +262 -0
  190. package/template/apps/server/src/module-host/job-port.ts +334 -0
  191. package/template/apps/server/src/module-host/request-context.test.ts +88 -2
  192. package/template/apps/server/src/module-host/request-context.ts +56 -22
  193. package/template/apps/server/src/module-host/route-installer.test.ts +10 -0
  194. package/template/apps/server/src/module-host/route-installer.ts +36 -1
  195. package/template/apps/server/src/module-host/sandbox-tool-registration.ts +42 -0
  196. package/template/apps/server/src/module-host/tool-handlers.ts +67 -31
  197. package/template/apps/server/src/module-host.ts +1 -0
  198. package/template/apps/server/src/module-runtime-validation.ts +7 -3
  199. package/template/apps/server/src/speech/contracts.ts +76 -0
  200. package/template/apps/server/src/speech/facade.ts +316 -0
  201. package/template/apps/server/src/speech/remote-runtime.test.ts +88 -0
  202. package/template/apps/server/src/speech/remote-runtime.ts +227 -0
  203. package/template/apps/server/src/speech/routes.ts +180 -0
  204. package/template/apps/server/src/system/actor.ts +39 -0
  205. package/template/apps/server/src/system/route-helpers.ts +1 -0
  206. package/template/apps/server/src/workflow/service.ts +2 -1
  207. package/template/apps/speech-server/Dockerfile +22 -0
  208. package/template/apps/speech-server/pyproject.toml +43 -0
  209. package/template/apps/speech-server/src/southwind_speech_server/__init__.py +1 -0
  210. package/template/apps/speech-server/src/southwind_speech_server/adapter.py +249 -0
  211. package/template/apps/speech-server/src/southwind_speech_server/app.py +183 -0
  212. package/template/apps/speech-server/src/southwind_speech_server/config.py +100 -0
  213. package/template/apps/speech-server/src/southwind_speech_server/contracts.py +103 -0
  214. package/template/apps/speech-server/src/southwind_speech_server/errors.py +6 -0
  215. package/template/apps/speech-server/src/southwind_speech_server/normalizer.py +108 -0
  216. package/template/apps/speech-server/src/southwind_speech_server/output_validation.py +128 -0
  217. package/template/apps/speech-server/src/southwind_speech_server/service.py +162 -0
  218. package/template/apps/speech-server/src/southwind_speech_server/store.py +209 -0
  219. package/template/apps/speech-server/tests/__init__.py +1 -0
  220. package/template/apps/speech-server/tests/test_config.py +33 -0
  221. package/template/apps/speech-server/tests/test_service.py +154 -0
  222. package/template/apps/speech-server/uv.lock +430 -0
  223. package/template/apps/web/src/app/business-routes.ts +22 -0
  224. package/template/apps/web/src/components/common/AccessSnapshotBoundary.vue +112 -0
  225. package/template/apps/web/src/components/common/AccessSnapshotBoundary.webtest.ts +130 -0
  226. package/template/apps/web/src/components/common/AccessSnapshotNotice.vue +117 -0
  227. package/template/apps/web/src/components/common/FormDialog.vue +6 -0
  228. package/template/apps/web/src/components/common/ModalLayout.webtest.ts +13 -0
  229. package/template/apps/web/src/composables/access-snapshot-change.ts +131 -0
  230. package/template/apps/web/src/composables/access-snapshot-change.webtest.ts +103 -0
  231. package/template/apps/web/src/composables/useAccessSnapshot.ts +48 -2
  232. package/template/apps/web/src/composables/useAccessSnapshot.webtest.ts +54 -0
  233. package/template/apps/web/src/composables/useAccessSnapshotMonitor.ts +42 -0
  234. package/template/apps/web/src/layout/AppShell.vue +28 -25
  235. package/template/apps/web/src/layout/AppShell.webtest.ts +1 -0
  236. package/template/apps/web/src/router/business-routes.ts +68 -0
  237. package/template/apps/web/src/router/business-routes.webtest.ts +85 -0
  238. package/template/apps/web/src/router/index.ts +7 -0
  239. package/template/apps/web/src/router/shared-scaffold-neutrality.webtest.ts +12 -0
  240. package/template/apps/web/src/services/access-api.ts +5 -1
  241. package/template/apps/web/src/services/access-api.webtest.ts +35 -0
  242. package/template/docker-compose.yml +127 -0
  243. package/template/docs/architecture/ai-egress.md +12 -9
  244. package/template/docs/architecture/ai-runtime.md +23 -11
  245. package/template/docs/architecture/durable-jobs.md +25 -3
  246. package/template/docs/architecture/foundation-infrastructure.md +163 -0
  247. package/template/docs/architecture/module-host.md +115 -0
  248. package/template/docs/architecture/server-module-interface.md +39 -0
  249. package/template/docs/architecture/web-access-snapshot-resilience.md +60 -0
  250. package/template/docs/development/business-web-routes.md +66 -0
  251. package/template/docs/development/custom-login-page.md +3 -3
  252. package/template/docs/development/module-lifecycle.md +71 -0
  253. package/template/docs/operations/observability.md +5 -1
  254. package/template/docs/platform/agent-runtime.md +21 -11
  255. package/template/docs/platform/agent-tool-sandbox.md +58 -0
  256. package/template/docs/platform/agent-tools.md +5 -0
  257. package/template/docs/platform/document-ai-runtime.md +204 -0
  258. package/template/docs/platform/governed-export-renderers.md +132 -0
  259. package/template/docs/platform/navigation-registry.md +5 -0
  260. package/template/docs/platform/organization-membership.md +2 -1
  261. package/template/docs/platform/speech-ai-runtime.md +156 -0
  262. package/template/docs/platform/system-crud-api.md +7 -1
  263. package/template/docs/platform/vision-ai-runtime.md +97 -0
  264. package/template/docs/platform/web-system-crud.md +6 -0
  265. package/template/package.json +16 -2
  266. package/template/packages/ai-client/index.ts +4 -0
  267. package/template/packages/ai-client/package.json +5 -2
  268. package/template/packages/ai-client/src/agent/client/artifact.ts +26 -0
  269. package/template/packages/ai-client/src/agent/client/attach.test.ts +124 -0
  270. package/template/packages/ai-client/src/agent/client/client.ts +119 -7
  271. package/template/packages/ai-client/src/agent/client/run.ts +14 -0
  272. package/template/packages/ai-client/src/agent/client/types.ts +18 -0
  273. package/template/packages/ai-client/src/agent/client/vision.test.ts +53 -0
  274. package/template/packages/ai-client/src/agent/contracts/contracts.test.ts +1 -0
  275. package/template/packages/ai-client/src/agent/contracts/events.ts +33 -0
  276. package/template/packages/ai-client/src/agent/contracts/runs.ts +71 -0
  277. package/template/packages/ai-client/src/agent/index.ts +1 -0
  278. package/template/packages/ai-client/src/artifact-ref.ts +8 -0
  279. package/template/packages/ai-client/src/document/client.test.ts +114 -0
  280. package/template/packages/ai-client/src/document/client.ts +134 -0
  281. package/template/packages/ai-client/src/document/contracts.ts +152 -0
  282. package/template/packages/ai-client/src/document/error.ts +43 -0
  283. package/template/packages/ai-client/src/document/index.ts +3 -0
  284. package/template/packages/ai-client/src/document/run.ts +104 -0
  285. package/template/packages/ai-client/src/document/validation.ts +115 -0
  286. package/template/packages/ai-client/src/embedding/client.test.ts +77 -0
  287. package/template/packages/ai-client/src/embedding/client.ts +96 -0
  288. package/template/packages/ai-client/src/embedding/contracts.ts +46 -0
  289. package/template/packages/ai-client/src/embedding/error.ts +61 -0
  290. package/template/packages/ai-client/src/embedding/index.ts +3 -0
  291. package/template/packages/ai-client/src/embedding/validation.ts +117 -0
  292. package/template/packages/ai-client/src/speech/client.test.ts +135 -0
  293. package/template/packages/ai-client/src/speech/client.ts +104 -0
  294. package/template/packages/ai-client/src/speech/contracts.ts +129 -0
  295. package/template/packages/ai-client/src/speech/error.ts +43 -0
  296. package/template/packages/ai-client/src/speech/index.ts +3 -0
  297. package/template/packages/ai-client/src/speech/run.ts +133 -0
  298. package/template/packages/ai-client/src/speech/validation.ts +132 -0
  299. package/template/packages/config/package.json +2 -2
  300. package/template/packages/database/drizzle/0039_early_ezekiel_stane.sql +1 -0
  301. package/template/packages/database/drizzle/0040_colossal_baron_strucker.sql +63 -0
  302. package/template/packages/database/drizzle/0041_yummy_prowler.sql +21 -0
  303. package/template/packages/database/drizzle/0042_puzzling_rumiko_fujikawa.sql +27 -0
  304. package/template/packages/database/drizzle/meta/0039_snapshot.json +7755 -0
  305. package/template/packages/database/drizzle/meta/0040_snapshot.json +8150 -0
  306. package/template/packages/database/drizzle/meta/0041_snapshot.json +8323 -0
  307. package/template/packages/database/drizzle/meta/0042_snapshot.json +8541 -0
  308. package/template/packages/database/drizzle/meta/_journal.json +28 -0
  309. package/template/packages/database/package.json +2 -2
  310. package/template/packages/database/src/agent-runtime-schema.test.ts +54 -0
  311. package/template/packages/database/src/schema/agent-runtime.ts +149 -0
  312. package/template/packages/database/src/schema/common.ts +2 -14
  313. package/template/packages/database/src/schema/durable-jobs.ts +5 -0
  314. package/template/packages/database/src/schema/governed-exports.ts +17 -5
  315. package/template/packages/database/src/schema/index.ts +3 -0
  316. package/template/packages/database/src/schema-durable-recovery.test.ts +31 -0
  317. package/template/packages/database/src/schema-governed-exports.test.ts +21 -1
  318. package/template/packages/database/src/schema.test.ts +6 -23
  319. package/template/packages/jobs/package.json +3 -2
  320. package/template/packages/jobs/src/handlers.test.ts +13 -0
  321. package/template/packages/jobs/src/handlers.ts +7 -1
  322. package/template/packages/jobs/src/types.ts +4 -0
  323. package/template/packages/modules/package.json +2 -2
  324. package/template/packages/modules/src/ai-operation-composition.test-support.ts +165 -0
  325. package/template/packages/modules/src/ai-operation-composition.test.ts +91 -127
  326. package/template/packages/modules/src/ai-operation-composition.ts +59 -1
  327. package/template/packages/modules/src/manifest.ts +25 -1
  328. package/template/packages/modules/src/tool-composition.ts +18 -0
  329. package/template/packages/server-sdk/index.ts +50 -0
  330. package/template/packages/server-sdk/package.json +10 -4
  331. package/template/packages/server-sdk/src/ai-artifact-port.ts +34 -0
  332. package/template/packages/server-sdk/src/ai-operation-port.ts +65 -0
  333. package/template/packages/server-sdk/src/ai-operation-registration.ts +42 -1
  334. package/template/packages/server-sdk/src/background-ai-port.ts +43 -0
  335. package/template/packages/server-sdk/src/data-governance-registration.ts +116 -0
  336. package/template/packages/server-sdk/src/durable-handler.ts +29 -0
  337. package/template/packages/server-sdk/src/jobs-port.ts +69 -0
  338. package/template/packages/server-sdk/src/module-host.ts +16 -2
  339. package/template/packages/server-sdk/src/request-context.ts +6 -2
  340. package/template/packages/server-sdk/src/sandbox-tool.ts +21 -0
  341. package/template/packages/server-sdk/src/tool-registration.ts +2 -0
  342. package/template/packages/shared/package.json +1 -1
  343. package/template/packages/shared/src/rbac.ts +30 -0
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import sqlite3
5
+ import time
6
+ import uuid
7
+
8
+ from .contracts import JsonValue
9
+ from .sqlite_serialization import encode
10
+ from .store import IdempotencyConflictError, ReleaseNotFoundError, RunRecord
11
+ from .store_cipher import RunStoreCipher
12
+
13
+
14
+ class SqliteCatalogMixin:
15
+ _database: sqlite3.Connection
16
+ _condition: asyncio.Condition
17
+ _cipher: RunStoreCipher
18
+
19
+ async def save_invocation(self, run_id: str, value: JsonValue) -> None:
20
+ async with self._condition:
21
+ updated = self._database.execute(
22
+ """
23
+ update agent_runs set invocation_cipher = ?, updated_at = ?
24
+ where run_id = ? and status not in ('completed', 'failed', 'cancelled')
25
+ """,
26
+ (self._cipher.seal(value), time.time(), run_id),
27
+ )
28
+ if updated.rowcount != 1:
29
+ raise KeyError(run_id)
30
+ self._database.commit()
31
+
32
+ async def record_evaluation(
33
+ self,
34
+ release_id: str,
35
+ evaluation_set_key: str,
36
+ evaluation_set_version: str,
37
+ metrics: dict[str, float],
38
+ ) -> str:
39
+ async with self._condition:
40
+ release = self._database.execute(
41
+ "select release_id from agent_releases where release_id = ?",
42
+ (release_id,),
43
+ ).fetchone()
44
+ if not release:
45
+ raise ReleaseNotFoundError
46
+ existing = self._database.execute(
47
+ """
48
+ select evaluation_id, metrics_json
49
+ from agent_evaluation_results
50
+ where release_id = ? and evaluation_set_key = ?
51
+ and evaluation_set_version = ?
52
+ """,
53
+ (release_id, evaluation_set_key, evaluation_set_version),
54
+ ).fetchone()
55
+ encoded = encode(metrics)
56
+ if existing:
57
+ if str(existing["metrics_json"]) != encoded:
58
+ raise IdempotencyConflictError
59
+ return str(existing["evaluation_id"])
60
+ evaluation_id = f"eval_{uuid.uuid4().hex}"
61
+ try:
62
+ self._database.execute(
63
+ """
64
+ insert into agent_evaluation_results values (?, ?, ?, ?, ?, ?)
65
+ """,
66
+ (
67
+ evaluation_id,
68
+ release_id,
69
+ evaluation_set_key,
70
+ evaluation_set_version,
71
+ encoded,
72
+ time.time(),
73
+ ),
74
+ )
75
+ self._database.commit()
76
+ except sqlite3.IntegrityError:
77
+ self._database.rollback()
78
+ raise IdempotencyConflictError from None
79
+ return evaluation_id
80
+
81
+ def _ensure_release(self, record: RunRecord) -> None:
82
+ if not isinstance(record.release, dict):
83
+ raise IdempotencyConflictError
84
+ release_id = record.release.get("releaseId")
85
+ if not isinstance(release_id, str):
86
+ raise IdempotencyConflictError
87
+ encoded = encode(record.release)
88
+ existing = self._database.execute(
89
+ """
90
+ select operation_key, snapshot_json from agent_releases
91
+ where release_id = ?
92
+ """,
93
+ (release_id,),
94
+ ).fetchone()
95
+ if existing and (
96
+ str(existing["operation_key"]) != record.operation_key
97
+ or str(existing["snapshot_json"]) != encoded
98
+ ):
99
+ raise IdempotencyConflictError
100
+ self._database.execute(
101
+ """
102
+ insert or ignore into agent_releases (
103
+ release_id, operation_key, snapshot_json, created_at
104
+ ) values (?, ?, ?, ?)
105
+ """,
106
+ (release_id, record.operation_key, encoded, time.time()),
107
+ )
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+
3
+ import sqlite3
4
+
5
+
6
+ def initialize_database(database: sqlite3.Connection) -> None:
7
+ database.execute("pragma foreign_keys = on")
8
+ database.executescript(
9
+ """
10
+ create table if not exists agent_runs (
11
+ run_id text primary key,
12
+ operation_key text not null,
13
+ input_json text not null,
14
+ idempotency_key text not null unique,
15
+ fingerprint text not null,
16
+ status text not null,
17
+ output_json text not null,
18
+ created_at real not null,
19
+ updated_at real not null,
20
+ invocation_cipher text,
21
+ release_json text not null default 'null',
22
+ trace_id text not null default ''
23
+ );
24
+ create table if not exists agent_events (
25
+ run_id text not null references agent_runs(run_id) on delete cascade,
26
+ sequence integer not null,
27
+ event_json text not null,
28
+ primary key (run_id, sequence)
29
+ );
30
+ """
31
+ )
32
+ columns = {
33
+ str(row["name"]) for row in database.execute("pragma table_info(agent_runs)")
34
+ }
35
+ if "invocation_cipher" not in columns:
36
+ database.execute("alter table agent_runs add column invocation_cipher text")
37
+ if "release_json" not in columns:
38
+ database.execute(
39
+ "alter table agent_runs add column release_json "
40
+ "text not null default 'null'"
41
+ )
42
+ if "trace_id" not in columns:
43
+ database.execute(
44
+ "alter table agent_runs add column trace_id text not null default ''"
45
+ )
46
+ database.executescript(
47
+ """
48
+ create table if not exists agent_releases (
49
+ release_id text primary key,
50
+ operation_key text not null,
51
+ snapshot_json text not null,
52
+ created_at real not null
53
+ );
54
+ create table if not exists agent_evaluation_results (
55
+ evaluation_id text primary key,
56
+ release_id text not null references agent_releases(release_id),
57
+ evaluation_set_key text not null,
58
+ evaluation_set_version text not null,
59
+ metrics_json text not null,
60
+ created_at real not null,
61
+ unique(release_id, evaluation_set_key, evaluation_set_version)
62
+ );
63
+ """
64
+ )
65
+ database.commit()
@@ -0,0 +1,16 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ from .store import CanonicalEvent
6
+
7
+
8
+ def encode(value: object) -> str:
9
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
10
+
11
+
12
+ def sequence_of(event: CanonicalEvent) -> int:
13
+ value = event.get("sequence")
14
+ if not isinstance(value, int):
15
+ raise RuntimeError("Agent Event sequence 无效")
16
+ return value
@@ -8,24 +8,29 @@ from pathlib import Path
8
8
  from typing import cast
9
9
 
10
10
  from .contracts import AgentError, AgentErrorCode, JsonValue, RunStatus
11
+ from .sqlite_catalog import SqliteCatalogMixin
12
+ from .sqlite_schema import initialize_database
13
+ from .sqlite_serialization import encode, sequence_of
11
14
  from .store import (
12
15
  CanonicalEvent,
13
16
  IdempotencyConflictError,
14
17
  RunRecord,
15
18
  status_for,
16
19
  )
20
+ from .store_cipher import RunStoreCipher
17
21
 
18
22
 
19
23
  class RunStoreCapacityError(Exception):
20
24
  pass
21
25
 
22
26
 
23
- class SqliteRunStore:
27
+ class SqliteRunStore(SqliteCatalogMixin):
24
28
  """Single-replica durable Run store with bounded rows, events and output."""
25
29
 
26
30
  def __init__(
27
31
  self,
28
32
  path: str,
33
+ cipher: RunStoreCipher,
29
34
  *,
30
35
  max_runs: int = 10_000,
31
36
  max_events_per_run: int = 2_000,
@@ -40,10 +45,15 @@ class SqliteRunStore:
40
45
  self._max_output_bytes = max_output_bytes
41
46
  self._terminal_ttl = terminal_ttl_seconds
42
47
  self._condition = asyncio.Condition()
43
- self._initialize()
48
+ self._cipher = cipher
49
+ initialize_database(self._database)
44
50
  self._recover_incomplete()
45
51
 
46
- async def create_or_get(self, record: RunRecord) -> tuple[RunRecord, bool]:
52
+ async def create_or_get(
53
+ self,
54
+ record: RunRecord,
55
+ invocation: JsonValue = None,
56
+ ) -> tuple[RunRecord, bool]:
47
57
  async with self._condition:
48
58
  existing = self._database.execute(
49
59
  "select run_id from agent_runs where idempotency_key = ?",
@@ -61,24 +71,35 @@ class SqliteRunStore:
61
71
  if count >= self._max_runs:
62
72
  raise RunStoreCapacityError("Agent Run Store 容量已满")
63
73
  now = time.time()
64
- self._database.execute(
65
- """
66
- insert into agent_runs (
67
- run_id, operation_key, input_json, idempotency_key,
68
- fingerprint, status, output_json, created_at, updated_at
69
- ) values (?, ?, ?, ?, ?, 'accepted', 'null', ?, ?)
70
- """,
71
- (
72
- record.run_id,
73
- record.operation_key,
74
- "null",
75
- record.idempotency_key,
76
- record.fingerprint,
77
- now,
78
- now,
79
- ),
80
- )
81
- self._database.commit()
74
+ try:
75
+ self._database.execute(
76
+ """
77
+ insert into agent_runs (
78
+ run_id, operation_key, input_json, idempotency_key,
79
+ fingerprint, status, output_json, created_at, updated_at,
80
+ release_json, trace_id, invocation_cipher
81
+ ) values (?, ?, ?, ?, ?, 'accepted', 'null', ?, ?, ?, ?, ?)
82
+ """,
83
+ (
84
+ record.run_id,
85
+ record.operation_key,
86
+ "null",
87
+ record.idempotency_key,
88
+ record.fingerprint,
89
+ now,
90
+ now,
91
+ encode(record.release),
92
+ record.trace_id,
93
+ self._cipher.seal(invocation)
94
+ if invocation is not None
95
+ else None,
96
+ ),
97
+ )
98
+ self._ensure_release(record)
99
+ self._database.commit()
100
+ except Exception:
101
+ self._database.rollback()
102
+ raise
82
103
  self._condition.notify_all()
83
104
  return record, True
84
105
 
@@ -168,38 +189,58 @@ class SqliteRunStore:
168
189
  record.terminal,
169
190
  )
170
191
 
171
- def close(self) -> None:
172
- self._database.close()
192
+ async def recoverable(self) -> list[tuple[RunRecord, JsonValue]]:
193
+ async with self._condition:
194
+ rows = self._database.execute(
195
+ """
196
+ select run_id, invocation_cipher from agent_runs
197
+ where status not in ('completed', 'failed', 'cancelled')
198
+ and invocation_cipher is not null
199
+ order by created_at
200
+ """
201
+ ).fetchall()
202
+ result: list[tuple[RunRecord, JsonValue]] = []
203
+ for row in rows:
204
+ record = self._read(str(row["run_id"]))
205
+ if record is not None:
206
+ result.append(
207
+ (
208
+ record,
209
+ self._cipher.open(str(row["invocation_cipher"])),
210
+ )
211
+ )
212
+ return result
173
213
 
174
- def _initialize(self) -> None:
175
- self._database.executescript(
176
- """
177
- create table if not exists agent_runs (
178
- run_id text primary key,
179
- operation_key text not null,
180
- input_json text not null,
181
- idempotency_key text not null unique,
182
- fingerprint text not null,
183
- status text not null,
184
- output_json text not null,
185
- created_at real not null,
186
- updated_at real not null
187
- );
188
- create table if not exists agent_events (
189
- run_id text not null references agent_runs(run_id) on delete cascade,
190
- sequence integer not null,
191
- event_json text not null,
192
- primary key (run_id, sequence)
193
- );
194
- """
195
- )
196
- self._database.commit()
214
+ async def clear_invocation(self, run_id: str) -> None:
215
+ async with self._condition:
216
+ self._database.execute(
217
+ "update agent_runs set invocation_cipher = null where run_id = ?",
218
+ (run_id,),
219
+ )
220
+ self._database.commit()
221
+
222
+ async def renew_lease(self, run_id: str) -> None:
223
+ record = await self.get(run_id)
224
+ if record is None or record.terminal:
225
+ raise KeyError(run_id)
226
+
227
+ async def release_lease(self, run_id: str) -> None:
228
+ if await self.get(run_id) is None:
229
+ raise KeyError(run_id)
230
+
231
+ async def probe(self) -> None:
232
+ async with self._condition:
233
+ self._database.execute("select 1").fetchone()
234
+
235
+ async def close(self) -> None:
236
+ self._database.close()
197
237
 
198
238
  def _recover_incomplete(self) -> None:
199
239
  rows = self._database.execute(
200
240
  """
201
241
  select run_id from agent_runs
202
242
  where status not in ('completed', 'failed', 'cancelled')
243
+ and invocation_cipher is null
203
244
  """
204
245
  ).fetchall()
205
246
  for row in rows:
@@ -248,6 +289,8 @@ class SqliteRunStore:
248
289
  status=cast(RunStatus, str(row["status"])),
249
290
  events=events,
250
291
  output=json.loads(str(row["output_json"])),
292
+ release=json.loads(str(row["release_json"])),
293
+ trace_id=str(row["trace_id"]),
251
294
  )
252
295
 
253
296
  def _insert_event(
@@ -304,14 +347,3 @@ class SqliteRunStore:
304
347
  (cutoff,),
305
348
  )
306
349
  self._database.commit()
307
-
308
-
309
- def encode(value: object) -> str:
310
- return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
311
-
312
-
313
- def sequence_of(event: CanonicalEvent) -> int:
314
- value = event.get("sequence")
315
- if not isinstance(value, int):
316
- raise RuntimeError("Agent Event sequence 无效")
317
- return value
@@ -17,6 +17,8 @@ class RunRecord:
17
17
  input: JsonValue
18
18
  idempotency_key: str
19
19
  fingerprint: str
20
+ release: JsonValue = None
21
+ trace_id: str = ""
20
22
  status: RunStatus = "accepted"
21
23
  events: list[CanonicalEvent] = field(default_factory=list)
22
24
  output: JsonValue = None
@@ -30,8 +32,16 @@ class IdempotencyConflictError(Exception):
30
32
  pass
31
33
 
32
34
 
35
+ class ReleaseNotFoundError(Exception):
36
+ pass
37
+
38
+
33
39
  class RunStore(Protocol):
34
- async def create_or_get(self, record: RunRecord) -> tuple[RunRecord, bool]: ...
40
+ async def create_or_get(
41
+ self,
42
+ record: RunRecord,
43
+ invocation: JsonValue = None,
44
+ ) -> tuple[RunRecord, bool]: ...
35
45
  async def get(self, run_id: str) -> RunRecord | None: ...
36
46
  async def append(
37
47
  self,
@@ -46,6 +56,20 @@ class RunStore(Protocol):
46
56
  after: int,
47
57
  wait_seconds: float,
48
58
  ) -> tuple[list[CanonicalEvent], bool]: ...
59
+ async def save_invocation(self, run_id: str, value: JsonValue) -> None: ...
60
+ async def recoverable(self) -> list[tuple[RunRecord, JsonValue]]: ...
61
+ async def clear_invocation(self, run_id: str) -> None: ...
62
+ async def record_evaluation(
63
+ self,
64
+ release_id: str,
65
+ evaluation_set_key: str,
66
+ evaluation_set_version: str,
67
+ metrics: dict[str, float],
68
+ ) -> str: ...
69
+ async def renew_lease(self, run_id: str) -> None: ...
70
+ async def release_lease(self, run_id: str) -> None: ...
71
+ async def probe(self) -> None: ...
72
+ async def close(self) -> None: ...
49
73
 
50
74
 
51
75
  class InMemoryRunStore:
@@ -55,8 +79,15 @@ class InMemoryRunStore:
55
79
  self._runs: dict[str, RunRecord] = {}
56
80
  self._idempotency: dict[str, str] = {}
57
81
  self._condition = asyncio.Condition()
82
+ self._invocations: dict[str, JsonValue] = {}
83
+ self._evaluations: dict[tuple[str, str, str], tuple[str, dict[str, float]]] = {}
84
+ self._releases: dict[str, tuple[str, JsonValue]] = {}
58
85
 
59
- async def create_or_get(self, record: RunRecord) -> tuple[RunRecord, bool]:
86
+ async def create_or_get(
87
+ self,
88
+ record: RunRecord,
89
+ invocation: JsonValue = None,
90
+ ) -> tuple[RunRecord, bool]:
60
91
  async with self._condition:
61
92
  existing_id = self._idempotency.get(record.idempotency_key)
62
93
  if existing_id is not None:
@@ -64,8 +95,16 @@ class InMemoryRunStore:
64
95
  if existing.fingerprint != record.fingerprint:
65
96
  raise IdempotencyConflictError
66
97
  return existing, False
98
+ release_id = release_id_of(record)
99
+ release = (record.operation_key, record.release)
100
+ existing_release = self._releases.get(release_id)
101
+ if existing_release is not None and existing_release != release:
102
+ raise IdempotencyConflictError
103
+ self._releases[release_id] = release
67
104
  self._runs[record.run_id] = record
68
105
  self._idempotency[record.idempotency_key] = record.run_id
106
+ if invocation is not None:
107
+ self._invocations[record.run_id] = invocation
69
108
  self._condition.notify_all()
70
109
  return record, True
71
110
 
@@ -131,14 +170,78 @@ class InMemoryRunStore:
131
170
  return [], False
132
171
  return list(record.events[after:]), record.terminal
133
172
 
173
+ async def save_invocation(self, run_id: str, value: JsonValue) -> None:
174
+ async with self._condition:
175
+ if run_id not in self._runs:
176
+ raise KeyError(run_id)
177
+ self._invocations[run_id] = value
178
+
179
+ async def recoverable(self) -> list[tuple[RunRecord, JsonValue]]:
180
+ async with self._condition:
181
+ return [
182
+ (record, self._invocations[run_id])
183
+ for run_id, record in self._runs.items()
184
+ if not record.terminal and run_id in self._invocations
185
+ ]
186
+
187
+ async def clear_invocation(self, run_id: str) -> None:
188
+ async with self._condition:
189
+ self._invocations.pop(run_id, None)
190
+
191
+ async def record_evaluation(
192
+ self,
193
+ release_id: str,
194
+ evaluation_set_key: str,
195
+ evaluation_set_version: str,
196
+ metrics: dict[str, float],
197
+ ) -> str:
198
+ async with self._condition:
199
+ if release_id not in self._releases:
200
+ raise ReleaseNotFoundError
201
+ key = (release_id, evaluation_set_key, evaluation_set_version)
202
+ existing = self._evaluations.get(key)
203
+ if existing:
204
+ if existing[1] != metrics:
205
+ raise IdempotencyConflictError
206
+ return existing[0]
207
+ evaluation_id = f"eval_{len(self._evaluations) + 1}"
208
+ self._evaluations[key] = (evaluation_id, metrics)
209
+ return evaluation_id
210
+
211
+ async def renew_lease(self, run_id: str) -> None:
212
+ record = self._runs.get(run_id)
213
+ if record is None or record.terminal:
214
+ raise KeyError(run_id)
215
+
216
+ async def release_lease(self, run_id: str) -> None:
217
+ if run_id not in self._runs:
218
+ raise KeyError(run_id)
219
+
220
+ async def probe(self) -> None:
221
+ return None
222
+
223
+ async def close(self) -> None:
224
+ return None
225
+
134
226
 
135
227
  def status_for(event_type: str, current: RunStatus) -> RunStatus:
136
228
  if event_type == "run.started":
137
229
  return "running"
138
230
  if event_type == "approval.required":
139
231
  return "awaiting-approval"
232
+ if event_type == "approval.resolved":
233
+ return "running"
140
234
  if event_type == "run.failed":
141
235
  return "failed"
142
236
  if event_type == "run.cancelled":
143
237
  return "cancelled"
144
238
  return current
239
+
240
+
241
+ def release_id_of(record: RunRecord) -> str:
242
+ if not isinstance(record.release, dict):
243
+ raise IdempotencyConflictError
244
+ release_id = record.release.get("releaseId")
245
+ if not isinstance(release_id, str):
246
+ raise IdempotencyConflictError
247
+ return release_id
@@ -0,0 +1,40 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from typing import cast
8
+
9
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
10
+
11
+ from .contracts import JsonValue
12
+
13
+
14
+ class RunStoreCipher:
15
+ def __init__(self, secret: str) -> None:
16
+ if len(secret.encode()) < 32:
17
+ raise ValueError("AGENT_RUN_ENCRYPTION_SECRET 至少需要 32 字节")
18
+ self._key = hashlib.sha256(secret.encode()).digest()
19
+
20
+ def seal(self, value: JsonValue) -> str:
21
+ nonce = os.urandom(12)
22
+ body = json.dumps(
23
+ value,
24
+ ensure_ascii=False,
25
+ separators=(",", ":"),
26
+ ).encode()
27
+ encrypted = AESGCM(self._key).encrypt(nonce, body, b"agent-run-v1")
28
+ return base64.urlsafe_b64encode(nonce + encrypted).decode()
29
+
30
+ def open(self, value: str) -> JsonValue:
31
+ try:
32
+ raw = base64.urlsafe_b64decode(value.encode())
33
+ body = AESGCM(self._key).decrypt(
34
+ raw[:12],
35
+ raw[12:],
36
+ b"agent-run-v1",
37
+ )
38
+ return cast(JsonValue, json.loads(body))
39
+ except Exception as error:
40
+ raise ValueError("Agent Run 加密上下文无法解密") from error
@@ -0,0 +1,89 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import secrets
5
+ from contextvars import Token
6
+
7
+ from opentelemetry import context, trace
8
+ from opentelemetry.context import Context
9
+ from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
10
+ from opentelemetry.sdk.resources import Resource
11
+ from opentelemetry.sdk.trace import TracerProvider
12
+ from opentelemetry.sdk.trace.export import BatchSpanProcessor
13
+ from opentelemetry.trace import (
14
+ NonRecordingSpan,
15
+ Span,
16
+ SpanContext,
17
+ TraceFlags,
18
+ )
19
+
20
+ from .store import RunRecord
21
+
22
+ _configured = False
23
+
24
+
25
+ def configure_telemetry() -> None:
26
+ global _configured
27
+ endpoint = os.environ.get("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT", "").strip()
28
+ if _configured or not endpoint:
29
+ return
30
+ provider = TracerProvider(
31
+ resource=Resource.create({"service.name": "southwind-agent-server"})
32
+ )
33
+ provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter(endpoint=endpoint)))
34
+ trace.set_tracer_provider(provider)
35
+ _configured = True
36
+
37
+
38
+ class AgentRunTrace:
39
+ """OTel span carrying metadata only; prompts, inputs and outputs stay excluded."""
40
+
41
+ def __init__(self, record: RunRecord) -> None:
42
+ parent = parent_context(record.trace_id)
43
+ self._span: Span = trace.get_tracer(__name__).start_span(
44
+ "agent.run",
45
+ context=parent,
46
+ attributes={
47
+ "agent.run.id": record.run_id,
48
+ "agent.operation.key": record.operation_key,
49
+ "agent.release.id": release_id(record),
50
+ },
51
+ )
52
+ self._token: Token[Context] | None = None
53
+
54
+ def start(self) -> None:
55
+ self._token = context.attach(trace.set_span_in_context(self._span))
56
+
57
+ def finish(self, status: str) -> None:
58
+ self._span.set_attribute("agent.run.status", status)
59
+ if self._token is not None:
60
+ context.detach(self._token)
61
+ self._span.end()
62
+
63
+
64
+ def parent_context(trace_id: str) -> context.Context | None:
65
+ if len(trace_id) != 32:
66
+ return None
67
+ try:
68
+ parsed = int(trace_id, 16)
69
+ except ValueError:
70
+ return None
71
+ if parsed == 0:
72
+ return None
73
+ parent = NonRecordingSpan(
74
+ SpanContext(
75
+ trace_id=parsed,
76
+ span_id=secrets.randbits(64) or 1,
77
+ is_remote=True,
78
+ trace_flags=TraceFlags(TraceFlags.SAMPLED),
79
+ )
80
+ )
81
+ return trace.set_span_in_context(parent)
82
+
83
+
84
+ def release_id(record: RunRecord) -> str:
85
+ if isinstance(record.release, dict):
86
+ value = record.release.get("releaseId")
87
+ if isinstance(value, str):
88
+ return value
89
+ return "unknown"