@bolt-foundry/gambit 0.8.3 → 0.8.5-rc.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (289) hide show
  1. package/CHANGELOG.md +38 -2
  2. package/README.md +79 -16
  3. package/esm/_dnt.polyfills.d.ts +17 -0
  4. package/esm/_dnt.polyfills.d.ts.map +1 -1
  5. package/esm/_dnt.polyfills.js +122 -0
  6. package/esm/deps/jsr.io/@std/collections/1.1.5/deep_merge.d.ts +322 -0
  7. package/esm/deps/jsr.io/@std/collections/1.1.5/deep_merge.d.ts.map +1 -0
  8. package/esm/deps/jsr.io/@std/collections/1.1.5/deep_merge.js +105 -0
  9. package/esm/deps/jsr.io/@std/fs/1.0.22/_create_walk_entry.d.ts +14 -0
  10. package/esm/deps/jsr.io/@std/fs/1.0.22/_create_walk_entry.d.ts.map +1 -0
  11. package/esm/deps/jsr.io/@std/fs/1.0.22/_create_walk_entry.js +34 -0
  12. package/esm/deps/jsr.io/@std/fs/1.0.22/_get_file_info_type.d.ts +13 -0
  13. package/esm/deps/jsr.io/@std/fs/1.0.22/_get_file_info_type.d.ts.map +1 -0
  14. package/esm/deps/jsr.io/@std/fs/1.0.22/_get_file_info_type.js +18 -0
  15. package/esm/deps/jsr.io/@std/fs/1.0.22/_is_same_path.d.ts +10 -0
  16. package/esm/deps/jsr.io/@std/fs/1.0.22/_is_same_path.d.ts.map +1 -0
  17. package/esm/deps/jsr.io/@std/fs/1.0.22/_is_same_path.js +17 -0
  18. package/esm/deps/jsr.io/@std/fs/1.0.22/_is_subdir.d.ts +12 -0
  19. package/esm/deps/jsr.io/@std/fs/1.0.22/_is_subdir.d.ts.map +1 -0
  20. package/esm/deps/jsr.io/@std/fs/1.0.22/_is_subdir.js +25 -0
  21. package/esm/deps/jsr.io/@std/fs/1.0.22/_to_path_string.d.ts +9 -0
  22. package/esm/deps/jsr.io/@std/fs/1.0.22/_to_path_string.d.ts.map +1 -0
  23. package/esm/deps/jsr.io/@std/fs/1.0.22/_to_path_string.js +13 -0
  24. package/esm/deps/jsr.io/@std/fs/1.0.22/copy.d.ts +117 -0
  25. package/esm/deps/jsr.io/@std/fs/1.0.22/copy.d.ts.map +1 -0
  26. package/esm/deps/jsr.io/@std/fs/1.0.22/copy.js +313 -0
  27. package/esm/deps/jsr.io/@std/fs/1.0.22/empty_dir.d.ts +48 -0
  28. package/esm/deps/jsr.io/@std/fs/1.0.22/empty_dir.d.ts.map +1 -0
  29. package/esm/deps/jsr.io/@std/fs/1.0.22/empty_dir.js +87 -0
  30. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_dir.d.ts +49 -0
  31. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_dir.d.ts.map +1 -0
  32. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_dir.js +102 -0
  33. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_file.d.ts +47 -0
  34. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_file.d.ts.map +1 -0
  35. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_file.js +90 -0
  36. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_link.d.ts +49 -0
  37. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_link.d.ts.map +1 -0
  38. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_link.js +61 -0
  39. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_symlink.d.ts +70 -0
  40. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_symlink.d.ts.map +1 -0
  41. package/esm/deps/jsr.io/@std/fs/1.0.22/ensure_symlink.js +156 -0
  42. package/esm/deps/jsr.io/@std/fs/1.0.22/eol.d.ts +52 -0
  43. package/esm/deps/jsr.io/@std/fs/1.0.22/eol.d.ts.map +1 -0
  44. package/esm/deps/jsr.io/@std/fs/1.0.22/eol.js +67 -0
  45. package/esm/deps/jsr.io/@std/fs/1.0.22/exists.d.ts +218 -0
  46. package/esm/deps/jsr.io/@std/fs/1.0.22/exists.d.ts.map +1 -0
  47. package/esm/deps/jsr.io/@std/fs/1.0.22/exists.js +271 -0
  48. package/esm/deps/jsr.io/@std/fs/1.0.22/expand_glob.d.ts +267 -0
  49. package/esm/deps/jsr.io/@std/fs/1.0.22/expand_glob.d.ts.map +1 -0
  50. package/esm/deps/jsr.io/@std/fs/1.0.22/expand_glob.js +442 -0
  51. package/esm/deps/jsr.io/@std/fs/1.0.22/mod.d.ts +29 -0
  52. package/esm/deps/jsr.io/@std/fs/1.0.22/mod.d.ts.map +1 -0
  53. package/esm/deps/jsr.io/@std/fs/1.0.22/mod.js +29 -0
  54. package/esm/deps/jsr.io/@std/fs/1.0.22/move.d.ts +86 -0
  55. package/esm/deps/jsr.io/@std/fs/1.0.22/move.d.ts.map +1 -0
  56. package/esm/deps/jsr.io/@std/fs/1.0.22/move.js +142 -0
  57. package/esm/deps/jsr.io/@std/fs/1.0.22/walk.d.ts +777 -0
  58. package/esm/deps/jsr.io/@std/fs/1.0.22/walk.d.ts.map +1 -0
  59. package/esm/deps/jsr.io/@std/fs/1.0.22/walk.js +846 -0
  60. package/esm/deps/jsr.io/@std/json/1.0.2/types.d.ts +5 -0
  61. package/esm/deps/jsr.io/@std/json/1.0.2/types.d.ts.map +1 -0
  62. package/esm/deps/jsr.io/@std/json/1.0.2/types.js +3 -0
  63. package/esm/deps/jsr.io/@std/jsonc/1.0.2/mod.d.ts +20 -0
  64. package/esm/deps/jsr.io/@std/jsonc/1.0.2/mod.d.ts.map +1 -0
  65. package/esm/deps/jsr.io/@std/jsonc/1.0.2/mod.js +21 -0
  66. package/esm/deps/jsr.io/@std/jsonc/1.0.2/parse.d.ts +21 -0
  67. package/esm/deps/jsr.io/@std/jsonc/1.0.2/parse.d.ts.map +1 -0
  68. package/esm/deps/jsr.io/@std/jsonc/1.0.2/parse.js +320 -0
  69. package/esm/deps/jsr.io/@std/toml/1.0.11/_parser.d.ts +93 -0
  70. package/esm/deps/jsr.io/@std/toml/1.0.11/_parser.d.ts.map +1 -0
  71. package/esm/deps/jsr.io/@std/toml/1.0.11/_parser.js +753 -0
  72. package/esm/deps/jsr.io/@std/toml/1.0.11/mod.d.ts +109 -0
  73. package/esm/deps/jsr.io/@std/toml/1.0.11/mod.d.ts.map +1 -0
  74. package/esm/deps/jsr.io/@std/toml/1.0.11/mod.js +110 -0
  75. package/esm/deps/jsr.io/@std/toml/1.0.11/parse.d.ts +21 -0
  76. package/esm/deps/jsr.io/@std/toml/1.0.11/parse.d.ts.map +1 -0
  77. package/esm/deps/jsr.io/@std/toml/1.0.11/parse.js +25 -0
  78. package/esm/deps/jsr.io/@std/toml/1.0.11/stringify.d.ts +35 -0
  79. package/esm/deps/jsr.io/@std/toml/1.0.11/stringify.d.ts.map +1 -0
  80. package/esm/deps/jsr.io/@std/toml/1.0.11/stringify.js +283 -0
  81. package/esm/gambit/simulator-ui/dist/bundle.js +10639 -4629
  82. package/esm/gambit/simulator-ui/dist/bundle.js.map +4 -4
  83. package/esm/mod.d.ts +13 -3
  84. package/esm/mod.d.ts.map +1 -1
  85. package/esm/mod.js +8 -2
  86. package/esm/src/cli_utils.d.ts +1 -0
  87. package/esm/src/cli_utils.d.ts.map +1 -1
  88. package/esm/src/cli_utils.js +13 -1
  89. package/esm/src/default_runtime.d.ts +46 -0
  90. package/esm/src/default_runtime.d.ts.map +1 -0
  91. package/esm/src/default_runtime.js +415 -0
  92. package/esm/src/durable_streams.js +26 -1
  93. package/esm/src/model_matchers.d.ts +10 -0
  94. package/esm/src/model_matchers.d.ts.map +1 -0
  95. package/esm/src/model_matchers.js +26 -0
  96. package/esm/src/openai_compat.d.ts +12 -1
  97. package/esm/src/openai_compat.d.ts.map +1 -1
  98. package/esm/src/openai_compat.js +53 -1
  99. package/esm/src/project_config.d.ts +47 -0
  100. package/esm/src/project_config.d.ts.map +1 -0
  101. package/esm/src/project_config.js +134 -0
  102. package/esm/src/providers/codex.d.ts +37 -0
  103. package/esm/src/providers/codex.d.ts.map +1 -0
  104. package/esm/src/providers/codex.js +810 -0
  105. package/esm/src/providers/google.d.ts +3 -1
  106. package/esm/src/providers/google.d.ts.map +1 -1
  107. package/esm/src/providers/google.js +82 -6
  108. package/esm/src/providers/ollama.d.ts +3 -1
  109. package/esm/src/providers/ollama.d.ts.map +1 -1
  110. package/esm/src/providers/ollama.js +238 -15
  111. package/esm/src/providers/openrouter.d.ts +6 -2
  112. package/esm/src/providers/openrouter.d.ts.map +1 -1
  113. package/esm/src/providers/openrouter.js +260 -23
  114. package/esm/src/providers/router.d.ts +19 -0
  115. package/esm/src/providers/router.d.ts.map +1 -0
  116. package/esm/src/providers/router.js +93 -0
  117. package/esm/src/server.d.ts +9 -0
  118. package/esm/src/server.d.ts.map +1 -1
  119. package/esm/src/server.js +3186 -652
  120. package/esm/src/server_feedback_grading_routes.d.ts +32 -0
  121. package/esm/src/server_feedback_grading_routes.d.ts.map +1 -0
  122. package/esm/src/server_feedback_grading_routes.js +305 -0
  123. package/esm/src/server_helpers.d.ts +4 -0
  124. package/esm/src/server_helpers.d.ts.map +1 -0
  125. package/esm/src/server_helpers.js +46 -0
  126. package/esm/src/server_session_store.d.ts +87 -0
  127. package/esm/src/server_session_store.d.ts.map +1 -0
  128. package/esm/src/server_session_store.js +873 -0
  129. package/esm/src/server_types.d.ts +110 -0
  130. package/esm/src/server_types.d.ts.map +1 -0
  131. package/esm/src/server_types.js +1 -0
  132. package/esm/src/server_ui_routes.d.ts +33 -0
  133. package/esm/src/server_ui_routes.d.ts.map +1 -0
  134. package/esm/src/server_ui_routes.js +135 -0
  135. package/esm/src/session_artifacts.d.ts +22 -0
  136. package/esm/src/session_artifacts.d.ts.map +1 -0
  137. package/esm/src/session_artifacts.js +243 -0
  138. package/esm/src/trace.d.ts.map +1 -1
  139. package/esm/src/trace.js +6 -3
  140. package/esm/src/workspace.d.ts +19 -0
  141. package/esm/src/workspace.d.ts.map +1 -0
  142. package/esm/src/workspace.js +164 -0
  143. package/esm/src/workspace_contract.d.ts +76 -0
  144. package/esm/src/workspace_contract.d.ts.map +1 -0
  145. package/esm/src/workspace_contract.js +74 -0
  146. package/package.json +2 -2
  147. package/script/_dnt.polyfills.d.ts +17 -0
  148. package/script/_dnt.polyfills.d.ts.map +1 -1
  149. package/script/_dnt.polyfills.js +122 -0
  150. package/script/deps/jsr.io/@std/collections/1.1.5/deep_merge.d.ts +322 -0
  151. package/script/deps/jsr.io/@std/collections/1.1.5/deep_merge.d.ts.map +1 -0
  152. package/script/deps/jsr.io/@std/collections/1.1.5/deep_merge.js +108 -0
  153. package/script/deps/jsr.io/@std/fs/1.0.22/_create_walk_entry.d.ts +14 -0
  154. package/script/deps/jsr.io/@std/fs/1.0.22/_create_walk_entry.d.ts.map +1 -0
  155. package/script/deps/jsr.io/@std/fs/1.0.22/_create_walk_entry.js +71 -0
  156. package/script/deps/jsr.io/@std/fs/1.0.22/_get_file_info_type.d.ts +13 -0
  157. package/script/deps/jsr.io/@std/fs/1.0.22/_get_file_info_type.d.ts.map +1 -0
  158. package/script/deps/jsr.io/@std/fs/1.0.22/_get_file_info_type.js +21 -0
  159. package/script/deps/jsr.io/@std/fs/1.0.22/_is_same_path.d.ts +10 -0
  160. package/script/deps/jsr.io/@std/fs/1.0.22/_is_same_path.d.ts.map +1 -0
  161. package/script/deps/jsr.io/@std/fs/1.0.22/_is_same_path.js +20 -0
  162. package/script/deps/jsr.io/@std/fs/1.0.22/_is_subdir.d.ts +12 -0
  163. package/script/deps/jsr.io/@std/fs/1.0.22/_is_subdir.d.ts.map +1 -0
  164. package/script/deps/jsr.io/@std/fs/1.0.22/_is_subdir.js +28 -0
  165. package/script/deps/jsr.io/@std/fs/1.0.22/_to_path_string.d.ts +9 -0
  166. package/script/deps/jsr.io/@std/fs/1.0.22/_to_path_string.d.ts.map +1 -0
  167. package/script/deps/jsr.io/@std/fs/1.0.22/_to_path_string.js +16 -0
  168. package/script/deps/jsr.io/@std/fs/1.0.22/copy.d.ts +117 -0
  169. package/script/deps/jsr.io/@std/fs/1.0.22/copy.d.ts.map +1 -0
  170. package/script/deps/jsr.io/@std/fs/1.0.22/copy.js +350 -0
  171. package/script/deps/jsr.io/@std/fs/1.0.22/empty_dir.d.ts +48 -0
  172. package/script/deps/jsr.io/@std/fs/1.0.22/empty_dir.d.ts.map +1 -0
  173. package/script/deps/jsr.io/@std/fs/1.0.22/empty_dir.js +124 -0
  174. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_dir.d.ts +49 -0
  175. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_dir.d.ts.map +1 -0
  176. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_dir.js +139 -0
  177. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_file.d.ts +47 -0
  178. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_file.d.ts.map +1 -0
  179. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_file.js +127 -0
  180. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_link.d.ts +49 -0
  181. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_link.d.ts.map +1 -0
  182. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_link.js +98 -0
  183. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_symlink.d.ts +70 -0
  184. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_symlink.d.ts.map +1 -0
  185. package/script/deps/jsr.io/@std/fs/1.0.22/ensure_symlink.js +193 -0
  186. package/script/deps/jsr.io/@std/fs/1.0.22/eol.d.ts +52 -0
  187. package/script/deps/jsr.io/@std/fs/1.0.22/eol.d.ts.map +1 -0
  188. package/script/deps/jsr.io/@std/fs/1.0.22/eol.js +105 -0
  189. package/script/deps/jsr.io/@std/fs/1.0.22/exists.d.ts +218 -0
  190. package/script/deps/jsr.io/@std/fs/1.0.22/exists.d.ts.map +1 -0
  191. package/script/deps/jsr.io/@std/fs/1.0.22/exists.js +308 -0
  192. package/script/deps/jsr.io/@std/fs/1.0.22/expand_glob.d.ts +267 -0
  193. package/script/deps/jsr.io/@std/fs/1.0.22/expand_glob.d.ts.map +1 -0
  194. package/script/deps/jsr.io/@std/fs/1.0.22/expand_glob.js +479 -0
  195. package/script/deps/jsr.io/@std/fs/1.0.22/mod.d.ts +29 -0
  196. package/script/deps/jsr.io/@std/fs/1.0.22/mod.d.ts.map +1 -0
  197. package/script/deps/jsr.io/@std/fs/1.0.22/mod.js +45 -0
  198. package/script/deps/jsr.io/@std/fs/1.0.22/move.d.ts +86 -0
  199. package/script/deps/jsr.io/@std/fs/1.0.22/move.d.ts.map +1 -0
  200. package/script/deps/jsr.io/@std/fs/1.0.22/move.js +179 -0
  201. package/script/deps/jsr.io/@std/fs/1.0.22/walk.d.ts +777 -0
  202. package/script/deps/jsr.io/@std/fs/1.0.22/walk.d.ts.map +1 -0
  203. package/script/deps/jsr.io/@std/fs/1.0.22/walk.js +883 -0
  204. package/script/deps/jsr.io/@std/json/1.0.2/types.d.ts +5 -0
  205. package/script/deps/jsr.io/@std/json/1.0.2/types.d.ts.map +1 -0
  206. package/script/deps/jsr.io/@std/json/1.0.2/types.js +4 -0
  207. package/script/deps/jsr.io/@std/jsonc/1.0.2/mod.d.ts +20 -0
  208. package/script/deps/jsr.io/@std/jsonc/1.0.2/mod.d.ts.map +1 -0
  209. package/script/deps/jsr.io/@std/jsonc/1.0.2/mod.js +37 -0
  210. package/script/deps/jsr.io/@std/jsonc/1.0.2/parse.d.ts +21 -0
  211. package/script/deps/jsr.io/@std/jsonc/1.0.2/parse.d.ts.map +1 -0
  212. package/script/deps/jsr.io/@std/jsonc/1.0.2/parse.js +323 -0
  213. package/script/deps/jsr.io/@std/toml/1.0.11/_parser.d.ts +93 -0
  214. package/script/deps/jsr.io/@std/toml/1.0.11/_parser.d.ts.map +1 -0
  215. package/script/deps/jsr.io/@std/toml/1.0.11/_parser.js +781 -0
  216. package/script/deps/jsr.io/@std/toml/1.0.11/mod.d.ts +109 -0
  217. package/script/deps/jsr.io/@std/toml/1.0.11/mod.d.ts.map +1 -0
  218. package/script/deps/jsr.io/@std/toml/1.0.11/mod.js +126 -0
  219. package/script/deps/jsr.io/@std/toml/1.0.11/parse.d.ts +21 -0
  220. package/script/deps/jsr.io/@std/toml/1.0.11/parse.d.ts.map +1 -0
  221. package/script/deps/jsr.io/@std/toml/1.0.11/parse.js +28 -0
  222. package/script/deps/jsr.io/@std/toml/1.0.11/stringify.d.ts +35 -0
  223. package/script/deps/jsr.io/@std/toml/1.0.11/stringify.d.ts.map +1 -0
  224. package/script/deps/jsr.io/@std/toml/1.0.11/stringify.js +286 -0
  225. package/script/gambit/simulator-ui/dist/bundle.js +10639 -4629
  226. package/script/gambit/simulator-ui/dist/bundle.js.map +4 -4
  227. package/script/mod.d.ts +13 -3
  228. package/script/mod.d.ts.map +1 -1
  229. package/script/mod.js +14 -5
  230. package/script/src/cli_utils.d.ts +1 -0
  231. package/script/src/cli_utils.d.ts.map +1 -1
  232. package/script/src/cli_utils.js +14 -1
  233. package/script/src/default_runtime.d.ts +46 -0
  234. package/script/src/default_runtime.d.ts.map +1 -0
  235. package/script/src/default_runtime.js +452 -0
  236. package/script/src/durable_streams.js +26 -1
  237. package/script/src/model_matchers.d.ts +10 -0
  238. package/script/src/model_matchers.d.ts.map +1 -0
  239. package/script/src/model_matchers.js +29 -0
  240. package/script/src/openai_compat.d.ts +12 -1
  241. package/script/src/openai_compat.d.ts.map +1 -1
  242. package/script/src/openai_compat.js +85 -0
  243. package/script/src/project_config.d.ts +47 -0
  244. package/script/src/project_config.d.ts.map +1 -0
  245. package/script/src/project_config.js +173 -0
  246. package/script/src/providers/codex.d.ts +37 -0
  247. package/script/src/providers/codex.d.ts.map +1 -0
  248. package/script/src/providers/codex.js +850 -0
  249. package/script/src/providers/google.d.ts +3 -1
  250. package/script/src/providers/google.d.ts.map +1 -1
  251. package/script/src/providers/google.js +82 -6
  252. package/script/src/providers/ollama.d.ts +3 -1
  253. package/script/src/providers/ollama.d.ts.map +1 -1
  254. package/script/src/providers/ollama.js +238 -15
  255. package/script/src/providers/openrouter.d.ts +6 -2
  256. package/script/src/providers/openrouter.d.ts.map +1 -1
  257. package/script/src/providers/openrouter.js +260 -23
  258. package/script/src/providers/router.d.ts +19 -0
  259. package/script/src/providers/router.d.ts.map +1 -0
  260. package/script/src/providers/router.js +96 -0
  261. package/script/src/server.d.ts +9 -0
  262. package/script/src/server.d.ts.map +1 -1
  263. package/script/src/server.js +3193 -659
  264. package/script/src/server_feedback_grading_routes.d.ts +32 -0
  265. package/script/src/server_feedback_grading_routes.d.ts.map +1 -0
  266. package/script/src/server_feedback_grading_routes.js +343 -0
  267. package/script/src/server_helpers.d.ts +4 -0
  268. package/script/src/server_helpers.d.ts.map +1 -0
  269. package/script/src/server_helpers.js +84 -0
  270. package/script/src/server_session_store.d.ts +87 -0
  271. package/script/src/server_session_store.d.ts.map +1 -0
  272. package/script/src/server_session_store.js +910 -0
  273. package/script/src/server_types.d.ts +110 -0
  274. package/script/src/server_types.d.ts.map +1 -0
  275. package/script/src/server_types.js +2 -0
  276. package/script/src/server_ui_routes.d.ts +33 -0
  277. package/script/src/server_ui_routes.d.ts.map +1 -0
  278. package/script/src/server_ui_routes.js +172 -0
  279. package/script/src/session_artifacts.d.ts +22 -0
  280. package/script/src/session_artifacts.d.ts.map +1 -0
  281. package/script/src/session_artifacts.js +279 -0
  282. package/script/src/trace.d.ts.map +1 -1
  283. package/script/src/trace.js +6 -3
  284. package/script/src/workspace.d.ts +19 -0
  285. package/script/src/workspace.d.ts.map +1 -0
  286. package/script/src/workspace.js +201 -0
  287. package/script/src/workspace_contract.d.ts +76 -0
  288. package/script/src/workspace_contract.d.ts.map +1 -0
  289. package/script/src/workspace_contract.js +82 -0
package/esm/src/server.js CHANGED
@@ -1,10 +1,19 @@
1
1
  import * as dntShim from "../_dnt.shims.js";
2
2
  import * as path from "../deps/jsr.io/@std/path/1.1.4/mod.js";
3
- import { isGambitEndSignal, runDeck } from "@bolt-foundry/gambit-core";
3
+ import { copy, ensureDir, existsSync } from "../deps/jsr.io/@std/fs/1.0.22/mod.js";
4
+ import { parse } from "../deps/jsr.io/@std/jsonc/1.0.2/mod.js";
5
+ import { parse as parseToml } from "../deps/jsr.io/@std/toml/1.0.11/mod.js";
6
+ import { isGambitEndSignal, isRunCanceledError, runDeck, } from "@bolt-foundry/gambit-core";
4
7
  import { sanitizeNumber } from "./test_bot.js";
5
8
  import { makeConsoleTracer } from "./trace.js";
6
9
  import { defaultSessionRoot } from "./cli_utils.js";
7
10
  import { loadDeck } from "@bolt-foundry/gambit-core";
11
+ import { createWorkspaceScaffold } from "./workspace.js";
12
+ import { assertSafeBuildBotRoot, randomId, resolveDefaultValue, } from "./server_helpers.js";
13
+ import { createSessionStore } from "./server_session_store.js";
14
+ import { handleFeedbackRoutes, handleGradingReferenceRoute, } from "./server_feedback_grading_routes.js";
15
+ import { handleUiRoutes } from "./server_ui_routes.js";
16
+ import { resolveWorkspaceIdFromRecord, resolveWorkspaceIdFromSearchParams, WORKSPACE_ROUTE_BASE, WORKSPACE_STATE_SCHEMA_VERSION, workspaceSchemaError, } from "./workspace_contract.js";
8
17
  import { appendDurableStreamEvent, handleDurableStreamRequest, } from "./durable_streams.js";
9
18
  const GAMBIT_TOOL_RESPOND = "gambit_respond";
10
19
  const logger = console;
@@ -51,34 +60,89 @@ const simulatorBundlePath = path.resolve(moduleDir, "..", "simulator-ui", "dist"
51
60
  const simulatorBundleSourceMapPath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "bundle.js.map");
52
61
  const simulatorFaviconDistPath = path.resolve(moduleDir, "..", "simulator-ui", "dist", "favicon.ico");
53
62
  const simulatorFaviconSrcPath = path.resolve(moduleDir, "..", "simulator-ui", "src", "favicon.ico");
63
+ const gambitVersion = (() => {
64
+ const envVersion = dntShim.Deno.env.get("GAMBIT_VERSION")?.trim();
65
+ if (envVersion)
66
+ return envVersion;
67
+ const readVersion = (configPath) => {
68
+ try {
69
+ const text = dntShim.Deno.readTextFileSync(configPath);
70
+ const data = parse(text);
71
+ const version = typeof data.version === "string"
72
+ ? data.version.trim()
73
+ : "";
74
+ return version || null;
75
+ }
76
+ catch (err) {
77
+ if (err instanceof dntShim.Deno.errors.NotFound)
78
+ return null;
79
+ throw err;
80
+ }
81
+ };
82
+ const candidates = [
83
+ path.resolve(moduleDir, "..", "deno.jsonc"),
84
+ path.resolve(moduleDir, "..", "deno.json"),
85
+ ];
86
+ for (const candidate of candidates) {
87
+ const version = readVersion(candidate);
88
+ if (version)
89
+ return version;
90
+ }
91
+ return "unknown";
92
+ })();
54
93
  const SIMULATOR_STREAM_ID = "gambit-simulator";
94
+ const WORKSPACE_STREAM_ID = "gambit-workspace";
55
95
  const GRADE_STREAM_ID = "gambit-grade";
56
96
  const TEST_STREAM_ID = "gambit-test";
97
+ const BUILD_STREAM_ID = "gambit-build";
98
+ const DEFAULT_TEST_BOT_SEED_PROMPT = "Start the conversation as the user. Do not wait for the assistant to speak first.";
99
+ const isWorkspaceEventDomain = (value) => value === "build" || value === "test" || value === "grade" ||
100
+ value === "session";
101
+ const extractPersistedWorkspacePayload = (record) => {
102
+ if (!isWorkspaceEventDomain(record.type))
103
+ return record;
104
+ const nested = record.data;
105
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) {
106
+ return record;
107
+ }
108
+ return nested;
109
+ };
110
+ const GAMBIT_BOT_SOURCE_DECK_URL = new URL("./decks/gambit-bot/PROMPT.md", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url);
111
+ const GAMBIT_BOT_SOURCE_DIR = GAMBIT_BOT_SOURCE_DECK_URL.protocol === "file:"
112
+ ? path.dirname(path.fromFileUrl(GAMBIT_BOT_SOURCE_DECK_URL))
113
+ : "";
114
+ const GAMBIT_BOT_POLICY_DIR = GAMBIT_BOT_SOURCE_DIR
115
+ ? path.join(GAMBIT_BOT_SOURCE_DIR, "policy")
116
+ : "";
117
+ async function ensureGambitPolicyInBotRoot(root) {
118
+ if (!GAMBIT_BOT_POLICY_DIR)
119
+ return;
120
+ try {
121
+ const info = await dntShim.Deno.stat(GAMBIT_BOT_POLICY_DIR);
122
+ if (!info.isDirectory)
123
+ return;
124
+ }
125
+ catch {
126
+ return;
127
+ }
128
+ const dest = path.join(root, ".gambit", "policy");
129
+ if (existsSync(dest))
130
+ return;
131
+ await ensureDir(path.dirname(dest));
132
+ await copy(GAMBIT_BOT_POLICY_DIR, dest, { overwrite: false });
133
+ }
57
134
  let availableTestDecks = [];
58
135
  const testDeckByPath = new Map();
59
136
  const testDeckById = new Map();
60
137
  let availableGraderDecks = [];
61
138
  const graderDeckByPath = new Map();
62
139
  const graderDeckById = new Map();
63
- function randomId(prefix) {
64
- const suffix = crypto.randomUUID().replace(/-/g, "").slice(0, 24);
65
- return `${prefix}-${suffix}`;
66
- }
67
- function resolveDefaultValue(raw) {
68
- if (typeof raw === "function") {
69
- try {
70
- return raw();
71
- }
72
- catch {
73
- return undefined;
74
- }
75
- }
76
- return raw;
77
- }
78
140
  async function describeDeckInputSchemaFromPath(deckPath) {
79
141
  try {
80
142
  const deck = await loadDeck(deckPath);
81
- return describeZodSchema(deck.inputSchema);
143
+ const tools = mapDeckTools(deck.actionDecks);
144
+ const desc = describeZodSchema(deck.inputSchema);
145
+ return tools ? { ...desc, tools } : desc;
82
146
  }
83
147
  catch (err) {
84
148
  const message = err instanceof Error ? err.message : String(err);
@@ -86,6 +150,22 @@ async function describeDeckInputSchemaFromPath(deckPath) {
86
150
  return { error: message };
87
151
  }
88
152
  }
153
+ function mapDeckTools(actionDecks) {
154
+ if (!Array.isArray(actionDecks) || actionDecks.length === 0) {
155
+ return undefined;
156
+ }
157
+ const described = actionDecks
158
+ .filter((action) => Boolean(action?.name && typeof action.name === "string"))
159
+ .map((action) => ({
160
+ name: action.name,
161
+ label: typeof action.label === "string" ? action.label : undefined,
162
+ description: typeof action.description === "string"
163
+ ? action.description
164
+ : undefined,
165
+ path: action.path,
166
+ }));
167
+ return described.length > 0 ? described : undefined;
168
+ }
89
169
  function describeZodSchema(schema) {
90
170
  try {
91
171
  const normalized = normalizeSchema(schema);
@@ -409,7 +489,7 @@ function buildInitFillPrompt(args) {
409
489
  schemaHints,
410
490
  };
411
491
  return [
412
- "You are filling missing required init fields for a Gambit Test Bot run.",
492
+ "You are filling missing required init fields for a Gambit Scenario run.",
413
493
  "Return ONLY valid JSON that includes values for the missing fields.",
414
494
  "Do not include any fields that are not listed as missing.",
415
495
  "If the only missing path is '(root)', return the full init JSON value.",
@@ -465,6 +545,389 @@ function validateInitInput(schema, value) {
465
545
  }
466
546
  return result.data;
467
547
  }
548
+ function jsonResponse(body, status = 200) {
549
+ return new Response(JSON.stringify(body), {
550
+ status,
551
+ headers: { "content-type": "application/json" },
552
+ });
553
+ }
554
+ function parseBodyObject(value) {
555
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
556
+ throw new Error("Request body must be a JSON object");
557
+ }
558
+ return value;
559
+ }
560
+ function toTextPart(role, value) {
561
+ if (typeof value === "string") {
562
+ return {
563
+ type: role === "assistant" ? "output_text" : "input_text",
564
+ text: value,
565
+ };
566
+ }
567
+ if (!value || typeof value !== "object" || Array.isArray(value))
568
+ return null;
569
+ const record = value;
570
+ const text = typeof record.text === "string" ? record.text : "";
571
+ if (!text)
572
+ return null;
573
+ const type = typeof record.type === "string" ? record.type : "";
574
+ if (type === "output_text")
575
+ return { type: "output_text", text };
576
+ if (type === "input_text")
577
+ return { type: "input_text", text };
578
+ return {
579
+ type: role === "assistant" ? "output_text" : "input_text",
580
+ text,
581
+ };
582
+ }
583
+ function normalizeMessageItem(item) {
584
+ const role = item.role;
585
+ if (role !== "system" && role !== "user" && role !== "assistant") {
586
+ throw new Error("message.role must be system, user, or assistant");
587
+ }
588
+ const rawContent = item.content;
589
+ const content = Array.isArray(rawContent)
590
+ ? rawContent.map((part) => toTextPart(role, part)).filter((part) => Boolean(part))
591
+ : [toTextPart(role, rawContent)].filter((part) => Boolean(part));
592
+ if (content.length === 0) {
593
+ throw new Error("message.content must include text");
594
+ }
595
+ return {
596
+ type: "message",
597
+ role,
598
+ content,
599
+ id: typeof item.id === "string" ? item.id : undefined,
600
+ };
601
+ }
602
+ function normalizeInputItems(input) {
603
+ if (typeof input === "string") {
604
+ return [{
605
+ type: "message",
606
+ role: "user",
607
+ content: [{ type: "input_text", text: input }],
608
+ }];
609
+ }
610
+ const arr = Array.isArray(input) ? input : [input];
611
+ const items = [];
612
+ for (const raw of arr) {
613
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
614
+ throw new Error("input items must be objects");
615
+ }
616
+ const item = raw;
617
+ const type = typeof item.type === "string" ? item.type : "";
618
+ if (type === "message") {
619
+ const normalized = normalizeMessageItem(item);
620
+ if (normalized)
621
+ items.push(normalized);
622
+ continue;
623
+ }
624
+ if (type === "function_call") {
625
+ const callId = item.call_id;
626
+ const name = item.name;
627
+ const args = item.arguments;
628
+ if (typeof callId !== "string" || typeof name !== "string" ||
629
+ typeof args !== "string") {
630
+ throw new Error("function_call requires call_id, name, and arguments strings");
631
+ }
632
+ items.push({
633
+ type: "function_call",
634
+ call_id: callId,
635
+ name,
636
+ arguments: args,
637
+ id: typeof item.id === "string" ? item.id : undefined,
638
+ });
639
+ continue;
640
+ }
641
+ if (type === "function_call_output") {
642
+ const callId = item.call_id;
643
+ const output = item.output;
644
+ if (typeof callId !== "string" || typeof output !== "string") {
645
+ throw new Error("function_call_output requires call_id and output strings");
646
+ }
647
+ items.push({
648
+ type: "function_call_output",
649
+ call_id: callId,
650
+ output,
651
+ id: typeof item.id === "string" ? item.id : undefined,
652
+ });
653
+ continue;
654
+ }
655
+ throw new Error(`Unsupported input item type: ${type || "(missing type)"}`);
656
+ }
657
+ return items;
658
+ }
659
+ function normalizeTools(tools) {
660
+ if (!Array.isArray(tools) || tools.length === 0)
661
+ return undefined;
662
+ const out = [];
663
+ for (const raw of tools) {
664
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
665
+ throw new Error("tools entries must be objects");
666
+ }
667
+ const item = raw;
668
+ const type = typeof item.type === "string" ? item.type : "";
669
+ if (type !== "function")
670
+ continue;
671
+ const nested = item.function;
672
+ if (nested && typeof nested === "object" && !Array.isArray(nested)) {
673
+ const fn = nested;
674
+ const name = fn.name;
675
+ if (typeof name !== "string" || !name) {
676
+ throw new Error("tool.function.name is required");
677
+ }
678
+ out.push({
679
+ type: "function",
680
+ function: {
681
+ name,
682
+ description: typeof fn.description === "string"
683
+ ? fn.description
684
+ : undefined,
685
+ parameters: (fn.parameters &&
686
+ typeof fn.parameters === "object" &&
687
+ !Array.isArray(fn.parameters))
688
+ ? fn.parameters
689
+ : {},
690
+ },
691
+ });
692
+ continue;
693
+ }
694
+ const name = item.name;
695
+ if (typeof name !== "string" || !name) {
696
+ throw new Error("tool.name is required");
697
+ }
698
+ out.push({
699
+ type: "function",
700
+ function: {
701
+ name,
702
+ description: typeof item.description === "string"
703
+ ? item.description
704
+ : undefined,
705
+ parameters: (item.parameters &&
706
+ typeof item.parameters === "object" &&
707
+ !Array.isArray(item.parameters))
708
+ ? item.parameters
709
+ : {},
710
+ },
711
+ });
712
+ }
713
+ return out.length ? out : undefined;
714
+ }
715
+ function normalizeToolChoice(choice) {
716
+ if (!choice)
717
+ return undefined;
718
+ if (choice === "none" || choice === "auto" || choice === "required") {
719
+ return choice;
720
+ }
721
+ if (!choice || typeof choice !== "object" || Array.isArray(choice)) {
722
+ return undefined;
723
+ }
724
+ const record = choice;
725
+ if (record.type === "allowed_tools" && Array.isArray(record.tools)) {
726
+ const tools = record.tools
727
+ .map((entry) => {
728
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
729
+ return null;
730
+ }
731
+ const tool = entry;
732
+ if (tool.type !== "function" || typeof tool.name !== "string") {
733
+ return null;
734
+ }
735
+ return { type: "function", name: tool.name };
736
+ })
737
+ .filter((entry) => Boolean(entry));
738
+ if (tools.length === 0)
739
+ return undefined;
740
+ const mode = record.mode === "none" || record.mode === "auto" ||
741
+ record.mode === "required"
742
+ ? record.mode
743
+ : undefined;
744
+ return { type: "allowed_tools", tools, mode };
745
+ }
746
+ if (record.type !== "function")
747
+ return undefined;
748
+ if (record.function && typeof record.function === "object") {
749
+ const fn = record.function;
750
+ if (typeof fn.name === "string" && fn.name.length > 0) {
751
+ return { type: "function", function: { name: fn.name } };
752
+ }
753
+ }
754
+ if (typeof record.name === "string" && record.name.length > 0) {
755
+ return { type: "function", function: { name: record.name } };
756
+ }
757
+ return undefined;
758
+ }
759
+ function sseFrame(event) {
760
+ const encoder = new TextEncoder();
761
+ const type = event && typeof event === "object" && !Array.isArray(event) &&
762
+ typeof event.type === "string"
763
+ ? event.type
764
+ : null;
765
+ if (type) {
766
+ return encoder.encode(`event: ${type}\ndata: ${JSON.stringify(event)}\n\n`);
767
+ }
768
+ return encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
769
+ }
770
+ function asJsonValue(value) {
771
+ if (value === null || typeof value === "string" || typeof value === "number" ||
772
+ typeof value === "boolean") {
773
+ return value;
774
+ }
775
+ if (Array.isArray(value)) {
776
+ return value.map((entry) => asJsonValue(entry));
777
+ }
778
+ if (value && typeof value === "object") {
779
+ const out = {};
780
+ for (const [key, entry] of Object.entries(value)) {
781
+ out[key] = asJsonValue(entry);
782
+ }
783
+ return out;
784
+ }
785
+ return String(value);
786
+ }
787
+ function toStrictContentPart(part) {
788
+ if (part.type === "output_text") {
789
+ return {
790
+ type: "output_text",
791
+ text: part.text,
792
+ annotations: [],
793
+ logprobs: [],
794
+ };
795
+ }
796
+ return {
797
+ type: part.type,
798
+ text: part.text,
799
+ };
800
+ }
801
+ function toStrictResponseItem(item, index) {
802
+ if (item.type === "message") {
803
+ return {
804
+ type: "message",
805
+ id: item.id ?? `msg_${index + 1}`,
806
+ status: "completed",
807
+ role: item.role,
808
+ content: item.content.map((part) => toStrictContentPart(part)),
809
+ };
810
+ }
811
+ if (item.type === "function_call") {
812
+ return {
813
+ type: "function_call",
814
+ id: item.id ?? item.call_id,
815
+ call_id: item.call_id,
816
+ name: item.name,
817
+ arguments: item.arguments,
818
+ status: "completed",
819
+ };
820
+ }
821
+ if (item.type === "function_call_output") {
822
+ return {
823
+ type: "function_call_output",
824
+ id: item.id ?? `${item.call_id}_out`,
825
+ call_id: item.call_id,
826
+ output: item.output,
827
+ status: "completed",
828
+ };
829
+ }
830
+ return {
831
+ type: "reasoning",
832
+ id: item.id ?? `rs_${index + 1}`,
833
+ content: (item.content ?? []).map((part) => toStrictContentPart(part)),
834
+ summary: item.summary.map((part) => toStrictContentPart(part)),
835
+ encrypted_content: item.encrypted_content ?? null,
836
+ };
837
+ }
838
+ function toStrictTools(tools) {
839
+ if (!tools || tools.length === 0)
840
+ return [];
841
+ return tools.map((tool) => ({
842
+ type: "function",
843
+ name: tool.function.name,
844
+ description: tool.function.description ?? null,
845
+ parameters: tool.function.parameters ?? null,
846
+ strict: false,
847
+ }));
848
+ }
849
+ function toStrictToolChoice(choice) {
850
+ if (!choice)
851
+ return "auto";
852
+ if (choice === "none" || choice === "auto" || choice === "required") {
853
+ return choice;
854
+ }
855
+ if (choice.type === "allowed_tools") {
856
+ return {
857
+ type: "allowed_tools",
858
+ tools: choice.tools,
859
+ mode: choice.mode ?? "auto",
860
+ };
861
+ }
862
+ return { type: "function", name: choice.function.name };
863
+ }
864
+ function toStrictResponseResource(args) {
865
+ const now = Math.floor(Date.now() / 1000);
866
+ const createdAt = args.response.created_at ?? args.response.created ?? now;
867
+ const status = args.statusOverride ?? args.response.status ?? "completed";
868
+ const usage = args.response.usage
869
+ ? {
870
+ input_tokens: args.response.usage.promptTokens ?? 0,
871
+ output_tokens: args.response.usage.completionTokens ?? 0,
872
+ total_tokens: args.response.usage.totalTokens ?? 0,
873
+ input_tokens_details: {
874
+ cached_tokens: 0,
875
+ },
876
+ output_tokens_details: {
877
+ reasoning_tokens: args.response.usage.reasoningTokens ?? 0,
878
+ },
879
+ }
880
+ : null;
881
+ return {
882
+ id: args.response.id,
883
+ object: "response",
884
+ created_at: createdAt,
885
+ completed_at: status === "completed" ? now : null,
886
+ status,
887
+ incomplete_details: null,
888
+ model: args.response.model ?? args.request.model,
889
+ previous_response_id: args.request.previous_response_id ?? null,
890
+ instructions: args.request.instructions ?? null,
891
+ output: (args.response.output ?? []).map((item, idx) => toStrictResponseItem(item, idx)),
892
+ error: args.response.error ?? null,
893
+ tools: toStrictTools(args.request.tools),
894
+ tool_choice: toStrictToolChoice(args.request.tool_choice),
895
+ truncation: args.response.truncation ?? args.request.truncation ??
896
+ "disabled",
897
+ parallel_tool_calls: args.response.parallel_tool_calls ??
898
+ args.request.parallel_tool_calls ?? false,
899
+ text: args.response.text
900
+ ? asJsonValue(args.response.text)
901
+ : args.request.text
902
+ ? asJsonValue(args.request.text)
903
+ : { format: { type: "text" } },
904
+ top_p: args.response.top_p ?? args.request.top_p ?? 1,
905
+ presence_penalty: args.response.presence_penalty ??
906
+ args.request.presence_penalty ?? 0,
907
+ frequency_penalty: args.response.frequency_penalty ??
908
+ args.request.frequency_penalty ?? 0,
909
+ top_logprobs: args.response.top_logprobs ?? args.request.top_logprobs ?? 0,
910
+ temperature: args.response.temperature ?? args.request.temperature ?? 1,
911
+ reasoning: args.request.reasoning
912
+ ? {
913
+ effort: args.request.reasoning.effort ?? null,
914
+ summary: args.request.reasoning.summary ?? null,
915
+ }
916
+ : null,
917
+ usage,
918
+ max_output_tokens: args.request.max_output_tokens ?? null,
919
+ max_tool_calls: args.request.max_tool_calls ?? null,
920
+ store: args.response.store ?? args.request.store ?? false,
921
+ background: args.response.background ?? args.request.background ?? false,
922
+ service_tier: args.response.service_tier ?? args.request.service_tier ??
923
+ "default",
924
+ metadata: args.request.metadata ? asJsonValue(args.request.metadata) : {},
925
+ safety_identifier: args.response.safety_identifier ??
926
+ args.request.safety_identifier ?? null,
927
+ prompt_cache_key: args.response.prompt_cache_key ??
928
+ args.request.prompt_cache_key ?? null,
929
+ };
930
+ }
468
931
  /**
469
932
  * Start the WebSocket simulator server used by the Gambit debug UI.
470
933
  */
@@ -475,6 +938,13 @@ export function startWebSocketSimulator(opts) {
475
938
  (initialContext !== undefined);
476
939
  const consoleTracer = opts.verbose ? makeConsoleTracer() : undefined;
477
940
  let resolvedDeckPath = resolveDeckPath(opts.deckPath);
941
+ const buildBotRootCache = new Map();
942
+ const activeWorkspaceId = opts.workspace?.id ?? null;
943
+ const activeWorkspaceOnboarding = Boolean(opts.workspace?.onboarding);
944
+ const workspaceScaffoldEnabled = Boolean(opts.workspace?.scaffoldEnabled);
945
+ const workspaceScaffoldRoot = opts.workspace?.scaffoldRoot
946
+ ? path.resolve(opts.workspace.scaffoldRoot)
947
+ : null;
478
948
  const sessionsRoot = (() => {
479
949
  const base = opts.sessionDir
480
950
  ? path.resolve(opts.sessionDir)
@@ -483,10 +953,23 @@ export function startWebSocketSimulator(opts) {
483
953
  dntShim.Deno.mkdirSync(base, { recursive: true });
484
954
  }
485
955
  catch (err) {
486
- logger.warn(`[sim] unable to ensure sessions directory ${base}: ${err instanceof Error ? err.message : err}`);
956
+ logger.warn(`[sim] unable to ensure workspace state directory ${base}: ${err instanceof Error ? err.message : err}`);
487
957
  }
488
958
  return base;
489
959
  })();
960
+ const workspaceRoot = (() => {
961
+ const dir = workspaceScaffoldRoot ?? sessionsRoot;
962
+ if (workspaceScaffoldEnabled) {
963
+ try {
964
+ dntShim.Deno.mkdirSync(dir, { recursive: true });
965
+ }
966
+ catch (err) {
967
+ logger.warn(`[sim] unable to ensure workspace directory ${dir}: ${err instanceof Error ? err.message : err}`);
968
+ }
969
+ }
970
+ return dir;
971
+ })();
972
+ const workspaceById = new Map();
490
973
  const ensureDir = (dir) => {
491
974
  try {
492
975
  dntShim.Deno.mkdirSync(dir, { recursive: true });
@@ -502,19 +985,270 @@ export function startWebSocketSimulator(opts) {
502
985
  return slug || "session";
503
986
  };
504
987
  const testBotRuns = new Map();
505
- const broadcastTestBot = (payload) => {
988
+ const broadcastTestBot = (payload, workspaceId) => {
989
+ if (workspaceId) {
990
+ const state = readSessionState(workspaceId);
991
+ if (state) {
992
+ appendWorkspaceEnvelope(state, "test", payload);
993
+ }
994
+ }
995
+ appendDurableStreamEvent(WORKSPACE_STREAM_ID, payload);
506
996
  appendDurableStreamEvent(TEST_STREAM_ID, payload);
507
997
  };
998
+ const buildBotRuns = new Map();
999
+ const registerWorkspace = (record) => {
1000
+ workspaceById.set(record.id, record);
1001
+ return record;
1002
+ };
1003
+ const resolveWorkspaceRecord = (workspaceId) => {
1004
+ if (!workspaceId)
1005
+ return null;
1006
+ const cached = workspaceById.get(workspaceId);
1007
+ if (cached)
1008
+ return cached;
1009
+ const state = readSessionState(workspaceId);
1010
+ const meta = state?.meta ?? {};
1011
+ const deckPath = typeof meta
1012
+ .workspaceRootDeckPath === "string"
1013
+ ? meta.workspaceRootDeckPath
1014
+ : typeof meta.deck === "string"
1015
+ ? meta.deck
1016
+ : undefined;
1017
+ const rootDir = typeof meta.workspaceRootDir ===
1018
+ "string"
1019
+ ? meta.workspaceRootDir
1020
+ : deckPath
1021
+ ? path.dirname(deckPath)
1022
+ : undefined;
1023
+ if (!deckPath || !rootDir)
1024
+ return null;
1025
+ const createdAt = typeof meta.workspaceCreatedAt ===
1026
+ "string"
1027
+ ? meta.workspaceCreatedAt
1028
+ : typeof meta.sessionCreatedAt === "string"
1029
+ ? meta.sessionCreatedAt
1030
+ : new Date().toISOString();
1031
+ return registerWorkspace({
1032
+ id: workspaceId,
1033
+ rootDir,
1034
+ rootDeckPath: deckPath,
1035
+ createdAt,
1036
+ });
1037
+ };
1038
+ const resolveBuildBotRoot = async (workspaceId) => {
1039
+ const override = dntShim.Deno.env.get("GAMBIT_SIMULATOR_BUILD_BOT_ROOT")?.trim();
1040
+ if (override) {
1041
+ const root = await dntShim.Deno.realPath(override);
1042
+ const info = await dntShim.Deno.stat(root);
1043
+ if (!info.isDirectory) {
1044
+ throw new Error(`Build bot root is not a directory: ${root}`);
1045
+ }
1046
+ assertSafeBuildBotRoot(root, GAMBIT_BOT_SOURCE_DIR);
1047
+ await ensureGambitPolicyInBotRoot(root);
1048
+ return root;
1049
+ }
1050
+ const cacheKey = workspaceId ?? "default";
1051
+ const cached = buildBotRootCache.get(cacheKey);
1052
+ if (cached)
1053
+ return cached;
1054
+ const record = resolveWorkspaceRecord(workspaceId);
1055
+ const candidate = record?.rootDir ?? path.dirname(resolvedDeckPath);
1056
+ const root = await dntShim.Deno.realPath(candidate);
1057
+ const info = await dntShim.Deno.stat(root);
1058
+ if (!info.isDirectory) {
1059
+ throw new Error(`Build bot root is not a directory: ${root}`);
1060
+ }
1061
+ assertSafeBuildBotRoot(root, GAMBIT_BOT_SOURCE_DIR);
1062
+ await ensureGambitPolicyInBotRoot(root);
1063
+ buildBotRootCache.set(cacheKey, root);
1064
+ return root;
1065
+ };
1066
+ const logWorkspaceBotRoot = async (endpoint, workspaceId) => {
1067
+ try {
1068
+ const root = await resolveBuildBotRoot(workspaceId);
1069
+ logger.info(`[sim] ${endpoint}: workspaceId=${workspaceId ?? "(none)"} botRoot=${root}`);
1070
+ }
1071
+ catch (err) {
1072
+ logger.warn(`[sim] ${endpoint}: workspaceId=${workspaceId ?? "(none)"} botRoot=<unresolved> ${err instanceof Error ? err.message : String(err)}`);
1073
+ }
1074
+ };
1075
+ if (opts.workspace?.id && opts.workspace.rootDir && opts.workspace.rootDeckPath) {
1076
+ registerWorkspace({
1077
+ id: opts.workspace.id,
1078
+ rootDir: opts.workspace.rootDir,
1079
+ rootDeckPath: opts.workspace.rootDeckPath,
1080
+ createdAt: new Date().toISOString(),
1081
+ });
1082
+ }
1083
+ const MAX_FILE_PREVIEW_BYTES = 250_000;
1084
+ const shouldReadBuildDeckLabel = (relativePath) => {
1085
+ const lower = path.basename(relativePath).toLowerCase();
1086
+ return lower === "prompt.md" || lower.endsWith(".deck.md");
1087
+ };
1088
+ const readBuildDeckLabel = async (fullPath) => {
1089
+ try {
1090
+ const text = await dntShim.Deno.readTextFile(fullPath);
1091
+ const lines = text.split(/\r?\n/);
1092
+ if (lines[0] !== "+++")
1093
+ return undefined;
1094
+ const endIndex = lines.indexOf("+++", 1);
1095
+ if (endIndex === -1)
1096
+ return undefined;
1097
+ const frontmatter = lines.slice(1, endIndex).join("\n");
1098
+ const parsed = parseToml(frontmatter);
1099
+ const label = typeof parsed.label === "string" ? parsed.label.trim() : "";
1100
+ return label.length > 0 ? label : undefined;
1101
+ }
1102
+ catch {
1103
+ return undefined;
1104
+ }
1105
+ };
1106
+ const listBuildBotFiles = async (root) => {
1107
+ const entries = [];
1108
+ const shouldSkipRelativePath = (relativePath) => {
1109
+ const segments = relativePath.split(/\\|\//g).filter(Boolean);
1110
+ return segments.includes(".gambit");
1111
+ };
1112
+ const walk = async (dir, relativePrefix) => {
1113
+ for await (const entry of dntShim.Deno.readDir(dir)) {
1114
+ if (entry.isSymlink)
1115
+ continue;
1116
+ const fullPath = path.join(dir, entry.name);
1117
+ const relPath = relativePrefix
1118
+ ? path.join(relativePrefix, entry.name)
1119
+ : entry.name;
1120
+ if (shouldSkipRelativePath(relPath))
1121
+ continue;
1122
+ if (entry.isDirectory) {
1123
+ entries.push({ path: relPath, type: "dir" });
1124
+ await walk(fullPath, relPath);
1125
+ }
1126
+ else if (entry.isFile) {
1127
+ const info = await dntShim.Deno.stat(fullPath);
1128
+ const label = shouldReadBuildDeckLabel(relPath)
1129
+ ? await readBuildDeckLabel(fullPath)
1130
+ : undefined;
1131
+ entries.push({
1132
+ path: relPath,
1133
+ type: "file",
1134
+ size: info.size,
1135
+ modifiedAt: info.mtime ? info.mtime.toISOString() : undefined,
1136
+ label,
1137
+ });
1138
+ }
1139
+ }
1140
+ };
1141
+ await walk(root, "");
1142
+ return entries;
1143
+ };
1144
+ const resolveBuildBotPath = async (root, inputPath) => {
1145
+ if (!inputPath || typeof inputPath !== "string") {
1146
+ throw new Error("path is required");
1147
+ }
1148
+ const normalizedInput = path.normalize(inputPath);
1149
+ const segments = normalizedInput.split(/\\|\//g);
1150
+ if (segments.includes("..")) {
1151
+ throw new Error("path traversal is not allowed");
1152
+ }
1153
+ const candidate = path.isAbsolute(normalizedInput)
1154
+ ? normalizedInput
1155
+ : path.resolve(root, normalizedInput);
1156
+ const relativePath = path.relative(root, candidate);
1157
+ if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
1158
+ throw new Error("path escapes bot root");
1159
+ }
1160
+ const stat = await dntShim.Deno.lstat(candidate);
1161
+ if (stat.isSymlink) {
1162
+ throw new Error("symlinks are not allowed");
1163
+ }
1164
+ const realCandidate = await dntShim.Deno.realPath(candidate);
1165
+ const realRelative = path.relative(root, realCandidate);
1166
+ if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) {
1167
+ throw new Error("path escapes bot root");
1168
+ }
1169
+ return { fullPath: candidate, relativePath, stat };
1170
+ };
1171
+ const readPreviewText = (bytes) => {
1172
+ const limit = Math.min(bytes.length, 8192);
1173
+ for (let i = 0; i < limit; i += 1) {
1174
+ if (bytes[i] === 0)
1175
+ return null;
1176
+ }
1177
+ const decoder = new TextDecoder("utf-8", { fatal: true });
1178
+ try {
1179
+ return decoder.decode(bytes);
1180
+ }
1181
+ catch {
1182
+ return null;
1183
+ }
1184
+ };
1185
+ const isBuildStreamDebugEnabled = (() => {
1186
+ const raw = dntShim.Deno.env.get("GAMBIT_BUILD_STREAM_DEBUG")?.trim()
1187
+ .toLowerCase();
1188
+ return raw === "1" || raw === "true" || raw === "yes";
1189
+ })();
1190
+ const logBuildStreamDebug = (event, payload) => {
1191
+ if (!isBuildStreamDebugEnabled)
1192
+ return;
1193
+ const ts = new Date().toISOString();
1194
+ if (payload && Object.keys(payload).length > 0) {
1195
+ logger.info(`[build-stream-debug] ${ts} ${event} ${JSON.stringify(payload)}`);
1196
+ return;
1197
+ }
1198
+ logger.info(`[build-stream-debug] ${ts} ${event}`);
1199
+ };
1200
+ const broadcastBuildBot = (payload, workspaceId) => {
1201
+ const record = payload && typeof payload === "object"
1202
+ ? payload
1203
+ : null;
1204
+ const type = record && typeof record.type === "string"
1205
+ ? record.type
1206
+ : "(unknown)";
1207
+ const runId = record && typeof record.runId === "string"
1208
+ ? record.runId
1209
+ : record && record.run && typeof record.run === "object" &&
1210
+ typeof record.run.id === "string"
1211
+ ? record.run.id
1212
+ : undefined;
1213
+ const traceType = type === "buildBotTrace" && record &&
1214
+ record.event && typeof record.event === "object" &&
1215
+ typeof record.event.type === "string"
1216
+ ? record.event.type
1217
+ : undefined;
1218
+ logBuildStreamDebug("broadcastBuildBot", {
1219
+ type,
1220
+ runId,
1221
+ traceType,
1222
+ });
1223
+ const eventWorkspaceId = workspaceId ??
1224
+ (typeof runId === "string" ? runId : undefined);
1225
+ if (eventWorkspaceId) {
1226
+ const state = readSessionState(eventWorkspaceId);
1227
+ if (state) {
1228
+ appendWorkspaceEnvelope(state, "build", payload);
1229
+ }
1230
+ }
1231
+ appendDurableStreamEvent(WORKSPACE_STREAM_ID, payload);
1232
+ appendDurableStreamEvent(BUILD_STREAM_ID, payload);
1233
+ };
508
1234
  let deckSlug = deckSlugFromPath(resolvedDeckPath);
509
1235
  let deckLabel = undefined;
1236
+ let rootStartMode = undefined;
510
1237
  const enrichStateWithSession = (state) => {
511
1238
  const meta = { ...(state.meta ?? {}) };
512
1239
  const now = new Date();
1240
+ meta.sessionUpdatedAt = now.toISOString();
513
1241
  if (typeof meta.sessionId !== "string") {
514
1242
  const stamp = now.toISOString().replace(/[:.]/g, "-");
515
1243
  meta.sessionId = `${deckSlug}-${stamp}`;
516
1244
  meta.sessionCreatedAt = now.toISOString();
517
1245
  }
1246
+ if (typeof meta.workspaceId !== "string") {
1247
+ meta.workspaceId = String(meta.sessionId);
1248
+ }
1249
+ if (typeof meta.workspaceSchemaVersion !== "string") {
1250
+ meta.workspaceSchemaVersion = WORKSPACE_STATE_SCHEMA_VERSION;
1251
+ }
518
1252
  if (typeof meta.deck !== "string") {
519
1253
  meta.deck = resolvedDeckPath;
520
1254
  }
@@ -528,39 +1262,143 @@ export function startWebSocketSimulator(opts) {
528
1262
  typeof meta.sessionDir === "string") {
529
1263
  meta.sessionStatePath = path.join(meta.sessionDir, "state.json");
530
1264
  }
1265
+ if (typeof meta.sessionEventsPath !== "string" &&
1266
+ typeof meta.sessionDir === "string") {
1267
+ meta.sessionEventsPath = path.join(meta.sessionDir, "events.jsonl");
1268
+ }
1269
+ if (typeof meta.sessionBuildStatePath !== "string" &&
1270
+ typeof meta.sessionDir === "string") {
1271
+ meta.sessionBuildStatePath = path.join(meta.sessionDir, "build_state.json");
1272
+ }
531
1273
  const dir = typeof meta.sessionDir === "string"
532
1274
  ? meta.sessionDir
533
1275
  : undefined;
534
1276
  return { state: { ...state, meta }, dir };
535
1277
  };
536
- const persistSessionState = (state) => {
537
- const { state: enriched, dir } = enrichStateWithSession(state);
538
- if (dir) {
539
- try {
540
- ensureDir(dir);
541
- const filePath = path.join(dir, "state.json");
542
- dntShim.Deno.writeTextFileSync(filePath, JSON.stringify(enriched, null, 2));
543
- }
544
- catch (err) {
545
- logger.warn(`[sim] failed to persist session state: ${err instanceof Error ? err.message : err}`);
546
- }
1278
+ const { parseFiniteInteger, selectCanonicalScenarioRunSummary, appendWorkspaceEnvelope, appendSessionEvent, appendFeedbackLog, appendGradingLog, appendServerErrorLog, persistSessionState, readSessionStateStrict, readSessionState, readBuildState, } = createSessionStore({
1279
+ sessionsRoot,
1280
+ ensureDir,
1281
+ randomId,
1282
+ logger,
1283
+ enrichStateWithSession,
1284
+ workspaceStateSchemaVersion: WORKSPACE_STATE_SCHEMA_VERSION,
1285
+ workspaceSchemaError,
1286
+ });
1287
+ const traceCategory = (type) => {
1288
+ switch (type) {
1289
+ case "message.user":
1290
+ case "model.result":
1291
+ return "turn";
1292
+ case "tool.call":
1293
+ case "tool.result":
1294
+ return "tool";
1295
+ case "log":
1296
+ case "monolog":
1297
+ return "status";
1298
+ case "run.start":
1299
+ case "run.end":
1300
+ case "deck.start":
1301
+ case "deck.end":
1302
+ case "action.start":
1303
+ case "action.end":
1304
+ case "model.call":
1305
+ return "lifecycle";
1306
+ default:
1307
+ return "trace";
547
1308
  }
548
- return enriched;
549
1309
  };
550
- const readSessionState = (sessionId) => {
551
- const dir = path.join(sessionsRoot, sessionId);
552
- const filePath = path.join(dir, "state.json");
553
- try {
554
- const text = dntShim.Deno.readTextFileSync(filePath);
555
- const parsed = JSON.parse(text);
556
- if (parsed && typeof parsed === "object") {
557
- return parsed;
558
- }
1310
+ const buildWorkspaceMeta = (record, base) => {
1311
+ const createdAt = typeof base?.sessionCreatedAt ===
1312
+ "string"
1313
+ ? base.sessionCreatedAt
1314
+ : typeof base
1315
+ ?.workspaceCreatedAt === "string"
1316
+ ? base.workspaceCreatedAt
1317
+ : new Date().toISOString();
1318
+ return {
1319
+ ...(base ?? {}),
1320
+ workspaceSchemaVersion: WORKSPACE_STATE_SCHEMA_VERSION,
1321
+ workspaceId: record.id,
1322
+ workspaceRootDeckPath: record.rootDeckPath,
1323
+ workspaceRootDir: record.rootDir,
1324
+ workspaceCreatedAt: base
1325
+ ?.workspaceCreatedAt ?? createdAt,
1326
+ sessionCreatedAt: base
1327
+ ?.sessionCreatedAt ?? createdAt,
1328
+ deck: record.rootDeckPath,
1329
+ deckSlug: deckSlugFromPath(record.rootDeckPath),
1330
+ sessionId: record.id,
1331
+ };
1332
+ };
1333
+ const createWorkspaceSession = async (opts) => {
1334
+ const createdAt = new Date().toISOString();
1335
+ if (workspaceScaffoldEnabled) {
1336
+ const scaffold = await createWorkspaceScaffold({
1337
+ baseDir: workspaceRoot,
1338
+ });
1339
+ const record = registerWorkspace(scaffold);
1340
+ persistSessionState({
1341
+ runId: record.id,
1342
+ messages: [],
1343
+ meta: buildWorkspaceMeta(record, {
1344
+ sessionCreatedAt: record.createdAt,
1345
+ workspaceCreatedAt: record.createdAt,
1346
+ workspaceOnboarding: opts?.onboarding ?? false,
1347
+ }),
1348
+ });
1349
+ return record;
559
1350
  }
560
- catch {
561
- // ignore
1351
+ const workspaceId = randomId("workspace");
1352
+ const rootDeckPath = resolvedDeckPath;
1353
+ const rootDir = path.dirname(rootDeckPath);
1354
+ const record = registerWorkspace({
1355
+ id: workspaceId,
1356
+ rootDir,
1357
+ rootDeckPath,
1358
+ createdAt,
1359
+ });
1360
+ persistSessionState({
1361
+ runId: record.id,
1362
+ messages: [],
1363
+ meta: buildWorkspaceMeta(record, {
1364
+ sessionCreatedAt: createdAt,
1365
+ workspaceCreatedAt: createdAt,
1366
+ workspaceOnboarding: opts?.onboarding ?? false,
1367
+ }),
1368
+ });
1369
+ return record;
1370
+ };
1371
+ if (opts.workspace?.id && opts.workspace.rootDir && opts.workspace.rootDeckPath) {
1372
+ const existing = readSessionState(opts.workspace.id);
1373
+ if (!existing) {
1374
+ persistSessionState({
1375
+ runId: opts.workspace.id,
1376
+ messages: [],
1377
+ meta: buildWorkspaceMeta({
1378
+ id: opts.workspace.id,
1379
+ rootDir: opts.workspace.rootDir,
1380
+ rootDeckPath: opts.workspace.rootDeckPath,
1381
+ }, {
1382
+ sessionCreatedAt: new Date().toISOString(),
1383
+ workspaceCreatedAt: new Date().toISOString(),
1384
+ workspaceOnboarding: activeWorkspaceOnboarding,
1385
+ }),
1386
+ });
562
1387
  }
563
- return undefined;
1388
+ }
1389
+ const activateWorkspaceDeck = async (workspaceId) => {
1390
+ if (!workspaceId)
1391
+ return;
1392
+ const record = resolveWorkspaceRecord(workspaceId);
1393
+ if (!record)
1394
+ return;
1395
+ const nextPath = resolveDeckPath(record.rootDeckPath);
1396
+ if (nextPath === resolvedDeckPath)
1397
+ return;
1398
+ resolvedDeckPath = nextPath;
1399
+ buildBotRootCache.delete("default");
1400
+ reloadPrimaryDeck();
1401
+ await deckLoadPromise.catch(() => null);
564
1402
  };
565
1403
  const deleteSessionState = (sessionId) => {
566
1404
  if (!sessionId ||
@@ -628,22 +1466,137 @@ export function startWebSocketSimulator(opts) {
628
1466
  return [];
629
1467
  }
630
1468
  };
631
- const buildSessionMeta = (sessionId, state) => {
632
- const meta = state?.meta ?? {};
633
- const createdAt = typeof meta.sessionCreatedAt === "string"
634
- ? meta.sessionCreatedAt
635
- : undefined;
636
- const deck = typeof meta.deck === "string" ? meta.deck : undefined;
637
- const deckSlug = typeof meta.deckSlug === "string"
638
- ? meta.deckSlug
1469
+ const getWorkspaceIdFromQuery = (url) => resolveWorkspaceIdFromSearchParams(url.searchParams);
1470
+ const getWorkspaceIdFromBody = (body) => {
1471
+ if (!body || typeof body !== "object")
1472
+ return undefined;
1473
+ return resolveWorkspaceIdFromRecord(body);
1474
+ };
1475
+ const findTestRunByWorkspaceId = (workspaceId) => {
1476
+ for (const candidate of testBotRuns.values()) {
1477
+ if (candidate.run.workspaceId === workspaceId ||
1478
+ candidate.run.sessionId === workspaceId) {
1479
+ return candidate;
1480
+ }
1481
+ }
1482
+ return undefined;
1483
+ };
1484
+ const buildWorkspaceReadModel = async (workspaceId, opts) => {
1485
+ let state;
1486
+ try {
1487
+ state = readSessionStateStrict(workspaceId, { withTraces: true });
1488
+ }
1489
+ catch (err) {
1490
+ return {
1491
+ error: err instanceof Error ? err.message : String(err),
1492
+ status: 400,
1493
+ };
1494
+ }
1495
+ if (!state) {
1496
+ return {
1497
+ error: "Workspace not found",
1498
+ status: 404,
1499
+ };
1500
+ }
1501
+ const buildEntry = buildBotRuns.get(workspaceId);
1502
+ const buildRun = buildEntry?.run ?? buildRunFromProjection(workspaceId);
1503
+ const requestedTestRunId = typeof opts?.requestedTestRunId === "string" &&
1504
+ opts.requestedTestRunId.trim().length > 0
1505
+ ? opts.requestedTestRunId
1506
+ : null;
1507
+ const requestedTestEntry = requestedTestRunId
1508
+ ? testBotRuns.get(requestedTestRunId)
639
1509
  : undefined;
640
- const testBotName = typeof meta.testBotName ===
641
- "string"
642
- ? meta.testBotName
1510
+ const requestedLiveRun = requestedTestEntry?.run &&
1511
+ (requestedTestEntry.run.workspaceId === workspaceId ||
1512
+ requestedTestEntry.run.sessionId === workspaceId)
1513
+ ? requestedTestEntry.run
643
1514
  : undefined;
644
- const gradingRuns = Array.isArray(meta.gradingRuns)
645
- ? meta.gradingRuns.map((run) => ({
646
- id: typeof run.id === "string" ? run.id : randomId("cal"),
1515
+ const persistedRequestedRun = requestedTestRunId
1516
+ ? readPersistedTestRunStatusById(state, workspaceId, requestedTestRunId)
1517
+ : null;
1518
+ const testEntry = requestedLiveRun
1519
+ ? undefined
1520
+ : findTestRunByWorkspaceId(workspaceId);
1521
+ const testRun = requestedLiveRun ?? persistedRequestedRun ??
1522
+ testEntry?.run ?? {
1523
+ id: "",
1524
+ status: "idle",
1525
+ messages: [],
1526
+ traces: [],
1527
+ toolInserts: [],
1528
+ workspaceId,
1529
+ sessionId: workspaceId,
1530
+ };
1531
+ if (!requestedLiveRun && !persistedRequestedRun && !testEntry) {
1532
+ syncTestBotRunFromState(testRun, state);
1533
+ const meta = state.meta && typeof state.meta === "object"
1534
+ ? state.meta
1535
+ : null;
1536
+ if (meta) {
1537
+ const selectedScenarioSummary = selectCanonicalScenarioRunSummary(meta);
1538
+ if (selectedScenarioSummary) {
1539
+ testRun.id = selectedScenarioSummary.scenarioRunId;
1540
+ if (testRun.status === "idle") {
1541
+ testRun.status = "completed";
1542
+ }
1543
+ }
1544
+ }
1545
+ }
1546
+ await deckLoadPromise.catch(() => null);
1547
+ const requestedDeck = opts?.requestedTestDeckPath ?? null;
1548
+ const testSelection = requestedDeck
1549
+ ? resolveTestDeck(requestedDeck)
1550
+ : availableTestDecks[0];
1551
+ const testSchemaDesc = testSelection
1552
+ ? await describeDeckInputSchemaFromPath(testSelection.path)
1553
+ : undefined;
1554
+ const session = {
1555
+ workspaceId,
1556
+ messages: state.messages,
1557
+ messageRefs: state.messageRefs,
1558
+ feedback: state.feedback,
1559
+ traces: state.traces,
1560
+ notes: state.notes,
1561
+ meta: state.meta,
1562
+ };
1563
+ return {
1564
+ workspaceId,
1565
+ build: { run: buildRun },
1566
+ test: {
1567
+ run: testRun,
1568
+ botPath: testSelection?.path ?? null,
1569
+ botLabel: testSelection?.label ?? null,
1570
+ botDescription: testSelection?.description ?? null,
1571
+ selectedDeckId: testSelection?.id ?? null,
1572
+ inputSchema: testSchemaDesc?.schema ?? null,
1573
+ inputSchemaError: testSchemaDesc?.error ?? null,
1574
+ defaults: { input: testSchemaDesc?.defaults },
1575
+ testDecks: availableTestDecks,
1576
+ },
1577
+ grade: {
1578
+ graderDecks: availableGraderDecks,
1579
+ sessions: listSessions(),
1580
+ },
1581
+ session,
1582
+ };
1583
+ };
1584
+ const buildSessionMeta = (sessionId, state) => {
1585
+ const meta = state?.meta ?? {};
1586
+ const createdAt = typeof meta.sessionCreatedAt === "string"
1587
+ ? meta.sessionCreatedAt
1588
+ : undefined;
1589
+ const deck = typeof meta.deck === "string" ? meta.deck : undefined;
1590
+ const deckSlug = typeof meta.deckSlug === "string"
1591
+ ? meta.deckSlug
1592
+ : undefined;
1593
+ const testBotName = typeof meta.testBotName ===
1594
+ "string"
1595
+ ? meta.testBotName
1596
+ : undefined;
1597
+ const gradingRuns = Array.isArray(meta.gradingRuns)
1598
+ ? meta.gradingRuns.map((run) => ({
1599
+ id: typeof run.id === "string" ? run.id : randomId("cal"),
647
1600
  graderId: run.graderId,
648
1601
  graderPath: run.graderPath,
649
1602
  graderLabel: run.graderLabel,
@@ -812,6 +1765,11 @@ export function startWebSocketSimulator(opts) {
812
1765
  role: msg.role,
813
1766
  content,
814
1767
  messageRefId: refId,
1768
+ messageSource: refs[i]?.source === "scenario" ||
1769
+ refs[i]?.source === "manual" ||
1770
+ refs[i]?.source === "artifact"
1771
+ ? refs[i].source
1772
+ : undefined,
815
1773
  feedback: refId ? feedbackByRef.get(refId) : undefined,
816
1774
  });
817
1775
  continue;
@@ -822,6 +1780,11 @@ export function startWebSocketSimulator(opts) {
822
1780
  role: "assistant",
823
1781
  content: respondSummary.displayText,
824
1782
  messageRefId: refId,
1783
+ messageSource: refs[i]?.source === "scenario" ||
1784
+ refs[i]?.source === "manual" ||
1785
+ refs[i]?.source === "artifact"
1786
+ ? refs[i].source
1787
+ : undefined,
825
1788
  feedback: refId ? feedbackByRef.get(refId) : undefined,
826
1789
  respondStatus: respondSummary.status,
827
1790
  respondCode: respondSummary.code,
@@ -851,32 +1814,229 @@ export function startWebSocketSimulator(opts) {
851
1814
  : fallbackToolInserts,
852
1815
  };
853
1816
  };
854
- const buildConversationMessages = (state) => {
1817
+ const buildScenarioConversationArtifacts = (state) => {
855
1818
  const rawMessages = state.messages ?? [];
1819
+ const refs = state.messageRefs ?? [];
856
1820
  const conversation = [];
857
- for (const msg of rawMessages) {
1821
+ const assistantTurns = [];
1822
+ for (let i = 0; i < rawMessages.length; i++) {
1823
+ const msg = rawMessages[i];
1824
+ const messageRefId = typeof refs[i]?.id === "string"
1825
+ ? refs[i].id
1826
+ : undefined;
858
1827
  if (msg?.role === "assistant" || msg?.role === "user") {
859
1828
  const content = stringifyContent(msg.content).trim();
860
1829
  if (!content)
861
1830
  continue;
862
- conversation.push({
1831
+ const nextMessage = {
863
1832
  role: msg.role,
864
1833
  content,
865
1834
  name: msg.name,
866
1835
  tool_calls: msg.tool_calls,
867
- });
1836
+ };
1837
+ const conversationIndex = conversation.length;
1838
+ conversation.push(nextMessage);
1839
+ if (nextMessage.role === "assistant") {
1840
+ assistantTurns.push({
1841
+ conversationIndex,
1842
+ message: nextMessage,
1843
+ messageRefId,
1844
+ });
1845
+ }
868
1846
  continue;
869
1847
  }
870
1848
  const respondSummary = summarizeRespondCall(msg);
871
1849
  if (respondSummary) {
872
- conversation.push({
1850
+ const nextMessage = {
873
1851
  role: "assistant",
874
1852
  content: respondSummary.displayText,
875
1853
  name: GAMBIT_TOOL_RESPOND,
1854
+ };
1855
+ const conversationIndex = conversation.length;
1856
+ conversation.push(nextMessage);
1857
+ assistantTurns.push({
1858
+ conversationIndex,
1859
+ message: nextMessage,
1860
+ messageRefId,
1861
+ });
1862
+ }
1863
+ }
1864
+ return { messages: conversation, assistantTurns };
1865
+ };
1866
+ const buildScenarioConversationArtifactsFromRun = (run) => {
1867
+ const conversation = [];
1868
+ const assistantTurns = [];
1869
+ const runMessages = Array.isArray(run.messages) ? run.messages : [];
1870
+ for (const msg of runMessages) {
1871
+ if (msg?.role !== "assistant" && msg?.role !== "user")
1872
+ continue;
1873
+ const content = typeof msg.content === "string" ? msg.content.trim() : "";
1874
+ if (!content)
1875
+ continue;
1876
+ const nextMessage = {
1877
+ role: msg.role,
1878
+ content,
1879
+ };
1880
+ const conversationIndex = conversation.length;
1881
+ conversation.push(nextMessage);
1882
+ if (nextMessage.role === "assistant") {
1883
+ assistantTurns.push({
1884
+ conversationIndex,
1885
+ message: nextMessage,
1886
+ messageRefId: msg.messageRefId,
1887
+ });
1888
+ }
1889
+ }
1890
+ return { messages: conversation, assistantTurns };
1891
+ };
1892
+ const normalizePersistedTestRunStatus = (value, workspaceId) => {
1893
+ if (!value || typeof value !== "object")
1894
+ return null;
1895
+ const raw = value;
1896
+ const id = typeof raw.id === "string" ? raw.id : "";
1897
+ if (!id)
1898
+ return null;
1899
+ const rawStatus = raw.status;
1900
+ const status = rawStatus === "running" || rawStatus === "completed" ||
1901
+ rawStatus === "error" || rawStatus === "canceled"
1902
+ ? rawStatus
1903
+ : "idle";
1904
+ return {
1905
+ id,
1906
+ status,
1907
+ workspaceId: typeof raw.workspaceId === "string"
1908
+ ? raw.workspaceId
1909
+ : workspaceId,
1910
+ sessionId: typeof raw.sessionId === "string"
1911
+ ? raw.sessionId
1912
+ : workspaceId,
1913
+ error: typeof raw.error === "string" ? raw.error : undefined,
1914
+ startedAt: typeof raw.startedAt === "string" ? raw.startedAt : undefined,
1915
+ finishedAt: typeof raw.finishedAt === "string"
1916
+ ? raw.finishedAt
1917
+ : undefined,
1918
+ maxTurns: typeof raw.maxTurns === "number" && Number.isFinite(raw.maxTurns)
1919
+ ? raw.maxTurns
1920
+ : undefined,
1921
+ messages: Array.isArray(raw.messages)
1922
+ ? raw.messages
1923
+ : [],
1924
+ traces: Array.isArray(raw.traces) ? raw.traces : [],
1925
+ toolInserts: Array.isArray(raw.toolInserts)
1926
+ ? raw.toolInserts
1927
+ : [],
1928
+ };
1929
+ };
1930
+ const readPersistedTestRunStatusById = (sessionState, workspaceId, requestedRunId) => {
1931
+ const eventsPath = typeof sessionState.meta?.sessionEventsPath === "string"
1932
+ ? sessionState.meta.sessionEventsPath
1933
+ : undefined;
1934
+ if (!eventsPath)
1935
+ return null;
1936
+ try {
1937
+ const text = dntShim.Deno.readTextFileSync(eventsPath);
1938
+ let latest = null;
1939
+ for (const line of text.split("\n")) {
1940
+ if (!line.trim())
1941
+ continue;
1942
+ let parsed = null;
1943
+ try {
1944
+ parsed = JSON.parse(line);
1945
+ }
1946
+ catch {
1947
+ continue;
1948
+ }
1949
+ if (!parsed || typeof parsed !== "object")
1950
+ continue;
1951
+ const payload = extractPersistedWorkspacePayload(parsed);
1952
+ if (payload.type !== "testBotStatus" &&
1953
+ payload.type !== "gambit.test.status")
1954
+ continue;
1955
+ const normalized = normalizePersistedTestRunStatus(payload.run, workspaceId);
1956
+ if (!normalized || normalized.id !== requestedRunId)
1957
+ continue;
1958
+ latest = normalized;
1959
+ }
1960
+ return latest;
1961
+ }
1962
+ catch {
1963
+ return null;
1964
+ }
1965
+ };
1966
+ const resolveMessageByRef = (state, messageRefId) => {
1967
+ const refs = Array.isArray(state.messageRefs) ? state.messageRefs : [];
1968
+ const messages = Array.isArray(state.messages) ? state.messages : [];
1969
+ const idx = refs.findIndex((ref) => ref?.id === messageRefId);
1970
+ if (idx < 0)
1971
+ return {};
1972
+ return {
1973
+ message: messages[idx],
1974
+ ref: refs[idx],
1975
+ };
1976
+ };
1977
+ const isFeedbackEligibleMessageRef = (state, messageRefId) => {
1978
+ const { message, ref } = resolveMessageByRef(state, messageRefId);
1979
+ if (!message)
1980
+ return false;
1981
+ if (message.role === "assistant")
1982
+ return true;
1983
+ if (message.role === "user" && ref?.source === "scenario")
1984
+ return true;
1985
+ return summarizeRespondCall(message) !== null;
1986
+ };
1987
+ const isFeedbackEligiblePersistedTestRunMessageRef = (state, runId, messageRefId) => {
1988
+ const eventsPath = typeof state.meta?.sessionEventsPath === "string"
1989
+ ? state.meta.sessionEventsPath
1990
+ : undefined;
1991
+ if (!eventsPath)
1992
+ return false;
1993
+ try {
1994
+ const text = dntShim.Deno.readTextFileSync(eventsPath);
1995
+ for (const line of text.split("\n")) {
1996
+ if (!line.trim())
1997
+ continue;
1998
+ let parsed = null;
1999
+ try {
2000
+ parsed = JSON.parse(line);
2001
+ }
2002
+ catch {
2003
+ continue;
2004
+ }
2005
+ if (!parsed || typeof parsed !== "object")
2006
+ continue;
2007
+ const payload = extractPersistedWorkspacePayload(parsed);
2008
+ if (payload.type !== "testBotStatus" &&
2009
+ payload.type !== "gambit.test.status") {
2010
+ continue;
2011
+ }
2012
+ const run = payload.run;
2013
+ if (!run || typeof run !== "object")
2014
+ continue;
2015
+ const runRecord = run;
2016
+ if (typeof runRecord.id !== "string" || runRecord.id !== runId) {
2017
+ continue;
2018
+ }
2019
+ if (!Array.isArray(runRecord.messages))
2020
+ continue;
2021
+ const found = runRecord.messages.some((entry) => {
2022
+ if (!entry || typeof entry !== "object")
2023
+ return false;
2024
+ const message = entry;
2025
+ if (message.messageRefId !== messageRefId)
2026
+ return false;
2027
+ if (message.role === "assistant")
2028
+ return true;
2029
+ return message.role === "user" &&
2030
+ message.messageSource === "scenario";
876
2031
  });
2032
+ if (found)
2033
+ return true;
877
2034
  }
878
2035
  }
879
- return conversation;
2036
+ catch {
2037
+ return false;
2038
+ }
2039
+ return false;
880
2040
  };
881
2041
  const deriveToolInsertsFromTraces = (state, messageCount) => {
882
2042
  const traces = Array.isArray(state.traces) ? state.traces : [];
@@ -913,27 +2073,84 @@ export function startWebSocketSimulator(opts) {
913
2073
  }
914
2074
  return inserts;
915
2075
  };
2076
+ const applyUserMessageRefSource = (previousState, nextState, source) => {
2077
+ if (!Array.isArray(nextState.messages) ||
2078
+ !Array.isArray(nextState.messageRefs)) {
2079
+ return nextState;
2080
+ }
2081
+ const startIndex = Math.max(0, previousState?.messages?.length ?? 0);
2082
+ const nextRefs = [...nextState.messageRefs];
2083
+ let changed = false;
2084
+ for (let idx = startIndex; idx < nextState.messages.length; idx++) {
2085
+ const msg = nextState.messages[idx];
2086
+ if (!msg || msg.role !== "user")
2087
+ continue;
2088
+ const ref = nextRefs[idx];
2089
+ if (!ref || typeof ref.id !== "string")
2090
+ continue;
2091
+ if (ref.source === source)
2092
+ continue;
2093
+ nextRefs[idx] = { ...ref, source };
2094
+ changed = true;
2095
+ }
2096
+ if (!changed)
2097
+ return nextState;
2098
+ return { ...nextState, messageRefs: nextRefs };
2099
+ };
916
2100
  const syncTestBotRunFromState = (run, state) => {
917
2101
  const snapshot = buildTestBotSnapshot(state);
918
2102
  run.messages = snapshot.messages;
919
2103
  run.toolInserts = snapshot.toolInserts;
920
- const sessionId = typeof state.meta?.sessionId === "string"
921
- ? state.meta.sessionId
922
- : undefined;
923
- if (sessionId)
924
- run.sessionId = sessionId;
2104
+ const workspaceId = typeof state.meta?.workspaceId === "string"
2105
+ ? state.meta.workspaceId
2106
+ : typeof state.meta?.sessionId === "string"
2107
+ ? state.meta.sessionId
2108
+ : undefined;
2109
+ if (workspaceId) {
2110
+ run.workspaceId = workspaceId;
2111
+ run.sessionId = workspaceId;
2112
+ }
925
2113
  const initFill = state.meta
926
2114
  ?.testBotInitFill;
927
2115
  if (initFill)
928
2116
  run.initFill = initFill;
929
2117
  run.traces = Array.isArray(state.traces) ? [...state.traces] : undefined;
930
2118
  };
2119
+ const syncBuildBotRunFromState = (run, state) => {
2120
+ const snapshot = buildTestBotSnapshot(state);
2121
+ run.messages = snapshot.messages;
2122
+ run.toolInserts = snapshot.toolInserts;
2123
+ run.traces = Array.isArray(state.traces) ? [...state.traces] : undefined;
2124
+ };
2125
+ const buildRunFromProjection = (workspaceId) => {
2126
+ const projection = readBuildState(workspaceId);
2127
+ const run = projection?.run;
2128
+ if (!run) {
2129
+ return {
2130
+ id: workspaceId,
2131
+ status: "idle",
2132
+ messages: [],
2133
+ traces: [],
2134
+ toolInserts: [],
2135
+ };
2136
+ }
2137
+ return {
2138
+ id: run.id || workspaceId,
2139
+ status: run.status,
2140
+ error: run.error,
2141
+ startedAt: run.startedAt,
2142
+ finishedAt: run.finishedAt,
2143
+ messages: Array.isArray(run.messages) ? run.messages : [],
2144
+ traces: Array.isArray(run.traces) ? run.traces : [],
2145
+ toolInserts: Array.isArray(run.toolInserts) ? run.toolInserts : [],
2146
+ };
2147
+ };
931
2148
  const startTestBotRun = (runOpts = {}) => {
932
2149
  const botDeckPath = typeof runOpts.botDeckPath === "string"
933
2150
  ? runOpts.botDeckPath
934
2151
  : undefined;
935
2152
  if (!botDeckPath) {
936
- throw new Error("Missing test bot deck path");
2153
+ throw new Error("Missing scenario deck path");
937
2154
  }
938
2155
  const defaultMaxTurns = 12;
939
2156
  const maxTurns = Math.round(sanitizeNumber(runOpts.maxTurnsOverride ?? defaultMaxTurns, defaultMaxTurns, { min: 1, max: 200 }));
@@ -945,6 +2162,8 @@ export function startWebSocketSimulator(opts) {
945
2162
  : "";
946
2163
  const botConfigPath = botDeckPath;
947
2164
  const testBotName = path.basename(botConfigPath).replace(/\.deck\.(md|ts)$/i, "");
2165
+ const selectedScenarioDeckId = runOpts.botDeckId ?? testBotName;
2166
+ const selectedScenarioDeckLabel = runOpts.botDeckLabel ?? testBotName;
948
2167
  const runId = randomId("testbot");
949
2168
  const startedAt = new Date().toISOString();
950
2169
  const controller = new AbortController();
@@ -963,9 +2182,14 @@ export function startWebSocketSimulator(opts) {
963
2182
  };
964
2183
  testBotRuns.set(runId, entry);
965
2184
  const run = entry.run;
2185
+ const emitTestBot = (payload) => broadcastTestBot(payload, run.workspaceId ?? runOpts.workspaceId);
966
2186
  if (runOpts.initFill)
967
2187
  run.initFill = runOpts.initFill;
968
2188
  let savedState = undefined;
2189
+ const baseMeta = runOpts.baseMeta ?? {};
2190
+ const workspaceMeta = runOpts.workspaceRecord
2191
+ ? buildWorkspaceMeta(runOpts.workspaceRecord, baseMeta)
2192
+ : baseMeta;
969
2193
  let lastCount = 0;
970
2194
  const capturedTraces = [];
971
2195
  if (runOpts.initFillTrace) {
@@ -976,20 +2200,26 @@ export function startWebSocketSimulator(opts) {
976
2200
  actionCallId,
977
2201
  name: "gambit_test_bot_init_fill",
978
2202
  args: runOpts.initFillTrace.args,
2203
+ toolKind: "internal",
979
2204
  }, {
980
2205
  type: "tool.result",
981
2206
  runId,
982
2207
  actionCallId,
983
2208
  name: "gambit_test_bot_init_fill",
984
2209
  result: runOpts.initFillTrace.result,
2210
+ toolKind: "internal",
985
2211
  });
986
2212
  }
987
- const setSessionId = (state) => {
988
- const sessionId = typeof state?.meta?.sessionId === "string"
989
- ? state.meta.sessionId
990
- : undefined;
991
- if (sessionId)
992
- run.sessionId = sessionId;
2213
+ const setWorkspaceId = (state) => {
2214
+ const workspaceId = typeof state?.meta?.workspaceId === "string"
2215
+ ? state.meta.workspaceId
2216
+ : typeof state?.meta?.sessionId === "string"
2217
+ ? state.meta.sessionId
2218
+ : undefined;
2219
+ if (workspaceId) {
2220
+ run.workspaceId = workspaceId;
2221
+ run.sessionId = workspaceId;
2222
+ }
993
2223
  };
994
2224
  const appendFromState = (state) => {
995
2225
  const snapshot = buildTestBotSnapshot(state);
@@ -1000,16 +2230,39 @@ export function startWebSocketSimulator(opts) {
1000
2230
  run.messages = snapshot.messages;
1001
2231
  run.toolInserts = snapshot.toolInserts;
1002
2232
  lastCount = rawLength;
1003
- setSessionId(state);
2233
+ setWorkspaceId(state);
1004
2234
  run.traces = Array.isArray(state.traces) ? [...state.traces] : undefined;
1005
2235
  if (shouldBroadcast) {
1006
- broadcastTestBot({ type: "testBotStatus", run });
2236
+ emitTestBot({ type: "testBotStatus", run });
2237
+ }
2238
+ };
2239
+ const pendingTraceEvents = [];
2240
+ const flushPendingTraceEvents = (state) => {
2241
+ if (!pendingTraceEvents.length)
2242
+ return;
2243
+ for (const pending of pendingTraceEvents) {
2244
+ appendSessionEvent(state, {
2245
+ ...pending,
2246
+ kind: "trace",
2247
+ category: traceCategory(pending.type),
2248
+ });
1007
2249
  }
2250
+ pendingTraceEvents.length = 0;
1008
2251
  };
1009
2252
  const tracer = (event) => {
1010
2253
  const stamped = event.ts ? event : { ...event, ts: Date.now() };
1011
2254
  capturedTraces.push(stamped);
1012
2255
  consoleTracer?.(stamped);
2256
+ if (savedState?.meta?.sessionId) {
2257
+ appendSessionEvent(savedState, {
2258
+ ...stamped,
2259
+ kind: "trace",
2260
+ category: traceCategory(stamped.type),
2261
+ });
2262
+ }
2263
+ else {
2264
+ pendingTraceEvents.push(stamped);
2265
+ }
1013
2266
  };
1014
2267
  let deckBotState = undefined;
1015
2268
  let sessionEnded = false;
@@ -1023,8 +2276,11 @@ export function startWebSocketSimulator(opts) {
1023
2276
  return undefined;
1024
2277
  };
1025
2278
  const generateDeckBotUserMessage = async (history, streamOpts) => {
1026
- const assistantMessage = getLastAssistantMessage(history);
1027
- if (!assistantMessage)
2279
+ const assistantMessage = getLastAssistantMessage(history)?.trim() || "";
2280
+ const seedPrompt = !assistantMessage && streamOpts?.allowEmptyAssistant
2281
+ ? DEFAULT_TEST_BOT_SEED_PROMPT
2282
+ : undefined;
2283
+ if (!assistantMessage && !seedPrompt)
1028
2284
  return "";
1029
2285
  const result = await runDeckWithFallback({
1030
2286
  path: botDeckPath,
@@ -1033,13 +2289,15 @@ export function startWebSocketSimulator(opts) {
1033
2289
  modelProvider: opts.modelProvider,
1034
2290
  state: deckBotState,
1035
2291
  allowRootStringInput: true,
1036
- initialUserMessage: assistantMessage,
2292
+ initialUserMessage: assistantMessage || seedPrompt,
1037
2293
  onStateUpdate: (state) => {
1038
2294
  deckBotState = state;
1039
2295
  },
1040
2296
  stream: Boolean(streamOpts?.onStreamText),
1041
2297
  onStreamText: streamOpts?.onStreamText,
1042
2298
  responsesMode: opts.responsesMode,
2299
+ workerSandbox: opts.workerSandbox,
2300
+ signal: controller.signal,
1043
2301
  });
1044
2302
  if (isGambitEndSignal(result)) {
1045
2303
  sessionEnded = true;
@@ -1050,7 +2308,10 @@ export function startWebSocketSimulator(opts) {
1050
2308
  };
1051
2309
  const loop = async () => {
1052
2310
  try {
1053
- if (!controller.signal.aborted) {
2311
+ const effectiveStartMode = rootStartMode ?? "assistant";
2312
+ const shouldRunInitial = effectiveStartMode !== "user" ||
2313
+ Boolean(initialUserMessage);
2314
+ if (!controller.signal.aborted && shouldRunInitial) {
1054
2315
  const initialResult = await runDeck({
1055
2316
  path: resolvedDeckPath,
1056
2317
  input: deckInput,
@@ -1064,22 +2325,33 @@ export function startWebSocketSimulator(opts) {
1064
2325
  allowRootStringInput: true,
1065
2326
  initialUserMessage: initialUserMessage || undefined,
1066
2327
  responsesMode: opts.responsesMode,
2328
+ workerSandbox: opts.workerSandbox,
2329
+ signal: controller.signal,
1067
2330
  onStateUpdate: (state) => {
2331
+ const nextStateWithSource = applyUserMessageRefSource(savedState, state, "scenario");
1068
2332
  const nextMeta = {
1069
- ...(savedState?.meta ?? {}),
1070
- ...(state.meta ?? {}),
2333
+ ...workspaceMeta,
2334
+ ...(nextStateWithSource.meta ?? {}),
1071
2335
  testBot: true,
1072
2336
  testBotRunId: runId,
1073
2337
  testBotConfigPath: botConfigPath,
1074
2338
  testBotName,
2339
+ scenarioRunId: runId,
2340
+ selectedScenarioDeckId,
2341
+ selectedScenarioDeckLabel,
2342
+ scenarioConfigPath: botConfigPath,
1075
2343
  ...(run.initFill ? { testBotInitFill: run.initFill } : {}),
2344
+ ...(runOpts.workspaceId
2345
+ ? { workspaceId: runOpts.workspaceId }
2346
+ : {}),
1076
2347
  };
1077
2348
  const enriched = persistSessionState({
1078
- ...state,
2349
+ ...nextStateWithSource,
1079
2350
  meta: nextMeta,
1080
2351
  traces: capturedTraces,
1081
2352
  });
1082
2353
  savedState = enriched;
2354
+ flushPendingTraceEvents(enriched);
1083
2355
  appendFromState(enriched);
1084
2356
  },
1085
2357
  });
@@ -1094,7 +2366,7 @@ export function startWebSocketSimulator(opts) {
1094
2366
  break;
1095
2367
  const history = savedState?.messages ?? [];
1096
2368
  const userMessage = await generateDeckBotUserMessage(history, {
1097
- onStreamText: (chunk) => broadcastTestBot({
2369
+ onStreamText: (chunk) => emitTestBot({
1098
2370
  type: "testBotStream",
1099
2371
  runId,
1100
2372
  role: "user",
@@ -1102,8 +2374,10 @@ export function startWebSocketSimulator(opts) {
1102
2374
  turn,
1103
2375
  ts: Date.now(),
1104
2376
  }),
2377
+ allowEmptyAssistant: effectiveStartMode === "user" &&
2378
+ !getLastAssistantMessage(history),
1105
2379
  });
1106
- broadcastTestBot({
2380
+ emitTestBot({
1107
2381
  type: "testBotStreamEnd",
1108
2382
  runId,
1109
2383
  role: "user",
@@ -1125,25 +2399,36 @@ export function startWebSocketSimulator(opts) {
1125
2399
  allowRootStringInput: true,
1126
2400
  initialUserMessage: userMessage,
1127
2401
  responsesMode: opts.responsesMode,
2402
+ workerSandbox: opts.workerSandbox,
2403
+ signal: controller.signal,
1128
2404
  onStateUpdate: (state) => {
2405
+ const nextStateWithSource = applyUserMessageRefSource(savedState, state, "scenario");
1129
2406
  const nextMeta = {
1130
- ...(savedState?.meta ?? {}),
1131
- ...(state.meta ?? {}),
2407
+ ...workspaceMeta,
2408
+ ...(nextStateWithSource.meta ?? {}),
1132
2409
  testBot: true,
1133
2410
  testBotRunId: runId,
1134
2411
  testBotConfigPath: botConfigPath,
1135
2412
  testBotName,
2413
+ scenarioRunId: runId,
2414
+ selectedScenarioDeckId,
2415
+ selectedScenarioDeckLabel,
2416
+ scenarioConfigPath: botConfigPath,
1136
2417
  ...(run.initFill ? { testBotInitFill: run.initFill } : {}),
2418
+ ...(runOpts.workspaceId
2419
+ ? { workspaceId: runOpts.workspaceId }
2420
+ : {}),
1137
2421
  };
1138
2422
  const enriched = persistSessionState({
1139
- ...state,
2423
+ ...nextStateWithSource,
1140
2424
  meta: nextMeta,
1141
2425
  traces: capturedTraces,
1142
2426
  });
1143
2427
  savedState = enriched;
2428
+ flushPendingTraceEvents(enriched);
1144
2429
  appendFromState(enriched);
1145
2430
  },
1146
- onStreamText: (chunk) => broadcastTestBot({
2431
+ onStreamText: (chunk) => emitTestBot({
1147
2432
  type: "testBotStream",
1148
2433
  runId,
1149
2434
  role: "assistant",
@@ -1156,7 +2441,7 @@ export function startWebSocketSimulator(opts) {
1156
2441
  sessionEnded = true;
1157
2442
  break;
1158
2443
  }
1159
- broadcastTestBot({
2444
+ emitTestBot({
1160
2445
  type: "testBotStreamEnd",
1161
2446
  runId,
1162
2447
  role: "assistant",
@@ -1165,12 +2450,18 @@ export function startWebSocketSimulator(opts) {
1165
2450
  });
1166
2451
  }
1167
2452
  run.status = controller.signal.aborted ? "canceled" : "completed";
1168
- broadcastTestBot({ type: "testBotStatus", run });
2453
+ emitTestBot({ type: "testBotStatus", run });
1169
2454
  }
1170
2455
  catch (err) {
1171
- run.status = "error";
1172
- run.error = err instanceof Error ? err.message : String(err);
1173
- broadcastTestBot({ type: "testBotStatus", run });
2456
+ if (controller.signal.aborted || isRunCanceledError(err)) {
2457
+ run.status = "canceled";
2458
+ run.error = undefined;
2459
+ }
2460
+ else {
2461
+ run.status = "error";
2462
+ run.error = err instanceof Error ? err.message : String(err);
2463
+ }
2464
+ emitTestBot({ type: "testBotStatus", run });
1174
2465
  }
1175
2466
  finally {
1176
2467
  if (savedState?.messages) {
@@ -1178,23 +2469,25 @@ export function startWebSocketSimulator(opts) {
1178
2469
  run.messages = snapshot.messages;
1179
2470
  run.toolInserts = snapshot.toolInserts;
1180
2471
  }
1181
- setSessionId(savedState);
2472
+ setWorkspaceId(savedState);
1182
2473
  run.traces = Array.isArray(savedState?.traces)
1183
2474
  ? [...(savedState?.traces ?? [])]
1184
2475
  : undefined;
1185
2476
  run.finishedAt = new Date().toISOString();
1186
2477
  entry.abort = null;
1187
2478
  entry.promise = null;
1188
- broadcastTestBot({ type: "testBotStatus", run });
2479
+ emitTestBot({ type: "testBotStatus", run });
1189
2480
  }
1190
2481
  };
1191
2482
  entry.promise = loop();
1192
- broadcastTestBot({ type: "testBotStatus", run });
2483
+ emitTestBot({ type: "testBotStatus", run });
1193
2484
  return run;
1194
2485
  };
1195
2486
  const persistFailedInitFill = (args) => {
1196
2487
  const failedRunId = randomId("testbot");
1197
2488
  const testBotName = path.basename(args.botDeckPath).replace(/\.deck\.(md|ts)$/i, "");
2489
+ const selectedScenarioDeckId = args.botDeckId ?? testBotName;
2490
+ const selectedScenarioDeckLabel = args.botDeckLabel ?? testBotName;
1198
2491
  const actionCallId = randomId("initfill");
1199
2492
  const traces = [
1200
2493
  {
@@ -1203,6 +2496,7 @@ export function startWebSocketSimulator(opts) {
1203
2496
  actionCallId,
1204
2497
  name: "gambit_test_bot_init_fill",
1205
2498
  args: { missing: args.initFill?.requested ?? [] },
2499
+ toolKind: "internal",
1206
2500
  },
1207
2501
  {
1208
2502
  type: "tool.result",
@@ -1213,6 +2507,7 @@ export function startWebSocketSimulator(opts) {
1213
2507
  error: args.error,
1214
2508
  provided: args.initFill?.provided,
1215
2509
  },
2510
+ toolKind: "internal",
1216
2511
  },
1217
2512
  ];
1218
2513
  const failedState = persistSessionState({
@@ -1224,25 +2519,52 @@ export function startWebSocketSimulator(opts) {
1224
2519
  testBotRunId: failedRunId,
1225
2520
  testBotConfigPath: args.botDeckPath,
1226
2521
  testBotName,
2522
+ scenarioRunId: failedRunId,
2523
+ selectedScenarioDeckId,
2524
+ selectedScenarioDeckLabel,
2525
+ scenarioConfigPath: args.botDeckPath,
1227
2526
  testBotInitFill: args.initFill,
1228
2527
  testBotInitFillError: args.error,
1229
2528
  },
1230
2529
  });
1231
- const sessionId = typeof failedState.meta?.sessionId === "string"
1232
- ? failedState.meta.sessionId
2530
+ const workspaceId = typeof failedState.meta?.workspaceId === "string"
2531
+ ? failedState.meta.workspaceId
1233
2532
  : undefined;
1234
- const sessionPath = typeof failedState.meta?.sessionStatePath === "string"
2533
+ const workspacePath = typeof failedState.meta?.sessionStatePath === "string"
1235
2534
  ? failedState.meta.sessionStatePath
1236
2535
  : undefined;
1237
- if (sessionPath) {
1238
- logger.warn(`[sim] init fill failed; session saved to ${sessionPath}`);
2536
+ if (workspacePath) {
2537
+ logger.warn(`[sim] init fill failed; workspace state saved to ${workspacePath}`);
2538
+ }
2539
+ return { workspaceId, workspacePath };
2540
+ };
2541
+ const resolvePreferredDeckPath = async (candidate) => {
2542
+ if (path.basename(candidate) === "PROMPT.md")
2543
+ return candidate;
2544
+ const promptPath = path.join(path.dirname(candidate), "PROMPT.md");
2545
+ try {
2546
+ const stat = await dntShim.Deno.stat(promptPath);
2547
+ if (stat.isFile)
2548
+ return promptPath;
1239
2549
  }
1240
- return { sessionId, sessionPath };
2550
+ catch {
2551
+ // ignore missing PROMPT.md
2552
+ }
2553
+ return candidate;
1241
2554
  };
1242
- const deckLoadPromise = loadDeck(resolvedDeckPath)
2555
+ const createDeckLoadPromise = () => resolvePreferredDeckPath(resolvedDeckPath)
2556
+ .then((preferredPath) => {
2557
+ resolvedDeckPath = preferredPath;
2558
+ return loadDeck(preferredPath);
2559
+ })
1243
2560
  .then((deck) => {
1244
2561
  resolvedDeckPath = deck.path;
2562
+ buildBotRootCache.clear();
1245
2563
  deckSlug = deckSlugFromPath(resolvedDeckPath);
2564
+ rootStartMode = deck.startMode === "assistant" ||
2565
+ deck.startMode === "user"
2566
+ ? deck.startMode
2567
+ : undefined;
1246
2568
  deckLabel = typeof deck.label === "string"
1247
2569
  ? deck.label
1248
2570
  : toDeckLabel(deck.path);
@@ -1264,7 +2586,8 @@ export function startWebSocketSimulator(opts) {
1264
2586
  });
1265
2587
  updateTestDeckRegistry(availableTestDecks);
1266
2588
  availableGraderDecks = (deck.graderDecks ?? []).map((graderDeck, index) => {
1267
- const label = graderDeck.label && typeof graderDeck.label === "string"
2589
+ const label = graderDeck.label &&
2590
+ typeof graderDeck.label === "string"
1268
2591
  ? graderDeck.label
1269
2592
  : toDeckLabel(graderDeck.path);
1270
2593
  const id = graderDeck.id && typeof graderDeck.id === "string"
@@ -1291,21 +2614,30 @@ export function startWebSocketSimulator(opts) {
1291
2614
  updateGraderDeckRegistry(availableGraderDecks);
1292
2615
  return null;
1293
2616
  });
1294
- const schemaPromise = deckLoadPromise
2617
+ const createSchemaPromise = (loadPromise) => loadPromise
1295
2618
  .then((deck) => {
1296
- const desc = deck ? describeZodSchema(deck.inputSchema) : {
1297
- error: "Deck failed to load",
1298
- };
2619
+ if (!deck) {
2620
+ return { error: "Deck failed to load" };
2621
+ }
2622
+ const desc = describeZodSchema(deck.inputSchema);
2623
+ const tools = mapDeckTools(deck.actionDecks);
2624
+ const next = tools ? { ...desc, tools } : desc;
1299
2625
  if (hasInitialContext) {
1300
- return { ...desc, defaults: initialContext };
2626
+ return { ...next, defaults: initialContext };
1301
2627
  }
1302
- return desc;
2628
+ return next;
1303
2629
  })
1304
2630
  .catch((err) => {
1305
2631
  const message = err instanceof Error ? err.message : String(err);
1306
2632
  logger.warn(`[sim] failed to load deck schema: ${message}`);
1307
2633
  return { error: message };
1308
2634
  });
2635
+ let deckLoadPromise = createDeckLoadPromise();
2636
+ let schemaPromise = createSchemaPromise(deckLoadPromise);
2637
+ const reloadPrimaryDeck = () => {
2638
+ deckLoadPromise = createDeckLoadPromise();
2639
+ schemaPromise = createSchemaPromise(deckLoadPromise);
2640
+ };
1309
2641
  const wantsSourceMap = Boolean(opts.sourceMap);
1310
2642
  const bundlePlatform = opts.bundlePlatform ?? "deno";
1311
2643
  const autoBundle = opts.autoBundle ?? true;
@@ -1325,6 +2657,7 @@ export function startWebSocketSimulator(opts) {
1325
2657
  logger.log(`[sim] auto-bundle enabled; rebuilding simulator UI (${forceBundle ? "forced" : "stale"})...`);
1326
2658
  logger.log(`[sim] bundling simulator UI (${forceBundle ? "forced" : "stale"})...`);
1327
2659
  try {
2660
+ const decode = new TextDecoder();
1328
2661
  const p = new dntShim.Deno.Command("deno", {
1329
2662
  args: [
1330
2663
  "bundle",
@@ -1336,13 +2669,23 @@ export function startWebSocketSimulator(opts) {
1336
2669
  "simulator-ui/src/main.tsx",
1337
2670
  ],
1338
2671
  cwd: path.resolve(moduleDir, ".."),
1339
- stdout: "null",
1340
- stderr: "null",
2672
+ stdout: "piped",
2673
+ stderr: "piped",
1341
2674
  });
1342
- p.outputSync();
2675
+ const out = p.outputSync();
2676
+ if (!out.success) {
2677
+ const stderr = decode.decode(out.stderr).trim();
2678
+ const stdout = decode.decode(out.stdout).trim();
2679
+ const details = stderr || stdout || `exit ${out.code}`;
2680
+ throw new Error(`simulator UI bundle command failed (exit ${out.code}): ${details}`);
2681
+ }
1343
2682
  }
1344
2683
  catch (err) {
1345
- logger.warn(`[sim] auto-bundle failed: ${err instanceof Error ? err.message : err}`);
2684
+ const message = err instanceof Error ? err.message : String(err);
2685
+ if (forceBundle) {
2686
+ throw new Error(`[sim] auto-bundle failed: ${message}`);
2687
+ }
2688
+ logger.warn(`[sim] auto-bundle failed: ${message}`);
1346
2689
  }
1347
2690
  }
1348
2691
  const server = dntShim.Deno.serve({ port, signal: opts.signal, onListen: () => { } }, async (req) => {
@@ -1350,6 +2693,286 @@ export function startWebSocketSimulator(opts) {
1350
2693
  if (url.pathname.startsWith("/api/durable-streams/stream/")) {
1351
2694
  return handleDurableStreamRequest(req);
1352
2695
  }
2696
+ if (url.pathname === "/v1/responses") {
2697
+ if (req.method !== "POST") {
2698
+ return new Response("Method not allowed", { status: 405 });
2699
+ }
2700
+ if (!opts.modelProvider.responses) {
2701
+ return jsonResponse({ error: "Configured provider does not support responses." }, 501);
2702
+ }
2703
+ try {
2704
+ const body = parseBodyObject(await req.json());
2705
+ const model = typeof body.model === "string" ? body.model : undefined;
2706
+ if (!model) {
2707
+ throw new Error("model is required");
2708
+ }
2709
+ const input = normalizeInputItems(body.input);
2710
+ const stream = body.stream === true;
2711
+ const instructions = typeof body.instructions === "string"
2712
+ ? body.instructions
2713
+ : undefined;
2714
+ const previousResponseId = typeof body.previous_response_id === "string"
2715
+ ? body.previous_response_id
2716
+ : undefined;
2717
+ const store = typeof body.store === "boolean"
2718
+ ? body.store
2719
+ : undefined;
2720
+ const tools = normalizeTools(body.tools);
2721
+ const toolChoice = normalizeToolChoice(body.tool_choice);
2722
+ const reasoning = (body.reasoning &&
2723
+ typeof body.reasoning === "object" &&
2724
+ !Array.isArray(body.reasoning))
2725
+ ? body.reasoning
2726
+ : undefined;
2727
+ const parallelToolCalls = typeof body.parallel_tool_calls === "boolean"
2728
+ ? body.parallel_tool_calls
2729
+ : undefined;
2730
+ const maxToolCalls = typeof body.max_tool_calls === "number"
2731
+ ? body.max_tool_calls
2732
+ : undefined;
2733
+ const temperature = typeof body.temperature === "number"
2734
+ ? body.temperature
2735
+ : undefined;
2736
+ const topP = typeof body.top_p === "number" ? body.top_p : undefined;
2737
+ const frequencyPenalty = typeof body.frequency_penalty === "number"
2738
+ ? body.frequency_penalty
2739
+ : undefined;
2740
+ const presencePenalty = typeof body.presence_penalty === "number"
2741
+ ? body.presence_penalty
2742
+ : undefined;
2743
+ const maxOutputTokens = typeof body.max_output_tokens === "number"
2744
+ ? body.max_output_tokens
2745
+ : undefined;
2746
+ const topLogprobs = typeof body.top_logprobs === "number"
2747
+ ? body.top_logprobs
2748
+ : undefined;
2749
+ const truncation = body.truncation === "auto" ||
2750
+ body.truncation === "disabled"
2751
+ ? body.truncation
2752
+ : undefined;
2753
+ const text = (body.text && typeof body.text === "object" &&
2754
+ !Array.isArray(body.text))
2755
+ ? body.text
2756
+ : undefined;
2757
+ const streamOptions = (body.stream_options &&
2758
+ typeof body.stream_options === "object" &&
2759
+ !Array.isArray(body.stream_options))
2760
+ ? body.stream_options
2761
+ : undefined;
2762
+ const background = typeof body.background === "boolean"
2763
+ ? body.background
2764
+ : undefined;
2765
+ const include = Array.isArray(body.include)
2766
+ ? body.include.filter((entry) => typeof entry === "string")
2767
+ : undefined;
2768
+ const serviceTier = body.service_tier === "auto" ||
2769
+ body.service_tier === "default" || body.service_tier === "flex" ||
2770
+ body.service_tier === "priority"
2771
+ ? body.service_tier
2772
+ : undefined;
2773
+ const metadata = (body.metadata &&
2774
+ typeof body.metadata === "object" &&
2775
+ !Array.isArray(body.metadata))
2776
+ ? body.metadata
2777
+ : undefined;
2778
+ const safetyIdentifier = typeof body.safety_identifier === "string"
2779
+ ? body.safety_identifier
2780
+ : undefined;
2781
+ const promptCacheKey = typeof body.prompt_cache_key === "string"
2782
+ ? body.prompt_cache_key
2783
+ : undefined;
2784
+ const passthrough = {};
2785
+ for (const [key, value] of Object.entries(body)) {
2786
+ if (key === "model" || key === "input" || key === "stream" ||
2787
+ key === "instructions" || key === "tools" ||
2788
+ key === "tool_choice" || key === "max_output_tokens" ||
2789
+ key === "previous_response_id" || key === "store" ||
2790
+ key === "reasoning" || key === "parallel_tool_calls" ||
2791
+ key === "max_tool_calls" || key === "temperature" ||
2792
+ key === "top_p" || key === "frequency_penalty" ||
2793
+ key === "presence_penalty" || key === "include" ||
2794
+ key === "text" || key === "stream_options" ||
2795
+ key === "background" || key === "truncation" ||
2796
+ key === "service_tier" || key === "top_logprobs" ||
2797
+ key === "metadata" || key === "safety_identifier" ||
2798
+ key === "prompt_cache_key" || key === "params") {
2799
+ continue;
2800
+ }
2801
+ passthrough[key] = value;
2802
+ }
2803
+ const explicitParams = (body.params &&
2804
+ typeof body.params === "object" &&
2805
+ !Array.isArray(body.params))
2806
+ ? body.params
2807
+ : undefined;
2808
+ const params = explicitParams || Object.keys(passthrough).length > 0
2809
+ ? { ...(explicitParams ?? {}), ...passthrough }
2810
+ : undefined;
2811
+ const requestBody = {
2812
+ model,
2813
+ input,
2814
+ instructions,
2815
+ previous_response_id: previousResponseId,
2816
+ store,
2817
+ tools,
2818
+ tool_choice: toolChoice,
2819
+ reasoning,
2820
+ parallel_tool_calls: parallelToolCalls,
2821
+ max_tool_calls: maxToolCalls,
2822
+ temperature,
2823
+ top_p: topP,
2824
+ frequency_penalty: frequencyPenalty,
2825
+ presence_penalty: presencePenalty,
2826
+ stream,
2827
+ stream_options: streamOptions,
2828
+ max_output_tokens: maxOutputTokens,
2829
+ top_logprobs: topLogprobs,
2830
+ truncation,
2831
+ text,
2832
+ include,
2833
+ background,
2834
+ service_tier: serviceTier,
2835
+ metadata,
2836
+ safety_identifier: safetyIdentifier,
2837
+ prompt_cache_key: promptCacheKey,
2838
+ params,
2839
+ };
2840
+ if (!stream) {
2841
+ const response = await opts.modelProvider.responses({
2842
+ request: requestBody,
2843
+ });
2844
+ return jsonResponse(toStrictResponseResource({
2845
+ request: requestBody,
2846
+ response,
2847
+ }));
2848
+ }
2849
+ const streamBody = new ReadableStream({
2850
+ start: async (controller) => {
2851
+ let sequence = 1;
2852
+ const itemIdByOutputIndex = new Map();
2853
+ const streamRequest = {
2854
+ ...requestBody,
2855
+ stream: true,
2856
+ };
2857
+ try {
2858
+ const result = await opts.modelProvider.responses({
2859
+ request: streamRequest,
2860
+ onStreamEvent: (event) => {
2861
+ if (event.type === "response.created") {
2862
+ controller.enqueue(sseFrame({
2863
+ type: "response.created",
2864
+ sequence_number: sequence++,
2865
+ response: toStrictResponseResource({
2866
+ request: streamRequest,
2867
+ response: event.response,
2868
+ statusOverride: "in_progress",
2869
+ }),
2870
+ }));
2871
+ return;
2872
+ }
2873
+ if (event.type === "response.output_text.delta") {
2874
+ const itemId = event.item_id ??
2875
+ itemIdByOutputIndex.get(event.output_index) ??
2876
+ `msg_${event.output_index + 1}`;
2877
+ itemIdByOutputIndex.set(event.output_index, itemId);
2878
+ controller.enqueue(sseFrame({
2879
+ type: "response.output_text.delta",
2880
+ sequence_number: sequence++,
2881
+ output_index: event.output_index,
2882
+ item_id: itemId,
2883
+ content_index: event.content_index ?? 0,
2884
+ delta: event.delta,
2885
+ logprobs: event.logprobs ?? [],
2886
+ }));
2887
+ return;
2888
+ }
2889
+ if (event.type === "response.output_text.done") {
2890
+ const itemId = event.item_id ??
2891
+ itemIdByOutputIndex.get(event.output_index) ??
2892
+ `msg_${event.output_index + 1}`;
2893
+ itemIdByOutputIndex.set(event.output_index, itemId);
2894
+ controller.enqueue(sseFrame({
2895
+ type: "response.output_text.done",
2896
+ sequence_number: sequence++,
2897
+ output_index: event.output_index,
2898
+ item_id: itemId,
2899
+ content_index: event.content_index ?? 0,
2900
+ text: event.text,
2901
+ logprobs: [],
2902
+ }));
2903
+ return;
2904
+ }
2905
+ if (event.type === "response.completed") {
2906
+ controller.enqueue(sseFrame({
2907
+ type: "response.completed",
2908
+ sequence_number: sequence++,
2909
+ response: toStrictResponseResource({
2910
+ request: streamRequest,
2911
+ response: event.response,
2912
+ statusOverride: "completed",
2913
+ }),
2914
+ }));
2915
+ return;
2916
+ }
2917
+ if (event.type === "response.failed") {
2918
+ controller.enqueue(sseFrame({
2919
+ type: "response.failed",
2920
+ sequence_number: sequence++,
2921
+ response: {
2922
+ ...toStrictResponseResource({
2923
+ request: streamRequest,
2924
+ response: {
2925
+ id: `resp_${crypto.randomUUID().slice(0, 8)}`,
2926
+ object: "response",
2927
+ output: [],
2928
+ status: "failed",
2929
+ error: event.error ??
2930
+ { message: "Unknown error" },
2931
+ },
2932
+ statusOverride: "failed",
2933
+ }),
2934
+ error: event.error ?? { message: "Unknown error" },
2935
+ },
2936
+ }));
2937
+ }
2938
+ },
2939
+ });
2940
+ controller.enqueue(sseFrame({
2941
+ type: "response.completed",
2942
+ sequence_number: sequence++,
2943
+ response: toStrictResponseResource({
2944
+ request: streamRequest,
2945
+ response: result,
2946
+ statusOverride: "completed",
2947
+ }),
2948
+ }));
2949
+ controller.enqueue(new TextEncoder().encode("data: [DONE]\n\n"));
2950
+ }
2951
+ catch (err) {
2952
+ controller.enqueue(sseFrame({
2953
+ type: "error",
2954
+ code: "internal_error",
2955
+ message: err instanceof Error ? err.message : String(err),
2956
+ param: null,
2957
+ }));
2958
+ }
2959
+ finally {
2960
+ controller.close();
2961
+ }
2962
+ },
2963
+ });
2964
+ return new Response(streamBody, {
2965
+ headers: {
2966
+ "content-type": "text/event-stream",
2967
+ "cache-control": "no-cache",
2968
+ "connection": "keep-alive",
2969
+ },
2970
+ });
2971
+ }
2972
+ catch (err) {
2973
+ return jsonResponse({ error: err instanceof Error ? err.message : String(err) }, 400);
2974
+ }
2975
+ }
1353
2976
  if (url.pathname === "/favicon.ico") {
1354
2977
  if (req.method !== "GET" && req.method !== "HEAD") {
1355
2978
  return new Response("Method not allowed", { status: 405 });
@@ -1372,16 +2995,72 @@ export function startWebSocketSimulator(opts) {
1372
2995
  }
1373
2996
  }
1374
2997
  }
1375
- if (url.pathname === "/api/calibrate") {
2998
+ const workspaceTestRunGetMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/test\/([^/]+)$/);
2999
+ if (workspaceTestRunGetMatch) {
1376
3000
  if (req.method !== "GET") {
1377
3001
  return new Response("Method not allowed", { status: 405 });
1378
3002
  }
1379
- await deckLoadPromise.catch(() => null);
1380
- const sessions = listSessions();
1381
- return new Response(JSON.stringify({
1382
- graderDecks: availableGraderDecks,
1383
- sessions,
1384
- }), { headers: { "content-type": "application/json" } });
3003
+ const workspaceId = decodeURIComponent(workspaceTestRunGetMatch[1]);
3004
+ const requestedTestRunId = decodeURIComponent(workspaceTestRunGetMatch[2]);
3005
+ await logWorkspaceBotRoot("/api/workspaces/:id/test/:runId", workspaceId);
3006
+ await activateWorkspaceDeck(workspaceId);
3007
+ const payload = await buildWorkspaceReadModel(workspaceId, {
3008
+ requestedTestDeckPath: url.searchParams.get("deckPath"),
3009
+ requestedTestRunId,
3010
+ });
3011
+ if ("error" in payload) {
3012
+ return new Response(JSON.stringify({ error: payload.error }), {
3013
+ status: payload.status,
3014
+ headers: { "content-type": "application/json" },
3015
+ });
3016
+ }
3017
+ return new Response(JSON.stringify(payload), {
3018
+ headers: { "content-type": "application/json" },
3019
+ });
3020
+ }
3021
+ const workspaceGradeRunGetMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)\/grade\/([^/]+)$/);
3022
+ if (workspaceGradeRunGetMatch) {
3023
+ if (req.method !== "GET") {
3024
+ return new Response("Method not allowed", { status: 405 });
3025
+ }
3026
+ const workspaceId = decodeURIComponent(workspaceGradeRunGetMatch[1]);
3027
+ const requestedGradeRunId = decodeURIComponent(workspaceGradeRunGetMatch[2]);
3028
+ await logWorkspaceBotRoot("/api/workspaces/:id/grade/:runId", workspaceId);
3029
+ await activateWorkspaceDeck(workspaceId);
3030
+ const payload = await buildWorkspaceReadModel(workspaceId, {
3031
+ requestedTestDeckPath: url.searchParams.get("deckPath"),
3032
+ requestedGradeRunId,
3033
+ });
3034
+ if ("error" in payload) {
3035
+ return new Response(JSON.stringify({ error: payload.error }), {
3036
+ status: payload.status,
3037
+ headers: { "content-type": "application/json" },
3038
+ });
3039
+ }
3040
+ return new Response(JSON.stringify(payload), {
3041
+ headers: { "content-type": "application/json" },
3042
+ });
3043
+ }
3044
+ const workspaceGetMatch = url.pathname.match(/^\/api\/workspaces\/([^/]+)$/);
3045
+ if (workspaceGetMatch) {
3046
+ if (req.method !== "GET") {
3047
+ return new Response("Method not allowed", { status: 405 });
3048
+ }
3049
+ const workspaceId = decodeURIComponent(workspaceGetMatch[1]);
3050
+ await logWorkspaceBotRoot("/api/workspaces/:id", workspaceId);
3051
+ await activateWorkspaceDeck(workspaceId);
3052
+ const payload = await buildWorkspaceReadModel(workspaceId, {
3053
+ requestedTestDeckPath: url.searchParams.get("deckPath"),
3054
+ });
3055
+ if ("error" in payload) {
3056
+ return new Response(JSON.stringify({ error: payload.error }), {
3057
+ status: payload.status,
3058
+ headers: { "content-type": "application/json" },
3059
+ });
3060
+ }
3061
+ return new Response(JSON.stringify(payload), {
3062
+ headers: { "content-type": "application/json" },
3063
+ });
1385
3064
  }
1386
3065
  if (url.pathname === "/api/calibrate/run") {
1387
3066
  if (req.method !== "POST") {
@@ -1389,10 +3068,12 @@ export function startWebSocketSimulator(opts) {
1389
3068
  }
1390
3069
  try {
1391
3070
  const body = await req.json();
1392
- if (!body.sessionId) {
1393
- throw new Error("Missing sessionId");
3071
+ const workspaceId = getWorkspaceIdFromBody(body);
3072
+ if (!workspaceId) {
3073
+ throw new Error("Missing workspaceId");
1394
3074
  }
1395
- const sessionId = body.sessionId;
3075
+ await logWorkspaceBotRoot("/api/calibrate/run", workspaceId);
3076
+ await activateWorkspaceDeck(workspaceId);
1396
3077
  await deckLoadPromise.catch(() => null);
1397
3078
  const grader = body.graderId
1398
3079
  ? resolveGraderDeck(body.graderId)
@@ -1400,9 +3081,28 @@ export function startWebSocketSimulator(opts) {
1400
3081
  if (!grader) {
1401
3082
  throw new Error("Unknown grader deck selection");
1402
3083
  }
1403
- const sessionState = readSessionState(sessionId);
3084
+ const sessionState = readSessionState(workspaceId);
1404
3085
  if (!sessionState) {
1405
- throw new Error("Session not found");
3086
+ throw new Error("Workspace not found");
3087
+ }
3088
+ const requestedScenarioRunId = typeof body.scenarioRunId === "string" &&
3089
+ body.scenarioRunId.trim().length > 0
3090
+ ? body.scenarioRunId
3091
+ : undefined;
3092
+ const requestedLiveRun = requestedScenarioRunId
3093
+ ? testBotRuns.get(requestedScenarioRunId)?.run
3094
+ : undefined;
3095
+ const requestedLiveRunMatchesWorkspace = Boolean(requestedLiveRun &&
3096
+ (requestedLiveRun.workspaceId === workspaceId ||
3097
+ requestedLiveRun.sessionId === workspaceId));
3098
+ const requestedPersistedRun = requestedScenarioRunId
3099
+ ? readPersistedTestRunStatusById(sessionState, workspaceId, requestedScenarioRunId)
3100
+ : null;
3101
+ const selectedScenarioRun = requestedLiveRunMatchesWorkspace
3102
+ ? requestedLiveRun
3103
+ : requestedPersistedRun;
3104
+ if (requestedScenarioRunId && !selectedScenarioRun) {
3105
+ throw new Error(`Scenario run "${requestedScenarioRunId}" not found for workspace`);
1406
3106
  }
1407
3107
  const graderSchema = await describeDeckInputSchemaFromPath(grader.path);
1408
3108
  const runMode = schemaHasField(graderSchema.schema, "messageToGrade")
@@ -1417,7 +3117,24 @@ export function startWebSocketSimulator(opts) {
1417
3117
  delete next.gradingRuns;
1418
3118
  return next;
1419
3119
  })();
1420
- const conversationMessages = buildConversationMessages(sessionState);
3120
+ const conversationArtifacts = selectedScenarioRun
3121
+ ? buildScenarioConversationArtifactsFromRun(selectedScenarioRun)
3122
+ : buildScenarioConversationArtifacts(sessionState);
3123
+ const conversationMessages = conversationArtifacts.messages;
3124
+ const activeScenarioRunId = requestedScenarioRunId ??
3125
+ (typeof sessionState.meta?.scenarioRunId === "string" &&
3126
+ sessionState.meta.scenarioRunId.trim().length > 0
3127
+ ? sessionState.meta.scenarioRunId
3128
+ : undefined);
3129
+ const sessionMetaForPayload = {
3130
+ ...(metaForGrading ?? {}),
3131
+ ...(activeScenarioRunId
3132
+ ? {
3133
+ scenarioRunId: activeScenarioRunId,
3134
+ testBotRunId: activeScenarioRunId,
3135
+ }
3136
+ : {}),
3137
+ };
1421
3138
  const sessionPayload = {
1422
3139
  messages: conversationMessages.length > 0
1423
3140
  ? conversationMessages.map((msg) => ({
@@ -1426,7 +3143,7 @@ export function startWebSocketSimulator(opts) {
1426
3143
  name: msg.name,
1427
3144
  }))
1428
3145
  : undefined,
1429
- meta: metaForGrading,
3146
+ meta: sessionMetaForPayload,
1430
3147
  notes: sessionState.notes
1431
3148
  ? { text: sessionState.notes.text }
1432
3149
  : undefined,
@@ -1452,10 +3169,20 @@ export function startWebSocketSimulator(opts) {
1452
3169
  gradingRuns: nextRuns,
1453
3170
  },
1454
3171
  });
1455
- const sessionMeta = buildSessionMeta(sessionId, nextState);
3172
+ appendGradingLog(nextState, {
3173
+ type: "grading.run",
3174
+ run: nextEntry,
3175
+ });
3176
+ const sessionMeta = buildSessionMeta(workspaceId, nextState);
3177
+ appendDurableStreamEvent(WORKSPACE_STREAM_ID, {
3178
+ type: "calibrateSession",
3179
+ workspaceId,
3180
+ run: nextEntry,
3181
+ session: sessionMeta,
3182
+ });
1456
3183
  appendDurableStreamEvent(GRADE_STREAM_ID, {
1457
3184
  type: "calibrateSession",
1458
- sessionId,
3185
+ workspaceId,
1459
3186
  run: nextEntry,
1460
3187
  session: sessionMeta,
1461
3188
  });
@@ -1467,11 +3194,13 @@ export function startWebSocketSimulator(opts) {
1467
3194
  if (runMode !== "turns") {
1468
3195
  entry = {
1469
3196
  id: runId,
3197
+ workspaceId,
1470
3198
  graderId: grader.id,
1471
3199
  graderPath: grader.path,
1472
3200
  graderLabel: grader.label,
1473
3201
  status: "running",
1474
3202
  runAt: startedAt,
3203
+ gradingRunId: runId,
1475
3204
  input: { session: sessionPayload },
1476
3205
  };
1477
3206
  currentState = upsertCalibrationRun(currentState, entry);
@@ -1487,27 +3216,28 @@ export function startWebSocketSimulator(opts) {
1487
3216
  });
1488
3217
  }
1489
3218
  const messages = sessionPayload.messages ?? [];
1490
- const assistantTurns = messages
1491
- .map((msg, idx) => ({ msg, idx }))
1492
- .filter(({ msg }) => msg.role === "assistant" &&
1493
- typeof msg.content === "string" &&
1494
- msg.content.trim().length > 0);
3219
+ const assistantTurns = conversationArtifacts.assistantTurns;
1495
3220
  const totalTurns = assistantTurns.length;
1496
3221
  const turns = [];
1497
3222
  entry = {
1498
3223
  id: runId,
3224
+ workspaceId,
1499
3225
  graderId: grader.id,
1500
3226
  graderPath: grader.path,
1501
3227
  graderLabel: grader.label,
1502
3228
  status: "running",
1503
3229
  runAt: startedAt,
3230
+ gradingRunId: runId,
3231
+ input: { session: sessionPayload },
1504
3232
  result: { mode: "turns", totalTurns, turns: [] },
1505
3233
  };
1506
3234
  currentState = upsertCalibrationRun(currentState, entry);
1507
3235
  if (totalTurns === 0) {
1508
3236
  return { mode: "turns", totalTurns, turns: [] };
1509
3237
  }
1510
- for (const { msg, idx } of assistantTurns) {
3238
+ for (const turnEntry of assistantTurns) {
3239
+ const msg = turnEntry.message;
3240
+ const idx = turnEntry.conversationIndex;
1511
3241
  const input = {
1512
3242
  session: {
1513
3243
  ...sessionPayload,
@@ -1527,6 +3257,9 @@ export function startWebSocketSimulator(opts) {
1527
3257
  });
1528
3258
  turns.push({
1529
3259
  index: idx,
3260
+ gradingRunId: runId,
3261
+ artifactRevisionId: randomId("grade-rev"),
3262
+ messageRefId: turnEntry.messageRefId,
1530
3263
  message: msg,
1531
3264
  input,
1532
3265
  result: turnResult,
@@ -1541,39 +3274,57 @@ export function startWebSocketSimulator(opts) {
1541
3274
  })();
1542
3275
  entry = {
1543
3276
  id: runId,
3277
+ workspaceId,
1544
3278
  graderId: grader.id,
1545
3279
  graderPath: grader.path,
1546
3280
  graderLabel: grader.label,
1547
3281
  status: "completed",
1548
3282
  runAt: startedAt,
3283
+ gradingRunId: runId,
1549
3284
  input: { session: sessionPayload },
1550
3285
  result,
1551
3286
  };
1552
3287
  }
1553
3288
  catch (err) {
1554
3289
  const message = err instanceof Error ? err.message : String(err);
3290
+ logger.error("[sim] calibrate run failed", {
3291
+ workspaceId,
3292
+ runId,
3293
+ runMode,
3294
+ graderId: grader.id,
3295
+ graderPath: grader.path,
3296
+ error: message,
3297
+ stack: err instanceof Error ? err.stack : undefined,
3298
+ });
1555
3299
  entry = {
1556
3300
  id: runId,
3301
+ workspaceId,
1557
3302
  graderId: grader.id,
1558
3303
  graderPath: grader.path,
1559
3304
  graderLabel: grader.label,
1560
3305
  status: "error",
1561
3306
  runAt: startedAt,
3307
+ gradingRunId: runId,
1562
3308
  input: { session: sessionPayload },
1563
3309
  error: message,
1564
3310
  };
1565
3311
  }
1566
3312
  const nextState = upsertCalibrationRun(currentState, entry);
1567
- const sessionMeta = buildSessionMeta(body.sessionId, nextState);
3313
+ const sessionMeta = buildSessionMeta(workspaceId, nextState);
1568
3314
  return new Response(JSON.stringify({
1569
- sessionId: body.sessionId,
3315
+ workspaceId,
1570
3316
  run: entry,
1571
3317
  session: sessionMeta,
1572
3318
  }), { headers: { "content-type": "application/json" } });
1573
3319
  }
1574
3320
  catch (err) {
1575
- return new Response(JSON.stringify({
1576
- error: err instanceof Error ? err.message : String(err),
3321
+ const message = err instanceof Error ? err.message : String(err);
3322
+ logger.error("[sim] /api/calibrate/run request failed", {
3323
+ error: message,
3324
+ stack: err instanceof Error ? err.stack : undefined,
3325
+ });
3326
+ return new Response(JSON.stringify({
3327
+ error: message,
1577
3328
  }), { status: 400, headers: { "content-type": "application/json" } });
1578
3329
  }
1579
3330
  }
@@ -1583,12 +3334,14 @@ export function startWebSocketSimulator(opts) {
1583
3334
  }
1584
3335
  try {
1585
3336
  const body = await req.json();
1586
- if (!body.sessionId || !body.refId) {
1587
- throw new Error("Missing sessionId or refId");
3337
+ const workspaceId = getWorkspaceIdFromBody(body);
3338
+ if (!workspaceId || !body.refId) {
3339
+ throw new Error("Missing workspaceId or refId");
1588
3340
  }
1589
- const state = readSessionState(body.sessionId);
3341
+ await logWorkspaceBotRoot("/api/calibrate/flag", workspaceId);
3342
+ const state = readSessionState(workspaceId);
1590
3343
  if (!state) {
1591
- throw new Error("Session not found");
3344
+ throw new Error("Workspace not found");
1592
3345
  }
1593
3346
  const meta = (state.meta && typeof state.meta === "object")
1594
3347
  ? { ...state.meta }
@@ -1599,22 +3352,25 @@ export function startWebSocketSimulator(opts) {
1599
3352
  const flagIndex = existingFlags.findIndex((flag) => flag?.refId === body.refId);
1600
3353
  let nextFlags;
1601
3354
  let flagged = false;
3355
+ let flagEntry;
1602
3356
  if (flagIndex >= 0) {
3357
+ flagEntry = existingFlags[flagIndex];
1603
3358
  nextFlags = existingFlags.filter((_, idx) => idx !== flagIndex);
1604
3359
  flagged = false;
1605
3360
  }
1606
3361
  else {
1607
3362
  const now = new Date().toISOString();
3363
+ flagEntry = {
3364
+ id: randomId("flag"),
3365
+ refId: body.refId,
3366
+ runId: body.runId,
3367
+ turnIndex: body.turnIndex,
3368
+ reason: body.reason?.trim() || undefined,
3369
+ createdAt: now,
3370
+ };
1608
3371
  nextFlags = [
1609
3372
  ...existingFlags,
1610
- {
1611
- id: randomId("flag"),
1612
- refId: body.refId,
1613
- runId: body.runId,
1614
- turnIndex: body.turnIndex,
1615
- reason: body.reason?.trim() || undefined,
1616
- createdAt: now,
1617
- },
3373
+ flagEntry,
1618
3374
  ];
1619
3375
  flagged = true;
1620
3376
  }
@@ -1625,14 +3381,25 @@ export function startWebSocketSimulator(opts) {
1625
3381
  gradingFlags: nextFlags,
1626
3382
  },
1627
3383
  });
1628
- const sessionMeta = buildSessionMeta(body.sessionId, updated);
3384
+ appendGradingLog(updated, {
3385
+ type: "grading.flag",
3386
+ flagged,
3387
+ flag: flagEntry,
3388
+ refId: body.refId,
3389
+ });
3390
+ const sessionMeta = buildSessionMeta(workspaceId, updated);
3391
+ appendDurableStreamEvent(WORKSPACE_STREAM_ID, {
3392
+ type: "calibrateSession",
3393
+ workspaceId,
3394
+ session: sessionMeta,
3395
+ });
1629
3396
  appendDurableStreamEvent(GRADE_STREAM_ID, {
1630
3397
  type: "calibrateSession",
1631
- sessionId: body.sessionId,
3398
+ workspaceId,
1632
3399
  session: sessionMeta,
1633
3400
  });
1634
3401
  return new Response(JSON.stringify({
1635
- sessionId: body.sessionId,
3402
+ workspaceId,
1636
3403
  flagged,
1637
3404
  flags: nextFlags,
1638
3405
  }), { headers: { "content-type": "application/json" } });
@@ -1649,12 +3416,14 @@ export function startWebSocketSimulator(opts) {
1649
3416
  }
1650
3417
  try {
1651
3418
  const body = await req.json();
1652
- if (!body.sessionId || !body.refId) {
1653
- throw new Error("Missing sessionId or refId");
3419
+ const workspaceId = getWorkspaceIdFromBody(body);
3420
+ if (!workspaceId || !body.refId) {
3421
+ throw new Error("Missing workspaceId or refId");
1654
3422
  }
1655
- const state = readSessionState(body.sessionId);
3423
+ await logWorkspaceBotRoot("/api/calibrate/flag/reason", workspaceId);
3424
+ const state = readSessionState(workspaceId);
1656
3425
  if (!state) {
1657
- throw new Error("Session not found");
3426
+ throw new Error("Workspace not found");
1658
3427
  }
1659
3428
  const meta = (state.meta && typeof state.meta === "object")
1660
3429
  ? { ...state.meta }
@@ -1678,104 +3447,25 @@ export function startWebSocketSimulator(opts) {
1678
3447
  gradingFlags: nextFlags,
1679
3448
  },
1680
3449
  });
1681
- const sessionMeta = buildSessionMeta(body.sessionId, updated);
1682
- appendDurableStreamEvent(GRADE_STREAM_ID, {
3450
+ appendGradingLog(updated, {
3451
+ type: "grading.flag.reason",
3452
+ flag: updatedFlag,
3453
+ refId: body.refId,
3454
+ });
3455
+ const sessionMeta = buildSessionMeta(workspaceId, updated);
3456
+ appendDurableStreamEvent(WORKSPACE_STREAM_ID, {
1683
3457
  type: "calibrateSession",
1684
- sessionId: body.sessionId,
3458
+ workspaceId,
1685
3459
  session: sessionMeta,
1686
3460
  });
1687
- return new Response(JSON.stringify({
1688
- sessionId: body.sessionId,
1689
- flags: nextFlags,
1690
- }), { headers: { "content-type": "application/json" } });
1691
- }
1692
- catch (err) {
1693
- return new Response(JSON.stringify({
1694
- error: err instanceof Error ? err.message : String(err),
1695
- }), { status: 400, headers: { "content-type": "application/json" } });
1696
- }
1697
- }
1698
- if (url.pathname === "/api/grading/reference") {
1699
- if (req.method !== "POST") {
1700
- return new Response("Method not allowed", { status: 405 });
1701
- }
1702
- try {
1703
- const body = await req.json();
1704
- if (!body.sessionId)
1705
- throw new Error("Missing sessionId");
1706
- if (!body.runId)
1707
- throw new Error("Missing runId");
1708
- if (!body.referenceSample) {
1709
- throw new Error("Missing referenceSample");
1710
- }
1711
- const score = body.referenceSample.score;
1712
- if (typeof score !== "number" || Number.isNaN(score)) {
1713
- throw new Error("Invalid reference score");
1714
- }
1715
- const reason = body.referenceSample.reason;
1716
- if (typeof reason !== "string" || reason.trim().length === 0) {
1717
- throw new Error("Missing reference reason");
1718
- }
1719
- const evidence = Array.isArray(body.referenceSample.evidence)
1720
- ? body.referenceSample.evidence.filter((e) => typeof e === "string" && e.trim().length > 0)
1721
- : undefined;
1722
- const state = readSessionState(body.sessionId);
1723
- if (!state)
1724
- throw new Error("Session not found");
1725
- const previousRuns = Array.isArray(state.meta?.gradingRuns)
1726
- ? (state.meta
1727
- .gradingRuns)
1728
- : Array.isArray(state.meta?.calibrationRuns)
1729
- ? state.meta?.calibrationRuns
1730
- : [];
1731
- const index = previousRuns.findIndex((run) => run.id === body.runId);
1732
- if (index < 0)
1733
- throw new Error("Run not found");
1734
- const run = previousRuns[index];
1735
- const nextRun = {
1736
- ...run,
1737
- };
1738
- if (typeof body.turnIndex === "number") {
1739
- const result = run.result;
1740
- const turnIndex = body.turnIndex;
1741
- if (!result || typeof result !== "object" ||
1742
- result.mode !== "turns" ||
1743
- !Array.isArray(result.turns)) {
1744
- throw new Error("Run does not support turn references");
1745
- }
1746
- const turns = result.turns.map((turn) => ({ ...turn }));
1747
- const targetIndex = turns.findIndex((turn) => turn.index === turnIndex);
1748
- if (targetIndex < 0) {
1749
- throw new Error("Turn not found");
1750
- }
1751
- turns[targetIndex] = {
1752
- ...turns[targetIndex],
1753
- referenceSample: { score, reason, evidence },
1754
- };
1755
- nextRun.result = { ...result, turns };
1756
- }
1757
- else {
1758
- nextRun.referenceSample = { score, reason, evidence };
1759
- }
1760
- const nextRuns = previousRuns.map((entry, i) => i === index ? nextRun : entry);
1761
- const nextState = persistSessionState({
1762
- ...state,
1763
- meta: {
1764
- ...(state.meta ?? {}),
1765
- gradingRuns: nextRuns,
1766
- },
1767
- });
1768
- const sessionMeta = buildSessionMeta(body.sessionId, nextState);
1769
3461
  appendDurableStreamEvent(GRADE_STREAM_ID, {
1770
3462
  type: "calibrateSession",
1771
- sessionId: body.sessionId,
1772
- run: nextRun,
3463
+ workspaceId,
1773
3464
  session: sessionMeta,
1774
3465
  });
1775
3466
  return new Response(JSON.stringify({
1776
- sessionId: body.sessionId,
1777
- run: nextRun,
1778
- session: sessionMeta,
3467
+ workspaceId,
3468
+ flags: nextFlags,
1779
3469
  }), { headers: { "content-type": "application/json" } });
1780
3470
  }
1781
3471
  catch (err) {
@@ -1784,8 +3474,28 @@ export function startWebSocketSimulator(opts) {
1784
3474
  }), { status: 400, headers: { "content-type": "application/json" } });
1785
3475
  }
1786
3476
  }
3477
+ const gradingReferenceResponse = await handleGradingReferenceRoute({
3478
+ url,
3479
+ req,
3480
+ getWorkspaceIdFromBody,
3481
+ logWorkspaceBotRoot,
3482
+ readSessionState,
3483
+ persistSessionState,
3484
+ appendGradingLog,
3485
+ buildSessionMeta,
3486
+ appendDurableStreamEvent,
3487
+ workspaceStreamId: WORKSPACE_STREAM_ID,
3488
+ gradeStreamId: GRADE_STREAM_ID,
3489
+ parseFiniteInteger,
3490
+ randomId,
3491
+ });
3492
+ if (gradingReferenceResponse)
3493
+ return gradingReferenceResponse;
1787
3494
  if (url.pathname === "/api/test") {
1788
3495
  if (req.method === "GET") {
3496
+ const workspaceId = getWorkspaceIdFromQuery(url);
3497
+ await logWorkspaceBotRoot("/api/test", workspaceId);
3498
+ await activateWorkspaceDeck(workspaceId);
1789
3499
  await deckLoadPromise.catch(() => null);
1790
3500
  const requestedDeck = url.searchParams.get("deckPath");
1791
3501
  const selection = requestedDeck
@@ -1793,7 +3503,7 @@ export function startWebSocketSimulator(opts) {
1793
3503
  : availableTestDecks[0];
1794
3504
  if (requestedDeck && !selection) {
1795
3505
  return new Response(JSON.stringify({
1796
- error: "Unknown test deck selection",
3506
+ error: "Unknown scenario deck selection",
1797
3507
  }), {
1798
3508
  status: 400,
1799
3509
  headers: { "content-type": "application/json" },
@@ -1837,6 +3547,7 @@ export function startWebSocketSimulator(opts) {
1837
3547
  let inheritBotInput = false;
1838
3548
  let userProvidedDeckInput = false;
1839
3549
  let initFillRequestMissing = undefined;
3550
+ let sessionId = undefined;
1840
3551
  try {
1841
3552
  const body = await req.json();
1842
3553
  if (typeof body.maxTurns === "number" && Number.isFinite(body.maxTurns)) {
@@ -1856,10 +3567,11 @@ export function startWebSocketSimulator(opts) {
1856
3567
  if (body.initFill && Array.isArray(body.initFill.missing)) {
1857
3568
  initFillRequestMissing = body.initFill.missing.filter((entry) => typeof entry === "string" && entry.trim().length > 0);
1858
3569
  }
3570
+ sessionId = getWorkspaceIdFromBody(body);
1859
3571
  if (typeof body.botDeckPath === "string") {
1860
3572
  const resolved = resolveTestDeck(body.botDeckPath);
1861
3573
  if (!resolved) {
1862
- return new Response(JSON.stringify({ error: "Unknown test deck selection" }), {
3574
+ return new Response(JSON.stringify({ error: "Unknown scenario deck selection" }), {
1863
3575
  status: 400,
1864
3576
  headers: { "content-type": "application/json" },
1865
3577
  });
@@ -1877,6 +3589,10 @@ export function startWebSocketSimulator(opts) {
1877
3589
  catch {
1878
3590
  // ignore parse errors; use defaults
1879
3591
  }
3592
+ if (sessionId) {
3593
+ await logWorkspaceBotRoot("/api/test/run", sessionId);
3594
+ await activateWorkspaceDeck(sessionId);
3595
+ }
1880
3596
  if (deckInput === undefined) {
1881
3597
  try {
1882
3598
  const desc = await schemaPromise;
@@ -1892,7 +3608,7 @@ export function startWebSocketSimulator(opts) {
1892
3608
  deckInput = cloneValue(botInput);
1893
3609
  }
1894
3610
  if (!botDeckSelection) {
1895
- return new Response(JSON.stringify({ error: "No test decks configured" }), { status: 400, headers: { "content-type": "application/json" } });
3611
+ return new Response(JSON.stringify({ error: "No scenario decks configured" }), { status: 400, headers: { "content-type": "application/json" } });
1896
3612
  }
1897
3613
  let initFillInfo;
1898
3614
  let initFillTrace;
@@ -1934,12 +3650,14 @@ export function startWebSocketSimulator(opts) {
1934
3650
  error: parsed.error,
1935
3651
  initFill: initFillInfo,
1936
3652
  botDeckPath: botDeckSelection.path,
3653
+ botDeckId: botDeckSelection.id,
3654
+ botDeckLabel: botDeckSelection.label,
1937
3655
  });
1938
3656
  return new Response(JSON.stringify({
1939
3657
  error: parsed.error,
1940
3658
  initFill: initFillInfo,
1941
- sessionId: failure.sessionId,
1942
- sessionPath: failure.sessionPath,
3659
+ workspaceId: failure.workspaceId,
3660
+ workspacePath: failure.workspacePath,
1943
3661
  }), {
1944
3662
  status: 400,
1945
3663
  headers: { "content-type": "application/json" },
@@ -1996,12 +3714,14 @@ export function startWebSocketSimulator(opts) {
1996
3714
  error: message,
1997
3715
  initFill: initFillInfo,
1998
3716
  botDeckPath: botDeckSelection.path,
3717
+ botDeckId: botDeckSelection.id,
3718
+ botDeckLabel: botDeckSelection.label,
1999
3719
  });
2000
3720
  return new Response(JSON.stringify({
2001
3721
  error: message,
2002
3722
  initFill: initFillInfo,
2003
- sessionId: failure.sessionId,
2004
- sessionPath: failure.sessionPath,
3723
+ workspaceId: failure.workspaceId,
3724
+ workspacePath: failure.workspacePath,
2005
3725
  }), {
2006
3726
  status: 400,
2007
3727
  headers: { "content-type": "application/json" },
@@ -2037,126 +3757,896 @@ export function startWebSocketSimulator(opts) {
2037
3757
  error: message,
2038
3758
  initFill: initFillInfo,
2039
3759
  botDeckPath: botDeckSelection.path,
3760
+ botDeckId: botDeckSelection.id,
3761
+ botDeckLabel: botDeckSelection.label,
2040
3762
  });
2041
3763
  return new Response(JSON.stringify({
2042
3764
  error: message,
2043
3765
  initFill: initFillInfo,
2044
- sessionId: failure.sessionId,
2045
- sessionPath: failure.sessionPath,
3766
+ workspaceId: failure.workspaceId,
3767
+ workspacePath: failure.workspacePath,
2046
3768
  }), { status: 400, headers: { "content-type": "application/json" } });
2047
3769
  }
3770
+ const existingSessionState = sessionId
3771
+ ? readSessionState(sessionId)
3772
+ : undefined;
3773
+ const workspaceRecord = sessionId
3774
+ ? resolveWorkspaceRecord(sessionId) ?? {
3775
+ id: sessionId,
3776
+ rootDir: path.dirname(resolvedDeckPath),
3777
+ rootDeckPath: resolvedDeckPath,
3778
+ createdAt: new Date().toISOString(),
3779
+ }
3780
+ : undefined;
3781
+ if (workspaceRecord && !resolveWorkspaceRecord(sessionId)) {
3782
+ registerWorkspace(workspaceRecord);
3783
+ }
2048
3784
  const run = startTestBotRun({
2049
3785
  maxTurnsOverride,
2050
3786
  deckInput,
2051
3787
  botInput,
2052
3788
  initialUserMessage,
2053
3789
  botDeckPath: botDeckSelection.path,
3790
+ botDeckId: botDeckSelection.id,
3791
+ botDeckLabel: botDeckSelection.label,
2054
3792
  initFill: initFillInfo,
2055
3793
  initFillTrace,
3794
+ workspaceId: sessionId,
3795
+ workspaceRecord,
3796
+ baseMeta: existingSessionState?.meta ??
3797
+ undefined,
2056
3798
  });
2057
3799
  return new Response(JSON.stringify({ run }), { headers: { "content-type": "application/json" } });
2058
3800
  }
2059
- if (url.pathname === "/api/test/status") {
2060
- const runId = url.searchParams.get("runId") ?? undefined;
2061
- const sessionId = url.searchParams.get("sessionId") ?? undefined;
2062
- let entry = runId ? testBotRuns.get(runId) : undefined;
2063
- if (!entry && sessionId) {
2064
- for (const candidate of testBotRuns.values()) {
2065
- if (candidate.run.sessionId === sessionId) {
2066
- entry = candidate;
2067
- break;
3801
+ if (url.pathname === "/api/test/message") {
3802
+ if (req.method !== "POST") {
3803
+ return new Response("Method not allowed", { status: 405 });
3804
+ }
3805
+ // 1) Parse request payload and stitch together run/session state.
3806
+ let payload = {};
3807
+ try {
3808
+ payload = await req.json();
3809
+ }
3810
+ catch {
3811
+ // ignore parse errors
3812
+ }
3813
+ const requestedRunId = typeof payload.runId === "string"
3814
+ ? payload.runId
3815
+ : undefined;
3816
+ let runId = requestedRunId;
3817
+ const workspaceId = (() => {
3818
+ const workspaceId = typeof payload.workspaceId === "string" &&
3819
+ payload.workspaceId.trim().length > 0
3820
+ ? payload.workspaceId
3821
+ : undefined;
3822
+ if (workspaceId)
3823
+ return workspaceId;
3824
+ return undefined;
3825
+ })();
3826
+ await logWorkspaceBotRoot("/api/test/message", workspaceId);
3827
+ if (workspaceId) {
3828
+ await activateWorkspaceDeck(workspaceId);
3829
+ }
3830
+ let savedState = workspaceId
3831
+ ? readSessionState(workspaceId, { withTraces: true })
3832
+ : undefined;
3833
+ if (savedState && requestedRunId) {
3834
+ const savedRunId = typeof savedState.meta?.testBotRunId === "string"
3835
+ ? savedState.meta.testBotRunId
3836
+ : savedState.runId;
3837
+ if (!savedRunId || savedRunId !== requestedRunId) {
3838
+ // Explicit runId in the same workspace means "start a fresh run".
3839
+ savedState = undefined;
3840
+ }
3841
+ }
3842
+ if (!savedState && runId) {
3843
+ const entry = testBotRuns.get(runId);
3844
+ const runWorkspaceId = entry?.run.workspaceId ?? entry?.run.sessionId;
3845
+ if (runWorkspaceId &&
3846
+ (!workspaceId || runWorkspaceId === workspaceId)) {
3847
+ savedState = readSessionState(runWorkspaceId, {
3848
+ withTraces: true,
3849
+ });
3850
+ }
3851
+ }
3852
+ if (savedState && !runId) {
3853
+ runId = typeof savedState.meta?.testBotRunId === "string"
3854
+ ? savedState.meta.testBotRunId
3855
+ : savedState.runId;
3856
+ }
3857
+ runId = runId ?? randomId("testbot");
3858
+ const workspaceRecord = workspaceId
3859
+ ? resolveWorkspaceRecord(workspaceId) ?? {
3860
+ id: workspaceId,
3861
+ rootDir: path.dirname(resolvedDeckPath),
3862
+ rootDeckPath: resolvedDeckPath,
3863
+ createdAt: new Date().toISOString(),
3864
+ }
3865
+ : undefined;
3866
+ if (workspaceRecord && !resolveWorkspaceRecord(workspaceId)) {
3867
+ registerWorkspace(workspaceRecord);
3868
+ }
3869
+ const workspaceMeta = workspaceRecord
3870
+ ? buildWorkspaceMeta(workspaceRecord, savedState?.meta ?? {})
3871
+ : (savedState?.meta ?? {});
3872
+ const existingEntry = testBotRuns.get(runId);
3873
+ if (existingEntry?.promise) {
3874
+ return new Response(JSON.stringify({ error: "Scenario run already in progress" }), { status: 409, headers: { "content-type": "application/json" } });
3875
+ }
3876
+ // 2) Resolve which scenario deck to use and derive initial input.
3877
+ await deckLoadPromise.catch(() => null);
3878
+ const requestedDeck = typeof payload.botDeckPath === "string"
3879
+ ? payload.botDeckPath
3880
+ : undefined;
3881
+ const selection = (() => {
3882
+ if (requestedDeck)
3883
+ return resolveTestDeck(requestedDeck);
3884
+ const metaPath = typeof savedState?.meta?.testBotConfigPath === "string"
3885
+ ? savedState.meta.testBotConfigPath
3886
+ : undefined;
3887
+ if (metaPath)
3888
+ return resolveTestDeck(metaPath);
3889
+ return availableTestDecks[0];
3890
+ })();
3891
+ if (requestedDeck && !selection) {
3892
+ return new Response(JSON.stringify({ error: "Unknown scenario deck selection" }), { status: 400, headers: { "content-type": "application/json" } });
3893
+ }
3894
+ const botConfigPath = selection?.path ?? resolvedDeckPath;
3895
+ const testBotName = selection
3896
+ ? path.basename(botConfigPath).replace(/\.deck\.(md|ts)$/i, "")
3897
+ : toDeckLabel(resolvedDeckPath);
3898
+ const selectedScenarioDeckId = selection?.id ?? testBotName;
3899
+ const selectedScenarioDeckLabel = selection?.label ?? testBotName;
3900
+ const message = typeof payload.message === "string"
3901
+ ? payload.message.trim()
3902
+ : "";
3903
+ const hasSavedMessages = (savedState?.messages?.length ?? 0) > 0;
3904
+ let deckInput = payload.context ?? payload.init;
3905
+ if (!hasSavedMessages && deckInput === undefined) {
3906
+ try {
3907
+ const desc = await schemaPromise;
3908
+ deckInput = desc.defaults !== undefined
3909
+ ? desc.defaults
3910
+ : deriveInitialFromSchema(desc.schema);
3911
+ }
3912
+ catch {
3913
+ // ignore; keep undefined
3914
+ }
3915
+ }
3916
+ const stream = typeof payload.stream === "boolean"
3917
+ ? payload.stream
3918
+ : true;
3919
+ const deckForStart = await deckLoadPromise.catch(() => null);
3920
+ const startMode = deckForStart &&
3921
+ (deckForStart.startMode === "assistant" ||
3922
+ deckForStart.startMode === "user")
3923
+ ? deckForStart.startMode
3924
+ : "assistant";
3925
+ const startOnly = !message && startMode === "assistant" &&
3926
+ !hasSavedMessages;
3927
+ if (!message && !startOnly) {
3928
+ return new Response(JSON.stringify({ error: "Missing message" }), { status: 400, headers: { "content-type": "application/json" } });
3929
+ }
3930
+ // 3) Initialize the run, sync from prior session state, and prep tracing.
3931
+ const entry = existingEntry ?? {
3932
+ run: {
3933
+ id: runId,
3934
+ status: "idle",
3935
+ messages: [],
3936
+ traces: [],
3937
+ toolInserts: [],
3938
+ },
3939
+ promise: null,
3940
+ abort: null,
3941
+ };
3942
+ testBotRuns.set(runId, entry);
3943
+ const run = entry.run;
3944
+ const emitTestBot = (payload) => broadcastTestBot(payload, run.workspaceId ?? workspaceId ?? runId);
3945
+ run.status = "running";
3946
+ run.error = undefined;
3947
+ run.startedAt = run.startedAt ?? new Date().toISOString();
3948
+ if (savedState) {
3949
+ syncTestBotRunFromState(run, savedState);
3950
+ }
3951
+ emitTestBot({ type: "testBotStatus", run });
3952
+ const controller = new AbortController();
3953
+ entry.abort = controller;
3954
+ const isAborted = () => controller.signal.aborted;
3955
+ const capturedTraces = Array.isArray(savedState?.traces)
3956
+ ? cloneTraces(savedState.traces)
3957
+ : [];
3958
+ const pendingTraceEvents = [];
3959
+ const flushPendingTraceEvents = (state) => {
3960
+ if (!pendingTraceEvents.length)
3961
+ return;
3962
+ for (const pending of pendingTraceEvents) {
3963
+ appendSessionEvent(state, {
3964
+ ...pending,
3965
+ kind: "trace",
3966
+ category: traceCategory(pending.type),
3967
+ });
3968
+ }
3969
+ pendingTraceEvents.length = 0;
3970
+ };
3971
+ const tracer = (event) => {
3972
+ const stamped = event.ts ? event : { ...event, ts: Date.now() };
3973
+ capturedTraces.push(stamped);
3974
+ consoleTracer?.(stamped);
3975
+ if (savedState?.meta?.sessionId) {
3976
+ appendSessionEvent(savedState, {
3977
+ ...stamped,
3978
+ kind: "trace",
3979
+ category: traceCategory(stamped.type),
3980
+ });
3981
+ }
3982
+ else {
3983
+ pendingTraceEvents.push(stamped);
3984
+ }
3985
+ };
3986
+ const appendFromState = (state) => {
3987
+ const snapshot = buildTestBotSnapshot(state);
3988
+ run.messages = snapshot.messages;
3989
+ run.toolInserts = snapshot.toolInserts;
3990
+ run.traces = Array.isArray(state.traces)
3991
+ ? [...state.traces]
3992
+ : undefined;
3993
+ const nextWorkspaceId = typeof state.meta?.workspaceId === "string"
3994
+ ? state.meta.workspaceId
3995
+ : typeof state.meta?.sessionId === "string"
3996
+ ? state.meta.sessionId
3997
+ : undefined;
3998
+ if (nextWorkspaceId) {
3999
+ run.workspaceId = nextWorkspaceId;
4000
+ run.sessionId = nextWorkspaceId;
4001
+ }
4002
+ emitTestBot({ type: "testBotStatus", run });
4003
+ };
4004
+ // 4) Execute the deck run(s): optional assistant start, then user message.
4005
+ entry.promise = (async () => {
4006
+ try {
4007
+ const countAssistantMessages = (state) => {
4008
+ if (!state?.messages?.length)
4009
+ return 0;
4010
+ let count = 0;
4011
+ for (const msg of state.messages) {
4012
+ if (msg?.role === "assistant")
4013
+ count += 1;
4014
+ }
4015
+ return count;
4016
+ };
4017
+ const runOnce = async (initialUserMessage, turn, shouldStream = stream) => {
4018
+ if (isAborted())
4019
+ return undefined;
4020
+ const hasSavedMessages = (savedState?.messages?.length ?? 0) > 0;
4021
+ const inputProvided = !hasSavedMessages &&
4022
+ deckInput !== undefined;
4023
+ const input = inputProvided ? deckInput : undefined;
4024
+ const result = await runDeck({
4025
+ path: resolvedDeckPath,
4026
+ input,
4027
+ inputProvided,
4028
+ modelProvider: opts.modelProvider,
4029
+ isRoot: true,
4030
+ allowRootStringInput: true,
4031
+ defaultModel: typeof payload.model === "string"
4032
+ ? payload.model
4033
+ : opts.model,
4034
+ modelOverride: typeof payload.modelForce === "string"
4035
+ ? payload.modelForce
4036
+ : opts.modelForce,
4037
+ trace: tracer,
4038
+ stream: shouldStream,
4039
+ state: savedState,
4040
+ responsesMode: opts.responsesMode,
4041
+ signal: controller.signal,
4042
+ initialUserMessage,
4043
+ onStateUpdate: (state) => {
4044
+ if (isAborted())
4045
+ return;
4046
+ const nextStateWithSource = applyUserMessageRefSource(savedState, state, "manual");
4047
+ const nextMeta = {
4048
+ ...workspaceMeta,
4049
+ ...(nextStateWithSource.meta ?? {}),
4050
+ testBot: true,
4051
+ testBotRunId: runId,
4052
+ testBotConfigPath: botConfigPath,
4053
+ testBotName,
4054
+ scenarioRunId: runId,
4055
+ selectedScenarioDeckId,
4056
+ selectedScenarioDeckLabel,
4057
+ scenarioConfigPath: botConfigPath,
4058
+ ...(workspaceId ? { workspaceId } : {}),
4059
+ };
4060
+ const enriched = persistSessionState({
4061
+ ...nextStateWithSource,
4062
+ meta: nextMeta,
4063
+ traces: capturedTraces,
4064
+ });
4065
+ savedState = enriched;
4066
+ flushPendingTraceEvents(enriched);
4067
+ appendFromState(enriched);
4068
+ },
4069
+ onStreamText: (chunk) => emitTestBot({
4070
+ type: "testBotStream",
4071
+ runId,
4072
+ role: "assistant",
4073
+ chunk,
4074
+ turn,
4075
+ ts: Date.now(),
4076
+ }),
4077
+ });
4078
+ if (isAborted())
4079
+ return result;
4080
+ if (shouldStream) {
4081
+ emitTestBot({
4082
+ type: "testBotStreamEnd",
4083
+ runId,
4084
+ role: "assistant",
4085
+ turn,
4086
+ ts: Date.now(),
4087
+ });
4088
+ }
4089
+ return result;
4090
+ };
4091
+ let assistantTurn = countAssistantMessages(savedState);
4092
+ if (startMode === "assistant" &&
4093
+ !hasSavedMessages) {
4094
+ if (isAborted()) {
4095
+ run.status = "canceled";
4096
+ return;
4097
+ }
4098
+ await runOnce(undefined, assistantTurn, stream);
4099
+ assistantTurn += 1;
4100
+ }
4101
+ let result = undefined;
4102
+ if (message) {
4103
+ if (isAborted()) {
4104
+ run.status = "canceled";
4105
+ return;
4106
+ }
4107
+ result = await runOnce(message, assistantTurn, stream);
2068
4108
  }
4109
+ if (isAborted()) {
4110
+ run.status = "canceled";
4111
+ }
4112
+ else if (result !== undefined && isGambitEndSignal(result)) {
4113
+ run.status = "completed";
4114
+ }
4115
+ else {
4116
+ run.status = "completed";
4117
+ }
4118
+ }
4119
+ catch (err) {
4120
+ if (isAborted() || isRunCanceledError(err)) {
4121
+ run.status = "canceled";
4122
+ run.error = undefined;
4123
+ }
4124
+ else {
4125
+ run.status = "error";
4126
+ run.error = err instanceof Error ? err.message : String(err);
4127
+ logger.warn(`[sim] build bot run failed (workspaceId=${workspaceId}): ${run.error}`);
4128
+ }
4129
+ }
4130
+ finally {
4131
+ if (savedState) {
4132
+ syncTestBotRunFromState(run, savedState);
4133
+ }
4134
+ run.finishedAt = new Date().toISOString();
4135
+ entry.abort = null;
4136
+ entry.promise = null;
4137
+ emitTestBot({ type: "testBotStatus", run });
4138
+ }
4139
+ })();
4140
+ // 5) Return the current run snapshot to the caller.
4141
+ return new Response(JSON.stringify({ run }), { headers: { "content-type": "application/json" } });
4142
+ }
4143
+ if (url.pathname === "/api/test/stop") {
4144
+ if (req.method !== "POST") {
4145
+ return new Response("Method not allowed", { status: 405 });
4146
+ }
4147
+ let runId = undefined;
4148
+ try {
4149
+ const body = await req.json();
4150
+ if (typeof body.runId === "string")
4151
+ runId = body.runId;
4152
+ }
4153
+ catch {
4154
+ // ignore
4155
+ }
4156
+ const entry = runId ? testBotRuns.get(runId) : undefined;
4157
+ const wasRunning = Boolean(entry?.promise);
4158
+ if (entry?.abort) {
4159
+ entry.abort.abort();
4160
+ }
4161
+ if (entry?.run?.status === "running") {
4162
+ entry.run.status = "canceled";
4163
+ entry.run.finishedAt = entry.run.finishedAt ??
4164
+ new Date().toISOString();
4165
+ }
4166
+ return new Response(JSON.stringify({
4167
+ stopped: wasRunning,
4168
+ run: entry?.run ?? {
4169
+ id: runId ?? "",
4170
+ status: "idle",
4171
+ messages: [],
4172
+ traces: [],
4173
+ toolInserts: [],
4174
+ },
4175
+ }), { headers: { "content-type": "application/json" } });
4176
+ }
4177
+ if (url.pathname === "/api/build/reset") {
4178
+ if (req.method !== "POST") {
4179
+ return new Response("Method not allowed", { status: 405 });
4180
+ }
4181
+ let workspaceId = undefined;
4182
+ try {
4183
+ const body = await req.json();
4184
+ workspaceId = getWorkspaceIdFromBody(body);
4185
+ }
4186
+ catch {
4187
+ // ignore
4188
+ }
4189
+ if (!workspaceId) {
4190
+ return new Response(JSON.stringify({ error: "Missing workspaceId" }), { status: 400, headers: { "content-type": "application/json" } });
4191
+ }
4192
+ const entry = buildBotRuns.get(workspaceId);
4193
+ if (entry?.abort) {
4194
+ entry.abort.abort();
4195
+ }
4196
+ if (entry?.run) {
4197
+ if (entry.run.status === "running") {
4198
+ entry.run.status = "canceled";
4199
+ }
4200
+ entry.run.finishedAt = entry.run.finishedAt ??
4201
+ new Date().toISOString();
4202
+ const state = readSessionState(workspaceId);
4203
+ if (state) {
4204
+ persistSessionState({
4205
+ ...state,
4206
+ meta: {
4207
+ ...(state.meta ?? {}),
4208
+ buildStatus: entry.run.status,
4209
+ buildFinishedAt: entry.run.finishedAt,
4210
+ buildError: entry.run.error,
4211
+ },
4212
+ });
4213
+ }
4214
+ }
4215
+ buildBotRuns.delete(workspaceId);
4216
+ broadcastBuildBot({
4217
+ type: "buildBotStatus",
4218
+ run: {
4219
+ id: workspaceId,
4220
+ status: "idle",
4221
+ messages: [],
4222
+ traces: [],
4223
+ toolInserts: [],
4224
+ },
4225
+ }, workspaceId);
4226
+ return new Response(JSON.stringify({ reset: true }), {
4227
+ headers: { "content-type": "application/json" },
4228
+ });
4229
+ }
4230
+ if (url.pathname === "/api/build/stop") {
4231
+ if (req.method !== "POST") {
4232
+ return new Response("Method not allowed", { status: 405 });
4233
+ }
4234
+ let workspaceId = undefined;
4235
+ try {
4236
+ const body = await req.json();
4237
+ workspaceId = getWorkspaceIdFromBody(body);
4238
+ }
4239
+ catch {
4240
+ // ignore
4241
+ }
4242
+ if (!workspaceId) {
4243
+ return new Response(JSON.stringify({ error: "Missing workspaceId" }), { status: 400, headers: { "content-type": "application/json" } });
4244
+ }
4245
+ const entry = buildBotRuns.get(workspaceId);
4246
+ const wasRunning = Boolean(entry?.promise);
4247
+ if (entry?.abort) {
4248
+ entry.abort.abort();
4249
+ }
4250
+ if (entry?.run?.status === "running") {
4251
+ entry.run.status = "canceled";
4252
+ entry.run.finishedAt = entry.run.finishedAt ??
4253
+ new Date().toISOString();
4254
+ }
4255
+ if (entry?.run) {
4256
+ const state = readSessionState(workspaceId);
4257
+ if (state) {
4258
+ persistSessionState({
4259
+ ...state,
4260
+ meta: {
4261
+ ...(state.meta ?? {}),
4262
+ buildStatus: entry.run.status,
4263
+ buildFinishedAt: entry.run.finishedAt,
4264
+ buildError: entry.run.error,
4265
+ },
4266
+ });
2069
4267
  }
2070
4268
  }
2071
4269
  const run = entry?.run ?? {
2072
- id: runId ?? "",
4270
+ id: workspaceId,
2073
4271
  status: "idle",
2074
4272
  messages: [],
2075
4273
  traces: [],
2076
4274
  toolInserts: [],
2077
- sessionId,
2078
4275
  };
2079
- if (!entry && sessionId) {
2080
- const state = readSessionState(sessionId);
4276
+ broadcastBuildBot({ type: "buildBotStatus", run, state: entry?.state ?? undefined }, workspaceId);
4277
+ return new Response(JSON.stringify({
4278
+ stopped: wasRunning,
4279
+ run,
4280
+ }), { headers: { "content-type": "application/json" } });
4281
+ }
4282
+ if (url.pathname === "/api/build/message") {
4283
+ if (req.method !== "POST") {
4284
+ return new Response("Method not allowed", { status: 405 });
4285
+ }
4286
+ let payload = {};
4287
+ try {
4288
+ payload = await req.json();
4289
+ }
4290
+ catch {
4291
+ // ignore
4292
+ }
4293
+ let workspaceId = typeof payload.workspaceId === "string"
4294
+ ? payload.workspaceId
4295
+ : typeof payload.runId === "string"
4296
+ ? payload.runId
4297
+ : undefined;
4298
+ if (!workspaceId) {
4299
+ const created = await createWorkspaceSession();
4300
+ workspaceId = created.id;
4301
+ }
4302
+ await logWorkspaceBotRoot("/api/build/message", workspaceId);
4303
+ const message = typeof payload.message === "string"
4304
+ ? payload.message
4305
+ : "";
4306
+ const workspaceRecord = resolveWorkspaceRecord(workspaceId) ?? {
4307
+ id: workspaceId,
4308
+ rootDir: path.dirname(resolvedDeckPath),
4309
+ rootDeckPath: resolvedDeckPath,
4310
+ createdAt: new Date().toISOString(),
4311
+ };
4312
+ if (!resolveWorkspaceRecord(workspaceId)) {
4313
+ registerWorkspace(workspaceRecord);
4314
+ }
4315
+ const existingEntry = buildBotRuns.get(workspaceId);
4316
+ if (existingEntry?.promise) {
4317
+ return new Response(JSON.stringify({ error: "Run already in progress" }), { status: 409, headers: { "content-type": "application/json" } });
4318
+ }
4319
+ const entry = existingEntry ?? {
4320
+ run: {
4321
+ id: workspaceId,
4322
+ status: "idle",
4323
+ messages: [],
4324
+ traces: [],
4325
+ toolInserts: [],
4326
+ },
4327
+ state: null,
4328
+ promise: null,
4329
+ abort: null,
4330
+ };
4331
+ buildBotRuns.set(workspaceId, entry);
4332
+ if (!entry.state) {
4333
+ const projection = readBuildState(workspaceId);
4334
+ if (projection?.state) {
4335
+ entry.state = projection.state;
4336
+ }
4337
+ }
4338
+ const run = entry.run;
4339
+ run.status = "running";
4340
+ run.error = undefined;
4341
+ run.startedAt = run.startedAt ?? new Date().toISOString();
4342
+ if (entry.state) {
4343
+ syncBuildBotRunFromState(run, entry.state);
4344
+ }
4345
+ broadcastBuildBot({
4346
+ type: "buildBotStatus",
4347
+ run,
4348
+ state: entry.state ?? undefined,
4349
+ }, workspaceId);
4350
+ const workspaceBaseState = readSessionState(workspaceId) ?? {
4351
+ runId: workspaceId,
4352
+ messages: [],
4353
+ meta: {},
4354
+ };
4355
+ persistSessionState({
4356
+ ...workspaceBaseState,
4357
+ meta: {
4358
+ ...buildWorkspaceMeta(workspaceRecord, workspaceBaseState.meta ?? {}),
4359
+ buildStatus: run.status,
4360
+ buildStartedAt: run.startedAt,
4361
+ },
4362
+ });
4363
+ const controller = new AbortController();
4364
+ entry.abort = controller;
4365
+ const isAborted = () => controller.signal.aborted;
4366
+ const botDeckUrl = new URL("./decks/gambit-bot/PROMPT.md", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url);
4367
+ if (botDeckUrl.protocol !== "file:") {
4368
+ run.status = "error";
4369
+ run.error = "Unable to resolve Gambit Bot deck path";
4370
+ broadcastBuildBot({ type: "buildBotStatus", run }, workspaceId);
4371
+ const state = readSessionState(workspaceId);
2081
4372
  if (state) {
2082
- run.id = typeof state.runId === "string" ? state.runId : run.id;
2083
- run.status = "completed";
2084
- syncTestBotRunFromState(run, state);
4373
+ persistSessionState({
4374
+ ...state,
4375
+ meta: {
4376
+ ...(state.meta ?? {}),
4377
+ buildStatus: "error",
4378
+ buildError: run.error,
4379
+ buildFinishedAt: new Date().toISOString(),
4380
+ },
4381
+ });
4382
+ }
4383
+ return new Response(JSON.stringify({ error: run.error }), { status: 500, headers: { "content-type": "application/json" } });
4384
+ }
4385
+ const botDeckPath = path.fromFileUrl(botDeckUrl);
4386
+ let botRoot;
4387
+ try {
4388
+ botRoot = await resolveBuildBotRoot(workspaceId);
4389
+ }
4390
+ catch (err) {
4391
+ const msg = err instanceof Error ? err.message : String(err);
4392
+ run.status = "error";
4393
+ run.error = msg;
4394
+ broadcastBuildBot({ type: "buildBotStatus", run }, workspaceId);
4395
+ const state = readSessionState(workspaceId);
4396
+ if (state) {
4397
+ persistSessionState({
4398
+ ...state,
4399
+ meta: {
4400
+ ...(state.meta ?? {}),
4401
+ buildStatus: "error",
4402
+ buildError: msg,
4403
+ buildFinishedAt: new Date().toISOString(),
4404
+ },
4405
+ });
2085
4406
  }
4407
+ return new Response(JSON.stringify({ error: msg }), { status: 400, headers: { "content-type": "application/json" } });
4408
+ }
4409
+ const prevBotRoot = dntShim.Deno.env.get("GAMBIT_BOT_ROOT");
4410
+ dntShim.Deno.env.set("GAMBIT_BOT_ROOT", botRoot);
4411
+ const capturedTraces = Array.isArray(entry.state?.traces)
4412
+ ? cloneTraces(entry.state.traces)
4413
+ : [];
4414
+ const tracer = (event) => {
4415
+ const stamped = event.ts ? event : { ...event, ts: Date.now() };
4416
+ capturedTraces.push(stamped);
4417
+ consoleTracer?.(stamped);
4418
+ broadcastBuildBot({
4419
+ type: "buildBotTrace",
4420
+ runId: workspaceId,
4421
+ event: stamped,
4422
+ }, workspaceId);
4423
+ };
4424
+ const appendFromState = (state) => {
4425
+ syncBuildBotRunFromState(run, state);
4426
+ run.traces = Array.isArray(state.traces) ? [...state.traces] : [];
4427
+ broadcastBuildBot({ type: "buildBotStatus", run, state }, workspaceId);
4428
+ const base = readSessionState(workspaceId) ?? {
4429
+ runId: workspaceId,
4430
+ messages: [],
4431
+ meta: {},
4432
+ };
4433
+ persistSessionState({
4434
+ ...base,
4435
+ meta: {
4436
+ ...buildWorkspaceMeta(workspaceRecord, base.meta ?? {}),
4437
+ buildStatus: run.status,
4438
+ buildStartedAt: run.startedAt,
4439
+ buildFinishedAt: run.finishedAt,
4440
+ buildError: run.error,
4441
+ },
4442
+ });
4443
+ };
4444
+ entry.promise = (async () => {
4445
+ try {
4446
+ const runOnce = async (initialUserMessage, turn, shouldStream = true) => {
4447
+ if (isAborted())
4448
+ return undefined;
4449
+ const result = await runDeck({
4450
+ path: botDeckPath,
4451
+ input: undefined,
4452
+ inputProvided: false,
4453
+ modelProvider: opts.modelProvider,
4454
+ allowRootStringInput: true,
4455
+ defaultModel: typeof payload.model === "string"
4456
+ ? payload.model
4457
+ : opts.model,
4458
+ modelOverride: typeof payload.modelForce === "string"
4459
+ ? payload.modelForce
4460
+ : opts.modelForce,
4461
+ trace: tracer,
4462
+ stream: shouldStream,
4463
+ state: entry.state ?? undefined,
4464
+ responsesMode: opts.responsesMode,
4465
+ signal: controller.signal,
4466
+ initialUserMessage,
4467
+ onStateUpdate: (state) => {
4468
+ if (isAborted())
4469
+ return;
4470
+ const nextState = {
4471
+ ...state,
4472
+ traces: capturedTraces,
4473
+ };
4474
+ entry.state = nextState;
4475
+ appendFromState(nextState);
4476
+ },
4477
+ onStreamText: (chunk) => broadcastBuildBot({
4478
+ type: "buildBotStream",
4479
+ runId: workspaceId,
4480
+ role: "assistant",
4481
+ chunk,
4482
+ turn,
4483
+ ts: Date.now(),
4484
+ }, workspaceId),
4485
+ });
4486
+ if (shouldStream) {
4487
+ broadcastBuildBot({
4488
+ type: "buildBotStreamEnd",
4489
+ runId: workspaceId,
4490
+ role: "assistant",
4491
+ turn,
4492
+ ts: Date.now(),
4493
+ }, workspaceId);
4494
+ }
4495
+ return result;
4496
+ };
4497
+ const hasSavedMessages = (entry.state?.messages?.length ?? 0) > 0;
4498
+ let assistantTurn = 0;
4499
+ if (Array.isArray(entry.state?.messages)) {
4500
+ for (const msg of entry.state.messages) {
4501
+ if (msg?.role === "assistant")
4502
+ assistantTurn += 1;
4503
+ }
4504
+ }
4505
+ if (!hasSavedMessages && message.trim().length === 0) {
4506
+ await runOnce(undefined, assistantTurn, true);
4507
+ }
4508
+ else {
4509
+ await runOnce(message, assistantTurn, true);
4510
+ }
4511
+ if (isAborted()) {
4512
+ run.status = "canceled";
4513
+ }
4514
+ else {
4515
+ run.status = "completed";
4516
+ }
4517
+ }
4518
+ catch (err) {
4519
+ if (isAborted() || isRunCanceledError(err)) {
4520
+ run.status = "canceled";
4521
+ run.error = undefined;
4522
+ }
4523
+ else {
4524
+ run.status = "error";
4525
+ run.error = err instanceof Error ? err.message : String(err);
4526
+ logger.warn(`[sim] build bot run failed (workspaceId=${workspaceId}): ${run.error}`);
4527
+ }
4528
+ }
4529
+ finally {
4530
+ run.finishedAt = new Date().toISOString();
4531
+ entry.abort = null;
4532
+ entry.promise = null;
4533
+ const base = readSessionState(workspaceId) ?? {
4534
+ runId: workspaceId,
4535
+ messages: [],
4536
+ meta: {},
4537
+ };
4538
+ persistSessionState({
4539
+ ...base,
4540
+ meta: {
4541
+ ...buildWorkspaceMeta(workspaceRecord, base.meta ?? {}),
4542
+ buildStatus: run.status,
4543
+ buildStartedAt: run.startedAt,
4544
+ buildFinishedAt: run.finishedAt,
4545
+ buildError: run.error,
4546
+ },
4547
+ });
4548
+ try {
4549
+ reloadPrimaryDeck();
4550
+ }
4551
+ catch (err) {
4552
+ logger.warn(`[sim] failed to reload primary deck after build: ${err instanceof Error ? err.message : String(err)}`);
4553
+ }
4554
+ broadcastBuildBot({ type: "buildBotStatus", run, state: entry.state ?? undefined }, workspaceId);
4555
+ if (prevBotRoot === undefined) {
4556
+ try {
4557
+ dntShim.Deno.env.delete("GAMBIT_BOT_ROOT");
4558
+ }
4559
+ catch {
4560
+ // ignore
4561
+ }
4562
+ }
4563
+ else {
4564
+ dntShim.Deno.env.set("GAMBIT_BOT_ROOT", prevBotRoot);
4565
+ }
4566
+ }
4567
+ })();
4568
+ return new Response(JSON.stringify({ run }), {
4569
+ headers: { "content-type": "application/json" },
4570
+ });
4571
+ }
4572
+ if (url.pathname === "/api/build/files") {
4573
+ if (req.method !== "GET") {
4574
+ return new Response("Method not allowed", { status: 405 });
2086
4575
  }
2087
- if (run.sessionId) {
2088
- const state = readSessionState(run.sessionId);
2089
- if (state) {
2090
- syncTestBotRunFromState(run, state);
2091
- }
4576
+ try {
4577
+ const workspaceId = getWorkspaceIdFromQuery(url);
4578
+ await logWorkspaceBotRoot("/api/build/files", workspaceId);
4579
+ const root = await resolveBuildBotRoot(workspaceId);
4580
+ const entries = await listBuildBotFiles(root);
4581
+ return new Response(JSON.stringify({ root, entries }), {
4582
+ headers: { "content-type": "application/json" },
4583
+ });
2092
4584
  }
2093
- await deckLoadPromise.catch(() => null);
2094
- const requestedDeck = url.searchParams.get("deckPath");
2095
- const selection = requestedDeck
2096
- ? resolveTestDeck(requestedDeck)
2097
- : availableTestDecks[0];
2098
- if (requestedDeck && !selection) {
2099
- return new Response(JSON.stringify({
2100
- error: "Unknown test deck selection",
2101
- }), {
4585
+ catch (err) {
4586
+ const message = err instanceof Error ? err.message : String(err);
4587
+ return new Response(JSON.stringify({ error: message }), {
2102
4588
  status: 400,
2103
4589
  headers: { "content-type": "application/json" },
2104
4590
  });
2105
4591
  }
2106
- if (selection) {
2107
- const schemaDesc = await describeDeckInputSchemaFromPath(selection.path);
2108
- return new Response(JSON.stringify({
2109
- run,
2110
- botPath: selection.path,
2111
- botLabel: selection.label,
2112
- botDescription: selection.description,
2113
- selectedDeckId: selection.id,
2114
- inputSchema: schemaDesc.schema,
2115
- inputSchemaError: schemaDesc.error,
2116
- defaults: { input: schemaDesc.defaults },
2117
- testDecks: availableTestDecks,
2118
- }), { headers: { "content-type": "application/json" } });
2119
- }
2120
- return new Response(JSON.stringify({
2121
- run,
2122
- botPath: null,
2123
- botLabel: null,
2124
- botDescription: null,
2125
- selectedDeckId: null,
2126
- inputSchema: null,
2127
- inputSchemaError: null,
2128
- defaults: {},
2129
- testDecks: availableTestDecks,
2130
- }), { headers: { "content-type": "application/json" } });
2131
4592
  }
2132
- if (url.pathname === "/api/test/stop") {
2133
- if (req.method !== "POST") {
4593
+ if (url.pathname === "/api/build/file") {
4594
+ if (req.method !== "GET") {
2134
4595
  return new Response("Method not allowed", { status: 405 });
2135
4596
  }
2136
- let runId = undefined;
2137
- try {
2138
- const body = await req.json();
2139
- if (typeof body.runId === "string")
2140
- runId = body.runId;
4597
+ const workspaceId = getWorkspaceIdFromQuery(url);
4598
+ await logWorkspaceBotRoot("/api/build/file", workspaceId);
4599
+ const inputPath = url.searchParams.get("path") ?? "";
4600
+ if (!inputPath) {
4601
+ appendServerErrorLog(workspaceId, {
4602
+ endpoint: "/api/build/file",
4603
+ status: 400,
4604
+ message: "Missing path",
4605
+ method: req.method,
4606
+ });
4607
+ return new Response(JSON.stringify({ error: "Missing path" }), {
4608
+ status: 400,
4609
+ headers: { "content-type": "application/json" },
4610
+ });
2141
4611
  }
2142
- catch {
2143
- // ignore
4612
+ try {
4613
+ const root = await resolveBuildBotRoot(workspaceId);
4614
+ const resolved = await resolveBuildBotPath(root, inputPath);
4615
+ if (!resolved.stat.isFile) {
4616
+ return new Response(JSON.stringify({ error: "Path is not a file" }), {
4617
+ status: 400,
4618
+ headers: { "content-type": "application/json" },
4619
+ });
4620
+ }
4621
+ if (resolved.stat.size > MAX_FILE_PREVIEW_BYTES) {
4622
+ return new Response(JSON.stringify({
4623
+ path: resolved.relativePath,
4624
+ tooLarge: true,
4625
+ size: resolved.stat.size,
4626
+ }), { headers: { "content-type": "application/json" } });
4627
+ }
4628
+ const bytes = await dntShim.Deno.readFile(resolved.fullPath);
4629
+ const text = readPreviewText(bytes);
4630
+ if (text === null) {
4631
+ return new Response(JSON.stringify({
4632
+ path: resolved.relativePath,
4633
+ binary: true,
4634
+ size: resolved.stat.size,
4635
+ }), { headers: { "content-type": "application/json" } });
4636
+ }
4637
+ return new Response(JSON.stringify({
4638
+ path: resolved.relativePath,
4639
+ contents: text,
4640
+ size: resolved.stat.size,
4641
+ }), { headers: { "content-type": "application/json" } });
2144
4642
  }
2145
- const entry = runId ? testBotRuns.get(runId) : undefined;
2146
- const wasRunning = Boolean(entry?.promise);
2147
- if (entry?.abort) {
2148
- entry.abort.abort();
4643
+ catch (err) {
4644
+ const message = err instanceof Error ? err.message : String(err);
4645
+ return new Response(JSON.stringify({ error: message }), {
4646
+ status: 400,
4647
+ headers: { "content-type": "application/json" },
4648
+ });
2149
4649
  }
2150
- return new Response(JSON.stringify({
2151
- stopped: wasRunning,
2152
- run: entry?.run ?? {
2153
- id: runId ?? "",
2154
- status: "idle",
2155
- messages: [],
2156
- traces: [],
2157
- toolInserts: [],
2158
- },
2159
- }), { headers: { "content-type": "application/json" } });
2160
4650
  }
2161
4651
  if (url.pathname === "/api/simulator/run") {
2162
4652
  if (req.method !== "POST") {
@@ -2178,18 +4668,44 @@ export function startWebSocketSimulator(opts) {
2178
4668
  simulatorCapturedTraces = [];
2179
4669
  simulatorCurrentRunId = undefined;
2180
4670
  }
2181
- if (payload.sessionId) {
2182
- const loaded = readSessionState(payload.sessionId);
2183
- if (loaded) {
2184
- simulatorSavedState = loaded;
2185
- simulatorCapturedTraces = Array.isArray(loaded.traces)
2186
- ? cloneTraces(loaded.traces)
2187
- : [];
4671
+ if (payload.workspaceId) {
4672
+ let loaded;
4673
+ try {
4674
+ loaded = readSessionStateStrict(payload.workspaceId, {
4675
+ withTraces: true,
4676
+ });
4677
+ }
4678
+ catch (err) {
4679
+ const message = err instanceof Error ? err.message : String(err);
4680
+ emitSimulator({ type: "error", message });
4681
+ return new Response(JSON.stringify({ error: message }), { status: 400, headers: { "content-type": "application/json" } });
2188
4682
  }
4683
+ if (!loaded) {
4684
+ const message = "Workspace not found";
4685
+ emitSimulator({ type: "error", message });
4686
+ return new Response(JSON.stringify({ error: message }), { status: 404, headers: { "content-type": "application/json" } });
4687
+ }
4688
+ simulatorSavedState = loaded;
4689
+ simulatorCapturedTraces = Array.isArray(loaded.traces)
4690
+ ? cloneTraces(loaded.traces)
4691
+ : [];
2189
4692
  }
2190
4693
  simulatorCurrentRunId = undefined;
2191
4694
  const stream = payload.stream ?? true;
2192
4695
  const forwardTrace = payload.trace ?? true;
4696
+ const pendingTraceEvents = [];
4697
+ const flushPendingTraceEvents = (state) => {
4698
+ if (!pendingTraceEvents.length)
4699
+ return;
4700
+ for (const pending of pendingTraceEvents) {
4701
+ appendSessionEvent(state, {
4702
+ ...pending,
4703
+ kind: "trace",
4704
+ category: traceCategory(pending.type),
4705
+ });
4706
+ }
4707
+ pendingTraceEvents.length = 0;
4708
+ };
2193
4709
  const tracer = (event) => {
2194
4710
  const stamped = event.ts ? event : { ...event, ts: Date.now() };
2195
4711
  if (stamped.type === "run.start") {
@@ -2199,6 +4715,16 @@ export function startWebSocketSimulator(opts) {
2199
4715
  consoleTracer?.(stamped);
2200
4716
  if (forwardTrace)
2201
4717
  emitSimulator({ type: "trace", event: stamped });
4718
+ if (simulatorSavedState?.meta?.sessionId) {
4719
+ appendSessionEvent(simulatorSavedState, {
4720
+ ...stamped,
4721
+ kind: "trace",
4722
+ category: traceCategory(stamped.type),
4723
+ });
4724
+ }
4725
+ else {
4726
+ pendingTraceEvents.push(stamped);
4727
+ }
2202
4728
  };
2203
4729
  let initialUserMessage = typeof payload.message === "string"
2204
4730
  ? payload.message
@@ -2245,6 +4771,7 @@ export function startWebSocketSimulator(opts) {
2245
4771
  traces: simulatorCapturedTraces,
2246
4772
  });
2247
4773
  simulatorSavedState = enrichedState;
4774
+ flushPendingTraceEvents(enrichedState);
2248
4775
  emitSimulator({ type: "state", state: enrichedState });
2249
4776
  },
2250
4777
  initialUserMessage,
@@ -2262,7 +4789,7 @@ export function startWebSocketSimulator(opts) {
2262
4789
  });
2263
4790
  return new Response(JSON.stringify({
2264
4791
  runId: simulatorCurrentRunId,
2265
- sessionId: simulatorSavedState?.meta?.sessionId,
4792
+ workspaceId: simulatorSavedState?.meta?.workspaceId,
2266
4793
  }), { headers: { "content-type": "application/json" } });
2267
4794
  }
2268
4795
  catch (err) {
@@ -2284,56 +4811,112 @@ export function startWebSocketSimulator(opts) {
2284
4811
  }
2285
4812
  try {
2286
4813
  const body = await req.json();
2287
- if (!body.sessionId) {
2288
- throw new Error("Missing sessionId");
4814
+ const workspaceId = getWorkspaceIdFromBody(body);
4815
+ if (!workspaceId) {
4816
+ throw new Error("Missing workspaceId");
2289
4817
  }
2290
4818
  if (!body.messageRefId) {
2291
4819
  throw new Error("Missing messageRefId");
2292
4820
  }
2293
- if (typeof body.score !== "number" || Number.isNaN(body.score)) {
4821
+ if (body.score !== null &&
4822
+ (typeof body.score !== "number" || Number.isNaN(body.score))) {
2294
4823
  throw new Error("Invalid score");
2295
4824
  }
2296
- const state = readSessionState(body.sessionId);
4825
+ let state;
4826
+ try {
4827
+ state = readSessionStateStrict(workspaceId, { withTraces: true });
4828
+ }
4829
+ catch (err) {
4830
+ throw new Error(err instanceof Error ? err.message : String(err));
4831
+ }
2297
4832
  if (!state)
2298
- throw new Error("Session not found");
4833
+ throw new Error("Workspace not found");
4834
+ const requestedRunId = typeof body.runId === "string" &&
4835
+ body.runId.trim().length > 0
4836
+ ? body.runId.trim()
4837
+ : undefined;
4838
+ const feedbackEligible = isFeedbackEligibleMessageRef(state, body.messageRefId) ||
4839
+ (requestedRunId
4840
+ ? isFeedbackEligiblePersistedTestRunMessageRef(state, requestedRunId, body.messageRefId)
4841
+ : false);
4842
+ if (!feedbackEligible) {
4843
+ throw new Error("Feedback target is not eligible");
4844
+ }
2299
4845
  simulatorSavedState = state;
2300
4846
  simulatorCapturedTraces = Array.isArray(state.traces)
2301
4847
  ? cloneTraces(state.traces)
2302
4848
  : [];
2303
- const clamped = Math.max(-3, Math.min(3, Math.round(body.score)));
2304
- const reason = typeof body.reason === "string"
2305
- ? body.reason
2306
- : undefined;
2307
- const runId = typeof state.runId === "string" ? state.runId : "run";
2308
4849
  const existing = state.feedback ?? [];
2309
4850
  const idx = existing.findIndex((f) => f.messageRefId === body.messageRefId);
2310
- const now = new Date().toISOString();
2311
- const entry = idx >= 0
2312
- ? {
2313
- ...existing[idx],
2314
- score: clamped,
2315
- reason,
2316
- runId: existing[idx].runId ?? runId,
4851
+ let entry;
4852
+ let feedback = existing;
4853
+ let deleted = false;
4854
+ if (body.score === null) {
4855
+ if (idx >= 0) {
4856
+ feedback = existing.filter((_, i) => i !== idx);
4857
+ deleted = true;
2317
4858
  }
2318
- : {
2319
- id: randomId("fb"),
2320
- runId,
2321
- messageRefId: body.messageRefId,
2322
- score: clamped,
2323
- reason,
2324
- createdAt: now,
2325
- };
2326
- const feedback = idx >= 0
2327
- ? existing.map((f, i) => i === idx ? entry : f)
2328
- : [...existing, entry];
4859
+ }
4860
+ else {
4861
+ const clamped = Math.max(-3, Math.min(3, Math.round(body.score)));
4862
+ const reason = typeof body.reason === "string"
4863
+ ? body.reason
4864
+ : undefined;
4865
+ const runId = requestedRunId ??
4866
+ (typeof state.runId === "string" ? state.runId : "run");
4867
+ const scenarioRunId = typeof state.meta?.scenarioRunId === "string"
4868
+ ? state.meta.scenarioRunId
4869
+ : runId;
4870
+ const now = new Date().toISOString();
4871
+ entry = idx >= 0
4872
+ ? {
4873
+ ...existing[idx],
4874
+ score: clamped,
4875
+ reason,
4876
+ runId: existing[idx].runId ?? runId,
4877
+ }
4878
+ : {
4879
+ id: randomId("fb"),
4880
+ runId,
4881
+ messageRefId: body.messageRefId,
4882
+ score: clamped,
4883
+ reason,
4884
+ createdAt: now,
4885
+ };
4886
+ if (entry) {
4887
+ entry.workspaceId = workspaceId;
4888
+ entry.scenarioRunId = scenarioRunId;
4889
+ }
4890
+ feedback = idx >= 0
4891
+ ? existing.map((f, i) => i === idx ? entry : f)
4892
+ : [...existing, entry];
4893
+ }
2329
4894
  const enriched = persistSessionState({
2330
4895
  ...state,
2331
4896
  feedback,
2332
4897
  traces: simulatorCapturedTraces,
2333
4898
  });
4899
+ appendFeedbackLog(enriched, {
4900
+ type: "feedback.update",
4901
+ messageRefId: body.messageRefId,
4902
+ feedback: entry,
4903
+ deleted,
4904
+ });
4905
+ appendSessionEvent(enriched, {
4906
+ type: "feedback.update",
4907
+ kind: "artifact",
4908
+ category: "feedback",
4909
+ workspaceId,
4910
+ scenarioRunId: typeof enriched.meta?.scenarioRunId === "string"
4911
+ ? enriched.meta.scenarioRunId
4912
+ : enriched.runId,
4913
+ messageRefId: body.messageRefId,
4914
+ feedback: entry,
4915
+ deleted,
4916
+ });
2334
4917
  simulatorSavedState = enriched;
2335
4918
  emitSimulator({ type: "state", state: enriched });
2336
- return new Response(JSON.stringify({ feedback: entry }), { headers: { "content-type": "application/json" } });
4919
+ return new Response(JSON.stringify({ feedback: entry, deleted }), { headers: { "content-type": "application/json" } });
2337
4920
  }
2338
4921
  catch (err) {
2339
4922
  return new Response(JSON.stringify({
@@ -2347,12 +4930,19 @@ export function startWebSocketSimulator(opts) {
2347
4930
  }
2348
4931
  try {
2349
4932
  const body = await req.json();
2350
- if (!body.sessionId) {
2351
- throw new Error("Missing sessionId");
4933
+ const workspaceId = getWorkspaceIdFromBody(body);
4934
+ if (!workspaceId) {
4935
+ throw new Error("Missing workspaceId");
4936
+ }
4937
+ let state;
4938
+ try {
4939
+ state = readSessionStateStrict(workspaceId, { withTraces: true });
4940
+ }
4941
+ catch (err) {
4942
+ throw new Error(err instanceof Error ? err.message : String(err));
2352
4943
  }
2353
- const state = readSessionState(body.sessionId);
2354
4944
  if (!state)
2355
- throw new Error("Session not found");
4945
+ throw new Error("Workspace not found");
2356
4946
  simulatorSavedState = state;
2357
4947
  simulatorCapturedTraces = Array.isArray(state.traces)
2358
4948
  ? cloneTraces(state.traces)
@@ -2363,6 +4953,13 @@ export function startWebSocketSimulator(opts) {
2363
4953
  notes: { text: body.text ?? "", updatedAt: now },
2364
4954
  traces: simulatorCapturedTraces,
2365
4955
  });
4956
+ appendSessionEvent(enriched, {
4957
+ type: "notes.update",
4958
+ kind: "artifact",
4959
+ category: "notes",
4960
+ workspaceId,
4961
+ notes: enriched.notes,
4962
+ });
2366
4963
  simulatorSavedState = enriched;
2367
4964
  emitSimulator({ type: "state", state: enriched });
2368
4965
  return new Response(JSON.stringify({ notes: enriched.notes, saved: true }), { headers: { "content-type": "application/json" } });
@@ -2379,15 +4976,22 @@ export function startWebSocketSimulator(opts) {
2379
4976
  }
2380
4977
  try {
2381
4978
  const body = await req.json();
2382
- if (!body.sessionId) {
2383
- throw new Error("Missing sessionId");
4979
+ const workspaceId = getWorkspaceIdFromBody(body);
4980
+ if (!workspaceId) {
4981
+ throw new Error("Missing workspaceId");
2384
4982
  }
2385
4983
  if (typeof body.score !== "number" || Number.isNaN(body.score)) {
2386
4984
  throw new Error("Invalid score");
2387
4985
  }
2388
- const state = readSessionState(body.sessionId);
4986
+ let state;
4987
+ try {
4988
+ state = readSessionStateStrict(workspaceId, { withTraces: true });
4989
+ }
4990
+ catch (err) {
4991
+ throw new Error(err instanceof Error ? err.message : String(err));
4992
+ }
2389
4993
  if (!state)
2390
- throw new Error("Session not found");
4994
+ throw new Error("Workspace not found");
2391
4995
  simulatorSavedState = state;
2392
4996
  simulatorCapturedTraces = Array.isArray(state.traces)
2393
4997
  ? cloneTraces(state.traces)
@@ -2399,6 +5003,13 @@ export function startWebSocketSimulator(opts) {
2399
5003
  conversationScore: { score: clamped, updatedAt: now },
2400
5004
  traces: simulatorCapturedTraces,
2401
5005
  });
5006
+ appendSessionEvent(enriched, {
5007
+ type: "conversation.score.update",
5008
+ kind: "artifact",
5009
+ category: "score",
5010
+ workspaceId,
5011
+ conversationScore: enriched.conversationScore,
5012
+ });
2402
5013
  simulatorSavedState = enriched;
2403
5014
  emitSimulator({ type: "state", state: enriched });
2404
5015
  return new Response(JSON.stringify({
@@ -2418,12 +5029,19 @@ export function startWebSocketSimulator(opts) {
2418
5029
  }
2419
5030
  try {
2420
5031
  const body = await req.json();
2421
- if (!body.sessionId) {
2422
- throw new Error("Missing sessionId");
5032
+ const workspaceId = getWorkspaceIdFromBody(body);
5033
+ if (!workspaceId) {
5034
+ throw new Error("Missing workspaceId");
5035
+ }
5036
+ let state;
5037
+ try {
5038
+ state = readSessionStateStrict(workspaceId, { withTraces: true });
5039
+ }
5040
+ catch (err) {
5041
+ throw new Error(err instanceof Error ? err.message : String(err));
2423
5042
  }
2424
- const state = readSessionState(body.sessionId);
2425
5043
  if (!state) {
2426
- throw new Error("Session not found");
5044
+ throw new Error("Workspace not found");
2427
5045
  }
2428
5046
  simulatorSavedState = state;
2429
5047
  simulatorCapturedTraces = Array.isArray(state.traces)
@@ -2438,48 +5056,34 @@ export function startWebSocketSimulator(opts) {
2438
5056
  }), { status: 400, headers: { "content-type": "application/json" } });
2439
5057
  }
2440
5058
  }
2441
- if (url.pathname === "/api/session") {
2442
- if (req.method !== "GET") {
2443
- return new Response("Method not allowed", { status: 405 });
2444
- }
2445
- const sessionId = url.searchParams.get("sessionId");
2446
- if (!sessionId) {
2447
- return new Response(JSON.stringify({ error: "Missing sessionId" }), { status: 400, headers: { "content-type": "application/json" } });
2448
- }
2449
- const state = readSessionState(sessionId);
2450
- if (!state) {
2451
- return new Response(JSON.stringify({ error: "Session not found" }), { status: 404, headers: { "content-type": "application/json" } });
2452
- }
2453
- return new Response(JSON.stringify({
2454
- sessionId,
2455
- messages: state.messages,
2456
- messageRefs: state.messageRefs,
2457
- feedback: state.feedback,
2458
- traces: state.traces,
2459
- notes: state.notes,
2460
- meta: state.meta,
2461
- }), { headers: { "content-type": "application/json" } });
2462
- }
2463
5059
  if (url.pathname === "/api/session/notes") {
2464
5060
  if (req.method !== "POST") {
2465
5061
  return new Response("Method not allowed", { status: 405 });
2466
5062
  }
2467
5063
  try {
2468
5064
  const body = await req.json();
2469
- if (!body.sessionId) {
2470
- throw new Error("Missing sessionId");
5065
+ const workspaceId = getWorkspaceIdFromBody(body);
5066
+ if (!workspaceId) {
5067
+ throw new Error("Missing workspaceId");
2471
5068
  }
2472
- const state = readSessionState(body.sessionId);
5069
+ const state = readSessionState(workspaceId);
2473
5070
  if (!state) {
2474
- throw new Error("Session not found");
5071
+ throw new Error("Workspace not found");
2475
5072
  }
2476
5073
  const now = new Date().toISOString();
2477
5074
  const nextState = persistSessionState({
2478
5075
  ...state,
2479
5076
  notes: { text: body.text ?? "", updatedAt: now },
2480
5077
  });
5078
+ appendSessionEvent(nextState, {
5079
+ type: "notes.update",
5080
+ kind: "artifact",
5081
+ category: "notes",
5082
+ workspaceId,
5083
+ notes: nextState.notes,
5084
+ });
2481
5085
  return new Response(JSON.stringify({
2482
- sessionId: body.sessionId,
5086
+ workspaceId,
2483
5087
  notes: nextState.notes,
2484
5088
  saved: true,
2485
5089
  }), { headers: { "content-type": "application/json" } });
@@ -2496,51 +5100,100 @@ export function startWebSocketSimulator(opts) {
2496
5100
  }
2497
5101
  try {
2498
5102
  const body = await req.json();
2499
- if (!body.sessionId) {
2500
- throw new Error("Missing sessionId");
5103
+ const workspaceId = getWorkspaceIdFromBody(body);
5104
+ if (!workspaceId) {
5105
+ throw new Error("Missing workspaceId");
2501
5106
  }
2502
5107
  if (!body.messageRefId) {
2503
5108
  throw new Error("Missing messageRefId");
2504
5109
  }
2505
- if (typeof body.score !== "number" || Number.isNaN(body.score)) {
5110
+ if (body.score !== null &&
5111
+ (typeof body.score !== "number" || Number.isNaN(body.score))) {
2506
5112
  throw new Error("Invalid score");
2507
5113
  }
2508
- const state = readSessionState(body.sessionId);
5114
+ const state = readSessionState(workspaceId);
2509
5115
  if (!state) {
2510
- throw new Error("Session not found");
5116
+ throw new Error("Workspace not found");
2511
5117
  }
2512
- const clamped = Math.max(-3, Math.min(3, Math.round(body.score)));
2513
- const reason = typeof body.reason === "string"
2514
- ? body.reason
5118
+ const requestedRunId = typeof body.runId === "string" &&
5119
+ body.runId.trim().length > 0
5120
+ ? body.runId.trim()
2515
5121
  : undefined;
2516
- const runId = typeof state.runId === "string"
2517
- ? state.runId
2518
- : "session";
5122
+ const feedbackEligible = isFeedbackEligibleMessageRef(state, body.messageRefId) ||
5123
+ (requestedRunId
5124
+ ? isFeedbackEligiblePersistedTestRunMessageRef(state, requestedRunId, body.messageRefId)
5125
+ : false);
5126
+ if (!feedbackEligible) {
5127
+ throw new Error("Feedback target is not eligible");
5128
+ }
2519
5129
  const existing = state.feedback ?? [];
2520
5130
  const idx = existing.findIndex((entry) => entry.messageRefId === body.messageRefId);
2521
- const now = new Date().toISOString();
2522
- const entry = idx >= 0
2523
- ? {
2524
- ...existing[idx],
2525
- score: clamped,
2526
- reason,
2527
- runId: existing[idx].runId ?? runId,
5131
+ let entry;
5132
+ let feedback = existing;
5133
+ let deleted = false;
5134
+ if (body.score === null) {
5135
+ if (idx >= 0) {
5136
+ feedback = existing.filter((_, i) => i !== idx);
5137
+ deleted = true;
2528
5138
  }
2529
- : {
2530
- id: randomId("fb"),
2531
- runId,
2532
- messageRefId: body.messageRefId,
2533
- score: clamped,
2534
- reason,
2535
- createdAt: now,
2536
- };
2537
- const feedback = idx >= 0
2538
- ? existing.map((item, i) => i === idx ? entry : item)
2539
- : [...existing, entry];
5139
+ }
5140
+ else {
5141
+ const clamped = Math.max(-3, Math.min(3, Math.round(body.score)));
5142
+ const reason = typeof body.reason === "string"
5143
+ ? body.reason
5144
+ : undefined;
5145
+ const runId = requestedRunId ??
5146
+ (typeof state.runId === "string" ? state.runId : "session");
5147
+ const scenarioRunId = requestedRunId ??
5148
+ (typeof state.meta?.scenarioRunId === "string"
5149
+ ? state.meta.scenarioRunId
5150
+ : runId);
5151
+ const now = new Date().toISOString();
5152
+ entry = idx >= 0
5153
+ ? {
5154
+ ...existing[idx],
5155
+ score: clamped,
5156
+ reason,
5157
+ runId: existing[idx].runId ?? runId,
5158
+ }
5159
+ : {
5160
+ id: randomId("fb"),
5161
+ runId,
5162
+ messageRefId: body.messageRefId,
5163
+ score: clamped,
5164
+ reason,
5165
+ createdAt: now,
5166
+ };
5167
+ if (entry) {
5168
+ entry.workspaceId = workspaceId;
5169
+ entry.scenarioRunId = scenarioRunId;
5170
+ }
5171
+ feedback = idx >= 0
5172
+ ? existing.map((item, i) => i === idx ? entry : item)
5173
+ : [...existing, entry];
5174
+ }
2540
5175
  const nextState = persistSessionState({
2541
5176
  ...state,
2542
5177
  feedback,
2543
5178
  });
5179
+ appendFeedbackLog(nextState, {
5180
+ type: "feedback.update",
5181
+ messageRefId: body.messageRefId,
5182
+ feedback: entry,
5183
+ deleted,
5184
+ });
5185
+ appendSessionEvent(nextState, {
5186
+ type: "feedback.update",
5187
+ kind: "artifact",
5188
+ category: "feedback",
5189
+ workspaceId,
5190
+ scenarioRunId: typeof nextState.meta?.scenarioRunId === "string"
5191
+ ? nextState.meta.scenarioRunId
5192
+ : nextState.runId,
5193
+ messageRefId: body.messageRefId,
5194
+ feedback: entry,
5195
+ deleted,
5196
+ });
2544
5197
  const testBotRunId = typeof nextState.meta?.testBotRunId === "string"
2545
5198
  ? nextState.meta.testBotRunId
2546
5199
  : undefined;
@@ -2548,13 +5201,14 @@ export function startWebSocketSimulator(opts) {
2548
5201
  const testEntry = testBotRuns.get(testBotRunId);
2549
5202
  if (testEntry) {
2550
5203
  syncTestBotRunFromState(testEntry.run, nextState);
2551
- broadcastTestBot({ type: "testBotStatus", run: testEntry.run });
5204
+ broadcastTestBot({ type: "testBotStatus", run: testEntry.run }, workspaceId);
2552
5205
  }
2553
5206
  }
2554
5207
  return new Response(JSON.stringify({
2555
- sessionId: body.sessionId,
5208
+ workspaceId,
2556
5209
  feedback: entry,
2557
- saved: true,
5210
+ saved: !deleted,
5211
+ deleted,
2558
5212
  }), { headers: { "content-type": "application/json" } });
2559
5213
  }
2560
5214
  catch (err) {
@@ -2569,118 +5223,17 @@ export function startWebSocketSimulator(opts) {
2569
5223
  }
2570
5224
  try {
2571
5225
  const body = await req.json();
2572
- if (!body.sessionId) {
2573
- throw new Error("Missing sessionId");
5226
+ const workspaceId = getWorkspaceIdFromBody(body);
5227
+ if (!workspaceId) {
5228
+ throw new Error("Missing workspaceId");
2574
5229
  }
2575
- const removed = deleteSessionState(body.sessionId);
5230
+ const removed = deleteSessionState(workspaceId);
2576
5231
  if (!removed) {
2577
- return new Response(JSON.stringify({ error: "Session not found" }), { status: 404, headers: { "content-type": "application/json" } });
2578
- }
2579
- return new Response(JSON.stringify({ sessionId: body.sessionId, deleted: true }), { headers: { "content-type": "application/json" } });
2580
- }
2581
- catch (err) {
2582
- return new Response(JSON.stringify({
2583
- error: err instanceof Error ? err.message : String(err),
2584
- }), { status: 400, headers: { "content-type": "application/json" } });
2585
- }
2586
- }
2587
- if (url.pathname === "/api/feedback") {
2588
- if (req.method !== "GET") {
2589
- return new Response("Method not allowed", { status: 405 });
2590
- }
2591
- const deckPathParam = url.searchParams.get("deckPath");
2592
- if (!deckPathParam) {
2593
- return new Response(JSON.stringify({ error: "Missing deckPath" }), { status: 400, headers: { "content-type": "application/json" } });
2594
- }
2595
- const items = [];
2596
- try {
2597
- for await (const entry of dntShim.Deno.readDir(sessionsRoot)) {
2598
- if (!entry.isDirectory)
2599
- continue;
2600
- const sessionId = entry.name;
2601
- const state = readSessionState(sessionId);
2602
- if (!state)
2603
- continue;
2604
- if (state.meta?.deck !== deckPathParam)
2605
- continue;
2606
- const feedbackList = Array.isArray(state.feedback)
2607
- ? state.feedback
2608
- : [];
2609
- feedbackList.forEach((fb) => {
2610
- if (!fb || typeof fb !== "object")
2611
- return;
2612
- const messageRefId = fb
2613
- .messageRefId;
2614
- if (typeof messageRefId !== "string")
2615
- return;
2616
- let messageContent = undefined;
2617
- if (Array.isArray(state.messageRefs) &&
2618
- Array.isArray(state.messages)) {
2619
- const idx = state.messageRefs.findIndex((ref) => ref?.id === messageRefId);
2620
- if (idx >= 0) {
2621
- messageContent = state.messages[idx]?.content;
2622
- }
2623
- }
2624
- items.push({
2625
- sessionId,
2626
- deck: state.meta?.deck,
2627
- sessionCreatedAt: state.meta?.sessionCreatedAt,
2628
- messageRefId,
2629
- score: fb.score,
2630
- reason: fb.reason,
2631
- createdAt: fb.createdAt,
2632
- archivedAt: fb.archivedAt,
2633
- messageContent,
2634
- });
2635
- });
2636
- }
2637
- }
2638
- catch (err) {
2639
- return new Response(JSON.stringify({
2640
- error: err instanceof Error ? err.message : String(err),
2641
- }), { status: 400, headers: { "content-type": "application/json" } });
2642
- }
2643
- items.sort((a, b) => {
2644
- const aTime = String(a.createdAt ?? "") || "";
2645
- const bTime = String(b.createdAt ?? "") || "";
2646
- return bTime.localeCompare(aTime);
2647
- });
2648
- return new Response(JSON.stringify({ deckPath: deckPathParam, items }), {
2649
- headers: { "content-type": "application/json" },
2650
- });
2651
- }
2652
- if (url.pathname === "/api/feedback/archive" && req.method === "POST") {
2653
- try {
2654
- const body = await req.json();
2655
- if (!body.sessionId || !body.messageRefId) {
2656
- throw new Error("Missing sessionId or messageRefId");
2657
- }
2658
- const state = readSessionState(body.sessionId);
2659
- if (!state || !Array.isArray(state.feedback)) {
2660
- throw new Error("Session not found");
2661
- }
2662
- const idx = state.feedback.findIndex((fb) => fb.messageRefId === body.messageRefId);
2663
- if (idx === -1)
2664
- throw new Error("Feedback not found");
2665
- const next = { ...state.feedback[idx] };
2666
- if (body.archived === false) {
2667
- delete next.archivedAt;
2668
- }
2669
- else {
2670
- next.archivedAt = new Date()
2671
- .toISOString();
5232
+ return new Response(JSON.stringify({ error: "Workspace not found" }), { status: 404, headers: { "content-type": "application/json" } });
2672
5233
  }
2673
- const nextFeedback = state.feedback.map((fb, i) => i === idx ? next : fb);
2674
- const updated = persistSessionState({
2675
- ...state,
2676
- feedback: nextFeedback,
2677
- });
2678
5234
  return new Response(JSON.stringify({
2679
- sessionId: body.sessionId,
2680
- messageRefId: body.messageRefId,
2681
- archivedAt: next.archivedAt,
2682
- saved: true,
2683
- feedbackCount: updated.feedback?.length ?? 0,
5235
+ workspaceId,
5236
+ deleted: true,
2684
5237
  }), { headers: { "content-type": "application/json" } });
2685
5238
  }
2686
5239
  catch (err) {
@@ -2689,102 +5242,42 @@ export function startWebSocketSimulator(opts) {
2689
5242
  }), { status: 400, headers: { "content-type": "application/json" } });
2690
5243
  }
2691
5244
  }
2692
- if (url.pathname === "/" || url.pathname.startsWith("/sessions/") ||
2693
- url.pathname.startsWith("/simulate") ||
2694
- url.pathname.startsWith("/debug") ||
2695
- url.pathname.startsWith("/editor") ||
2696
- url.pathname.startsWith("/docs") ||
2697
- url.pathname.startsWith("/test") ||
2698
- url.pathname.startsWith("/grade")) {
2699
- const hasBundle = await canServeReactBundle();
2700
- if (!hasBundle) {
2701
- return new Response("Simulator UI bundle missing. Run `deno task bundle:sim` (or start with `--bundle`).", { status: 500 });
2702
- }
2703
- await deckLoadPromise.catch(() => null);
2704
- const resolvedLabel = deckLabel ?? toDeckLabel(resolvedDeckPath);
2705
- return new Response(simulatorReactHtml(resolvedDeckPath, resolvedLabel), {
2706
- headers: { "content-type": "text/html; charset=utf-8" },
2707
- });
2708
- }
2709
- if (url.pathname === "/schema") {
2710
- const desc = await schemaPromise;
2711
- const deck = await deckLoadPromise.catch(() => null);
2712
- const startMode = deck &&
2713
- (deck.startMode === "assistant" || deck.startMode === "user")
2714
- ? deck.startMode
2715
- : undefined;
2716
- return new Response(JSON.stringify({
2717
- deck: resolvedDeckPath,
2718
- startMode,
2719
- ...desc,
2720
- }), {
2721
- headers: { "content-type": "application/json; charset=utf-8" },
2722
- });
2723
- }
2724
- if (url.pathname === "/api/deck-source") {
2725
- if (req.method !== "GET") {
2726
- return new Response("Method not allowed", { status: 405 });
2727
- }
2728
- try {
2729
- const content = await dntShim.Deno.readTextFile(resolvedDeckPath);
2730
- return new Response(JSON.stringify({
2731
- path: resolvedDeckPath,
2732
- content,
2733
- }), { headers: { "content-type": "application/json; charset=utf-8" } });
2734
- }
2735
- catch (err) {
2736
- const message = err instanceof Error ? err.message : String(err);
2737
- return new Response(JSON.stringify({
2738
- path: resolvedDeckPath,
2739
- error: message,
2740
- }), {
2741
- status: 500,
2742
- headers: { "content-type": "application/json; charset=utf-8" },
2743
- });
2744
- }
2745
- }
2746
- if (url.pathname === "/ui/bundle.js") {
2747
- const data = await readReactBundle();
2748
- if (!data) {
2749
- return new Response("Bundle missing. Run `deno task bundle:sim` (or start with `--bundle`).", { status: 404 });
2750
- }
2751
- try {
2752
- const headers = new Headers({
2753
- "content-type": "application/javascript; charset=utf-8",
2754
- });
2755
- // Hint the browser about the external source map since Deno's bundle
2756
- // output does not embed a sourceMappingURL comment.
2757
- if (shouldAdvertiseSourceMap()) {
2758
- headers.set("SourceMap", "/ui/bundle.js.map");
2759
- }
2760
- return new Response(data, { headers });
2761
- }
2762
- catch (err) {
2763
- return new Response(`Failed to read bundle: ${err instanceof Error ? err.message : String(err)}`, { status: 500 });
2764
- }
2765
- }
2766
- if (url.pathname === "/ui/bundle.js.map") {
2767
- const data = await readReactBundleSourceMap();
2768
- if (!data) {
2769
- return new Response("Source map missing. Run `deno task bundle:sim:sourcemap` (or start with `--bundle --sourcemap`).", { status: 404 });
2770
- }
2771
- try {
2772
- return new Response(data, {
2773
- headers: {
2774
- "content-type": "application/json; charset=utf-8",
2775
- },
2776
- });
2777
- }
2778
- catch (err) {
2779
- return new Response(`Failed to read source map: ${err instanceof Error ? err.message : String(err)}`, { status: 500 });
2780
- }
2781
- }
2782
- if (url.pathname === "/sessions") {
2783
- const sessions = listSessions();
2784
- return new Response(JSON.stringify({ sessions }), {
2785
- headers: { "content-type": "application/json; charset=utf-8" },
2786
- });
2787
- }
5245
+ const feedbackResponse = await handleFeedbackRoutes({
5246
+ url,
5247
+ req,
5248
+ sessionsRoot,
5249
+ getWorkspaceIdFromBody,
5250
+ readSessionState,
5251
+ persistSessionState,
5252
+ appendFeedbackLog,
5253
+ appendSessionEvent,
5254
+ });
5255
+ if (feedbackResponse)
5256
+ return feedbackResponse;
5257
+ const uiRoutesResponse = await handleUiRoutes({
5258
+ url,
5259
+ req,
5260
+ workspaceRouteBase: WORKSPACE_ROUTE_BASE,
5261
+ activeWorkspaceId,
5262
+ activeWorkspaceOnboarding,
5263
+ resolvedDeckPath,
5264
+ deckLabel,
5265
+ getWorkspaceIdFromQuery,
5266
+ activateWorkspaceDeck,
5267
+ schemaPromise,
5268
+ deckLoadPromise,
5269
+ canServeReactBundle,
5270
+ simulatorReactHtml,
5271
+ toDeckLabel,
5272
+ readReactBundle,
5273
+ shouldAdvertiseSourceMap,
5274
+ readReactBundleSourceMap,
5275
+ listSessions,
5276
+ createWorkspaceSession,
5277
+ workspaceStateSchemaVersion: WORKSPACE_STATE_SCHEMA_VERSION,
5278
+ });
5279
+ if (uiRoutesResponse)
5280
+ return uiRoutesResponse;
2788
5281
  return new Response("Not found", { status: 404 });
2789
5282
  });
2790
5283
  const listenPort = server.addr.port;
@@ -2919,9 +5412,36 @@ async function readRemoteBundle(url, kind) {
2919
5412
  return null;
2920
5413
  }
2921
5414
  }
2922
- function simulatorReactHtml(deckPath, deckLabel) {
5415
+ function simulatorReactHtml(deckPath, deckLabel, opts) {
2923
5416
  const safeDeckPath = deckPath.replaceAll("<", "&lt;").replaceAll(">", "&gt;");
2924
5417
  const safeDeckLabel = deckLabel?.replaceAll("<", "&lt;").replaceAll(">", "&gt;") ?? null;
5418
+ const buildTabEnabled = (() => {
5419
+ const raw = dntShim.Deno.env.get("GAMBIT_SIMULATOR_BUILD_TAB");
5420
+ if (raw === undefined)
5421
+ return true;
5422
+ const normalized = raw.trim().toLowerCase();
5423
+ return !(normalized === "0" || normalized === "false" ||
5424
+ normalized === "no" ||
5425
+ normalized === "off");
5426
+ })();
5427
+ const chatAccordionEnabled = (() => {
5428
+ const raw = dntShim.Deno.env.get("GAMBIT_SIMULATOR_CHAT_ACCORDION");
5429
+ if (raw === undefined)
5430
+ return true;
5431
+ const normalized = raw.trim().toLowerCase();
5432
+ return normalized === "1" || normalized === "true" ||
5433
+ normalized === "yes" ||
5434
+ normalized === "on";
5435
+ })();
5436
+ const buildStreamDebugEnabled = (() => {
5437
+ const raw = dntShim.Deno.env.get("GAMBIT_SIMULATOR_BUILD_STREAM_DEBUG");
5438
+ if (raw === undefined)
5439
+ return false;
5440
+ const normalized = raw.trim().toLowerCase();
5441
+ return normalized === "1" || normalized === "true" ||
5442
+ normalized === "yes" ||
5443
+ normalized === "on";
5444
+ })();
2925
5445
  const bundleStamp = (() => {
2926
5446
  try {
2927
5447
  const stat = dntShim.Deno.statSync(simulatorBundlePath);
@@ -2935,6 +5455,8 @@ function simulatorReactHtml(deckPath, deckLabel) {
2935
5455
  const bundleUrl = bundleStamp
2936
5456
  ? `/ui/bundle.js?v=${bundleStamp}`
2937
5457
  : "/ui/bundle.js";
5458
+ const workspaceId = opts?.workspaceId ?? null;
5459
+ const workspaceOnboarding = Boolean(opts?.onboarding);
2938
5460
  return `<!doctype html>
2939
5461
  <html lang="en">
2940
5462
  <head>
@@ -2951,6 +5473,12 @@ function simulatorReactHtml(deckPath, deckLabel) {
2951
5473
  <script>
2952
5474
  window.__GAMBIT_DECK_PATH__ = ${JSON.stringify(safeDeckPath)};
2953
5475
  window.__GAMBIT_DECK_LABEL__ = ${JSON.stringify(safeDeckLabel)};
5476
+ window.__GAMBIT_VERSION__ = ${JSON.stringify(gambitVersion)};
5477
+ window.__GAMBIT_BUILD_TAB_ENABLED__ = ${JSON.stringify(buildTabEnabled)};
5478
+ window.__GAMBIT_CHAT_ACCORDION_ENABLED__ = ${JSON.stringify(chatAccordionEnabled)};
5479
+ window.__GAMBIT_WORKSPACE_ID__ = ${JSON.stringify(workspaceId)};
5480
+ window.__GAMBIT_WORKSPACE_ONBOARDING__ = ${JSON.stringify(workspaceOnboarding)};
5481
+ window.__GAMBIT_BUILD_STREAM_DEBUG__ = ${JSON.stringify(buildStreamDebugEnabled)};
2954
5482
  </script>
2955
5483
  <script type="module" src="${bundleUrl}"></script>
2956
5484
  </body>
@@ -2995,6 +5523,9 @@ async function runDeckWithFallback(args) {
2995
5523
  stream: args.stream,
2996
5524
  onStreamText: args.onStreamText,
2997
5525
  responsesMode: args.responsesMode,
5526
+ workerSandbox: args.workerSandbox,
5527
+ signal: args.signal,
5528
+ onCancel: args.onCancel,
2998
5529
  });
2999
5530
  }
3000
5531
  catch (error) {
@@ -3011,6 +5542,9 @@ async function runDeckWithFallback(args) {
3011
5542
  stream: args.stream,
3012
5543
  onStreamText: args.onStreamText,
3013
5544
  responsesMode: args.responsesMode,
5545
+ workerSandbox: args.workerSandbox,
5546
+ signal: args.signal,
5547
+ onCancel: args.onCancel,
3014
5548
  });
3015
5549
  }
3016
5550
  throw error;