@reticlehq/server 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/SKILL.md +16 -8
- package/dist/chromium-launch-options.d.ts +3 -1
- package/dist/chromium-launch-options.js +2 -1
- package/dist/command/cli/adhoc-verdict.d.ts +1 -0
- package/dist/command/cli/adhoc-verdict.js +6 -3
- package/dist/command/cli/answers/open-note.d.ts +2 -0
- package/dist/command/cli/answers/open-note.js +22 -0
- package/dist/command/cli/cli-flow-commands.js +4 -8
- package/dist/command/cli/cli-parse.d.ts +1 -1
- package/dist/command/cli/cli-parse.js +10 -10
- package/dist/command/cli/cli-verify.d.ts +1 -1
- package/dist/command/cli/cli-verify.js +35 -25
- package/dist/command/cli/cloud-cli.js +51 -12
- package/dist/command/cli/cloud-login.d.ts +8 -0
- package/dist/command/cli/cloud-login.js +36 -2
- package/dist/command/cli/demo/demo-page.js +11 -3
- package/dist/command/cli/doctor/browser/chromium-hint.d.ts +46 -0
- package/dist/command/cli/doctor/browser/chromium-hint.js +99 -1
- package/dist/command/cli/drive/drive-attach.d.ts +27 -2
- package/dist/command/cli/drive/drive-attach.js +71 -4
- package/dist/command/cli/drive/drive-command.d.ts +2 -1
- package/dist/command/cli/drive/drive-command.js +13 -7
- package/dist/command/cli/lifecycle/daemon-lifecycle.js +4 -1
- package/dist/command/cli/ports/port-holder.d.ts +10 -0
- package/dist/command/cli/ports/port-holder.js +24 -0
- package/dist/command/cli/setup-mcp-cli.js +6 -4
- package/dist/command/cli/tutorial.d.ts +2 -2
- package/dist/command/cli/tutorial.js +6 -9
- package/dist/command/cli.d.ts +6 -0
- package/dist/command/cli.js +28 -22
- package/dist/command/daemon/binding/daemon-status-probe.d.ts +2 -0
- package/dist/command/daemon/binding/daemon-status-probe.js +1 -0
- package/dist/command/daemon/daemon-resolve.d.ts +14 -0
- package/dist/command/daemon/daemon-resolve.js +33 -2
- package/dist/command/setup/agent-writer.d.ts +1 -1
- package/dist/command/setup/agent-writer.js +3 -2
- package/dist/command/setup/bringup/connect-lease.d.ts +1 -0
- package/dist/command/setup/bringup/connect-lease.js +1 -0
- package/dist/command/setup/bringup/dev-server-wait.d.ts +8 -0
- package/dist/command/setup/bringup/dev-server-wait.js +19 -0
- package/dist/command/setup/init/init-port.d.ts +4 -0
- package/dist/command/setup/init/init-port.js +10 -0
- package/dist/command/setup/init/init-runtime.d.ts +10 -0
- package/dist/command/setup/init/init-runtime.js +18 -1
- package/dist/command/setup/node-effects.js +2 -1
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +5 -0
- package/dist/command/setup/run-setup.js +35 -27
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/version/version-skew.js +11 -2
- package/dist/features/crawl/crawl-tools.js +4 -0
- package/dist/features/crawl/crawl.d.ts +6 -0
- package/dist/features/crawl/crawl.js +9 -1
- package/dist/features/exhaust/ledger.d.ts +7 -0
- package/dist/features/exhaust/ledger.js +35 -9
- package/dist/features/harness/app-findings.d.ts +2 -0
- package/dist/features/harness/app-findings.js +52 -0
- package/dist/features/harness/drive-report.d.ts +27 -1
- package/dist/features/harness/drive-report.js +162 -11
- package/dist/features/harness/drivers.d.ts +10 -14
- package/dist/features/harness/drivers.js +10 -14
- package/dist/features/harness/fill-values.d.ts +3 -73
- package/dist/features/harness/fill-values.js +3 -169
- package/dist/features/harness/goals.d.ts +21 -0
- package/dist/features/harness/goals.js +77 -0
- package/dist/features/harness/harness.d.ts +21 -0
- package/dist/features/harness/harness.js +69 -2
- package/dist/features/harness/platform/drive-target.d.ts +30 -0
- package/dist/features/harness/platform/drive-target.js +81 -0
- package/dist/features/harness/platform/local-apps.d.ts +94 -0
- package/dist/features/harness/platform/local-apps.js +204 -0
- package/dist/features/harness/platform/remote-drive.d.ts +180 -0
- package/dist/features/harness/platform/remote-drive.js +365 -0
- package/dist/features/harness/platform/script.d.ts +48 -0
- package/dist/features/harness/platform/script.js +128 -0
- package/dist/features/harness/platform/server-driver.d.ts +41 -0
- package/dist/features/harness/platform/server-driver.js +179 -0
- package/dist/features/harness/platform-config.d.ts +7 -0
- package/dist/features/harness/platform-config.js +14 -2
- package/dist/features/harness/script/app-controls.d.ts +5 -0
- package/dist/features/harness/script/app-controls.js +40 -0
- package/dist/features/harness/script/lane-ids.d.ts +14 -0
- package/dist/features/harness/script/lane-ids.js +16 -0
- package/dist/features/harness/script/script-run.d.ts +41 -0
- package/dist/features/harness/script/script-run.js +299 -0
- package/dist/features/license/license.d.ts +2 -2
- package/dist/features/visual/visual-tools.d.ts +10 -1
- package/dist/features/visual/visual-tools.js +40 -3
- package/dist/hooks/driven-by.d.ts +4 -0
- package/dist/hooks/driven-by.js +16 -0
- package/dist/hooks/hook-commands.js +15 -4
- package/dist/hooks/tool-log.d.ts +9 -0
- package/dist/hooks/tool-log.js +29 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +76 -82
- package/dist/judgement/runs/artifact/build-verification-run.js +6 -0
- package/dist/judgement/runs/artifact/replay-mapping.d.ts +1 -1
- package/dist/judgement/runs/artifact/replay-mapping.js +16 -6
- package/dist/judgement/runs/artifact/run-store.d.ts +1 -1
- package/dist/judgement/runs/artifact/run-store.js +26 -1
- package/dist/judgement/runs/drive-run.d.ts +22 -0
- package/dist/judgement/runs/drive-run.js +99 -1
- package/dist/judgement/runs/repair-prompt.js +1 -1
- package/dist/judgement/runs/reticle-runner.js +8 -1
- package/dist/judgement/runs/verification-sync.d.ts +3 -2
- package/dist/judgement/runs/verification-sync.js +4 -3
- package/dist/language/flows/change/gate.d.ts +10 -0
- package/dist/language/flows/change/gate.js +22 -0
- package/dist/language/flows/decision.js +34 -0
- package/dist/language/flows/flow-anchor.d.ts +54 -8
- package/dist/language/flows/flow-anchor.js +88 -3
- package/dist/language/flows/flow-cross-step.d.ts +15 -0
- package/dist/language/flows/flow-cross-step.js +21 -0
- package/dist/language/flows/flow-learning.js +8 -11
- package/dist/language/flows/flow-mutate-tools.js +5 -3
- package/dist/language/flows/flow-replay-run.d.ts +23 -9
- package/dist/language/flows/flow-replay-run.js +101 -94
- package/dist/language/flows/flow-replay.d.ts +23 -1
- package/dist/language/flows/flow-replay.js +92 -19
- package/dist/language/flows/flow-scope.js +5 -1
- package/dist/language/flows/flow-step-runners.d.ts +20 -2
- package/dist/language/flows/flow-step-runners.js +236 -42
- package/dist/language/flows/flow-store-for-session.d.ts +2 -2
- package/dist/language/flows/flow-store-for-session.js +2 -2
- package/dist/language/flows/flow-tools.js +58 -34
- package/dist/language/flows/flows.js +5 -2
- package/dist/language/flows/heal-run.js +6 -4
- package/dist/language/flows/recording/flake.d.ts +2 -2
- package/dist/language/flows/recording/tape/recordings.d.ts +30 -4
- package/dist/language/flows/recording/tape/recordings.js +40 -8
- package/dist/language/flows/replay-from-hud.d.ts +20 -0
- package/dist/language/flows/replay-from-hud.js +51 -0
- package/dist/language/flows/replay.d.ts +9 -5
- package/dist/language/flows/replay.js +27 -9
- package/dist/language/flows/suite/suite-flakes.js +2 -0
- package/dist/language/flows/suite-selection.d.ts +2 -15
- package/dist/language/flows/suite-selection.js +4 -3
- package/dist/language/flows/verify-change-tools.js +8 -4
- package/dist/launch-chromium.d.ts +27 -5
- package/dist/launch-chromium.js +38 -8
- package/dist/memory/cloud/cloud-config.d.ts +12 -0
- package/dist/memory/cloud/cloud-config.js +15 -0
- package/dist/memory/cloud/cloud-sync.d.ts +17 -14
- package/dist/memory/cloud/cloud-sync.js +12 -1
- package/dist/memory/cloud/harness-config.d.ts +26 -1
- package/dist/memory/cloud/harness-config.js +99 -4
- package/dist/memory/cloud/hud-notices-source.d.ts +21 -0
- package/dist/memory/cloud/hud-notices-source.js +128 -0
- package/dist/memory/cloud/memory-scope.d.ts +144 -0
- package/dist/memory/cloud/memory-scope.js +205 -0
- package/dist/memory/cloud/sync-cycle.d.ts +12 -3
- package/dist/memory/cloud/sync-cycle.js +16 -3
- package/dist/memory/cloud/sync-daemon.js +7 -5
- package/dist/memory/cloud/sync-disk.d.ts +1 -1
- package/dist/memory/cloud/sync-disk.js +5 -8
- package/dist/memory/impact/account-watch.d.ts +19 -0
- package/dist/memory/impact/account-watch.js +34 -0
- package/dist/memory/impact/impact-recorder.d.ts +8 -0
- package/dist/memory/impact/impact-recorder.js +12 -2
- package/dist/memory/impact/impact-store.d.ts +16 -0
- package/dist/memory/impact/impact-store.js +142 -26
- package/dist/memory/intent/intent-shard.d.ts +2 -2
- package/dist/memory/intent/intent-tools.js +80 -2
- package/dist/memory/journal/envelope.d.ts +8 -8
- package/dist/memory/journal/journal-recorder.js +7 -1
- package/dist/memory/journal/on-disk/retention.d.ts +3 -0
- package/dist/memory/journal/on-disk/retention.js +5 -0
- package/dist/memory/journal/on-disk/startup-maintenance.js +3 -1
- package/dist/memory/journal/on-disk/workspace-tiers.js +7 -4
- package/dist/memory/journal/session-end.js +6 -4
- package/dist/memory/project/artifact-root-resolver.d.ts +26 -1
- package/dist/memory/project/artifact-root-resolver.js +94 -13
- package/dist/memory/project/artifact-root.d.ts +42 -5
- package/dist/memory/project/artifact-root.js +87 -8
- package/dist/memory/project/dir/reticle-dir.d.ts +4 -2
- package/dist/memory/project/dir/reticle-dir.js +3 -1
- package/dist/memory/project/project-tools.js +22 -3
- package/dist/memory/project/session-root.d.ts +24 -1
- package/dist/memory/project/session-root.js +41 -5
- package/dist/memory/project/sync-status.d.ts +39 -0
- package/dist/memory/project/sync-status.js +90 -0
- package/dist/memory/recall/memory-tools.js +2 -0
- package/dist/memory/recall/project-memory.d.ts +19 -0
- package/dist/memory/recall/project-memory.js +45 -6
- package/dist/portal/bridge/bridge.d.ts +3 -1
- package/dist/portal/bridge/bridge.js +19 -2
- package/dist/portal/bridge/pairing-token.d.ts +9 -0
- package/dist/portal/bridge/pairing-token.js +17 -0
- package/dist/portal/input/real-input.d.ts +81 -10
- package/dist/portal/input/real-input.js +207 -22
- package/dist/portal/input/viewport-tools.js +3 -3
- package/dist/portal/pool/browser-pool.d.ts +22 -9
- package/dist/portal/pool/browser-pool.js +70 -35
- package/dist/portal/pool/playwright-launcher.d.ts +13 -0
- package/dist/portal/pool/playwright-launcher.js +17 -1
- package/dist/portal/pool/pool-contract.d.ts +7 -4
- package/dist/portal/session/command-payload.d.ts +8 -0
- package/dist/portal/session/command-payload.js +19 -0
- package/dist/portal/session/dev-server/dev-command.d.ts +12 -0
- package/dist/portal/session/dev-server/dev-command.js +61 -12
- package/dist/portal/session/facts/departure-note.d.ts +22 -0
- package/dist/portal/session/facts/departure-note.js +47 -0
- package/dist/portal/session/facts/session-replaced.d.ts +5 -0
- package/dist/portal/session/facts/session-replaced.js +5 -0
- package/dist/portal/session/first-run-wiring.d.ts +42 -0
- package/dist/portal/session/first-run-wiring.js +132 -0
- package/dist/portal/session/human/notes-ledger.d.ts +20 -0
- package/dist/portal/session/human/notes-ledger.js +55 -0
- package/dist/portal/session/human/review-store.d.ts +12 -0
- package/dist/portal/session/human/review-store.js +18 -10
- package/dist/portal/session/live-control-tools.d.ts +0 -13
- package/dist/portal/session/live-control-tools.js +28 -3
- package/dist/portal/session/no-session-diagnosis.d.ts +7 -0
- package/dist/portal/session/no-session-diagnosis.js +15 -0
- package/dist/portal/session/no-session-next-action.d.ts +11 -0
- package/dist/portal/session/no-session-next-action.js +31 -5
- package/dist/portal/session/no-session-watch.d.ts +14 -1
- package/dist/portal/session/no-session-watch.js +63 -10
- package/dist/portal/session/presence/session-mirror.js +3 -0
- package/dist/portal/session/serving-directory.d.ts +27 -0
- package/dist/portal/session/serving-directory.js +45 -0
- package/dist/portal/session/session-health.d.ts +7 -0
- package/dist/portal/session/session-health.js +6 -0
- package/dist/portal/session/session-info.d.ts +8 -1
- package/dist/portal/session/session-info.js +6 -0
- package/dist/portal/session/session-manager.d.ts +21 -0
- package/dist/portal/session/session-manager.js +44 -8
- package/dist/portal/session/session-reaper.js +4 -1
- package/dist/portal/session/session-successor.d.ts +5 -0
- package/dist/portal/session/session-tools.js +26 -3
- package/dist/portal/session/session.d.ts +7 -2
- package/dist/portal/session/session.js +21 -18
- package/dist/start-options.d.ts +6 -0
- package/dist/status-payload.d.ts +10 -0
- package/dist/status-payload.js +23 -0
- package/dist/surface/http-server.js +11 -1
- package/dist/surface/mcp/mcp-proxy.d.ts +5 -0
- package/dist/surface/mcp/mcp-proxy.js +36 -5
- package/dist/surface/mcp/mcp.d.ts +11 -6
- package/dist/surface/mcp/mcp.js +27 -13
- package/dist/surface/mcp/server-instructions.d.ts +4 -2
- package/dist/surface/mcp/server-instructions.js +14 -4
- package/dist/surface/mcp/shared-params.js +2 -2
- package/dist/surface/tools/act/navigation/navigate-arrival.d.ts +8 -0
- package/dist/surface/tools/act/navigation/navigate-arrival.js +56 -2
- package/dist/surface/tools/act/settle-in-flight.js +25 -3
- package/dist/surface/tools/act-sequence-tool.js +2 -2
- package/dist/surface/tools/act-tools.js +23 -7
- package/dist/surface/tools/assert/assert-verdict.d.ts +1 -1
- package/dist/surface/tools/assert/assert-verdict.js +17 -2
- package/dist/surface/tools/browser-tools.js +8 -1
- package/dist/surface/tools/chat-drives.d.ts +26 -0
- package/dist/surface/tools/chat-drives.js +165 -0
- package/dist/surface/tools/empty-session-list.d.ts +41 -0
- package/dist/surface/tools/empty-session-list.js +80 -0
- package/dist/surface/tools/error-recovery.d.ts +6 -0
- package/dist/surface/tools/error-recovery.js +37 -4
- package/dist/surface/tools/explore-tools.d.ts +10 -1
- package/dist/surface/tools/explore-tools.js +82 -13
- package/dist/surface/tools/feedback-tools.js +14 -0
- package/dist/surface/tools/harness-explore.d.ts +96 -14
- package/dist/surface/tools/harness-explore.js +227 -206
- package/dist/surface/tools/harness-plan.d.ts +12 -0
- package/dist/surface/tools/harness-plan.js +31 -0
- package/dist/surface/tools/harness-script.d.ts +8 -0
- package/dist/surface/tools/harness-script.js +342 -0
- package/dist/surface/tools/harness-toolset.d.ts +14 -1
- package/dist/surface/tools/harness-toolset.js +77 -6
- package/dist/surface/tools/invoke-tool.js +52 -1
- package/dist/surface/tools/lease-seed.d.ts +29 -0
- package/dist/surface/tools/lease-seed.js +123 -0
- package/dist/surface/tools/lease-tools.d.ts +3 -23
- package/dist/surface/tools/lease-tools.js +30 -131
- package/dist/surface/tools/live-call-text.js +2 -2
- package/dist/surface/tools/next-step.d.ts +19 -0
- package/dist/surface/tools/next-step.js +159 -0
- package/dist/surface/tools/observe-tools.js +2 -4
- package/dist/surface/tools/platform-moment.d.ts +12 -0
- package/dist/surface/tools/platform-moment.js +168 -0
- package/dist/surface/tools/query-hint-schema.d.ts +7 -0
- package/dist/surface/tools/query-hint-schema.js +10 -0
- package/dist/surface/tools/read-tools.js +8 -2
- package/dist/surface/tools/real-input-attempt.js +96 -13
- package/dist/surface/tools/tool-kit.d.ts +15 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/telemetry.d.ts +12 -0
- package/dist/telemetry/telemetry.js +43 -14
- package/dist/telemetry/verification-of.js +1 -0
- package/dist/wire-journal.js +4 -1
- package/docs/README.md +1 -1
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/doctor.mdx +6 -0
- package/docs/cli/drive.mdx +8 -2
- package/docs/cli/init.mdx +11 -13
- package/docs/cli/status.mdx +5 -1
- package/docs/cli/version.mdx +3 -3
- package/docs/cli.mdx +1 -1
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +13 -11
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +22 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +17 -7
- package/docs/telemetry.md +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -2
- package/docs/what-is-recorded.md +13 -3
- package/docs/what-is-reticle.mdx +3 -1
- package/package.json +8 -12
- package/dist/features/harness/driver.d.ts +0 -50
- package/dist/features/harness/driver.js +0 -236
- package/dist/features/harness/jev-driver.d.ts +0 -188
- package/dist/features/harness/jev-driver.js +0 -678
- package/dist/features/harness/openai-driver.d.ts +0 -58
- package/dist/features/harness/openai-driver.js +0 -206
- package/dist/memory/project/dir/fill-value-store.d.ts +0 -32
- package/dist/memory/project/dir/fill-value-store.js +0 -88
- package/docs/local-registry.md +0 -143
|
@@ -1,77 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What to type into a field,
|
|
2
|
+
* What to type into a text field, from its label alone.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and every decision it makes (which tool, which element, what should follow) is a decision, not a
|
|
7
|
-
* composition. But a text field is a composition. "A card number", "a search term that returns
|
|
8
|
-
* results", "a description a human would write" cannot be enumerated from the page, and a label-keyed
|
|
9
|
-
* table of guesses is what stands in for them today.
|
|
10
|
-
*
|
|
11
|
-
* ## Why this is escalation rather than partnership
|
|
12
|
-
*
|
|
13
|
-
* Put a generating model in the per-turn loop and you pay its per-turn tax on every step, which is
|
|
14
|
-
* the cost the cheap driver exists to avoid. Typing is a small minority of what a drive does —
|
|
15
|
-
* clicks and navigations are the overwhelming majority — so the model is called for the few steps
|
|
16
|
-
* that compose text, never for the many that select, and a drive with no text fields never calls it
|
|
17
|
-
* at all.
|
|
18
|
-
*
|
|
19
|
-
* ## Why it is cached, and cached where it is
|
|
20
|
-
*
|
|
21
|
-
* A generated value is paid for ONCE and then belongs to the project: the same field on the same
|
|
22
|
-
* app wants the same value tomorrow, and a replay must send exactly what the recording sent or it
|
|
23
|
-
* is not a replay. Writing them to `.reticle` makes the second drive free, makes the values
|
|
24
|
-
* reviewable by a human who can correct a bad one by hand, and keeps them out of the model's way
|
|
25
|
-
* entirely. It is the same economics as the flows themselves: pay once, replay forever.
|
|
26
|
-
*
|
|
27
|
-
* ## What it must never do
|
|
28
|
-
*
|
|
29
|
-
* Fill a slot, never propose a call. The driver decides what to click and what to claim; this
|
|
30
|
-
* answers one question — "what text goes in this box" — and a wrong answer costs one act, not a
|
|
31
|
-
* wrong verdict. Nothing here touches what is judged.
|
|
32
|
-
*/
|
|
33
|
-
import { type HarnessDriverOptions } from './driver.js';
|
|
34
|
-
import { type OpenAiDriverOptions } from './openai-driver.js';
|
|
35
|
-
/** What the caller must be able to do for values to survive past this drive. */
|
|
36
|
-
export interface FillCache {
|
|
37
|
-
get(label: string): string | undefined;
|
|
38
|
-
set(label: string, value: string): void;
|
|
39
|
-
}
|
|
40
|
-
/**
|
|
41
|
-
* The fallback, and still the answer for most fields.
|
|
42
|
-
*
|
|
43
|
-
* An exploration drive needs PLAUSIBLE input, not creative input: the engine is judging what the app
|
|
44
|
-
* did with the value, not the value. A field that rejects everything here is itself worth finding,
|
|
45
|
-
* and shows up as an act that did not settle.
|
|
4
|
+
* Used by the crawl, which fills a field to see what the app does with it and never needs the value
|
|
5
|
+
* to be clever. The Harness fills fields on the platform, where its engine decides them.
|
|
46
6
|
*/
|
|
47
7
|
export declare function heuristicFillValue(label: string): string;
|
|
48
|
-
/**
|
|
49
|
-
* Whether a label is worth spending a model call on.
|
|
50
|
-
*
|
|
51
|
-
* The heuristic is RIGHT about an email box and an amount box, and a generated "john@example.com"
|
|
52
|
-
* is not an improvement on `harness@reticle.dev`. What it cannot do is a field whose label it does
|
|
53
|
-
* not recognise at all, where it falls back to typing the words "reticle harness" into whatever the
|
|
54
|
-
* app asked for. That fallback is the whole of what generation is for.
|
|
55
|
-
*/
|
|
56
|
-
export declare function needsGeneration(label: string): boolean;
|
|
57
|
-
/**
|
|
58
|
-
* The seam the driver calls: a label in, a value out, always.
|
|
59
|
-
*
|
|
60
|
-
* Three layers, cheapest first, and the expensive one is skipped entirely unless the two above it
|
|
61
|
-
* have nothing better than "type the words reticle harness into this box".
|
|
62
|
-
*/
|
|
63
|
-
export declare function fillValues(deps: {
|
|
64
|
-
cache?: FillCache;
|
|
65
|
-
/** GPT, the generator a Jev drive pairs with. Asked ahead of `generator` when both are set. */
|
|
66
|
-
openai?: OpenAiDriverOptions;
|
|
67
|
-
/** Anthropic, for a machine that holds only that key. */
|
|
68
|
-
generator?: HarnessDriverOptions;
|
|
69
|
-
/** One line about where the drive is, so a value suits the page it is typed on. */
|
|
70
|
-
context?: () => string;
|
|
71
|
-
/**
|
|
72
|
-
* A value the user SUPPLIED for this field — the same `RETICLE_SECRET_<FIELD>` replay reads for
|
|
73
|
-
* a redacted one. Checked first so a drive gets past a login, and never cached: the cache is a
|
|
74
|
-
* file on disk.
|
|
75
|
-
*/
|
|
76
|
-
secret?: (label: string) => string | undefined;
|
|
77
|
-
}): (label: string) => Promise<string>;
|
|
@@ -1,55 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* What to type into a field,
|
|
2
|
+
* What to type into a text field, from its label alone.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* and every decision it makes (which tool, which element, what should follow) is a decision, not a
|
|
7
|
-
* composition. But a text field is a composition. "A card number", "a search term that returns
|
|
8
|
-
* results", "a description a human would write" cannot be enumerated from the page, and a label-keyed
|
|
9
|
-
* table of guesses is what stands in for them today.
|
|
10
|
-
*
|
|
11
|
-
* ## Why this is escalation rather than partnership
|
|
12
|
-
*
|
|
13
|
-
* Put a generating model in the per-turn loop and you pay its per-turn tax on every step, which is
|
|
14
|
-
* the cost the cheap driver exists to avoid. Typing is a small minority of what a drive does —
|
|
15
|
-
* clicks and navigations are the overwhelming majority — so the model is called for the few steps
|
|
16
|
-
* that compose text, never for the many that select, and a drive with no text fields never calls it
|
|
17
|
-
* at all.
|
|
18
|
-
*
|
|
19
|
-
* ## Why it is cached, and cached where it is
|
|
20
|
-
*
|
|
21
|
-
* A generated value is paid for ONCE and then belongs to the project: the same field on the same
|
|
22
|
-
* app wants the same value tomorrow, and a replay must send exactly what the recording sent or it
|
|
23
|
-
* is not a replay. Writing them to `.reticle` makes the second drive free, makes the values
|
|
24
|
-
* reviewable by a human who can correct a bad one by hand, and keeps them out of the model's way
|
|
25
|
-
* entirely. It is the same economics as the flows themselves: pay once, replay forever.
|
|
26
|
-
*
|
|
27
|
-
* ## What it must never do
|
|
28
|
-
*
|
|
29
|
-
* Fill a slot, never propose a call. The driver decides what to click and what to claim; this
|
|
30
|
-
* answers one question — "what text goes in this box" — and a wrong answer costs one act, not a
|
|
31
|
-
* wrong verdict. Nothing here touches what is judged.
|
|
32
|
-
*/
|
|
33
|
-
import { DEFAULT_HARNESS_BASE_URL, } from './driver.js';
|
|
34
|
-
import { DEFAULT_OPENAI_MODEL, OPENAI_REASONING_EFFORT, openAiCompletionsUrl, } from './openai-driver.js';
|
|
35
|
-
/** Deliberately small. A field value is a phrase; anything longer is the model misunderstanding. */
|
|
36
|
-
const MAX_TOKENS = 64;
|
|
37
|
-
const ANTHROPIC_VERSION = '2023-06-01';
|
|
38
|
-
const MESSAGES_PATH = '/v1/messages';
|
|
39
|
-
/**
|
|
40
|
-
* How many values one drive will generate.
|
|
41
|
-
*
|
|
42
|
-
* The cache makes the steady state nearly free, but the FIRST drive on a form-heavy app is the one
|
|
43
|
-
* that pays, and an uncapped loop on a page with forty inputs is a bill nobody agreed to. Past the
|
|
44
|
-
* cap the heuristic answers, which is what every drive used before this existed.
|
|
45
|
-
*/
|
|
46
|
-
const MAX_PER_DRIVE = 12;
|
|
47
|
-
/**
|
|
48
|
-
* The fallback, and still the answer for most fields.
|
|
49
|
-
*
|
|
50
|
-
* An exploration drive needs PLAUSIBLE input, not creative input: the engine is judging what the app
|
|
51
|
-
* did with the value, not the value. A field that rejects everything here is itself worth finding,
|
|
52
|
-
* and shows up as an act that did not settle.
|
|
4
|
+
* Used by the crawl, which fills a field to see what the app does with it and never needs the value
|
|
5
|
+
* to be clever. The Harness fills fields on the platform, where its engine decides them.
|
|
53
6
|
*/
|
|
54
7
|
export function heuristicFillValue(label) {
|
|
55
8
|
const l = label.toLowerCase();
|
|
@@ -71,122 +24,3 @@ export function heuristicFillValue(label) {
|
|
|
71
24
|
return '42';
|
|
72
25
|
return 'reticle harness';
|
|
73
26
|
}
|
|
74
|
-
/**
|
|
75
|
-
* Whether a label is worth spending a model call on.
|
|
76
|
-
*
|
|
77
|
-
* The heuristic is RIGHT about an email box and an amount box, and a generated "john@example.com"
|
|
78
|
-
* is not an improvement on `harness@reticle.dev`. What it cannot do is a field whose label it does
|
|
79
|
-
* not recognise at all, where it falls back to typing the words "reticle harness" into whatever the
|
|
80
|
-
* app asked for. That fallback is the whole of what generation is for.
|
|
81
|
-
*/
|
|
82
|
-
export function needsGeneration(label) {
|
|
83
|
-
return heuristicFillValue(label) === heuristicFillValue('');
|
|
84
|
-
}
|
|
85
|
-
const PROMPT = (label, context) => `A test harness is filling in a form field on a web application so it can check what the app does with the input.\n\n` +
|
|
86
|
-
`The field is labelled: "${label}"\n` +
|
|
87
|
-
`${context}\n\n` +
|
|
88
|
-
`Reply with ONLY the value to type. No quotes, no explanation, no markdown. It must be something a ` +
|
|
89
|
-
`real user would plausibly enter, and short. If the field wants an identifier or a code, invent a ` +
|
|
90
|
-
`well-formed one.`;
|
|
91
|
-
/**
|
|
92
|
-
* A model's one-line answer as a field value, or nothing. A model asked for one value occasionally
|
|
93
|
-
* answers with a sentence about the value; anything long enough to be prose is not a field value,
|
|
94
|
-
* and typing prose proves nothing about the app.
|
|
95
|
-
*/
|
|
96
|
-
function asValue(raw) {
|
|
97
|
-
const text = raw?.trim().split('\n')[0];
|
|
98
|
-
if (text === undefined || 0 === text.length)
|
|
99
|
-
return undefined;
|
|
100
|
-
return 80 < text.length ? undefined : text.replace(/^["']|["']$/g, '');
|
|
101
|
-
}
|
|
102
|
-
/** One string from GPT over Chat Completions — directly, or through the platform with its key. */
|
|
103
|
-
async function generateWithOpenAi(options, label, context) {
|
|
104
|
-
const call = options.fetch ?? ((url, init) => fetch(url, init));
|
|
105
|
-
try {
|
|
106
|
-
const res = await call(openAiCompletionsUrl(options), {
|
|
107
|
-
method: 'POST',
|
|
108
|
-
headers: { 'content-type': 'application/json', authorization: `Bearer ${options.apiKey}` },
|
|
109
|
-
body: JSON.stringify({
|
|
110
|
-
model: options.model ?? DEFAULT_OPENAI_MODEL,
|
|
111
|
-
max_completion_tokens: MAX_TOKENS,
|
|
112
|
-
reasoning_effort: OPENAI_REASONING_EFFORT,
|
|
113
|
-
messages: [{ role: 'user', content: PROMPT(label, context) }],
|
|
114
|
-
}),
|
|
115
|
-
});
|
|
116
|
-
if (!res.ok)
|
|
117
|
-
return undefined;
|
|
118
|
-
const parsed = JSON.parse(await res.text());
|
|
119
|
-
return asValue(parsed.choices?.[0]?.message?.content ?? undefined);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/** One string from a generating model, or nothing. Never throws: a drive must not fail over a field. */
|
|
126
|
-
async function generateOne(options, label, context) {
|
|
127
|
-
const call = options.fetch ?? ((url, init) => fetch(url, init));
|
|
128
|
-
const baseUrl = options.baseUrl ?? DEFAULT_HARNESS_BASE_URL;
|
|
129
|
-
try {
|
|
130
|
-
const res = await call(`${baseUrl}${MESSAGES_PATH}`, {
|
|
131
|
-
method: 'POST',
|
|
132
|
-
headers: {
|
|
133
|
-
'content-type': 'application/json',
|
|
134
|
-
'x-api-key': options.apiKey,
|
|
135
|
-
'anthropic-version': ANTHROPIC_VERSION,
|
|
136
|
-
},
|
|
137
|
-
body: JSON.stringify({
|
|
138
|
-
model: options.model ?? 'claude-sonnet-5',
|
|
139
|
-
max_tokens: MAX_TOKENS,
|
|
140
|
-
messages: [{ role: 'user', content: PROMPT(label, context) }],
|
|
141
|
-
}),
|
|
142
|
-
});
|
|
143
|
-
if (!res.ok)
|
|
144
|
-
return undefined;
|
|
145
|
-
const parsed = JSON.parse(await res.text());
|
|
146
|
-
const content = parsed.content ?? [];
|
|
147
|
-
return asValue(content
|
|
148
|
-
.filter((block) => 'text' === block.type)
|
|
149
|
-
.map((block) => block.text ?? '')
|
|
150
|
-
.join(''));
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
// Offline, refused, rate-limited, malformed. All the same answer: use the heuristic.
|
|
154
|
-
return undefined;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
/**
|
|
158
|
-
* The seam the driver calls: a label in, a value out, always.
|
|
159
|
-
*
|
|
160
|
-
* Three layers, cheapest first, and the expensive one is skipped entirely unless the two above it
|
|
161
|
-
* have nothing better than "type the words reticle harness into this box".
|
|
162
|
-
*/
|
|
163
|
-
export function fillValues(deps) {
|
|
164
|
-
let spent = 0;
|
|
165
|
-
return async (label) => {
|
|
166
|
-
const supplied = deps.secret?.(label);
|
|
167
|
-
if (supplied !== undefined && supplied.length > 0)
|
|
168
|
-
return supplied;
|
|
169
|
-
const cached = deps.cache?.get(label);
|
|
170
|
-
if (cached !== undefined)
|
|
171
|
-
return cached;
|
|
172
|
-
const fallback = heuristicFillValue(label);
|
|
173
|
-
const { openai, generator } = deps;
|
|
174
|
-
if ((openai === undefined && generator === undefined) ||
|
|
175
|
-
!needsGeneration(label) ||
|
|
176
|
-
MAX_PER_DRIVE <= spent)
|
|
177
|
-
return fallback;
|
|
178
|
-
spent += 1;
|
|
179
|
-
const context = deps.context?.() ?? '';
|
|
180
|
-
const generated = openai !== undefined
|
|
181
|
-
? await generateWithOpenAi(openai, label, context)
|
|
182
|
-
: generator !== undefined
|
|
183
|
-
? await generateOne(generator, label, context)
|
|
184
|
-
: undefined;
|
|
185
|
-
if (generated === undefined)
|
|
186
|
-
return fallback;
|
|
187
|
-
// Cached under the LABEL, not the ref: refs expire with the page, labels are what the next drive
|
|
188
|
-
// and every replay will meet again.
|
|
189
|
-
deps.cache?.set(label, generated);
|
|
190
|
-
return generated;
|
|
191
|
-
};
|
|
192
|
-
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface GoalCheck {
|
|
2
|
+
/** The text that had to be on the page. */
|
|
3
|
+
text: string;
|
|
4
|
+
/** The engine's verdict on it: only `yes` is proved. */
|
|
5
|
+
verified: string;
|
|
6
|
+
}
|
|
7
|
+
/** The observable goals in a persona: whatever it quoted. */
|
|
8
|
+
export declare function goalsIn(persona: string | undefined): string[];
|
|
9
|
+
/**
|
|
10
|
+
* Check each goal on the page the drive ended on, recording ONE assert for the drive.
|
|
11
|
+
*
|
|
12
|
+
* Each quote used to be its own assert, and every assert is a check in the drive's run, so a goal
|
|
13
|
+
* that quotes where it starts ("from "Count is 0" to "Count is 1"") recorded a "no" for the start
|
|
14
|
+
* text a working app had rightly replaced, and the run synced as a failing flow. The page is read
|
|
15
|
+
* once to answer each quote; the one recorded assert covers the quotes shown, or all of them when
|
|
16
|
+
* none is, so a goal the page never reached still leaves a failed check behind. A check that cannot
|
|
17
|
+
* run is `unknown`, never skipped.
|
|
18
|
+
*/
|
|
19
|
+
export declare function checkGoals(invoke: (name: string, args: Record<string, unknown>) => Promise<unknown>, goals: readonly string[]): Promise<GoalCheck[]>;
|
|
20
|
+
/** One line naming the goals the drive did not prove, or undefined when it proved them all. */
|
|
21
|
+
export declare function unprovedGoals(checks: readonly GoalCheck[]): string | undefined;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The goals a drive was asked to prove, and whether it did.
|
|
3
|
+
*
|
|
4
|
+
* A drive asked to "check that "Ada Lovelace" and "Grace Hopper" are listed" replayed old journeys,
|
|
5
|
+
* dismissed a reminder and reported `finished` without looking for either name (#1316). The driver's
|
|
6
|
+
* word for it is not evidence, so after every drive the harness checks each goal itself, with the
|
|
7
|
+
* same assert any agent would make, and reports one verdict per goal.
|
|
8
|
+
*/
|
|
9
|
+
import { PredicateKind, ReticleTool, Verified, asRecord } from '@reticlehq/core';
|
|
10
|
+
/** More than this and the persona is a document, not a set of goals. */
|
|
11
|
+
const MAX_GOALS = 10;
|
|
12
|
+
const MAX_GOAL_LENGTH = 200;
|
|
13
|
+
/** A string the persona quoted: "straight" or “curly”. Single quotes are apostrophes too often. */
|
|
14
|
+
const QUOTED = /"([^"\n]{1,200})"|“([^”\n]{1,200})”/g;
|
|
15
|
+
/** The observable goals in a persona: whatever it quoted. */
|
|
16
|
+
export function goalsIn(persona) {
|
|
17
|
+
if (persona === undefined)
|
|
18
|
+
return [];
|
|
19
|
+
const found = [...persona.matchAll(QUOTED)].map((m) => (m[1] ?? m[2] ?? '').trim());
|
|
20
|
+
return [...new Set(found.filter((g) => 0 < g.length))].slice(0, MAX_GOALS);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Check each goal on the page the drive ended on, recording ONE assert for the drive.
|
|
24
|
+
*
|
|
25
|
+
* Each quote used to be its own assert, and every assert is a check in the drive's run, so a goal
|
|
26
|
+
* that quotes where it starts ("from "Count is 0" to "Count is 1"") recorded a "no" for the start
|
|
27
|
+
* text a working app had rightly replaced, and the run synced as a failing flow. The page is read
|
|
28
|
+
* once to answer each quote; the one recorded assert covers the quotes shown, or all of them when
|
|
29
|
+
* none is, so a goal the page never reached still leaves a failed check behind. A check that cannot
|
|
30
|
+
* run is `unknown`, never skipped.
|
|
31
|
+
*/
|
|
32
|
+
export async function checkGoals(invoke, goals) {
|
|
33
|
+
const texts = [
|
|
34
|
+
...new Set(goals.slice(0, MAX_GOALS).map((g) => g.trim().slice(0, MAX_GOAL_LENGTH))),
|
|
35
|
+
].filter((t) => 0 < t.length);
|
|
36
|
+
if (0 === texts.length)
|
|
37
|
+
return [];
|
|
38
|
+
let page;
|
|
39
|
+
try {
|
|
40
|
+
const tree = asRecord(await invoke(ReticleTool.SNAPSHOT, { mode: SNAPSHOT_FULL }))['tree'];
|
|
41
|
+
if ('string' === typeof tree)
|
|
42
|
+
page = tree;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
/* unread: every quote rests on the assert below */
|
|
46
|
+
}
|
|
47
|
+
const shown = texts.filter((t) => true === page?.includes(t));
|
|
48
|
+
const recorded = 0 < shown.length ? shown : texts;
|
|
49
|
+
let verified = Verified.UNKNOWN;
|
|
50
|
+
try {
|
|
51
|
+
const result = asRecord(await invoke(ReticleTool.ASSERT, { predicate: allOf(recorded) }));
|
|
52
|
+
if ('string' === typeof result['verified'])
|
|
53
|
+
verified = result['verified'];
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* stays unknown: the goal was not proved */
|
|
57
|
+
}
|
|
58
|
+
// A quote the page read did not show is a "no" only beside one it did: with none shown, every
|
|
59
|
+
// quote rests on the recorded assert.
|
|
60
|
+
return texts.map((text) => ({
|
|
61
|
+
text,
|
|
62
|
+
verified: 0 === shown.length || shown.includes(text) ? verified : Verified.NO,
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
const SNAPSHOT_FULL = 'full';
|
|
66
|
+
function allOf(texts) {
|
|
67
|
+
const each = texts.map((contains) => ({ kind: PredicateKind.TEXT, contains }));
|
|
68
|
+
return 1 === each.length ? (each[0] ?? {}) : { kind: PredicateKind.ALL_OF, predicates: each };
|
|
69
|
+
}
|
|
70
|
+
/** One line naming the goals the drive did not prove, or undefined when it proved them all. */
|
|
71
|
+
export function unprovedGoals(checks) {
|
|
72
|
+
const missed = checks.filter((c) => Verified.YES !== c.verified);
|
|
73
|
+
if (0 === missed.length)
|
|
74
|
+
return undefined;
|
|
75
|
+
return (`Not proved: ${missed.map((c) => `"${c.text}" (${c.verified})`).join(', ')}. ` +
|
|
76
|
+
'The drive did not finish the job it was given, whatever its own account says.');
|
|
77
|
+
}
|
|
@@ -107,7 +107,12 @@ export declare const StopReason: {
|
|
|
107
107
|
readonly STALLED: "stalled";
|
|
108
108
|
/** The toolset or the model itself failed in a way the loop cannot continue through. */
|
|
109
109
|
readonly BROKEN: "broken";
|
|
110
|
+
/** A person switched autonomous driving off while it was driving. Not a failure. */
|
|
111
|
+
readonly STOPPED: "stopped";
|
|
110
112
|
};
|
|
113
|
+
/** Thrown by a driver when a person, not a fault, ended the drive. */
|
|
114
|
+
export declare class DriveStoppedError extends Error {
|
|
115
|
+
}
|
|
111
116
|
export type StopReason = (typeof StopReason)[keyof typeof StopReason];
|
|
112
117
|
export interface HarnessResult {
|
|
113
118
|
stopReason: StopReason;
|
|
@@ -118,7 +123,23 @@ export interface HarnessResult {
|
|
|
118
123
|
usage: TokenUsage;
|
|
119
124
|
/** Set when `stopReason` is BROKEN. */
|
|
120
125
|
error?: string;
|
|
126
|
+
/**
|
|
127
|
+
* Whether the drive ran at least one check: an `act_and_wait` with an `until`, or an `assert`,
|
|
128
|
+
* that did not error. A drive without one clicked things and proved nothing.
|
|
129
|
+
*/
|
|
130
|
+
proved: boolean;
|
|
131
|
+
/**
|
|
132
|
+
* Whether the drive reached its whole goal, as the platform judged when it finished. Checks that
|
|
133
|
+
* held are not the goal reached: "open each section" passed on two presses of Sign in.
|
|
134
|
+
*/
|
|
135
|
+
goalMet?: boolean;
|
|
121
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* The answer to a `finish` that came before anything was proved. Sent once per drive: a real drive
|
|
139
|
+
* asked to "click twice and check it reads 2" clicked once, finished, and saved a flow whose replay
|
|
140
|
+
* verifies nothing, which is the paid feature producing an empty pass.
|
|
141
|
+
*/
|
|
142
|
+
export declare const MSG_FINISH_UNPROVED: string;
|
|
122
143
|
/**
|
|
123
144
|
* The one tool the harness adds to Reticle's surface.
|
|
124
145
|
*
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* 3. **Everything is injected** — the model, the tools, the clock. The loop below has no network in
|
|
18
18
|
* it, which is what lets the whole thing be tested against a scripted model with no API key.
|
|
19
19
|
*/
|
|
20
|
+
import { ReticleTool, asRecord } from '@reticlehq/core';
|
|
20
21
|
import { withTimeout } from './with-timeout.js';
|
|
21
22
|
/** Why a run stopped. Every one of these is a real outcome; none of them is an error. */
|
|
22
23
|
export const StopReason = {
|
|
@@ -28,7 +29,33 @@ export const StopReason = {
|
|
|
28
29
|
STALLED: 'stalled',
|
|
29
30
|
/** The toolset or the model itself failed in a way the loop cannot continue through. */
|
|
30
31
|
BROKEN: 'broken',
|
|
32
|
+
/** A person switched autonomous driving off while it was driving. Not a failure. */
|
|
33
|
+
STOPPED: 'stopped',
|
|
31
34
|
};
|
|
35
|
+
/** Thrown by a driver when a person, not a fault, ended the drive. */
|
|
36
|
+
export class DriveStoppedError extends Error {
|
|
37
|
+
}
|
|
38
|
+
/** The finish's judgement of the goal, when it carried one. */
|
|
39
|
+
function goalMetOf(args) {
|
|
40
|
+
const met = args['goalMet'];
|
|
41
|
+
return 'boolean' === typeof met ? { goalMet: met } : {};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The answer to a `finish` that came before anything was proved. Sent once per drive: a real drive
|
|
45
|
+
* asked to "click twice and check it reads 2" clicked once, finished, and saved a flow whose replay
|
|
46
|
+
* verifies nothing, which is the paid feature producing an empty pass.
|
|
47
|
+
*/
|
|
48
|
+
export const MSG_FINISH_UNPROVED = `Not finished: nothing has been proved yet. Before finishing, end the journey with ` +
|
|
49
|
+
`${ReticleTool.ACT_AND_WAIT} and an \`until\` naming the end state it must reach, or ` +
|
|
50
|
+
`${ReticleTool.ASSERT} on it. A drive that proves nothing replays as "verified nothing".`;
|
|
51
|
+
/** A check that ran: the only thing that turns a drive into evidence. */
|
|
52
|
+
function isProof(outcome) {
|
|
53
|
+
if (outcome.isError)
|
|
54
|
+
return false;
|
|
55
|
+
if (ReticleTool.ASSERT === outcome.name)
|
|
56
|
+
return true;
|
|
57
|
+
return ReticleTool.ACT_AND_WAIT === outcome.name && asRecord(outcome.args)['until'] !== undefined;
|
|
58
|
+
}
|
|
32
59
|
/**
|
|
33
60
|
* The one tool the harness adds to Reticle's surface.
|
|
34
61
|
*
|
|
@@ -126,6 +153,8 @@ export async function runHarness(driver, toolset, options = {}) {
|
|
|
126
153
|
const history = [{ role: 'user', text: OPENING }];
|
|
127
154
|
const toolCalls = [];
|
|
128
155
|
const usage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
|
156
|
+
const proved = () => toolCalls.some(isProof);
|
|
157
|
+
let askedToProve = false;
|
|
129
158
|
for (let step = 0; step < maxSteps; step += 1) {
|
|
130
159
|
let turn;
|
|
131
160
|
try {
|
|
@@ -133,11 +162,12 @@ export async function runHarness(driver, toolset, options = {}) {
|
|
|
133
162
|
}
|
|
134
163
|
catch (error) {
|
|
135
164
|
return {
|
|
136
|
-
stopReason: StopReason.BROKEN,
|
|
165
|
+
stopReason: error instanceof DriveStoppedError ? StopReason.STOPPED : StopReason.BROKEN,
|
|
137
166
|
summary: '',
|
|
138
167
|
steps: step,
|
|
139
168
|
toolCalls,
|
|
140
169
|
usage,
|
|
170
|
+
proved: proved(),
|
|
141
171
|
error: error instanceof Error ? error.message : String(error),
|
|
142
172
|
};
|
|
143
173
|
}
|
|
@@ -156,10 +186,38 @@ export async function runHarness(driver, toolset, options = {}) {
|
|
|
156
186
|
steps: step + 1,
|
|
157
187
|
toolCalls,
|
|
158
188
|
usage,
|
|
189
|
+
proved: proved(),
|
|
159
190
|
};
|
|
160
191
|
}
|
|
161
192
|
history.push({ role: 'assistant', text: turn.text, calls: turn.calls });
|
|
162
193
|
const finish = turn.calls.find((call) => FINISH_TOOL.name === call.name);
|
|
194
|
+
if (finish !== undefined && !proved() && !askedToProve) {
|
|
195
|
+
// Whatever else the turn asked for runs first: a check sent alongside `finish` is proof.
|
|
196
|
+
const others = await Promise.all(turn.calls.filter((c) => c !== finish).map((c) => invokeOne(toolset, c)));
|
|
197
|
+
toolCalls.push(...others);
|
|
198
|
+
if (proved()) {
|
|
199
|
+
const summary = finish.args['summary'];
|
|
200
|
+
return {
|
|
201
|
+
stopReason: StopReason.FINISHED,
|
|
202
|
+
summary: 'string' === typeof summary ? summary : turn.text,
|
|
203
|
+
steps: step + 1,
|
|
204
|
+
toolCalls,
|
|
205
|
+
usage,
|
|
206
|
+
proved: true,
|
|
207
|
+
...goalMetOf(finish.args),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
askedToProve = true;
|
|
211
|
+
const refused = {
|
|
212
|
+
id: finish.id,
|
|
213
|
+
name: finish.name,
|
|
214
|
+
args: finish.args,
|
|
215
|
+
result: { error: MSG_FINISH_UNPROVED },
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
history.push({ role: 'tool', outcomes: [...others, refused] });
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
163
221
|
if (finish !== undefined) {
|
|
164
222
|
const summary = finish.args['summary'];
|
|
165
223
|
return {
|
|
@@ -168,6 +226,8 @@ export async function runHarness(driver, toolset, options = {}) {
|
|
|
168
226
|
steps: step + 1,
|
|
169
227
|
toolCalls,
|
|
170
228
|
usage,
|
|
229
|
+
proved: proved(),
|
|
230
|
+
...goalMetOf(finish.args),
|
|
171
231
|
};
|
|
172
232
|
}
|
|
173
233
|
// Every call the model asked for is answered, including ones that fail. A dropped result leaves
|
|
@@ -177,7 +237,14 @@ export async function runHarness(driver, toolset, options = {}) {
|
|
|
177
237
|
toolCalls.push(...outcomes);
|
|
178
238
|
history.push({ role: 'tool', outcomes });
|
|
179
239
|
}
|
|
180
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
stopReason: StopReason.BUDGET,
|
|
242
|
+
summary: '',
|
|
243
|
+
steps: maxSteps,
|
|
244
|
+
toolCalls,
|
|
245
|
+
usage,
|
|
246
|
+
proved: proved(),
|
|
247
|
+
};
|
|
181
248
|
}
|
|
182
249
|
/**
|
|
183
250
|
* One tool call, with its failure captured rather than thrown.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make ready the tab a platform drive runs in, as its spec asks, and say what was and was not done.
|
|
3
|
+
*
|
|
4
|
+
* The person's own tab, or one opened for the drive: headless, or in a window they can watch. Then
|
|
5
|
+
* the HUD as asked. Every step that cannot be done is reported, never thrown: a drive asked for a
|
|
6
|
+
* window on a machine with no display still runs headless if that is all there is, and says so.
|
|
7
|
+
*/
|
|
8
|
+
import { HudVisibility, type DriveApplied, type DriveSpec, type SpecIgnored } from '@reticlehq/core';
|
|
9
|
+
/** How many times the HUD is asked for before giving up: a page's HUD mounts after it connects. */
|
|
10
|
+
export declare const HUD_ATTEMPTS = 4;
|
|
11
|
+
export declare const HUD_RETRY_MS = 500;
|
|
12
|
+
/** What a refused drive is told when no address was given to open. */
|
|
13
|
+
export declare const NO_ADDRESS_TO_OPEN = "Nothing says where this app runs, so no browser could be opened for it. Open the app once with Reticle running, or give the drive an address.";
|
|
14
|
+
export interface DriveTargetPorts {
|
|
15
|
+
/** The person's own tab for the drive, as the chat already picks it. Null: none may be driven. */
|
|
16
|
+
pickTab: () => Promise<string | null | undefined>;
|
|
17
|
+
/** Open `url` in a browser of the daemon's own; answers the tab's session id. Throws why not. */
|
|
18
|
+
open: (url: string, headed: boolean, hud: HudVisibility | undefined) => Promise<string>;
|
|
19
|
+
/** Ask the page for this HUD; throws when the page did not take it (no HUD yet, an older SDK). */
|
|
20
|
+
tuneHud: (sessionId: string, hud: HudVisibility) => Promise<void>;
|
|
21
|
+
sleep: (ms: number) => Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface PreparedDrive {
|
|
24
|
+
sessionId: string | null | undefined;
|
|
25
|
+
applied: DriveApplied;
|
|
26
|
+
ignored: SpecIgnored[];
|
|
27
|
+
/** Why the drive cannot run, in a person's words, when it cannot. */
|
|
28
|
+
refusal?: string;
|
|
29
|
+
}
|
|
30
|
+
export declare function prepareDrive(spec: DriveSpec, fallbackUrl: string | undefined, ports: DriveTargetPorts): Promise<PreparedDrive>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Make ready the tab a platform drive runs in, as its spec asks, and say what was and was not done.
|
|
3
|
+
*
|
|
4
|
+
* The person's own tab, or one opened for the drive: headless, or in a window they can watch. Then
|
|
5
|
+
* the HUD as asked. Every step that cannot be done is reported, never thrown: a drive asked for a
|
|
6
|
+
* window on a machine with no display still runs headless if that is all there is, and says so.
|
|
7
|
+
*/
|
|
8
|
+
import { DriveMode, DriveTarget, HudVisibility, SpecIgnoredReason, } from '@reticlehq/core';
|
|
9
|
+
/** How many times the HUD is asked for before giving up: a page's HUD mounts after it connects. */
|
|
10
|
+
export const HUD_ATTEMPTS = 4;
|
|
11
|
+
export const HUD_RETRY_MS = 500;
|
|
12
|
+
/** What a refused drive is told when no address was given to open. */
|
|
13
|
+
export const NO_ADDRESS_TO_OPEN = 'Nothing says where this app runs, so no browser could be opened for it. Open the app once with Reticle running, or give the drive an address.';
|
|
14
|
+
export async function prepareDrive(spec, fallbackUrl, ports) {
|
|
15
|
+
const mode = spec.mode ?? DriveMode.LOCAL;
|
|
16
|
+
const ignored = [];
|
|
17
|
+
let target = spec.target ?? DriveTarget.TAB;
|
|
18
|
+
let sessionId;
|
|
19
|
+
let url;
|
|
20
|
+
if (DriveTarget.TAB !== target) {
|
|
21
|
+
url = spec.url ?? fallbackUrl;
|
|
22
|
+
if (url === undefined)
|
|
23
|
+
return {
|
|
24
|
+
sessionId: null,
|
|
25
|
+
applied: { target, mode },
|
|
26
|
+
ignored,
|
|
27
|
+
refusal: NO_ADDRESS_TO_OPEN,
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
sessionId = await ports.open(url, DriveTarget.HEADED === target, spec.hud);
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
34
|
+
// A window that cannot open falls back to a browser with none: the drive still runs.
|
|
35
|
+
if (DriveTarget.HEADED !== target)
|
|
36
|
+
return { sessionId: null, applied: { target, mode, url }, ignored, refusal: detail };
|
|
37
|
+
ignored.push({ field: 'target', reason: SpecIgnoredReason.UNAVAILABLE, detail });
|
|
38
|
+
target = DriveTarget.HEADLESS;
|
|
39
|
+
try {
|
|
40
|
+
sessionId = await ports.open(url, false, spec.hud);
|
|
41
|
+
}
|
|
42
|
+
catch (again) {
|
|
43
|
+
const why = again instanceof Error ? again.message : String(again);
|
|
44
|
+
return { sessionId: null, applied: { target, mode, url }, ignored, refusal: why };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
sessionId = await ports.pickTab();
|
|
50
|
+
}
|
|
51
|
+
const applied = {
|
|
52
|
+
target,
|
|
53
|
+
mode,
|
|
54
|
+
...(url === undefined ? {} : { url }),
|
|
55
|
+
...('string' === typeof sessionId ? { sessionId } : {}),
|
|
56
|
+
};
|
|
57
|
+
if (spec.hud !== undefined && 'string' === typeof sessionId) {
|
|
58
|
+
const taken = await tune(ports, sessionId, spec.hud);
|
|
59
|
+
if (taken === undefined)
|
|
60
|
+
applied.hud = spec.hud;
|
|
61
|
+
else
|
|
62
|
+
ignored.push({ field: 'hud', reason: SpecIgnoredReason.UNAVAILABLE, detail: taken });
|
|
63
|
+
}
|
|
64
|
+
return { sessionId, applied, ignored };
|
|
65
|
+
}
|
|
66
|
+
/** Ask for the HUD until the page takes it; answers why not when it never does. */
|
|
67
|
+
async function tune(ports, sessionId, hud) {
|
|
68
|
+
let why = 'the page did not take it';
|
|
69
|
+
for (let attempt = 1; attempt <= HUD_ATTEMPTS; attempt += 1) {
|
|
70
|
+
try {
|
|
71
|
+
await ports.tuneHud(sessionId, hud);
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
why = error instanceof Error ? error.message : String(error);
|
|
76
|
+
if (attempt < HUD_ATTEMPTS)
|
|
77
|
+
await ports.sleep(HUD_RETRY_MS);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return why;
|
|
81
|
+
}
|