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