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