flock-core 0.4.542__py3-none-any.whl → 0.5.0__py3-none-any.whl

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.

Potentially problematic release.


This version of flock-core might be problematic. Click here for more details.

Files changed (501) hide show
  1. flock/__init__.py +12 -217
  2. flock/agent.py +1079 -0
  3. flock/api/themes.py +71 -0
  4. flock/artifacts.py +86 -0
  5. flock/cli.py +147 -0
  6. flock/components.py +189 -0
  7. flock/dashboard/__init__.py +30 -0
  8. flock/dashboard/collector.py +559 -0
  9. flock/dashboard/events.py +188 -0
  10. flock/dashboard/graph_builder.py +563 -0
  11. flock/dashboard/launcher.py +235 -0
  12. flock/dashboard/models/graph.py +156 -0
  13. flock/dashboard/service.py +991 -0
  14. flock/dashboard/static_v2/assets/index-DFRnI_mt.js +111 -0
  15. flock/dashboard/static_v2/assets/index-fPLNdmp1.css +1 -0
  16. flock/dashboard/static_v2/index.html +13 -0
  17. flock/dashboard/websocket.py +246 -0
  18. flock/engines/__init__.py +6 -0
  19. flock/engines/dspy_engine.py +932 -0
  20. flock/examples.py +131 -0
  21. flock/frontend/README.md +778 -0
  22. flock/frontend/docs/DESIGN_SYSTEM.md +1980 -0
  23. flock/frontend/index.html +12 -0
  24. flock/frontend/package-lock.json +4337 -0
  25. flock/frontend/package.json +48 -0
  26. flock/frontend/src/App.tsx +139 -0
  27. flock/frontend/src/__tests__/integration/graph-snapshot.test.tsx +647 -0
  28. flock/frontend/src/__tests__/integration/indexeddb-persistence.test.tsx +699 -0
  29. flock/frontend/src/components/common/BuildInfo.tsx +39 -0
  30. flock/frontend/src/components/common/EmptyState.module.css +115 -0
  31. flock/frontend/src/components/common/EmptyState.tsx +128 -0
  32. flock/frontend/src/components/common/ErrorBoundary.module.css +169 -0
  33. flock/frontend/src/components/common/ErrorBoundary.tsx +118 -0
  34. flock/frontend/src/components/common/KeyboardShortcutsDialog.css +251 -0
  35. flock/frontend/src/components/common/KeyboardShortcutsDialog.tsx +151 -0
  36. flock/frontend/src/components/common/LoadingSpinner.module.css +97 -0
  37. flock/frontend/src/components/common/LoadingSpinner.tsx +29 -0
  38. flock/frontend/src/components/controls/PublishControl.css +547 -0
  39. flock/frontend/src/components/controls/PublishControl.test.tsx +543 -0
  40. flock/frontend/src/components/controls/PublishControl.tsx +432 -0
  41. flock/frontend/src/components/details/DetailWindowContainer.tsx +58 -0
  42. flock/frontend/src/components/details/LiveOutputTab.test.tsx +792 -0
  43. flock/frontend/src/components/details/LiveOutputTab.tsx +220 -0
  44. flock/frontend/src/components/details/MessageDetailWindow.tsx +439 -0
  45. flock/frontend/src/components/details/MessageHistoryTab.tsx +374 -0
  46. flock/frontend/src/components/details/NodeDetailWindow.test.tsx +501 -0
  47. flock/frontend/src/components/details/NodeDetailWindow.tsx +218 -0
  48. flock/frontend/src/components/details/RunStatusTab.tsx +348 -0
  49. flock/frontend/src/components/details/tabs.test.tsx +1015 -0
  50. flock/frontend/src/components/filters/ArtifactTypeFilter.tsx +21 -0
  51. flock/frontend/src/components/filters/CorrelationIDFilter.module.css +102 -0
  52. flock/frontend/src/components/filters/CorrelationIDFilter.test.tsx +197 -0
  53. flock/frontend/src/components/filters/CorrelationIDFilter.tsx +121 -0
  54. flock/frontend/src/components/filters/FilterFlyout.module.css +104 -0
  55. flock/frontend/src/components/filters/FilterFlyout.tsx +80 -0
  56. flock/frontend/src/components/filters/FilterPills.module.css +220 -0
  57. flock/frontend/src/components/filters/FilterPills.test.tsx +189 -0
  58. flock/frontend/src/components/filters/FilterPills.tsx +143 -0
  59. flock/frontend/src/components/filters/ProducerFilter.tsx +21 -0
  60. flock/frontend/src/components/filters/SavedFiltersControl.module.css +60 -0
  61. flock/frontend/src/components/filters/SavedFiltersControl.test.tsx +158 -0
  62. flock/frontend/src/components/filters/SavedFiltersControl.tsx +159 -0
  63. flock/frontend/src/components/filters/TagFilter.tsx +21 -0
  64. flock/frontend/src/components/filters/TimeRangeFilter.module.css +115 -0
  65. flock/frontend/src/components/filters/TimeRangeFilter.test.tsx +154 -0
  66. flock/frontend/src/components/filters/TimeRangeFilter.tsx +110 -0
  67. flock/frontend/src/components/filters/VisibilityFilter.tsx +21 -0
  68. flock/frontend/src/components/graph/AgentNode.test.tsx +77 -0
  69. flock/frontend/src/components/graph/AgentNode.tsx +324 -0
  70. flock/frontend/src/components/graph/GraphCanvas.tsx +613 -0
  71. flock/frontend/src/components/graph/MessageFlowEdge.tsx +128 -0
  72. flock/frontend/src/components/graph/MessageNode.test.tsx +64 -0
  73. flock/frontend/src/components/graph/MessageNode.tsx +129 -0
  74. flock/frontend/src/components/graph/MiniMap.tsx +47 -0
  75. flock/frontend/src/components/graph/TransformEdge.tsx +123 -0
  76. flock/frontend/src/components/layout/DashboardLayout.css +420 -0
  77. flock/frontend/src/components/layout/DashboardLayout.tsx +287 -0
  78. flock/frontend/src/components/layout/Header.module.css +88 -0
  79. flock/frontend/src/components/layout/Header.tsx +52 -0
  80. flock/frontend/src/components/modules/HistoricalArtifactsModule.module.css +288 -0
  81. flock/frontend/src/components/modules/HistoricalArtifactsModule.tsx +450 -0
  82. flock/frontend/src/components/modules/HistoricalArtifactsModuleWrapper.tsx +13 -0
  83. flock/frontend/src/components/modules/JsonAttributeRenderer.tsx +140 -0
  84. flock/frontend/src/components/modules/ModuleRegistry.test.ts +333 -0
  85. flock/frontend/src/components/modules/ModuleRegistry.ts +93 -0
  86. flock/frontend/src/components/modules/ModuleWindow.tsx +223 -0
  87. flock/frontend/src/components/modules/TraceModuleJaeger.tsx +1971 -0
  88. flock/frontend/src/components/modules/TraceModuleJaegerWrapper.tsx +13 -0
  89. flock/frontend/src/components/modules/registerModules.ts +29 -0
  90. flock/frontend/src/components/settings/AdvancedSettings.tsx +175 -0
  91. flock/frontend/src/components/settings/AppearanceSettings.tsx +185 -0
  92. flock/frontend/src/components/settings/GraphSettings.tsx +110 -0
  93. flock/frontend/src/components/settings/MultiSelect.tsx +235 -0
  94. flock/frontend/src/components/settings/SettingsPanel.css +327 -0
  95. flock/frontend/src/components/settings/SettingsPanel.tsx +131 -0
  96. flock/frontend/src/components/settings/ThemeSelector.tsx +298 -0
  97. flock/frontend/src/components/settings/TracingSettings.tsx +404 -0
  98. flock/frontend/src/hooks/useKeyboardShortcuts.ts +148 -0
  99. flock/frontend/src/hooks/useModulePersistence.test.ts +442 -0
  100. flock/frontend/src/hooks/useModulePersistence.ts +154 -0
  101. flock/frontend/src/hooks/useModules.ts +157 -0
  102. flock/frontend/src/hooks/usePersistence.ts +141 -0
  103. flock/frontend/src/main.tsx +13 -0
  104. flock/frontend/src/services/api.ts +337 -0
  105. flock/frontend/src/services/graphService.test.ts +330 -0
  106. flock/frontend/src/services/graphService.ts +75 -0
  107. flock/frontend/src/services/indexeddb.test.ts +793 -0
  108. flock/frontend/src/services/indexeddb.ts +848 -0
  109. flock/frontend/src/services/layout.test.ts +437 -0
  110. flock/frontend/src/services/layout.ts +357 -0
  111. flock/frontend/src/services/themeApplicator.ts +140 -0
  112. flock/frontend/src/services/themeService.ts +77 -0
  113. flock/frontend/src/services/websocket.ts +650 -0
  114. flock/frontend/src/store/filterStore.test.ts +250 -0
  115. flock/frontend/src/store/filterStore.ts +272 -0
  116. flock/frontend/src/store/graphStore.test.ts +570 -0
  117. flock/frontend/src/store/graphStore.ts +462 -0
  118. flock/frontend/src/store/moduleStore.test.ts +253 -0
  119. flock/frontend/src/store/moduleStore.ts +75 -0
  120. flock/frontend/src/store/settingsStore.ts +188 -0
  121. flock/frontend/src/store/streamStore.ts +68 -0
  122. flock/frontend/src/store/uiStore.test.ts +54 -0
  123. flock/frontend/src/store/uiStore.ts +122 -0
  124. flock/frontend/src/store/wsStore.ts +34 -0
  125. flock/frontend/src/styles/index.css +15 -0
  126. flock/frontend/src/styles/scrollbar.css +47 -0
  127. flock/frontend/src/styles/variables.css +488 -0
  128. flock/frontend/src/test/setup.ts +1 -0
  129. flock/frontend/src/types/filters.ts +47 -0
  130. flock/frontend/src/types/graph.ts +95 -0
  131. flock/frontend/src/types/modules.ts +10 -0
  132. flock/frontend/src/types/theme.ts +55 -0
  133. flock/frontend/src/utils/artifacts.ts +24 -0
  134. flock/frontend/src/utils/mockData.ts +98 -0
  135. flock/frontend/src/utils/performance.ts +16 -0
  136. flock/frontend/src/vite-env.d.ts +17 -0
  137. flock/frontend/tsconfig.json +27 -0
  138. flock/frontend/tsconfig.node.json +11 -0
  139. flock/frontend/vite.config.ts +25 -0
  140. flock/frontend/vitest.config.ts +11 -0
  141. flock/{core/util → helper}/cli_helper.py +9 -5
  142. flock/{core/logging → logging}/__init__.py +2 -3
  143. flock/logging/auto_trace.py +159 -0
  144. flock/{core/logging → logging}/formatters/enum_builder.py +3 -4
  145. flock/{core/logging → logging}/formatters/theme_builder.py +19 -44
  146. flock/{core/logging → logging}/formatters/themed_formatter.py +69 -107
  147. flock/{core/logging → logging}/logging.py +78 -61
  148. flock/{core/logging → logging}/telemetry.py +66 -26
  149. flock/{core/logging → logging}/telemetry_exporter/base_exporter.py +2 -2
  150. flock/logging/telemetry_exporter/duckdb_exporter.py +216 -0
  151. flock/{core/logging → logging}/telemetry_exporter/file_exporter.py +13 -10
  152. flock/{core/logging → logging}/telemetry_exporter/sqlite_exporter.py +2 -3
  153. flock/logging/trace_and_logged.py +304 -0
  154. flock/mcp/__init__.py +91 -0
  155. flock/{core/mcp/mcp_client.py → mcp/client.py} +131 -158
  156. flock/{core/mcp/mcp_config.py → mcp/config.py} +86 -132
  157. flock/mcp/manager.py +286 -0
  158. flock/mcp/servers/sse/__init__.py +1 -1
  159. flock/mcp/servers/sse/flock_sse_server.py +16 -58
  160. flock/mcp/servers/stdio/__init__.py +1 -1
  161. flock/mcp/servers/stdio/flock_stdio_server.py +13 -53
  162. flock/mcp/servers/streamable_http/flock_streamable_http_server.py +22 -67
  163. flock/mcp/servers/websockets/flock_websocket_server.py +12 -45
  164. flock/{core/mcp/flock_mcp_tool_base.py → mcp/tool.py} +24 -78
  165. flock/mcp/types/__init__.py +42 -0
  166. flock/{core/mcp → mcp}/types/callbacks.py +12 -15
  167. flock/{core/mcp → mcp}/types/factories.py +7 -6
  168. flock/{core/mcp → mcp}/types/handlers.py +13 -18
  169. flock/{core/mcp → mcp}/types/types.py +70 -74
  170. flock/{core/mcp → mcp}/util/helpers.py +3 -3
  171. flock/orchestrator.py +970 -0
  172. flock/registry.py +148 -0
  173. flock/runtime.py +262 -0
  174. flock/service.py +277 -0
  175. flock/store.py +1214 -0
  176. flock/subscription.py +111 -0
  177. flock/themes/andromeda.toml +1 -1
  178. flock/themes/apple-system-colors.toml +1 -1
  179. flock/themes/arcoiris.toml +1 -1
  180. flock/themes/atomonelight.toml +1 -1
  181. flock/themes/ayu copy.toml +1 -1
  182. flock/themes/ayu-light.toml +1 -1
  183. flock/themes/belafonte-day.toml +1 -1
  184. flock/themes/belafonte-night.toml +1 -1
  185. flock/themes/blulocodark.toml +1 -1
  186. flock/themes/breeze.toml +1 -1
  187. flock/themes/broadcast.toml +1 -1
  188. flock/themes/brogrammer.toml +1 -1
  189. flock/themes/builtin-dark.toml +1 -1
  190. flock/themes/builtin-pastel-dark.toml +1 -1
  191. flock/themes/catppuccin-latte.toml +1 -1
  192. flock/themes/catppuccin-macchiato.toml +1 -1
  193. flock/themes/catppuccin-mocha.toml +1 -1
  194. flock/themes/cga.toml +1 -1
  195. flock/themes/chalk.toml +1 -1
  196. flock/themes/ciapre.toml +1 -1
  197. flock/themes/coffee-theme.toml +1 -1
  198. flock/themes/cyberpunkscarletprotocol.toml +1 -1
  199. flock/themes/dark+.toml +1 -1
  200. flock/themes/darkermatrix.toml +1 -1
  201. flock/themes/darkmatrix.toml +2 -2
  202. flock/themes/darkside.toml +1 -1
  203. flock/themes/deep.toml +2 -2
  204. flock/themes/desert.toml +1 -1
  205. flock/themes/django.toml +1 -1
  206. flock/themes/djangosmooth.toml +1 -1
  207. flock/themes/doomone.toml +1 -1
  208. flock/themes/dotgov.toml +1 -1
  209. flock/themes/dracula+.toml +1 -1
  210. flock/themes/duckbones.toml +1 -1
  211. flock/themes/encom.toml +1 -1
  212. flock/themes/espresso.toml +1 -1
  213. flock/themes/everblush.toml +1 -1
  214. flock/themes/fairyfloss.toml +1 -1
  215. flock/themes/fideloper.toml +1 -1
  216. flock/themes/fishtank.toml +1 -1
  217. flock/themes/flexoki-light.toml +1 -1
  218. flock/themes/floraverse.toml +1 -1
  219. flock/themes/framer.toml +1 -1
  220. flock/themes/galizur.toml +1 -1
  221. flock/themes/github.toml +1 -1
  222. flock/themes/grass.toml +1 -1
  223. flock/themes/grey-green.toml +1 -1
  224. flock/themes/gruvboxlight.toml +1 -1
  225. flock/themes/guezwhoz.toml +1 -1
  226. flock/themes/harper.toml +1 -1
  227. flock/themes/hax0r-blue.toml +1 -1
  228. flock/themes/hopscotch.256.toml +1 -1
  229. flock/themes/ic-green-ppl.toml +1 -1
  230. flock/themes/iceberg-dark.toml +1 -1
  231. flock/themes/japanesque.toml +1 -1
  232. flock/themes/jubi.toml +1 -1
  233. flock/themes/kibble.toml +1 -1
  234. flock/themes/kolorit.toml +1 -1
  235. flock/themes/kurokula.toml +1 -1
  236. flock/themes/materialdesigncolors.toml +1 -1
  237. flock/themes/matrix.toml +1 -1
  238. flock/themes/mellifluous.toml +1 -1
  239. flock/themes/midnight-in-mojave.toml +1 -1
  240. flock/themes/monokai-remastered.toml +1 -1
  241. flock/themes/monokai-soda.toml +1 -1
  242. flock/themes/neon.toml +1 -1
  243. flock/themes/neopolitan.toml +5 -5
  244. flock/themes/nord-light.toml +1 -1
  245. flock/themes/ocean.toml +1 -1
  246. flock/themes/onehalfdark.toml +1 -1
  247. flock/themes/onehalflight.toml +1 -1
  248. flock/themes/palenighthc.toml +1 -1
  249. flock/themes/paulmillr.toml +1 -1
  250. flock/themes/pencildark.toml +1 -1
  251. flock/themes/pnevma.toml +1 -1
  252. flock/themes/purple-rain.toml +1 -1
  253. flock/themes/purplepeter.toml +1 -1
  254. flock/themes/raycast-dark.toml +1 -1
  255. flock/themes/red-sands.toml +1 -1
  256. flock/themes/relaxed.toml +1 -1
  257. flock/themes/retro.toml +1 -1
  258. flock/themes/rose-pine.toml +1 -1
  259. flock/themes/royal.toml +1 -1
  260. flock/themes/ryuuko.toml +1 -1
  261. flock/themes/sakura.toml +1 -1
  262. flock/themes/scarlet-protocol.toml +1 -1
  263. flock/themes/seoulbones-dark.toml +1 -1
  264. flock/themes/shades-of-purple.toml +1 -1
  265. flock/themes/smyck.toml +1 -1
  266. flock/themes/softserver.toml +1 -1
  267. flock/themes/solarized-darcula.toml +1 -1
  268. flock/themes/square.toml +1 -1
  269. flock/themes/sugarplum.toml +1 -1
  270. flock/themes/thayer-bright.toml +1 -1
  271. flock/themes/tokyonight.toml +1 -1
  272. flock/themes/tomorrow.toml +1 -1
  273. flock/themes/ubuntu.toml +1 -1
  274. flock/themes/ultradark.toml +1 -1
  275. flock/themes/ultraviolent.toml +1 -1
  276. flock/themes/unikitty.toml +1 -1
  277. flock/themes/urple.toml +1 -1
  278. flock/themes/vesper.toml +1 -1
  279. flock/themes/vimbones.toml +1 -1
  280. flock/themes/wildcherry.toml +1 -1
  281. flock/themes/wilmersdorf.toml +1 -1
  282. flock/themes/wryan.toml +1 -1
  283. flock/themes/xcodedarkhc.toml +1 -1
  284. flock/themes/xcodelight.toml +1 -1
  285. flock/themes/zenbones-light.toml +1 -1
  286. flock/themes/zenwritten-dark.toml +1 -1
  287. flock/utilities.py +301 -0
  288. flock/utility/output_utility_component.py +226 -0
  289. flock/visibility.py +107 -0
  290. flock_core-0.5.0.dist-info/METADATA +964 -0
  291. flock_core-0.5.0.dist-info/RECORD +525 -0
  292. flock_core-0.5.0.dist-info/entry_points.txt +2 -0
  293. {flock_core-0.4.542.dist-info → flock_core-0.5.0.dist-info}/licenses/LICENSE +1 -1
  294. flock/adapter/__init__.py +0 -14
  295. flock/adapter/azure_adapter.py +0 -68
  296. flock/adapter/chroma_adapter.py +0 -73
  297. flock/adapter/faiss_adapter.py +0 -97
  298. flock/adapter/pinecone_adapter.py +0 -51
  299. flock/adapter/vector_base.py +0 -47
  300. flock/cli/assets/release_notes.md +0 -140
  301. flock/cli/config.py +0 -8
  302. flock/cli/constants.py +0 -36
  303. flock/cli/create_agent.py +0 -1
  304. flock/cli/create_flock.py +0 -280
  305. flock/cli/execute_flock.py +0 -620
  306. flock/cli/load_agent.py +0 -1
  307. flock/cli/load_examples.py +0 -1
  308. flock/cli/load_flock.py +0 -192
  309. flock/cli/load_release_notes.py +0 -20
  310. flock/cli/loaded_flock_cli.py +0 -254
  311. flock/cli/manage_agents.py +0 -459
  312. flock/cli/registry_management.py +0 -889
  313. flock/cli/runner.py +0 -41
  314. flock/cli/settings.py +0 -857
  315. flock/cli/utils.py +0 -135
  316. flock/cli/view_results.py +0 -29
  317. flock/cli/yaml_editor.py +0 -396
  318. flock/config.py +0 -56
  319. flock/core/__init__.py +0 -44
  320. flock/core/api/__init__.py +0 -10
  321. flock/core/api/custom_endpoint.py +0 -45
  322. flock/core/api/endpoints.py +0 -262
  323. flock/core/api/main.py +0 -162
  324. flock/core/api/models.py +0 -101
  325. flock/core/api/run_store.py +0 -224
  326. flock/core/api/runner.py +0 -44
  327. flock/core/api/service.py +0 -214
  328. flock/core/config/flock_agent_config.py +0 -11
  329. flock/core/config/scheduled_agent_config.py +0 -40
  330. flock/core/context/context.py +0 -214
  331. flock/core/context/context_manager.py +0 -40
  332. flock/core/context/context_vars.py +0 -11
  333. flock/core/evaluation/utils.py +0 -395
  334. flock/core/execution/batch_executor.py +0 -369
  335. flock/core/execution/evaluation_executor.py +0 -438
  336. flock/core/execution/local_executor.py +0 -31
  337. flock/core/execution/opik_executor.py +0 -103
  338. flock/core/execution/temporal_executor.py +0 -166
  339. flock/core/flock.py +0 -1003
  340. flock/core/flock_agent.py +0 -1258
  341. flock/core/flock_evaluator.py +0 -60
  342. flock/core/flock_factory.py +0 -513
  343. flock/core/flock_module.py +0 -207
  344. flock/core/flock_registry.py +0 -702
  345. flock/core/flock_router.py +0 -83
  346. flock/core/flock_scheduler.py +0 -166
  347. flock/core/flock_server_manager.py +0 -136
  348. flock/core/interpreter/python_interpreter.py +0 -689
  349. flock/core/logging/live_capture.py +0 -137
  350. flock/core/logging/trace_and_logged.py +0 -59
  351. flock/core/mcp/__init__.py +0 -1
  352. flock/core/mcp/flock_mcp_server.py +0 -640
  353. flock/core/mcp/mcp_client_manager.py +0 -201
  354. flock/core/mcp/types/__init__.py +0 -1
  355. flock/core/mixin/dspy_integration.py +0 -445
  356. flock/core/mixin/prompt_parser.py +0 -125
  357. flock/core/serialization/__init__.py +0 -13
  358. flock/core/serialization/callable_registry.py +0 -52
  359. flock/core/serialization/flock_serializer.py +0 -854
  360. flock/core/serialization/json_encoder.py +0 -41
  361. flock/core/serialization/secure_serializer.py +0 -175
  362. flock/core/serialization/serializable.py +0 -342
  363. flock/core/serialization/serialization_utils.py +0 -409
  364. flock/core/util/file_path_utils.py +0 -223
  365. flock/core/util/hydrator.py +0 -309
  366. flock/core/util/input_resolver.py +0 -141
  367. flock/core/util/loader.py +0 -59
  368. flock/core/util/splitter.py +0 -219
  369. flock/di.py +0 -41
  370. flock/evaluators/__init__.py +0 -1
  371. flock/evaluators/declarative/__init__.py +0 -1
  372. flock/evaluators/declarative/declarative_evaluator.py +0 -217
  373. flock/evaluators/memory/memory_evaluator.py +0 -90
  374. flock/evaluators/test/test_case_evaluator.py +0 -38
  375. flock/evaluators/zep/zep_evaluator.py +0 -59
  376. flock/modules/__init__.py +0 -1
  377. flock/modules/assertion/__init__.py +0 -1
  378. flock/modules/assertion/assertion_module.py +0 -286
  379. flock/modules/callback/__init__.py +0 -1
  380. flock/modules/callback/callback_module.py +0 -91
  381. flock/modules/enterprise_memory/README.md +0 -99
  382. flock/modules/enterprise_memory/enterprise_memory_module.py +0 -526
  383. flock/modules/mem0/__init__.py +0 -1
  384. flock/modules/mem0/mem0_module.py +0 -126
  385. flock/modules/mem0_async/__init__.py +0 -1
  386. flock/modules/mem0_async/async_mem0_module.py +0 -126
  387. flock/modules/memory/__init__.py +0 -1
  388. flock/modules/memory/memory_module.py +0 -429
  389. flock/modules/memory/memory_parser.py +0 -125
  390. flock/modules/memory/memory_storage.py +0 -736
  391. flock/modules/output/__init__.py +0 -1
  392. flock/modules/output/output_module.py +0 -196
  393. flock/modules/performance/__init__.py +0 -1
  394. flock/modules/performance/metrics_module.py +0 -678
  395. flock/modules/zep/__init__.py +0 -1
  396. flock/modules/zep/zep_module.py +0 -192
  397. flock/platform/docker_tools.py +0 -49
  398. flock/platform/jaeger_install.py +0 -86
  399. flock/routers/__init__.py +0 -1
  400. flock/routers/agent/__init__.py +0 -1
  401. flock/routers/agent/agent_router.py +0 -236
  402. flock/routers/agent/handoff_agent.py +0 -58
  403. flock/routers/conditional/conditional_router.py +0 -486
  404. flock/routers/default/__init__.py +0 -1
  405. flock/routers/default/default_router.py +0 -80
  406. flock/routers/feedback/feedback_router.py +0 -114
  407. flock/routers/list_generator/list_generator_router.py +0 -166
  408. flock/routers/llm/__init__.py +0 -1
  409. flock/routers/llm/llm_router.py +0 -365
  410. flock/tools/__init__.py +0 -0
  411. flock/tools/azure_tools.py +0 -781
  412. flock/tools/code_tools.py +0 -167
  413. flock/tools/file_tools.py +0 -149
  414. flock/tools/github_tools.py +0 -157
  415. flock/tools/markdown_tools.py +0 -205
  416. flock/tools/system_tools.py +0 -9
  417. flock/tools/text_tools.py +0 -810
  418. flock/tools/web_tools.py +0 -92
  419. flock/tools/zendesk_tools.py +0 -501
  420. flock/webapp/__init__.py +0 -1
  421. flock/webapp/app/__init__.py +0 -0
  422. flock/webapp/app/api/__init__.py +0 -0
  423. flock/webapp/app/api/agent_management.py +0 -237
  424. flock/webapp/app/api/execution.py +0 -503
  425. flock/webapp/app/api/flock_management.py +0 -125
  426. flock/webapp/app/api/registry_viewer.py +0 -29
  427. flock/webapp/app/chat.py +0 -662
  428. flock/webapp/app/config.py +0 -104
  429. flock/webapp/app/dependencies.py +0 -117
  430. flock/webapp/app/main.py +0 -1086
  431. flock/webapp/app/middleware.py +0 -113
  432. flock/webapp/app/models_ui.py +0 -7
  433. flock/webapp/app/services/__init__.py +0 -0
  434. flock/webapp/app/services/feedback_file_service.py +0 -363
  435. flock/webapp/app/services/flock_service.py +0 -345
  436. flock/webapp/app/services/sharing_models.py +0 -81
  437. flock/webapp/app/services/sharing_store.py +0 -597
  438. flock/webapp/app/templates/theme_mapper.html +0 -326
  439. flock/webapp/app/theme_mapper.py +0 -811
  440. flock/webapp/app/utils.py +0 -85
  441. flock/webapp/run.py +0 -219
  442. flock/webapp/static/css/chat.css +0 -301
  443. flock/webapp/static/css/components.css +0 -167
  444. flock/webapp/static/css/header.css +0 -39
  445. flock/webapp/static/css/layout.css +0 -281
  446. flock/webapp/static/css/sidebar.css +0 -127
  447. flock/webapp/static/css/two-pane.css +0 -48
  448. flock/webapp/templates/base.html +0 -389
  449. flock/webapp/templates/chat.html +0 -152
  450. flock/webapp/templates/chat_settings.html +0 -19
  451. flock/webapp/templates/flock_editor.html +0 -16
  452. flock/webapp/templates/index.html +0 -12
  453. flock/webapp/templates/partials/_agent_detail_form.html +0 -93
  454. flock/webapp/templates/partials/_agent_list.html +0 -18
  455. flock/webapp/templates/partials/_agent_manager_view.html +0 -51
  456. flock/webapp/templates/partials/_agent_tools_checklist.html +0 -14
  457. flock/webapp/templates/partials/_chat_container.html +0 -15
  458. flock/webapp/templates/partials/_chat_messages.html +0 -57
  459. flock/webapp/templates/partials/_chat_settings_form.html +0 -85
  460. flock/webapp/templates/partials/_create_flock_form.html +0 -50
  461. flock/webapp/templates/partials/_dashboard_flock_detail.html +0 -17
  462. flock/webapp/templates/partials/_dashboard_flock_file_list.html +0 -16
  463. flock/webapp/templates/partials/_dashboard_flock_properties_preview.html +0 -28
  464. flock/webapp/templates/partials/_dashboard_upload_flock_form.html +0 -16
  465. flock/webapp/templates/partials/_dynamic_input_form_content.html +0 -22
  466. flock/webapp/templates/partials/_env_vars_table.html +0 -23
  467. flock/webapp/templates/partials/_execution_form.html +0 -127
  468. flock/webapp/templates/partials/_execution_view_container.html +0 -28
  469. flock/webapp/templates/partials/_flock_file_list.html +0 -23
  470. flock/webapp/templates/partials/_flock_properties_form.html +0 -52
  471. flock/webapp/templates/partials/_flock_upload_form.html +0 -16
  472. flock/webapp/templates/partials/_header_flock_status.html +0 -5
  473. flock/webapp/templates/partials/_live_logs.html +0 -13
  474. flock/webapp/templates/partials/_load_manager_view.html +0 -49
  475. flock/webapp/templates/partials/_registry_table.html +0 -25
  476. flock/webapp/templates/partials/_registry_viewer_content.html +0 -70
  477. flock/webapp/templates/partials/_results_display.html +0 -78
  478. flock/webapp/templates/partials/_settings_env_content.html +0 -9
  479. flock/webapp/templates/partials/_settings_theme_content.html +0 -14
  480. flock/webapp/templates/partials/_settings_view.html +0 -36
  481. flock/webapp/templates/partials/_share_chat_link_snippet.html +0 -11
  482. flock/webapp/templates/partials/_share_link_snippet.html +0 -35
  483. flock/webapp/templates/partials/_sidebar.html +0 -74
  484. flock/webapp/templates/partials/_structured_data_view.html +0 -40
  485. flock/webapp/templates/partials/_theme_preview.html +0 -36
  486. flock/webapp/templates/registry_viewer.html +0 -84
  487. flock/webapp/templates/shared_run_page.html +0 -140
  488. flock/workflow/__init__.py +0 -0
  489. flock/workflow/activities.py +0 -237
  490. flock/workflow/agent_activities.py +0 -24
  491. flock/workflow/agent_execution_activity.py +0 -240
  492. flock/workflow/flock_workflow.py +0 -225
  493. flock/workflow/temporal_config.py +0 -96
  494. flock/workflow/temporal_setup.py +0 -60
  495. flock_core-0.4.542.dist-info/METADATA +0 -676
  496. flock_core-0.4.542.dist-info/RECORD +0 -572
  497. flock_core-0.4.542.dist-info/entry_points.txt +0 -2
  498. /flock/{core/logging → logging}/formatters/themes.py +0 -0
  499. /flock/{core/logging → logging}/span_middleware/baggage_span_processor.py +0 -0
  500. /flock/{core/mcp → mcp}/util/__init__.py +0 -0
  501. {flock_core-0.4.542.dist-info → flock_core-0.5.0.dist-info}/WHEEL +0 -0
flock/store.py ADDED
@@ -0,0 +1,1214 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ """Blackboard storage primitives and metadata envelopes.
5
+
6
+ Future backends should read the docstrings on :class:`FilterConfig`,
7
+ :class:`ConsumptionRecord`, and :class:`BlackboardStore` to understand the
8
+ contract expected by the REST layer and dashboard.
9
+ """
10
+
11
+ import asyncio
12
+ import json
13
+ import re
14
+ from asyncio import Lock
15
+ from collections import defaultdict
16
+ from collections.abc import Iterable
17
+ from dataclasses import dataclass, field
18
+ from datetime import datetime, timedelta, timezone
19
+ from pathlib import Path
20
+ from typing import Any, TypeVar
21
+ from uuid import UUID
22
+
23
+ import aiosqlite
24
+ from opentelemetry import trace
25
+
26
+ from flock.artifacts import Artifact
27
+ from flock.registry import type_registry
28
+ from flock.visibility import (
29
+ AfterVisibility,
30
+ LabelledVisibility,
31
+ PrivateVisibility,
32
+ PublicVisibility,
33
+ TenantVisibility,
34
+ Visibility,
35
+ )
36
+
37
+
38
+ T = TypeVar("T")
39
+ tracer = trace.get_tracer(__name__)
40
+
41
+ ISO_DURATION_RE = re.compile(
42
+ r"^P(?:T?(?:(?P<hours>\d+)H)?(?:(?P<minutes>\d+)M)?(?:(?P<seconds>\d+)S)?)$"
43
+ )
44
+
45
+
46
+ def _parse_iso_duration(value: str | None) -> timedelta:
47
+ if not value:
48
+ return timedelta(0)
49
+ match = ISO_DURATION_RE.match(value)
50
+ if not match:
51
+ return timedelta(0)
52
+ hours = int(match.group("hours") or 0)
53
+ minutes = int(match.group("minutes") or 0)
54
+ seconds = int(match.group("seconds") or 0)
55
+ return timedelta(hours=hours, minutes=minutes, seconds=seconds)
56
+
57
+
58
+ def _deserialize_visibility(data: Any) -> Visibility:
59
+ if isinstance(data, Visibility):
60
+ return data
61
+ if not data:
62
+ return PublicVisibility()
63
+ kind = data.get("kind") if isinstance(data, dict) else None
64
+ if kind == "Public":
65
+ return PublicVisibility()
66
+ if kind == "Private":
67
+ return PrivateVisibility(agents=set(data.get("agents", [])))
68
+ if kind == "Labelled":
69
+ return LabelledVisibility(required_labels=set(data.get("required_labels", [])))
70
+ if kind == "Tenant":
71
+ return TenantVisibility(tenant_id=data.get("tenant_id"))
72
+ if kind == "After":
73
+ ttl = _parse_iso_duration(data.get("ttl"))
74
+ then_data = data.get("then") if isinstance(data, dict) else None
75
+ then_visibility = _deserialize_visibility(then_data) if then_data else None
76
+ return AfterVisibility(ttl=ttl, then=then_visibility)
77
+ return PublicVisibility()
78
+
79
+
80
+ @dataclass(slots=True)
81
+ class ConsumptionRecord:
82
+ """Historical record describing which agent consumed an artifact."""
83
+
84
+ artifact_id: UUID
85
+ consumer: str
86
+ run_id: str | None = None
87
+ correlation_id: str | None = None
88
+ consumed_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
89
+
90
+
91
+ @dataclass(slots=True)
92
+ class FilterConfig:
93
+ """Shared filter configuration used by all stores."""
94
+
95
+ type_names: set[str] | None = None
96
+ produced_by: set[str] | None = None
97
+ correlation_id: str | None = None
98
+ tags: set[str] | None = None
99
+ visibility: set[str] | None = None
100
+ start: datetime | None = None
101
+ end: datetime | None = None
102
+
103
+
104
+ @dataclass(slots=True)
105
+ class ArtifactEnvelope:
106
+ """Wrapper returned when ``embed_meta`` is requested."""
107
+
108
+ artifact: Artifact
109
+ consumptions: list[ConsumptionRecord] = field(default_factory=list)
110
+
111
+
112
+ @dataclass(slots=True)
113
+ class AgentSnapshotRecord:
114
+ """Persistent metadata about an agent's behaviour."""
115
+
116
+ agent_name: str
117
+ description: str
118
+ subscriptions: list[str]
119
+ output_types: list[str]
120
+ labels: list[str]
121
+ first_seen: datetime
122
+ last_seen: datetime
123
+ signature: str
124
+
125
+
126
+ class BlackboardStore:
127
+ async def publish(self, artifact: Artifact) -> None:
128
+ raise NotImplementedError
129
+
130
+ async def get(self, artifact_id: UUID) -> Artifact | None:
131
+ raise NotImplementedError
132
+
133
+ async def list(self) -> list[Artifact]:
134
+ raise NotImplementedError
135
+
136
+ async def list_by_type(self, type_name: str) -> list[Artifact]:
137
+ raise NotImplementedError
138
+
139
+ async def get_by_type(self, artifact_type: type[T]) -> list[T]:
140
+ """Get artifacts by Pydantic type, returning data already cast.
141
+
142
+ Args:
143
+ artifact_type: The Pydantic model class (e.g., BugAnalysis)
144
+
145
+ Returns:
146
+ List of data objects of the specified type (not Artifact wrappers)
147
+
148
+ Example:
149
+ bug_analyses = await store.get_by_type(BugAnalysis)
150
+ # Returns list[BugAnalysis] directly, no .data access needed
151
+ """
152
+ raise NotImplementedError
153
+
154
+ async def record_consumptions(
155
+ self,
156
+ records: Iterable[ConsumptionRecord],
157
+ ) -> None:
158
+ """Persist one or more consumption events."""
159
+ raise NotImplementedError
160
+
161
+ async def query_artifacts(
162
+ self,
163
+ filters: FilterConfig | None = None,
164
+ *,
165
+ limit: int = 50,
166
+ offset: int = 0,
167
+ embed_meta: bool = False,
168
+ ) -> tuple[list[Artifact | ArtifactEnvelope], int]:
169
+ """Search artifacts with filtering and pagination."""
170
+ raise NotImplementedError
171
+
172
+ async def fetch_graph_artifacts(
173
+ self,
174
+ filters: FilterConfig | None = None,
175
+ *,
176
+ limit: int = 500,
177
+ offset: int = 0,
178
+ ) -> tuple[list[ArtifactEnvelope], int]:
179
+ """Return artifact envelopes (artifact + consumptions) for graph assembly."""
180
+ artifacts, total = await self.query_artifacts(
181
+ filters=filters,
182
+ limit=limit,
183
+ offset=offset,
184
+ embed_meta=True,
185
+ )
186
+
187
+ envelopes: list[ArtifactEnvelope] = []
188
+ for item in artifacts:
189
+ if isinstance(item, ArtifactEnvelope):
190
+ envelopes.append(item)
191
+ elif isinstance(item, Artifact):
192
+ envelopes.append(ArtifactEnvelope(artifact=item))
193
+ return envelopes, total
194
+
195
+ async def summarize_artifacts(
196
+ self,
197
+ filters: FilterConfig | None = None,
198
+ ) -> dict[str, Any]:
199
+ """Return aggregate artifact statistics for the given filters."""
200
+ raise NotImplementedError
201
+
202
+ async def agent_history_summary(
203
+ self,
204
+ agent_id: str,
205
+ filters: FilterConfig | None = None,
206
+ ) -> dict[str, Any]:
207
+ """Return produced/consumed counts for the specified agent."""
208
+ raise NotImplementedError
209
+
210
+ async def upsert_agent_snapshot(self, snapshot: AgentSnapshotRecord) -> None:
211
+ """Persist metadata describing an agent."""
212
+ raise NotImplementedError
213
+
214
+ async def load_agent_snapshots(self) -> list[AgentSnapshotRecord]:
215
+ """Return all persisted agent metadata records."""
216
+ raise NotImplementedError
217
+
218
+ async def clear_agent_snapshots(self) -> None:
219
+ """Remove all persisted agent metadata."""
220
+ raise NotImplementedError
221
+
222
+
223
+ class InMemoryBlackboardStore(BlackboardStore):
224
+ """Simple in-memory implementation suitable for local dev and tests."""
225
+
226
+ def __init__(self) -> None:
227
+ self._lock = Lock()
228
+ self._by_id: dict[UUID, Artifact] = {}
229
+ self._by_type: dict[str, list[Artifact]] = defaultdict(list)
230
+ self._consumptions_by_artifact: dict[UUID, list[ConsumptionRecord]] = defaultdict(list)
231
+ self._agent_snapshots: dict[str, AgentSnapshotRecord] = {}
232
+
233
+ async def publish(self, artifact: Artifact) -> None:
234
+ async with self._lock:
235
+ self._by_id[artifact.id] = artifact
236
+ self._by_type[artifact.type].append(artifact)
237
+
238
+ async def get(self, artifact_id: UUID) -> Artifact | None:
239
+ async with self._lock:
240
+ return self._by_id.get(artifact_id)
241
+
242
+ async def list(self) -> list[Artifact]:
243
+ async with self._lock:
244
+ return list(self._by_id.values())
245
+
246
+ async def list_by_type(self, type_name: str) -> list[Artifact]:
247
+ async with self._lock:
248
+ canonical = type_registry.resolve_name(type_name)
249
+ return list(self._by_type.get(canonical, []))
250
+
251
+ async def get_by_type(self, artifact_type: type[T]) -> list[T]:
252
+ async with self._lock:
253
+ canonical = type_registry.resolve_name(artifact_type.__name__)
254
+ artifacts = self._by_type.get(canonical, [])
255
+ return [artifact_type(**artifact.payload) for artifact in artifacts] # type: ignore
256
+
257
+ async def extend(self, artifacts: Iterable[Artifact]) -> None: # pragma: no cover - helper
258
+ for artifact in artifacts:
259
+ await self.publish(artifact)
260
+
261
+ async def record_consumptions(
262
+ self,
263
+ records: Iterable[ConsumptionRecord],
264
+ ) -> None:
265
+ async with self._lock:
266
+ for record in records:
267
+ self._consumptions_by_artifact[record.artifact_id].append(record)
268
+
269
+ async def query_artifacts(
270
+ self,
271
+ filters: FilterConfig | None = None,
272
+ *,
273
+ limit: int = 50,
274
+ offset: int = 0,
275
+ embed_meta: bool = False,
276
+ ) -> tuple[list[Artifact | ArtifactEnvelope], int]:
277
+ async with self._lock:
278
+ artifacts = list(self._by_id.values())
279
+
280
+ filters = filters or FilterConfig()
281
+ canonical: set[str] | None = None
282
+ if filters.type_names:
283
+ canonical = {type_registry.resolve_name(name) for name in filters.type_names}
284
+
285
+ visibility_filter = filters.visibility or set()
286
+
287
+ def _matches(artifact: Artifact) -> bool:
288
+ if canonical and artifact.type not in canonical:
289
+ return False
290
+ if filters.produced_by and artifact.produced_by not in filters.produced_by:
291
+ return False
292
+ if filters.correlation_id and (
293
+ artifact.correlation_id is None
294
+ or str(artifact.correlation_id) != filters.correlation_id
295
+ ):
296
+ return False
297
+ if filters.tags and not filters.tags.issubset(artifact.tags):
298
+ return False
299
+ if visibility_filter and artifact.visibility.kind not in visibility_filter:
300
+ return False
301
+ if filters.start and artifact.created_at < filters.start:
302
+ return False
303
+ return not (filters.end and artifact.created_at > filters.end)
304
+
305
+ filtered = [artifact for artifact in artifacts if _matches(artifact)]
306
+ filtered.sort(key=lambda a: (a.created_at, a.id))
307
+
308
+ total = len(filtered)
309
+ offset = max(offset, 0)
310
+ if limit <= 0:
311
+ page = filtered[offset:]
312
+ else:
313
+ page = filtered[offset : offset + limit]
314
+
315
+ if not embed_meta:
316
+ return page, total
317
+
318
+ envelopes = [
319
+ ArtifactEnvelope(
320
+ artifact=artifact,
321
+ consumptions=list(self._consumptions_by_artifact.get(artifact.id, [])),
322
+ )
323
+ for artifact in page
324
+ ]
325
+ return envelopes, total
326
+
327
+ async def summarize_artifacts(
328
+ self,
329
+ filters: FilterConfig | None = None,
330
+ ) -> dict[str, Any]:
331
+ filters = filters or FilterConfig()
332
+ artifacts, total = await self.query_artifacts(
333
+ filters=filters,
334
+ limit=0,
335
+ offset=0,
336
+ embed_meta=False,
337
+ )
338
+
339
+ by_type: dict[str, int] = {}
340
+ by_producer: dict[str, int] = {}
341
+ by_visibility: dict[str, int] = {}
342
+ tag_counts: dict[str, int] = {}
343
+ earliest: datetime | None = None
344
+ latest: datetime | None = None
345
+
346
+ for artifact in artifacts:
347
+ if not isinstance(artifact, Artifact):
348
+ raise TypeError("Expected Artifact instance")
349
+ by_type[artifact.type] = by_type.get(artifact.type, 0) + 1
350
+ by_producer[artifact.produced_by] = by_producer.get(artifact.produced_by, 0) + 1
351
+ kind = getattr(artifact.visibility, "kind", "Unknown")
352
+ by_visibility[kind] = by_visibility.get(kind, 0) + 1
353
+ for tag in artifact.tags:
354
+ tag_counts[tag] = tag_counts.get(tag, 0) + 1
355
+ if earliest is None or artifact.created_at < earliest:
356
+ earliest = artifact.created_at
357
+ if latest is None or artifact.created_at > latest:
358
+ latest = artifact.created_at
359
+
360
+ if earliest and latest:
361
+ span = latest - earliest
362
+ if span.days >= 2:
363
+ span_label = f"{span.days} days"
364
+ elif span.total_seconds() >= 3600:
365
+ hours = span.total_seconds() / 3600
366
+ span_label = f"{hours:.1f} hours"
367
+ elif span.total_seconds() > 0:
368
+ minutes = max(1, int(span.total_seconds() / 60))
369
+ span_label = f"{minutes} minutes"
370
+ else:
371
+ span_label = "moments"
372
+ else:
373
+ span_label = "empty"
374
+
375
+ return {
376
+ "total": total,
377
+ "by_type": by_type,
378
+ "by_producer": by_producer,
379
+ "by_visibility": by_visibility,
380
+ "tag_counts": tag_counts,
381
+ "earliest_created_at": earliest.isoformat() if earliest else None,
382
+ "latest_created_at": latest.isoformat() if latest else None,
383
+ "is_full_window": filters.start is None and filters.end is None,
384
+ "window_span_label": span_label,
385
+ }
386
+
387
+ async def agent_history_summary(
388
+ self,
389
+ agent_id: str,
390
+ filters: FilterConfig | None = None,
391
+ ) -> dict[str, Any]:
392
+ filters = filters or FilterConfig()
393
+ envelopes, _ = await self.query_artifacts(
394
+ filters=filters,
395
+ limit=0,
396
+ offset=0,
397
+ embed_meta=True,
398
+ )
399
+
400
+ produced_total = 0
401
+ produced_by_type: dict[str, int] = defaultdict(int)
402
+ consumed_total = 0
403
+ consumed_by_type: dict[str, int] = defaultdict(int)
404
+
405
+ for envelope in envelopes:
406
+ if not isinstance(envelope, ArtifactEnvelope):
407
+ raise TypeError("Expected ArtifactEnvelope instance")
408
+ artifact = envelope.artifact
409
+ if artifact.produced_by == agent_id:
410
+ produced_total += 1
411
+ produced_by_type[artifact.type] += 1
412
+ for consumption in envelope.consumptions:
413
+ if consumption.consumer == agent_id:
414
+ consumed_total += 1
415
+ consumed_by_type[artifact.type] += 1
416
+
417
+ return {
418
+ "produced": {"total": produced_total, "by_type": dict(produced_by_type)},
419
+ "consumed": {"total": consumed_total, "by_type": dict(consumed_by_type)},
420
+ }
421
+
422
+ async def upsert_agent_snapshot(self, snapshot: AgentSnapshotRecord) -> None:
423
+ async with self._lock:
424
+ self._agent_snapshots[snapshot.agent_name] = snapshot
425
+
426
+ async def load_agent_snapshots(self) -> list[AgentSnapshotRecord]:
427
+ async with self._lock:
428
+ return list(self._agent_snapshots.values())
429
+
430
+ async def clear_agent_snapshots(self) -> None:
431
+ async with self._lock:
432
+ self._agent_snapshots.clear()
433
+
434
+
435
+ __all__ = [
436
+ "AgentSnapshotRecord",
437
+ "BlackboardStore",
438
+ "InMemoryBlackboardStore",
439
+ "SQLiteBlackboardStore",
440
+ ]
441
+
442
+
443
+ class SQLiteBlackboardStore(BlackboardStore):
444
+ """SQLite-backed implementation of :class:`BlackboardStore`."""
445
+
446
+ SCHEMA_VERSION = 3
447
+
448
+ def __init__(self, db_path: str, *, timeout: float = 5.0) -> None:
449
+ self._db_path = Path(db_path)
450
+ self._timeout = timeout
451
+ self._connection: aiosqlite.Connection | None = None
452
+ self._connection_lock = asyncio.Lock()
453
+ self._write_lock = asyncio.Lock()
454
+ self._schema_ready = False
455
+
456
+ async def publish(self, artifact: Artifact) -> None: # type: ignore[override]
457
+ with tracer.start_as_current_span("sqlite_store.publish"):
458
+ conn = await self._get_connection()
459
+
460
+ payload_json = json.dumps(artifact.payload)
461
+ visibility_json = json.dumps(artifact.visibility.model_dump(mode="json"))
462
+ tags_json = json.dumps(sorted(artifact.tags))
463
+ created_at = artifact.created_at.isoformat()
464
+
465
+ try:
466
+ canonical_type = type_registry.resolve_name(artifact.type)
467
+ except Exception:
468
+ canonical_type = artifact.type
469
+
470
+ record = {
471
+ "artifact_id": str(artifact.id),
472
+ "type": artifact.type,
473
+ "canonical_type": canonical_type,
474
+ "produced_by": artifact.produced_by,
475
+ "payload": payload_json,
476
+ "version": artifact.version,
477
+ "visibility": visibility_json,
478
+ "tags": tags_json,
479
+ "correlation_id": str(artifact.correlation_id) if artifact.correlation_id else None,
480
+ "partition_key": artifact.partition_key,
481
+ "created_at": created_at,
482
+ }
483
+
484
+ async with self._write_lock:
485
+ await conn.execute(
486
+ """
487
+ INSERT INTO artifacts (
488
+ artifact_id,
489
+ type,
490
+ canonical_type,
491
+ produced_by,
492
+ payload,
493
+ version,
494
+ visibility,
495
+ tags,
496
+ correlation_id,
497
+ partition_key,
498
+ created_at
499
+ ) VALUES (
500
+ :artifact_id,
501
+ :type,
502
+ :canonical_type,
503
+ :produced_by,
504
+ :payload,
505
+ :version,
506
+ :visibility,
507
+ :tags,
508
+ :correlation_id,
509
+ :partition_key,
510
+ :created_at
511
+ )
512
+ ON CONFLICT(artifact_id) DO UPDATE SET
513
+ type=excluded.type,
514
+ canonical_type=excluded.canonical_type,
515
+ produced_by=excluded.produced_by,
516
+ payload=excluded.payload,
517
+ version=excluded.version,
518
+ visibility=excluded.visibility,
519
+ tags=excluded.tags,
520
+ correlation_id=excluded.correlation_id,
521
+ partition_key=excluded.partition_key,
522
+ created_at=excluded.created_at
523
+ """,
524
+ record,
525
+ )
526
+ await conn.commit()
527
+
528
+ async def record_consumptions( # type: ignore[override]
529
+ self,
530
+ records: Iterable[ConsumptionRecord],
531
+ ) -> None:
532
+ with tracer.start_as_current_span("sqlite_store.record_consumptions"):
533
+ rows = [
534
+ (
535
+ str(record.artifact_id),
536
+ record.consumer,
537
+ record.run_id,
538
+ record.correlation_id,
539
+ record.consumed_at.isoformat(),
540
+ )
541
+ for record in records
542
+ ]
543
+ if not rows:
544
+ return
545
+
546
+ conn = await self._get_connection()
547
+ async with self._write_lock:
548
+ await conn.executemany(
549
+ """
550
+ INSERT OR REPLACE INTO artifact_consumptions (
551
+ artifact_id,
552
+ consumer,
553
+ run_id,
554
+ correlation_id,
555
+ consumed_at
556
+ ) VALUES (?, ?, ?, ?, ?)
557
+ """,
558
+ rows,
559
+ )
560
+ await conn.commit()
561
+
562
+ async def fetch_graph_artifacts( # type: ignore[override]
563
+ self,
564
+ filters: FilterConfig | None = None,
565
+ *,
566
+ limit: int = 500,
567
+ offset: int = 0,
568
+ ) -> tuple[list[ArtifactEnvelope], int]:
569
+ with tracer.start_as_current_span("sqlite_store.fetch_graph_artifacts"):
570
+ return await super().fetch_graph_artifacts(
571
+ filters,
572
+ limit=limit,
573
+ offset=offset,
574
+ )
575
+
576
+ async def get(self, artifact_id: UUID) -> Artifact | None: # type: ignore[override]
577
+ with tracer.start_as_current_span("sqlite_store.get"):
578
+ conn = await self._get_connection()
579
+ cursor = await conn.execute(
580
+ """
581
+ SELECT
582
+ artifact_id,
583
+ type,
584
+ canonical_type,
585
+ produced_by,
586
+ payload,
587
+ version,
588
+ visibility,
589
+ tags,
590
+ correlation_id,
591
+ partition_key,
592
+ created_at
593
+ FROM artifacts
594
+ WHERE artifact_id = ?
595
+ """,
596
+ (str(artifact_id),),
597
+ )
598
+ row = await cursor.fetchone()
599
+ await cursor.close()
600
+ if row is None:
601
+ return None
602
+ return self._row_to_artifact(row)
603
+
604
+ async def list(self) -> list[Artifact]: # type: ignore[override]
605
+ with tracer.start_as_current_span("sqlite_store.list"):
606
+ conn = await self._get_connection()
607
+ cursor = await conn.execute(
608
+ """
609
+ SELECT
610
+ artifact_id,
611
+ type,
612
+ canonical_type,
613
+ produced_by,
614
+ payload,
615
+ version,
616
+ visibility,
617
+ tags,
618
+ correlation_id,
619
+ partition_key,
620
+ created_at
621
+ FROM artifacts
622
+ ORDER BY created_at ASC, rowid ASC
623
+ """
624
+ )
625
+ rows = await cursor.fetchall()
626
+ await cursor.close()
627
+ return [self._row_to_artifact(row) for row in rows]
628
+
629
+ async def list_by_type(self, type_name: str) -> list[Artifact]: # type: ignore[override]
630
+ with tracer.start_as_current_span("sqlite_store.list_by_type"):
631
+ conn = await self._get_connection()
632
+ canonical = type_registry.resolve_name(type_name)
633
+ cursor = await conn.execute(
634
+ """
635
+ SELECT
636
+ artifact_id,
637
+ type,
638
+ canonical_type,
639
+ produced_by,
640
+ payload,
641
+ version,
642
+ visibility,
643
+ tags,
644
+ correlation_id,
645
+ partition_key,
646
+ created_at
647
+ FROM artifacts
648
+ WHERE canonical_type = ?
649
+ ORDER BY created_at ASC, rowid ASC
650
+ """,
651
+ (canonical,),
652
+ )
653
+ rows = await cursor.fetchall()
654
+ await cursor.close()
655
+ return [self._row_to_artifact(row) for row in rows]
656
+
657
+ async def get_by_type(self, artifact_type: type[T]) -> list[T]: # type: ignore[override]
658
+ with tracer.start_as_current_span("sqlite_store.get_by_type"):
659
+ conn = await self._get_connection()
660
+ canonical = type_registry.resolve_name(artifact_type.__name__)
661
+ cursor = await conn.execute(
662
+ """
663
+ SELECT payload
664
+ FROM artifacts
665
+ WHERE canonical_type = ?
666
+ ORDER BY created_at ASC, rowid ASC
667
+ """,
668
+ (canonical,),
669
+ )
670
+ rows = await cursor.fetchall()
671
+ await cursor.close()
672
+ results: list[T] = []
673
+ for row in rows:
674
+ payload = json.loads(row["payload"])
675
+ results.append(artifact_type(**payload)) # type: ignore[arg-type]
676
+ return results
677
+
678
+ async def query_artifacts(
679
+ self,
680
+ filters: FilterConfig | None = None,
681
+ *,
682
+ limit: int = 50,
683
+ offset: int = 0,
684
+ embed_meta: bool = False,
685
+ ) -> tuple[list[Artifact | ArtifactEnvelope], int]:
686
+ filters = filters or FilterConfig()
687
+ conn = await self._get_connection()
688
+
689
+ where_clause, params = self._build_filters(filters)
690
+ count_query = f"SELECT COUNT(*) AS total FROM artifacts{where_clause}" # nosec B608 - where_clause contains only parameter placeholders from _build_filters
691
+ cursor = await conn.execute(count_query, tuple(params)) # nosec B608
692
+ total_row = await cursor.fetchone()
693
+ await cursor.close()
694
+ total = total_row["total"] if total_row else 0
695
+
696
+ query = f"""
697
+ SELECT
698
+ artifact_id,
699
+ type,
700
+ canonical_type,
701
+ produced_by,
702
+ payload,
703
+ version,
704
+ visibility,
705
+ tags,
706
+ correlation_id,
707
+ partition_key,
708
+ created_at
709
+ FROM artifacts
710
+ {where_clause}
711
+ ORDER BY created_at ASC, rowid ASC
712
+ """ # nosec B608 - where_clause contains only parameter placeholders from _build_filters
713
+ query_params: tuple[Any, ...]
714
+ if limit <= 0:
715
+ if offset > 0:
716
+ query += " LIMIT -1 OFFSET ?"
717
+ query_params = (*params, max(offset, 0))
718
+ else:
719
+ query_params = tuple(params)
720
+ else:
721
+ query += " LIMIT ? OFFSET ?"
722
+ query_params = (*params, limit, max(offset, 0))
723
+
724
+ cursor = await conn.execute(query, query_params)
725
+ rows = await cursor.fetchall()
726
+ await cursor.close()
727
+ artifacts = [self._row_to_artifact(row) for row in rows]
728
+
729
+ if not embed_meta or not artifacts:
730
+ return artifacts, total
731
+
732
+ artifact_ids = [str(artifact.id) for artifact in artifacts]
733
+ placeholders = ", ".join("?" for _ in artifact_ids)
734
+ consumption_query = f"""
735
+ SELECT
736
+ artifact_id,
737
+ consumer,
738
+ run_id,
739
+ correlation_id,
740
+ consumed_at
741
+ FROM artifact_consumptions
742
+ WHERE artifact_id IN ({placeholders})
743
+ ORDER BY consumed_at ASC
744
+ """ # nosec B608 - placeholders string contains only '?' characters
745
+ cursor = await conn.execute(consumption_query, artifact_ids)
746
+ consumption_rows = await cursor.fetchall()
747
+ await cursor.close()
748
+
749
+ consumptions_map: dict[UUID, list[ConsumptionRecord]] = defaultdict(list)
750
+ for row in consumption_rows:
751
+ artifact_uuid = UUID(row["artifact_id"])
752
+ consumptions_map[artifact_uuid].append(
753
+ ConsumptionRecord(
754
+ artifact_id=artifact_uuid,
755
+ consumer=row["consumer"],
756
+ run_id=row["run_id"],
757
+ correlation_id=row["correlation_id"],
758
+ consumed_at=datetime.fromisoformat(row["consumed_at"]),
759
+ )
760
+ )
761
+
762
+ envelopes: list[ArtifactEnvelope] = [
763
+ ArtifactEnvelope(
764
+ artifact=artifact,
765
+ consumptions=consumptions_map.get(artifact.id, []),
766
+ )
767
+ for artifact in artifacts
768
+ ]
769
+ return envelopes, total
770
+
771
+ async def summarize_artifacts(
772
+ self,
773
+ filters: FilterConfig | None = None,
774
+ ) -> dict[str, Any]:
775
+ filters = filters or FilterConfig()
776
+ conn = await self._get_connection()
777
+
778
+ where_clause, params = self._build_filters(filters)
779
+ params_tuple = tuple(params)
780
+
781
+ count_query = f"SELECT COUNT(*) AS total FROM artifacts{where_clause}" # nosec B608 - where_clause contains only parameter placeholders from _build_filters
782
+ cursor = await conn.execute(count_query, params_tuple) # nosec B608
783
+ total_row = await cursor.fetchone()
784
+ await cursor.close()
785
+ total = total_row["total"] if total_row else 0
786
+
787
+ by_type_query = f"""
788
+ SELECT canonical_type, COUNT(*) AS count
789
+ FROM artifacts
790
+ {where_clause}
791
+ GROUP BY canonical_type
792
+ """ # nosec B608 - where_clause contains only parameter placeholders from _build_filters
793
+ cursor = await conn.execute(by_type_query, params_tuple)
794
+ by_type_rows = await cursor.fetchall()
795
+ await cursor.close()
796
+ by_type = {row["canonical_type"]: row["count"] for row in by_type_rows}
797
+
798
+ by_producer_query = f"""
799
+ SELECT produced_by, COUNT(*) AS count
800
+ FROM artifacts
801
+ {where_clause}
802
+ GROUP BY produced_by
803
+ """ # nosec B608 - where_clause contains only parameter placeholders from _build_filters
804
+ cursor = await conn.execute(by_producer_query, params_tuple)
805
+ by_producer_rows = await cursor.fetchall()
806
+ await cursor.close()
807
+ by_producer = {row["produced_by"]: row["count"] for row in by_producer_rows}
808
+
809
+ by_visibility_query = f"""
810
+ SELECT json_extract(visibility, '$.kind') AS visibility_kind, COUNT(*) AS count
811
+ FROM artifacts
812
+ {where_clause}
813
+ GROUP BY json_extract(visibility, '$.kind')
814
+ """ # nosec B608 - where_clause contains only parameter placeholders from _build_filters
815
+ cursor = await conn.execute(by_visibility_query, params_tuple)
816
+ by_visibility_rows = await cursor.fetchall()
817
+ await cursor.close()
818
+ by_visibility = {
819
+ (row["visibility_kind"] or "Unknown"): row["count"] for row in by_visibility_rows
820
+ }
821
+
822
+ tag_query = f"""
823
+ SELECT json_each.value AS tag, COUNT(*) AS count
824
+ FROM artifacts
825
+ JOIN json_each(artifacts.tags)
826
+ {where_clause}
827
+ GROUP BY json_each.value
828
+ """ # nosec B608 - where_clause contains only parameter placeholders produced by _build_filters
829
+ cursor = await conn.execute(tag_query, params_tuple)
830
+ tag_rows = await cursor.fetchall()
831
+ await cursor.close()
832
+ tag_counts = {row["tag"]: row["count"] for row in tag_rows}
833
+
834
+ range_query = f"""
835
+ SELECT MIN(created_at) AS earliest, MAX(created_at) AS latest
836
+ FROM artifacts
837
+ {where_clause}
838
+ """ # nosec B608 - safe composition using parameterized where_clause
839
+ cursor = await conn.execute(range_query, params_tuple)
840
+ range_row = await cursor.fetchone()
841
+ await cursor.close()
842
+ earliest = range_row["earliest"] if range_row and range_row["earliest"] else None
843
+ latest = range_row["latest"] if range_row and range_row["latest"] else None
844
+
845
+ return {
846
+ "total": total,
847
+ "by_type": by_type,
848
+ "by_producer": by_producer,
849
+ "by_visibility": by_visibility,
850
+ "tag_counts": tag_counts,
851
+ "earliest_created_at": earliest,
852
+ "latest_created_at": latest,
853
+ }
854
+
855
+ async def agent_history_summary(
856
+ self,
857
+ agent_id: str,
858
+ filters: FilterConfig | None = None,
859
+ ) -> dict[str, Any]:
860
+ filters = filters or FilterConfig()
861
+ conn = await self._get_connection()
862
+
863
+ produced_total = 0
864
+ produced_by_type: dict[str, int] = {}
865
+
866
+ if filters.produced_by and agent_id not in filters.produced_by:
867
+ produced_total = 0
868
+ else:
869
+ produced_filter = FilterConfig(
870
+ type_names=set(filters.type_names) if filters.type_names else None,
871
+ produced_by={agent_id},
872
+ correlation_id=filters.correlation_id,
873
+ tags=set(filters.tags) if filters.tags else None,
874
+ visibility=set(filters.visibility) if filters.visibility else None,
875
+ start=filters.start,
876
+ end=filters.end,
877
+ )
878
+ where_clause, params = self._build_filters(produced_filter)
879
+ produced_query = f"""
880
+ SELECT canonical_type, COUNT(*) AS count
881
+ FROM artifacts
882
+ {where_clause}
883
+ GROUP BY canonical_type
884
+ """ # nosec B608 - produced_filter yields parameter placeholders only
885
+ cursor = await conn.execute(produced_query, tuple(params))
886
+ rows = await cursor.fetchall()
887
+ await cursor.close()
888
+ produced_by_type = {row["canonical_type"]: row["count"] for row in rows}
889
+ produced_total = sum(produced_by_type.values())
890
+
891
+ where_clause, params = self._build_filters(filters, table_alias="a")
892
+ params_with_consumer = (*params, agent_id)
893
+ consumption_query = f"""
894
+ SELECT a.canonical_type AS canonical_type, COUNT(*) AS count
895
+ FROM artifact_consumptions c
896
+ JOIN artifacts a ON a.artifact_id = c.artifact_id
897
+ {where_clause}
898
+ {"AND" if where_clause else "WHERE"} c.consumer = ?
899
+ GROUP BY a.canonical_type
900
+ """ # nosec B608 - where_clause joins parameter placeholders only
901
+ cursor = await conn.execute(consumption_query, params_with_consumer)
902
+ consumption_rows = await cursor.fetchall()
903
+ await cursor.close()
904
+
905
+ consumed_by_type = {row["canonical_type"]: row["count"] for row in consumption_rows}
906
+ consumed_total = sum(consumed_by_type.values())
907
+
908
+ return {
909
+ "produced": {"total": produced_total, "by_type": produced_by_type},
910
+ "consumed": {"total": consumed_total, "by_type": consumed_by_type},
911
+ }
912
+
913
+ async def upsert_agent_snapshot(self, snapshot: AgentSnapshotRecord) -> None:
914
+ with tracer.start_as_current_span("sqlite_store.upsert_agent_snapshot"):
915
+ conn = await self._get_connection()
916
+ payload = {
917
+ "agent_name": snapshot.agent_name,
918
+ "description": snapshot.description,
919
+ "subscriptions": json.dumps(snapshot.subscriptions),
920
+ "output_types": json.dumps(snapshot.output_types),
921
+ "labels": json.dumps(snapshot.labels),
922
+ "first_seen": snapshot.first_seen.isoformat(),
923
+ "last_seen": snapshot.last_seen.isoformat(),
924
+ "signature": snapshot.signature,
925
+ }
926
+ async with self._write_lock:
927
+ await conn.execute(
928
+ """
929
+ INSERT INTO agent_snapshots (
930
+ agent_name, description, subscriptions, output_types, labels,
931
+ first_seen, last_seen, signature
932
+ ) VALUES (
933
+ :agent_name, :description, :subscriptions, :output_types, :labels,
934
+ :first_seen, :last_seen, :signature
935
+ )
936
+ ON CONFLICT(agent_name) DO UPDATE SET
937
+ description=excluded.description,
938
+ subscriptions=excluded.subscriptions,
939
+ output_types=excluded.output_types,
940
+ labels=excluded.labels,
941
+ first_seen=excluded.first_seen,
942
+ last_seen=excluded.last_seen,
943
+ signature=excluded.signature
944
+ """,
945
+ payload,
946
+ )
947
+ await conn.commit()
948
+
949
+ async def load_agent_snapshots(self) -> list[AgentSnapshotRecord]:
950
+ with tracer.start_as_current_span("sqlite_store.load_agent_snapshots"):
951
+ conn = await self._get_connection()
952
+ cursor = await conn.execute(
953
+ """
954
+ SELECT agent_name, description, subscriptions, output_types, labels,
955
+ first_seen, last_seen, signature
956
+ FROM agent_snapshots
957
+ """
958
+ )
959
+ rows = await cursor.fetchall()
960
+ await cursor.close()
961
+
962
+ snapshots: list[AgentSnapshotRecord] = []
963
+ for row in rows:
964
+ snapshots.append(
965
+ AgentSnapshotRecord(
966
+ agent_name=row["agent_name"],
967
+ description=row["description"],
968
+ subscriptions=json.loads(row["subscriptions"] or "[]"),
969
+ output_types=json.loads(row["output_types"] or "[]"),
970
+ labels=json.loads(row["labels"] or "[]"),
971
+ first_seen=datetime.fromisoformat(row["first_seen"]),
972
+ last_seen=datetime.fromisoformat(row["last_seen"]),
973
+ signature=row["signature"],
974
+ )
975
+ )
976
+ return snapshots
977
+
978
+ async def clear_agent_snapshots(self) -> None:
979
+ with tracer.start_as_current_span("sqlite_store.clear_agent_snapshots"):
980
+ conn = await self._get_connection()
981
+ async with self._write_lock:
982
+ await conn.execute("DELETE FROM agent_snapshots")
983
+ await conn.commit()
984
+
985
+ async def ensure_schema(self) -> None:
986
+ conn = await self._ensure_connection()
987
+ await self._apply_schema(conn)
988
+
989
+ async def close(self) -> None:
990
+ async with self._connection_lock:
991
+ if self._connection is not None:
992
+ await self._connection.close()
993
+ self._connection = None
994
+ self._schema_ready = False
995
+
996
+ async def vacuum(self) -> None:
997
+ """Run SQLite VACUUM for maintenance."""
998
+ with tracer.start_as_current_span("sqlite_store.vacuum"):
999
+ conn = await self._get_connection()
1000
+ async with self._write_lock:
1001
+ await conn.execute("VACUUM")
1002
+ await conn.commit()
1003
+
1004
+ async def delete_before(self, before: datetime) -> int:
1005
+ """Delete artifacts persisted before the given timestamp."""
1006
+ with tracer.start_as_current_span("sqlite_store.delete_before"):
1007
+ conn = await self._get_connection()
1008
+ async with self._write_lock:
1009
+ cursor = await conn.execute(
1010
+ "DELETE FROM artifacts WHERE created_at < ?", (before.isoformat(),)
1011
+ )
1012
+ await conn.commit()
1013
+ deleted = cursor.rowcount or 0
1014
+ await cursor.close()
1015
+ return deleted
1016
+
1017
+ async def _ensure_connection(self) -> aiosqlite.Connection:
1018
+ async with self._connection_lock:
1019
+ if self._connection is None:
1020
+ self._db_path.parent.mkdir(parents=True, exist_ok=True)
1021
+ conn = await aiosqlite.connect(
1022
+ str(self._db_path), timeout=self._timeout, isolation_level=None
1023
+ )
1024
+ conn.row_factory = aiosqlite.Row
1025
+ await conn.execute("PRAGMA journal_mode=WAL;")
1026
+ await conn.execute("PRAGMA synchronous=NORMAL;")
1027
+ await conn.execute("PRAGMA foreign_keys=ON;")
1028
+ self._connection = conn
1029
+ self._schema_ready = False
1030
+ return self._connection
1031
+
1032
+ async def _get_connection(self) -> aiosqlite.Connection:
1033
+ conn = await self._ensure_connection()
1034
+ if not self._schema_ready:
1035
+ await self._apply_schema(conn)
1036
+ return conn
1037
+
1038
+ async def _apply_schema(self, conn: aiosqlite.Connection) -> None:
1039
+ async with self._connection_lock:
1040
+ await conn.execute(
1041
+ """
1042
+ CREATE TABLE IF NOT EXISTS schema_meta (
1043
+ id INTEGER PRIMARY KEY CHECK (id = 1),
1044
+ version INTEGER NOT NULL,
1045
+ applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
1046
+ )
1047
+ """
1048
+ )
1049
+ await conn.execute(
1050
+ """
1051
+ INSERT OR IGNORE INTO schema_meta (id, version)
1052
+ VALUES (1, ?)
1053
+ """,
1054
+ (self.SCHEMA_VERSION,),
1055
+ )
1056
+ await conn.execute(
1057
+ """
1058
+ CREATE TABLE IF NOT EXISTS artifacts (
1059
+ artifact_id TEXT PRIMARY KEY,
1060
+ type TEXT NOT NULL,
1061
+ canonical_type TEXT NOT NULL,
1062
+ produced_by TEXT NOT NULL,
1063
+ payload TEXT NOT NULL,
1064
+ version INTEGER NOT NULL,
1065
+ visibility TEXT NOT NULL,
1066
+ tags TEXT NOT NULL,
1067
+ correlation_id TEXT,
1068
+ partition_key TEXT,
1069
+ created_at TEXT NOT NULL
1070
+ )
1071
+ """
1072
+ )
1073
+ await conn.execute(
1074
+ """
1075
+ CREATE INDEX IF NOT EXISTS idx_artifacts_canonical_type_created
1076
+ ON artifacts(canonical_type, created_at)
1077
+ """
1078
+ )
1079
+ await conn.execute(
1080
+ """
1081
+ CREATE INDEX IF NOT EXISTS idx_artifacts_produced_by_created
1082
+ ON artifacts(produced_by, created_at)
1083
+ """
1084
+ )
1085
+ await conn.execute(
1086
+ """
1087
+ CREATE INDEX IF NOT EXISTS idx_artifacts_correlation
1088
+ ON artifacts(correlation_id)
1089
+ """
1090
+ )
1091
+ await conn.execute(
1092
+ """
1093
+ CREATE INDEX IF NOT EXISTS idx_artifacts_partition
1094
+ ON artifacts(partition_key)
1095
+ """
1096
+ )
1097
+ await conn.execute(
1098
+ """
1099
+ CREATE TABLE IF NOT EXISTS artifact_consumptions (
1100
+ artifact_id TEXT NOT NULL,
1101
+ consumer TEXT NOT NULL,
1102
+ run_id TEXT,
1103
+ correlation_id TEXT,
1104
+ consumed_at TEXT NOT NULL,
1105
+ PRIMARY KEY (artifact_id, consumer, consumed_at)
1106
+ )
1107
+ """
1108
+ )
1109
+ await conn.execute(
1110
+ """
1111
+ CREATE INDEX IF NOT EXISTS idx_consumptions_artifact
1112
+ ON artifact_consumptions(artifact_id)
1113
+ """
1114
+ )
1115
+ await conn.execute(
1116
+ """
1117
+ CREATE INDEX IF NOT EXISTS idx_consumptions_consumer
1118
+ ON artifact_consumptions(consumer)
1119
+ """
1120
+ )
1121
+ await conn.execute(
1122
+ """
1123
+ CREATE INDEX IF NOT EXISTS idx_consumptions_correlation
1124
+ ON artifact_consumptions(correlation_id)
1125
+ """
1126
+ )
1127
+ await conn.execute(
1128
+ """
1129
+ CREATE TABLE IF NOT EXISTS agent_snapshots (
1130
+ agent_name TEXT PRIMARY KEY,
1131
+ description TEXT NOT NULL,
1132
+ subscriptions TEXT NOT NULL,
1133
+ output_types TEXT NOT NULL,
1134
+ labels TEXT NOT NULL,
1135
+ first_seen TEXT NOT NULL,
1136
+ last_seen TEXT NOT NULL,
1137
+ signature TEXT NOT NULL
1138
+ )
1139
+ """
1140
+ )
1141
+ await conn.execute(
1142
+ "UPDATE schema_meta SET version=? WHERE id=1",
1143
+ (self.SCHEMA_VERSION,),
1144
+ )
1145
+ await conn.commit()
1146
+ self._schema_ready = True
1147
+
1148
+ def _build_filters(
1149
+ self,
1150
+ filters: FilterConfig,
1151
+ *,
1152
+ table_alias: str | None = None,
1153
+ ) -> tuple[str, list[Any]]:
1154
+ prefix = f"{table_alias}." if table_alias else ""
1155
+ conditions: list[str] = []
1156
+ params: list[Any] = []
1157
+
1158
+ if filters.type_names:
1159
+ canonical = {type_registry.resolve_name(name) for name in filters.type_names}
1160
+ placeholders = ", ".join("?" for _ in canonical)
1161
+ conditions.append(f"{prefix}canonical_type IN ({placeholders})")
1162
+ params.extend(sorted(canonical))
1163
+
1164
+ if filters.produced_by:
1165
+ placeholders = ", ".join("?" for _ in filters.produced_by)
1166
+ conditions.append(f"{prefix}produced_by IN ({placeholders})")
1167
+ params.extend(sorted(filters.produced_by))
1168
+
1169
+ if filters.correlation_id:
1170
+ conditions.append(f"{prefix}correlation_id = ?")
1171
+ params.append(filters.correlation_id)
1172
+
1173
+ if filters.visibility:
1174
+ placeholders = ", ".join("?" for _ in filters.visibility)
1175
+ conditions.append(f"json_extract({prefix}visibility, '$.kind') IN ({placeholders})")
1176
+ params.extend(sorted(filters.visibility))
1177
+
1178
+ if filters.start is not None:
1179
+ conditions.append(f"{prefix}created_at >= ?")
1180
+ params.append(filters.start.isoformat())
1181
+
1182
+ if filters.end is not None:
1183
+ conditions.append(f"{prefix}created_at <= ?")
1184
+ params.append(filters.end.isoformat())
1185
+
1186
+ if filters.tags:
1187
+ column = f"{prefix}tags" if table_alias else "artifacts.tags"
1188
+ for tag in sorted(filters.tags):
1189
+ conditions.append(
1190
+ f"EXISTS (SELECT 1 FROM json_each({column}) WHERE json_each.value = ?)" # nosec B608 - column is internal constant
1191
+ )
1192
+ params.append(tag)
1193
+
1194
+ where_clause = f" WHERE {' AND '.join(conditions)}" if conditions else ""
1195
+ return where_clause, params
1196
+
1197
+ def _row_to_artifact(self, row: Any) -> Artifact:
1198
+ payload = json.loads(row["payload"])
1199
+ visibility_data = json.loads(row["visibility"])
1200
+ tags = json.loads(row["tags"])
1201
+ correlation_raw = row["correlation_id"]
1202
+ correlation = UUID(correlation_raw) if correlation_raw else None
1203
+ return Artifact(
1204
+ id=UUID(row["artifact_id"]),
1205
+ type=row["type"],
1206
+ payload=payload,
1207
+ produced_by=row["produced_by"],
1208
+ visibility=_deserialize_visibility(visibility_data),
1209
+ tags=set(tags),
1210
+ correlation_id=correlation,
1211
+ partition_key=row["partition_key"],
1212
+ created_at=datetime.fromisoformat(row["created_at"]),
1213
+ version=row["version"],
1214
+ )