tulingcode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +134 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/tuling +179 -0
- package/bunfig.toml +7 -0
- package/drizzle.config.ts +10 -0
- package/git +0 -0
- package/migration/20260127222353_familiar_lady_ursula/migration.sql +90 -0
- package/migration/20260127222353_familiar_lady_ursula/snapshot.json +796 -0
- package/migration/20260211171708_add_project_commands/migration.sql +1 -0
- package/migration/20260211171708_add_project_commands/snapshot.json +806 -0
- package/migration/20260213144116_wakeful_the_professor/migration.sql +11 -0
- package/migration/20260213144116_wakeful_the_professor/snapshot.json +897 -0
- package/migration/20260225215848_workspace/migration.sql +7 -0
- package/migration/20260225215848_workspace/snapshot.json +959 -0
- package/migration/20260227213759_add_session_workspace_id/migration.sql +2 -0
- package/migration/20260227213759_add_session_workspace_id/snapshot.json +983 -0
- package/migration/20260228203230_blue_harpoon/migration.sql +17 -0
- package/migration/20260228203230_blue_harpoon/snapshot.json +1102 -0
- package/migration/20260303231226_add_workspace_fields/migration.sql +5 -0
- package/migration/20260303231226_add_workspace_fields/snapshot.json +1013 -0
- package/migration/20260309230000_move_org_to_state/migration.sql +3 -0
- package/migration/20260309230000_move_org_to_state/snapshot.json +1156 -0
- package/migration/20260312043431_session_message_cursor/migration.sql +4 -0
- package/migration/20260312043431_session_message_cursor/snapshot.json +1168 -0
- package/migration/20260323234822_events/migration.sql +13 -0
- package/migration/20260323234822_events/snapshot.json +1271 -0
- package/migration/20260410174513_workspace-name/migration.sql +16 -0
- package/migration/20260410174513_workspace-name/snapshot.json +1271 -0
- package/migration/20260413175956_chief_energizer/migration.sql +13 -0
- package/migration/20260413175956_chief_energizer/snapshot.json +1399 -0
- package/migration/20260422160000_context_inheritance/migration.sql +3 -0
- package/migration/20260422170000_task_registry/migration.sql +18 -0
- package/migration/20260423145421_remove_session_entry/migration.sql +4 -0
- package/migration/20260515000000_actor_rename/migration.sql +7 -0
- package/migration/20260515010000_memory_fts/migration.sql +33 -0
- package/migration/20260515020000_user_task/migration.sql +29 -0
- package/migration/20260519000000_last_checkpoint_message_id/migration.sql +1 -0
- package/migration/20260521000000_message_agent_id/migration.sql +2 -0
- package/migration/20260521000100_actor_registry_v6/migration.sql +25 -0
- package/migration/20260521010000_memory_fts_v6/migration.sql +33 -0
- package/migration/20260521020000_memory_fts_triggers/migration.sql +17 -0
- package/migration/20260526000000_agent_id_main/migration.sql +14 -0
- package/migration/20260527000000_actor_lifecycle/migration.sql +8 -0
- package/migration/20260527000100_inbox/migration.sql +12 -0
- package/migration/20260529000000_task_todo_redesign/migration.sql +16 -0
- package/migration/20260603000000_task_in_progress_owner/migration.sql +1 -0
- package/migration/20260603000000_workflow_run/migration.sql +17 -0
- package/migration/20260604000000_workflow_script_sha/migration.sql +1 -0
- package/migration/20260608000000_claude_import/migration.sql +7 -0
- package/migration/20260608010000_claude_import_message_ids/migration.sql +1 -0
- package/migration/20260609000000_history_fts/migration.sql +29 -0
- package/migration/20260609230000_workflow_agent_timeout/migration.sql +1 -0
- package/package.json +196 -0
- package/parsers-config.ts +290 -0
- package/script/build.ts +267 -0
- package/script/check-migrations.ts +16 -0
- package/script/fix-node-pty.ts +28 -0
- package/script/generate.ts +23 -0
- package/script/postinstall.mjs +102 -0
- package/script/publish.ts +60 -0
- package/script/run-workspace-server +106 -0
- package/script/schema.ts +63 -0
- package/script/time.ts +6 -0
- package/script/trace-imports.ts +153 -0
- package/script/upgrade-opentui.ts +64 -0
- package/src/account/account.sql.ts +39 -0
- package/src/account/account.ts +456 -0
- package/src/account/repo.ts +166 -0
- package/src/account/schema.ts +99 -0
- package/src/account/url.ts +8 -0
- package/src/acp/README.md +174 -0
- package/src/acp/agent.ts +1783 -0
- package/src/acp/session.ts +116 -0
- package/src/acp/types.ts +24 -0
- package/src/actor/actor.sql.ts +38 -0
- package/src/actor/events.ts +67 -0
- package/src/actor/index.ts +2 -0
- package/src/actor/registry.ts +412 -0
- package/src/actor/return-header.ts +24 -0
- package/src/actor/schema.ts +47 -0
- package/src/actor/spawn-ref.ts +16 -0
- package/src/actor/spawn.ts +741 -0
- package/src/actor/turn.ts +49 -0
- package/src/actor/waiter.ts +166 -0
- package/src/agent/agent.ts +554 -0
- package/src/agent/config.ts +5 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/checkpoint-writer.txt +167 -0
- package/src/agent/prompt/compaction.txt +9 -0
- package/src/agent/prompt/distill.txt +199 -0
- package/src/agent/prompt/dream.txt +155 -0
- package/src/agent/prompt/explore.txt +18 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/audio.d.ts +9 -0
- package/src/auth/index.ts +97 -0
- package/src/bus/bus-event.ts +33 -0
- package/src/bus/global.ts +12 -0
- package/src/bus/index.ts +193 -0
- package/src/cli/bootstrap.ts +33 -0
- package/src/cli/cmd/account.ts +258 -0
- package/src/cli/cmd/acp.ts +70 -0
- package/src/cli/cmd/agent.ts +248 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/db.ts +120 -0
- package/src/cli/cmd/debug/agent.ts +192 -0
- package/src/cli/cmd/debug/config.ts +17 -0
- package/src/cli/cmd/debug/file.ts +100 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +61 -0
- package/src/cli/cmd/debug/ripgrep.ts +105 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +23 -0
- package/src/cli/cmd/debug/snapshot.ts +53 -0
- package/src/cli/cmd/export.ts +306 -0
- package/src/cli/cmd/generate.ts +50 -0
- package/src/cli/cmd/github.ts +1647 -0
- package/src/cli/cmd/import.ts +208 -0
- package/src/cli/cmd/mcp.ts +812 -0
- package/src/cli/cmd/models.ts +88 -0
- package/src/cli/cmd/plug.ts +233 -0
- package/src/cli/cmd/pr.ts +138 -0
- package/src/cli/cmd/providers.ts +705 -0
- package/src/cli/cmd/run-completion.ts +77 -0
- package/src/cli/cmd/run.ts +694 -0
- package/src/cli/cmd/serve.ts +21 -0
- package/src/cli/cmd/session.ts +181 -0
- package/src/cli/cmd/stats.ts +413 -0
- package/src/cli/cmd/tui/app.tsx +1130 -0
- package/src/cli/cmd/tui/asset/TEN_VAD_LICENSE +12 -0
- package/src/cli/cmd/tui/asset/charge.wav +0 -0
- package/src/cli/cmd/tui/asset/pulse-a.wav +0 -0
- package/src/cli/cmd/tui/asset/pulse-b.wav +0 -0
- package/src/cli/cmd/tui/asset/pulse-c.wav +0 -0
- package/src/cli/cmd/tui/asset/ten_vad.wasm +0 -0
- package/src/cli/cmd/tui/asset/ten_vad_loader.js +30 -0
- package/src/cli/cmd/tui/attach.ts +83 -0
- package/src/cli/cmd/tui/component/background-image.tsx +150 -0
- package/src/cli/cmd/tui/component/bg-pulse.tsx +130 -0
- package/src/cli/cmd/tui/component/border.tsx +21 -0
- package/src/cli/cmd/tui/component/dialog-agent.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-command.tsx +208 -0
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog-go-upsell.tsx +157 -0
- package/src/cli/cmd/tui/component/dialog-image-list.tsx +111 -0
- package/src/cli/cmd/tui/component/dialog-logo-design.tsx +37 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-mimo-login.tsx +224 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +253 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +490 -0
- package/src/cli/cmd/tui/component/dialog-session-delete-failed.tsx +101 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +269 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-skill.tsx +42 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +170 -0
- package/src/cli/cmd/tui/component/dialog-tag.tsx +44 -0
- package/src/cli/cmd/tui/component/dialog-theme-list.tsx +50 -0
- package/src/cli/cmd/tui/component/dialog-variant.tsx +39 -0
- package/src/cli/cmd/tui/component/dialog-workflows.tsx +62 -0
- package/src/cli/cmd/tui/component/dialog-workspace-create.tsx +289 -0
- package/src/cli/cmd/tui/component/dialog-workspace-unavailable.tsx +81 -0
- package/src/cli/cmd/tui/component/dialog-worktree.tsx +90 -0
- package/src/cli/cmd/tui/component/error-component.tsx +92 -0
- package/src/cli/cmd/tui/component/logo.tsx +961 -0
- package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +684 -0
- package/src/cli/cmd/tui/component/prompt/cwd.ts +0 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +108 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1812 -0
- package/src/cli/cmd/tui/component/prompt/part.ts +16 -0
- package/src/cli/cmd/tui/component/prompt/stash.tsx +101 -0
- package/src/cli/cmd/tui/component/spinner.tsx +24 -0
- package/src/cli/cmd/tui/component/starry-background.tsx +305 -0
- package/src/cli/cmd/tui/component/startup-loading.tsx +67 -0
- package/src/cli/cmd/tui/component/task-item.tsx +63 -0
- package/src/cli/cmd/tui/component/textarea-keybindings.ts +73 -0
- package/src/cli/cmd/tui/component/todo-item.tsx +32 -0
- package/src/cli/cmd/tui/config/cwd.ts +5 -0
- package/src/cli/cmd/tui/config/tui-migrate.ts +151 -0
- package/src/cli/cmd/tui/config/tui-schema.ts +38 -0
- package/src/cli/cmd/tui/config/tui.ts +219 -0
- package/src/cli/cmd/tui/context/args.tsx +16 -0
- package/src/cli/cmd/tui/context/directory.ts +15 -0
- package/src/cli/cmd/tui/context/event.ts +45 -0
- package/src/cli/cmd/tui/context/exit.tsx +65 -0
- package/src/cli/cmd/tui/context/helper.tsx +25 -0
- package/src/cli/cmd/tui/context/keybind.tsx +105 -0
- package/src/cli/cmd/tui/context/kv.tsx +76 -0
- package/src/cli/cmd/tui/context/language.tsx +91 -0
- package/src/cli/cmd/tui/context/local.tsx +455 -0
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/project.tsx +109 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +61 -0
- package/src/cli/cmd/tui/context/sdk.tsx +150 -0
- package/src/cli/cmd/tui/context/sync.tsx +828 -0
- package/src/cli/cmd/tui/context/theme/aura.json +69 -0
- package/src/cli/cmd/tui/context/theme/ayu.json +80 -0
- package/src/cli/cmd/tui/context/theme/carbonfox.json +248 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-frappe.json +230 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +230 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +225 -0
- package/src/cli/cmd/tui/context/theme/cursor.json +249 -0
- package/src/cli/cmd/tui/context/theme/dracula.json +219 -0
- package/src/cli/cmd/tui/context/theme/everforest.json +241 -0
- package/src/cli/cmd/tui/context/theme/flexoki.json +237 -0
- package/src/cli/cmd/tui/context/theme/github.json +233 -0
- package/src/cli/cmd/tui/context/theme/gruvbox.json +242 -0
- package/src/cli/cmd/tui/context/theme/kanagawa.json +77 -0
- package/src/cli/cmd/tui/context/theme/lucent-orng.json +234 -0
- package/src/cli/cmd/tui/context/theme/material.json +235 -0
- package/src/cli/cmd/tui/context/theme/matrix.json +77 -0
- package/src/cli/cmd/tui/context/theme/mercury.json +252 -0
- package/src/cli/cmd/tui/context/theme/monokai.json +221 -0
- package/src/cli/cmd/tui/context/theme/nightowl.json +221 -0
- package/src/cli/cmd/tui/context/theme/nord.json +223 -0
- package/src/cli/cmd/tui/context/theme/one-dark.json +84 -0
- package/src/cli/cmd/tui/context/theme/orng.json +249 -0
- package/src/cli/cmd/tui/context/theme/osaka-jade.json +93 -0
- package/src/cli/cmd/tui/context/theme/palenight.json +222 -0
- package/src/cli/cmd/tui/context/theme/rosepine.json +234 -0
- package/src/cli/cmd/tui/context/theme/solarized.json +223 -0
- package/src/cli/cmd/tui/context/theme/synthwave84.json +226 -0
- package/src/cli/cmd/tui/context/theme/tokyonight.json +243 -0
- package/src/cli/cmd/tui/context/theme/tulingcode.json +245 -0
- package/src/cli/cmd/tui/context/theme/vercel.json +245 -0
- package/src/cli/cmd/tui/context/theme/vesper.json +218 -0
- package/src/cli/cmd/tui/context/theme/zenburn.json +223 -0
- package/src/cli/cmd/tui/context/theme.tsx +1298 -0
- package/src/cli/cmd/tui/context/thinking.ts +48 -0
- package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/event.ts +56 -0
- package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +193 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +54 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +114 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/cwd.tsx +45 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/files.tsx +62 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/footer.tsx +93 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/goal.tsx +84 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/instructions.tsx +54 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/lsp.tsx +66 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +98 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/task.tsx +95 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +51 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/tps.ts +31 -0
- package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +274 -0
- package/src/cli/cmd/tui/i18n/en.ts +397 -0
- package/src/cli/cmd/tui/i18n/es.ts +433 -0
- package/src/cli/cmd/tui/i18n/fr.ts +440 -0
- package/src/cli/cmd/tui/i18n/ja.ts +392 -0
- package/src/cli/cmd/tui/i18n/locales.ts +82 -0
- package/src/cli/cmd/tui/i18n/ru.ts +452 -0
- package/src/cli/cmd/tui/i18n/zh.ts +390 -0
- package/src/cli/cmd/tui/i18n/zht.ts +360 -0
- package/src/cli/cmd/tui/layer.ts +6 -0
- package/src/cli/cmd/tui/plugin/api.tsx +402 -0
- package/src/cli/cmd/tui/plugin/index.ts +3 -0
- package/src/cli/cmd/tui/plugin/internal.ts +35 -0
- package/src/cli/cmd/tui/plugin/runtime.ts +1030 -0
- package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
- package/src/cli/cmd/tui/routes/home.tsx +165 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +76 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +116 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/dialog-timeline.tsx +47 -0
- package/src/cli/cmd/tui/routes/session/footer.tsx +91 -0
- package/src/cli/cmd/tui/routes/session/index.tsx +2532 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +691 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +488 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +97 -0
- package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +142 -0
- package/src/cli/cmd/tui/thread.ts +246 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +61 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +95 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +223 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +42 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +123 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +452 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +207 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -0
- package/src/cli/cmd/tui/ui/spinner.ts +378 -0
- package/src/cli/cmd/tui/ui/toast.tsx +102 -0
- package/src/cli/cmd/tui/util/clipboard.ts +203 -0
- package/src/cli/cmd/tui/util/editor.ts +35 -0
- package/src/cli/cmd/tui/util/image-protocol.ts +35 -0
- package/src/cli/cmd/tui/util/index.ts +6 -0
- package/src/cli/cmd/tui/util/model.ts +23 -0
- package/src/cli/cmd/tui/util/provider-origin.ts +7 -0
- package/src/cli/cmd/tui/util/revert-diff.ts +18 -0
- package/src/cli/cmd/tui/util/scroll.ts +23 -0
- package/src/cli/cmd/tui/util/selection.ts +23 -0
- package/src/cli/cmd/tui/util/signal.ts +41 -0
- package/src/cli/cmd/tui/util/sound.ts +154 -0
- package/src/cli/cmd/tui/util/system-locale.ts +209 -0
- package/src/cli/cmd/tui/util/terminal.ts +110 -0
- package/src/cli/cmd/tui/util/transcript.ts +112 -0
- package/src/cli/cmd/tui/util/vad.ts +229 -0
- package/src/cli/cmd/tui/util/voice.ts +360 -0
- package/src/cli/cmd/tui/win32.ts +130 -0
- package/src/cli/cmd/tui/worker.ts +104 -0
- package/src/cli/cmd/uninstall.ts +351 -0
- package/src/cli/cmd/upgrade.ts +79 -0
- package/src/cli/cmd/web.ts +81 -0
- package/src/cli/effect/prompt.ts +25 -0
- package/src/cli/error.ts +82 -0
- package/src/cli/heap.ts +59 -0
- package/src/cli/i18n.ts +15 -0
- package/src/cli/logo.ts +53 -0
- package/src/cli/network.ts +62 -0
- package/src/cli/ui.ts +133 -0
- package/src/cli/upgrade.ts +41 -0
- package/src/command/index.ts +276 -0
- package/src/command/template/initialize.txt +66 -0
- package/src/command/template/review.txt +101 -0
- package/src/config/agent.ts +197 -0
- package/src/config/command.ts +69 -0
- package/src/config/config.ts +1024 -0
- package/src/config/console-state.ts +16 -0
- package/src/config/entry-name.ts +16 -0
- package/src/config/error.ts +21 -0
- package/src/config/formatter.ts +17 -0
- package/src/config/history.ts +21 -0
- package/src/config/index.ts +16 -0
- package/src/config/keybinds.ts +127 -0
- package/src/config/layout.ts +10 -0
- package/src/config/lsp.ts +45 -0
- package/src/config/managed.ts +70 -0
- package/src/config/markdown.ts +97 -0
- package/src/config/mcp.ts +172 -0
- package/src/config/model-id.ts +14 -0
- package/src/config/parse.ts +44 -0
- package/src/config/paths.ts +73 -0
- package/src/config/permission.ts +76 -0
- package/src/config/plugin.ts +88 -0
- package/src/config/provider.ts +118 -0
- package/src/config/server.ts +20 -0
- package/src/config/skills.ts +16 -0
- package/src/config/variable.ts +90 -0
- package/src/control-plane/adaptors/index.ts +52 -0
- package/src/control-plane/adaptors/worktree.ts +47 -0
- package/src/control-plane/dev/debug-workspace-plugin.ts +73 -0
- package/src/control-plane/schema.ts +19 -0
- package/src/control-plane/sse.ts +66 -0
- package/src/control-plane/types.ts +34 -0
- package/src/control-plane/util.ts +37 -0
- package/src/control-plane/workspace-context.ts +26 -0
- package/src/control-plane/workspace.sql.ts +17 -0
- package/src/control-plane/workspace.ts +615 -0
- package/src/effect/app-runtime.ts +146 -0
- package/src/effect/bootstrap-runtime.ts +33 -0
- package/src/effect/bridge.ts +48 -0
- package/src/effect/cross-spawn-spawner.ts +514 -0
- package/src/effect/index.ts +5 -0
- package/src/effect/instance-ref.ts +11 -0
- package/src/effect/instance-registry.ts +12 -0
- package/src/effect/instance-state.ts +81 -0
- package/src/effect/logger.ts +73 -0
- package/src/effect/memo-map.ts +3 -0
- package/src/effect/observability.ts +107 -0
- package/src/effect/run-service.ts +52 -0
- package/src/effect/runner.ts +210 -0
- package/src/effect/runtime.ts +19 -0
- package/src/env/index.ts +37 -0
- package/src/file/ignore.ts +81 -0
- package/src/file/index.ts +664 -0
- package/src/file/protected.ts +59 -0
- package/src/file/ripgrep.ts +485 -0
- package/src/file/watcher.ts +163 -0
- package/src/flag/flag.ts +164 -0
- package/src/format/formatter.ts +403 -0
- package/src/format/index.ts +203 -0
- package/src/git/index.ts +260 -0
- package/src/global/index.ts +54 -0
- package/src/history/backfill.ts +162 -0
- package/src/history/extract.ts +67 -0
- package/src/history/fts-query.ts +15 -0
- package/src/history/fts.sql.ts +20 -0
- package/src/history/index.ts +10 -0
- package/src/history/resolve.ts +65 -0
- package/src/history/service.ts +258 -0
- package/src/history/writer.ts +112 -0
- package/src/id/id.ts +87 -0
- package/src/ide/index.ts +73 -0
- package/src/inbox/inbox-ref.ts +38 -0
- package/src/inbox/inbox.sql.ts +26 -0
- package/src/inbox/inbox.ts +223 -0
- package/src/inbox/index.ts +3 -0
- package/src/inbox/render.ts +40 -0
- package/src/index.ts +260 -0
- package/src/installation/index.ts +351 -0
- package/src/installation/version.ts +8 -0
- package/src/lsp/client.ts +249 -0
- package/src/lsp/diagnostic.ts +29 -0
- package/src/lsp/index.ts +3 -0
- package/src/lsp/language.ts +120 -0
- package/src/lsp/launch.ts +21 -0
- package/src/lsp/lsp.ts +519 -0
- package/src/lsp/server.ts +1956 -0
- package/src/mcp/auth.ts +144 -0
- package/src/mcp/index.ts +944 -0
- package/src/mcp/oauth-callback.ts +232 -0
- package/src/mcp/oauth-provider.ts +214 -0
- package/src/memory/fts-query.ts +37 -0
- package/src/memory/fts.sql.ts +19 -0
- package/src/memory/index.ts +1 -0
- package/src/memory/paths.ts +116 -0
- package/src/memory/reconcile.ts +144 -0
- package/src/memory/service.ts +144 -0
- package/src/metrics/client.ts +40 -0
- package/src/metrics/event.ts +43 -0
- package/src/metrics/index.ts +5 -0
- package/src/metrics/installation.ts +18 -0
- package/src/metrics/subscriber.ts +58 -0
- package/src/metrics/util.ts +9 -0
- package/src/node.ts +6 -0
- package/src/npm/config.ts +0 -0
- package/src/npm/index.ts +293 -0
- package/src/npmcli-config.d.ts +43 -0
- package/src/patch/index.ts +680 -0
- package/src/permission/arity.ts +163 -0
- package/src/permission/evaluate.ts +15 -0
- package/src/permission/index.ts +379 -0
- package/src/permission/schema.ts +17 -0
- package/src/plugin/checkpoint-splitover.ts +60 -0
- package/src/plugin/cloud-ai.ts +329 -0
- package/src/plugin/cloudflare.ts +76 -0
- package/src/plugin/codex.ts +607 -0
- package/src/plugin/github-copilot/copilot.ts +368 -0
- package/src/plugin/github-copilot/models.ts +153 -0
- package/src/plugin/index.ts +493 -0
- package/src/plugin/install.ts +439 -0
- package/src/plugin/loader.ts +216 -0
- package/src/plugin/matcher.ts +33 -0
- package/src/plugin/meta.ts +188 -0
- package/src/plugin/mimo-free.ts +153 -0
- package/src/plugin/mimo.ts +124 -0
- package/src/plugin/shared.ts +323 -0
- package/src/plugin/subagent-progress-checker.ts +147 -0
- package/src/project/bootstrap.ts +59 -0
- package/src/project/index.ts +2 -0
- package/src/project/instance.ts +190 -0
- package/src/project/project-id.ts +48 -0
- package/src/project/project.sql.ts +16 -0
- package/src/project/project.ts +501 -0
- package/src/project/schema.ts +15 -0
- package/src/project/vcs.ts +227 -0
- package/src/provider/auth.ts +234 -0
- package/src/provider/error.ts +216 -0
- package/src/provider/index.ts +5 -0
- package/src/provider/models.ts +180 -0
- package/src/provider/provider.ts +1782 -0
- package/src/provider/schema.ts +36 -0
- package/src/provider/sdk/copilot/README.md +5 -0
- package/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts +170 -0
- package/src/provider/sdk/copilot/chat/get-response-metadata.ts +15 -0
- package/src/provider/sdk/copilot/chat/map-openai-compatible-finish-reason.ts +19 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-api-types.ts +64 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts +815 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-chat-options.ts +28 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-metadata-extractor.ts +44 -0
- package/src/provider/sdk/copilot/chat/openai-compatible-prepare-tools.ts +83 -0
- package/src/provider/sdk/copilot/copilot-provider.ts +100 -0
- package/src/provider/sdk/copilot/index.ts +2 -0
- package/src/provider/sdk/copilot/openai-compatible-error.ts +27 -0
- package/src/provider/sdk/copilot/responses/convert-to-openai-responses-input.ts +335 -0
- package/src/provider/sdk/copilot/responses/map-openai-responses-finish-reason.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-config.ts +18 -0
- package/src/provider/sdk/copilot/responses/openai-error.ts +22 -0
- package/src/provider/sdk/copilot/responses/openai-responses-api-types.ts +214 -0
- package/src/provider/sdk/copilot/responses/openai-responses-language-model.ts +1770 -0
- package/src/provider/sdk/copilot/responses/openai-responses-prepare-tools.ts +173 -0
- package/src/provider/sdk/copilot/responses/openai-responses-settings.ts +1 -0
- package/src/provider/sdk/copilot/responses/tool/code-interpreter.ts +87 -0
- package/src/provider/sdk/copilot/responses/tool/file-search.ts +127 -0
- package/src/provider/sdk/copilot/responses/tool/image-generation.ts +114 -0
- package/src/provider/sdk/copilot/responses/tool/local-shell.ts +64 -0
- package/src/provider/sdk/copilot/responses/tool/web-search-preview.ts +103 -0
- package/src/provider/sdk/copilot/responses/tool/web-search.ts +102 -0
- package/src/provider/transform.ts +1322 -0
- package/src/pty/index.ts +364 -0
- package/src/pty/pty.bun.ts +26 -0
- package/src/pty/pty.node.ts +27 -0
- package/src/pty/pty.ts +25 -0
- package/src/pty/schema.ts +17 -0
- package/src/question/index.ts +252 -0
- package/src/question/schema.ts +17 -0
- package/src/server/adapter.bun.ts +40 -0
- package/src/server/adapter.node.ts +66 -0
- package/src/server/adapter.ts +21 -0
- package/src/server/error.ts +53 -0
- package/src/server/event.ts +7 -0
- package/src/server/fence.ts +81 -0
- package/src/server/mdns.ts +60 -0
- package/src/server/middleware.ts +92 -0
- package/src/server/projectors.ts +28 -0
- package/src/server/proxy.ts +171 -0
- package/src/server/routes/control/index.ts +160 -0
- package/src/server/routes/control/workspace.ts +203 -0
- package/src/server/routes/global.ts +287 -0
- package/src/server/routes/instance/bash-interactive.ts +82 -0
- package/src/server/routes/instance/config.ts +89 -0
- package/src/server/routes/instance/event.ts +88 -0
- package/src/server/routes/instance/experimental.ts +408 -0
- package/src/server/routes/instance/file.ts +190 -0
- package/src/server/routes/instance/httpapi/config.ts +51 -0
- package/src/server/routes/instance/httpapi/permission.ts +72 -0
- package/src/server/routes/instance/httpapi/project.ts +62 -0
- package/src/server/routes/instance/httpapi/provider.ts +142 -0
- package/src/server/routes/instance/httpapi/question.ts +121 -0
- package/src/server/routes/instance/httpapi/server.ts +136 -0
- package/src/server/routes/instance/index.ts +301 -0
- package/src/server/routes/instance/mcp.ts +260 -0
- package/src/server/routes/instance/middleware.ts +35 -0
- package/src/server/routes/instance/permission.ts +73 -0
- package/src/server/routes/instance/project.ts +122 -0
- package/src/server/routes/instance/provider.ts +158 -0
- package/src/server/routes/instance/pty.ts +247 -0
- package/src/server/routes/instance/question.ts +162 -0
- package/src/server/routes/instance/session.ts +1296 -0
- package/src/server/routes/instance/sync.ts +143 -0
- package/src/server/routes/instance/trace.ts +59 -0
- package/src/server/routes/instance/tui.ts +384 -0
- package/src/server/routes/instance/workflows.ts +72 -0
- package/src/server/routes/ui.ts +55 -0
- package/src/server/server.ts +136 -0
- package/src/server/workspace.ts +122 -0
- package/src/session/auto-dream.ts +123 -0
- package/src/session/boundary.ts +77 -0
- package/src/session/budgeted-read.ts +118 -0
- package/src/session/checkpoint-align.ts +29 -0
- package/src/session/checkpoint-context.ts +36 -0
- package/src/session/checkpoint-paths.ts +86 -0
- package/src/session/checkpoint-progress-reconcile.ts +111 -0
- package/src/session/checkpoint-retry.ts +192 -0
- package/src/session/checkpoint-templates.ts +114 -0
- package/src/session/checkpoint-validator.ts +259 -0
- package/src/session/checkpoint.ts +1478 -0
- package/src/session/classify.ts +92 -0
- package/src/session/claude-import.sql.ts +13 -0
- package/src/session/claude-import.ts +379 -0
- package/src/session/compaction.ts +543 -0
- package/src/session/goal.ts +232 -0
- package/src/session/index.ts +1 -0
- package/src/session/instruction.ts +276 -0
- package/src/session/last-message-info.ts +32 -0
- package/src/session/llm-request-prefix.ts +82 -0
- package/src/session/llm.ts +735 -0
- package/src/session/max-mode.ts +397 -0
- package/src/session/message-v2.ts +1136 -0
- package/src/session/message.ts +191 -0
- package/src/session/overflow.ts +53 -0
- package/src/session/prefix-capture-ref.ts +48 -0
- package/src/session/processor.ts +962 -0
- package/src/session/projectors.ts +137 -0
- package/src/session/prompt/anthropic.txt +154 -0
- package/src/session/prompt/beast.txt +155 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/codex.txt +79 -0
- package/src/session/prompt/compose.txt +115 -0
- package/src/session/prompt/copilot-gpt-5.txt +143 -0
- package/src/session/prompt/default.txt +151 -0
- package/src/session/prompt/gemini.txt +155 -0
- package/src/session/prompt/gpt.txt +107 -0
- package/src/session/prompt/kimi.txt +95 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/trinity.txt +97 -0
- package/src/session/prompt.ts +3355 -0
- package/src/session/prune.ts +481 -0
- package/src/session/retry.ts +166 -0
- package/src/session/revert.ts +161 -0
- package/src/session/run-state.ts +135 -0
- package/src/session/schema.ts +36 -0
- package/src/session/session.sql.ts +110 -0
- package/src/session/session.ts +908 -0
- package/src/session/status.ts +89 -0
- package/src/session/summary.ts +163 -0
- package/src/session/system.ts +86 -0
- package/src/session/todo.ts +77 -0
- package/src/share/index.ts +2 -0
- package/src/share/session.ts +57 -0
- package/src/share/share-next.ts +381 -0
- package/src/share/share.sql.ts +13 -0
- package/src/shell/shell.ts +110 -0
- package/src/skill/compose/.bundle/ask/SKILL.md +58 -0
- package/src/skill/compose/.bundle/brainstorm/SKILL.md +220 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/frame-template.html +214 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/helper.js +88 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/server.cjs +354 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/start-server.sh +148 -0
- package/src/skill/compose/.bundle/brainstorm/scripts/stop-server.sh +56 -0
- package/src/skill/compose/.bundle/brainstorm/spec-document-reviewer-prompt.md +50 -0
- package/src/skill/compose/.bundle/brainstorm/visual-companion.md +287 -0
- package/src/skill/compose/.bundle/debug/CREATION-LOG.md +119 -0
- package/src/skill/compose/.bundle/debug/SKILL.md +297 -0
- package/src/skill/compose/.bundle/debug/condition-based-waiting-example.ts +158 -0
- package/src/skill/compose/.bundle/debug/condition-based-waiting.md +115 -0
- package/src/skill/compose/.bundle/debug/defense-in-depth.md +122 -0
- package/src/skill/compose/.bundle/debug/find-polluter.sh +63 -0
- package/src/skill/compose/.bundle/debug/root-cause-tracing.md +169 -0
- package/src/skill/compose/.bundle/debug/test-academic.md +14 -0
- package/src/skill/compose/.bundle/debug/test-pressure-1.md +58 -0
- package/src/skill/compose/.bundle/debug/test-pressure-2.md +68 -0
- package/src/skill/compose/.bundle/debug/test-pressure-3.md +69 -0
- package/src/skill/compose/.bundle/execute/SKILL.md +71 -0
- package/src/skill/compose/.bundle/feedback/SKILL.md +214 -0
- package/src/skill/compose/.bundle/merge/SKILL.md +252 -0
- package/src/skill/compose/.bundle/new-skill/SKILL.md +656 -0
- package/src/skill/compose/.bundle/new-skill/anthropic-best-practices.md +1150 -0
- package/src/skill/compose/.bundle/new-skill/examples/CLAUDE_MD_TESTING.md +189 -0
- package/src/skill/compose/.bundle/new-skill/graphviz-conventions.dot +172 -0
- package/src/skill/compose/.bundle/new-skill/persuasion-principles.md +187 -0
- package/src/skill/compose/.bundle/new-skill/render-graphs.js +168 -0
- package/src/skill/compose/.bundle/new-skill/testing-skills-with-subagents.md +384 -0
- package/src/skill/compose/.bundle/parallel/SKILL.md +182 -0
- package/src/skill/compose/.bundle/plan/SKILL.md +161 -0
- package/src/skill/compose/.bundle/plan/plan-document-reviewer-prompt.md +50 -0
- package/src/skill/compose/.bundle/report/SKILL.md +180 -0
- package/src/skill/compose/.bundle/review/SKILL.md +104 -0
- package/src/skill/compose/.bundle/review/code-reviewer.md +171 -0
- package/src/skill/compose/.bundle/subagent/SKILL.md +344 -0
- package/src/skill/compose/.bundle/subagent/code-quality-reviewer-prompt.md +24 -0
- package/src/skill/compose/.bundle/subagent/implementer-prompt.md +126 -0
- package/src/skill/compose/.bundle/subagent/spec-reviewer-prompt.md +112 -0
- package/src/skill/compose/.bundle/tdd/SKILL.md +372 -0
- package/src/skill/compose/.bundle/tdd/testing-anti-patterns.md +299 -0
- package/src/skill/compose/.bundle/verify/SKILL.md +140 -0
- package/src/skill/compose/.bundle/worktree/SKILL.md +234 -0
- package/src/skill/compose/LICENSE-karpathy +28 -0
- package/src/skill/compose/LICENSE-superpowers +26 -0
- package/src/skill/compose/bundle.macro.ts +30 -0
- package/src/skill/compose/extract.ts +85 -0
- package/src/skill/discovery.ts +116 -0
- package/src/skill/index.ts +311 -0
- package/src/snapshot/index.ts +777 -0
- package/src/sql.d.ts +4 -0
- package/src/storage/db.bun.ts +8 -0
- package/src/storage/db.node.ts +8 -0
- package/src/storage/db.ts +172 -0
- package/src/storage/index.ts +26 -0
- package/src/storage/json-migration.ts +426 -0
- package/src/storage/schema.sql.ts +10 -0
- package/src/storage/schema.ts +7 -0
- package/src/storage/storage.ts +331 -0
- package/src/sync/README.md +179 -0
- package/src/sync/event.sql.ts +16 -0
- package/src/sync/index.ts +278 -0
- package/src/sync/schema.ts +14 -0
- package/src/task/events.ts +28 -0
- package/src/task/gate-state.ts +54 -0
- package/src/task/gate.ts +116 -0
- package/src/task/index.ts +1 -0
- package/src/task/registry.ts +387 -0
- package/src/task/schema.ts +43 -0
- package/src/task/task.sql.ts +50 -0
- package/src/team/events.ts +22 -0
- package/src/team/index.ts +113 -0
- package/src/team/schema.ts +31 -0
- package/src/temporary.ts +33 -0
- package/src/tool/actor.shell.txt +72 -0
- package/src/tool/actor.ts +803 -0
- package/src/tool/actor.txt +103 -0
- package/src/tool/apply_patch.ts +308 -0
- package/src/tool/apply_patch.txt +33 -0
- package/src/tool/bash-interactive.ts +183 -0
- package/src/tool/bash.ts +696 -0
- package/src/tool/bash.txt +123 -0
- package/src/tool/change-directory.ts +91 -0
- package/src/tool/codesearch.ts +63 -0
- package/src/tool/codesearch.txt +12 -0
- package/src/tool/edit.ts +685 -0
- package/src/tool/edit.txt +10 -0
- package/src/tool/external-directory.ts +132 -0
- package/src/tool/glob.ts +100 -0
- package/src/tool/glob.txt +6 -0
- package/src/tool/grep.ts +145 -0
- package/src/tool/grep.txt +8 -0
- package/src/tool/history.ts +146 -0
- package/src/tool/history.txt +17 -0
- package/src/tool/index.ts +4 -0
- package/src/tool/invalid.ts +20 -0
- package/src/tool/invocation-style.ts +17 -0
- package/src/tool/lsp.ts +91 -0
- package/src/tool/lsp.txt +19 -0
- package/src/tool/mcp-exa.ts +78 -0
- package/src/tool/memory-path-guard.ts +162 -0
- package/src/tool/memory.ts +81 -0
- package/src/tool/memory.txt +69 -0
- package/src/tool/multiedit.ts +61 -0
- package/src/tool/multiedit.txt +41 -0
- package/src/tool/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +90 -0
- package/src/tool/question.ts +67 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/read.ts +327 -0
- package/src/tool/read.txt +14 -0
- package/src/tool/registry.ts +413 -0
- package/src/tool/schema.ts +17 -0
- package/src/tool/session-cwd.ts +35 -0
- package/src/tool/shell-tokenize.ts +346 -0
- package/src/tool/shell-wrap.ts +190 -0
- package/src/tool/skill.ts +76 -0
- package/src/tool/skill.txt +5 -0
- package/src/tool/task.shell.txt +57 -0
- package/src/tool/task.ts +456 -0
- package/src/tool/task.txt +56 -0
- package/src/tool/tool.ts +153 -0
- package/src/tool/truncate.ts +201 -0
- package/src/tool/truncation-dir.ts +4 -0
- package/src/tool/webfetch.ts +199 -0
- package/src/tool/webfetch.txt +13 -0
- package/src/tool/websearch/index.ts +104 -0
- package/src/tool/websearch/mimo.ts +118 -0
- package/src/tool/websearch/websearch.txt +14 -0
- package/src/tool/workflow.ts +164 -0
- package/src/tool/workflow.txt +25 -0
- package/src/tool/write.ts +88 -0
- package/src/tool/write.txt +9 -0
- package/src/util/abort.ts +35 -0
- package/src/util/archive.ts +15 -0
- package/src/util/color.ts +17 -0
- package/src/util/data-url.ts +9 -0
- package/src/util/defer.ts +10 -0
- package/src/util/effect-http-client.ts +11 -0
- package/src/util/effect-zod.ts +367 -0
- package/src/util/error.ts +78 -0
- package/src/util/filesystem.ts +243 -0
- package/src/util/fn.ts +21 -0
- package/src/util/format.ts +20 -0
- package/src/util/iife.ts +3 -0
- package/src/util/index.ts +12 -0
- package/src/util/keybind.ts +101 -0
- package/src/util/lazy.ts +18 -0
- package/src/util/local-context.ts +23 -0
- package/src/util/locale.ts +79 -0
- package/src/util/lock.ts +96 -0
- package/src/util/log.ts +197 -0
- package/src/util/media.ts +26 -0
- package/src/util/mimo-process.ts +24 -0
- package/src/util/network.ts +9 -0
- package/src/util/process.ts +174 -0
- package/src/util/queue.ts +32 -0
- package/src/util/record.ts +3 -0
- package/src/util/rpc.ts +64 -0
- package/src/util/schema.ts +53 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +5 -0
- package/src/util/update-schema.ts +13 -0
- package/src/util/which.ts +14 -0
- package/src/util/wildcard.ts +57 -0
- package/src/workflow/builtin/deep-research.js +391 -0
- package/src/workflow/builtin.ts +54 -0
- package/src/workflow/events.ts +72 -0
- package/src/workflow/meta.ts +335 -0
- package/src/workflow/persistence.ts +312 -0
- package/src/workflow/resolve.ts +45 -0
- package/src/workflow/runtime-ref.ts +18 -0
- package/src/workflow/runtime.ts +1234 -0
- package/src/workflow/sandbox.ts +280 -0
- package/src/workflow/workflow.sql.ts +31 -0
- package/src/workflow/workspace.ts +69 -0
- package/src/worktree/index.ts +614 -0
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +133 -0
- package/test/account/repo.test.ts +352 -0
- package/test/account/service.test.ts +456 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +725 -0
- package/test/actor/cancel-cascade.test.ts +432 -0
- package/test/actor/no-completion-listener.test.ts +41 -0
- package/test/actor/poststop-progress-write-permission.repro.test.ts +414 -0
- package/test/actor/registry-render.test.ts +113 -0
- package/test/actor/registry-status.test.ts +111 -0
- package/test/actor/registry.test.ts +619 -0
- package/test/actor/return-header.test.ts +40 -0
- package/test/actor/spawn-lifecycle.test.ts +346 -0
- package/test/actor/spawn-no-deadlock.test.ts +340 -0
- package/test/actor/spawn-notification.test.ts +393 -0
- package/test/actor/spawn-task-autostart.test.ts +530 -0
- package/test/actor/spawn.test.ts +1072 -0
- package/test/actor/status-event-payload.test.ts +132 -0
- package/test/actor/terminology.test.ts +39 -0
- package/test/actor/turn.test.ts +125 -0
- package/test/actor/waiter.test.ts +246 -0
- package/test/agent/agent.test.ts +874 -0
- package/test/agent/allowlist.test.ts +45 -0
- package/test/auth/auth.test.ts +86 -0
- package/test/bus/bus-effect.test.ts +162 -0
- package/test/bus/bus-integration.test.ts +87 -0
- package/test/bus/bus.test.ts +219 -0
- package/test/cli/account.test.ts +26 -0
- package/test/cli/cmd/tui/prompt-part.test.ts +47 -0
- package/test/cli/error.test.ts +18 -0
- package/test/cli/github-action.test.ts +198 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/import.test.ts +54 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/run-completion.test.ts +131 -0
- package/test/cli/tui/keybind-plugin.test.ts +90 -0
- package/test/cli/tui/plugin-add.test.ts +111 -0
- package/test/cli/tui/plugin-install.test.ts +87 -0
- package/test/cli/tui/plugin-lifecycle.test.ts +224 -0
- package/test/cli/tui/plugin-loader-entrypoint.test.ts +484 -0
- package/test/cli/tui/plugin-loader-pure.test.ts +71 -0
- package/test/cli/tui/plugin-loader.test.ts +816 -0
- package/test/cli/tui/plugin-toggle.test.ts +157 -0
- package/test/cli/tui/revert-diff.test.ts +35 -0
- package/test/cli/tui/route-agent-id.test.ts +26 -0
- package/test/cli/tui/sidebar-tps.test.ts +63 -0
- package/test/cli/tui/slot-replace.test.tsx +47 -0
- package/test/cli/tui/sync-bucket.test.ts +29 -0
- package/test/cli/tui/theme-store.test.ts +51 -0
- package/test/cli/tui/thread.test.ts +121 -0
- package/test/cli/tui/transcript.test.ts +426 -0
- package/test/cli/tui/use-event.test.tsx +175 -0
- package/test/cli/tui/voice.test.ts +269 -0
- package/test/command/deep-research-command.test.ts +16 -0
- package/test/config/agent-color.test.ts +77 -0
- package/test/config/checkpoint-fork.test.ts +21 -0
- package/test/config/config.test.ts +2577 -0
- package/test/config/fixtures/empty-frontmatter.md +4 -0
- package/test/config/fixtures/frontmatter.md +28 -0
- package/test/config/fixtures/markdown-header.md +11 -0
- package/test/config/fixtures/no-frontmatter.md +1 -0
- package/test/config/fixtures/weird-model-id.md +13 -0
- package/test/config/lsp.test.ts +87 -0
- package/test/config/markdown.test.ts +228 -0
- package/test/config/plugin.test.ts +0 -0
- package/test/config/tui.test.ts +627 -0
- package/test/control-plane/adaptors.test.ts +71 -0
- package/test/control-plane/sse.test.ts +56 -0
- package/test/effect/app-runtime-logger.test.ts +92 -0
- package/test/effect/cross-spawn-spawner.test.ts +411 -0
- package/test/effect/instance-state.test.ts +482 -0
- package/test/effect/observability.test.ts +46 -0
- package/test/effect/run-service.test.ts +46 -0
- package/test/effect/runner-warn-log.test.ts +111 -0
- package/test/effect/runner.test.ts +494 -0
- package/test/fake/provider.ts +90 -0
- package/test/file/fsmonitor.test.ts +68 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +956 -0
- package/test/file/path-traversal.test.ts +204 -0
- package/test/file/ripgrep.test.ts +214 -0
- package/test/file/watcher.test.ts +249 -0
- package/test/filesystem/filesystem.test.ts +319 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.test.ts +58 -0
- package/test/fixture/fixture.ts +190 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/lsp/fake-lsp-server.js +75 -0
- package/test/fixture/plug-worker.ts +93 -0
- package/test/fixture/plugin-meta-worker.ts +19 -0
- package/test/fixture/skills/agents-sdk/SKILL.md +152 -0
- package/test/fixture/skills/agents-sdk/references/callable.md +92 -0
- package/test/fixture/skills/cloudflare/SKILL.md +211 -0
- package/test/fixture/skills/index.json +6 -0
- package/test/fixture/tui-plugin.ts +328 -0
- package/test/fixture/tui-runtime.ts +31 -0
- package/test/format/format.test.ts +244 -0
- package/test/git/git.test.ts +128 -0
- package/test/global/fixture/global-paths-worker.ts +17 -0
- package/test/global/mimocode-home.test.ts +143 -0
- package/test/history/backfill.test.ts +149 -0
- package/test/history/extract.test.ts +106 -0
- package/test/history/fts-query.test.ts +30 -0
- package/test/history/resolve.test.ts +130 -0
- package/test/history/service.test.ts +210 -0
- package/test/history/writer.test.ts +163 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/inbox/drain-in-loop.test.ts +230 -0
- package/test/inbox/fork-agent-compat.test.ts +387 -0
- package/test/inbox/gc-on-init.test.ts +167 -0
- package/test/inbox/send-no-block.test.ts +120 -0
- package/test/inbox/sender-cancel-independence.test.ts +160 -0
- package/test/inbox/wake-matrix.test.ts +141 -0
- package/test/installation/installation.test.ts +226 -0
- package/test/keybind.test.ts +421 -0
- package/test/lib/effect.ts +53 -0
- package/test/lib/filesystem.ts +10 -0
- package/test/lib/llm-server.ts +770 -0
- package/test/lib/scripted-llm-server.ts +245 -0
- package/test/lsp/client.test.ts +98 -0
- package/test/lsp/index.test.ts +109 -0
- package/test/lsp/launch.test.ts +22 -0
- package/test/lsp/lifecycle.test.ts +184 -0
- package/test/mcp/headers.test.ts +178 -0
- package/test/mcp/lifecycle.test.ts +824 -0
- package/test/mcp/oauth-auto-connect.test.ts +281 -0
- package/test/mcp/oauth-browser.test.ts +268 -0
- package/test/mcp/oauth-callback.test.ts +34 -0
- package/test/memory/abort-leak-webfetch.ts +49 -0
- package/test/memory/abort-leak.test.ts +127 -0
- package/test/memory/cc-frontmatter.test.ts +85 -0
- package/test/memory/cc-paths.test.ts +60 -0
- package/test/memory/cc-reconcile.test.ts +239 -0
- package/test/memory/cc-search.test.ts +151 -0
- package/test/memory/fts-query.test.ts +48 -0
- package/test/memory/fts-rowid-stability.test.ts +271 -0
- package/test/memory/paths.test.ts +210 -0
- package/test/memory/reconcile.test.ts +115 -0
- package/test/memory/service.test.ts +169 -0
- package/test/npm.test.ts +18 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/abort.test.ts +116 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/disabled.test.ts +51 -0
- package/test/permission/next.test.ts +1080 -0
- package/test/permission/non-interactive.test.ts +55 -0
- package/test/permission-task.test.ts +326 -0
- package/test/plugin/actor-hooks.test.ts +1471 -0
- package/test/plugin/auth-override.test.ts +79 -0
- package/test/plugin/checkpoint-splitover.test.ts +434 -0
- package/test/plugin/cloudflare.test.ts +68 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/plugin/github-copilot-models.test.ts +163 -0
- package/test/plugin/install-concurrency.test.ts +140 -0
- package/test/plugin/install.test.ts +570 -0
- package/test/plugin/loader-shared.test.ts +1169 -0
- package/test/plugin/matcher.test.ts +97 -0
- package/test/plugin/meta.test.ts +137 -0
- package/test/plugin/mimo.test.ts +257 -0
- package/test/plugin/shared.test.ts +88 -0
- package/test/plugin/subagent-progress-checker.test.ts +227 -0
- package/test/plugin/trigger.test.ts +116 -0
- package/test/plugin/workspace-adaptor.test.ts +109 -0
- package/test/preload.ts +102 -0
- package/test/project/migrate-global.test.ts +150 -0
- package/test/project/project-id.test.ts +64 -0
- package/test/project/project.test.ts +481 -0
- package/test/project/vcs.test.ts +286 -0
- package/test/project/worktree-remove.test.ts +126 -0
- package/test/project/worktree.test.ts +214 -0
- package/test/provider/amazon-bedrock.test.ts +462 -0
- package/test/provider/copilot/convert-to-copilot-messages.test.ts +523 -0
- package/test/provider/copilot/copilot-chat-model.test.ts +592 -0
- package/test/provider/error.test.ts +160 -0
- package/test/provider/gitlab-duo.test.ts +413 -0
- package/test/provider/model-groups.test.ts +389 -0
- package/test/provider/provider-chunk-timeout.test.ts +23 -0
- package/test/provider/provider.test.ts +2648 -0
- package/test/provider/transform.test.ts +3379 -0
- package/test/pty/pty-output-isolation.test.ts +146 -0
- package/test/pty/pty-session.test.ts +102 -0
- package/test/pty/pty-shell.test.ts +69 -0
- package/test/question/question.test.ts +464 -0
- package/test/server/global-session-list.test.ts +105 -0
- package/test/server/project-init-git.test.ts +122 -0
- package/test/server/session-actions.test.ts +49 -0
- package/test/server/session-list.test.ts +110 -0
- package/test/server/session-messages.test.ts +220 -0
- package/test/server/session-prompt-busy.test.ts +146 -0
- package/test/server/session-select.test.ts +100 -0
- package/test/server/session-task-route.test.ts +165 -0
- package/test/server/summarize-route-main-slice.test.ts +99 -0
- package/test/server/trace-attributes.test.ts +76 -0
- package/test/server/workflows-route.test.ts +279 -0
- package/test/session/bootstrap-skip-system.test.ts +121 -0
- package/test/session/boundary.test.ts +33 -0
- package/test/session/budgeted-read.test.ts +74 -0
- package/test/session/checkpoint-align.test.ts +58 -0
- package/test/session/checkpoint-boundary.test.ts +186 -0
- package/test/session/checkpoint-child-session.test.ts +508 -0
- package/test/session/checkpoint-context.test.ts +141 -0
- package/test/session/checkpoint-drain.test.ts +188 -0
- package/test/session/checkpoint-extract-titles.test.ts +58 -0
- package/test/session/checkpoint-fork-mode.test.ts +576 -0
- package/test/session/checkpoint-main-slice.test.ts +259 -0
- package/test/session/checkpoint-paths.test.ts +78 -0
- package/test/session/checkpoint-permission.test.ts +136 -0
- package/test/session/checkpoint-progress-reconcile.test.ts +219 -0
- package/test/session/checkpoint-rebuild-unify.test.ts +143 -0
- package/test/session/checkpoint-rebuild-v3.test.ts +248 -0
- package/test/session/checkpoint-render-verify.test.ts +512 -0
- package/test/session/checkpoint-retry.test.ts +150 -0
- package/test/session/checkpoint-splitover-integration.test.ts +533 -0
- package/test/session/checkpoint-templates.test.ts +51 -0
- package/test/session/checkpoint-thresholds.test.ts +120 -0
- package/test/session/checkpoint-validator.test.ts +189 -0
- package/test/session/classify-integration.test.ts +476 -0
- package/test/session/classify.test.ts +335 -0
- package/test/session/compaction-agent-scope.test.ts +164 -0
- package/test/session/context-inheritance.test.ts +46 -0
- package/test/session/fork-prefix-invariant.test.ts +116 -0
- package/test/session/goal.test.ts +106 -0
- package/test/session/instruction.test.ts +387 -0
- package/test/session/invalid-output-continuation.test.ts +150 -0
- package/test/session/last-message-info.test.ts +47 -0
- package/test/session/length-tool-safety.test.ts +121 -0
- package/test/session/llm-request-prefix.test.ts +197 -0
- package/test/session/llm-retry.test.ts +59 -0
- package/test/session/llm-system-prompt.test.ts +479 -0
- package/test/session/llm.test.ts +1272 -0
- package/test/session/main-lifecycle.test.ts +51 -0
- package/test/session/main-runloop-history-invariant.test.ts +182 -0
- package/test/session/max-mode-econnreset.test.ts +229 -0
- package/test/session/max-mode.test.ts +54 -0
- package/test/session/message-v2-filter.test.ts +197 -0
- package/test/session/message-v2.test.ts +1119 -0
- package/test/session/messages-default-main.test.ts +105 -0
- package/test/session/messages-pagination.test.ts +888 -0
- package/test/session/overflow.test.ts +576 -0
- package/test/session/processor-effect.test.ts +853 -0
- package/test/session/prompt-effect.test.ts +1574 -0
- package/test/session/prompt-rebuild-loop.test.ts +108 -0
- package/test/session/prompt-rebuild-reset.test.ts +67 -0
- package/test/session/prompt-sweep.test.ts +145 -0
- package/test/session/prompt-task-gate.test.ts +127 -0
- package/test/session/prompt.test.ts +703 -0
- package/test/session/prune-main-slice.test.ts +272 -0
- package/test/session/prune-skip-system.test.ts +346 -0
- package/test/session/prune.test.ts +419 -0
- package/test/session/rebuild-microcompact.test.ts +318 -0
- package/test/session/recall-reminder.test.ts +37 -0
- package/test/session/retry.test.ts +410 -0
- package/test/session/revert-compact.test.ts +639 -0
- package/test/session/run-state-tuple-key.test.ts +152 -0
- package/test/session/session-create-registers-main.test.ts +70 -0
- package/test/session/session.test.ts +181 -0
- package/test/session/snapshot-tool-race.test.ts +301 -0
- package/test/session/structured-output-integration.test.ts +264 -0
- package/test/session/structured-output-retry.test.ts +127 -0
- package/test/session/structured-output.test.ts +397 -0
- package/test/session/summary-main-slice.test.ts +170 -0
- package/test/session/system.test.ts +72 -0
- package/test/share/share-next.test.ts +332 -0
- package/test/shell/shell.test.ts +73 -0
- package/test/skill/compose-review.test.ts +141 -0
- package/test/skill/discovery.test.ts +116 -0
- package/test/skill/skill.test.ts +465 -0
- package/test/snapshot/snapshot.test.ts +1531 -0
- package/test/storage/db.test.ts +16 -0
- package/test/storage/json-migration.test.ts +831 -0
- package/test/storage/storage.test.ts +293 -0
- package/test/sync/index.test.ts +237 -0
- package/test/task/gate-state.test.ts +66 -0
- package/test/task/gate.test.ts +167 -0
- package/test/task/registry.test.ts +152 -0
- package/test/task/state-machine.test.ts +292 -0
- package/test/team/migrate-to-inbox.test.ts +124 -0
- package/test/team/team.test.ts +75 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/actor-cancel.test.ts +206 -0
- package/test/tool/actor-recover.test.ts +50 -0
- package/test/tool/actor-send.test.ts +200 -0
- package/test/tool/actor-status.test.ts +296 -0
- package/test/tool/actor-wait.test.ts +193 -0
- package/test/tool/actor.shell.test.ts +250 -0
- package/test/tool/actor.test.ts +748 -0
- package/test/tool/apply_patch.test.ts +626 -0
- package/test/tool/bash.test.ts +1195 -0
- package/test/tool/describe-workflow.test.ts +12 -0
- package/test/tool/edit.test.ts +691 -0
- package/test/tool/external-directory.test.ts +207 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +65179 -0
- package/test/tool/glob.test.ts +81 -0
- package/test/tool/grep.test.ts +114 -0
- package/test/tool/history.test.ts +144 -0
- package/test/tool/invocation-style.test.ts +30 -0
- package/test/tool/memory-edit-ask-skip.test.ts +62 -0
- package/test/tool/memory-path-guard.test.ts +594 -0
- package/test/tool/memory.test.ts +71 -0
- package/test/tool/question.test.ts +167 -0
- package/test/tool/read.test.ts +483 -0
- package/test/tool/registry-invocation-style.test.ts +121 -0
- package/test/tool/registry.test.ts +164 -0
- package/test/tool/shell-tokenize.test.ts +273 -0
- package/test/tool/shell-wrap-missing-script.test.ts +128 -0
- package/test/tool/shell-wrap.test.ts +257 -0
- package/test/tool/skill.test.ts +99 -0
- package/test/tool/task-recover.test.ts +36 -0
- package/test/tool/task.shell.test.ts +234 -0
- package/test/tool/task.test.ts +296 -0
- package/test/tool/tool-def-shell-shape.test.ts +23 -0
- package/test/tool/tool-define.test.ts +59 -0
- package/test/tool/truncation.test.ts +253 -0
- package/test/tool/webfetch.test.ts +103 -0
- package/test/tool/whitelist.test.ts +373 -0
- package/test/tool/write.test.ts +244 -0
- package/test/util/data-url.test.ts +14 -0
- package/test/util/effect-zod.test.ts +869 -0
- package/test/util/error.test.ts +38 -0
- package/test/util/filesystem.test.ts +656 -0
- package/test/util/format.test.ts +59 -0
- package/test/util/glob.test.ts +164 -0
- package/test/util/iife.test.ts +36 -0
- package/test/util/lazy.test.ts +50 -0
- package/test/util/lock.test.ts +72 -0
- package/test/util/log.test.ts +44 -0
- package/test/util/module.test.ts +59 -0
- package/test/util/process.test.ts +128 -0
- package/test/util/timeout.test.ts +21 -0
- package/test/util/which.test.ts +100 -0
- package/test/util/wildcard.test.ts +90 -0
- package/test/workflow/builtin.test.ts +22 -0
- package/test/workflow/deep-research-cluster.test.ts +47 -0
- package/test/workflow/lib.ts +243 -0
- package/test/workflow/meta.test.ts +142 -0
- package/test/workflow/model-routing.test.ts +68 -0
- package/test/workflow/persistence.test.ts +229 -0
- package/test/workflow/resolve.test.ts +37 -0
- package/test/workflow/runtime-nested.test.ts +419 -0
- package/test/workflow/runtime-worktree.test.ts +261 -0
- package/test/workflow/runtime.test.ts +1078 -0
- package/test/workflow/sandbox.test.ts +259 -0
- package/test/workflow/tool.test.ts +473 -0
- package/test/workflow/verify-wow.test.ts +144 -0
- package/test/workflow/workspace.test.ts +88 -0
- package/test/workspace/workspace-restore.test.ts +281 -0
- package/test/worktree/index.test.ts +30 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,1234 @@
|
|
|
1
|
+
import { Context, Deferred, Effect, Exit, Fiber, Layer, Scope } from "effect"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import { createHash } from "node:crypto"
|
|
4
|
+
import { spawnRef } from "@/actor/spawn-ref"
|
|
5
|
+
import { workflowRef } from "./runtime-ref"
|
|
6
|
+
import { Config } from "@/config"
|
|
7
|
+
import { EffectBridge } from "@/effect"
|
|
8
|
+
import { Bus } from "@/bus"
|
|
9
|
+
import { Inbox } from "@/inbox"
|
|
10
|
+
import { Worktree } from "@/worktree"
|
|
11
|
+
import { Provider } from "@/provider"
|
|
12
|
+
import { InstanceRef } from "@/effect/instance-ref"
|
|
13
|
+
import { Instance } from "@/project/instance"
|
|
14
|
+
import { Identifier } from "@/id/id"
|
|
15
|
+
import type { SessionID } from "@/session/schema"
|
|
16
|
+
import type { ProviderID, ModelID } from "@/provider/schema"
|
|
17
|
+
import { parseMeta } from "./meta"
|
|
18
|
+
import { evalScript, type HostFn } from "./sandbox"
|
|
19
|
+
import { makeFileHooks, resolveInWorkspace } from "./workspace"
|
|
20
|
+
import { isInlineScript, resolveWorkflowScript } from "./resolve"
|
|
21
|
+
import { WorkflowAgentFailed, WorkflowChildFailed, WorkflowFinished, WorkflowLog, WorkflowPhase, WorkflowStarted } from "./events"
|
|
22
|
+
import { WorkflowPersistence, journalKeyBase } from "./persistence"
|
|
23
|
+
import type { RunSummary } from "./persistence"
|
|
24
|
+
import { Log, Lock } from "@/util"
|
|
25
|
+
|
|
26
|
+
const log = Log.create({ service: "workflow.runtime" })
|
|
27
|
+
|
|
28
|
+
/** Default wall-clock budget for a whole workflow script (12h research default). */
|
|
29
|
+
const SCRIPT_DEADLINE_MS = 12 * 60 * 60 * 1000
|
|
30
|
+
/** Unique sentinel for the per-agent timeout race: a timeout winner can never
|
|
31
|
+
* collide with an agent deliverable (those are object | string | null). */
|
|
32
|
+
const STRAGGLER_TIMEOUT = Symbol("straggler-timeout")
|
|
33
|
+
/** Hard ceiling on total agents a single run may spawn (lifecycle cap). */
|
|
34
|
+
const MAX_LIFECYCLE_AGENTS = 1000
|
|
35
|
+
/** Default soft cap on concurrent agents when the caller does not specify one. */
|
|
36
|
+
const DEFAULT_MAX_CONCURRENT = 16
|
|
37
|
+
/** Marker prefix on errors from STRUCTURAL workflow faults (cycle, over-depth,
|
|
38
|
+
* unknown name) — workflow-wiring bugs that must fail the whole tree loud rather
|
|
39
|
+
* than degrade to the never-throw null that a child's RUNTIME failure yields. The
|
|
40
|
+
* workflow() hook re-propagates any child outcome whose error carries this marker,
|
|
41
|
+
* so the fault surfaces at the root run the user launched. */
|
|
42
|
+
const WORKFLOW_STRUCTURAL_ERROR = "WorkflowStructuralError"
|
|
43
|
+
|
|
44
|
+
type RunStatus = "running" | "completed" | "failed" | "cancelled"
|
|
45
|
+
|
|
46
|
+
export type RunOutcome =
|
|
47
|
+
| { status: "completed"; result: unknown }
|
|
48
|
+
| { status: "failed"; error: string }
|
|
49
|
+
| { status: "cancelled" }
|
|
50
|
+
|
|
51
|
+
interface RunEntry {
|
|
52
|
+
runID: string
|
|
53
|
+
sessionID: SessionID
|
|
54
|
+
status: RunStatus
|
|
55
|
+
deferred: Deferred.Deferred<RunOutcome>
|
|
56
|
+
fiber: Fiber.Fiber<void> | undefined
|
|
57
|
+
childActorIDs: Set<string>
|
|
58
|
+
worktrees: Set<string> // worktree directories pending disposition, for cancel cleanup
|
|
59
|
+
childRunIDs: Set<string> // child workflow runIDs, for recursive cancel/reclaim
|
|
60
|
+
name: string
|
|
61
|
+
running: number
|
|
62
|
+
succeeded: number
|
|
63
|
+
failed: number
|
|
64
|
+
agentCount: number
|
|
65
|
+
capWarned: boolean
|
|
66
|
+
// Model refs already warned about this run, so an unresolvable ref (e.g. a
|
|
67
|
+
// workflow using "lite" with no model_groups.lite configured) logs ONCE per
|
|
68
|
+
// run instead of once per agent spawn. Per-run, not layer-global, so a later
|
|
69
|
+
// run re-warns. See resolveAgentModel.
|
|
70
|
+
warnedModelRefs: Set<string>
|
|
71
|
+
currentPhase: string | undefined
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface StartInput {
|
|
75
|
+
script: string
|
|
76
|
+
sessionID: SessionID
|
|
77
|
+
parentActorID: string
|
|
78
|
+
args?: unknown
|
|
79
|
+
model?: { providerID: ProviderID; modelID: ModelID }
|
|
80
|
+
maxConcurrentAgents?: number
|
|
81
|
+
// Hard ceiling on total agents this run may spawn (lifecycle cap). Defaults to
|
|
82
|
+
// MAX_LIFECYCLE_AGENTS (1000). Over-cap agent() calls return null (graceful
|
|
83
|
+
// degradation, never-throw), NOT throw — so a fan-out that wants more agents
|
|
84
|
+
// than the cap degrades to the cap-limited subset instead of aborting the run.
|
|
85
|
+
// Lowerable for tests; tunable in prod.
|
|
86
|
+
maxLifecycleAgents?: number
|
|
87
|
+
/** Per-agent wall-clock timeout (ms). When an individual agent() call's spawned
|
|
88
|
+
* child produces no terminal outcome within this window, it is gracefully
|
|
89
|
+
* cancelled and agent() resolves to null (the never-throw failure sentinel), so
|
|
90
|
+
* one hung agent (e.g. an LLM TTFT wall) cannot stall a parallel/pipeline barrier
|
|
91
|
+
* indefinitely. Default undefined = OFF (only the global scriptDeadlineMs bounds a
|
|
92
|
+
* run). A per-call agent(prompt,{timeoutMs}) overrides this. */
|
|
93
|
+
agentTimeoutMs?: number
|
|
94
|
+
scriptDeadlineMs?: number
|
|
95
|
+
// Internal (resume-only): when true, launch ignores any persisted journal and
|
|
96
|
+
// truncates the stale `.jsonl` before the run appends. resume() sets this on the
|
|
97
|
+
// script-change path (stored script_sha != current script's sha, MR104 P1-2) so
|
|
98
|
+
// an EDITED script never replays results journaled against the OLD body. start()
|
|
99
|
+
// never sets it (a fresh runID has no prior journal — nothing to invalidate).
|
|
100
|
+
freshJournal?: boolean
|
|
101
|
+
/** Root dir the guest's file primitives (readFile/writeFile/glob/exists) are
|
|
102
|
+
* jailed to. Defaults to the caller's worktree. A child workflow inherits the
|
|
103
|
+
* parent's workspace unless its workflow() opts override it. */
|
|
104
|
+
workspace?: string
|
|
105
|
+
/** Resolved names of ancestor workflows (root = empty). A workflow() whose
|
|
106
|
+
* resolved child name is already here is a cycle → throw. */
|
|
107
|
+
lineage?: readonly string[]
|
|
108
|
+
/** Current nesting depth (root run = 0). */
|
|
109
|
+
depth?: number
|
|
110
|
+
/** Max nesting depth before workflow() throws. Defaults to config (8). */
|
|
111
|
+
maxDepth?: number
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Options the guest may pass to `agent(prompt, opts?)`. */
|
|
115
|
+
interface AgentOpts {
|
|
116
|
+
agentType?: string
|
|
117
|
+
tools?: readonly string[]
|
|
118
|
+
/** A model reference resolved host-side via Provider.resolveModelRef: either a
|
|
119
|
+
* "provider/model" literal or a configured tier/group name (e.g. "lite").
|
|
120
|
+
* Omitted → the run's default model. Unknown group → falls back to the run
|
|
121
|
+
* default (never throws to the guest). */
|
|
122
|
+
model?: string
|
|
123
|
+
schema?: Record<string, unknown>
|
|
124
|
+
isolation?: "worktree"
|
|
125
|
+
label?: string
|
|
126
|
+
phase?: string
|
|
127
|
+
/** Per-call override of the run's agentTimeoutMs (ms). */
|
|
128
|
+
timeoutMs?: number
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface Interface {
|
|
132
|
+
readonly start: (input: StartInput) => Effect.Effect<{ runID: string }>
|
|
133
|
+
readonly status: (input: {
|
|
134
|
+
runID: string
|
|
135
|
+
}) => Effect.Effect<{ status: RunStatus | "unknown"; agentCount: number; currentPhase?: string }>
|
|
136
|
+
readonly wait: (input: { runID: string; timeoutMs?: number }) => Effect.Effect<RunOutcome>
|
|
137
|
+
readonly cancel: (input: { runID: string }) => Effect.Effect<void>
|
|
138
|
+
readonly list: (input?: { sessionID?: SessionID }) => Effect.Effect<RunSummary[]>
|
|
139
|
+
readonly resume: (input: { runID: string; agentTimeoutMs?: number }) => Effect.Effect<{ runID: string; resumed: boolean }>
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export class Service extends Context.Service<Service, Interface>()("@tulingcode/WorkflowRuntime") {}
|
|
143
|
+
|
|
144
|
+
/** A plain promise-based semaphore: at most `max` concurrent `run` callbacks. */
|
|
145
|
+
function makeSemaphore(max: number) {
|
|
146
|
+
let active = 0
|
|
147
|
+
const queue: Array<() => void> = []
|
|
148
|
+
const release = () => {
|
|
149
|
+
active--
|
|
150
|
+
const next = queue.shift()
|
|
151
|
+
if (next) next()
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
run<T>(fn: () => Promise<T>): Promise<T> {
|
|
155
|
+
return new Promise<T>((resolve, reject) => {
|
|
156
|
+
const attempt = () => {
|
|
157
|
+
active++
|
|
158
|
+
fn().then(
|
|
159
|
+
(value) => {
|
|
160
|
+
release()
|
|
161
|
+
resolve(value)
|
|
162
|
+
},
|
|
163
|
+
(err) => {
|
|
164
|
+
release()
|
|
165
|
+
reject(err)
|
|
166
|
+
},
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
if (active < max) attempt()
|
|
170
|
+
else queue.push(attempt)
|
|
171
|
+
})
|
|
172
|
+
},
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function cpuCount(): number {
|
|
177
|
+
const n = os.cpus().length
|
|
178
|
+
return n > 0 ? n : 4
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export const layer = Layer.effect(
|
|
182
|
+
Service,
|
|
183
|
+
Effect.gen(function* () {
|
|
184
|
+
const bus = yield* Bus.Service
|
|
185
|
+
const inbox = yield* Inbox.Service
|
|
186
|
+
const worktree = yield* Worktree.Service
|
|
187
|
+
const provider = yield* Provider.Service
|
|
188
|
+
// Resolve the Config service handle at layer scope (a legitimate layer dep,
|
|
189
|
+
// satisfied by Config.defaultLayer) so the requirement is discharged here and
|
|
190
|
+
// does NOT leak into start/resume's effect signatures. Only config.get() runs
|
|
191
|
+
// lazily below — it reads the per-instance ALS context and returns Effect<Info>
|
|
192
|
+
// with no requirement, so it stays out of the public method types.
|
|
193
|
+
const config = yield* Config.Service
|
|
194
|
+
const scope = yield* Scope.Scope
|
|
195
|
+
const runs = new Map<string, RunEntry>()
|
|
196
|
+
|
|
197
|
+
// Resolve a guest-supplied model ref (a "provider/model" literal OR a
|
|
198
|
+
// tier/group name like "lite") to a concrete {providerID, modelID} via the
|
|
199
|
+
// Provider service — host-side, inside the runtime's own Layer scope (so it
|
|
200
|
+
// survives resume(), which re-reads the script with no fresh StartInput).
|
|
201
|
+
// NEVER throws to the guest: an unknown group (resolveModelRef throwing
|
|
202
|
+
// ModelGroupNotFoundError) falls back to the run default, matching agent()'s
|
|
203
|
+
// never-throw contract. undefined ref → the run default unchanged.
|
|
204
|
+
const resolveAgentModel = (
|
|
205
|
+
ref: string | undefined,
|
|
206
|
+
fallback: { providerID: ProviderID; modelID: ModelID } | undefined,
|
|
207
|
+
warned: Set<string>,
|
|
208
|
+
): Effect.Effect<{ providerID: ProviderID; modelID: ModelID } | undefined> =>
|
|
209
|
+
ref === undefined
|
|
210
|
+
? Effect.succeed(fallback)
|
|
211
|
+
: provider.resolveModelRef(ref).pipe(
|
|
212
|
+
Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })),
|
|
213
|
+
Effect.catchCause(() =>
|
|
214
|
+
Effect.sync(() => {
|
|
215
|
+
// Leave a breadcrumb so a bad ref isn't pure silence: an unknown
|
|
216
|
+
// group/tier, a typo, or an out-of-tree script passing the old
|
|
217
|
+
// {providerID, modelID} object (not a string) all land here and
|
|
218
|
+
// silently use the run default. Warn ONCE per unique ref per run
|
|
219
|
+
// (a fan-out like deep-research would otherwise log on every
|
|
220
|
+
// agent spawn). For a non-string ref, log its sorted keys (e.g.
|
|
221
|
+
// "modelID,providerID") so the operator sees it's the legacy
|
|
222
|
+
// object shape — keys are schema names, no user data.
|
|
223
|
+
const shown =
|
|
224
|
+
typeof ref === "string"
|
|
225
|
+
? ref
|
|
226
|
+
: `{${Object.keys(ref as object)
|
|
227
|
+
.sort()
|
|
228
|
+
.join(",")}}`
|
|
229
|
+
if (!warned.has(shown)) {
|
|
230
|
+
warned.add(shown)
|
|
231
|
+
log.warn("workflow agent model ref did not resolve — using run default", { ref: shown })
|
|
232
|
+
}
|
|
233
|
+
return fallback
|
|
234
|
+
}),
|
|
235
|
+
),
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
// Process-wide concurrency ceiling: ONE semaphore shared by every run
|
|
239
|
+
// (including nested children), so tree-wide concurrent agents can never
|
|
240
|
+
// exceed it regardless of nesting depth. It is a PURE process/config property,
|
|
241
|
+
// sized SOLELY from config.workflow.maxConcurrentAgents (falling back to the
|
|
242
|
+
// min(16, 2×cores) default) — NEVER seeded or raised by any per-launch
|
|
243
|
+
// maxConcurrentAgents input. A per-run input only ever NARROWS that run's own
|
|
244
|
+
// semaphore (clamped ≤ global, below); it can neither raise the global nor bind
|
|
245
|
+
// a later run to an earlier run's cap. Resolved LAZILY on the first launch
|
|
246
|
+
// (config.get reads the per-instance ALS context, live inside launch but NOT at
|
|
247
|
+
// layer-build time) and memoized at service scope so every subsequent launch() —
|
|
248
|
+
// including nested children — shares the same semaphore. `cfg`/`globalMax`/
|
|
249
|
+
// `globalSem` are reused by later tasks (T12 maxDepth, T14 maxLifecycleAgents).
|
|
250
|
+
let cfg: Config.Info | undefined
|
|
251
|
+
let globalMax = 0
|
|
252
|
+
let globalSem: ReturnType<typeof makeSemaphore> | undefined
|
|
253
|
+
const ensureGlobal = Effect.fn("WorkflowRuntime.ensureGlobal")(function* () {
|
|
254
|
+
if (globalSem) return globalSem
|
|
255
|
+
// Resolve config once (this is the only suspension point). Cached on `cfg`
|
|
256
|
+
// for reuse by later per-run reads (maxDepth, maxLifecycleAgents).
|
|
257
|
+
cfg ??= yield* config.get()
|
|
258
|
+
globalMax = Math.max(
|
|
259
|
+
1,
|
|
260
|
+
cfg.workflow?.maxConcurrentAgents ?? Math.min(DEFAULT_MAX_CONCURRENT, 2 * Math.max(1, cpuCount())),
|
|
261
|
+
)
|
|
262
|
+
// Assign synchronously with ??= so two concurrent first-launches that both
|
|
263
|
+
// passed the guard above (the `config.get()` await is a suspension point)
|
|
264
|
+
// converge on ONE semaphore instead of transiently doubling the ceiling.
|
|
265
|
+
// Frozen for the process lifetime: a later config change to
|
|
266
|
+
// maxConcurrentAgents does NOT rebuild it (acceptable while workflow is
|
|
267
|
+
// experimental — the global ceiling is a process/config property).
|
|
268
|
+
globalSem ??= makeSemaphore(globalMax)
|
|
269
|
+
return globalSem
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
// Debounced counter flush: coalesce high-rate running/succeeded/failed updates
|
|
273
|
+
// to at most one DB write per ~250ms per run. flushNow is the synchronous final
|
|
274
|
+
// flush on terminal. All best-effort.
|
|
275
|
+
const flushTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
276
|
+
const flushNow = (entry: RunEntry) => {
|
|
277
|
+
const t = flushTimers.get(entry.runID)
|
|
278
|
+
if (t) {
|
|
279
|
+
clearTimeout(t)
|
|
280
|
+
flushTimers.delete(entry.runID)
|
|
281
|
+
}
|
|
282
|
+
return WorkflowPersistence.flushCounters({
|
|
283
|
+
runID: entry.runID,
|
|
284
|
+
running: entry.running,
|
|
285
|
+
succeeded: entry.succeeded,
|
|
286
|
+
failed: entry.failed,
|
|
287
|
+
}).pipe(Effect.ignore)
|
|
288
|
+
}
|
|
289
|
+
const scheduleFlush = (entry: RunEntry) => {
|
|
290
|
+
if (flushTimers.has(entry.runID)) return
|
|
291
|
+
flushTimers.set(
|
|
292
|
+
entry.runID,
|
|
293
|
+
setTimeout(() => {
|
|
294
|
+
flushTimers.delete(entry.runID)
|
|
295
|
+
Effect.runFork(
|
|
296
|
+
WorkflowPersistence.flushCounters({
|
|
297
|
+
runID: entry.runID,
|
|
298
|
+
running: entry.running,
|
|
299
|
+
succeeded: entry.succeeded,
|
|
300
|
+
failed: entry.failed,
|
|
301
|
+
}).pipe(Effect.ignore),
|
|
302
|
+
)
|
|
303
|
+
}, 250),
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Best-effort cleanup for a NON-SUCCESS terminal (cancel, deadline, script
|
|
308
|
+
// failure): graceful-cancel any in-flight child agents and remove every
|
|
309
|
+
// worktree the run still owns, then clear the set. NEVER throws — a reclaim
|
|
310
|
+
// failure must not mask the original terminal cause. NOT called on success:
|
|
311
|
+
// kept (success+changed) worktrees are the deliverable and must survive.
|
|
312
|
+
const reclaim = (entry: RunEntry) =>
|
|
313
|
+
Effect.gen(function* () {
|
|
314
|
+
const actor = spawnRef.current
|
|
315
|
+
if (actor) {
|
|
316
|
+
yield* Effect.forEach(
|
|
317
|
+
[...entry.childActorIDs],
|
|
318
|
+
(childID) => actor.cancel(entry.sessionID, childID, "graceful").pipe(Effect.ignore),
|
|
319
|
+
{ concurrency: "unbounded", discard: true },
|
|
320
|
+
)
|
|
321
|
+
}
|
|
322
|
+
yield* Effect.forEach(
|
|
323
|
+
[...entry.worktrees],
|
|
324
|
+
(directory) => worktree.remove({ directory }).pipe(Effect.ignore),
|
|
325
|
+
{ concurrency: "unbounded", discard: true },
|
|
326
|
+
)
|
|
327
|
+
entry.worktrees.clear()
|
|
328
|
+
// Recurse into child workflow RUNS (populated by workflow()). Cancelling the
|
|
329
|
+
// orchestrator tears down the whole tree — a child still "running" here is
|
|
330
|
+
// cancelled via cancelEntry (mutually recursive with reclaim).
|
|
331
|
+
// SAFETY: childRunIDs edges are parent→child only (added solely at the
|
|
332
|
+
// workflow() call site with a freshly-minted child runID), so the graph is a
|
|
333
|
+
// tree and this recursion is finite. The status-flip guard alone does NOT stop
|
|
334
|
+
// a cycle (a node's flip is post-order, after its reclaim returns), so
|
|
335
|
+
// acyclicity is load-bearing — the workflow() cycle guard (Task 12) is what
|
|
336
|
+
// keeps it true as the call graph grows.
|
|
337
|
+
yield* Effect.forEach(
|
|
338
|
+
[...entry.childRunIDs],
|
|
339
|
+
(childRunID) =>
|
|
340
|
+
Effect.gen(function* () {
|
|
341
|
+
const child = runs.get(childRunID)
|
|
342
|
+
if (child && child.status === "running") yield* cancelEntry(child)
|
|
343
|
+
}).pipe(Effect.ignore),
|
|
344
|
+
{ concurrency: "unbounded", discard: true },
|
|
345
|
+
)
|
|
346
|
+
})
|
|
347
|
+
|
|
348
|
+
const cancelEntry = (entry: RunEntry): Effect.Effect<void> =>
|
|
349
|
+
Effect.gen(function* () {
|
|
350
|
+
if (entry.status !== "running") return
|
|
351
|
+
yield* reclaim(entry)
|
|
352
|
+
yield* flushNow(entry)
|
|
353
|
+
yield* WorkflowPersistence.recordTerminal({ runID: entry.runID, status: "cancelled" }).pipe(Effect.ignore)
|
|
354
|
+
if (entry.fiber) yield* Fiber.interrupt(entry.fiber)
|
|
355
|
+
entry.status = "cancelled"
|
|
356
|
+
yield* Deferred.succeed(entry.deferred, { status: "cancelled" })
|
|
357
|
+
yield* bus.publish(WorkflowFinished, { sessionID: entry.sessionID, runID: entry.runID, status: "cancelled" })
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
const waitFor = (childRunID: string) =>
|
|
361
|
+
Effect.gen(function* () {
|
|
362
|
+
const child = runs.get(childRunID)
|
|
363
|
+
if (!child) return { status: "failed" as const, error: "child run missing" }
|
|
364
|
+
return yield* Deferred.await(child.deferred)
|
|
365
|
+
})
|
|
366
|
+
|
|
367
|
+
const launch = Effect.fn("WorkflowRuntime.launch")(function* (input: StartInput, runID: string, name: string) {
|
|
368
|
+
// The guest body is the script with the `meta` literal blanked out (parseMeta
|
|
369
|
+
// preserves line numbers). start already validated meta and resume only loads
|
|
370
|
+
// a previously-validated script, so this parse is purely to extract the body;
|
|
371
|
+
// it never gates here. Fall back to the raw script if parse somehow fails.
|
|
372
|
+
const parsed = parseMeta(input.script)
|
|
373
|
+
const body = parsed.ok ? parsed.body : input.script
|
|
374
|
+
// Resolve the workspace root ONCE at launch (the Instance ALS context is
|
|
375
|
+
// live here — the bridge below captures it). Default = the caller's
|
|
376
|
+
// worktree. Captured in the closure so the file hooks read it synchronously
|
|
377
|
+
// and never touch ALS from inside the forked work fiber.
|
|
378
|
+
const workspaceRoot = input.workspace ?? Instance.worktree
|
|
379
|
+
const fileHooks = makeFileHooks(workspaceRoot)
|
|
380
|
+
const deferred = yield* Deferred.make<RunOutcome>()
|
|
381
|
+
const entry: RunEntry = {
|
|
382
|
+
runID,
|
|
383
|
+
sessionID: input.sessionID,
|
|
384
|
+
status: "running",
|
|
385
|
+
deferred,
|
|
386
|
+
fiber: undefined,
|
|
387
|
+
childActorIDs: new Set<string>(),
|
|
388
|
+
worktrees: new Set<string>(),
|
|
389
|
+
childRunIDs: new Set<string>(),
|
|
390
|
+
name,
|
|
391
|
+
running: 0,
|
|
392
|
+
succeeded: 0,
|
|
393
|
+
failed: 0,
|
|
394
|
+
agentCount: 0,
|
|
395
|
+
capWarned: false,
|
|
396
|
+
warnedModelRefs: new Set<string>(),
|
|
397
|
+
currentPhase: undefined,
|
|
398
|
+
}
|
|
399
|
+
runs.set(runID, entry)
|
|
400
|
+
// Stamp a sha256 of the FULL script body (the exact bytes writeScript persists
|
|
401
|
+
// and resume's readScript reads back), so resume can detect a between-cycle
|
|
402
|
+
// edit by comparing this to the current file's sha — apples-to-apples, MR104
|
|
403
|
+
// P1-2. recordStart re-stamps it on every (re)launch, so a changed-script
|
|
404
|
+
// relaunch overwrites the stale sha and a subsequent resume replays correctly.
|
|
405
|
+
const scriptSha = createHash("sha256").update(input.script).digest("hex")
|
|
406
|
+
yield* WorkflowPersistence.recordStart({
|
|
407
|
+
runID,
|
|
408
|
+
sessionID: input.sessionID,
|
|
409
|
+
name,
|
|
410
|
+
parentActorID: input.parentActorID,
|
|
411
|
+
args: input.args,
|
|
412
|
+
scriptSha,
|
|
413
|
+
agentTimeoutMs: input.agentTimeoutMs,
|
|
414
|
+
}).pipe(Effect.ignore)
|
|
415
|
+
yield* WorkflowPersistence.writeScript(runID, input.script).pipe(Effect.ignore)
|
|
416
|
+
|
|
417
|
+
// Replay journal: prior agent() results (empty on a fresh run). On resume,
|
|
418
|
+
// a cache hit returns instantly with no spawn; misses spawn + append. The
|
|
419
|
+
// occ counter disambiguates byte-identical calls into distinct slots.
|
|
420
|
+
// freshJournal (resume's script-change path) truncates the stale `.jsonl`
|
|
421
|
+
// FIRST so loadJournal returns empty AND the run's appends don't interleave
|
|
422
|
+
// with results journaled against the old script body — a later resume would
|
|
423
|
+
// otherwise read both and replay the wrong results.
|
|
424
|
+
if (input.freshJournal) yield* WorkflowPersistence.clearJournal(runID).pipe(Effect.ignore)
|
|
425
|
+
const journal = yield* WorkflowPersistence.loadJournal(runID)
|
|
426
|
+
const occ = new Map<string, number>()
|
|
427
|
+
const pass = journal.pass
|
|
428
|
+
|
|
429
|
+
// Capture the bridge BEFORE forking so it snapshots the caller's
|
|
430
|
+
// Instance/Workspace context — the quickjs Promise boundary in agent()
|
|
431
|
+
// would otherwise lose it.
|
|
432
|
+
const bridge = yield* EffectBridge.make()
|
|
433
|
+
|
|
434
|
+
// Resolve the process-wide ceiling NOW (under the live Instance context) so
|
|
435
|
+
// its semaphore object exists before any spawn site closes over it. Sized
|
|
436
|
+
// PURELY from config (memoized after the first launch); a per-launch
|
|
437
|
+
// maxConcurrentAgents never seeds or raises it — it only narrows this run's
|
|
438
|
+
// own semaphore below.
|
|
439
|
+
const globalSemLocal = yield* ensureGlobal()
|
|
440
|
+
// Nesting safety (T12): carried through every run. lineage = resolved names of
|
|
441
|
+
// ancestor workflows (root = empty); depth = this run's level (root = 0). A
|
|
442
|
+
// workflow() whose child name is already in lineage is a cycle, and a child
|
|
443
|
+
// beyond maxDepth is over-deep — both throw at the call site (workflowHook).
|
|
444
|
+
// maxDepth precedence: explicit per-run input > config > module default 8.
|
|
445
|
+
const lineage = input.lineage ?? []
|
|
446
|
+
const depth = input.depth ?? 0
|
|
447
|
+
const maxDepth = input.maxDepth ?? cfg?.workflow?.maxDepth ?? 8
|
|
448
|
+
// Per-run soft cap: defaults to the global ceiling, clamped to ≤ global so a
|
|
449
|
+
// child can shrink its own concurrency but never exceed the process ceiling.
|
|
450
|
+
// The 2×cores clamp is GONE — the global semaphore is the real throttle.
|
|
451
|
+
const requested = input.maxConcurrentAgents ?? globalMax
|
|
452
|
+
const max = Math.max(1, Math.min(requested, globalMax))
|
|
453
|
+
const sem = makeSemaphore(max)
|
|
454
|
+
// Lifecycle cap (total agents over the run's life). Resolved once here so
|
|
455
|
+
// both spawn paths (shared + isolated) share it; over-cap calls return null.
|
|
456
|
+
const lifecycleCap = input.maxLifecycleAgents ?? cfg?.workflow?.maxLifecycleAgents ?? MAX_LIFECYCLE_AGENTS
|
|
457
|
+
// Over-cap → null (see maxLifecycleAgents doc): warn ONCE per run so the
|
|
458
|
+
// dropped work is visible without spamming a log line per over-cap call.
|
|
459
|
+
const warnCapOnce = () => {
|
|
460
|
+
if (entry.capWarned) return
|
|
461
|
+
entry.capWarned = true
|
|
462
|
+
log.warn("workflow lifecycle agent cap reached — over-cap agents return null", {
|
|
463
|
+
runID,
|
|
464
|
+
cap: lifecycleCap,
|
|
465
|
+
})
|
|
466
|
+
}
|
|
467
|
+
// Per-agent wall-clock timeout. Run-level default (OFF unless set); a per-call
|
|
468
|
+
// opts.timeoutMs overrides it. Resolved per agent() call since opts is per-call.
|
|
469
|
+
const runAgentTimeoutMs = input.agentTimeoutMs
|
|
470
|
+
// Race a child's outcome-await against the effective per-agent timeout. On a
|
|
471
|
+
// TRUE timeout: gracefully cancel that one child (the lever reclaim uses) and
|
|
472
|
+
// yield null — the never-throw sentinel the guest already tolerates, so a hung
|
|
473
|
+
// agent can't stall a parallel/pipeline barrier. A genuine null deliverable
|
|
474
|
+
// (agent failed fast) is NOT a timeout → no cancel. No timeout configured
|
|
475
|
+
// (undefined / <=0) ⇒ await unbounded (current behavior, only scriptDeadline bounds).
|
|
476
|
+
const awaitWithTimeout = <A>(
|
|
477
|
+
actorID: string,
|
|
478
|
+
opts: AgentOpts,
|
|
479
|
+
await_: Effect.Effect<A | null>,
|
|
480
|
+
// Optional side-channel: set when the timeout branch wins, so the caller
|
|
481
|
+
// can distinguish a TRUE timeout (reason="timeout") from a fast actor-error
|
|
482
|
+
// null. Never throws; called once at most. Pure observability.
|
|
483
|
+
onTimeout?: () => void,
|
|
484
|
+
) => {
|
|
485
|
+
const ms = opts.timeoutMs ?? runAgentTimeoutMs
|
|
486
|
+
if (!ms || ms <= 0) return await_
|
|
487
|
+
return Effect.raceFirst(
|
|
488
|
+
await_,
|
|
489
|
+
Effect.sleep(`${ms} millis`).pipe(Effect.as(STRAGGLER_TIMEOUT as unknown as A | null)),
|
|
490
|
+
).pipe(
|
|
491
|
+
Effect.flatMap((r) =>
|
|
492
|
+
r === (STRAGGLER_TIMEOUT as unknown)
|
|
493
|
+
? (spawnRef.current
|
|
494
|
+
? spawnRef.current.cancel(input.sessionID, actorID, "graceful").pipe(Effect.ignore)
|
|
495
|
+
: Effect.void
|
|
496
|
+
).pipe(
|
|
497
|
+
Effect.tap(() =>
|
|
498
|
+
Effect.sync(() => {
|
|
499
|
+
try {
|
|
500
|
+
onTimeout?.()
|
|
501
|
+
} catch {
|
|
502
|
+
/* observability must never escape */
|
|
503
|
+
}
|
|
504
|
+
}),
|
|
505
|
+
),
|
|
506
|
+
Effect.as(null),
|
|
507
|
+
)
|
|
508
|
+
: Effect.succeed(r),
|
|
509
|
+
),
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
// Publish a WorkflowAgentFailed event for an agent() call that resolved to
|
|
514
|
+
// null. Pure observability — counters and the agent() return value are
|
|
515
|
+
// unaffected. Wrapped in try/catch so a bus problem can never break a run.
|
|
516
|
+
type FailReason = "over-cap" | "spawn-reject" | "timeout" | "actor-error" | "no-deliverable"
|
|
517
|
+
const publishAgentFailed = (
|
|
518
|
+
o: AgentOpts,
|
|
519
|
+
reason: FailReason,
|
|
520
|
+
info: { actorID?: string; errorMessage?: string } = {},
|
|
521
|
+
) => {
|
|
522
|
+
try {
|
|
523
|
+
Effect.runFork(
|
|
524
|
+
bus
|
|
525
|
+
.publish(WorkflowAgentFailed, {
|
|
526
|
+
sessionID: input.sessionID,
|
|
527
|
+
runID,
|
|
528
|
+
actorID: info.actorID,
|
|
529
|
+
agentType: o.agentType ?? "general",
|
|
530
|
+
label: o.label,
|
|
531
|
+
phase: o.phase ?? entry.currentPhase,
|
|
532
|
+
reason,
|
|
533
|
+
errorMessage: info.errorMessage,
|
|
534
|
+
})
|
|
535
|
+
.pipe(Effect.ignore),
|
|
536
|
+
)
|
|
537
|
+
} catch {
|
|
538
|
+
/* observability must never escape */
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
yield* bus.publish(WorkflowStarted, { sessionID: input.sessionID, runID, name })
|
|
543
|
+
|
|
544
|
+
// Observability-only spawn description from label/phase: "[Phase] label",
|
|
545
|
+
// or just one of them, or undefined (then spawn falls back to agentType —
|
|
546
|
+
// see spawn.ts `input.description ?? input.agentType`). label/phase NEVER
|
|
547
|
+
// touch currentPhase/counters/schema — they are purely the per-agent tag
|
|
548
|
+
// the actor registry stores and the /workflows view surfaces.
|
|
549
|
+
const spawnDescription = (o: AgentOpts) =>
|
|
550
|
+
o.label ? (o.phase ? `[${o.phase}] ${o.label}` : o.label) : o.phase ? `[${o.phase}]` : undefined
|
|
551
|
+
|
|
552
|
+
// Shared-tree spawn (default): the existing behavior. SUBAGENT mode — the
|
|
553
|
+
// worker shares the run's parent session (cheaper, no per-agent session).
|
|
554
|
+
// Safe since lastAssistant is agent-scoped (fix 59597264): each subagent's
|
|
555
|
+
// result is extracted by its own agentID, so concurrent same-session
|
|
556
|
+
// subagents don't cross-contaminate. context:"none" keeps each worker free
|
|
557
|
+
// of parent history (parallel fan-out is the use case). NEVER throw to the
|
|
558
|
+
// guest for spawn/turn failures — resolve to null so the script continues.
|
|
559
|
+
const spawnShared = async (
|
|
560
|
+
actor: NonNullable<typeof spawnRef.current>,
|
|
561
|
+
prompt: string,
|
|
562
|
+
o: AgentOpts,
|
|
563
|
+
resolvedModel: { providerID: ProviderID; modelID: ModelID } | undefined,
|
|
564
|
+
) => {
|
|
565
|
+
// COUNTER INVARIANT: running++ exactly once BEFORE the spawn attempt, and
|
|
566
|
+
// running-- + (succeeded XOR failed)++ exactly once AFTER it settles. The
|
|
567
|
+
// bookkeeping lives OUTSIDE the bridge so it still runs when the bridge
|
|
568
|
+
// result is the spawn-reject sentinel (null). Counters settle on whether a
|
|
569
|
+
// DELIVERABLE was produced (value !== null) — the exact thing the guest
|
|
570
|
+
// observes. An agent whose turn errored finishes with status:"success" but
|
|
571
|
+
// no finalText/structured, so its deliverable is null and the guest sees a
|
|
572
|
+
// failure; the counter must agree. A spawn reject also yields null → failed.
|
|
573
|
+
entry.running++
|
|
574
|
+
scheduleFlush(entry)
|
|
575
|
+
// Failure-reason refs: defaults to "actor-error" (the broad catch-all) and
|
|
576
|
+
// is narrowed at known branch points. Read once at the end, on null return,
|
|
577
|
+
// to publish WorkflowAgentFailed. agent()'s null contract is unchanged.
|
|
578
|
+
let reason: FailReason = "actor-error"
|
|
579
|
+
let actorID: string | undefined
|
|
580
|
+
let errorMessage: string | undefined
|
|
581
|
+
const value = await bridge
|
|
582
|
+
.promise(
|
|
583
|
+
Effect.gen(function* () {
|
|
584
|
+
const spawned = yield* actor.spawn({
|
|
585
|
+
mode: "subagent",
|
|
586
|
+
sessionID: input.sessionID,
|
|
587
|
+
agentType: o.agentType ?? "general",
|
|
588
|
+
description: spawnDescription(o),
|
|
589
|
+
task: prompt,
|
|
590
|
+
context: "none",
|
|
591
|
+
tools: o.tools ? [...o.tools] : "INHERIT",
|
|
592
|
+
background: true,
|
|
593
|
+
parentActorID: input.parentActorID,
|
|
594
|
+
model: resolvedModel,
|
|
595
|
+
// Register the child in the reclaim set the instant the actor
|
|
596
|
+
// exists — synchronously inside the spawn Effect, BEFORE its work
|
|
597
|
+
// fiber detaches. A cancel racing this spawn would otherwise miss
|
|
598
|
+
// it (the child runs detached in the actor scope, so interrupting
|
|
599
|
+
// the workflow fiber can't stop it) and leak an orphan. MR104 #2.
|
|
600
|
+
onActorID: (id) => entry.childActorIDs.add(id),
|
|
601
|
+
...(o.schema ? { format: { type: "json_schema" as const, schema: o.schema, retryCount: 2 } } : {}),
|
|
602
|
+
})
|
|
603
|
+
actorID = spawned.actorID
|
|
604
|
+
// Bound the outcome-await by the per-agent timeout: a hung child times
|
|
605
|
+
// out to null (and is cancelled) rather than stalling the barrier. The
|
|
606
|
+
// deliverable is computed inside the awaited Effect so the timeout wraps
|
|
607
|
+
// the whole await→extract. schema requested ⇒ structured ?? null (never
|
|
608
|
+
// prose finalText: prose breaks `r.fields`-style scripts + our pipeline).
|
|
609
|
+
const deliverable = yield* awaitWithTimeout(
|
|
610
|
+
spawned.actorID,
|
|
611
|
+
o,
|
|
612
|
+
Deferred.await(spawned.outcome).pipe(
|
|
613
|
+
Effect.map((outcome) => {
|
|
614
|
+
if (outcome.status !== "success") {
|
|
615
|
+
reason = "actor-error"
|
|
616
|
+
errorMessage = (outcome as { error?: string }).error
|
|
617
|
+
return null
|
|
618
|
+
}
|
|
619
|
+
const v = o.schema
|
|
620
|
+
? (outcome.structured ?? null)
|
|
621
|
+
: (outcome.structured ?? outcome.finalText ?? null)
|
|
622
|
+
if (v === null) reason = "no-deliverable"
|
|
623
|
+
return v
|
|
624
|
+
}),
|
|
625
|
+
),
|
|
626
|
+
() => {
|
|
627
|
+
reason = "timeout"
|
|
628
|
+
},
|
|
629
|
+
)
|
|
630
|
+
entry.childActorIDs.delete(spawned.actorID)
|
|
631
|
+
return deliverable
|
|
632
|
+
}),
|
|
633
|
+
)
|
|
634
|
+
.catch((e) => {
|
|
635
|
+
reason = "spawn-reject"
|
|
636
|
+
errorMessage = e instanceof Error ? e.message : String(e)
|
|
637
|
+
return null
|
|
638
|
+
})
|
|
639
|
+
entry.running--
|
|
640
|
+
if (value !== null) entry.succeeded++
|
|
641
|
+
else {
|
|
642
|
+
entry.failed++
|
|
643
|
+
publishAgentFailed(o, reason, { actorID, errorMessage })
|
|
644
|
+
}
|
|
645
|
+
scheduleFlush(entry)
|
|
646
|
+
return value
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Isolated spawn: fresh worktree, file tools rebound to it via Instance.provide.
|
|
650
|
+
const spawnIsolated = async (
|
|
651
|
+
actor: NonNullable<typeof spawnRef.current>,
|
|
652
|
+
prompt: string,
|
|
653
|
+
o: AgentOpts,
|
|
654
|
+
resolvedModel: { providerID: ProviderID; modelID: ModelID } | undefined,
|
|
655
|
+
) => {
|
|
656
|
+
// Failure-reason refs (parallel to spawnShared); see there for rationale.
|
|
657
|
+
let reason: FailReason = "actor-error"
|
|
658
|
+
let actorIDOut: string | undefined
|
|
659
|
+
let errorMessage: string | undefined
|
|
660
|
+
// 1) Create + fully populate a worktree (createFromInfo awaits boot).
|
|
661
|
+
const info = await bridge
|
|
662
|
+
.promise(
|
|
663
|
+
Effect.gen(function* () {
|
|
664
|
+
const i = yield* worktree.makeWorktreeInfo()
|
|
665
|
+
yield* worktree.createFromInfo(i)
|
|
666
|
+
return i
|
|
667
|
+
}),
|
|
668
|
+
)
|
|
669
|
+
.catch((e) => {
|
|
670
|
+
errorMessage = e instanceof Error ? e.message : String(e)
|
|
671
|
+
return null
|
|
672
|
+
})
|
|
673
|
+
if (!info) {
|
|
674
|
+
publishAgentFailed(o, "spawn-reject", { errorMessage })
|
|
675
|
+
return null
|
|
676
|
+
}
|
|
677
|
+
// Register the worktree for cleanup the moment it exists on disk — BEFORE
|
|
678
|
+
// the spawn attempt. If spawn rejects or the agent fails, cancel-cleanup
|
|
679
|
+
// (and the disposition below) can still reclaim it; nothing orphans.
|
|
680
|
+
entry.worktrees.add(info.directory)
|
|
681
|
+
const base = await bridge.promise(worktree.head(info.directory)).catch(() => "")
|
|
682
|
+
// 2) A bridge bound to the worktree's InstanceContext: provide InstanceRef =
|
|
683
|
+
// worktree ctx so Effect-side reads resolve there; the Instance.provide
|
|
684
|
+
// wrap below covers raw-ALS tool reads (the load-bearing part). The outer
|
|
685
|
+
// Instance.provide is what reroutes the agent's file tools; wtBridge is
|
|
686
|
+
// defense-in-depth for any Effect-side InstanceRef read during dispatch.
|
|
687
|
+
const wtCtx = await Instance.provide({
|
|
688
|
+
directory: info.directory,
|
|
689
|
+
fn: () => Promise.resolve(Instance.current),
|
|
690
|
+
})
|
|
691
|
+
const wtBridge = await bridge.promise(EffectBridge.make().pipe(Effect.provideService(InstanceRef, wtCtx)))
|
|
692
|
+
// 3) Spawn + await INSIDE Instance.provide({worktree}) — AsyncLocalStorage
|
|
693
|
+
// propagates the worktree dir across the actor's forked work fiber, so the
|
|
694
|
+
// agent's read/write/bash resolve to the worktree, not the parent tree.
|
|
695
|
+
// COUNTER INVARIANT (isolated path): running++ here, BEFORE the spawn
|
|
696
|
+
// attempt, so it pairs with the settle below regardless of spawn-reject
|
|
697
|
+
// (spawned === null). The settle (running-- + succeeded/failed++) runs once
|
|
698
|
+
// on every disposition path after `succeeded` is known.
|
|
699
|
+
entry.running++
|
|
700
|
+
scheduleFlush(entry)
|
|
701
|
+
const spawned = await Instance.provide({
|
|
702
|
+
directory: info.directory,
|
|
703
|
+
fn: () =>
|
|
704
|
+
wtBridge
|
|
705
|
+
.promise(
|
|
706
|
+
Effect.gen(function* () {
|
|
707
|
+
const s = yield* actor.spawn({
|
|
708
|
+
mode: "subagent",
|
|
709
|
+
sessionID: input.sessionID,
|
|
710
|
+
agentType: o.agentType ?? "general",
|
|
711
|
+
description: spawnDescription(o),
|
|
712
|
+
task: prompt,
|
|
713
|
+
context: "none",
|
|
714
|
+
tools: o.tools ? [...o.tools] : "INHERIT",
|
|
715
|
+
background: true,
|
|
716
|
+
parentActorID: input.parentActorID,
|
|
717
|
+
model: resolvedModel,
|
|
718
|
+
// Same MR104 #2 fix as spawnShared: register the child in the
|
|
719
|
+
// reclaim set synchronously inside the spawn Effect, before its
|
|
720
|
+
// work fiber detaches, so a racing cancel never orphans it.
|
|
721
|
+
onActorID: (id) => entry.childActorIDs.add(id),
|
|
722
|
+
...(o.schema ? { format: { type: "json_schema" as const, schema: o.schema, retryCount: 2 } } : {}),
|
|
723
|
+
})
|
|
724
|
+
actorIDOut = s.actorID
|
|
725
|
+
// Bound the await by the per-agent timeout. On timeout the helper
|
|
726
|
+
// cancels the child and yields null; we surface that as a null
|
|
727
|
+
// `spawned` so the disposition below takes the same path as a
|
|
728
|
+
// spawn-reject/failure (worktree reclaimed, value null, failed++) —
|
|
729
|
+
// a hung isolated agent can't stall the barrier or leak a worktree.
|
|
730
|
+
const outcome = yield* awaitWithTimeout(s.actorID, o, Deferred.await(s.outcome), () => {
|
|
731
|
+
reason = "timeout"
|
|
732
|
+
})
|
|
733
|
+
entry.childActorIDs.delete(s.actorID)
|
|
734
|
+
if (outcome === null) return null
|
|
735
|
+
if (outcome.status !== "success") {
|
|
736
|
+
reason = "actor-error"
|
|
737
|
+
errorMessage = (outcome as { error?: string }).error
|
|
738
|
+
}
|
|
739
|
+
return { actorID: s.actorID, outcome }
|
|
740
|
+
}),
|
|
741
|
+
)
|
|
742
|
+
.catch((e) => {
|
|
743
|
+
reason = "spawn-reject"
|
|
744
|
+
errorMessage = e instanceof Error ? e.message : String(e)
|
|
745
|
+
return null
|
|
746
|
+
}),
|
|
747
|
+
}).catch((e) => {
|
|
748
|
+
reason = "spawn-reject"
|
|
749
|
+
errorMessage = e instanceof Error ? e.message : String(e)
|
|
750
|
+
return null
|
|
751
|
+
})
|
|
752
|
+
// 4) Disposition. KEEP the worktree only when the agent SUCCEEDED and left
|
|
753
|
+
// changes (the deliverable, surfaced via _worktree). In every other case
|
|
754
|
+
// — pristine (untouched), spawn rejected, or agent failed/cancelled —
|
|
755
|
+
// remove it so nothing leaks on disk. Guard: an empty base means head()
|
|
756
|
+
// failed at create time; treat as CHANGED (never trust an unreliable
|
|
757
|
+
// pristine check to authorize a delete).
|
|
758
|
+
const succeeded = !!spawned && spawned.outcome.status === "success"
|
|
759
|
+
// Settle the counter once here — after `succeeded` is known and before any
|
|
760
|
+
// disposition branch — so it runs exactly once on every path (spawn-reject
|
|
761
|
+
// → spawned===null → failed++, keep, remove). Pairs with the running++ above.
|
|
762
|
+
// We REUSE the existing `succeeded` discriminant (read-only; the worktree
|
|
763
|
+
// disposition below owns it) rather than the returned deliverable: in the
|
|
764
|
+
// isolated path a successful agent's work is its worktree, so a status
|
|
765
|
+
// success is a success even when it returned no text.
|
|
766
|
+
entry.running--
|
|
767
|
+
if (succeeded) entry.succeeded++
|
|
768
|
+
else {
|
|
769
|
+
entry.failed++
|
|
770
|
+
publishAgentFailed(o, reason, { actorID: actorIDOut, errorMessage })
|
|
771
|
+
}
|
|
772
|
+
scheduleFlush(entry)
|
|
773
|
+
// The success deliverable. When a schema was requested it MUST be the
|
|
774
|
+
// validated structured object — never prose finalText (see the shared-spawn
|
|
775
|
+
// path above for why: prose breaks `r.fields`-style scripts + our pipeline
|
|
776
|
+
// null-injection). schema requested ⇒ structured ?? null.
|
|
777
|
+
const value =
|
|
778
|
+
spawned && spawned.outcome.status === "success"
|
|
779
|
+
? o.schema
|
|
780
|
+
? (spawned.outcome.structured ?? null)
|
|
781
|
+
: (spawned.outcome.structured ?? spawned.outcome.finalText ?? null)
|
|
782
|
+
: null
|
|
783
|
+
const pristine =
|
|
784
|
+
base !== "" && (await bridge.promise(worktree.isPristine(info.directory, base)).catch(() => false))
|
|
785
|
+
const keep = succeeded && !pristine
|
|
786
|
+
if (!keep) {
|
|
787
|
+
await bridge.promise(worktree.remove({ directory: info.directory })).catch(() => undefined)
|
|
788
|
+
entry.worktrees.delete(info.directory)
|
|
789
|
+
return succeeded ? value : null
|
|
790
|
+
}
|
|
791
|
+
// keep: the worktree stays on disk and tracked until an integrate step or
|
|
792
|
+
// cancel reclaims it; surface its branch so the script can act on it.
|
|
793
|
+
const wt = { branch: info.branch, directory: info.directory, changed: true }
|
|
794
|
+
if (value && typeof value === "object" && !Array.isArray(value)) return { ...(value as object), _worktree: wt }
|
|
795
|
+
return { _worktree: wt, result: value }
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
const agent: HostFn = (prompt: unknown, opts?: unknown) => {
|
|
799
|
+
const o = (opts ?? {}) as AgentOpts
|
|
800
|
+
const promptStr = String(prompt)
|
|
801
|
+
// Isolated agents are never journaled in v1 (their deliverable is a
|
|
802
|
+
// worktree the journal can't reconstruct) — always spawn.
|
|
803
|
+
if (o.isolation !== "worktree") {
|
|
804
|
+
const base = journalKeyBase(promptStr, {
|
|
805
|
+
agentType: o.agentType,
|
|
806
|
+
model: o.model,
|
|
807
|
+
schema: o.schema,
|
|
808
|
+
phase: o.phase,
|
|
809
|
+
})
|
|
810
|
+
const n = occ.get(base) ?? 0
|
|
811
|
+
occ.set(base, n + 1)
|
|
812
|
+
const key = base + ":" + n
|
|
813
|
+
if (journal.results.has(key)) {
|
|
814
|
+
// Cache hit: no spawn, no agentCount increment (would hit the 1000
|
|
815
|
+
// cap on replays alone). Outcome counter DOES climb so the live view
|
|
816
|
+
// reflects reality as replay proceeds.
|
|
817
|
+
entry.succeeded++
|
|
818
|
+
scheduleFlush(entry)
|
|
819
|
+
return Promise.resolve(journal.results.get(key))
|
|
820
|
+
}
|
|
821
|
+
return (async () => {
|
|
822
|
+
// Spawn UNDER the semaphore (governs concurrency). The journal append
|
|
823
|
+
// happens AFTER the slot is released, so file IO never holds a slot.
|
|
824
|
+
const result = await sem.run(async () =>
|
|
825
|
+
globalSemLocal.run(async () => {
|
|
826
|
+
if (entry.agentCount >= lifecycleCap) {
|
|
827
|
+
warnCapOnce()
|
|
828
|
+
publishAgentFailed(o, "over-cap")
|
|
829
|
+
return null
|
|
830
|
+
}
|
|
831
|
+
entry.agentCount++
|
|
832
|
+
const actor = spawnRef.current
|
|
833
|
+
if (!actor) throw new Error("Actor service unavailable")
|
|
834
|
+
// Resolve the guest's model ref host-side AFTER the journal key was
|
|
835
|
+
// computed above (the key hashes the raw `o.model` ref, NOT the
|
|
836
|
+
// resolved struct, so resume keys stay stable across config changes).
|
|
837
|
+
// Never-throws: an unknown group falls back to input.model.
|
|
838
|
+
const resolvedModel = await bridge.promise(resolveAgentModel(o.model, input.model, entry.warnedModelRefs))
|
|
839
|
+
return spawnShared(actor, promptStr, o, resolvedModel)
|
|
840
|
+
}),
|
|
841
|
+
)
|
|
842
|
+
// Cache successful results only (null = failure/spawn-reject/killed →
|
|
843
|
+
// not journaled → re-runs on resume, self-heal). SYNCHRONOUS append so
|
|
844
|
+
// the result is durable the instant it resolves: a mid-run process exit
|
|
845
|
+
// / SIGKILL / deadline leaves a journal with every completed agent, which
|
|
846
|
+
// is the whole point of resume. A sync write (unlike an awaited async
|
|
847
|
+
// Effect.promise(fs)) does NOT starve the quickjs sandbox pump — verified.
|
|
848
|
+
// Effect.ignore'd so a write failure can't break the agent.
|
|
849
|
+
if (result !== null) {
|
|
850
|
+
await Effect.runPromise(
|
|
851
|
+
WorkflowPersistence.appendJournalSync(runID, [{ t: "agent", key, result, pass }]).pipe(Effect.ignore),
|
|
852
|
+
)
|
|
853
|
+
}
|
|
854
|
+
return result
|
|
855
|
+
})()
|
|
856
|
+
}
|
|
857
|
+
return sem.run(async () =>
|
|
858
|
+
globalSemLocal.run(async () => {
|
|
859
|
+
if (entry.agentCount >= lifecycleCap) {
|
|
860
|
+
warnCapOnce()
|
|
861
|
+
publishAgentFailed(o, "over-cap")
|
|
862
|
+
return null
|
|
863
|
+
}
|
|
864
|
+
entry.agentCount++
|
|
865
|
+
const actor = spawnRef.current
|
|
866
|
+
if (!actor) throw new Error("Actor service unavailable")
|
|
867
|
+
// Resolve the guest's model ref host-side (isolated agents aren't
|
|
868
|
+
// journaled, so there's no key to keep stable here). Never-throws.
|
|
869
|
+
const resolvedModel = await bridge.promise(resolveAgentModel(o.model, input.model, entry.warnedModelRefs))
|
|
870
|
+
return spawnIsolated(actor, promptStr, o, resolvedModel)
|
|
871
|
+
}),
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const phase: HostFn = (title: unknown) => {
|
|
876
|
+
entry.currentPhase = String(title)
|
|
877
|
+
Effect.runFork(WorkflowPersistence.recordPhase({ runID, phase: String(title) }).pipe(Effect.ignore))
|
|
878
|
+
Effect.runFork(WorkflowPersistence.appendJournal(runID, { t: "phase", title: String(title), pass }).pipe(Effect.ignore))
|
|
879
|
+
Effect.runFork(bus.publish(WorkflowPhase, { sessionID: input.sessionID, runID, title: String(title) }))
|
|
880
|
+
return undefined
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
const logHook: HostFn = (message: unknown) => {
|
|
884
|
+
Effect.runFork(WorkflowPersistence.appendJournal(runID, { t: "log", msg: String(message), pass }).pipe(Effect.ignore))
|
|
885
|
+
Effect.runFork(bus.publish(WorkflowLog, { sessionID: input.sessionID, runID, message: String(message) }))
|
|
886
|
+
return undefined
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// workflow(nameOrScript, args?, opts?) — schedule a CHILD workflow as its
|
|
890
|
+
// own independent sub-run, awaited inline. Mirrors agent()→Actor.spawn one
|
|
891
|
+
// level up: mint a deterministic child runID (stable across resume so the
|
|
892
|
+
// parent journal can find the child), resolve name→script, launch it, await
|
|
893
|
+
// its RunOutcome. A child that fails resolves to null (never-throw, like
|
|
894
|
+
// agent()) so parallel/pipeline over children degrade gracefully. An unknown
|
|
895
|
+
// name THROWS (Effect.die → the guest call rejects → the run fails loud).
|
|
896
|
+
const workflowOcc = new Map<string, number>()
|
|
897
|
+
const workflowHook: HostFn = (nameOrScript: unknown, childArgs?: unknown, opts?: unknown) => {
|
|
898
|
+
const spec = String(nameOrScript)
|
|
899
|
+
const o = (opts ?? {}) as { workspace?: string; maxConcurrentAgents?: number }
|
|
900
|
+
// Content key over the SEMANTIC inputs that reach the child (spec + args).
|
|
901
|
+
// occ disambiguates byte-identical workflow() calls into distinct slots.
|
|
902
|
+
const base = createHash("sha256")
|
|
903
|
+
.update(JSON.stringify({ spec, args: childArgs ?? null }))
|
|
904
|
+
.digest("hex")
|
|
905
|
+
const n = workflowOcc.get(base) ?? 0
|
|
906
|
+
workflowOcc.set(base, n + 1)
|
|
907
|
+
const key = base + ":" + n
|
|
908
|
+
// Parent-journal hit: a completed child replays its result with NO relaunch
|
|
909
|
+
// (the two-level resume short-circuit — parent journal skips the whole child
|
|
910
|
+
// sub-run; the child's own journal would handle agent-level skip if it were
|
|
911
|
+
// re-run). Counts as a succeeded outcome so the live view reflects replay
|
|
912
|
+
// progress. The "wf:" prefix keeps this slot namespace disjoint from agent() keys.
|
|
913
|
+
if (journal.results.has("wf:" + key)) {
|
|
914
|
+
entry.succeeded++
|
|
915
|
+
scheduleFlush(entry)
|
|
916
|
+
return Promise.resolve(journal.results.get("wf:" + key))
|
|
917
|
+
}
|
|
918
|
+
const childRunID = "wf_" + createHash("sha256").update(runID + key).digest("hex")
|
|
919
|
+
return bridge.promise(
|
|
920
|
+
Effect.gen(function* () {
|
|
921
|
+
const childScript = isInlineScript(spec)
|
|
922
|
+
? spec
|
|
923
|
+
: yield* Effect.promise(() => resolveWorkflowScript(spec, workspaceRoot, Instance.worktree))
|
|
924
|
+
if (childScript === null)
|
|
925
|
+
return yield* Effect.die(new Error(`${WORKFLOW_STRUCTURAL_ERROR}: unknown workflow: ${JSON.stringify(spec)}`))
|
|
926
|
+
// Nesting guards (T12) — LAUNCH path only (a journal HIT early-returned
|
|
927
|
+
// above without deriving childName/childRunID, and a cached child already
|
|
928
|
+
// completed in a prior pass, so re-validating would be wrong). The child's
|
|
929
|
+
// lineage name is its resolved saved name, or a content-hash label for an
|
|
930
|
+
// inline body so distinct inline children don't collide AND an inline body
|
|
931
|
+
// that re-invokes itself is still caught as a cycle. Over-depth and cycle
|
|
932
|
+
// are SCRIPT-LOGIC errors → Effect.die (fail loud), same posture as the
|
|
933
|
+
// unknown-name die above. The guest await rejects → the orchestrator script
|
|
934
|
+
// throws → the parent run fails with this message.
|
|
935
|
+
// NOTE: saved names key on the name alone (args-independent), so saved
|
|
936
|
+
// A→A with different args IS a cycle; an inline body keys on its content
|
|
937
|
+
// hash WHICH INCLUDES args, so inline A→A with different args is NOT a
|
|
938
|
+
// cycle and is bounded only by maxDepth.
|
|
939
|
+
const childName = isInlineScript(spec) ? "inline:" + base.slice(0, 12) : spec
|
|
940
|
+
if (depth + 1 > maxDepth) {
|
|
941
|
+
return yield* Effect.die(new Error(`${WORKFLOW_STRUCTURAL_ERROR}: workflow nesting exceeds maxDepth (${maxDepth})`))
|
|
942
|
+
}
|
|
943
|
+
if (lineage.includes(childName)) {
|
|
944
|
+
return yield* Effect.die(
|
|
945
|
+
new Error(`${WORKFLOW_STRUCTURAL_ERROR}: workflow cycle detected: ${childName} is already an ancestor`),
|
|
946
|
+
)
|
|
947
|
+
}
|
|
948
|
+
entry.childRunIDs.add(childRunID)
|
|
949
|
+
// The child is an independent sub-run: it gets its own per-run lifecycle
|
|
950
|
+
// cap + per-agent timeout (defaults), deliberately NOT inherited from the
|
|
951
|
+
// parent. Tree-wide concurrency is bounded by the global semaphore,
|
|
952
|
+
// not by propagating these per-run knobs.
|
|
953
|
+
yield* launch(
|
|
954
|
+
{
|
|
955
|
+
script: childScript,
|
|
956
|
+
sessionID: input.sessionID,
|
|
957
|
+
parentActorID: input.parentActorID,
|
|
958
|
+
args: childArgs,
|
|
959
|
+
model: input.model,
|
|
960
|
+
// A child may narrow its workspace to a subdir but never widen it
|
|
961
|
+
// beyond the parent's root — resolveInWorkspace throws on escape
|
|
962
|
+
// (a script-logic error → fail loud), same posture as the jail itself.
|
|
963
|
+
workspace: o.workspace ? resolveInWorkspace(workspaceRoot, String(o.workspace)) : workspaceRoot,
|
|
964
|
+
maxConcurrentAgents: o.maxConcurrentAgents,
|
|
965
|
+
scriptDeadlineMs: input.scriptDeadlineMs,
|
|
966
|
+
// Extend the nesting context for the child (T12): append this child to
|
|
967
|
+
// the ancestor lineage, increment depth, carry the same cap down.
|
|
968
|
+
lineage: [...lineage, childName],
|
|
969
|
+
depth: depth + 1,
|
|
970
|
+
maxDepth,
|
|
971
|
+
},
|
|
972
|
+
childRunID,
|
|
973
|
+
isInlineScript(spec) ? "inline" : spec,
|
|
974
|
+
)
|
|
975
|
+
const childOutcome = yield* waitFor(childRunID)
|
|
976
|
+
// Structural faults (cycle / depth / unknown-name) are workflow-wiring
|
|
977
|
+
// BUGS, not runtime conditions — propagate them loud instead of degrading
|
|
978
|
+
// to null like a child's runtime failure, so the fault surfaces at the root
|
|
979
|
+
// run. Each ancestor re-dies in turn; slice from the marker so the message
|
|
980
|
+
// doesn't accrete a "workflow script rejected:" prefix at every level.
|
|
981
|
+
if (childOutcome.status === "failed" && childOutcome.error.includes(WORKFLOW_STRUCTURAL_ERROR)) {
|
|
982
|
+
const idx = childOutcome.error.indexOf(WORKFLOW_STRUCTURAL_ERROR)
|
|
983
|
+
return yield* Effect.die(new Error(childOutcome.error.slice(idx)))
|
|
984
|
+
}
|
|
985
|
+
// Runtime failure (NOT structural — that path re-died above): the child's
|
|
986
|
+
// agents failed, it hit its deadline, or it was cancelled. workflow() still
|
|
987
|
+
// returns null (never-throw); this event records WHY for triage. Mirrors
|
|
988
|
+
// WorkflowAgentFailed. Fire-and-forget so a bus problem can't break the run.
|
|
989
|
+
if (childOutcome.status !== "completed") {
|
|
990
|
+
yield* bus
|
|
991
|
+
.publish(WorkflowChildFailed, {
|
|
992
|
+
sessionID: input.sessionID,
|
|
993
|
+
runID,
|
|
994
|
+
childRunID,
|
|
995
|
+
name: isInlineScript(spec) ? "inline" : spec,
|
|
996
|
+
status: childOutcome.status, // "failed" | "cancelled"
|
|
997
|
+
...(childOutcome.status === "failed" ? { error: childOutcome.error } : {}),
|
|
998
|
+
})
|
|
999
|
+
.pipe(Effect.ignore)
|
|
1000
|
+
}
|
|
1001
|
+
const value = childOutcome.status === "completed" ? (childOutcome.result ?? null) : null
|
|
1002
|
+
// Journal ONLY a successful child (null = failure → not cached → re-runs
|
|
1003
|
+
// on resume, self-heal — same contract as agent()). Synchronous append so
|
|
1004
|
+
// it survives a mid-run kill.
|
|
1005
|
+
if (value !== null) {
|
|
1006
|
+
yield* WorkflowPersistence.appendJournalSync(runID, [
|
|
1007
|
+
{ t: "agent", key: "wf:" + key, result: value, pass },
|
|
1008
|
+
]).pipe(Effect.ignore)
|
|
1009
|
+
}
|
|
1010
|
+
return value
|
|
1011
|
+
}),
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
const hooks: Record<string, HostFn> = {
|
|
1016
|
+
agent,
|
|
1017
|
+
phase,
|
|
1018
|
+
log: logHook,
|
|
1019
|
+
workflow: workflowHook,
|
|
1020
|
+
readFile: fileHooks.readFile,
|
|
1021
|
+
writeFile: fileHooks.writeFile,
|
|
1022
|
+
glob: fileHooks.glob,
|
|
1023
|
+
exists: fileHooks.exists,
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const work = Effect.gen(function* () {
|
|
1027
|
+
// Object-form tryPromise: bare tryPromise wraps any rejection as an
|
|
1028
|
+
// UnknownError whose .message is the useless "An error occurred in
|
|
1029
|
+
// Effect.tryPromise" (the real error lands in .cause), so the failed-run
|
|
1030
|
+
// error field / WorkflowFinished.error below would be opaque. Catching to
|
|
1031
|
+
// the raw Error makes result.failure the sandbox Error itself, whose
|
|
1032
|
+
// .message already carries the guest {name,message,stack} (vm.dump
|
|
1033
|
+
// preserves it through the sandbox throw site) — a script-logic crash is
|
|
1034
|
+
// then diagnosable from the run's error alone, no repro needed.
|
|
1035
|
+
// Per-run PRNG seed = first 4 bytes of sha1(runID). runID is unique-per-run
|
|
1036
|
+
// and persisted, so resume of the SAME run derives the SAME seed → guest
|
|
1037
|
+
// Math.random replays identically (the replay invariant). Two UNRELATED runs
|
|
1038
|
+
// of the same script get DIFFERENT runIDs → different seeds → different
|
|
1039
|
+
// sequences, so sampling-style scripts get fresh coverage instead of
|
|
1040
|
+
// repeating the same picks. Bun's lifetime-classify verification sample
|
|
1041
|
+
// is the motivating use case.
|
|
1042
|
+
const seed = createHash("sha1").update(runID).digest().readUInt32BE(0)
|
|
1043
|
+
const result = yield* Effect.tryPromise({
|
|
1044
|
+
try: () => evalScript(body, hooks, { deadlineMs: input.scriptDeadlineMs ?? SCRIPT_DEADLINE_MS, args: input.args, seed }),
|
|
1045
|
+
catch: (e) => (e instanceof Error ? e : new Error(String(e))),
|
|
1046
|
+
}).pipe(Effect.result)
|
|
1047
|
+
|
|
1048
|
+
if (result._tag === "Success") {
|
|
1049
|
+
entry.status = "completed"
|
|
1050
|
+
yield* flushNow(entry)
|
|
1051
|
+
yield* WorkflowPersistence.recordTerminal({ runID, status: "completed" }).pipe(Effect.ignore)
|
|
1052
|
+
yield* Deferred.succeed(deferred, { status: "completed", result: result.success })
|
|
1053
|
+
yield* bus.publish(WorkflowFinished, { sessionID: input.sessionID, runID, status: "completed" })
|
|
1054
|
+
// Notify the parent so its next turn drains a completion message, the
|
|
1055
|
+
// same way background actors notify on terminal (see actor/spawn.ts
|
|
1056
|
+
// forkWork.notify). Fire-and-forget: a notify failure (e.g. parent row
|
|
1057
|
+
// gone) must never fail the run, and wait-ers are already unblocked
|
|
1058
|
+
// above by Deferred.succeed.
|
|
1059
|
+
yield* inbox
|
|
1060
|
+
.send({
|
|
1061
|
+
receiverSessionID: input.sessionID,
|
|
1062
|
+
receiverActorID: input.parentActorID,
|
|
1063
|
+
senderSessionID: input.sessionID,
|
|
1064
|
+
senderActorID: "workflow",
|
|
1065
|
+
type: "actor_notification",
|
|
1066
|
+
content: `Workflow completed. run_id: ${runID}\n` + JSON.stringify(result.success ?? null).slice(0, 4000),
|
|
1067
|
+
})
|
|
1068
|
+
.pipe(Effect.ignore)
|
|
1069
|
+
return
|
|
1070
|
+
}
|
|
1071
|
+
// Non-success terminal: reclaim in-flight agents + worktrees so a
|
|
1072
|
+
// deadline-fire / script throw leaves a clean slate for a convergent
|
|
1073
|
+
// re-run. Success path does NOT reclaim — kept worktrees are the deliverable.
|
|
1074
|
+
yield* reclaim(entry)
|
|
1075
|
+
const error = result.failure instanceof Error ? result.failure.message : String(result.failure)
|
|
1076
|
+
entry.status = "failed"
|
|
1077
|
+
log.warn("workflow run failed", { runID, error })
|
|
1078
|
+
yield* flushNow(entry)
|
|
1079
|
+
yield* WorkflowPersistence.recordTerminal({ runID, status: "failed", error }).pipe(Effect.ignore)
|
|
1080
|
+
yield* Deferred.succeed(deferred, { status: "failed", error })
|
|
1081
|
+
yield* bus.publish(WorkflowFinished, { sessionID: input.sessionID, runID, status: "failed", error })
|
|
1082
|
+
yield* inbox
|
|
1083
|
+
.send({
|
|
1084
|
+
receiverSessionID: input.sessionID,
|
|
1085
|
+
receiverActorID: input.parentActorID,
|
|
1086
|
+
senderSessionID: input.sessionID,
|
|
1087
|
+
senderActorID: "workflow",
|
|
1088
|
+
type: "actor_notification",
|
|
1089
|
+
content: `Workflow failed. run_id: ${runID}\nerror: ${error}`,
|
|
1090
|
+
})
|
|
1091
|
+
.pipe(Effect.ignore)
|
|
1092
|
+
})
|
|
1093
|
+
|
|
1094
|
+
entry.fiber = yield* work.pipe(Effect.forkIn(scope))
|
|
1095
|
+
return { runID }
|
|
1096
|
+
})
|
|
1097
|
+
|
|
1098
|
+
const start = Effect.fn("WorkflowRuntime.start")(function* (input: StartInput) {
|
|
1099
|
+
const parsed = parseMeta(input.script)
|
|
1100
|
+
if (!parsed.ok) return yield* Effect.die(parsed.error)
|
|
1101
|
+
const runID = Identifier.descending("workflow")
|
|
1102
|
+
return yield* launch(input, runID, parsed.meta.name)
|
|
1103
|
+
})
|
|
1104
|
+
|
|
1105
|
+
const status = Effect.fn("WorkflowRuntime.status")(function* (input: { runID: string }) {
|
|
1106
|
+
const entry = runs.get(input.runID)
|
|
1107
|
+
if (!entry) return { status: "unknown" as const, agentCount: 0 }
|
|
1108
|
+
return {
|
|
1109
|
+
status: entry.status,
|
|
1110
|
+
agentCount: entry.agentCount,
|
|
1111
|
+
...(entry.currentPhase !== undefined ? { currentPhase: entry.currentPhase } : {}),
|
|
1112
|
+
}
|
|
1113
|
+
})
|
|
1114
|
+
|
|
1115
|
+
const wait = Effect.fn("WorkflowRuntime.wait")(function* (input: { runID: string; timeoutMs?: number }) {
|
|
1116
|
+
const entry = runs.get(input.runID)
|
|
1117
|
+
if (!entry) return { status: "failed" as const, error: `unknown runID ${input.runID}` }
|
|
1118
|
+
if (input.timeoutMs === undefined) return yield* Deferred.await(entry.deferred)
|
|
1119
|
+
const raced = yield* Deferred.await(entry.deferred).pipe(
|
|
1120
|
+
Effect.timeout(input.timeoutMs),
|
|
1121
|
+
Effect.catchTag("TimeoutError", () => Effect.succeed(null)),
|
|
1122
|
+
)
|
|
1123
|
+
if (raced === null) return { status: "failed" as const, error: "workflow wait timed out" }
|
|
1124
|
+
return raced
|
|
1125
|
+
})
|
|
1126
|
+
|
|
1127
|
+
const cancel = Effect.fn("WorkflowRuntime.cancel")(function* (input: { runID: string }) {
|
|
1128
|
+
const entry = runs.get(input.runID)
|
|
1129
|
+
if (!entry) return
|
|
1130
|
+
yield* cancelEntry(entry)
|
|
1131
|
+
})
|
|
1132
|
+
|
|
1133
|
+
const list = Effect.fn("WorkflowRuntime.list")(function* (input?: { sessionID?: SessionID }) {
|
|
1134
|
+
return yield* WorkflowPersistence.list(input)
|
|
1135
|
+
})
|
|
1136
|
+
|
|
1137
|
+
// Re-launch a persisted run under the SAME runID via the shared launch path.
|
|
1138
|
+
// recordStart's onConflictDoUpdate flips the existing row back to "running" and
|
|
1139
|
+
// runs.set overwrites the stale terminal entry (its old fiber is already done).
|
|
1140
|
+
// model/concurrency/deadline are not persisted in v1 — launch applies defaults.
|
|
1141
|
+
const resume = Effect.fn("WorkflowRuntime.resume")(function* (input: { runID: string; agentTimeoutMs?: number }) {
|
|
1142
|
+
// SERIALIZE same-runID resume with the repo's in-process reader/writer lock
|
|
1143
|
+
// (util/lock.ts: a module-global Map mutex). The live-guard below is a
|
|
1144
|
+
// check-then-act (read runs.get → decide → launch) and is NOT atomic on its
|
|
1145
|
+
// own: two concurrent resume(sameRunID) of a completed run would BOTH read
|
|
1146
|
+
// status !== "running", BOTH pass the guard, and BOTH launch() — and launch
|
|
1147
|
+
// does runs.set(runID, entry), so the second clobbers the first (orphaned
|
|
1148
|
+
// fiber, raced counter flush) and both append to the same .jsonl journal.
|
|
1149
|
+
// Holding the write lock across the guard THROUGH launch closes that window:
|
|
1150
|
+
// the first waiter launches and flips the entry to "running" before releasing,
|
|
1151
|
+
// so the second waiter sees status "running" at the guard and bails. We do NOT
|
|
1152
|
+
// hold it for the whole run (launch forks the work fiber and returns once the
|
|
1153
|
+
// entry is "running") — only the resume decision + entry creation is serialized.
|
|
1154
|
+
// LIMITATION: this is in-process only. Two SEPARATE processes resuming the same
|
|
1155
|
+
// runID against the same DB (e.g. two server instances) are NOT covered — there
|
|
1156
|
+
// is no shared/file-lock infra in this repo to reuse, and cross-process resume
|
|
1157
|
+
// is out of scope for MR104 P2-1.
|
|
1158
|
+
// Acquire as a JS Promise<Disposable> (Lock.write is promise-based; there is no
|
|
1159
|
+
// existing Effect-context consumer to mirror, so we bridge via Effect.promise),
|
|
1160
|
+
// and release in Effect.ensuring so it ALWAYS releases — even if load /
|
|
1161
|
+
// readScript / launch throws — otherwise a failed resume would deadlock every
|
|
1162
|
+
// future resume of this runID.
|
|
1163
|
+
const lock = yield* Effect.promise(() => Lock.write("workflow-resume:" + input.runID))
|
|
1164
|
+
return yield* Effect.gen(function* () {
|
|
1165
|
+
// Refuse to resume a run that is still LIVE in this process: launch would
|
|
1166
|
+
// runs.set() over the live entry, orphaning the running fiber (double parent
|
|
1167
|
+
// notify, raced counter flush, unreclaimable by cancel). The DB row is NOT the
|
|
1168
|
+
// signal — a process-exited run still reads "running" there and IS resumable;
|
|
1169
|
+
// a live `runs` entry means a fiber is actually executing here.
|
|
1170
|
+
const live = runs.get(input.runID)
|
|
1171
|
+
if (live && live.status === "running") return { runID: input.runID, resumed: false }
|
|
1172
|
+
const row = yield* WorkflowPersistence.load(input.runID)
|
|
1173
|
+
if (!row) return { runID: input.runID, resumed: false }
|
|
1174
|
+
// readScript is Effect.promise — a missing file rejects as a DEFECT, which
|
|
1175
|
+
// Effect.exit captures (Effect.result/option/catchAll do not catch defects in
|
|
1176
|
+
// this effect version). Treat a missing or empty script as not-resumable.
|
|
1177
|
+
const read = yield* WorkflowPersistence.readScript(input.runID).pipe(Effect.exit)
|
|
1178
|
+
const script = Exit.isSuccess(read) ? read.value : ""
|
|
1179
|
+
if (!script) return { runID: input.runID, resumed: false }
|
|
1180
|
+
// Script-change invalidation (MR104 P1-2): the journal keys results by
|
|
1181
|
+
// {prompt,agentType,model,schema,phase}+occ, NOT by the script body — so a
|
|
1182
|
+
// between-cycle edit would replay OLD results onto NEW code paths (silent
|
|
1183
|
+
// divergence). Compare the persisted sha (stamped at the prior launch) to the
|
|
1184
|
+
// CURRENT script's sha; on any mismatch — including a null stored sha (a run
|
|
1185
|
+
// recorded before this column existed → "unknown" → treat as changed) — pass
|
|
1186
|
+
// freshJournal so launch truncates the stale journal and runs from scratch,
|
|
1187
|
+
// re-stamping the new sha for the next resume. A match → normal replay.
|
|
1188
|
+
const currentSha = createHash("sha256").update(script).digest("hex")
|
|
1189
|
+
const freshJournal = row.scriptSha !== currentSha
|
|
1190
|
+
yield* launch(
|
|
1191
|
+
{
|
|
1192
|
+
script,
|
|
1193
|
+
sessionID: row.sessionID,
|
|
1194
|
+
parentActorID: row.parentActorID ?? "main",
|
|
1195
|
+
args: row.args,
|
|
1196
|
+
freshJournal,
|
|
1197
|
+
// Per-agent timeout: caller's explicit override > persisted value > undefined (off).
|
|
1198
|
+
// The row's agent_timeout_ms was stamped at the original launch (or last resume
|
|
1199
|
+
// that supplied an explicit override), so a UI-side resume that doesn't know
|
|
1200
|
+
// the original launch params (e.g. TUI's /workflows resume) inherits the
|
|
1201
|
+
// original timeout instead of silently dropping to unbounded — which used to
|
|
1202
|
+
// let a wedged mimo TTFT stall the resumed run forever.
|
|
1203
|
+
agentTimeoutMs: input.agentTimeoutMs ?? row.agentTimeoutMs,
|
|
1204
|
+
},
|
|
1205
|
+
input.runID,
|
|
1206
|
+
row.name,
|
|
1207
|
+
)
|
|
1208
|
+
return { runID: input.runID, resumed: true }
|
|
1209
|
+
}).pipe(Effect.ensuring(Effect.sync(() => lock[Symbol.dispose]())))
|
|
1210
|
+
})
|
|
1211
|
+
|
|
1212
|
+
const impl = Service.of({ start, status, wait, cancel, list, resume })
|
|
1213
|
+
// Late-bind the impl so the `workflow` tool can resolve it without forcing a
|
|
1214
|
+
// WorkflowRuntime.Service requirement onto ToolRegistry.layer. See
|
|
1215
|
+
// runtime-ref.ts for rationale.
|
|
1216
|
+
workflowRef.current = impl
|
|
1217
|
+
yield* Effect.addFinalizer(() =>
|
|
1218
|
+
Effect.sync(() => {
|
|
1219
|
+
if (workflowRef.current === impl) workflowRef.current = undefined
|
|
1220
|
+
}),
|
|
1221
|
+
)
|
|
1222
|
+
return impl
|
|
1223
|
+
}),
|
|
1224
|
+
)
|
|
1225
|
+
|
|
1226
|
+
export const defaultLayer = layer.pipe(
|
|
1227
|
+
Layer.provide(Bus.defaultLayer),
|
|
1228
|
+
Layer.provide(Inbox.defaultLayer),
|
|
1229
|
+
Layer.provide(Worktree.defaultLayer),
|
|
1230
|
+
Layer.provide(Provider.defaultLayer),
|
|
1231
|
+
Layer.provide(Config.defaultLayer),
|
|
1232
|
+
)
|
|
1233
|
+
|
|
1234
|
+
export * as WorkflowRuntime from "./runtime"
|