talos-code 0.0.1 → 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/LICENSE +661 -0
- package/README.md +111 -3
- package/THIRD_PARTY_NOTICES.md +40 -0
- package/dist/archive/zip.js +73 -0
- package/dist/args.js +97 -0
- package/dist/automations/history-store.js +34 -0
- package/dist/automations/policy-store.js +43 -0
- package/dist/automations/runner.js +107 -0
- package/dist/automations/schedule.js +85 -0
- package/dist/automations/windows-task.js +53 -0
- package/dist/commands/advanced-cli.js +233 -0
- package/dist/commands/checkpoint-cli.js +45 -0
- package/dist/commands/config-cli.js +107 -0
- package/dist/commands/context.js +17 -0
- package/dist/commands/extensions-cli.js +207 -0
- package/dist/commands/project-cli.js +74 -0
- package/dist/commands/project-commands.js +143 -0
- package/dist/commands/provider-cli.js +185 -0
- package/dist/commands/services-cli.js +248 -0
- package/dist/commands/session-cli.js +171 -0
- package/dist/commands/system-cli.js +405 -0
- package/dist/config/commands.js +60 -0
- package/dist/config/load.js +382 -0
- package/dist/config/migrations.js +48 -0
- package/dist/config/types.js +11 -0
- package/dist/diagnostics/development-log.js +154 -0
- package/dist/diagnostics/doctor.js +104 -0
- package/dist/diagnostics/redact.js +80 -0
- package/dist/diagnostics/zip.js +52 -0
- package/dist/errors.js +156 -0
- package/dist/events/bridge.js +130 -0
- package/dist/extensions/installer.js +178 -0
- package/dist/extensions/package-schema.js +24 -0
- package/dist/headless/result.js +84 -0
- package/dist/headless/run.js +230 -0
- package/dist/i18n/en/approval.js +43 -0
- package/dist/i18n/en/common.js +9 -0
- package/dist/i18n/en/credentials.js +82 -0
- package/dist/i18n/en/errors.js +246 -0
- package/dist/i18n/en/firstrun.js +89 -0
- package/dist/i18n/en/providers.js +37 -0
- package/dist/i18n/en/screen.js +736 -0
- package/dist/i18n/en/tools.js +127 -0
- package/dist/i18n/en.js +14 -0
- package/dist/i18n/error-view.js +307 -0
- package/dist/i18n/index.js +19 -0
- package/dist/i18n/kernel-map.js +139 -0
- package/dist/io.js +45 -0
- package/dist/main.js +330 -0
- package/dist/paths.js +41 -0
- package/dist/protocol/v2/codec.js +9 -0
- package/dist/protocol/v2/events.js +555 -0
- package/dist/protocol/v2/index.js +4 -0
- package/dist/protocol/v2/replay.js +28 -0
- package/dist/protocol/v2/types.js +1 -0
- package/dist/provider/control-plane.js +135 -0
- package/dist/provider/environment-keys.js +275 -0
- package/dist/provider/health.js +110 -0
- package/dist/provider/missing-key.js +48 -0
- package/dist/provider/model-catalog.js +168 -0
- package/dist/provider/provider-text.js +13 -0
- package/dist/provider/store.js +95 -0
- package/dist/provider/system-keyring.js +214 -0
- package/dist/runtime/active-run.js +63 -0
- package/dist/runtime/agent-tree.js +74 -0
- package/dist/runtime/attachments.js +84 -0
- package/dist/runtime/brokered-executor.js +395 -0
- package/dist/runtime/context-status.js +76 -0
- package/dist/runtime/create-runtime.js +111 -0
- package/dist/runtime/model-profile.js +361 -0
- package/dist/runtime/output-store.js +137 -0
- package/dist/runtime/provider-attempts.js +185 -0
- package/dist/runtime/replay-buffer.js +153 -0
- package/dist/runtime/repo.js +95 -0
- package/dist/runtime/session-facade.js +135 -0
- package/dist/runtime/session-summary.js +125 -0
- package/dist/runtime/supervisor.js +262 -0
- package/dist/runtime/talos-composition.js +1006 -0
- package/dist/runtime/types.js +5 -0
- package/dist/runtime/usage-snapshot.js +82 -0
- package/dist/security/auto-classifier.js +38 -0
- package/dist/security/credential-free-environment.js +54 -0
- package/dist/security/evaluate.js +243 -0
- package/dist/security/execution-backends.js +236 -0
- package/dist/security/execution-broker.js +102 -0
- package/dist/security/forge-scan.js +74 -0
- package/dist/security/from-approval.js +39 -0
- package/dist/security/mxc-execution-backend.js +281 -0
- package/dist/security/permission-engine.js +138 -0
- package/dist/security/permission-explanation.js +54 -0
- package/dist/security/persist.js +179 -0
- package/dist/security/plugin-guard.js +230 -0
- package/dist/security/process-tree-evidence-store.js +74 -0
- package/dist/security/process-tree-probe.js +554 -0
- package/dist/security/project-resource-inventory.js +186 -0
- package/dist/security/project-trust-gate.js +38 -0
- package/dist/security/project-trust.js +367 -0
- package/dist/security/rule-parser.js +201 -0
- package/dist/security/shell-segmentation.js +68 -0
- package/dist/security/trust-authority.js +324 -0
- package/dist/security/types.js +1 -0
- package/dist/security/workspace-identity.js +102 -0
- package/dist/services/automation-facade.js +59 -0
- package/dist/services/forge-facade.js +77 -0
- package/dist/services/hook-facade.js +240 -0
- package/dist/services/index.js +16 -0
- package/dist/services/library-facade.js +174 -0
- package/dist/services/mcp-facade.js +348 -0
- package/dist/services/memory-facade.js +126 -0
- package/dist/services/notes-facade.js +157 -0
- package/dist/services/plugin-facade.js +308 -0
- package/dist/services/research-facade.js +110 -0
- package/dist/services/task-board-facade.js +236 -0
- package/dist/services/workflow-catalog.js +246 -0
- package/dist/sessions/export-format.js +98 -0
- package/dist/sessions/metadata-store.js +160 -0
- package/dist/sessions/share.js +122 -0
- package/dist/sessions/transfer.js +16 -0
- package/dist/subcommands.js +62 -0
- package/dist/tui/agent-roster.js +36 -0
- package/dist/tui/app.js +3761 -0
- package/dist/tui/approval.js +36 -0
- package/dist/tui/boot/boot-sequence.js +76 -0
- package/dist/tui/boot/cinematic.js +243 -0
- package/dist/tui/boot/desktop-logo.js +82 -0
- package/dist/tui/boot.js +3 -0
- package/dist/tui/busy-input.js +58 -0
- package/dist/tui/catalog-service.js +559 -0
- package/dist/tui/components/assistant-stream.js +1 -0
- package/dist/tui/components/command-menu.js +68 -0
- package/dist/tui/components/composer.js +219 -0
- package/dist/tui/components/diff.js +35 -0
- package/dist/tui/components/footer.js +48 -0
- package/dist/tui/components/header.js +6 -0
- package/dist/tui/components/markdown.js +305 -0
- package/dist/tui/components/status-indicator.js +32 -0
- package/dist/tui/components/terminal-shell.js +205 -0
- package/dist/tui/components/thinking-row.js +19 -0
- package/dist/tui/components/tool-row.js +97 -0
- package/dist/tui/components/transcript-virtualizer.js +165 -0
- package/dist/tui/components/transcript.js +43 -0
- package/dist/tui/diff-model.js +77 -0
- package/dist/tui/editor-history.js +21 -0
- package/dist/tui/editor.js +210 -0
- package/dist/tui/event-adapter.js +436 -0
- package/dist/tui/exit-output.js +58 -0
- package/dist/tui/external-editor.js +53 -0
- package/dist/tui/file-completion.js +89 -0
- package/dist/tui/focus-manager.js +5 -0
- package/dist/tui/highlight.js +30 -0
- package/dist/tui/input-router.js +18 -0
- package/dist/tui/interrupt.js +99 -0
- package/dist/tui/keybindings.js +194 -0
- package/dist/tui/keymap-resolver.js +91 -0
- package/dist/tui/launch-state.js +139 -0
- package/dist/tui/line-diff.js +57 -0
- package/dist/tui/live-activity.js +45 -0
- package/dist/tui/metrics.js +123 -0
- package/dist/tui/onboarding.js +36 -0
- package/dist/tui/overlays/agent-tree.js +39 -0
- package/dist/tui/overlays/approval-dialog.js +640 -0
- package/dist/tui/overlays/automation-center.js +51 -0
- package/dist/tui/overlays/checkpoint-picker.js +47 -0
- package/dist/tui/overlays/context-inspector.js +36 -0
- package/dist/tui/overlays/effort-line.js +110 -0
- package/dist/tui/overlays/forge-center.js +46 -0
- package/dist/tui/overlays/help-dialog.js +16 -0
- package/dist/tui/overlays/history-picker.js +37 -0
- package/dist/tui/overlays/hook-center.js +70 -0
- package/dist/tui/overlays/library-center.js +55 -0
- package/dist/tui/overlays/mcp-center.js +60 -0
- package/dist/tui/overlays/memory-center.js +63 -0
- package/dist/tui/overlays/model-picker.js +72 -0
- package/dist/tui/overlays/notes-center.js +53 -0
- package/dist/tui/overlays/overlay-host.js +8 -0
- package/dist/tui/overlays/plugin-center.js +76 -0
- package/dist/tui/overlays/provider-picker.js +145 -0
- package/dist/tui/overlays/queue-editor.js +32 -0
- package/dist/tui/overlays/research-center.js +59 -0
- package/dist/tui/overlays/scroll-window.js +234 -0
- package/dist/tui/overlays/session-picker.js +115 -0
- package/dist/tui/overlays/tasks-center.js +83 -0
- package/dist/tui/overlays/theme-picker.js +21 -0
- package/dist/tui/overlays/transcript-search.js +58 -0
- package/dist/tui/overlays/trust-center.js +29 -0
- package/dist/tui/overlays/workflow-center.js +46 -0
- package/dist/tui/project-file-index.js +214 -0
- package/dist/tui/project-references.js +85 -0
- package/dist/tui/project-trust-prompt.js +287 -0
- package/dist/tui/prompt-history-store.js +116 -0
- package/dist/tui/queue-store.js +110 -0
- package/dist/tui/regions/budget.js +59 -0
- package/dist/tui/regions/views.js +96 -0
- package/dist/tui/render-coordinator.js +148 -0
- package/dist/tui/render-scheduler.js +4 -0
- package/dist/tui/run.js +69 -0
- package/dist/tui/selection-list.js +29 -0
- package/dist/tui/session-controller.js +778 -0
- package/dist/tui/session-export.js +54 -0
- package/dist/tui/shell-input.js +35 -0
- package/dist/tui/shell-layout.js +48 -0
- package/dist/tui/shell-model.js +98 -0
- package/dist/tui/slash-commands.js +80 -0
- package/dist/tui/state.js +151 -0
- package/dist/tui/status-bar.js +125 -0
- package/dist/tui/status-view.js +40 -0
- package/dist/tui/terminal-capabilities.js +9 -0
- package/dist/tui/terminal-session.js +11 -0
- package/dist/tui/text-width.js +66 -0
- package/dist/tui/theme-catalog.js +25 -0
- package/dist/tui/theme-store.js +23 -0
- package/dist/tui/theme.js +23 -0
- package/dist/tui/tool-display.js +135 -0
- package/dist/tui/tool-facts.js +1 -0
- package/dist/tui/tools/bash-renderer.js +32 -0
- package/dist/tui/tools/change.js +61 -0
- package/dist/tui/tools/edit-renderer.js +17 -0
- package/dist/tui/tools/generic-renderer.js +13 -0
- package/dist/tui/tools/list-renderer.js +14 -0
- package/dist/tui/tools/read-renderer.js +13 -0
- package/dist/tui/tools/registry.js +20 -0
- package/dist/tui/tools/row-format.js +187 -0
- package/dist/tui/tools/search-renderer.js +38 -0
- package/dist/tui/tools/shared.js +97 -0
- package/dist/tui/tools/write-renderer.js +15 -0
- package/dist/tui/transcript-model.js +441 -0
- package/dist/tui/ui-preferences.js +79 -0
- package/dist/tui/usage-view.js +77 -0
- package/dist/tui/vim-mode.js +99 -0
- package/dist/update/check.js +15 -0
- package/dist/update/npm.js +51 -0
- package/dist/update/run.js +129 -0
- package/dist/version.js +2 -0
- package/dist/workspace/checkpoint-store.js +210 -0
- package/dist/workspace/checkpoint.js +413 -0
- package/dist/workspace/restore.js +232 -0
- package/package.json +63 -5
- package/vendor/context-engine/package.json +11 -0
- package/vendor/context-engine/src/compaction-planner.mjs +57 -0
- package/vendor/context-engine/src/contracts.mjs +48 -0
- package/vendor/context-engine/src/engine.mjs +445 -0
- package/vendor/context-engine/src/node/context-export.mjs +164 -0
- package/vendor/context-engine/src/node/legacy-import.mjs +92 -0
- package/vendor/context-engine/src/node/migrations/001-context.sql +126 -0
- package/vendor/context-engine/src/node/sqlite-store.mjs +85 -0
- package/vendor/context-engine/src/node/sqlite-worker.mjs +559 -0
- package/vendor/context-engine/src/profiles.mjs +10 -0
- package/vendor/context-engine/src/retrieval.mjs +104 -0
- package/vendor/context-engine/src/summary.mjs +97 -0
- package/vendor/context-engine/src/usage.mjs +34 -0
- package/vendor/harness-ui/package.json +38 -0
- package/vendor/harness-ui/src/acp-agent.mjs +350 -0
- package/vendor/harness-ui/src/agent-service.mjs +2188 -0
- package/vendor/harness-ui/src/agui-events.mjs +452 -0
- package/vendor/harness-ui/src/ambiente-solo-server.mjs +121 -0
- package/vendor/harness-ui/src/artifact-store.mjs +39 -0
- package/vendor/harness-ui/src/assistenza.mjs +123 -0
- package/vendor/harness-ui/src/automation-scheduler.mjs +69 -0
- package/vendor/harness-ui/src/automation-store.mjs +145 -0
- package/vendor/harness-ui/src/browser-annota.mjs +639 -0
- package/vendor/harness-ui/src/browser-frame.mjs +211 -0
- package/vendor/harness-ui/src/browser-proxy-universale.mjs +519 -0
- package/vendor/harness-ui/src/browser-proxy.mjs +87 -0
- package/vendor/harness-ui/src/browser-sessione-viva.mjs +329 -0
- package/vendor/harness-ui/src/browser-stream.mjs +445 -0
- package/vendor/harness-ui/src/browser-vivo.mjs +694 -0
- package/vendor/harness-ui/src/chat-image-attachments.mjs +72 -0
- package/vendor/harness-ui/src/config.mjs +697 -0
- package/vendor/harness-ui/src/contesto-del-progetto.mjs +385 -0
- package/vendor/harness-ui/src/context-asset-adapter.mjs +72 -0
- package/vendor/harness-ui/src/context-desktop-service.mjs +253 -0
- package/vendor/harness-ui/src/context-embedding-runtime.mjs +252 -0
- package/vendor/harness-ui/src/context-inference-scheduler.mjs +81 -0
- package/vendor/harness-ui/src/context-native-compaction.mjs +75 -0
- package/vendor/harness-ui/src/context-provider-adapter.mjs +141 -0
- package/vendor/harness-ui/src/context-runtime.mjs +118 -0
- package/vendor/harness-ui/src/context-token-counters.mjs +184 -0
- package/vendor/harness-ui/src/context-tool-catalog.mjs +86 -0
- package/vendor/harness-ui/src/context-tool-output.mjs +72 -0
- package/vendor/harness-ui/src/costo-elenco.mjs +252 -0
- package/vendor/harness-ui/src/custom-task.mjs +171 -0
- package/vendor/harness-ui/src/doctor.mjs +142 -0
- package/vendor/harness-ui/src/document-filename.mjs +97 -0
- package/vendor/harness-ui/src/document-generator.mjs +493 -0
- package/vendor/harness-ui/src/document-report.mjs +331 -0
- package/vendor/harness-ui/src/duckduckgo-search.mjs +155 -0
- package/vendor/harness-ui/src/elenco-profondo.mjs +337 -0
- package/vendor/harness-ui/src/favicon-proxy.mjs +113 -0
- package/vendor/harness-ui/src/forge-contract.mjs +221 -0
- package/vendor/harness-ui/src/frequent-dirs.mjs +134 -0
- package/vendor/harness-ui/src/generated-image-store.mjs +147 -0
- package/vendor/harness-ui/src/generation-idle.mjs +332 -0
- package/vendor/harness-ui/src/gguf-header.mjs +207 -0
- package/vendor/harness-ui/src/git-service.mjs +626 -0
- package/vendor/harness-ui/src/gitignore-elenco.mjs +604 -0
- package/vendor/harness-ui/src/harness-receipt-keypair.mjs +207 -0
- package/vendor/harness-ui/src/hf-direct-transfer.mjs +170 -0
- package/vendor/harness-ui/src/hf-hub-client.mjs +106 -0
- package/vendor/harness-ui/src/hf-image-proxy.mjs +105 -0
- package/vendor/harness-ui/src/hf-model-transfer.mjs +245 -0
- package/vendor/harness-ui/src/hook-registry.mjs +186 -0
- package/vendor/harness-ui/src/http-app.mjs +6259 -0
- package/vendor/harness-ui/src/http-lifecycle.mjs +132 -0
- package/vendor/harness-ui/src/id-archivio.mjs +27 -0
- package/vendor/harness-ui/src/image-generator.mjs +143 -0
- package/vendor/harness-ui/src/istruzioni-di-progetto.mjs +234 -0
- package/vendor/harness-ui/src/kernel/dist/kernelPerIlBanco.js +518 -0
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +10437 -0
- package/vendor/harness-ui/src/library-policy-store.mjs +175 -0
- package/vendor/harness-ui/src/library-store.mjs +652 -0
- package/vendor/harness-ui/src/llama-server-supervisor.mjs +629 -0
- package/vendor/harness-ui/src/local-model-store.mjs +339 -0
- package/vendor/harness-ui/src/local-runtime-contract.mjs +66 -0
- package/vendor/harness-ui/src/local-runtime-events.mjs +44 -0
- package/vendor/harness-ui/src/local-runtime-llama-server.mjs +244 -0
- package/vendor/harness-ui/src/local-runtime-probe.mjs +401 -0
- package/vendor/harness-ui/src/machine-capacity.mjs +66 -0
- package/vendor/harness-ui/src/mappa-cartelle.mjs +491 -0
- package/vendor/harness-ui/src/mcp-client.mjs +98 -0
- package/vendor/harness-ui/src/mcp-registry.mjs +157 -0
- package/vendor/harness-ui/src/mcp-session.mjs +177 -0
- package/vendor/harness-ui/src/memory-store.mjs +227 -0
- package/vendor/harness-ui/src/model-catalog-models-dev.mjs +276 -0
- package/vendor/harness-ui/src/model-catalog.mjs +129 -0
- package/vendor/harness-ui/src/model-destination.mjs +189 -0
- package/vendor/harness-ui/src/modifica-ancorata.mjs +177 -0
- package/vendor/harness-ui/src/native-provider-adapter.mjs +205 -0
- package/vendor/harness-ui/src/notes-store.mjs +250 -0
- package/vendor/harness-ui/src/openai-compatible-runtime.mjs +428 -0
- package/vendor/harness-ui/src/openrouter-oauth.mjs +339 -0
- package/vendor/harness-ui/src/path-policy.mjs +442 -0
- package/vendor/harness-ui/src/plugin-registry.mjs +780 -0
- package/vendor/harness-ui/src/plugin-session.mjs +180 -0
- package/vendor/harness-ui/src/process-policy.mjs +345 -0
- package/vendor/harness-ui/src/prompt-enhancer-provider.mjs +94 -0
- package/vendor/harness-ui/src/provider-auth-cloud.mjs +95 -0
- package/vendor/harness-ui/src/provider-credential-store.mjs +541 -0
- package/vendor/harness-ui/src/provider-probe.mjs +582 -0
- package/vendor/harness-ui/src/provider-registry.mjs +1633 -0
- package/vendor/harness-ui/src/pty-terminal.mjs +312 -0
- package/vendor/harness-ui/src/public-problem.mjs +109 -0
- package/vendor/harness-ui/src/research/card.mjs +235 -0
- package/vendor/harness-ui/src/research/citations.mjs +142 -0
- package/vendor/harness-ui/src/research/collector.mjs +275 -0
- package/vendor/harness-ui/src/research/deposito-a-pezzi.mjs +139 -0
- package/vendor/harness-ui/src/research/dossier.mjs +114 -0
- package/vendor/harness-ui/src/research/esportazioni.mjs +560 -0
- package/vendor/harness-ui/src/research/fetch-cache.mjs +465 -0
- package/vendor/harness-ui/src/research/fidelity.mjs +122 -0
- package/vendor/harness-ui/src/research/independence.mjs +159 -0
- package/vendor/harness-ui/src/research/ledger.mjs +166 -0
- package/vendor/harness-ui/src/research/markdown-server.mjs +565 -0
- package/vendor/harness-ui/src/research/narration.mjs +181 -0
- package/vendor/harness-ui/src/research/open-cards.mjs +131 -0
- package/vendor/harness-ui/src/research/opposing.mjs +305 -0
- package/vendor/harness-ui/src/research/outline.mjs +111 -0
- package/vendor/harness-ui/src/research/page-budget.mjs +209 -0
- package/vendor/harness-ui/src/research/pdf.mjs +291 -0
- package/vendor/harness-ui/src/research/plan.mjs +301 -0
- package/vendor/harness-ui/src/research/raccolta-viva.mjs +452 -0
- package/vendor/harness-ui/src/research/recheck-document.mjs +69 -0
- package/vendor/harness-ui/src/research/recheck-history.mjs +192 -0
- package/vendor/harness-ui/src/research/recheck.mjs +194 -0
- package/vendor/harness-ui/src/research/report.mjs +203 -0
- package/vendor/harness-ui/src/research/run.mjs +527 -0
- package/vendor/harness-ui/src/research/synthesis.mjs +318 -0
- package/vendor/harness-ui/src/research/verification.mjs +572 -0
- package/vendor/harness-ui/src/research-orchestrator.mjs +2679 -0
- package/vendor/harness-ui/src/research-store.mjs +1133 -0
- package/vendor/harness-ui/src/runtime-build-manifest.mjs +26 -0
- package/vendor/harness-ui/src/runtime-contract.mjs +59 -0
- package/vendor/harness-ui/src/runtime-owner-adapter.mjs +1348 -0
- package/vendor/harness-ui/src/runtime-owner-contract.mjs +32 -0
- package/vendor/harness-ui/src/scheda-di-lavoro.mjs +249 -0
- package/vendor/harness-ui/src/search-source-store.mjs +172 -0
- package/vendor/harness-ui/src/session-registry.mjs +6095 -0
- package/vendor/harness-ui/src/session-store.mjs +220 -0
- package/vendor/harness-ui/src/sessione-pronta.mjs +73 -0
- package/vendor/harness-ui/src/setup-stato.mjs +31 -0
- package/vendor/harness-ui/src/sezioni-istruzioni.mjs +204 -0
- package/vendor/harness-ui/src/skill-registry.mjs +120 -0
- package/vendor/harness-ui/src/sse-replay-coalescente.mjs +0 -0
- package/vendor/harness-ui/src/static-files.mjs +96 -0
- package/vendor/harness-ui/src/stream-partition.mjs +123 -0
- package/vendor/harness-ui/src/subagent-orchestrator.mjs +453 -0
- package/vendor/harness-ui/src/task-catalog.mjs +65 -0
- package/vendor/harness-ui/src/tasks-store.mjs +220 -0
- package/vendor/harness-ui/src/terminal-registry.mjs +312 -0
- package/vendor/harness-ui/src/terminal-ws.mjs +170 -0
- package/vendor/harness-ui/src/tool-forge-store.mjs +299 -0
- package/vendor/harness-ui/src/tool-schema-normalize.mjs +100 -0
- package/vendor/harness-ui/src/usage-cache.mjs +315 -0
- package/vendor/harness-ui/src/workspace-browser.mjs +213 -0
- package/vendor/harness-ui/src/workspace-context.mjs +124 -0
- package/vendor/harness-ui/src/workspace-disk.mjs +62 -0
- package/vendor/harness-ui/src/workspace-files.mjs +589 -0
- package/vendor/harness-ui/src/workspace-info.mjs +189 -0
- package/vendor/harness-ui/src/workspace-launch-store.mjs +150 -0
- package/vendor/harness-ui/src/workspace-tree.mjs +67 -0
- package/vendor/harness-ui/src/workspace-watcher.mjs +161 -0
- package/vendor/manifest.json +170 -0
|
@@ -0,0 +1,2188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-service.mjs — espone `talosLavora` (AVM-harness) come servizio per
|
|
3
|
+
* Harness UI. Piano `elegant-spinning-dongarra.md`, FASE 1, §1.2.
|
|
4
|
+
*
|
|
5
|
+
* ⛔⛔⛔ 02/09 — CORRETTO, questo commento era rimasto FALSO per due giorni.
|
|
6
|
+
* Descriveva un import relativo diretto verso `talosHarness.mjs` come
|
|
7
|
+
* cartella sorella — dal commit `16677c48` (31/8) quell'import non esiste
|
|
8
|
+
* più: `talosLavora` (riga 104 sotto) passa per `createOwnerRuntimeAdapter()`
|
|
9
|
+
* (`runtime-owner-adapter.mjs`), che carica il kernel SOLO se
|
|
10
|
+
* `TALOS_OWNER_RUNTIME_MODULE` è impostata a un percorso assoluto —
|
|
11
|
+
* altrimenti lancia `OwnerRuntimeUnavailableError` su OGNI giro, nuovo o
|
|
12
|
+
* ripreso. La cartella sorella (`AVM-harness/mobile/scripts/harness-talos/
|
|
13
|
+
* talosHarness.mjs`, verificata il 24/8 e ancora vera oggi) resta il valore
|
|
14
|
+
* giusto da passare in quella variabile — ma va DICHIARATA a chi avvia il
|
|
15
|
+
* server, non più assunta dal codice. Il server è rimasto acceso per due
|
|
16
|
+
* giorni con la copia PRECEDENTE (senza questo requisito) finché un riavvio
|
|
17
|
+
* il 02/09 non ha esposto il buco: ogni sessione, nuova o ripresa,
|
|
18
|
+
* rispondeva "Il runtime agente non è configurato per questa
|
|
19
|
+
* installazione." Vedi `.claude/LEDGER-RUNTIME-OWNER-MODULE-2026-09-02.md`.
|
|
20
|
+
*
|
|
21
|
+
* ⛔ `talosLavora` non sa niente di AG-UI: riporta dati grezzi (`onGiro`,
|
|
22
|
+
* `onScrittura`) tramite i quattro parametri opzionali aggiunti in
|
|
23
|
+
* AVM-harness (piano §1.2, stesso giorno). Questo file è il SOLO punto dove
|
|
24
|
+
* quei dati grezzi diventano eventi AG-UI (agui-events.mjs) — la separazione
|
|
25
|
+
* è deliberata: talosLavora resta provabile senza sapere di HTTP/SSE, e
|
|
26
|
+
* agui-events.mjs resta provabile senza sapere di talosLavora.
|
|
27
|
+
*/
|
|
28
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
29
|
+
import { join as joinPercorso, relative as percorsoRelativo, sep as separatorePercorso } from 'node:path';
|
|
30
|
+
|
|
31
|
+
import { createOwnerRuntimeAdapter } from './runtime-owner-adapter.mjs';
|
|
32
|
+
import { salvaArtefatto as salvaArtefattoReale } from './artifact-store.mjs';
|
|
33
|
+
import { salvaVoce as salvaVoceLibreriaReale } from './library-store.mjs'; // 06/9: un artefatto e' lavoro, e il lavoro si ritrova
|
|
34
|
+
import { generateTalosDocument as generateTalosDocumentReale, TALOS_SOURCE_TEXT_FORMATS, verifyTalosDocument as verifyTalosDocumentReale } from './document-generator.mjs';
|
|
35
|
+
import { generaImmagineOpenRouter as generaImmagineOpenRouterReale } from './image-generator.mjs';
|
|
36
|
+
import { leggiContestoWorkspace as leggiContestoWorkspaceReale } from './workspace-context.mjs';
|
|
37
|
+
import { contestoDelProgetto as contestoDelProgettoReale, aggiornamentoInCoda as aggiornamentoInCodaReale } from './contesto-del-progetto.mjs'; // BC-07 (11/09): il preambolo a 4 blocchi, e l'aggiornamento che si APPENDE invece di riscrivere il prefisso
|
|
38
|
+
import { creaFiltroGitignore as creaFiltroGitignoreReale } from './gitignore-elenco.mjs'; // P-13: le regole che decidono cosa NON elencare
|
|
39
|
+
import { creaFileWorkspace as creaFileWorkspaceReale, WorkspaceFileError } from './workspace-files.mjs';
|
|
40
|
+
import { preparaToolMcpPerSessione as preparaToolMcpPerSessioneReale } from './mcp-session.mjs';
|
|
41
|
+
import { caricaSkill as caricaSkillReale } from './skill-registry.mjs';
|
|
42
|
+
import { schemaIngressoAttrezzo } from './tool-schema-normalize.mjs';
|
|
43
|
+
import {
|
|
44
|
+
cercaVoci as cercaVociLibreria,
|
|
45
|
+
creaCursoriLibreria,
|
|
46
|
+
elencaVoci as elencaVociReale,
|
|
47
|
+
elencaVociConTesto as elencaVociConTestoReale,
|
|
48
|
+
eliminaVoce as eliminaVoceReale,
|
|
49
|
+
eliminaVoci as eliminaVociReale,
|
|
50
|
+
impaginaVoci as impaginaVociLibreria,
|
|
51
|
+
leggiBytesVoce as leggiBytesVoceReale,
|
|
52
|
+
leggiVoce as leggiVoceReale,
|
|
53
|
+
origineVoce as origineVoceReale,
|
|
54
|
+
rinominaVoce as rinominaVoceReale,
|
|
55
|
+
trovaVoce as trovaVoceLibreria,
|
|
56
|
+
} from './library-store.mjs';
|
|
57
|
+
import {
|
|
58
|
+
aggiornaNota as aggiornaNotaReale,
|
|
59
|
+
creaNota as creaNotaReale,
|
|
60
|
+
elencaNote as elencaNoteReale,
|
|
61
|
+
eliminaNota as eliminaNotaReale,
|
|
62
|
+
leggiNota as leggiNotaReale,
|
|
63
|
+
} from './notes-store.mjs';
|
|
64
|
+
import {
|
|
65
|
+
aggiornaAttivita as aggiornaAttivitaReale,
|
|
66
|
+
completaAttivita as completaAttivitaReale,
|
|
67
|
+
creaAttivita as creaAttivitaReale,
|
|
68
|
+
elencaAttivita as elencaAttivitaReale,
|
|
69
|
+
eliminaAttivita as eliminaAttivitaReale,
|
|
70
|
+
leggiAttivita as leggiAttivitaReale,
|
|
71
|
+
} from './tasks-store.mjs';
|
|
72
|
+
import {
|
|
73
|
+
aggiornaMemoria as aggiornaMemoriaReale,
|
|
74
|
+
cercaMemorie,
|
|
75
|
+
creaMemoria as creaMemoriaReale,
|
|
76
|
+
elencaMemorie as elencaMemorieReale,
|
|
77
|
+
eliminaMemoria as eliminaMemoriaReale,
|
|
78
|
+
leggiMemoria as leggiMemoriaReale,
|
|
79
|
+
} from './memory-store.mjs';
|
|
80
|
+
import {
|
|
81
|
+
elencaToolForgiati as elencaToolForgiatiReale,
|
|
82
|
+
installaToolForgiato as installaToolForgiatoReale,
|
|
83
|
+
ToolForgeStoreError,
|
|
84
|
+
} from './tool-forge-store.mjs';
|
|
85
|
+
import {
|
|
86
|
+
MODALITA_SUPPORTATE as MODALITA_SUPPORTATE_LIBRERIA,
|
|
87
|
+
creaRicevutaPolitica as creaRicevutaPoliticaLibreria,
|
|
88
|
+
creaRicevutePolitica as creaRicevutePoliticaLibreria,
|
|
89
|
+
leggiPolitica as leggiPoliticaReale,
|
|
90
|
+
ricordaRicevutaPolitica as ricordaRicevutaPoliticaLibreria,
|
|
91
|
+
scriviPolitica as scriviPoliticaReale,
|
|
92
|
+
} from './library-policy-store.mjs';
|
|
93
|
+
import { improntaPacchettoPlugin, statoTrustPlugin } from './plugin-registry.mjs';
|
|
94
|
+
|
|
95
|
+
/*
|
|
96
|
+
* ⛔ A-6(b): le frasi del RIPIEGO della riverifica all'uso. `statoTrustPlugin` porta già la sua
|
|
97
|
+
* `frase` per ogni motivo; queste servono solo se un motivo nuovo nascesse senza frase, e ci
|
|
98
|
+
* sono perché il ripiego precedente ne diceva UNA SOLA — «non è più quello che hai approvato» —
|
|
99
|
+
* anche quando il motivo era `mai-approvato`, cioè raccontava una manomissione dove c'era
|
|
100
|
+
* soltanto una fiducia mai data.
|
|
101
|
+
*/
|
|
102
|
+
const FRASI_RIVERIFICA = Object.freeze({
|
|
103
|
+
'mai-approvato': 'Questo plugin non è approvato: aprilo dal pannello e approvalo se lo vuoi usare.',
|
|
104
|
+
'regola-precedente': 'Questo plugin era stato approvato con la regola precedente: approvalo di nuovo.',
|
|
105
|
+
'contenuto-cambiato': 'Il contenuto di questo plugin è cambiato da quando l\'hai approvato.',
|
|
106
|
+
});
|
|
107
|
+
import { preparaToolPluginPerSessione as preparaToolPluginPerSessioneReale } from './plugin-session.mjs';
|
|
108
|
+
import { eseguiHook as eseguiHookReale } from './hook-registry.mjs';
|
|
109
|
+
import {
|
|
110
|
+
artifactCreated,
|
|
111
|
+
eventiPerRisposta,
|
|
112
|
+
comandoUtenteFinito, comandoUtenteIniziato, eventoPerEsitoTool, toolCallOutput,
|
|
113
|
+
eventoPerScrittura,
|
|
114
|
+
eventoPerUsage,
|
|
115
|
+
hookInvoked,
|
|
116
|
+
reasoningMessageContent,
|
|
117
|
+
reasoningMessageEnd,
|
|
118
|
+
reasoningMessageStart,
|
|
119
|
+
runError,
|
|
120
|
+
runFinished,
|
|
121
|
+
runStarted,
|
|
122
|
+
textMessageContent,
|
|
123
|
+
textMessageEnd,
|
|
124
|
+
textMessageStart,
|
|
125
|
+
toolCallArgs,
|
|
126
|
+
toolCallStart,
|
|
127
|
+
} from './agui-events.mjs';
|
|
128
|
+
|
|
129
|
+
const OWNER_RUNTIME = createOwnerRuntimeAdapter();
|
|
130
|
+
const chiamaConRitenta = (options) => OWNER_RUNTIME.chiamaConRitenta(options);
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* ⛔⛔⛔ 06/9, owner: «il modello deve avere gli occhi sulla sezione Browser anche se sono io a
|
|
134
|
+
* navigarci dentro». La pagina la legge il SERVER, con la stessa funzione dell'attrezzo `naviga`
|
|
135
|
+
* (validazione degli indirizzi già scritta e provata): dal browser non si può, il confine di
|
|
136
|
+
* origine lo vieta. Esportata qui perché la rotta HTTP la usi senza conoscere l'adattatore.
|
|
137
|
+
*/
|
|
138
|
+
export function leggiPaginaPerLaVista(url) {
|
|
139
|
+
return OWNER_RUNTIME.leggiPagina(url);
|
|
140
|
+
}
|
|
141
|
+
const compattaConversazioneReale = (messaggi, chiamaModello) => OWNER_RUNTIME.compattaConversazione(messaggi, chiamaModello);
|
|
142
|
+
const eseguiComandoSandboxatoReale = (...args) => OWNER_RUNTIME.eseguiComandoSandboxato(...args);
|
|
143
|
+
const eseguiFlowForge = (...args) => OWNER_RUNTIME.eseguiFlowForge(...args);
|
|
144
|
+
const FORGE_PREFISSO_NOME_TOOL = OWNER_RUNTIME.forgeToolPrefix;
|
|
145
|
+
const talosLavoraReale = (input) => OWNER_RUNTIME.talosLavora(input);
|
|
146
|
+
const validaManifestForge = (manifest) => OWNER_RUNTIME.validaManifestForge(manifest);
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* ⭐⭐⭐ 28/8 — un artefatto molto grande sarebbe un evento SSE molto grande
|
|
150
|
+
* (l'html non passa dal tetto di 8.000 caratteri che il kernel applica al
|
|
151
|
+
* TESTO tornato al modello — quel tetto riguarda `esito`, non l'html grezzo
|
|
152
|
+
* inoltrato qui via `onArtefatto`, side-channel separato, stesso principio
|
|
153
|
+
* di `onScrittura`). Rifiutato PRIMA di costruire l'evento, mai troncato in
|
|
154
|
+
* silenzio: un HTML troncato a metà tag è peggio di un rifiuto dichiarato.
|
|
155
|
+
*/
|
|
156
|
+
const ARTEFATTO_MAX_BYTE = 400_000; // stesso tetto di artifactTools.ts mobile
|
|
157
|
+
|
|
158
|
+
/** ⭐ 29/8 — FASE H: i `media_type`/mime VERI che i due percorsi di image-generator.mjs possono tornare (png sempre, jpeg/webp se il fornitore li dichiara) — mai un'estensione inventata per un formato ignoto, ricade su 'png' onestamente. */
|
|
159
|
+
const ESTENSIONE_PER_MEDIA_TYPE = Object.freeze({ 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp' });
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* ⛔ 'giri-esauriti' e 'fermato' sono ENTRAMBI RunError, non solo il primo:
|
|
163
|
+
* nessuno dei due è un successo, e trattarli diversamente costringerebbe
|
|
164
|
+
* ogni chiamante a conoscere il vocabolario interno di talosLavora invece di
|
|
165
|
+
* leggere lo standard AG-UI. `code` porta la stringa originale — chi vuole
|
|
166
|
+
* distinguerli può farlo senza che questo file decida per lui.
|
|
167
|
+
*/
|
|
168
|
+
function esitoInEventoFinale({ threadId, runId, esito }) {
|
|
169
|
+
if (esito.comeFinita === 'concluso') {
|
|
170
|
+
return runFinished({
|
|
171
|
+
threadId,
|
|
172
|
+
runId,
|
|
173
|
+
outcome: { type: 'success' },
|
|
174
|
+
/*
|
|
175
|
+
* ⭐⭐⭐ Piano procedi-col-generare-un-snoopy-neumann.md, Fase 3 —
|
|
176
|
+
* `esito.usage` esisteva già in `talosHarness.mjs` (accumulato per
|
|
177
|
+
* TALOS-BANCO) ma veniva scartato qui: mai tradotto in nessun
|
|
178
|
+
* evento, quindi mai visibile a chi guarda una sessione HTTP. `null`
|
|
179
|
+
* quando nessun giro l'ha mai riportato — inoltrato com'è, mai
|
|
180
|
+
* inventato.
|
|
181
|
+
*/
|
|
182
|
+
result: { detto: esito.detto, compattazioni: esito.compattazioni, premesseNegate: esito.premesseNegate, usage: esito.usage ?? null },
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
return runError({ message: esito.detto, code: esito.comeFinita });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Avvia un task vero attraverso `talosLavora` e traduce ogni evento in AG-UI,
|
|
190
|
+
* consegnandolo a `onEvento` man mano che accade (mai in un batch finale: chi
|
|
191
|
+
* guarda una sessione dal vivo deve vedere i tool-call mentre succedono, non
|
|
192
|
+
* dopo).
|
|
193
|
+
*
|
|
194
|
+
* @param {object} input
|
|
195
|
+
* @param {string} input.cartella
|
|
196
|
+
* @param {{consegna:string}} input.task
|
|
197
|
+
* @param {string} input.modello
|
|
198
|
+
* @param {string} input.chiave
|
|
199
|
+
* @param {string} [input.comandoProva]
|
|
200
|
+
* @param {(evento:object)=>void} input.onEvento
|
|
201
|
+
* @param {AbortSignal} [input.segnaleStop]
|
|
202
|
+
* @param {Array<object>} [input.messaggiIniziali] — per resume/fork (§1.4)
|
|
203
|
+
* @param {{effort?:string, summary?:string}} [input.reasoning] — ⭐ 27/8, R1:
|
|
204
|
+
* passato così com'è a talosLavora/OpenRouter. Assente: nessun ragionamento
|
|
205
|
+
* richiesto, nessun evento Reasoning*, comportamento di prima.
|
|
206
|
+
* @param {boolean} [input.mobile] — piano `procedi-col-generare-un-snoopy-neumann.md`,
|
|
207
|
+
* Fase 3: sessione avviata da un client mobile. Solo l'attrezzo `shell`
|
|
208
|
+
* se ne accorge (vedi talosHarness.mjs, `eseguiComandoSandboxato`) — ogni
|
|
209
|
+
* altro attrezzo si comporta identico, `false` è il default di sempre.
|
|
210
|
+
* @param {()=>string|null|undefined} [input.codaMessaggiFn] — FASE D (28/8),
|
|
211
|
+
* coda messaggi. Stesso principio di `hookFn`/`onDelega`: inoltrato SENZA
|
|
212
|
+
* logica propria, la coda vera (FIFO, per-sessione) vive in
|
|
213
|
+
* `session-registry.mjs`. Il kernel la chiama SOLO quando un giro conclude
|
|
214
|
+
* senza tool-call — vedi `talosHarness.mjs`, LEDGER-FASE-D-CODA.md.
|
|
215
|
+
* @param {typeof talosLavoraReale} [input.talosLavoraFn] — SOLO per test: la
|
|
216
|
+
* funzione reale è il default, iniettarne una finta evita di dover far
|
|
217
|
+
* girare un vero ciclo (già provato per conto suo in AVM-harness) solo per
|
|
218
|
+
* provare la traduzione degli eventi.
|
|
219
|
+
* @returns {Promise<{threadId:string, runId:string, ok:boolean, esito:object|null, erroreInterno:string|null}>}
|
|
220
|
+
* Non lancia MAI: un fallimento interno (talosLavora che getta invece di
|
|
221
|
+
* tornare un esito) emette comunque un RunError su `onEvento` prima di
|
|
222
|
+
* tornare — un chiamante HTTP/SSE non deve gestire due canali di errore
|
|
223
|
+
* diversi (eccezione E RunError) per la stessa cosa.
|
|
224
|
+
*/
|
|
225
|
+
export async function avviaSessione({
|
|
226
|
+
cartella, task, modello, chiave, comandoProva,
|
|
227
|
+
/*
|
|
228
|
+
* ⛔⛔⛔ 11/09/2026 — DOVE FINISCE UN FILE *GENERATO*, che non è dove il modello LEGGE.
|
|
229
|
+
*
|
|
230
|
+
* Difetto misurato sulla sessione `91ae0634` dell'owner: `document_create` rispondeva
|
|
231
|
+
* `EPERM: operation not permitted, open 'C:\Qwen 3.8 ….pdf'`. Il PDF era corretto; falliva solo
|
|
232
|
+
* la scrittura, e sempre alla RADICE DEL DISCO. La catena, per intero:
|
|
233
|
+
* 1. la sessione nasce su `C:\Users\…\AVM-harness-desktop` con permesso «Read only»;
|
|
234
|
+
* 2. l'owner alza il permesso a «Full access» (riga 2242 del suo `.jsonl`);
|
|
235
|
+
* 3. `session-registry.mjs` ricalcola la cartella effettiva con `cartellaEffettivaPerPermessi`,
|
|
236
|
+
* che per «Full access» ritorna `parse(base).root` — cioè `C:\` (è VOLUTO: al kernel non si
|
|
237
|
+
* insegna un permesso nuovo, gli si consegna una cartella più larga);
|
|
238
|
+
* 4. `creaFileWorkspace` scrive «alla radice del workspace» ⇒ `C:\<nome>.pdf`;
|
|
239
|
+
* 5. Windows rifiuta. E non è un caso: la radice del volume di sistema è protetta da ACL che
|
|
240
|
+
* lasciano al gruppo Users creare CARTELLE ma non FILE (WinTips.org, «FIX: Write Access
|
|
241
|
+
* Denied on Drive C:\», e Microsoft Learn «Access Control: Understanding Windows File And
|
|
242
|
+
* Registry Permissions», letti l'11/09/2026) ⇒ nessun nome diverso può mai riuscire.
|
|
243
|
+
*
|
|
244
|
+
* ⛔ La cura NON è restringere «Full access» (allargare è la funzione, e il modello deve poter
|
|
245
|
+
* leggere tutto il disco). È separare le due domande, che finora erano una sola:
|
|
246
|
+
* **da dove si legge** (il workspace, largo quanto il permesso dice) e **dove si DEPOSITA un
|
|
247
|
+
* file appena generato** (la cartella da cui la sessione è partita, che l'owner ha scelto e
|
|
248
|
+
* che ha i suoi permessi). `cartellaCreazioni` è la seconda. Assente ⇒ è la prima, cioè
|
|
249
|
+
* esattamente il comportamento di prima per ogni chiamante che non la passa (tutti i test).
|
|
250
|
+
*
|
|
251
|
+
* ⭐ È anche la regola del minimo privilegio applicata al verso giusto: «un agente che deve solo
|
|
252
|
+
* leggere una cartella non ha bisogno di scrivere nella radice del filesystem» (Firecrawl,
|
|
253
|
+
* «AI Agent Sandbox: How to Safely Run Autonomous Agents in 2026», 11/09/2026).
|
|
254
|
+
*/
|
|
255
|
+
cartellaCreazioni = null,
|
|
256
|
+
onEvento, segnaleStop, messaggiIniziali, reasoning, contextHooks, mobile = false,
|
|
257
|
+
checkpointSessionId = null, checkpointOperation = 'start',
|
|
258
|
+
fallbackProviders = [], onCambioFornitore: depositaCambioFornitore,
|
|
259
|
+
/*
|
|
260
|
+
* ⛔⛔⛔ 02/09 — LEDGER-STREAMING-SCROLL-TERMINALE-2026-09-02.md, §6/§7.
|
|
261
|
+
* L'etichetta del permesso della sessione ("Read only"/"Workspace
|
|
262
|
+
* write"/"On request"/"Full access") — SOLO per dichiararla in
|
|
263
|
+
* RunStarted.contesto, accanto a `modello` e `reasoning`. Nessuna
|
|
264
|
+
* logica: la regola vive in `livelloAccesso`/`chiediApprovazioneFn`
|
|
265
|
+
* (kernel). Perché: la sessione dell'owner è stata messa in "Read only"
|
|
266
|
+
* da un altro client fra un giro e l'altro, l'attrezzo scrivi ha
|
|
267
|
+
* rifiutato per "sola lettura" e la UI diceva ancora "Full access" —
|
|
268
|
+
* nessun evento portava il permesso VERO del giro. Ora ogni giro lo
|
|
269
|
+
* dichiara, e la cronologia lo mostra sotto la bolla utente.
|
|
270
|
+
*/
|
|
271
|
+
permessi = null,
|
|
272
|
+
strumentiEstesi, ricercaWeb,
|
|
273
|
+
// ⭐ 04/9, R-03 — trasporto iniettato per la ricerca SENZA chiave (DuckDuckGo, `duckduckgo-search.mjs`): inoltrato al kernel com'è (parametro `richiediRicercaFn` di talosLavora), undefined per le fonti con chiave.
|
|
274
|
+
richiediRicercaFn,
|
|
275
|
+
/*
|
|
276
|
+
* ⭐⭐⭐ 29/8 — FASE K, R2 planner costoso + editor economico. Stesso
|
|
277
|
+
* principio di `ricercaWeb`/`reasoning`: inoltrato SENZA logica
|
|
278
|
+
* propria — il pre-loop, il gate `livelloAccesso:'lettura'`, il
|
|
279
|
+
* filtro read-safe degli attrezzi vivono TUTTI dentro
|
|
280
|
+
* `talosHarness.mjs` (questo file non li duplica). `undefined` per
|
|
281
|
+
* default (owner, 29/8: "Configurabile, nessun default forzato") —
|
|
282
|
+
* PARITÀ, il loop resta quello di sempre, un modello solo.
|
|
283
|
+
*/
|
|
284
|
+
modelloPlanner,
|
|
285
|
+
/*
|
|
286
|
+
* ⭐⭐⭐ 29/8 — FASE H, `generate_image`. A differenza di `ricercaWeb`:
|
|
287
|
+
* SEMPRE definito (config.mjs, `parseImmagine` — un default onesto e
|
|
288
|
+
* reale, mai `undefined`), perché `generate_image` non ha una
|
|
289
|
+
* credenziale propria da mancare — riusa `chiave`, la stessa già
|
|
290
|
+
* richiesta per far girare il modello di chat (senza quella la
|
|
291
|
+
* sessione non parte affatto, vedi session-registry.avvia()). Questo
|
|
292
|
+
* È il one-up dichiarato: zero secondo sistema di
|
|
293
|
+
* configurazione provider.
|
|
294
|
+
*/
|
|
295
|
+
immagine,
|
|
296
|
+
generaImmagineFn = generaImmagineOpenRouterReale,
|
|
297
|
+
persistGeneratedImageFn,
|
|
298
|
+
removeGeneratedImageFn,
|
|
299
|
+
/*
|
|
300
|
+
* ⭐⭐⭐ 29/8 — FASE D, firma Ed25519 delle ricevute. Stesso principio di
|
|
301
|
+
* `ricercaWeb` appena sopra: inoltrato SENZA logica propria a
|
|
302
|
+
* `talosLavoraFn` — la decisione (dove vive la chiave) vive tutta in
|
|
303
|
+
* `config.mjs`/`harness-receipt-keypair.mjs`.
|
|
304
|
+
*/
|
|
305
|
+
firma,
|
|
306
|
+
/*
|
|
307
|
+
* ⭐⭐⭐ 28/8 — pillola permessi: entrambi opzionali, inoltrati SENZA
|
|
308
|
+
* logica propria a talosLavoraFn (la decisione COSA rifiutare vive
|
|
309
|
+
* tutta nel kernel, `verificaPermessoScrittura` — vedi la sua doc).
|
|
310
|
+
* Questo file resta un adattatore, non un secondo posto dove la
|
|
311
|
+
* regola potrebbe divergere in silenzio.
|
|
312
|
+
*/
|
|
313
|
+
livelloAccesso, chiediApprovazioneFn,
|
|
314
|
+
/*
|
|
315
|
+
* ⭐⭐⭐ FASE B (28/8) — stesso principio di `livelloAccesso`/
|
|
316
|
+
* `chiediApprovazioneFn`: inoltrato senza logica propria, la semantica
|
|
317
|
+
* di ogni valore vive nel kernel (`verificaPermessoScrittura`).
|
|
318
|
+
*/
|
|
319
|
+
permessiPerAttrezzo,
|
|
320
|
+
/*
|
|
321
|
+
* ⭐⭐⭐ 28/8 — FASE A (hook), piano `elegant-spinning-dongarra.md`.
|
|
322
|
+
* Stesso principio di `livelloAccesso`/`chiediApprovazioneFn` appena
|
|
323
|
+
* sopra: inoltrato SENZA logica propria, la decisione (quale hook
|
|
324
|
+
* fidato blocca cosa) vive tutta in `costruisciHookFn`
|
|
325
|
+
* (`session-registry.mjs`) e nel kernel (`talosHarness.mjs`). Prima
|
|
326
|
+
* di questo commit `hookFn` veniva costruito da `session-registry.mjs`
|
|
327
|
+
* ma MAI arrivava qui — silenziosamente ignorato, nessun errore:
|
|
328
|
+
* il gap dichiarato in `LEDGER-FASE-A-HOOKS.md`, chiuso ora.
|
|
329
|
+
*/
|
|
330
|
+
hookFn,
|
|
331
|
+
/*
|
|
332
|
+
* ⭐⭐⭐ FASE C (28/8) — sub-agenti, stesso principio di `hookFn` appena
|
|
333
|
+
* sopra: inoltrato SENZA logica propria, la decisione (limiti di
|
|
334
|
+
* concorrenza/profondità, isolamento) vive tutta in
|
|
335
|
+
* `subagent-orchestrator.mjs` (session-registry.mjs) e nel kernel
|
|
336
|
+
* (`talosHarness.mjs`). Lo stesso gap di `hookFn` prima di FASE A —
|
|
337
|
+
* un parametro costruito dal chiamante ma mai arrivato fin qui — non
|
|
338
|
+
* si ripete: aggiunto nello stesso commit del resto della fase.
|
|
339
|
+
*/
|
|
340
|
+
onDelega,
|
|
341
|
+
/*
|
|
342
|
+
* ⭐⭐⭐ FASE D (28/8) — coda messaggi su una sessione IN CORSO. Stesso
|
|
343
|
+
* principio di `hookFn`/`onDelega`: inoltrato SENZA logica propria, la
|
|
344
|
+
* coda vera (FIFO, `voce.codaMessaggi`) vive in `session-registry.mjs`.
|
|
345
|
+
* Stesso gap da non ripetere: un parametro costruito dal chiamante ma
|
|
346
|
+
* mai arrivato fin qui — aggiunto nello stesso commit del resto della
|
|
347
|
+
* fase, non un secondo giro.
|
|
348
|
+
*/
|
|
349
|
+
codaMessaggiFn,
|
|
350
|
+
/*
|
|
351
|
+
* ⭐⭐⭐ FASE N, ottavo sistema (30/8) — Deep Research, "fetta onesta".
|
|
352
|
+
* Stesso principio ESATTO di `hookFn`/`onDelega`/`codaMessaggiFn`
|
|
353
|
+
* sopra: inoltrati SENZA logica propria — gli 8 thin delegate verso
|
|
354
|
+
* `research-orchestrator.mjs` vivono tutti in `session-registry.mjs`
|
|
355
|
+
* (avviaESegui), questo file li passa soltanto fino a `talosLavoraFn`.
|
|
356
|
+
* Stesso gap da non ripetere: aggiunti nello stesso commit del resto
|
|
357
|
+
* della fase, non un secondo giro.
|
|
358
|
+
*/
|
|
359
|
+
onRicercaLista, onRicercaAvvia, onRicercaLeggi, onRicercaRinomina,
|
|
360
|
+
onRicercaPausa, onRicercaRiprendi, onRicercaAnnulla, onRicercaElimina,
|
|
361
|
+
/*
|
|
362
|
+
* ⭐⭐⭐⭐ L8 (12/09/2026) — il compositore del record del rapporto della ricerca
|
|
363
|
+
* approfondita. Inoltrato SENZA logica propria, come tutto il resto in questo file: lo
|
|
364
|
+
* costruisce `research-orchestrator.componiRapportoRicerca` (che usa `src/research/report.mjs`)
|
|
365
|
+
* e lo usa il kernel dentro `research_deposit`. Assente ⇒ il deposito scrive il testo così
|
|
366
|
+
* com'è, cioè il comportamento di prima di oggi.
|
|
367
|
+
*/
|
|
368
|
+
componiRapportoRicercaFn,
|
|
369
|
+
/*
|
|
370
|
+
* ⭐⭐⭐⭐ L9 (12/09/2026) — la ricerca approfondita: la cache della corsa e la finestra di
|
|
371
|
+
* pagina. Inoltrati SENZA logica propria, come `componiRapportoRicercaFn` qui sopra: chi li
|
|
372
|
+
* costruisce è `research-orchestrator.mjs`, chi li usa è il kernel dentro `web_search` e
|
|
373
|
+
* `naviga`. Assenti (ogni sessione che non è una ricerca, il banco, i test) ⇒ comportamento
|
|
374
|
+
* bit-per-bit di ieri.
|
|
375
|
+
*/
|
|
376
|
+
cacheWeb,
|
|
377
|
+
onPaginaLetta,
|
|
378
|
+
/*
|
|
379
|
+
* ⭐⭐⭐ 29/8 — FASE E, seconda meta'. Stesso principio di
|
|
380
|
+
* `hookFn`/`onDelega`/`codaMessaggiFn` sopra: inoltrato SENZA logica
|
|
381
|
+
* propria — la scoperta/trust/connessione vive tutta in
|
|
382
|
+
* `mcp-session.mjs`/`mcp-registry.mjs`.
|
|
383
|
+
*
|
|
384
|
+
* ⛔⛔⛔ A differenza di quei tre, PERO', qui c'e' un vero lavoro da
|
|
385
|
+
* fare in QUESTO file (non solo un pass-through): connettersi a un
|
|
386
|
+
* server MCP e' I/O asincrono, e i tool MCP devono essere pronti
|
|
387
|
+
* PRIMA della prima chiamata al modello (dentro talosLavoraFn) — ma
|
|
388
|
+
* DOPO RunStarted (riga 199, gia' sincrono prima di questo). Questo
|
|
389
|
+
* e' esattamente il punto giusto: gia' dentro il corpo async di
|
|
390
|
+
* avviaSessione, gia' dopo RunStarted, gia' prima di talosLavoraFn.
|
|
391
|
+
* Non serve toccare session-registry.mjs (avvia()/avviaLibero()
|
|
392
|
+
* restano sincrone) ne' i 110 punti che chiamano quelle due funzioni
|
|
393
|
+
* nei test — investigato PRIMA di scrivere, non assunto (vedi
|
|
394
|
+
* elegant-spinning-dongarra.md, FASE E).
|
|
395
|
+
*
|
|
396
|
+
* `cartellaTrustMcp` ASSENTE (ogni test esistente, e — finche'
|
|
397
|
+
* session-registry.mjs non lo passa — ogni sessione reale di oggi)
|
|
398
|
+
* ⇒ ZERO lavoro nuovo: nessuna I/O, toolMcp/chiamaToolMcpFn restano
|
|
399
|
+
* `undefined`, comportamento bit-per-bit quello di oggi. Stessa
|
|
400
|
+
* garanzia gia' data per ogni altro parametro di questa lista.
|
|
401
|
+
*/
|
|
402
|
+
cartellaTrustMcp,
|
|
403
|
+
preparaToolMcpPerSessioneFn = preparaToolMcpPerSessioneReale,
|
|
404
|
+
/*
|
|
405
|
+
* ⭐⭐⭐ 29/8 — FASE F (Skills). Piu' semplice di toolMcp sopra: una
|
|
406
|
+
* skill e' testo, nessuna connessione da preparare, nessun trust da
|
|
407
|
+
* verificare (mai un hash-gate qui — vedi la doc in
|
|
408
|
+
* skill-registry.mjs sul perche'). Chiamata SEMPRE (non dietro un
|
|
409
|
+
* flag come cartellaTrustMcp): caricaSkill({cartella}) e' gia'
|
|
410
|
+
* economica sul percorso comune ("nessuna .harness-ui-skills/") —
|
|
411
|
+
* un solo tentativo di lettura cartella che fallisce con ENOENT,
|
|
412
|
+
* stesso costo di leggiContestoWorkspaceFn poco sotto. Iniettabile
|
|
413
|
+
* per i test (mai una vera lettura disco nella suite unitaria).
|
|
414
|
+
*/
|
|
415
|
+
caricaSkillDisponibiliFn = caricaSkillReale,
|
|
416
|
+
/*
|
|
417
|
+
* ⭐⭐⭐ 29/8 — FASE G (Plugin system). Stesso gate esplicito di
|
|
418
|
+
* `cartellaTrustMcp` sopra (a differenza di `caricaSkillDisponibiliFn`,
|
|
419
|
+
* sempre chiamata): un tool di plugin ESEGUE un comando locale, e un
|
|
420
|
+
* hook di plugin intercetta il ciclo dell'agente — entrambi la
|
|
421
|
+
* stessa categoria di rischio di un server MCP o di un hook
|
|
422
|
+
* standalone, mai testo inerte come una skill. Assente (ogni test
|
|
423
|
+
* esistente, e — finché session-registry.mjs non lo passa — ogni
|
|
424
|
+
* sessione reale di oggi) ⇒ ZERO lavoro nuovo, stessa garanzia già
|
|
425
|
+
* data per `cartellaTrustMcp`.
|
|
426
|
+
*
|
|
427
|
+
* ⛔ A differenza di MCP: nessun `chiudiPlugin()` nel `finally` sotto
|
|
428
|
+
* — un tool di plugin non apre una connessione persistente, spawna
|
|
429
|
+
* ON DEMAND ad ogni chiamata (vedi `plugin-session.mjs`), zero
|
|
430
|
+
* risorsa da rilasciare a fine run.
|
|
431
|
+
*/
|
|
432
|
+
cartellaTrustPlugin,
|
|
433
|
+
preparaToolPluginPerSessioneFn = preparaToolPluginPerSessioneReale,
|
|
434
|
+
eseguiHookFn = eseguiHookReale,
|
|
435
|
+
talosLavoraFn = talosLavoraReale,
|
|
436
|
+
leggiContestoWorkspaceFn = leggiContestoWorkspaceReale,
|
|
437
|
+
// ⭐ P-13 (10/09) — iniettabili per le prove, come ogni altro *Fn qui: nessun test deve
|
|
438
|
+
// camminare un albero vero per provare l'ordine degli eventi.
|
|
439
|
+
contestoDelProgettoFn = contestoDelProgettoReale,
|
|
440
|
+
aggiornamentoInCodaFn = aggiornamentoInCodaReale,
|
|
441
|
+
creaFiltroGitignoreFn = creaFiltroGitignoreReale,
|
|
442
|
+
salvaArtefattoFn = salvaArtefattoReale,
|
|
443
|
+
/*
|
|
444
|
+
* ⛔⛔⛔ 06/9, owner: «con i modelli a chiave API gli artefatti vengono creati, ma non salvati
|
|
445
|
+
* nella libreria». Vero, e peggio di così: non erano salvati DA NESSUNA PARTE. `artifact-store.mjs`
|
|
446
|
+
* li tiene in una Map in memoria — dichiarato nella sua doc, «non sopravvive a un riavvio del
|
|
447
|
+
* server» — quindi un artefatto spariva al primo riavvio e non compariva mai in Libreria.
|
|
448
|
+
* Un artefatto è lavoro prodotto per la persona, non un'anteprima: si salva dove lo ritrova.
|
|
449
|
+
* La Map resta com'è (serve a servire la rotta senza toccare il disco a ogni apertura); qui si
|
|
450
|
+
* aggiunge la copia durevole, nella Libreria del progetto.
|
|
451
|
+
*/
|
|
452
|
+
salvaVoceLibreriaFn = salvaVoceLibreriaReale,
|
|
453
|
+
generateTalosDocumentFn = generateTalosDocumentReale,
|
|
454
|
+
verifyTalosDocumentFn = verifyTalosDocumentReale,
|
|
455
|
+
creaFileWorkspaceFn = creaFileWorkspaceReale,
|
|
456
|
+
/*
|
|
457
|
+
* ⭐⭐⭐ 29/8 — FASE N (Libreria), prima fetta. Stesso principio di
|
|
458
|
+
* `caricaSkillDisponibiliFn`: SOLO i quattro punti di contatto I/O
|
|
459
|
+
* con `library-store.mjs` sono iniettabili (per i test — mai un vero
|
|
460
|
+
* filesystem mockato altrove, il modulo stesso è già testato per
|
|
461
|
+
* conto suo). `impaginaVoci`/`cercaVoci`/`creaCursoriLibreria` sono
|
|
462
|
+
* PURE — usate direttamente, mai iniettate: non c'è I/O da fingere.
|
|
463
|
+
* Nessun gate/trust (stesso motivo di `caricaSkillDisponibiliFn`:
|
|
464
|
+
* una voce di Libreria è un file locale come un altro, non un
|
|
465
|
+
* comando o una connessione — vedi la doc in library-store.mjs).
|
|
466
|
+
*/
|
|
467
|
+
elencaVociFn = elencaVociReale,
|
|
468
|
+
elencaVociConTestoFn = elencaVociConTestoReale,
|
|
469
|
+
leggiVoceFn = leggiVoceReale,
|
|
470
|
+
/*
|
|
471
|
+
* ⛔ 14/09 (F03 della review) — la porta BINARIA della Libreria, sorella di `leggiVoceFn`. Non è un doppione:
|
|
472
|
+
* `leggiVoce` legge in utf8 tutto ciò che non è un'immagine ed è giusta così (serve al MODELLO, che vuole testo),
|
|
473
|
+
* mentre qui i byte devono restare byte. Vedi la doc di `leggiBytesVoce` in `library-store.mjs`.
|
|
474
|
+
*/
|
|
475
|
+
leggiBytesVoceFn = leggiBytesVoceReale,
|
|
476
|
+
origineVoceFn = origineVoceReale,
|
|
477
|
+
/*
|
|
478
|
+
* ⭐⭐⭐ 29/8 — FASE N, seconda fetta (mutazioni). Stesso principio
|
|
479
|
+
* appena sopra: solo i punti di contatto I/O sono iniettabili
|
|
480
|
+
* (`rinominaVoceFn`/`eliminaVoceFn`); `trovaVoceLibreria` è PURA,
|
|
481
|
+
* usata direttamente. `library_export` non ha un I/O tutto suo —
|
|
482
|
+
* compone `leggiBytesVoceFn` (già iniettabile sopra) con
|
|
483
|
+
* `creaFileWorkspaceFn` (già iniettabile per onDocumento/onImmagine,
|
|
484
|
+
* riuso diretto, nessuna funzione nuova per "scrivi un file nel
|
|
485
|
+
* workspace").
|
|
486
|
+
*/
|
|
487
|
+
rinominaVoceFn = rinominaVoceReale,
|
|
488
|
+
eliminaVoceFn = eliminaVoceReale,
|
|
489
|
+
/*
|
|
490
|
+
* ⭐ (16/09/2026) — la porta del LOTTO per library_delete (richiesta owner): sorella di
|
|
491
|
+
* `eliminaVoceFn`, stessa forma contrattuale, iniettabile per gli stessi motivi.
|
|
492
|
+
*/
|
|
493
|
+
eliminaVociFn = eliminaVociReale,
|
|
494
|
+
/*
|
|
495
|
+
* ⭐⭐⭐ 29/8 — FASE N, terza fetta (library_context_policy_update).
|
|
496
|
+
* Stesso principio dei punti I/O sopra: solo leggiPoliticaFn/
|
|
497
|
+
* scriviPoliticaFn iniettabili — creaRicevutaPolitica/
|
|
498
|
+
* ricordaRicevutaPolitica sono PURE (nessuna I/O da fingere).
|
|
499
|
+
*/
|
|
500
|
+
leggiPoliticaFn = leggiPoliticaReale,
|
|
501
|
+
scriviPoliticaFn = scriviPoliticaReale,
|
|
502
|
+
/*
|
|
503
|
+
* ⭐⭐⭐ FASE N, quarto sistema (30/8) — Notes. `cartellaNote`: GLOBALE,
|
|
504
|
+
* non `cartella` (il workspace di QUESTA sessione) — una nota non è
|
|
505
|
+
* un artefatto di un progetto, vedi la doc in notes-store.mjs. Stesso
|
|
506
|
+
* pattern di threading di `cartellaTrustHook`/`cartellaTrustMcp`/
|
|
507
|
+
* `cartellaTrustPlugin`: nessun default QUI, il default reale vive in
|
|
508
|
+
* session-registry.mjs. Nessun gate di fiducia (come le skill/Libreria
|
|
509
|
+
* di lettura): una nota è un file locale dell'owner, non un comando o
|
|
510
|
+
* una connessione.
|
|
511
|
+
*/
|
|
512
|
+
cartellaNote,
|
|
513
|
+
elencaNoteFn = elencaNoteReale,
|
|
514
|
+
creaNotaFn = creaNotaReale,
|
|
515
|
+
aggiornaNotaFn = aggiornaNotaReale,
|
|
516
|
+
eliminaNotaFn = eliminaNotaReale,
|
|
517
|
+
leggiNotaFn = leggiNotaReale,
|
|
518
|
+
/*
|
|
519
|
+
* ⭐⭐⭐ FASE N, quinto sistema (30/8) — Tasks. Stesso pattern ESATTO di
|
|
520
|
+
* cartellaNote/elencaNoteFn appena sopra: GLOBALE, nessun default
|
|
521
|
+
* QUI (il default reale vive in session-registry.mjs).
|
|
522
|
+
*/
|
|
523
|
+
cartellaAttivita,
|
|
524
|
+
elencaAttivitaFn = elencaAttivitaReale,
|
|
525
|
+
creaAttivitaFn = creaAttivitaReale,
|
|
526
|
+
completaAttivitaFn = completaAttivitaReale,
|
|
527
|
+
aggiornaAttivitaFn = aggiornaAttivitaReale,
|
|
528
|
+
eliminaAttivitaFn = eliminaAttivitaReale,
|
|
529
|
+
leggiAttivitaFn = leggiAttivitaReale,
|
|
530
|
+
/*
|
|
531
|
+
* ⭐⭐⭐ FASE N, sesto sistema (30/8) — Memory. Stesso pattern ESATTO
|
|
532
|
+
* di cartellaAttivita/elencaAttivitaFn appena sopra: GLOBALE, nessun
|
|
533
|
+
* default QUI. `cercaMemorieFn` è PURA (mai iniettata: non c'è I/O
|
|
534
|
+
* da fingere), stesso trattamento di `impaginaVociLibreria`.
|
|
535
|
+
*/
|
|
536
|
+
cartellaMemoria,
|
|
537
|
+
elencaMemorieFn = elencaMemorieReale,
|
|
538
|
+
creaMemoriaFn = creaMemoriaReale,
|
|
539
|
+
aggiornaMemoriaFn = aggiornaMemoriaReale,
|
|
540
|
+
eliminaMemoriaFn = eliminaMemoriaReale,
|
|
541
|
+
leggiMemoriaFn = leggiMemoriaReale,
|
|
542
|
+
/*
|
|
543
|
+
* ⭐⭐⭐⭐ FASE N, nono e ultimo sistema (30/8) — Tool Forge. Stesso
|
|
544
|
+
* pattern ESATTO di cartellaMemoria/elencaMemorieFn appena sopra:
|
|
545
|
+
* GLOBALE (nessun default QUI, il default reale vive in
|
|
546
|
+
* session-registry.mjs — vedi la doc in tool-forge-store.mjs sul
|
|
547
|
+
* perché GLOBALE come Notes/Tasks/Memory).
|
|
548
|
+
*/
|
|
549
|
+
cartellaForge,
|
|
550
|
+
elencaToolForgiatiFn = elencaToolForgiatiReale,
|
|
551
|
+
installaToolForgiatoFn = installaToolForgiatoReale,
|
|
552
|
+
}) {
|
|
553
|
+
const threadId = randomUUID();
|
|
554
|
+
const runId = randomUUID();
|
|
555
|
+
|
|
556
|
+
/*
|
|
557
|
+
* ⭐ Il pannello "Ambiente" del Context Rail — prima statico/demo (§1.3 del
|
|
558
|
+
* piano). `branch` è onestamente `null` per ogni task di progetti/ (nessuno
|
|
559
|
+
* è un repository git, vedi workspace-context.mjs) — non un difetto qui.
|
|
560
|
+
* ⛔ Letto PRIMA di RunStarted apposta: un fallimento di `git` (comando
|
|
561
|
+
* assente, permessi) non deve mai impedire l'avvio della sessione — per
|
|
562
|
+
* questo leggiContestoWorkspace non lancia mai, anche se questo file non
|
|
563
|
+
* lo intercetta con un try/catch: la garanzia vive nella funzione stessa.
|
|
564
|
+
*/
|
|
565
|
+
/*
|
|
566
|
+
* ⭐ La cartella dove si DEPOSITA un file generato — vedi la doc di `cartellaCreazioni` in testa
|
|
567
|
+
* a questa funzione. Senza il parametro è `cartella`: il comportamento di prima, invariato.
|
|
568
|
+
*/
|
|
569
|
+
const cartellaPerCreare = cartellaCreazioni || cartella;
|
|
570
|
+
/*
|
|
571
|
+
* ⛔ Il pannello File e la scheda Review mostrano un percorso RELATIVO AL WORKSPACE: se il file
|
|
572
|
+
* è stato depositato altrove (cioè `cartellaPerCreare !== cartella`), il solo nome sarebbe una
|
|
573
|
+
* bugia — punterebbe alla radice del workspace, dove il file non c'è. Si ricalcola davvero.
|
|
574
|
+
* Quando il deposito è FUORI dal workspace `relative` produce un percorso con `..`: la si
|
|
575
|
+
* lascia passare com'è (è onesta) invece di inventare un percorso interno che non esiste.
|
|
576
|
+
*/
|
|
577
|
+
const percorsoNellAlbero = (nome) => (
|
|
578
|
+
cartellaPerCreare === cartella
|
|
579
|
+
? nome
|
|
580
|
+
: percorsoRelativo(cartella, joinPercorso(cartellaPerCreare, nome)).split(separatorePercorso).join('/')
|
|
581
|
+
);
|
|
582
|
+
const contestoWorkspace = leggiContestoWorkspaceFn({ cartella, progetto: task?.progetto ?? null });
|
|
583
|
+
const contesto = {
|
|
584
|
+
...contestoWorkspace,
|
|
585
|
+
modello,
|
|
586
|
+
reasoning: reasoning ?? null,
|
|
587
|
+
permessi: permessi ?? null,
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
onEvento(runStarted({ threadId, runId, input: task, contesto }));
|
|
591
|
+
|
|
592
|
+
/*
|
|
593
|
+
* ⛔⛔⛔ P-13 (10/09) — L'ELENCO DEI FILE, e perché sta ESATTAMENTE qui.
|
|
594
|
+
*
|
|
595
|
+
* IL DIFETTO, misurato il 22/08: l'attrezzo `elenca` arriva a profondità 2; i 106 percorsi dei
|
|
596
|
+
* task del corpus `storia` stanno a profondità 4-6, ZERO a profondità ≤2; e 35 consegne su 35
|
|
597
|
+
* non nominano nessun file. Il modello non può risolverli — non per bravura: non li VEDE.
|
|
598
|
+
*
|
|
599
|
+
* ⛔ IL PUNTO NON È UNA PREFERENZA, È UNA MISURA. Il primo tentativo agganciava questo in
|
|
600
|
+
* `session-registry.mjs`, dentro `avviaESegui`: un solo tick di ritardo lì fa cadere 148
|
|
601
|
+
* test su 2084, perché `avviaSessione` emette RunStarted come sua PRIMA riga e chi chiama
|
|
602
|
+
* conta su quell'evento già nel buffer al ritorno (la riga 2245 di quel file lo dice a
|
|
603
|
+
* chiare lettere). Qui invece siamo DOPO RunStarted e PRIMA di talosLavoraFn — lo stesso
|
|
604
|
+
* posto, e per la stessa ragione, dove già vivono MCP (sotto), Skills e Plugin.
|
|
605
|
+
* ⛔ E SE FALLISCE NON SI FERMA NIENTE: stesso principio delle Skills poche righe sotto —
|
|
606
|
+
* una sessione che non parte perché non si sono potuti elencare i file sarebbe una cura
|
|
607
|
+
* molto peggiore della malattia.
|
|
608
|
+
* ⭐ Al kernel arriva una STRINGA già pronta: `contesto-del-progetto.mjs` tiene la cache per
|
|
609
|
+
* cartella (39 ms la prima volta su questo repo, 2 ms dopo — misurato), e la stabilità di
|
|
610
|
+
* quella stringa è ciò che fa prendere la cache del fornitore: costa un sesto e prende
|
|
611
|
+
* dalla terza chiamata (22/08, 16.768 token su 16.811 letti dalla cache).
|
|
612
|
+
*/
|
|
613
|
+
let testoContestoProgetto;
|
|
614
|
+
try {
|
|
615
|
+
/*
|
|
616
|
+
* ⛔ I DUE CONTRATTI, e non è pignoleria: `contestoDelProgetto` chiama `creaFiltro(radice)`
|
|
617
|
+
* con una STRINGA; `creaFiltroGitignore` vuole `{radice}`. Passata la stringa nuda, Node
|
|
618
|
+
* lancia ERR_INVALID_ARG_TYPE, il catch là dentro lo scambia per un .gitignore illeggibile
|
|
619
|
+
* e l'elenco esce SENZA FILTRO. Trovato dal vivo il 10/09 sulla cartella vera: 1500
|
|
620
|
+
* percorsi troncati e 25.163 token invece di ~6.500, con dentro i file di log. Non un
|
|
621
|
+
* errore visibile: un elenco che sembra funzionare e costa quattro volte tanto.
|
|
622
|
+
*/
|
|
623
|
+
const preambolo = await contestoDelProgettoFn({
|
|
624
|
+
cartella,
|
|
625
|
+
creaFiltro: (radice) => creaFiltroGitignoreFn({ radice }),
|
|
626
|
+
/*
|
|
627
|
+
* ⭐ BC-07 — I TRE VALORI CHE ENTRANO NEL PREAMBOLO e che possono cambiare senza che il
|
|
628
|
+
* disco cambi: cartella, permesso del giro, modello. Sono anche la chiave della cache in
|
|
629
|
+
* `contesto-del-progetto.mjs`, e per questo il prefisso resta byte-identico fra due
|
|
630
|
+
* messaggi consecutivi e cambia SOLO quando uno dei tre cambia davvero.
|
|
631
|
+
* ⛔ `permessi` e' l'ETICHETTA gia' calcolata per RunStarted.contesto (02/09): la stessa
|
|
632
|
+
* che la persona vede nella pillola. Due nomi diversi per lo stesso permesso — uno a
|
|
633
|
+
* schermo e uno nel prompt — sarebbero due verita' da tenere allineate a mano.
|
|
634
|
+
*/
|
|
635
|
+
permesso: permessi ?? null,
|
|
636
|
+
modello: modello ?? null,
|
|
637
|
+
piattaforma: process.platform,
|
|
638
|
+
});
|
|
639
|
+
testoContestoProgetto = preambolo?.testo;
|
|
640
|
+
|
|
641
|
+
/*
|
|
642
|
+
* ⛔⛔⛔ IL CONTESTO SI APPENDE, NON SI RISCRIVE — e qui sta la differenza fra le due cose.
|
|
643
|
+
*
|
|
644
|
+
* Su una sessione FRESCA il kernel mette il preambolo in testa (subito dopo le istruzioni) e
|
|
645
|
+
* non lo tocca piu'. Su un SEGUITO (`messaggiIniziali` pieno) il kernel lo IGNORA del tutto:
|
|
646
|
+
* il prefisso e' gia' dentro la conversazione salvata, e riscriverlo la' significherebbe
|
|
647
|
+
* invalidare tutto cio' che sta dopo — la lookup della cache lavora sul prefisso INTERO fino
|
|
648
|
+
* al breakpoint, non sul singolo blocco (Claude Platform Docs, «Prompt caching», letto
|
|
649
|
+
* 11/09/2026: «Cache hits require 100% identical prompt segments»).
|
|
650
|
+
* ⇒ Se nel frattempo e' cambiato qualcosa (un `WorkspaceChanged` ha invalidato la mappa, un
|
|
651
|
+
* `AGENTS.md` e' stato salvato, il permesso del giro e' cambiato), il preambolo nuovo si
|
|
652
|
+
* APPENDE IN CODA dichiarando che sostituisce. Se non e' cambiato niente, non si appende
|
|
653
|
+
* niente: zero token.
|
|
654
|
+
* ⛔ Il confronto non tiene nessuno stato in memoria: legge dalla conversazione stessa che
|
|
655
|
+
* cosa il modello ha davvero davanti (`preamboloVistoDa`). Una mappa in memoria mentirebbe
|
|
656
|
+
* dopo un riavvio del server, e la conversazione salvata no.
|
|
657
|
+
*/
|
|
658
|
+
if (Array.isArray(messaggiIniziali) && messaggiIniziali.length > 0 && testoContestoProgetto) {
|
|
659
|
+
const coda = aggiornamentoInCodaFn({ storia: messaggiIniziali, testo: testoContestoProgetto });
|
|
660
|
+
if (coda) messaggiIniziali = [...messaggiIniziali, { role: 'system', content: coda }];
|
|
661
|
+
}
|
|
662
|
+
} catch {
|
|
663
|
+
// ⭐ nessun preambolo: si parte esattamente come prima di BC-07, zero differenza per la sessione.
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/*
|
|
667
|
+
* ⭐⭐⭐ 29/8 — FASE E: DOPO RunStarted (sincrono, sopra), PRIMA di
|
|
668
|
+
* talosLavoraFn (sotto) — vedi la doc sul parametro `cartellaTrustMcp`.
|
|
669
|
+
*/
|
|
670
|
+
let toolMcp;
|
|
671
|
+
let chiamaToolMcpFn;
|
|
672
|
+
let chiudiMcp = async () => {};
|
|
673
|
+
if (cartellaTrustMcp) {
|
|
674
|
+
const preparato = await preparaToolMcpPerSessioneFn({ cartella, cartellaTrust: cartellaTrustMcp });
|
|
675
|
+
toolMcp = preparato.toolMcp;
|
|
676
|
+
chiamaToolMcpFn = preparato.chiamaToolMcpFn;
|
|
677
|
+
chiudiMcp = preparato.chiudiTutti;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/*
|
|
681
|
+
* ⭐⭐⭐ 29/8 — FASE F (Skills). Nessun gate (a differenza di MCP
|
|
682
|
+
* sopra): caricaSkill() e' gia' economica sul percorso comune, vedi
|
|
683
|
+
* la doc su caricaSkillDisponibiliFn. Un .harness-ui-skills/
|
|
684
|
+
* malformato non deve MAI impedire alla sessione di partire —
|
|
685
|
+
* degrada a "nessuna skill", stesso principio di costruisciHookFn
|
|
686
|
+
* in session-registry.mjs (try/catch a vuoto, mai un blocco).
|
|
687
|
+
*/
|
|
688
|
+
let skillsDisponibili;
|
|
689
|
+
let caricaSkillFn;
|
|
690
|
+
try {
|
|
691
|
+
const { skills } = await caricaSkillDisponibiliFn({ cartella });
|
|
692
|
+
if (skills.length > 0) {
|
|
693
|
+
skillsDisponibili = skills.map((s) => ({ name: s.name, description: s.description }));
|
|
694
|
+
caricaSkillFn = async (nome) => {
|
|
695
|
+
const skill = skills.find((s) => s.name === nome);
|
|
696
|
+
if (!skill) throw new Error(`skill "${nome}" scomparsa fra l'offerta al modello e la chiamata`);
|
|
697
|
+
return skill.corpo;
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
} catch {
|
|
701
|
+
// ⭐ un .harness-ui-skills/ malformato non deve mai bloccare l'avvio: skillsDisponibili resta undefined, zero tool nuovo offerto.
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/*
|
|
705
|
+
* ⭐⭐⭐ 29/8 — FASE G (Plugin system). Stesso gate esplicito di MCP
|
|
706
|
+
* sopra (`cartellaTrustPlugin`), stesso punto (dopo RunStarted,
|
|
707
|
+
* prima di talosLavoraFn) — vedi la doc sul parametro.
|
|
708
|
+
* `preparaToolPluginPerSessioneFn` già filtra ai soli plugin FIDATI
|
|
709
|
+
* (hash sull'intero manifesto, `plugin-session.mjs`): un plugin mai
|
|
710
|
+
* fidato non contribuisce né un tool né un hook, qui come là.
|
|
711
|
+
*/
|
|
712
|
+
let toolPlugin;
|
|
713
|
+
let eseguiToolPluginFn;
|
|
714
|
+
let hookPlugin = [];
|
|
715
|
+
if (cartellaTrustPlugin) {
|
|
716
|
+
const preparato = await preparaToolPluginPerSessioneFn({ cartella, cartellaTrust: cartellaTrustPlugin });
|
|
717
|
+
toolPlugin = preparato.toolPlugin;
|
|
718
|
+
eseguiToolPluginFn = preparato.eseguiToolPluginFn;
|
|
719
|
+
hookPlugin = preparato.hookPlugin;
|
|
720
|
+
|
|
721
|
+
/*
|
|
722
|
+
* ⛔⛔⛔ A2 (17/09/2026) — LA FIDUCIA SI RIVERIFICA AL MOMENTO DELL'USO, non solo all'avvio.
|
|
723
|
+
*
|
|
724
|
+
* Il cancello sopra gira UNA volta, quando la sessione parte. Fra quel momento e la chiamata
|
|
725
|
+
* di un attrezzo passa tutto il tempo che il modello impiega a ragionare, e in quella
|
|
726
|
+
* finestra il pacchetto può cambiare sotto: il revisore l'ha misurato riscrivendo `run.js`
|
|
727
|
+
* dopo la verifica — e il codice scambiato girava.
|
|
728
|
+
* ⛔ È la forma classica del TOCTOU: «verifico lo stato → [buco] → uso lo stato»
|
|
729
|
+
* (en.wikipedia.org/wiki/Time-of-check_to_time-of-use, e la stessa raccomandazione nelle
|
|
730
|
+
* fonti sul contenimento dei punti d'ingresso dei plugin — OpenHands/software-agent-sdk
|
|
731
|
+
* #5101, WindWang2/exp-rs #756: il controllo va fatto alla validazione **e** all'uso.
|
|
732
|
+
* Letti il 17/09/2026.)
|
|
733
|
+
*
|
|
734
|
+
* ⛔ IL COSTO, rimisurato il 17/09/2026 e CORRETTO: la prima stesura di questo commento
|
|
735
|
+
* riportava 0,8 / 2,3 / 16,2 ms misurando la sola impronta, non ciò che gira davvero qui.
|
|
736
|
+
* Il numero onesto è quello di TUTTO il wrapper (impronta + lettura dell'archivio di
|
|
737
|
+
* fiducia), mediana su 20 giri a cache calda, insieme al termine di paragone che decide:
|
|
738
|
+
* · pacchetto minuscolo (3 file, 2 KB) wrapper **1,3 ms** · UN attrezzo **71 ms**
|
|
739
|
+
* · normale (40 file, 200 KB) wrapper **2,9 ms** · UN attrezzo **56 ms**
|
|
740
|
+
* · grosso (400 file, 4 MB) wrapper **19,3 ms** · UN attrezzo **61 ms**
|
|
741
|
+
* ⛔ Il revisore, sulla stessa macchina ma sotto carico, ha misurato 1,5 / 6,5 / 48,2 ms
|
|
742
|
+
* (massimo 62): due-tre volte i miei. Non lo si sceglie il numero comodo — si dice che
|
|
743
|
+
* BALLANO con il carico, e che nel suo caso peggiore il wrapper resta comunque sotto il
|
|
744
|
+
* costo di un solo avvio di processo.
|
|
745
|
+
* ⇒ La conclusione regge con i numeri di tutti e due: NESSUNA cache per mtime+size. Sarebbe
|
|
746
|
+
* una seconda verità da tenere allineata per un guadagno che sta sotto il rumore di ciò che
|
|
747
|
+
* la riverifica protegge. Se un giorno un pacchetto la rendesse visibile, la cache va
|
|
748
|
+
* aggiunta QUI e dichiarata, non dedotta.
|
|
749
|
+
*
|
|
750
|
+
* ⛔ Copre gli ATTREZZI, non gli HOOK: quelli li arma `talosLavora` e passano da
|
|
751
|
+
* `costruisciHookFn`, fuori da questa funzione. Per loro la verifica resta quella d'avvio.
|
|
752
|
+
* È un buco più stretto ma è un buco, ed è scritto qui invece che taciuto.
|
|
753
|
+
*/
|
|
754
|
+
if (eseguiToolPluginFn) {
|
|
755
|
+
const eseguiSenzaRiverifica = eseguiToolPluginFn;
|
|
756
|
+
eseguiToolPluginFn = async (nomeEsposto, argomenti) => {
|
|
757
|
+
/*
|
|
758
|
+
* ⛔⛔⛔ A-3, SECONDA STESURA (17/09/2026): l'id si CHIEDE a chi ha armato la sessione, non
|
|
759
|
+
* si ricava tagliando il nome esposto.
|
|
760
|
+
* La prima stesura lo estraeva con una espressione, e il terzo controllo ha misurato che
|
|
761
|
+
* quella espressione e il cancello del caricamento NON dicevano la stessa cosa: un id
|
|
762
|
+
* `_sano` si caricava e poi non veniva mai riconosciuto qui — l'attrezzo c'era e non
|
|
763
|
+
* funzionava, per sempre, senza che nessuno lo dicesse.
|
|
764
|
+
* ⇒ Una verità sola: `pluginIdDiTool` viene dalla mappa dell'instradamento, dove l'id è
|
|
765
|
+
* quello VERO. Se l'attrezzo non è in quella mappa non è di questa sessione, e non si
|
|
766
|
+
* esegue.
|
|
767
|
+
*/
|
|
768
|
+
const pluginId = preparato.pluginIdDiTool?.(nomeEsposto) ?? null;
|
|
769
|
+
if (!pluginId) return '⛔ Non riconosco a quale plugin appartiene questo strumento, quindi non lo eseguo.';
|
|
770
|
+
let stato;
|
|
771
|
+
try {
|
|
772
|
+
const hash = await improntaPacchettoPlugin({ cartella, pluginId });
|
|
773
|
+
stato = await statoTrustPlugin({ cartellaTrust: cartellaTrustPlugin, pluginId, hash });
|
|
774
|
+
} catch (errore) {
|
|
775
|
+
/* Un pacchetto che non si legge più non autorizza: si dice, non si esegue. */
|
|
776
|
+
return `⛔ Non riesco più a controllare i file di questo plugin, quindi non lo eseguo. (${errore?.message ?? 'lettura fallita'})`;
|
|
777
|
+
}
|
|
778
|
+
/*
|
|
779
|
+
* ⛔ A-6(b): la frase è quella del MOTIVO vero. Prima il ripiego diceva sempre «non è più
|
|
780
|
+
* quello che hai approvato» anche quando il motivo era `mai-approvato`, cioè raccontava
|
|
781
|
+
* una manomissione dove c'era una fiducia mai data.
|
|
782
|
+
*/
|
|
783
|
+
if (!stato.fidato) return `⛔ ${stato.frase ?? FRASI_RIVERIFICA[stato.motivo] ?? FRASI_RIVERIFICA['mai-approvato']}`;
|
|
784
|
+
return eseguiSenzaRiverifica(nomeEsposto, argomenti);
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/*
|
|
790
|
+
* ⭐⭐⭐⭐ FASE N, nono e ultimo sistema (30/8) — Tool Forge. Nessun
|
|
791
|
+
* gate di fiducia (come le Skills sopra — a differenza di MCP/Plugin:
|
|
792
|
+
* un manifest forgiato è già validato ALL'INSTALLAZIONE da
|
|
793
|
+
* validaManifestForge, non da un click di fiducia dell'owner qui).
|
|
794
|
+
* `eseguiCapacitaForge` compone le funzioni Notes/Tasks/Memory GIÀ
|
|
795
|
+
* iniettate sopra — zero nuova primitiva di dominio, solo un nuovo
|
|
796
|
+
* modo di comporle (porto diretto di createLocalCapabilities in
|
|
797
|
+
* talosIntegration.ts mobile). `toolForge`/`eseguiToolForgeFn` sono
|
|
798
|
+
* costruiti SEMPRE (un .tool-forge-store/ malformato degrada a
|
|
799
|
+
* "nessun tool forgiato", mai un blocco dell'avvio — stesso
|
|
800
|
+
* principio di caricaSkillDisponibiliFn sopra).
|
|
801
|
+
*/
|
|
802
|
+
async function eseguiCapacitaForge(capacita, input) {
|
|
803
|
+
switch (capacita) {
|
|
804
|
+
case 'tasks.list':
|
|
805
|
+
return elencaAttivitaFn({ cartella: cartellaAttivita });
|
|
806
|
+
case 'tasks.create':
|
|
807
|
+
return creaAttivitaFn({ cartella: cartellaAttivita, title: String(input?.title ?? ''), description: input?.description ?? null, priority: input?.priority ?? 'normal' });
|
|
808
|
+
case 'tasks.setStatus':
|
|
809
|
+
return completaAttivitaFn({ cartella: cartellaAttivita, id: String(input?.id ?? ''), status: input?.status ?? 'done' });
|
|
810
|
+
case 'notes.list':
|
|
811
|
+
return elencaNoteFn({ cartella: cartellaNote });
|
|
812
|
+
case 'notes.create':
|
|
813
|
+
return creaNotaFn({ cartella: cartellaNote, title: String(input?.title ?? ''), content: String(input?.content ?? '') });
|
|
814
|
+
case 'notes.update':
|
|
815
|
+
// ⛔ porto diretto di talosIntegration.ts mobile: title/content passati SOLO se presenti (spread condizionale), mai `title: undefined` esplicito — aggiornaNotaFn tratterebbe comunque undefined come "non toccare", ma questa è la forma VERA del mobile, non solo equivalente.
|
|
816
|
+
return aggiornaNotaFn({
|
|
817
|
+
cartella: cartellaNote, id: String(input?.id ?? ''),
|
|
818
|
+
...(input?.title !== undefined ? { title: input.title } : {}),
|
|
819
|
+
...(input?.content !== undefined ? { content: input.content } : {}),
|
|
820
|
+
});
|
|
821
|
+
case 'memory.search': {
|
|
822
|
+
const tutte = await elencaMemorieFn({ cartella: cartellaMemoria });
|
|
823
|
+
return cercaMemorie(tutte, { query: String(input?.query ?? ''), limit: input?.limit });
|
|
824
|
+
}
|
|
825
|
+
case 'memory.create': {
|
|
826
|
+
const { voce } = await creaMemoriaFn({ cartella: cartellaMemoria, title: String(input?.title ?? ''), content: String(input?.content ?? ''), kind: input?.kind ?? 'procedure' });
|
|
827
|
+
return voce;
|
|
828
|
+
}
|
|
829
|
+
default:
|
|
830
|
+
throw new Error(`TALOS_FORGE_CAPABILITY_UNAVAILABLE:${capacita}`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
let toolForge;
|
|
835
|
+
let eseguiToolForgeFn;
|
|
836
|
+
try {
|
|
837
|
+
const installati = await elencaToolForgiatiFn({ cartella: cartellaForge });
|
|
838
|
+
const abilitati = installati.filter((t) => t.abilitato);
|
|
839
|
+
if (abilitati.length > 0) {
|
|
840
|
+
const manifestPerNome = new Map(abilitati.map((t) => [`${FORGE_PREFISSO_NOME_TOOL}${t.id}`, t.manifest]));
|
|
841
|
+
toolForge = abilitati.map((t) => ({
|
|
842
|
+
name: `${FORGE_PREFISSO_NOME_TOOL}${t.id}`,
|
|
843
|
+
description: t.manifest.description,
|
|
844
|
+
/*
|
|
845
|
+
* ⛔⛔⛔ 03/9 — QUI passava lo schema del manifest COSI' COM'E'.
|
|
846
|
+
*
|
|
847
|
+
* Un manifest Forge scrive le proprietà in forma abbreviata
|
|
848
|
+
* (`nome_contatto: 'string'` invece di `{ type: 'string' }`), che è
|
|
849
|
+
* comoda e NON è JSON Schema. OpenRouter non se ne accorge perché non
|
|
850
|
+
* costruisce grammatiche; llama.cpp deve farlo e si ferma:
|
|
851
|
+
* «JSON schema conversion failed: Unrecognized schema: "string"»,
|
|
852
|
+
* due volte — esattamente le due proprietà di quel manifest.
|
|
853
|
+
* ⇒ Il difetto era nostro e vecchio: l'ha scoperto il primo motore
|
|
854
|
+
* abbastanza severo da leggere quello schema davvero.
|
|
855
|
+
*/
|
|
856
|
+
inputSchema: schemaIngressoAttrezzo(t.manifest.inputSchema),
|
|
857
|
+
}));
|
|
858
|
+
eseguiToolForgeFn = async (nome, argomenti) => {
|
|
859
|
+
const manifest = manifestPerNome.get(nome);
|
|
860
|
+
if (!manifest) throw new Error(`unknown forged tool: ${nome}`);
|
|
861
|
+
return eseguiFlowForge(manifest, argomenti, { capacitaFn: eseguiCapacitaForge });
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
} catch {
|
|
865
|
+
// ⭐ un .tool-forge-store/ malformato non deve mai bloccare l'avvio: toolForge resta undefined, zero tool nuovo offerto.
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
/**
|
|
869
|
+
* ⭐⭐⭐⭐ Il tool_create dispatch — valida via validaManifestForge
|
|
870
|
+
* (kernel) PRIMA di scrivere, mai un tentativo di installazione con
|
|
871
|
+
* un manifest sospetto. Porto diretto dei messaggi mobile
|
|
872
|
+
* (`forgeCreateTool.ts`): "That tool could not be created: ...",
|
|
873
|
+
* "Created "X" — it stays off until the user enables it in Tool
|
|
874
|
+
* Forge." — verbatim, non riformulati.
|
|
875
|
+
*/
|
|
876
|
+
const onForgeCrea = async (argomenti) => {
|
|
877
|
+
const manifestoGrezzo = {
|
|
878
|
+
id: argomenti?.id, title: argomenti?.title, description: argomenti?.description,
|
|
879
|
+
inputSchema: argomenti?.input_schema, flow: argomenti?.flow,
|
|
880
|
+
};
|
|
881
|
+
const validazione = validaManifestForge(manifestoGrezzo);
|
|
882
|
+
if (!validazione.ok) {
|
|
883
|
+
return { ok: false, esito: `That tool could not be created: ${validazione.diagnostica.join('; ') || 'the manifest is invalid'}.` };
|
|
884
|
+
}
|
|
885
|
+
try {
|
|
886
|
+
await installaToolForgiatoFn({
|
|
887
|
+
cartella: cartellaForge, manifest: manifestoGrezzo,
|
|
888
|
+
capacita: validazione.capacita, azioni: validazione.azioni, rischio: validazione.rischio,
|
|
889
|
+
});
|
|
890
|
+
} catch (errore) {
|
|
891
|
+
if (errore instanceof ToolForgeStoreError) return { ok: false, esito: `That tool could not be created: ${errore.message}.` };
|
|
892
|
+
throw errore;
|
|
893
|
+
}
|
|
894
|
+
return { ok: true, esito: `Created "${manifestoGrezzo.title}" — it stays off until the user enables it in Tool Forge.` };
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
/*
|
|
898
|
+
* ⭐⭐⭐ 29/8 — FASE G: gli hook di un plugin fidato NON passano dal
|
|
899
|
+
* trust hash-vincolato di hook-registry.mjs (namespace diverso, per
|
|
900
|
+
* costruzione: uno standalone è fidato per hash del SUO comando, uno
|
|
901
|
+
* di plugin per hash dell'INTERO manifesto — vedi la doc in
|
|
902
|
+
* plugin-session.mjs sul perché mischiare i due sarebbe un bug di
|
|
903
|
+
* sicurezza) — sono GIÀ fidati qui, il plugin intero lo è.
|
|
904
|
+
*
|
|
905
|
+
* Si fondono in QUESTO file, non dentro `costruisciHookFn`
|
|
906
|
+
* (session-registry.mjs): quella funzione è costruita PRIMA di
|
|
907
|
+
* questo prep asincrono (l'ordine lo fissa già FASE A — hookFn
|
|
908
|
+
* sincrono, passato com'è), e conosce solo `.harness-ui-hooks.json`.
|
|
909
|
+
* Questo è il solo punto dove un evento hook incontra ENTRAMBE le
|
|
910
|
+
* fonti. `hookFn` (il parametro ricevuto, gli hook standalone) resta
|
|
911
|
+
* la prima parola: se rifiuta, gli hook di plugin non girano nemmeno
|
|
912
|
+
* — stessa semantica AND ("il primo che rifiuta vince") già in uso
|
|
913
|
+
* dentro il ciclo di `costruisciHookFn` per gli hook standalone fra
|
|
914
|
+
* loro, estesa qui a una seconda fonte.
|
|
915
|
+
*
|
|
916
|
+
* PARITÀ: hookPlugin vuoto (ogni sessione senza plugin fidati con
|
|
917
|
+
* hook, cioè ogni sessione oggi) ⇒ hookFnConPlugin === hookFn,
|
|
918
|
+
* stesso riferimento, zero wrapping — comportamento bit-per-bit
|
|
919
|
+
* quello di prima di FASE G.
|
|
920
|
+
*/
|
|
921
|
+
const hookFnConPlugin = hookPlugin.length === 0
|
|
922
|
+
? hookFn
|
|
923
|
+
: async (evento) => {
|
|
924
|
+
if (hookFn) {
|
|
925
|
+
const esitoBase = await hookFn(evento);
|
|
926
|
+
if (esitoBase?.consentito === false) return esitoBase;
|
|
927
|
+
}
|
|
928
|
+
const pertinenti = hookPlugin.filter((h) => h.eventi.includes(evento.tipo));
|
|
929
|
+
for (const hook of pertinenti) {
|
|
930
|
+
let esito;
|
|
931
|
+
try {
|
|
932
|
+
esito = await eseguiHookFn({ hook, evento, cartella });
|
|
933
|
+
} catch {
|
|
934
|
+
esito = { consentito: false, motivo: `l'hook di plugin "${hook.id}" è fallito nell'esecuzione.` };
|
|
935
|
+
}
|
|
936
|
+
onEvento(hookInvoked({ hookId: hook.id, tipo: evento.tipo, azione: evento.azione, esito }));
|
|
937
|
+
if (esito?.consentito === false) return esito;
|
|
938
|
+
}
|
|
939
|
+
return { consentito: true };
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
/*
|
|
943
|
+
* ⭐⭐⭐ 27/8, R1 — un messageId per il testo e uno per il ragionamento,
|
|
944
|
+
* PER GIRO (una Map, non due variabili: `talosLavora` numera i giri da
|
|
945
|
+
* 0, e un giro può ripassare da qui più volte in task lunghi). Aperto
|
|
946
|
+
* al PRIMO delta di quel tipo per quel giro, chiuso quando `onGiro`
|
|
947
|
+
* segnala che il giro è concluso — mai aperto due volte per lo stesso
|
|
948
|
+
* giro (altrimenti TextMessageStart/ReasoningMessageStart duplicati,
|
|
949
|
+
* stesso difetto già chiuso stanotte per i bubble duplicati via
|
|
950
|
+
* `_sequenza`/replay SSE, causa diversa stessa famiglia).
|
|
951
|
+
*/
|
|
952
|
+
const messaggiTestoPerGiro = new Map();
|
|
953
|
+
const messaggiRagionamentoPerGiro = new Map();
|
|
954
|
+
/*
|
|
955
|
+
* ⭐⭐⭐ Piano procedi-col-generare-un-snoopy-neumann.md, Fase 4 — un
|
|
956
|
+
* `Set` di toolCallId PER GIRO (non una Map messageId-per-giro come
|
|
957
|
+
* sopra: una tool-call non ha un "messaggio", ha un `toolCallId` già
|
|
958
|
+
* suo — vedi `talosHarness.mjs`, `tipo:'tool-inizio'`/`'tool-args'`).
|
|
959
|
+
* Popolato al primo `tool-inizio` di un id, letto (e svuotato) quando
|
|
960
|
+
* `onGiro` segnala la fine del giro — stesso ciclo di vita delle due
|
|
961
|
+
* Map sopra, stessa ragione: mai un ToolCallStart/Args duplicato
|
|
962
|
+
* quando `eventiPerRisposta` traduce la risposta finale.
|
|
963
|
+
*/
|
|
964
|
+
const toolCallIdStreamatiPerGiro = new Map();
|
|
965
|
+
/*
|
|
966
|
+
* ⭐ SEMPRE passato (non condizionato da `reasoning`): chiedere lo
|
|
967
|
+
* streaming del TESTO è a costo zero — stessi token, consegnati a
|
|
968
|
+
* pezzi invece che in un colpo solo — mentre `reasoning` da solo
|
|
969
|
+
* cambia comportamento/costo del modello ed è per questo opzionale.
|
|
970
|
+
* Le due cose sono indipendenti in chiamaConRitenta (vedi la sua doc).
|
|
971
|
+
*/
|
|
972
|
+
const onDelta = (evento) => {
|
|
973
|
+
if (evento.tipo === 'tool-inizio' || evento.tipo === 'tool-args') {
|
|
974
|
+
/*
|
|
975
|
+
* ⭐⭐⭐ Piano procedi-col-generare-un-snoopy-neumann.md, Fase 4 —
|
|
976
|
+
* argomenti di tool-call a pezzi, il canale che mancava (prima
|
|
977
|
+
* d'ora `ToolCallArgs` arrivava tutto insieme, a fine giro — vedi
|
|
978
|
+
* `eventiPerRisposta`). `app.js` gestisce già oggi argomenti
|
|
979
|
+
* parziali con ri-parse tollerante: zero modifiche frontend.
|
|
980
|
+
*/
|
|
981
|
+
let streamati = toolCallIdStreamatiPerGiro.get(evento.giro);
|
|
982
|
+
if (!streamati) { streamati = new Set(); toolCallIdStreamatiPerGiro.set(evento.giro, streamati); }
|
|
983
|
+
if (evento.tipo === 'tool-inizio') {
|
|
984
|
+
streamati.add(evento.toolCallId);
|
|
985
|
+
/*
|
|
986
|
+
* ⭐ OSS-1 (17/09/2026) — L'ORA DEL SERVER, TIMBRATA QUI.
|
|
987
|
+
*
|
|
988
|
+
* Nella scheda «Processi» ogni riga diceva «0 s» al REPLAY: il client ricavava l'istante
|
|
989
|
+
* da `ricevutoA`, cioe' da quando l'evento gli ARRIVA, e al replay arrivano tutti insieme.
|
|
990
|
+
* ⇒ L'istante deve viaggiare DENTRO l'evento.
|
|
991
|
+
*
|
|
992
|
+
* ⛔ Timbrato qui e non nel kernel: `tool-inizio` e' il verbale del parser SSE, e due prove
|
|
993
|
+
* di `talosHarness.test.mjs` ne confrontano la forma per intero (misurato: un campo in
|
|
994
|
+
* piu' le faceva rosse). `onDelta` e' chiamato in modo SINCRONO dal parser, quindi questo
|
|
995
|
+
* `Date.now()` cade nello stesso millisecondo — nessuna approssimazione comprata.
|
|
996
|
+
* ⛔ `evento.avviatoA` ha comunque la precedenza: il giorno in cui un kernel lo misurera'
|
|
997
|
+
* piu' vicino alla fonte, quel valore vince senza toccare questa riga.
|
|
998
|
+
*/
|
|
999
|
+
onEvento(toolCallStart({ toolCallId: evento.toolCallId, toolCallName: evento.nome, avviatoA: evento.avviatoA ?? Date.now() }));
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
onEvento(toolCallArgs({ toolCallId: evento.toolCallId, delta: evento.delta }));
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
/*
|
|
1006
|
+
* ⛔⛔⛔ 11/09/2026, owner: «se la UI è avvisata di N attrezzi partiti, deve
|
|
1007
|
+
* vedere finire N». Il kernel annuncia un `tool-inizio` appena una chiamata
|
|
1008
|
+
* comincia, ma DUE strade la lasciano senza esito — la valanga di copie
|
|
1009
|
+
* identiche del modello locale (misurato: 4 annunciate, 2 sopravvissute) e
|
|
1010
|
+
* lo stop premuto a metà risposta (2 annunciate, 2 orfane). In entrambi i
|
|
1011
|
+
* casi restavano indicatori che giravano per sempre.
|
|
1012
|
+
*
|
|
1013
|
+
* ⭐ Si chiude con un `ToolCallResult`, cioè un evento che il frontend
|
|
1014
|
+
* DISEGNA GIÀ: nessuna riga di UI cambia. È la forma che la spec AG-UI
|
|
1015
|
+
* prescrive (`messageId` + `toolCallId` + `content`, `role` opzionale —
|
|
1016
|
+
* `docs/concepts/messages.mdx`, letto via ctx7 l'11/09/2026) e la cura che
|
|
1017
|
+
* ag-ui#1168 descrive per lo stesso guasto: «a fresh message ID is used so
|
|
1018
|
+
* the client creates a proper standalone ToolMessage and closes the spinner
|
|
1019
|
+
* correctly».
|
|
1020
|
+
*
|
|
1021
|
+
* ⛔ E il `content` PORTA IL MOTIVO, non è un segnaposto vuoto: openclaw
|
|
1022
|
+
* #42112 («persisted orphaned toolCall poisons session replay») vale anche
|
|
1023
|
+
* per noi, perché `session-registry.mjs` persiste questi eventi — chi
|
|
1024
|
+
* rilegge la sessione domani deve trovare scritto perché quell'attrezzo non
|
|
1025
|
+
* è mai partito, non un buco.
|
|
1026
|
+
*/
|
|
1027
|
+
if (evento.tipo === 'tool-annullato') {
|
|
1028
|
+
onEvento(eventoPerEsitoTool({ messageId: randomUUID(), toolCallId: evento.toolCallId, content: evento.motivo, outcome: 'cancelled' }));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
/*
|
|
1032
|
+
* ⛔⛔ E QUI SOTTO C'ERA UNA TRAPPOLA: la riga successiva manda al ramo
|
|
1033
|
+
* «ragionamento» QUALUNQUE tipo che non sia 'testo' — cioè un tipo nuovo,
|
|
1034
|
+
* aggiunto un giorno nel kernel da chi non legge questo file, sarebbe
|
|
1035
|
+
* comparso a schermo come un ragionamento del modello, inventato di sana
|
|
1036
|
+
* pianta. È esattamente il motivo per cui `tool-annullato` non poteva
|
|
1037
|
+
* essere emesso dal solo kernel. Adesso i tipi sconosciuti si IGNORANO:
|
|
1038
|
+
* non sapere e mentire non sono la stessa cosa.
|
|
1039
|
+
*/
|
|
1040
|
+
if (evento.tipo !== 'testo' && evento.tipo !== 'ragionamento') return;
|
|
1041
|
+
const mappa = evento.tipo === 'testo' ? messaggiTestoPerGiro : messaggiRagionamentoPerGiro;
|
|
1042
|
+
let messageId = mappa.get(evento.giro);
|
|
1043
|
+
if (!messageId) {
|
|
1044
|
+
messageId = randomUUID();
|
|
1045
|
+
mappa.set(evento.giro, messageId);
|
|
1046
|
+
onEvento(evento.tipo === 'testo'
|
|
1047
|
+
? textMessageStart({ messageId })
|
|
1048
|
+
: reasoningMessageStart({ messageId }));
|
|
1049
|
+
}
|
|
1050
|
+
onEvento(evento.tipo === 'testo'
|
|
1051
|
+
? textMessageContent({ messageId, delta: evento.delta })
|
|
1052
|
+
: reasoningMessageContent({ messageId, delta: evento.delta }));
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
const ricevuteToolPerId = new Map();
|
|
1056
|
+
|
|
1057
|
+
const onGiro = (evento) => {
|
|
1058
|
+
if (evento.tipo === 'ricevuta') {
|
|
1059
|
+
const ricevuta = evento.ricevuta;
|
|
1060
|
+
const toolCallId = ricevuta && typeof ricevuta === 'object' && !Array.isArray(ricevuta)
|
|
1061
|
+
&& typeof ricevuta.toolCallId === 'string' ? ricevuta.toolCallId : '';
|
|
1062
|
+
if (toolCallId) ricevuteToolPerId.set(toolCallId, ricevuta);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (evento.tipo === 'risposta') {
|
|
1066
|
+
/*
|
|
1067
|
+
* ⛔ Chiude PRIMA di tradurre la risposta finale: un consumer che
|
|
1068
|
+
* legge gli eventi in ordine deve vedere End prima del prossimo
|
|
1069
|
+
* Start (di un giro successivo), mai i due mescolati.
|
|
1070
|
+
*/
|
|
1071
|
+
const messageIdTesto = messaggiTestoPerGiro.get(evento.giro);
|
|
1072
|
+
if (messageIdTesto) { onEvento(textMessageEnd({ messageId: messageIdTesto })); messaggiTestoPerGiro.delete(evento.giro); }
|
|
1073
|
+
const messageIdRagionamento = messaggiRagionamentoPerGiro.get(evento.giro);
|
|
1074
|
+
if (messageIdRagionamento) { onEvento(reasoningMessageEnd({ messageId: messageIdRagionamento })); messaggiRagionamentoPerGiro.delete(evento.giro); }
|
|
1075
|
+
/*
|
|
1076
|
+
* ⭐⭐⭐ Piano procedi-col-generare-un-snoopy-neumann.md, Fase 4 —
|
|
1077
|
+
* stesso principio delle due righe sopra, applicato alle
|
|
1078
|
+
* tool-call: `toolCallsGiaStreamate` dice a `eventiPerRisposta`
|
|
1079
|
+
* quali id non ri-emettere (già mandati a pezzi da onDelta sopra).
|
|
1080
|
+
* Letto QUI, poi tolto — un giro successivo riparte da un Set
|
|
1081
|
+
* vuoto, mai quello del giro precedente.
|
|
1082
|
+
*/
|
|
1083
|
+
const toolCallsGiaStreamate = toolCallIdStreamatiPerGiro.get(evento.giro);
|
|
1084
|
+
if (toolCallsGiaStreamate) toolCallIdStreamatiPerGiro.delete(evento.giro);
|
|
1085
|
+
|
|
1086
|
+
const messageId = randomUUID();
|
|
1087
|
+
for (const e of eventiPerRisposta(evento.risposta, { messageId, testoGiaStreamato: Boolean(messageIdTesto), toolCallsGiaStreamate })) onEvento(e);
|
|
1088
|
+
/*
|
|
1089
|
+
* ⭐⭐⭐ Piano procedi-col-generare-un-snoopy-neumann.md, Fase 3 —
|
|
1090
|
+
* `evento.totali`, quando presente (talosHarness.mjs lo omette
|
|
1091
|
+
* finché nessun giro ha mai riportato `usage`), è già la somma
|
|
1092
|
+
* cumulativa fino a questo giro: inoltrato com'è, dopo gli eventi
|
|
1093
|
+
* della risposta (un consumer vede prima il testo/tool-call del
|
|
1094
|
+
* giro, poi il totale aggiornato — mai il contrario).
|
|
1095
|
+
*/
|
|
1096
|
+
if (evento.totali) onEvento(eventoPerUsage(evento.totali));
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
/*
|
|
1100
|
+
* ⛔ D-10B — l'uscita mentre esce. Sta PRIMA di `tool-esito` perche' e' cio' che arriva prima:
|
|
1101
|
+
* chi legge gli eventi in ordine vede l'avanzamento e poi il risultato, mai il contrario.
|
|
1102
|
+
*/
|
|
1103
|
+
if (evento.tipo === 'tool-uscita') {
|
|
1104
|
+
onEvento(toolCallOutput({ toolCallId: evento.toolCallId, delta: evento.delta }));
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
if (evento.tipo === 'tool-esito') {
|
|
1108
|
+
/* ⭐ OSS-1/OSS-2 — `durataMs`, `comando` e `cwd` arrivano dal kernel solo per gli attrezzi che
|
|
1109
|
+
li misurano (oggi `prova` e `shell`) e passano di qui invariati.
|
|
1110
|
+
M4-E: la ricevuta e' gia' stata prodotta dal kernel; qui viene solo correlata per id,
|
|
1111
|
+
consumata una volta e inoltrata senza interpretarla. */
|
|
1112
|
+
const ricevuta = ricevuteToolPerId.get(evento.toolCallId);
|
|
1113
|
+
if (ricevuta) ricevuteToolPerId.delete(evento.toolCallId);
|
|
1114
|
+
onEvento(eventoPerEsitoTool({
|
|
1115
|
+
messageId: randomUUID(), toolCallId: evento.toolCallId, content: evento.content,
|
|
1116
|
+
durataMs: evento.durataMs, comando: evento.comando, cwd: evento.cwd,
|
|
1117
|
+
...(ricevuta ? {receipt: ricevuta} : {}),
|
|
1118
|
+
}));
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
|
|
1122
|
+
/*
|
|
1123
|
+
* ⭐ 27/8 — `esisteva` ora arriva DIRETTAMENTE da `talosHarness.mjs`
|
|
1124
|
+
* (`premessaDellaScrittura`, che lo calcola comunque leggendo `prima`
|
|
1125
|
+
* per il cancello semantico): dice se il file era già sul disco PRIMA
|
|
1126
|
+
* di questa scrittura. Prima di questa riga era una approssimazione
|
|
1127
|
+
* per sessione (un `Set` di percorsi già scritti IN QUESTA sessione),
|
|
1128
|
+
* che etichettava "nuovo" ogni file toccato per la prima volta nel
|
|
1129
|
+
* run anche se esisteva da sempre su disco — bug del pannello Review.
|
|
1130
|
+
*
|
|
1131
|
+
* ⭐⭐⭐ 27/8 (secondo giro) — `contenutoPrima`, quarto argomento:
|
|
1132
|
+
* stessa storia di `esisteva`, un livello più in la. `talosHarness.mjs`
|
|
1133
|
+
* ora tiene (invece di buttare) il testo che legge comunque dal disco
|
|
1134
|
+
* per calcolare `esisteva` — questo file lo inoltra cosi' com'e', zero
|
|
1135
|
+
* logica qui: la traduzione in un formato di evento vive tutta in
|
|
1136
|
+
* `agui-events.mjs`, come per `esisteva`.
|
|
1137
|
+
*/
|
|
1138
|
+
const onScrittura = (percorso, contenuto, esisteva, contenutoPrima) => {
|
|
1139
|
+
onEvento(eventoPerScrittura({ percorso, contenuto, esisteva, contenutoPrima }));
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
/*
|
|
1143
|
+
* ⭐⭐⭐ 28/8 — side-channel dell'attrezzo `artifact_create` (talosHarness.mjs),
|
|
1144
|
+
* STESSO principio di `onScrittura`: il tool torna al modello una riga di
|
|
1145
|
+
* testo ("created: ..."), l'HTML vero arriva qui, separato, per diventare
|
|
1146
|
+
* un evento AG-UI che il frontend può renderizzare (iframe sandboxato —
|
|
1147
|
+
* vedi app.js). Se assente (nessun `talosLavora` la offre mai a
|
|
1148
|
+
* TALOS-BANCO), il kernel usa un id locale deterministico: qui SEMPRE
|
|
1149
|
+
* presente, quindi SEMPRE questo id vince, mai quello di fallback.
|
|
1150
|
+
*/
|
|
1151
|
+
const onArtefatto = async (titolo, html) => {
|
|
1152
|
+
if (Buffer.byteLength(html, 'utf8') > ARTEFATTO_MAX_BYTE) {
|
|
1153
|
+
// ⛔ Nessun evento, niente salvato: il tool torna comunque un id (il modello non deve credere che nulla sia successo), ma la UI non riceve mai un artefatto troncato/enorme.
|
|
1154
|
+
return { id: `artefatto-rifiutato-troppo-grande-${randomUUID()}` };
|
|
1155
|
+
}
|
|
1156
|
+
const id = randomUUID();
|
|
1157
|
+
salvaArtefattoFn(id, html);
|
|
1158
|
+
onEvento(artifactCreated({ messageId: randomUUID(), id, titolo }));
|
|
1159
|
+
/*
|
|
1160
|
+
* La copia durevole, in Libreria. ⛔ Non blocca e non fa fallire il giro: se la Libreria non è
|
|
1161
|
+
* scrivibile l'artefatto resta comunque a schermo e apribile — meglio un artefatto senza copia
|
|
1162
|
+
* che un giro rotto per una scrittura. Il motivo si vede nel log del server, mai in silenzio.
|
|
1163
|
+
*/
|
|
1164
|
+
try {
|
|
1165
|
+
await salvaVoceLibreriaFn({
|
|
1166
|
+
cartella,
|
|
1167
|
+
nome: `${(titolo || 'artefatto').replace(/[\\/:*?"<>|]/g, '-').slice(0, 80)}.html`,
|
|
1168
|
+
mediaType: 'text/html',
|
|
1169
|
+
origine: 'generated',
|
|
1170
|
+
/* ⭐ BC-38 (12/09/2026) — «da chi è stato creato» a schermo: `salvaVoce` tiene
|
|
1171
|
+
`modello` solo per le voci generate, e questa lo è. Senza, il dettaglio poteva dire
|
|
1172
|
+
soltanto «il modello» — misurato: 14 voci su 14 sul disco dell'owner erano senza. */
|
|
1173
|
+
modello,
|
|
1174
|
+
testo: html,
|
|
1175
|
+
});
|
|
1176
|
+
} catch (errore) {
|
|
1177
|
+
console.error('[artefatti] copia in Libreria non riuscita:', errore instanceof Error ? errore.message : errore);
|
|
1178
|
+
}
|
|
1179
|
+
return { id };
|
|
1180
|
+
};
|
|
1181
|
+
|
|
1182
|
+
/*
|
|
1183
|
+
* ⭐⭐⭐ 28/8 — side-channel dell'attrezzo `document_create`
|
|
1184
|
+
* (talosHarness.mjs), stesso principio di `onArtefatto`/`onScrittura`:
|
|
1185
|
+
* il kernel resta a zero dipendenze, la generazione vera (7 librerie
|
|
1186
|
+
* npm — owner 28/8, "sì, aggiungile", SOLO qui) vive interamente in
|
|
1187
|
+
* questo backend. Pipeline identica al mobile
|
|
1188
|
+
* (documentTools.ts/documentGenerator.ts, letti per intero prima di
|
|
1189
|
+
* scrivere): generate → verify → salva — MAI il passo dopo se quello
|
|
1190
|
+
* prima è fallito, e ogni fallimento porta la RAGIONE vera, non un
|
|
1191
|
+
* "errore tecnico" generico.
|
|
1192
|
+
*
|
|
1193
|
+
* ⛔⛔ 07/9, O-37 — owner: «con i modelli a chiave API gli artefatti vengono creati ma non
|
|
1194
|
+
* salvati nella Libreria». RIPRODOTTO con un giro vero (GLM 5.3 Flash, istanza di prova 4311):
|
|
1195
|
+
* il `.docx` finiva nel workspace, e in Libreria non arrivava niente. La causa era QUI, e non era
|
|
1196
|
+
* un guasto: era un commento rimasto indietro. Diceva «qui non c'è una Libreria (il desktop non ne
|
|
1197
|
+
* ha una)» — vero fino al 28/8, falso dal 29/8, quando è nata la FASE N. `onArtefatto` (gli
|
|
1198
|
+
* artefatti HTML) la copia c'era già; `onDocumento` no, e il modello usa proprio questo.
|
|
1199
|
+
* ⇒ Il file finisce nel WORKSPACE vero, stesso
|
|
1200
|
+
* trattamento di `scrivi`: un evento StateDelta con `op:'add'` (mai
|
|
1201
|
+
* 'replace' — creaFileWorkspaceFn rifiuta un nome già esistente,
|
|
1202
|
+
* quindi ogni successo qui È per costruzione un file nuovo) fa scattare
|
|
1203
|
+
* lo stesso refresh dell'albero già provato per `scrivi`
|
|
1204
|
+
* (`segnalaScritturaNellAlbero`, app.js) — nessun meccanismo nuovo sul
|
|
1205
|
+
* frontend. Il `value` è il testo VERO per i formati testuali (si
|
|
1206
|
+
* vede nella scheda Review, come un file scritto normale); per i
|
|
1207
|
+
* formati binari (docx/xlsx/pptx/pdf) è una riga onesta — mai i byte
|
|
1208
|
+
* grezzi dentro un evento SSE/JSON, che li corromperebbe comunque
|
|
1209
|
+
* (non sono UTF-8 valido).
|
|
1210
|
+
*/
|
|
1211
|
+
/*
|
|
1212
|
+
* ⛔⛔⛔ BC-11 (11/09/2026) — «MODE» LO SCRIVE IL MODELLO, E LO SCRIVE COME GLI PARE.
|
|
1213
|
+
*
|
|
1214
|
+
* Il kernel consegna `argomenti` VERBATIM a questo callback (talosHarness.mjs:6175,
|
|
1215
|
+
* `onDocumento(argomenti)`; il commento alla riga 5069 lo dichiara: «`argomenti` passa SEMPRE
|
|
1216
|
+
* verbatim»), quindi una chiave che lo schema non nomina arriva qui intatta. È l'unico posto in
|
|
1217
|
+
* cui la modalità può essere letta senza toccare il kernel.
|
|
1218
|
+
*
|
|
1219
|
+
* ⛔ Perché tre nomi e non uno: misurato sulle due sessioni di BC-11, il modello ha sbagliato il
|
|
1220
|
+
* nome dell'argomento **46 volte su 308 chiamate** — `shell.command` invece di `comando` 39
|
|
1221
|
+
* volte (30 nella sessione `8dde6bff`, 9 nella `37e10d21`), `scrivi.path` 2, `scrivi.content`
|
|
1222
|
+
* 1, `scrivi.contuto` 1 (un refuso suo). Ogni volta il campo arrivava `undefined` e il giro era
|
|
1223
|
+
* perso. Non è un difetto del nostro modello: anthropics/claude-quickstarts#348 (letto
|
|
1224
|
+
* 11/09/2026) documenta la stessa cosa sull'implementazione di riferimento del fornitore — «the
|
|
1225
|
+
* class expects `new_str`… but Claude actually outputs `insert_text`. This causes insert
|
|
1226
|
+
* commands to fail». ⇒ Si accettano gli alias, in UN posto solo. Stessa scelta di hermes-agent
|
|
1227
|
+
* v0.21, che sul suo `patch` accetta entrambe le forme e ne pubblicizza una sola
|
|
1228
|
+
* (`tools/file_tools.py:2747`, PATCH_SCHEMA: «The handler accepts BOTH shapes from any model
|
|
1229
|
+
* regardless»).
|
|
1230
|
+
* ⛔ E `append:true` come booleano è la terza forma perché è quella che un modello scrive quando
|
|
1231
|
+
* l'idea gliel'ha data la frase «call it again to append», non un nome di enum.
|
|
1232
|
+
*/
|
|
1233
|
+
const MODALITA_DOCUMENTO = { append: 'accoda', accoda: 'accoda', add: 'accoda', new: 'nuovo', nuovo: 'nuovo', create: 'nuovo', replace: 'nuovo' };
|
|
1234
|
+
const modalitaDelDocumento = (argomenti) => {
|
|
1235
|
+
if (argomenti?.append === true) return 'accoda';
|
|
1236
|
+
const grezza = argomenti?.mode ?? argomenti?.modalita ?? argomenti?.modality;
|
|
1237
|
+
if (grezza === undefined || grezza === null || grezza === '') return 'nuovo';
|
|
1238
|
+
return MODALITA_DOCUMENTO[String(grezza).trim().toLowerCase()] ?? null; // null = detto male: si risponde a parole, non si indovina
|
|
1239
|
+
};
|
|
1240
|
+
/*
|
|
1241
|
+
* ⛔ I formati BINARI non si accodano, e non è una prudenza: un `.docx`/`.xlsx`/`.pptx` è uno zip
|
|
1242
|
+
* e un `.pdf` ha un trailer con la tavola degli offset in fondo — concatenare due file di
|
|
1243
|
+
* questi tipi produce un file che si APRE come corrotto, cioè il peggiore degli esiti: una
|
|
1244
|
+
* scrittura «riuscita» che ha distrutto il lavoro dei giri precedenti. L'insieme è lo stesso
|
|
1245
|
+
* `testuale` già usato più sotto per decidere cosa mostrare in chat: una definizione sola.
|
|
1246
|
+
*/
|
|
1247
|
+
/*
|
|
1248
|
+
* ⛔⛔ E `html` NON è accodabile, contro ogni aspettativa — l'ho verificato nel generatore prima
|
|
1249
|
+
* di scriverlo, non dedotto: `document-generator.mjs:234-245` non scrive `body` così com'è, lo
|
|
1250
|
+
* AVVOLGE (`<!doctype html>`, `<html><head>…`, un `<h1>` col titolo, ogni blocco separato da
|
|
1251
|
+
* riga vuota dentro un `<p>`, poi `</body></html>`) e lo passa da `escapeHtml`. Accodare un
|
|
1252
|
+
* secondo documento completo a uno che finisce con `</body></html>` produce un file con due
|
|
1253
|
+
* doctype: una scrittura «riuscita» che rompe il risultato. I formati sorgente
|
|
1254
|
+
* (`TALOS_SOURCE_TEXT_FORMATS`) passano invece VERBATIM (`document-generator.mjs:223-225`), e
|
|
1255
|
+
* `md`/`csv` sono testo piatto — quelli sì.
|
|
1256
|
+
* ⛔ Che `format:'html'` non sappia scrivere una pagina scritta a mano è un difetto a parte, più
|
|
1257
|
+
* grande di questo, e sta nel rapporto: NON è questa funzione a poterlo curare.
|
|
1258
|
+
*/
|
|
1259
|
+
const formatoAccodabile = (formato) => TALOS_SOURCE_TEXT_FORMATS.includes(formato) || ['md', 'csv'].includes(formato);
|
|
1260
|
+
|
|
1261
|
+
const onDocumento = async (argomenti) => {
|
|
1262
|
+
const modalita = modalitaDelDocumento(argomenti);
|
|
1263
|
+
if (modalita === null) {
|
|
1264
|
+
return {
|
|
1265
|
+
ok: false,
|
|
1266
|
+
esito: `"${argomenti?.mode ?? argomenti?.modalita}" is not a mode. Use mode:"append" to add to the end of an existing file, or leave mode out to create a new one.`,
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
let documento;
|
|
1270
|
+
try {
|
|
1271
|
+
documento = await generateTalosDocumentFn(argomenti);
|
|
1272
|
+
} catch (errore) {
|
|
1273
|
+
const dettaglio = errore instanceof Error ? errore.message : String(errore);
|
|
1274
|
+
return { ok: false, esito: `The document was not created: ${dettaglio}` };
|
|
1275
|
+
}
|
|
1276
|
+
if (modalita === 'accoda' && !formatoAccodabile(documento.format)) {
|
|
1277
|
+
return {
|
|
1278
|
+
ok: false,
|
|
1279
|
+
esito: `A .${documento.format} file cannot be appended to: it is a binary container, and joining two of them produces a corrupt file. `
|
|
1280
|
+
/* ⛔ 11/9 — `html` era in questo elenco e NON è accodabile: `formatoAccodabile` (sei righe
|
|
1281
|
+
sopra) ammette `TALOS_SOURCE_TEXT_FORMATS` più `md` e `csv`, e `html` non è in nessuno
|
|
1282
|
+
dei due. Il commento sopra lo diceva già, questa riga no: il suggerimento mandava il
|
|
1283
|
+
modello dritto sull'errore che stava rifiutando. */
|
|
1284
|
+
+ 'Send the whole document in one call, or use a text format (md, csv, txt, or a source format) if you need to build it in pieces.',
|
|
1285
|
+
};
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
const controllo = await verifyTalosDocumentFn(documento);
|
|
1289
|
+
if (!controllo.ok) {
|
|
1290
|
+
return {
|
|
1291
|
+
ok: false,
|
|
1292
|
+
esito: `The file was written but failed its check (${controllo.detail}), so it was discarded. Tell the user, and try a simpler structure.`,
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
let salvato;
|
|
1297
|
+
try {
|
|
1298
|
+
salvato = await creaFileWorkspaceFn({ cartella: cartellaPerCreare, nome: documento.fileName, bytes: documento.bytes, modalita });
|
|
1299
|
+
} catch (errore) {
|
|
1300
|
+
const dettaglio = errore instanceof WorkspaceFileError ? errore.message : (errore instanceof Error ? errore.message : String(errore));
|
|
1301
|
+
/*
|
|
1302
|
+
* ⛔⛔⛔ BC-11 — ERA QUESTO MESSAGGIO A INSEGNARE `_p2.html`.
|
|
1303
|
+
*
|
|
1304
|
+
* Fino all'11/09/2026 un nome già preso rispondeva «Do not silently retry with the same name
|
|
1305
|
+
* — offer a different title, or ask»: cioè l'harness stesso diceva al modello di inventarsi un
|
|
1306
|
+
* NOME DIVERSO. Nelle due sessioni di BC-11 il modello ha fatto esattamente questo di sua
|
|
1307
|
+
* iniziativa — `_p2.html`, `_p3.html`, `_p4.html`, `_p5.html`, `_p6core.js` e poi un passo di
|
|
1308
|
+
* «assemblaggio» che non è mai arrivato in fondo.
|
|
1309
|
+
* ⇒ Un rifiuto deve nominare la STRADA GIUSTA, non solo vietarne una. È la forma che usa
|
|
1310
|
+
* cline nell'errore del suo `insert_line` (`sdk-diff-edit-coordinator.ts:401`, letto
|
|
1311
|
+
* 11/09/2026): non dice solo che il valore è fuori intervallo, dice «Use N to append at
|
|
1312
|
+
* EOF» — il messaggio porta la mossa successiva.
|
|
1313
|
+
* ⛔ Il ramo `FILE_EXISTS` è distinto dagli altri perché è l'unico con una strada giusta da
|
|
1314
|
+
* nominare: un tetto superato o un contenuto mancante vogliono altro, e il loro messaggio
|
|
1315
|
+
* (scritto in `workspace-files.mjs`) lo dice già.
|
|
1316
|
+
*/
|
|
1317
|
+
if (errore instanceof WorkspaceFileError && errore.code === 'FILE_EXISTS') {
|
|
1318
|
+
return {
|
|
1319
|
+
ok: false,
|
|
1320
|
+
esito: `"${documento.fileName}" already exists. To ADD to it, call document_create again with the same title and mode:"append" — the new body goes at the end of that same file. `
|
|
1321
|
+
+ 'To make a separate file instead, use a different title. Do not invent numbered variants of the same name.',
|
|
1322
|
+
};
|
|
1323
|
+
}
|
|
1324
|
+
return {
|
|
1325
|
+
ok: false,
|
|
1326
|
+
esito: `"${documento.fileName}" was created and checked, but it could not be saved to the workspace: ${dettaglio}`,
|
|
1327
|
+
};
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
const testuale = TALOS_SOURCE_TEXT_FORMATS.includes(documento.format) || ['md', 'csv', 'html'].includes(documento.format);
|
|
1331
|
+
const valore = testuale ? new TextDecoder('utf-8').decode(documento.bytes) : `[binary ${documento.format} file, ${documento.bytes.byteLength} bytes]`;
|
|
1332
|
+
/*
|
|
1333
|
+
* ⛔ PO-05 — `[binary docx file, 7714 bytes]` è una riga onesta e inservibile: dice che il file
|
|
1334
|
+
* esiste e non dà modo di averlo. L'allegato viaggia accanto al valore (mai al posto suo: chi
|
|
1335
|
+
* legge la chat come testo continua a vedere la stessa riga di prima) e porta il minimo per
|
|
1336
|
+
* costruire il collegamento — nome, formato, byte. I BYTE no: quelli stanno dietro la rotta.
|
|
1337
|
+
* ⛔ Vale per TUTTI i formati, non solo i binari: anche un `.md` generato si scarica con un clic,
|
|
1338
|
+
* ed è la differenza fra «te lo mostro» e «te lo do».
|
|
1339
|
+
*/
|
|
1340
|
+
/*
|
|
1341
|
+
* ⛔ BC-11 — `esisteva` era scritto `false` a mano, e la ragione stava nel commento sopra
|
|
1342
|
+
* («creaFileWorkspaceFn rifiuta un nome già esistente, quindi ogni successo qui È per
|
|
1343
|
+
* costruzione un file nuovo»). Con `mode:"append"` quella costruzione non vale più: un
|
|
1344
|
+
* successo può ora atterrare su un file che c'era. Si legge dall'esito VERO della scrittura
|
|
1345
|
+
* (`salvato.accodato`), mai da una costanza che non è più vera — un pannello Review che dice
|
|
1346
|
+
* «nuovo» su un file cresciuto è la stessa classe di bugia che questo file rifiuta altrove.
|
|
1347
|
+
*/
|
|
1348
|
+
onEvento(eventoPerScrittura({
|
|
1349
|
+
percorso: percorsoNellAlbero(salvato.percorso),
|
|
1350
|
+
contenuto: valore,
|
|
1351
|
+
esisteva: salvato.accodato === true,
|
|
1352
|
+
allegato: { nome: documento.fileName, formato: documento.format, byte: documento.bytes.byteLength },
|
|
1353
|
+
}));
|
|
1354
|
+
|
|
1355
|
+
/*
|
|
1356
|
+
* La copia durevole in Libreria, come per gli artefatti HTML. ⛔ Non blocca e non fa fallire il
|
|
1357
|
+
* giro: se la Libreria non è scrivibile il documento resta comunque nel workspace, dove il
|
|
1358
|
+
* modello l'ha messo — meglio un documento senza copia che un giro rotto per una scrittura. Il
|
|
1359
|
+
* motivo si vede nel log del server, mai in silenzio.
|
|
1360
|
+
* ⛔ I binari viaggiano in base64 (`salvaVoce` lo prevede): un `docx` dentro un campo di testo
|
|
1361
|
+
* non è UTF-8 valido e si corromperebbe.
|
|
1362
|
+
*/
|
|
1363
|
+
/*
|
|
1364
|
+
* ⛔ BC-11 — una AGGIUNTA non è una voce nuova di Libreria. Copiare in Libreria anche i pezzi
|
|
1365
|
+
* accodati produrrebbe N voci con lo stesso nome per UN file (e `salvaVoce` le terrebbe
|
|
1366
|
+
* tutte): la Libreria custodisce documenti, non frammenti. Il file intero resta nel
|
|
1367
|
+
* workspace, che è dove il modello e la persona lo vedono; la copia durevole si fa quando il
|
|
1368
|
+
* documento NASCE. ⇒ Se un giorno servirà la copia del file completo, vorrà rileggere il
|
|
1369
|
+
* file dal disco — non concatenare i pezzi qui, che sarebbe un secondo stato da tenere
|
|
1370
|
+
* allineato.
|
|
1371
|
+
*/
|
|
1372
|
+
if (salvato.accodato !== true) {
|
|
1373
|
+
try {
|
|
1374
|
+
await salvaVoceLibreriaFn({
|
|
1375
|
+
cartella,
|
|
1376
|
+
nome: documento.fileName,
|
|
1377
|
+
mediaType: documento.mediaType,
|
|
1378
|
+
origine: 'generated',
|
|
1379
|
+
modello, // ⭐ BC-38: chi ha scritto il documento, letto dal meta e mai dedotto dal nome
|
|
1380
|
+
...(testuale
|
|
1381
|
+
? { testo: new TextDecoder('utf-8').decode(documento.bytes) }
|
|
1382
|
+
: { base64: Buffer.from(documento.bytes).toString('base64') }),
|
|
1383
|
+
});
|
|
1384
|
+
} catch (errore) {
|
|
1385
|
+
console.error('[documenti] copia in Libreria non riuscita:', errore instanceof Error ? errore.message : errore);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
const dimensione = Math.max(1, Math.round(documento.bytes.byteLength / 1024));
|
|
1390
|
+
/*
|
|
1391
|
+
* ⛔⛔ BC-11 — L'ESITO DICE LA MOSSA SUCCESSIVA, ed è la parte che fa risparmiare i giri.
|
|
1392
|
+
* Lo schema di `document_create` vive nel kernel e non nomina `mode` (fuori dalla mia lane:
|
|
1393
|
+
* vedi il rapporto), quindi l'UNICO canale per far sapere al modello che l'aggiunta esiste è
|
|
1394
|
+
* il risultato dell'attrezzo — che il modello legge per intero, a ogni giro. È la stessa
|
|
1395
|
+
* scelta di hermes-agent v0.21 (`tools/file_tools.py:2729`), che nella risposta di
|
|
1396
|
+
* `write_file` mette «The result's verified:true means the on-disk content hash was
|
|
1397
|
+
* confirmed — do NOT re-read the file to check the write landed»: una riga nell'esito che
|
|
1398
|
+
* toglie un giro di verifica a ogni scrittura.
|
|
1399
|
+
* ⛔ Solo per i formati accodabili: suggerire l'aggiunta su un `.docx` sarebbe un consiglio che
|
|
1400
|
+
* porta a un file corrotto.
|
|
1401
|
+
*/
|
|
1402
|
+
const comeContinuare = formatoAccodabile(documento.format)
|
|
1403
|
+
? ' To make it longer, call document_create again with the same title and mode:"append" instead of writing a second file.'
|
|
1404
|
+
: '';
|
|
1405
|
+
if (salvato.accodato === true) {
|
|
1406
|
+
const totale = Math.max(1, Math.round((salvato.byteTotali ?? documento.bytes.byteLength) / 1024));
|
|
1407
|
+
return {
|
|
1408
|
+
ok: true,
|
|
1409
|
+
esito: `Appended ${dimensione} KB to "${documento.fileName}" — it is now ${totale} KB. Checked by reopening it: ${controllo.detail}.${comeContinuare}`,
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
return {
|
|
1413
|
+
ok: true,
|
|
1414
|
+
esito: `Created "${documento.fileName}" (${dimensione} KB) in the workspace. Checked by reopening it: ${controllo.detail}.${comeContinuare}`,
|
|
1415
|
+
};
|
|
1416
|
+
};
|
|
1417
|
+
|
|
1418
|
+
/*
|
|
1419
|
+
* ⭐⭐⭐ 29/8 — side-channel dell'attrezzo `generate_image`
|
|
1420
|
+
* (talosHarness.mjs), STESSO principio di `onDocumento` appena
|
|
1421
|
+
* sopra: il kernel resta a zero dipendenze, la chiamata vera vive
|
|
1422
|
+
* qui (`image-generator.mjs`). A differenza di `onDocumento` non
|
|
1423
|
+
* c'è un passo "verify" separato — `generaImmagineFn` stessa lancia
|
|
1424
|
+
* se la risposta è malformata (vedi la sua doc, `TALOS_IMAGE_*`),
|
|
1425
|
+
* quindi il primo `try` copre GENERAZIONE, non solo la chiamata di
|
|
1426
|
+
* rete grezza.
|
|
1427
|
+
*/
|
|
1428
|
+
const onImmagine = async (argomenti) => {
|
|
1429
|
+
// ⛔ difesa in profondità: `immagine` è SEMPRE presente quando session-registry.mjs offre questo attrezzo (config.mjs, parseImmagine non torna mai undefined) — ma un chiamante diverso di agent-service.mjs che offra 'generate_image' senza wireare `immagine` non deve MAI vedere un crash, stesso principio onesto di web_search senza provider.
|
|
1430
|
+
if (!immagine) {
|
|
1431
|
+
return { ok: false, esito: 'image generation is not configured on this harness: no model was set.' };
|
|
1432
|
+
}
|
|
1433
|
+
let immagineGenerata;
|
|
1434
|
+
const prompt = String(argomenti?.prompt ?? '');
|
|
1435
|
+
try {
|
|
1436
|
+
immagineGenerata = await generaImmagineFn({
|
|
1437
|
+
prompt, shape: argomenti?.shape, modello: immagine.modello, nativo: immagine.nativo, chiave,
|
|
1438
|
+
});
|
|
1439
|
+
} catch (errore) {
|
|
1440
|
+
const dettaglio = errore instanceof Error ? errore.message : String(errore);
|
|
1441
|
+
return { ok: false, esito: `The image was not generated: ${dettaglio}` };
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
const estensione = ESTENSIONE_PER_MEDIA_TYPE[immagineGenerata.mediaType] ?? 'png';
|
|
1445
|
+
let persistito = null;
|
|
1446
|
+
if (typeof persistGeneratedImageFn === 'function') {
|
|
1447
|
+
try {
|
|
1448
|
+
persistito = await persistGeneratedImageFn({
|
|
1449
|
+
bytes: immagineGenerata.bytes,
|
|
1450
|
+
mimeType: immagineGenerata.mediaType,
|
|
1451
|
+
source: `openrouter/${immagine.modello}`,
|
|
1452
|
+
promptHash: createHash('sha256').update(prompt).digest('hex'),
|
|
1453
|
+
});
|
|
1454
|
+
} catch (errore) {
|
|
1455
|
+
const dettaglio = errore instanceof Error ? errore.message : String(errore);
|
|
1456
|
+
return {
|
|
1457
|
+
ok: false,
|
|
1458
|
+
esito: `The image was generated but could not be saved safely: ${dettaglio}. Check local storage in Doctor before trying again.`,
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
let salvato;
|
|
1463
|
+
try {
|
|
1464
|
+
salvato = await creaFileWorkspaceFn({ cartella: cartellaPerCreare, nome: `${immagineGenerata.fileStem}.${estensione}`, bytes: immagineGenerata.bytes });
|
|
1465
|
+
} catch (errore) {
|
|
1466
|
+
if (persistito?.id && typeof removeGeneratedImageFn === 'function') await removeGeneratedImageFn(persistito.id).catch(() => {});
|
|
1467
|
+
const dettaglio = errore instanceof WorkspaceFileError ? errore.message : (errore instanceof Error ? errore.message : String(errore));
|
|
1468
|
+
return {
|
|
1469
|
+
ok: false,
|
|
1470
|
+
esito: `The image was generated but could not be saved to the workspace: ${dettaglio}. Do not silently retry with the same prompt — offer a different title, or ask.`,
|
|
1471
|
+
};
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
// ⛔ mai i byte grezzi dentro un evento SSE/JSON (non sono UTF-8 valido) — stessa disciplina già in uso per un documento binario in onDocumento.
|
|
1475
|
+
/*
|
|
1476
|
+
* ⛔⛔ 10/09, owner: «OGNI artefatto va salvato in libreria». Misurato in questo file prima di
|
|
1477
|
+
* toccarlo: `salvaVoceLibreriaFn` era chiamata DUE volte — per l'artefatto HTML e per il
|
|
1478
|
+
* documento — e l'immagine generata non era nessuna delle due. Finiva solo nel workspace, e chi
|
|
1479
|
+
* riapriva la sessione domani non la ritrovava fra le cose prodotte.
|
|
1480
|
+
* ⛔ Come per l'artefatto e il documento, questa scrittura NON blocca e non fa fallire il giro: se
|
|
1481
|
+
* la Libreria non è scrivibile l'immagine resta comunque nel workspace, dove il modello l'ha
|
|
1482
|
+
* messa. Meglio un'immagine senza copia che un giro rotto per una copia.
|
|
1483
|
+
* ⛔ I byte viaggiano in base64 (`salvaVoce` lo prevede): un PNG dentro un campo di testo non è
|
|
1484
|
+
* UTF-8 valido e si corromperebbe — lo stesso motivo per cui il documento binario fa così.
|
|
1485
|
+
*/
|
|
1486
|
+
try {
|
|
1487
|
+
await salvaVoceLibreriaFn({
|
|
1488
|
+
cartella,
|
|
1489
|
+
nome: `${immagineGenerata.fileStem}.${estensione}`,
|
|
1490
|
+
mediaType: immagineGenerata.mediaType,
|
|
1491
|
+
origine: 'generated',
|
|
1492
|
+
/* ⭐ BC-38: qui l'autore è il modello di IMMAGINE, non quello della conversazione — sono
|
|
1493
|
+
due cose diverse (`immagine.modello`, riga 1303) e scriverne uno per l'altro sarebbe una
|
|
1494
|
+
provenienza falsa. Ripiego sul modello della sessione solo se il primo manca. */
|
|
1495
|
+
modello: immagine?.modello || modello,
|
|
1496
|
+
base64: Buffer.from(immagineGenerata.bytes).toString('base64'),
|
|
1497
|
+
});
|
|
1498
|
+
} catch (errore) {
|
|
1499
|
+
console.error('[immagini] copia in Libreria non riuscita:', errore instanceof Error ? errore.message : errore);
|
|
1500
|
+
}
|
|
1501
|
+
/*
|
|
1502
|
+
* ⛔ PO-05, stessa cura del documento: `[image image/png, 51234 bytes]` è una riga onesta e
|
|
1503
|
+
* inservibile. Con l'allegato accanto, la chat costruisce la scheda con nome, formato,
|
|
1504
|
+
* dimensione misurata e il collegamento che scarica i byte veri.
|
|
1505
|
+
*/
|
|
1506
|
+
onEvento(eventoPerScrittura({
|
|
1507
|
+
percorso: percorsoNellAlbero(salvato.percorso),
|
|
1508
|
+
contenuto: `[image ${immagineGenerata.mediaType}, ${immagineGenerata.bytes.byteLength} bytes]`,
|
|
1509
|
+
esisteva: false,
|
|
1510
|
+
allegato: { nome: `${immagineGenerata.fileStem}.${estensione}`, formato: estensione, byte: immagineGenerata.bytes.byteLength },
|
|
1511
|
+
}));
|
|
1512
|
+
|
|
1513
|
+
const dimensioneKb = Math.max(1, Math.round(immagineGenerata.bytes.byteLength / 1024));
|
|
1514
|
+
return {
|
|
1515
|
+
ok: true,
|
|
1516
|
+
esito: `Generated and saved "${salvato.percorso}" (${dimensioneKb} KB) with ${immagine.modello}.`,
|
|
1517
|
+
};
|
|
1518
|
+
};
|
|
1519
|
+
|
|
1520
|
+
/*
|
|
1521
|
+
* ⭐⭐⭐ 29/8 — FASE N (Libreria), prima fetta. `argomenti` è quello che
|
|
1522
|
+
* il modello ha scritto (i nomi di campo inglesi dello schema JSON —
|
|
1523
|
+
* `origin`/`file_type`/`page_size`/`page_token`, ecc., mai tradotti
|
|
1524
|
+
* qui): la traduzione verso i nomi italiani di library-store.mjs vive
|
|
1525
|
+
* SOLO in questi quattro punti, che il kernel non vede.
|
|
1526
|
+
*
|
|
1527
|
+
* `cursoriLibreria` — una Map per QUESTO run (stessa vita di
|
|
1528
|
+
* `libraryListCursors` mobile, un'istanza per chat): un `page_token`
|
|
1529
|
+
* sopravvive fra i giri dello STESSO run, non fra due run diversi —
|
|
1530
|
+
* un resume/follow-up ricostruisce agent-service da capo, quindi un
|
|
1531
|
+
* token vecchio torna onestamente CURSOR_INVALID (il contratto del
|
|
1532
|
+
* tool lo prevede già, mai un crash).
|
|
1533
|
+
*/
|
|
1534
|
+
const cursoriLibreria = creaCursoriLibreria();
|
|
1535
|
+
const onLibreriaLista = async (argomenti) => {
|
|
1536
|
+
const voci = await elencaVociFn({ cartella });
|
|
1537
|
+
return impaginaVociLibreria(voci, {
|
|
1538
|
+
origine: argomenti?.origin ?? 'all',
|
|
1539
|
+
fileType: argomenti?.file_type ?? 'all',
|
|
1540
|
+
pageSize: argomenti?.page_size ?? 10,
|
|
1541
|
+
pageToken: argomenti?.page_token,
|
|
1542
|
+
}, cursoriLibreria);
|
|
1543
|
+
};
|
|
1544
|
+
const onLibreriaCerca = async (argomenti) => {
|
|
1545
|
+
const voci = await elencaVociConTestoFn({ cartella });
|
|
1546
|
+
return cercaVociLibreria(voci, { query: argomenti?.query ?? '', limit: argomenti?.limit ?? 5, offset: argomenti?.offset ?? 0 });
|
|
1547
|
+
};
|
|
1548
|
+
const onLibreriaLeggi = async (argomenti) => leggiVoceFn({ cartella, id: argomenti?.id ?? '' });
|
|
1549
|
+
const onLibreriaOrigine = async (argomenti) => origineVoceFn({ cartella, id: argomenti?.id ?? '' });
|
|
1550
|
+
|
|
1551
|
+
/*
|
|
1552
|
+
* ⭐⭐⭐ 29/8 — FASE N, seconda fetta (mutazioni). Stesso contratto
|
|
1553
|
+
* ESATTO di onDocumento/onImmagine sopra: `(spec) => {ok, esito}` —
|
|
1554
|
+
* il kernel passa `argomenti` (i nomi di campo inglesi dello schema
|
|
1555
|
+
* JSON: id/name/reference) verbatim; questi tre scrivono il
|
|
1556
|
+
* messaggio finale PER INTERO (a differenza dei 4 callback di
|
|
1557
|
+
* lettura sopra, che tornano dati grezzi per un formattatore del
|
|
1558
|
+
* kernel — qui l'esito è un singolo messaggio, non una pagina).
|
|
1559
|
+
*/
|
|
1560
|
+
const onLibreriaRinomina = async (argomenti) => {
|
|
1561
|
+
const risultato = await rinominaVoceFn({ cartella, id: argomenti?.id ?? '', nome: argomenti?.name ?? '' });
|
|
1562
|
+
if (!risultato) {
|
|
1563
|
+
return { ok: false, esito: `No Library file has the id "${argomenti?.id}". Use library_list or library_search to find it.` };
|
|
1564
|
+
}
|
|
1565
|
+
return { ok: true, esito: `Renamed «${risultato.nomePrima}» to «${risultato.nomeDopo}».` };
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
const onLibreriaElimina = async (argomenti) => {
|
|
1569
|
+
/*
|
|
1570
|
+
* ⭐ (16/09/2026) — via a LOTTI (richiesta owner): `ids` array non vuoto ⇒ si elaborano SOLO
|
|
1571
|
+
* gli `ids` — `id` NON si processa a parte (ramo nuovo, non una fusione; lo schema chiede al
|
|
1572
|
+
* modello di ripetere in `id` il primo degli `ids` per compatibilità col campo richiesto).
|
|
1573
|
+
* Un id già sparito NON è un guasto: finisce nell'elenco degli assenti, l'esito resta onesto.
|
|
1574
|
+
* In ogni altro caso (`ids` assente o array vuoto) il flusso singolo resta IDENTICO a prima.
|
|
1575
|
+
*/
|
|
1576
|
+
if (Array.isArray(argomenti?.ids) && argomenti.ids.length > 0) {
|
|
1577
|
+
const { eliminate, assenti } = await eliminaVociFn({ cartella, ids: argomenti.ids });
|
|
1578
|
+
const parti = [];
|
|
1579
|
+
if (eliminate.length > 0) {
|
|
1580
|
+
parti.push(`Removed ${eliminate.length} ${eliminate.length === 1 ? 'file' : 'files'} from the Library: `
|
|
1581
|
+
+ eliminate.map((voce) => `«${voce.nome}»`).join(', ') + '.');
|
|
1582
|
+
}
|
|
1583
|
+
if (assenti.length > 0) parti.push(`Not found in the Library (already gone?): ${assenti.join(', ')}.`);
|
|
1584
|
+
return { ok: true, esito: parti.join(' ') || 'Nothing was deleted.' };
|
|
1585
|
+
}
|
|
1586
|
+
const risultato = await eliminaVoceFn({ cartella, id: argomenti?.id ?? '' });
|
|
1587
|
+
if (!risultato) {
|
|
1588
|
+
return { ok: false, esito: `No Library file has the id "${argomenti?.id}". It may already be gone.` };
|
|
1589
|
+
}
|
|
1590
|
+
return { ok: true, esito: `«${risultato.nome}» has been removed from the Library.` };
|
|
1591
|
+
};
|
|
1592
|
+
|
|
1593
|
+
/*
|
|
1594
|
+
* ⭐⭐⭐ 29/8 — FASE N, library_export. A differenza di onDocumento/
|
|
1595
|
+
* onImmagine (generano contenuto NUOVO): qui il contenuto esiste già
|
|
1596
|
+
* nella Libreria — "esportare" sul desktop vuol dire materializzarlo
|
|
1597
|
+
* come file vero DENTRO il workspace stesso, riusando
|
|
1598
|
+
* `creaFileWorkspaceFn` (già iniettato per onDocumento/onImmagine,
|
|
1599
|
+
* nessuna funzione nuova). Nessun picker di sistema: quel confine
|
|
1600
|
+
* (Libreria privata → storage condiviso) esiste solo su Android —
|
|
1601
|
+
* vedi la doc nel kernel. Un nome già occupato nel workspace
|
|
1602
|
+
* rifiuta onestamente (`creaFileWorkspaceFn` lancia `FILE_EXISTS`),
|
|
1603
|
+
* mai una sovrascrittura silenziosa.
|
|
1604
|
+
*/
|
|
1605
|
+
/**
|
|
1606
|
+
* L'anteprima che finisce nell'evento a schermo, decisa dal MEDIA TYPE e non dal caso: il testo si legge, il
|
|
1607
|
+
* binario si DICHIARA. ⛔ Mai un `toString('utf8')` alla cieca sui byte di uno zip o di un PDF — a schermo
|
|
1608
|
+
* arriverebbero righe di caratteri sostituti, cioè un contenuto che il file non ha. Il confine testo/binario è
|
|
1609
|
+
* quello già in uso in `context-asset-adapter.mjs` (`text/*` più `application/json`), esteso ai suffissi
|
|
1610
|
+
* `+json`/`+xml` che l'ecosistema usa per i formati derivati: non un criterio nuovo inventato qui.
|
|
1611
|
+
*/
|
|
1612
|
+
function anteprimaDiUnFileEsportato({ mediaType, bytes }) {
|
|
1613
|
+
const tipo = String(mediaType || 'application/octet-stream');
|
|
1614
|
+
const testuale = tipo.startsWith('text/') || tipo === 'application/json' || tipo === 'application/xml'
|
|
1615
|
+
|| tipo.endsWith('+json') || tipo.endsWith('+xml');
|
|
1616
|
+
return testuale ? bytes.toString('utf8') : `[${tipo}, ${bytes.byteLength} bytes]`;
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
const onLibreriaEsporta = async (argomenti) => {
|
|
1620
|
+
const voci = await elencaVociFn({ cartella });
|
|
1621
|
+
const trovata = trovaVoceLibreria(voci, argomenti?.reference ?? '');
|
|
1622
|
+
if (!trovata) {
|
|
1623
|
+
return { ok: false, esito: `No available Library file exactly matches "${argomenti?.reference}". Ask for the exact filename or Library id.` };
|
|
1624
|
+
}
|
|
1625
|
+
if (trovata.ambiguo) {
|
|
1626
|
+
return { ok: false, esito: `More than one Library file is named "${argomenti?.reference}". Ask the user which one; do not choose for them.` };
|
|
1627
|
+
}
|
|
1628
|
+
/*
|
|
1629
|
+
* ⛔⛔⛔ 14/09 (F03 della review, riprodotto): qui si leggeva con `leggiVoceFn`, cioè la porta del MODELLO, che
|
|
1630
|
+
* decodifica in utf8 tutto ciò che non è un'immagine. Un `.docx` (che è uno zip), un `.pdf`, un `.xlsx`
|
|
1631
|
+
* passavano da lì e ogni byte non valido diventava U+FFFD, IRREVERSIBILMENTE: l'esportazione consegnava un
|
|
1632
|
+
* file rotto dichiarando «Exported», con un conteggio di byte che non era nemmeno quello del file vero.
|
|
1633
|
+
* La porta giusta esiste da sempre ed è la stessa dello scarico dalla UI — `leggiBytesVoce`.
|
|
1634
|
+
* ⛔ E il suo rifiuto si dice: il tetto dei 64 MB e la scheda-senza-file LANCIANO. Un attrezzo del modello non
|
|
1635
|
+
* deve esplodere per questo, ma nemmeno tacere: torna `ok:false` col motivo, e nessun file scritto.
|
|
1636
|
+
*/
|
|
1637
|
+
let letta;
|
|
1638
|
+
try {
|
|
1639
|
+
letta = await leggiBytesVoceFn({ cartella, id: trovata.id });
|
|
1640
|
+
} catch (errore) {
|
|
1641
|
+
const dettaglio = errore instanceof Error ? errore.message : String(errore);
|
|
1642
|
+
return { ok: false, esito: `"${trovata.nome}" could not be read from the Library: ${dettaglio}. No copy was saved.` };
|
|
1643
|
+
}
|
|
1644
|
+
if (!letta) {
|
|
1645
|
+
return { ok: false, esito: `"${trovata.nome}" is no longer available. No copy was saved.` };
|
|
1646
|
+
}
|
|
1647
|
+
const { bytes } = letta;
|
|
1648
|
+
let salvato;
|
|
1649
|
+
try {
|
|
1650
|
+
salvato = await creaFileWorkspaceFn({ cartella: cartellaPerCreare, nome: letta.nome, bytes });
|
|
1651
|
+
} catch (errore) {
|
|
1652
|
+
const dettaglio = errore instanceof WorkspaceFileError ? errore.message : (errore instanceof Error ? errore.message : String(errore));
|
|
1653
|
+
return { ok: false, esito: `"${letta.nome}" could not be saved into the workspace: ${dettaglio}. Do not silently retry with the same name — offer a different name, or ask.` };
|
|
1654
|
+
}
|
|
1655
|
+
// ⛔ mai i byte grezzi dentro un evento SSE/JSON — stessa disciplina già in uso per un documento/immagine binari in onDocumento/onImmagine.
|
|
1656
|
+
onEvento(eventoPerScrittura({ percorso: percorsoNellAlbero(salvato.percorso), contenuto: anteprimaDiUnFileEsportato(letta), esisteva: false }));
|
|
1657
|
+
return { ok: true, esito: `Exported "${letta.nome}" into the workspace (${bytes.byteLength} bytes).` };
|
|
1658
|
+
};
|
|
1659
|
+
|
|
1660
|
+
/*
|
|
1661
|
+
* ⭐⭐⭐ 29/8 — FASE N, terza fetta (library_context_policy_update).
|
|
1662
|
+
* `ricevutePolitica` — una Map per QUESTO run (stessa vita di
|
|
1663
|
+
* `cursoriLibreria` sopra): un `receipt_id` non sopravvive a un
|
|
1664
|
+
* resume — `undo` con un id di un run precedente torna onestamente
|
|
1665
|
+
* "non trovato" (mobile stesso lo prevede come esito normale).
|
|
1666
|
+
*
|
|
1667
|
+
* La validazione per-azione ("mode è richiesto solo per set_mode",
|
|
1668
|
+
* porto di `libraryContextPolicyTools.ts`, il `.superRefine` Zod
|
|
1669
|
+
* vive QUI perché il kernel passa `argomenti` verbatim, senza
|
|
1670
|
+
* saperne il significato) vive PRIMA della lettura della politica:
|
|
1671
|
+
* un input incompleto non deve costare un giro di I/O.
|
|
1672
|
+
*/
|
|
1673
|
+
const ricevutePolitica = creaRicevutePoliticaLibreria();
|
|
1674
|
+
const onLibreriaPolitica = async (argomenti) => {
|
|
1675
|
+
const azione = argomenti?.action;
|
|
1676
|
+
const campoMancante = (campo) => ({ ok: false, esito: `${campo} is required when action is ${azione}. Nothing was changed.` });
|
|
1677
|
+
if (azione === 'set_mode' && typeof argomenti?.mode !== 'string') return campoMancante('mode');
|
|
1678
|
+
if (azione === 'set_enabled' && typeof argomenti?.enabled !== 'boolean') return campoMancante('enabled');
|
|
1679
|
+
if ((azione === 'include_files' || azione === 'exclude_files') && !Array.isArray(argomenti?.file_ids)) return campoMancante('file_ids');
|
|
1680
|
+
if (azione === 'undo' && typeof argomenti?.receipt_id !== 'string') return campoMancante('receipt_id');
|
|
1681
|
+
|
|
1682
|
+
let attuale;
|
|
1683
|
+
try {
|
|
1684
|
+
attuale = await leggiPoliticaFn({ cartella });
|
|
1685
|
+
} catch (errore) {
|
|
1686
|
+
return { ok: false, esito: `The current Library policy could not be read. Nothing was changed. (${errore instanceof Error ? errore.message : String(errore)})` };
|
|
1687
|
+
}
|
|
1688
|
+
if (attuale.revision !== argomenti?.expected_revision) {
|
|
1689
|
+
return { ok: false, esito: `Library policy changed before this update (expected revision ${argomenti?.expected_revision}, current revision ${attuale.revision}). Read the current policy and ask again; nothing was changed.` };
|
|
1690
|
+
}
|
|
1691
|
+
|
|
1692
|
+
if (azione === 'undo') {
|
|
1693
|
+
/*
|
|
1694
|
+
* ⭐⭐⭐ 29/8 — porto diretto di `receiptLookupIds` mobile
|
|
1695
|
+
* (`libraryContextPolicyTools.ts` righe 268-274): il messaggio
|
|
1696
|
+
* di successo finisce la frase con un punto subito dopo l'id
|
|
1697
|
+
* ("Undo receipt: libpol-xxx."), e un modello che lo rilegge e
|
|
1698
|
+
* lo cita può includere quel punto per errore — provato dal
|
|
1699
|
+
* vivo dalla mia stessa suite di test, non solo temuto: un
|
|
1700
|
+
* `\S+` naive nel MIO test ha catturato il punto, riproducendo
|
|
1701
|
+
* esattamente lo scenario che mobile aveva già previsto.
|
|
1702
|
+
*/
|
|
1703
|
+
const idEsatto = String(argomenti.receipt_id).trim();
|
|
1704
|
+
const idSenzaPunteggiatura = idEsatto.replace(/[.,;:!?]+$/u, '');
|
|
1705
|
+
const ricevuta = ricevutePolitica.get(idEsatto) ?? (idSenzaPunteggiatura !== idEsatto ? ricevutePolitica.get(idSenzaPunteggiatura) : undefined);
|
|
1706
|
+
if (!ricevuta || ricevuta.revisioneDopo !== attuale.revision) {
|
|
1707
|
+
return { ok: false, esito: 'That Library policy undo receipt is missing, expired, already used, or belongs to another scope. Nothing was changed.' };
|
|
1708
|
+
}
|
|
1709
|
+
let ripristinata;
|
|
1710
|
+
try {
|
|
1711
|
+
ripristinata = await scriviPoliticaFn({ cartella, valore: ricevuta.prima, revisioneAttesa: attuale.revision });
|
|
1712
|
+
} catch (errore) {
|
|
1713
|
+
return { ok: false, esito: `The Library policy could not be restored: ${errore instanceof Error ? errore.message : String(errore)}` };
|
|
1714
|
+
}
|
|
1715
|
+
ricevutePolitica.delete(ricevuta.receiptId);
|
|
1716
|
+
return { ok: true, esito: `Restored the previous Library policy at revision ${ripristinata.revision}.` };
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
let prossima = { enabled: attuale.enabled, mode: attuale.mode, includedFileIds: attuale.includedFileIds, excludedFileIds: attuale.excludedFileIds };
|
|
1720
|
+
if (azione === 'set_mode') {
|
|
1721
|
+
if (!MODALITA_SUPPORTATE_LIBRERIA.includes(argomenti.mode)) {
|
|
1722
|
+
return { ok: false, esito: `This harness only supports the "${MODALITA_SUPPORTATE_LIBRERIA[0]}" mode today; "${argomenti.mode}" is not implemented. Nothing was changed.` };
|
|
1723
|
+
}
|
|
1724
|
+
prossima.mode = argomenti.mode;
|
|
1725
|
+
} else if (azione === 'set_enabled') {
|
|
1726
|
+
prossima.enabled = argomenti.enabled;
|
|
1727
|
+
} else if (azione === 'include_files') {
|
|
1728
|
+
const aggiunte = new Set(argomenti.file_ids);
|
|
1729
|
+
prossima.includedFileIds = [...new Set([...attuale.includedFileIds, ...argomenti.file_ids])];
|
|
1730
|
+
prossima.excludedFileIds = attuale.excludedFileIds.filter((id) => !aggiunte.has(id));
|
|
1731
|
+
} else if (azione === 'exclude_files') {
|
|
1732
|
+
const aggiunte = new Set(argomenti.file_ids);
|
|
1733
|
+
prossima.excludedFileIds = [...new Set([...attuale.excludedFileIds, ...argomenti.file_ids])];
|
|
1734
|
+
prossima.includedFileIds = attuale.includedFileIds.filter((id) => !aggiunte.has(id));
|
|
1735
|
+
} else if (azione === 'clear_overrides') {
|
|
1736
|
+
prossima = { enabled: true, mode: MODALITA_SUPPORTATE_LIBRERIA[0], includedFileIds: [], excludedFileIds: [] };
|
|
1737
|
+
} else {
|
|
1738
|
+
return { ok: false, esito: `Unknown action "${azione}". Nothing was changed.` };
|
|
1739
|
+
}
|
|
1740
|
+
|
|
1741
|
+
let aggiornata;
|
|
1742
|
+
try {
|
|
1743
|
+
aggiornata = await scriviPoliticaFn({ cartella, valore: prossima, revisioneAttesa: attuale.revision });
|
|
1744
|
+
} catch (errore) {
|
|
1745
|
+
return { ok: false, esito: `The Library policy could not be updated: ${errore instanceof Error ? errore.message : String(errore)}` };
|
|
1746
|
+
}
|
|
1747
|
+
const ricevuta = creaRicevutaPoliticaLibreria({ azione, prima: attuale, revisionePrima: attuale.revision, revisioneDopo: aggiornata.revision });
|
|
1748
|
+
ricordaRicevutaPoliticaLibreria(ricevutePolitica, ricevuta);
|
|
1749
|
+
return { ok: true, esito: `Updated the Library policy to revision ${aggiornata.revision}. Undo receipt: ${ricevuta.receiptId}.` };
|
|
1750
|
+
};
|
|
1751
|
+
|
|
1752
|
+
/*
|
|
1753
|
+
* ⭐⭐⭐ FASE N, quarto sistema (30/8) — Notes. `cartellaNote` è GLOBALE
|
|
1754
|
+
* (non `cartella`, il workspace di questa sessione — vedi la doc in
|
|
1755
|
+
* notes-store.mjs) — stesso contratto "questo file non sa DOVE/COME"
|
|
1756
|
+
* dei callback Libreria sopra: `argomenti` verbatim, la traduzione
|
|
1757
|
+
* verso i nomi italiani dello store vive SOLO qui.
|
|
1758
|
+
*/
|
|
1759
|
+
const onNoteLista = async (argomenti) => {
|
|
1760
|
+
const note = await elencaNoteFn({ cartella: cartellaNote });
|
|
1761
|
+
/*
|
|
1762
|
+
* ⛔ NON `Number(argomenti?.limit) || 20` — trovato da un test
|
|
1763
|
+
* proprio, non ipotizzato: `||` tratta `0` come assente e lo
|
|
1764
|
+
* riporterebbe al default 20 invece che al minimo 1. `limit:0` è
|
|
1765
|
+
* un valore VALIDO (fuori range, va clampato), non un'assenza.
|
|
1766
|
+
*/
|
|
1767
|
+
const richiesto = Number(argomenti?.limit);
|
|
1768
|
+
const limite = Number.isFinite(richiesto) ? Math.min(Math.max(richiesto, 1), 50) : 20;
|
|
1769
|
+
return { note: note.slice(0, limite), totale: note.length };
|
|
1770
|
+
};
|
|
1771
|
+
|
|
1772
|
+
const onNoteCrea = async (argomenti) => {
|
|
1773
|
+
try {
|
|
1774
|
+
const creata = await creaNotaFn({ cartella: cartellaNote, title: argomenti?.title, content: argomenti?.content });
|
|
1775
|
+
return { ok: true, esito: `Saved the note «${creata.titolo}» (id ${creata.id}).` };
|
|
1776
|
+
} catch (errore) {
|
|
1777
|
+
return { ok: false, esito: errore instanceof Error ? errore.message : String(errore) };
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
|
|
1781
|
+
const onNoteAggiorna = async (argomenti) => {
|
|
1782
|
+
// ⛔ Rifiutato QUI, non nello store: una patch vuota non è un errore di validazione del campo, è "niente da fare" — stesso principio del tool mobile (verbatim, notesWriteTools.ts).
|
|
1783
|
+
if (argomenti?.title === undefined && argomenti?.content === undefined) {
|
|
1784
|
+
return { ok: false, esito: 'Nothing to change: pass a new title, a new body, or both.' };
|
|
1785
|
+
}
|
|
1786
|
+
try {
|
|
1787
|
+
const aggiornata = await aggiornaNotaFn({ cartella: cartellaNote, id: argomenti?.id, title: argomenti?.title, content: argomenti?.content });
|
|
1788
|
+
return { ok: true, esito: `Updated the note «${aggiornata.titolo}».` };
|
|
1789
|
+
} catch (errore) {
|
|
1790
|
+
if (errore?.code === 'NOTE_NOT_FOUND') {
|
|
1791
|
+
return { ok: false, esito: 'There is no note with that id. Call notes_list to see the current ones.' };
|
|
1792
|
+
}
|
|
1793
|
+
return { ok: false, esito: errore instanceof Error ? errore.message : String(errore) };
|
|
1794
|
+
}
|
|
1795
|
+
};
|
|
1796
|
+
|
|
1797
|
+
const onNoteElimina = async (argomenti) => {
|
|
1798
|
+
const id = argomenti?.id ?? '';
|
|
1799
|
+
// ⛔ Verificato PRIMA di cancellare (eliminaNotaFn è idempotente, mai un throw su un id già assente): stesso messaggio a due facce del tool mobile — "cancellata ora" contro "già assente, niente da fare".
|
|
1800
|
+
const esisteva = await leggiNotaFn({ cartella: cartellaNote, id });
|
|
1801
|
+
await eliminaNotaFn({ cartella: cartellaNote, id });
|
|
1802
|
+
return esisteva
|
|
1803
|
+
? { ok: true, esito: 'That note has been deleted.' }
|
|
1804
|
+
: { ok: true, esito: 'There was no note with that id — nothing to delete.' };
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
/*
|
|
1808
|
+
* ⭐⭐⭐ FASE N, quinto sistema (30/8) — Tasks. `cartellaAttivita` è
|
|
1809
|
+
* GLOBALE (stesso principio di cartellaNote sopra). Stesso contratto
|
|
1810
|
+
* "questo file non sa DOVE/COME" dei callback Notes.
|
|
1811
|
+
*/
|
|
1812
|
+
const onAttivitaLista = async (argomenti) => {
|
|
1813
|
+
const tutte = await elencaAttivitaFn({ cartella: cartellaAttivita });
|
|
1814
|
+
const stato = argomenti?.status ?? 'all';
|
|
1815
|
+
const filtrate = stato === 'all' ? tutte : tutte.filter((a) => (stato === 'done' ? a.stato === 'done' : a.stato !== 'done'));
|
|
1816
|
+
const richiesto = Number(argomenti?.limit);
|
|
1817
|
+
const limite = Number.isFinite(richiesto) ? Math.min(Math.max(richiesto, 1), 50) : 20;
|
|
1818
|
+
return { attivita: filtrate.slice(0, limite), totale: filtrate.length };
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
const onAttivitaCrea = async (argomenti) => {
|
|
1822
|
+
try {
|
|
1823
|
+
const creata = await creaAttivitaFn({ cartella: cartellaAttivita, title: argomenti?.title, description: argomenti?.description, priority: argomenti?.priority ?? 'normal' });
|
|
1824
|
+
return { ok: true, esito: `Added the task «${creata.titolo}» (id ${creata.id}).` };
|
|
1825
|
+
} catch (errore) {
|
|
1826
|
+
return { ok: false, esito: errore instanceof Error ? errore.message : String(errore) };
|
|
1827
|
+
}
|
|
1828
|
+
};
|
|
1829
|
+
|
|
1830
|
+
const onAttivitaCompleta = async (argomenti) => {
|
|
1831
|
+
try {
|
|
1832
|
+
const stato = argomenti?.status ?? 'done';
|
|
1833
|
+
const aggiornata = await completaAttivitaFn({ cartella: cartellaAttivita, id: argomenti?.id, status: stato });
|
|
1834
|
+
return { ok: true, esito: stato === 'done' ? `Marked «${aggiornata.titolo}» as done.` : `Moved «${aggiornata.titolo}» to ${stato}.` };
|
|
1835
|
+
} catch (errore) {
|
|
1836
|
+
if (errore?.code === 'TASK_NOT_FOUND') {
|
|
1837
|
+
return { ok: false, esito: 'There is no task with that id. Call tasks_list to see the current ones.' };
|
|
1838
|
+
}
|
|
1839
|
+
return { ok: false, esito: errore instanceof Error ? errore.message : String(errore) };
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
const onAttivitaAggiorna = async (argomenti) => {
|
|
1844
|
+
// ⛔ Rifiutato QUI, non nello store: una patch vuota non è un errore di validazione del campo, è "niente da fare" — stesso principio del tool mobile. Per marcare fatto/iniziato c'è tasks_complete, mai qui.
|
|
1845
|
+
if (argomenti?.title === undefined && argomenti?.description === undefined && argomenti?.priority === undefined) {
|
|
1846
|
+
return { ok: false, esito: 'Nothing to change: send at least a title, a description or a priority. To mark a task done, use tasks_complete.' };
|
|
1847
|
+
}
|
|
1848
|
+
try {
|
|
1849
|
+
const aggiornata = await aggiornaAttivitaFn({ cartella: cartellaAttivita, id: argomenti?.id, title: argomenti?.title, description: argomenti?.description, priority: argomenti?.priority });
|
|
1850
|
+
return { ok: true, esito: `Updated the task «${aggiornata.titolo}».` };
|
|
1851
|
+
} catch (errore) {
|
|
1852
|
+
if (errore?.code === 'TASK_NOT_FOUND') {
|
|
1853
|
+
return { ok: false, esito: 'There is no task with that id. Call tasks_list to see the current ones.' };
|
|
1854
|
+
}
|
|
1855
|
+
return { ok: false, esito: errore instanceof Error ? errore.message : String(errore) };
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
const onAttivitaElimina = async (argomenti) => {
|
|
1860
|
+
const id = argomenti?.id ?? '';
|
|
1861
|
+
// ⛔ Stesso principio di onNoteElimina: verificato PRIMA di cancellare, per distinguere "cancellata ora" da "già assente".
|
|
1862
|
+
const esisteva = await leggiAttivitaFn({ cartella: cartellaAttivita, id });
|
|
1863
|
+
await eliminaAttivitaFn({ cartella: cartellaAttivita, id });
|
|
1864
|
+
return esisteva
|
|
1865
|
+
? { ok: true, esito: 'That task has been deleted.' }
|
|
1866
|
+
: { ok: true, esito: 'There was no task with that id — nothing to delete.' };
|
|
1867
|
+
};
|
|
1868
|
+
|
|
1869
|
+
/*
|
|
1870
|
+
* ⭐⭐⭐ FASE N, sesto sistema (30/8) — Memory. `cartellaMemoria` è
|
|
1871
|
+
* GLOBALE (stesso principio di cartellaNote/cartellaAttivita sopra).
|
|
1872
|
+
*/
|
|
1873
|
+
const onMemoriaCerca = async (argomenti) => {
|
|
1874
|
+
const tutte = await elencaMemorieFn({ cartella: cartellaMemoria });
|
|
1875
|
+
const richiesto = Number(argomenti?.limit);
|
|
1876
|
+
const limite = Number.isFinite(richiesto) ? Math.min(Math.max(richiesto, 1), 20) : 5;
|
|
1877
|
+
return cercaMemorie(tutte, { query: argomenti?.query ?? '', limit: limite });
|
|
1878
|
+
};
|
|
1879
|
+
|
|
1880
|
+
const onMemoriaScrivi = async (argomenti) => {
|
|
1881
|
+
try {
|
|
1882
|
+
const { voce, duplicato } = await creaMemoriaFn({ cartella: cartellaMemoria, title: argomenti?.title, content: argomenti?.content, kind: argomenti?.kind ?? 'preference' });
|
|
1883
|
+
// ⛔ Porto diretto del ramo dedup mobile (memoryWriteTools.ts): un doppione NON è un fallimento, e' la postcondizione "c'e' gia' una memoria con questo titolo" gia' vera.
|
|
1884
|
+
return duplicato
|
|
1885
|
+
? { ok: true, esito: `Already remembered as «${voce.titolo}» (id ${voce.id}). Nothing new was written. Use memory_update if the fact has changed.` }
|
|
1886
|
+
: { ok: true, esito: `Remembered as «${voce.titolo}» (id ${voce.id}): ${voce.contenuto}` };
|
|
1887
|
+
} catch (errore) {
|
|
1888
|
+
return { ok: false, esito: errore instanceof Error ? errore.message : String(errore) };
|
|
1889
|
+
}
|
|
1890
|
+
};
|
|
1891
|
+
|
|
1892
|
+
const onMemoriaAggiorna = async (argomenti) => {
|
|
1893
|
+
if (argomenti?.title === undefined && argomenti?.content === undefined && argomenti?.kind === undefined) {
|
|
1894
|
+
return { ok: false, esito: 'Nothing to change: send at least one of title, content or kind. To remove the memory entirely, use memory_delete.' };
|
|
1895
|
+
}
|
|
1896
|
+
try {
|
|
1897
|
+
const aggiornata = await aggiornaMemoriaFn({ cartella: cartellaMemoria, id: argomenti?.id, title: argomenti?.title, content: argomenti?.content, kind: argomenti?.kind });
|
|
1898
|
+
return { ok: true, esito: `Memory «${aggiornata.titolo}» updated.` };
|
|
1899
|
+
} catch (errore) {
|
|
1900
|
+
if (errore?.code === 'MEMORY_NOT_FOUND') {
|
|
1901
|
+
return { ok: false, esito: `No memory has the id "${argomenti?.id}". Use memory_search to find the right one.` };
|
|
1902
|
+
}
|
|
1903
|
+
return { ok: false, esito: errore instanceof Error ? errore.message : String(errore) };
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
|
|
1907
|
+
const onMemoriaElimina = async (argomenti) => {
|
|
1908
|
+
const id = argomenti?.id ?? '';
|
|
1909
|
+
// ⛔ Stesso principio di onNoteElimina/onAttivitaElimina: verificato PRIMA di cancellare.
|
|
1910
|
+
const esisteva = await leggiMemoriaFn({ cartella: cartellaMemoria, id });
|
|
1911
|
+
await eliminaMemoriaFn({ cartella: cartellaMemoria, id });
|
|
1912
|
+
return esisteva
|
|
1913
|
+
? { ok: true, esito: 'That memory has been removed from this device.' }
|
|
1914
|
+
: { ok: true, esito: `No memory has the id "${id}". It may already be gone.` };
|
|
1915
|
+
};
|
|
1916
|
+
|
|
1917
|
+
try {
|
|
1918
|
+
const esito = await talosLavoraFn({
|
|
1919
|
+
cartella, task, modello, chiave, comandoProva, segnaleStop, messaggiIniziali, mobile,
|
|
1920
|
+
checkpointSessionId, checkpointOperation,
|
|
1921
|
+
// ⭐ P-13 — il kernel lo mette in testa al prompt, subito dopo le istruzioni e PRIMA della
|
|
1922
|
+
// consegna: un contenuto stabile messo DOPO uno variabile non viene mai riusato dalla cache.
|
|
1923
|
+
contestoDelProgetto: testoContestoProgetto,
|
|
1924
|
+
onGiro, onScrittura, onDelta, reasoning, contextHooks,
|
|
1925
|
+
fallbackProviders,
|
|
1926
|
+
onAvviso: async messaggio => {
|
|
1927
|
+
const messageId = randomUUID();
|
|
1928
|
+
await onEvento(textMessageStart({ messageId, role: 'assistant' }), { durable: true });
|
|
1929
|
+
await onEvento(textMessageContent({ messageId, delta: messaggio }), { durable: true });
|
|
1930
|
+
await onEvento(textMessageEnd({ messageId }), { durable: true });
|
|
1931
|
+
},
|
|
1932
|
+
onCambioFornitore: async evento => {
|
|
1933
|
+
if (typeof depositaCambioFornitore !== 'function') throw new Error('Il cambio non è collegato alla sessione.');
|
|
1934
|
+
for (const messageId of messaggiTestoPerGiro.values()) await onEvento(textMessageEnd({ messageId }), { durable: true });
|
|
1935
|
+
for (const messageId of messaggiRagionamentoPerGiro.values()) await onEvento(reasoningMessageEnd({ messageId }), { durable: true });
|
|
1936
|
+
for (const ids of toolCallIdStreamatiPerGiro.values()) for (const toolCallId of ids) await onEvento(eventoPerEsitoTool({ messageId: randomUUID(), toolCallId, content: 'Richiesta interrotta prima dell’esecuzione.' }), { durable: true });
|
|
1937
|
+
messaggiTestoPerGiro.clear(); messaggiRagionamentoPerGiro.clear(); toolCallIdStreamatiPerGiro.clear();
|
|
1938
|
+
const messageId = randomUUID();
|
|
1939
|
+
await onEvento(textMessageStart({ messageId, role: 'assistant' }), { durable: true });
|
|
1940
|
+
await onEvento(textMessageContent({ messageId, delta: evento.messaggio }), { durable: true });
|
|
1941
|
+
await onEvento(textMessageEnd({ messageId }), { durable: true });
|
|
1942
|
+
await onEvento({ type: 'CUSTOM', name: 'cambio-fornitore', value: evento }, { durable: true });
|
|
1943
|
+
await depositaCambioFornitore(evento);
|
|
1944
|
+
},
|
|
1945
|
+
onConsumoFornitore: async evento => onEvento({ type: 'CUSTOM', name: 'consumo-fornitore', value: evento }, { durable: true }),
|
|
1946
|
+
strumentiEstesi, ricercaWeb, richiediRicercaFn, onArtefatto, onDocumento, onImmagine, modelloPlanner,
|
|
1947
|
+
// ⭐ L9 (12/09/2026) — inoltrati SENZA logica propria, come tutto il resto in questo file:
|
|
1948
|
+
// la cache della corsa e la finestra della pagina le costruisce `research-orchestrator.mjs`
|
|
1949
|
+
// («raccolta viva»). Assenti ⇒ il kernel si comporta esattamente come ieri.
|
|
1950
|
+
cacheWeb, onPaginaLetta,
|
|
1951
|
+
livelloAccesso, chiediApprovazioneFn, hookFn: hookFnConPlugin, permessiPerAttrezzo, onDelega, codaMessaggiFn,
|
|
1952
|
+
firma, toolMcp, chiamaToolMcpFn, skillsDisponibili, caricaSkillFn, toolPlugin, eseguiToolPluginFn,
|
|
1953
|
+
onLibreriaLista, onLibreriaCerca, onLibreriaLeggi, onLibreriaOrigine,
|
|
1954
|
+
onLibreriaRinomina, onLibreriaElimina, onLibreriaEsporta, onLibreriaPolitica,
|
|
1955
|
+
onNoteLista, onNoteCrea, onNoteAggiorna, onNoteElimina,
|
|
1956
|
+
onAttivitaLista, onAttivitaCrea, onAttivitaCompleta, onAttivitaAggiorna, onAttivitaElimina,
|
|
1957
|
+
onMemoriaCerca, onMemoriaScrivi, onMemoriaAggiorna, onMemoriaElimina,
|
|
1958
|
+
onRicercaLista, onRicercaAvvia, onRicercaLeggi, onRicercaRinomina,
|
|
1959
|
+
onRicercaPausa, onRicercaRiprendi, onRicercaAnnulla, onRicercaElimina,
|
|
1960
|
+
componiRapportoRicercaFn,
|
|
1961
|
+
onForgeCrea, toolForge, eseguiToolForgeFn,
|
|
1962
|
+
});
|
|
1963
|
+
onEvento(esitoInEventoFinale({ threadId, runId, esito }));
|
|
1964
|
+
return { threadId, runId, ok: esito.comeFinita === 'concluso', esito, erroreInterno: null };
|
|
1965
|
+
} catch (errore) {
|
|
1966
|
+
/*
|
|
1967
|
+
* ⛔ talosLavora oggi non lancia per un esito del TASK (giri esauriti,
|
|
1968
|
+
* fermato, premesse negate sono tutti valori di ritorno, non eccezioni)
|
|
1969
|
+
* — un throw qui è un guasto del SERVIZIO (rete giù senza che
|
|
1970
|
+
* chiamaConRitenta l'abbia già assorbito, un bug), non del task. Va
|
|
1971
|
+
* dichiarato come tale: RunError con un code dedicato, mai confuso con
|
|
1972
|
+
* 'giri-esauriti'/'fermato'.
|
|
1973
|
+
*/
|
|
1974
|
+
const messaggio = errore instanceof Error ? errore.message : String(errore);
|
|
1975
|
+
/*
|
|
1976
|
+
* ⛔ 09/09 — il `code` era FISSO a 'internal-error', e con esso spariva l'unica cosa che la chat
|
|
1977
|
+
* poteva usare per riconoscere l'errore: una `ContextEngineError` arriva qui con il suo
|
|
1978
|
+
* `CTX_TRUNCATED_SUMMARY` / `CTX_INVALID_SOURCE` / `CTX_SUMMARY_RESPONSE_INVALID` addosso, e
|
|
1979
|
+
* questa riga lo buttava. Misurato in tre giri veri con glm-5.3-flash: il messaggio (italiano)
|
|
1980
|
+
* sopravviveva, il codice no, e la chat doveva indovinare dalla frase.
|
|
1981
|
+
* ⛔ Resta vero ciò che dice il commento sopra: un throw è un guasto del SERVIZIO. Per questo il
|
|
1982
|
+
* codice si prende solo se è una stringa in forma di codice (nessuno spazio, non vuota) e MAI
|
|
1983
|
+
* uno di quelli che descrivono un esito del task — un errore interno non deve poter fingersi
|
|
1984
|
+
* 'fermato' o 'giri-esauriti' passando per il `.code` di un'eccezione qualunque.
|
|
1985
|
+
*/
|
|
1986
|
+
const ESITI_DEL_TASK = new Set(['fermato', 'giri-esauriti', 'premesse-negate', 'concluso']);
|
|
1987
|
+
const codiceGrezzo = typeof errore?.code === 'string' ? errore.code.trim() : '';
|
|
1988
|
+
const code = codiceGrezzo && !/\s/.test(codiceGrezzo) && !ESITI_DEL_TASK.has(codiceGrezzo)
|
|
1989
|
+
? codiceGrezzo
|
|
1990
|
+
: 'internal-error';
|
|
1991
|
+
onEvento(runError({ message: messaggio, code }));
|
|
1992
|
+
/*
|
|
1993
|
+
* ⭐⭐⭐ BC-44 (12/09/2026) — IL CODICE VIAGGIAVA SOLO NELL'EVENTO, e chi conclude legge il
|
|
1994
|
+
* VALORE DI RITORNO. Questo ramo tornava `erroreInterno` (la frase) e buttava `code`: chi
|
|
1995
|
+
* riceve la conclusione (`onConclusioneFn` — la ricerca approfondita, la delega) vedeva un
|
|
1996
|
+
* guasto senza nome, e per distinguere «la rete è caduta» da «il codice ha un bug» doveva
|
|
1997
|
+
* andarsi a rileggere il `.jsonl` della sessione. Due file, nessun ponte: la ricerca
|
|
1998
|
+
* `dec896c0` è finita `failed` con la causa scritta solo nel registro degli eventi.
|
|
1999
|
+
* ⛔ ADDITIVO, e la riga sopra non cambia: l'evento `RunError` porta esattamente quello che
|
|
2000
|
+
* portava, stesso `message` e stesso `code`. Qui si aggiunge un campo al ritorno, e chi
|
|
2001
|
+
* non lo legge non se ne accorge.
|
|
2002
|
+
*/
|
|
2003
|
+
return { threadId, runId, ok: false, esito: null, erroreInterno: messaggio, codiceErrore: code };
|
|
2004
|
+
} finally {
|
|
2005
|
+
/*
|
|
2006
|
+
* ⭐⭐⭐ 29/8 — FASE E: un server MCP è un processo figlio VERO
|
|
2007
|
+
* (a differenza di ogni altra risorsa di questo file) — lasciarlo
|
|
2008
|
+
* aperto oltre la vita di QUESTO run sarebbe il primo leak di
|
|
2009
|
+
* processo mai introdotto in questo file. `chiudiMcp` è sempre
|
|
2010
|
+
* definita (no-op se `cartellaTrustMcp` era assente, sopra) — un
|
|
2011
|
+
* `finally` gira sia sul ritorno riuscito sia su quello d'errore,
|
|
2012
|
+
* mai un percorso che lascia una connessione aperta.
|
|
2013
|
+
*/
|
|
2014
|
+
await chiudiMcp();
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
|
|
2018
|
+
/**
|
|
2019
|
+
* "Compatta ora" (piano §1.4) — chiede al modello un riassunto della
|
|
2020
|
+
* conversazione FINALE di una sessione già conclusa. Riusa
|
|
2021
|
+
* `compattaConversazione` di talosHarness.mjs, la STESSA funzione che Stadio A
|
|
2022
|
+
* chiama dentro il ciclo di `talosLavora` (mai duplicata: un secondo
|
|
2023
|
+
* riassuntore divergerebbe in silenzio, stesso motivo per cui questo intero
|
|
2024
|
+
* file importa il kernel invece di copiarlo) — qui semplicemente invocata
|
|
2025
|
+
* FUORI dal ciclo, su richiesta esplicita invece che al checkpoint automatico.
|
|
2026
|
+
*
|
|
2027
|
+
* ⛔ Costa una vera chiamata al modello — dichiarato nella doc di
|
|
2028
|
+
* compattaConversazione stessa, non un'operazione gratuita solo perché è un
|
|
2029
|
+
* pulsante nella UI.
|
|
2030
|
+
*
|
|
2031
|
+
* ⛔ Non lancia mai: compattaConversazione stessa intercetta un fallimento di
|
|
2032
|
+
* chiamaModello (rete giù, 429 oltre i ritentativi) e torna
|
|
2033
|
+
* `compattato:false` invece di propagare — qui basta restituire quel valore,
|
|
2034
|
+
* nessun try/catch in più da aggiungere.
|
|
2035
|
+
*
|
|
2036
|
+
* @param {object} input
|
|
2037
|
+
* @param {Array<object>} input.messaggiFinali — la conversazione da compattare
|
|
2038
|
+
* @param {string} input.modello
|
|
2039
|
+
* @param {string} input.chiave
|
|
2040
|
+
* @param {typeof fetch} [input.fetchDiRete] — SOLO per test
|
|
2041
|
+
* @param {typeof compattaConversazioneReale} [input.compattaConversazioneFn] — SOLO per test
|
|
2042
|
+
* @returns {Promise<{compattato:boolean, messaggi:Array<object>, usage:object|null}>}
|
|
2043
|
+
*/
|
|
2044
|
+
export async function compattaSessione({
|
|
2045
|
+
messaggiFinali, modello, chiave, fetchDiRete = fetch,
|
|
2046
|
+
compattaConversazioneFn = compattaConversazioneReale,
|
|
2047
|
+
}) {
|
|
2048
|
+
const chiamaModello = (richiesta) => chiamaConRitenta({
|
|
2049
|
+
modello, chiave, messaggi: richiesta, attrezzi: [], fetchDiRete,
|
|
2050
|
+
});
|
|
2051
|
+
return compattaConversazioneFn(messaggiFinali, chiamaModello);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* ⭐⭐⭐⭐ L9 (12/09/2026) — UNA DOMANDA SOLA A UN MODELLO, senza attrezzi e senza giro.
|
|
2056
|
+
*
|
|
2057
|
+
* ⛔⛔ A che cosa serve, e perché non poteva vivere altrove: il GIUDICE della ricerca
|
|
2058
|
+
* approfondita. `verification.mjs` chiede una funzione `ask(affermazione, passaggio) =>
|
|
2059
|
+
* Promise<string>` e non sa niente di fornitori, chiavi o ritentativi — è puro apposta. Chi
|
|
2060
|
+
* quelle cose le sa è questo file, che le sa già per `compattaSessione` qui sopra.
|
|
2061
|
+
*
|
|
2062
|
+
* ⛔ Nessuna seconda implementazione del trasporto: `chiamaConRitenta` è LA STESSA funzione del
|
|
2063
|
+
* kernel che ogni giro usa — stesso backoff, stesso rispetto del segnale di stop, stesso
|
|
2064
|
+
* `fetchDiRete` iniettabile per provare senza rete. Scriverne una qui accanto vorrebbe dire
|
|
2065
|
+
* due politiche di ritentativo che divergono al primo 429.
|
|
2066
|
+
*
|
|
2067
|
+
* ⛔ `attrezzi: []` non è un dettaglio: un giudice che potesse chiamare attrezzi potrebbe
|
|
2068
|
+
* andare a CERCARE conferme, e il suo compito è l'opposto — dire se quel passaggio, DA SOLO,
|
|
2069
|
+
* sostiene l'affermazione. È la riga portante di `talosResearchJudgePrompt` («non usare altro:
|
|
2070
|
+
* né quello che sai, né quello che ti sembra probabile»), e qui è resa impossibile da violare.
|
|
2071
|
+
*
|
|
2072
|
+
* ⛔ Torna una STRINGA, vuota quando il modello non ha detto niente: chi la interpreta è
|
|
2073
|
+
* `talosResearchParseVerdict`, che su una risposta illeggibile risponde `unchecked` col motivo.
|
|
2074
|
+
* Un lancio qui diventerebbe «il giudice non ha risposto» sull'affermazione, che è comunque
|
|
2075
|
+
* onesto — ma una stringa vuota è più vicina al fatto.
|
|
2076
|
+
*
|
|
2077
|
+
* @param {object} input
|
|
2078
|
+
* @param {string} input.modello
|
|
2079
|
+
* @param {string} input.chiave
|
|
2080
|
+
* @param {string} input.prompt
|
|
2081
|
+
* @param {AbortSignal} [input.segnaleStop]
|
|
2082
|
+
* @param {typeof fetch} [input.fetchDiRete] — SOLO per test
|
|
2083
|
+
* @returns {Promise<string>}
|
|
2084
|
+
*/
|
|
2085
|
+
export async function chiediAlModelloUnaVolta({ modello, chiave, prompt, segnaleStop, fetchDiRete = fetch }) {
|
|
2086
|
+
const { scelta } = await chiamaConRitenta({
|
|
2087
|
+
modello, chiave, messaggi: [{ role: 'user', content: String(prompt ?? '') }], attrezzi: [],
|
|
2088
|
+
fetchDiRete, ...(segnaleStop ? { segnaleStop } : {}),
|
|
2089
|
+
});
|
|
2090
|
+
return String(scelta?.content ?? '').trim();
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
/**
|
|
2094
|
+
* Il comando diretto (`!comando` nel composer, piano §1.3-BIS.T seconda
|
|
2095
|
+
* metà) — esegue UN comando nella cartella di una sessione, FUORI dal ciclo
|
|
2096
|
+
* di `talosLavora`: nessun modello coinvolto, l'owner sceglie il comando,
|
|
2097
|
+
* non un attrezzo che il modello sceglie di chiamare.
|
|
2098
|
+
*
|
|
2099
|
+
* ⛔ Riusa `eseguiComandoSandboxato` (talosHarness.mjs) — STESSA funzione
|
|
2100
|
+
* che l'attrezzo `shell` chiama dentro il ciclo, stessi livelli onesti
|
|
2101
|
+
* (`wsl2`/`adb-shell-on-device`/`none`), mai una seconda implementazione
|
|
2102
|
+
* che diverge in silenzio.
|
|
2103
|
+
*
|
|
2104
|
+
* ⛔ Emette un RunStarted/RunFinished che avvolge un SOLO ToolCallStart/
|
|
2105
|
+
* Args/Result — non un vero "run" nel senso di talosLavora, ma lo stesso
|
|
2106
|
+
* vocabolario di eventi: chi ascolta (handleRealEvent in app.js) non ha
|
|
2107
|
+
* bisogno di un ramo nuovo, funziona già per come è scritto oggi.
|
|
2108
|
+
*/
|
|
2109
|
+
export async function eseguiComandoDiretto({
|
|
2110
|
+
cartella, comando, onEvento, mobile = false,
|
|
2111
|
+
/* ⛔ D-10F — dove gira questo comando: 'wsl2', 'windows', o null = «come prima» (il ripiego
|
|
2112
|
+
automatico). E' una scelta della SESSIONE, non una conseguenza di quale programma hai scritto. */
|
|
2113
|
+
dove = null,
|
|
2114
|
+
eseguiComandoSandboxatoFn = eseguiComandoSandboxatoReale,
|
|
2115
|
+
}) {
|
|
2116
|
+
/*
|
|
2117
|
+
* ⛔⛔⛔ D-10D — questo NON è più un giro del modello: ha il suo vocabolario.
|
|
2118
|
+
* Vedi `comandoUtenteIniziato` in agui-events.mjs per il perché e per la fonte (AWS Bedrock
|
|
2119
|
+
* AgentCore: due operazioni distinte sulla stessa sessione, eseguibili insieme).
|
|
2120
|
+
* Il `toolCallId` resta, perché la resa in chat della riga «comando» è già quella e non cambia.
|
|
2121
|
+
*/
|
|
2122
|
+
const comandoId = randomUUID();
|
|
2123
|
+
const toolCallId = randomUUID();
|
|
2124
|
+
onEvento(comandoUtenteIniziato({ comandoId, comando }));
|
|
2125
|
+
onEvento(toolCallStart({ toolCallId, toolCallName: 'shell' }));
|
|
2126
|
+
onEvento(toolCallArgs({ toolCallId, delta: JSON.stringify({ comando }) }));
|
|
2127
|
+
/*
|
|
2128
|
+
* ⛔⛔⛔ D-10B — È QUI CHE IL DEBITO FA PIÙ MALE: questo è il comando scritto DALLA PERSONA col
|
|
2129
|
+
* `!` del composer. Misurato prima della cura: 2.091 ms di schermo fermo su un comando da
|
|
2130
|
+
* 2.091 ms — un `await` dell'intero comando e poi un solo `ToolCallResult`. Chi lancia
|
|
2131
|
+
* `!npm test` guarda un riquadro vuoto finché non finisce, e non sa nemmeno se è partito.
|
|
2132
|
+
*
|
|
2133
|
+
* ⛔ L'accorpamento è lo stesso dell'attrezzo shell del modello (kernel, `talosLavora`): ogni
|
|
2134
|
+
* 120 ms oppure appena il pezzo supera i 2 KB, con un tetto. Un evento per ogni `data` di un
|
|
2135
|
+
* `npm test` inonderebbe l'SSE con eventi da pochi byte.
|
|
2136
|
+
* ⛔ Il testo definitivo resta quello del `ToolCallResult` qui sotto, tagliato da `uscitaUtile`:
|
|
2137
|
+
* questi pezzi sono avanzamento, non storia — `session-registry` non li persiste (sono
|
|
2138
|
+
* effimeri come `WorkspaceChanged`, stessa lezione: 1,9 MB rigiocati a ogni apertura).
|
|
2139
|
+
* Ricerca 10/09/2026: AG-UI, «a vocabulary of typed events that agents emit to frontends», dove
|
|
2140
|
+
* l'avanzamento è distinto dal messaggio finale; Vercel Academy, «Streaming and Tool Rendering».
|
|
2141
|
+
*/
|
|
2142
|
+
let accumulato = '';
|
|
2143
|
+
let ultimoInvio = 0;
|
|
2144
|
+
let mandati = 0;
|
|
2145
|
+
const TETTO_USCITA_IN_CORSO = 40_000;
|
|
2146
|
+
const svuota = () => {
|
|
2147
|
+
if (!accumulato || mandati >= TETTO_USCITA_IN_CORSO) return;
|
|
2148
|
+
const delta = accumulato.slice(0, TETTO_USCITA_IN_CORSO - mandati);
|
|
2149
|
+
accumulato = '';
|
|
2150
|
+
mandati += delta.length;
|
|
2151
|
+
onEvento(toolCallOutput({ toolCallId, delta }));
|
|
2152
|
+
};
|
|
2153
|
+
const risultato = await eseguiComandoSandboxatoFn(comando, cartella, {
|
|
2154
|
+
mobile,
|
|
2155
|
+
/* ⛔ D-10D-bis: si chiede al kernel di dire DOVE si e' fermato il comando, cosi' il prossimo
|
|
2156
|
+
riparte da li'. Chi non lo chiede non vede nessuna differenza. */
|
|
2157
|
+
tracciaCartella: true,
|
|
2158
|
+
dove,
|
|
2159
|
+
onPezzo: ({ testo }) => {
|
|
2160
|
+
accumulato += testo;
|
|
2161
|
+
const ora = Date.now();
|
|
2162
|
+
if (accumulato.length >= 2_048 || ora - ultimoInvio >= 120) { ultimoInvio = ora; svuota(); }
|
|
2163
|
+
},
|
|
2164
|
+
});
|
|
2165
|
+
svuota(); // ⛔ l'ultimo pezzo non resta in mano: sarebbe il difetto di prima, in piccolo
|
|
2166
|
+
const content = `exit ${risultato.codice} [sandbox: ${risultato.enforcement}]\n${risultato.testo}`;
|
|
2167
|
+
onEvento(eventoPerEsitoTool({ messageId: randomUUID(), toolCallId, content }));
|
|
2168
|
+
onEvento(comandoUtenteFinito({ comandoId, codice: risultato.codice, enforcement: risultato.enforcement }));
|
|
2169
|
+
/*
|
|
2170
|
+
* ⭐⭐⭐ D-10S (11/09) — `comando` e `testo` TORNANO al chiamante, e non è una comodità.
|
|
2171
|
+
* Finora l'uscita di un comando `!` viveva solo negli eventi: la persona la vedeva, il modello
|
|
2172
|
+
* no. Chi poi chiedeva «e allora?» parlava di una cosa che per il modello non era mai successa.
|
|
2173
|
+
* Per cucire il comando nella cronologia della sessione (session-registry, `shell()`) servono
|
|
2174
|
+
* qui: il testo della domanda e il testo della risposta.
|
|
2175
|
+
* ⛔ Sono campi IN PIÙ: chi legge `ok`/`codice`/`enforcement`/`cartellaFinale` non vede differenza.
|
|
2176
|
+
* ⛔ `risultato.testo` è già passato da `uscitaUtile`, cioè è già tagliato: la cucitura non deve
|
|
2177
|
+
* ritagliare di nuovo con un'altra regola, o la persona e il modello leggerebbero due cose
|
|
2178
|
+
* diverse sotto lo stesso nome.
|
|
2179
|
+
*/
|
|
2180
|
+
return {
|
|
2181
|
+
ok: true,
|
|
2182
|
+
codice: risultato.codice,
|
|
2183
|
+
enforcement: risultato.enforcement,
|
|
2184
|
+
cartellaFinale: risultato.cartellaFinale ?? null,
|
|
2185
|
+
comando,
|
|
2186
|
+
testo: risultato.testo ?? '',
|
|
2187
|
+
};
|
|
2188
|
+
}
|