@reticlehq/server 2.4.0 → 2.5.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/SKILL.md +702 -0
- package/dist/bridge/auth-failure-reason.d.ts +19 -0
- package/dist/bridge/auth-failure-reason.js +29 -0
- package/dist/{bridge-security.d.ts → bridge/bridge-security.d.ts} +1 -1
- package/dist/{bridge-security.js → bridge/bridge-security.js} +1 -1
- package/dist/{bridge.d.ts → bridge/bridge.d.ts} +1 -1
- package/dist/{bridge.js → bridge/bridge.js} +63 -13
- package/dist/{bridge.test-harness.d.ts → bridge/bridge.test-harness.d.ts} +1 -11
- package/dist/{bridge.test-harness.js → bridge/bridge.test-harness.js} +9 -9
- package/dist/{pairing-token.js → bridge/pairing-token.js} +1 -1
- package/dist/capsule/capsule-store.d.ts +8 -3
- package/dist/capsule/capsule-store.js +7 -2
- package/dist/capsule/capsule.js +3 -3
- package/dist/capsule/causal-summary.d.ts +8 -0
- package/dist/capsule/causal-summary.js +22 -12
- package/dist/capsule/divergence.d.ts +9 -4
- package/dist/capsule/divergence.js +8 -8
- package/dist/capsule/predicate-to-links.js +11 -8
- package/dist/{cli-doctor.js → cli/cli-doctor.js} +9 -2
- package/dist/{cli-flow-commands.d.ts → cli/cli-flow-commands.d.ts} +2 -2
- package/dist/{cli-flow-commands.js → cli/cli-flow-commands.js} +15 -15
- package/dist/{cli-launch.d.ts → cli/cli-launch.d.ts} +32 -3
- package/dist/cli/cli-launch.js +171 -0
- package/dist/{cli-parse.d.ts → cli/cli-parse.d.ts} +2 -11
- package/dist/{cli-parse.js → cli/cli-parse.js} +113 -60
- package/dist/{cli-port.d.ts → cli/cli-port.d.ts} +12 -0
- package/dist/{cli-port.js → cli/cli-port.js} +9 -9
- package/dist/cli/cli-update-commands.d.ts +4 -0
- package/dist/cli/cli-update-commands.js +93 -0
- package/dist/{cli-verify.js → cli/cli-verify.js} +14 -14
- package/dist/{cloud-cli.js → cli/cloud-cli.js} +29 -24
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +174 -92
- package/dist/cloud/cloud-config.d.ts +0 -1
- package/dist/cloud/cloud-config.js +9 -9
- package/dist/cloud/cloud-sync.d.ts +1 -7
- package/dist/cloud/cloud-sync.js +12 -12
- package/dist/crawl/crawl-empty.d.ts +16 -0
- package/dist/crawl/crawl-empty.js +24 -0
- package/dist/crawl/crawl-tools.js +4 -1
- package/dist/crawl/crawl.js +26 -7
- package/dist/daemon/daemon-resilience.d.ts +49 -0
- package/dist/daemon/daemon-resilience.js +191 -0
- package/dist/daemon/daemon-usefulness.d.ts +61 -0
- package/dist/daemon/daemon-usefulness.js +62 -0
- package/dist/{daemon.d.ts → daemon/daemon.d.ts} +33 -8
- package/dist/{daemon.js → daemon/daemon.js} +80 -48
- package/dist/daemon/idle-grace.d.ts +32 -0
- package/dist/daemon/idle-grace.js +41 -0
- package/dist/{idle-shutdown.d.ts → daemon/idle-shutdown.d.ts} +13 -0
- package/dist/{idle-shutdown.js → daemon/idle-shutdown.js} +27 -4
- package/dist/domain/domain-model.js +2 -2
- package/dist/events/body-failures.js +8 -8
- package/dist/events/contradictions.d.ts +1 -1
- package/dist/events/contradictions.js +60 -10
- package/dist/events/echo-mismatch.js +10 -10
- package/dist/events/event-filters.js +6 -6
- package/dist/events/json-salvage.d.ts +2 -1
- package/dist/events/json-salvage.js +6 -6
- package/dist/events/observed-in-window.d.ts +12 -0
- package/dist/events/observed-in-window.js +21 -0
- package/dist/events/predicate-eval.d.ts +43 -20
- package/dist/events/predicate-eval.js +159 -55
- package/dist/events/predicate.d.ts +2 -2
- package/dist/events/predicate.js +112 -35
- package/dist/events/reconcile.js +5 -5
- package/dist/events/ring-buffer.js +1 -1
- package/dist/events/stale-response.js +6 -6
- package/dist/events/testid-near-miss.d.ts +22 -0
- package/dist/events/testid-near-miss.js +33 -0
- package/dist/events/unit-mismatch.js +8 -8
- package/dist/flows/affected-tools.js +19 -4
- package/dist/flows/affected.js +1 -1
- package/dist/flows/annotate-target.d.ts +1 -0
- package/dist/flows/annotate-target.js +20 -0
- package/dist/flows/annotate-tools.js +25 -3
- package/dist/flows/annotate.js +17 -4
- package/dist/flows/assertion-integrity.d.ts +2 -1
- package/dist/flows/assertion-tiers-store.d.ts +3 -66
- package/dist/flows/assertion-tiers-store.js +2 -2
- package/dist/flows/attributed-failure.d.ts +17 -0
- package/dist/flows/attributed-failure.js +20 -0
- package/dist/flows/buddy-status.d.ts +2 -1
- package/dist/flows/buddy-status.js +2 -2
- package/dist/flows/coverage.d.ts +3 -2
- package/dist/flows/coverage.js +2 -2
- package/dist/flows/decision.js +24 -9
- package/dist/flows/empty-flow.d.ts +6 -0
- package/dist/flows/empty-flow.js +24 -0
- package/dist/flows/flake-store.d.ts +0 -1
- package/dist/flows/flake-store.js +1 -1
- package/dist/flows/flake.d.ts +2 -1
- package/dist/flows/flake.js +2 -2
- package/dist/flows/flow-classify.js +12 -0
- package/dist/flows/flow-replay-run.js +4 -4
- package/dist/flows/flow-replay.d.ts +5 -7
- package/dist/flows/flow-replay.js +114 -47
- package/dist/flows/flow-report.js +1 -1
- package/dist/flows/flow-scope.d.ts +1 -6
- package/dist/flows/flow-step-runners.d.ts +26 -6
- package/dist/flows/flow-step-runners.js +157 -6
- package/dist/flows/flow-success.js +20 -15
- package/dist/flows/flow-tools.js +53 -7
- package/dist/flows/flows.js +15 -11
- package/dist/flows/gate.d.ts +4 -3
- package/dist/flows/gate.js +1 -1
- package/dist/flows/heal-run.d.ts +0 -8
- package/dist/flows/heal-run.js +5 -5
- package/dist/flows/heal.js +3 -3
- package/dist/flows/predicate-to-expect.d.ts +38 -0
- package/dist/flows/predicate-to-expect.js +95 -0
- package/dist/flows/replay.d.ts +16 -1
- package/dist/flows/replay.js +58 -22
- package/dist/flows/role-anchor-nearest.d.ts +6 -0
- package/dist/flows/role-anchor-nearest.js +45 -0
- package/dist/flows/server-verify.d.ts +1 -1
- package/dist/flows/server-verify.js +5 -5
- package/dist/flows/verify-change-tools.js +32 -10
- package/dist/flows/verify-change-verdict.d.ts +15 -0
- package/dist/flows/verify-change-verdict.js +25 -0
- package/dist/flows/watch-batcher.d.ts +3 -2
- package/dist/honesty/already-true.d.ts +2 -0
- package/dist/honesty/already-true.js +31 -0
- package/dist/honesty/blind-spots.d.ts +16 -3
- package/dist/honesty/blind-spots.js +24 -12
- package/dist/honesty/honesty.d.ts +3 -2
- package/dist/honesty/honesty.js +4 -4
- package/dist/honesty/uncaptured-bodies.js +1 -1
- package/dist/honesty/unread-outcome.js +2 -2
- package/dist/honesty/verified.d.ts +22 -3
- package/dist/honesty/verified.js +48 -8
- package/dist/http-server.js +14 -7
- package/dist/hunt/hunt-report.d.ts +2 -1
- package/dist/hunt/hunt-report.js +2 -2
- package/dist/index.d.ts +8 -6
- package/dist/index.js +32 -25
- package/dist/init/agent-rules.d.ts +2 -1
- package/dist/init/agent-rules.js +45 -7
- package/dist/init/app-choice.d.ts +20 -0
- package/dist/init/app-choice.js +25 -0
- package/dist/init/astro-host.d.ts +21 -0
- package/dist/init/astro-host.js +27 -0
- package/dist/init/astro-patch.d.ts +16 -0
- package/dist/init/astro-patch.js +240 -0
- package/dist/init/capabilities.js +4 -1
- package/dist/init/cra.d.ts +37 -0
- package/dist/init/cra.js +90 -0
- package/dist/init/cursor.js +39 -4
- package/dist/init/desktop-doctor.d.ts +3 -2
- package/dist/init/desktop-doctor.js +4 -4
- package/dist/init/detect.d.ts +3 -2
- package/dist/init/detect.js +16 -5
- package/dist/init/mcp-registered.d.ts +12 -0
- package/dist/init/mcp-registered.js +14 -0
- package/dist/init/mcp.d.ts +27 -2
- package/dist/init/mcp.js +40 -4
- package/dist/init/next-patch.d.ts +2 -4
- package/dist/init/next-patch.js +109 -19
- package/dist/init/node-io.js +23 -6
- package/dist/init/plan-framework.d.ts +16 -1
- package/dist/init/plan-framework.js +132 -8
- package/dist/init/plan.d.ts +73 -8
- package/dist/init/plan.js +133 -34
- package/dist/init/project-id.d.ts +5 -9
- package/dist/init/project-id.js +10 -22
- package/dist/init/run.d.ts +5 -0
- package/dist/init/run.js +210 -36
- package/dist/init/slash-command.d.ts +9 -0
- package/dist/init/slash-command.js +10 -1
- package/dist/init/snippets.d.ts +7 -10
- package/dist/init/snippets.js +44 -15
- package/dist/init/vite-config.js +1 -1
- package/dist/init/workspace-apps.d.ts +23 -0
- package/dist/init/workspace-apps.js +68 -0
- package/dist/input/driven-redaction.js +1 -1
- package/dist/input/network-detail.js +8 -8
- package/dist/input/network-mock-tools.js +2 -2
- package/dist/input/network-mock.js +4 -4
- package/dist/input/real-input.js +8 -3
- package/dist/input/scroll-find.js +2 -2
- package/dist/input/scroll-tools.d.ts +0 -5
- package/dist/input/scroll-tools.js +11 -4
- package/dist/input/viewport-tools.js +2 -2
- package/dist/journal/ambient-store.d.ts +0 -2
- package/dist/journal/ambient-store.js +1 -1
- package/dist/journal/ambient.js +1 -1
- package/dist/journal/attach-journal.d.ts +2 -1
- package/dist/journal/deviation-report.js +6 -6
- package/dist/journal/deviation-service.js +1 -1
- package/dist/journal/envelope-store.d.ts +0 -2
- package/dist/journal/envelope-store.js +1 -1
- package/dist/journal/envelope.d.ts +2 -3
- package/dist/journal/envelope.js +3 -3
- package/dist/journal/journal-query.js +1 -1
- package/dist/journal/journal-recorder.d.ts +2 -1
- package/dist/journal/journal-recorder.js +2 -2
- package/dist/journal/rollups.js +9 -9
- package/dist/journal/session-end.d.ts +2 -1
- package/dist/journal/session-journal.js +4 -4
- package/dist/license/license.d.ts +0 -14
- package/dist/license/license.js +8 -8
- package/dist/log.d.ts +10 -0
- package/dist/log.js +11 -1
- package/dist/mcp/mcp-is-error.d.ts +13 -0
- package/dist/mcp/mcp-is-error.js +18 -0
- package/dist/mcp/mcp-outage.d.ts +31 -0
- package/dist/mcp/mcp-outage.js +48 -0
- package/dist/mcp/mcp-proxy.d.ts +124 -0
- package/dist/mcp/mcp-proxy.js +697 -0
- package/dist/{mcp.d.ts → mcp/mcp.d.ts} +13 -10
- package/dist/{mcp.js → mcp/mcp.js} +110 -35
- package/dist/mcp/proxy-handshake.d.ts +3 -0
- package/dist/mcp/proxy-handshake.js +54 -0
- package/dist/mcp/proxy-lifecycle.d.ts +64 -0
- package/dist/mcp/proxy-lifecycle.js +70 -0
- package/dist/mcp/tool-catalog-cache.d.ts +29 -0
- package/dist/mcp/tool-catalog-cache.js +55 -0
- package/dist/oracles/propose-consequences.d.ts +2 -1
- package/dist/oracles/propose-consequences.js +16 -11
- package/dist/oracles/self-instrument.d.ts +2 -1
- package/dist/oracles/self-instrument.js +1 -1
- package/dist/phenomena/phenomena.js +3 -3
- package/dist/platform.d.ts +16 -0
- package/dist/platform.js +16 -0
- package/dist/pool/playwright-launcher.d.ts +15 -2
- package/dist/pool/playwright-launcher.js +21 -2
- package/dist/project/fs-port.js +1 -1
- package/dist/project/project-tools.js +2 -2
- package/dist/runs/build-verification-run.js +1 -1
- package/dist/runs/repair-prompt.d.ts +0 -6
- package/dist/runs/repair-prompt.js +3 -3
- package/dist/runs/risk-classify.js +1 -1
- package/dist/runs/run-diff.d.ts +0 -2
- package/dist/runs/run-diff.js +3 -3
- package/dist/runs/run-tools.js +2 -2
- package/dist/runs/verification-sync.js +2 -2
- package/dist/runs/verify-http.js +1 -1
- package/dist/runs/verify-server.js +1 -1
- package/dist/session/command-timeout.d.ts +11 -3
- package/dist/session/command-timeout.js +2 -2
- package/dist/session/control-envelope.d.ts +6 -0
- package/dist/session/control-envelope.js +7 -1
- package/dist/session/dev-server-probe.d.ts +13 -0
- package/dist/session/dev-server-probe.js +52 -0
- package/dist/session/last-act.d.ts +17 -3
- package/dist/session/last-act.js +16 -4
- package/dist/session/live-control-tools.js +7 -2
- package/dist/session/live-control.js +1 -1
- package/dist/session/looks-like-dev-server.d.ts +14 -0
- package/dist/session/looks-like-dev-server.js +25 -0
- package/dist/session/no-session-diagnosis.d.ts +38 -0
- package/dist/session/no-session-diagnosis.js +70 -0
- package/dist/session/no-session-watch.d.ts +19 -0
- package/dist/session/no-session-watch.js +61 -0
- package/dist/session/observed-state.d.ts +4 -0
- package/dist/session/observed-state.js +19 -1
- package/dist/session/output-budget.js +1 -1
- package/dist/session/session-health.d.ts +8 -1
- package/dist/session/session-health.js +8 -7
- package/dist/session/session-info.d.ts +26 -7
- package/dist/session/session-info.js +42 -1
- package/dist/session/session-lease.d.ts +0 -11
- package/dist/session/session-lease.js +1 -1
- package/dist/session/session-manager.d.ts +3 -0
- package/dist/session/session-manager.js +52 -9
- package/dist/session/session-readiness.d.ts +1 -1
- package/dist/session/session-readiness.js +6 -1
- package/dist/session/session-reaper.js +1 -1
- package/dist/session/session-reconnect.d.ts +32 -0
- package/dist/session/session-reconnect.js +42 -0
- package/dist/session/session.d.ts +14 -2
- package/dist/session/session.js +69 -26
- package/dist/session/state-select.d.ts +1 -1
- package/dist/session/state-select.js +1 -1
- package/dist/status-payload.d.ts +19 -0
- package/dist/status-payload.js +11 -0
- package/dist/telemetry/argument-shape.js +10 -1
- package/dist/telemetry/browser-mode.d.ts +27 -0
- package/dist/telemetry/browser-mode.js +33 -0
- package/dist/telemetry/bug-found.d.ts +17 -21
- package/dist/telemetry/bug-found.js +120 -8
- package/dist/telemetry/cli-telemetry.d.ts +2 -12
- package/dist/telemetry/cli-telemetry.js +27 -4
- package/dist/telemetry/daemon-telemetry.d.ts +19 -0
- package/dist/telemetry/daemon-telemetry.js +12 -2
- package/dist/telemetry/dev-repo.js +4 -4
- package/dist/telemetry/error-fingerprint.js +21 -2
- package/dist/telemetry/feedback-cli.d.ts +1 -1
- package/dist/telemetry/feedback-cli.js +4 -4
- package/dist/telemetry/feedback-context.d.ts +22 -0
- package/dist/telemetry/feedback-context.js +41 -1
- package/dist/telemetry/feedback-delivery.d.ts +20 -0
- package/dist/telemetry/feedback-delivery.js +30 -0
- package/dist/telemetry/feedback-outbox.d.ts +22 -0
- package/dist/telemetry/feedback-outbox.js +87 -0
- package/dist/telemetry/feedback.d.ts +22 -1
- package/dist/telemetry/feedback.js +67 -8
- package/dist/telemetry/git-facts.js +1 -1
- package/dist/telemetry/session-metrics.d.ts +2 -0
- package/dist/telemetry/session-metrics.js +68 -6
- package/dist/telemetry/telemetry.d.ts +27 -2
- package/dist/telemetry/telemetry.js +119 -29
- package/dist/telemetry/verification-of.d.ts +22 -0
- package/dist/telemetry/verification-of.js +84 -0
- package/dist/tools/act-capsule.js +2 -2
- package/dist/tools/act-danger.d.ts +12 -0
- package/dist/tools/act-danger.js +20 -3
- package/dist/tools/act-helpers.js +4 -4
- package/dist/tools/act-tools.d.ts +0 -8
- package/dist/tools/act-tools.js +84 -115
- package/dist/tools/act-view.js +7 -7
- package/dist/tools/alias-args.d.ts +17 -0
- package/dist/tools/alias-args.js +25 -0
- package/dist/tools/assert-grade.js +9 -9
- package/dist/tools/assert-verdict.d.ts +3 -1
- package/dist/tools/assert-verdict.js +8 -5
- package/dist/tools/browser-tools.js +30 -11
- package/dist/tools/contract-tools.js +1 -1
- package/dist/tools/coverage-identity.d.ts +34 -0
- package/dist/tools/coverage-identity.js +46 -0
- package/dist/tools/coverage-tools.js +10 -14
- package/dist/tools/dynamic-tools.d.ts +2 -1
- package/dist/tools/dynamic-tools.js +90 -18
- package/dist/tools/error-recovery.d.ts +20 -2
- package/dist/tools/error-recovery.js +179 -4
- package/dist/tools/feedback-tools.d.ts +12 -1
- package/dist/tools/feedback-tools.js +28 -5
- package/dist/tools/invoke-tool.d.ts +0 -6
- package/dist/tools/invoke-tool.js +57 -32
- package/dist/tools/lease-hint.d.ts +17 -0
- package/dist/tools/lease-hint.js +24 -0
- package/dist/tools/lease-tools.js +6 -5
- package/dist/tools/merge-tools.d.ts +10 -1
- package/dist/tools/merge-tools.js +9 -3
- package/dist/tools/merged-name-redirect.d.ts +13 -0
- package/dist/tools/merged-name-redirect.js +55 -0
- package/dist/tools/navigate-result.d.ts +17 -0
- package/dist/tools/navigate-result.js +31 -0
- package/dist/tools/observe-tools.js +54 -20
- package/dist/tools/observed-nothing.d.ts +25 -0
- package/dist/tools/observed-nothing.js +33 -0
- package/dist/tools/query-paginate.js +2 -2
- package/dist/tools/query-shape.d.ts +18 -0
- package/dist/tools/query-shape.js +39 -0
- package/dist/tools/read-tools.js +81 -20
- package/dist/tools/real-input-attempt.d.ts +28 -0
- package/dist/tools/real-input-attempt.js +88 -0
- package/dist/tools/reconcile-tools.js +7 -7
- package/dist/tools/reload-result.d.ts +14 -0
- package/dist/tools/reload-result.js +29 -0
- package/dist/tools/snapshot-delta.d.ts +4 -5
- package/dist/tools/snapshot-delta.js +0 -0
- package/dist/tools/tool-kit.d.ts +8 -0
- package/dist/tools/tool-surface.d.ts +83 -0
- package/dist/tools/tool-surface.js +204 -0
- package/dist/tools/tools-helpers.js +6 -6
- package/dist/tools/tools.d.ts +15 -0
- package/dist/tools/tools.js +53 -17
- package/dist/tools/unadvertised-help.d.ts +5 -0
- package/dist/tools/unadvertised-help.js +39 -0
- package/dist/trace.d.ts +69 -0
- package/dist/trace.js +150 -0
- package/dist/update/nudge-credit.d.ts +4 -0
- package/dist/update/nudge-credit.js +53 -0
- package/dist/update/sdk-sync.d.ts +21 -0
- package/dist/update/sdk-sync.js +42 -0
- package/dist/update/update-checker.js +3 -3
- package/dist/update/update-nudge.d.ts +63 -6
- package/dist/update/update-nudge.js +128 -15
- package/dist/update/updater.d.ts +1 -9
- package/dist/update/updater.js +13 -8
- package/dist/version/peer-announce.d.ts +4 -0
- package/dist/version/peer-announce.js +30 -0
- package/dist/{server-version.js → version/server-version.js} +5 -1
- package/dist/version/version-nudge.d.ts +28 -0
- package/dist/version/version-nudge.js +40 -0
- package/dist/version/version-skew.d.ts +66 -0
- package/dist/version/version-skew.js +71 -0
- package/dist/visual/visual-diff.js +2 -2
- package/dist/visual/visual-tools.js +4 -4
- package/docs/agent-cheatsheet.md +85 -0
- package/docs/architecture.md +113 -0
- package/docs/benchmarks.md +255 -0
- package/docs/debugging.md +102 -0
- package/docs/deploy-checks.md +69 -0
- package/docs/desktop-apps.md +256 -0
- package/docs/enterprise.md +62 -0
- package/docs/fixtures.md +28 -0
- package/docs/flows.md +166 -0
- package/docs/getting-started.md +469 -0
- package/docs/human-control.md +63 -0
- package/docs/integration-patterns.md +171 -0
- package/docs/local-registry.md +126 -0
- package/docs/mint.json +61 -0
- package/docs/multi-agent-testing.md +55 -0
- package/docs/platform-integration.md +122 -0
- package/docs/telemetry-contract.md +214 -0
- package/docs/telemetry.md +184 -0
- package/docs/testing.md +82 -0
- package/docs/token-efficiency.md +80 -0
- package/docs/usage.md +1046 -0
- package/package.json +8 -5
- package/dist/cli-launch.js +0 -112
- package/dist/daemon-resilience.d.ts +0 -22
- package/dist/daemon-resilience.js +0 -85
- package/dist/mcp-proxy.d.ts +0 -67
- package/dist/mcp-proxy.js +0 -337
- package/dist/tools/profiles.d.ts +0 -37
- package/dist/tools/profiles.js +0 -139
- /package/dist/{pairing-token.d.ts → bridge/pairing-token.d.ts} +0 -0
- /package/dist/{token-auth.d.ts → bridge/token-auth.d.ts} +0 -0
- /package/dist/{token-auth.js → bridge/token-auth.js} +0 -0
- /package/dist/{cli-doctor.d.ts → cli/cli-doctor.d.ts} +0 -0
- /package/dist/{cli-verify.d.ts → cli/cli-verify.d.ts} +0 -0
- /package/dist/{cloud-cli.d.ts → cli/cloud-cli.d.ts} +0 -0
- /package/dist/{server-version.d.ts → version/server-version.d.ts} +0 -0
|
@@ -8,6 +8,45 @@
|
|
|
8
8
|
* on stable, human-authored substrings of the thrown messages (see session-manager.ts, the flow/
|
|
9
9
|
* baseline stores). No clock, no IO — unit-testable in isolation.
|
|
10
10
|
*/
|
|
11
|
+
import { TRANSPORT_LIMITS } from '@reticlehq/core';
|
|
12
|
+
import { SELF_RECOVERING_MARKER } from '../session/no-session-diagnosis.js';
|
|
13
|
+
/** Marks where an over-long message had its middle removed. Named so tests assert it by value. */
|
|
14
|
+
const ELISION = '… [elided] …';
|
|
15
|
+
/**
|
|
16
|
+
* Bound a message before the agent ever sees it, keeping BOTH ends.
|
|
17
|
+
*
|
|
18
|
+
* Error messages interpolate caller-supplied values — a ref, a baseline name, a selector — and
|
|
19
|
+
* nothing upstream bounds them, so a 100KB argument came back as a 100KB tool result. Capping here
|
|
20
|
+
* rather than at each throw site is deliberate: this is the single funnel every tool error passes
|
|
21
|
+
* through, so it also covers the tools nobody has written yet.
|
|
22
|
+
*
|
|
23
|
+
* The middle is what goes. Head-truncation would be simpler and wrong: the recovery table keys off
|
|
24
|
+
* substrings that sit at the END of the message ("… no longer resolves to an element"), so cutting
|
|
25
|
+
* the tail severs the part that makes an error recognizable and re-labels a known condition as a
|
|
26
|
+
* possible Reticle defect.
|
|
27
|
+
*/
|
|
28
|
+
function capMessage(message) {
|
|
29
|
+
const max = TRANSPORT_LIMITS.MAX_ERROR_LENGTH;
|
|
30
|
+
if (max >= message.length)
|
|
31
|
+
return message;
|
|
32
|
+
const keep = Math.floor((max - ELISION.length) / 2);
|
|
33
|
+
return `${message.slice(0, keep)}${ELISION}${message.slice(message.length - keep)}`;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Messages that already spell out the exact retry, so any hint we add can only argue with them.
|
|
37
|
+
*
|
|
38
|
+
* The no-session diagnosis is one (it inspected the machine and named the cause). The native-input
|
|
39
|
+
* refusal is the other: it names the tool, the argument and the follow-up call to make instead — and
|
|
40
|
+
* it was still collecting the defect ask, inviting a bug report about a refusal that had just told
|
|
41
|
+
* the agent precisely what to do.
|
|
42
|
+
*/
|
|
43
|
+
const SELF_RECOVERING = [/cannot drive native input/i];
|
|
44
|
+
/** A message that already carries its own concrete next action, so nothing should be appended. */
|
|
45
|
+
function isSelfRecovering(message) {
|
|
46
|
+
if (message.includes(SELF_RECOVERING_MARKER))
|
|
47
|
+
return true;
|
|
48
|
+
return SELF_RECOVERING.some((pattern) => pattern.test(message));
|
|
49
|
+
}
|
|
11
50
|
/** The recovery hints, named so they are not free strings and can be asserted in tests. */
|
|
12
51
|
export const RECOVERY = {
|
|
13
52
|
NO_SESSION: 'No app is connected to Reticle. Ask the human to start their app in dev with @reticlehq/browser ' +
|
|
@@ -21,18 +60,71 @@ export const RECOVERY = {
|
|
|
21
60
|
'the tab to the front, or run `reticle drive <url>` for a guaranteed scriptable context.',
|
|
22
61
|
MISSING_BASELINE: 'That baseline does not exist yet. Call reticle_baseline { action: "list" } to see saved names, or ' +
|
|
23
62
|
'reticle_baseline { action: "save", name } to capture one before diffing against it.',
|
|
24
|
-
MISSING_RECORDING:
|
|
25
|
-
|
|
63
|
+
MISSING_RECORDING:
|
|
64
|
+
// `recordingName`, not `name`. The hint said `name`, so an agent that followed it exactly got
|
|
65
|
+
// InvalidParams — a recovery that fails is worse than none, because it spends the retry.
|
|
66
|
+
'No recording by that name is in progress. Start one with ' +
|
|
67
|
+
'reticle_record { action: "start", recordingName } before annotating, stopping, or saving it.',
|
|
26
68
|
STALE_REF: 'That ref is stale: refs are invalidated whenever the DOM re-renders, so any action that ' +
|
|
27
69
|
'navigated, opened a modal, re-sorted a list or changed the page invalidates every ref taken ' +
|
|
28
70
|
'before it. Reticle refuses here rather than clicking whatever now occupies that slot. Call ' +
|
|
29
71
|
'reticle_query again for a fresh ref and retry the action — and prefer reticle_act_and_wait ' +
|
|
30
72
|
'{ until } when an action changes the page, so the next ref is taken after it settles.',
|
|
73
|
+
INVALID_NAME: 'That name is not a usable one. A flow, baseline or recording name must be a single safe path ' +
|
|
74
|
+
'segment — letters, digits, dash and underscore only, starting with a letter or digit, at most ' +
|
|
75
|
+
'64 characters. No slashes, no "..", no leading dot: the name becomes a filename under ' +
|
|
76
|
+
'.reticle/, so anything that could escape that directory is refused. Retry with a slug like ' +
|
|
77
|
+
'"checkout-happy-path". This is an invalid call, not a Reticle defect: there is nothing to report.',
|
|
78
|
+
NO_SUCH_OPTION: 'That <select> has no option with the value you asked for, and the message above lists the ones ' +
|
|
79
|
+
'it does have. Reticle refuses rather than assigning it: an unmatched value deselects everything, ' +
|
|
80
|
+
'and the change event that follows has been observed persisting an empty setting into the app. ' +
|
|
81
|
+
'Retry with one of the listed values — note they are option VALUES, which often differ from the ' +
|
|
82
|
+
'visible labels shown in brackets. This is an invalid call, not a Reticle defect.',
|
|
31
83
|
BAD_ARGUMENTS: "That call did not match the tool's schema — the message above names the tool and the exact " +
|
|
32
84
|
"argument it wanted. Re-read that tool's parameters and retry with the missing or corrected " +
|
|
33
85
|
'argument. This is an invalid call, not a Reticle defect: there is nothing to report.',
|
|
86
|
+
WRONG_TARGET: 'That action does not apply to this element — the ref resolved, the element just cannot take it ' +
|
|
87
|
+
'(you cannot fill a <button>, submit outside a form, or upload to anything but <input ' +
|
|
88
|
+
'type="file">). Call reticle_snapshot or reticle_query to find the control that does, or ' +
|
|
89
|
+
'reticle_inspect on this ref to see what it actually is. This is an invalid call, not a Reticle ' +
|
|
90
|
+
'defect: there is nothing to report.',
|
|
91
|
+
NOT_EDITABLE: 'The field is disabled or readonly, so a user could not edit it either — forcing the value would ' +
|
|
92
|
+
'put the app in a state nobody can reach, which is why Reticle refuses. This is usually the app ' +
|
|
93
|
+
'telling you something: drive whatever ENABLES the field (open the editor, satisfy the ' +
|
|
94
|
+
'precondition) and act again. If the field should have been editable, that is a bug in the app, ' +
|
|
95
|
+
'not in Reticle.',
|
|
96
|
+
CONFIRM_DANGEROUS: 'Reticle blocked a potentially destructive control (delete, remove, revoke…) on purpose. If you ' +
|
|
97
|
+
'mean it, retry the same action with args.confirmDangerous set to true. This is a deliberate ' +
|
|
98
|
+
'refusal, not a defect: there is nothing to report.',
|
|
99
|
+
UNSUPPORTED_SURFACE: 'That surface is not supported yet, and Reticle refuses rather than pretending — a rich-text ' +
|
|
100
|
+
'editor keeps its own document model, so writing to the DOM would look right and submit the old ' +
|
|
101
|
+
'content. Drive the same outcome another way (a plain input, a command, a keyboard action) or ' +
|
|
102
|
+
'assert on the result instead of typing it. Known gap, already reported: no need to file it.',
|
|
34
103
|
TOKEN_REQUIRED: 'The bridge binds beyond localhost and requires a pairing token. Set the same token in the SDK ' +
|
|
35
104
|
'init (@reticlehq/core) and the Reticle server config, then reconnect.',
|
|
105
|
+
SCOPE_MISMATCH: 'A tab IS connected — it just belongs to a different project than the daemon is scoped to, so ' +
|
|
106
|
+
'nothing is wrong with the app and there is nothing to report. The message above lists each ' +
|
|
107
|
+
'connected session with its sessionId: pass that sessionId to this tool to target it, or ' +
|
|
108
|
+
"restart the daemon from that app's directory so it becomes the default. Call reticle_sessions " +
|
|
109
|
+
'for the same list at any time.',
|
|
110
|
+
SESSION_GONE: 'The tab this call was targeting disconnected while the command was in flight, so the command ' +
|
|
111
|
+
'never completed — do not assume it applied or that it did not. Call reticle_sessions: a tab ' +
|
|
112
|
+
'that reloaded comes back under a NEW sessionId (every ref taken before it is gone — re-query), ' +
|
|
113
|
+
'and an empty list means the page was closed and the human has to reopen it.',
|
|
114
|
+
COMMAND_TIMEOUT: 'The page did not answer within the command window. That is a fact about the page, not a Reticle ' +
|
|
115
|
+
'failure: check reticle_sessions for `throttled`/`stale` on this session — a backgrounded tab is ' +
|
|
116
|
+
'throttled and may never answer, so ask the human to bring it to the front or drive your own ' +
|
|
117
|
+
'browser with reticle_lease. If the tab is in front, the page is busy or blocked (a long ' +
|
|
118
|
+
'synchronous task, an alert/confirm dialog); reticle_console usually shows what it hit. Retry ' +
|
|
119
|
+
'once the cause is addressed.',
|
|
120
|
+
FLOW_STEP_MISSING: 'A recorded step points at an element that is not on the page now. Either the app changed under ' +
|
|
121
|
+
'the flow — run reticle_flow_heal to re-anchor it, then reticle_flow_verify — or an earlier step ' +
|
|
122
|
+
'did not do what it used to, leaving the run on the wrong page: check reticle_snapshot to see ' +
|
|
123
|
+
'where it actually is. This is app/flow drift, which is exactly what the flow exists to catch.',
|
|
124
|
+
NO_POOL: 'Reticle cannot launch its own browser here: the daemon-managed pool is absent, or Playwright ' +
|
|
125
|
+
'has no Chromium installed (the message above says which). Run Reticle via `reticle mcp`, and ' +
|
|
126
|
+
'ask the human for `npx playwright install chromium` if that is what is missing. Meanwhile drive ' +
|
|
127
|
+
'a tab the human already has open — reticle_sessions lists them.',
|
|
36
128
|
};
|
|
37
129
|
/** Ordered match rules; the first hit wins. Substrings track the thrown messages they recover. */
|
|
38
130
|
const RULES = [
|
|
@@ -43,9 +135,46 @@ const RULES = [
|
|
|
43
135
|
{ match: /no baseline named/i, hint: RECOVERY.MISSING_BASELINE },
|
|
44
136
|
{ match: /no (?:active|compiled) recording named/i, hint: RECOVERY.MISSING_RECORDING },
|
|
45
137
|
{ match: /pairing token is required/i, hint: RECOVERY.TOKEN_REQUIRED },
|
|
138
|
+
// Three conditions the daemon understands perfectly and still asked for a bug report about. Each
|
|
139
|
+
// needs its own rule: none of them contains "no browser session connected" (the scope miss is
|
|
140
|
+
// "no browser session FOR project 'x'", which is the opposite claim — sessions exist).
|
|
141
|
+
{
|
|
142
|
+
match: /scope mismatch|session\(s\) ARE connected under a different project/i,
|
|
143
|
+
hint: RECOVERY.SCOPE_MISMATCH,
|
|
144
|
+
},
|
|
145
|
+
{ match: /^session disconnected$|session .* never connected/i, hint: RECOVERY.SESSION_GONE },
|
|
146
|
+
{ match: /^command '[^']*' timed out after/i, hint: RECOVERY.COMMAND_TIMEOUT },
|
|
147
|
+
// Flow replay: the recorded anchor is gone. Ordinary drift, and the tool that fixes it exists.
|
|
148
|
+
{ match: /did not resolve in current page/i, hint: RECOVERY.FLOW_STEP_MISSING },
|
|
149
|
+
{
|
|
150
|
+
match: /browser pool (?:unavailable|is shut down)|Chromium is not installed/i,
|
|
151
|
+
hint: RECOVERY.NO_POOL,
|
|
152
|
+
},
|
|
46
153
|
// The commonest post-action condition there is. Unmatched, it fell through to FEEDBACK_ASK and
|
|
47
154
|
// told the agent a successful click's aftermath might be a bug in Reticle.
|
|
48
155
|
{ match: /no longer resolves to an element/i, hint: RECOVERY.STALE_REF },
|
|
156
|
+
{ match: /no <option> with value/i, hint: RECOVERY.NO_SUCH_OPTION },
|
|
157
|
+
// Every guard in the browser's executeAction. These are Reticle-authored refusals of an invalid
|
|
158
|
+
// call, and ALL of them used to fall through to the feedback ask — including the destructive-action
|
|
159
|
+
// block, which carries its own retry instruction and was still telling the agent Reticle might be
|
|
160
|
+
// broken. Order matters: contenteditable and disabled/readonly are `cannot <verb> …` messages too,
|
|
161
|
+
// so they must be tested before the general wrong-target rule.
|
|
162
|
+
{ match: /contenteditable/i, hint: RECOVERY.UNSUPPORTED_SURFACE },
|
|
163
|
+
{ match: /cannot \w+ a (disabled|readonly) </i, hint: RECOVERY.NOT_EDITABLE },
|
|
164
|
+
// Three spellings ship — "action", "native action", "WebMCP tool" — and the rule matched one, so
|
|
165
|
+
// two thirds of the same deliberate refusal still read as a possible defect.
|
|
166
|
+
{
|
|
167
|
+
match: /potentially destructive (?:\w+ )*(?:action|tool) blocked/i,
|
|
168
|
+
hint: RECOVERY.CONFIRM_DANGEROUS,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
match: /^cannot [\w()]+ (?:a|an|into a|on a) <|no form to submit|upload target must be|is not an HTMLElement/i,
|
|
172
|
+
hint: RECOVERY.WRONG_TARGET,
|
|
173
|
+
},
|
|
174
|
+
// Authored by Reticle, about the caller's arguments: the message already names the valid answers.
|
|
175
|
+
{ match: /^unknown action '/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
176
|
+
{ match: /^unsupported query strategy '/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
177
|
+
{ match: /^\w+ requires a string `\w+`/i, hint: RECOVERY.BAD_ARGUMENTS },
|
|
49
178
|
// A message naming a `reticle_*` tool is one WE authored about our own API — an invalid call, not
|
|
50
179
|
// an unanticipated failure. Left unmatched it collected the feedback ask, which pushed agents to
|
|
51
180
|
// file reports about their own bad arguments. Kept LAST so a specific rule above always wins.
|
|
@@ -54,14 +183,42 @@ const RULES = [
|
|
|
54
183
|
hint: RECOVERY.BAD_ARGUMENTS,
|
|
55
184
|
},
|
|
56
185
|
];
|
|
57
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* The actionable next move for a known error message, or undefined when none is recognized.
|
|
188
|
+
*
|
|
189
|
+
* A message that already carries its own next action gets NO second one. The no-session diagnosis
|
|
190
|
+
* inspects the machine and says which of three causes this actually is; pairing that with the
|
|
191
|
+
* generic "ask the human to start their app" produced a result that contradicted itself — the error
|
|
192
|
+
* saying a server is running and the recovery saying to go start one.
|
|
193
|
+
*/
|
|
58
194
|
export function recoveryFor(message) {
|
|
195
|
+
if (isSelfRecovering(message))
|
|
196
|
+
return undefined;
|
|
59
197
|
for (const rule of RULES) {
|
|
60
198
|
if (rule.match.test(message))
|
|
61
199
|
return rule.hint;
|
|
62
200
|
}
|
|
63
201
|
return undefined;
|
|
64
202
|
}
|
|
203
|
+
/**
|
|
204
|
+
* A schema rejection: the call named a parameter that does not exist, or gave one the wrong type.
|
|
205
|
+
*
|
|
206
|
+
* Matched by the SHAPES the validators actually produce (zod's codes, and the MCP layer's wrapper),
|
|
207
|
+
* because these are our own machinery talking, not the app.
|
|
208
|
+
*/
|
|
209
|
+
const ARGUMENT_REJECTION = /Unrecognized key|unrecognized_keys|invalid_type|Invalid arguments for tool|Unknown parameters? for|^Required |Expected \w+, received/i;
|
|
210
|
+
/**
|
|
211
|
+
* What to say instead of asking for a bug report. The schema already stated the problem precisely;
|
|
212
|
+
* this only says who can fix it and where to look.
|
|
213
|
+
*/
|
|
214
|
+
const ARGUMENT_RECOVERY = "That call did not match the tool's schema — the message above names the parameter. Nothing ran, " +
|
|
215
|
+
'so no result is affected. Call reticle_tools { names: ["<tool>"] } for its exact parameters and ' +
|
|
216
|
+
'retry.';
|
|
217
|
+
/**
|
|
218
|
+
* `invalid <thing> name: <value>` — the shape every name validator in the server throws. Matched on
|
|
219
|
+
* the phrasing rather than per-caller so a new store that validates a name is covered by default.
|
|
220
|
+
*/
|
|
221
|
+
const INVALID_NAME_REJECTION = /^invalid (?:[a-z]+ )*name: /i;
|
|
65
222
|
/**
|
|
66
223
|
* The ask attached to errors we do NOT recognize. An unrecognized error is, by definition, the case
|
|
67
224
|
* where Reticle failed in a way nobody anticipated — the single highest-value moment to hear from the
|
|
@@ -79,7 +236,25 @@ export const FEEDBACK_ASK = 'This error is not one Reticle recognizes, which mea
|
|
|
79
236
|
* known; the feedback ask is added when it is NOT — the two are mutually exclusive by design, so the
|
|
80
237
|
* agent always gets exactly one next move.
|
|
81
238
|
*/
|
|
82
|
-
export function buildErrorPayload(
|
|
239
|
+
export function buildErrorPayload(rawMessage) {
|
|
240
|
+
// Cap FIRST, then classify on the capped text — so what the classifier reads is exactly what the
|
|
241
|
+
// agent reads. Classifying the full message and emitting a shorter one would let a recovery hint
|
|
242
|
+
// reference a phrase that is no longer there.
|
|
243
|
+
const message = capMessage(rawMessage);
|
|
244
|
+
// A self-diagnosing message gets NEITHER a generic recovery nor the feedback ask. It already
|
|
245
|
+
// inspected the machine and named the cause; a second, contradictory hint is noise, and inviting a
|
|
246
|
+
// bug report about a condition Reticle diagnosed itself is exactly backwards.
|
|
247
|
+
if (isSelfRecovering(message))
|
|
248
|
+
return { error: message };
|
|
249
|
+
// An argument rejection is the agent's to fix, not a Reticle defect. Reported from the field: a
|
|
250
|
+
// clean `unrecognized_keys: ["value"]` came back wrapped in "may be a defect in Reticle", which
|
|
251
|
+
// spends the agent's turn and fills the feedback channel with reports about malformed calls.
|
|
252
|
+
if (ARGUMENT_REJECTION.test(message))
|
|
253
|
+
return { error: message, recovery: ARGUMENT_RECOVERY };
|
|
254
|
+
// Reticle's own name validators rejecting a caller's value: an argument rejection that the schema
|
|
255
|
+
// cannot express (the shape is a runtime pattern, not a type), so it needs its own arm.
|
|
256
|
+
if (INVALID_NAME_REJECTION.test(message))
|
|
257
|
+
return { error: message, recovery: RECOVERY.INVALID_NAME };
|
|
83
258
|
const recovery = recoveryFor(message);
|
|
84
259
|
return recovery !== undefined
|
|
85
260
|
? { error: message, recovery }
|
|
@@ -12,7 +12,18 @@ export declare const FEEDBACK_PROMPT: {
|
|
|
12
12
|
readonly ask: "Ask the human, in your own words and only once: how did this verification go, and what would have made Reticle more useful? If they answer, file it with `reticle feedback` (they run it) or relay it yourself.";
|
|
13
13
|
readonly command: "reticle feedback";
|
|
14
14
|
};
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* Tools whose completion means "a verification just happened" — the only moment worth asking at,
|
|
17
|
+
* and the set that decides what `verification_completed` counts.
|
|
18
|
+
*
|
|
19
|
+
* ACT_AND_WAIT belongs here and was missing for a long time. It declares `verified` in its
|
|
20
|
+
* outputSchema, returns the whole verdict block, and its own description calls it "one hop for the
|
|
21
|
+
* act->observe->assert loop" — it IS the verification path most agents take. The guard that should
|
|
22
|
+
* have caught the omission keyed on the tool NAME (/assert|verify/), which act_and_wait does not
|
|
23
|
+
* match, so it slipped through in silence. Measured over a day of real telemetry: act_and_wait 14
|
|
24
|
+
* calls, assert ZERO, verification_completed 2. Agents were verifying the entire time and the metric
|
|
25
|
+
* could not see it. The contract test now checks the SHAPE instead.
|
|
26
|
+
*/
|
|
16
27
|
export declare const VERIFICATION_TOOLS: ReadonlySet<string>;
|
|
17
28
|
/**
|
|
18
29
|
* The human prompt, when one is due. Resettable for tests.
|
|
@@ -30,6 +30,9 @@ function sessionFacts(deps, sessionId) {
|
|
|
30
30
|
return {
|
|
31
31
|
...(session.runtime !== undefined ? { runtime: session.runtime } : {}),
|
|
32
32
|
...(session.engine !== undefined ? { engine: session.engine } : {}),
|
|
33
|
+
// The app's own account of what it is. Without this the reported stack came from the daemon's
|
|
34
|
+
// working directory, so feedback from one app was filed against another's framework.
|
|
35
|
+
...(0 < session.adapters.length ? { adapters: session.adapters } : {}),
|
|
33
36
|
driven,
|
|
34
37
|
};
|
|
35
38
|
}
|
|
@@ -83,7 +86,10 @@ export const FEEDBACK_TOOLS = [
|
|
|
83
86
|
.describe('Only to attach a specific tab s runtime/engine context. Omit it — a report about a session that never connected is still worth filing.'),
|
|
84
87
|
},
|
|
85
88
|
outputSchema: {
|
|
89
|
+
/** Confirmed delivery. Always false here: the agent path queues and returns. */
|
|
86
90
|
sent: z.boolean(),
|
|
91
|
+
/** Validated, redacted and queued. This is the one to read. */
|
|
92
|
+
accepted: z.boolean(),
|
|
87
93
|
reason: z.string().optional(),
|
|
88
94
|
redacted: z.array(z.string()),
|
|
89
95
|
context: z.unknown(),
|
|
@@ -106,12 +112,17 @@ export const FEEDBACK_TOOLS = [
|
|
|
106
112
|
...optionalText(args, 'impact'),
|
|
107
113
|
...optionalText(args, 'currentApproach'),
|
|
108
114
|
...optionalText(args, 'model'),
|
|
109
|
-
}, {
|
|
115
|
+
}, {
|
|
116
|
+
session: sessionFacts(deps, asString(args['sessionId'])),
|
|
117
|
+
// The AGENT does not wait for the network. `reticle feedback` (a human, at a terminal,
|
|
118
|
+
// watching for an answer) still does — see submitFeedback's `background`.
|
|
119
|
+
background: true,
|
|
120
|
+
});
|
|
110
121
|
return {
|
|
111
122
|
...receipt,
|
|
112
|
-
note: receipt.
|
|
113
|
-
? '
|
|
114
|
-
: 'NOT
|
|
123
|
+
note: receipt.accepted
|
|
124
|
+
? 'Accepted — thank you. It sends in the background so you are not kept waiting; if it fails you will be told on a later tool result. Continue your task; do not retry a failing call just to re-report it.'
|
|
125
|
+
: 'NOT accepted (see reason). Tell the human what you found so it is not lost.',
|
|
115
126
|
};
|
|
116
127
|
},
|
|
117
128
|
},
|
|
@@ -128,8 +139,20 @@ export const FEEDBACK_PROMPT = {
|
|
|
128
139
|
ask: 'Ask the human, in your own words and only once: how did this verification go, and what would have made Reticle more useful? If they answer, file it with `reticle feedback` (they run it) or relay it yourself.',
|
|
129
140
|
command: 'reticle feedback',
|
|
130
141
|
};
|
|
131
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Tools whose completion means "a verification just happened" — the only moment worth asking at,
|
|
144
|
+
* and the set that decides what `verification_completed` counts.
|
|
145
|
+
*
|
|
146
|
+
* ACT_AND_WAIT belongs here and was missing for a long time. It declares `verified` in its
|
|
147
|
+
* outputSchema, returns the whole verdict block, and its own description calls it "one hop for the
|
|
148
|
+
* act->observe->assert loop" — it IS the verification path most agents take. The guard that should
|
|
149
|
+
* have caught the omission keyed on the tool NAME (/assert|verify/), which act_and_wait does not
|
|
150
|
+
* match, so it slipped through in silence. Measured over a day of real telemetry: act_and_wait 14
|
|
151
|
+
* calls, assert ZERO, verification_completed 2. Agents were verifying the entire time and the metric
|
|
152
|
+
* could not see it. The contract test now checks the SHAPE instead.
|
|
153
|
+
*/
|
|
132
154
|
export const VERIFICATION_TOOLS = new Set([
|
|
155
|
+
ReticleTool.ACT_AND_WAIT,
|
|
133
156
|
ReticleTool.ASSERT,
|
|
134
157
|
ReticleTool.FLOW_VERIFY,
|
|
135
158
|
ReticleTool.VERIFY_CHANGE,
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import type { ToolDef, ToolDeps } from './tools.js';
|
|
2
|
-
/**
|
|
3
|
-
* The live-session tools whose result MUST carry the
|
|
4
|
-
* session-health envelope. Owned in ONE place — not retrofitted per handler — so a throttled tab
|
|
5
|
-
* can never return a healthy-looking result from any of these. `runTool` is the single choke point
|
|
6
|
-
* (mcp.ts + tool-invoker.ts) that splices health on; the guard test asserts the set is exhaustive.
|
|
7
|
-
*/
|
|
8
2
|
export declare const SESSION_BOUND_TOOLS: ReadonlySet<string>;
|
|
9
3
|
/**
|
|
10
4
|
* Tools that carry a `sessionId` arg but are NOT live-session-health tools — they read/write
|
|
@@ -2,18 +2,32 @@ import { healthEnvelope } from '../session/session-health.js';
|
|
|
2
2
|
import { TelemetryActor, TelemetryEventKind } from '@reticlehq/core';
|
|
3
3
|
import { getSessionMetrics } from '../telemetry/session-metrics.js';
|
|
4
4
|
import { getTelemetry } from '../telemetry/telemetry.js';
|
|
5
|
-
import { VERIFICATION_TOOLS } from './feedback-tools.js';
|
|
6
5
|
import { takeUpdateNudge } from '../update/update-nudge.js';
|
|
6
|
+
import { takeVersionSkew } from '../version/version-nudge.js';
|
|
7
|
+
import { noteToolCall } from '../daemon/daemon-usefulness.js';
|
|
7
8
|
import { bugsInResult } from '../telemetry/bug-found.js';
|
|
9
|
+
import { verificationOf } from '../telemetry/verification-of.js';
|
|
8
10
|
import { asString } from './tools-helpers.js';
|
|
9
11
|
import { ReticleTool } from './tool-names.js';
|
|
10
12
|
import { takeFeedbackPrompt } from './feedback-tools.js';
|
|
13
|
+
import { takeFeedbackUndelivered } from '../telemetry/feedback-delivery.js';
|
|
14
|
+
import { span } from '../trace.js';
|
|
11
15
|
/**
|
|
12
16
|
* The live-session tools whose result MUST carry the
|
|
13
17
|
* session-health envelope. Owned in ONE place — not retrofitted per handler — so a throttled tab
|
|
14
18
|
* can never return a healthy-looking result from any of these. `runTool` is the single choke point
|
|
15
19
|
* (mcp.ts + tool-invoker.ts) that splices health on; the guard test asserts the set is exhaustive.
|
|
16
20
|
*/
|
|
21
|
+
/**
|
|
22
|
+
* Tools that DRIVE the page. An action with no verdict after it is the signature of the loop
|
|
23
|
+
* breaking mid-task — see `abandonedActions`. `act_and_wait` is one of these AND a verification, so
|
|
24
|
+
* it settles its own action, which is exactly right: it is the tool that does both.
|
|
25
|
+
*/
|
|
26
|
+
const ACTION_TOOLS = new Set([
|
|
27
|
+
ReticleTool.ACT,
|
|
28
|
+
ReticleTool.ACT_SEQUENCE,
|
|
29
|
+
ReticleTool.ACT_AND_WAIT,
|
|
30
|
+
]);
|
|
17
31
|
export const SESSION_BOUND_TOOLS = new Set([
|
|
18
32
|
ReticleTool.SNAPSHOT,
|
|
19
33
|
ReticleTool.QUERY,
|
|
@@ -72,44 +86,27 @@ export const SESSION_EXEMPT_TOOLS = new Set([
|
|
|
72
86
|
ReticleTool.FEEDBACK,
|
|
73
87
|
]);
|
|
74
88
|
function isPlainObject(value) {
|
|
75
|
-
return typeof value
|
|
89
|
+
return 'object' === typeof value && value !== null && !Array.isArray(value);
|
|
76
90
|
}
|
|
77
91
|
/**
|
|
78
92
|
* Emit `verification_completed` when a verification tool produced a verdict.
|
|
79
93
|
*
|
|
80
94
|
* Read off the RESULT here rather than emitted from `decideVerified`, which is where the verdict is
|
|
81
95
|
* actually decided: that function is pure, and the repo's rules keep clocks, IO and side effects out
|
|
82
|
-
* of pure logic for good reason. Reading the result also means all
|
|
83
|
-
* by one site instead of
|
|
96
|
+
* of pure logic for good reason. Reading the result also means all the verification tools are covered
|
|
97
|
+
* by one site instead of N handler edits, and the next one is covered the moment it joins the set.
|
|
84
98
|
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
* putting in front of an investor — not "tools were called" but "this many greens were caught lying".
|
|
99
|
+
* The RULE itself lives in verification-of.ts, because getting it wrong is silent and it feeds the
|
|
100
|
+
* one number shown to investors — see that file for the two ways it was wrong.
|
|
88
101
|
*/
|
|
89
|
-
function recordVerification(toolName, result, durationMs) {
|
|
90
|
-
|
|
102
|
+
function recordVerification(toolName, result, durationMs, brand) {
|
|
103
|
+
const verification = verificationOf(toolName, result, durationMs, brand);
|
|
104
|
+
if (verification === undefined)
|
|
91
105
|
return;
|
|
92
|
-
|
|
93
|
-
// flow_verify reports `status: pass|fail`; assert reports a boolean `pass`. Accept either shape so
|
|
94
|
-
// the whole family is covered without normalizing four tools' contracts for a metric's convenience.
|
|
95
|
-
const passed = typeof result['pass'] === 'boolean'
|
|
96
|
-
? result['pass']
|
|
97
|
-
: result['status'] === 'pass'
|
|
98
|
-
? true
|
|
99
|
-
: undefined;
|
|
100
|
-
if (verified === undefined && passed === undefined)
|
|
101
|
-
return; // not a verdict-bearing result
|
|
102
|
-
const metrics = getSessionMetrics();
|
|
103
|
-
metrics.recordVerification();
|
|
106
|
+
getSessionMetrics().recordVerification();
|
|
104
107
|
void getTelemetry().emit(TelemetryEventKind.VERIFICATION_COMPLETED, {
|
|
105
108
|
actor: TelemetryActor.AGENT,
|
|
106
|
-
verification
|
|
107
|
-
via: toolName,
|
|
108
|
-
verified: verified ?? (passed === true ? 'yes' : 'no'),
|
|
109
|
-
passed: passed ?? verified === 'yes',
|
|
110
|
-
falseGreenCaught: passed === true && verified === 'no',
|
|
111
|
-
durationMs,
|
|
112
|
-
},
|
|
109
|
+
verification,
|
|
113
110
|
});
|
|
114
111
|
}
|
|
115
112
|
/**
|
|
@@ -121,7 +118,7 @@ function recordVerification(toolName, result, durationMs) {
|
|
|
121
118
|
*/
|
|
122
119
|
function reportBugsFound(toolName, result) {
|
|
123
120
|
const bugs = bugsInResult(toolName, result);
|
|
124
|
-
if (bugs.length
|
|
121
|
+
if (0 === bugs.length)
|
|
125
122
|
return;
|
|
126
123
|
const metrics = getSessionMetrics();
|
|
127
124
|
for (const bug of bugs) {
|
|
@@ -151,6 +148,12 @@ export async function runTool(tool, deps, args) {
|
|
|
151
148
|
// Started/settled as a pair rather than a bare counter: several agents can be inside runTool at
|
|
152
149
|
// once, so a single "last start" field would attribute one tool's duration to another. The returned
|
|
153
150
|
// closure carries this call's own identity, which is also what makes peak-concurrency measurable.
|
|
151
|
+
// A daemon that has served even one tool call is doing a job for somebody; see daemon-usefulness.
|
|
152
|
+
noteToolCall();
|
|
153
|
+
// An ACTION is what gets abandoned. Counted here, against verifications, so "the agent drove the
|
|
154
|
+
// page and then wandered off" becomes a number instead of an impression.
|
|
155
|
+
if (ACTION_TOOLS.has(tool.name))
|
|
156
|
+
getSessionMetrics().recordAction();
|
|
154
157
|
const settleTiming = getSessionMetrics().startToolCall(tool.name, args);
|
|
155
158
|
const startedAt = Date.now();
|
|
156
159
|
const rawSessionId = asString(args['sessionId']);
|
|
@@ -175,7 +178,10 @@ export async function runTool(tool, deps, args) {
|
|
|
175
178
|
deps.pool?.touch(leaseId);
|
|
176
179
|
let raw;
|
|
177
180
|
try {
|
|
178
|
-
|
|
181
|
+
// The one dispatch point every tool call passes through, so it is where the trace's root span
|
|
182
|
+
// belongs: with RETICLE_TRACE on, every stage that runs underneath inherits this call's id and
|
|
183
|
+
// nests under it. Free when off — see trace.ts.
|
|
184
|
+
raw = await span('tool.handler', { tool: tool.name, session: session?.id }, () => tool.handler(deps, args));
|
|
179
185
|
}
|
|
180
186
|
finally {
|
|
181
187
|
// In a `finally` so a THROWN call still settles: otherwise every failing tool would leak a
|
|
@@ -187,7 +193,10 @@ export async function runTool(tool, deps, args) {
|
|
|
187
193
|
// session-bound early return, because two of the four verification tools (flow_verify,
|
|
188
194
|
// verify_change) are session-EXEMPT and would otherwise never carry it.
|
|
189
195
|
if (isPlainObject(raw)) {
|
|
190
|
-
|
|
196
|
+
// The brand comes from the session's own PAGE_HEALTH report, so it is present for the four
|
|
197
|
+
// session-bound verification tools and absent for flow_verify (session-exempt) — which replays
|
|
198
|
+
// into a browser Reticle launched, where `browser` already says what happened.
|
|
199
|
+
recordVerification(tool.name, raw, Date.now() - startedAt, session?.brand);
|
|
191
200
|
reportBugsFound(tool.name, raw);
|
|
192
201
|
}
|
|
193
202
|
const prompt = isPlainObject(raw) ? takeFeedbackPrompt(tool.name) : undefined;
|
|
@@ -195,12 +204,28 @@ export async function runTool(tool, deps, args) {
|
|
|
195
204
|
// and ASK for is something it will never ask for. Spliced on ANY tool result, not just a
|
|
196
205
|
// verification, because an out-of-date install is worth mentioning whatever the agent is doing.
|
|
197
206
|
const update = isPlainObject(raw) ? takeUpdateNudge() : undefined;
|
|
198
|
-
|
|
207
|
+
// Same one-shot channel, for the same reason. Skew was only ever reported in
|
|
208
|
+
// reticle_sessions.versionSkew and a CLI log line — two places an agent driving a flow never
|
|
209
|
+
// looks — so it could work a whole session against a mismatched pair and never learn the one fact
|
|
210
|
+
// that explains the behaviour. It rides out here on whatever tool it happens to be calling.
|
|
211
|
+
const skew = isPlainObject(raw) ? takeVersionSkew() : undefined;
|
|
212
|
+
// A feedback report that was accepted and then failed to send. Same one-shot channel, because the
|
|
213
|
+
// reporter is the only person who can act on it and they are not reading the daemon log — and a
|
|
214
|
+
// report announced as accepted and then silently lost is the failure the awaited send existed to
|
|
215
|
+
// prevent. See feedback-delivery.ts.
|
|
216
|
+
const undelivered = isPlainObject(raw) ? takeFeedbackUndelivered() : undefined;
|
|
217
|
+
const result = prompt === undefined && update === undefined && skew === undefined && undelivered === undefined
|
|
199
218
|
? raw
|
|
200
219
|
: {
|
|
201
220
|
...raw,
|
|
202
221
|
...(prompt !== undefined ? { feedback_prompt: prompt } : {}),
|
|
203
222
|
...(update !== undefined ? { update_available: update } : {}),
|
|
223
|
+
...(skew !== undefined ? { version_skew: skew } : {}),
|
|
224
|
+
...(undelivered !== undefined
|
|
225
|
+
? {
|
|
226
|
+
feedback_undelivered: `your earlier report did NOT send: ${undelivered}. Tell the human what you found so it is not lost.`,
|
|
227
|
+
}
|
|
228
|
+
: {}),
|
|
204
229
|
};
|
|
205
230
|
if (!bound || !isPlainObject(result))
|
|
206
231
|
return result;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a leased tab did not dial in.
|
|
3
|
+
*
|
|
4
|
+
* The lease pool opens the app itself, which is what makes it the highest-value path in the product
|
|
5
|
+
* — it does not wait for the human's tab. Measured over a day: lease sessions had a median of 30
|
|
6
|
+
* tool calls and produced 46% of all bugs found, against a median of 1 call for everything else.
|
|
7
|
+
*
|
|
8
|
+
* When it fails it is almost always a PORT MISMATCH: the tab loads, and the app's SDK dials the port
|
|
9
|
+
* it was BUILT with, which need not be the daemon that issued the lease. Verified by driving it —
|
|
10
|
+
* against a matching daemon `acquire` returns ready:true and the session is driveable immediately;
|
|
11
|
+
* against a different one the tab connects to the OTHER daemon instead.
|
|
12
|
+
*
|
|
13
|
+
* The old hint asked "is <url> running with @reticlehq/core enabled?", which is the one thing that
|
|
14
|
+
* cannot be in doubt: the pool just navigated a real browser to it. Naming the true cause first is
|
|
15
|
+
* the difference between a two-minute fix and abandoning the best path Reticle has.
|
|
16
|
+
*/
|
|
17
|
+
export declare function leaseNotConnectedHint(url: string, port: number): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a leased tab did not dial in.
|
|
3
|
+
*
|
|
4
|
+
* The lease pool opens the app itself, which is what makes it the highest-value path in the product
|
|
5
|
+
* — it does not wait for the human's tab. Measured over a day: lease sessions had a median of 30
|
|
6
|
+
* tool calls and produced 46% of all bugs found, against a median of 1 call for everything else.
|
|
7
|
+
*
|
|
8
|
+
* When it fails it is almost always a PORT MISMATCH: the tab loads, and the app's SDK dials the port
|
|
9
|
+
* it was BUILT with, which need not be the daemon that issued the lease. Verified by driving it —
|
|
10
|
+
* against a matching daemon `acquire` returns ready:true and the session is driveable immediately;
|
|
11
|
+
* against a different one the tab connects to the OTHER daemon instead.
|
|
12
|
+
*
|
|
13
|
+
* The old hint asked "is <url> running with @reticlehq/core enabled?", which is the one thing that
|
|
14
|
+
* cannot be in doubt: the pool just navigated a real browser to it. Naming the true cause first is
|
|
15
|
+
* the difference between a two-minute fix and abandoning the best path Reticle has.
|
|
16
|
+
*/
|
|
17
|
+
export function leaseNotConnectedHint(url, port) {
|
|
18
|
+
return (`the leased tab loaded ${url} but never dialled this daemon (port ${String(port)}). The usual ` +
|
|
19
|
+
`cause is a PORT MISMATCH: the app's SDK connects to the port it was BUILT with, so a daemon on ` +
|
|
20
|
+
`a different port never hears from it — check the app's reticle port (\`.reticle.json\`, ` +
|
|
21
|
+
`RETICLE_PORT, or the build plugin's \`port\`) matches ${String(port)}, or start the daemon on ` +
|
|
22
|
+
`the port the app expects. If the app carries no Reticle SDK at all, run \`reticle init\` in it ` +
|
|
23
|
+
`first. The tab stays leased either way — release it with reticle_lease{action:"release"}.`);
|
|
24
|
+
}
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* Attach-only: the pool drives a browser against an already-running dev server — it never starts one.
|
|
10
10
|
*/
|
|
11
11
|
import { z } from 'zod';
|
|
12
|
-
import {
|
|
12
|
+
import { leaseNotConnectedHint } from './lease-hint.js';
|
|
13
|
+
import { RETICLE_URL_PARAM, RETICLE_DEFAULT_PORT } from '@reticlehq/core';
|
|
13
14
|
import { ReticleTool } from './tool-names.js';
|
|
14
15
|
import { asString } from './tools-helpers.js';
|
|
15
16
|
const POOL_UNAVAILABLE = 'browser pool unavailable — the lease tools need the daemon-managed pool (start Reticle via `reticle mcp`).';
|
|
@@ -58,7 +59,7 @@ export function cleanNavError(err) {
|
|
|
58
59
|
}
|
|
59
60
|
/** A fresh, collision-resistant lease id. Uses crypto at the I/O boundary (not pure logic). */
|
|
60
61
|
function newLeaseId() {
|
|
61
|
-
const uuid = typeof globalThis.crypto?.randomUUID
|
|
62
|
+
const uuid = 'function' === typeof globalThis.crypto?.randomUUID
|
|
62
63
|
? globalThis.crypto.randomUUID()
|
|
63
64
|
: `${String(Date.now())}-${String(performance.now())}`;
|
|
64
65
|
return `lease-${uuid}`;
|
|
@@ -119,7 +120,7 @@ export const LEASE_TOOLS = [
|
|
|
119
120
|
if (pool === undefined)
|
|
120
121
|
throw new Error(POOL_UNAVAILABLE);
|
|
121
122
|
const url = asString(args['url']);
|
|
122
|
-
if (url === undefined || url.length
|
|
123
|
+
if (url === undefined || 0 === url.length)
|
|
123
124
|
throw new Error('reticle_lease{action:"acquire"} requires a url');
|
|
124
125
|
const projectId = asString(args['projectId']);
|
|
125
126
|
const sessionId = newLeaseId();
|
|
@@ -144,7 +145,7 @@ export const LEASE_TOOLS = [
|
|
|
144
145
|
...(ready
|
|
145
146
|
? {}
|
|
146
147
|
: {
|
|
147
|
-
hint:
|
|
148
|
+
hint: leaseNotConnectedHint(url, deps.bridgePort ?? RETICLE_DEFAULT_PORT),
|
|
148
149
|
}),
|
|
149
150
|
};
|
|
150
151
|
},
|
|
@@ -166,7 +167,7 @@ export const LEASE_TOOLS = [
|
|
|
166
167
|
if (pool === undefined)
|
|
167
168
|
throw new Error(POOL_UNAVAILABLE);
|
|
168
169
|
const sessionId = asString(args['sessionId']);
|
|
169
|
-
if (sessionId === undefined || sessionId.length
|
|
170
|
+
if (sessionId === undefined || 0 === sessionId.length) {
|
|
170
171
|
throw new Error('reticle_lease{action:"release"} requires a sessionId');
|
|
171
172
|
}
|
|
172
173
|
await pool.release(sessionId);
|
|
@@ -16,11 +16,13 @@ import type { ToolDef } from './tool-kit.js';
|
|
|
16
16
|
* like flow_replay/flow_verify/flow_heal that return their own documented verdict shapes — are NOT
|
|
17
17
|
* merged. Only sibling families whose value is the capability, not the contract, are consolidated.
|
|
18
18
|
*/
|
|
19
|
-
|
|
19
|
+
interface MergeSpec {
|
|
20
20
|
name: string;
|
|
21
21
|
description: string;
|
|
22
22
|
/** action value → the original tool whose handler serves it. */
|
|
23
23
|
actions: Record<string, ToolDef>;
|
|
24
|
+
/** A concrete call, carried through to the merged def (see ToolDef.example). */
|
|
25
|
+
example?: Record<string, unknown>;
|
|
24
26
|
}
|
|
25
27
|
/** A merge declared by member NAME, resolved against the assembled tool list. */
|
|
26
28
|
export interface MergePlan {
|
|
@@ -28,6 +30,12 @@ export interface MergePlan {
|
|
|
28
30
|
description: string;
|
|
29
31
|
/** action value → the existing tool name whose handler serves it. */
|
|
30
32
|
members: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* A concrete call for the merged tool. Merging discards the members' own examples — the schema is
|
|
35
|
+
* a union and theirs are per-member — so a merged tool that reaches the core surface has no
|
|
36
|
+
* example at all unless the plan states one.
|
|
37
|
+
*/
|
|
38
|
+
example?: Record<string, unknown>;
|
|
31
39
|
}
|
|
32
40
|
/**
|
|
33
41
|
* Apply the consolidation to an assembled tool list: drop each plan's members and each retired name from
|
|
@@ -40,3 +48,4 @@ export interface MergePlan {
|
|
|
40
48
|
*/
|
|
41
49
|
export declare function applyMerges(tools: readonly ToolDef[], plans: readonly MergePlan[], retired?: readonly string[]): ToolDef[];
|
|
42
50
|
export declare function mergeTools(spec: MergeSpec): ToolDef;
|
|
51
|
+
export {};
|