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,3355 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import os from "os"
|
|
3
|
+
import z from "zod"
|
|
4
|
+
import { SessionID, MessageID, PartID } from "./schema"
|
|
5
|
+
import { MessageV2 } from "./message-v2"
|
|
6
|
+
import { classifyAssistantStep } from "./classify"
|
|
7
|
+
import { Log } from "../util"
|
|
8
|
+
import { SessionRevert } from "./revert"
|
|
9
|
+
import * as Session from "./session"
|
|
10
|
+
import { Agent } from "../agent/agent"
|
|
11
|
+
import { SYSTEM_SPAWNED_AGENT_TYPES } from "@/agent/config"
|
|
12
|
+
import { Provider } from "../provider"
|
|
13
|
+
import { ModelID, ProviderID } from "../provider/schema"
|
|
14
|
+
import { type Tool as AITool, type ModelMessage, tool, jsonSchema, type ToolExecutionOptions, asSchema } from "ai"
|
|
15
|
+
import type { JSONSchema7 } from "@ai-sdk/provider"
|
|
16
|
+
import { SessionPrune } from "./prune"
|
|
17
|
+
import { SessionCheckpoint } from "./checkpoint"
|
|
18
|
+
import { SessionCompaction } from "./compaction"
|
|
19
|
+
import { computeLastMessageInfo } from "./last-message-info"
|
|
20
|
+
import { pressureLevel, isOverflow as overflowCheck } from "./overflow"
|
|
21
|
+
import { Config } from "@/config"
|
|
22
|
+
import { Global } from "@/global"
|
|
23
|
+
import { Bus } from "../bus"
|
|
24
|
+
import { ProviderTransform } from "../provider"
|
|
25
|
+
import { SystemPrompt } from "./system"
|
|
26
|
+
import { Instruction } from "./instruction"
|
|
27
|
+
import { TuiEvent } from "@/cli/cmd/tui/event"
|
|
28
|
+
import { Plugin } from "../plugin"
|
|
29
|
+
import BUILD_SWITCH from "../session/prompt/build-switch.txt"
|
|
30
|
+
import MAX_STEPS from "../session/prompt/max-steps.txt"
|
|
31
|
+
import PROMPT_COMPOSE from "../session/prompt/compose.txt"
|
|
32
|
+
import { composeSkillsBlock } from "@/skill/compose/extract"
|
|
33
|
+
import { ToolRegistry } from "../tool"
|
|
34
|
+
import { MCP } from "../mcp"
|
|
35
|
+
import { LSP } from "../lsp"
|
|
36
|
+
import { Flag } from "../flag/flag"
|
|
37
|
+
import { ulid } from "ulid"
|
|
38
|
+
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
|
39
|
+
import * as CrossSpawnSpawner from "@/effect/cross-spawn-spawner"
|
|
40
|
+
import * as Stream from "effect/Stream"
|
|
41
|
+
import { Command } from "../command"
|
|
42
|
+
import { pathToFileURL, fileURLToPath } from "url"
|
|
43
|
+
import { ConfigMarkdown } from "../config"
|
|
44
|
+
import { SessionSummary } from "./summary"
|
|
45
|
+
import { NamedError } from "@tuling-ai/shared/util/error"
|
|
46
|
+
import { SessionProcessor } from "./processor"
|
|
47
|
+
import { buildLLMRequestPrefix } from "./llm-request-prefix"
|
|
48
|
+
import { prefixCaptureRef } from "./prefix-capture-ref"
|
|
49
|
+
import { spawnRef } from "@/actor/spawn-ref"
|
|
50
|
+
import { Inbox } from "@/inbox"
|
|
51
|
+
import { sessionPromptRef } from "@/inbox/inbox-ref"
|
|
52
|
+
import { Tool } from "@/tool"
|
|
53
|
+
import { Permission } from "@/permission"
|
|
54
|
+
import { SessionStatus } from "./status"
|
|
55
|
+
import { LLM } from "./llm"
|
|
56
|
+
import { MaxMode } from "./max-mode"
|
|
57
|
+
import { Shell } from "@/shell/shell"
|
|
58
|
+
import { AppFileSystem } from "@tuling-ai/shared/filesystem"
|
|
59
|
+
import { Truncate } from "@/tool"
|
|
60
|
+
import { decodeDataUrl } from "@/util/data-url"
|
|
61
|
+
import { Process } from "@/util"
|
|
62
|
+
import { Cause, Effect, Exit, Layer, Option, Scope, Context } from "effect"
|
|
63
|
+
import { EffectLogger } from "@/effect"
|
|
64
|
+
import { InstanceState } from "@/effect"
|
|
65
|
+
import { ActorTool, type ActorPromptOps } from "@/tool/actor"
|
|
66
|
+
import { SessionRunState } from "./run-state"
|
|
67
|
+
import { Goal } from "./goal"
|
|
68
|
+
import { TaskGate, MAX_TASK_GATE_MAIN_REACT } from "@/task/gate"
|
|
69
|
+
import { TaskGateState } from "@/task/gate-state"
|
|
70
|
+
import { TaskRegistry } from "@/task/registry"
|
|
71
|
+
import { EffectBridge } from "@/effect"
|
|
72
|
+
import { Team } from "@/team"
|
|
73
|
+
import { ActorRegistry } from "@/actor/registry"
|
|
74
|
+
import { Metrics } from "@/metrics"
|
|
75
|
+
import { resolveInvocationStyle, type ToolStyleConfig } from "../tool/invocation-style"
|
|
76
|
+
import { shouldAutoDream, shouldAutoDistill, DREAM_TASK, DISTILL_TASK, AUTO_DREAM_TITLE, AUTO_DISTILL_TITLE } from "./auto-dream"
|
|
77
|
+
|
|
78
|
+
// @ts-ignore
|
|
79
|
+
globalThis.AI_SDK_LOG_WARNINGS = false
|
|
80
|
+
|
|
81
|
+
// Recall-reminder hints, rendered in each tool's configured invocation style so
|
|
82
|
+
// shell-mode sessions never see a JSON-shaped example (which primes models to
|
|
83
|
+
// emit JSON and crash the shell parser). `memory` has no shell form, so it is
|
|
84
|
+
// always JSON. Exported for unit testing.
|
|
85
|
+
export function recallHintLines(toolCfg: ToolStyleConfig | undefined): string[] {
|
|
86
|
+
const taskHint =
|
|
87
|
+
resolveInvocationStyle(toolCfg, "task") === "shell" ? "- task list" : `- task({ operation: "list" })`
|
|
88
|
+
const actorHint =
|
|
89
|
+
resolveInvocationStyle(toolCfg, "actor") === "shell"
|
|
90
|
+
? "- actor status <actor_id>"
|
|
91
|
+
: `- actor({ operation: "status", actor_id: "<id>" })`
|
|
92
|
+
// memory has no shell form (no shell.parse) → always JSON.
|
|
93
|
+
return [`- memory({ operation: "search", query: "<keyword>" })`, taskHint, actorHint]
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Cap on goal-driven main-loop re-entries per turn — the safety valve against
|
|
98
|
+
* a never-satisfiable condition burning tokens forever. Higher than spawned
|
|
99
|
+
* actors' MAX_PRE_REACT (=3) because main-session goals are usually larger.
|
|
100
|
+
* TODO: lift to tulingcode.json config (e.g. session.maxGoalReact).
|
|
101
|
+
*/
|
|
102
|
+
const MAX_GOAL_REACT = 12
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Number of consecutive finished assistant steps with an identical action
|
|
106
|
+
* signature that trips the repeated-step nudge. Three in a row is a strong
|
|
107
|
+
* signal the model is stuck repeating itself rather than making progress.
|
|
108
|
+
*/
|
|
109
|
+
const REPEATED_STEP_THRESHOLD = 3
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Deterministic JSON serialization with sorted object keys, so that two
|
|
113
|
+
* semantically-identical tool inputs produce the same string regardless of the
|
|
114
|
+
* order the model happened to emit the keys in. `JSON.stringify` preserves
|
|
115
|
+
* insertion order, and models routinely re-emit the same arguments with keys in
|
|
116
|
+
* a different order (e.g. {url,format} vs {format,url}) — without this the
|
|
117
|
+
* signatures would differ and the repeated-step check would miss real loops.
|
|
118
|
+
*/
|
|
119
|
+
function stableStringify(value: unknown): string {
|
|
120
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null"
|
|
121
|
+
if (Array.isArray(value)) return "[" + value.map(stableStringify).join(",") + "]"
|
|
122
|
+
const keys = Object.keys(value as Record<string, unknown>).sort()
|
|
123
|
+
return (
|
|
124
|
+
"{" +
|
|
125
|
+
keys.map((k) => JSON.stringify(k) + ":" + stableStringify((value as Record<string, unknown>)[k])).join(",") +
|
|
126
|
+
"}"
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Stable signature for an assistant step's *action* — the tool calls it made
|
|
132
|
+
* (name + key-order-independent input). Text and reasoning are excluded on
|
|
133
|
+
* purpose: in a ReAct loop the model narrates each step in slightly different
|
|
134
|
+
* words while taking the exact same action, and some models emit their
|
|
135
|
+
* reasoning as plain text parts — counting either would mask the repeated
|
|
136
|
+
* action we want to catch. Returns undefined when a step makes no tool calls
|
|
137
|
+
* (e.g. a pure-text turn), since there is no repeated *action* to compare.
|
|
138
|
+
*/
|
|
139
|
+
function stepSignature(parts: MessageV2.Part[]): string | undefined {
|
|
140
|
+
const segments: string[] = []
|
|
141
|
+
for (const part of parts) {
|
|
142
|
+
if (part.type === "tool") {
|
|
143
|
+
segments.push("tool:" + part.tool + ":" + stableStringify(part.state.input ?? {}))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (segments.length === 0) return undefined
|
|
147
|
+
return segments.join("\n")
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const STRUCTURED_OUTPUT_DESCRIPTION = `Use this tool to return your final response in the requested structured format.
|
|
151
|
+
|
|
152
|
+
IMPORTANT:
|
|
153
|
+
- You MUST call this tool exactly once at the end of your response
|
|
154
|
+
- The input must be valid JSON matching the required schema
|
|
155
|
+
- Complete all necessary research and tool calls BEFORE calling this tool
|
|
156
|
+
- This tool provides your final answer - no further actions are taken after calling it`
|
|
157
|
+
|
|
158
|
+
const STRUCTURED_OUTPUT_SYSTEM_PROMPT = `IMPORTANT: The user has requested structured output. You MUST use the StructuredOutput tool to provide your final response. Do NOT respond with plain text - you MUST call the StructuredOutput tool with your answer formatted according to the schema.`
|
|
159
|
+
|
|
160
|
+
const PREDICT_SYSTEM = `You predict the single most likely next message a user will send to a coding assistant, based on the conversation so far. Output only that next message as one short, natural first-person request (what the user would type). No preamble, no quotes, no explanation, no markdown. Keep it under 100 characters.`
|
|
161
|
+
|
|
162
|
+
const PREDICT_NUDGE = `Based on the conversation above, write the user's most likely next message:`
|
|
163
|
+
|
|
164
|
+
const OUTPUT_LENGTH_CONTINUATION_LIMIT = Flag.tulingcode_OUTPUT_LENGTH_CONTINUATION_LIMIT
|
|
165
|
+
const INVALID_OUTPUT_CONTINUATION_LIMIT = Flag.tulingcode_INVALID_OUTPUT_CONTINUATION_LIMIT
|
|
166
|
+
|
|
167
|
+
const log = Log.create({ service: "session.prompt" })
|
|
168
|
+
const elog = EffectLogger.create({ service: "session.prompt" })
|
|
169
|
+
|
|
170
|
+
export interface Interface {
|
|
171
|
+
readonly cancel: (sessionID: SessionID) => Effect.Effect<void>
|
|
172
|
+
readonly prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts>
|
|
173
|
+
readonly loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts>
|
|
174
|
+
readonly shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts>
|
|
175
|
+
readonly command: (input: CommandInput) => Effect.Effect<MessageV2.WithParts>
|
|
176
|
+
readonly resolvePromptParts: (template: string) => Effect.Effect<PromptInput["parts"]>
|
|
177
|
+
readonly sweepOrphanAssistants: (sessionID: SessionID) => Effect.Effect<void>
|
|
178
|
+
readonly predict: (input: { sessionID: SessionID }) => Effect.Effect<string>
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export class Service extends Context.Service<Service, Interface>()("@tulingcode/SessionPrompt") {}
|
|
182
|
+
|
|
183
|
+
export const layer = Layer.effect(
|
|
184
|
+
Service,
|
|
185
|
+
Effect.gen(function* () {
|
|
186
|
+
const bus = yield* Bus.Service
|
|
187
|
+
const status = yield* SessionStatus.Service
|
|
188
|
+
const sessions = yield* Session.Service
|
|
189
|
+
const agents = yield* Agent.Service
|
|
190
|
+
const provider = yield* Provider.Service
|
|
191
|
+
const processor = yield* SessionProcessor.Service
|
|
192
|
+
const prune = yield* SessionPrune.Service
|
|
193
|
+
const checkpoint = yield* SessionCheckpoint.Service
|
|
194
|
+
const compaction = yield* SessionCompaction.Service
|
|
195
|
+
const config = yield* Config.Service
|
|
196
|
+
const plugin = yield* Plugin.Service
|
|
197
|
+
const commands = yield* Command.Service
|
|
198
|
+
const permission = yield* Permission.Service
|
|
199
|
+
const fsys = yield* AppFileSystem.Service
|
|
200
|
+
const mcp = yield* MCP.Service
|
|
201
|
+
const lsp = yield* LSP.Service
|
|
202
|
+
const registry = yield* ToolRegistry.Service
|
|
203
|
+
const truncate = yield* Truncate.Service
|
|
204
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
|
205
|
+
const scope = yield* Scope.Scope
|
|
206
|
+
const instruction = yield* Instruction.Service
|
|
207
|
+
const state = yield* SessionRunState.Service
|
|
208
|
+
const goal = yield* Goal.Service
|
|
209
|
+
const taskGateState = yield* TaskGateState.Service
|
|
210
|
+
const taskRegistry = yield* TaskRegistry.Service
|
|
211
|
+
const revert = yield* SessionRevert.Service
|
|
212
|
+
const summary = yield* SessionSummary.Service
|
|
213
|
+
const sys = yield* SystemPrompt.Service
|
|
214
|
+
const llm = yield* LLM.Service
|
|
215
|
+
const actorRegistry = yield* ActorRegistry.Service
|
|
216
|
+
const inbox = yield* Inbox.Service
|
|
217
|
+
|
|
218
|
+
// Track sessions that have already shown the "loaded instructions" toast so we
|
|
219
|
+
// surface it once per primary session rather than on every run-loop turn.
|
|
220
|
+
const instructionsNotified = new Set<SessionID>()
|
|
221
|
+
|
|
222
|
+
// Late-bind prefix-capture helper so SessionCheckpoint.tryStartCheckpointWriter
|
|
223
|
+
// can call buildLLMRequestPrefix without forming a layer cycle
|
|
224
|
+
// (ToolRegistry → SessionCheckpoint → ToolRegistry). See prefix-capture-ref.ts.
|
|
225
|
+
// The closure resolves Agent.Info and Provider.Model internally so checkpoint.ts
|
|
226
|
+
// only needs to pass string IDs.
|
|
227
|
+
const capture: typeof prefixCaptureRef.current = (input) =>
|
|
228
|
+
Effect.gen(function* () {
|
|
229
|
+
const empty = { system: [] as string[], tools: {} as Record<string, AITool>, inheritedMessages: [] as ModelMessage[], parentPermission: [] as Permission.Ruleset }
|
|
230
|
+
const ag = yield* agents.get(input.agentName).pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
231
|
+
if (!ag) return empty
|
|
232
|
+
const model = yield* provider
|
|
233
|
+
.getModel(input.providerID as ProviderID, input.modelID as ModelID)
|
|
234
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
235
|
+
if (!model) return empty
|
|
236
|
+
const [skills, env, instructions] = yield* Effect.all([
|
|
237
|
+
sys.skills(ag),
|
|
238
|
+
Effect.sync(() => sys.environment(model)),
|
|
239
|
+
instruction.system().pipe(Effect.orDie),
|
|
240
|
+
])
|
|
241
|
+
// (checkpoint-writer never requests json_schema output, so STRUCTURED_OUTPUT_SYSTEM_PROMPT
|
|
242
|
+
// is not included; parent's runLoop adds it conditionally based on user.format)
|
|
243
|
+
const additions = [...env, ...(skills ? [skills] : []), ...instructions.content]
|
|
244
|
+
const prefix = yield* buildLLMRequestPrefix({
|
|
245
|
+
sessionID: input.sessionID,
|
|
246
|
+
agent: ag,
|
|
247
|
+
model,
|
|
248
|
+
msgs: input.msgs as Parameters<typeof buildLLMRequestPrefix>[0]["msgs"],
|
|
249
|
+
additions,
|
|
250
|
+
}).pipe(
|
|
251
|
+
Effect.provideService(LLM.Service, llm),
|
|
252
|
+
Effect.provideService(ToolRegistry.Service, registry),
|
|
253
|
+
Effect.catch(() => Effect.succeed(empty)),
|
|
254
|
+
)
|
|
255
|
+
return { ...prefix, parentPermission: ag.permission }
|
|
256
|
+
})
|
|
257
|
+
prefixCaptureRef.current = capture
|
|
258
|
+
yield* Effect.addFinalizer(() =>
|
|
259
|
+
Effect.sync(() => {
|
|
260
|
+
if (prefixCaptureRef.current === capture) prefixCaptureRef.current = undefined
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
const runner = Effect.fn("SessionPrompt.runner")(function* () {
|
|
265
|
+
return yield* EffectBridge.make()
|
|
266
|
+
})
|
|
267
|
+
const ops = Effect.fn("SessionPrompt.ops")(function* () {
|
|
268
|
+
const run = yield* runner()
|
|
269
|
+
return {
|
|
270
|
+
cancel: (sessionID: SessionID) => run.fork(cancel(sessionID)),
|
|
271
|
+
resolvePromptParts: (template: string) => resolvePromptParts(template),
|
|
272
|
+
prompt: (input: PromptInput) => prompt(input),
|
|
273
|
+
} satisfies ActorPromptOps
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) {
|
|
277
|
+
yield* elog.info("cancel", { sessionID })
|
|
278
|
+
yield* state.cancel(sessionID)
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
const resolvePromptParts = Effect.fn("SessionPrompt.resolvePromptParts")(function* (template: string) {
|
|
282
|
+
const ctx = yield* InstanceState.context
|
|
283
|
+
const parts: PromptInput["parts"] = [{ type: "text", text: template }]
|
|
284
|
+
const files = ConfigMarkdown.files(template)
|
|
285
|
+
const seen = new Set<string>()
|
|
286
|
+
yield* Effect.forEach(
|
|
287
|
+
files,
|
|
288
|
+
Effect.fnUntraced(function* (match) {
|
|
289
|
+
const name = match[1]
|
|
290
|
+
if (seen.has(name)) return
|
|
291
|
+
seen.add(name)
|
|
292
|
+
const filepath = name.startsWith("~/")
|
|
293
|
+
? path.join(os.homedir(), name.slice(2))
|
|
294
|
+
: path.resolve(ctx.worktree, name)
|
|
295
|
+
|
|
296
|
+
const info = yield* fsys.stat(filepath).pipe(Effect.option)
|
|
297
|
+
if (Option.isNone(info)) {
|
|
298
|
+
const found = yield* agents.get(name)
|
|
299
|
+
if (found) parts.push({ type: "agent", name: found.name })
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
const stat = info.value
|
|
303
|
+
parts.push({
|
|
304
|
+
type: "file",
|
|
305
|
+
url: pathToFileURL(filepath).href,
|
|
306
|
+
filename: name,
|
|
307
|
+
mime: stat.type === "Directory" ? "application/x-directory" : "text/plain",
|
|
308
|
+
})
|
|
309
|
+
}),
|
|
310
|
+
{ concurrency: "unbounded", discard: true },
|
|
311
|
+
)
|
|
312
|
+
return parts
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
const title = Effect.fn("SessionPrompt.ensureTitle")(function* (input: {
|
|
316
|
+
session: Session.Info
|
|
317
|
+
history: MessageV2.WithParts[]
|
|
318
|
+
providerID: ProviderID
|
|
319
|
+
modelID: ModelID
|
|
320
|
+
}) {
|
|
321
|
+
if (input.session.parentID) return
|
|
322
|
+
if (!Session.isDefaultTitle(input.session.title)) return
|
|
323
|
+
|
|
324
|
+
const real = (m: MessageV2.WithParts) =>
|
|
325
|
+
m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
|
|
326
|
+
const idx = input.history.findIndex(real)
|
|
327
|
+
if (idx === -1) return
|
|
328
|
+
if (input.history.filter(real).length !== 1) return
|
|
329
|
+
|
|
330
|
+
const context = input.history.slice(0, idx + 1)
|
|
331
|
+
const firstUser = context[idx]
|
|
332
|
+
if (!firstUser || firstUser.info.role !== "user") return
|
|
333
|
+
const firstInfo = firstUser.info
|
|
334
|
+
|
|
335
|
+
const subtasks = firstUser.parts.filter((p): p is MessageV2.SubtaskPart => p.type === "subtask")
|
|
336
|
+
const onlySubtasks = subtasks.length > 0 && firstUser.parts.every((p) => p.type === "subtask")
|
|
337
|
+
|
|
338
|
+
const ag = yield* agents.get("title")
|
|
339
|
+
if (!ag) return
|
|
340
|
+
const mdl = ag.modelRef
|
|
341
|
+
? yield* provider.resolveModelRef(ag.modelRef, input.providerID)
|
|
342
|
+
: ag.model
|
|
343
|
+
? yield* provider.getModel(ag.model.providerID, ag.model.modelID)
|
|
344
|
+
: ((yield* provider.getSmallModel(input.providerID)) ??
|
|
345
|
+
(yield* provider.getModel(input.providerID, input.modelID)))
|
|
346
|
+
const msgs = onlySubtasks
|
|
347
|
+
? [{ role: "user" as const, content: subtasks.map((p) => p.prompt).join("\n") }]
|
|
348
|
+
: yield* MessageV2.toModelMessagesEffect(context, mdl)
|
|
349
|
+
const text = yield* llm
|
|
350
|
+
.stream({
|
|
351
|
+
agent: ag,
|
|
352
|
+
user: firstInfo,
|
|
353
|
+
system: [],
|
|
354
|
+
small: true,
|
|
355
|
+
tools: {},
|
|
356
|
+
model: mdl,
|
|
357
|
+
sessionID: input.session.id,
|
|
358
|
+
retries: 2,
|
|
359
|
+
messages: [{ role: "user", content: "Generate a title for this conversation:\n" }, ...msgs],
|
|
360
|
+
})
|
|
361
|
+
.pipe(
|
|
362
|
+
Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
|
|
363
|
+
Stream.map((e) => e.text),
|
|
364
|
+
Stream.mkString,
|
|
365
|
+
Effect.orDie,
|
|
366
|
+
)
|
|
367
|
+
const cleaned = text
|
|
368
|
+
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
|
|
369
|
+
.split("\n")
|
|
370
|
+
.map((line) => line.trim())
|
|
371
|
+
.find((line) => line.length > 0)
|
|
372
|
+
if (!cleaned) return
|
|
373
|
+
const t = cleaned.length > 100 ? cleaned.substring(0, 97) + "..." : cleaned
|
|
374
|
+
yield* sessions
|
|
375
|
+
.setTitle({ sessionID: input.session.id, title: t })
|
|
376
|
+
.pipe(Effect.catchCause((cause) => elog.error("failed to generate title", { error: Cause.squash(cause) })))
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const predict = Effect.fn("SessionPrompt.predict")(function* (input: { sessionID: SessionID }) {
|
|
380
|
+
const cfg = yield* config.get()
|
|
381
|
+
if (cfg.experimental?.predict_next_prompt === false) return ""
|
|
382
|
+
|
|
383
|
+
const history = yield* sessions.messages({ sessionID: input.sessionID, agentID: "main" })
|
|
384
|
+
const real = (m: MessageV2.WithParts) =>
|
|
385
|
+
m.info.role === "user" && !m.parts.every((p) => "synthetic" in p && p.synthetic)
|
|
386
|
+
const userIdx = history.findLastIndex(real)
|
|
387
|
+
if (userIdx === -1) return ""
|
|
388
|
+
const lastUser = history[userIdx]
|
|
389
|
+
if (lastUser.info.role !== "user") return ""
|
|
390
|
+
|
|
391
|
+
// Only the assistant turn that actually answered this user message counts.
|
|
392
|
+
// Bail if that turn is still running (an incomplete assistant after it),
|
|
393
|
+
// so we never pair the newest prompt with a stale/older result.
|
|
394
|
+
const assistants = history
|
|
395
|
+
.slice(userIdx + 1)
|
|
396
|
+
.filter((m): m is MessageV2.WithParts & { info: MessageV2.Assistant } => m.info.role === "assistant")
|
|
397
|
+
if (assistants.length === 0) return ""
|
|
398
|
+
if (assistants.some((m) => m.info.time.completed === undefined)) return ""
|
|
399
|
+
const lastAssistant = assistants[assistants.length - 1]
|
|
400
|
+
|
|
401
|
+
const base = yield* agents.get("title")
|
|
402
|
+
if (!base) return ""
|
|
403
|
+
// Reuse the lightweight title agent's settings but swap its prompt for the
|
|
404
|
+
// prediction prompt — its default ("output ONLY a thread title") would
|
|
405
|
+
// otherwise be prepended ahead of PREDICT_SYSTEM and win.
|
|
406
|
+
const ag = { ...base, prompt: PREDICT_SYSTEM }
|
|
407
|
+
const mdl = ag.modelRef
|
|
408
|
+
? yield* provider.resolveModelRef(ag.modelRef, lastAssistant.info.providerID)
|
|
409
|
+
: ag.model
|
|
410
|
+
? yield* provider.getModel(ag.model.providerID, ag.model.modelID)
|
|
411
|
+
: ((yield* provider.getSmallModel(lastAssistant.info.providerID)) ??
|
|
412
|
+
(yield* provider.getModel(lastAssistant.info.providerID, lastAssistant.info.modelID)))
|
|
413
|
+
|
|
414
|
+
const msgs = yield* MessageV2.toModelMessagesEffect([lastUser, lastAssistant], mdl, { stripMedia: true })
|
|
415
|
+
const text = yield* llm
|
|
416
|
+
.stream({
|
|
417
|
+
agent: ag,
|
|
418
|
+
user: lastUser.info,
|
|
419
|
+
system: [],
|
|
420
|
+
small: true,
|
|
421
|
+
tools: {},
|
|
422
|
+
model: mdl,
|
|
423
|
+
sessionID: input.sessionID,
|
|
424
|
+
retries: 1,
|
|
425
|
+
messages: [...msgs, { role: "user", content: PREDICT_NUDGE }],
|
|
426
|
+
})
|
|
427
|
+
.pipe(
|
|
428
|
+
Stream.filter((e): e is Extract<LLM.Event, { type: "text-delta" }> => e.type === "text-delta"),
|
|
429
|
+
Stream.map((e) => e.text),
|
|
430
|
+
Stream.mkString,
|
|
431
|
+
Effect.orElseSucceed(() => ""),
|
|
432
|
+
)
|
|
433
|
+
const cleaned = text
|
|
434
|
+
.replace(/<think>[\s\S]*?<\/think>\s*/g, "")
|
|
435
|
+
.split("\n")
|
|
436
|
+
.map((line) => line.trim())
|
|
437
|
+
.find((line) => line.length > 0)
|
|
438
|
+
if (!cleaned) return ""
|
|
439
|
+
const stripped = cleaned.replace(quoteTrimRegex, "")
|
|
440
|
+
return stripped.length > 120 ? stripped.substring(0, 117) + "..." : stripped
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
const insertReminders = Effect.fn("SessionPrompt.insertReminders")(function* (input: {
|
|
444
|
+
messages: MessageV2.WithParts[]
|
|
445
|
+
agent: Agent.Info
|
|
446
|
+
session: Session.Info
|
|
447
|
+
}) {
|
|
448
|
+
const userMessage = input.messages.findLast((msg) => msg.info.role === "user")
|
|
449
|
+
if (!userMessage) return input.messages
|
|
450
|
+
|
|
451
|
+
const composeModeMsg = input.messages.find(
|
|
452
|
+
(msg) => msg.info.role === "user" && msg.info.agent === "compose",
|
|
453
|
+
)
|
|
454
|
+
if (composeModeMsg) {
|
|
455
|
+
const composeModeBlock = composeSkillsBlock()
|
|
456
|
+
composeModeMsg.parts.unshift({
|
|
457
|
+
id: PartID.ascending(),
|
|
458
|
+
messageID: composeModeMsg.info.id,
|
|
459
|
+
sessionID: composeModeMsg.info.sessionID,
|
|
460
|
+
type: "text",
|
|
461
|
+
text: PROMPT_COMPOSE + (composeModeBlock ? "\n\n" + composeModeBlock : ""),
|
|
462
|
+
synthetic: true,
|
|
463
|
+
})
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const assistantMessage = input.messages.findLast((msg) => msg.info.role === "assistant")
|
|
467
|
+
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
|
|
468
|
+
const plan = Session.plan(input.session)
|
|
469
|
+
if (!(yield* fsys.existsSafe(plan))) return input.messages
|
|
470
|
+
const part = yield* sessions.updatePart({
|
|
471
|
+
id: PartID.ascending(),
|
|
472
|
+
messageID: userMessage.info.id,
|
|
473
|
+
sessionID: userMessage.info.sessionID,
|
|
474
|
+
type: "text",
|
|
475
|
+
text: `${BUILD_SWITCH}\n\nA plan file exists at ${plan}. You should execute on the plan defined within it`,
|
|
476
|
+
synthetic: true,
|
|
477
|
+
})
|
|
478
|
+
userMessage.parts.push(part)
|
|
479
|
+
return input.messages
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (input.agent.name !== "plan" || assistantMessage?.info.agent === "plan") return input.messages
|
|
483
|
+
|
|
484
|
+
const plan = Session.plan(input.session)
|
|
485
|
+
const exists = yield* fsys.existsSafe(plan)
|
|
486
|
+
if (!exists) yield* fsys.ensureDir(path.dirname(plan)).pipe(Effect.catch(Effect.die))
|
|
487
|
+
const part = yield* sessions.updatePart({
|
|
488
|
+
id: PartID.ascending(),
|
|
489
|
+
messageID: userMessage.info.id,
|
|
490
|
+
sessionID: userMessage.info.sessionID,
|
|
491
|
+
type: "text",
|
|
492
|
+
text: `<system-reminder>
|
|
493
|
+
Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits (with the exception of the plan file mentioned below), run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system. This supersedes any other instructions you have received.
|
|
494
|
+
|
|
495
|
+
## Plan File Info:
|
|
496
|
+
${exists ? `A plan file already exists at ${plan}. You can read it and make incremental edits using the edit tool.` : `No plan file exists yet. You should create your plan at ${plan} using the write tool.`}
|
|
497
|
+
You should build your plan incrementally by writing to or editing this file. NOTE that this is the only file you are allowed to edit - other than this you are only allowed to take READ-ONLY actions.
|
|
498
|
+
|
|
499
|
+
## Plan Workflow
|
|
500
|
+
|
|
501
|
+
### Phase 1: Initial Understanding
|
|
502
|
+
Goal: Gain a comprehensive understanding of the user's request by reading through code and asking them questions. Critical: In this phase you should only use the explore subagent type.
|
|
503
|
+
|
|
504
|
+
1. Focus on understanding the user's request and the code associated with their request
|
|
505
|
+
|
|
506
|
+
2. **Launch up to 3 explore agents IN PARALLEL** (single message, multiple tool calls) to efficiently explore the codebase.
|
|
507
|
+
- Use 1 agent when the task is isolated to known files, the user provided specific file paths, or you're making a small targeted change.
|
|
508
|
+
- Use multiple agents when: the scope is uncertain, multiple areas of the codebase are involved, or you need to understand existing patterns before planning.
|
|
509
|
+
- Quality over quantity - 3 agents maximum, but you should try to use the minimum number of agents necessary (usually just 1)
|
|
510
|
+
- If using multiple agents: Provide each agent with a specific search focus or area to explore. Example: One agent searches for existing implementations, another explores related components, a third investigates testing patterns
|
|
511
|
+
|
|
512
|
+
3. After exploring the code, use the question tool to clarify ambiguities in the user request up front.
|
|
513
|
+
|
|
514
|
+
### Phase 2: Design
|
|
515
|
+
Goal: Design an implementation approach.
|
|
516
|
+
|
|
517
|
+
Launch general agent(s) to design the implementation based on the user's intent and your exploration results from Phase 1.
|
|
518
|
+
|
|
519
|
+
You can launch up to 1 agent(s) in parallel.
|
|
520
|
+
|
|
521
|
+
**Guidelines:**
|
|
522
|
+
- **Default**: Launch at least 1 Plan agent for most tasks - it helps validate your understanding and consider alternatives
|
|
523
|
+
- **Skip agents**: Only for truly trivial tasks (typo fixes, single-line changes, simple renames)
|
|
524
|
+
|
|
525
|
+
Examples of when to use multiple agents:
|
|
526
|
+
- The task touches multiple parts of the codebase
|
|
527
|
+
- It's a large refactor or architectural change
|
|
528
|
+
- There are many edge cases to consider
|
|
529
|
+
- You'd benefit from exploring different approaches
|
|
530
|
+
|
|
531
|
+
Example perspectives by task type:
|
|
532
|
+
- New feature: simplicity vs performance vs maintainability
|
|
533
|
+
- Bug fix: root cause vs workaround vs prevention
|
|
534
|
+
- Refactoring: minimal change vs clean architecture
|
|
535
|
+
|
|
536
|
+
In the agent prompt:
|
|
537
|
+
- Provide comprehensive background context from Phase 1 exploration including filenames and code path traces
|
|
538
|
+
- Describe requirements and constraints
|
|
539
|
+
- Request a detailed implementation plan
|
|
540
|
+
|
|
541
|
+
### Phase 3: Review
|
|
542
|
+
Goal: Review the plan(s) from Phase 2 and ensure alignment with the user's intentions.
|
|
543
|
+
1. Read the critical files identified by agents to deepen your understanding
|
|
544
|
+
2. Ensure that the plans align with the user's original request
|
|
545
|
+
3. Use question tool to clarify any remaining questions with the user
|
|
546
|
+
|
|
547
|
+
### Phase 4: Final Plan
|
|
548
|
+
Goal: Write your final plan to the plan file (the only file you can edit).
|
|
549
|
+
- Include only your recommended approach, not all alternatives
|
|
550
|
+
- Ensure that the plan file is concise enough to scan quickly, but detailed enough to execute effectively
|
|
551
|
+
- Include the paths of critical files to be modified
|
|
552
|
+
- Include a verification section describing how to test the changes end-to-end (run the code, use MCP tools, run tests)
|
|
553
|
+
|
|
554
|
+
### Phase 5: Call plan_exit tool
|
|
555
|
+
At the very end of your turn, once you have asked the user questions and are happy with your final plan file - you should always call plan_exit to indicate to the user that you are done planning.
|
|
556
|
+
This is critical - your turn should only end with either asking the user a question or calling plan_exit. Do not stop unless it's for these 2 reasons.
|
|
557
|
+
|
|
558
|
+
**Important:** Use question tool to clarify requirements/approach, use plan_exit to request plan approval. Do NOT use question tool to ask "Is this plan okay?" - that's what plan_exit does.
|
|
559
|
+
|
|
560
|
+
NOTE: At any point in time through this workflow you should feel free to ask the user questions or clarifications. Don't make large assumptions about user intent. The goal is to present a well researched plan to the user, and tie any loose ends before implementation begins.
|
|
561
|
+
</system-reminder>`,
|
|
562
|
+
synthetic: true,
|
|
563
|
+
})
|
|
564
|
+
userMessage.parts.push(part)
|
|
565
|
+
return input.messages
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input: {
|
|
569
|
+
agent: Agent.Info
|
|
570
|
+
model: Provider.Model
|
|
571
|
+
session: Session.Info
|
|
572
|
+
tools?: Record<string, boolean>
|
|
573
|
+
processor: Pick<SessionProcessor.Handle, "message" | "updateToolCall" | "completeToolCall">
|
|
574
|
+
bypassAgentCheck: boolean
|
|
575
|
+
messages: MessageV2.WithParts[]
|
|
576
|
+
agentID?: string
|
|
577
|
+
task_id?: string
|
|
578
|
+
}) {
|
|
579
|
+
using _ = log.time("resolveTools")
|
|
580
|
+
const tools: Record<string, AITool> = {}
|
|
581
|
+
const run = yield* runner()
|
|
582
|
+
const promptOps = yield* ops()
|
|
583
|
+
|
|
584
|
+
// Per-tool runtime whitelist: when the LLM call is being made on behalf
|
|
585
|
+
// of a registered actor (subagent or peer), look up the actor row and,
|
|
586
|
+
// if `actor.tools` is an array, reject calls to tools not in the
|
|
587
|
+
// whitelist. `INHERIT` and a missing actor row both mean full access.
|
|
588
|
+
const whitelistFor = Effect.fn("SessionPrompt.whitelistFor")(function* () {
|
|
589
|
+
if (!input.agentID) return undefined
|
|
590
|
+
const actor = yield* actorRegistry.get(input.session.id, input.agentID)
|
|
591
|
+
if (!actor || !Array.isArray(actor.tools)) return undefined
|
|
592
|
+
return new Set(actor.tools)
|
|
593
|
+
})
|
|
594
|
+
const whitelist = yield* whitelistFor()
|
|
595
|
+
const rejectionFor = (toolID: string) => ({
|
|
596
|
+
title: "Tool not permitted",
|
|
597
|
+
output: `The "${toolID}" tool is not in this actor's whitelist. Allowed tools: ${
|
|
598
|
+
whitelist ? [...whitelist].join(", ") : "(none)"
|
|
599
|
+
}.`,
|
|
600
|
+
metadata: { rejected: true, reason: "tool-whitelist" as const },
|
|
601
|
+
})
|
|
602
|
+
|
|
603
|
+
const context = (args: any, options: ToolExecutionOptions): Tool.Context => ({
|
|
604
|
+
sessionID: input.session.id,
|
|
605
|
+
abort: options.abortSignal!,
|
|
606
|
+
messageID: input.processor.message.id,
|
|
607
|
+
callID: options.toolCallId,
|
|
608
|
+
extra: { model: input.model, bypassAgentCheck: input.bypassAgentCheck, promptOps },
|
|
609
|
+
agent: input.agent.name,
|
|
610
|
+
actorID: input.agentID,
|
|
611
|
+
taskId: input.task_id,
|
|
612
|
+
messages: input.messages,
|
|
613
|
+
metadata: (val) =>
|
|
614
|
+
input.processor.updateToolCall(options.toolCallId, (match) => {
|
|
615
|
+
if (!["running", "pending"].includes(match.state.status)) return match
|
|
616
|
+
return {
|
|
617
|
+
...match,
|
|
618
|
+
state: {
|
|
619
|
+
title: val.title,
|
|
620
|
+
metadata: val.metadata,
|
|
621
|
+
status: "running",
|
|
622
|
+
input: args,
|
|
623
|
+
time: { start: Date.now() },
|
|
624
|
+
},
|
|
625
|
+
}
|
|
626
|
+
}),
|
|
627
|
+
ask: (req) =>
|
|
628
|
+
permission
|
|
629
|
+
.ask(
|
|
630
|
+
{
|
|
631
|
+
...req,
|
|
632
|
+
sessionID: input.session.id,
|
|
633
|
+
tool: { messageID: input.processor.message.id, callID: options.toolCallId },
|
|
634
|
+
ruleset: Permission.merge(input.agent.permission, input.session.permission ?? []),
|
|
635
|
+
// System-spawned background agents (checkpoint-writer, dream, distill)
|
|
636
|
+
// have no human to answer a permission prompt — fail clean, don't hang.
|
|
637
|
+
interactive: !SYSTEM_SPAWNED_AGENT_TYPES.has(input.agent.name),
|
|
638
|
+
},
|
|
639
|
+
options.abortSignal,
|
|
640
|
+
)
|
|
641
|
+
.pipe(Effect.orDie),
|
|
642
|
+
})
|
|
643
|
+
|
|
644
|
+
for (const item of yield* registry.tools({
|
|
645
|
+
modelID: ModelID.make(input.model.api.id),
|
|
646
|
+
providerID: input.model.providerID,
|
|
647
|
+
agent: input.agent,
|
|
648
|
+
})) {
|
|
649
|
+
const schema = ProviderTransform.schema(input.model, z.toJSONSchema(item.parameters))
|
|
650
|
+
tools[item.id] = tool({
|
|
651
|
+
description: item.description,
|
|
652
|
+
inputSchema: jsonSchema(schema),
|
|
653
|
+
execute(args, options) {
|
|
654
|
+
return run.promise(
|
|
655
|
+
Effect.gen(function* () {
|
|
656
|
+
const startTs = Date.now()
|
|
657
|
+
const callID = options?.toolCallId ?? "?"
|
|
658
|
+
log.debug("tool execute start", {
|
|
659
|
+
tool: item.id,
|
|
660
|
+
callID,
|
|
661
|
+
sessionID: input.session.id,
|
|
662
|
+
})
|
|
663
|
+
const ctx = context(args, options)
|
|
664
|
+
if (whitelist && !whitelist.has(item.id)) {
|
|
665
|
+
const output = rejectionFor(item.id)
|
|
666
|
+
log.debug("tool execute rejected", {
|
|
667
|
+
tool: item.id,
|
|
668
|
+
callID,
|
|
669
|
+
durationMs: Date.now() - startTs,
|
|
670
|
+
})
|
|
671
|
+
yield* input.processor.completeToolCall(options.toolCallId, output)
|
|
672
|
+
return output
|
|
673
|
+
}
|
|
674
|
+
yield* plugin.trigger(
|
|
675
|
+
"tool.execute.before",
|
|
676
|
+
{ tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID },
|
|
677
|
+
{ args },
|
|
678
|
+
)
|
|
679
|
+
const result = yield* item.execute(args, ctx)
|
|
680
|
+
log.debug("tool execute done", {
|
|
681
|
+
tool: item.id,
|
|
682
|
+
callID,
|
|
683
|
+
durationMs: Date.now() - startTs,
|
|
684
|
+
ok: true,
|
|
685
|
+
})
|
|
686
|
+
const output = {
|
|
687
|
+
...result,
|
|
688
|
+
attachments: result.attachments?.map((attachment) => ({
|
|
689
|
+
...attachment,
|
|
690
|
+
id: PartID.ascending(),
|
|
691
|
+
sessionID: ctx.sessionID,
|
|
692
|
+
messageID: input.processor.message.id,
|
|
693
|
+
})),
|
|
694
|
+
}
|
|
695
|
+
yield* plugin.trigger(
|
|
696
|
+
"tool.execute.after",
|
|
697
|
+
{ tool: item.id, sessionID: ctx.sessionID, callID: ctx.callID, args },
|
|
698
|
+
output,
|
|
699
|
+
)
|
|
700
|
+
yield* bus
|
|
701
|
+
.publish(Metrics.ToolCall, {
|
|
702
|
+
sessionID: ctx.sessionID,
|
|
703
|
+
tool_name: item.id,
|
|
704
|
+
input_bytes: Metrics.jsonByteLength(args),
|
|
705
|
+
output_bytes: Buffer.byteLength(output.output ?? "", "utf8"),
|
|
706
|
+
tool_call_id: options.toolCallId,
|
|
707
|
+
tool_call_status: "success",
|
|
708
|
+
})
|
|
709
|
+
.pipe(Effect.ignore)
|
|
710
|
+
if (options.abortSignal?.aborted) {
|
|
711
|
+
yield* input.processor.completeToolCall(options.toolCallId, output)
|
|
712
|
+
}
|
|
713
|
+
return output
|
|
714
|
+
}),
|
|
715
|
+
)
|
|
716
|
+
},
|
|
717
|
+
})
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
for (const [key, item] of Object.entries(yield* mcp.tools())) {
|
|
721
|
+
const execute = item.execute
|
|
722
|
+
if (!execute) continue
|
|
723
|
+
|
|
724
|
+
const schema = yield* Effect.promise(() => Promise.resolve(asSchema(item.inputSchema).jsonSchema))
|
|
725
|
+
const transformed = ProviderTransform.schema(input.model, schema)
|
|
726
|
+
item.inputSchema = jsonSchema(transformed)
|
|
727
|
+
item.execute = (args, opts) =>
|
|
728
|
+
run.promise(
|
|
729
|
+
Effect.gen(function* () {
|
|
730
|
+
const startTs = Date.now()
|
|
731
|
+
const callID = opts?.toolCallId ?? "?"
|
|
732
|
+
log.debug("tool execute start (mcp)", {
|
|
733
|
+
tool: key,
|
|
734
|
+
callID,
|
|
735
|
+
sessionID: input.session.id,
|
|
736
|
+
})
|
|
737
|
+
const ctx = context(args, opts)
|
|
738
|
+
if (whitelist && !whitelist.has(key)) {
|
|
739
|
+
const rejection = rejectionFor(key)
|
|
740
|
+
const output = {
|
|
741
|
+
title: rejection.title,
|
|
742
|
+
metadata: rejection.metadata,
|
|
743
|
+
output: rejection.output,
|
|
744
|
+
attachments: [],
|
|
745
|
+
content: [{ type: "text" as const, text: rejection.output }],
|
|
746
|
+
}
|
|
747
|
+
log.debug("tool execute rejected (mcp)", {
|
|
748
|
+
tool: key,
|
|
749
|
+
callID,
|
|
750
|
+
durationMs: Date.now() - startTs,
|
|
751
|
+
})
|
|
752
|
+
yield* input.processor.completeToolCall(opts.toolCallId, output)
|
|
753
|
+
return output
|
|
754
|
+
}
|
|
755
|
+
yield* plugin.trigger(
|
|
756
|
+
"tool.execute.before",
|
|
757
|
+
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId },
|
|
758
|
+
{ args },
|
|
759
|
+
)
|
|
760
|
+
yield* ctx.ask({ permission: key, metadata: {}, patterns: ["*"], always: ["*"] })
|
|
761
|
+
const result: Awaited<ReturnType<NonNullable<typeof execute>>> = yield* Effect.promise(() =>
|
|
762
|
+
execute(args, opts),
|
|
763
|
+
)
|
|
764
|
+
log.debug("tool execute done (mcp)", {
|
|
765
|
+
tool: key,
|
|
766
|
+
callID,
|
|
767
|
+
durationMs: Date.now() - startTs,
|
|
768
|
+
ok: true,
|
|
769
|
+
})
|
|
770
|
+
yield* plugin.trigger(
|
|
771
|
+
"tool.execute.after",
|
|
772
|
+
{ tool: key, sessionID: ctx.sessionID, callID: opts.toolCallId, args },
|
|
773
|
+
result,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
const textParts: string[] = []
|
|
777
|
+
yield* bus
|
|
778
|
+
.publish(Metrics.ToolCall, {
|
|
779
|
+
sessionID: ctx.sessionID,
|
|
780
|
+
tool_name: key,
|
|
781
|
+
input_bytes: Metrics.jsonByteLength(args),
|
|
782
|
+
output_bytes: Metrics.jsonByteLength(result.content ?? ""),
|
|
783
|
+
tool_call_id: opts.toolCallId,
|
|
784
|
+
tool_call_status: "success",
|
|
785
|
+
})
|
|
786
|
+
.pipe(Effect.ignore)
|
|
787
|
+
const attachments: Omit<MessageV2.FilePart, "id" | "sessionID" | "messageID">[] = []
|
|
788
|
+
for (const contentItem of result.content) {
|
|
789
|
+
if (contentItem.type === "text") textParts.push(contentItem.text)
|
|
790
|
+
else if (contentItem.type === "image") {
|
|
791
|
+
attachments.push({
|
|
792
|
+
type: "file",
|
|
793
|
+
mime: contentItem.mimeType,
|
|
794
|
+
url: `data:${contentItem.mimeType};base64,${contentItem.data}`,
|
|
795
|
+
})
|
|
796
|
+
} else if (contentItem.type === "resource") {
|
|
797
|
+
const { resource } = contentItem
|
|
798
|
+
if (resource.text) textParts.push(resource.text)
|
|
799
|
+
if (resource.blob) {
|
|
800
|
+
attachments.push({
|
|
801
|
+
type: "file",
|
|
802
|
+
mime: resource.mimeType ?? "application/octet-stream",
|
|
803
|
+
url: `data:${resource.mimeType ?? "application/octet-stream"};base64,${resource.blob}`,
|
|
804
|
+
filename: resource.uri,
|
|
805
|
+
})
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const truncated = yield* truncate.output(textParts.join("\n\n"), {}, input.agent)
|
|
811
|
+
const metadata = {
|
|
812
|
+
...result.metadata,
|
|
813
|
+
truncated: truncated.truncated,
|
|
814
|
+
...(truncated.truncated && { outputPath: truncated.outputPath }),
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const output = {
|
|
818
|
+
title: "",
|
|
819
|
+
metadata,
|
|
820
|
+
output: truncated.content,
|
|
821
|
+
attachments: attachments.map((attachment) => ({
|
|
822
|
+
...attachment,
|
|
823
|
+
id: PartID.ascending(),
|
|
824
|
+
sessionID: ctx.sessionID,
|
|
825
|
+
messageID: input.processor.message.id,
|
|
826
|
+
})),
|
|
827
|
+
content: result.content,
|
|
828
|
+
}
|
|
829
|
+
if (opts.abortSignal?.aborted) {
|
|
830
|
+
yield* input.processor.completeToolCall(opts.toolCallId, output)
|
|
831
|
+
}
|
|
832
|
+
return output
|
|
833
|
+
}),
|
|
834
|
+
)
|
|
835
|
+
tools[key] = item
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return tools
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
const handleSubtask = Effect.fn("SessionPrompt.handleSubtask")(function* (input: {
|
|
842
|
+
task: MessageV2.SubtaskPart
|
|
843
|
+
model: Provider.Model
|
|
844
|
+
lastUser: MessageV2.User
|
|
845
|
+
sessionID: SessionID
|
|
846
|
+
session: Session.Info
|
|
847
|
+
msgs: MessageV2.WithParts[]
|
|
848
|
+
}) {
|
|
849
|
+
const { task, model, lastUser, sessionID, session, msgs } = input
|
|
850
|
+
const ctx = yield* InstanceState.context
|
|
851
|
+
const promptOps = yield* ops()
|
|
852
|
+
const { actor: actorTool } = yield* registry.named()
|
|
853
|
+
const taskModel = task.model ? yield* getModel(task.model.providerID, task.model.modelID, sessionID) : model
|
|
854
|
+
const assistantMessage: MessageV2.Assistant = yield* sessions.updateMessage({
|
|
855
|
+
id: MessageID.ascending(),
|
|
856
|
+
role: "assistant",
|
|
857
|
+
parentID: lastUser.id,
|
|
858
|
+
sessionID,
|
|
859
|
+
agentID: lastUser.agentID,
|
|
860
|
+
mode: task.agent,
|
|
861
|
+
agent: task.agent,
|
|
862
|
+
variant: lastUser.model.variant,
|
|
863
|
+
path: { cwd: ctx.directory, root: ctx.worktree },
|
|
864
|
+
cost: 0,
|
|
865
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
866
|
+
modelID: taskModel.id,
|
|
867
|
+
providerID: taskModel.providerID,
|
|
868
|
+
time: { created: Date.now() },
|
|
869
|
+
})
|
|
870
|
+
const taskArgs = {
|
|
871
|
+
operation: {
|
|
872
|
+
action: "run" as const,
|
|
873
|
+
prompt: task.prompt,
|
|
874
|
+
description: task.description,
|
|
875
|
+
subagent_type: task.agent,
|
|
876
|
+
command: task.command,
|
|
877
|
+
},
|
|
878
|
+
}
|
|
879
|
+
let part: MessageV2.ToolPart = yield* sessions.updatePart({
|
|
880
|
+
id: PartID.ascending(),
|
|
881
|
+
messageID: assistantMessage.id,
|
|
882
|
+
sessionID: assistantMessage.sessionID,
|
|
883
|
+
type: "tool",
|
|
884
|
+
callID: ulid(),
|
|
885
|
+
tool: ActorTool.id,
|
|
886
|
+
state: {
|
|
887
|
+
status: "running",
|
|
888
|
+
input: taskArgs,
|
|
889
|
+
time: { start: Date.now() },
|
|
890
|
+
},
|
|
891
|
+
})
|
|
892
|
+
yield* plugin.trigger(
|
|
893
|
+
"tool.execute.before",
|
|
894
|
+
{ tool: ActorTool.id, sessionID, callID: part.id },
|
|
895
|
+
{ args: taskArgs },
|
|
896
|
+
)
|
|
897
|
+
|
|
898
|
+
const taskAgent = yield* agents.get(task.agent)
|
|
899
|
+
if (!taskAgent) {
|
|
900
|
+
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
|
|
901
|
+
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
|
|
902
|
+
const error = new NamedError.Unknown({ message: `Agent not found: "${task.agent}".${hint}` })
|
|
903
|
+
yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
|
|
904
|
+
throw error
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
let error: Error | undefined
|
|
908
|
+
const taskAbort = new AbortController()
|
|
909
|
+
const result = yield* actorTool
|
|
910
|
+
.execute(taskArgs, {
|
|
911
|
+
agent: task.agent,
|
|
912
|
+
messageID: assistantMessage.id,
|
|
913
|
+
sessionID,
|
|
914
|
+
abort: taskAbort.signal,
|
|
915
|
+
callID: part.callID,
|
|
916
|
+
extra: { bypassAgentCheck: true, promptOps },
|
|
917
|
+
messages: msgs,
|
|
918
|
+
metadata: (val: { title?: string; metadata?: Record<string, any> }) =>
|
|
919
|
+
Effect.gen(function* () {
|
|
920
|
+
part = yield* sessions.updatePart({
|
|
921
|
+
...part,
|
|
922
|
+
type: "tool",
|
|
923
|
+
state: { ...part.state, ...val },
|
|
924
|
+
} satisfies MessageV2.ToolPart)
|
|
925
|
+
}),
|
|
926
|
+
ask: (req: any) =>
|
|
927
|
+
permission
|
|
928
|
+
.ask({
|
|
929
|
+
...req,
|
|
930
|
+
sessionID,
|
|
931
|
+
ruleset: Permission.merge(taskAgent.permission, session.permission ?? []),
|
|
932
|
+
})
|
|
933
|
+
.pipe(Effect.orDie),
|
|
934
|
+
})
|
|
935
|
+
.pipe(
|
|
936
|
+
Effect.catchCause((cause) => {
|
|
937
|
+
const defect = Cause.squash(cause)
|
|
938
|
+
error = defect instanceof Error ? defect : new Error(String(defect))
|
|
939
|
+
log.error("subtask execution failed", { error, agent: task.agent, description: task.description })
|
|
940
|
+
return Effect.void
|
|
941
|
+
}),
|
|
942
|
+
Effect.onInterrupt(() =>
|
|
943
|
+
Effect.gen(function* () {
|
|
944
|
+
taskAbort.abort()
|
|
945
|
+
assistantMessage.finish = "tool-calls"
|
|
946
|
+
assistantMessage.time.completed = Date.now()
|
|
947
|
+
yield* sessions.updateMessage(assistantMessage)
|
|
948
|
+
if (part.state.status === "running") {
|
|
949
|
+
yield* sessions.updatePart({
|
|
950
|
+
...part,
|
|
951
|
+
state: {
|
|
952
|
+
status: "error",
|
|
953
|
+
error: "Cancelled",
|
|
954
|
+
time: { start: part.state.time.start, end: Date.now() },
|
|
955
|
+
metadata: part.state.metadata,
|
|
956
|
+
input: part.state.input,
|
|
957
|
+
},
|
|
958
|
+
} satisfies MessageV2.ToolPart)
|
|
959
|
+
}
|
|
960
|
+
}),
|
|
961
|
+
),
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
const attachments = result?.attachments?.map((attachment) => ({
|
|
965
|
+
...attachment,
|
|
966
|
+
id: PartID.ascending(),
|
|
967
|
+
sessionID,
|
|
968
|
+
messageID: assistantMessage.id,
|
|
969
|
+
}))
|
|
970
|
+
|
|
971
|
+
yield* plugin.trigger(
|
|
972
|
+
"tool.execute.after",
|
|
973
|
+
{ tool: ActorTool.id, sessionID, callID: part.id, args: taskArgs },
|
|
974
|
+
result,
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
assistantMessage.finish = "tool-calls"
|
|
978
|
+
assistantMessage.time.completed = Date.now()
|
|
979
|
+
yield* sessions.updateMessage(assistantMessage)
|
|
980
|
+
|
|
981
|
+
if (result && part.state.status === "running") {
|
|
982
|
+
yield* sessions.updatePart({
|
|
983
|
+
...part,
|
|
984
|
+
state: {
|
|
985
|
+
status: "completed",
|
|
986
|
+
input: part.state.input,
|
|
987
|
+
title: result.title,
|
|
988
|
+
metadata: result.metadata,
|
|
989
|
+
output: result.output,
|
|
990
|
+
attachments,
|
|
991
|
+
time: { ...part.state.time, end: Date.now() },
|
|
992
|
+
},
|
|
993
|
+
} satisfies MessageV2.ToolPart)
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (!result) {
|
|
997
|
+
yield* sessions.updatePart({
|
|
998
|
+
...part,
|
|
999
|
+
state: {
|
|
1000
|
+
status: "error",
|
|
1001
|
+
error: error ? `Tool execution failed: ${error.message}` : "Tool execution failed",
|
|
1002
|
+
time: {
|
|
1003
|
+
start: part.state.status === "running" ? part.state.time.start : Date.now(),
|
|
1004
|
+
end: Date.now(),
|
|
1005
|
+
},
|
|
1006
|
+
metadata: part.state.status === "pending" ? undefined : part.state.metadata,
|
|
1007
|
+
input: part.state.input,
|
|
1008
|
+
},
|
|
1009
|
+
} satisfies MessageV2.ToolPart)
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (!task.command) return
|
|
1013
|
+
|
|
1014
|
+
const summaryUserMsg: MessageV2.User = {
|
|
1015
|
+
id: MessageID.ascending(),
|
|
1016
|
+
sessionID,
|
|
1017
|
+
role: "user",
|
|
1018
|
+
agentID: lastUser.agentID,
|
|
1019
|
+
time: { created: Date.now() },
|
|
1020
|
+
agent: lastUser.agent,
|
|
1021
|
+
model: lastUser.model,
|
|
1022
|
+
}
|
|
1023
|
+
yield* sessions.updateMessage(summaryUserMsg)
|
|
1024
|
+
yield* sessions.updatePart({
|
|
1025
|
+
id: PartID.ascending(),
|
|
1026
|
+
messageID: summaryUserMsg.id,
|
|
1027
|
+
sessionID,
|
|
1028
|
+
type: "text",
|
|
1029
|
+
text: "Summarize the actor tool output above and continue with your task.",
|
|
1030
|
+
synthetic: true,
|
|
1031
|
+
} satisfies MessageV2.TextPart)
|
|
1032
|
+
})
|
|
1033
|
+
|
|
1034
|
+
const shellImpl = Effect.fn("SessionPrompt.shellImpl")(function* (input: ShellInput) {
|
|
1035
|
+
const ctx = yield* InstanceState.context
|
|
1036
|
+
const run = yield* runner()
|
|
1037
|
+
const session = yield* sessions.get(input.sessionID)
|
|
1038
|
+
if (session.revert) {
|
|
1039
|
+
yield* revert.cleanup(session)
|
|
1040
|
+
}
|
|
1041
|
+
const agent = yield* agents.get(input.agent)
|
|
1042
|
+
if (!agent) {
|
|
1043
|
+
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
|
|
1044
|
+
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
|
|
1045
|
+
const error = new NamedError.Unknown({ message: `Agent not found: "${input.agent}".${hint}` })
|
|
1046
|
+
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
|
|
1047
|
+
throw error
|
|
1048
|
+
}
|
|
1049
|
+
const inputModel = input.modelRef
|
|
1050
|
+
? yield* provider
|
|
1051
|
+
.resolveModelRef(input.modelRef)
|
|
1052
|
+
.pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
|
|
1053
|
+
: input.model
|
|
1054
|
+
const agentModel = agent.modelRef
|
|
1055
|
+
? yield* provider
|
|
1056
|
+
.resolveModelRef(agent.modelRef)
|
|
1057
|
+
.pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
|
|
1058
|
+
: agent.model
|
|
1059
|
+
const model = inputModel ?? agentModel ?? (yield* lastModel(input.sessionID))
|
|
1060
|
+
const userMsg: MessageV2.User = {
|
|
1061
|
+
id: input.messageID ?? MessageID.ascending(),
|
|
1062
|
+
sessionID: input.sessionID,
|
|
1063
|
+
time: { created: Date.now() },
|
|
1064
|
+
role: "user",
|
|
1065
|
+
agent: input.agent,
|
|
1066
|
+
model: { providerID: model.providerID, modelID: model.modelID },
|
|
1067
|
+
}
|
|
1068
|
+
yield* sessions.updateMessage(userMsg)
|
|
1069
|
+
const userPart: MessageV2.Part = {
|
|
1070
|
+
type: "text",
|
|
1071
|
+
id: PartID.ascending(),
|
|
1072
|
+
messageID: userMsg.id,
|
|
1073
|
+
sessionID: input.sessionID,
|
|
1074
|
+
text: "The following tool was executed by the user",
|
|
1075
|
+
synthetic: true,
|
|
1076
|
+
}
|
|
1077
|
+
yield* sessions.updatePart(userPart)
|
|
1078
|
+
|
|
1079
|
+
const msg: MessageV2.Assistant = {
|
|
1080
|
+
id: MessageID.ascending(),
|
|
1081
|
+
sessionID: input.sessionID,
|
|
1082
|
+
parentID: userMsg.id,
|
|
1083
|
+
agentID: userMsg.agentID,
|
|
1084
|
+
mode: input.agent,
|
|
1085
|
+
agent: input.agent,
|
|
1086
|
+
cost: 0,
|
|
1087
|
+
path: { cwd: ctx.directory, root: ctx.worktree },
|
|
1088
|
+
time: { created: Date.now() },
|
|
1089
|
+
role: "assistant",
|
|
1090
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
1091
|
+
modelID: model.modelID,
|
|
1092
|
+
providerID: model.providerID,
|
|
1093
|
+
}
|
|
1094
|
+
yield* sessions.updateMessage(msg)
|
|
1095
|
+
const part: MessageV2.ToolPart = {
|
|
1096
|
+
type: "tool",
|
|
1097
|
+
id: PartID.ascending(),
|
|
1098
|
+
messageID: msg.id,
|
|
1099
|
+
sessionID: input.sessionID,
|
|
1100
|
+
tool: "bash",
|
|
1101
|
+
callID: ulid(),
|
|
1102
|
+
state: {
|
|
1103
|
+
status: "running",
|
|
1104
|
+
time: { start: Date.now() },
|
|
1105
|
+
input: { command: input.command },
|
|
1106
|
+
},
|
|
1107
|
+
}
|
|
1108
|
+
yield* sessions.updatePart(part)
|
|
1109
|
+
|
|
1110
|
+
const sh = Shell.preferred()
|
|
1111
|
+
const shellName = (
|
|
1112
|
+
process.platform === "win32" ? path.win32.basename(sh, ".exe") : path.basename(sh)
|
|
1113
|
+
).toLowerCase()
|
|
1114
|
+
const invocations: Record<string, { args: string[] }> = {
|
|
1115
|
+
nu: { args: ["-c", input.command] },
|
|
1116
|
+
fish: { args: ["-c", input.command] },
|
|
1117
|
+
zsh: {
|
|
1118
|
+
args: [
|
|
1119
|
+
"-l",
|
|
1120
|
+
"-c",
|
|
1121
|
+
`
|
|
1122
|
+
__oc_cwd=$PWD
|
|
1123
|
+
[[ -f ~/.zshenv ]] && source ~/.zshenv >/dev/null 2>&1 || true
|
|
1124
|
+
[[ -f "\${ZDOTDIR:-$HOME}/.zshrc" ]] && source "\${ZDOTDIR:-$HOME}/.zshrc" >/dev/null 2>&1 || true
|
|
1125
|
+
cd "$__oc_cwd"
|
|
1126
|
+
eval ${JSON.stringify(input.command)}
|
|
1127
|
+
`,
|
|
1128
|
+
],
|
|
1129
|
+
},
|
|
1130
|
+
bash: {
|
|
1131
|
+
args: [
|
|
1132
|
+
"-l",
|
|
1133
|
+
"-c",
|
|
1134
|
+
`
|
|
1135
|
+
__oc_cwd=$PWD
|
|
1136
|
+
shopt -s expand_aliases
|
|
1137
|
+
[[ -f ~/.bashrc ]] && source ~/.bashrc >/dev/null 2>&1 || true
|
|
1138
|
+
cd "$__oc_cwd"
|
|
1139
|
+
eval ${JSON.stringify(input.command)}
|
|
1140
|
+
`,
|
|
1141
|
+
],
|
|
1142
|
+
},
|
|
1143
|
+
cmd: { args: ["/c", input.command] },
|
|
1144
|
+
powershell: { args: ["-NoProfile", "-Command", input.command] },
|
|
1145
|
+
pwsh: { args: ["-NoProfile", "-Command", input.command] },
|
|
1146
|
+
"": { args: ["-c", input.command] },
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
const args = (invocations[shellName] ?? invocations[""]).args
|
|
1150
|
+
const cwd = ctx.directory
|
|
1151
|
+
const shellEnv = yield* plugin.trigger(
|
|
1152
|
+
"shell.env",
|
|
1153
|
+
{ cwd, sessionID: input.sessionID, callID: part.callID },
|
|
1154
|
+
{ env: {} },
|
|
1155
|
+
)
|
|
1156
|
+
|
|
1157
|
+
const cmd = ChildProcess.make(sh, args, {
|
|
1158
|
+
cwd,
|
|
1159
|
+
extendEnv: true,
|
|
1160
|
+
env: { ...shellEnv.env, TERM: "dumb" },
|
|
1161
|
+
stdin: "ignore",
|
|
1162
|
+
forceKillAfter: "3 seconds",
|
|
1163
|
+
})
|
|
1164
|
+
|
|
1165
|
+
let output = ""
|
|
1166
|
+
let aborted = false
|
|
1167
|
+
|
|
1168
|
+
const finish = Effect.uninterruptible(
|
|
1169
|
+
Effect.gen(function* () {
|
|
1170
|
+
if (aborted) {
|
|
1171
|
+
output += "\n\n" + ["<metadata>", "User aborted the command", "</metadata>"].join("\n")
|
|
1172
|
+
}
|
|
1173
|
+
if (!msg.time.completed) {
|
|
1174
|
+
msg.time.completed = Date.now()
|
|
1175
|
+
yield* sessions.updateMessage(msg)
|
|
1176
|
+
}
|
|
1177
|
+
if (part.state.status === "running") {
|
|
1178
|
+
part.state = {
|
|
1179
|
+
status: "completed",
|
|
1180
|
+
time: { ...part.state.time, end: Date.now() },
|
|
1181
|
+
input: part.state.input,
|
|
1182
|
+
title: "",
|
|
1183
|
+
metadata: { output, description: "" },
|
|
1184
|
+
output,
|
|
1185
|
+
}
|
|
1186
|
+
yield* sessions.updatePart(part)
|
|
1187
|
+
}
|
|
1188
|
+
}),
|
|
1189
|
+
)
|
|
1190
|
+
|
|
1191
|
+
const exit = yield* Effect.gen(function* () {
|
|
1192
|
+
const handle = yield* spawner.spawn(cmd)
|
|
1193
|
+
yield* Stream.runForEach(Stream.decodeText(handle.all), (chunk) =>
|
|
1194
|
+
Effect.sync(() => {
|
|
1195
|
+
output += chunk
|
|
1196
|
+
if (part.state.status === "running") {
|
|
1197
|
+
part.state.metadata = { output, description: "" }
|
|
1198
|
+
void run.fork(sessions.updatePart(part))
|
|
1199
|
+
}
|
|
1200
|
+
}),
|
|
1201
|
+
)
|
|
1202
|
+
yield* handle.exitCode
|
|
1203
|
+
}).pipe(
|
|
1204
|
+
Effect.scoped,
|
|
1205
|
+
Effect.onInterrupt(() =>
|
|
1206
|
+
Effect.sync(() => {
|
|
1207
|
+
aborted = true
|
|
1208
|
+
}),
|
|
1209
|
+
),
|
|
1210
|
+
Effect.orDie,
|
|
1211
|
+
Effect.ensuring(finish),
|
|
1212
|
+
Effect.exit,
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
if (Exit.isFailure(exit) && !Cause.hasInterruptsOnly(exit.cause)) {
|
|
1216
|
+
return yield* Effect.failCause(exit.cause)
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
return { info: msg, parts: [part] }
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
const getModel = Effect.fn("SessionPrompt.getModel")(function* (
|
|
1223
|
+
providerID: ProviderID,
|
|
1224
|
+
modelID: ModelID,
|
|
1225
|
+
sessionID: SessionID,
|
|
1226
|
+
) {
|
|
1227
|
+
const exit = yield* provider.getModel(providerID, modelID).pipe(Effect.exit)
|
|
1228
|
+
if (Exit.isSuccess(exit)) return exit.value
|
|
1229
|
+
const err = Cause.squash(exit.cause)
|
|
1230
|
+
if (Provider.ModelNotFoundError.isInstance(err)) {
|
|
1231
|
+
const hint = err.data.suggestions?.length ? ` Did you mean: ${err.data.suggestions.join(", ")}?` : ""
|
|
1232
|
+
yield* bus.publish(Session.Event.Error, {
|
|
1233
|
+
sessionID,
|
|
1234
|
+
error: new NamedError.Unknown({
|
|
1235
|
+
message: `Model not found: ${err.data.providerID}/${err.data.modelID}.${hint}`,
|
|
1236
|
+
}).toObject(),
|
|
1237
|
+
})
|
|
1238
|
+
}
|
|
1239
|
+
return yield* Effect.failCause(exit.cause)
|
|
1240
|
+
})
|
|
1241
|
+
|
|
1242
|
+
const lastModel = Effect.fnUntraced(function* (sessionID: SessionID) {
|
|
1243
|
+
const match = yield* sessions.findMessage(
|
|
1244
|
+
sessionID,
|
|
1245
|
+
(m) => m.info.role === "user" && !!m.info.model,
|
|
1246
|
+
{ agentID: "*" },
|
|
1247
|
+
)
|
|
1248
|
+
if (Option.isSome(match) && match.value.info.role === "user") return match.value.info.model
|
|
1249
|
+
return yield* provider.defaultModel()
|
|
1250
|
+
})
|
|
1251
|
+
|
|
1252
|
+
const createUserMessage = Effect.fn("SessionPrompt.createUserMessage")(function* (input: PromptInput) {
|
|
1253
|
+
const agentName = input.agent || (yield* agents.defaultAgent())
|
|
1254
|
+
const ag = yield* agents.get(agentName)
|
|
1255
|
+
if (!ag) {
|
|
1256
|
+
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
|
|
1257
|
+
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
|
|
1258
|
+
const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
|
|
1259
|
+
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
|
|
1260
|
+
throw error
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
const inputModel = input.modelRef
|
|
1264
|
+
? yield* provider
|
|
1265
|
+
.resolveModelRef(input.modelRef)
|
|
1266
|
+
.pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
|
|
1267
|
+
: input.model
|
|
1268
|
+
const agentModel = ag.modelRef
|
|
1269
|
+
? yield* provider
|
|
1270
|
+
.resolveModelRef(ag.modelRef)
|
|
1271
|
+
.pipe(Effect.map((m) => ({ providerID: m.providerID, modelID: m.id })))
|
|
1272
|
+
: ag.model
|
|
1273
|
+
const model = inputModel ?? agentModel ?? (yield* lastModel(input.sessionID))
|
|
1274
|
+
const same = agentModel && model.providerID === agentModel.providerID && model.modelID === agentModel.modelID
|
|
1275
|
+
const full =
|
|
1276
|
+
!input.variant && ag.variant && same
|
|
1277
|
+
? yield* provider.getModel(model.providerID, model.modelID).pipe(Effect.catchDefect(() => Effect.void))
|
|
1278
|
+
: undefined
|
|
1279
|
+
const variant = input.variant ?? (ag.variant && full?.variants?.[ag.variant] ? ag.variant : undefined)
|
|
1280
|
+
|
|
1281
|
+
const info: MessageV2.User = {
|
|
1282
|
+
id: input.messageID ?? MessageID.ascending(),
|
|
1283
|
+
role: "user",
|
|
1284
|
+
sessionID: input.sessionID,
|
|
1285
|
+
agentID: input.agentID,
|
|
1286
|
+
time: { created: Date.now() },
|
|
1287
|
+
tools: input.tools,
|
|
1288
|
+
agent: ag.name,
|
|
1289
|
+
model: {
|
|
1290
|
+
providerID: model.providerID,
|
|
1291
|
+
modelID: model.modelID,
|
|
1292
|
+
variant,
|
|
1293
|
+
},
|
|
1294
|
+
system: input.system,
|
|
1295
|
+
format: input.format,
|
|
1296
|
+
provenance: input.provenance,
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
yield* Effect.addFinalizer(() => instruction.clear(info.id))
|
|
1300
|
+
|
|
1301
|
+
type Draft<T> = T extends MessageV2.Part ? Omit<T, "id"> & { id?: string } : never
|
|
1302
|
+
const assign = (part: Draft<MessageV2.Part>): MessageV2.Part => ({
|
|
1303
|
+
...part,
|
|
1304
|
+
id: part.id ? PartID.make(part.id) : PartID.ascending(),
|
|
1305
|
+
})
|
|
1306
|
+
|
|
1307
|
+
const resolvePart: (part: PromptInput["parts"][number]) => Effect.Effect<Draft<MessageV2.Part>[]> = Effect.fn(
|
|
1308
|
+
"SessionPrompt.resolveUserPart",
|
|
1309
|
+
)(function* (part) {
|
|
1310
|
+
if (part.type === "file") {
|
|
1311
|
+
if (part.source?.type === "resource") {
|
|
1312
|
+
const { clientName, uri } = part.source
|
|
1313
|
+
log.info("mcp resource", { clientName, uri, mime: part.mime })
|
|
1314
|
+
const pieces: Draft<MessageV2.Part>[] = [
|
|
1315
|
+
{
|
|
1316
|
+
messageID: info.id,
|
|
1317
|
+
sessionID: input.sessionID,
|
|
1318
|
+
type: "text",
|
|
1319
|
+
synthetic: true,
|
|
1320
|
+
text: `Reading MCP resource: ${part.filename} (${uri})`,
|
|
1321
|
+
},
|
|
1322
|
+
]
|
|
1323
|
+
const exit = yield* mcp.readResource(clientName, uri).pipe(Effect.exit)
|
|
1324
|
+
if (Exit.isSuccess(exit)) {
|
|
1325
|
+
const content = exit.value
|
|
1326
|
+
if (!content) throw new Error(`Resource not found: ${clientName}/${uri}`)
|
|
1327
|
+
const items = Array.isArray(content.contents) ? content.contents : [content.contents]
|
|
1328
|
+
for (const c of items) {
|
|
1329
|
+
if ("text" in c && c.text) {
|
|
1330
|
+
pieces.push({
|
|
1331
|
+
messageID: info.id,
|
|
1332
|
+
sessionID: input.sessionID,
|
|
1333
|
+
type: "text",
|
|
1334
|
+
synthetic: true,
|
|
1335
|
+
text: c.text,
|
|
1336
|
+
})
|
|
1337
|
+
} else if ("blob" in c && c.blob) {
|
|
1338
|
+
const mime = "mimeType" in c ? c.mimeType : part.mime
|
|
1339
|
+
pieces.push({
|
|
1340
|
+
messageID: info.id,
|
|
1341
|
+
sessionID: input.sessionID,
|
|
1342
|
+
type: "text",
|
|
1343
|
+
synthetic: true,
|
|
1344
|
+
text: `[Binary content: ${mime}]`,
|
|
1345
|
+
})
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID })
|
|
1349
|
+
} else {
|
|
1350
|
+
const error = Cause.squash(exit.cause)
|
|
1351
|
+
log.error("failed to read MCP resource", { error, clientName, uri })
|
|
1352
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1353
|
+
pieces.push({
|
|
1354
|
+
messageID: info.id,
|
|
1355
|
+
sessionID: input.sessionID,
|
|
1356
|
+
type: "text",
|
|
1357
|
+
synthetic: true,
|
|
1358
|
+
text: `Failed to read MCP resource ${part.filename}: ${message}`,
|
|
1359
|
+
})
|
|
1360
|
+
}
|
|
1361
|
+
return pieces
|
|
1362
|
+
}
|
|
1363
|
+
const url = new URL(part.url)
|
|
1364
|
+
switch (url.protocol) {
|
|
1365
|
+
case "data:":
|
|
1366
|
+
if (part.mime === "text/plain") {
|
|
1367
|
+
return [
|
|
1368
|
+
{
|
|
1369
|
+
messageID: info.id,
|
|
1370
|
+
sessionID: input.sessionID,
|
|
1371
|
+
type: "text",
|
|
1372
|
+
synthetic: true,
|
|
1373
|
+
text: `Called the Read tool with the following input: ${JSON.stringify({ filePath: part.filename })}`,
|
|
1374
|
+
},
|
|
1375
|
+
{
|
|
1376
|
+
messageID: info.id,
|
|
1377
|
+
sessionID: input.sessionID,
|
|
1378
|
+
type: "text",
|
|
1379
|
+
synthetic: true,
|
|
1380
|
+
text: decodeDataUrl(part.url),
|
|
1381
|
+
},
|
|
1382
|
+
{ ...part, messageID: info.id, sessionID: input.sessionID },
|
|
1383
|
+
]
|
|
1384
|
+
}
|
|
1385
|
+
break
|
|
1386
|
+
case "file:": {
|
|
1387
|
+
log.info("file", { mime: part.mime })
|
|
1388
|
+
const filepath = fileURLToPath(part.url)
|
|
1389
|
+
if (yield* fsys.isDir(filepath)) part.mime = "application/x-directory"
|
|
1390
|
+
|
|
1391
|
+
const { read } = yield* registry.named()
|
|
1392
|
+
const execRead = (args: Parameters<typeof read.execute>[0], extra?: Tool.Context["extra"]) => {
|
|
1393
|
+
const controller = new AbortController()
|
|
1394
|
+
return read
|
|
1395
|
+
.execute(args, {
|
|
1396
|
+
sessionID: input.sessionID,
|
|
1397
|
+
abort: controller.signal,
|
|
1398
|
+
agent: input.agent!,
|
|
1399
|
+
messageID: info.id,
|
|
1400
|
+
extra: { bypassCwdCheck: true, ...extra },
|
|
1401
|
+
messages: [],
|
|
1402
|
+
metadata: () => Effect.void,
|
|
1403
|
+
ask: () => Effect.void,
|
|
1404
|
+
})
|
|
1405
|
+
.pipe(Effect.onInterrupt(() => Effect.sync(() => controller.abort())))
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
if (part.mime === "text/plain") {
|
|
1409
|
+
let offset: number | undefined
|
|
1410
|
+
let limit: number | undefined
|
|
1411
|
+
const range = { start: url.searchParams.get("start"), end: url.searchParams.get("end") }
|
|
1412
|
+
if (range.start != null) {
|
|
1413
|
+
const filePathURI = part.url.split("?")[0]
|
|
1414
|
+
let start = parseInt(range.start)
|
|
1415
|
+
let end = range.end ? parseInt(range.end) : undefined
|
|
1416
|
+
if (start === end) {
|
|
1417
|
+
const symbols = yield* lsp.documentSymbol(filePathURI).pipe(Effect.catch(() => Effect.succeed([])))
|
|
1418
|
+
for (const symbol of symbols) {
|
|
1419
|
+
let r: LSP.Range | undefined
|
|
1420
|
+
if ("range" in symbol) r = symbol.range
|
|
1421
|
+
else if ("location" in symbol) r = symbol.location.range
|
|
1422
|
+
if (r?.start?.line && r?.start?.line === start) {
|
|
1423
|
+
start = r.start.line
|
|
1424
|
+
end = r?.end?.line ?? start
|
|
1425
|
+
break
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
offset = Math.max(start, 1)
|
|
1430
|
+
if (end) limit = end - (offset - 1)
|
|
1431
|
+
}
|
|
1432
|
+
const args = { filePath: filepath, offset, limit }
|
|
1433
|
+
const pieces: Draft<MessageV2.Part>[] = [
|
|
1434
|
+
{
|
|
1435
|
+
messageID: info.id,
|
|
1436
|
+
sessionID: input.sessionID,
|
|
1437
|
+
type: "text",
|
|
1438
|
+
synthetic: true,
|
|
1439
|
+
text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
|
|
1440
|
+
},
|
|
1441
|
+
]
|
|
1442
|
+
const exit = yield* provider.getModel(info.model.providerID, info.model.modelID).pipe(
|
|
1443
|
+
Effect.flatMap((mdl) => execRead(args, { model: mdl })),
|
|
1444
|
+
Effect.exit,
|
|
1445
|
+
)
|
|
1446
|
+
if (Exit.isSuccess(exit)) {
|
|
1447
|
+
const result = exit.value
|
|
1448
|
+
pieces.push({
|
|
1449
|
+
messageID: info.id,
|
|
1450
|
+
sessionID: input.sessionID,
|
|
1451
|
+
type: "text",
|
|
1452
|
+
synthetic: true,
|
|
1453
|
+
text: result.output,
|
|
1454
|
+
})
|
|
1455
|
+
if (result.attachments?.length) {
|
|
1456
|
+
pieces.push(
|
|
1457
|
+
...result.attachments.map((a) => ({
|
|
1458
|
+
...a,
|
|
1459
|
+
synthetic: true,
|
|
1460
|
+
filename: a.filename ?? part.filename,
|
|
1461
|
+
messageID: info.id,
|
|
1462
|
+
sessionID: input.sessionID,
|
|
1463
|
+
})),
|
|
1464
|
+
)
|
|
1465
|
+
} else {
|
|
1466
|
+
pieces.push({ ...part, messageID: info.id, sessionID: input.sessionID })
|
|
1467
|
+
}
|
|
1468
|
+
} else {
|
|
1469
|
+
const error = Cause.squash(exit.cause)
|
|
1470
|
+
log.error("failed to read file", { error })
|
|
1471
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1472
|
+
yield* bus.publish(Session.Event.Error, {
|
|
1473
|
+
sessionID: input.sessionID,
|
|
1474
|
+
error: new NamedError.Unknown({ message }).toObject(),
|
|
1475
|
+
})
|
|
1476
|
+
pieces.push({
|
|
1477
|
+
messageID: info.id,
|
|
1478
|
+
sessionID: input.sessionID,
|
|
1479
|
+
type: "text",
|
|
1480
|
+
synthetic: true,
|
|
1481
|
+
text: `Read tool failed to read ${filepath} with the following error: ${message}`,
|
|
1482
|
+
})
|
|
1483
|
+
}
|
|
1484
|
+
return pieces
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if (part.mime === "application/x-directory") {
|
|
1488
|
+
const args = { filePath: filepath }
|
|
1489
|
+
const exit = yield* execRead(args).pipe(Effect.exit)
|
|
1490
|
+
if (Exit.isFailure(exit)) {
|
|
1491
|
+
const error = Cause.squash(exit.cause)
|
|
1492
|
+
log.error("failed to read directory", { error })
|
|
1493
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
1494
|
+
yield* bus.publish(Session.Event.Error, {
|
|
1495
|
+
sessionID: input.sessionID,
|
|
1496
|
+
error: new NamedError.Unknown({ message }).toObject(),
|
|
1497
|
+
})
|
|
1498
|
+
return [
|
|
1499
|
+
{
|
|
1500
|
+
messageID: info.id,
|
|
1501
|
+
sessionID: input.sessionID,
|
|
1502
|
+
type: "text",
|
|
1503
|
+
synthetic: true,
|
|
1504
|
+
text: `Read tool failed to read ${filepath} with the following error: ${message}`,
|
|
1505
|
+
},
|
|
1506
|
+
]
|
|
1507
|
+
}
|
|
1508
|
+
return [
|
|
1509
|
+
{
|
|
1510
|
+
messageID: info.id,
|
|
1511
|
+
sessionID: input.sessionID,
|
|
1512
|
+
type: "text",
|
|
1513
|
+
synthetic: true,
|
|
1514
|
+
text: `Called the Read tool with the following input: ${JSON.stringify(args)}`,
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
messageID: info.id,
|
|
1518
|
+
sessionID: input.sessionID,
|
|
1519
|
+
type: "text",
|
|
1520
|
+
synthetic: true,
|
|
1521
|
+
text: exit.value.output,
|
|
1522
|
+
},
|
|
1523
|
+
{ ...part, messageID: info.id, sessionID: input.sessionID },
|
|
1524
|
+
]
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
return [
|
|
1528
|
+
{
|
|
1529
|
+
messageID: info.id,
|
|
1530
|
+
sessionID: input.sessionID,
|
|
1531
|
+
type: "text",
|
|
1532
|
+
synthetic: true,
|
|
1533
|
+
text: `Called the Read tool with the following input: {"filePath":"${filepath}"}`,
|
|
1534
|
+
},
|
|
1535
|
+
{
|
|
1536
|
+
id: part.id,
|
|
1537
|
+
messageID: info.id,
|
|
1538
|
+
sessionID: input.sessionID,
|
|
1539
|
+
type: "file",
|
|
1540
|
+
url:
|
|
1541
|
+
`data:${part.mime};base64,` +
|
|
1542
|
+
Buffer.from(yield* fsys.readFile(filepath).pipe(Effect.catch(Effect.die))).toString("base64"),
|
|
1543
|
+
mime: part.mime,
|
|
1544
|
+
filename: part.filename!,
|
|
1545
|
+
source: part.source,
|
|
1546
|
+
},
|
|
1547
|
+
]
|
|
1548
|
+
}
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (part.type === "agent") {
|
|
1553
|
+
const perm = Permission.evaluate("task", part.name, ag.permission)
|
|
1554
|
+
const hint = perm.action === "deny" ? " . Invoked by user; guaranteed to exist." : ""
|
|
1555
|
+
return [
|
|
1556
|
+
{ ...part, messageID: info.id, sessionID: input.sessionID },
|
|
1557
|
+
{
|
|
1558
|
+
messageID: info.id,
|
|
1559
|
+
sessionID: input.sessionID,
|
|
1560
|
+
type: "text",
|
|
1561
|
+
synthetic: true,
|
|
1562
|
+
text:
|
|
1563
|
+
" Use the above message and context to generate a prompt and call the actor tool with subagent: " +
|
|
1564
|
+
part.name +
|
|
1565
|
+
hint,
|
|
1566
|
+
},
|
|
1567
|
+
]
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
return [{ ...part, messageID: info.id, sessionID: input.sessionID }]
|
|
1571
|
+
})
|
|
1572
|
+
|
|
1573
|
+
const parts = yield* Effect.forEach(input.parts, resolvePart, { concurrency: "unbounded" }).pipe(
|
|
1574
|
+
Effect.map((x) => x.flat().map(assign)),
|
|
1575
|
+
)
|
|
1576
|
+
|
|
1577
|
+
yield* plugin.trigger(
|
|
1578
|
+
"chat.message",
|
|
1579
|
+
{
|
|
1580
|
+
sessionID: input.sessionID,
|
|
1581
|
+
agent: input.agent,
|
|
1582
|
+
model: input.model,
|
|
1583
|
+
messageID: input.messageID,
|
|
1584
|
+
variant: input.variant,
|
|
1585
|
+
},
|
|
1586
|
+
{ message: info, parts },
|
|
1587
|
+
)
|
|
1588
|
+
|
|
1589
|
+
const parsed = MessageV2.Info.safeParse(info)
|
|
1590
|
+
if (!parsed.success) {
|
|
1591
|
+
log.error("invalid user message before save", {
|
|
1592
|
+
sessionID: input.sessionID,
|
|
1593
|
+
messageID: info.id,
|
|
1594
|
+
agent: info.agent,
|
|
1595
|
+
model: info.model,
|
|
1596
|
+
issues: parsed.error.issues,
|
|
1597
|
+
})
|
|
1598
|
+
}
|
|
1599
|
+
parts.forEach((part, index) => {
|
|
1600
|
+
const p = MessageV2.Part.safeParse(part)
|
|
1601
|
+
if (p.success) return
|
|
1602
|
+
log.error("invalid user part before save", {
|
|
1603
|
+
sessionID: input.sessionID,
|
|
1604
|
+
messageID: info.id,
|
|
1605
|
+
partID: part.id,
|
|
1606
|
+
partType: part.type,
|
|
1607
|
+
index,
|
|
1608
|
+
issues: p.error.issues,
|
|
1609
|
+
part,
|
|
1610
|
+
})
|
|
1611
|
+
})
|
|
1612
|
+
|
|
1613
|
+
yield* sessions.updateMessage(info)
|
|
1614
|
+
for (const part of parts) yield* sessions.updatePart(part)
|
|
1615
|
+
|
|
1616
|
+
return { info, parts }
|
|
1617
|
+
}, Effect.scoped)
|
|
1618
|
+
|
|
1619
|
+
const sweepOrphanAssistants = Effect.fn("SessionPrompt.sweepOrphanAssistants")(function* (sessionID: SessionID) {
|
|
1620
|
+
const msgs = yield* sessions.messages({ sessionID, agentID: "*" })
|
|
1621
|
+
const now = Date.now()
|
|
1622
|
+
// 1 hour — must exceed Task 1's chunkMs (300s) plus Task 2's
|
|
1623
|
+
// PERSISTENT_RETRY worst-case backoff (10 attempts × 5 min cap =
|
|
1624
|
+
// 50 min) so a still-active in-flight request is never falsely
|
|
1625
|
+
// swept while its retry chain is making progress.
|
|
1626
|
+
const ORPHAN_AGE_MS = 3_600_000
|
|
1627
|
+
for (const m of msgs) {
|
|
1628
|
+
if (m.info.role !== "assistant") continue
|
|
1629
|
+
if (m.info.time?.completed) continue
|
|
1630
|
+
const created = m.info.time?.created ?? 0
|
|
1631
|
+
if (now - created < ORPHAN_AGE_MS) continue
|
|
1632
|
+
m.info.time = { ...m.info.time, completed: now }
|
|
1633
|
+
m.info.error =
|
|
1634
|
+
m.info.error ??
|
|
1635
|
+
new MessageV2.AbortedError({
|
|
1636
|
+
message: "Abandoned: previous request interrupted before completion",
|
|
1637
|
+
}).toObject()
|
|
1638
|
+
yield* sessions.updateMessage(m.info).pipe(
|
|
1639
|
+
Effect.catchCause((cause) =>
|
|
1640
|
+
elog.warn("orphan-update-failed", {
|
|
1641
|
+
sessionID,
|
|
1642
|
+
messageID: m.info.id,
|
|
1643
|
+
cause,
|
|
1644
|
+
}),
|
|
1645
|
+
),
|
|
1646
|
+
)
|
|
1647
|
+
yield* elog.info("orphan-assistant-cleared", {
|
|
1648
|
+
sessionID,
|
|
1649
|
+
messageID: m.info.id,
|
|
1650
|
+
})
|
|
1651
|
+
}
|
|
1652
|
+
})
|
|
1653
|
+
|
|
1654
|
+
const prompt: (input: PromptInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.prompt")(
|
|
1655
|
+
function* (input: PromptInput) {
|
|
1656
|
+
const session = yield* sessions.get(input.sessionID)
|
|
1657
|
+
if (input.source !== "spawn" && input.source !== "hook") {
|
|
1658
|
+
yield* revert.cleanup(session)
|
|
1659
|
+
yield* sweepOrphanAssistants(input.sessionID)
|
|
1660
|
+
}
|
|
1661
|
+
const message = yield* createUserMessage(input)
|
|
1662
|
+
yield* sessions.touch(input.sessionID)
|
|
1663
|
+
|
|
1664
|
+
const permissions: Permission.Ruleset = []
|
|
1665
|
+
for (const [t, enabled] of Object.entries(input.tools ?? {})) {
|
|
1666
|
+
permissions.push({ permission: t, action: enabled ? "allow" : "deny", pattern: "*" })
|
|
1667
|
+
}
|
|
1668
|
+
if (permissions.length > 0) {
|
|
1669
|
+
session.permission = permissions
|
|
1670
|
+
yield* sessions.setPermission({ sessionID: session.id, permission: permissions })
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
if (input.noReply === true) return message
|
|
1674
|
+
return yield* loop({ sessionID: input.sessionID, agentID: input.agentID ?? "main", task_id: input.task_id })
|
|
1675
|
+
},
|
|
1676
|
+
)
|
|
1677
|
+
|
|
1678
|
+
const lastAssistant = Effect.fnUntraced(function* (sessionID: SessionID, agentID?: string) {
|
|
1679
|
+
if (agentID !== undefined) {
|
|
1680
|
+
// Agent-scoped: return THIS agent's newest message (assistant preferred).
|
|
1681
|
+
// Critical for concurrent same-session subagents — a session-wide lookup
|
|
1682
|
+
// collapses concurrent actors' return values onto whichever finished last.
|
|
1683
|
+
// messages() yields oldest-first/newest-last, so findLast picks the newest
|
|
1684
|
+
// assistant and the last element is the newest message overall.
|
|
1685
|
+
const own = yield* sessions.messages({ sessionID, agentID })
|
|
1686
|
+
const lastAsst = own.findLast((m) => m.info.role === "assistant")
|
|
1687
|
+
if (lastAsst) return lastAsst
|
|
1688
|
+
if (own.length > 0) return own[own.length - 1]
|
|
1689
|
+
// fall through to session-wide if this agent has no messages yet
|
|
1690
|
+
}
|
|
1691
|
+
const match = yield* sessions.findMessage(sessionID, (m) => m.info.role !== "user", { agentID: "*" })
|
|
1692
|
+
if (Option.isSome(match)) return match.value
|
|
1693
|
+
const msgs = yield* sessions.messages({ sessionID, limit: 1, agentID: "*" })
|
|
1694
|
+
if (msgs.length > 0) return msgs[0]
|
|
1695
|
+
throw new Error("Impossible")
|
|
1696
|
+
})
|
|
1697
|
+
|
|
1698
|
+
const runLoop: (sessionID: SessionID, agentID?: string, task_id?: string) => Effect.Effect<MessageV2.WithParts> = Effect.fn(
|
|
1699
|
+
"SessionPrompt.run",
|
|
1700
|
+
)(
|
|
1701
|
+
function* (sessionID: SessionID, agentID?: string, task_id?: string) {
|
|
1702
|
+
const ctx = yield* InstanceState.context
|
|
1703
|
+
const slog = elog.with({ sessionID })
|
|
1704
|
+
let structured: unknown | undefined
|
|
1705
|
+
let step = 0
|
|
1706
|
+
const session = yield* sessions.get(sessionID)
|
|
1707
|
+
let lastFinishedForPrune: MessageV2.Assistant | undefined
|
|
1708
|
+
let lastModelForPrune: Provider.Model | undefined
|
|
1709
|
+
let outputLengthContinuations = 0
|
|
1710
|
+
// Shared local counter for "model finished but produced nothing usable"
|
|
1711
|
+
// (think-only / empty). T04's generic-invalid retries reuse this same
|
|
1712
|
+
// counter — do not add a second one. Local to runLoop so a fresh user
|
|
1713
|
+
// turn resets it (no cross-message pollution), same as outputLengthContinuations.
|
|
1714
|
+
let invalidContinuations = 0
|
|
1715
|
+
// structured-output 专用 retry:上限来自 lastUser.format.retryCount(默认 2),
|
|
1716
|
+
// 与 invalidContinuations(generic invalid)分离,互不污染。局部于 runLoop,
|
|
1717
|
+
// 新一轮用户 turn 自动归零。
|
|
1718
|
+
let structuredRetries = 0
|
|
1719
|
+
const agentMetrics = { tokens_in: 0, tokens_out: 0, files_changed: 0 }
|
|
1720
|
+
const publishAgentRequest = (phase: string, taskType: string) =>
|
|
1721
|
+
bus
|
|
1722
|
+
.publish(Metrics.AgentRequest, {
|
|
1723
|
+
sessionID,
|
|
1724
|
+
phase,
|
|
1725
|
+
task_type: taskType,
|
|
1726
|
+
surface: Flag.tulingcode_CLIENT,
|
|
1727
|
+
total_tokens_in: agentMetrics.tokens_in,
|
|
1728
|
+
total_tokens_out: agentMetrics.tokens_out,
|
|
1729
|
+
files_changed: agentMetrics.files_changed,
|
|
1730
|
+
validation_status: "skipped",
|
|
1731
|
+
})
|
|
1732
|
+
.pipe(Effect.ignore)
|
|
1733
|
+
// Trim freed space but `lastFinished.tokens` still reflects pre-trim state.
|
|
1734
|
+
// Skip one overflow check so the model can respond on the trimmed context;
|
|
1735
|
+
// its new assistant message will carry accurate tokens for the next check.
|
|
1736
|
+
let skipOverflowCheck = false
|
|
1737
|
+
|
|
1738
|
+
// Contract (T05): on finish="length", inject a continuation nudge ONLY for
|
|
1739
|
+
// plain text. If any non-providerExecuted client tool part exists we bail
|
|
1740
|
+
// (return false) and let classify route the normal tool-observation re-loop.
|
|
1741
|
+
// This guarantees "no output-length continuation when a tool is involved" —
|
|
1742
|
+
// it does NOT guarantee a stream-time-truncated tool never executed, since
|
|
1743
|
+
// the AI SDK runs tools mid-stream before the finish reason is known.
|
|
1744
|
+
const autoContinueOutputLength = Effect.fn("SessionPrompt.autoContinueOutputLength")(function* (input: {
|
|
1745
|
+
lastUser: MessageV2.User
|
|
1746
|
+
assistant: MessageV2.Assistant
|
|
1747
|
+
}) {
|
|
1748
|
+
if (input.assistant.finish !== "length" || input.assistant.error || input.assistant.summary) return false
|
|
1749
|
+
if (
|
|
1750
|
+
MessageV2.parts(input.assistant.id).some((part) => part.type === "tool" && !part.metadata?.providerExecuted)
|
|
1751
|
+
) {
|
|
1752
|
+
return false
|
|
1753
|
+
}
|
|
1754
|
+
if (outputLengthContinuations >= OUTPUT_LENGTH_CONTINUATION_LIMIT) {
|
|
1755
|
+
input.assistant.error = new MessageV2.OutputLengthError({}).toObject()
|
|
1756
|
+
yield* sessions.updateMessage(input.assistant)
|
|
1757
|
+
yield* bus.publish(Session.Event.Error, {
|
|
1758
|
+
sessionID: input.assistant.sessionID,
|
|
1759
|
+
error: input.assistant.error,
|
|
1760
|
+
})
|
|
1761
|
+
return false
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
outputLengthContinuations++
|
|
1765
|
+
yield* slog.info("auto-continuing output length", { attempt: outputLengthContinuations })
|
|
1766
|
+
const msg = yield* sessions.updateMessage({
|
|
1767
|
+
id: MessageID.ascending(),
|
|
1768
|
+
role: "user" as const,
|
|
1769
|
+
sessionID: input.lastUser.sessionID,
|
|
1770
|
+
agentID: input.lastUser.agentID,
|
|
1771
|
+
agent: input.lastUser.agent,
|
|
1772
|
+
model: input.lastUser.model,
|
|
1773
|
+
tools: input.lastUser.tools,
|
|
1774
|
+
format: input.lastUser.format,
|
|
1775
|
+
time: { created: Date.now() },
|
|
1776
|
+
})
|
|
1777
|
+
yield* sessions.updatePart({
|
|
1778
|
+
id: PartID.ascending(),
|
|
1779
|
+
messageID: msg.id,
|
|
1780
|
+
sessionID: msg.sessionID,
|
|
1781
|
+
type: "text",
|
|
1782
|
+
synthetic: true,
|
|
1783
|
+
text: [
|
|
1784
|
+
"<system-reminder>",
|
|
1785
|
+
"The previous assistant response hit the model output token limit before completing.",
|
|
1786
|
+
"Continue the same task from the exact point where it stopped.",
|
|
1787
|
+
"Do not restart, recap, or repeat prior reasoning. Keep reasoning concise, prefer concrete tool calls or final output, and only stop when the user's task is complete or genuinely blocked.",
|
|
1788
|
+
"</system-reminder>",
|
|
1789
|
+
].join("\n"),
|
|
1790
|
+
} satisfies MessageV2.TextPart)
|
|
1791
|
+
return true
|
|
1792
|
+
})
|
|
1793
|
+
|
|
1794
|
+
// Task stop-condition gate (main agent only). Before honoring a stop,
|
|
1795
|
+
// list non-terminal tasks in the session: if any remain, inject a
|
|
1796
|
+
// nudge as a synthetic user turn and re-enter (return true) so the
|
|
1797
|
+
// model closes them with `task done` / `task abandon`. ReAct cap +
|
|
1798
|
+
// counter mirror the goal gate; cap-exceeded allows stop with a
|
|
1799
|
+
// warn log (no reportedStatus on main). owner=undefined picks up
|
|
1800
|
+
// tasks orphaned by subagent gates that hit their own cap. Runs
|
|
1801
|
+
// BEFORE goalGate because task state is cheaper to settle and a
|
|
1802
|
+
// pending-task board pollutes any goal verdict.
|
|
1803
|
+
const taskGate = Effect.fn("SessionPrompt.taskGate")(function* (lastUser: MessageV2.User) {
|
|
1804
|
+
if ((agentID ?? "main") !== "main") return false
|
|
1805
|
+
// If the main agent has the `task` tool stripped (Permission.disabled),
|
|
1806
|
+
// a nudge to call `task done` is unsatisfiable and would re-loop to
|
|
1807
|
+
// cap. Skip the gate entirely. Mirrors the canWrite skip in
|
|
1808
|
+
// actor/spawn.ts (Permission.disabled(["write"], ...) check on
|
|
1809
|
+
// forkAgentInfo). Per-session resolution means this checks the
|
|
1810
|
+
// agent's static permission only (good enough for v1; session-
|
|
1811
|
+
// level overrides re-enabling task on a denied agent are
|
|
1812
|
+
// pathological and out of scope).
|
|
1813
|
+
const mainAgent = yield* agents.get("main").pipe(Effect.orElseSucceed(() => undefined))
|
|
1814
|
+
if (mainAgent && Permission.disabled(["task"], mainAgent.permission).has("task")) return false
|
|
1815
|
+
// Per-message `tools` is the second tool-strip layer (llm.ts:720
|
|
1816
|
+
// `input.user.tools?.[k] !== false` filter), separate from
|
|
1817
|
+
// Permission.disabled. A slash command pinning a narrow toolset for
|
|
1818
|
+
// its turn can drop `task` even when permission allows it; nudging
|
|
1819
|
+
// is then unsatisfiable. Same skip rationale, narrower window.
|
|
1820
|
+
if (lastUser.tools?.["task"] === false) return false
|
|
1821
|
+
|
|
1822
|
+
const count = yield* taskGateState.get(sessionID)
|
|
1823
|
+
// runLoop is annotated `R = never`; TaskGate.decide raises a
|
|
1824
|
+
// TaskRegistry.Service requirement that we close locally with the
|
|
1825
|
+
// layer-resolved binding so it doesn't leak into runLoop's R-set.
|
|
1826
|
+
const decision = yield* TaskGate.decide({
|
|
1827
|
+
session_id: sessionID,
|
|
1828
|
+
owner: undefined,
|
|
1829
|
+
reactCount: count,
|
|
1830
|
+
maxReact: MAX_TASK_GATE_MAIN_REACT,
|
|
1831
|
+
mode: "main",
|
|
1832
|
+
}).pipe(Effect.provideService(TaskRegistry.Service, taskRegistry))
|
|
1833
|
+
if (!decision.needReentry) {
|
|
1834
|
+
if (decision.capExceeded) {
|
|
1835
|
+
yield* slog.warn("task gate hit cap; allowing stop", {
|
|
1836
|
+
sessionID,
|
|
1837
|
+
incompleteTasks: decision.incompleteTasks,
|
|
1838
|
+
})
|
|
1839
|
+
}
|
|
1840
|
+
yield* taskGateState.clear(sessionID)
|
|
1841
|
+
return false
|
|
1842
|
+
}
|
|
1843
|
+
yield* taskGateState.bump(sessionID)
|
|
1844
|
+
const reentry = yield* sessions.updateMessage({
|
|
1845
|
+
id: MessageID.ascending(),
|
|
1846
|
+
role: "user" as const,
|
|
1847
|
+
sessionID,
|
|
1848
|
+
agentID: lastUser.agentID,
|
|
1849
|
+
agent: lastUser.agent,
|
|
1850
|
+
model: lastUser.model,
|
|
1851
|
+
tools: lastUser.tools,
|
|
1852
|
+
format: lastUser.format,
|
|
1853
|
+
time: { created: Date.now() },
|
|
1854
|
+
})
|
|
1855
|
+
yield* sessions.updatePart({
|
|
1856
|
+
id: PartID.ascending(),
|
|
1857
|
+
messageID: reentry.id,
|
|
1858
|
+
sessionID,
|
|
1859
|
+
type: "text",
|
|
1860
|
+
synthetic: true,
|
|
1861
|
+
text: decision.reentryText,
|
|
1862
|
+
} satisfies MessageV2.TextPart)
|
|
1863
|
+
return true
|
|
1864
|
+
})
|
|
1865
|
+
|
|
1866
|
+
// Goal stop-condition gate (main agent only). Before honoring a stop,
|
|
1867
|
+
// an independent judge model reads the transcript and decides whether
|
|
1868
|
+
// the active goal is satisfied. Not satisfied → inject the judge's
|
|
1869
|
+
// reason as a synthetic user turn and signal the caller to keep working
|
|
1870
|
+
// (return true). This is the main-loop analogue of actor.preStop ReAct
|
|
1871
|
+
// re-entry, which only fires for spawned actors. fail-open on any judge
|
|
1872
|
+
// error so a flaky judge can never trap the user.
|
|
1873
|
+
const goalGate = Effect.fn("SessionPrompt.goalGate")(function* (lastUser: MessageV2.User) {
|
|
1874
|
+
if ((agentID ?? "main") !== "main") return false
|
|
1875
|
+
const active = yield* goal.get(sessionID)
|
|
1876
|
+
if (!active) return false
|
|
1877
|
+
|
|
1878
|
+
const transcriptMsgs = yield* MessageV2.filterCompactedEffect(sessionID, {
|
|
1879
|
+
contextFrom: session.contextFrom,
|
|
1880
|
+
contextWatermark: session.contextWatermark,
|
|
1881
|
+
agentID: "main",
|
|
1882
|
+
})
|
|
1883
|
+
// Anchor the verdict to the assistant turn the judge just evaluated, so
|
|
1884
|
+
// the TUI can render a per-turn marker the user can trace back to.
|
|
1885
|
+
const judgedMessageID = transcriptMsgs.findLast((m) => m.info.role === "assistant")?.info.id
|
|
1886
|
+
const verdict = yield* goal
|
|
1887
|
+
.evaluate({
|
|
1888
|
+
condition: active.condition,
|
|
1889
|
+
msgs: transcriptMsgs,
|
|
1890
|
+
model: lastUser.model,
|
|
1891
|
+
})
|
|
1892
|
+
.pipe(
|
|
1893
|
+
Effect.catch((err) =>
|
|
1894
|
+
Effect.gen(function* () {
|
|
1895
|
+
yield* slog.warn("goal judge failed; allowing stop", { error: String(err) })
|
|
1896
|
+
return { ok: true, reason: "judge error", judgeFailed: true } as Goal.Verdict & {
|
|
1897
|
+
judgeFailed: true
|
|
1898
|
+
}
|
|
1899
|
+
}),
|
|
1900
|
+
),
|
|
1901
|
+
)
|
|
1902
|
+
|
|
1903
|
+
if (verdict.ok || verdict.impossible) {
|
|
1904
|
+
yield* slog.info("goal satisfied; allowing stop", {
|
|
1905
|
+
sessionID,
|
|
1906
|
+
impossible: verdict.impossible === true,
|
|
1907
|
+
})
|
|
1908
|
+
// Publish the final verdict (goal cleared) so the TUI can render the
|
|
1909
|
+
// ✓/⊘ result line before the indicator disappears. goal.clear also
|
|
1910
|
+
// publishes goal:undefined, but the TUI keeps lastVerdict sticky.
|
|
1911
|
+
yield* bus.publish(Goal.Event.Updated, {
|
|
1912
|
+
sessionID,
|
|
1913
|
+
goal: undefined,
|
|
1914
|
+
lastVerdict: {
|
|
1915
|
+
...verdict,
|
|
1916
|
+
attempt: active.react,
|
|
1917
|
+
messageID: judgedMessageID,
|
|
1918
|
+
error: "judgeFailed" in verdict ? true : undefined,
|
|
1919
|
+
},
|
|
1920
|
+
})
|
|
1921
|
+
yield* goal.clear(sessionID)
|
|
1922
|
+
return false
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
const count = yield* goal.bumpReact(sessionID)
|
|
1926
|
+
if (count > MAX_GOAL_REACT) {
|
|
1927
|
+
yield* slog.warn("goal hit MAX_GOAL_REACT cap; allowing stop", {
|
|
1928
|
+
sessionID,
|
|
1929
|
+
condition: active.condition,
|
|
1930
|
+
count,
|
|
1931
|
+
})
|
|
1932
|
+
yield* bus.publish(Goal.Event.Updated, {
|
|
1933
|
+
sessionID,
|
|
1934
|
+
goal: undefined,
|
|
1935
|
+
lastVerdict: { ...verdict, attempt: count, messageID: judgedMessageID },
|
|
1936
|
+
})
|
|
1937
|
+
yield* goal.clear(sessionID)
|
|
1938
|
+
return false
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
yield* slog.info("goal not satisfied; re-entering", { sessionID, attempt: count })
|
|
1942
|
+
yield* bus.publish(Goal.Event.Updated, {
|
|
1943
|
+
sessionID,
|
|
1944
|
+
goal: { condition: active.condition },
|
|
1945
|
+
lastVerdict: { ...verdict, attempt: count, messageID: judgedMessageID },
|
|
1946
|
+
})
|
|
1947
|
+
const reentry = yield* sessions.updateMessage({
|
|
1948
|
+
id: MessageID.ascending(),
|
|
1949
|
+
role: "user" as const,
|
|
1950
|
+
sessionID,
|
|
1951
|
+
agentID: lastUser.agentID,
|
|
1952
|
+
agent: lastUser.agent,
|
|
1953
|
+
model: lastUser.model,
|
|
1954
|
+
tools: lastUser.tools,
|
|
1955
|
+
format: lastUser.format,
|
|
1956
|
+
time: { created: Date.now() },
|
|
1957
|
+
})
|
|
1958
|
+
yield* sessions.updatePart({
|
|
1959
|
+
id: PartID.ascending(),
|
|
1960
|
+
messageID: reentry.id,
|
|
1961
|
+
sessionID,
|
|
1962
|
+
type: "text",
|
|
1963
|
+
synthetic: true,
|
|
1964
|
+
text: [
|
|
1965
|
+
"<system-reminder>",
|
|
1966
|
+
`Your goal is not yet satisfied: "${active.condition}".`,
|
|
1967
|
+
"A judge reviewed the transcript and reported what is still missing:",
|
|
1968
|
+
verdict.reason,
|
|
1969
|
+
"Keep working toward the goal. Do not stop until it is genuinely met or impossible.",
|
|
1970
|
+
"</system-reminder>",
|
|
1971
|
+
].join("\n"),
|
|
1972
|
+
} satisfies MessageV2.TextPart)
|
|
1973
|
+
return true
|
|
1974
|
+
})
|
|
1975
|
+
|
|
1976
|
+
// think-only (reasoning only) / empty (nothing at all) steps finish with
|
|
1977
|
+
// a non-tool stop but carry no usable answer. Without intervention the loop
|
|
1978
|
+
// breaks and hands the user an assistant with no final text. Nudge the model
|
|
1979
|
+
// to produce a final answer or call a real tool; give up (write a terminal
|
|
1980
|
+
// error) once the shared counter is exhausted so we never loop forever.
|
|
1981
|
+
const autoContinueInvalidOutput = Effect.fn("SessionPrompt.autoContinueInvalidOutput")(function* (input: {
|
|
1982
|
+
lastUser: MessageV2.User
|
|
1983
|
+
assistant: MessageV2.Assistant
|
|
1984
|
+
reason: string
|
|
1985
|
+
}) {
|
|
1986
|
+
if (input.assistant.error || input.assistant.summary || input.assistant.structured !== undefined) return false
|
|
1987
|
+
if (invalidContinuations >= INVALID_OUTPUT_CONTINUATION_LIMIT) {
|
|
1988
|
+
input.assistant.error = new MessageV2.InvalidOutputError({ message: input.reason }).toObject()
|
|
1989
|
+
yield* sessions.updateMessage(input.assistant)
|
|
1990
|
+
yield* bus.publish(Session.Event.Error, {
|
|
1991
|
+
sessionID: input.assistant.sessionID,
|
|
1992
|
+
error: input.assistant.error,
|
|
1993
|
+
})
|
|
1994
|
+
return false
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
invalidContinuations++
|
|
1998
|
+
yield* slog.info("auto-continuing invalid output", { attempt: invalidContinuations, reason: input.reason })
|
|
1999
|
+
const msg = yield* sessions.updateMessage({
|
|
2000
|
+
id: MessageID.ascending(),
|
|
2001
|
+
role: "user" as const,
|
|
2002
|
+
sessionID: input.lastUser.sessionID,
|
|
2003
|
+
agentID: input.lastUser.agentID,
|
|
2004
|
+
agent: input.lastUser.agent,
|
|
2005
|
+
model: input.lastUser.model,
|
|
2006
|
+
tools: input.lastUser.tools,
|
|
2007
|
+
format: input.lastUser.format,
|
|
2008
|
+
time: { created: Date.now() },
|
|
2009
|
+
})
|
|
2010
|
+
yield* sessions.updatePart({
|
|
2011
|
+
id: PartID.ascending(),
|
|
2012
|
+
messageID: msg.id,
|
|
2013
|
+
sessionID: msg.sessionID,
|
|
2014
|
+
type: "text",
|
|
2015
|
+
synthetic: true,
|
|
2016
|
+
text: [
|
|
2017
|
+
"<system-reminder>",
|
|
2018
|
+
"Your previous response contained no usable answer (it had only reasoning, or was empty).",
|
|
2019
|
+
"Provide a final answer to the user now, or call a valid tool to make progress on the task.",
|
|
2020
|
+
"Do not respond with only reasoning/thinking.",
|
|
2021
|
+
"</system-reminder>",
|
|
2022
|
+
].join("\n"),
|
|
2023
|
+
} satisfies MessageV2.TextPart)
|
|
2024
|
+
return true
|
|
2025
|
+
})
|
|
2026
|
+
|
|
2027
|
+
// json_schema mode but the model never produced structured output (plain
|
|
2028
|
+
// text stop, empty, think-only, or any other non-tool terminal). Retry up
|
|
2029
|
+
// to lastUser.format.retryCount with a repair nudge; on exhaustion write a
|
|
2030
|
+
// StructuredOutputError carrying the *real* retry count. Separate from
|
|
2031
|
+
// invalidContinuations: structured retries are bounded by the per-request
|
|
2032
|
+
// retryCount, not the generic invalid-output limit.
|
|
2033
|
+
const autoRetryStructuredOutput = Effect.fn("SessionPrompt.autoRetryStructuredOutput")(function* (input: {
|
|
2034
|
+
lastUser: MessageV2.User
|
|
2035
|
+
assistant: MessageV2.Assistant
|
|
2036
|
+
}) {
|
|
2037
|
+
if (input.assistant.error || input.assistant.summary || input.assistant.structured !== undefined) return false
|
|
2038
|
+
const limit = input.lastUser.format?.type === "json_schema" ? input.lastUser.format.retryCount : 0
|
|
2039
|
+
if (structuredRetries >= limit) {
|
|
2040
|
+
input.assistant.error = new MessageV2.StructuredOutputError({
|
|
2041
|
+
message: "Model did not produce structured output",
|
|
2042
|
+
retries: structuredRetries,
|
|
2043
|
+
}).toObject()
|
|
2044
|
+
yield* sessions.updateMessage(input.assistant)
|
|
2045
|
+
yield* bus.publish(Session.Event.Error, {
|
|
2046
|
+
sessionID: input.assistant.sessionID,
|
|
2047
|
+
error: input.assistant.error,
|
|
2048
|
+
})
|
|
2049
|
+
return false
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
structuredRetries++
|
|
2053
|
+
yield* slog.info("retrying structured output", { attempt: structuredRetries })
|
|
2054
|
+
const msg = yield* sessions.updateMessage({
|
|
2055
|
+
id: MessageID.ascending(),
|
|
2056
|
+
role: "user" as const,
|
|
2057
|
+
sessionID: input.lastUser.sessionID,
|
|
2058
|
+
agentID: input.lastUser.agentID,
|
|
2059
|
+
agent: input.lastUser.agent,
|
|
2060
|
+
model: input.lastUser.model,
|
|
2061
|
+
tools: input.lastUser.tools,
|
|
2062
|
+
// Must carry format so the next iteration re-registers the StructuredOutput tool.
|
|
2063
|
+
format: input.lastUser.format,
|
|
2064
|
+
time: { created: Date.now() },
|
|
2065
|
+
})
|
|
2066
|
+
yield* sessions.updatePart({
|
|
2067
|
+
id: PartID.ascending(),
|
|
2068
|
+
messageID: msg.id,
|
|
2069
|
+
sessionID: msg.sessionID,
|
|
2070
|
+
type: "text",
|
|
2071
|
+
synthetic: true,
|
|
2072
|
+
text: [
|
|
2073
|
+
"<system-reminder>",
|
|
2074
|
+
"Your previous response did not produce valid structured output via the StructuredOutput tool",
|
|
2075
|
+
"(it was plain text, empty, or only reasoning).",
|
|
2076
|
+
"You MUST call the StructuredOutput tool now, passing JSON that matches the requested schema.",
|
|
2077
|
+
"Do not reply with plain text and do not respond with only reasoning/thinking.",
|
|
2078
|
+
"</system-reminder>",
|
|
2079
|
+
].join("\n"),
|
|
2080
|
+
} satisfies MessageV2.TextPart)
|
|
2081
|
+
return true
|
|
2082
|
+
})
|
|
2083
|
+
|
|
2084
|
+
// content-filter is terminal on first occurrence: re-sending the same
|
|
2085
|
+
// turn would just get filtered again, so there is no nudge / counter.
|
|
2086
|
+
// Write a user-visible error (rendered via the session.error toast) and
|
|
2087
|
+
// let the caller break.
|
|
2088
|
+
const writeContentFilterError = Effect.fn("SessionPrompt.writeContentFilterError")(function* (input: {
|
|
2089
|
+
assistant: MessageV2.Assistant
|
|
2090
|
+
}) {
|
|
2091
|
+
if (input.assistant.error) return
|
|
2092
|
+
input.assistant.error = new MessageV2.ContentFilterError({
|
|
2093
|
+
message: "The response was withheld by the model provider's content safety filter.",
|
|
2094
|
+
}).toObject()
|
|
2095
|
+
yield* sessions.updateMessage(input.assistant)
|
|
2096
|
+
yield* bus.publish(Session.Event.Error, {
|
|
2097
|
+
sessionID: input.assistant.sessionID,
|
|
2098
|
+
error: input.assistant.error,
|
|
2099
|
+
})
|
|
2100
|
+
})
|
|
2101
|
+
|
|
2102
|
+
// A `failed` classification (model "error" finish, or an error already set
|
|
2103
|
+
// by the stream-error path) is terminal. If the step already carries an
|
|
2104
|
+
// error (e.g. APIError written when the stream threw, processor.ts:581),
|
|
2105
|
+
// keep it; otherwise write a ModelError so the loop never breaks silently
|
|
2106
|
+
// without a user-visible failure.
|
|
2107
|
+
const writeModelError = Effect.fn("SessionPrompt.writeModelError")(function* (input: {
|
|
2108
|
+
assistant: MessageV2.Assistant
|
|
2109
|
+
reason: string
|
|
2110
|
+
}) {
|
|
2111
|
+
if (input.assistant.error) return
|
|
2112
|
+
input.assistant.error = new MessageV2.ModelError({ message: input.reason }).toObject()
|
|
2113
|
+
yield* sessions.updateMessage(input.assistant)
|
|
2114
|
+
yield* bus.publish(Session.Event.Error, {
|
|
2115
|
+
sessionID: input.assistant.sessionID,
|
|
2116
|
+
error: input.assistant.error,
|
|
2117
|
+
})
|
|
2118
|
+
})
|
|
2119
|
+
|
|
2120
|
+
while (true) {
|
|
2121
|
+
// F55: only main agent sets session status to busy; subagent runners
|
|
2122
|
+
// must not touch session-level status (Runner.onBusy is Effect.void
|
|
2123
|
+
// for non-main actors per F47).
|
|
2124
|
+
if (!agentID || agentID === "main") yield* status.set(sessionID, { type: "busy" })
|
|
2125
|
+
yield* inbox.drain(sessionID, agentID ?? "main").pipe(Effect.ignore)
|
|
2126
|
+
yield* slog.info("loop", { step })
|
|
2127
|
+
|
|
2128
|
+
// F37: filter by agentID so subagent slices stay isolated from the
|
|
2129
|
+
// main agent's slice within the same session. Without this, an actor
|
|
2130
|
+
// (explore/general/etc) spawned via tulingcode's shared-sessionID
|
|
2131
|
+
// design would see the parent's full conversation here and drift
|
|
2132
|
+
// off-task. agentID === "main" => main agent slice (agent_id = 'main'
|
|
2133
|
+
// in DB), agentID === "explore-1" => only explore-1's slice.
|
|
2134
|
+
let msgs = yield* MessageV2.filterCompactedEffect(sessionID, {
|
|
2135
|
+
contextFrom: session.contextFrom,
|
|
2136
|
+
contextWatermark: session.contextWatermark,
|
|
2137
|
+
agentID: agentID ?? "main",
|
|
2138
|
+
})
|
|
2139
|
+
|
|
2140
|
+
let lastUser: MessageV2.User | undefined
|
|
2141
|
+
let lastAssistant: MessageV2.Assistant | undefined
|
|
2142
|
+
let lastFinished: MessageV2.Assistant | undefined
|
|
2143
|
+
let tasks: MessageV2.SubtaskPart[] = []
|
|
2144
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
2145
|
+
const msg = msgs[i]
|
|
2146
|
+
if (!lastUser && msg.info.role === "user") lastUser = msg.info
|
|
2147
|
+
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info
|
|
2148
|
+
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info
|
|
2149
|
+
if (lastUser && lastFinished) break
|
|
2150
|
+
const task = msg.parts.filter((part): part is MessageV2.SubtaskPart => part.type === "subtask")
|
|
2151
|
+
if (task && !lastFinished) tasks.push(...task)
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
if (!lastUser) throw new Error("No user message found in stream. This should never happen.")
|
|
2155
|
+
|
|
2156
|
+
// Per-user-message active recall reminder. Once the session has
|
|
2157
|
+
// any memory artifacts (memory dir populated OR tasks recorded),
|
|
2158
|
+
// append a brief recall protocol so the agent's reflex to query
|
|
2159
|
+
// memory.search / task / actor / Read stays warm across many
|
|
2160
|
+
// post-rebuild turns. Cost ~120 tokens per turn, conditional on
|
|
2161
|
+
// hasMemoryOrTasks.
|
|
2162
|
+
const lastUserMsgForRecall = msgs.findLast((m) => m.info.role === "user")
|
|
2163
|
+
if (lastUserMsgForRecall) {
|
|
2164
|
+
const hasRecallTarget = yield* checkpoint
|
|
2165
|
+
.hasMemoryOrTasks(sessionID)
|
|
2166
|
+
.pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2167
|
+
if (hasRecallTarget) {
|
|
2168
|
+
const sessMemDir = path.join(Global.Path.data, "memory", "sessions", sessionID)
|
|
2169
|
+
const hints = recallHintLines((yield* config.get()).tool)
|
|
2170
|
+
lastUserMsgForRecall.parts.push({
|
|
2171
|
+
id: PartID.ascending(),
|
|
2172
|
+
messageID: lastUserMsgForRecall.info.id,
|
|
2173
|
+
sessionID,
|
|
2174
|
+
type: "text" as const,
|
|
2175
|
+
synthetic: true,
|
|
2176
|
+
text: [
|
|
2177
|
+
"<system-reminder>",
|
|
2178
|
+
`This session has memory at ${sessMemDir}/. Recall content`,
|
|
2179
|
+
"not in your context with:",
|
|
2180
|
+
hints[0],
|
|
2181
|
+
`- Read(file_path="${sessMemDir}/...")`,
|
|
2182
|
+
hints[1],
|
|
2183
|
+
hints[2],
|
|
2184
|
+
"",
|
|
2185
|
+
"Don't ask the user about something memory may already record.",
|
|
2186
|
+
"</system-reminder>",
|
|
2187
|
+
].join("\n"),
|
|
2188
|
+
})
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
|
|
2192
|
+
const lastAssistantMsg = msgs.findLast(
|
|
2193
|
+
(msg) => msg.info.role === "assistant" && msg.info.id === lastAssistant?.id,
|
|
2194
|
+
)
|
|
2195
|
+
// Some providers return "stop" even when the assistant message contains tool calls.
|
|
2196
|
+
// Keep the loop running so tool results can be sent back to the model.
|
|
2197
|
+
// Skip provider-executed tool parts — those were fully handled within the
|
|
2198
|
+
// provider's stream (e.g. DWS Agent Platform) and don't need a re-loop.
|
|
2199
|
+
const hasToolCalls =
|
|
2200
|
+
lastAssistantMsg?.parts.some((part) => part.type === "tool" && !part.metadata?.providerExecuted) ?? false
|
|
2201
|
+
|
|
2202
|
+
if (
|
|
2203
|
+
lastAssistant?.finish === "length" &&
|
|
2204
|
+
!hasToolCalls &&
|
|
2205
|
+
lastUser.id < lastAssistant.id &&
|
|
2206
|
+
(yield* autoContinueOutputLength({ lastUser, assistant: lastAssistant }))
|
|
2207
|
+
) {
|
|
2208
|
+
continue
|
|
2209
|
+
}
|
|
2210
|
+
|
|
2211
|
+
if (lastAssistant) {
|
|
2212
|
+
const classification = classifyAssistantStep({
|
|
2213
|
+
phase: "existing-assistant",
|
|
2214
|
+
lastUser,
|
|
2215
|
+
assistant: lastAssistant,
|
|
2216
|
+
parts: lastAssistantMsg?.parts ?? [],
|
|
2217
|
+
})
|
|
2218
|
+
if (classification.type === "filtered") {
|
|
2219
|
+
yield* writeContentFilterError({ assistant: lastAssistant })
|
|
2220
|
+
yield* slog.info("exiting loop", { classification: classification.type })
|
|
2221
|
+
break
|
|
2222
|
+
}
|
|
2223
|
+
if (classification.type === "failed") {
|
|
2224
|
+
yield* writeModelError({ assistant: lastAssistant, reason: classification.reason })
|
|
2225
|
+
yield* slog.info("exiting loop", { classification: classification.type, reason: classification.reason })
|
|
2226
|
+
break
|
|
2227
|
+
}
|
|
2228
|
+
if (classification.type === "think-only" || classification.type === "invalid") {
|
|
2229
|
+
const reason = classification.type === "invalid" ? classification.reason : "think-only"
|
|
2230
|
+
if (yield* autoContinueInvalidOutput({ lastUser, assistant: lastAssistant, reason })) continue
|
|
2231
|
+
yield* slog.info("exiting loop", { classification: classification.type })
|
|
2232
|
+
break
|
|
2233
|
+
}
|
|
2234
|
+
if (classification.type === "final" && classification.degraded)
|
|
2235
|
+
yield* slog.warn("degraded final on abnormal finish", { finish: lastAssistant.finish })
|
|
2236
|
+
if (classification.type !== "continue") {
|
|
2237
|
+
if (yield* taskGate(lastUser)) continue
|
|
2238
|
+
if (yield* goalGate(lastUser)) continue
|
|
2239
|
+
yield* slog.info("exiting loop", { classification: classification.type })
|
|
2240
|
+
break
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
|
|
2244
|
+
step++
|
|
2245
|
+
if (step === 1)
|
|
2246
|
+
yield* title({
|
|
2247
|
+
session,
|
|
2248
|
+
modelID: lastUser.model.modelID,
|
|
2249
|
+
providerID: lastUser.model.providerID,
|
|
2250
|
+
history: msgs,
|
|
2251
|
+
}).pipe(Effect.ignore, Effect.forkIn(scope))
|
|
2252
|
+
|
|
2253
|
+
if (step === 1 && !session.parentID) {
|
|
2254
|
+
const cfg = yield* config.get()
|
|
2255
|
+
const dreamTrigger = yield* shouldAutoDream(cfg).pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2256
|
+
const distillTrigger = yield* shouldAutoDistill(cfg).pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2257
|
+
const mdl = { providerID: lastUser.model.providerID, modelID: lastUser.model.modelID }
|
|
2258
|
+
// AppRuntime is imported dynamically (not at module top level) to keep
|
|
2259
|
+
// the session layer out of the app-runtime module-init cycle
|
|
2260
|
+
// (prompt → app-runtime → AppLayer → SessionPrompt). Only loaded when a
|
|
2261
|
+
// trigger actually fires. Detached fire-and-forget on the full runtime.
|
|
2262
|
+
if (dreamTrigger || distillTrigger) {
|
|
2263
|
+
const { AppRuntime } = yield* Effect.promise(() => import("@/effect/app-runtime"))
|
|
2264
|
+
if (dreamTrigger) {
|
|
2265
|
+
AppRuntime.runPromise(
|
|
2266
|
+
Session.Service.use((svc) =>
|
|
2267
|
+
Effect.gen(function* () {
|
|
2268
|
+
const s = yield* svc.create({ title: AUTO_DREAM_TITLE })
|
|
2269
|
+
const sp = yield* Service
|
|
2270
|
+
yield* sp.prompt({ sessionID: s.id, agent: "dream", model: mdl, parts: [{ type: "text", text: DREAM_TASK }] })
|
|
2271
|
+
}),
|
|
2272
|
+
),
|
|
2273
|
+
).catch((err) => log.error("auto-dream prompt failed", { error: String(err) }))
|
|
2274
|
+
}
|
|
2275
|
+
if (distillTrigger) {
|
|
2276
|
+
AppRuntime.runPromise(
|
|
2277
|
+
Session.Service.use((svc) =>
|
|
2278
|
+
Effect.gen(function* () {
|
|
2279
|
+
const s = yield* svc.create({ title: AUTO_DISTILL_TITLE })
|
|
2280
|
+
const sp = yield* Service
|
|
2281
|
+
yield* sp.prompt({ sessionID: s.id, agent: "distill", model: mdl, parts: [{ type: "text", text: DISTILL_TASK }] })
|
|
2282
|
+
}),
|
|
2283
|
+
),
|
|
2284
|
+
).catch((err) => log.error("auto-distill prompt failed", { error: String(err) }))
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
|
|
2289
|
+
const model = yield* getModel(lastUser.model.providerID, lastUser.model.modelID, sessionID)
|
|
2290
|
+
lastModelForPrune = model
|
|
2291
|
+
lastFinishedForPrune = lastFinished
|
|
2292
|
+
const task = tasks.pop()
|
|
2293
|
+
|
|
2294
|
+
if (task?.type === "subtask") {
|
|
2295
|
+
yield* handleSubtask({ task, model, lastUser, sessionID, session, msgs })
|
|
2296
|
+
continue
|
|
2297
|
+
}
|
|
2298
|
+
|
|
2299
|
+
// Detect compaction boundary: if the last user message has a compaction
|
|
2300
|
+
// part, route to compact.process() instead of the normal LLM flow.
|
|
2301
|
+
const lastUserMsgForCompaction = msgs.findLast((m) => m.info.role === "user")
|
|
2302
|
+
if (lastUserMsgForCompaction?.parts.some((p) => p.type === "compaction")) {
|
|
2303
|
+
const compactionPart = lastUserMsgForCompaction.parts.find(
|
|
2304
|
+
(p): p is MessageV2.CompactionPart => p.type === "compaction",
|
|
2305
|
+
)
|
|
2306
|
+
const allMsgs = yield* sessions.messages({ sessionID })
|
|
2307
|
+
const result = yield* compaction.process({
|
|
2308
|
+
parentID: lastUser.id,
|
|
2309
|
+
messages: allMsgs,
|
|
2310
|
+
sessionID,
|
|
2311
|
+
auto: compactionPart?.auto ?? false,
|
|
2312
|
+
overflow: compactionPart?.overflow,
|
|
2313
|
+
agentID: lastUser.agentID,
|
|
2314
|
+
})
|
|
2315
|
+
if (result === "stop") break
|
|
2316
|
+
continue
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
// Memory flush nudge at high context pressure
|
|
2320
|
+
if (lastFinished && lastFinished.summary !== true && model) {
|
|
2321
|
+
const cfg = yield* config.get()
|
|
2322
|
+
const pressure = pressureLevel({ cfg, tokens: lastFinished.tokens, model })
|
|
2323
|
+
if (pressure >= 2) {
|
|
2324
|
+
// Inject nudge as a synthetic text part on the last user message
|
|
2325
|
+
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
|
2326
|
+
if (
|
|
2327
|
+
lastUserMsg &&
|
|
2328
|
+
!lastUserMsg.parts.some((p) => p.type === "text" && p.text?.includes("Context is filling up"))
|
|
2329
|
+
) {
|
|
2330
|
+
lastUserMsg.parts.push({
|
|
2331
|
+
id: PartID.ascending(),
|
|
2332
|
+
messageID: lastUserMsg.info.id,
|
|
2333
|
+
sessionID,
|
|
2334
|
+
type: "text",
|
|
2335
|
+
synthetic: true,
|
|
2336
|
+
text: [
|
|
2337
|
+
"<system-reminder>",
|
|
2338
|
+
`Context is filling up (${pressure >= 3 ? ">85%" : ">70%"}).`,
|
|
2339
|
+
"If you have important learnings or decisions from this session,",
|
|
2340
|
+
"consider writing them to memory now before context may be reset.",
|
|
2341
|
+
"</system-reminder>",
|
|
2342
|
+
].join("\n"),
|
|
2343
|
+
})
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
|
|
2348
|
+
// Repeated-step nudge: if the last REPEATED_STEP_THRESHOLD finished
|
|
2349
|
+
// assistant steps made an identical tool call, the model is likely
|
|
2350
|
+
// stuck looping. Inject a reminder on the last user message asking it
|
|
2351
|
+
// to change approach. Mirrors the memory-flush nudge above (synthetic
|
|
2352
|
+
// text part, deduped per build).
|
|
2353
|
+
if (lastFinished) {
|
|
2354
|
+
const recentSignatures: string[] = []
|
|
2355
|
+
for (let i = msgs.length - 1; i >= 0 && recentSignatures.length < REPEATED_STEP_THRESHOLD; i--) {
|
|
2356
|
+
const m = msgs[i]
|
|
2357
|
+
if (m.info.role !== "assistant" || !m.info.finish) continue
|
|
2358
|
+
const sig = stepSignature(m.parts)
|
|
2359
|
+
if (sig === undefined) break
|
|
2360
|
+
recentSignatures.push(sig)
|
|
2361
|
+
}
|
|
2362
|
+
const repeating =
|
|
2363
|
+
recentSignatures.length === REPEATED_STEP_THRESHOLD &&
|
|
2364
|
+
recentSignatures.every((sig) => sig === recentSignatures[0])
|
|
2365
|
+
if (repeating) {
|
|
2366
|
+
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
|
2367
|
+
if (
|
|
2368
|
+
lastUserMsg &&
|
|
2369
|
+
!lastUserMsg.parts.some(
|
|
2370
|
+
(p) => p.type === "text" && p.text?.includes("repeating the same action"),
|
|
2371
|
+
)
|
|
2372
|
+
) {
|
|
2373
|
+
lastUserMsg.parts.push({
|
|
2374
|
+
id: PartID.ascending(),
|
|
2375
|
+
messageID: lastUserMsg.info.id,
|
|
2376
|
+
sessionID,
|
|
2377
|
+
type: "text",
|
|
2378
|
+
synthetic: true,
|
|
2379
|
+
text: [
|
|
2380
|
+
"<system-reminder>",
|
|
2381
|
+
`Your last ${REPEATED_STEP_THRESHOLD} steps have been identical — you appear to be`,
|
|
2382
|
+
"repeating the same action without making progress. Stop and reconsider:",
|
|
2383
|
+
"the current approach is not working. Try a different strategy, use a",
|
|
2384
|
+
"different tool, or if you are blocked, explain the blocker to the user",
|
|
2385
|
+
"instead of repeating the same step again.",
|
|
2386
|
+
"</system-reminder>",
|
|
2387
|
+
].join("\n"),
|
|
2388
|
+
})
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
// Resolve the agent for this iteration once. Both the management
|
|
2394
|
+
// hooks below (fireCheckpoints, overflow handler) and the existing
|
|
2395
|
+
// agent-not-found check later in the iteration reuse this binding.
|
|
2396
|
+
// Bounded computation agents (native + hidden — currently title,
|
|
2397
|
+
// summary, checkpoint-writer) are exempt from context management;
|
|
2398
|
+
// see docs/superpowers/specs/2026-04-28-bounded-computation-agents-design.md
|
|
2399
|
+
const agent = yield* agents.get(lastUser.agent)
|
|
2400
|
+
const isBoundedComputation =
|
|
2401
|
+
agent?.native === true && agent?.hidden === true
|
|
2402
|
+
|
|
2403
|
+
// Fire background checkpoint writers for any newly-crossed thresholds
|
|
2404
|
+
// based on the latest completed assistant message's tokens. Must run
|
|
2405
|
+
// BEFORE the overflow/maxThreshold check below so maxCrossed flag is
|
|
2406
|
+
// set in time to trigger rebuild on this same iteration.
|
|
2407
|
+
if (!skipOverflowCheck && !isBoundedComputation && lastFinished && lastFinished.tokens) {
|
|
2408
|
+
const fireOps = yield* ops()
|
|
2409
|
+
yield* prune
|
|
2410
|
+
.fireCheckpoints({
|
|
2411
|
+
sessionID,
|
|
2412
|
+
model,
|
|
2413
|
+
tokens: lastFinished.tokens,
|
|
2414
|
+
promptOps: fireOps,
|
|
2415
|
+
agentID: lastUser.agentID,
|
|
2416
|
+
})
|
|
2417
|
+
.pipe(Effect.ignore)
|
|
2418
|
+
}
|
|
2419
|
+
|
|
2420
|
+
if (
|
|
2421
|
+
!skipOverflowCheck &&
|
|
2422
|
+
!isBoundedComputation &&
|
|
2423
|
+
lastFinished &&
|
|
2424
|
+
lastFinished.summary !== true &&
|
|
2425
|
+
(overflowCheck({ cfg: yield* config.get(), tokens: lastFinished.tokens, model }) ||
|
|
2426
|
+
(yield* prune.maxThresholdCrossed(sessionID)))
|
|
2427
|
+
) {
|
|
2428
|
+
// Subagent overflow → per-actor compaction (lossy LLM summarization
|
|
2429
|
+
// scoped to the actor's (sessionID, agent_id) slice). Subagents
|
|
2430
|
+
// don't have checkpoints, so checkpoint+discard does not apply.
|
|
2431
|
+
// Gate must exclude agentID="main" — F49+F50 made main carry
|
|
2432
|
+
// agentID="main", so a bare `if (lastUser.agentID)` would route
|
|
2433
|
+
// main to this subagent path and skip the checkpoint rebuild
|
|
2434
|
+
// below. See checkpoint.ts:715 for the matching gate.
|
|
2435
|
+
if (lastUser.agentID && lastUser.agentID !== "main") {
|
|
2436
|
+
yield* compaction
|
|
2437
|
+
.create({
|
|
2438
|
+
sessionID,
|
|
2439
|
+
agent: lastUser.agent,
|
|
2440
|
+
model: { providerID: model.providerID, modelID: model.id },
|
|
2441
|
+
auto: true,
|
|
2442
|
+
agentID: lastUser.agentID,
|
|
2443
|
+
})
|
|
2444
|
+
.pipe(Effect.ignore)
|
|
2445
|
+
// After inserting the boundary, the actor's filterCompactedEffect
|
|
2446
|
+
// slice begins at the boundary marker — context is freed for the
|
|
2447
|
+
// next iteration's stream. Skip the next overflow check so the
|
|
2448
|
+
// model can respond on the trimmed context.
|
|
2449
|
+
skipOverflowCheck = true
|
|
2450
|
+
continue
|
|
2451
|
+
}
|
|
2452
|
+
|
|
2453
|
+
// Main-agent overflow: insert a checkpoint boundary marker (never
|
|
2454
|
+
// deletes DB messages) so the next iteration rebuilds from the
|
|
2455
|
+
// freshest checkpoint. Fall back to compaction only when no boundary
|
|
2456
|
+
// can be produced.
|
|
2457
|
+
const hasCP = yield* checkpoint.hasCheckpoint(sessionID).pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2458
|
+
if (hasCP) {
|
|
2459
|
+
// Wait for any running writer so the freshest checkpoint is available
|
|
2460
|
+
yield* checkpoint.waitForWriter(sessionID).pipe(Effect.ignore)
|
|
2461
|
+
|
|
2462
|
+
const boundary = yield* checkpoint
|
|
2463
|
+
.lastBoundary(sessionID)
|
|
2464
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
2465
|
+
const boundaryMsg = boundary ? msgs.find((m) => m.info.id === boundary) : undefined
|
|
2466
|
+
const inserted = boundary
|
|
2467
|
+
? yield* checkpoint
|
|
2468
|
+
.insertRebuildBoundary({
|
|
2469
|
+
sessionID,
|
|
2470
|
+
boundary,
|
|
2471
|
+
lastMessageInfo: computeLastMessageInfo(msgs.map((m) => m.info)),
|
|
2472
|
+
agentID: lastUser.agentID,
|
|
2473
|
+
agent: lastUser.agent,
|
|
2474
|
+
model: { providerID: model.providerID, modelID: model.id },
|
|
2475
|
+
boundaryCreatedAt: boundaryMsg?.info.time.created,
|
|
2476
|
+
})
|
|
2477
|
+
.pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2478
|
+
: false
|
|
2479
|
+
|
|
2480
|
+
if (inserted) {
|
|
2481
|
+
yield* prune.resetThresholds(sessionID)
|
|
2482
|
+
skipOverflowCheck = true
|
|
2483
|
+
continue
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
// F39: no checkpoint — fall back to compaction (LLM-driven lossy summary).
|
|
2488
|
+
// Better than mechanical trim: preserves semantic content via summary.
|
|
2489
|
+
yield* compaction
|
|
2490
|
+
.create({
|
|
2491
|
+
sessionID,
|
|
2492
|
+
agent: lastUser.agent,
|
|
2493
|
+
model: { providerID: model.providerID, modelID: model.id },
|
|
2494
|
+
auto: true,
|
|
2495
|
+
agentID: lastUser.agentID,
|
|
2496
|
+
})
|
|
2497
|
+
.pipe(Effect.ignore)
|
|
2498
|
+
skipOverflowCheck = true
|
|
2499
|
+
continue
|
|
2500
|
+
}
|
|
2501
|
+
skipOverflowCheck = false
|
|
2502
|
+
|
|
2503
|
+
// `agent` resolved at iteration start; reuse here for the
|
|
2504
|
+
// agent-not-found user-visible error.
|
|
2505
|
+
if (!agent) {
|
|
2506
|
+
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
|
|
2507
|
+
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
|
|
2508
|
+
const error = new NamedError.Unknown({ message: `Agent not found: "${lastUser.agent}".${hint}` })
|
|
2509
|
+
yield* bus.publish(Session.Event.Error, { sessionID, error: error.toObject() })
|
|
2510
|
+
throw error
|
|
2511
|
+
}
|
|
2512
|
+
const maxSteps = agent.steps ?? Infinity
|
|
2513
|
+
const isLastStep = step >= maxSteps
|
|
2514
|
+
msgs = yield* insertReminders({ messages: msgs, agent, session })
|
|
2515
|
+
|
|
2516
|
+
const msg: MessageV2.Assistant = {
|
|
2517
|
+
id: MessageID.ascending(),
|
|
2518
|
+
parentID: lastUser.id,
|
|
2519
|
+
role: "assistant",
|
|
2520
|
+
agentID: lastUser.agentID,
|
|
2521
|
+
mode: agent.name,
|
|
2522
|
+
agent: agent.name,
|
|
2523
|
+
variant: lastUser.model.variant,
|
|
2524
|
+
path: { cwd: ctx.directory, root: ctx.worktree },
|
|
2525
|
+
cost: 0,
|
|
2526
|
+
tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } },
|
|
2527
|
+
modelID: model.id,
|
|
2528
|
+
providerID: model.providerID,
|
|
2529
|
+
time: { created: Date.now() },
|
|
2530
|
+
sessionID,
|
|
2531
|
+
}
|
|
2532
|
+
yield* sessions.updateMessage(msg)
|
|
2533
|
+
const handle = yield* processor.create({
|
|
2534
|
+
assistantMessage: msg,
|
|
2535
|
+
sessionID,
|
|
2536
|
+
model,
|
|
2537
|
+
agentMetrics,
|
|
2538
|
+
})
|
|
2539
|
+
|
|
2540
|
+
const outcome: "break" | "continue" = yield* Effect.gen(function* () {
|
|
2541
|
+
const lastUserMsg = msgs.findLast((m) => m.info.role === "user")
|
|
2542
|
+
const bypassAgentCheck = lastUserMsg?.parts.some((p) => p.type === "agent") ?? false
|
|
2543
|
+
|
|
2544
|
+
const tools = yield* resolveTools({
|
|
2545
|
+
agent,
|
|
2546
|
+
session,
|
|
2547
|
+
model,
|
|
2548
|
+
tools: lastUser.tools,
|
|
2549
|
+
processor: handle,
|
|
2550
|
+
bypassAgentCheck,
|
|
2551
|
+
messages: msgs,
|
|
2552
|
+
agentID: lastUser.agentID,
|
|
2553
|
+
task_id,
|
|
2554
|
+
})
|
|
2555
|
+
|
|
2556
|
+
if (lastUser.format?.type === "json_schema") {
|
|
2557
|
+
tools["StructuredOutput"] = createStructuredOutputTool({
|
|
2558
|
+
schema: lastUser.format.schema,
|
|
2559
|
+
onSuccess(output) {
|
|
2560
|
+
structured = output
|
|
2561
|
+
},
|
|
2562
|
+
})
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
if (step === 1)
|
|
2566
|
+
yield* summary.summarize({ sessionID, messageID: lastUser.id }).pipe(Effect.ignore, Effect.forkIn(scope))
|
|
2567
|
+
|
|
2568
|
+
if (step > 1 && lastFinished) {
|
|
2569
|
+
for (const m of msgs) {
|
|
2570
|
+
if (m.info.role !== "user" || m.info.id <= lastFinished.id) continue
|
|
2571
|
+
for (const p of m.parts) {
|
|
2572
|
+
if (p.type !== "text" || p.ignored || p.synthetic) continue
|
|
2573
|
+
if (!p.text.trim()) continue
|
|
2574
|
+
p.text = [
|
|
2575
|
+
"<system-reminder>",
|
|
2576
|
+
"The user sent the following message:",
|
|
2577
|
+
p.text,
|
|
2578
|
+
"",
|
|
2579
|
+
"Please address this message and continue with your tasks.",
|
|
2580
|
+
"</system-reminder>",
|
|
2581
|
+
].join("\n")
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
}
|
|
2585
|
+
|
|
2586
|
+
yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs })
|
|
2587
|
+
|
|
2588
|
+
const format = lastUser.format ?? { type: "text" as const }
|
|
2589
|
+
|
|
2590
|
+
// Determine if this iteration is for a fork agent (contextMode === "full").
|
|
2591
|
+
// Fork agents use the frozen ForkContext snapshot captured at spawn time
|
|
2592
|
+
// (system + inheritedMessages) rather than recomputing from their own
|
|
2593
|
+
// agent identity — which would diverge from the parent and break the
|
|
2594
|
+
// prefix cache.
|
|
2595
|
+
const actorRecord = lastUser.agentID
|
|
2596
|
+
? yield* actorRegistry.get(sessionID, lastUser.agentID).pipe(
|
|
2597
|
+
Effect.orElseSucceed(() => undefined),
|
|
2598
|
+
)
|
|
2599
|
+
: undefined
|
|
2600
|
+
// v9 registers main as `mode: "main"` with `contextMode: "full"`.
|
|
2601
|
+
// Only spawned actors (subagent/peer) carry a frozen ForkContext;
|
|
2602
|
+
// main is the captor, never the captured.
|
|
2603
|
+
const isForkAgent =
|
|
2604
|
+
actorRecord?.contextMode === "full" &&
|
|
2605
|
+
(actorRecord.mode === "subagent" || actorRecord.mode === "peer")
|
|
2606
|
+
|
|
2607
|
+
// Fork path: read frozen ForkContext from Actor service (late-bound via
|
|
2608
|
+
// spawnRef to break the Actor → SessionPrompt → Actor layer cycle).
|
|
2609
|
+
// If forkCtx is missing (race / cleanup bug / spawn skipped), fail the
|
|
2610
|
+
// actor so the next prune turn can spawn a fresh fork.
|
|
2611
|
+
if (isForkAgent) {
|
|
2612
|
+
const forkCtxEffect = spawnRef.current?.getForkContext(lastUser.agentID!)
|
|
2613
|
+
const forkCtx = forkCtxEffect ? yield* forkCtxEffect : undefined
|
|
2614
|
+
if (!forkCtx) {
|
|
2615
|
+
yield* slog.warn("fork agent runLoop: missing forkContext, failing actor", {
|
|
2616
|
+
sessionID,
|
|
2617
|
+
agentID: lastUser.agentID,
|
|
2618
|
+
})
|
|
2619
|
+
yield* actorRegistry
|
|
2620
|
+
.updateStatus(sessionID, lastUser.agentID!, { status: "idle", lastOutcome: "failure", lastError: "missing fork context" })
|
|
2621
|
+
.pipe(Effect.ignore)
|
|
2622
|
+
return "break" as const
|
|
2623
|
+
}
|
|
2624
|
+
const ownNew = msgs.filter(
|
|
2625
|
+
(m) => m.info.id > forkCtx.watermarkMsgID && m.info.agentID === lastUser.agentID,
|
|
2626
|
+
)
|
|
2627
|
+
const ownNewModelMsgs = yield* MessageV2.toModelMessagesEffect(ownNew, model)
|
|
2628
|
+
const prebuiltSystem = forkCtx.system
|
|
2629
|
+
const modelMsgs: ModelMessage[] = [...forkCtx.inheritedMessages, ...ownNewModelMsgs]
|
|
2630
|
+
// additions is empty for fork agents: system is taken verbatim from
|
|
2631
|
+
// forkCtx.system. Passed as `system` to handle.process for logging/replay.
|
|
2632
|
+
const additions: string[] = []
|
|
2633
|
+
// Note: fork uses `tools` from resolveTools (not `forkCtx.tools`) — runtime
|
|
2634
|
+
// tool dispatch needs execute closures, which `forkCtx.tools` does not carry.
|
|
2635
|
+
// Schema parity with parent is currently a consequence of checkpoint-writer
|
|
2636
|
+
// having no toolAllowlist (Task 2.6 + agent.test.ts guard). See ForkContext.tools
|
|
2637
|
+
// JSDoc in packages/tulingcode/src/actor/spawn.ts for the full contract.
|
|
2638
|
+
const result = yield* handle.process({
|
|
2639
|
+
user: lastUser,
|
|
2640
|
+
agent,
|
|
2641
|
+
// Fork inherits the parent agent's permission (captured at spawn into
|
|
2642
|
+
// ForkContext). This drives llm.ts resolveTools/disabled() to the SAME
|
|
2643
|
+
// visible tool set as the parent → prompt-cache parity on the inherited
|
|
2644
|
+
// prefix. Scope: this affects tool VISIBILITY only; the per-call ask
|
|
2645
|
+
// ruleset (built separately in resolveTools' ask closure) is unchanged.
|
|
2646
|
+
// Parity is exact modulo non-default `session.permission`: the parent's
|
|
2647
|
+
// visibility ruleset is merge(parent.permission, session.permission)
|
|
2648
|
+
// while the fork's is merge(writer.permission, parentPermission) — so a
|
|
2649
|
+
// session-level rule pins the parent but not the fork. Still a strict
|
|
2650
|
+
// improvement over the old bespoke "*":"deny" block (which always
|
|
2651
|
+
// diverged). The `?? session.permission` is defense-in-depth only:
|
|
2652
|
+
// parentPermission is a required field (empty `[]` on a missed capture,
|
|
2653
|
+
// which `??` does NOT override), so the fallback fires solely if a future
|
|
2654
|
+
// refactor makes the field optional.
|
|
2655
|
+
permission: forkCtx.parentPermission ?? session.permission,
|
|
2656
|
+
sessionID,
|
|
2657
|
+
parentSessionID: session.parentID,
|
|
2658
|
+
system: additions,
|
|
2659
|
+
prebuiltSystem,
|
|
2660
|
+
messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])],
|
|
2661
|
+
tools,
|
|
2662
|
+
model,
|
|
2663
|
+
toolChoice: format.type === "json_schema" ? "required" : undefined,
|
|
2664
|
+
agentID: lastUser.agentID,
|
|
2665
|
+
})
|
|
2666
|
+
|
|
2667
|
+
if (
|
|
2668
|
+
result === "continue" &&
|
|
2669
|
+
(yield* autoContinueOutputLength({ lastUser, assistant: handle.message }))
|
|
2670
|
+
) {
|
|
2671
|
+
return "continue" as const
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
if (structured !== undefined) {
|
|
2675
|
+
handle.message.structured = structured
|
|
2676
|
+
handle.message.finish = handle.message.finish ?? "stop"
|
|
2677
|
+
yield* sessions.updateMessage(handle.message)
|
|
2678
|
+
return "break" as const
|
|
2679
|
+
}
|
|
2680
|
+
|
|
2681
|
+
const forkClassification = classifyAssistantStep({
|
|
2682
|
+
phase: "after-process",
|
|
2683
|
+
lastUser,
|
|
2684
|
+
assistant: handle.message,
|
|
2685
|
+
parts: MessageV2.parts(handle.message.id),
|
|
2686
|
+
processResult: result,
|
|
2687
|
+
})
|
|
2688
|
+
if (forkClassification.type === "filtered") {
|
|
2689
|
+
yield* writeContentFilterError({ assistant: handle.message })
|
|
2690
|
+
return "break" as const
|
|
2691
|
+
}
|
|
2692
|
+
if (forkClassification.type === "failed") {
|
|
2693
|
+
yield* writeModelError({ assistant: handle.message, reason: forkClassification.reason })
|
|
2694
|
+
return "break" as const
|
|
2695
|
+
}
|
|
2696
|
+
if (forkClassification.type !== "continue" && !handle.message.error && format.type === "json_schema") {
|
|
2697
|
+
if (yield* autoRetryStructuredOutput({ lastUser, assistant: handle.message }))
|
|
2698
|
+
return "continue" as const
|
|
2699
|
+
return "break" as const
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
if (
|
|
2703
|
+
(forkClassification.type === "think-only" || forkClassification.type === "invalid") &&
|
|
2704
|
+
format.type !== "json_schema"
|
|
2705
|
+
) {
|
|
2706
|
+
const reason =
|
|
2707
|
+
forkClassification.type === "invalid" ? forkClassification.reason : "think-only"
|
|
2708
|
+
if (yield* autoContinueInvalidOutput({ lastUser, assistant: handle.message, reason }))
|
|
2709
|
+
return "continue" as const
|
|
2710
|
+
return "break" as const
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
if (forkClassification.type === "final" && forkClassification.degraded)
|
|
2714
|
+
yield* slog.warn("degraded final on abnormal finish", { finish: handle.message.finish })
|
|
2715
|
+
if (result === "stop") return "break" as const
|
|
2716
|
+
// Fork agents are always subagents (lastUser.agentID is set); use
|
|
2717
|
+
// per-actor compaction on overflow (same as non-fork subagent path).
|
|
2718
|
+
if (!isBoundedComputation && result === "overflow") {
|
|
2719
|
+
yield* compaction
|
|
2720
|
+
.create({
|
|
2721
|
+
sessionID,
|
|
2722
|
+
agent: lastUser.agent,
|
|
2723
|
+
model: { providerID: model.providerID, modelID: model.id },
|
|
2724
|
+
auto: true,
|
|
2725
|
+
overflow: true,
|
|
2726
|
+
agentID: lastUser.agentID,
|
|
2727
|
+
})
|
|
2728
|
+
.pipe(Effect.ignore)
|
|
2729
|
+
}
|
|
2730
|
+
return "continue" as const
|
|
2731
|
+
}
|
|
2732
|
+
|
|
2733
|
+
const [skills, env, instructions] = yield* Effect.all([
|
|
2734
|
+
sys.skills(agent),
|
|
2735
|
+
Effect.sync(() => sys.environment(model)),
|
|
2736
|
+
instruction.system().pipe(Effect.orDie),
|
|
2737
|
+
])
|
|
2738
|
+
// Surface which instruction files (CLAUDE.md, AGENTS.md, ...) were loaded.
|
|
2739
|
+
// Only for primary sessions (subagents would be noisy) and once per session.
|
|
2740
|
+
if (!session.parentID && !instructionsNotified.has(sessionID)) {
|
|
2741
|
+
instructionsNotified.add(sessionID)
|
|
2742
|
+
const worktree = (yield* InstanceState.context).worktree
|
|
2743
|
+
const files = Array.from(instructions.paths, (p) => Instruction.display(p, worktree))
|
|
2744
|
+
if (files.length > 0) {
|
|
2745
|
+
yield* bus.publish(TuiEvent.InstructionsLoaded, { files }).pipe(Effect.ignore)
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
const additions = [
|
|
2749
|
+
...env,
|
|
2750
|
+
...(skills ? [skills] : []),
|
|
2751
|
+
...instructions.content,
|
|
2752
|
+
...(format.type === "json_schema" ? [STRUCTURED_OUTPUT_SYSTEM_PROMPT] : []),
|
|
2753
|
+
]
|
|
2754
|
+
// Note: `buildLLMRequestPrefix` also returns a `tools` field, but we
|
|
2755
|
+
// intentionally don't use it here — the `tools` variable from `resolveTools`
|
|
2756
|
+
// (set earlier via `handle.process({tools: ...})`) carries `execute` closures
|
|
2757
|
+
// the AI SDK needs for runtime tool dispatch, while `buildLLMRequestPrefix`
|
|
2758
|
+
// produces schema-only tools. Schema bytes match between both paths (both call
|
|
2759
|
+
// registry.tools with identical args), so prefix cache parity holds.
|
|
2760
|
+
// Main runLoop: no watermark — LLM must see the full msgs list,
|
|
2761
|
+
// including this turn's intermediate assistant turns (tool reads,
|
|
2762
|
+
// task creates, etc.) so each step doesn't replay from the bare
|
|
2763
|
+
// user prompt. The watermark is for fork capture only (frozen
|
|
2764
|
+
// snapshot of parent-view at spawn time).
|
|
2765
|
+
const { system: prebuiltSystem, inheritedMessages: modelMsgs } =
|
|
2766
|
+
yield* buildLLMRequestPrefix({
|
|
2767
|
+
sessionID,
|
|
2768
|
+
agent,
|
|
2769
|
+
model,
|
|
2770
|
+
msgs,
|
|
2771
|
+
additions,
|
|
2772
|
+
}).pipe(
|
|
2773
|
+
Effect.provideService(LLM.Service, llm),
|
|
2774
|
+
Effect.provideService(ToolRegistry.Service, registry),
|
|
2775
|
+
)
|
|
2776
|
+
const maxModeCfg = (yield* config.get()).experimental?.maxMode
|
|
2777
|
+
const useMaxMode =
|
|
2778
|
+
agent.name === MaxMode.MAX_MODE_AGENT && maxModeCfg !== undefined && format.type !== "json_schema"
|
|
2779
|
+
|
|
2780
|
+
const processArgs = {
|
|
2781
|
+
user: lastUser,
|
|
2782
|
+
agent,
|
|
2783
|
+
permission: session.permission,
|
|
2784
|
+
sessionID,
|
|
2785
|
+
parentSessionID: session.parentID,
|
|
2786
|
+
// system: additions is preserved for non-LLM consumers of StreamInput (e.g.,
|
|
2787
|
+
// MessageV2.User.system for logging/replay); llm.stream itself uses prebuiltSystem.
|
|
2788
|
+
system: additions,
|
|
2789
|
+
prebuiltSystem,
|
|
2790
|
+
messages: [...modelMsgs, ...(isLastStep ? [{ role: "assistant" as const, content: MAX_STEPS }] : [])],
|
|
2791
|
+
tools,
|
|
2792
|
+
model,
|
|
2793
|
+
toolChoice: format.type === "json_schema" ? ("required" as const) : undefined,
|
|
2794
|
+
agentID: lastUser.agentID,
|
|
2795
|
+
}
|
|
2796
|
+
|
|
2797
|
+
const result = useMaxMode
|
|
2798
|
+
? yield* MaxMode.runMaxStep({
|
|
2799
|
+
// runMaxStep reuses the identical per-step args as handle.process,
|
|
2800
|
+
// plus the orchestration handles it needs.
|
|
2801
|
+
...processArgs,
|
|
2802
|
+
handle,
|
|
2803
|
+
llm,
|
|
2804
|
+
candidates: maxModeCfg?.candidates,
|
|
2805
|
+
setStatus: (message) =>
|
|
2806
|
+
status.set(sessionID, message ? { type: "busy", message } : { type: "busy" }),
|
|
2807
|
+
})
|
|
2808
|
+
: yield* handle.process(processArgs)
|
|
2809
|
+
|
|
2810
|
+
if (
|
|
2811
|
+
result === "continue" &&
|
|
2812
|
+
(yield* autoContinueOutputLength({ lastUser, assistant: handle.message }))
|
|
2813
|
+
) {
|
|
2814
|
+
return "continue" as const
|
|
2815
|
+
}
|
|
2816
|
+
|
|
2817
|
+
if (structured !== undefined) {
|
|
2818
|
+
handle.message.structured = structured
|
|
2819
|
+
handle.message.finish = handle.message.finish ?? "stop"
|
|
2820
|
+
yield* sessions.updateMessage(handle.message)
|
|
2821
|
+
return "break" as const
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
const classification = classifyAssistantStep({
|
|
2825
|
+
phase: "after-process",
|
|
2826
|
+
lastUser,
|
|
2827
|
+
assistant: handle.message,
|
|
2828
|
+
parts: MessageV2.parts(handle.message.id),
|
|
2829
|
+
processResult: result,
|
|
2830
|
+
})
|
|
2831
|
+
if (classification.type === "filtered") {
|
|
2832
|
+
yield* writeContentFilterError({ assistant: handle.message })
|
|
2833
|
+
return "break" as const
|
|
2834
|
+
}
|
|
2835
|
+
if (classification.type === "failed") {
|
|
2836
|
+
yield* writeModelError({ assistant: handle.message, reason: classification.reason })
|
|
2837
|
+
return "break" as const
|
|
2838
|
+
}
|
|
2839
|
+
if (classification.type !== "continue" && !handle.message.error && format.type === "json_schema") {
|
|
2840
|
+
if (yield* autoRetryStructuredOutput({ lastUser, assistant: handle.message })) return "continue" as const
|
|
2841
|
+
return "break" as const
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
if (
|
|
2845
|
+
(classification.type === "think-only" || classification.type === "invalid") &&
|
|
2846
|
+
format.type !== "json_schema"
|
|
2847
|
+
) {
|
|
2848
|
+
const reason = classification.type === "invalid" ? classification.reason : "think-only"
|
|
2849
|
+
if (yield* autoContinueInvalidOutput({ lastUser, assistant: handle.message, reason }))
|
|
2850
|
+
return "continue" as const
|
|
2851
|
+
return "break" as const
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
if (classification.type === "final" && classification.degraded)
|
|
2855
|
+
yield* slog.warn("degraded final on abnormal finish", { finish: handle.message.finish })
|
|
2856
|
+
if (result === "stop") return "break" as const
|
|
2857
|
+
if (!isBoundedComputation && result === "overflow") {
|
|
2858
|
+
// Subagent overflow → per-actor compaction. Insert a boundary
|
|
2859
|
+
// tagged with the subagent's agent_id; the next runLoop iteration
|
|
2860
|
+
// will see a trimmed context (filterCompactedEffect stops at
|
|
2861
|
+
// the boundary).
|
|
2862
|
+
// Gate must exclude "main" — see comment at the matching gate
|
|
2863
|
+
// earlier in this file (~line 1716) and at checkpoint.ts:715.
|
|
2864
|
+
if (lastUser.agentID && lastUser.agentID !== "main") {
|
|
2865
|
+
yield* compaction
|
|
2866
|
+
.create({
|
|
2867
|
+
sessionID,
|
|
2868
|
+
agent: lastUser.agent,
|
|
2869
|
+
model: { providerID: model.providerID, modelID: model.id },
|
|
2870
|
+
auto: true,
|
|
2871
|
+
overflow: true,
|
|
2872
|
+
agentID: lastUser.agentID,
|
|
2873
|
+
})
|
|
2874
|
+
.pipe(Effect.ignore)
|
|
2875
|
+
return "continue" as const
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
// Main-agent provider-signalled overflow: insert a checkpoint
|
|
2879
|
+
// boundary marker (never deletes). Prefer rebuild over compaction:
|
|
2880
|
+
// if a writer is running or finished, wait (bounded) and rebuild
|
|
2881
|
+
// from it. Fall back to compaction only when no boundary exists.
|
|
2882
|
+
const writerRunning = yield* checkpoint.isWriterRunning(sessionID)
|
|
2883
|
+
.pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2884
|
+
const hasCP = yield* checkpoint.hasCheckpoint(sessionID)
|
|
2885
|
+
.pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2886
|
+
|
|
2887
|
+
if (writerRunning || hasCP) {
|
|
2888
|
+
yield* checkpoint.waitForWriter(sessionID).pipe(Effect.ignore)
|
|
2889
|
+
const boundary2 = yield* checkpoint.lastBoundary(sessionID)
|
|
2890
|
+
.pipe(Effect.catch(() => Effect.succeed(undefined)))
|
|
2891
|
+
const boundary2Msg = boundary2 ? msgs.find((m) => m.info.id === boundary2) : undefined
|
|
2892
|
+
const inserted2 = boundary2
|
|
2893
|
+
? yield* checkpoint
|
|
2894
|
+
.insertRebuildBoundary({
|
|
2895
|
+
sessionID,
|
|
2896
|
+
boundary: boundary2,
|
|
2897
|
+
lastMessageInfo: computeLastMessageInfo(msgs.map((m) => m.info)),
|
|
2898
|
+
agentID: lastUser.agentID,
|
|
2899
|
+
agent: lastUser.agent,
|
|
2900
|
+
model: { providerID: model.providerID, modelID: model.id },
|
|
2901
|
+
boundaryCreatedAt: boundary2Msg?.info.time.created,
|
|
2902
|
+
})
|
|
2903
|
+
.pipe(Effect.catch(() => Effect.succeed(false)))
|
|
2904
|
+
: false
|
|
2905
|
+
|
|
2906
|
+
if (inserted2) {
|
|
2907
|
+
yield* prune.resetThresholds(sessionID)
|
|
2908
|
+
return "continue" as const
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
|
|
2912
|
+
// F39: no checkpoint — fall back to compaction (LLM-driven lossy summary).
|
|
2913
|
+
yield* compaction
|
|
2914
|
+
.create({
|
|
2915
|
+
sessionID,
|
|
2916
|
+
agent: lastUser.agent,
|
|
2917
|
+
model: { providerID: model.providerID, modelID: model.id },
|
|
2918
|
+
auto: true,
|
|
2919
|
+
overflow: true,
|
|
2920
|
+
agentID: lastUser.agentID,
|
|
2921
|
+
})
|
|
2922
|
+
.pipe(Effect.ignore)
|
|
2923
|
+
}
|
|
2924
|
+
return "continue" as const
|
|
2925
|
+
}).pipe(Effect.ensuring(instruction.clear(handle.message.id)))
|
|
2926
|
+
if (outcome === "break") {
|
|
2927
|
+
if (yield* taskGate(lastUser)) continue
|
|
2928
|
+
if (yield* goalGate(lastUser)) continue
|
|
2929
|
+
break
|
|
2930
|
+
}
|
|
2931
|
+
continue
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
const promptOps = yield* ops()
|
|
2935
|
+
if (lastModelForPrune && lastFinishedForPrune) {
|
|
2936
|
+
yield* prune
|
|
2937
|
+
.prune({
|
|
2938
|
+
sessionID,
|
|
2939
|
+
model: lastModelForPrune,
|
|
2940
|
+
tokens: lastFinishedForPrune.tokens,
|
|
2941
|
+
lastAssistantTime: lastFinishedForPrune.time.completed,
|
|
2942
|
+
promptOps,
|
|
2943
|
+
})
|
|
2944
|
+
.pipe(Effect.ignore, Effect.forkIn(scope))
|
|
2945
|
+
}
|
|
2946
|
+
const final = yield* lastAssistant(sessionID, agentID)
|
|
2947
|
+
const finalIsError = final.info.role === "assistant" && !!final.info.error
|
|
2948
|
+
const lastUserForMetrics = yield* sessions.findMessage(
|
|
2949
|
+
sessionID,
|
|
2950
|
+
(m) => m.info.role === "user",
|
|
2951
|
+
{ agentID: "*" },
|
|
2952
|
+
)
|
|
2953
|
+
yield* publishAgentRequest(
|
|
2954
|
+
finalIsError ? "error" : "completed",
|
|
2955
|
+
Option.isSome(lastUserForMetrics) ? lastUserForMetrics.value.info.agent : final.info.agent,
|
|
2956
|
+
)
|
|
2957
|
+
return final
|
|
2958
|
+
},
|
|
2959
|
+
)
|
|
2960
|
+
|
|
2961
|
+
const loop: (input: z.infer<typeof LoopInput>) => Effect.Effect<MessageV2.WithParts> = Effect.fn(
|
|
2962
|
+
"SessionPrompt.loop",
|
|
2963
|
+
)(function* (input: z.infer<typeof LoopInput>) {
|
|
2964
|
+
const agentID = input.agentID ?? "main"
|
|
2965
|
+
return yield* state.ensureRunning(
|
|
2966
|
+
input.sessionID,
|
|
2967
|
+
agentID,
|
|
2968
|
+
lastAssistant(input.sessionID, agentID),
|
|
2969
|
+
runLoop(input.sessionID, agentID, input.task_id),
|
|
2970
|
+
)
|
|
2971
|
+
})
|
|
2972
|
+
|
|
2973
|
+
const shell: (input: ShellInput) => Effect.Effect<MessageV2.WithParts> = Effect.fn("SessionPrompt.shell")(
|
|
2974
|
+
function* (input: ShellInput) {
|
|
2975
|
+
return yield* state.startShell(input.sessionID, lastAssistant(input.sessionID), shellImpl(input))
|
|
2976
|
+
},
|
|
2977
|
+
)
|
|
2978
|
+
|
|
2979
|
+
const command = Effect.fn("SessionPrompt.command")(function* (input: CommandInput) {
|
|
2980
|
+
yield* elog.info("command", { sessionID: input.sessionID, command: input.command, agent: input.agent })
|
|
2981
|
+
const cmd = yield* commands.get(input.command)
|
|
2982
|
+
if (!cmd) {
|
|
2983
|
+
const available = (yield* commands.list()).map((c) => c.name)
|
|
2984
|
+
const hint = available.length ? ` Available commands: ${available.join(", ")}` : ""
|
|
2985
|
+
const error = new NamedError.Unknown({ message: `Command not found: "${input.command}".${hint}` })
|
|
2986
|
+
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
|
|
2987
|
+
throw error
|
|
2988
|
+
}
|
|
2989
|
+
const agentName = cmd.agent ?? input.agent ?? (yield* agents.defaultAgent())
|
|
2990
|
+
|
|
2991
|
+
// /goal — set or clear a session-level stop-condition goal. The condition
|
|
2992
|
+
// text itself becomes the prompt for this turn (the working agent starts
|
|
2993
|
+
// pursuing it immediately); the main runLoop then refuses to stop until
|
|
2994
|
+
// the judge says it's satisfied. See session/goal.ts.
|
|
2995
|
+
if (input.command === Command.Default.GOAL) {
|
|
2996
|
+
const condition = input.arguments.trim()
|
|
2997
|
+
if (condition === "" || condition === "clear" || condition === "reset") {
|
|
2998
|
+
yield* goal.clear(input.sessionID)
|
|
2999
|
+
return yield* prompt({
|
|
3000
|
+
sessionID: input.sessionID,
|
|
3001
|
+
messageID: input.messageID,
|
|
3002
|
+
agent: agentName,
|
|
3003
|
+
parts: [{ type: "text", text: "Goal cleared.", synthetic: true }],
|
|
3004
|
+
noReply: true,
|
|
3005
|
+
})
|
|
3006
|
+
}
|
|
3007
|
+
yield* goal.set(input.sessionID, condition)
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
const raw = input.arguments.match(argsRegex) ?? []
|
|
3011
|
+
const args = raw.map((arg) => arg.replace(quoteTrimRegex, ""))
|
|
3012
|
+
const templateCommand = yield* Effect.promise(async () => cmd.template)
|
|
3013
|
+
|
|
3014
|
+
let template: string
|
|
3015
|
+
if (cmd.source === "skill") {
|
|
3016
|
+
template = input.arguments
|
|
3017
|
+
} else {
|
|
3018
|
+
const placeholders = templateCommand.match(placeholderRegex) ?? []
|
|
3019
|
+
let last = 0
|
|
3020
|
+
for (const item of placeholders) {
|
|
3021
|
+
const value = Number(item.slice(1))
|
|
3022
|
+
if (value > last) last = value
|
|
3023
|
+
}
|
|
3024
|
+
|
|
3025
|
+
const withArgs = templateCommand.replaceAll(placeholderRegex, (_, index) => {
|
|
3026
|
+
const position = Number(index)
|
|
3027
|
+
const argIndex = position - 1
|
|
3028
|
+
if (argIndex >= args.length) return ""
|
|
3029
|
+
if (position === last) return args.slice(argIndex).join(" ")
|
|
3030
|
+
return args[argIndex]
|
|
3031
|
+
})
|
|
3032
|
+
const usesArgumentsPlaceholder = templateCommand.includes("$ARGUMENTS")
|
|
3033
|
+
template = withArgs.replaceAll("$ARGUMENTS", input.arguments)
|
|
3034
|
+
|
|
3035
|
+
if (placeholders.length === 0 && !usesArgumentsPlaceholder && input.arguments.trim()) {
|
|
3036
|
+
template = template + "\n\n" + input.arguments
|
|
3037
|
+
}
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
const shellMatches = ConfigMarkdown.shell(template)
|
|
3041
|
+
if (shellMatches.length > 0) {
|
|
3042
|
+
const sh = Shell.preferred()
|
|
3043
|
+
const results = yield* Effect.promise(() =>
|
|
3044
|
+
Promise.all(
|
|
3045
|
+
shellMatches.map(async ([, cmd]) => (await Process.text([cmd], { shell: sh, nothrow: true })).text),
|
|
3046
|
+
),
|
|
3047
|
+
)
|
|
3048
|
+
let index = 0
|
|
3049
|
+
template = template.replace(bashRegex, () => results[index++])
|
|
3050
|
+
}
|
|
3051
|
+
template = template.trim()
|
|
3052
|
+
|
|
3053
|
+
const taskModel = yield* Effect.gen(function* () {
|
|
3054
|
+
if (cmd.model) return Provider.parseModel(cmd.model)
|
|
3055
|
+
if (cmd.agent) {
|
|
3056
|
+
const cmdAgent = yield* agents.get(cmd.agent)
|
|
3057
|
+
if (cmdAgent?.model) return cmdAgent.model
|
|
3058
|
+
}
|
|
3059
|
+
if (input.model) return Provider.parseModel(input.model)
|
|
3060
|
+
return yield* lastModel(input.sessionID)
|
|
3061
|
+
})
|
|
3062
|
+
|
|
3063
|
+
yield* getModel(taskModel.providerID, taskModel.modelID, input.sessionID)
|
|
3064
|
+
|
|
3065
|
+
const agent = yield* agents.get(agentName)
|
|
3066
|
+
if (!agent) {
|
|
3067
|
+
const available = (yield* agents.list()).filter((a) => !a.hidden).map((a) => a.name)
|
|
3068
|
+
const hint = available.length ? ` Available agents: ${available.join(", ")}` : ""
|
|
3069
|
+
const error = new NamedError.Unknown({ message: `Agent not found: "${agentName}".${hint}` })
|
|
3070
|
+
yield* bus.publish(Session.Event.Error, { sessionID: input.sessionID, error: error.toObject() })
|
|
3071
|
+
throw error
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
const templateParts = yield* resolvePromptParts(template)
|
|
3075
|
+
const isSubtask = (agent.mode === "subagent" && cmd.subtask !== false) || cmd.subtask === true
|
|
3076
|
+
|
|
3077
|
+
let parts: PromptInput["parts"]
|
|
3078
|
+
if (isSubtask) {
|
|
3079
|
+
const promptText = cmd.source === "skill"
|
|
3080
|
+
? templateCommand + (input.arguments.trim() ? "\n\n" + input.arguments : "")
|
|
3081
|
+
: (templateParts.find((y): y is typeof y & { type: "text"; text: string } => y.type === "text"))?.text ?? ""
|
|
3082
|
+
parts = [
|
|
3083
|
+
{
|
|
3084
|
+
type: "subtask" as const,
|
|
3085
|
+
agent: agent.name,
|
|
3086
|
+
description: cmd.description ?? "",
|
|
3087
|
+
command: input.command,
|
|
3088
|
+
model: { providerID: taskModel.providerID, modelID: taskModel.modelID },
|
|
3089
|
+
prompt: promptText,
|
|
3090
|
+
},
|
|
3091
|
+
]
|
|
3092
|
+
} else if (cmd.source === "skill") {
|
|
3093
|
+
const visibleText = input.arguments.trim()
|
|
3094
|
+
? `/${input.command} ${input.arguments}`
|
|
3095
|
+
: `/${input.command}`
|
|
3096
|
+
const skillPart = {
|
|
3097
|
+
type: "text" as const,
|
|
3098
|
+
text: `<skill_content name="${input.command}">\n${templateCommand}\n</skill_content>`,
|
|
3099
|
+
synthetic: true,
|
|
3100
|
+
}
|
|
3101
|
+
const attachments = templateParts.filter((p): p is Exclude<typeof p, { type: "text" }> => p.type !== "text")
|
|
3102
|
+
parts = [{ type: "text" as const, text: visibleText }, skillPart, ...attachments, ...(input.parts ?? [])]
|
|
3103
|
+
} else {
|
|
3104
|
+
parts = [...templateParts, ...(input.parts ?? [])]
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
const userAgent = isSubtask ? (input.agent ?? (yield* agents.defaultAgent())) : agentName
|
|
3108
|
+
const userModel = isSubtask
|
|
3109
|
+
? input.model
|
|
3110
|
+
? Provider.parseModel(input.model)
|
|
3111
|
+
: yield* lastModel(input.sessionID)
|
|
3112
|
+
: taskModel
|
|
3113
|
+
|
|
3114
|
+
yield* plugin.trigger(
|
|
3115
|
+
"command.execute.before",
|
|
3116
|
+
{ command: input.command, sessionID: input.sessionID, arguments: input.arguments },
|
|
3117
|
+
{ parts },
|
|
3118
|
+
)
|
|
3119
|
+
|
|
3120
|
+
const result = yield* prompt({
|
|
3121
|
+
sessionID: input.sessionID,
|
|
3122
|
+
messageID: input.messageID,
|
|
3123
|
+
model: userModel,
|
|
3124
|
+
agent: userAgent,
|
|
3125
|
+
parts,
|
|
3126
|
+
variant: input.variant,
|
|
3127
|
+
})
|
|
3128
|
+
yield* bus.publish(Command.Event.Executed, {
|
|
3129
|
+
name: input.command,
|
|
3130
|
+
sessionID: input.sessionID,
|
|
3131
|
+
arguments: input.arguments,
|
|
3132
|
+
messageID: result.info.id,
|
|
3133
|
+
})
|
|
3134
|
+
return result
|
|
3135
|
+
})
|
|
3136
|
+
|
|
3137
|
+
const impl = Service.of({
|
|
3138
|
+
cancel,
|
|
3139
|
+
prompt,
|
|
3140
|
+
loop,
|
|
3141
|
+
shell,
|
|
3142
|
+
command,
|
|
3143
|
+
resolvePromptParts,
|
|
3144
|
+
sweepOrphanAssistants,
|
|
3145
|
+
predict,
|
|
3146
|
+
})
|
|
3147
|
+
sessionPromptRef.current = { loop: impl.loop }
|
|
3148
|
+
yield* Effect.addFinalizer(() =>
|
|
3149
|
+
Effect.sync(() => {
|
|
3150
|
+
if (sessionPromptRef.current?.loop === impl.loop) sessionPromptRef.current = undefined
|
|
3151
|
+
}),
|
|
3152
|
+
)
|
|
3153
|
+
return impl
|
|
3154
|
+
}),
|
|
3155
|
+
)
|
|
3156
|
+
|
|
3157
|
+
export const defaultLayer = Layer.suspend(() =>
|
|
3158
|
+
layer.pipe(
|
|
3159
|
+
Layer.provide(SessionRunState.defaultLayer),
|
|
3160
|
+
Layer.provide(SessionStatus.defaultLayer),
|
|
3161
|
+
Layer.provide(SessionPrune.defaultLayer),
|
|
3162
|
+
Layer.provide(SessionCheckpoint.defaultLayer),
|
|
3163
|
+
Layer.provide(SessionCompaction.defaultLayer),
|
|
3164
|
+
Layer.provide(SessionProcessor.defaultLayer),
|
|
3165
|
+
Layer.provide(Command.defaultLayer),
|
|
3166
|
+
Layer.provide(Permission.defaultLayer),
|
|
3167
|
+
Layer.provide(MCP.defaultLayer),
|
|
3168
|
+
Layer.provide(LSP.defaultLayer),
|
|
3169
|
+
Layer.provide(ToolRegistry.defaultLayer),
|
|
3170
|
+
Layer.provide(Truncate.defaultLayer),
|
|
3171
|
+
Layer.provide(Provider.defaultLayer),
|
|
3172
|
+
Layer.provide(Instruction.defaultLayer),
|
|
3173
|
+
Layer.provide(AppFileSystem.defaultLayer),
|
|
3174
|
+
Layer.provide(Plugin.defaultLayer),
|
|
3175
|
+
Layer.provide(Session.defaultLayer),
|
|
3176
|
+
Layer.provide(SessionRevert.defaultLayer),
|
|
3177
|
+
Layer.provide(
|
|
3178
|
+
Layer.mergeAll(
|
|
3179
|
+
Config.defaultLayer,
|
|
3180
|
+
SessionSummary.defaultLayer,
|
|
3181
|
+
Team.defaultLayer,
|
|
3182
|
+
ActorRegistry.defaultLayer,
|
|
3183
|
+
Agent.defaultLayer,
|
|
3184
|
+
SystemPrompt.defaultLayer,
|
|
3185
|
+
LLM.defaultLayer,
|
|
3186
|
+
Bus.layer,
|
|
3187
|
+
CrossSpawnSpawner.defaultLayer,
|
|
3188
|
+
Inbox.defaultLayer,
|
|
3189
|
+
Goal.defaultLayer,
|
|
3190
|
+
TaskGateState.defaultLayer,
|
|
3191
|
+
TaskRegistry.defaultLayer,
|
|
3192
|
+
),
|
|
3193
|
+
),
|
|
3194
|
+
),
|
|
3195
|
+
)
|
|
3196
|
+
export const PromptInput = z.object({
|
|
3197
|
+
sessionID: SessionID.zod,
|
|
3198
|
+
messageID: MessageID.zod.optional(),
|
|
3199
|
+
model: z
|
|
3200
|
+
.object({
|
|
3201
|
+
providerID: ProviderID.zod,
|
|
3202
|
+
modelID: ModelID.zod,
|
|
3203
|
+
})
|
|
3204
|
+
.optional(),
|
|
3205
|
+
modelRef: z
|
|
3206
|
+
.string()
|
|
3207
|
+
.optional()
|
|
3208
|
+
.describe(
|
|
3209
|
+
"Model group/tier name (e.g. ultra/standard/lite) or a literal provider/model. Resolved provider-aware. Takes precedence over `model` when both are set.",
|
|
3210
|
+
),
|
|
3211
|
+
agent: z.string().optional(),
|
|
3212
|
+
agentID: z.string().optional(),
|
|
3213
|
+
task_id: z.string().optional()
|
|
3214
|
+
.describe("If the spawning caller bound this prompt to a specific user-task (T4 etc), pass its TID. Propagates to Tool.Context.taskId so memory-path-guard allows writes to tasks/<task_id>/*.md."),
|
|
3215
|
+
source: z.enum(["user", "spawn", "hook"]).optional(),
|
|
3216
|
+
provenance: MessageV2.Provenance.optional(),
|
|
3217
|
+
noReply: z.boolean().optional(),
|
|
3218
|
+
tools: z
|
|
3219
|
+
.record(z.string(), z.boolean())
|
|
3220
|
+
.optional()
|
|
3221
|
+
.describe("@deprecated tools and permissions have been merged, you can set permissions on the session itself now"),
|
|
3222
|
+
format: MessageV2.Format.optional(),
|
|
3223
|
+
system: z.string().optional(),
|
|
3224
|
+
variant: z.string().optional(),
|
|
3225
|
+
parts: z.array(
|
|
3226
|
+
z.discriminatedUnion("type", [
|
|
3227
|
+
MessageV2.TextPart.omit({
|
|
3228
|
+
messageID: true,
|
|
3229
|
+
sessionID: true,
|
|
3230
|
+
})
|
|
3231
|
+
.partial({
|
|
3232
|
+
id: true,
|
|
3233
|
+
})
|
|
3234
|
+
.meta({
|
|
3235
|
+
ref: "TextPartInput",
|
|
3236
|
+
}),
|
|
3237
|
+
MessageV2.FilePart.omit({
|
|
3238
|
+
messageID: true,
|
|
3239
|
+
sessionID: true,
|
|
3240
|
+
})
|
|
3241
|
+
.partial({
|
|
3242
|
+
id: true,
|
|
3243
|
+
})
|
|
3244
|
+
.meta({
|
|
3245
|
+
ref: "FilePartInput",
|
|
3246
|
+
}),
|
|
3247
|
+
MessageV2.AgentPart.omit({
|
|
3248
|
+
messageID: true,
|
|
3249
|
+
sessionID: true,
|
|
3250
|
+
})
|
|
3251
|
+
.partial({
|
|
3252
|
+
id: true,
|
|
3253
|
+
})
|
|
3254
|
+
.meta({
|
|
3255
|
+
ref: "AgentPartInput",
|
|
3256
|
+
}),
|
|
3257
|
+
MessageV2.SubtaskPart.omit({
|
|
3258
|
+
messageID: true,
|
|
3259
|
+
sessionID: true,
|
|
3260
|
+
})
|
|
3261
|
+
.partial({
|
|
3262
|
+
id: true,
|
|
3263
|
+
})
|
|
3264
|
+
.meta({
|
|
3265
|
+
ref: "SubtaskPartInput",
|
|
3266
|
+
}),
|
|
3267
|
+
]),
|
|
3268
|
+
),
|
|
3269
|
+
})
|
|
3270
|
+
export type PromptInput = z.infer<typeof PromptInput>
|
|
3271
|
+
|
|
3272
|
+
export const LoopInput = z.object({
|
|
3273
|
+
sessionID: SessionID.zod,
|
|
3274
|
+
agentID: z.string().optional(),
|
|
3275
|
+
task_id: z.string().optional(),
|
|
3276
|
+
})
|
|
3277
|
+
|
|
3278
|
+
export const ShellInput = z.object({
|
|
3279
|
+
sessionID: SessionID.zod,
|
|
3280
|
+
messageID: MessageID.zod.optional(),
|
|
3281
|
+
agent: z.string(),
|
|
3282
|
+
model: z
|
|
3283
|
+
.object({
|
|
3284
|
+
providerID: ProviderID.zod,
|
|
3285
|
+
modelID: ModelID.zod,
|
|
3286
|
+
})
|
|
3287
|
+
.optional(),
|
|
3288
|
+
modelRef: z
|
|
3289
|
+
.string()
|
|
3290
|
+
.optional()
|
|
3291
|
+
.describe(
|
|
3292
|
+
"Model group/tier name (e.g. ultra/standard/lite) or a literal provider/model. Resolved provider-aware. Takes precedence over `model` when both are set.",
|
|
3293
|
+
),
|
|
3294
|
+
command: z.string(),
|
|
3295
|
+
})
|
|
3296
|
+
export type ShellInput = z.infer<typeof ShellInput>
|
|
3297
|
+
|
|
3298
|
+
export const CommandInput = z.object({
|
|
3299
|
+
messageID: MessageID.zod.optional(),
|
|
3300
|
+
sessionID: SessionID.zod,
|
|
3301
|
+
agent: z.string().optional(),
|
|
3302
|
+
model: z.string().optional(),
|
|
3303
|
+
arguments: z.string(),
|
|
3304
|
+
command: z.string(),
|
|
3305
|
+
variant: z.string().optional(),
|
|
3306
|
+
parts: z
|
|
3307
|
+
.array(
|
|
3308
|
+
z.discriminatedUnion("type", [
|
|
3309
|
+
MessageV2.FilePart.omit({
|
|
3310
|
+
messageID: true,
|
|
3311
|
+
sessionID: true,
|
|
3312
|
+
}).partial({
|
|
3313
|
+
id: true,
|
|
3314
|
+
}),
|
|
3315
|
+
]),
|
|
3316
|
+
)
|
|
3317
|
+
.optional(),
|
|
3318
|
+
})
|
|
3319
|
+
export type CommandInput = z.infer<typeof CommandInput>
|
|
3320
|
+
|
|
3321
|
+
/** @internal Exported for testing */
|
|
3322
|
+
export function createStructuredOutputTool(input: {
|
|
3323
|
+
schema: Record<string, any>
|
|
3324
|
+
onSuccess: (output: unknown) => void
|
|
3325
|
+
}): AITool {
|
|
3326
|
+
// Remove $schema property if present (not needed for tool input)
|
|
3327
|
+
const { $schema: _, ...toolSchema } = input.schema
|
|
3328
|
+
|
|
3329
|
+
return tool({
|
|
3330
|
+
description: STRUCTURED_OUTPUT_DESCRIPTION,
|
|
3331
|
+
inputSchema: jsonSchema(toolSchema as JSONSchema7),
|
|
3332
|
+
async execute(args) {
|
|
3333
|
+
// AI SDK validates args against inputSchema before calling execute()
|
|
3334
|
+
input.onSuccess(args)
|
|
3335
|
+
return {
|
|
3336
|
+
output: "Structured output captured successfully.",
|
|
3337
|
+
title: "Structured Output",
|
|
3338
|
+
metadata: { valid: true },
|
|
3339
|
+
}
|
|
3340
|
+
},
|
|
3341
|
+
toModelOutput({ output }) {
|
|
3342
|
+
return {
|
|
3343
|
+
type: "text",
|
|
3344
|
+
value: output.output,
|
|
3345
|
+
}
|
|
3346
|
+
},
|
|
3347
|
+
})
|
|
3348
|
+
}
|
|
3349
|
+
const bashRegex = /!`([^`]+)`/g
|
|
3350
|
+
// Match [Image N] as single token, quoted strings, or non-space sequences
|
|
3351
|
+
const argsRegex = /(?:\[Image\s+\d+\]|"[^"]*"|'[^']*'|[^\s"']+)/gi
|
|
3352
|
+
const placeholderRegex = /\$(\d+)/g
|
|
3353
|
+
const quoteTrimRegex = /^["']|["']$/g
|
|
3354
|
+
|
|
3355
|
+
export * as SessionPrompt from "./prompt"
|