@vascend/talos 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +202 -0
- package/dist/backend/asset-events.d.ts +31 -0
- package/dist/backend/asset-events.js +149 -0
- package/dist/backend/assets.d.ts +31 -0
- package/dist/backend/assets.js +210 -0
- package/dist/backend/auth.d.ts +18 -0
- package/dist/backend/auth.js +59 -0
- package/dist/backend/config.d.ts +14 -0
- package/dist/backend/config.js +41 -0
- package/dist/backend/conversations.d.ts +21 -0
- package/dist/backend/conversations.js +141 -0
- package/dist/backend/crypto.d.ts +9 -0
- package/dist/backend/crypto.js +68 -0
- package/dist/backend/engine.d.ts +13 -0
- package/dist/backend/engine.js +78 -0
- package/dist/backend/http.d.ts +25 -0
- package/dist/backend/http.js +112 -0
- package/dist/backend/index.d.ts +1 -0
- package/dist/backend/index.js +24 -0
- package/dist/backend/routes.d.ts +14 -0
- package/dist/backend/routes.js +169 -0
- package/dist/backend/sdk.d.ts +12 -0
- package/dist/backend/sdk.js +11 -0
- package/dist/backend/server.d.ts +12 -0
- package/dist/backend/server.js +91 -0
- package/dist/backend/temp-image-store.d.ts +46 -0
- package/dist/backend/temp-image-store.js +197 -0
- package/dist/backend/types.d.ts +103 -0
- package/dist/backend/types.js +29 -0
- package/dist/backend/users.d.ts +13 -0
- package/dist/backend/users.js +76 -0
- package/dist/backend/ws-handler.d.ts +14 -0
- package/dist/backend/ws-handler.js +72 -0
- package/dist/backend/ws.d.ts +19 -0
- package/dist/backend/ws.js +146 -0
- package/dist/core/agent.d.ts +56 -0
- package/dist/core/agent.js +1783 -0
- package/dist/core/background.d.ts +45 -0
- package/dist/core/background.js +140 -0
- package/dist/core/bootstrap.d.ts +45 -0
- package/dist/core/bootstrap.js +294 -0
- package/dist/core/cli-args.d.ts +41 -0
- package/dist/core/cli-args.js +282 -0
- package/dist/core/collapsible-output.d.ts +38 -0
- package/dist/core/collapsible-output.js +143 -0
- package/dist/core/compaction-checkpoint.d.ts +9 -0
- package/dist/core/compaction-checkpoint.js +58 -0
- package/dist/core/compaction.d.ts +42 -0
- package/dist/core/compaction.js +270 -0
- package/dist/core/config.d.ts +49 -0
- package/dist/core/config.js +417 -0
- package/dist/core/context-ledger.d.ts +99 -0
- package/dist/core/context-ledger.js +222 -0
- package/dist/core/context-policy.d.ts +17 -0
- package/dist/core/context-policy.js +81 -0
- package/dist/core/continuation-review.d.ts +23 -0
- package/dist/core/continuation-review.js +64 -0
- package/dist/core/crash-report.d.ts +3 -0
- package/dist/core/crash-report.js +27 -0
- package/dist/core/credentials.d.ts +11 -0
- package/dist/core/credentials.js +183 -0
- package/dist/core/doctor.d.ts +24 -0
- package/dist/core/doctor.js +125 -0
- package/dist/core/effort.d.ts +5 -0
- package/dist/core/effort.js +42 -0
- package/dist/core/env-paths.d.ts +3 -0
- package/dist/core/env-paths.js +38 -0
- package/dist/core/execution-guidance.d.ts +9 -0
- package/dist/core/execution-guidance.js +73 -0
- package/dist/core/execution-target.d.ts +41 -0
- package/dist/core/execution-target.js +69 -0
- package/dist/core/file-coordination.d.ts +66 -0
- package/dist/core/file-coordination.js +176 -0
- package/dist/core/file-lease-store.d.ts +14 -0
- package/dist/core/file-lease-store.js +89 -0
- package/dist/core/image-limits.d.ts +26 -0
- package/dist/core/image-limits.js +205 -0
- package/dist/core/interruptible-completion.d.ts +3 -0
- package/dist/core/interruptible-completion.js +42 -0
- package/dist/core/kingdoms-mode-state.d.ts +24 -0
- package/dist/core/kingdoms-mode-state.js +139 -0
- package/dist/core/licenza.d.ts +38 -0
- package/dist/core/licenza.js +125 -0
- package/dist/core/model-limits.d.ts +2 -0
- package/dist/core/model-limits.js +36 -0
- package/dist/core/output-lifecycle.d.ts +44 -0
- package/dist/core/output-lifecycle.js +190 -0
- package/dist/core/package-update.d.ts +15 -0
- package/dist/core/package-update.js +121 -0
- package/dist/core/permission-queue.d.ts +2 -0
- package/dist/core/permission-queue.js +20 -0
- package/dist/core/prompt-attachment.d.ts +21 -0
- package/dist/core/prompt-attachment.js +123 -0
- package/dist/core/request-plan.d.ts +16 -0
- package/dist/core/request-plan.js +36 -0
- package/dist/core/run-timing.d.ts +17 -0
- package/dist/core/run-timing.js +123 -0
- package/dist/core/self-update.d.ts +17 -0
- package/dist/core/self-update.js +54 -0
- package/dist/core/session-refs.d.ts +27 -0
- package/dist/core/session-refs.js +126 -0
- package/dist/core/session-storage.d.ts +4 -0
- package/dist/core/session-storage.js +48 -0
- package/dist/core/session-title.d.ts +1 -0
- package/dist/core/session-title.js +21 -0
- package/dist/core/session-usage.d.ts +17 -0
- package/dist/core/session-usage.js +74 -0
- package/dist/core/session.d.ts +58 -0
- package/dist/core/session.js +296 -0
- package/dist/core/settings.d.ts +17 -0
- package/dist/core/settings.js +107 -0
- package/dist/core/storage-retention.d.ts +21 -0
- package/dist/core/storage-retention.js +227 -0
- package/dist/core/text-sanitize.d.ts +1 -0
- package/dist/core/text-sanitize.js +35 -0
- package/dist/core/tool-artifacts.d.ts +6 -0
- package/dist/core/tool-artifacts.js +51 -0
- package/dist/core/tool-compress.d.ts +8 -0
- package/dist/core/tool-compress.js +111 -0
- package/dist/core/tool-executor.d.ts +16 -0
- package/dist/core/tool-executor.js +97 -0
- package/dist/core/tool-result-index.d.ts +52 -0
- package/dist/core/tool-result-index.js +327 -0
- package/dist/core/tool-result-store.d.ts +38 -0
- package/dist/core/tool-result-store.js +190 -0
- package/dist/core/tool-scheduler.d.ts +16 -0
- package/dist/core/tool-scheduler.js +35 -0
- package/dist/core/trust.d.ts +21 -0
- package/dist/core/trust.js +95 -0
- package/dist/core/types.d.ts +402 -0
- package/dist/core/types.js +1 -0
- package/dist/core/update-check.d.ts +26 -0
- package/dist/core/update-check.js +89 -0
- package/dist/core/update-process.d.ts +10 -0
- package/dist/core/update-process.js +31 -0
- package/dist/core/user-inbox.d.ts +23 -0
- package/dist/core/user-inbox.js +56 -0
- package/dist/core/user-input.d.ts +10 -0
- package/dist/core/user-input.js +35 -0
- package/dist/core/version.d.ts +1 -0
- package/dist/core/version.js +10 -0
- package/dist/core/vibe-modes.d.ts +38 -0
- package/dist/core/vibe-modes.js +383 -0
- package/dist/core/workspace-roots.d.ts +13 -0
- package/dist/core/workspace-roots.js +91 -0
- package/dist/core/workspace-sessions.d.ts +51 -0
- package/dist/core/workspace-sessions.js +243 -0
- package/dist/cowork/client.d.ts +29 -0
- package/dist/cowork/client.js +49 -0
- package/dist/cowork/engine-bridge.d.ts +22 -0
- package/dist/cowork/engine-bridge.js +98 -0
- package/dist/cowork/index.d.ts +11 -0
- package/dist/cowork/index.js +16 -0
- package/dist/cowork/ipc.d.ts +97 -0
- package/dist/cowork/ipc.js +96 -0
- package/dist/cowork/manager.d.ts +66 -0
- package/dist/cowork/manager.js +325 -0
- package/dist/cowork/store.d.ts +23 -0
- package/dist/cowork/store.js +182 -0
- package/dist/cowork/sync.d.ts +20 -0
- package/dist/cowork/sync.js +164 -0
- package/dist/cowork/types.d.ts +78 -0
- package/dist/cowork/types.js +21 -0
- package/dist/extensions/ext-tools.d.ts +3 -0
- package/dist/extensions/ext-tools.js +73 -0
- package/dist/extensions/loader.d.ts +91 -0
- package/dist/extensions/loader.js +474 -0
- package/dist/extensions/settings.d.ts +20 -0
- package/dist/extensions/settings.js +86 -0
- package/dist/extensions/types.d.ts +219 -0
- package/dist/extensions/types.js +1 -0
- package/dist/horizon/async-queue.d.ts +9 -0
- package/dist/horizon/async-queue.js +46 -0
- package/dist/horizon/engine.d.ts +85 -0
- package/dist/horizon/engine.js +603 -0
- package/dist/horizon/index.d.ts +4 -0
- package/dist/horizon/index.js +2 -0
- package/dist/horizon/main-team.d.ts +163 -0
- package/dist/horizon/main-team.js +774 -0
- package/dist/horizon/types.d.ts +233 -0
- package/dist/horizon/types.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +233 -0
- package/dist/mcp/protocol.d.ts +81 -0
- package/dist/mcp/protocol.js +122 -0
- package/dist/package.json +1 -0
- package/dist/packages/packages.d.ts +12 -0
- package/dist/packages/packages.js +323 -0
- package/dist/prompts/templates.d.ts +13 -0
- package/dist/prompts/templates.js +139 -0
- package/dist/providers/anthropic-oauth.d.ts +13 -0
- package/dist/providers/anthropic-oauth.js +124 -0
- package/dist/providers/anthropic.d.ts +27 -0
- package/dist/providers/anthropic.js +526 -0
- package/dist/providers/catalog.d.ts +15 -0
- package/dist/providers/catalog.js +166 -0
- package/dist/providers/effort.d.ts +12 -0
- package/dist/providers/effort.js +102 -0
- package/dist/providers/model-catalog.d.ts +43 -0
- package/dist/providers/model-catalog.js +70 -0
- package/dist/providers/model-effort.d.ts +5 -0
- package/dist/providers/model-effort.js +43 -0
- package/dist/providers/model-registry.d.ts +39 -0
- package/dist/providers/model-registry.js +142 -0
- package/dist/providers/models-dev.d.ts +9 -0
- package/dist/providers/models-dev.js +97 -0
- package/dist/providers/oauth-callback-server.d.ts +28 -0
- package/dist/providers/oauth-callback-server.js +184 -0
- package/dist/providers/oauth-orchestrator.d.ts +58 -0
- package/dist/providers/oauth-orchestrator.js +159 -0
- package/dist/providers/oauth-store.d.ts +12 -0
- package/dist/providers/oauth-store.js +139 -0
- package/dist/providers/open-browser.d.ts +20 -0
- package/dist/providers/open-browser.js +92 -0
- package/dist/providers/openai-compat.d.ts +14 -0
- package/dist/providers/openai-compat.js +523 -0
- package/dist/providers/openai-oauth.d.ts +15 -0
- package/dist/providers/openai-oauth.js +382 -0
- package/dist/providers/openrouter-auth.d.ts +73 -0
- package/dist/providers/openrouter-auth.js +321 -0
- package/dist/providers/openrouter-manual-fallback.d.ts +25 -0
- package/dist/providers/openrouter-manual-fallback.js +161 -0
- package/dist/providers/openrouter.d.ts +15 -0
- package/dist/providers/openrouter.js +107 -0
- package/dist/providers/plan-limits.d.ts +6 -0
- package/dist/providers/plan-limits.js +175 -0
- package/dist/providers/provider-reset.d.ts +4 -0
- package/dist/providers/provider-reset.js +83 -0
- package/dist/providers/quota-limits.d.ts +4 -0
- package/dist/providers/quota-limits.js +33 -0
- package/dist/providers/registry.d.ts +33 -0
- package/dist/providers/registry.js +213 -0
- package/dist/providers/retry.d.ts +37 -0
- package/dist/providers/retry.js +347 -0
- package/dist/rpc/server.d.ts +23 -0
- package/dist/rpc/server.js +222 -0
- package/dist/sdk.d.ts +67 -0
- package/dist/sdk.js +64 -0
- package/dist/skills/skills.d.ts +17 -0
- package/dist/skills/skills.js +155 -0
- package/dist/text-sanitize.js +28 -0
- package/dist/tools/ask-user.d.ts +2 -0
- package/dist/tools/ask-user.js +60 -0
- package/dist/tools/builtin.d.ts +48 -0
- package/dist/tools/builtin.js +2494 -0
- package/dist/tools/capabilities.d.ts +5 -0
- package/dist/tools/capabilities.js +55 -0
- package/dist/tools/computer-capture.d.ts +2 -0
- package/dist/tools/computer-capture.js +91 -0
- package/dist/tools/computer-posix.d.ts +5 -0
- package/dist/tools/computer-posix.js +136 -0
- package/dist/tools/computer-use.d.ts +14 -0
- package/dist/tools/computer-use.js +108 -0
- package/dist/tools/discovery.d.ts +12 -0
- package/dist/tools/discovery.js +205 -0
- package/dist/tools/execute.d.ts +2 -0
- package/dist/tools/execute.js +73 -0
- package/dist/tools/normalize-input.d.ts +17 -0
- package/dist/tools/normalize-input.js +174 -0
- package/dist/tools/output-artifact.d.ts +3 -0
- package/dist/tools/output-artifact.js +80 -0
- package/dist/tools/pagination.d.ts +15 -0
- package/dist/tools/pagination.js +93 -0
- package/dist/tools/plan-status.d.ts +2 -0
- package/dist/tools/plan-status.js +109 -0
- package/dist/tools/program-input.d.ts +3 -0
- package/dist/tools/program-input.js +19 -0
- package/dist/tools/program.d.ts +3 -0
- package/dist/tools/program.js +567 -0
- package/dist/tools/protocol-catalog.d.ts +12 -0
- package/dist/tools/protocol-catalog.js +46 -0
- package/dist/tools/ramification-tools.d.ts +12 -0
- package/dist/tools/ramification-tools.js +528 -0
- package/dist/tools/regex-compat.d.ts +9 -0
- package/dist/tools/regex-compat.js +96 -0
- package/dist/tools/registry.d.ts +51 -0
- package/dist/tools/registry.js +185 -0
- package/dist/tools/resolve.d.ts +2 -0
- package/dist/tools/resolve.js +107 -0
- package/dist/tools/shell-env.d.ts +2 -0
- package/dist/tools/shell-env.js +7 -0
- package/dist/tools/team-input.d.ts +3 -0
- package/dist/tools/team-input.js +129 -0
- package/dist/tools/team-tools.d.ts +7 -0
- package/dist/tools/team-tools.js +348 -0
- package/dist/tools/workspace-sessions.d.ts +2 -0
- package/dist/tools/workspace-sessions.js +35 -0
- package/dist/tui/account-entry.d.ts +1 -0
- package/dist/tui/account-entry.js +93 -0
- package/dist/tui/attachment-highlight.d.ts +1 -0
- package/dist/tui/attachment-highlight.js +24 -0
- package/dist/tui/attachment-refs.d.ts +2 -0
- package/dist/tui/attachment-refs.js +3 -0
- package/dist/tui/bottom-input.d.ts +219 -0
- package/dist/tui/bottom-input.js +1958 -0
- package/dist/tui/branding.d.ts +107 -0
- package/dist/tui/branding.js +750 -0
- package/dist/tui/clipboard-image.d.ts +10 -0
- package/dist/tui/clipboard-image.js +163 -0
- package/dist/tui/clipboard-text.d.ts +9 -0
- package/dist/tui/clipboard-text.js +51 -0
- package/dist/tui/clipboard-windows.d.ts +8 -0
- package/dist/tui/clipboard-windows.js +84 -0
- package/dist/tui/collapsible-command.d.ts +15 -0
- package/dist/tui/collapsible-command.js +48 -0
- package/dist/tui/command-feedback.d.ts +10 -0
- package/dist/tui/command-feedback.js +46 -0
- package/dist/tui/command-output-registry.d.ts +28 -0
- package/dist/tui/command-output-registry.js +103 -0
- package/dist/tui/command-transcript.d.ts +12 -0
- package/dist/tui/command-transcript.js +90 -0
- package/dist/tui/commands.d.ts +20 -0
- package/dist/tui/commands.js +131 -0
- package/dist/tui/context-bar.d.ts +4 -0
- package/dist/tui/context-bar.js +74 -0
- package/dist/tui/context-meter.d.ts +25 -0
- package/dist/tui/context-meter.js +81 -0
- package/dist/tui/cost-binding.d.ts +8 -0
- package/dist/tui/cost-binding.js +43 -0
- package/dist/tui/cost-tracker.d.ts +49 -0
- package/dist/tui/cost-tracker.js +122 -0
- package/dist/tui/diff.d.ts +9 -0
- package/dist/tui/diff.js +119 -0
- package/dist/tui/i18n-catalog.d.ts +5 -0
- package/dist/tui/i18n-catalog.js +184 -0
- package/dist/tui/i18n.d.ts +6 -0
- package/dist/tui/i18n.js +10 -0
- package/dist/tui/image-attachments.d.ts +46 -0
- package/dist/tui/image-attachments.js +167 -0
- package/dist/tui/images.d.ts +12 -0
- package/dist/tui/images.js +87 -0
- package/dist/tui/intro.d.ts +13 -0
- package/dist/tui/intro.js +109 -0
- package/dist/tui/language-preference.d.ts +6 -0
- package/dist/tui/language-preference.js +24 -0
- package/dist/tui/live-output.d.ts +2 -0
- package/dist/tui/live-output.js +18 -0
- package/dist/tui/locales.d.ts +19 -0
- package/dist/tui/locales.js +22 -0
- package/dist/tui/login-menu.d.ts +17 -0
- package/dist/tui/login-menu.js +58 -0
- package/dist/tui/logo.d.ts +8 -0
- package/dist/tui/logo.js +305 -0
- package/dist/tui/markdown-inline.d.ts +3 -0
- package/dist/tui/markdown-inline.js +99 -0
- package/dist/tui/markdown-table.d.ts +4 -0
- package/dist/tui/markdown-table.js +63 -0
- package/dist/tui/markdown.d.ts +11 -0
- package/dist/tui/markdown.js +257 -0
- package/dist/tui/mcp-transcript.d.ts +7 -0
- package/dist/tui/mcp-transcript.js +40 -0
- package/dist/tui/model-details.d.ts +2 -0
- package/dist/tui/model-details.js +21 -0
- package/dist/tui/model-selection.d.ts +7 -0
- package/dist/tui/model-selection.js +41 -0
- package/dist/tui/native-scrollback.d.ts +27 -0
- package/dist/tui/native-scrollback.js +134 -0
- package/dist/tui/onboarding.d.ts +72 -0
- package/dist/tui/onboarding.js +207 -0
- package/dist/tui/paste-collapse.d.ts +39 -0
- package/dist/tui/paste-collapse.js +117 -0
- package/dist/tui/perm-mode.d.ts +8 -0
- package/dist/tui/perm-mode.js +41 -0
- package/dist/tui/prompt-queue-store.d.ts +26 -0
- package/dist/tui/prompt-queue-store.js +87 -0
- package/dist/tui/prompt-queue.d.ts +47 -0
- package/dist/tui/prompt-queue.js +187 -0
- package/dist/tui/provider-menu.d.ts +33 -0
- package/dist/tui/provider-menu.js +175 -0
- package/dist/tui/question.d.ts +24 -0
- package/dist/tui/question.js +32 -0
- package/dist/tui/queue-screen.d.ts +25 -0
- package/dist/tui/queue-screen.js +152 -0
- package/dist/tui/queue-team.d.ts +3 -0
- package/dist/tui/queue-team.js +28 -0
- package/dist/tui/queued-questions.d.ts +20 -0
- package/dist/tui/queued-questions.js +163 -0
- package/dist/tui/quick-help.d.ts +1 -0
- package/dist/tui/quick-help.js +53 -0
- package/dist/tui/repl.d.ts +78 -0
- package/dist/tui/repl.js +4130 -0
- package/dist/tui/restore-queued-input.d.ts +3 -0
- package/dist/tui/restore-queued-input.js +20 -0
- package/dist/tui/review-detail.d.ts +6 -0
- package/dist/tui/review-detail.js +56 -0
- package/dist/tui/review-files.d.ts +3 -0
- package/dist/tui/review-files.js +22 -0
- package/dist/tui/review-render.d.ts +22 -0
- package/dist/tui/review-render.js +98 -0
- package/dist/tui/review-screen.d.ts +38 -0
- package/dist/tui/review-screen.js +358 -0
- package/dist/tui/review-store.d.ts +60 -0
- package/dist/tui/review-store.js +270 -0
- package/dist/tui/review-team.d.ts +2 -0
- package/dist/tui/review-team.js +27 -0
- package/dist/tui/selector-mouse.d.ts +26 -0
- package/dist/tui/selector-mouse.js +65 -0
- package/dist/tui/selector.d.ts +51 -0
- package/dist/tui/selector.js +299 -0
- package/dist/tui/session-replay.d.ts +10 -0
- package/dist/tui/session-replay.js +78 -0
- package/dist/tui/session-usage.d.ts +22 -0
- package/dist/tui/session-usage.js +55 -0
- package/dist/tui/skill-transcript.d.ts +5 -0
- package/dist/tui/skill-transcript.js +31 -0
- package/dist/tui/startup.d.ts +5 -0
- package/dist/tui/startup.js +61 -0
- package/dist/tui/status-line-loader.d.ts +8 -0
- package/dist/tui/status-line-loader.js +33 -0
- package/dist/tui/status-line.d.ts +38 -0
- package/dist/tui/status-line.js +213 -0
- package/dist/tui/system-language.d.ts +8 -0
- package/dist/tui/system-language.js +41 -0
- package/dist/tui/task-labels.d.ts +2 -0
- package/dist/tui/task-labels.js +7 -0
- package/dist/tui/term-bg.d.ts +4 -0
- package/dist/tui/term-bg.js +67 -0
- package/dist/tui/terminal-screen.d.ts +12 -0
- package/dist/tui/terminal-screen.js +1 -0
- package/dist/tui/terminal-text.d.ts +7 -0
- package/dist/tui/terminal-text.js +123 -0
- package/dist/tui/theme.d.ts +25 -0
- package/dist/tui/theme.js +383 -0
- package/dist/tui/tool-style.d.ts +7 -0
- package/dist/tui/tool-style.js +119 -0
- package/dist/tui/update-prompt.d.ts +14 -0
- package/dist/tui/update-prompt.js +104 -0
- package/dist/tui/usage-footer.d.ts +9 -0
- package/dist/tui/usage-footer.js +123 -0
- package/dist/tui/vascend-login.d.ts +68 -0
- package/dist/tui/vascend-login.js +355 -0
- package/dist/tui/web-login.d.ts +28 -0
- package/dist/tui/web-login.js +115 -0
- package/dist/vascend/activity-status.d.ts +12 -0
- package/dist/vascend/activity-status.js +68 -0
- package/dist/vascend/architecture-map.d.ts +73 -0
- package/dist/vascend/architecture-map.js +424 -0
- package/dist/vascend/attention-layout.d.ts +19 -0
- package/dist/vascend/attention-layout.js +141 -0
- package/dist/vascend/budget-allocator.d.ts +31 -0
- package/dist/vascend/budget-allocator.js +79 -0
- package/dist/vascend/candidate-collector.d.ts +20 -0
- package/dist/vascend/candidate-collector.js +220 -0
- package/dist/vascend/code-events.d.ts +24 -0
- package/dist/vascend/code-events.js +304 -0
- package/dist/vascend/code-graph-memory.d.ts +38 -0
- package/dist/vascend/code-graph-memory.js +334 -0
- package/dist/vascend/code-graph-runtime.d.ts +19 -0
- package/dist/vascend/code-graph-runtime.js +190 -0
- package/dist/vascend/code-graph-types.d.ts +58 -0
- package/dist/vascend/code-graph-types.js +50 -0
- package/dist/vascend/code-parser.d.ts +4 -0
- package/dist/vascend/code-parser.js +257 -0
- package/dist/vascend/competitor-knowledge.d.ts +53 -0
- package/dist/vascend/competitor-knowledge.js +262 -0
- package/dist/vascend/completeness.d.ts +16 -0
- package/dist/vascend/completeness.js +90 -0
- package/dist/vascend/context-autotuner.d.ts +45 -0
- package/dist/vascend/context-autotuner.js +231 -0
- package/dist/vascend/context-candidate.d.ts +45 -0
- package/dist/vascend/context-candidate.js +150 -0
- package/dist/vascend/context-capsule.d.ts +51 -0
- package/dist/vascend/context-capsule.js +133 -0
- package/dist/vascend/context-fabric.d.ts +108 -0
- package/dist/vascend/context-fabric.js +480 -0
- package/dist/vascend/context-feedback-loop.d.ts +56 -0
- package/dist/vascend/context-feedback-loop.js +204 -0
- package/dist/vascend/context-learning-identity.d.ts +4 -0
- package/dist/vascend/context-learning-identity.js +8 -0
- package/dist/vascend/context-links.d.ts +14 -0
- package/dist/vascend/context-links.js +83 -0
- package/dist/vascend/context-lint.d.ts +22 -0
- package/dist/vascend/context-lint.js +91 -0
- package/dist/vascend/context-market-selection.d.ts +17 -0
- package/dist/vascend/context-market-selection.js +50 -0
- package/dist/vascend/context-metrics.d.ts +25 -0
- package/dist/vascend/context-metrics.js +57 -0
- package/dist/vascend/context-pack-render.d.ts +16 -0
- package/dist/vascend/context-pack-render.js +52 -0
- package/dist/vascend/context-pareto.d.ts +33 -0
- package/dist/vascend/context-pareto.js +165 -0
- package/dist/vascend/context-pattern-explorer.d.ts +41 -0
- package/dist/vascend/context-pattern-explorer.js +276 -0
- package/dist/vascend/context-regret.d.ts +32 -0
- package/dist/vascend/context-regret.js +93 -0
- package/dist/vascend/context-retriever.d.ts +22 -0
- package/dist/vascend/context-retriever.js +243 -0
- package/dist/vascend/context-scope-index.d.ts +5 -0
- package/dist/vascend/context-scope-index.js +96 -0
- package/dist/vascend/context-scorer.d.ts +35 -0
- package/dist/vascend/context-scorer.js +162 -0
- package/dist/vascend/context-search-index.d.ts +12 -0
- package/dist/vascend/context-search-index.js +25 -0
- package/dist/vascend/context-snapshot.d.ts +22 -0
- package/dist/vascend/context-snapshot.js +135 -0
- package/dist/vascend/context-window-compiler.d.ts +42 -0
- package/dist/vascend/context-window-compiler.js +237 -0
- package/dist/vascend/contract-memory.d.ts +61 -0
- package/dist/vascend/contract-memory.js +435 -0
- package/dist/vascend/council.d.ts +58 -0
- package/dist/vascend/council.js +337 -0
- package/dist/vascend/decision-promotion.d.ts +48 -0
- package/dist/vascend/decision-promotion.js +410 -0
- package/dist/vascend/delta-context.d.ts +12 -0
- package/dist/vascend/delta-context.js +36 -0
- package/dist/vascend/dev-context-compiler.d.ts +27 -0
- package/dist/vascend/dev-context-compiler.js +133 -0
- package/dist/vascend/dev-context-layout.d.ts +30 -0
- package/dist/vascend/dev-context-layout.js +262 -0
- package/dist/vascend/evidence-slots.d.ts +14 -0
- package/dist/vascend/evidence-slots.js +55 -0
- package/dist/vascend/expansion-levels.d.ts +15 -0
- package/dist/vascend/expansion-levels.js +178 -0
- package/dist/vascend/file-lock.d.ts +7 -0
- package/dist/vascend/file-lock.js +95 -0
- package/dist/vascend/granular-kingdom.d.ts +25 -0
- package/dist/vascend/granular-kingdom.js +259 -0
- package/dist/vascend/granular-launcher.d.ts +48 -0
- package/dist/vascend/granular-launcher.js +202 -0
- package/dist/vascend/kingdom-bus.d.ts +25 -0
- package/dist/vascend/kingdom-bus.js +54 -0
- package/dist/vascend/kingdom-channel.d.ts +22 -0
- package/dist/vascend/kingdom-channel.js +180 -0
- package/dist/vascend/kingdom-kpi.d.ts +48 -0
- package/dist/vascend/kingdom-kpi.js +172 -0
- package/dist/vascend/kingdom-live.d.ts +28 -0
- package/dist/vascend/kingdom-live.js +61 -0
- package/dist/vascend/kingdom-run.d.ts +18 -0
- package/dist/vascend/kingdom-run.js +506 -0
- package/dist/vascend/kingdom-scribe.d.ts +20 -0
- package/dist/vascend/kingdom-scribe.js +52 -0
- package/dist/vascend/kingdom.d.ts +79 -0
- package/dist/vascend/kingdom.js +870 -0
- package/dist/vascend/lexical-index.d.ts +10 -0
- package/dist/vascend/lexical-index.js +99 -0
- package/dist/vascend/lexical-search.d.ts +29 -0
- package/dist/vascend/lexical-search.js +262 -0
- package/dist/vascend/long-term-context.d.ts +51 -0
- package/dist/vascend/long-term-context.js +194 -0
- package/dist/vascend/mandate.d.ts +33 -0
- package/dist/vascend/mandate.js +107 -0
- package/dist/vascend/memory-packet.d.ts +13 -0
- package/dist/vascend/memory-packet.js +155 -0
- package/dist/vascend/memory-status.d.ts +22 -0
- package/dist/vascend/memory-status.js +73 -0
- package/dist/vascend/memory-store.d.ts +62 -0
- package/dist/vascend/memory-store.js +273 -0
- package/dist/vascend/objective-revision.d.ts +69 -0
- package/dist/vascend/objective-revision.js +319 -0
- package/dist/vascend/patch-plan-store.d.ts +33 -0
- package/dist/vascend/patch-plan-store.js +240 -0
- package/dist/vascend/patch-plan.d.ts +97 -0
- package/dist/vascend/patch-plan.js +345 -0
- package/dist/vascend/plan-progress.d.ts +11 -0
- package/dist/vascend/plan-progress.js +68 -0
- package/dist/vascend/plan-store.d.ts +64 -0
- package/dist/vascend/plan-store.js +460 -0
- package/dist/vascend/proof-chains.d.ts +19 -0
- package/dist/vascend/proof-chains.js +120 -0
- package/dist/vascend/ramification-store.d.ts +25 -0
- package/dist/vascend/ramification-store.js +94 -0
- package/dist/vascend/ramification.d.ts +193 -0
- package/dist/vascend/ramification.js +583 -0
- package/dist/vascend/recall-planner.d.ts +17 -0
- package/dist/vascend/recall-planner.js +109 -0
- package/dist/vascend/requirements.d.ts +31 -0
- package/dist/vascend/requirements.js +151 -0
- package/dist/vascend/reviewer.d.ts +9 -0
- package/dist/vascend/reviewer.js +93 -0
- package/dist/vascend/room-graph.d.ts +29 -0
- package/dist/vascend/room-graph.js +242 -0
- package/dist/vascend/room-memory.d.ts +57 -0
- package/dist/vascend/room-memory.js +631 -0
- package/dist/vascend/runtime.d.ts +84 -0
- package/dist/vascend/runtime.js +286 -0
- package/dist/vascend/self-improve.d.ts +104 -0
- package/dist/vascend/self-improve.js +396 -0
- package/dist/vascend/session-search.d.ts +52 -0
- package/dist/vascend/session-search.js +349 -0
- package/dist/vascend/supersession-graph.d.ts +14 -0
- package/dist/vascend/supersession-graph.js +70 -0
- package/dist/vascend/team-areas.d.ts +9 -0
- package/dist/vascend/team-areas.js +28 -0
- package/dist/vascend/team-handoff.d.ts +24 -0
- package/dist/vascend/team-handoff.js +37 -0
- package/dist/vascend/team-scheduler.d.ts +13 -0
- package/dist/vascend/team-scheduler.js +74 -0
- package/dist/vascend/team-session.d.ts +42 -0
- package/dist/vascend/team-session.js +142 -0
- package/dist/vascend/test-failure-memory.d.ts +76 -0
- package/dist/vascend/test-failure-memory.js +354 -0
- package/dist/vascend/token-market.d.ts +42 -0
- package/dist/vascend/token-market.js +144 -0
- package/dist/vascend/tuner-baseline-store.d.ts +52 -0
- package/dist/vascend/tuner-baseline-store.js +446 -0
- package/dist/vascend/user-profile.d.ts +44 -0
- package/dist/vascend/user-profile.js +131 -0
- package/dist/vascend/vision.d.ts +94 -0
- package/dist/vascend/vision.js +375 -0
- package/dist/vascend/work-agent.d.ts +12 -0
- package/dist/vascend/work-agent.js +83 -0
- package/dist/vascend/work-queue.d.ts +62 -0
- package/dist/vascend/work-queue.js +255 -0
- package/dist/vascend/work-runner.d.ts +64 -0
- package/dist/vascend/work-runner.js +137 -0
- package/dist/vascend/worktree.d.ts +38 -0
- package/dist/vascend/worktree.js +206 -0
- package/dist/vendor/computer-use/README.md +7 -0
- package/dist/vendor/computer-use/computer-desktop.js +211 -0
- package/dist/vendor/computer-use/computer-observe.js +315 -0
- package/dist/vendor/computer-use/computer-scripts.js +1230 -0
- package/dist/vendor/computer-use/computer-tools.js +2136 -0
- package/dist/vendor/computer-use/package.json +1 -0
- package/dist/vendor/computer-use/rac/atomic-input.js +91 -0
- package/dist/vendor/computer-use/rac/grammatica.js +101 -0
- package/dist/vendor/computer-use/rac/parser.js +185 -0
- package/dist/vendor/computer-use/rac/protocol.js +136 -0
- package/install.ps1 +30 -0
- package/install.sh +50 -0
- package/package.json +56 -0
|
@@ -0,0 +1,2136 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Computer Use locale per Horizon: qui vivono le DEFINIZIONI degli strumenti e
|
|
4
|
+
// la politica (consenso, validazione, verifica). Il layer nativo — PowerShell,
|
|
5
|
+
// user32, UI Automation — sta in computer-scripts.js, l'osservazione pura in
|
|
6
|
+
// computer-observe.js, l'inventario del desktop in computer-desktop.js.
|
|
7
|
+
//
|
|
8
|
+
// Tre principi, imparati dagli usi reali:
|
|
9
|
+
// 1. Si agisce sui CONTROLLI, non sui pixel. Un click stimato da un'immagine
|
|
10
|
+
// sbaglia bersaglio appena la finestra si sposta; Invoke/SetValue di UI
|
|
11
|
+
// Automation colpiscono l'elemento e lo portano anche in vista.
|
|
12
|
+
// 2. Si osserva per INTENTO. Il tree di una app reale ha centinaia di nodi:
|
|
13
|
+
// l'agente chiede cosa cerca e riceve righe compatte, non inventari.
|
|
14
|
+
// 3. Ogni azione RIPORTA il proprio esito. Chiudere con "verifica tu" costava
|
|
15
|
+
// due chiamate per ogni passo: adesso l'azione porta con se' il delta.
|
|
16
|
+
|
|
17
|
+
const crypto = require('crypto');
|
|
18
|
+
const Native = require('./computer-scripts');
|
|
19
|
+
const Observe = require('./computer-observe');
|
|
20
|
+
const Desktop = require('./computer-desktop');
|
|
21
|
+
const { EXTENSION_ID, SERVER_CAPABILITIES } = require('./rac/protocol.js');
|
|
22
|
+
const { normalizeAtomicOperations } = require('./rac/atomic-input.js');
|
|
23
|
+
const { creaParser: creaRacPreviewParser } = require('./rac/parser.js');
|
|
24
|
+
const { LIMITI: RAC_LIMITI } = require('./rac/grammatica.js');
|
|
25
|
+
|
|
26
|
+
const { MAX_TEXT, PASTE_THRESHOLD, keyCodes, keyChords } = Native;
|
|
27
|
+
const { integer, cleanBounds, cleanElement, compactElement, selectorFor, pointerFor,
|
|
28
|
+
filterElements, similarity, diffElements, describeChanges, searchedTargets,
|
|
29
|
+
startMenuMatches, ok, warn, fail } = Observe;
|
|
30
|
+
const { applicationName, publicProcess, publicApplication, uiApplication, publicTarget } = Desktop;
|
|
31
|
+
|
|
32
|
+
const MAX_WAIT_MS = 10000;
|
|
33
|
+
const MAX_CONDITION_WAIT_MS = 20000;
|
|
34
|
+
const MAX_LAUNCH_WAIT_MS = 20000;
|
|
35
|
+
const MAX_BATCH_STEPS = 12;
|
|
36
|
+
const MAX_BATCH_DELAY_MS = 20000;
|
|
37
|
+
// Ispezione e azione successiva avvengono a meno di un secondo di distanza e
|
|
38
|
+
// camminavano il tree due volte. Ogni mutazione invalida la cache.
|
|
39
|
+
const TREE_CACHE_MS = 1200;
|
|
40
|
+
|
|
41
|
+
function computerUseTools(options = {}) {
|
|
42
|
+
const captureDesktop = options.captureDesktop;
|
|
43
|
+
const desktopInfo = typeof options.desktopInfo === 'function' ? options.desktopInfo : Native.defaultDesktopInfo;
|
|
44
|
+
const performAction = typeof options.performAction === 'function' ? options.performAction : Native.defaultAction;
|
|
45
|
+
const listWindows = typeof options.listWindows === 'function' ? options.listWindows : Native.defaultWindows;
|
|
46
|
+
// Se l'integrazione fornisce le proprie fonti (test, adapter), non apriamo
|
|
47
|
+
// canali nativi a sua insaputa: i processi si ricavano dalle finestre e i
|
|
48
|
+
// percorsi UI Automation / avvio applicazioni restano disattivati, con
|
|
49
|
+
// degradazione sui percorsi a coordinate.
|
|
50
|
+
const adapted = typeof options.listWindows === 'function';
|
|
51
|
+
const listProcesses = typeof options.listProcesses === 'function'
|
|
52
|
+
? options.listProcesses
|
|
53
|
+
: (adapted ? null : Native.defaultProcesses);
|
|
54
|
+
const inspectUI = typeof options.inspectUI === 'function' ? options.inspectUI : Native.defaultUiElements;
|
|
55
|
+
const uiAction = typeof options.uiAction === 'function'
|
|
56
|
+
? options.uiAction
|
|
57
|
+
: (typeof options.inspectUI === 'function' ? null : Native.defaultUiAction);
|
|
58
|
+
const launchApp = typeof options.launchApp === 'function'
|
|
59
|
+
? options.launchApp
|
|
60
|
+
: (adapted ? null : Native.defaultLaunch);
|
|
61
|
+
const openPath = typeof options.openPath === 'function'
|
|
62
|
+
? options.openPath
|
|
63
|
+
: (adapted ? null : Native.defaultOpenPath);
|
|
64
|
+
const listInstalledApps = typeof options.listInstalledApps === 'function'
|
|
65
|
+
? options.listInstalledApps
|
|
66
|
+
: (adapted ? null : Native.defaultStartMenuApps);
|
|
67
|
+
const onStateEvent = typeof options.onStateEvent === 'function' ? options.onStateEvent : () => {};
|
|
68
|
+
const reactiveHost = options.reactiveHost && typeof options.reactiveHost.open === 'function'
|
|
69
|
+
? options.reactiveHost : null;
|
|
70
|
+
const reactiveToolStream = options.reactiveToolStream
|
|
71
|
+
&& typeof options.reactiveToolStream.execute === 'function'
|
|
72
|
+
? options.reactiveToolStream : null;
|
|
73
|
+
const reactiveFastLane = options.reactiveFastLane
|
|
74
|
+
&& typeof options.reactiveFastLane.setGoal === 'function'
|
|
75
|
+
? options.reactiveFastLane : null;
|
|
76
|
+
const externalControllerEnabled = options.externalControllerEnabled === true && !!reactiveFastLane;
|
|
77
|
+
const racStrictMode = options.racStrictMode === true;
|
|
78
|
+
const allowLegacyRac = !racStrictMode || options.allowLegacyRac === true;
|
|
79
|
+
const consentMode = options.consentMode === 'always' || options.consentMode === 'once'
|
|
80
|
+
? options.consentMode : '';
|
|
81
|
+
// Pipeline visual-token (iniettata dal main): inversione dei frame per il
|
|
82
|
+
// modello, mappa adattiva dei patch, pacchetto temporale. Nei test resta
|
|
83
|
+
// assente e ogni percorso degrada al comportamento classico.
|
|
84
|
+
const visualFrame = options.visualFrame
|
|
85
|
+
&& typeof options.visualFrame.pngPerModello === 'function' ? options.visualFrame : null;
|
|
86
|
+
if (typeof captureDesktop !== 'function') return [];
|
|
87
|
+
|
|
88
|
+
const desktop = Desktop.createDesktopInventory({
|
|
89
|
+
listWindows,
|
|
90
|
+
listProcesses,
|
|
91
|
+
resolveProcessIcon: typeof options.resolveProcessIcon === 'function' ? options.resolveProcessIcon : null,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const resumedTarget = reactiveHost && typeof reactiveHost.getTargetDescriptor === 'function'
|
|
95
|
+
? reactiveHost.getTargetDescriptor() : null;
|
|
96
|
+
let runtimeAuthorized = !!consentMode;
|
|
97
|
+
let lastSnapshot = null;
|
|
98
|
+
// Affinita di sessione: il controller RAC puo sopravvivere a un turno e
|
|
99
|
+
// conserva quale finestra era collegata. Il nuovo run rilegge comunque la
|
|
100
|
+
// finestra prima di catturare/agire e non eredita snapshot o target UIA.
|
|
101
|
+
let attachedTarget = resumedTarget && /^\d+$/.test(String(resumedTarget.windowId || '')) ? {
|
|
102
|
+
windowId: String(resumedTarget.windowId),
|
|
103
|
+
title: String(resumedTarget.windowTitle || resumedTarget.title || ''),
|
|
104
|
+
bounds: resumedTarget.windowBounds || resumedTarget.bounds || { x: 0, y: 0, width: 0, height: 0 },
|
|
105
|
+
processId: Number(resumedTarget.processId) || 0,
|
|
106
|
+
process: String(resumedTarget.process || ''),
|
|
107
|
+
applicationId: String(resumedTarget.applicationId || ''),
|
|
108
|
+
applicationName: String(resumedTarget.applicationName || resumedTarget.process || resumedTarget.windowTitle || ''),
|
|
109
|
+
visible: true,
|
|
110
|
+
minimized: false,
|
|
111
|
+
} : null;
|
|
112
|
+
let targetCache = new Map();
|
|
113
|
+
let treeCache = null;
|
|
114
|
+
// Il controller puo restare caldo tra due turni. Non riutilizziamo mai il
|
|
115
|
+
// vecchio frame, ma continuiamo la sua sequenza per evitare che un nuovo
|
|
116
|
+
// checkpoint venga rifiutato come replay se i due turni si sovrappongono.
|
|
117
|
+
const resumedObservation = reactiveHost && typeof reactiveHost.getObservation === 'function'
|
|
118
|
+
? reactiveHost.getObservation() : null;
|
|
119
|
+
const resumedObservationSequence = Number(resumedObservation && resumedObservation.observationId);
|
|
120
|
+
let observationSequence = Number.isSafeInteger(resumedObservationSequence)
|
|
121
|
+
&& resumedObservationSequence >= 0 ? resumedObservationSequence : 0;
|
|
122
|
+
let currentObservation = null;
|
|
123
|
+
let lastObservationError = null;
|
|
124
|
+
let atomicFailure = { code: '', count: 0 };
|
|
125
|
+
|
|
126
|
+
const approve = async (ctx, detail) => {
|
|
127
|
+
if (!ctx || typeof ctx.confirm !== 'function') return false;
|
|
128
|
+
return !!(await ctx.confirm('computer_use', detail));
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const authorized = async (ctx, detail) => {
|
|
132
|
+
// L'avviso visibile del composer e' il gate primario. Manteniamo il vecchio
|
|
133
|
+
// confirm come fallback per integrazioni che invocano i tool senza grant.
|
|
134
|
+
if (runtimeAuthorized) return true;
|
|
135
|
+
runtimeAuthorized = await approve(ctx, detail);
|
|
136
|
+
return runtimeAuthorized;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const stateEvent = (event) => { try { onStateEvent(event); } catch (_) { /* indicatore best-effort */ } };
|
|
140
|
+
const inputPreview = (operations) => {
|
|
141
|
+
const preview = { sequence: [] };
|
|
142
|
+
for (const operation of Array.isArray(operations) ? operations : []) {
|
|
143
|
+
if (operation.type === 'shortcut') {
|
|
144
|
+
preview.keyboard = { ...(preview.keyboard || {}), keys: operation.keys.slice(0, 4) };
|
|
145
|
+
preview.sequence.push({ type: 'shortcut', keys: operation.keys.slice(0, 4) });
|
|
146
|
+
} else if (operation.type === 'text') {
|
|
147
|
+
preview.keyboard = { ...(preview.keyboard || {}), characterCount: operation.text.length };
|
|
148
|
+
preview.sequence.push({ type: 'text', characterCount: operation.text.length });
|
|
149
|
+
} else if (operation.type === 'mouse') {
|
|
150
|
+
preview.mouse = { dx: operation.dx, dy: operation.dy, scroll: operation.scroll };
|
|
151
|
+
preview.sequence.push({ type: 'mouse', dx: operation.dx, dy: operation.dy, scroll: operation.scroll });
|
|
152
|
+
} else if (operation.type === 'click') {
|
|
153
|
+
preview.mouse = { ...(preview.mouse || {}), button: operation.button };
|
|
154
|
+
preview.sequence.push({ type: 'click', button: operation.button });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return preview;
|
|
158
|
+
};
|
|
159
|
+
const racCommandPreview = (command) => {
|
|
160
|
+
const parser = creaRacPreviewParser();
|
|
161
|
+
const events = [...parser.push(String(command || '')), ...parser.fine()];
|
|
162
|
+
const mouseByObservation = new Map();
|
|
163
|
+
const sequence = [];
|
|
164
|
+
for (const event of events) {
|
|
165
|
+
if (!event || event.tipo !== 'chunk' || !event.chunk) continue;
|
|
166
|
+
const chunk = event.chunk;
|
|
167
|
+
if (chunk.tipo === 'mouse') {
|
|
168
|
+
mouseByObservation.set(String(chunk.observationId), {
|
|
169
|
+
dx: chunk.dx, dy: chunk.dy, scroll: chunk.scroll,
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (chunk.tipo !== 'keyset') continue;
|
|
174
|
+
const mouse = mouseByObservation.get(String(chunk.observationId)) || {};
|
|
175
|
+
sequence.push({
|
|
176
|
+
type: 'frame',
|
|
177
|
+
frame: Number(chunk.indice) + 1,
|
|
178
|
+
durationMs: RAC_LIMITI.CHUNK_MS,
|
|
179
|
+
keys: Array.isArray(chunk.tasti) ? chunk.tasti.slice(0, 4) : [],
|
|
180
|
+
mouse: {
|
|
181
|
+
dx: Math.trunc((Number(mouse.dx) || 0) / RAC_LIMITI.CHUNK_PER_AZIONE),
|
|
182
|
+
dy: Math.trunc((Number(mouse.dy) || 0) / RAC_LIMITI.CHUNK_PER_AZIONE),
|
|
183
|
+
scroll: Math.trunc((Number(mouse.scroll) || 0) / RAC_LIMITI.CHUNK_PER_AZIONE),
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
const current = sequence[0] || {};
|
|
188
|
+
return {
|
|
189
|
+
sequence,
|
|
190
|
+
...(current.keys && current.keys.length ? { keyboard: { keys: current.keys } } : {}),
|
|
191
|
+
...(current.mouse ? { mouse: current.mouse } : {}),
|
|
192
|
+
concurrent: true,
|
|
193
|
+
frameDurationMs: RAC_LIMITI.CHUNK_MS,
|
|
194
|
+
};
|
|
195
|
+
};
|
|
196
|
+
const invalidateVisualState = (reason = 'computer_state_changed', notifyHost = true) => {
|
|
197
|
+
lastSnapshot = null;
|
|
198
|
+
treeCache = null;
|
|
199
|
+
currentObservation = null;
|
|
200
|
+
if (notifyHost && reactiveHost && typeof reactiveHost.invalidateObservation === 'function') {
|
|
201
|
+
void reactiveHost.invalidateObservation(reason);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
const denied = (what) => fail('NOT_AUTHORIZED', `${what} non autorizzato.`,
|
|
205
|
+
['Chiedi all utente di attivare Computer Use dal composer, poi riprova.']);
|
|
206
|
+
|
|
207
|
+
const checkpointObservation = async (input = {}) => {
|
|
208
|
+
if (!reactiveHost || typeof reactiveHost.checkpoint !== 'function' || !attachedTarget) {
|
|
209
|
+
lastObservationError = { code: 'RAC_HOST_UNAVAILABLE' };
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
const observationId = reactiveHost && typeof reactiveHost.allocateObservationId === 'function'
|
|
213
|
+
? String(reactiveHost.allocateObservationId()) : String(++observationSequence);
|
|
214
|
+
const allocated = Number(observationId);
|
|
215
|
+
if (Number.isSafeInteger(allocated)) observationSequence = Math.max(observationSequence, allocated);
|
|
216
|
+
const observation = {
|
|
217
|
+
observationId,
|
|
218
|
+
windowId: String(attachedTarget.windowId || ''),
|
|
219
|
+
capturedAt: Number(input.capturedAt) || Date.now(),
|
|
220
|
+
monotonicAt: Number(input.monotonicAt) || Number(process.hrtime.bigint()) / 1e6,
|
|
221
|
+
checksum: String(input.checksum || ''),
|
|
222
|
+
width: Number(input.width) || 0,
|
|
223
|
+
height: Number(input.height) || 0,
|
|
224
|
+
bounds: input.bounds && typeof input.bounds === 'object' ? input.bounds : attachedTarget.bounds,
|
|
225
|
+
affine: input.affine && typeof input.affine === 'object' ? input.affine : null,
|
|
226
|
+
source: String(input.source || 'computer_use'),
|
|
227
|
+
};
|
|
228
|
+
let result = await reactiveHost.checkpoint(observation);
|
|
229
|
+
// Compatibilita con host gia caldi o aggiornati durante lo sviluppo: un
|
|
230
|
+
// safety stop/lease scaduta non deve trasformare tutte le osservazioni
|
|
231
|
+
// successive in null. Riarmiamo il target e riproviamo lo stesso keyframe.
|
|
232
|
+
if (result && ['SAFETY_STOP', 'SESSION_CLOSED', 'SESSION_EXPIRED'].includes(String(result.code || ''))
|
|
233
|
+
&& typeof reactiveHost.rearmTarget === 'function') {
|
|
234
|
+
const rearmed = await reactiveHost.rearmTarget('checkpoint_retry');
|
|
235
|
+
if (rearmed && rearmed.ok === true) result = await reactiveHost.checkpoint(observation);
|
|
236
|
+
}
|
|
237
|
+
if (!result || result.ok !== true) {
|
|
238
|
+
lastObservationError = {
|
|
239
|
+
code: String(result && result.code || 'OBSERVATION_CHECKPOINT_FAILED'),
|
|
240
|
+
error: String(result && result.error || ''),
|
|
241
|
+
};
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
lastObservationError = null;
|
|
245
|
+
currentObservation = result.observation || observation;
|
|
246
|
+
return currentObservation;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const visualObservation = async (shot, source) => {
|
|
250
|
+
if (!shot || !shot.data) return null;
|
|
251
|
+
const checksum = crypto.createHash('sha256')
|
|
252
|
+
.update(Buffer.from(String(shot.data), 'base64')).digest('hex');
|
|
253
|
+
return checkpointObservation({
|
|
254
|
+
capturedAt: Date.now(), monotonicAt: Number(process.hrtime.bigint()) / 1e6, checksum,
|
|
255
|
+
width: shot.width, height: shot.height, bounds: shot.bounds,
|
|
256
|
+
affine: shot.affine, source,
|
|
257
|
+
});
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Dopo una traiettoria RAC il frame successivo deve entrare nello stesso
|
|
261
|
+
// risultato del tool. In precedenza currentObservation veniva azzerata e il
|
|
262
|
+
// planner era costretto a un computer_inspect separato: quello trasformava il
|
|
263
|
+
// loop reattivo in un giro LLM -> tool da diversi secondi.
|
|
264
|
+
const capturePostActionObservation = async () => {
|
|
265
|
+
if (!attachedTarget) return null;
|
|
266
|
+
const alive = await refreshAttached();
|
|
267
|
+
if (!alive || !attachedTarget) return null;
|
|
268
|
+
const shot = await captureDesktop({
|
|
269
|
+
windowId: attachedTarget.windowId,
|
|
270
|
+
windowTitle: attachedTarget.title,
|
|
271
|
+
windowBounds: attachedTarget.bounds,
|
|
272
|
+
});
|
|
273
|
+
if (!shot || !shot.data) return null;
|
|
274
|
+
const bounds = shot.bounds && typeof shot.bounds === 'object'
|
|
275
|
+
? shot.bounds : attachedTarget.bounds;
|
|
276
|
+
lastSnapshot = {
|
|
277
|
+
id: 'snap-' + crypto.randomUUID(), capturedAt: Date.now(),
|
|
278
|
+
width: Number(shot.width) || 1, height: Number(shot.height) || 1, bounds,
|
|
279
|
+
};
|
|
280
|
+
const observation = await visualObservation({ ...shot, bounds }, 'computer_control_post_action');
|
|
281
|
+
if (observation) lastSnapshot.observationId = observation.observationId;
|
|
282
|
+
stateEvent({ type: 'snapshot', attached: attachedTarget, bounds, reactive: true });
|
|
283
|
+
return observation ? { shot, bounds, observation } : null;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const semanticObservation = async (rows, source = 'computer_inspect') => {
|
|
287
|
+
const checksum = crypto.createHash('sha256')
|
|
288
|
+
.update(JSON.stringify((Array.isArray(rows) ? rows : []).map(compactElement))).digest('hex');
|
|
289
|
+
return checkpointObservation({
|
|
290
|
+
capturedAt: Date.now(), monotonicAt: Number(process.hrtime.bigint()) / 1e6, checksum,
|
|
291
|
+
width: attachedTarget && attachedTarget.bounds && attachedTarget.bounds.width,
|
|
292
|
+
height: attachedTarget && attachedTarget.bounds && attachedTarget.bounds.height,
|
|
293
|
+
bounds: attachedTarget && attachedTarget.bounds, source,
|
|
294
|
+
});
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// La lettura del tree ha DUE esiti diversi che non vanno confusi: una
|
|
298
|
+
// finestra senza controlli e una lettura non riuscita. Trattarli allo stesso
|
|
299
|
+
// modo rendeva Computer Use cieco — su Spotify un carattere di controllo nei
|
|
300
|
+
// nomi rompeva il JSON e il tool rispondeva "nessun controllo", spingendo il
|
|
301
|
+
// modello a tirare a indovinare sulle coordinate.
|
|
302
|
+
const readTree = async ({ force = false, includeText = false } = {}) => {
|
|
303
|
+
const scope = (attachedTarget && attachedTarget.windowId || '') + (includeText ? '+text' : '');
|
|
304
|
+
if (!force && treeCache && treeCache.scope === scope && Date.now() - treeCache.at < TREE_CACHE_MS) {
|
|
305
|
+
return treeCache.rows;
|
|
306
|
+
}
|
|
307
|
+
let response = null;
|
|
308
|
+
try { response = await inspectUI({ ...(attachedTarget || {}), includeText }); } catch (error) {
|
|
309
|
+
response = { ok: false, code: 'NATIVE_ERROR', error: String(error && error.message || error) };
|
|
310
|
+
}
|
|
311
|
+
// Gli adapter storici (e i test) ritornano un array puro: resta valido.
|
|
312
|
+
const rows = Array.isArray(response) ? response
|
|
313
|
+
: (response && Array.isArray(response.rows) ? response.rows : []);
|
|
314
|
+
const failure = !Array.isArray(response) && response && response.ok === false
|
|
315
|
+
? { code: String(response.code || 'NATIVE_ERROR'), error: String(response.error || 'Lettura dei controlli non riuscita.') }
|
|
316
|
+
: null;
|
|
317
|
+
const cleaned = rows
|
|
318
|
+
.map((row, index) => cleanElement(row, index, attachedTarget && attachedTarget.windowId || ''))
|
|
319
|
+
.filter(Boolean).slice(0, 320);
|
|
320
|
+
treeCache = { rows: cleaned, at: Date.now(), scope, failure };
|
|
321
|
+
if (targetCache.size > 1200) targetCache = new Map();
|
|
322
|
+
for (const row of cleaned) targetCache.set(row.targetId, row);
|
|
323
|
+
return cleaned;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const lastReadFailure = () => treeCache && treeCache.failure ? treeCache.failure : null;
|
|
327
|
+
|
|
328
|
+
// Una sola scansione UIA valida tutti i target di una sequenza: prima ogni
|
|
329
|
+
// passaggio rilanciava PowerShell, moltiplicando la latenza proprio quando il
|
|
330
|
+
// modello aveva correttamente programmato un batch.
|
|
331
|
+
const resolveTargets = async (targetIds) => {
|
|
332
|
+
const ids = [...new Set((Array.isArray(targetIds) ? targetIds : [targetIds])
|
|
333
|
+
.map((value) => String(value || '')).filter((value) => targetCache.has(value)))];
|
|
334
|
+
const resolved = new Map();
|
|
335
|
+
if (!ids.length) return resolved;
|
|
336
|
+
// La cache descrive il momento dell inspect, non necessariamente quello
|
|
337
|
+
// dell azione. Una lettura fresca impedisce che un popup o un rerender
|
|
338
|
+
// facciano sopravvivere un target ormai rimosso.
|
|
339
|
+
const current = await readTree({ force: true });
|
|
340
|
+
for (const id of ids) {
|
|
341
|
+
const expected = targetCache.get(id);
|
|
342
|
+
let best = null; let bestScore = -1;
|
|
343
|
+
for (const row of current) {
|
|
344
|
+
const score = similarity(expected, row);
|
|
345
|
+
if (score > bestScore) { best = row; bestScore = score; }
|
|
346
|
+
}
|
|
347
|
+
// Un controllo che non esiste piu' non deve essere riutilizzato soltanto
|
|
348
|
+
// perche' era presente nella scansione precedente. Popup, navigazioni e
|
|
349
|
+
// rerender cambiano spesso il tree fra due passaggi: consegnare il target
|
|
350
|
+
// vecchio faceva finire tastiera e mouse sul controllo sbagliato.
|
|
351
|
+
if (bestScore >= 5) resolved.set(id, best);
|
|
352
|
+
}
|
|
353
|
+
return resolved;
|
|
354
|
+
};
|
|
355
|
+
|
|
356
|
+
const resolveTarget = async (targetId) => (await resolveTargets([targetId])).get(String(targetId || '')) || null;
|
|
357
|
+
|
|
358
|
+
const verification = async (baseline, verify, freshTree = false) => {
|
|
359
|
+
if (verify === 'none' || !baseline || !attachedTarget) {
|
|
360
|
+
return { verified: false, verificationRequired: true };
|
|
361
|
+
}
|
|
362
|
+
// Chi ha gia' riletto il tree DOPO l'azione (la verifica del testo scritto)
|
|
363
|
+
// non paga una seconda camminata.
|
|
364
|
+
const after = freshTree && treeCache ? treeCache.rows : await readTree({ force: true });
|
|
365
|
+
const failure = lastReadFailure();
|
|
366
|
+
if (failure) {
|
|
367
|
+
return {
|
|
368
|
+
verified: false,
|
|
369
|
+
verificationRequired: true,
|
|
370
|
+
verificationError: failure.error,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const delta = diffElements(baseline, after);
|
|
374
|
+
const changed = delta.changeCount > 0;
|
|
375
|
+
return {
|
|
376
|
+
// Un delta vuoto prova soltanto che il tree e' rimasto uguale. Non prova
|
|
377
|
+
// che il click, Invio o la navigazione abbiano raggiunto l'obiettivo.
|
|
378
|
+
verified: changed,
|
|
379
|
+
verificationRequired: !changed,
|
|
380
|
+
changeCount: delta.changeCount,
|
|
381
|
+
...(delta.changes.length ? { changes: delta.changes } : {}),
|
|
382
|
+
observation: describeChanges(delta),
|
|
383
|
+
};
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const wantsVerification = (input) => {
|
|
387
|
+
const requested = String(input && input.verify || '').toLowerCase();
|
|
388
|
+
if (requested === 'none') return 'none';
|
|
389
|
+
if (requested === 'delta') return 'delta';
|
|
390
|
+
return attachedTarget && treeCache && treeCache.rows.length ? 'delta' : 'none';
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const baselineFor = (verify) => verify === 'delta' && treeCache ? treeCache.rows : null;
|
|
394
|
+
|
|
395
|
+
const actionOutcome = async (name, result, { pointer, baseline, verify, extra = {}, freshTree = false } = {}) => {
|
|
396
|
+
stateEvent({
|
|
397
|
+
type: 'action', phase: result && result.ok === true ? 'complete' : 'error', action: name,
|
|
398
|
+
...(pointer ? { pointer } : {}), attached: attachedTarget,
|
|
399
|
+
});
|
|
400
|
+
if (!result || result.ok !== true) {
|
|
401
|
+
invalidateVisualState();
|
|
402
|
+
return fail(String(result && result.code || 'ACTION_FAILED'),
|
|
403
|
+
String(result && result.error || 'Azione non riuscita.'),
|
|
404
|
+
['computer_inspect per rileggere i controlli, poi riprova con il target aggiornato'],
|
|
405
|
+
{ action: name, ...extra });
|
|
406
|
+
}
|
|
407
|
+
const checked = await verification(baseline, verify, freshTree);
|
|
408
|
+
lastSnapshot = null;
|
|
409
|
+
currentObservation = null;
|
|
410
|
+
if (reactiveHost && typeof reactiveHost.invalidateObservation === 'function') {
|
|
411
|
+
await reactiveHost.invalidateObservation('computer_action_completed');
|
|
412
|
+
}
|
|
413
|
+
const verifiedObservation = treeCache && Array.isArray(treeCache.rows)
|
|
414
|
+
? await semanticObservation(treeCache.rows, 'computer_action_verification') : null;
|
|
415
|
+
const summary = checked.verified
|
|
416
|
+
? `${name}: ${checked.observation}.`
|
|
417
|
+
: verify === 'delta'
|
|
418
|
+
? `${name} eseguito, ma il risultato non e stato confermato.`
|
|
419
|
+
: `${name} eseguito.`;
|
|
420
|
+
const envelope = checked.verified || verify === 'none' ? ok : warn;
|
|
421
|
+
return envelope(summary,
|
|
422
|
+
{
|
|
423
|
+
action: name,
|
|
424
|
+
...extra,
|
|
425
|
+
...checked,
|
|
426
|
+
observationId: verifiedObservation && verifiedObservation.observationId || null,
|
|
427
|
+
note: checked.verified
|
|
428
|
+
? 'Esito verificato rileggendo i controlli: nessuno screenshot necessario.'
|
|
429
|
+
: 'Il comando e stato inviato, ma non c e evidenza che l obiettivo sia stato raggiunto. Verifica lo stato prima di proseguire.',
|
|
430
|
+
},
|
|
431
|
+
checked.verified ? [] : ['computer_inspect per confermare lo stato']);
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const nativeRequest = (element, request) => ({
|
|
435
|
+
windowId: attachedTarget && attachedTarget.windowId || '',
|
|
436
|
+
selector: selectorFor(element),
|
|
437
|
+
...request,
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const nativeBlocked = (native) => !!native
|
|
441
|
+
&& (native.code === 'TARGET_GONE' || native.code === 'PASSWORD' || native.code === 'DISABLED');
|
|
442
|
+
|
|
443
|
+
// Percorso nativo con ripiego a coordinate, condiviso da azioni singole e
|
|
444
|
+
// batch: prima il pattern UI Automation, poi il click sui bounds appena
|
|
445
|
+
// riletti dal tree vivo (piu' precisi di quelli in cache).
|
|
446
|
+
const nativeOrPointer = async (element, pointer, request, fallback) => {
|
|
447
|
+
const native = await uiAction(nativeRequest(element, request));
|
|
448
|
+
if (native && native.ok === true) return { ok: true, method: String(native.method || request.action) };
|
|
449
|
+
if (nativeBlocked(native)) return native;
|
|
450
|
+
const fresh = cleanBounds(native && native.bounds);
|
|
451
|
+
const spot = fresh ? pointerFor({ bounds: fresh }) : pointer;
|
|
452
|
+
if (!spot) return native && native.error ? native : { ok: false, error: 'Azione non riuscita.' };
|
|
453
|
+
const result = await fallback(spot);
|
|
454
|
+
return result && result.ok === true ? { ...result, method: 'Pointer' } : result;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
// Il testo e' arrivato nel campo? true/false quando il controllo espone un
|
|
458
|
+
// valore leggibile, null quando non c'e' nulla da confrontare (in quel caso
|
|
459
|
+
// resta valida la verifica per delta). Confronto tollerante: le app spesso
|
|
460
|
+
// normalizzano spazi o completano da sole.
|
|
461
|
+
const textLanded = async (element, text) => {
|
|
462
|
+
const expected = String(text || '').trim().toLocaleLowerCase();
|
|
463
|
+
if (!expected) return null;
|
|
464
|
+
// Lettura FRESCA: subito dopo l'input la cache del tree e' vecchia di
|
|
465
|
+
// millisecondi ma descrive lo stato precedente.
|
|
466
|
+
const rows = await readTree({ force: true });
|
|
467
|
+
let best = null; let bestScore = -1;
|
|
468
|
+
for (const row of rows) {
|
|
469
|
+
const score = similarity(element, row);
|
|
470
|
+
if (score > bestScore) { best = row; bestScore = score; }
|
|
471
|
+
}
|
|
472
|
+
const current = bestScore >= 5 ? best : null;
|
|
473
|
+
if (!current) return null;
|
|
474
|
+
const before = String(element && element.value || '').trim().toLocaleLowerCase();
|
|
475
|
+
const value = String(current.value || '').trim().toLocaleLowerCase();
|
|
476
|
+
if (!value && !before) return null;
|
|
477
|
+
if (value.includes(expected)) return true;
|
|
478
|
+
// Il caso che ingannava: il vecchio valore ("montagem") e' contenuto nel
|
|
479
|
+
// testo nuovo ("montagem rugada"). Campo invariato = testo non arrivato.
|
|
480
|
+
if (value === before) return false;
|
|
481
|
+
// Il campo e' cambiato ma non contiene tutto: lo accettiamo solo se e' un
|
|
482
|
+
// prefisso credibile di quello che volevamo (app che troncano o normalizzano).
|
|
483
|
+
return value.length >= 3 && expected.startsWith(value);
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
// Su un campo di testo "invoke" finiva su ExpandCollapse, cioe' apriva e al
|
|
487
|
+
// colpo dopo CHIUDEVA il pannello di ricerca portandosi via il cursore. Su un
|
|
488
|
+
// campo si clicca, come farebbe una persona: e' l'unico gesto che sposta il
|
|
489
|
+
// fuoco anche nel DOM delle app Chromium. Un mode esplicito vince sempre.
|
|
490
|
+
const TEXT_ENTRY_TYPES = Object.freeze(['Edit', 'ComboBox', 'SearchBox', 'Document']);
|
|
491
|
+
|
|
492
|
+
const activateMode = (element, requested) => {
|
|
493
|
+
const mode = String(requested || 'invoke').toLowerCase();
|
|
494
|
+
if (mode !== 'invoke') return mode;
|
|
495
|
+
const textField = TEXT_ENTRY_TYPES.includes(String(element && element.type || ''))
|
|
496
|
+
|| (Array.isArray(element && element.actions) && element.actions.includes('setValue'));
|
|
497
|
+
return textField ? 'click' : 'invoke';
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
const clickAt = (spot, input = {}) => performAction('click', {
|
|
501
|
+
x: spot.x, y: spot.y, windowId: attachedTarget && attachedTarget.windowId,
|
|
502
|
+
button: String(input.button || 'left'), clicks: integer(input.clicks, 1, 3, 1),
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
const fillAt = (spot, { text, clear, submit }) => performAction('fill', {
|
|
506
|
+
x: spot.x, y: spot.y, windowId: attachedTarget && attachedTarget.windowId,
|
|
507
|
+
text, ...(clear ? { clear: true } : {}), submit: submit === true,
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
const refreshAttached = async () => {
|
|
511
|
+
if (!attachedTarget) return true;
|
|
512
|
+
const current = (await desktop.windowsNow()).find((row) => row.windowId === attachedTarget.windowId);
|
|
513
|
+
if (!current) {
|
|
514
|
+
attachedTarget = null; targetCache = new Map(); invalidateVisualState();
|
|
515
|
+
if (reactiveHost && typeof reactiveHost.clearTarget === 'function') reactiveHost.clearTarget();
|
|
516
|
+
return false;
|
|
517
|
+
}
|
|
518
|
+
attachedTarget = {
|
|
519
|
+
...current,
|
|
520
|
+
applicationId: attachedTarget.applicationId || '',
|
|
521
|
+
applicationName: attachedTarget.applicationName || '',
|
|
522
|
+
icon: attachedTarget.icon || '',
|
|
523
|
+
};
|
|
524
|
+
return true;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
const attachWindow = async (match, applications) => {
|
|
528
|
+
const application = (Array.isArray(applications) ? applications : [])
|
|
529
|
+
.find((row) => row.process.toLocaleLowerCase() === match.process.toLocaleLowerCase());
|
|
530
|
+
if (application) await desktop.hydrateApplicationIcons([application]);
|
|
531
|
+
attachedTarget = {
|
|
532
|
+
...match,
|
|
533
|
+
applicationId: application && application.applicationId || '',
|
|
534
|
+
applicationName: application && application.name || applicationName(match.process, [match]),
|
|
535
|
+
icon: application && application.icon || '',
|
|
536
|
+
};
|
|
537
|
+
targetCache = new Map();
|
|
538
|
+
// armTarget e' il reset autorevole. Evitiamo una invalidate fire-and-
|
|
539
|
+
// forget che poteva arrivare dopo arm/checkpoint e cancellare il frame
|
|
540
|
+
// appena creato.
|
|
541
|
+
invalidateVisualState('target_rearming', false);
|
|
542
|
+
// Un target nel tray o minimizzato deve diventare realmente catturabile
|
|
543
|
+
// prima dello snapshot; l'attach e' gia' coperto dal consenso esplicito.
|
|
544
|
+
const focus = await performAction('focusWindow', { windowId: match.windowId });
|
|
545
|
+
if (focus && focus.ok === false) {
|
|
546
|
+
attachedTarget = null;
|
|
547
|
+
if (reactiveHost && typeof reactiveHost.clearTarget === 'function') reactiveHost.clearTarget();
|
|
548
|
+
return fail('FOREGROUND_LOST', String(focus.error || 'Non riesco a ripristinare la finestra selezionata.'),
|
|
549
|
+
['Chiedi all utente di portare la finestra in primo piano, poi richiama computer_attach']);
|
|
550
|
+
}
|
|
551
|
+
// Una finestra minimizzata viene enumerata con bounds fittizi (-25600) e
|
|
552
|
+
// minimized: true. Restituire quella fotografia DOPO averla ripristinata
|
|
553
|
+
// faceva credere al modello che l'attach non avesse funzionato e lo
|
|
554
|
+
// costringeva a un secondo attach identico. Si rilegge SOLO quando c'era
|
|
555
|
+
// davvero qualcosa da ripristinare: una finestra gia' visibile non paga
|
|
556
|
+
// una seconda enumerazione.
|
|
557
|
+
if (match.minimized === true || match.bounds.x <= -20000) {
|
|
558
|
+
for (const delay of [80, 260]) {
|
|
559
|
+
desktop.invalidate();
|
|
560
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
561
|
+
if (!(await refreshAttached())) {
|
|
562
|
+
return fail('WINDOW_CLOSED', 'La finestra e stata chiusa durante il collegamento.',
|
|
563
|
+
['computer_processes per scegliere un nuovo target']);
|
|
564
|
+
}
|
|
565
|
+
if (!attachedTarget.minimized && attachedTarget.bounds.x > -20000) break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (reactiveHost) {
|
|
569
|
+
if (typeof reactiveHost.armTarget === 'function') {
|
|
570
|
+
const armed = await reactiveHost.armTarget({
|
|
571
|
+
windowId: attachedTarget.windowId,
|
|
572
|
+
windowTitle: attachedTarget.title,
|
|
573
|
+
windowBounds: attachedTarget.bounds,
|
|
574
|
+
});
|
|
575
|
+
if (armed && armed.ok === false) {
|
|
576
|
+
lastObservationError = {
|
|
577
|
+
code: String(armed.code || 'RAC_ARM_FAILED'),
|
|
578
|
+
error: String(armed.error || ''),
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
} else if (typeof reactiveHost.setTarget === 'function') {
|
|
582
|
+
reactiveHost.setTarget(attachedTarget.windowId);
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
let observation = null;
|
|
586
|
+
try {
|
|
587
|
+
const shot = await captureDesktop({
|
|
588
|
+
windowId: attachedTarget.windowId,
|
|
589
|
+
windowTitle: attachedTarget.title,
|
|
590
|
+
windowBounds: attachedTarget.bounds,
|
|
591
|
+
racObservation: true,
|
|
592
|
+
});
|
|
593
|
+
observation = await visualObservation(shot, 'computer_attach');
|
|
594
|
+
} catch (_) { /* l'attach semantico resta valido, RAC richiedera uno snapshot */ }
|
|
595
|
+
stateEvent({ type: 'attach', attached: attachedTarget });
|
|
596
|
+
if (externalControllerEnabled && typeof reactiveFastLane.arm === 'function') {
|
|
597
|
+
try { await reactiveFastLane.arm(); } catch (_) { /* stato esposto da computer_reactive_status */ }
|
|
598
|
+
}
|
|
599
|
+
const attachNext = externalControllerEnabled
|
|
600
|
+
? ['computer_reactive_goal con il risultato finale richiesto']
|
|
601
|
+
: ['computer_control con observationId e grammatica ACTION; continua con il nextObservationId restituito'];
|
|
602
|
+
return ok(`Collegato a ${attachedTarget.applicationName || attachedTarget.title || attachedTarget.process}.`,
|
|
603
|
+
{
|
|
604
|
+
attached: publicTarget(attachedTarget),
|
|
605
|
+
observationId: observation && observation.observationId || null,
|
|
606
|
+
...(!observation && lastObservationError ? { racObservationError: lastObservationError } : {}),
|
|
607
|
+
note: externalControllerEnabled
|
|
608
|
+
? 'Finestra collegata al controller visuale esterno opzionale.'
|
|
609
|
+
: 'Finestra collegata. Il modello corrente e il controller RAC: usa computer_control e il nextObservationId restituito.',
|
|
610
|
+
},
|
|
611
|
+
attachNext);
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
// Coordinate valide solo se ancorate allo screenshot corrente: il modello
|
|
615
|
+
// ragiona sui pixel dell'immagine ricevuta, non sul monitor reale.
|
|
616
|
+
const snapshotPointer = (input) => {
|
|
617
|
+
if (!lastSnapshot) {
|
|
618
|
+
return { error: fail('NO_SNAPSHOT', 'Serve un computer_snapshot aggiornato prima di usare coordinate.',
|
|
619
|
+
['computer_inspect per agire senza coordinate', 'computer_snapshot con include_image true se serve vedere']) };
|
|
620
|
+
}
|
|
621
|
+
if (input && input.snapshot_id && input.snapshot_id !== lastSnapshot.id) {
|
|
622
|
+
return { error: fail('STALE_SNAPSHOT', 'Lo screenshot indicato non e piu quello corrente.',
|
|
623
|
+
['computer_snapshot per acquisirne uno nuovo']) };
|
|
624
|
+
}
|
|
625
|
+
if (Date.now() - lastSnapshot.capturedAt > 90000) {
|
|
626
|
+
invalidateVisualState();
|
|
627
|
+
return { error: fail('STALE_SNAPSHOT', 'Lo screenshot e troppo vecchio.',
|
|
628
|
+
['computer_snapshot per acquisirne uno nuovo prima di agire']) };
|
|
629
|
+
}
|
|
630
|
+
const sourceX = Number(input && input.x); const sourceY = Number(input && input.y);
|
|
631
|
+
if (!Number.isFinite(sourceX) || !Number.isFinite(sourceY)) {
|
|
632
|
+
return { error: fail('BAD_INPUT', 'Coordinate non valide.', ['Passa x e y in pixel dello screenshot corrente']) };
|
|
633
|
+
}
|
|
634
|
+
if (sourceX < 0 || sourceY < 0 || sourceX > lastSnapshot.width || sourceY > lastSnapshot.height) {
|
|
635
|
+
return { error: fail('OUT_OF_BOUNDS', 'Le coordinate sono fuori dallo screenshot corrente.',
|
|
636
|
+
['Rileggi lo screenshot e usa coordinate al suo interno']) };
|
|
637
|
+
}
|
|
638
|
+
// Se la cattura e' ridimensionata, convertiamo alle coordinate reali.
|
|
639
|
+
return {
|
|
640
|
+
pointer: {
|
|
641
|
+
x: Math.round(lastSnapshot.bounds.x + (sourceX * lastSnapshot.bounds.width / lastSnapshot.width)),
|
|
642
|
+
y: Math.round(lastSnapshot.bounds.y + (sourceY * lastSnapshot.bounds.height / lastSnapshot.height)),
|
|
643
|
+
},
|
|
644
|
+
};
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const missingTarget = (hint) => fail('TARGET_UNKNOWN',
|
|
648
|
+
'Questo target_id non appartiene a una lettura corrente.',
|
|
649
|
+
[hint || 'computer_inspect per rileggere i controlli e ottenere target_id validi']);
|
|
650
|
+
|
|
651
|
+
const rearmReactiveTarget = async (reason = 'rac_recovery') => {
|
|
652
|
+
currentObservation = null;
|
|
653
|
+
lastObservationError = null;
|
|
654
|
+
if (!reactiveHost || typeof reactiveHost.rearmTarget !== 'function') return null;
|
|
655
|
+
const rearmed = await reactiveHost.rearmTarget(reason);
|
|
656
|
+
if (!rearmed || rearmed.ok !== true) {
|
|
657
|
+
lastObservationError = {
|
|
658
|
+
code: String(rearmed && rearmed.code || 'RAC_REARM_FAILED'),
|
|
659
|
+
error: String(rearmed && rearmed.error || ''),
|
|
660
|
+
};
|
|
661
|
+
return null;
|
|
662
|
+
}
|
|
663
|
+
return capturePostActionObservation().catch(() => null);
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
// MCP-RAC CLI: il modello emette direttamente la grammatica ACTION. Il
|
|
667
|
+
// valore della stringa viene letto dai delta JSON del provider e ogni
|
|
668
|
+
// segmento delimitato da `;` puo essere committato prima che la tool call sia
|
|
669
|
+
// terminata. Non esiste un percorso steps implicito quando il protocollo e'
|
|
670
|
+
// obbligatorio: un errore di grammatica deve essere visibile e correggibile.
|
|
671
|
+
const executeRacCommand = async (command, ctx, toolName = 'computer_control') => {
|
|
672
|
+
const stream = String(command || '');
|
|
673
|
+
if (!stream) {
|
|
674
|
+
return JSON.stringify(fail('RAC_COMMAND_REQUIRED', 'Serve un comando MCP-RAC ACTION completo.',
|
|
675
|
+
['Usa l observationId corrente e la grammatica indicata da computer_control']));
|
|
676
|
+
}
|
|
677
|
+
if (!reactiveToolStream) {
|
|
678
|
+
return JSON.stringify(fail('RAC_UNAVAILABLE', 'Il canale MCP-RAC non e disponibile in questo run.',
|
|
679
|
+
['Riavvia Horizon e riprova nella modalita Work o Code']));
|
|
680
|
+
}
|
|
681
|
+
if (!attachedTarget || !/^\d+$/.test(String(attachedTarget.windowId || ''))) {
|
|
682
|
+
await reactiveToolStream.discard('no_attached_window');
|
|
683
|
+
return JSON.stringify(fail('NO_ATTACHED_WINDOW', 'MCP-RAC richiede una finestra collegata.',
|
|
684
|
+
['computer_processes e computer_attach, poi computer_control']));
|
|
685
|
+
}
|
|
686
|
+
if (!currentObservation || !/^\d+$/.test(String(currentObservation.observationId || ''))) {
|
|
687
|
+
await reactiveToolStream.discard('observation_required');
|
|
688
|
+
return JSON.stringify(fail('OBSERVATION_REQUIRED', 'MCP-RAC richiede una osservazione autorevole corrente.',
|
|
689
|
+
racStrictMode
|
|
690
|
+
? ['computer_control_reset per ottenere un observationId fresco, poi computer_control']
|
|
691
|
+
: ['computer_snapshot oppure computer_inspect, poi computer_control']));
|
|
692
|
+
}
|
|
693
|
+
const allowed = await authorized(ctx, 'Esegui il comando MCP-RAC nella finestra collegata');
|
|
694
|
+
if (!allowed) {
|
|
695
|
+
await reactiveToolStream.discard('consent_denied');
|
|
696
|
+
return JSON.stringify(denied('Controllo MCP-RAC'));
|
|
697
|
+
}
|
|
698
|
+
const presentedInput = racCommandPreview(stream);
|
|
699
|
+
stateEvent({ type: 'action', phase: 'start', action: toolName, attached: attachedTarget,
|
|
700
|
+
input: presentedInput });
|
|
701
|
+
// Ring a 5 Hz durante l'azione: i frame catturati mentre l'ACTION esegue
|
|
702
|
+
// diventano UN Temporal Visual Packet (frame finale + timeline + zone
|
|
703
|
+
// cambiate) allegato al risultato: una sola osservazione temporale del
|
|
704
|
+
// movimento invece di piu' chiamate visive. I frame RAC arrivano gia'
|
|
705
|
+
// invertiti e normalizzati 1280x720 dal main.
|
|
706
|
+
const motionFrames = [];
|
|
707
|
+
let motionTimer = null;
|
|
708
|
+
if (visualFrame && typeof visualFrame.pacchettoDaPngList === 'function' && attachedTarget) {
|
|
709
|
+
const catturaMovimento = async () => {
|
|
710
|
+
if (motionFrames.length >= 10 || !attachedTarget) return;
|
|
711
|
+
try {
|
|
712
|
+
const frame = await captureDesktop({
|
|
713
|
+
windowId: attachedTarget.windowId,
|
|
714
|
+
windowTitle: attachedTarget.title,
|
|
715
|
+
windowBounds: attachedTarget.bounds,
|
|
716
|
+
racObservation: true,
|
|
717
|
+
});
|
|
718
|
+
if (frame && frame.data) motionFrames.push(String(frame.data));
|
|
719
|
+
} catch (_) { /* un frame perso non ferma l'azione */ }
|
|
720
|
+
};
|
|
721
|
+
void catturaMovimento();
|
|
722
|
+
motionTimer = setInterval(catturaMovimento, 200);
|
|
723
|
+
try { motionTimer.unref(); } catch (_) { /* timer di processo */ }
|
|
724
|
+
}
|
|
725
|
+
const fermaMovimento = () => {
|
|
726
|
+
if (motionTimer) clearInterval(motionTimer);
|
|
727
|
+
motionTimer = null;
|
|
728
|
+
};
|
|
729
|
+
try {
|
|
730
|
+
const result = await reactiveToolStream.execute(stream, {
|
|
731
|
+
signal: ctx && ctx.signal,
|
|
732
|
+
tool: toolName,
|
|
733
|
+
});
|
|
734
|
+
if (!result || result.ok !== true) {
|
|
735
|
+
fermaMovimento();
|
|
736
|
+
const recovered = await rearmReactiveTarget('rac_action_failed');
|
|
737
|
+
stateEvent({ type: 'action', phase: 'error', action: toolName, attached: attachedTarget,
|
|
738
|
+
input: presentedInput });
|
|
739
|
+
return JSON.stringify(fail('RAC_STOPPED', `MCP-RAC arrestato: ${String(result && result.error || 'errore sconosciuto')}.`,
|
|
740
|
+
recovered
|
|
741
|
+
? ['Continua con computer_control usando observationId']
|
|
742
|
+
: (racStrictMode
|
|
743
|
+
? ['computer_control_reset per riarmare RAC']
|
|
744
|
+
: ['computer_control_reset per riarmare RAC',
|
|
745
|
+
'computer_inspect e computer_batch come fallback UI Automation']),
|
|
746
|
+
{ rearmed: !!recovered, observationId: recovered && recovered.observation.observationId || null }));
|
|
747
|
+
}
|
|
748
|
+
// La ACTION conclusa non autorizza una seconda traiettoria cieca. Cattura
|
|
749
|
+
// subito il frame successivo e lo restituisce nella medesima tool result,
|
|
750
|
+
// evitando il giro screenshot/inspect osservato nelle sessioni reali.
|
|
751
|
+
currentObservation = null;
|
|
752
|
+
const nextFrame = await capturePostActionObservation().catch(() => null);
|
|
753
|
+
fermaMovimento();
|
|
754
|
+
if (!nextFrame) currentObservation = null;
|
|
755
|
+
// Il movimento in UNA immagine: se il ring ha almeno due frame, il media
|
|
756
|
+
// per il modello diventa il Temporal Visual Packet (frame finale +
|
|
757
|
+
// timeline + zone cambiate); la UI (_meta) resta il frame reale pulito.
|
|
758
|
+
const motionPacket = motionFrames.length >= 2 && visualFrame
|
|
759
|
+
&& typeof visualFrame.pacchettoDaPngList === 'function'
|
|
760
|
+
? visualFrame.pacchettoDaPngList(motionFrames, { intervalloMs: 200 })
|
|
761
|
+
: null;
|
|
762
|
+
stateEvent({ type: 'action', phase: 'complete', action: toolName, attached: attachedTarget,
|
|
763
|
+
input: presentedInput });
|
|
764
|
+
const structuredContent = ok('Comando MCP-RAC completato; input rilasciati.', {
|
|
765
|
+
action: toolName,
|
|
766
|
+
mode: 'reactive',
|
|
767
|
+
transport: result.transport,
|
|
768
|
+
actionObservationId: result.observationId || null,
|
|
769
|
+
observationId: nextFrame && nextFrame.observation.observationId || null,
|
|
770
|
+
nextObservationId: nextFrame && nextFrame.observation.observationId || null,
|
|
771
|
+
firstDispatchAt: result.firstDispatchAt || null,
|
|
772
|
+
telemetry: result.telemetry || null,
|
|
773
|
+
capabilities: result.capabilities || null,
|
|
774
|
+
...(motionPacket ? {
|
|
775
|
+
temporal_packet: motionPacket.metadati,
|
|
776
|
+
temporal_packet_note: motionPacket.nota,
|
|
777
|
+
...(visualFrame.attiva() ? { inverted: true } : {}),
|
|
778
|
+
} : {}),
|
|
779
|
+
}, nextFrame
|
|
780
|
+
? ['Continua direttamente con computer_control usando nextObservationId; non chiamare inspect o snapshot.']
|
|
781
|
+
: ['La cattura automatica non e riuscita: usa computer_snapshot o computer_inspect prima della prossima ACTION.']);
|
|
782
|
+
if (toolName !== 'computer_control') return JSON.stringify(structuredContent);
|
|
783
|
+
return {
|
|
784
|
+
output: JSON.stringify(structuredContent),
|
|
785
|
+
structuredContent,
|
|
786
|
+
...(nextFrame ? {
|
|
787
|
+
_meta: { 'vascend/computer-use': { image: {
|
|
788
|
+
type: 'image', mimeType: 'image/png', data: String(nextFrame.shot.data),
|
|
789
|
+
} } },
|
|
790
|
+
} : {}),
|
|
791
|
+
...(motionPacket
|
|
792
|
+
? { media: [{ mime: 'image/png', data: String(motionPacket.data), source: 'computer-temporal-packet' }] }
|
|
793
|
+
: (nextFrame
|
|
794
|
+
? { media: [{ mime: 'image/png', data: String(nextFrame.shot.data), source: 'computer-control-next-frame' }] }
|
|
795
|
+
: {})),
|
|
796
|
+
};
|
|
797
|
+
} catch (error) {
|
|
798
|
+
fermaMovimento();
|
|
799
|
+
await reactiveToolStream.interrupt(String(error && error.message || 'reactive_error'));
|
|
800
|
+
const recovered = await rearmReactiveTarget('rac_exception');
|
|
801
|
+
stateEvent({ type: 'action', phase: 'error', action: toolName, attached: attachedTarget,
|
|
802
|
+
input: presentedInput });
|
|
803
|
+
return JSON.stringify(fail('RAC_STOPPED', `MCP-RAC arrestato: ${String(error && error.message || error)}.`,
|
|
804
|
+
recovered
|
|
805
|
+
? ['Continua con computer_control usando observationId']
|
|
806
|
+
: (racStrictMode
|
|
807
|
+
? ['computer_control_reset per riarmare RAC']
|
|
808
|
+
: ['computer_control_reset per riarmare RAC', 'computer_batch come fallback UI Automation']),
|
|
809
|
+
{ rearmed: !!recovered, observationId: recovered && recovered.observation.observationId || null }));
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// Per tastiera e mouse non chiediamo al modello di compilare manualmente una
|
|
814
|
+
// frase in keyset da 33 ms. Le operazioni restano nel medesimo host RAC,
|
|
815
|
+
// vincolate a finestra e observationId, ma Ctrl+A + testo diventa una sola
|
|
816
|
+
// chiamata deterministica invece di una catena di tentativi e screenshot.
|
|
817
|
+
const executeRacAtomicInput = async (operations, ctx) => {
|
|
818
|
+
const normalized = normalizeAtomicOperations(operations);
|
|
819
|
+
if (!normalized.ok) {
|
|
820
|
+
return JSON.stringify(fail(normalized.code, normalized.error,
|
|
821
|
+
['Usa operations con type shortcut, text, mouse o click.']));
|
|
822
|
+
}
|
|
823
|
+
if (!reactiveHost || typeof reactiveHost.atomicInput !== 'function') {
|
|
824
|
+
return JSON.stringify(fail('RAC_UNAVAILABLE', 'Il controllo nativo di tastiera e mouse non e disponibile.',
|
|
825
|
+
['Riavvia Horizon per caricare il nuovo host RAC.']));
|
|
826
|
+
}
|
|
827
|
+
if (!attachedTarget || !/^\d+$/.test(String(attachedTarget.windowId || ''))) {
|
|
828
|
+
return JSON.stringify(fail('NO_ATTACHED_WINDOW', 'Collega prima la finestra da controllare.',
|
|
829
|
+
['computer_processes e computer_attach']));
|
|
830
|
+
}
|
|
831
|
+
if (!currentObservation || !/^\d+$/.test(String(currentObservation.observationId || ''))) {
|
|
832
|
+
return JSON.stringify(fail('OBSERVATION_REQUIRED', 'Serve una osservazione autorevole corrente.',
|
|
833
|
+
['computer_control_reset per ottenere un observationId fresco']));
|
|
834
|
+
}
|
|
835
|
+
const allowed = await authorized(ctx, 'Usa tastiera e mouse nella finestra collegata');
|
|
836
|
+
if (!allowed) return JSON.stringify(denied('Controllo MCP-RAC'));
|
|
837
|
+
if (reactiveToolStream) await reactiveToolStream.discard('atomic_input');
|
|
838
|
+
const presentedInput = inputPreview(normalized.operations);
|
|
839
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_control', attached: attachedTarget,
|
|
840
|
+
input: presentedInput });
|
|
841
|
+
const actionObservationId = String(currentObservation.observationId);
|
|
842
|
+
const result = await reactiveHost.atomicInput({
|
|
843
|
+
windowId: String(attachedTarget.windowId),
|
|
844
|
+
observationId: actionObservationId,
|
|
845
|
+
operations: normalized.operations,
|
|
846
|
+
});
|
|
847
|
+
if (!result || result.ok !== true) {
|
|
848
|
+
const code = String(result && result.code || 'RAC_INPUT_FAILED');
|
|
849
|
+
atomicFailure = { code, count: atomicFailure.code === code ? atomicFailure.count + 1 : 1 };
|
|
850
|
+
const recoverable = ['STALE_OBSERVATION', 'OBSERVATION_REQUIRED', 'SESSION_EXPIRED', 'SAFETY_STOP'].includes(code);
|
|
851
|
+
const recovered = recoverable && atomicFailure.count === 1
|
|
852
|
+
? await rearmReactiveTarget('rac_atomic_input_failed') : null;
|
|
853
|
+
stateEvent({ type: 'action', phase: 'error', action: 'computer_control', attached: attachedTarget,
|
|
854
|
+
input: presentedInput });
|
|
855
|
+
const deterministic = !recoverable;
|
|
856
|
+
return JSON.stringify(fail(code,
|
|
857
|
+
String(result && result.error || 'Input nativo non eseguito.'),
|
|
858
|
+
recovered ? ['Continua una sola volta con computer_control usando il nuovo observationId']
|
|
859
|
+
: deterministic
|
|
860
|
+
? ['Non ripetere la stessa azione e non acquisire altri screenshot: segnala il guasto nativo.']
|
|
861
|
+
: ['computer_control_reset una sola volta'],
|
|
862
|
+
{ observationId: recovered && recovered.observation.observationId || null,
|
|
863
|
+
retryable: !!recovered, repeatedFailureCount: atomicFailure.count,
|
|
864
|
+
circuitOpen: deterministic || atomicFailure.count > 1 }));
|
|
865
|
+
}
|
|
866
|
+
atomicFailure = { code: '', count: 0 };
|
|
867
|
+
currentObservation = null;
|
|
868
|
+
const nextFrame = await capturePostActionObservation().catch(() => null);
|
|
869
|
+
if (!nextFrame) currentObservation = null;
|
|
870
|
+
stateEvent({ type: 'action', phase: 'complete', action: 'computer_control', attached: attachedTarget,
|
|
871
|
+
input: presentedInput });
|
|
872
|
+
const structuredContent = ok('Input RAC eseguito con tastiera e mouse nativi.', {
|
|
873
|
+
action: 'computer_control', mode: 'reactive', transport: 'native-atomic',
|
|
874
|
+
operationCount: result.operationCount,
|
|
875
|
+
actionObservationId,
|
|
876
|
+
observationId: nextFrame && nextFrame.observation.observationId || null,
|
|
877
|
+
nextObservationId: nextFrame && nextFrame.observation.observationId || null,
|
|
878
|
+
telemetry: result.telemetry || null,
|
|
879
|
+
capabilities: result.capabilities || null,
|
|
880
|
+
}, nextFrame
|
|
881
|
+
? ['Continua con computer_control usando il frame e nextObservationId restituiti.']
|
|
882
|
+
: ['computer_control_reset per acquisire una nuova osservazione.']);
|
|
883
|
+
return {
|
|
884
|
+
output: JSON.stringify(structuredContent), structuredContent,
|
|
885
|
+
...(nextFrame ? {
|
|
886
|
+
_meta: { 'vascend/computer-use': { image: {
|
|
887
|
+
type: 'image', mimeType: 'image/png', data: String(nextFrame.shot.data),
|
|
888
|
+
} } },
|
|
889
|
+
media: [{ mime: 'image/png', data: String(nextFrame.shot.data), source: 'computer-control-next-frame' }],
|
|
890
|
+
} : {}),
|
|
891
|
+
};
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const tools = [
|
|
895
|
+
{
|
|
896
|
+
name: 'computer_processes',
|
|
897
|
+
direct: true,
|
|
898
|
+
description: 'Cerca applicazioni, processi e finestre reali del desktop, incluse quelle minimizzate o nel tray. Passa sempre query quando l utente nomina un app (es. Spotify o Claude): il risultato e ordinato per corrispondenza esatta e non dipende dall ordine o dal troncamento dell elenco generale. Poi usa computer_attach con il window_id restituito; se l app non risulta avviata usa computer_launch. Non legge memoria o command line dei processi.',
|
|
899
|
+
inputSchema: { type: 'object', properties: {
|
|
900
|
+
query: { type: 'string', description: 'Nome app/processo o parte del titolo, ad esempio Spotify, spotify.exe o Claude.' },
|
|
901
|
+
limit: { type: 'number', description: 'Numero massimo di risultati per sezione (1-50, default 20 con query).' },
|
|
902
|
+
} },
|
|
903
|
+
handler: async (input, ctx) => {
|
|
904
|
+
const allowed = await authorized(ctx, 'Visualizza le applicazioni e le finestre aperte sul computer');
|
|
905
|
+
if (!allowed) return JSON.stringify(denied('Accesso a Computer Use'));
|
|
906
|
+
stateEvent({ type: 'processes', phase: 'start' });
|
|
907
|
+
const { applications, processes, windows } = await desktop.targetsNow((progress) => {
|
|
908
|
+
stateEvent({ type: 'processes', ...progress, windowCount: progress.windows.length });
|
|
909
|
+
});
|
|
910
|
+
const query = String(input && input.query || '').trim().slice(0, 160);
|
|
911
|
+
const limit = integer(input && input.limit, 1, 50, query ? 20 : 24);
|
|
912
|
+
const matchedApplications = searchedTargets(applications, query, limit,
|
|
913
|
+
(row) => [row.name, row.process, row.titles]);
|
|
914
|
+
const matchedProcesses = searchedTargets(processes, query, query ? limit : Math.min(48, limit * 2),
|
|
915
|
+
(row) => [row.process, row.title]);
|
|
916
|
+
const matchedWindows = searchedTargets(windows, query, query ? limit : Math.min(48, limit * 2),
|
|
917
|
+
(row) => [row.process, row.title]);
|
|
918
|
+
// Le icone sono UI-only e relativamente costose da estrarre dagli exe:
|
|
919
|
+
// basta anticipare quelle dei primi risultati controllabili. L'app scelta
|
|
920
|
+
// viene comunque idratata sempre durante computer_attach.
|
|
921
|
+
await desktop.hydrateApplicationIcons(matchedApplications
|
|
922
|
+
.filter((application) => application.controllable).slice(0, query ? 8 : 6));
|
|
923
|
+
stateEvent({
|
|
924
|
+
type: 'processes', phase: 'complete', query,
|
|
925
|
+
applicationCount: matchedApplications.length, processCount: matchedProcesses.length,
|
|
926
|
+
windowCount: matchedWindows.length, totalApplicationCount: applications.length,
|
|
927
|
+
totalProcessCount: processes.length, totalWindowCount: windows.length,
|
|
928
|
+
applications: matchedApplications.map(uiApplication), processes: matchedProcesses, windows: matchedWindows,
|
|
929
|
+
});
|
|
930
|
+
const matched = matchedApplications.length + matchedProcesses.length + matchedWindows.length > 0;
|
|
931
|
+
const payload = {
|
|
932
|
+
trust: 'UNTRUSTED_DESKTOP_DATA',
|
|
933
|
+
query,
|
|
934
|
+
matched,
|
|
935
|
+
attached: publicTarget(attachedTarget),
|
|
936
|
+
applicationCount: matchedApplications.length,
|
|
937
|
+
processCount: matchedProcesses.length,
|
|
938
|
+
windowCount: matchedWindows.length,
|
|
939
|
+
totalApplicationCount: applications.length,
|
|
940
|
+
totalProcessCount: processes.length,
|
|
941
|
+
totalWindowCount: windows.length,
|
|
942
|
+
applications: matchedApplications.map(publicApplication),
|
|
943
|
+
processes: matchedProcesses,
|
|
944
|
+
windows: matchedWindows,
|
|
945
|
+
};
|
|
946
|
+
if (query && !matched) {
|
|
947
|
+
return JSON.stringify(warn(`Nessun processo o finestra corrisponde a "${query}".`,
|
|
948
|
+
{ ...payload, hint: `Nessun processo o finestra corrisponde a "${query}". Verifica che l applicazione sia avviata.` },
|
|
949
|
+
[`computer_launch con app "${query}" per avviarla`, 'computer_processes senza query per vedere cosa e aperto']));
|
|
950
|
+
}
|
|
951
|
+
return JSON.stringify(ok(query
|
|
952
|
+
? `${matchedApplications.length} applicazioni corrispondono a "${query}".`
|
|
953
|
+
: `${applications.length} applicazioni e ${windows.length} finestre rilevate.`,
|
|
954
|
+
payload, ['computer_attach con il window_id scelto']));
|
|
955
|
+
},
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
name: 'computer_launch',
|
|
959
|
+
description: 'Avvia una applicazione gia installata cercandola fra le scorciatoie del menu Start e, quando la finestra compare, la collega da sola. Usalo quando computer_processes non trova l app perche non e in esecuzione: senza questo passo la catena si interrompe. Non installa nulla e non esegue comandi arbitrari.',
|
|
960
|
+
inputSchema: { type: 'object', properties: {
|
|
961
|
+
app: { type: 'string', description: 'Nome dell applicazione, ad esempio Spotify, Blocco note, Visual Studio Code.' },
|
|
962
|
+
wait_ms: { type: 'number', description: 'Attesa della comparsa della finestra (1000-20000, default 9000).' },
|
|
963
|
+
}, required: ['app'] },
|
|
964
|
+
handler: async (input, ctx) => {
|
|
965
|
+
const app = String(input && input.app || '').trim().slice(0, 120);
|
|
966
|
+
if (!app) return JSON.stringify(fail('BAD_INPUT', 'Nome applicazione mancante.', ['Indica app, ad esempio "Spotify"']));
|
|
967
|
+
if (!launchApp) {
|
|
968
|
+
return JSON.stringify(fail('UNSUPPORTED', 'Questa integrazione non puo avviare applicazioni.',
|
|
969
|
+
['computer_processes per collegare una applicazione gia aperta']));
|
|
970
|
+
}
|
|
971
|
+
const allowed = await authorized(ctx, `Avvia l applicazione ${app} sul computer`);
|
|
972
|
+
if (!allowed) return JSON.stringify(denied('Avvio di una applicazione'));
|
|
973
|
+
const before = new Set((await desktop.windowsNow()).map((row) => row.windowId));
|
|
974
|
+
let candidates = [];
|
|
975
|
+
if (listInstalledApps) {
|
|
976
|
+
let entries = [];
|
|
977
|
+
try { entries = await listInstalledApps(); } catch (_) { entries = []; }
|
|
978
|
+
candidates = startMenuMatches(entries, app, 4);
|
|
979
|
+
}
|
|
980
|
+
stateEvent({ type: 'launch', phase: 'start', app });
|
|
981
|
+
const launched = await launchApp({ target: candidates.length ? candidates[0].path : app });
|
|
982
|
+
if (!launched || launched.ok !== true) {
|
|
983
|
+
stateEvent({ type: 'launch', phase: 'error', app });
|
|
984
|
+
return JSON.stringify(fail('LAUNCH_FAILED', String(launched && launched.error || `Non riesco ad avviare ${app}.`),
|
|
985
|
+
['computer_processes per cercarla con un altro nome', 'Chiedi all utente di aprirla manualmente'],
|
|
986
|
+
candidates.length ? { candidates: candidates.map((row) => row.name) } : {}));
|
|
987
|
+
}
|
|
988
|
+
const waitMs = integer(input && input.wait_ms, 1000, MAX_LAUNCH_WAIT_MS, 9000);
|
|
989
|
+
const deadline = Date.now() + waitMs;
|
|
990
|
+
let fresh = [];
|
|
991
|
+
while (Date.now() < deadline && !(ctx && ctx.signal && ctx.signal.aborted)) {
|
|
992
|
+
await new Promise((resolve) => setTimeout(resolve, 700));
|
|
993
|
+
desktop.invalidate();
|
|
994
|
+
const rows = await desktop.windowsNow();
|
|
995
|
+
fresh = searchedTargets(rows.filter((row) => !before.has(row.windowId)), app, 6,
|
|
996
|
+
(row) => [row.process, row.title]);
|
|
997
|
+
if (fresh.length) break;
|
|
998
|
+
// Alcune applicazioni riusano una finestra esistente invece di
|
|
999
|
+
// crearne una: se ne resta una sola che corrisponde, e' il target.
|
|
1000
|
+
const existing = searchedTargets(rows.filter((row) => row.visible), app, 6,
|
|
1001
|
+
(row) => [row.process, row.title]);
|
|
1002
|
+
if (existing.length === 1) { fresh = existing; break; }
|
|
1003
|
+
}
|
|
1004
|
+
stateEvent({ type: 'launch', phase: 'complete', app, windowCount: fresh.length });
|
|
1005
|
+
if (!fresh.length) {
|
|
1006
|
+
return JSON.stringify(warn(`${app} avviata, ma nessuna finestra e ancora comparsa.`,
|
|
1007
|
+
{ app, launched: true },
|
|
1008
|
+
['computer_wait con for_title per attenderla', 'computer_processes con query per collegarla']));
|
|
1009
|
+
}
|
|
1010
|
+
if (fresh.length > 1) {
|
|
1011
|
+
return JSON.stringify(warn(`${app} avviata: ${fresh.length} finestre disponibili.`,
|
|
1012
|
+
{ app, launched: true,
|
|
1013
|
+
candidates: fresh.map((row) => ({ windowId: row.windowId, title: row.title, process: row.process })) },
|
|
1014
|
+
['computer_attach con il window_id scelto']));
|
|
1015
|
+
}
|
|
1016
|
+
const { applications } = await desktop.targetsNow();
|
|
1017
|
+
const attached = await attachWindow(fresh[0], applications);
|
|
1018
|
+
return JSON.stringify(attached.ok
|
|
1019
|
+
? { ...attached, app, launched: true, summary: `${app} avviata e collegata.` }
|
|
1020
|
+
: attached);
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: 'computer_attach',
|
|
1025
|
+
direct: true,
|
|
1026
|
+
description: 'Collega Computer Use a una applicazione, finestra o processo rilevato. Ripristina anche una finestra minimizzata/nascosta. Ispezione semantica, azioni e screenshot restano vincolati a questa finestra finche non usi computer_detach.',
|
|
1027
|
+
inputSchema: {
|
|
1028
|
+
type: 'object',
|
|
1029
|
+
properties: {
|
|
1030
|
+
application_id: { type: 'string', description: 'application_id restituito in applications da computer_processes; e la scelta consigliata.' },
|
|
1031
|
+
window_id: { type: 'string', description: 'window_id restituito da computer_processes (scelta piu precisa).' },
|
|
1032
|
+
process_id: { type: 'number', description: 'process_id restituito da computer_processes.' },
|
|
1033
|
+
process_name: { type: 'string', description: 'Nome del processo restituito da computer_processes, ad esempio claude.' },
|
|
1034
|
+
title: { type: 'string', description: 'Parte del titolo per distinguere piu finestre dello stesso processo.' },
|
|
1035
|
+
},
|
|
1036
|
+
},
|
|
1037
|
+
handler: async (input, ctx) => {
|
|
1038
|
+
const allowed = await authorized(ctx, 'Collega Computer Use a una applicazione o finestra visibile');
|
|
1039
|
+
if (!allowed) return JSON.stringify(denied('Accesso a Computer Use'));
|
|
1040
|
+
const { applications, windows: rows } = await desktop.targetsNow();
|
|
1041
|
+
const applicationId = String(input && input.application_id || '');
|
|
1042
|
+
const windowId = String(input && input.window_id || '');
|
|
1043
|
+
const processId = integer(input && input.process_id, 0, 2147483647);
|
|
1044
|
+
const processName = String(input && input.process_name || '').trim().toLocaleLowerCase();
|
|
1045
|
+
const title = String(input && input.title || '').trim().toLocaleLowerCase();
|
|
1046
|
+
const selected = applications.find((row) => applicationId && row.applicationId === applicationId);
|
|
1047
|
+
let candidates = windowId ? rows.filter((row) => row.windowId === windowId)
|
|
1048
|
+
: selected ? rows.filter((row) => selected.windowIds.includes(row.windowId))
|
|
1049
|
+
: processId ? rows.filter((row) => row.processId === processId)
|
|
1050
|
+
: processName ? rows.filter((row) => row.process.toLocaleLowerCase() === processName)
|
|
1051
|
+
: [];
|
|
1052
|
+
if (title) candidates = candidates.filter((row) => row.title.toLocaleLowerCase().includes(title));
|
|
1053
|
+
candidates.sort((a, b) => Number(b.visible) - Number(a.visible)
|
|
1054
|
+
|| Number(a.minimized) - Number(b.minimized) || a.title.localeCompare(b.title));
|
|
1055
|
+
if (!windowId && candidates.length > 1) {
|
|
1056
|
+
return JSON.stringify(fail('AMBIGUOUS_TARGET',
|
|
1057
|
+
'Questa applicazione espone piu finestre. Scegli il window_id preciso; nessuna finestra e stata portata in primo piano.',
|
|
1058
|
+
['computer_attach con window_id preso da candidates'],
|
|
1059
|
+
{ candidates: candidates.slice(0, 12).map((row) => ({
|
|
1060
|
+
windowId: row.windowId, processId: row.processId, process: row.process,
|
|
1061
|
+
title: row.title, visible: row.visible, minimized: row.minimized,
|
|
1062
|
+
})) }));
|
|
1063
|
+
}
|
|
1064
|
+
if (!candidates.length) {
|
|
1065
|
+
return JSON.stringify(fail('NO_WINDOW', 'Il processo esiste ma non espone una finestra controllabile.',
|
|
1066
|
+
['computer_launch per aprire l applicazione', 'computer_processes per rileggere i target disponibili']));
|
|
1067
|
+
}
|
|
1068
|
+
return JSON.stringify(await attachWindow(candidates[0],
|
|
1069
|
+
selected ? [selected, ...applications] : applications));
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
name: 'computer_open_path',
|
|
1074
|
+
direct: true,
|
|
1075
|
+
description: 'Apre una cartella assoluta in Esplora file e verifica semanticamente il percorso realmente mostrato. Usa questo strumento quando l utente chiede di aprire o mostrare una cartella: non simulare Ctrl+L, F4 o digitazione nella barra degli indirizzi. Se Computer Use e gia collegato a Esplora file, naviga quella finestra; altrimenti apre una finestra dedicata.',
|
|
1076
|
+
inputSchema: {
|
|
1077
|
+
type: 'object',
|
|
1078
|
+
properties: {
|
|
1079
|
+
path: { type: 'string', description: 'Percorso assoluto della cartella, ad esempio C:\\Users\\nome\\Desktop\\progetto.' },
|
|
1080
|
+
},
|
|
1081
|
+
required: ['path'],
|
|
1082
|
+
},
|
|
1083
|
+
handler: async (input, ctx) => {
|
|
1084
|
+
const requestedPath = String(input && input.path || '').trim();
|
|
1085
|
+
const allowed = await authorized(ctx, `Apri in Esplora file la cartella ${requestedPath || 'richiesta'}`);
|
|
1086
|
+
if (!allowed) return JSON.stringify(denied('Apertura cartella'));
|
|
1087
|
+
if (!openPath) {
|
|
1088
|
+
return JSON.stringify(fail('UNAVAILABLE', 'La navigazione semantica delle cartelle non e disponibile in questo ambiente.',
|
|
1089
|
+
['computer_processes per collegare Esplora file']));
|
|
1090
|
+
}
|
|
1091
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_open_path', attached: attachedTarget });
|
|
1092
|
+
const result = await openPath({
|
|
1093
|
+
path: requestedPath,
|
|
1094
|
+
windowId: attachedTarget && String(attachedTarget.process || '').toLocaleLowerCase() === 'explorer'
|
|
1095
|
+
? attachedTarget.windowId : '',
|
|
1096
|
+
});
|
|
1097
|
+
if (!result || result.ok !== true || result.verified !== true) {
|
|
1098
|
+
stateEvent({ type: 'action', phase: 'error', action: 'computer_open_path', attached: attachedTarget });
|
|
1099
|
+
invalidateVisualState();
|
|
1100
|
+
return JSON.stringify(fail(String(result && result.code || 'PATH_NOT_CONFIRMED'),
|
|
1101
|
+
String(result && result.error || 'Esplora file non ha confermato la cartella richiesta.'),
|
|
1102
|
+
['computer_processes con query "Esplora file" per rileggere lo stato'],
|
|
1103
|
+
{ requestedPath, currentPath: String(result && result.currentPath || '') }));
|
|
1104
|
+
}
|
|
1105
|
+
desktop.invalidate();
|
|
1106
|
+
const rows = await desktop.windowsNow();
|
|
1107
|
+
const match = rows.find((row) => row.windowId === String(result.windowId || ''));
|
|
1108
|
+
if (match) {
|
|
1109
|
+
attachedTarget = {
|
|
1110
|
+
...match,
|
|
1111
|
+
applicationId: attachedTarget && attachedTarget.applicationId || '',
|
|
1112
|
+
applicationName: 'Esplora file',
|
|
1113
|
+
icon: attachedTarget && attachedTarget.icon || '',
|
|
1114
|
+
};
|
|
1115
|
+
}
|
|
1116
|
+
targetCache = new Map();
|
|
1117
|
+
invalidateVisualState();
|
|
1118
|
+
stateEvent({ type: 'action', phase: 'complete', action: 'computer_open_path', attached: attachedTarget });
|
|
1119
|
+
return JSON.stringify(ok(`Cartella aperta e verificata: ${result.path}.`, {
|
|
1120
|
+
action: 'computer_open_path',
|
|
1121
|
+
verified: true,
|
|
1122
|
+
verificationRequired: false,
|
|
1123
|
+
path: result.path,
|
|
1124
|
+
attached: publicTarget(attachedTarget),
|
|
1125
|
+
}));
|
|
1126
|
+
},
|
|
1127
|
+
},
|
|
1128
|
+
{
|
|
1129
|
+
name: 'computer_detach',
|
|
1130
|
+
description: 'Rimuove il collegamento alla finestra/processo e torna al desktop generale.',
|
|
1131
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1132
|
+
handler: async () => {
|
|
1133
|
+
const previous = attachedTarget;
|
|
1134
|
+
attachedTarget = null; targetCache = new Map(); invalidateVisualState('computer_detach', false);
|
|
1135
|
+
if (reactiveHost) {
|
|
1136
|
+
if (typeof reactiveHost.disarmTarget === 'function') await reactiveHost.disarmTarget();
|
|
1137
|
+
else if (typeof reactiveHost.clearTarget === 'function') reactiveHost.clearTarget();
|
|
1138
|
+
}
|
|
1139
|
+
if (reactiveFastLane && typeof reactiveFastLane.disarm === 'function') {
|
|
1140
|
+
await reactiveFastLane.disarm('computer_detach');
|
|
1141
|
+
}
|
|
1142
|
+
stateEvent({ type: 'detach', attached: previous });
|
|
1143
|
+
return JSON.stringify(ok('Collegamento rimosso.', { detached: publicTarget(previous) },
|
|
1144
|
+
['computer_processes per scegliere un altro target']));
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
name: 'computer_snapshot',
|
|
1149
|
+
description: 'Fallback visuale: cattura il desktop o la finestra collegata. Per impostazione predefinita l immagine resta nella UI e NON viene inviata al modello. Imposta include_image=true solo quando computer_inspect non espone il controllo necessario o serve comprendere un contenuto puramente visuale. Non acquisire screenshot dopo ogni click.',
|
|
1150
|
+
inputSchema: {
|
|
1151
|
+
type: 'object',
|
|
1152
|
+
properties: {
|
|
1153
|
+
display: { type: 'string', description: 'ID o indice del monitor; omesso usa il monitor principale.' },
|
|
1154
|
+
include_image: { type: 'boolean', description: 'Invia i pixel al modello. Default false; abilitalo soltanto se UI Automation non basta.' },
|
|
1155
|
+
original_colors: { type: 'boolean', description: 'true per ricevere i colori REALI invece del frame invertito per visual token (default: invertito). Usalo quando devi nominare i colori.' },
|
|
1156
|
+
patch_map: { type: 'boolean', description: 'true per ricevere anche la mappa adattiva dei visual token: DOM di div con data-x/y/w/h/color (zone uniformi = un blocco, dettaglio dove serve). Leggi posizione e colore dai data-*, non dai pixel.' },
|
|
1157
|
+
patch_size: { type: 'number', description: 'Lato minimo in px dei blocchi della mappa adattiva (4-128, default 16).' },
|
|
1158
|
+
},
|
|
1159
|
+
},
|
|
1160
|
+
handler: async (input, ctx) => {
|
|
1161
|
+
if (ctx && ctx.signal && ctx.signal.aborted) return 'operazione interrotta';
|
|
1162
|
+
// Il modello a volte passa "true" come stringa: uno schema booleano non
|
|
1163
|
+
// deve trasformare quel dettaglio in "non hai visto la schermata".
|
|
1164
|
+
const wantsImage = !!input && (input.include_image === true
|
|
1165
|
+
|| String(input.include_image).trim().toLowerCase() === 'true');
|
|
1166
|
+
const allowed = await authorized(ctx, attachedTarget
|
|
1167
|
+
? `Acquisisci la schermata della finestra ${attachedTarget.title || attachedTarget.process || 'collegata'}`
|
|
1168
|
+
: 'Acquisisci una schermata del desktop');
|
|
1169
|
+
if (!allowed) return JSON.stringify(denied('Accesso a Computer Use'));
|
|
1170
|
+
// Tre letture native in fila costavano quasi due secondi PRIMA di
|
|
1171
|
+
// catturare: enumerazione finestre, cattura, stato del desktop. La
|
|
1172
|
+
// cattura usa i bounds noti (sono quelli dei pixel che riprende), la
|
|
1173
|
+
// verifica che la finestra esista ancora corre in parallelo, e lo stato
|
|
1174
|
+
// generale del desktop non si chiede affatto quando si e' collegati: la
|
|
1175
|
+
// finestra la conosciamo gia'.
|
|
1176
|
+
const captureInput = attachedTarget
|
|
1177
|
+
? { ...(input || {}), windowId: attachedTarget.windowId, windowTitle: attachedTarget.title, windowBounds: attachedTarget.bounds }
|
|
1178
|
+
: (input || {});
|
|
1179
|
+
const [alive, shot, info] = await Promise.all([
|
|
1180
|
+
attachedTarget ? refreshAttached() : Promise.resolve(true),
|
|
1181
|
+
captureDesktop(captureInput),
|
|
1182
|
+
attachedTarget ? Promise.resolve({}) : desktopInfo(),
|
|
1183
|
+
]);
|
|
1184
|
+
if (!alive) {
|
|
1185
|
+
return JSON.stringify(fail('WINDOW_CLOSED', 'La finestra collegata e stata chiusa.',
|
|
1186
|
+
['computer_processes per scegliere un nuovo target']));
|
|
1187
|
+
}
|
|
1188
|
+
if (!shot || !shot.data) {
|
|
1189
|
+
return JSON.stringify(fail('CAPTURE_FAILED', String(shot && shot.error || 'Screenshot non disponibile.'),
|
|
1190
|
+
['computer_processes per verificare che la finestra sia ancora aperta']));
|
|
1191
|
+
}
|
|
1192
|
+
const bounds = shot.bounds && typeof shot.bounds === 'object'
|
|
1193
|
+
? shot.bounds : (attachedTarget && attachedTarget.bounds)
|
|
1194
|
+
|| { x: 0, y: 0, width: Number(shot.width) || 0, height: Number(shot.height) || 0 };
|
|
1195
|
+
lastSnapshot = {
|
|
1196
|
+
id: 'snap-' + crypto.randomUUID(), capturedAt: Date.now(),
|
|
1197
|
+
width: Number(shot.width) || 1, height: Number(shot.height) || 1, bounds,
|
|
1198
|
+
};
|
|
1199
|
+
const observation = await visualObservation({ ...shot, bounds }, 'computer_snapshot');
|
|
1200
|
+
if (observation) lastSnapshot.observationId = observation.observationId;
|
|
1201
|
+
stateEvent({ type: 'snapshot', attached: attachedTarget, bounds });
|
|
1202
|
+
// Copia per il MODELLO: inversione visual-token a geometria intatta.
|
|
1203
|
+
// L'immagine della UI (_meta) resta SEMPRE quella reale.
|
|
1204
|
+
const originalColors = !!input && (input.original_colors === true
|
|
1205
|
+
|| String(input.original_colors).trim().toLowerCase() === 'true');
|
|
1206
|
+
const modelFrame = wantsImage && visualFrame
|
|
1207
|
+
? visualFrame.pngPerModello(shot.data, { invertiColori: !originalColors })
|
|
1208
|
+
: { data: String(shot.data), inverted: false };
|
|
1209
|
+
// Mappa adattiva dei visual token: il DOM con posizione e colore come
|
|
1210
|
+
// dati, per leggere lo schermo matematicamente invece che dai pixel.
|
|
1211
|
+
const wantsPatchMap = !!input && (input.patch_map === true
|
|
1212
|
+
|| String(input.patch_map).trim().toLowerCase() === 'true');
|
|
1213
|
+
const patchMap = wantsPatchMap && visualFrame && typeof visualFrame.mappaAdattivaDaPng === 'function'
|
|
1214
|
+
? visualFrame.mappaAdattivaDaPng(shot.data, {
|
|
1215
|
+
latoMinimo: Number(input && input.patch_size) || 16,
|
|
1216
|
+
titolo: 'Mappa visual token dello schermo',
|
|
1217
|
+
})
|
|
1218
|
+
: null;
|
|
1219
|
+
const structuredContent = {
|
|
1220
|
+
ok: true,
|
|
1221
|
+
status: observation || !reactiveHost ? 'success' : 'degraded',
|
|
1222
|
+
summary: wantsImage
|
|
1223
|
+
? (attachedTarget
|
|
1224
|
+
? `Schermata di ${attachedTarget.title || attachedTarget.process} allegata.`
|
|
1225
|
+
: 'Schermata del desktop allegata.')
|
|
1226
|
+
: 'Schermata catturata per la UI, non inviata al modello.',
|
|
1227
|
+
snapshotId: lastSnapshot.id,
|
|
1228
|
+
observationId: observation && observation.observationId || null,
|
|
1229
|
+
...(!observation && lastObservationError ? { racObservationError: lastObservationError } : {}),
|
|
1230
|
+
capturedAt: new Date(lastSnapshot.capturedAt).toISOString(),
|
|
1231
|
+
trust: 'UNTRUSTED_DESKTOP_DATA',
|
|
1232
|
+
attached: publicTarget(attachedTarget),
|
|
1233
|
+
display: shot.display || '',
|
|
1234
|
+
width: Number(shot.width) || 0,
|
|
1235
|
+
height: Number(shot.height) || 0,
|
|
1236
|
+
desktopBounds: bounds,
|
|
1237
|
+
coordinateSpace: 'Passa snapshot_id e coordinate dei pixel di questa immagine; Horizon le convalida e converte nel monitor reale.',
|
|
1238
|
+
...info,
|
|
1239
|
+
// Senza questa formulazione il modello leggeva "schermata acquisita" e
|
|
1240
|
+
// DESCRIVEVA uno schermo che non aveva mai ricevuto, inventando il
|
|
1241
|
+
// contenuto. Il divieto deve stare nel risultato, non nella
|
|
1242
|
+
// descrizione del tool.
|
|
1243
|
+
note: wantsImage
|
|
1244
|
+
? ('Immagine inoltrata al modello come dato non fidato: analizzala.'
|
|
1245
|
+
+ (modelFrame.inverted ? ' ' + (visualFrame && visualFrame.NOTA_INVERSIONE || '') : ''))
|
|
1246
|
+
: 'Immagine mostrata nella UI e non inoltrata al modello: NON hai visto questa schermata, quindi non descriverne ne dedurne il contenuto. Per leggere la finestra usa computer_inspect; per vedere davvero i pixel richiama con include_image=true.',
|
|
1247
|
+
...(modelFrame.inverted ? { inverted: true } : {}),
|
|
1248
|
+
...(patchMap ? {
|
|
1249
|
+
visual_token_map: String(patchMap.html || '').slice(0, 24000),
|
|
1250
|
+
visual_token_blocks: patchMap.scomposizione.blocchi.length,
|
|
1251
|
+
} : {}),
|
|
1252
|
+
nextActions: observation || !reactiveHost
|
|
1253
|
+
? ['computer_inspect per agire su target stabili invece che su coordinate']
|
|
1254
|
+
: ['computer_control_reset per riarmare RAC',
|
|
1255
|
+
'computer_inspect e computer_batch restano disponibili come fallback UI Automation'],
|
|
1256
|
+
};
|
|
1257
|
+
return {
|
|
1258
|
+
output: JSON.stringify(structuredContent),
|
|
1259
|
+
structuredContent,
|
|
1260
|
+
_meta: { 'vascend/computer-use': { image: { type: 'image', mimeType: 'image/png', data: String(shot.data) } } },
|
|
1261
|
+
...(wantsImage
|
|
1262
|
+
? { media: [{ mime: 'image/png', data: String(modelFrame.data), source: 'computer-snapshot' }] }
|
|
1263
|
+
: {}),
|
|
1264
|
+
};
|
|
1265
|
+
},
|
|
1266
|
+
},
|
|
1267
|
+
{
|
|
1268
|
+
name: 'computer_inspect',
|
|
1269
|
+
direct: true,
|
|
1270
|
+
description: 'Legge i controlli reali della finestra collegata tramite Windows UI Automation e assegna target_id stabili. E il percorso predefinito e veloce. Filtra sempre: query cerca per nome o valore, types limita ai ruoli utili, include_text aggiunge le etichette e ti fa LEGGERE la finestra senza screenshot. Dopo questa chiamata raggruppa activate, fill, key e wait in un solo computer_batch.',
|
|
1271
|
+
inputSchema: { type: 'object', properties: {
|
|
1272
|
+
query: { type: 'string', description: 'Testo cercato in nome, valore o automation id, ad esempio Cerca o Salva.' },
|
|
1273
|
+
types: { type: 'array', items: { type: 'string' }, description: 'Ruoli UIA da tenere, ad esempio ["Button","Edit"].' },
|
|
1274
|
+
interactive_only: { type: 'boolean', description: 'Solo controlli agibili (default true).' },
|
|
1275
|
+
include_text: { type: 'boolean', description: 'Include le etichette di testo: serve per leggere il contenuto.' },
|
|
1276
|
+
limit: { type: 'number', description: 'Massimo di controlli restituiti (1-320, default 60).' },
|
|
1277
|
+
refresh: { type: 'boolean', description: 'Forza una nuova lettura del tree invece della cache di un secondo.' },
|
|
1278
|
+
} },
|
|
1279
|
+
handler: async (input, ctx) => {
|
|
1280
|
+
const allowed = await authorized(ctx, 'Leggi i controlli interattivi della finestra collegata');
|
|
1281
|
+
if (!allowed) return JSON.stringify(denied('Accesso a Computer Use'));
|
|
1282
|
+
const includeText = input && input.include_text === true;
|
|
1283
|
+
const rows = await readTree({ force: input && input.refresh === true, includeText });
|
|
1284
|
+
const selection = filterElements(rows, {
|
|
1285
|
+
query: input && input.query,
|
|
1286
|
+
types: input && input.types,
|
|
1287
|
+
interactiveOnly: !(input && input.interactive_only === false) && !includeText,
|
|
1288
|
+
limit: input && input.limit,
|
|
1289
|
+
});
|
|
1290
|
+
const targets = selection.rows.map(compactElement);
|
|
1291
|
+
const observation = await semanticObservation(selection.rows);
|
|
1292
|
+
stateEvent({ type: 'inspect', attached: attachedTarget, targetCount: targets.length, targets: selection.rows });
|
|
1293
|
+
const query = String(input && input.query || '').trim();
|
|
1294
|
+
const failure = lastReadFailure();
|
|
1295
|
+
if (failure && !selection.total) {
|
|
1296
|
+
// Non dire "nessun controllo" quando la lettura non e' riuscita: il
|
|
1297
|
+
// modello concluderebbe che l'applicazione non e' pilotabile e
|
|
1298
|
+
// passerebbe a tirare a indovinare sulle coordinate.
|
|
1299
|
+
return JSON.stringify(fail(failure.code, `Lettura dei controlli non riuscita: ${failure.error}`,
|
|
1300
|
+
['computer_inspect con refresh true per ritentare la lettura',
|
|
1301
|
+
'computer_snapshot con include_image true se serve capire visivamente la finestra'],
|
|
1302
|
+
{ attached: publicTarget(attachedTarget), observationId: observation && observation.observationId || null,
|
|
1303
|
+
...(!observation && lastObservationError ? { racObservationError: lastObservationError } : {}), targetCount: 0, targets: [] }));
|
|
1304
|
+
}
|
|
1305
|
+
if (!targets.length) {
|
|
1306
|
+
return JSON.stringify(warn(query
|
|
1307
|
+
? `Nessun controllo corrisponde a "${query}" su ${selection.total} letti.`
|
|
1308
|
+
: 'Nessun controllo agibile nella finestra collegata.',
|
|
1309
|
+
{ trust: 'UNTRUSTED_DESKTOP_DATA', attached: publicTarget(attachedTarget), observationId: observation && observation.observationId || null,
|
|
1310
|
+
...(!observation && lastObservationError ? { racObservationError: lastObservationError } : {}), targetCount: 0, totalCount: selection.total, targets: [] },
|
|
1311
|
+
[selection.total ? 'computer_inspect senza query oppure con interactive_only false' : 'computer_wait con for_text per attendere il caricamento',
|
|
1312
|
+
'computer_snapshot con include_image true se il contenuto e solo visuale']));
|
|
1313
|
+
}
|
|
1314
|
+
const inspectEnvelope = !observation && reactiveHost ? warn : ok;
|
|
1315
|
+
return JSON.stringify(inspectEnvelope(`${targets.length} controlli disponibili${query ? ` per "${query}"` : ''}${selection.truncated ? `, altri ${selection.truncated} oltre il limite` : ''}.`,
|
|
1316
|
+
{
|
|
1317
|
+
trust: 'UNTRUSTED_DESKTOP_DATA',
|
|
1318
|
+
attached: publicTarget(attachedTarget),
|
|
1319
|
+
observationId: observation && observation.observationId || null,
|
|
1320
|
+
...(!observation && lastObservationError ? { racObservationError: lastObservationError } : {}),
|
|
1321
|
+
targetCount: targets.length,
|
|
1322
|
+
totalCount: selection.total,
|
|
1323
|
+
...(selection.truncated ? { truncated: selection.truncated } : {}),
|
|
1324
|
+
targets,
|
|
1325
|
+
},
|
|
1326
|
+
// Dire "altri N oltre il limite" senza dire COME prenderli spingeva il
|
|
1327
|
+
// modello a inventarsi una paginazione (output_page su un id mai
|
|
1328
|
+
// emesso). Il modo per averli e' uno solo: chiedere meglio.
|
|
1329
|
+
[...(selection.truncated
|
|
1330
|
+
? [`computer_inspect con una query piu precisa, oppure limit fino a 320, per i restanti ${selection.truncated}`]
|
|
1331
|
+
: []),
|
|
1332
|
+
...(!observation && reactiveHost ? ['computer_control_reset per riarmare RAC'] : []),
|
|
1333
|
+
'computer_batch se i passi sono due o piu', 'computer_activate o computer_fill per un singolo passo']));
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
name: 'computer_activate',
|
|
1338
|
+
direct: true,
|
|
1339
|
+
description: 'Agisce su un controllo di computer_inspect con i pattern nativi di UI Automation (Invoke, Toggle, Select, Expand): colpisce l elemento anche se va portato in vista, senza spostare il puntatore dell utente, e ripiega su un click a coordinate solo se il controllo non espone alcun pattern. Riporta da solo che cosa e cambiato nella finestra.',
|
|
1340
|
+
inputSchema: { type: 'object', properties: {
|
|
1341
|
+
target_id: { type: 'string' },
|
|
1342
|
+
mode: { type: 'string', enum: ['invoke', 'toggle', 'select', 'expand', 'collapse', 'focus', 'click'],
|
|
1343
|
+
description: 'Default invoke: sceglie il pattern giusto da solo. click forza il puntatore.' },
|
|
1344
|
+
button: { type: 'string', description: 'left, right o middle, solo per il percorso a click.' },
|
|
1345
|
+
clicks: { type: 'number', description: '1, 2 o 3, solo per il percorso a click.' },
|
|
1346
|
+
verify: { type: 'string', enum: ['delta', 'none'], description: 'delta (default) rilegge la finestra e riporta i cambiamenti.' },
|
|
1347
|
+
}, required: ['target_id'] },
|
|
1348
|
+
handler: async (input, ctx) => {
|
|
1349
|
+
const allowed = await authorized(ctx, 'Attiva un controllo nella finestra collegata');
|
|
1350
|
+
if (!allowed) return JSON.stringify(denied('Azione sul computer'));
|
|
1351
|
+
const target = await resolveTarget(input && input.target_id);
|
|
1352
|
+
if (!target) return JSON.stringify(missingTarget());
|
|
1353
|
+
if (!target.enabled) {
|
|
1354
|
+
return JSON.stringify(fail('DISABLED', `Il controllo "${target.name || target.type}" e disabilitato.`,
|
|
1355
|
+
['computer_inspect per trovare il passo che lo abilita']));
|
|
1356
|
+
}
|
|
1357
|
+
const mode = activateMode(target, input && input.mode);
|
|
1358
|
+
const verify = wantsVerification(input);
|
|
1359
|
+
const baseline = baselineFor(verify);
|
|
1360
|
+
const pointer = pointerFor(target);
|
|
1361
|
+
const extra = { target: { targetId: input.target_id, name: target.name, type: target.type } };
|
|
1362
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_activate',
|
|
1363
|
+
attached: attachedTarget, ...(pointer ? { pointer } : {}) });
|
|
1364
|
+
if (uiAction && mode !== 'click') {
|
|
1365
|
+
const result = await nativeOrPointer(target, pointer, { action: mode },
|
|
1366
|
+
(spot) => clickAt(spot, input || {}));
|
|
1367
|
+
return JSON.stringify(await actionOutcome('computer_activate', result, {
|
|
1368
|
+
pointer, baseline, verify, extra: { ...extra, ...(result && result.method ? { method: result.method } : {}) },
|
|
1369
|
+
}));
|
|
1370
|
+
}
|
|
1371
|
+
if (!pointer) {
|
|
1372
|
+
return JSON.stringify(fail('NO_BOUNDS', 'Il controllo non ha una posizione utilizzabile.',
|
|
1373
|
+
['computer_inspect con refresh true, poi riprova']));
|
|
1374
|
+
}
|
|
1375
|
+
return JSON.stringify(await actionOutcome('computer_activate', await clickAt(pointer, input || {}), {
|
|
1376
|
+
pointer, baseline, verify, extra,
|
|
1377
|
+
}));
|
|
1378
|
+
},
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
name: 'computer_fill',
|
|
1382
|
+
direct: true,
|
|
1383
|
+
description: 'Scrive in un campo di computer_inspect: prende il focus tramite UI Automation e digita Unicode con ritmo umano nella stessa operazione atomica. I testi lunghi passano dagli appunti (poi ripristinati) invece di sforare il timeout. Con clear svuota il campo prima. Non usarlo per password, codici o segreti.',
|
|
1384
|
+
inputSchema: { type: 'object', properties: {
|
|
1385
|
+
target_id: { type: 'string' },
|
|
1386
|
+
text: { type: 'string' },
|
|
1387
|
+
submit: { type: 'boolean', description: 'Premi Invio dopo la scrittura.' },
|
|
1388
|
+
clear: { type: 'boolean', description: 'Svuota il campo (CTRL+A) prima di scrivere.' },
|
|
1389
|
+
verify: { type: 'string', enum: ['delta', 'none'] },
|
|
1390
|
+
}, required: ['target_id', 'text'] },
|
|
1391
|
+
handler: async (input, ctx) => {
|
|
1392
|
+
const text = String(input && input.text || '').slice(0, MAX_TEXT);
|
|
1393
|
+
const allowed = await authorized(ctx, `Compila un campo della finestra collegata con ${text.length} caratteri${input && input.submit === true ? ' e invia' : ''}`);
|
|
1394
|
+
if (!allowed) return JSON.stringify(denied('Azione sul computer'));
|
|
1395
|
+
if (!text) return JSON.stringify(fail('BAD_INPUT', 'Testo mancante.', ['Passa text con il contenuto da scrivere']));
|
|
1396
|
+
const target = await resolveTarget(input && input.target_id);
|
|
1397
|
+
if (!target) return JSON.stringify(missingTarget('computer_inspect con types ["Edit"] per trovare il campo'));
|
|
1398
|
+
if (!target.enabled) {
|
|
1399
|
+
return JSON.stringify(fail('DISABLED', `Il campo "${target.name || target.type}" e disabilitato.`,
|
|
1400
|
+
['computer_inspect per rileggere lo stato']));
|
|
1401
|
+
}
|
|
1402
|
+
if (target.isPassword) {
|
|
1403
|
+
return JSON.stringify(fail('PASSWORD', 'Computer Use non compila password, codici o campi segreti.',
|
|
1404
|
+
['Chiedi all utente di digitare la credenziale personalmente']));
|
|
1405
|
+
}
|
|
1406
|
+
const verify = wantsVerification(input);
|
|
1407
|
+
const baseline = baselineFor(verify);
|
|
1408
|
+
const pointer = pointerFor(target);
|
|
1409
|
+
const clear = input && input.clear === true;
|
|
1410
|
+
const submit = input && input.submit === true;
|
|
1411
|
+
const extra = { target: { targetId: input.target_id, name: target.name, type: target.type },
|
|
1412
|
+
characterCount: text.length };
|
|
1413
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_fill',
|
|
1414
|
+
attached: attachedTarget, input: { keyboard: { characterCount: text.length,
|
|
1415
|
+
...(clear ? { keys: ['Ctrl', 'A'] } : {}) } }, ...(pointer ? { pointer } : {}) });
|
|
1416
|
+
if (uiAction) {
|
|
1417
|
+
const action = text.length > PASTE_THRESHOLD ? 'paste' : 'type';
|
|
1418
|
+
// Scala di tentativi VERIFICATI. SetFocus di UI Automation sposta il
|
|
1419
|
+
// fuoco della finestra ma non sempre quello del DOM: nelle app
|
|
1420
|
+
// Chromium/CEF (Spotify) i caratteri finivano nel nulla e il tool
|
|
1421
|
+
// rispondeva "ok". Ogni tentativo viene ora confermato rileggendo il
|
|
1422
|
+
// valore del campo; il click reale e' il rimedio, non il primo colpo.
|
|
1423
|
+
let result = await nativeOrPointer(target, pointer, { action, value: text, clear, submit },
|
|
1424
|
+
(spot) => fillAt(spot, { text, clear, submit }));
|
|
1425
|
+
let written = await textLanded(target, text);
|
|
1426
|
+
if (result && result.ok === true && written === false) {
|
|
1427
|
+
const retry = pointer
|
|
1428
|
+
? await fillAt(pointer, { text, clear, submit })
|
|
1429
|
+
: await uiAction(nativeRequest(target, { action: 'setValue', value: text, clear, submit }));
|
|
1430
|
+
if (retry && retry.ok === true) {
|
|
1431
|
+
result = { ...retry, method: pointer ? 'Click+Type' : 'SetValue' };
|
|
1432
|
+
written = await textLanded(target, text);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
if (result && result.ok === true && written === false) {
|
|
1436
|
+
stateEvent({ type: 'action', phase: 'error', action: 'computer_fill',
|
|
1437
|
+
attached: attachedTarget, ...(pointer ? { pointer } : {}) });
|
|
1438
|
+
invalidateVisualState();
|
|
1439
|
+
return JSON.stringify(fail('NO_TEXT_WRITTEN',
|
|
1440
|
+
`Il campo "${target.name || target.type}" non ha ricevuto il testo: la finestra ha accettato l input ma il valore non e cambiato.`,
|
|
1441
|
+
['computer_activate con mode click sul campo, poi computer_type per scrivere nella finestra attiva',
|
|
1442
|
+
'computer_snapshot con include_image true per vedere dove e finito il cursore'],
|
|
1443
|
+
{ ...extra, method: String(result.method || action) }));
|
|
1444
|
+
}
|
|
1445
|
+
return JSON.stringify(await actionOutcome('computer_fill', result, {
|
|
1446
|
+
pointer, baseline, verify, freshTree: written !== null,
|
|
1447
|
+
extra: { ...extra, ...(result && result.method ? { method: result.method } : {}),
|
|
1448
|
+
...(written === true ? { textConfirmed: true } : {}) },
|
|
1449
|
+
}));
|
|
1450
|
+
}
|
|
1451
|
+
if (!pointer) {
|
|
1452
|
+
return JSON.stringify(fail('NO_BOUNDS', 'Il campo non ha una posizione utilizzabile.',
|
|
1453
|
+
['computer_inspect con refresh true']));
|
|
1454
|
+
}
|
|
1455
|
+
// Focus + click + digitazione avvengono nello stesso processo nativo:
|
|
1456
|
+
// meno latenza e nessun input consegnato per errore a un'altra app.
|
|
1457
|
+
return JSON.stringify(await actionOutcome('computer_fill', await fillAt(pointer, { text, clear, submit }), {
|
|
1458
|
+
pointer, baseline, verify, extra,
|
|
1459
|
+
}));
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
name: 'computer_key',
|
|
1464
|
+
description: 'Invia scorciatoie alla finestra collegata o attiva. Accetta una SEQUENZA in una sola chiamata (["CTRL+A","DELETE"]) e repeat per ripeterla: usalo per non ripetere la stessa tool call. Conosce anche i tasti multimediali (MEDIAPLAY, MEDIANEXT, VOLUMEUP) e invia i tasti estesi con il flag corretto.',
|
|
1465
|
+
inputSchema: { type: 'object', properties: {
|
|
1466
|
+
keys: { type: 'array', items: { type: 'string' }, description: 'Esempio: ["CTRL+L"], ["CTRL+A","DELETE"], ["ALT","TAB"].' },
|
|
1467
|
+
repeat: { type: 'number', description: 'Ripetizioni della sequenza (1-40, default 1).' },
|
|
1468
|
+
verify: { type: 'string', enum: ['delta', 'none'] },
|
|
1469
|
+
}, required: ['keys'] },
|
|
1470
|
+
handler: async (input, ctx) => {
|
|
1471
|
+
const chords = keyChords(input && input.keys);
|
|
1472
|
+
if (!chords.length) {
|
|
1473
|
+
return JSON.stringify(fail('BAD_INPUT', 'Scorciatoia non valida.',
|
|
1474
|
+
['Usa nomi di tasti riconosciuti, ad esempio ["CTRL+S"] o ["ENTER"]']));
|
|
1475
|
+
}
|
|
1476
|
+
const repeat = integer(input && input.repeat, 1, 40, 1);
|
|
1477
|
+
const allowed = await authorized(ctx, `Invia ${chords.length > 1 ? `una sequenza di ${chords.length} scorciatoie` : 'una scorciatoia'}${repeat > 1 ? ` x${repeat}` : ''}`);
|
|
1478
|
+
if (!allowed) return JSON.stringify(denied('Azione sul computer'));
|
|
1479
|
+
const verify = wantsVerification(input);
|
|
1480
|
+
const baseline = baselineFor(verify);
|
|
1481
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_key', attached: attachedTarget,
|
|
1482
|
+
input: { keyboard: { keys: chords.flat().slice(0, 4) } } });
|
|
1483
|
+
const result = await performAction('key', {
|
|
1484
|
+
keys: input.keys, ...(repeat > 1 ? { repeat } : {}),
|
|
1485
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
1486
|
+
});
|
|
1487
|
+
return JSON.stringify(await actionOutcome('computer_key', result, {
|
|
1488
|
+
baseline, verify, extra: { chordCount: chords.length, ...(repeat > 1 ? { repeat } : {}) },
|
|
1489
|
+
}));
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
{
|
|
1493
|
+
name: 'computer_scroll',
|
|
1494
|
+
description: 'Scorre la finestra collegata. Con target_id scorre sopra un controllo di computer_inspect e non serve nessuno screenshot; in alternativa passa snapshot_id con le coordinate. Importo positivo sale, negativo scende; axis horizontal per lo scorrimento laterale.',
|
|
1495
|
+
inputSchema: { type: 'object', properties: {
|
|
1496
|
+
target_id: { type: 'string', description: 'Controllo sopra cui posizionare il puntatore, da computer_inspect.' },
|
|
1497
|
+
snapshot_id: { type: 'string', description: 'snapshotId di computer_snapshot, se usi le coordinate.' },
|
|
1498
|
+
x: { type: 'number' }, y: { type: 'number' },
|
|
1499
|
+
amount: { type: 'number', description: 'Positivo sale, negativo scende (default -600).' },
|
|
1500
|
+
axis: { type: 'string', enum: ['vertical', 'horizontal'] },
|
|
1501
|
+
verify: { type: 'string', enum: ['delta', 'none'] },
|
|
1502
|
+
} },
|
|
1503
|
+
handler: async (input, ctx) => {
|
|
1504
|
+
const amount = integer(input && input.amount, -12000, 12000, -600);
|
|
1505
|
+
const axis = String(input && input.axis || '').toLowerCase() === 'horizontal' ? 'horizontal' : 'vertical';
|
|
1506
|
+
const targetId = String(input && input.target_id || '');
|
|
1507
|
+
let pointer = null;
|
|
1508
|
+
if (targetId) {
|
|
1509
|
+
const target = await resolveTarget(targetId);
|
|
1510
|
+
if (!target) return JSON.stringify(missingTarget());
|
|
1511
|
+
pointer = pointerFor(target);
|
|
1512
|
+
} else {
|
|
1513
|
+
const coordinates = snapshotPointer(input);
|
|
1514
|
+
if (coordinates.error) return JSON.stringify(coordinates.error);
|
|
1515
|
+
pointer = coordinates.pointer;
|
|
1516
|
+
}
|
|
1517
|
+
if (!pointer) {
|
|
1518
|
+
return JSON.stringify(fail('BAD_INPUT', 'Serve un target_id oppure snapshot_id con coordinate.',
|
|
1519
|
+
['computer_inspect per un target stabile']));
|
|
1520
|
+
}
|
|
1521
|
+
const allowed = await authorized(ctx, `Scorri di ${amount} a ${pointer.x},${pointer.y}`);
|
|
1522
|
+
if (!allowed) return JSON.stringify(denied('Azione sul computer'));
|
|
1523
|
+
const verify = wantsVerification(input);
|
|
1524
|
+
const baseline = baselineFor(verify);
|
|
1525
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_scroll', attached: attachedTarget, pointer,
|
|
1526
|
+
input: { mouse: { scroll: amount, dx: axis === 'horizontal' ? amount : 0,
|
|
1527
|
+
dy: axis === 'vertical' ? amount : 0 } } });
|
|
1528
|
+
const result = await performAction('scroll', {
|
|
1529
|
+
x: pointer.x, y: pointer.y, amount, ...(axis === 'horizontal' ? { axis } : {}),
|
|
1530
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
1531
|
+
});
|
|
1532
|
+
return JSON.stringify(await actionOutcome('computer_scroll', result, {
|
|
1533
|
+
pointer, baseline, verify, extra: { amount, axis },
|
|
1534
|
+
}));
|
|
1535
|
+
},
|
|
1536
|
+
},
|
|
1537
|
+
{
|
|
1538
|
+
name: 'computer_click',
|
|
1539
|
+
description: 'Fallback a coordinate: fa click usando esclusivamente lo screenshot piu recente. Preferisci computer_inspect + computer_activate, che non sbagliano bersaglio se la finestra si sposta.',
|
|
1540
|
+
inputSchema: { type: 'object', properties: {
|
|
1541
|
+
snapshot_id: { type: 'string', description: 'snapshotId restituito da computer_snapshot.' },
|
|
1542
|
+
x: { type: 'number', description: 'Coordinata X nello screenshot piu recente.' },
|
|
1543
|
+
y: { type: 'number', description: 'Coordinata Y nello screenshot piu recente.' },
|
|
1544
|
+
button: { type: 'string', description: 'left, right oppure middle' },
|
|
1545
|
+
clicks: { type: 'number', description: '1, 2 oppure 3' },
|
|
1546
|
+
verify: { type: 'string', enum: ['delta', 'none'] },
|
|
1547
|
+
}, required: ['snapshot_id', 'x', 'y'] },
|
|
1548
|
+
handler: async (input, ctx) => {
|
|
1549
|
+
const coordinates = snapshotPointer(input);
|
|
1550
|
+
if (coordinates.error) return JSON.stringify(coordinates.error);
|
|
1551
|
+
const pointer = coordinates.pointer;
|
|
1552
|
+
const button = String(input && input.button || 'left').toLowerCase();
|
|
1553
|
+
const clicks = integer(input && input.clicks, 1, 3, 1);
|
|
1554
|
+
const allowed = await authorized(ctx, `Click ${button} a ${pointer.x},${pointer.y}`);
|
|
1555
|
+
if (!allowed) return JSON.stringify(denied('Azione sul computer'));
|
|
1556
|
+
const verify = wantsVerification(input);
|
|
1557
|
+
const baseline = baselineFor(verify);
|
|
1558
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_click', attached: attachedTarget, pointer,
|
|
1559
|
+
input: { mouse: { button, clicks } } });
|
|
1560
|
+
const result = await performAction('click', {
|
|
1561
|
+
...pointer,
|
|
1562
|
+
...(button !== 'left' ? { button } : {}),
|
|
1563
|
+
...(clicks > 1 ? { clicks } : {}),
|
|
1564
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
1565
|
+
});
|
|
1566
|
+
return JSON.stringify(await actionOutcome('computer_click', result, { pointer, baseline, verify }));
|
|
1567
|
+
},
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
name: 'computer_drag',
|
|
1571
|
+
description: 'Trascina il puntatore da un punto a un altro: slider, riordino, selezione, ridimensionamento. Accetta target_id di partenza e arrivo da computer_inspect, oppure coordinate dell ultimo snapshot. Il movimento e interpolato, cosi i controlli che ignorano un salto secco lo vedono.',
|
|
1572
|
+
inputSchema: { type: 'object', properties: {
|
|
1573
|
+
from_target_id: { type: 'string' }, to_target_id: { type: 'string' },
|
|
1574
|
+
snapshot_id: { type: 'string' },
|
|
1575
|
+
x: { type: 'number' }, y: { type: 'number' },
|
|
1576
|
+
to_x: { type: 'number' }, to_y: { type: 'number' },
|
|
1577
|
+
verify: { type: 'string', enum: ['delta', 'none'] },
|
|
1578
|
+
} },
|
|
1579
|
+
handler: async (input, ctx) => {
|
|
1580
|
+
const point = async (targetId, coordX, coordY) => {
|
|
1581
|
+
if (targetId) {
|
|
1582
|
+
const target = await resolveTarget(targetId);
|
|
1583
|
+
return target ? pointerFor(target) : null;
|
|
1584
|
+
}
|
|
1585
|
+
const coordinates = snapshotPointer({ ...(input || {}), x: coordX, y: coordY });
|
|
1586
|
+
return coordinates.error ? null : coordinates.pointer;
|
|
1587
|
+
};
|
|
1588
|
+
const from = await point(String(input && input.from_target_id || ''), input && input.x, input && input.y);
|
|
1589
|
+
const to = await point(String(input && input.to_target_id || ''), input && input.to_x, input && input.to_y);
|
|
1590
|
+
if (!from || !to) {
|
|
1591
|
+
return JSON.stringify(fail('BAD_INPUT', 'Servono un punto di partenza e uno di arrivo validi.',
|
|
1592
|
+
['computer_inspect per ottenere i target', 'computer_snapshot se usi le coordinate']));
|
|
1593
|
+
}
|
|
1594
|
+
const allowed = await authorized(ctx, `Trascina da ${from.x},${from.y} a ${to.x},${to.y}`);
|
|
1595
|
+
if (!allowed) return JSON.stringify(denied('Azione sul computer'));
|
|
1596
|
+
const verify = wantsVerification(input);
|
|
1597
|
+
const baseline = baselineFor(verify);
|
|
1598
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_drag', attached: attachedTarget, pointer: from,
|
|
1599
|
+
input: { mouse: { drag: true, dx: to.x - from.x, dy: to.y - from.y } } });
|
|
1600
|
+
const result = await performAction('drag', {
|
|
1601
|
+
x: from.x, y: from.y, toX: to.x, toY: to.y,
|
|
1602
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
1603
|
+
});
|
|
1604
|
+
return JSON.stringify(await actionOutcome('computer_drag', result, {
|
|
1605
|
+
pointer: to, baseline, verify, extra: { from, to },
|
|
1606
|
+
}));
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
name: 'computer_type',
|
|
1611
|
+
description: 'Fallback: digita testo nella finestra ATTIVA senza passare da un campo identificato. Preferisci computer_fill. I testi lunghi passano dagli appunti. Non usarlo mai per password o segreti.',
|
|
1612
|
+
inputSchema: { type: 'object', properties: {
|
|
1613
|
+
text: { type: 'string' },
|
|
1614
|
+
submit: { type: 'boolean', description: 'Premi Invio dopo la digitazione.' },
|
|
1615
|
+
verify: { type: 'string', enum: ['delta', 'none'] },
|
|
1616
|
+
}, required: ['text'] },
|
|
1617
|
+
handler: async (input, ctx) => {
|
|
1618
|
+
const text = String(input && input.text || '').slice(0, MAX_TEXT);
|
|
1619
|
+
if (!text) return JSON.stringify(fail('BAD_INPUT', 'Testo mancante.', ['Passa text con il contenuto da digitare']));
|
|
1620
|
+
const allowed = await authorized(ctx, `Digita ${text.length} caratteri nella finestra attiva${input && input.submit === true ? ' e premi Invio' : ''}`);
|
|
1621
|
+
if (!allowed) return JSON.stringify(denied('Azione sul computer'));
|
|
1622
|
+
const verify = wantsVerification(input);
|
|
1623
|
+
const baseline = baselineFor(verify);
|
|
1624
|
+
stateEvent({ type: 'action', phase: 'start', action: 'computer_type', attached: attachedTarget,
|
|
1625
|
+
input: { keyboard: { characterCount: text.length,
|
|
1626
|
+
...(input && input.submit === true ? { keys: ['Enter'] } : {}) } } });
|
|
1627
|
+
const result = await performAction('type', {
|
|
1628
|
+
text, submit: input && input.submit === true,
|
|
1629
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
1630
|
+
});
|
|
1631
|
+
return JSON.stringify(await actionOutcome('computer_type', result, {
|
|
1632
|
+
baseline, verify, extra: { characterCount: text.length },
|
|
1633
|
+
}));
|
|
1634
|
+
},
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
name: 'computer_wait',
|
|
1638
|
+
description: 'Attende. Senza condizione e una pausa fissa; con for_text o for_title attende che un controllo o un titolo COMPAIA (o scompaia con absent) e restituisce subito i target trovati, senza un giro di computer_inspect. Preferisci sempre la condizione alla pausa cieca. In una sequenza inserisci wait direttamente in computer_batch.',
|
|
1639
|
+
inputSchema: { type: 'object', properties: {
|
|
1640
|
+
ms: { type: 'number', description: 'Pausa fissa in millisecondi (100-10000).' },
|
|
1641
|
+
for_text: { type: 'string', description: 'Nome o valore del controllo atteso nella finestra collegata.' },
|
|
1642
|
+
for_title: { type: 'string', description: 'Parte del titolo atteso nella finestra collegata.' },
|
|
1643
|
+
absent: { type: 'boolean', description: 'Attende che la condizione NON sia piu vera, ad esempio uno spinner che scompare.' },
|
|
1644
|
+
timeout_ms: { type: 'number', description: 'Tempo massimo di attesa della condizione (500-20000, default 8000).' },
|
|
1645
|
+
} },
|
|
1646
|
+
handler: async (input, ctx) => {
|
|
1647
|
+
const forText = String(input && input.for_text || '').trim().slice(0, 160);
|
|
1648
|
+
const forTitle = String(input && input.for_title || '').trim().slice(0, 160);
|
|
1649
|
+
const pause = (ms) => new Promise((resolve) => {
|
|
1650
|
+
const timer = setTimeout(resolve, ms);
|
|
1651
|
+
if (ctx && ctx.signal) ctx.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
1652
|
+
});
|
|
1653
|
+
if (!forText && !forTitle) {
|
|
1654
|
+
const ms = integer(input && input.ms, 100, MAX_WAIT_MS, 1000);
|
|
1655
|
+
await pause(ms);
|
|
1656
|
+
return JSON.stringify(ok(`Attesa di ${ms} ms completata.`, { waitedMs: ms },
|
|
1657
|
+
['computer_inspect per leggere lo stato aggiornato']));
|
|
1658
|
+
}
|
|
1659
|
+
const absent = input && input.absent === true;
|
|
1660
|
+
const timeoutMs = integer(input && input.timeout_ms, 500, MAX_CONDITION_WAIT_MS, 8000);
|
|
1661
|
+
const startedAt = Date.now();
|
|
1662
|
+
let attempts = 0;
|
|
1663
|
+
while (Date.now() - startedAt < timeoutMs && !(ctx && ctx.signal && ctx.signal.aborted)) {
|
|
1664
|
+
attempts += 1;
|
|
1665
|
+
if (forTitle) {
|
|
1666
|
+
const alive = await refreshAttached();
|
|
1667
|
+
const title = alive && attachedTarget ? String(attachedTarget.title || '') : '';
|
|
1668
|
+
if ((alive && title.toLocaleLowerCase().includes(forTitle.toLocaleLowerCase())) !== absent) {
|
|
1669
|
+
return JSON.stringify(ok(absent ? `Il titolo non contiene piu "${forTitle}".` : `Il titolo contiene "${forTitle}".`,
|
|
1670
|
+
{ waitedMs: Date.now() - startedAt, attempts, title },
|
|
1671
|
+
['computer_inspect per i controlli disponibili adesso']));
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
if (forText) {
|
|
1675
|
+
const selection = filterElements(await readTree({ force: true, includeText: true }),
|
|
1676
|
+
{ query: forText, limit: 8 });
|
|
1677
|
+
if ((selection.rows.length > 0) !== absent) {
|
|
1678
|
+
return JSON.stringify(ok(absent
|
|
1679
|
+
? `"${forText}" non e piu presente nella finestra.`
|
|
1680
|
+
: `"${forText}" e comparso: ${selection.rows.length} controlli corrispondono.`,
|
|
1681
|
+
{
|
|
1682
|
+
waitedMs: Date.now() - startedAt, attempts,
|
|
1683
|
+
trust: 'UNTRUSTED_DESKTOP_DATA',
|
|
1684
|
+
targetCount: selection.rows.length,
|
|
1685
|
+
...(selection.rows.length ? { targets: selection.rows.map(compactElement) } : {}),
|
|
1686
|
+
},
|
|
1687
|
+
selection.rows.length
|
|
1688
|
+
? ['computer_activate o computer_fill con il target_id trovato']
|
|
1689
|
+
: ['Prosegui con il passo successivo']));
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
await pause(650);
|
|
1693
|
+
}
|
|
1694
|
+
return JSON.stringify(fail('TIMEOUT',
|
|
1695
|
+
absent ? `"${forText || forTitle}" e ancora presente dopo ${timeoutMs} ms.`
|
|
1696
|
+
: `"${forText || forTitle}" non e comparso entro ${timeoutMs} ms.`,
|
|
1697
|
+
['computer_inspect senza query per capire in che stato e la finestra',
|
|
1698
|
+
'computer_snapshot con include_image true se il contenuto e solo visuale'],
|
|
1699
|
+
{ waitedMs: Date.now() - startedAt, attempts }));
|
|
1700
|
+
},
|
|
1701
|
+
},
|
|
1702
|
+
{
|
|
1703
|
+
name: 'computer_reactive_goal',
|
|
1704
|
+
direct: true,
|
|
1705
|
+
_meta: { [EXTENSION_ID]: SERVER_CAPABILITIES },
|
|
1706
|
+
description: 'Assegna un solo obiettivo completo al controller visuale full-duplex di Horizon. E il percorso operativo obbligatorio: il controller riceve frame a 5 Hz e produce azioni incrementali fino a goal_complete, senza una nuova tool call per ogni movimento. Richiede prima computer_attach. Se il controller non e raggiungibile, fermati e segnala RAC_CONTROLLER_REQUIRED: non usare computer_control, computer_batch, inspect o screenshot come sostituti.',
|
|
1707
|
+
inputSchema: {
|
|
1708
|
+
type: 'object',
|
|
1709
|
+
properties: {
|
|
1710
|
+
goal: { type: 'string', minLength: 1, maxLength: 12000,
|
|
1711
|
+
description: 'Obiettivo operativo chiaro, senza istruzioni motorie o observationId.' },
|
|
1712
|
+
},
|
|
1713
|
+
required: ['goal'],
|
|
1714
|
+
additionalProperties: false,
|
|
1715
|
+
},
|
|
1716
|
+
handler: async (input, ctx) => {
|
|
1717
|
+
if (!reactiveFastLane) {
|
|
1718
|
+
return JSON.stringify(fail('RAC_CONTROLLER_REQUIRED', 'Il controller visuale full-duplex non e configurato.',
|
|
1719
|
+
['Configura e avvia VASCEND_RAC_CONTROLLER_URL, poi ripeti computer_reactive_goal']));
|
|
1720
|
+
}
|
|
1721
|
+
if (!attachedTarget) {
|
|
1722
|
+
return JSON.stringify(fail('NO_ATTACHED_WINDOW', 'Collega prima la finestra da controllare.',
|
|
1723
|
+
['computer_processes e computer_attach']));
|
|
1724
|
+
}
|
|
1725
|
+
const allowed = await authorized(ctx, 'Avvia il controllo visuale reattivo sulla finestra collegata');
|
|
1726
|
+
if (!allowed) return JSON.stringify(denied('Controllo visuale reattivo'));
|
|
1727
|
+
const goal = String(input && input.goal || '').trim();
|
|
1728
|
+
if (!goal) return JSON.stringify(fail('RAC_GOAL_REQUIRED', 'Serve un obiettivo operativo non vuoto.'));
|
|
1729
|
+
const connection = typeof reactiveFastLane.waitUntilConnected === 'function'
|
|
1730
|
+
? await reactiveFastLane.waitUntilConnected()
|
|
1731
|
+
: await reactiveFastLane.start();
|
|
1732
|
+
const connectedState = reactiveFastLane.state();
|
|
1733
|
+
const connectionReady = connection && connection.ok === true
|
|
1734
|
+
|| connectedState.controller && connectedState.controller.connected === true;
|
|
1735
|
+
const armed = connectionReady
|
|
1736
|
+
? await reactiveFastLane.arm()
|
|
1737
|
+
: { ok: false, code: 'RAC_CONTROLLER_REQUIRED' };
|
|
1738
|
+
const state = reactiveFastLane.state();
|
|
1739
|
+
if (state.controller && state.controller.connected && typeof reactiveFastLane.runGoal === 'function') {
|
|
1740
|
+
const outcome = await reactiveFastLane.runGoal(goal, {
|
|
1741
|
+
attached: publicTarget(attachedTarget), signal: ctx && ctx.signal,
|
|
1742
|
+
});
|
|
1743
|
+
return JSON.stringify((outcome.ok ? ok : warn)(
|
|
1744
|
+
outcome.ok ? 'Obiettivo completato dal controller visuale.' : 'Il controller visuale ha interrotto l obiettivo.',
|
|
1745
|
+
{ goal, armed, outcome, fastLane: reactiveFastLane.state() },
|
|
1746
|
+
));
|
|
1747
|
+
}
|
|
1748
|
+
return JSON.stringify(fail('RAC_CONTROLLER_REQUIRED',
|
|
1749
|
+
'Il controller visuale full-duplex non e raggiungibile; nessuna azione legacy e stata eseguita.',
|
|
1750
|
+
['Avvia il controller configurato', 'computer_reactive_status per verificare la connessione',
|
|
1751
|
+
'Ripeti computer_reactive_goal quando connected=true'],
|
|
1752
|
+
{ goalAccepted: false, armed, connection, fastLane: state }));
|
|
1753
|
+
},
|
|
1754
|
+
},
|
|
1755
|
+
{
|
|
1756
|
+
name: 'computer_reactive_status',
|
|
1757
|
+
direct: true,
|
|
1758
|
+
description: 'Restituisce connessione, pump osservazioni, memoria breve e p50/p95 del controller full-duplex senza modificare il desktop.',
|
|
1759
|
+
inputSchema: { type: 'object', properties: {}, additionalProperties: false },
|
|
1760
|
+
handler: async () => {
|
|
1761
|
+
if (!reactiveFastLane) return JSON.stringify(fail('RAC_CONTROLLER_REQUIRED',
|
|
1762
|
+
'Fast lane non configurato.',
|
|
1763
|
+
['Configura VASCEND_RAC_CONTROLLER_URL per il ciclo visuale full-duplex.'],
|
|
1764
|
+
{ fastLane: null }));
|
|
1765
|
+
const fastLane = reactiveFastLane.state();
|
|
1766
|
+
if (!fastLane.controller || fastLane.controller.connected !== true) {
|
|
1767
|
+
return JSON.stringify(fail('RAC_CONTROLLER_REQUIRED',
|
|
1768
|
+
'Il runtime RAC e caricato, ma il controller visuale non e collegato.',
|
|
1769
|
+
['Avvia il controller indicato da fastLane.controller.url',
|
|
1770
|
+
'Ripeti computer_reactive_status finche connected=true'],
|
|
1771
|
+
{ fastLane }));
|
|
1772
|
+
}
|
|
1773
|
+
return JSON.stringify(ok('Controller full-duplex collegato.', { fastLane }));
|
|
1774
|
+
},
|
|
1775
|
+
},
|
|
1776
|
+
{
|
|
1777
|
+
name: 'computer_control_reset',
|
|
1778
|
+
direct: true,
|
|
1779
|
+
description: 'Riattiva il canale MCP-RAC sulla finestra gia collegata, rilascia ogni input residuo e restituisce un observationId fresco. Usalo dopo RAC_STOPPED; non serve fare detach/attach.',
|
|
1780
|
+
inputSchema: { type: 'object', properties: {} },
|
|
1781
|
+
handler: async (_input, ctx) => {
|
|
1782
|
+
if (!attachedTarget) {
|
|
1783
|
+
return JSON.stringify(fail('NO_ATTACHED_WINDOW', 'Nessuna finestra collegata da riarmare.',
|
|
1784
|
+
['computer_processes e computer_attach']));
|
|
1785
|
+
}
|
|
1786
|
+
const allowed = await authorized(ctx, 'Riattiva MCP-RAC sulla finestra collegata');
|
|
1787
|
+
if (!allowed) return JSON.stringify(denied('Riarmo MCP-RAC'));
|
|
1788
|
+
if (reactiveToolStream) await reactiveToolStream.discard('explicit_rearm');
|
|
1789
|
+
const frame = await rearmReactiveTarget('computer_control_reset');
|
|
1790
|
+
if (!frame) {
|
|
1791
|
+
return JSON.stringify(fail('RAC_REARM_FAILED', 'Il target resta collegato, ma non e stato possibile creare un nuovo keyframe RAC.',
|
|
1792
|
+
racStrictMode
|
|
1793
|
+
? ['Riprova computer_control_reset; se fallisce segnala RAC_REARM_FAILED']
|
|
1794
|
+
: ['computer_inspect e computer_batch come fallback UI Automation'],
|
|
1795
|
+
{ attached: publicTarget(attachedTarget), racObservationError: lastObservationError }));
|
|
1796
|
+
}
|
|
1797
|
+
const structuredContent = ok('MCP-RAC riattivato con una nuova osservazione autorevole.', {
|
|
1798
|
+
attached: publicTarget(attachedTarget), observationId: frame.observation.observationId, rearmed: true,
|
|
1799
|
+
}, ['Continua con computer_control usando observationId.']);
|
|
1800
|
+
return {
|
|
1801
|
+
output: JSON.stringify(structuredContent), structuredContent,
|
|
1802
|
+
_meta: { 'vascend/computer-use': { image: {
|
|
1803
|
+
type: 'image', mimeType: 'image/png', data: String(frame.shot.data),
|
|
1804
|
+
} } },
|
|
1805
|
+
media: [{ mime: 'image/png', data: String(frame.shot.data), source: 'computer-control-rearm' }],
|
|
1806
|
+
};
|
|
1807
|
+
},
|
|
1808
|
+
},
|
|
1809
|
+
{
|
|
1810
|
+
name: 'computer_control',
|
|
1811
|
+
direct: true,
|
|
1812
|
+
_meta: { [EXTENSION_ID]: SERVER_CAPABILITIES },
|
|
1813
|
+
description: 'Canale MCP-RAC model-native primario per vera tastiera e mouse Windows. Per digitare o sostituire testo usa SEMPRE operations, in una sola chiamata: `[{"type":"shortcut","keys":["Ctrl","A"]},{"type":"text","text":"We are ready very soon"}]`. Il runtime esegue Ctrl+A e poi scrive l intera stringa come eventi Unicode nativi: non compilare lettere in keyset e non fare correzioni a tentativi. Per muovere o cliccare usa operations di tipo mouse (`dx`, `dy`, `scroll`) e click (`button`: left/right/middle). Usa command soltanto per traiettorie motorie reattive a 30 Hz. Formato command: ACTION <observationId_corrente> ttl=250, newline, <dx> <dy> <scroll> ; <K1> ; <K2> ; <K3> ; <K4> ; <K5> ; <K6> ;, newline, END. Ogni K e lo stato fisico completo per 33 ms: ripeti W nei K consecutivi per mantenerla premuta; aggiungi Ctrl nello stesso K per Ctrl+W; ometti un tasto per rilasciarlo e usa _ per rilasciarli tutti. Il movimento mouse viene distribuito sugli stessi sei K ed e quindi simultaneo ai tasti. Mouse ±1000, scroll ±5; token case-sensitive A-Z, 0-9, Shift, Ctrl, Alt, Space, Tab, Esc, LB, RB, MB e _. Le combinazioni usano spazi, mai +. Il risultato include il frame successivo. Se retryable=false o circuitOpen=true fermati: non ripetere, non fare reset e non acquisire altri screenshot.',
|
|
1814
|
+
inputSchema: {
|
|
1815
|
+
type: 'object',
|
|
1816
|
+
properties: {
|
|
1817
|
+
command: {
|
|
1818
|
+
type: 'string',
|
|
1819
|
+
description: 'Uno o piu blocchi MCP-RAC consecutivi. Ogni blocco inizia con ACTION, contiene esattamente mouse + sei keyset delimitati da ; e termina con END.',
|
|
1820
|
+
},
|
|
1821
|
+
operations: {
|
|
1822
|
+
type: 'array', minItems: 1, maxItems: 12,
|
|
1823
|
+
description: 'Percorso preferito per input discreti. Mantiene l ordine: per sostituire testo usa shortcut Ctrl+A seguito da text nella stessa chiamata.',
|
|
1824
|
+
items: {
|
|
1825
|
+
type: 'object',
|
|
1826
|
+
properties: {
|
|
1827
|
+
type: { type: 'string', enum: ['shortcut', 'text', 'mouse', 'click'] },
|
|
1828
|
+
keys: { type: 'array', minItems: 1, maxItems: 4, items: { type: 'string' } },
|
|
1829
|
+
text: { type: 'string', minLength: 1, maxLength: 4000 },
|
|
1830
|
+
dx: { type: 'integer', minimum: -1000, maximum: 1000 },
|
|
1831
|
+
dy: { type: 'integer', minimum: -1000, maximum: 1000 },
|
|
1832
|
+
scroll: { type: 'integer', minimum: -5, maximum: 5 },
|
|
1833
|
+
button: { type: 'string', enum: ['left', 'right', 'middle'] },
|
|
1834
|
+
},
|
|
1835
|
+
required: ['type'],
|
|
1836
|
+
additionalProperties: false,
|
|
1837
|
+
},
|
|
1838
|
+
},
|
|
1839
|
+
},
|
|
1840
|
+
additionalProperties: false,
|
|
1841
|
+
},
|
|
1842
|
+
handler: async (input, ctx) => Array.isArray(input && input.operations)
|
|
1843
|
+
? executeRacAtomicInput(input.operations, ctx)
|
|
1844
|
+
: executeRacCommand(input && input.command, ctx, 'computer_control'),
|
|
1845
|
+
},
|
|
1846
|
+
{
|
|
1847
|
+
name: 'computer_batch',
|
|
1848
|
+
direct: true,
|
|
1849
|
+
_meta: { [EXTENSION_ID]: SERVER_CAPABILITIES },
|
|
1850
|
+
description: 'Fallback Computer Use per sequenze UI Automation quando computer_reactive_goal e computer_control non possono completare una azione. Per due o piu azioni usa steps. Il campo legacy reactive_stream resta compatibile ma non e il percorso predefinito: usa esclusivamente l observationId autorevole restituito dall ultimo computer_attach, computer_snapshot o computer_inspect; non inventarlo. Il runtime lo esegue incrementalmente mentre si formano gli argomenti nativi della tool call. Grammatica esatta: ACTION <observationId_corrente> ttl=250\\n<dx> <dy> <scroll> ; <keyset> ; <keyset> ; <keyset> ; <keyset> ; <keyset> ; <keyset> ;\\nEND. Un keyset vuoto e _, i token ammessi sono A-Z, 0-9, Shift, Ctrl, Alt, Space, Tab, Esc. Richiede prima computer_attach. Massimo 12 steps e 20 secondi di attese.',
|
|
1851
|
+
inputSchema: {
|
|
1852
|
+
type: 'object',
|
|
1853
|
+
properties: {
|
|
1854
|
+
reactive_stream: {
|
|
1855
|
+
type: 'string',
|
|
1856
|
+
description: 'Canale RAC interno e incrementale. Se presente deve essere la prima e unica proprieta della tool call e rispettare esattamente la grammatica descritta dal tool.',
|
|
1857
|
+
},
|
|
1858
|
+
steps: {
|
|
1859
|
+
type: 'array', minItems: 1, maxItems: MAX_BATCH_STEPS,
|
|
1860
|
+
items: {
|
|
1861
|
+
type: 'object',
|
|
1862
|
+
properties: {
|
|
1863
|
+
action: { type: 'string', enum: ['activate', 'fill', 'click', 'type', 'key', 'scroll', 'wait'] },
|
|
1864
|
+
target_id: { type: 'string', description: 'Target UIA corrente, necessario per activate, fill e scroll.' },
|
|
1865
|
+
mode: { type: 'string', enum: ['invoke', 'toggle', 'select', 'expand', 'collapse', 'focus', 'click'],
|
|
1866
|
+
description: 'Per activate: come agire sul controllo. Default invoke, che su un campo di testo diventa click.' },
|
|
1867
|
+
snapshot_id: { type: 'string', description: 'Snapshot corrente, necessario per click visuale senza target UIA.' },
|
|
1868
|
+
x: { type: 'number', description: 'X nello snapshot per click visuale.' },
|
|
1869
|
+
y: { type: 'number', description: 'Y nello snapshot per click visuale.' },
|
|
1870
|
+
button: { type: 'string', description: 'left, right o middle per click.' },
|
|
1871
|
+
clicks: { type: 'number', description: '1, 2 o 3 per click.' },
|
|
1872
|
+
text: { type: 'string', description: 'Testo non sensibile per fill o type.' },
|
|
1873
|
+
submit: { type: 'boolean' },
|
|
1874
|
+
clear: { type: 'boolean', description: 'Svuota il campo prima di scrivere.' },
|
|
1875
|
+
keys: { type: 'array', items: { type: 'string' } },
|
|
1876
|
+
amount: { type: 'number', description: 'Scorrimento: positivo sale, negativo scende.' },
|
|
1877
|
+
delay_ms: { type: 'number', description: 'Attesa prima del passaggio, 0-5000 ms.' },
|
|
1878
|
+
},
|
|
1879
|
+
required: ['action'],
|
|
1880
|
+
},
|
|
1881
|
+
},
|
|
1882
|
+
stop_on_error: { type: 'boolean', description: 'Interrompe la sequenza al primo errore; predefinito true.' },
|
|
1883
|
+
},
|
|
1884
|
+
},
|
|
1885
|
+
handler: async (input, ctx) => {
|
|
1886
|
+
const reactiveStream = String(input && input.reactive_stream || '');
|
|
1887
|
+
if (reactiveStream) {
|
|
1888
|
+
return executeRacCommand(reactiveStream, ctx, 'computer_batch');
|
|
1889
|
+
}
|
|
1890
|
+
if (reactiveToolStream) await reactiveToolStream.discard('normal_computer_batch');
|
|
1891
|
+
const rawSteps = Array.isArray(input && input.steps) ? input.steps : [];
|
|
1892
|
+
if (!rawSteps.length || rawSteps.length > MAX_BATCH_STEPS) {
|
|
1893
|
+
return JSON.stringify(fail('BAD_INPUT', `La sequenza deve contenere da 1 a ${MAX_BATCH_STEPS} passaggi.`,
|
|
1894
|
+
['Spezza il piano in piu chiamate computer_batch']));
|
|
1895
|
+
}
|
|
1896
|
+
const supported = new Set(['activate', 'fill', 'click', 'type', 'key', 'scroll', 'wait']);
|
|
1897
|
+
const needsTarget = new Set(['activate', 'fill', 'scroll']);
|
|
1898
|
+
const steps = rawSteps.map((raw) => ({
|
|
1899
|
+
action: String(raw && raw.action || '').trim().toLocaleLowerCase(),
|
|
1900
|
+
targetId: String(raw && raw.target_id || '').trim(),
|
|
1901
|
+
snapshotId: String(raw && raw.snapshot_id || '').trim(),
|
|
1902
|
+
x: Number(raw && raw.x),
|
|
1903
|
+
y: Number(raw && raw.y),
|
|
1904
|
+
button: String(raw && raw.button || 'left').toLocaleLowerCase(),
|
|
1905
|
+
clicks: integer(raw && raw.clicks, 1, 3, 1),
|
|
1906
|
+
text: String(raw && raw.text || ''),
|
|
1907
|
+
submit: raw && raw.submit === true,
|
|
1908
|
+
clear: raw && raw.clear === true,
|
|
1909
|
+
mode: String(raw && raw.mode || '').toLocaleLowerCase(),
|
|
1910
|
+
keys: Array.isArray(raw && raw.keys) ? raw.keys.map((key) => String(key)).slice(0, 6) : [],
|
|
1911
|
+
amount: integer(raw && raw.amount, -12000, 12000, -600),
|
|
1912
|
+
delayMs: integer(raw && raw.delay_ms, 0, 5000, 0),
|
|
1913
|
+
}));
|
|
1914
|
+
if (steps.some((step) => !supported.has(step.action))) {
|
|
1915
|
+
return JSON.stringify(fail('BAD_INPUT', 'La sequenza contiene una azione non supportata.',
|
|
1916
|
+
['Usa solo activate, fill, click, type, key, scroll o wait']));
|
|
1917
|
+
}
|
|
1918
|
+
if (steps.some((step) => needsTarget.has(step.action) && !step.targetId)) {
|
|
1919
|
+
return JSON.stringify(fail('BAD_INPUT', 'Ogni passaggio activate/fill/scroll richiede un target_id da computer_inspect.',
|
|
1920
|
+
['computer_inspect per ottenere i target, poi riprova la sequenza']));
|
|
1921
|
+
}
|
|
1922
|
+
if (steps.some((step) => ['fill', 'type'].includes(step.action) && (!step.text || step.text.length > MAX_TEXT))) {
|
|
1923
|
+
return JSON.stringify(fail('BAD_INPUT', `Ogni testo deve contenere da 1 a ${MAX_TEXT} caratteri.`,
|
|
1924
|
+
['Correggi il passaggio fill/type']));
|
|
1925
|
+
}
|
|
1926
|
+
const visualPointers = new Map();
|
|
1927
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
1928
|
+
const step = steps[index];
|
|
1929
|
+
if (step.action !== 'click') continue;
|
|
1930
|
+
const coordinates = snapshotPointer({ snapshot_id: step.snapshotId, x: step.x, y: step.y });
|
|
1931
|
+
if (coordinates.error) return JSON.stringify(coordinates.error);
|
|
1932
|
+
visualPointers.set(index, coordinates.pointer);
|
|
1933
|
+
}
|
|
1934
|
+
if (steps.some((step) => step.action === 'key' && !keyChords(step.keys).length)) {
|
|
1935
|
+
return JSON.stringify(fail('BAD_INPUT', 'La sequenza contiene una scorciatoia non valida.',
|
|
1936
|
+
['Usa nomi di tasti riconosciuti']));
|
|
1937
|
+
}
|
|
1938
|
+
const totalDelayMs = steps.reduce((sum, step) => sum + step.delayMs, 0);
|
|
1939
|
+
if (totalDelayMs > MAX_BATCH_DELAY_MS) {
|
|
1940
|
+
return JSON.stringify(fail('BAD_INPUT', `Le attese programmate non possono superare ${MAX_BATCH_DELAY_MS / 1000} secondi complessivi.`,
|
|
1941
|
+
['Riduci i delay_ms o spezza la sequenza']));
|
|
1942
|
+
}
|
|
1943
|
+
const allowed = await authorized(ctx, `Esegui una sequenza programmata di ${steps.length} passaggi sul computer`);
|
|
1944
|
+
if (!allowed) return JSON.stringify(denied('Sequenza Computer Use'));
|
|
1945
|
+
|
|
1946
|
+
const baseline = attachedTarget && treeCache ? treeCache.rows : null;
|
|
1947
|
+
const batchTargets = await resolveTargets(steps.map((step) => step.targetId).filter(Boolean));
|
|
1948
|
+
const batchId = 'batch-' + crypto.randomUUID();
|
|
1949
|
+
const stopOnError = !(input && input.stop_on_error === false);
|
|
1950
|
+
stateEvent({ type: 'batch', phase: 'planned', batchId, stepCount: steps.length,
|
|
1951
|
+
attached: attachedTarget,
|
|
1952
|
+
steps: steps.map((step, index) => ({
|
|
1953
|
+
index: index + 1,
|
|
1954
|
+
action: 'computer_' + step.action,
|
|
1955
|
+
...(step.targetId ? { targetId: step.targetId } : {}),
|
|
1956
|
+
...(['fill', 'type'].includes(step.action) ? { characterCount: step.text.length, submit: step.submit } : {}),
|
|
1957
|
+
...(step.action === 'click' ? { pointer: visualPointers.get(index), button: step.button, clicks: step.clicks } : {}),
|
|
1958
|
+
...(step.action === 'key' ? { keys: step.keys } : {}),
|
|
1959
|
+
...(step.action === 'scroll' ? { amount: step.amount } : {}),
|
|
1960
|
+
...(step.delayMs ? { delayMs: step.delayMs } : {}),
|
|
1961
|
+
})) });
|
|
1962
|
+
|
|
1963
|
+
const results = [];
|
|
1964
|
+
let aborted = false;
|
|
1965
|
+
const waitFor = (ms) => new Promise((resolve) => {
|
|
1966
|
+
if (!ms || ctx && ctx.signal && ctx.signal.aborted) { resolve(); return; }
|
|
1967
|
+
const timer = setTimeout(resolve, ms);
|
|
1968
|
+
if (ctx && ctx.signal) ctx.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
|
|
1969
|
+
});
|
|
1970
|
+
|
|
1971
|
+
for (let index = 0; index < steps.length; index += 1) {
|
|
1972
|
+
const step = steps[index];
|
|
1973
|
+
if (ctx && ctx.signal && ctx.signal.aborted) { aborted = true; break; }
|
|
1974
|
+
if (step.delayMs) {
|
|
1975
|
+
stateEvent({ type: 'batch_step', phase: 'scheduled', batchId, stepIndex: index + 1,
|
|
1976
|
+
stepCount: steps.length, action: 'computer_' + step.action, delayMs: step.delayMs,
|
|
1977
|
+
attached: attachedTarget });
|
|
1978
|
+
await waitFor(step.delayMs);
|
|
1979
|
+
if (ctx && ctx.signal && ctx.signal.aborted) { aborted = true; break; }
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
let result = { ok: false, error: 'Passaggio non eseguito.' };
|
|
1983
|
+
const target = needsTarget.has(step.action) ? batchTargets.get(step.targetId) || null : null;
|
|
1984
|
+
const pointer = target ? pointerFor(target) : visualPointers.get(index) || null;
|
|
1985
|
+
stateEvent({ type: 'batch_step', phase: 'start', batchId, stepIndex: index + 1,
|
|
1986
|
+
stepCount: steps.length, action: 'computer_' + step.action,
|
|
1987
|
+
...(pointer ? { pointer } : {}), attached: attachedTarget });
|
|
1988
|
+
|
|
1989
|
+
if (needsTarget.has(step.action) && (!target || !target.enabled)) {
|
|
1990
|
+
result = { ok: false, error: 'Il controllo non e piu disponibile o abilitato.' };
|
|
1991
|
+
} else if (step.action === 'fill' && target.isPassword) {
|
|
1992
|
+
result = { ok: false, error: 'Computer Use non compila password o campi segreti in una sequenza.' };
|
|
1993
|
+
} else if (step.action === 'activate') {
|
|
1994
|
+
// Il mode chiesto dal modello va RISPETTATO, e su un campo di testo
|
|
1995
|
+
// il default diventa un click: il batch continuava a usare
|
|
1996
|
+
// ExpandCollapse, quindi apriva e poi RICHIUDEVA il pannello di
|
|
1997
|
+
// ricerca mentre la ricevuta diceva "activate ok".
|
|
1998
|
+
const mode = activateMode(target, step.mode);
|
|
1999
|
+
result = uiAction && mode !== 'click'
|
|
2000
|
+
? await nativeOrPointer(target, pointer, { action: mode }, (spot) => clickAt(spot))
|
|
2001
|
+
: await clickAt(pointer);
|
|
2002
|
+
} else if (step.action === 'fill') {
|
|
2003
|
+
const fillRequest = { text: step.text, clear: step.clear, submit: step.submit };
|
|
2004
|
+
result = uiAction
|
|
2005
|
+
? await nativeOrPointer(target, pointer, {
|
|
2006
|
+
action: step.text.length > PASTE_THRESHOLD ? 'paste' : 'type',
|
|
2007
|
+
value: step.text, clear: step.clear, submit: step.submit,
|
|
2008
|
+
}, (spot) => fillAt(spot, fillRequest))
|
|
2009
|
+
: await fillAt(pointer, fillRequest);
|
|
2010
|
+
// Anche in sequenza una scrittura va confermata: "5 di 5 completati"
|
|
2011
|
+
// con il campo invariato e' la ricevuta che ha fatto girare a vuoto
|
|
2012
|
+
// interi giri di verifiche.
|
|
2013
|
+
if (result && result.ok === true && (await textLanded(target, step.text)) === false) {
|
|
2014
|
+
const retry = pointer ? await fillAt(pointer, fillRequest) : null;
|
|
2015
|
+
const landed = retry && retry.ok === true ? await textLanded(target, step.text) : false;
|
|
2016
|
+
result = landed === false
|
|
2017
|
+
? { ok: false, code: 'NO_TEXT_WRITTEN', error: 'Il campo non ha ricevuto il testo.' }
|
|
2018
|
+
: { ...(retry || result), method: 'Click+Type' };
|
|
2019
|
+
}
|
|
2020
|
+
} else if (step.action === 'click') {
|
|
2021
|
+
result = await clickAt(pointer, { button: step.button, clicks: step.clicks });
|
|
2022
|
+
} else if (step.action === 'type') {
|
|
2023
|
+
result = await performAction('type', {
|
|
2024
|
+
text: step.text, submit: step.submit,
|
|
2025
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
2026
|
+
});
|
|
2027
|
+
} else if (step.action === 'scroll') {
|
|
2028
|
+
result = await performAction('scroll', {
|
|
2029
|
+
x: pointer.x, y: pointer.y, amount: step.amount,
|
|
2030
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
2031
|
+
});
|
|
2032
|
+
} else if (step.action === 'key') {
|
|
2033
|
+
result = await performAction('key', {
|
|
2034
|
+
keys: step.keys,
|
|
2035
|
+
...(attachedTarget && attachedTarget.windowId ? { windowId: attachedTarget.windowId } : {}),
|
|
2036
|
+
});
|
|
2037
|
+
} else if (step.action === 'wait') {
|
|
2038
|
+
result = { ok: true, waitedMs: step.delayMs };
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
const stepOk = !!(result && result.ok === true);
|
|
2042
|
+
const receipt = { index: index + 1, action: 'computer_' + step.action, ok: stepOk,
|
|
2043
|
+
...(stepOk && result.method ? { method: result.method } : {}),
|
|
2044
|
+
...(stepOk ? {} : { error: String(result && result.error || 'Passaggio non riuscito.') }) };
|
|
2045
|
+
results.push(receipt);
|
|
2046
|
+
stateEvent({ type: 'batch_step', phase: stepOk ? 'complete' : 'error', batchId,
|
|
2047
|
+
stepIndex: index + 1, stepCount: steps.length, action: receipt.action,
|
|
2048
|
+
...(pointer ? { pointer } : {}), attached: attachedTarget,
|
|
2049
|
+
...(stepOk ? {} : { error: receipt.error }) });
|
|
2050
|
+
if (!stepOk && stopOnError) break;
|
|
2051
|
+
}
|
|
2052
|
+
|
|
2053
|
+
// Le coordinate erano tutte ancorate allo stesso snapshot e sono state
|
|
2054
|
+
// validate prima di iniziare: invalidiamo solo al termine del batch.
|
|
2055
|
+
lastSnapshot = null;
|
|
2056
|
+
|
|
2057
|
+
const errorCount = results.filter((result) => !result.ok).length;
|
|
2058
|
+
const completedCount = results.filter((result) => result.ok).length;
|
|
2059
|
+
stateEvent({ type: 'batch', phase: aborted ? 'aborted' : errorCount ? 'error' : 'complete',
|
|
2060
|
+
batchId, stepCount: steps.length, completedCount, errorCount, attached: attachedTarget });
|
|
2061
|
+
const checked = await verification(baseline, aborted ? 'none' : 'delta');
|
|
2062
|
+
const payload = {
|
|
2063
|
+
batchId, aborted, stepCount: steps.length, completedCount, errorCount, steps: results, ...checked,
|
|
2064
|
+
note: checked.verified
|
|
2065
|
+
? 'Esito verificato rileggendo i controlli: nessuno screenshot necessario.'
|
|
2066
|
+
: 'Sequenza terminata. Se serve una verifica, usa una sola computer_inspect finale; evita screenshot intermedi.',
|
|
2067
|
+
};
|
|
2068
|
+
const summary = checked.verified
|
|
2069
|
+
? `${completedCount} di ${steps.length} passaggi eseguiti: ${checked.observation}.`
|
|
2070
|
+
: `${completedCount} di ${steps.length} passaggi eseguiti, risultato finale non confermato.`;
|
|
2071
|
+
if (!aborted && errorCount === 0) {
|
|
2072
|
+
const envelope = checked.verified ? ok : warn;
|
|
2073
|
+
return JSON.stringify(envelope(summary, payload,
|
|
2074
|
+
checked.verified ? [] : [
|
|
2075
|
+
'computer_inspect per confermare lo stato finale',
|
|
2076
|
+
'Non dichiarare completato il compito finche la postcondizione non e visibile',
|
|
2077
|
+
]));
|
|
2078
|
+
}
|
|
2079
|
+
return JSON.stringify(fail(aborted ? 'ABORTED' : 'STEP_FAILED', summary,
|
|
2080
|
+
['computer_inspect per rileggere i controlli e ripartire dal passaggio non riuscito'], payload));
|
|
2081
|
+
},
|
|
2082
|
+
},
|
|
2083
|
+
];
|
|
2084
|
+
|
|
2085
|
+
// Vecchie sessioni possono ricordare il nome iniziale del tool. L'alias evita
|
|
2086
|
+
// una tool call fallita, ma indirizza chiaramente i nuovi piani al nome
|
|
2087
|
+
// canonico computer_snapshot.
|
|
2088
|
+
const snapshotTool = tools.find((tool) => tool.name === 'computer_snapshot');
|
|
2089
|
+
if (snapshotTool) tools.push({
|
|
2090
|
+
...snapshotTool,
|
|
2091
|
+
name: 'computer_screenshot',
|
|
2092
|
+
description: 'Alias di compatibilita per computer_snapshot. Preferisci computer_snapshot nei nuovi piani.',
|
|
2093
|
+
});
|
|
2094
|
+
|
|
2095
|
+
// Nelle superfici Horizon migrate il modello non puo ricadere per inerzia
|
|
2096
|
+
// nel protocollo precedente. Il fallback puo essere riabilitato soltanto
|
|
2097
|
+
// dall'operatore con VASCEND_RAC_LEGACY_FALLBACK=1.
|
|
2098
|
+
const strictAllowed = new Set([
|
|
2099
|
+
'computer_processes', 'computer_launch', 'computer_attach', 'computer_open_path',
|
|
2100
|
+
'computer_detach', 'computer_control', 'computer_control_reset',
|
|
2101
|
+
]);
|
|
2102
|
+
if (externalControllerEnabled) {
|
|
2103
|
+
strictAllowed.add('computer_reactive_goal');
|
|
2104
|
+
strictAllowed.add('computer_reactive_status');
|
|
2105
|
+
}
|
|
2106
|
+
const exposedTools = allowLegacyRac ? tools : tools.filter((tool) => strictAllowed.has(tool.name));
|
|
2107
|
+
/* Nessuna superficie `ui`: Computer Use NON apre piu' una scheda Anteprima.
|
|
2108
|
+
Il lavoro si segue dove sta il resto del run — la riga viva nella tendina
|
|
2109
|
+
delle tool call — invece che in un pannello a fianco che nel caso peggiore
|
|
2110
|
+
restava fermo su "In preparazione". */
|
|
2111
|
+
return exposedTools.map((tool) => ({
|
|
2112
|
+
...tool,
|
|
2113
|
+
...(tool._meta && typeof tool._meta === 'object' ? { _meta: tool._meta } : {}),
|
|
2114
|
+
annotations: {
|
|
2115
|
+
...(tool.annotations && typeof tool.annotations === 'object' ? tool.annotations : {}),
|
|
2116
|
+
openWorldHint: true,
|
|
2117
|
+
},
|
|
2118
|
+
}));
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
module.exports = {
|
|
2122
|
+
VK: Native.VK,
|
|
2123
|
+
MAX_TEXT,
|
|
2124
|
+
MAX_BATCH_STEPS,
|
|
2125
|
+
MAX_BATCH_DELAY_MS,
|
|
2126
|
+
keyCodes,
|
|
2127
|
+
keyChords,
|
|
2128
|
+
runPowerShell: Native.runPowerShell,
|
|
2129
|
+
defaultAction: Native.defaultAction,
|
|
2130
|
+
defaultWindows: Native.defaultWindows,
|
|
2131
|
+
defaultProcesses: Native.defaultProcesses,
|
|
2132
|
+
defaultUiElements: Native.defaultUiElements,
|
|
2133
|
+
defaultUiAction: Native.defaultUiAction,
|
|
2134
|
+
defaultLaunch: Native.defaultLaunch,
|
|
2135
|
+
computerUseTools,
|
|
2136
|
+
};
|