@epoch-ai/cli 2.2.4
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/.artifacts/unit/junit.xml +2823 -0
- package/.project-map/backups/20260530_223453/.project-map.json +90101 -0
- package/.project-map/backups/20260530_223507/.project-map.json +90101 -0
- package/.project-map/backups/20260530_223512/.project-map.json +90101 -0
- package/.project-map/backups/20260530_223512/map.toon +666 -0
- package/.project-map/backups/20260530_223516/.project-map.json +90101 -0
- package/.project-map/backups/20260530_223516/map.toon +666 -0
- package/.project-map/backups/20260530_223520/.project-map.json +90101 -0
- package/.project-map/backups/20260530_223520/map.toon +666 -0
- package/AGENTS.md +47 -0
- package/BUN_SHELL_MIGRATION_PLAN.md +136 -0
- package/Dockerfile +18 -0
- package/README.md +15 -0
- package/bin/epochcli +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/20260418092949_add_yolo_to_session/migration.sql +2 -0
- package/migration/20260418092949_add_yolo_to_session/snapshot.json +1199 -0
- package/migration/20260419120000_add_intervention_to_session/migration.sql +2 -0
- package/package.json +186 -0
- package/parsers-config.ts +290 -0
- package/script/build-node.ts +71 -0
- package/script/build.ts +255 -0
- package/script/check-migrations.ts +16 -0
- package/script/fix-node-pty.ts +28 -0
- package/script/postinstall.mjs +131 -0
- package/script/publish.ts +184 -0
- package/script/schema.ts +63 -0
- package/script/seed-e2e.ts +60 -0
- package/script/upgrade-opentui.ts +64 -0
- package/specs/effect-migration.md +310 -0
- package/specs/tui-plugins.md +436 -0
- package/specs/v2.md +14 -0
- package/src/account/account.sql.ts +39 -0
- package/src/account/index.ts +465 -0
- package/src/account/repo.ts +163 -0
- package/src/account/schema.ts +91 -0
- package/src/acp/README.md +174 -0
- package/src/acp/agent.ts +1847 -0
- package/src/acp/session.ts +116 -0
- package/src/acp/types.ts +24 -0
- package/src/agent/agent.ts +445 -0
- package/src/agent/generate.txt +75 -0
- package/src/agent/prompt/compaction.txt +15 -0
- package/src/agent/prompt/explore.txt +9 -0
- package/src/agent/prompt/summary.txt +11 -0
- package/src/agent/prompt/title.txt +44 -0
- package/src/auth/index.ts +110 -0
- package/src/bus/bus-event.ts +40 -0
- package/src/bus/global.ts +10 -0
- package/src/bus/index.ts +232 -0
- package/src/cli/bootstrap.ts +17 -0
- package/src/cli/cmd/account.ts +257 -0
- package/src/cli/cmd/acp.ts +70 -0
- package/src/cli/cmd/agent.ts +245 -0
- package/src/cli/cmd/cmd.ts +7 -0
- package/src/cli/cmd/db.ts +119 -0
- package/src/cli/cmd/debug/agent.ts +167 -0
- package/src/cli/cmd/debug/config.ts +16 -0
- package/src/cli/cmd/debug/file.ts +97 -0
- package/src/cli/cmd/debug/index.ts +48 -0
- package/src/cli/cmd/debug/lsp.ts +53 -0
- package/src/cli/cmd/debug/ripgrep.ts +87 -0
- package/src/cli/cmd/debug/scrap.ts +16 -0
- package/src/cli/cmd/debug/skill.ts +16 -0
- package/src/cli/cmd/debug/snapshot.ts +52 -0
- package/src/cli/cmd/export.ts +89 -0
- package/src/cli/cmd/generate.ts +38 -0
- package/src/cli/cmd/github.ts +1639 -0
- package/src/cli/cmd/import.ts +169 -0
- package/src/cli/cmd/mcp.ts +754 -0
- package/src/cli/cmd/models.ts +78 -0
- package/src/cli/cmd/plug.ts +233 -0
- package/src/cli/cmd/pr.ts +127 -0
- package/src/cli/cmd/providers.ts +478 -0
- package/src/cli/cmd/run.ts +681 -0
- package/src/cli/cmd/serve.ts +24 -0
- package/src/cli/cmd/session.ts +159 -0
- package/src/cli/cmd/stats.ts +410 -0
- package/src/cli/cmd/tui/app.tsx +945 -0
- package/src/cli/cmd/tui/attach.ts +88 -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 +171 -0
- package/src/cli/cmd/tui/component/dialog-console-org.tsx +103 -0
- package/src/cli/cmd/tui/component/dialog-mcp.tsx +86 -0
- package/src/cli/cmd/tui/component/dialog-model.tsx +190 -0
- package/src/cli/cmd/tui/component/dialog-provider.tsx +364 -0
- package/src/cli/cmd/tui/component/dialog-session-list.tsx +108 -0
- package/src/cli/cmd/tui/component/dialog-session-rename.tsx +31 -0
- package/src/cli/cmd/tui/component/dialog-skill.tsx +36 -0
- package/src/cli/cmd/tui/component/dialog-stash.tsx +87 -0
- package/src/cli/cmd/tui/component/dialog-status.tsx +168 -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-workspace-list.tsx +320 -0
- package/src/cli/cmd/tui/component/error-component.tsx +92 -0
- package/src/cli/cmd/tui/component/logo.tsx +85 -0
- package/src/cli/cmd/tui/component/plugin-route-missing.tsx +14 -0
- package/src/cli/cmd/tui/component/prompt/autocomplete.tsx +672 -0
- package/src/cli/cmd/tui/component/prompt/frecency.tsx +90 -0
- package/src/cli/cmd/tui/component/prompt/history.tsx +109 -0
- package/src/cli/cmd/tui/component/prompt/index.tsx +1336 -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/startup-loading.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/component/workspace/dialog-session-list.tsx +151 -0
- package/src/cli/cmd/tui/context/args.tsx +15 -0
- package/src/cli/cmd/tui/context/directory.ts +13 -0
- package/src/cli/cmd/tui/context/exit.tsx +60 -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 +52 -0
- package/src/cli/cmd/tui/context/local.tsx +456 -0
- package/src/cli/cmd/tui/context/plugin-keybinds.ts +41 -0
- package/src/cli/cmd/tui/context/prompt.tsx +18 -0
- package/src/cli/cmd/tui/context/route.tsx +52 -0
- package/src/cli/cmd/tui/context/sdk.tsx +115 -0
- package/src/cli/cmd/tui/context/sync.tsx +516 -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 +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin-macchiato.json +233 -0
- package/src/cli/cmd/tui/context/theme/catppuccin.json +112 -0
- package/src/cli/cmd/tui/context/theme/cobalt2.json +228 -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/epochcli.json +245 -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 +237 -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/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 +1236 -0
- package/src/cli/cmd/tui/context/tui-config.tsx +9 -0
- package/src/cli/cmd/tui/event.ts +48 -0
- package/src/cli/cmd/tui/feature-plugins/home/footer.tsx +93 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips-view.tsx +145 -0
- package/src/cli/cmd/tui/feature-plugins/home/tips.tsx +50 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/context.tsx +63 -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/lsp.tsx +66 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/mcp.tsx +96 -0
- package/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx +48 -0
- package/src/cli/cmd/tui/feature-plugins/system/plugins.tsx +270 -0
- package/src/cli/cmd/tui/plugin/api.tsx +397 -0
- package/src/cli/cmd/tui/plugin/index.ts +3 -0
- package/src/cli/cmd/tui/plugin/internal.ts +27 -0
- package/src/cli/cmd/tui/plugin/runtime.ts +1031 -0
- package/src/cli/cmd/tui/plugin/slots.tsx +60 -0
- package/src/cli/cmd/tui/routes/home.tsx +84 -0
- package/src/cli/cmd/tui/routes/session/dialog-fork-from-timeline.tsx +65 -0
- package/src/cli/cmd/tui/routes/session/dialog-message.tsx +110 -0
- package/src/cli/cmd/tui/routes/session/dialog-subagent.tsx +26 -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 +2161 -0
- package/src/cli/cmd/tui/routes/session/permission.tsx +691 -0
- package/src/cli/cmd/tui/routes/session/question.tsx +468 -0
- package/src/cli/cmd/tui/routes/session/sidebar.tsx +70 -0
- package/src/cli/cmd/tui/routes/session/subagent-footer.tsx +131 -0
- package/src/cli/cmd/tui/thread.ts +241 -0
- package/src/cli/cmd/tui/ui/dialog-alert.tsx +59 -0
- package/src/cli/cmd/tui/ui/dialog-confirm.tsx +89 -0
- package/src/cli/cmd/tui/ui/dialog-export-options.tsx +211 -0
- package/src/cli/cmd/tui/ui/dialog-help.tsx +40 -0
- package/src/cli/cmd/tui/ui/dialog-prompt.tsx +115 -0
- package/src/cli/cmd/tui/ui/dialog-select.tsx +417 -0
- package/src/cli/cmd/tui/ui/dialog.tsx +192 -0
- package/src/cli/cmd/tui/ui/link.tsx +28 -0
- package/src/cli/cmd/tui/ui/spinner.ts +368 -0
- package/src/cli/cmd/tui/ui/toast.tsx +100 -0
- package/src/cli/cmd/tui/util/clipboard.ts +192 -0
- package/src/cli/cmd/tui/util/editor.ts +37 -0
- package/src/cli/cmd/tui/util/model.ts +23 -0
- package/src/cli/cmd/tui/util/provider-origin.ts +20 -0
- package/src/cli/cmd/tui/util/scroll.ts +23 -0
- package/src/cli/cmd/tui/util/selection.ts +25 -0
- package/src/cli/cmd/tui/util/signal.ts +7 -0
- package/src/cli/cmd/tui/util/terminal.ts +114 -0
- package/src/cli/cmd/tui/util/transcript.ts +112 -0
- package/src/cli/cmd/tui/win32.ts +129 -0
- package/src/cli/cmd/tui/worker.ts +195 -0
- package/src/cli/cmd/uninstall.ts +353 -0
- package/src/cli/cmd/upgrade.ts +73 -0
- package/src/cli/cmd/web.ts +81 -0
- package/src/cli/effect/prompt.ts +25 -0
- package/src/cli/error.ts +46 -0
- package/src/cli/heap.ts +59 -0
- package/src/cli/logo.ts +6 -0
- package/src/cli/network.ts +60 -0
- package/src/cli/ui.ts +133 -0
- package/src/cli/upgrade.ts +31 -0
- package/src/command/index.ts +197 -0
- package/src/command/template/initialize.txt +66 -0
- package/src/command/template/review.txt +101 -0
- package/src/config/config.ts +1610 -0
- package/src/config/console-state.ts +15 -0
- package/src/config/markdown.ts +99 -0
- package/src/config/paths.ts +167 -0
- package/src/config/tui-migrate.ts +155 -0
- package/src/config/tui-schema.ts +37 -0
- package/src/config/tui.ts +179 -0
- package/src/config/validator.ts +52 -0
- package/src/control-plane/adaptors/index.ts +20 -0
- package/src/control-plane/adaptors/worktree.ts +42 -0
- package/src/control-plane/schema.ts +17 -0
- package/src/control-plane/sse.ts +66 -0
- package/src/control-plane/types.ts +32 -0
- package/src/control-plane/workspace.sql.ts +17 -0
- package/src/control-plane/workspace.ts +168 -0
- package/src/effect/cross-spawn-spawner.ts +502 -0
- package/src/effect/instance-ref.ts +6 -0
- package/src/effect/instance-registry.ts +12 -0
- package/src/effect/instance-state.ts +82 -0
- package/src/effect/run-service.ts +33 -0
- package/src/effect/runner.ts +216 -0
- package/src/env/index.ts +28 -0
- package/src/file/ignore.ts +82 -0
- package/src/file/index.ts +686 -0
- package/src/file/protected.ts +59 -0
- package/src/file/ripgrep.ts +376 -0
- package/src/file/time.ts +133 -0
- package/src/file/watcher.ts +172 -0
- package/src/filesystem/index.ts +236 -0
- package/src/flag/flag.ts +157 -0
- package/src/format/formatter.ts +413 -0
- package/src/format/index.ts +203 -0
- package/src/git/index.ts +303 -0
- package/src/global/index.ts +54 -0
- package/src/id/id.ts +85 -0
- package/src/ide/index.ts +74 -0
- package/src/index.ts +253 -0
- package/src/installation/index.ts +355 -0
- package/src/installation/meta.ts +7 -0
- package/src/lsp/client.ts +256 -0
- package/src/lsp/index.ts +558 -0
- package/src/lsp/language.ts +120 -0
- package/src/lsp/launch.ts +21 -0
- package/src/lsp/server.ts +1968 -0
- package/src/mcp/auth.ts +173 -0
- package/src/mcp/index.ts +1250 -0
- package/src/mcp/oauth-callback.ts +216 -0
- package/src/mcp/oauth-provider.ts +185 -0
- package/src/mcp/schema-loader.ts +82 -0
- package/src/node.ts +1 -0
- package/src/npm/index.ts +188 -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 +323 -0
- package/src/permission/schema.ts +17 -0
- package/src/plugin/cloudflare.ts +67 -0
- package/src/plugin/codex.ts +608 -0
- package/src/plugin/github-copilot/copilot.ts +361 -0
- package/src/plugin/github-copilot/models.ts +144 -0
- package/src/plugin/index.ts +288 -0
- package/src/plugin/install.ts +439 -0
- package/src/plugin/loader.ts +174 -0
- package/src/plugin/meta.ts +188 -0
- package/src/plugin/shared.ts +323 -0
- package/src/project/bootstrap.ts +29 -0
- package/src/project/instance.ts +175 -0
- package/src/project/project.sql.ts +16 -0
- package/src/project/project.ts +519 -0
- package/src/project/schema.ts +16 -0
- package/src/project/state.ts +70 -0
- package/src/project/vcs.ts +240 -0
- package/src/provider/auth.ts +253 -0
- package/src/provider/error.ts +297 -0
- package/src/provider/models.ts +162 -0
- package/src/provider/provider.ts +1776 -0
- package/src/provider/schema.ts +38 -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 +814 -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 +1769 -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 +1124 -0
- package/src/pty/index.ts +397 -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 +224 -0
- package/src/question/schema.ts +17 -0
- package/src/server/error.ts +36 -0
- package/src/server/event.ts +7 -0
- package/src/server/instance.ts +315 -0
- package/src/server/mdns.ts +60 -0
- package/src/server/middleware.ts +33 -0
- package/src/server/projectors.ts +28 -0
- package/src/server/proxy.ts +130 -0
- package/src/server/router.ts +105 -0
- package/src/server/routes/config.ts +92 -0
- package/src/server/routes/event.ts +83 -0
- package/src/server/routes/experimental.ts +374 -0
- package/src/server/routes/file.ts +197 -0
- package/src/server/routes/global.ts +312 -0
- package/src/server/routes/mcp.ts +225 -0
- package/src/server/routes/permission.ts +69 -0
- package/src/server/routes/project.ts +118 -0
- package/src/server/routes/provider.ts +171 -0
- package/src/server/routes/pty.ts +210 -0
- package/src/server/routes/question.ts +99 -0
- package/src/server/routes/session.ts +984 -0
- package/src/server/routes/tui.ts +378 -0
- package/src/server/routes/workspace.ts +94 -0
- package/src/server/server.ts +353 -0
- package/src/session/compaction.ts +86 -0
- package/src/session/index.ts +904 -0
- package/src/session/instruction.ts +261 -0
- package/src/session/llm/monitor.ts +87 -0
- package/src/session/llm.ts +1676 -0
- package/src/session/message-v2.ts +1082 -0
- package/src/session/message.ts +191 -0
- package/src/session/overflow.ts +35 -0
- package/src/session/processor.ts +635 -0
- package/src/session/projectors.ts +136 -0
- package/src/session/prompt/build-switch.txt +5 -0
- package/src/session/prompt/builder.ts +135 -0
- package/src/session/prompt/default.txt +11 -0
- package/src/session/prompt/engine.ts +1072 -0
- package/src/session/prompt/gemma4.txt +1 -0
- package/src/session/prompt/max-steps.txt +16 -0
- package/src/session/prompt/orchestrator.ts +426 -0
- package/src/session/prompt/plan.txt +28 -0
- package/src/session/prompt/qwen.txt +19 -0
- package/src/session/prompt/resolver.ts +670 -0
- package/src/session/prompt/router.ts +197 -0
- package/src/session/prompt/state.ts +96 -0
- package/src/session/prompt/types.ts +115 -0
- package/src/session/prompt/utils.ts +15 -0
- package/src/session/prompt.ts +362 -0
- package/src/session/retry.ts +106 -0
- package/src/session/revert.ts +176 -0
- package/src/session/sanitizer.ts +125 -0
- package/src/session/schema.ts +38 -0
- package/src/session/session.sql.ts +106 -0
- package/src/session/status.ts +102 -0
- package/src/session/summary.ts +183 -0
- package/src/session/system.ts +79 -0
- package/src/session/todo.ts +166 -0
- package/src/session/worker.ts +382 -0
- package/src/shell/shell.ts +110 -0
- package/src/skill/discovery.ts +116 -0
- package/src/skill/index.ts +287 -0
- package/src/snapshot/index.ts +726 -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 +174 -0
- package/src/storage/json-migration.ts +387 -0
- package/src/storage/schema.sql.ts +10 -0
- package/src/storage/schema.ts +4 -0
- package/src/storage/storage.ts +353 -0
- package/src/sync/README.md +179 -0
- package/src/sync/event.sql.ts +16 -0
- package/src/sync/index.ts +263 -0
- package/src/sync/schema.ts +14 -0
- package/src/tool/apply_patch.ts +281 -0
- package/src/tool/apply_patch.txt +1 -0
- package/src/tool/arbitration.txt +5 -0
- package/src/tool/bash.ts +494 -0
- package/src/tool/bash.txt +2 -0
- package/src/tool/batch.ts +183 -0
- package/src/tool/batch.txt +1 -0
- package/src/tool/codesearch.ts +132 -0
- package/src/tool/codesearch.txt +1 -0
- package/src/tool/edit.ts +734 -0
- package/src/tool/edit.txt +1 -0
- package/src/tool/external-directory.ts +46 -0
- package/src/tool/glob.ts +73 -0
- package/src/tool/glob.txt +2 -0
- package/src/tool/grep.ts +156 -0
- package/src/tool/grep.txt +2 -0
- package/src/tool/invalid.ts +20 -0
- package/src/tool/ls.ts +121 -0
- package/src/tool/ls.txt +1 -0
- package/src/tool/lsp.ts +97 -0
- package/src/tool/lsp.txt +1 -0
- package/src/tool/multiedit.ts +46 -0
- package/src/tool/multiedit.txt +1 -0
- package/src/tool/plan-enter.txt +14 -0
- package/src/tool/plan-exit.txt +13 -0
- package/src/tool/plan.ts +131 -0
- package/src/tool/question.ts +46 -0
- package/src/tool/question.txt +10 -0
- package/src/tool/read.ts +332 -0
- package/src/tool/read.txt +1 -0
- package/src/tool/registry.ts +288 -0
- package/src/tool/revert.ts +37 -0
- package/src/tool/schema.ts +17 -0
- package/src/tool/skill.ts +105 -0
- package/src/tool/task.ts +150 -0
- package/src/tool/task.txt +3 -0
- package/src/tool/task_complete.ts +21 -0
- package/src/tool/tool.ts +112 -0
- package/src/tool/truncate.ts +144 -0
- package/src/tool/truncation-dir.ts +4 -0
- package/src/tool/webfetch.ts +206 -0
- package/src/tool/webfetch.txt +1 -0
- package/src/tool/websearch.ts +150 -0
- package/src/tool/websearch.txt +1 -0
- package/src/tool/write.ts +101 -0
- package/src/tool/write.txt +1 -0
- package/src/util/abort.ts +35 -0
- package/src/util/ai-sdk.ts +59 -0
- package/src/util/archive.ts +17 -0
- package/src/util/color.ts +19 -0
- package/src/util/context.ts +25 -0
- package/src/util/data-url.ts +9 -0
- package/src/util/defer.ts +12 -0
- package/src/util/effect-http-client.ts +11 -0
- package/src/util/effect-zod.ts +98 -0
- package/src/util/error.ts +77 -0
- package/src/util/filesystem.ts +245 -0
- package/src/util/flock.ts +333 -0
- package/src/util/fn.ts +21 -0
- package/src/util/format.ts +20 -0
- package/src/util/glob.ts +34 -0
- package/src/util/hash.ts +7 -0
- package/src/util/iife.ts +3 -0
- package/src/util/keybind.ts +103 -0
- package/src/util/lazy.ts +23 -0
- package/src/util/locale.ts +81 -0
- package/src/util/lock.ts +98 -0
- package/src/util/log-parser.ts +114 -0
- package/src/util/log.ts +250 -0
- package/src/util/network.ts +23 -0
- package/src/util/process.ts +176 -0
- package/src/util/queue.ts +32 -0
- package/src/util/record.ts +3 -0
- package/src/util/rpc.ts +66 -0
- package/src/util/schema.ts +53 -0
- package/src/util/scrap.ts +10 -0
- package/src/util/session-analyzer.ts +331 -0
- package/src/util/session-telemetry.ts +91 -0
- package/src/util/signal.ts +12 -0
- package/src/util/timeout.ts +14 -0
- package/src/util/token.ts +7 -0
- package/src/util/tokenizer.ts +50 -0
- package/src/util/toon.ts +45 -0
- package/src/util/update-schema.ts +13 -0
- package/src/util/which.ts +14 -0
- package/src/util/wildcard.ts +59 -0
- package/src/worktree/index.ts +612 -0
- package/sst-env.d.ts +10 -0
- package/test/AGENTS.md +81 -0
- package/test/account/repo.test.ts +326 -0
- package/test/account/service.test.ts +393 -0
- package/test/acp/agent-interface.test.ts +51 -0
- package/test/acp/event-subscription.test.ts +685 -0
- package/test/agent/agent.test.ts +716 -0
- package/test/auth/auth.test.ts +58 -0
- package/test/bus/bus-effect.test.ts +164 -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/github-action.test.ts +198 -0
- package/test/cli/github-remote.test.ts +80 -0
- package/test/cli/plugin-auth-picker.test.ts +120 -0
- package/test/cli/tui/keybind-plugin.test.ts +90 -0
- package/test/cli/tui/plugin-add.test.ts +107 -0
- package/test/cli/tui/plugin-install.test.ts +89 -0
- package/test/cli/tui/plugin-lifecycle.test.ts +225 -0
- package/test/cli/tui/plugin-loader-entrypoint.test.ts +492 -0
- package/test/cli/tui/plugin-loader-pure.test.ts +72 -0
- package/test/cli/tui/plugin-loader.test.ts +752 -0
- package/test/cli/tui/plugin-toggle.test.ts +159 -0
- package/test/cli/tui/slot-replace.test.tsx +47 -0
- package/test/cli/tui/theme-store.test.ts +51 -0
- package/test/cli/tui/thread.test.ts +128 -0
- package/test/cli/tui/transcript.test.ts +426 -0
- package/test/config/agent-color.test.ts +71 -0
- package/test/config/config.test.ts +2337 -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/markdown.test.ts +228 -0
- package/test/config/tui.test.ts +800 -0
- package/test/control-plane/sse.test.ts +56 -0
- package/test/effect/cross-spawn-spawner.test.ts +412 -0
- package/test/effect/instance-state.test.ts +482 -0
- package/test/effect/run-service.test.ts +46 -0
- package/test/effect/runner.test.ts +523 -0
- package/test/fake/provider.ts +82 -0
- package/test/file/fsmonitor.test.ts +62 -0
- package/test/file/ignore.test.ts +10 -0
- package/test/file/index.test.ts +946 -0
- package/test/file/path-traversal.test.ts +198 -0
- package/test/file/ripgrep.test.ts +54 -0
- package/test/file/time.test.ts +445 -0
- package/test/file/watcher.test.ts +247 -0
- package/test/filesystem/filesystem.test.ts +319 -0
- package/test/fixture/db.ts +11 -0
- package/test/fixture/fixture.test.ts +26 -0
- package/test/fixture/fixture.ts +172 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/fixture/lsp/fake-lsp-server.js +77 -0
- package/test/fixture/plug-worker.ts +93 -0
- package/test/fixture/plugin-meta-worker.ts +26 -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 +27 -0
- package/test/format/format.test.ts +171 -0
- package/test/git/git.test.ts +128 -0
- package/test/ide/ide.test.ts +82 -0
- package/test/installation/installation.test.ts +152 -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 +794 -0
- package/test/lsp/client.test.ts +95 -0
- package/test/lsp/index.test.ts +133 -0
- package/test/lsp/launch.test.ts +22 -0
- package/test/lsp/lifecycle.test.ts +147 -0
- package/test/mcp/headers.test.ts +153 -0
- package/test/mcp/lifecycle.test.ts +750 -0
- package/test/mcp/oauth-auto-connect.test.ts +199 -0
- package/test/mcp/oauth-browser.test.ts +249 -0
- package/test/mcp/sc-approve-validator.test.ts +431 -0
- package/test/memory/abort-leak.test.ts +137 -0
- package/test/npm.test.ts +18 -0
- package/test/patch/patch.test.ts +348 -0
- package/test/permission/arity.test.ts +33 -0
- package/test/permission/next.test.ts +1123 -0
- package/test/permission-task.test.ts +323 -0
- package/test/plugin/auth-override.test.ts +74 -0
- package/test/plugin/codex.test.ts +123 -0
- package/test/plugin/github-copilot-models.test.ts +117 -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 +1136 -0
- package/test/plugin/meta.test.ts +137 -0
- package/test/plugin/shared.test.ts +88 -0
- package/test/plugin/trigger.test.ts +111 -0
- package/test/preload.ts +90 -0
- package/test/project/migrate-global.test.ts +140 -0
- package/test/project/project.test.ts +459 -0
- package/test/project/state.test.ts +115 -0
- package/test/project/vcs.test.ts +228 -0
- package/test/project/worktree-remove.test.ts +96 -0
- package/test/project/worktree.test.ts +173 -0
- package/test/provider/amazon-bedrock.test.ts +447 -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 +49 -0
- package/test/provider/gitlab-duo.test.ts +412 -0
- package/test/provider/provider.test.ts +2494 -0
- package/test/provider/transform.test.ts +2944 -0
- package/test/pty/pty-output-isolation.test.ts +141 -0
- package/test/pty/pty-session.test.ts +92 -0
- package/test/pty/pty-shell.test.ts +59 -0
- package/test/question/question.test.ts +453 -0
- package/test/server/global-session-list.test.ts +89 -0
- package/test/server/project-init-git.test.ts +121 -0
- package/test/server/session-actions.test.ts +83 -0
- package/test/server/session-list.test.ts +98 -0
- package/test/server/session-messages.test.ts +159 -0
- package/test/server/session-select.test.ts +84 -0
- package/test/session/compaction.test.ts +683 -0
- package/test/session/continuity-handover.test.ts +620 -0
- package/test/session/deterministic-handover.test.ts +328 -0
- package/test/session/doom-protection.test.ts +247 -0
- package/test/session/hard-reset.test.ts +179 -0
- package/test/session/instruction.test.ts +286 -0
- package/test/session/llm/monitor.test.ts +53 -0
- package/test/session/llm-sanitizer.test.ts +90 -0
- package/test/session/llm-zones-e2e.test.ts +61 -0
- package/test/session/llm.test.ts +1308 -0
- package/test/session/mcpx-normalization.test.ts +86 -0
- package/test/session/mcpx-syntax-recovery.test.ts +28 -0
- package/test/session/message-v2.test.ts +957 -0
- package/test/session/messages-pagination.test.ts +885 -0
- package/test/session/processor-effect.test.ts +805 -0
- package/test/session/prompt/builder.test.ts +71 -0
- package/test/session/prompt/engine-loop.test.ts +80 -0
- package/test/session/prompt/orchestrator.test.ts +108 -0
- package/test/session/prompt/resolver.test.ts +211 -0
- package/test/session/prompt/router.test.ts +84 -0
- package/test/session/prompt/state.test.ts +57 -0
- package/test/session/prompt-effect.test.ts +1241 -0
- package/test/session/prompt.test.ts +522 -0
- package/test/session/refactor-system-zones.test.ts +241 -0
- package/test/session/retry.test.ts +232 -0
- package/test/session/revert-compact.test.ts +621 -0
- package/test/session/sanitizer.test.ts +61 -0
- package/test/session/session.test.ts +142 -0
- package/test/session/snapshot-tool-race.test.ts +242 -0
- package/test/session/structured-output-integration.test.ts +233 -0
- package/test/session/structured-output.test.ts +391 -0
- package/test/session/system.test.ts +59 -0
- package/test/session/telemetry.test.ts +35 -0
- package/test/shell/shell.test.ts +73 -0
- package/test/skill/discovery.test.ts +116 -0
- package/test/skill/skill.test.ts +392 -0
- package/test/snapshot/snapshot.test.ts +1404 -0
- package/test/storage/db.test.ts +14 -0
- package/test/storage/json-migration.test.ts +791 -0
- package/test/storage/storage.test.ts +295 -0
- package/test/sync/index.test.ts +191 -0
- package/test/tool/__snapshots__/tool.test.ts.snap +9 -0
- package/test/tool/apply_patch.test.ts +567 -0
- package/test/tool/bash.test.ts +1099 -0
- package/test/tool/edit.test.ts +681 -0
- package/test/tool/external-directory.test.ts +198 -0
- package/test/tool/fixtures/large-image.png +0 -0
- package/test/tool/fixtures/models-api.json +65179 -0
- package/test/tool/grep.test.ts +111 -0
- package/test/tool/question.test.ts +126 -0
- package/test/tool/read.test.ts +468 -0
- package/test/tool/registry.test.ts +126 -0
- package/test/tool/skill.test.ts +167 -0
- package/test/tool/task.test.ts +49 -0
- package/test/tool/tool-define.test.ts +101 -0
- package/test/tool/truncation.test.ts +161 -0
- package/test/tool/webfetch.test.ts +101 -0
- package/test/tool/write.test.ts +354 -0
- package/test/util/data-url.test.ts +14 -0
- package/test/util/effect-zod.test.ts +61 -0
- package/test/util/error.test.ts +38 -0
- package/test/util/filesystem.test.ts +656 -0
- package/test/util/flock.test.ts +383 -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-parser.test.ts +61 -0
- package/test/util/module.test.ts +59 -0
- package/test/util/process.test.ts +128 -0
- package/test/util/telemetry-integration.test.ts +104 -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-regex.js +50 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,1676 @@
|
|
|
1
|
+
import { Provider } from "@/provider/provider"
|
|
2
|
+
import { Log } from "@/util/log"
|
|
3
|
+
import { SessionTelemetry } from "@/util/session-telemetry"
|
|
4
|
+
import { Cause, Effect, Layer, Record, ServiceMap } from "effect"
|
|
5
|
+
import * as Queue from "effect/Queue"
|
|
6
|
+
import * as Stream from "effect/Stream"
|
|
7
|
+
import { streamText, wrapLanguageModel, type ModelMessage, type Tool, tool, jsonSchema, generateText } from "@/util/ai-sdk"
|
|
8
|
+
import Ajv from "ajv"
|
|
9
|
+
|
|
10
|
+
const ajv = new Ajv({
|
|
11
|
+
strict: false,
|
|
12
|
+
allErrors: true,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
import { mergeDeep, pipe } from "remeda"
|
|
16
|
+
import { GitLabWorkflowLanguageModel } from "gitlab-ai-provider"
|
|
17
|
+
import { ProviderTransform } from "@/provider/transform"
|
|
18
|
+
import { Config } from "@/config/config"
|
|
19
|
+
import { Instance } from "@/project/instance"
|
|
20
|
+
import type { Agent } from "@/agent/agent"
|
|
21
|
+
import { MessageV2 } from "./message-v2"
|
|
22
|
+
import { SanitizerMiddleware } from "./sanitizer"
|
|
23
|
+
import { Plugin } from "@/plugin"
|
|
24
|
+
import { SystemPrompt } from "./system"
|
|
25
|
+
import { PromptBuilder, type ZoneStructuredPayload } from "./prompt/builder"
|
|
26
|
+
import { Flag } from "@/flag/flag"
|
|
27
|
+
import { Permission } from "@/permission"
|
|
28
|
+
import { Auth } from "@/auth"
|
|
29
|
+
import { Installation } from "@/installation"
|
|
30
|
+
import { ToonEncoder } from "@/util/toon"
|
|
31
|
+
import { MCP } from "@/mcp/index"
|
|
32
|
+
import { StreamingMonitor } from "./llm/monitor"
|
|
33
|
+
import { SchemaContextLoader } from "@/mcp/schema-loader"
|
|
34
|
+
|
|
35
|
+
export namespace LLM {
|
|
36
|
+
const log = Log.create({ service: "llm" })
|
|
37
|
+
export const OUTPUT_TOKEN_MAX = ProviderTransform.OUTPUT_TOKEN_MAX
|
|
38
|
+
|
|
39
|
+
interface SessionMetadata {
|
|
40
|
+
phaseTurnCount: number
|
|
41
|
+
lastPhase: string
|
|
42
|
+
consecutiveFailures: Map<string, number>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const sessionMetadata = new Map<string, SessionMetadata>()
|
|
46
|
+
|
|
47
|
+
export type StreamInput = {
|
|
48
|
+
user: MessageV2.User
|
|
49
|
+
sessionID: string
|
|
50
|
+
parentSessionID?: string
|
|
51
|
+
model: Provider.Model
|
|
52
|
+
agent: Agent.Info
|
|
53
|
+
permission?: Permission.Ruleset
|
|
54
|
+
system: { zone1: string[]; zone2: string[] }
|
|
55
|
+
operationalFacts?: string[]
|
|
56
|
+
instructions?: string[]
|
|
57
|
+
messages: ModelMessage[]
|
|
58
|
+
small?: boolean
|
|
59
|
+
yolo?: boolean
|
|
60
|
+
isContinue?: boolean
|
|
61
|
+
tools: Record<string, Tool>
|
|
62
|
+
retries?: number
|
|
63
|
+
toolChoice?: "auto" | "required" | "none"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type StreamRequest = StreamInput & {
|
|
67
|
+
abort: AbortSignal
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export type Event = Awaited<ReturnType<typeof stream>>["fullStream"] extends AsyncIterable<infer T> ? T : never
|
|
71
|
+
|
|
72
|
+
export interface Interface {
|
|
73
|
+
readonly stream: (input: StreamInput) => Stream.Stream<Event, unknown>
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class Service extends ServiceMap.Service<Service, Interface>()("@epochcli/LLM") {}
|
|
77
|
+
|
|
78
|
+
export const layer = Layer.effect(
|
|
79
|
+
Service,
|
|
80
|
+
Effect.gen(function* () {
|
|
81
|
+
return Service.of({
|
|
82
|
+
stream(input) {
|
|
83
|
+
return Stream.scoped(
|
|
84
|
+
Stream.unwrap(
|
|
85
|
+
Effect.gen(function* () {
|
|
86
|
+
const ctrl = yield* Effect.acquireRelease(
|
|
87
|
+
Effect.sync(() => new AbortController()),
|
|
88
|
+
(ctrl) => Effect.sync(() => ctrl.abort()),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
const result = yield* Effect.promise(() => LLM.stream({ ...input, abort: ctrl.signal }))
|
|
92
|
+
|
|
93
|
+
// Robustly suppress unhandled rejections on known background promises returned by the AI SDK
|
|
94
|
+
// Explicitly access getters because for...in misses non-enumerable properties on the prototype.
|
|
95
|
+
const promiseKeys = ["text", "usage", "finishReason", "toolCalls", "toolResults", "warnings", "providerMetadata", "steps", "response"]
|
|
96
|
+
for (const key of promiseKeys) {
|
|
97
|
+
try {
|
|
98
|
+
const value = (result as any)[key]
|
|
99
|
+
if (value instanceof Promise) {
|
|
100
|
+
value.catch(() => {})
|
|
101
|
+
}
|
|
102
|
+
} catch {}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return Stream.fromAsyncIterable(result.fullStream, (e) =>
|
|
106
|
+
e instanceof Error ? e : new Error(String(e)),
|
|
107
|
+
)
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
)
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
}),
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
export const defaultLayer = layer
|
|
117
|
+
|
|
118
|
+
export function parseGroundTruthRules(
|
|
119
|
+
raw: string,
|
|
120
|
+
activePacks: string[] = ["core_interaction_pack"],
|
|
121
|
+
contextLimit?: number,
|
|
122
|
+
): { operationalFacts: string; behavioralRules: string; projectSpecific: string } {
|
|
123
|
+
const zones = {
|
|
124
|
+
operationalFacts: "",
|
|
125
|
+
behavioralRules: "",
|
|
126
|
+
projectSpecific: "",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const zone1Match = raw.match(/(ZONE 1 & 3:.*?)(?=ZONE 2:|$)/s)
|
|
130
|
+
if (zone1Match) {
|
|
131
|
+
const factsStr = zone1Match[1]
|
|
132
|
+
const factsRegex = /fact_\d+,\s*"[^"]+",\s*"[^"]+",\s*"([^"]+)"/g
|
|
133
|
+
const extractedFacts: string[] = []
|
|
134
|
+
let match
|
|
135
|
+
while ((match = factsRegex.exec(factsStr)) !== null) {
|
|
136
|
+
let fact = match[1]
|
|
137
|
+
// Dynamic Context Limit Injection
|
|
138
|
+
if (fact.includes("[CONTEXT_LIMIT]")) {
|
|
139
|
+
const limitStr = `${Math.round((contextLimit ?? 32000) / 1000)}K tokens`
|
|
140
|
+
fact = fact.replace("[CONTEXT_LIMIT]", limitStr)
|
|
141
|
+
}
|
|
142
|
+
extractedFacts.push(`- ${fact}`)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (extractedFacts.length > 0) {
|
|
146
|
+
zones.operationalFacts = `ZONE 1 & 3: OPERATIONAL FACTS\n${extractedFacts.join("\n")}`
|
|
147
|
+
} else {
|
|
148
|
+
zones.operationalFacts = factsStr.trim()
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const zone2Match = raw.match(/(ZONE 2: BEHAVIORAL RULE PACKS.*?)(?=ZONE 3: PROJECT-SPECIFIC RULES|$)/s)
|
|
153
|
+
if (zone2Match) {
|
|
154
|
+
const zone2Str = zone2Match[1]
|
|
155
|
+
|
|
156
|
+
const ruleIds = new Set<string>()
|
|
157
|
+
for (const targetPack of activePacks) {
|
|
158
|
+
const packRegex = new RegExp(`${targetPack}:\\s*\\[(.*?)\\]`)
|
|
159
|
+
const packMatch = zone2Str.match(packRegex)
|
|
160
|
+
if (packMatch) {
|
|
161
|
+
packMatch[1].split(",").forEach((s: string) => {
|
|
162
|
+
const id = s.trim().split(".").pop() || ""
|
|
163
|
+
if (id) ruleIds.add(id)
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (ruleIds.size > 0) {
|
|
169
|
+
const rulesRegex = /([a-z]+_\d+),\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)"/g
|
|
170
|
+
const extractedRules: string[] = []
|
|
171
|
+
let rMatch
|
|
172
|
+
while ((rMatch = rulesRegex.exec(zone2Str)) !== null) {
|
|
173
|
+
if (ruleIds.has(rMatch[1])) {
|
|
174
|
+
extractedRules.push(`Trigger: ${rMatch[2]}\nBehaviour: ${rMatch[3]}\nExample: ${rMatch[4]}\n`)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (extractedRules.length > 0) {
|
|
179
|
+
zones.behavioralRules = `ZONE 2: BEHAVIORAL RULES\n\n${extractedRules.join("\n")}`
|
|
180
|
+
} else {
|
|
181
|
+
zones.behavioralRules = zone2Str.trim()
|
|
182
|
+
}
|
|
183
|
+
} else {
|
|
184
|
+
zones.behavioralRules = zone2Str.trim()
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const zone3Specific = raw.match(/(ZONE 3: PROJECT-SPECIFIC RULES.*?)$/s)
|
|
189
|
+
if (zone3Specific) zones.projectSpecific = zone3Specific[1].trim()
|
|
190
|
+
|
|
191
|
+
if (!zones.operationalFacts && !zones.behavioralRules && !zones.projectSpecific) {
|
|
192
|
+
zones.behavioralRules = raw.trim()
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return zones
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export async function stream(input: StreamRequest) {
|
|
199
|
+
const l = log
|
|
200
|
+
.clone()
|
|
201
|
+
.tag("providerID", input.model.providerID)
|
|
202
|
+
.tag("modelID", input.model.id)
|
|
203
|
+
.tag("sessionID", input.sessionID)
|
|
204
|
+
.tag("small", (input.small ?? false).toString())
|
|
205
|
+
.tag("agent", input.agent.name)
|
|
206
|
+
.tag("mode", input.agent.mode)
|
|
207
|
+
l.info("stream", {
|
|
208
|
+
modelID: input.model.id,
|
|
209
|
+
providerID: input.model.providerID,
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
// Update Session Metadata for Turn Tracking & Stagnation Detection
|
|
213
|
+
let meta = sessionMetadata.get(input.sessionID)
|
|
214
|
+
if (!meta) {
|
|
215
|
+
meta = { phaseTurnCount: 0, lastPhase: input.agent.name, consecutiveFailures: new Map() }
|
|
216
|
+
sessionMetadata.set(input.sessionID, meta)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (meta.lastPhase !== input.agent.name) {
|
|
220
|
+
meta.phaseTurnCount = 0
|
|
221
|
+
meta.lastPhase = input.agent.name
|
|
222
|
+
}
|
|
223
|
+
meta.phaseTurnCount++
|
|
224
|
+
|
|
225
|
+
const [language, cfg, providerInfo, auth] = await Promise.all([
|
|
226
|
+
Provider.getLanguage(input.model),
|
|
227
|
+
Config.get(),
|
|
228
|
+
Provider.getProvider(input.model.providerID),
|
|
229
|
+
Auth.get(input.model.providerID),
|
|
230
|
+
])
|
|
231
|
+
// TODO: move this to a proper hook
|
|
232
|
+
const isOpenaiOauth = providerInfo.id === "openai" && auth?.type === "oauth"
|
|
233
|
+
|
|
234
|
+
const payload: ZoneStructuredPayload = {
|
|
235
|
+
zone1_critical_rules: [`Current Phase: [${input.agent.name.toUpperCase()}].`],
|
|
236
|
+
zone2_context_files: [],
|
|
237
|
+
zone3_active_cursor: [],
|
|
238
|
+
zone4_guidelines: [],
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
const fsNode = await import("fs/promises")
|
|
243
|
+
const pathNode = await import("path")
|
|
244
|
+
const lastUsedPath = pathNode.join(Instance.directory, ".spec_last_used")
|
|
245
|
+
const lastUsed = await fsNode.readFile(lastUsedPath, "utf-8").catch(() => null)
|
|
246
|
+
if (lastUsed) {
|
|
247
|
+
payload.zone1_critical_rules.push(`ACTIVE PROJECT CONTEXT:\n- Feature Path: ${lastUsed.trim()}\n- Note: Subagents MUST NOT run \`sc_init\` if an active feature path is already established. Use \`map\` tools to explore this path.`)
|
|
248
|
+
}
|
|
249
|
+
} catch (e) {
|
|
250
|
+
l.debug("Failed to fetch active project context for subagent", { error: String(e) })
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (input.operationalFacts && input.operationalFacts.length > 0) {
|
|
254
|
+
payload.zone1_critical_rules.push(...input.operationalFacts)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (input.system?.zone1) {
|
|
258
|
+
payload.zone1_critical_rules.push(...input.system.zone1)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (input.yolo) {
|
|
262
|
+
payload.zone1_critical_rules.push(
|
|
263
|
+
"CRITICAL: YOLO mode is active. You MUST execute tasks autonomously until the work is completely finished. " +
|
|
264
|
+
"You are NOT allowed to stop and ask for user input. " +
|
|
265
|
+
"When AND ONLY WHEN the entire job is done, you MUST call the 'task_complete' tool to terminate the session.",
|
|
266
|
+
)
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
payload.zone1_critical_rules.push(
|
|
270
|
+
"ENVIRONMENT CONTEXT: You are operating in a context-constrained environment. " +
|
|
271
|
+
"You MUST work strictly file-by-file. NEVER attempt to create or modify multiple files in a single response or generate massive code blocks at once. " +
|
|
272
|
+
"Wait for the tool result confirmation before proceeding to the next file.",
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
if (input.instructions && input.instructions.length > 0) {
|
|
276
|
+
payload.zone4_guidelines.push(...input.instructions)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Phase Stagnation Detection (Task 3.2)
|
|
280
|
+
if (input.agent.name === "plan" && meta.phaseTurnCount >= 8) {
|
|
281
|
+
const nudge = `Supervisor Note: You have been in the [PLAN] phase for ${meta.phaseTurnCount} turns. If the implementation plan is complete and tasks are defined, you should run 'sc_approve' to transition to the [BUILD] phase.`
|
|
282
|
+
payload.zone1_critical_rules.push(nudge)
|
|
283
|
+
l.info("stagnation nudge", { turnCount: meta.phaseTurnCount })
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
if (input.agent.name === "build" && meta.phaseTurnCount >= 5) {
|
|
287
|
+
const nudge = `Supervisor Note: You have been in the [BUILD] phase for ${meta.phaseTurnCount} turns. You should proceed immediately to implementation using the 'write' or 'edit' tools to advance the project state.`
|
|
288
|
+
payload.zone1_critical_rules.push(nudge)
|
|
289
|
+
l.info("stagnation nudge", { turnCount: meta.phaseTurnCount, phase: "build" })
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
let activeRulePacks: string[] = ["core_interaction_pack"]
|
|
293
|
+
let thinkingEffort: "high" | "low" = "low"
|
|
294
|
+
|
|
295
|
+
// Phase 1: Intent Classification (Clerk / local-side) - Task 1.1
|
|
296
|
+
// The Clerk detects user intent and shifts the active epochcli Agent.
|
|
297
|
+
if (providerInfo.id === "local-main") {
|
|
298
|
+
try {
|
|
299
|
+
const sideModel = await Provider.getSideModel()
|
|
300
|
+
if (sideModel) {
|
|
301
|
+
const sideLanguage = await Provider.getLanguage(sideModel)
|
|
302
|
+
|
|
303
|
+
// Extract conversation tail for structural context (Task 1.1)
|
|
304
|
+
const tailCount = 10
|
|
305
|
+
const recentMessages = input.messages.slice(-tailCount)
|
|
306
|
+
const conversationTail = recentMessages
|
|
307
|
+
.map((m) => {
|
|
308
|
+
let content = ""
|
|
309
|
+
if (typeof m.content === "string") {
|
|
310
|
+
content = m.content
|
|
311
|
+
} else if (Array.isArray(m.content)) {
|
|
312
|
+
content = m.content
|
|
313
|
+
.map((c) => {
|
|
314
|
+
if (c.type === "text") return c.text
|
|
315
|
+
if (c.type === "tool-call") return `[Tool Call: ${c.toolName}]`
|
|
316
|
+
if (c.type === "tool-result") return `[Tool Result: ${c.toolName}]`
|
|
317
|
+
return `[${c.type}]`
|
|
318
|
+
})
|
|
319
|
+
.join(" ")
|
|
320
|
+
}
|
|
321
|
+
// Truncate individual message content to keep the transcript lean
|
|
322
|
+
const truncated = content.length > 300 ? content.slice(0, 250) + "... [truncated]" : content
|
|
323
|
+
return `${m.role.toUpperCase()}: ${truncated}`
|
|
324
|
+
})
|
|
325
|
+
.join("\n\n")
|
|
326
|
+
|
|
327
|
+
if (conversationTail) {
|
|
328
|
+
const { RuleRouter } = await import("./prompt/router")
|
|
329
|
+
const { Agent } = await import("@/agent/agent")
|
|
330
|
+
|
|
331
|
+
// Debug info for the Clerk Turn
|
|
332
|
+
const sideProvider = await Provider.getProvider(sideModel.providerID)
|
|
333
|
+
l.debug("clerk", {
|
|
334
|
+
message: `Using model: ${sideModel.providerID}/${sideModel.id} at ${sideProvider?.options?.baseURL}`,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
let groundTruths = ""
|
|
338
|
+
let continuityReportContext = ""
|
|
339
|
+
try {
|
|
340
|
+
const fsNode = await import("fs/promises")
|
|
341
|
+
const pathNode = await import("path")
|
|
342
|
+
const rulesPath = pathNode.join(".history", "project_rules.toon")
|
|
343
|
+
const rulesContext = await fsNode.readFile(rulesPath, "utf-8").catch(() => "")
|
|
344
|
+
// Use an empty array for packs here since we just want operational facts for the Clerk
|
|
345
|
+
if (rulesContext) {
|
|
346
|
+
const parsedRules = parseGroundTruthRules(rulesContext, [], sideModel.limit.context)
|
|
347
|
+
if (parsedRules.operationalFacts) groundTruths = parsedRules.operationalFacts
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Load continuity report to help Clerk understand overarching state
|
|
351
|
+
try {
|
|
352
|
+
const continuityContent = await fsNode.readFile(".epoch-continuity.toon", "utf-8")
|
|
353
|
+
// Truncate if it's absurdly large, though toon files should be managed.
|
|
354
|
+
continuityReportContext =
|
|
355
|
+
continuityContent.length > 2000
|
|
356
|
+
? continuityContent.slice(0, 2000) + "... [truncated]"
|
|
357
|
+
: continuityContent
|
|
358
|
+
} catch (e) {
|
|
359
|
+
// File might not exist yet (first epoch), ignore
|
|
360
|
+
}
|
|
361
|
+
} catch (e) {}
|
|
362
|
+
|
|
363
|
+
l.debug("clerk", { message: "Supervising conversation..." })
|
|
364
|
+
|
|
365
|
+
// Task 4.2: Arbitration Mechanism
|
|
366
|
+
let identifiedAgent: string | undefined
|
|
367
|
+
|
|
368
|
+
// Check for initial persona lock (Spec CLI One-Shot)
|
|
369
|
+
const firstUserMsg = input.messages.find((m) => m.role === "user")
|
|
370
|
+
let firstMsgText = ""
|
|
371
|
+
if (typeof firstUserMsg?.content === "string") {
|
|
372
|
+
firstMsgText = firstUserMsg.content
|
|
373
|
+
} else if (Array.isArray(firstUserMsg?.content)) {
|
|
374
|
+
firstMsgText = firstUserMsg.content
|
|
375
|
+
.filter((c) => c.type === "text")
|
|
376
|
+
.map((c) => c.text)
|
|
377
|
+
.join("\n")
|
|
378
|
+
}
|
|
379
|
+
const isOneShot =
|
|
380
|
+
firstMsgText.toLowerCase().includes("one-shot") || firstMsgText.toLowerCase().includes("spec cli")
|
|
381
|
+
|
|
382
|
+
// Check if planning is actually finished
|
|
383
|
+
let planningFinished = false
|
|
384
|
+
try {
|
|
385
|
+
const fsNode = await import("fs/promises")
|
|
386
|
+
const pathNode = await import("path")
|
|
387
|
+
// We don't know the feature name easily here without parsing,
|
|
388
|
+
// but we can look for any .spec-tasks-approved file in projects/active
|
|
389
|
+
const projectDir = "projects/active"
|
|
390
|
+
const entries = await fsNode.readdir(projectDir, { withFileTypes: true })
|
|
391
|
+
for (const entry of entries) {
|
|
392
|
+
if (entry.isDirectory()) {
|
|
393
|
+
const approvedFile = pathNode.join(projectDir, entry.name, ".spec-tasks-approved")
|
|
394
|
+
const exists = await fsNode
|
|
395
|
+
.access(approvedFile)
|
|
396
|
+
.then(() => true)
|
|
397
|
+
.catch(() => false)
|
|
398
|
+
if (exists) {
|
|
399
|
+
planningFinished = true
|
|
400
|
+
break
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
} catch (e) {}
|
|
405
|
+
|
|
406
|
+
// Check for recent objections to the supervisor
|
|
407
|
+
// Give the proxy a moment of silence between the end of the last turn and the start of supervision
|
|
408
|
+
await new Promise((r) => setTimeout(r, 1000))
|
|
409
|
+
|
|
410
|
+
// Concurrently identify agent, rule packs, and thinking effort
|
|
411
|
+
const [identifiedAgentResult, identifiedPacks, identifiedEffort] = await Promise.all([
|
|
412
|
+
(async () => {
|
|
413
|
+
if (isOneShot && !planningFinished) {
|
|
414
|
+
l.debug("clerk", { message: "One-Shot planning in progress. Locking persona to: plan" })
|
|
415
|
+
return "plan"
|
|
416
|
+
} else if (planningFinished) {
|
|
417
|
+
l.debug("clerk", { message: "Planning finished semaphore detected. Deterministic handover to: build" })
|
|
418
|
+
return "build"
|
|
419
|
+
} else {
|
|
420
|
+
return RuleRouter.identifyAgent(conversationTail, input.agent.name, continuityReportContext)
|
|
421
|
+
}
|
|
422
|
+
})(),
|
|
423
|
+
RuleRouter.identifyRulePacks(conversationTail, sideLanguage),
|
|
424
|
+
RuleRouter.identifyThinkingEffort(conversationTail, sideLanguage),
|
|
425
|
+
])
|
|
426
|
+
|
|
427
|
+
identifiedAgent = identifiedAgentResult
|
|
428
|
+
activeRulePacks = identifiedPacks
|
|
429
|
+
thinkingEffort = identifiedEffort
|
|
430
|
+
|
|
431
|
+
l.debug("clerk", { message: `Identified agent: ${identifiedAgent}` })
|
|
432
|
+
l.debug("clerk", { message: `Identified rule packs: ${activeRulePacks.join(", ")}` })
|
|
433
|
+
l.debug("clerk", { message: `Identified thinking effort: ${thinkingEffort}` })
|
|
434
|
+
|
|
435
|
+
if (identifiedAgent !== input.agent.name) {
|
|
436
|
+
log.debug("Clerk identified agent shift", { from: input.agent.name, to: identifiedAgent })
|
|
437
|
+
const newAgent = await Agent.get(identifiedAgent)
|
|
438
|
+
if (newAgent) {
|
|
439
|
+
l.debug("clerk", { message: `SHIFTING agent to: ${identifiedAgent}` })
|
|
440
|
+
input.agent = newAgent
|
|
441
|
+
// Update the directive in Zone 1
|
|
442
|
+
payload.zone1_critical_rules[0] = `Current Phase: [${input.agent.name.toUpperCase()}].`
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
} catch (e) {
|
|
448
|
+
l.error("clerk", { message: "Intent classification failed", error: String(e) })
|
|
449
|
+
log.warn("Clerk intent classification failed", { error: String(e) })
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (providerInfo.id === "local-main") {
|
|
454
|
+
try {
|
|
455
|
+
let rulesContext = ""
|
|
456
|
+
const mcpClientsRecord = await MCP.clients()
|
|
457
|
+
const mcpClients = Object.values(mcpClientsRecord) as any[]
|
|
458
|
+
const gtCli = mcpClients.find((c: any) => c.id === "ground")
|
|
459
|
+
if (gtCli) {
|
|
460
|
+
log.debug("Fetching ground truth rules")
|
|
461
|
+
const gtRes = await gtCli.client.callTool({ name: "gt_status", arguments: {} })
|
|
462
|
+
if (gtRes.content && gtRes.content.length > 0 && gtRes.content[0].type === "text") {
|
|
463
|
+
rulesContext = gtRes.content[0].text
|
|
464
|
+
}
|
|
465
|
+
} else {
|
|
466
|
+
// Fallback rule load
|
|
467
|
+
try {
|
|
468
|
+
const fsNode = await import("fs/promises")
|
|
469
|
+
const pathNode = await import("path")
|
|
470
|
+
rulesContext = await fsNode.readFile(pathNode.join(".history", "project_rules.toon"), "utf-8")
|
|
471
|
+
} catch (e) {
|
|
472
|
+
// ignore missing file
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (rulesContext) {
|
|
477
|
+
const parsedRules = parseGroundTruthRules(rulesContext, activeRulePacks, input.model.limit.context)
|
|
478
|
+
if (parsedRules.operationalFacts) {
|
|
479
|
+
payload.zone1_critical_rules.push(parsedRules.operationalFacts)
|
|
480
|
+
}
|
|
481
|
+
if (parsedRules.behavioralRules) payload.zone2_context_files.push(parsedRules.behavioralRules)
|
|
482
|
+
if (parsedRules.projectSpecific) payload.zone3_active_cursor.push(parsedRules.projectSpecific)
|
|
483
|
+
}
|
|
484
|
+
} catch (e) {
|
|
485
|
+
log.warn("Phase 1 Pre-Generation Rules Context fetch failed", { error: String(e) })
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
payload.zone2_context_files.push(
|
|
490
|
+
[
|
|
491
|
+
// use agent prompt otherwise provider prompt
|
|
492
|
+
...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model, input.isContinue)),
|
|
493
|
+
// any custom prompt passed into this call
|
|
494
|
+
...(input.system?.zone2 ?? []),
|
|
495
|
+
// any custom prompt from last user message
|
|
496
|
+
...(input.user.system ? [input.user.system] : []),
|
|
497
|
+
]
|
|
498
|
+
.filter((x) => x)
|
|
499
|
+
.join("\n\n"),
|
|
500
|
+
)
|
|
501
|
+
|
|
502
|
+
if (input.user.cursorContext) {
|
|
503
|
+
const { file, line, code } = input.user.cursorContext
|
|
504
|
+
payload.zone3_active_cursor.push(
|
|
505
|
+
`Active Cursor Context:\n File: ${file}\n Line ${line}: ${code} # <--- CURSOR HERE`,
|
|
506
|
+
)
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const system: string[] = [PromptBuilder.build(payload)]
|
|
510
|
+
const header = system[0]
|
|
511
|
+
await Plugin.trigger(
|
|
512
|
+
"experimental.chat.system.transform",
|
|
513
|
+
{ sessionID: input.sessionID, model: input.model },
|
|
514
|
+
{ system },
|
|
515
|
+
)
|
|
516
|
+
// rejoin to maintain 2-part structure for caching if header unchanged
|
|
517
|
+
if (system.length > 2 && system[0] === header) {
|
|
518
|
+
const rest = system.slice(1)
|
|
519
|
+
system.length = 0
|
|
520
|
+
system.push(header, rest.join("\n"))
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const isWorkflow = language instanceof GitLabWorkflowLanguageModel
|
|
524
|
+
const isSmallReasoningModel =
|
|
525
|
+
input.model.api?.id?.includes("gemma-4") ||
|
|
526
|
+
input.model.api?.id?.includes("google-gemma-26b") ||
|
|
527
|
+
input.model.api?.id?.toLowerCase().includes("qwen") ||
|
|
528
|
+
input.model.id?.includes("big-pickle")
|
|
529
|
+
const isReasoningModel = input.model.capabilities.reasoning || isSmallReasoningModel
|
|
530
|
+
|
|
531
|
+
const variant =
|
|
532
|
+
!input.small && input.model.variants && input.user.model.variant
|
|
533
|
+
? input.model.variants[input.user.model.variant]
|
|
534
|
+
: {}
|
|
535
|
+
const base = input.small
|
|
536
|
+
? ProviderTransform.smallOptions(input.model)
|
|
537
|
+
: ProviderTransform.options({
|
|
538
|
+
model: input.model,
|
|
539
|
+
sessionID: input.sessionID,
|
|
540
|
+
providerOptions: providerInfo.options,
|
|
541
|
+
thinkingEffort: isReasoningModel ? thinkingEffort : undefined,
|
|
542
|
+
})
|
|
543
|
+
const options: Record<string, any> = pipe(
|
|
544
|
+
base,
|
|
545
|
+
mergeDeep(input.model.options),
|
|
546
|
+
mergeDeep(input.agent.options),
|
|
547
|
+
mergeDeep(variant),
|
|
548
|
+
)
|
|
549
|
+
if (input.operationalFacts && input.operationalFacts.length > 0) {
|
|
550
|
+
options.operationalFacts = input.operationalFacts
|
|
551
|
+
}
|
|
552
|
+
if (isOpenaiOauth) {
|
|
553
|
+
options.instructions = system.join("\n")
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
// Phase 1: Context Fetching skipped (agent uses mcpx tool directly)
|
|
557
|
+
if (false) {
|
|
558
|
+
try {
|
|
559
|
+
let mcpContext = ""
|
|
560
|
+
const mcpxTool = input.tools["mcpx"]
|
|
561
|
+
|
|
562
|
+
let activePath = "."
|
|
563
|
+
|
|
564
|
+
if (mcpxTool) {
|
|
565
|
+
try {
|
|
566
|
+
log.debug("Fetching current state from spec via mcpx")
|
|
567
|
+
const statusRes = await mcpxTool.execute!({ server: "spec", tool: "sc_status", flags: {} }, options as any)
|
|
568
|
+
if (statusRes.content && statusRes.content.length > 0 && statusRes.content[0].type === "text") {
|
|
569
|
+
const text = statusRes.content[0].text
|
|
570
|
+
const featureMatch = text.match(/Feature: (.+)/)
|
|
571
|
+
if (featureMatch) {
|
|
572
|
+
activePath = featureMatch[1].trim()
|
|
573
|
+
}
|
|
574
|
+
mcpContext += `Spec CLI Context:\n${ToonEncoder.encode({ active_feature: activePath, status: text })}\n`
|
|
575
|
+
}
|
|
576
|
+
} catch (e) {
|
|
577
|
+
log.debug("Failed to fetch spec status via mcpx", { error: String(e) })
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
try {
|
|
581
|
+
log.debug("Fetching localized map from map for path via mcpx", { activePath })
|
|
582
|
+
const mapRes = await mcpxTool.execute!(
|
|
583
|
+
{ server: "map", tool: "pm_query", flags: { path: activePath } },
|
|
584
|
+
options as any,
|
|
585
|
+
)
|
|
586
|
+
if (mapRes.content && mapRes.content.length > 0 && mapRes.content[0].type === "text") {
|
|
587
|
+
mcpContext += `Project Map Context:\n${ToonEncoder.encode({ localized_map: mapRes.content[0].text })}\n`
|
|
588
|
+
}
|
|
589
|
+
} catch (e) {
|
|
590
|
+
log.debug("Failed to fetch map localized map via mcpx", { error: String(e) })
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (mcpContext) {
|
|
595
|
+
payload.zone1_critical_rules.push(mcpContext)
|
|
596
|
+
}
|
|
597
|
+
} catch (e) {
|
|
598
|
+
log.warn("Phase 1 Pre-Generation MCP Context fetch failed", { error: String(e) })
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Positional Prompt Architecture: Assemble Zone-based messages
|
|
603
|
+
const initialMessages = PromptBuilder.buildMessages(payload, {
|
|
604
|
+
isSmallReasoningModel,
|
|
605
|
+
thinkingEffort: isReasoningModel ? thinkingEffort : undefined,
|
|
606
|
+
})
|
|
607
|
+
const systemContent = initialMessages
|
|
608
|
+
.filter((m) => m.role === "system")
|
|
609
|
+
.map((m) => m.content)
|
|
610
|
+
.join("\n\n")
|
|
611
|
+
const otherInitial = initialMessages.filter((m) => m.role !== "system")
|
|
612
|
+
|
|
613
|
+
const internalStateCheck = `
|
|
614
|
+
<|channel>thought
|
|
615
|
+
[INTERNAL STATE CHECK]
|
|
616
|
+
- Mode: ${thinkingEffort.toUpperCase()} thinking / Adaptive efficiency active.
|
|
617
|
+
- Role: Assigned to [${input.agent.name.toUpperCase()}].
|
|
618
|
+
- Constraint: ${Math.round(input.model.limit.context / 1000)}K token budget. Concise CoT.
|
|
619
|
+
Ready to process user request strictly under these parameters.
|
|
620
|
+
`.trim()
|
|
621
|
+
|
|
622
|
+
// Truncate older proactive validation errors to prevent streaming loops and context bloat
|
|
623
|
+
let validationErrorCount = 0
|
|
624
|
+
for (let i = input.messages.length - 1; i >= 0; i--) {
|
|
625
|
+
const msg = input.messages[i]
|
|
626
|
+
if (msg.role === "tool" && Array.isArray(msg.content)) {
|
|
627
|
+
for (let j = 0; j < msg.content.length; j++) {
|
|
628
|
+
const part: any = msg.content[j]
|
|
629
|
+
if (
|
|
630
|
+
part.type === "tool-result" &&
|
|
631
|
+
part.isError &&
|
|
632
|
+
typeof part.result === "string" &&
|
|
633
|
+
part.result.includes("INVALID ARGUMENTS:")
|
|
634
|
+
) {
|
|
635
|
+
validationErrorCount++
|
|
636
|
+
if (validationErrorCount > 2) {
|
|
637
|
+
part.result = "INVALID ARGUMENTS: [TRUNCATED - Refer to most recent validation error]"
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
const messages = isOpenaiOauth
|
|
645
|
+
? input.messages
|
|
646
|
+
: isWorkflow
|
|
647
|
+
? input.messages
|
|
648
|
+
: mergeMessages([...otherInitial, ...input.messages, { role: "user" as const, content: internalStateCheck }])
|
|
649
|
+
|
|
650
|
+
const params = await Plugin.trigger(
|
|
651
|
+
"chat.params",
|
|
652
|
+
{
|
|
653
|
+
sessionID: input.sessionID,
|
|
654
|
+
agent: input.agent.name,
|
|
655
|
+
model: input.model,
|
|
656
|
+
provider: providerInfo,
|
|
657
|
+
message: input.user,
|
|
658
|
+
},
|
|
659
|
+
{
|
|
660
|
+
temperature: input.model.capabilities.temperature
|
|
661
|
+
? (input.agent.temperature ?? ProviderTransform.temperature(input.model))
|
|
662
|
+
: undefined,
|
|
663
|
+
topP: input.agent.topP ?? ProviderTransform.topP(input.model),
|
|
664
|
+
topK: ProviderTransform.topK(input.model),
|
|
665
|
+
maxOutputTokens: ProviderTransform.maxOutputTokens(input.model),
|
|
666
|
+
options,
|
|
667
|
+
},
|
|
668
|
+
)
|
|
669
|
+
|
|
670
|
+
const { headers } = await Plugin.trigger(
|
|
671
|
+
"chat.headers",
|
|
672
|
+
{
|
|
673
|
+
sessionID: input.sessionID,
|
|
674
|
+
agent: input.agent.name,
|
|
675
|
+
model: input.model,
|
|
676
|
+
provider: providerInfo,
|
|
677
|
+
message: input.user,
|
|
678
|
+
},
|
|
679
|
+
{
|
|
680
|
+
headers: {},
|
|
681
|
+
},
|
|
682
|
+
)
|
|
683
|
+
|
|
684
|
+
let resolvedTools = resolveTools(input)
|
|
685
|
+
|
|
686
|
+
try {
|
|
687
|
+
const availableSkills = await (await import("../skill")).Skill.available(input.agent)
|
|
688
|
+
if (availableSkills.length === 0) {
|
|
689
|
+
const { skill, ...rest } = resolvedTools as any
|
|
690
|
+
resolvedTools = rest as any
|
|
691
|
+
}
|
|
692
|
+
} catch (e) {}
|
|
693
|
+
|
|
694
|
+
const tools = Record.map(resolvedTools, (toolDef, toolName) => {
|
|
695
|
+
if (!toolDef.execute) return toolDef
|
|
696
|
+
const originalExecute = toolDef.execute
|
|
697
|
+
return {
|
|
698
|
+
...toolDef,
|
|
699
|
+
execute: async (args: any, options: any) => {
|
|
700
|
+
const interception = await interceptToolLoop({
|
|
701
|
+
toolName,
|
|
702
|
+
args,
|
|
703
|
+
messages: options.messages ?? input.messages,
|
|
704
|
+
provider: providerInfo,
|
|
705
|
+
cfg,
|
|
706
|
+
})
|
|
707
|
+
if (interception) return interception
|
|
708
|
+
|
|
709
|
+
let result
|
|
710
|
+
let isError = false
|
|
711
|
+
try {
|
|
712
|
+
result = await originalExecute(args, options)
|
|
713
|
+
} catch (e: any) {
|
|
714
|
+
result = e
|
|
715
|
+
isError = true
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const resultStr =
|
|
719
|
+
typeof result === "string"
|
|
720
|
+
? result
|
|
721
|
+
: result instanceof Error
|
|
722
|
+
? String(result.message || result)
|
|
723
|
+
: JSON.stringify(result)
|
|
724
|
+
|
|
725
|
+
// Task 1.1: Auto-Fallback for sc_guidance prerequisite
|
|
726
|
+
if (
|
|
727
|
+
resultStr.includes("You must run `spec sc_guidance`") ||
|
|
728
|
+
resultStr.includes("You must run \\`spec sc_guidance\\`")
|
|
729
|
+
) {
|
|
730
|
+
log.info("Auto-fallback triggered for missing prerequisite sc_guidance")
|
|
731
|
+
let guidanceOutput = ""
|
|
732
|
+
try {
|
|
733
|
+
const allTools = resolveTools(input)
|
|
734
|
+
const guidanceToolKey = Object.keys(allTools).find((k) => k.includes("sc_guidance"))
|
|
735
|
+
if (guidanceToolKey && allTools[guidanceToolKey] && allTools[guidanceToolKey].execute) {
|
|
736
|
+
const guidanceResult = await allTools[guidanceToolKey].execute!({}, options)
|
|
737
|
+
guidanceOutput = typeof guidanceResult === "string" ? guidanceResult : JSON.stringify(guidanceResult)
|
|
738
|
+
} else if (allTools["mcpx"] && allTools["mcpx"].execute) {
|
|
739
|
+
const guidanceResult = await allTools["mcpx"].execute!(
|
|
740
|
+
{ server: "spec", tool: "sc_guidance", flags: {} },
|
|
741
|
+
options,
|
|
742
|
+
)
|
|
743
|
+
guidanceOutput = typeof guidanceResult === "string" ? guidanceResult : JSON.stringify(guidanceResult)
|
|
744
|
+
}
|
|
745
|
+
} catch (fallbackError) {
|
|
746
|
+
guidanceOutput = "Failed to auto-execute sc_guidance: " + String(fallbackError)
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
const hybridResponse =
|
|
750
|
+
"System overriding sc_approve. Prerequisite missing. Auto-executing sc_guidance. Here is the guidance you must review... Read this, then you may call sc_approve.\n\n" +
|
|
751
|
+
guidanceOutput
|
|
752
|
+
if (isError) {
|
|
753
|
+
return hybridResponse
|
|
754
|
+
} else {
|
|
755
|
+
return hybridResponse
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
// Task 2.1 & 2.2: Clerk Interceptor (Middleware) for generic prerequisite errors
|
|
760
|
+
if (isError && (resultStr.includes("You must run") || resultStr.includes("prerequisite"))) {
|
|
761
|
+
if (providerInfo.id === "local-main") {
|
|
762
|
+
try {
|
|
763
|
+
log.info("Triggering Clerk Interceptor for prerequisite error")
|
|
764
|
+
const sideModel = await Provider.getSideModel()
|
|
765
|
+
if (sideModel) {
|
|
766
|
+
const sideLanguage = await Provider.getLanguage(sideModel)
|
|
767
|
+
|
|
768
|
+
// Build history string
|
|
769
|
+
const tailCount = 5
|
|
770
|
+
const recentMessages = (options.messages ?? input.messages).slice(-tailCount)
|
|
771
|
+
const historyStr = recentMessages
|
|
772
|
+
.map(
|
|
773
|
+
(m: any) =>
|
|
774
|
+
`${m.role}: ${typeof m.content === "string" ? m.content : JSON.stringify(m.content).slice(0, 200)}`,
|
|
775
|
+
)
|
|
776
|
+
.join("\n")
|
|
777
|
+
|
|
778
|
+
const systemPrompt =
|
|
779
|
+
"Look at the last tool error and the chat history. Write a concise, commanding one-sentence instruction telling the main agent exactly which tool to use next to resolve the prerequisite."
|
|
780
|
+
const prompt = `History:\n${historyStr}\n\nTool Error:\n${resultStr}\n\nDirective:`
|
|
781
|
+
|
|
782
|
+
const { generateText } = await import("@/util/ai-sdk")
|
|
783
|
+
const res = await generateText({
|
|
784
|
+
model: sideLanguage,
|
|
785
|
+
system: systemPrompt,
|
|
786
|
+
prompt: prompt,
|
|
787
|
+
abortSignal: AbortSignal.timeout(10000),
|
|
788
|
+
maxRetries: 0,
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
const clerkText = res.text.trim()
|
|
792
|
+
if (clerkText) {
|
|
793
|
+
log.info("Clerk interceptor generated directive", { directive: clerkText })
|
|
794
|
+
throw new Error(`CRITICAL SYSTEM DIRECTIVE: ${clerkText}\n\nOriginal Error:\n${resultStr}`)
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
} catch (clerkErr) {
|
|
798
|
+
log.warn("Clerk interceptor failed", { error: String(clerkErr) })
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (isError) {
|
|
804
|
+
// Task: Reactive Help Injection for mcpx syntax errors
|
|
805
|
+
if (toolName === "mcpx" && args.server && args.tool) {
|
|
806
|
+
try {
|
|
807
|
+
const resultText = typeof result === "string" ? result : (result?.message || JSON.stringify(result))
|
|
808
|
+
if (resultText.includes("Error (Exit") || resultText.includes("missing field") || resultText.includes("invalid type")) {
|
|
809
|
+
log.info("mcpx syntax error detected, fetching help output", { server: args.server, tool: args.tool })
|
|
810
|
+
const allTools = resolveTools(input)
|
|
811
|
+
if (allTools["mcpx"] && allTools["mcpx"].execute) {
|
|
812
|
+
const helpResult = await allTools["mcpx"].execute!({
|
|
813
|
+
server: args.server,
|
|
814
|
+
tool: args.tool,
|
|
815
|
+
args: ["--help"]
|
|
816
|
+
}, options)
|
|
817
|
+
|
|
818
|
+
const helpOutput = typeof helpResult === "string" ? helpResult : (helpResult?.output ?? JSON.stringify(helpResult))
|
|
819
|
+
|
|
820
|
+
let guidance = "The command failed with a syntax error. Review the correct schema below and retry with fixed arguments."
|
|
821
|
+
if (resultText.includes("missing field title") && args.server === "spec") {
|
|
822
|
+
guidance = "The 'spec' tool failed because Tasks.json is invalid. Specifically, it is missing the 'title' field in one or more task objects. Review your Tasks.json file, ensure 'title' is used instead of (or in addition to) 'description', and retry."
|
|
823
|
+
} else if (resultText.includes("missing field id") && args.server === "spec" && args.tool === "sc_todo_start") {
|
|
824
|
+
guidance = "The 'sc_todo_start' tool requires an '--id' flag. Additionally, ensure that the ID you are passing exactly matches the ID in Tasks.json, and that all IDs in Tasks.json follow a numeric-style format (e.g., '1', '1.1')."
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
const augmentedError = `${resultText}\n\n[SYSTEM GUIDANCE: ${guidance}]\n\n${helpOutput}`
|
|
828
|
+
|
|
829
|
+
if (result instanceof Error) {
|
|
830
|
+
result.message = augmentedError
|
|
831
|
+
} else if (typeof result === "object") {
|
|
832
|
+
result.output = augmentedError
|
|
833
|
+
} else {
|
|
834
|
+
result = augmentedError
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
} catch (helpErr) {
|
|
839
|
+
log.warn("Failed to fetch reactive help for mcpx", { error: String(helpErr) })
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
throw result
|
|
843
|
+
}
|
|
844
|
+
return result
|
|
845
|
+
},
|
|
846
|
+
}
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
// LiteLLM and some Anthropic proxies require the tools parameter to be present
|
|
850
|
+
// when message history contains tool calls, even if no tools are being used.
|
|
851
|
+
// Add a dummy tool that is never called to satisfy this validation.
|
|
852
|
+
// This is enabled for:
|
|
853
|
+
// 1. Providers with "litellm" in their ID or API ID (auto-detected)
|
|
854
|
+
// 2. Providers with explicit "litellmProxy: true" option (opt-in for custom gateways)
|
|
855
|
+
const isLiteLLMProxy =
|
|
856
|
+
providerInfo.options?.["litellmProxy"] === true ||
|
|
857
|
+
input.model.providerID.toLowerCase().includes("litellm") ||
|
|
858
|
+
input.model.api.id.toLowerCase().includes("litellm")
|
|
859
|
+
|
|
860
|
+
// LiteLLM/Bedrock rejects requests where the message history contains tool
|
|
861
|
+
// calls but no tools param is present. When there are no active tools (e.g.
|
|
862
|
+
// during compaction), inject a stub tool to satisfy the validation requirement.
|
|
863
|
+
// The stub description explicitly tells the model not to call it.
|
|
864
|
+
if (isLiteLLMProxy && Object.keys(tools).length === 0 && hasToolCalls(input.messages)) {
|
|
865
|
+
tools["_noop"] = tool({
|
|
866
|
+
description: "Do not call this tool. It exists only for API compatibility and must never be invoked.",
|
|
867
|
+
inputSchema: jsonSchema({
|
|
868
|
+
type: "object",
|
|
869
|
+
properties: {
|
|
870
|
+
reason: { type: "string", description: "Unused" },
|
|
871
|
+
},
|
|
872
|
+
}),
|
|
873
|
+
execute: async () => ({ output: "", title: "", metadata: {} }),
|
|
874
|
+
})
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
// Wire up toolExecutor for DWS workflow models so that tool calls
|
|
878
|
+
// from the workflow service are executed via epochcli's tool system
|
|
879
|
+
// and results sent back over the WebSocket.
|
|
880
|
+
if (language instanceof GitLabWorkflowLanguageModel) {
|
|
881
|
+
const workflowModel = language
|
|
882
|
+
workflowModel.systemPrompt = system.join("\n")
|
|
883
|
+
workflowModel.toolExecutor = async (toolName, argsJson, _requestID) => {
|
|
884
|
+
const t = tools[toolName]
|
|
885
|
+
if (!t || !t.execute) {
|
|
886
|
+
return { result: "", error: `Unknown tool: ${toolName}` }
|
|
887
|
+
}
|
|
888
|
+
try {
|
|
889
|
+
const result = await t.execute!(JSON.parse(argsJson), {
|
|
890
|
+
toolCallId: _requestID,
|
|
891
|
+
messages: input.messages,
|
|
892
|
+
abortSignal: input.abort,
|
|
893
|
+
})
|
|
894
|
+
const output = typeof result === "string" ? result : (result?.output ?? JSON.stringify(result))
|
|
895
|
+
return {
|
|
896
|
+
result: output,
|
|
897
|
+
metadata: typeof result === "object" ? result?.metadata : undefined,
|
|
898
|
+
title: typeof result === "object" ? result?.title : undefined,
|
|
899
|
+
}
|
|
900
|
+
} catch (e: any) {
|
|
901
|
+
return { result: "", error: e.message ?? String(e) }
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
if (providerInfo.id.startsWith("local-")) {
|
|
907
|
+
await new Promise((r) => setTimeout(r, 2000))
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
return streamText({
|
|
911
|
+
system: systemContent,
|
|
912
|
+
onFinish(event) {
|
|
913
|
+
// Worker is now handled sequentially in engine.ts
|
|
914
|
+
},
|
|
915
|
+
onError(error) {
|
|
916
|
+
l.error("stream error", {
|
|
917
|
+
error,
|
|
918
|
+
})
|
|
919
|
+
},
|
|
920
|
+
async experimental_repairToolCall(failed) {
|
|
921
|
+
// Phase 2 OutputInterceptor: Catch broken JSON from local-main and use local-side to fix it.
|
|
922
|
+
if (providerInfo.id === "local-main") {
|
|
923
|
+
try {
|
|
924
|
+
const sideProviderConfig = cfg.provider?.["local-side"]
|
|
925
|
+
if (sideProviderConfig) {
|
|
926
|
+
log.debug("Attempting to repair broken JSON tool call with local-side Clerk")
|
|
927
|
+
const sideModel = await Provider.getSideModel()
|
|
928
|
+
if (!sideModel) return failed.toolCall
|
|
929
|
+
const sideLanguage = await Provider.getLanguage(sideModel)
|
|
930
|
+
const repairResponse = await generateText({
|
|
931
|
+
model: sideLanguage,
|
|
932
|
+
system:
|
|
933
|
+
"You are a JSON repair utility. The user will provide a broken JSON tool call. Your ONLY job is to output the repaired, valid JSON object that matches the intended schema. DO NOT output any markdown, explanations, or other text. ONLY the valid JSON object.",
|
|
934
|
+
prompt: `Broken JSON: ${(failed.toolCall as any).args}\n\nError: ${failed.error.message}`,
|
|
935
|
+
abortSignal: AbortSignal.timeout(15000),
|
|
936
|
+
maxRetries: 0,
|
|
937
|
+
})
|
|
938
|
+
try {
|
|
939
|
+
const repairedArgs = JSON.parse(repairResponse.text.trim())
|
|
940
|
+
log.info("Successfully repaired JSON with local-side")
|
|
941
|
+
|
|
942
|
+
const repairEvent: Log.EnhancedModelExecutionEvent = {
|
|
943
|
+
timestamp: Date.now(),
|
|
944
|
+
mainEpochId: input.sessionID,
|
|
945
|
+
event: "END_GENERATE",
|
|
946
|
+
providerId: "local-side",
|
|
947
|
+
phase: "Phase 2",
|
|
948
|
+
metrics: { json_repaired: true },
|
|
949
|
+
}
|
|
950
|
+
l.info("json repair", repairEvent)
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
...failed.toolCall,
|
|
954
|
+
args: repairedArgs,
|
|
955
|
+
}
|
|
956
|
+
} catch (parseErr) {
|
|
957
|
+
log.warn("local-side failed to output valid JSON for repair")
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
} catch (e) {
|
|
961
|
+
log.error("Failed during local-side JSON repair attempt", { error: String(e) })
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
const lower = failed.toolCall.toolName.toLowerCase()
|
|
966
|
+
if (lower !== failed.toolCall.toolName && tools[lower]) {
|
|
967
|
+
l.info("repairing tool call", {
|
|
968
|
+
tool: failed.toolCall.toolName,
|
|
969
|
+
repaired: lower,
|
|
970
|
+
})
|
|
971
|
+
return {
|
|
972
|
+
...failed.toolCall,
|
|
973
|
+
toolName: lower,
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
throw new Error(`Tool call failed: ${failed.error.message}`)
|
|
977
|
+
},
|
|
978
|
+
temperature: params.temperature,
|
|
979
|
+
topP: params.topP,
|
|
980
|
+
topK: params.topK,
|
|
981
|
+
providerOptions: ProviderTransform.providerOptions(input.model, params.options),
|
|
982
|
+
activeTools: Object.keys(tools).filter((x) => x !== "invalid"),
|
|
983
|
+
tools,
|
|
984
|
+
toolChoice: input.toolChoice,
|
|
985
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
986
|
+
abortSignal: input.abort,
|
|
987
|
+
headers: {
|
|
988
|
+
...(input.model.providerID.startsWith("epochcli")
|
|
989
|
+
? {
|
|
990
|
+
"x-epochcli-project": Instance.project.id,
|
|
991
|
+
"x-epochcli-session": input.sessionID,
|
|
992
|
+
"x-epochcli-request": input.user.id,
|
|
993
|
+
"x-epochcli-client": Flag.EPOCHCLI_CLIENT,
|
|
994
|
+
}
|
|
995
|
+
: {
|
|
996
|
+
"x-session-affinity": input.sessionID,
|
|
997
|
+
...(input.parentSessionID ? { "x-parent-session-id": input.parentSessionID } : {}),
|
|
998
|
+
"User-Agent": `epochcli/${Installation.VERSION}`,
|
|
999
|
+
}),
|
|
1000
|
+
...input.model.headers,
|
|
1001
|
+
...headers,
|
|
1002
|
+
},
|
|
1003
|
+
maxRetries: input.retries ?? 0,
|
|
1004
|
+
messages,
|
|
1005
|
+
model: wrapLanguageModel({
|
|
1006
|
+
model: language,
|
|
1007
|
+
middleware: [
|
|
1008
|
+
{
|
|
1009
|
+
specificationVersion: "v3" as const,
|
|
1010
|
+
async transformParams(args) {
|
|
1011
|
+
if (args.type === "stream" || args.type === "generate") {
|
|
1012
|
+
const targetMessages = ProviderTransform.message(
|
|
1013
|
+
args.params.prompt as ModelMessage[],
|
|
1014
|
+
input.model,
|
|
1015
|
+
options,
|
|
1016
|
+
)
|
|
1017
|
+
const targetKey = ProviderTransform.sdkKey(input.model.api.npm) ?? input.model.providerID
|
|
1018
|
+
const isAzure = input.model.api.npm === "@ai-sdk/azure"
|
|
1019
|
+
args.params.prompt = SanitizerMiddleware.stripProviderOptions(targetMessages, targetKey, isAzure)
|
|
1020
|
+
}
|
|
1021
|
+
return args.params
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
// Telemetry middleware
|
|
1025
|
+
{
|
|
1026
|
+
specificationVersion: "v3" as const,
|
|
1027
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
1028
|
+
const startTime = Date.now()
|
|
1029
|
+
const truncatedPayload = Log.truncatePayload(params.prompt)
|
|
1030
|
+
const mainEpochId = input.sessionID
|
|
1031
|
+
const phase = input.model.providerID.includes("local-side") ? "Phase 1/3" : "Phase 2"
|
|
1032
|
+
|
|
1033
|
+
const startEvent: Log.EnhancedModelExecutionEvent = {
|
|
1034
|
+
timestamp: startTime,
|
|
1035
|
+
mainEpochId,
|
|
1036
|
+
event: "START_GENERATE",
|
|
1037
|
+
providerId: input.model.providerID,
|
|
1038
|
+
phase,
|
|
1039
|
+
activeAgent: input.agent.name,
|
|
1040
|
+
contextLimit: input.model.limit.context,
|
|
1041
|
+
toolCount: Object.keys(tools).length,
|
|
1042
|
+
payload: truncatedPayload,
|
|
1043
|
+
tools: Flag.EPOCHCLI_DEBUG_FULL_PROMPT ? tools : undefined,
|
|
1044
|
+
}
|
|
1045
|
+
l.debug("model execution start", startEvent)
|
|
1046
|
+
SessionTelemetry.emitModelEvent(startEvent)
|
|
1047
|
+
|
|
1048
|
+
try {
|
|
1049
|
+
const res = await doGenerate()
|
|
1050
|
+
const endTime = Date.now()
|
|
1051
|
+
|
|
1052
|
+
const endEvent: Log.EnhancedModelExecutionEvent = {
|
|
1053
|
+
timestamp: endTime,
|
|
1054
|
+
mainEpochId,
|
|
1055
|
+
event: "END_GENERATE",
|
|
1056
|
+
providerId: input.model.providerID,
|
|
1057
|
+
phase,
|
|
1058
|
+
activeAgent: input.agent.name,
|
|
1059
|
+
toolCount: Object.keys(tools).length,
|
|
1060
|
+
metrics: {
|
|
1061
|
+
ttftMs: endTime - startTime,
|
|
1062
|
+
promptTokens: (res.usage as any)?.promptTokens,
|
|
1063
|
+
completionTokens: (res.usage as any)?.completionTokens,
|
|
1064
|
+
tps: (res.usage as any)?.completionTokens
|
|
1065
|
+
? (res.usage as any).completionTokens / ((endTime - startTime) / 1000)
|
|
1066
|
+
: undefined,
|
|
1067
|
+
},
|
|
1068
|
+
}
|
|
1069
|
+
SessionTelemetry.emitModelEvent(endEvent)
|
|
1070
|
+
|
|
1071
|
+
l.debug("model execution end", endEvent)
|
|
1072
|
+
return res
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
const errorEvent: Log.EnhancedModelExecutionEvent = {
|
|
1075
|
+
timestamp: Date.now(),
|
|
1076
|
+
mainEpochId,
|
|
1077
|
+
event: "ERROR",
|
|
1078
|
+
providerId: input.model.providerID,
|
|
1079
|
+
phase,
|
|
1080
|
+
payload: { error: String(e) },
|
|
1081
|
+
}
|
|
1082
|
+
l.error("model execution error", errorEvent)
|
|
1083
|
+
throw e
|
|
1084
|
+
}
|
|
1085
|
+
},
|
|
1086
|
+
wrapStream: async ({ doStream, params }) => {
|
|
1087
|
+
const startTime = Date.now()
|
|
1088
|
+
const truncatedPayload = Log.truncatePayload(params.prompt)
|
|
1089
|
+
const mainEpochId = input.sessionID
|
|
1090
|
+
const phase = input.model.providerID.includes("local-side") ? "Phase 1/3" : "Phase 2"
|
|
1091
|
+
|
|
1092
|
+
const startEvent: Log.EnhancedModelExecutionEvent = {
|
|
1093
|
+
timestamp: startTime,
|
|
1094
|
+
mainEpochId,
|
|
1095
|
+
event: "START_GENERATE",
|
|
1096
|
+
providerId: input.model.providerID,
|
|
1097
|
+
phase,
|
|
1098
|
+
activeAgent: input.agent.name,
|
|
1099
|
+
contextLimit: input.model.limit.context,
|
|
1100
|
+
toolCount: Object.keys(tools).length,
|
|
1101
|
+
payload: truncatedPayload,
|
|
1102
|
+
tools: Flag.EPOCHCLI_DEBUG_FULL_PROMPT ? tools : undefined,
|
|
1103
|
+
}
|
|
1104
|
+
l.debug("model execution start", startEvent)
|
|
1105
|
+
SessionTelemetry.emitModelEvent(startEvent)
|
|
1106
|
+
|
|
1107
|
+
try {
|
|
1108
|
+
const { stream, ...rest } = await doStream()
|
|
1109
|
+
let firstTokenTime: number | undefined
|
|
1110
|
+
let tokenCount = 0
|
|
1111
|
+
const monitor = new StreamingMonitor()
|
|
1112
|
+
|
|
1113
|
+
const iterator = (async function* () {
|
|
1114
|
+
for await (const chunk of stream as any) {
|
|
1115
|
+
const textDelta = chunk.textDelta ?? chunk.delta
|
|
1116
|
+
if (!firstTokenTime && chunk.type === "text-delta" && textDelta) {
|
|
1117
|
+
firstTokenTime = Date.now()
|
|
1118
|
+
}
|
|
1119
|
+
if (chunk.type === "text-delta" || chunk.type === "tool-call-delta") {
|
|
1120
|
+
tokenCount++
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
if (chunk.type === "text-delta" && typeof textDelta === "string") {
|
|
1124
|
+
if (monitor.push(textDelta)) {
|
|
1125
|
+
const offendingText = monitor.getOffendingText()
|
|
1126
|
+
l.warn("Streaming loop detected, aborting...", { offendingText })
|
|
1127
|
+
|
|
1128
|
+
// Signal abortion to the provider if possible (though we only have the signal)
|
|
1129
|
+
try {
|
|
1130
|
+
;(input.abort as any).dispatchEvent?.(new Event("abort"))
|
|
1131
|
+
} catch (e) {}
|
|
1132
|
+
|
|
1133
|
+
yield {
|
|
1134
|
+
type: "text-delta",
|
|
1135
|
+
textDelta: `\n\n[SYSTEM INTERVENTION: Thinking loop detected. Offending sequence: "${offendingText}". Generation aborted.]`,
|
|
1136
|
+
delta: `\n\n[SYSTEM INTERVENTION: Thinking loop detected. Offending sequence: "${offendingText}". Generation aborted.]`,
|
|
1137
|
+
} as any
|
|
1138
|
+
return
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
yield chunk
|
|
1143
|
+
}
|
|
1144
|
+
})()
|
|
1145
|
+
|
|
1146
|
+
const readableStream = new ReadableStream({
|
|
1147
|
+
async pull(controller) {
|
|
1148
|
+
try {
|
|
1149
|
+
const { value, done } = await iterator.next()
|
|
1150
|
+
if (done) {
|
|
1151
|
+
const endTime = Date.now()
|
|
1152
|
+
const endEvent: Log.EnhancedModelExecutionEvent = {
|
|
1153
|
+
timestamp: endTime,
|
|
1154
|
+
mainEpochId,
|
|
1155
|
+
event: "END_GENERATE",
|
|
1156
|
+
providerId: input.model.providerID,
|
|
1157
|
+
phase,
|
|
1158
|
+
activeAgent: input.agent.name,
|
|
1159
|
+
toolCount: Object.keys(tools).length,
|
|
1160
|
+
metrics: {
|
|
1161
|
+
ttftMs: firstTokenTime ? firstTokenTime - startTime : undefined,
|
|
1162
|
+
tps:
|
|
1163
|
+
tokenCount && firstTokenTime
|
|
1164
|
+
? tokenCount / ((endTime - firstTokenTime) / 1000)
|
|
1165
|
+
: undefined,
|
|
1166
|
+
promptTokens: (await (rest as any).usage)?.promptTokens,
|
|
1167
|
+
completionTokens: (await (rest as any).usage)?.completionTokens,
|
|
1168
|
+
loop_detected: monitor.getOffendingText() !== undefined,
|
|
1169
|
+
},
|
|
1170
|
+
}
|
|
1171
|
+
l.debug("model execution end", endEvent)
|
|
1172
|
+
controller.close()
|
|
1173
|
+
} else {
|
|
1174
|
+
controller.enqueue(value)
|
|
1175
|
+
}
|
|
1176
|
+
} catch (e) {
|
|
1177
|
+
const errorEvent: Log.EnhancedModelExecutionEvent = {
|
|
1178
|
+
timestamp: Date.now(),
|
|
1179
|
+
mainEpochId,
|
|
1180
|
+
event: "ERROR",
|
|
1181
|
+
providerId: input.model.providerID,
|
|
1182
|
+
phase,
|
|
1183
|
+
payload: { error: String(e) },
|
|
1184
|
+
}
|
|
1185
|
+
l.error("model execution error", errorEvent)
|
|
1186
|
+
controller.error(e)
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
cancel(reason) {
|
|
1190
|
+
iterator.return?.(reason)
|
|
1191
|
+
},
|
|
1192
|
+
})
|
|
1193
|
+
|
|
1194
|
+
return { stream: readableStream, ...rest }
|
|
1195
|
+
} catch (e) {
|
|
1196
|
+
const errorEvent: Log.EnhancedModelExecutionEvent = {
|
|
1197
|
+
timestamp: Date.now(),
|
|
1198
|
+
mainEpochId,
|
|
1199
|
+
event: "ERROR",
|
|
1200
|
+
providerId: input.model.providerID,
|
|
1201
|
+
phase,
|
|
1202
|
+
payload: { error: String(e) },
|
|
1203
|
+
}
|
|
1204
|
+
l.error("model execution error", errorEvent)
|
|
1205
|
+
throw e
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
// NativeTokenParser middleware
|
|
1210
|
+
{
|
|
1211
|
+
specificationVersion: "v3" as const,
|
|
1212
|
+
wrapGenerate: async ({ doGenerate, params }) => {
|
|
1213
|
+
const res = await doGenerate()
|
|
1214
|
+
// Replace <|"> with markdown backticks
|
|
1215
|
+
;(res as any).text = (res as any).text?.replace(/<\|">/g, "```")
|
|
1216
|
+
return res
|
|
1217
|
+
},
|
|
1218
|
+
wrapStream: async ({ doStream, params }) => {
|
|
1219
|
+
const { stream, ...rest } = await doStream()
|
|
1220
|
+
|
|
1221
|
+
const iterator = (async function* () {
|
|
1222
|
+
for await (const chunk of stream as any) {
|
|
1223
|
+
if (chunk.type === "text-delta") {
|
|
1224
|
+
if (typeof chunk.textDelta === "string") {
|
|
1225
|
+
chunk.textDelta = chunk.textDelta.replace(/<\|">/g, "```")
|
|
1226
|
+
}
|
|
1227
|
+
if (typeof chunk.delta === "string") {
|
|
1228
|
+
chunk.delta = chunk.delta.replace(/<\|">/g, "```")
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
yield chunk
|
|
1232
|
+
}
|
|
1233
|
+
})()
|
|
1234
|
+
|
|
1235
|
+
const readableStream = new ReadableStream({
|
|
1236
|
+
async pull(controller) {
|
|
1237
|
+
const { value, done } = await iterator.next()
|
|
1238
|
+
if (done) {
|
|
1239
|
+
controller.close()
|
|
1240
|
+
} else {
|
|
1241
|
+
controller.enqueue(value)
|
|
1242
|
+
}
|
|
1243
|
+
},
|
|
1244
|
+
cancel(reason) {
|
|
1245
|
+
iterator.return?.(reason)
|
|
1246
|
+
},
|
|
1247
|
+
})
|
|
1248
|
+
|
|
1249
|
+
return { stream: readableStream, ...rest }
|
|
1250
|
+
},
|
|
1251
|
+
},
|
|
1252
|
+
],
|
|
1253
|
+
}),
|
|
1254
|
+
experimental_telemetry: {
|
|
1255
|
+
isEnabled: cfg.experimental?.openTelemetry,
|
|
1256
|
+
metadata: {
|
|
1257
|
+
userId: cfg.username ?? "unknown",
|
|
1258
|
+
sessionId: input.sessionID,
|
|
1259
|
+
},
|
|
1260
|
+
},
|
|
1261
|
+
})
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
function resolveTools(input: Pick<StreamInput, "tools" | "agent" | "permission" | "user">) {
|
|
1265
|
+
const disabled = Permission.disabled(
|
|
1266
|
+
Object.keys(input.tools),
|
|
1267
|
+
Permission.merge(input.agent.permission, input.permission ?? []),
|
|
1268
|
+
)
|
|
1269
|
+
return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
function mergeMessages(messages: ModelMessage[]): ModelMessage[] {
|
|
1273
|
+
const result: ModelMessage[] = []
|
|
1274
|
+
for (const msg of messages) {
|
|
1275
|
+
const last = result[result.length - 1]
|
|
1276
|
+
if (last && last.role === msg.role) {
|
|
1277
|
+
if (typeof last.content === "string" && typeof msg.content === "string") {
|
|
1278
|
+
last.content += "\n\n" + msg.content
|
|
1279
|
+
continue
|
|
1280
|
+
}
|
|
1281
|
+
if (Array.isArray(last.content) && Array.isArray(msg.content)) {
|
|
1282
|
+
;(last.content as any[]).push(...msg.content)
|
|
1283
|
+
continue
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
result.push(msg)
|
|
1287
|
+
}
|
|
1288
|
+
return result
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// Check if messages contain any tool-call content
|
|
1292
|
+
// Used to determine if a dummy tool should be added for LiteLLM proxy compatibility
|
|
1293
|
+
export function hasToolCalls(messages: ModelMessage[]): boolean {
|
|
1294
|
+
for (const msg of messages) {
|
|
1295
|
+
if (!Array.isArray(msg.content)) continue
|
|
1296
|
+
for (const part of msg.content) {
|
|
1297
|
+
if (part.type === "tool-call" || part.type === "tool-result") return true
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
return false
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
/**
|
|
1304
|
+
* Normalizes mcpx tool arguments (pos/flags) into a single object for validation.
|
|
1305
|
+
*/
|
|
1306
|
+
export function normalizeMcpxArguments(args: any): Record<string, any> {
|
|
1307
|
+
const normalized: Record<string, any> = {}
|
|
1308
|
+
|
|
1309
|
+
// Extract from flags
|
|
1310
|
+
if (args.flags && typeof args.flags === "object") {
|
|
1311
|
+
Object.assign(normalized, args.flags)
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// Extract from args (heuristic for common patterns)
|
|
1315
|
+
if (Array.isArray(args.args)) {
|
|
1316
|
+
for (let i = 0; i < args.args.length; i++) {
|
|
1317
|
+
const arg = args.args[i]
|
|
1318
|
+
if (typeof arg === "string" && arg.startsWith("--")) {
|
|
1319
|
+
const parts = arg.slice(2).split("=")
|
|
1320
|
+
const key = parts[0]
|
|
1321
|
+
const val = parts.length > 1 ? parts[1] : args.args[i + 1]
|
|
1322
|
+
if (key) {
|
|
1323
|
+
normalized[key] = val
|
|
1324
|
+
if (parts.length === 1) i++ // skip next since it was used as value
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
return normalized
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
/**
|
|
1334
|
+
* Performs a proactive validation turn with the Clerk.
|
|
1335
|
+
*/
|
|
1336
|
+
async function validateArgumentsProactively(input: {
|
|
1337
|
+
toolName: string
|
|
1338
|
+
args: any
|
|
1339
|
+
schema: any
|
|
1340
|
+
provider: any
|
|
1341
|
+
cfg: Config.Info
|
|
1342
|
+
isMcpx?: boolean
|
|
1343
|
+
mcpxServer?: string
|
|
1344
|
+
l: any
|
|
1345
|
+
}): Promise<string | null> {
|
|
1346
|
+
try {
|
|
1347
|
+
if (input.schema) {
|
|
1348
|
+
// Attempt strict deterministic validation first to avoid LLM hallucinations
|
|
1349
|
+
try {
|
|
1350
|
+
const validate = ajv.compile(input.schema)
|
|
1351
|
+
// For MCPX wrappers, the actual arguments to validate are nested
|
|
1352
|
+
// The interceptToolLoop already attempts to unwrap them into validationArgs,
|
|
1353
|
+
// but we ensure we are validating the correct payload against the sub-tool schema.
|
|
1354
|
+
const payloadToValidate = input.isMcpx ? normalizeMcpxArguments(input.args) : input.args
|
|
1355
|
+
|
|
1356
|
+
if (validate(payloadToValidate)) {
|
|
1357
|
+
input.l.info("Deterministic schema validation passed", { tool: input.toolName })
|
|
1358
|
+
return null // Valid, bypass side-model
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
input.l.info("Deterministic schema validation failed, falling back to side-model for explanation", {
|
|
1362
|
+
tool: input.toolName,
|
|
1363
|
+
errors: validate.errors
|
|
1364
|
+
})
|
|
1365
|
+
} catch (e) {
|
|
1366
|
+
input.l.warn("Failed to compile schema for deterministic validation", { error: String(e) })
|
|
1367
|
+
// Fall through to side-model if we can't compile
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
|
|
1371
|
+
const sideModel = await Provider.getSideModel()
|
|
1372
|
+
if (!sideModel) return null
|
|
1373
|
+
const sideLanguage = await Provider.getLanguage(sideModel)
|
|
1374
|
+
|
|
1375
|
+
const systemPrompt = input.isMcpx
|
|
1376
|
+
? `You are a tool argument validator. The user is attempting to call a sub-tool via the 'mcpx' tool wrapper. Compare the proposed SUB-TOOL arguments with the provided JSON schema.
|
|
1377
|
+
|
|
1378
|
+
CRITICAL CONSTRAINTS:
|
|
1379
|
+
1. You MUST ONLY validate against the properties explicitly defined in the provided JSON schema.
|
|
1380
|
+
2. DO NOT invent new required properties or 'extra' fields. If a property is NOT in the schema, it is NOT required.
|
|
1381
|
+
3. If the proposed arguments satisfy the schema's 'required' array and property types, you MUST output 'VALID'.
|
|
1382
|
+
4. If they are invalid, output 'INVALID: <concise_reason> EXAMPLE: <strict_valid_json_example>'. You MUST explicitly explain what was missing or incorrect based SOLELY on the schema.
|
|
1383
|
+
|
|
1384
|
+
TECHNICAL FORMATTING for 'mcpx' wrapper:
|
|
1385
|
+
- Positional arguments (like command names or paths) MUST be passed in the 'args' array of strings.
|
|
1386
|
+
- Named flags (like --path) should be in the 'flags' record.
|
|
1387
|
+
- EXAMPLE for 'spec sc_status': \`{ "server": "spec", "tool": "sc_status", "args": [] }\`
|
|
1388
|
+
- Your output example MUST be a JSON object containing "server", "tool", and "args" (and/or "flags").`
|
|
1389
|
+
: `You are a tool argument validator. Compare the proposed arguments with the provided JSON schema.
|
|
1390
|
+
|
|
1391
|
+
CRITICAL CONSTRAINTS:
|
|
1392
|
+
1. You MUST ONLY validate against the properties explicitly defined in the provided JSON schema.
|
|
1393
|
+
2. DO NOT invent new required properties, 'extra' fields, or metadata requirements. If a property is NOT in the schema, it is NOT required.
|
|
1394
|
+
3. If the proposed arguments satisfy the schema's 'required' array and property types, you MUST output 'VALID'.
|
|
1395
|
+
4. If they are invalid, output 'INVALID: <concise_reason> EXAMPLE: <strict_valid_json_example>'. You MUST explicitly explain what was missing or incorrect based SOLELY on the schema, and provide a strict, concrete JSON example of what the valid arguments should look like according to that schema.`
|
|
1396
|
+
|
|
1397
|
+
const prompt = `Tool: ${input.toolName}\nProposed Args: ${JSON.stringify(input.args)}\nSchema: ${JSON.stringify(input.schema)}`
|
|
1398
|
+
|
|
1399
|
+
const res = await generateText({
|
|
1400
|
+
model: sideLanguage,
|
|
1401
|
+
system: systemPrompt,
|
|
1402
|
+
prompt: `${prompt}\n\nValidation Result:`,
|
|
1403
|
+
abortSignal: AbortSignal.timeout(10000),
|
|
1404
|
+
maxRetries: 0,
|
|
1405
|
+
})
|
|
1406
|
+
|
|
1407
|
+
const result = res.text.trim()
|
|
1408
|
+
if (result.startsWith("INVALID")) {
|
|
1409
|
+
return result.replace(/^INVALID:\s*/, "")
|
|
1410
|
+
}
|
|
1411
|
+
return null
|
|
1412
|
+
} catch (e) {
|
|
1413
|
+
input.l.warn("Proactive validation failed", { error: String(e) })
|
|
1414
|
+
return null // Graceful fall-through
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
export async function interceptToolLoop(input: {
|
|
1419
|
+
toolName: string
|
|
1420
|
+
args: any
|
|
1421
|
+
messages: ModelMessage[]
|
|
1422
|
+
provider: any
|
|
1423
|
+
cfg: Config.Info
|
|
1424
|
+
}) {
|
|
1425
|
+
const l = log.clone()
|
|
1426
|
+
l.error(`!!! DEBUG: interceptToolLoop called for ${input.toolName}. History length: ${input.messages.length}`)
|
|
1427
|
+
|
|
1428
|
+
// Task: Proactive Validation (Clerk / local-side)
|
|
1429
|
+
if (input.provider.id === "local-main") {
|
|
1430
|
+
try {
|
|
1431
|
+
let schemaToolName = input.toolName
|
|
1432
|
+
let validationArgs = input.args
|
|
1433
|
+
|
|
1434
|
+
let schema: any = undefined
|
|
1435
|
+
|
|
1436
|
+
let isMcpx = false
|
|
1437
|
+
let mcpxServer = ""
|
|
1438
|
+
|
|
1439
|
+
// Specialized handling for mcpx sub-tools
|
|
1440
|
+
if (input.toolName === "mcpx" && input.args.server && input.args.tool) {
|
|
1441
|
+
isMcpx = true
|
|
1442
|
+
mcpxServer = input.args.server
|
|
1443
|
+
schemaToolName = input.args.tool
|
|
1444
|
+
validationArgs = normalizeMcpxArguments(input.args)
|
|
1445
|
+
schema = await SchemaContextLoader.getMcpxToolSchema(
|
|
1446
|
+
input.args.server,
|
|
1447
|
+
input.args.tool,
|
|
1448
|
+
MCP.resolveMcpxBinary(input.cfg),
|
|
1449
|
+
)
|
|
1450
|
+
} else {
|
|
1451
|
+
schema = await SchemaContextLoader.getToolSchema(schemaToolName)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
if (schema) {
|
|
1455
|
+
log.info("Performing proactive validation", { tool: schemaToolName })
|
|
1456
|
+
const hint = await validateArgumentsProactively({
|
|
1457
|
+
toolName: schemaToolName,
|
|
1458
|
+
args: validationArgs,
|
|
1459
|
+
schema,
|
|
1460
|
+
provider: input.provider,
|
|
1461
|
+
cfg: input.cfg,
|
|
1462
|
+
isMcpx,
|
|
1463
|
+
mcpxServer,
|
|
1464
|
+
l,
|
|
1465
|
+
})
|
|
1466
|
+
|
|
1467
|
+
if (hint) {
|
|
1468
|
+
log.info("Proactive validation caught error", { tool: schemaToolName, hint })
|
|
1469
|
+
return {
|
|
1470
|
+
error: `INVALID ARGUMENTS: ${hint}`,
|
|
1471
|
+
output: "",
|
|
1472
|
+
title: "Argument Validation",
|
|
1473
|
+
metadata: { schema_validated: true, proactive: true },
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
} catch (e) {
|
|
1478
|
+
l.warn("Proactive validation turn failed", { error: String(e) })
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
let identicalCount = 0
|
|
1483
|
+
let globalSequentialFailureCount = 0
|
|
1484
|
+
let identicalChainActive = true
|
|
1485
|
+
let failureChainActive = true
|
|
1486
|
+
|
|
1487
|
+
// For constructing the timeline prompt
|
|
1488
|
+
const actionTimeline: string[] = []
|
|
1489
|
+
|
|
1490
|
+
// Scan backwards through messages
|
|
1491
|
+
for (let i = input.messages.length - 1; i >= 0; i--) {
|
|
1492
|
+
if (!identicalChainActive && !failureChainActive && actionTimeline.length >= 8) break
|
|
1493
|
+
|
|
1494
|
+
const msg = input.messages[i]
|
|
1495
|
+
|
|
1496
|
+
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
1497
|
+
for (const part of msg.content) {
|
|
1498
|
+
if (part.type === "tool-call") {
|
|
1499
|
+
const tName = (part as any).toolName || (part as any).name
|
|
1500
|
+
const tArgs = (part as any).args
|
|
1501
|
+
const tId = (part as any).toolCallId || (part as any).id
|
|
1502
|
+
|
|
1503
|
+
let callResult = "unknown"
|
|
1504
|
+
let callOutput = ""
|
|
1505
|
+
let isErrorResult = false
|
|
1506
|
+
|
|
1507
|
+
// Find corresponding tool result
|
|
1508
|
+
const nextMsg = input.messages[i + 1]
|
|
1509
|
+
if (nextMsg && nextMsg.role === "user" && Array.isArray(nextMsg.content)) {
|
|
1510
|
+
const result = (nextMsg.content as any[]).find((c) => c.type === "tool-result" && c.toolCallId === tId)
|
|
1511
|
+
if (result) {
|
|
1512
|
+
isErrorResult = !!(result as any).isError
|
|
1513
|
+
callResult = isErrorResult ? "error" : "completed"
|
|
1514
|
+
|
|
1515
|
+
const outputVal = (result as any).output?.value || (result as any).output || ""
|
|
1516
|
+
callOutput = typeof outputVal === "string" ? outputVal : JSON.stringify(outputVal)
|
|
1517
|
+
|
|
1518
|
+
// Trim output for the timeline to prevent massive prompts
|
|
1519
|
+
if (callOutput.length > 500) {
|
|
1520
|
+
callOutput = callOutput.substring(0, 500) + "... [truncated]"
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
// 1. Build Action Timeline (limit to 8)
|
|
1526
|
+
if (actionTimeline.length < 8) {
|
|
1527
|
+
actionTimeline.unshift(
|
|
1528
|
+
`Tool '${tName}' executed (input: ${JSON.stringify(tArgs)}) -> Result: ${callResult} -> Output/Error: ${callOutput}`,
|
|
1529
|
+
)
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// 2. Global Sequential Failures
|
|
1533
|
+
if (failureChainActive) {
|
|
1534
|
+
if (isErrorResult) {
|
|
1535
|
+
globalSequentialFailureCount++
|
|
1536
|
+
} else {
|
|
1537
|
+
// Any successful tool call breaks the global failure chain
|
|
1538
|
+
failureChainActive = false
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
// 3. Identical arguments chain (only for the currently invoked tool)
|
|
1543
|
+
if (tName === input.toolName) {
|
|
1544
|
+
if (identicalChainActive) {
|
|
1545
|
+
if (JSON.stringify(tArgs) === JSON.stringify(input.args)) {
|
|
1546
|
+
identicalCount++
|
|
1547
|
+
} else {
|
|
1548
|
+
identicalChainActive = false
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
} else if (msg.role === "user" && typeof msg.content === "string") {
|
|
1555
|
+
// Ignore internal state checks
|
|
1556
|
+
if (!msg.content.includes("[INTERNAL STATE CHECK]")) {
|
|
1557
|
+
// User interrupted or added new text
|
|
1558
|
+
identicalChainActive = false
|
|
1559
|
+
failureChainActive = false
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
|
|
1564
|
+
const isIdenticalLoop = identicalCount >= 1
|
|
1565
|
+
const isFailureLoop = globalSequentialFailureCount >= 3
|
|
1566
|
+
|
|
1567
|
+
// Schema-Aware Error Recovery (Task 2.2)
|
|
1568
|
+
if (globalSequentialFailureCount >= 2 && !isIdenticalLoop && !isFailureLoop) {
|
|
1569
|
+
try {
|
|
1570
|
+
const schema = await SchemaContextLoader.getToolSchema(input.toolName)
|
|
1571
|
+
if (schema && input.provider.id === "local-main") {
|
|
1572
|
+
log.debug("Generating schema-aware correction hint with local-side Clerk")
|
|
1573
|
+
const sideModel = await Provider.getSideModel()
|
|
1574
|
+
if (!sideModel) return null
|
|
1575
|
+
const sideLanguage = await Provider.getLanguage(sideModel)
|
|
1576
|
+
|
|
1577
|
+
const systemPrompt =
|
|
1578
|
+
"You are a tool argument validator. Compare the failed arguments with the provided JSON schema. Identify the mistake and provide a concise, helpful correction hint. Do not be verbose. Example: 'You are using --path, but sc_guidance accepts no arguments. Try calling it without flags.'"
|
|
1579
|
+
|
|
1580
|
+
const prompt = `Tool: ${input.toolName}\nFailed Args: ${JSON.stringify(input.args)}\nSchema: ${JSON.stringify(schema)}`
|
|
1581
|
+
|
|
1582
|
+
const hint = await generateText({
|
|
1583
|
+
model: sideLanguage,
|
|
1584
|
+
system: systemPrompt,
|
|
1585
|
+
prompt: `${prompt}\n\nCorrection Hint:`,
|
|
1586
|
+
abortSignal: AbortSignal.timeout(10000),
|
|
1587
|
+
maxRetries: 0,
|
|
1588
|
+
})
|
|
1589
|
+
|
|
1590
|
+
return {
|
|
1591
|
+
error: `INVALID ARGUMENTS: ${hint.text}`,
|
|
1592
|
+
output: "",
|
|
1593
|
+
title: "Argument Validation",
|
|
1594
|
+
metadata: { schema_validated: true },
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
} catch (e) {
|
|
1598
|
+
log.warn("Failed to generate schema-aware hint", { error: String(e) })
|
|
1599
|
+
}
|
|
1600
|
+
}
|
|
1601
|
+
|
|
1602
|
+
if (isIdenticalLoop || isFailureLoop) {
|
|
1603
|
+
const loopType = isIdenticalLoop ? "IDENTICAL_ARGS" : "SEQUENTIAL_FAILURES"
|
|
1604
|
+
log.warn(`Loop detected for tool ${input.toolName}`, {
|
|
1605
|
+
toolName: input.toolName,
|
|
1606
|
+
args: input.args,
|
|
1607
|
+
loopType,
|
|
1608
|
+
globalSequentialFailureCount,
|
|
1609
|
+
})
|
|
1610
|
+
|
|
1611
|
+
// Use local-side to generate an intervention
|
|
1612
|
+
if (input.provider.id === "local-main") {
|
|
1613
|
+
try {
|
|
1614
|
+
const sideProviderConfig = input.cfg.provider?.["local-side"]
|
|
1615
|
+
if (sideProviderConfig) {
|
|
1616
|
+
log.debug("Generating intervention directive with local-side Clerk")
|
|
1617
|
+
const sideModel = await Provider.getSideModel()
|
|
1618
|
+
if (!sideModel) return null
|
|
1619
|
+
const sideLanguage = await Provider.getLanguage(sideModel)
|
|
1620
|
+
|
|
1621
|
+
let continuityContext = ""
|
|
1622
|
+
for (let i = 0; i < input.messages.length; i++) {
|
|
1623
|
+
const m = input.messages[i]
|
|
1624
|
+
if (
|
|
1625
|
+
m.role === "user" &&
|
|
1626
|
+
typeof m.content === "string" &&
|
|
1627
|
+
m.content.includes("Hi, we are continuing a project as the context window ran out")
|
|
1628
|
+
) {
|
|
1629
|
+
continuityContext = `\n\nContext (Epoch Continuity Report):\n${m.content}\n`
|
|
1630
|
+
break
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
const systemPrompt =
|
|
1635
|
+
"You are an AI supervisor monitoring a main agent. The main agent is stuck in a doom loop or has stagnated. Analyze the recent failed attempts and the provided Action Timeline. Provide a concise, stern directive telling the agent to STOP calling this tool or repeating this behavior. Explain why its current approach is failing based on the error messages in the timeline, and suggest a specific actionable alternative strategy (like editing a file or using a different tool). Do not output anything other than the directive."
|
|
1636
|
+
|
|
1637
|
+
const timelineStr = actionTimeline.map((a, idx) => `Turn ${idx + 1}: ${a}`).join("\n")
|
|
1638
|
+
|
|
1639
|
+
const prompt = isIdenticalLoop
|
|
1640
|
+
? `Tool: ${input.toolName}\nArgs: ${JSON.stringify(input.args)}\nStatus: Stuck in an infinite loop with identical arguments.${continuityContext}\n\nRecent Action Timeline:\n${timelineStr}`
|
|
1641
|
+
: `Tool: ${input.toolName}\nStatus: Stuck in a trial-and-error loop where all recent attempts have failed.${continuityContext}\n\nRecent Action Timeline:\n${timelineStr}`
|
|
1642
|
+
|
|
1643
|
+
const intervention = await generateText({
|
|
1644
|
+
model: sideLanguage,
|
|
1645
|
+
system: systemPrompt,
|
|
1646
|
+
prompt: `${prompt}\n\nPlease provide the intervention directive:`,
|
|
1647
|
+
abortSignal: AbortSignal.timeout(15000),
|
|
1648
|
+
maxRetries: 0,
|
|
1649
|
+
})
|
|
1650
|
+
|
|
1651
|
+
const interventionEvent: Log.EnhancedModelExecutionEvent = {
|
|
1652
|
+
timestamp: Date.now(),
|
|
1653
|
+
mainEpochId: "test", // placeholder, will be real in stream()
|
|
1654
|
+
event: "END_GENERATE",
|
|
1655
|
+
providerId: "local-side",
|
|
1656
|
+
phase: "Phase 2",
|
|
1657
|
+
metrics: { loop_detected: true, loop_type: loopType },
|
|
1658
|
+
}
|
|
1659
|
+
log.info("intervention", interventionEvent)
|
|
1660
|
+
|
|
1661
|
+
return { error: `SYSTEM INTERVENTION: ${intervention.text}`, output: "", title: "", metadata: {} }
|
|
1662
|
+
}
|
|
1663
|
+
} catch (e) {
|
|
1664
|
+
log.error("Failed to generate intervention with local-side", { error: String(e) })
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
return {
|
|
1668
|
+
error: `SYSTEM INTERVENTION: You are stuck in a ${isIdenticalLoop ? "loop calling this tool with the exact same arguments" : "repeated failure loop with this tool"}. Stop and reconsider your approach.`,
|
|
1669
|
+
output: "",
|
|
1670
|
+
title: "",
|
|
1671
|
+
metadata: {},
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
return null
|
|
1675
|
+
}
|
|
1676
|
+
}
|