prompt_objects 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (292) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -0
  3. data/README.md +211 -19
  4. data/exe/poop +5 -0
  5. data/exe/prompt_objects +1 -1108
  6. data/exe/prompt_objects_mcp +5 -3
  7. data/lib/prompt_objects/capability.rb +33 -0
  8. data/lib/prompt_objects/cli/diagnostics_command.rb +69 -0
  9. data/lib/prompt_objects/cli/env_command.rb +435 -0
  10. data/lib/prompt_objects/cli/events_command.rb +145 -0
  11. data/lib/prompt_objects/cli/explore_command.rb +155 -0
  12. data/lib/prompt_objects/cli/helpers.rb +91 -0
  13. data/lib/prompt_objects/cli/message_command.rb +216 -0
  14. data/lib/prompt_objects/cli/repl_command.rb +366 -0
  15. data/lib/prompt_objects/cli/serve_command.rb +162 -0
  16. data/lib/prompt_objects/cli.rb +64 -422
  17. data/lib/prompt_objects/connectors/base.rb +16 -29
  18. data/lib/prompt_objects/connectors/mcp.rb +82 -98
  19. data/lib/prompt_objects/domain/records.rb +45 -0
  20. data/lib/prompt_objects/environment/exporter.rb +5 -4
  21. data/lib/prompt_objects/environment/git.rb +21 -1
  22. data/lib/prompt_objects/environment/importer.rb +5 -4
  23. data/lib/prompt_objects/environment/manager.rb +44 -6
  24. data/lib/prompt_objects/environment/manifest.rb +26 -4
  25. data/lib/prompt_objects/environment.rb +1350 -18
  26. data/lib/prompt_objects/execution/blocking_executor.rb +88 -0
  27. data/lib/prompt_objects/execution/context.rb +137 -0
  28. data/lib/prompt_objects/execution/contracts.rb +59 -0
  29. data/lib/prompt_objects/execution/permit_pool.rb +49 -0
  30. data/lib/prompt_objects/execution/policy.rb +47 -0
  31. data/lib/prompt_objects/execution/resource_lock_manager.rb +63 -0
  32. data/lib/prompt_objects/execution/runner.rb +498 -0
  33. data/lib/prompt_objects/execution/scheduler.rb +325 -0
  34. data/lib/prompt_objects/llm/factory.rb +4 -4
  35. data/lib/prompt_objects/llm/gemini_adapter.rb +25 -8
  36. data/lib/prompt_objects/llm/openai_adapter.rb +9 -1
  37. data/lib/prompt_objects/llm/pricing.rb +9 -2
  38. data/lib/prompt_objects/llm/response.rb +9 -4
  39. data/lib/prompt_objects/loader.rb +28 -1
  40. data/lib/prompt_objects/mcp/server.rb +6 -3
  41. data/lib/prompt_objects/mcp/tools/get_conversation.rb +15 -11
  42. data/lib/prompt_objects/mcp/tools/get_pending_requests.rb +2 -2
  43. data/lib/prompt_objects/mcp/tools/respond_to_request.rb +3 -3
  44. data/lib/prompt_objects/mcp/tools/send_message.rb +42 -32
  45. data/lib/prompt_objects/operations/diagnostics.rb +115 -0
  46. data/lib/prompt_objects/operations/retention.rb +75 -0
  47. data/lib/prompt_objects/operations/run_trace_builder.rb +152 -0
  48. data/lib/prompt_objects/primitives/http_get.rb +2 -0
  49. data/lib/prompt_objects/primitives/list_files.rb +2 -0
  50. data/lib/prompt_objects/primitives/read_file.rb +2 -0
  51. data/lib/prompt_objects/primitives/think.rb +36 -0
  52. data/lib/prompt_objects/primitives/write_file.rb +9 -0
  53. data/lib/prompt_objects/prompt_object.rb +190 -432
  54. data/lib/prompt_objects/repositories/artifact_repository.rb +109 -0
  55. data/lib/prompt_objects/repositories/base_repository.rb +37 -0
  56. data/lib/prompt_objects/repositories/definition_repository.rb +36 -0
  57. data/lib/prompt_objects/repositories/env_data_repository.rb +103 -0
  58. data/lib/prompt_objects/repositories/event_repository.rb +105 -0
  59. data/lib/prompt_objects/repositories/message_repository.rb +73 -0
  60. data/lib/prompt_objects/repositories/run_repository.rb +254 -0
  61. data/lib/prompt_objects/repositories/thread_repository.rb +87 -0
  62. data/lib/prompt_objects/repositories/tool_execution_repository.rb +88 -0
  63. data/lib/prompt_objects/repositories/workspace_session_repository.rb +61 -0
  64. data/lib/prompt_objects/server/api/routes.rb +561 -52
  65. data/lib/prompt_objects/server/app.rb +134 -1
  66. data/lib/prompt_objects/server/file_watcher.rb +9 -0
  67. data/lib/prompt_objects/server/public/assets/index-BjKlODDD.js +90 -0
  68. data/lib/prompt_objects/server/public/assets/index-w3fpIb5c.css +1 -0
  69. data/lib/prompt_objects/server/public/index.html +2 -2
  70. data/lib/prompt_objects/server/websocket_handler.rb +202 -207
  71. data/lib/prompt_objects/server.rb +170 -1
  72. data/lib/prompt_objects/service.rb +129 -0
  73. data/lib/prompt_objects/session/store.rb +774 -15
  74. data/lib/prompt_objects/universal/add_capability.rb +1 -1
  75. data/lib/prompt_objects/universal/create_capability.rb +4 -2
  76. data/lib/prompt_objects/universal/delete_env_data.rb +78 -0
  77. data/lib/prompt_objects/universal/delete_primitive.rb +1 -1
  78. data/lib/prompt_objects/universal/dispatch_po.rb +54 -0
  79. data/lib/prompt_objects/universal/get_env_data.rb +58 -0
  80. data/lib/prompt_objects/universal/get_run_status.rb +30 -0
  81. data/lib/prompt_objects/universal/join_runs.rb +35 -0
  82. data/lib/prompt_objects/universal/list_env_data.rb +57 -0
  83. data/lib/prompt_objects/universal/modify_primitive.rb +1 -1
  84. data/lib/prompt_objects/universal/render_artifact.rb +91 -0
  85. data/lib/prompt_objects/universal/request_primitive.rb +1 -1
  86. data/lib/prompt_objects/universal/set_env_data.rb +101 -0
  87. data/lib/prompt_objects/version.rb +5 -0
  88. data/lib/prompt_objects.rb +36 -6
  89. data/objects/coordinator.md +8 -4
  90. data/templates/arc-agi-1/objects/data_manager.md +2 -2
  91. data/templates/arc-agi-1/objects/observer.md +6 -2
  92. data/templates/arc-agi-1/objects/solver.md +14 -4
  93. data/templates/arc-agi-1/objects/verifier.md +6 -2
  94. data/templates/arc-agi-3/manifest.yml +19 -0
  95. data/templates/arc-agi-3/objects/analyst.md +79 -0
  96. data/templates/arc-agi-3/objects/player.md +76 -0
  97. data/templates/arc-agi-3/objects/strategist.md +97 -0
  98. data/templates/arc-agi-3/primitives/action_log.rb +133 -0
  99. data/templates/arc-agi-3/primitives/arc3_api.rb +223 -0
  100. data/templates/arc-agi-3/primitives/arc3_scorecard.rb +131 -0
  101. data/templates/arc-agi-3/primitives/frame_diff.rb +143 -0
  102. data/templates/arc-agi-3/primitives/frame_info.rb +96 -0
  103. data/templates/arc-agi-3/primitives/render_frame.rb +140 -0
  104. data/templates/basic/objects/basic.md +11 -3
  105. data/templates/chiptune/extras/sonic_pi_boot.rb +220 -0
  106. data/templates/chiptune/manifest.yml +20 -0
  107. data/templates/chiptune/objects/conductor.md +184 -0
  108. data/templates/chiptune/objects/sonic_pi.md +65 -0
  109. data/templates/chiptune/objects/voice_bass.md +44 -0
  110. data/templates/chiptune/objects/voice_lead.md +44 -0
  111. data/templates/chiptune/objects/voice_pad.md +43 -0
  112. data/templates/chiptune/objects/voice_perc.md +45 -0
  113. data/templates/chiptune/primitives/play_note.rb +59 -0
  114. data/templates/chiptune/primitives/play_sequence.rb +55 -0
  115. data/templates/chiptune/primitives/query_sonic_pi.rb +141 -0
  116. data/templates/chiptune/primitives/set_synth.rb +57 -0
  117. data/templates/chiptune/primitives/set_tempo.rb +45 -0
  118. data/templates/chiptune/primitives/stop_voice.rb +46 -0
  119. data/templates/chiptune/primitives/support/osc_bridge.rb +33 -0
  120. data/templates/chiptune/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  121. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  122. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  123. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  124. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  125. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  126. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  127. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  128. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  129. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  130. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  131. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  132. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  133. data/templates/chiptune/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  134. data/templates/developer/objects/code_reviewer.md +1 -1
  135. data/templates/developer/objects/coordinator.md +16 -3
  136. data/templates/developer/objects/debugger.md +3 -3
  137. data/templates/empty/objects/assistant.md +10 -5
  138. data/templates/minimal/objects/assistant.md +10 -5
  139. data/templates/music/extras/sonic_pi_boot.rb +220 -0
  140. data/templates/music/manifest.yml +16 -0
  141. data/templates/music/objects/conductor.md +154 -0
  142. data/templates/music/objects/sonic_pi.md +65 -0
  143. data/templates/music/primitives/play_note.rb +55 -0
  144. data/templates/music/primitives/play_sequence.rb +51 -0
  145. data/templates/music/primitives/query_sonic_pi.rb +139 -0
  146. data/templates/music/primitives/set_synth.rb +53 -0
  147. data/templates/music/primitives/set_tempo.rb +43 -0
  148. data/templates/music/primitives/stop_voice.rb +44 -0
  149. data/templates/music/primitives/support/osc_bridge.rb +33 -0
  150. data/templates/music/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  151. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  152. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  153. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  154. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  155. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  156. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  157. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  158. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  159. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  160. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  161. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  162. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  163. data/templates/music/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  164. data/templates/music-beginner/README.md +100 -0
  165. data/templates/music-beginner/extras/README.md +27 -0
  166. data/templates/music-beginner/extras/sonic_pi_boot.rb +297 -0
  167. data/templates/music-beginner/manifest.yml +25 -0
  168. data/templates/music-beginner/objects/conductor.md +222 -0
  169. data/templates/music-beginner/objects/engine_setup.md +54 -0
  170. data/templates/music-beginner/objects/voice_bass.md +43 -0
  171. data/templates/music-beginner/objects/voice_keys.md +48 -0
  172. data/templates/music-beginner/objects/voice_pad.md +46 -0
  173. data/templates/music-beginner/objects/voice_percussion.md +45 -0
  174. data/templates/music-beginner/objects/voice_texture.md +37 -0
  175. data/templates/music-beginner/primitives/play_pattern.rb +118 -0
  176. data/templates/music-beginner/primitives/query_music_engine.rb +94 -0
  177. data/templates/music-beginner/primitives/set_instrument.rb +47 -0
  178. data/templates/music-beginner/primitives/set_tempo.rb +43 -0
  179. data/templates/music-beginner/primitives/set_voice_level.rb +46 -0
  180. data/templates/music-beginner/primitives/stop_voice.rb +37 -0
  181. data/templates/music-beginner/primitives/support/osc_bridge.rb +25 -0
  182. data/templates/music-beginner/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  183. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  184. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  185. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  186. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  187. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  188. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  189. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  190. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  191. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  192. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  193. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  194. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  195. data/templates/music-beginner/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  196. data/templates/music-pro/extras/AUDITION.md +100 -0
  197. data/templates/music-pro/extras/sonic_pi_boot.rb +546 -0
  198. data/templates/music-pro/manifest.yml +19 -0
  199. data/templates/music-pro/objects/conductor.md +126 -0
  200. data/templates/music-pro/objects/sonic_pi.md +35 -0
  201. data/templates/music-pro/objects/voice_harmony.md +48 -0
  202. data/templates/music-pro/objects/voice_rhythm.md +43 -0
  203. data/templates/music-pro/objects/voice_texture.md +42 -0
  204. data/templates/music-pro/primitives/define_patch.rb +46 -0
  205. data/templates/music-pro/primitives/query_music_engine.rb +69 -0
  206. data/templates/music-pro/primitives/set_live_pattern.rb +70 -0
  207. data/templates/music-pro/primitives/set_tempo.rb +31 -0
  208. data/templates/music-pro/primitives/stop_voice.rb +31 -0
  209. data/templates/music-pro/primitives/support/music_protocol.rb +136 -0
  210. data/templates/music-pro/primitives/support/osc_bridge.rb +33 -0
  211. data/templates/music-pro/primitives/trigger_event.rb +44 -0
  212. data/templates/music-pro/vendor/osc-ruby-1.1.5/LICENSE +19 -0
  213. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/address_pattern.rb +58 -0
  214. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/broadcast_client.rb +24 -0
  215. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/bundle.rb +46 -0
  216. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/client.rb +13 -0
  217. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/core_ext/time.rb +6 -0
  218. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/em_server.rb +63 -0
  219. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/message.rb +46 -0
  220. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/network_packet.rb +42 -0
  221. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_argument.rb +26 -0
  222. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_packet.rb +140 -0
  223. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/osc_types.rb +53 -0
  224. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby/server.rb +91 -0
  225. data/templates/music-pro/vendor/osc-ruby-1.1.5/lib/osc-ruby.rb +18 -0
  226. data/templates/writer/objects/coordinator.md +14 -3
  227. data/templates/writer/objects/editor.md +1 -1
  228. data/templates/writer/objects/researcher.md +2 -2
  229. data/tools/thread-explorer.html +28 -1
  230. metadata +175 -65
  231. data/CLAUDE.md +0 -176
  232. data/Gemfile +0 -10
  233. data/Gemfile.lock +0 -233
  234. data/IMPLEMENTATION_PLAN.md +0 -1073
  235. data/Rakefile +0 -27
  236. data/design-doc-v2.md +0 -1232
  237. data/frontend/.gitignore +0 -3
  238. data/frontend/index.html +0 -17
  239. data/frontend/package-lock.json +0 -4540
  240. data/frontend/package.json +0 -36
  241. data/frontend/postcss.config.js +0 -6
  242. data/frontend/src/App.tsx +0 -103
  243. data/frontend/src/canvas/CanvasView.tsx +0 -113
  244. data/frontend/src/canvas/ForceLayout.ts +0 -115
  245. data/frontend/src/canvas/SceneManager.ts +0 -587
  246. data/frontend/src/canvas/canvasStore.ts +0 -47
  247. data/frontend/src/canvas/constants.ts +0 -95
  248. data/frontend/src/canvas/controls/CameraControls.ts +0 -98
  249. data/frontend/src/canvas/edges/MessageArc.ts +0 -149
  250. data/frontend/src/canvas/inspector/InspectorPanel.tsx +0 -31
  251. data/frontend/src/canvas/inspector/POInspector.tsx +0 -262
  252. data/frontend/src/canvas/inspector/ToolCallInspector.tsx +0 -67
  253. data/frontend/src/canvas/nodes/PONode.ts +0 -249
  254. data/frontend/src/canvas/nodes/ToolCallNode.ts +0 -116
  255. data/frontend/src/canvas/types.ts +0 -24
  256. data/frontend/src/components/ContextMenu.tsx +0 -68
  257. data/frontend/src/components/Inspector.tsx +0 -232
  258. data/frontend/src/components/MarkdownMessage.tsx +0 -155
  259. data/frontend/src/components/MethodList.tsx +0 -90
  260. data/frontend/src/components/ModelSelector.tsx +0 -115
  261. data/frontend/src/components/NotificationPanel.tsx +0 -130
  262. data/frontend/src/components/ObjectList.tsx +0 -78
  263. data/frontend/src/components/PaneSlot.tsx +0 -30
  264. data/frontend/src/components/SourcePane.tsx +0 -202
  265. data/frontend/src/components/SystemBar.tsx +0 -74
  266. data/frontend/src/components/Transcript.tsx +0 -76
  267. data/frontend/src/components/UsagePanel.tsx +0 -105
  268. data/frontend/src/components/Workspace.tsx +0 -260
  269. data/frontend/src/components/index.ts +0 -12
  270. data/frontend/src/hooks/useResize.ts +0 -55
  271. data/frontend/src/hooks/useWebSocket.ts +0 -517
  272. data/frontend/src/index.css +0 -103
  273. data/frontend/src/main.tsx +0 -10
  274. data/frontend/src/store/index.ts +0 -279
  275. data/frontend/src/types/index.ts +0 -165
  276. data/frontend/tailwind.config.js +0 -44
  277. data/frontend/tsconfig.json +0 -30
  278. data/frontend/vite.config.ts +0 -29
  279. data/lib/prompt_objects/server/public/assets/index-D1myxE0l.js +0 -4345
  280. data/lib/prompt_objects/server/public/assets/index-DdCcwC-Z.css +0 -1
  281. data/lib/prompt_objects/universal/add_primitive.rb +0 -118
  282. data/lib/prompt_objects/universal/ask_human.rb +0 -109
  283. data/lib/prompt_objects/universal/create_primitive.rb +0 -174
  284. data/lib/prompt_objects/universal/list_primitives.rb +0 -145
  285. data/lib/prompt_objects/universal/think.rb +0 -41
  286. data/prompt_objects.gemspec +0 -50
  287. data/templates/basic/.gitignore +0 -2
  288. data/templates/developer/.gitignore +0 -5
  289. data/templates/empty/.gitignore +0 -5
  290. data/templates/empty/objects/.gitkeep +0 -0
  291. data/templates/minimal/.gitignore +0 -5
  292. data/templates/writer/.gitignore +0 -5
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "json"
4
4
  require "rack"
5
+ require "yaml"
5
6
 
6
7
  module PromptObjects
7
8
  module Server
@@ -11,11 +12,27 @@ module PromptObjects
11
12
  class Routes
12
13
  def initialize(runtime)
13
14
  @runtime = runtime
15
+ if runtime.session_store
16
+ @workspace_repository = Repositories::WorkspaceSessionRepository.new(runtime.session_store)
17
+ @thread_repository = Repositories::ThreadRepository.new(runtime.session_store)
18
+ @run_repository = Repositories::RunRepository.new(runtime.session_store)
19
+ @message_repository = Repositories::MessageRepository.new(runtime.session_store)
20
+ @event_repository = Repositories::EventRepository.new(runtime.session_store)
21
+ @artifact_repository = Repositories::ArtifactRepository.new(runtime.session_store)
22
+ @env_data_repository = Repositories::EnvDataRepository.new(runtime.session_store)
23
+ @run_trace_builder = Operations::RunTraceBuilder.new(runtime.session_store)
24
+ end
14
25
  end
15
26
 
16
27
  def call(env)
17
28
  request = Rack::Request.new(env)
18
- path = request.path_info.sub("/api", "")
29
+ # Falcon delivers PATH_INFO as ASCII-8BIT. SQLite binds binary strings
30
+ # as BLOBs, which never compare equal to TEXT columns — so any keyed
31
+ # lookup from a URL path (artifacts, sessions) would silently miss.
32
+ # Normalize to UTF-8 before routing.
33
+ path = request.path_info.dup.force_encoding(Encoding::UTF_8)
34
+ path = path.scrub unless path.valid_encoding?
35
+ path = path.sub("/api", "")
19
36
 
20
37
  response = route(request, path)
21
38
  json_response(response)
@@ -42,6 +59,8 @@ module PromptObjects
42
59
  case path
43
60
  when "/environment"
44
61
  get_environment
62
+ when "/diagnostics"
63
+ @runtime.operational_diagnostics
45
64
  when "/prompt_objects"
46
65
  list_prompt_objects
47
66
  when "/primitives"
@@ -56,8 +75,38 @@ module PromptObjects
56
75
  list_sessions($1)
57
76
  when %r{^/prompt_objects/([^/]+)$}
58
77
  get_prompt_object($1)
78
+ when "/sessions"
79
+ get_environment_sessions
80
+ when "/workspaces"
81
+ list_workspaces
82
+ when %r{^/workspaces/([^/]+)/snapshot$}
83
+ get_workspace_snapshot($1)
84
+ when %r{^/workspaces/([^/]+)/threads$}
85
+ list_runtime_threads($1, _request)
86
+ when %r{^/workspaces/([^/]+)/runs$}
87
+ list_runtime_runs($1, _request)
88
+ when %r{^/workspaces/([^/]+)/events$}
89
+ list_workspace_events($1, _request)
90
+ when %r{^/workspaces/([^/]+)/artifacts$}
91
+ list_runtime_artifacts($1)
92
+ when %r{^/workspaces/([^/]+)/env_data$}
93
+ list_runtime_env_data($1)
94
+ when %r{^/threads/([^/]+)/messages$}
95
+ list_runtime_messages($1, _request)
96
+ when %r{^/threads/([^/]+)$}
97
+ get_runtime_thread($1)
98
+ when %r{^/runs/([^/]+)/events$}
99
+ list_run_events($1, _request)
100
+ when %r{^/runs/([^/]+)/trace$}
101
+ get_run_trace($1)
102
+ when %r{^/runs/([^/]+)$}
103
+ get_runtime_run($1)
59
104
  when %r{^/events/session/([^/]+)$}
60
105
  get_session_events($1)
106
+ when %r{^/artifacts/([^/]+)$}
107
+ get_artifact($1)
108
+ when %r{^/sessions/([^/]+)/env_data$}
109
+ get_session_env_data($1)
61
110
  when %r{^/sessions/([^/]+)/usage$}
62
111
  get_session_usage($1, _request)
63
112
  when %r{^/sessions/([^/]+)/export$}
@@ -69,12 +118,32 @@ module PromptObjects
69
118
 
70
119
  def route_post(request, path)
71
120
  case path
121
+ when "/messages"
122
+ inject_message(request.body.read)
72
123
  when "/prompt_objects"
73
124
  create_prompt_object(request.body.read)
74
125
  when %r{^/prompt_objects/([^/]+)/message$}
75
126
  send_message($1, request.body.read)
76
127
  when %r{^/prompt_objects/([^/]+)/sessions$}
77
128
  create_session($1, request.body.read)
129
+ when "/sessions"
130
+ create_environment_session_endpoint(request.body.read)
131
+ when %r{^/sessions/([^/]+)/activate$}
132
+ activate_session($1)
133
+ when %r{^/sessions/([^/]+)/rename$}
134
+ rename_session_endpoint($1, request.body.read)
135
+ when %r{^/sessions/([^/]+)/delete$}
136
+ delete_session_endpoint($1)
137
+ when %r{^/artifacts/([^/]+)/message$}
138
+ artifact_message($1, request.body.read)
139
+ when %r{^/runs/([^/]+)/cancel$}
140
+ cancel_runtime_run($1, request.body.read)
141
+ when %r{^/workspaces/([^/]+)/threads$}
142
+ create_runtime_thread($1, request.body.read)
143
+ when %r{^/threads/([^/]+)/rename$}
144
+ rename_runtime_thread($1, request.body.read)
145
+ when %r{^/threads/([^/]+)/archive$}
146
+ archive_runtime_thread($1)
78
147
  else
79
148
  { error: "Not found", path: path }
80
149
  end
@@ -122,13 +191,89 @@ module PromptObjects
122
191
  po_full(po)
123
192
  end
124
193
 
194
+ # POST /api/prompt_objects — create a new Prompt-Object from JSON.
195
+ # Body: { name, description, capabilities?, identity?, behavior? }
196
+ # Mirrors the create_capability universal: writes a .md and loads it.
125
197
  def create_prompt_object(body)
126
- # TODO: Implement PO creation via API
127
- { error: "Not implemented" }
198
+ params = JSON.parse(body)
199
+ name = params["name"].to_s
200
+ description = params["description"].to_s
201
+ capabilities = params["capabilities"] || []
202
+ identity = params["identity"] || "You are a helpful prompt object."
203
+ behavior = params["behavior"] || "Help with the request."
204
+
205
+ return { error: "name is required" } if name.empty?
206
+ unless name.match?(/\A[a-z][a-z0-9_]*\z/)
207
+ return { error: "name must be lowercase letters, numbers, and underscores, starting with a letter" }
208
+ end
209
+ return { error: "A capability named '#{name}' already exists" } if @runtime.registry.exists?(name)
210
+ return { error: "No objects directory available" } unless @runtime.objects_dir
211
+
212
+ frontmatter = { "name" => name, "description" => description, "capabilities" => capabilities }
213
+ body_md = <<~MD
214
+ # #{name.split('_').map(&:capitalize).join(' ')}
215
+
216
+ ## Identity
217
+
218
+ #{identity}
219
+
220
+ ## Behavior
221
+
222
+ #{behavior}
223
+ MD
224
+ content = "#{frontmatter.to_yaml}---\n\n#{body_md}"
225
+
226
+ path = File.join(@runtime.objects_dir, "#{name}.md")
227
+ File.write(path, content, encoding: "UTF-8")
228
+ po = @runtime.load_prompt_object(path)
229
+
230
+ po_summary(po)
231
+ rescue JSON::ParserError
232
+ { error: "Invalid JSON body" }
233
+ rescue => e
234
+ { error: e.message }
128
235
  end
129
236
 
130
237
  # === Messages ===
131
238
 
239
+ # POST /api/messages — Universal message injection endpoint.
240
+ # External systems (webhooks, cron, bots) use this to trigger POs.
241
+ # Uses runtime.inject_message — same code path as human/timer messages.
242
+ def inject_message(body)
243
+ params = JSON.parse(body)
244
+ target_po = params["target_po"]
245
+ content = params["content"]
246
+ from_source = params["from_source"] || "api"
247
+
248
+ return { error: "target_po is required" } unless target_po && !target_po.empty?
249
+ return { error: "content is required" } unless content && !content.empty?
250
+
251
+ run, response = @runtime.execute_message_run(
252
+ target_po: target_po,
253
+ content: content,
254
+ from_source: from_source,
255
+ workspace_session_id: params["workspace_session_id"],
256
+ thread_id: params["thread_id"],
257
+ client_request_id: params["client_request_id"],
258
+ deadline: params["deadline"],
259
+ tui_mode: false
260
+ )
261
+ {
262
+ response: response,
263
+ run_id: run.id,
264
+ thread_id: run.thread_id,
265
+ workspace_session_id: run.workspace_session_id,
266
+ status: run.status,
267
+ target_po: target_po,
268
+ from_source: from_source
269
+ }
270
+ rescue JSON::ParserError
271
+ { error: "Invalid JSON body" }
272
+ rescue PromptObjects::Error => e
273
+ { error: e.message }
274
+ end
275
+
276
+ # POST /api/prompt_objects/:name/message — Legacy per-PO message endpoint.
132
277
  def send_message(po_name, body)
133
278
  po = @runtime.registry.get(po_name)
134
279
 
@@ -138,50 +283,76 @@ module PromptObjects
138
283
 
139
284
  params = JSON.parse(body)
140
285
  message = params["message"]
141
- session_id = params["session_id"]
142
286
  new_thread = params["new_thread"]
143
287
 
144
288
  return { error: "Message is required" } unless message && !message.empty?
145
289
 
146
- # Create a new thread if requested
290
+ workspace_id = params["workspace_session_id"] || @runtime.active_session(initiated_by: "api")[:id]
291
+ thread_id = params["thread_id"] || params["session_id"]
147
292
  if new_thread
148
- session_id = po.new_thread
293
+ thread_id = @runtime.create_runtime_thread(
294
+ workspace_session_id: workspace_id,
295
+ po_name: po_name,
296
+ thread_type: "api",
297
+ name: params["name"]
298
+ ).id
149
299
  end
150
-
151
- # Switch to specified session if provided
152
- if session_id && session_id != po.session_id
153
- po.switch_session(session_id)
154
- end
155
-
156
- request_session_id = po.session_id
157
-
158
- # Send the message through the same path as WebSocket
159
- context = @runtime.context(tui_mode: false)
160
- context.current_capability = "human"
161
-
162
- # Log to bus
163
- @runtime.bus.publish(from: "human", to: po.name, message: message, session_id: request_session_id)
164
-
165
- response = po.receive(message, context: context)
166
-
167
- # Log response to bus
168
- @runtime.bus.publish(from: po.name, to: "human", message: response, session_id: request_session_id)
169
-
170
- # Count events for this session
171
- event_count = if @runtime.session_store
172
- @runtime.session_store.get_events(session_id: request_session_id).length
173
- end
174
-
300
+ run, response = @runtime.execute_message_run(
301
+ target_po: po_name,
302
+ content: message,
303
+ from_source: params["from_source"] || "api",
304
+ workspace_session_id: workspace_id,
305
+ thread_id: thread_id,
306
+ client_request_id: params["client_request_id"],
307
+ deadline: params["deadline"],
308
+ tui_mode: false
309
+ )
175
310
  {
176
311
  response: response,
177
- po_name: po.name,
178
- session_id: request_session_id,
179
- event_count: event_count
312
+ po_name: po_name,
313
+ session_id: run.thread_id,
314
+ thread_id: run.thread_id,
315
+ run_id: run.id,
316
+ workspace_session_id: run.workspace_session_id,
317
+ status: run.status,
318
+ event_count: @event_repository.run_events(run_id: run.id).length
180
319
  }
181
320
  rescue JSON::ParserError
182
321
  { error: "Invalid JSON body" }
183
322
  end
184
323
 
324
+ def cancel_runtime_run(run_id, body)
325
+ params = body.to_s.empty? ? {} : JSON.parse(body)
326
+ cancelled = @runtime.cancel_run(run_id, reason: params["reason"] || "api request")
327
+ { run_id: run_id, cancelled: cancelled }
328
+ rescue JSON::ParserError
329
+ { error: "Invalid JSON body" }
330
+ end
331
+
332
+ def create_runtime_thread(workspace_id, body)
333
+ params = JSON.parse(body)
334
+ thread = @runtime.create_runtime_thread(
335
+ workspace_session_id: workspace_id,
336
+ po_name: params["po_name"],
337
+ thread_type: params["thread_type"] || "human",
338
+ name: params["name"]
339
+ )
340
+ serialize_record(thread)
341
+ rescue JSON::ParserError
342
+ { error: "Invalid JSON body" }
343
+ end
344
+
345
+ def rename_runtime_thread(thread_id, body)
346
+ params = JSON.parse(body)
347
+ serialize_record(@runtime.rename_runtime_thread(thread_id, name: params["name"]))
348
+ rescue JSON::ParserError
349
+ { error: "Invalid JSON body" }
350
+ end
351
+
352
+ def archive_runtime_thread(thread_id)
353
+ serialize_record(@runtime.archive_runtime_thread(thread_id))
354
+ end
355
+
185
356
  # === Sessions ===
186
357
 
187
358
  def list_sessions(po_name)
@@ -191,9 +362,12 @@ module PromptObjects
191
362
  return { error: "Not found", name: po_name }
192
363
  end
193
364
 
194
- sessions = po.list_sessions.map do |s|
195
- PromptObject.serialize_session(s)
196
- end
365
+ workspace_id = @runtime.active_session(initiated_by: "api")[:id]
366
+ sessions = @thread_repository.list(
367
+ workspace_session_id: workspace_id,
368
+ po_name: po_name,
369
+ include_archived: true
370
+ ).map { |thread| serialize_record(thread) }
197
371
 
198
372
  { sessions: sessions }
199
373
  end
@@ -205,23 +379,19 @@ module PromptObjects
205
379
  return { error: "Prompt object not found", name: po_name }
206
380
  end
207
381
 
208
- return { error: "No session store" } unless @runtime.session_store
209
-
210
- session = @runtime.session_store.get_session(session_id)
211
-
212
- unless session && session[:po_name] == po_name
382
+ thread = @thread_repository.get(session_id)
383
+ unless thread && thread.po_name == po_name
213
384
  return { error: "Session not found", id: session_id }
214
385
  end
215
-
216
- messages = @runtime.session_store.get_messages(session_id)
386
+ messages = @message_repository.list(thread_id: session_id, limit: 10_000)
217
387
 
218
388
  {
219
- id: session[:id],
220
- name: session[:name],
221
- po_name: session[:po_name],
389
+ id: thread.id,
390
+ name: thread.name,
391
+ po_name: thread.po_name,
222
392
  messages: messages.map { |m| format_message(m) },
223
- created_at: session[:created_at]&.iso8601,
224
- updated_at: session[:updated_at]&.iso8601
393
+ created_at: thread.created_at&.iso8601,
394
+ updated_at: thread.updated_at&.iso8601
225
395
  }
226
396
  end
227
397
 
@@ -235,15 +405,173 @@ module PromptObjects
235
405
  params = body.empty? ? {} : JSON.parse(body)
236
406
  session_name = params["name"]
237
407
 
238
- session_id = po.new_session(name: session_name)
408
+ workspace_id = params["workspace_session_id"] || @runtime.active_session(initiated_by: "api")[:id]
409
+ thread = @runtime.create_runtime_thread(
410
+ workspace_session_id: workspace_id,
411
+ po_name: po_name,
412
+ thread_type: "human",
413
+ name: session_name
414
+ )
239
415
 
240
416
  {
241
417
  success: true,
242
- session_id: session_id,
418
+ session_id: thread.id,
419
+ thread_id: thread.id,
420
+ workspace_session_id: thread.workspace_session_id,
243
421
  name: session_name
244
422
  }
245
423
  end
246
424
 
425
+ # === Run runtime queries ===
426
+
427
+ def list_workspaces
428
+ return no_runtime_store unless @workspace_repository
429
+
430
+ { workspaces: @workspace_repository.list.map { |workspace| serialize_record(workspace) } }
431
+ end
432
+
433
+ def get_workspace_snapshot(id)
434
+ return no_runtime_store unless @workspace_repository
435
+
436
+ workspace = @workspace_repository.get(id)
437
+ return { error: "Workspace not found", id: id } unless workspace
438
+
439
+ {
440
+ version: Execution::Contracts::PROTOCOL_VERSION,
441
+ type: "workspace_snapshot",
442
+ payload: {
443
+ cursor: @event_repository.current_cursor(workspace_session_id: id),
444
+ workspace: serialize_record(workspace),
445
+ threads: @thread_repository.list(
446
+ workspace_session_id: id,
447
+ include_archived: true
448
+ ).map { |thread| serialize_record(thread) },
449
+ runs: @run_repository.list(
450
+ workspace_session_id: id,
451
+ limit: 500
452
+ ).map { |run| serialize_record(run) },
453
+ artifacts: @artifact_repository.list(
454
+ workspace_session_id: id
455
+ ).map { |artifact| serialize_record(artifact) }
456
+ }
457
+ }
458
+ end
459
+
460
+ def list_runtime_threads(workspace_id, request)
461
+ return no_runtime_store unless @thread_repository
462
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
463
+
464
+ threads = @thread_repository.list(
465
+ workspace_session_id: workspace_id,
466
+ po_name: request.params["po_name"],
467
+ include_archived: request.params["include_archived"] == "true"
468
+ )
469
+ { threads: threads.map { |thread| serialize_record(thread) } }
470
+ end
471
+
472
+ def get_runtime_thread(id)
473
+ return no_runtime_store unless @thread_repository
474
+
475
+ thread = @thread_repository.get(id)
476
+ thread ? serialize_record(thread) : { error: "Thread not found", id: id }
477
+ end
478
+
479
+ def list_runtime_messages(thread_id, request)
480
+ return no_runtime_store unless @message_repository
481
+ return { error: "Thread not found", id: thread_id } unless @thread_repository.get(thread_id)
482
+
483
+ after_id = nonnegative_integer(request.params["after_id"])
484
+ limit = bounded_limit(request.params["limit"], maximum: 500)
485
+ messages = @message_repository.list(thread_id: thread_id, after_id: after_id, limit: limit)
486
+ {
487
+ thread_id: thread_id,
488
+ messages: messages.map { |message| serialize_runtime_message(message) },
489
+ next_after_id: messages.last&.dig(:id)
490
+ }
491
+ end
492
+
493
+ def list_runtime_runs(workspace_id, request)
494
+ return no_runtime_store unless @run_repository
495
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
496
+
497
+ statuses = request.params["status"]&.split(",")
498
+ runs = @run_repository.list(
499
+ workspace_session_id: workspace_id,
500
+ thread_id: request.params["thread_id"],
501
+ statuses: statuses,
502
+ limit: bounded_limit(request.params["limit"], maximum: 500)
503
+ )
504
+ { runs: runs.map { |run| serialize_record(run) } }
505
+ rescue ArgumentError => e
506
+ { error: e.message }
507
+ end
508
+
509
+ def get_runtime_run(id)
510
+ return no_runtime_store unless @run_repository
511
+
512
+ run = @run_repository.get(id)
513
+ run ? serialize_record(run) : { error: "Run not found", id: id }
514
+ end
515
+
516
+ def list_run_events(run_id, request)
517
+ return no_runtime_store unless @event_repository
518
+ return { error: "Run not found", id: run_id } unless @run_repository.get(run_id)
519
+
520
+ events = @event_repository.run_events(
521
+ run_id: run_id,
522
+ after_sequence: nonnegative_integer(request.params["after_sequence"])
523
+ )
524
+ { run_id: run_id, events: events.map { |event| serialize_record(event) } }
525
+ end
526
+
527
+ def get_run_trace(run_id)
528
+ return no_runtime_store unless @run_trace_builder
529
+ return { error: "Run not found", id: run_id } unless @run_repository.get(run_id)
530
+
531
+ { trace: serialize_value(@run_trace_builder.build(run_id)) }
532
+ end
533
+
534
+ def list_workspace_events(workspace_id, request)
535
+ return no_runtime_store unless @event_repository
536
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
537
+
538
+ events = @event_repository.workspace_events(
539
+ workspace_session_id: workspace_id,
540
+ after_cursor: nonnegative_integer(request.params["after_cursor"]),
541
+ limit: bounded_limit(request.params["limit"], maximum: 1_000)
542
+ )
543
+ {
544
+ workspace_session_id: workspace_id,
545
+ events: events.map { |event| serialize_value(event) },
546
+ next_cursor: events.last&.dig(:cursor) || nonnegative_integer(request.params["after_cursor"])
547
+ }
548
+ end
549
+
550
+ def list_runtime_artifacts(workspace_id)
551
+ return no_runtime_store unless @artifact_repository
552
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
553
+
554
+ {
555
+ workspace_session_id: workspace_id,
556
+ cursor: @event_repository.current_cursor(workspace_session_id: workspace_id),
557
+ artifacts: @artifact_repository.list(
558
+ workspace_session_id: workspace_id
559
+ ).map { |artifact| serialize_record(artifact) }
560
+ }
561
+ end
562
+
563
+ def list_runtime_env_data(workspace_id)
564
+ return no_runtime_store unless @env_data_repository
565
+ return { error: "Workspace not found", id: workspace_id } unless @workspace_repository.get(workspace_id)
566
+
567
+ {
568
+ workspace_session_id: workspace_id,
569
+ entries: @env_data_repository.list(
570
+ workspace_session_id: workspace_id
571
+ ).map { |entry| serialize_value(entry) }
572
+ }
573
+ end
574
+
247
575
  # === Primitives ===
248
576
 
249
577
  def list_primitives
@@ -314,6 +642,144 @@ module PromptObjects
314
642
  }
315
643
  end
316
644
 
645
+ # === Artifacts ===
646
+
647
+ def get_artifact(key)
648
+ return { error: "No session store" } unless @runtime.session_store
649
+
650
+ artifact = @artifact_repository&.get(key)
651
+ return serialize_record(artifact).merge(rendered_by: artifact.owner_po) if artifact
652
+
653
+ { error: "Artifact not found", key: key }
654
+ rescue => e
655
+ { error: "Artifact lookup failed: #{e.message}", key: key }
656
+ end
657
+
658
+ # POST /api/artifacts/:key/message — BURP (Bidirectional UI Rendering
659
+ # Protocol). Interactive artifacts call PromptObjects.send(...) in the
660
+ # browser, which lands here; the message is injected into the PO that
661
+ # rendered the artifact, with provenance "artifact:<title>".
662
+ MAX_ARTIFACT_MESSAGE_LENGTH = 4_000
663
+
664
+ def artifact_message(key, body)
665
+ params = JSON.parse(body)
666
+ content = params["content"]
667
+
668
+ return { error: "content is required" } unless content.is_a?(String) && !content.strip.empty?
669
+ if content.length > MAX_ARTIFACT_MESSAGE_LENGTH
670
+ return { error: "content too long (max #{MAX_ARTIFACT_MESSAGE_LENGTH} characters)" }
671
+ end
672
+
673
+ artifact = get_artifact(key)
674
+ return artifact if artifact[:error]
675
+
676
+ target_po = artifact[:owner_po] || artifact[:rendered_by]
677
+ unless target_po && @runtime.registry.get(target_po).is_a?(PromptObject)
678
+ return { error: "Rendering PO '#{target_po}' not found for artifact", key: key }
679
+ end
680
+
681
+ from_source = "artifact:#{artifact[:title] || key}"
682
+ # The from_source provenance is UI metadata the LLM never sees — make
683
+ # the interface origin visible in the message itself, so the PO knows
684
+ # to respond by re-rendering the artifact rather than with prose.
685
+ framed_content = %([via your artifact "#{artifact[:title] || key}"] #{content}\n\n) +
686
+ "(The human is looking at the artifact, not this chat — respond by re-rendering " \
687
+ "\"#{artifact[:title] || key}\" with render_artifact in replace mode, updated for this action.)"
688
+ run = @runtime.enqueue_run(
689
+ target_po: target_po,
690
+ content: framed_content,
691
+ from_source: from_source,
692
+ workspace_session_id: artifact[:workspace_session_id],
693
+ thread_id: artifact[:owner_thread_id],
694
+ client_request_id: params["client_request_id"],
695
+ deadline: params["deadline"]
696
+ )
697
+ @runtime.schedule_run(run.id, tui_mode: false)
698
+ {
699
+ queued: true,
700
+ run_id: run.id,
701
+ thread_id: run.thread_id,
702
+ workspace_session_id: run.workspace_session_id,
703
+ target_po: target_po,
704
+ from_source: from_source
705
+ }
706
+ rescue JSON::ParserError
707
+ { error: "Invalid JSON body" }
708
+ rescue PromptObjects::Error => e
709
+ { error: e.message }
710
+ end
711
+
712
+ # === Environment Sessions (shared-context "jams") ===
713
+
714
+ # GET /api/sessions — list environment sessions, with which is active.
715
+ def get_environment_sessions
716
+ return { error: "No session store" } unless @runtime.session_store
717
+
718
+ { sessions: @runtime.environment_sessions.map { |s| serialize_environment_session(s) } }
719
+ end
720
+
721
+ # POST /api/sessions — create a new environment session (becomes active).
722
+ def create_environment_session_endpoint(body)
723
+ return { error: "No session store" } unless @runtime.session_store
724
+
725
+ name = parse_json(body)["name"]
726
+ session = @runtime.create_environment_session(name: name)
727
+ serialize_environment_session(session.merge(active: true))
728
+ end
729
+
730
+ # POST /api/sessions/:id/activate — switch the active environment session.
731
+ def activate_session(id)
732
+ session = @runtime.switch_active_session(id)
733
+ return { error: "Session not found", id: id } unless session
734
+
735
+ serialize_environment_session(session.merge(active: true))
736
+ end
737
+
738
+ # POST /api/sessions/:id/rename — rename an environment session.
739
+ def rename_session_endpoint(id, body)
740
+ name = parse_json(body)["name"]
741
+ return { error: "name is required" } if name.nil? || name.to_s.empty?
742
+
743
+ session = @runtime.rename_session(id, name)
744
+ return { error: "Session not found", id: id } unless session
745
+
746
+ serialize_environment_session(session)
747
+ end
748
+
749
+ # POST /api/sessions/:id/delete — delete an environment session.
750
+ def delete_session_endpoint(id)
751
+ ok = @runtime.delete_environment_session(id)
752
+ ok ? { deleted: id } : { error: "Session not found", id: id }
753
+ end
754
+
755
+ def serialize_environment_session(s)
756
+ {
757
+ id: s[:id],
758
+ name: s[:name],
759
+ active: s[:active] || false,
760
+ message_count: s[:message_count] || 0,
761
+ created_at: s[:created_at]&.iso8601,
762
+ updated_at: s[:updated_at]&.iso8601
763
+ }
764
+ end
765
+
766
+ def parse_json(body)
767
+ JSON.parse(body.to_s.empty? ? "{}" : body)
768
+ rescue JSON::ParserError
769
+ {}
770
+ end
771
+
772
+ # === Env Data ===
773
+
774
+ def get_session_env_data(session_id)
775
+ return { error: "No session store" } unless @runtime.session_store
776
+
777
+ root_thread_id = @runtime.env_data_scope(session_id)
778
+ entries = @runtime.session_store.list_env_data_full(root_thread_id: root_thread_id)
779
+
780
+ { root_thread_id: root_thread_id, entries: entries }
781
+ end
782
+
317
783
  # === Usage ===
318
784
 
319
785
  def get_session_usage(session_id, request)
@@ -365,6 +831,44 @@ module PromptObjects
365
831
 
366
832
  # === Helpers ===
367
833
 
834
+ def no_runtime_store
835
+ { error: "Run runtime storage is unavailable" }
836
+ end
837
+
838
+ def bounded_limit(value, maximum:)
839
+ requested = value.to_i
840
+ requested = 100 unless requested.positive?
841
+ [requested, maximum].min
842
+ end
843
+
844
+ def nonnegative_integer(value)
845
+ [value.to_i, 0].max
846
+ end
847
+
848
+ def serialize_record(record)
849
+ serialize_value(record.to_h)
850
+ end
851
+
852
+ def serialize_runtime_message(message)
853
+ serialize_value(message).merge(
854
+ id: message[:message_uid] || message[:id].to_s,
855
+ cursor_id: message[:id]
856
+ )
857
+ end
858
+
859
+ def serialize_value(value)
860
+ case value
861
+ when Time
862
+ value.iso8601
863
+ when Hash
864
+ value.to_h { |key, nested| [key, serialize_value(nested)] }
865
+ when Array
866
+ value.map { |nested| serialize_value(nested) }
867
+ else
868
+ value
869
+ end
870
+ end
871
+
368
872
  def po_summary(po)
369
873
  po.to_summary_hash(registry: @runtime.registry)
370
874
  end
@@ -398,7 +902,12 @@ module PromptObjects
398
902
  status,
399
903
  {
400
904
  "content-type" => "application/json",
401
- "access-control-allow-origin" => "*"
905
+ "access-control-allow-origin" => "*",
906
+ # The API is live state — sessions, artifacts, PO status all change
907
+ # constantly. Without this, browsers heuristically cache same-URL
908
+ # GETs, so a refresh after creating a session (or rendering an
909
+ # artifact) re-reads a stale list and the new thing never appears.
910
+ "cache-control" => "no-store, must-revalidate"
402
911
  },
403
912
  [body]
404
913
  ]