@reticlehq/server 3.4.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/auth/cloud-session.d.ts +4 -4
- package/dist/command/cli/auth/device-flow.d.ts +2 -2
- 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 +13 -11
- 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 +110 -12
- package/dist/command/cli/cloud-login.d.ts +9 -1
- package/dist/command/cli/cloud-login.js +43 -8
- 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 +15 -5
- 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 +45 -23
- 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 +10 -0
- package/dist/command/setup/bringup/dev-server-wait.js +20 -1
- 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 +71 -9
- package/dist/command/setup/node-effects.js +8 -2
- package/dist/command/setup/remaining-steps.js +2 -1
- package/dist/command/setup/run-setup.d.ts +7 -0
- package/dist/command/setup/run-setup.js +38 -25
- package/dist/command/setup/setup-command.d.ts +2 -1
- package/dist/command/setup/setup-command.js +2 -0
- package/dist/command/setup/setup-mcp.d.ts +2 -0
- package/dist/command/setup/setup-mcp.js +14 -5
- package/dist/command/version/peer-announce.d.ts +6 -2
- package/dist/command/version/peer-announce.js +8 -6
- package/dist/command/version/version-nudge.d.ts +17 -3
- package/dist/command/version/version-nudge.js +25 -4
- 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 -69
- package/dist/features/harness/fill-values.js +3 -133
- 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 +91 -89
- package/dist/judgement/capsule/capsule-store.d.ts +4 -4
- 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 +21 -3
- package/dist/memory/cloud/sync-cycle.js +45 -5
- 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 +88 -5
- package/dist/memory/journal/envelope.d.ts +16 -16
- package/dist/memory/journal/journal-query.d.ts +14 -0
- package/dist/memory/journal/journal-query.js +15 -0
- 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 +36 -1
- package/dist/memory/project/artifact-root-resolver.js +134 -26
- 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 +7 -4
- package/dist/portal/bridge/bridge.js +25 -3
- 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 +19 -9
- 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 +12 -10
- package/dist/portal/session/session.js +30 -31
- 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.d.ts +1 -1
- package/dist/surface/http-server.js +15 -4
- 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 +28 -14
- package/dist/surface/mcp/proxy/proxy-log.js +3 -5
- 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 +25 -9
- 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/dynamic-tools.js +29 -2
- 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 +38 -6
- 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 -202
- 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 +64 -9
- 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 +25 -1
- package/dist/surface/tools/tool-kit.js +4 -6
- package/dist/surface/tools/tools.js +20 -48
- package/dist/telemetry/argument-shape.js +1 -1
- package/dist/telemetry/hud-metrics.d.ts +2 -0
- package/dist/telemetry/hud-metrics.js +17 -0
- package/dist/telemetry/onboarding-firsts.d.ts +2 -2
- package/dist/telemetry/onboarding-firsts.js +2 -2
- 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/actions.mdx +1 -2
- package/docs/agent-cheatsheet.md +1 -1
- package/docs/autodrive.md +29 -25
- package/docs/cli/cloud.mdx +6 -4
- 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 +14 -8
- package/docs/desktop.mdx +5 -1
- package/docs/environment.mdx +3 -7
- package/docs/getting-started.md +14 -2
- package/docs/human-control.md +1 -2
- package/docs/index.mdx +4 -3
- package/docs/install-agentic.mdx +1 -1
- package/docs/install-manual.mdx +2 -2
- package/docs/packages/next.mdx +3 -0
- package/docs/packages/tauri.mdx +2 -2
- package/docs/packages/vite-plugin.mdx +24 -1
- package/docs/packages.mdx +1 -1
- package/docs/quickstart.mdx +23 -11
- package/docs/telemetry.md +1 -1
- package/docs/tools/act.mdx +1 -1
- package/docs/tools/session-and-feedback.mdx +9 -9
- package/docs/usage.md +5 -3
- 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 -46
- package/dist/features/harness/openai-driver.js +0 -194
- 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
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The platform's Harness, seen from the daemon: the platform decides, this machine executes.
|
|
3
|
+
*
|
|
4
|
+
* Every turn is one HTTPS request this daemon makes: it posts what the last calls did, and the
|
|
5
|
+
* platform answers with the next calls, chosen by its own model, persona and verification mode. The
|
|
6
|
+
* loop around it (`runHarness`) is unchanged: it executes the calls through Reticle's own tools and
|
|
7
|
+
* hands the outcomes back here. Nothing is held open between turns, a lost response is retried with
|
|
8
|
+
* the same turn number, and the platform answers that from its record without a second model call.
|
|
9
|
+
*
|
|
10
|
+
* This is the only Harness driver: every decision is the platform's, for a workspace with a plan or
|
|
11
|
+
* trial. A secret field travels by name only, and its value is filled in here before the call runs.
|
|
12
|
+
*/
|
|
13
|
+
import { ReticleEnv, cloudUrlFrom } from '@reticlehq/core';
|
|
14
|
+
import { DriveStoppedError, } from '../harness.js';
|
|
15
|
+
export const SERVER_DRIVER_NAME = 'server';
|
|
16
|
+
const RUNS_PATH = '/v1/harness/runs';
|
|
17
|
+
const TURN_TIMEOUT_MS = 110_000;
|
|
18
|
+
const RETRIES = 1;
|
|
19
|
+
/** The platform's code for a run whose project had autonomous driving switched off. */
|
|
20
|
+
const HARNESS_OFF = 'harness_off';
|
|
21
|
+
const FINISH = 'finish';
|
|
22
|
+
/** The platform's url and this project's key, when the machine is linked. */
|
|
23
|
+
export function serverOptionsFromEnv(env) {
|
|
24
|
+
const url = cloudUrlFrom(env);
|
|
25
|
+
const apiKey = env[ReticleEnv.API_KEY];
|
|
26
|
+
return url === undefined || apiKey === undefined || 0 === apiKey.length
|
|
27
|
+
? undefined
|
|
28
|
+
: { url: url.replace(/\/+$/, ''), apiKey };
|
|
29
|
+
}
|
|
30
|
+
export class ServerHarnessError extends Error {
|
|
31
|
+
status;
|
|
32
|
+
constructor(message, status) {
|
|
33
|
+
super(message);
|
|
34
|
+
this.status = status;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export function serverDriver(options) {
|
|
38
|
+
const doFetch = options.fetch ?? ((url, init) => fetch(url, init));
|
|
39
|
+
let runId;
|
|
40
|
+
let turn = 0;
|
|
41
|
+
let seen = 0;
|
|
42
|
+
const call = async (path, body) => {
|
|
43
|
+
let last;
|
|
44
|
+
for (let attempt = 0; attempt <= RETRIES; attempt += 1) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await doFetch(`${options.url}${path}`, {
|
|
47
|
+
method: 'POST',
|
|
48
|
+
headers: {
|
|
49
|
+
'content-type': 'application/json',
|
|
50
|
+
authorization: `Bearer ${options.apiKey}`,
|
|
51
|
+
},
|
|
52
|
+
body: JSON.stringify(body),
|
|
53
|
+
signal: AbortSignal.timeout(TURN_TIMEOUT_MS),
|
|
54
|
+
});
|
|
55
|
+
const text = await res.text();
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const message = errorMessage(text) ?? `the platform answered ${String(res.status)}`;
|
|
58
|
+
// A person turned it off: the drive ends, and the result says that rather than "broken".
|
|
59
|
+
if (HARNESS_OFF === errorCode(text))
|
|
60
|
+
throw new DriveStoppedError(message);
|
|
61
|
+
// A refusal is an answer; only a network failure is worth asking again.
|
|
62
|
+
throw new ServerHarnessError(message, res.status);
|
|
63
|
+
}
|
|
64
|
+
return JSON.parse(text);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof ServerHarnessError || error instanceof DriveStoppedError)
|
|
68
|
+
throw error;
|
|
69
|
+
last = error;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
throw last instanceof Error ? last : new Error(String(last));
|
|
73
|
+
};
|
|
74
|
+
return {
|
|
75
|
+
async turn(input) {
|
|
76
|
+
if (runId === undefined) {
|
|
77
|
+
const started = (await call(RUNS_PATH, {
|
|
78
|
+
...(options.persona === undefined ? {} : { persona: options.persona }),
|
|
79
|
+
...(options.journey === undefined ? {} : { journey: options.journey }),
|
|
80
|
+
...(options.mode === undefined ? {} : { mode: options.mode }),
|
|
81
|
+
...(options.plan === undefined ? {} : { plan: options.plan }),
|
|
82
|
+
...(options.maxSteps === undefined ? {} : { maxSteps: options.maxSteps }),
|
|
83
|
+
...(options.planSteps === undefined ? {} : { planSteps: options.planSteps }),
|
|
84
|
+
...(options.vocabulary === undefined ? {} : { vocabulary: options.vocabulary }),
|
|
85
|
+
secrets: secretNames(options.env ?? process.env),
|
|
86
|
+
tools: input.tools.filter((t) => FINISH !== t.name).map(toolSpec),
|
|
87
|
+
}));
|
|
88
|
+
if ('string' !== typeof started.runId)
|
|
89
|
+
throw new ServerHarnessError('the platform started no run', 502);
|
|
90
|
+
runId = started.runId;
|
|
91
|
+
}
|
|
92
|
+
// Only what happened since the last turn: the platform holds everything before it.
|
|
93
|
+
const fresh = input.history.slice(seen);
|
|
94
|
+
seen = input.history.length;
|
|
95
|
+
const outcomes = fresh.flatMap((entry) => 'tool' === entry.role ? entry.outcomes.map(outcomeOf) : []);
|
|
96
|
+
const reply = (await call(`${RUNS_PATH}/${encodeURIComponent(runId)}/turn`, {
|
|
97
|
+
turn,
|
|
98
|
+
outcomes,
|
|
99
|
+
}));
|
|
100
|
+
turn = reply.turn + 1;
|
|
101
|
+
// The platform's spend, reported so the drive's cost is visible here as it is for a local one.
|
|
102
|
+
const usage = reply.usage === undefined ? {} : { usage: reply.usage };
|
|
103
|
+
if (reply.done) {
|
|
104
|
+
// The platform has finished: `finish` ends the local loop the same way a local model would.
|
|
105
|
+
return {
|
|
106
|
+
text: reply.text,
|
|
107
|
+
...usage,
|
|
108
|
+
calls: [
|
|
109
|
+
{
|
|
110
|
+
id: `server-finish-${String(reply.turn)}`,
|
|
111
|
+
name: FINISH,
|
|
112
|
+
args: {
|
|
113
|
+
summary: reply.summary ?? reply.text,
|
|
114
|
+
...('boolean' === typeof reply.goalMet ? { goalMet: reply.goalMet } : {}),
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
// A field the platform knows only by name is filled here, from this machine, and never sent.
|
|
121
|
+
const env = options.env ?? process.env;
|
|
122
|
+
return {
|
|
123
|
+
text: reply.text,
|
|
124
|
+
calls: reply.calls.map((c) => ({ ...c, args: withSecrets(c.args, env) })),
|
|
125
|
+
...usage,
|
|
126
|
+
};
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function toolSpec(tool) {
|
|
131
|
+
return { name: tool.name, description: tool.description, inputSchema: tool.inputSchema };
|
|
132
|
+
}
|
|
133
|
+
function outcomeOf(o) {
|
|
134
|
+
return { id: o.id, name: o.name, result: o.result, isError: o.isError };
|
|
135
|
+
}
|
|
136
|
+
function errorCode(text) {
|
|
137
|
+
try {
|
|
138
|
+
const body = JSON.parse(text);
|
|
139
|
+
return 'string' === typeof body.error?.code ? body.error.code : undefined;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function errorMessage(text) {
|
|
146
|
+
try {
|
|
147
|
+
const body = JSON.parse(text);
|
|
148
|
+
return 'string' === typeof body.error?.message ? body.error.message : undefined;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/** The prefix of a variable holding one secret field: `RETICLE_SECRET_AUTH_PASSWORD`. */
|
|
155
|
+
const SECRET_ENV_PREFIX = 'RETICLE_SECRET_';
|
|
156
|
+
/** What the platform types into a field whose secret this machine holds. */
|
|
157
|
+
const SECRET_PLACEHOLDER = 'reticle-secret:';
|
|
158
|
+
/** The secret fields this machine can fill, by name only. */
|
|
159
|
+
export function secretNames(env) {
|
|
160
|
+
return Object.keys(env)
|
|
161
|
+
.filter((key) => key.startsWith(SECRET_ENV_PREFIX) && 0 < (env[key] ?? '').length)
|
|
162
|
+
.map((key) => key.slice(SECRET_ENV_PREFIX.length));
|
|
163
|
+
}
|
|
164
|
+
/** The call's arguments with every secret placeholder replaced by its value from this machine. */
|
|
165
|
+
export function withSecrets(args, env) {
|
|
166
|
+
const swap = (value) => {
|
|
167
|
+
if ('string' === typeof value && value.startsWith(SECRET_PLACEHOLDER)) {
|
|
168
|
+
const field = value.slice(SECRET_PLACEHOLDER.length);
|
|
169
|
+
const name = field.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase();
|
|
170
|
+
return env[`${SECRET_ENV_PREFIX}${name}`] ?? value;
|
|
171
|
+
}
|
|
172
|
+
if (Array.isArray(value))
|
|
173
|
+
return value.map(swap);
|
|
174
|
+
if ('object' === typeof value && null !== value)
|
|
175
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, swap(v)]));
|
|
176
|
+
return value;
|
|
177
|
+
};
|
|
178
|
+
return swap(args);
|
|
179
|
+
}
|
|
@@ -40,6 +40,13 @@ export interface PlatformModelConfig {
|
|
|
40
40
|
* not have its silence treated as a refusal.
|
|
41
41
|
*/
|
|
42
42
|
providerReady: boolean;
|
|
43
|
+
/** Harness credits used and held this 30 days. Absent when the plan is unbounded or the API is older. */
|
|
44
|
+
credits?: {
|
|
45
|
+
used: number;
|
|
46
|
+
limit: number;
|
|
47
|
+
};
|
|
48
|
+
/** The platform that answered, no trailing slash: where the HUD's settings and plan links go. */
|
|
49
|
+
platformUrl?: string;
|
|
43
50
|
}
|
|
44
51
|
/** A GET, narrowed to what this file uses, so a test can answer it without a network. */
|
|
45
52
|
export type ConfigFetch = (url: string, init: {
|
|
@@ -48,11 +48,21 @@ function parse(body) {
|
|
|
48
48
|
const ready = 'object' === typeof available && null !== available
|
|
49
49
|
? available[provider]
|
|
50
50
|
: undefined;
|
|
51
|
+
const credits = record['credits'];
|
|
52
|
+
const used = 'object' === typeof credits && null !== credits
|
|
53
|
+
? credits['used']
|
|
54
|
+
: undefined;
|
|
55
|
+
const limit = 'object' === typeof credits && null !== credits
|
|
56
|
+
? credits['limit']
|
|
57
|
+
: undefined;
|
|
51
58
|
return {
|
|
52
59
|
provider,
|
|
53
60
|
harnessEnabled: 'boolean' === typeof enabled ? enabled : true,
|
|
54
61
|
harnessEntitled: 'boolean' === typeof entitled ? entitled : true,
|
|
55
62
|
providerReady: 'boolean' === typeof ready ? ready : true,
|
|
63
|
+
...('number' === typeof used && 'number' === typeof limit
|
|
64
|
+
? { credits: { used, limit } }
|
|
65
|
+
: {}),
|
|
56
66
|
};
|
|
57
67
|
}
|
|
58
68
|
catch {
|
|
@@ -78,14 +88,16 @@ export async function fetchPlatformConfig(env, doFetch = (url, init) => fetch(ur
|
|
|
78
88
|
controller.abort();
|
|
79
89
|
}, timeoutMs);
|
|
80
90
|
try {
|
|
81
|
-
const
|
|
91
|
+
const base = host.replace(/\/+$/, '');
|
|
92
|
+
const res = await doFetch(`${base}${CONFIG_PATH}`, {
|
|
82
93
|
method: 'GET',
|
|
83
94
|
headers: { authorization: `Bearer ${key}` },
|
|
84
95
|
signal: controller.signal,
|
|
85
96
|
});
|
|
86
97
|
if (!res.ok)
|
|
87
98
|
return undefined;
|
|
88
|
-
|
|
99
|
+
const config = parse(await res.text());
|
|
100
|
+
return config === undefined ? undefined : { ...config, platformUrl: base };
|
|
89
101
|
}
|
|
90
102
|
catch {
|
|
91
103
|
return undefined;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app map a drive leaves for the next plan: the controls on every page the drive looked at.
|
|
3
|
+
*
|
|
4
|
+
* The planner reads the start page live. The crawl's clicks alone spent their budget on a
|
|
5
|
+
* dashboard's navigation and never listed the Refund button on its Transactions page, so no journey
|
|
6
|
+
* went near the app's costliest defects. Every lane snapshots the pages it reaches, and those
|
|
7
|
+
* snapshots already hold the controls a plan needs.
|
|
8
|
+
*/
|
|
9
|
+
import { ReticleTool, asRecord, parseInteractive } from '@reticlehq/core';
|
|
10
|
+
/** The most controls kept: a dashboard's navigation and its pages' main buttons, not its rows. */
|
|
11
|
+
const MAX_APP_CONTROLS = 120;
|
|
12
|
+
/** A snapshot line as a page's control: `button "Refund"`, with no ref, value or state. */
|
|
13
|
+
const NAMED_CONTROL = /^-?\s*([a-z]+) "([^"]+)"/;
|
|
14
|
+
/** The page part of an address: its path and hash, without the session query a lease adds. */
|
|
15
|
+
function pageOf(route) {
|
|
16
|
+
return route.replace(/\?[^#]*/, '') || '/';
|
|
17
|
+
}
|
|
18
|
+
/** Every named control a drive saw, by page, then what the crawl clicked. */
|
|
19
|
+
export function appControlsOf(calls, crawled) {
|
|
20
|
+
const seen = [];
|
|
21
|
+
for (const call of calls) {
|
|
22
|
+
if (ReticleTool.SNAPSHOT !== call.name || call.isError)
|
|
23
|
+
continue;
|
|
24
|
+
const result = asRecord(call.result);
|
|
25
|
+
const tree = result['tree'];
|
|
26
|
+
const route = asRecord(result['status'])['route'];
|
|
27
|
+
if ('string' !== typeof tree || 'string' !== typeof route)
|
|
28
|
+
continue;
|
|
29
|
+
for (const item of parseInteractive(tree)) {
|
|
30
|
+
const named = NAMED_CONTROL.exec(item.desc.trim());
|
|
31
|
+
if (named !== null)
|
|
32
|
+
seen.push(`${pageOf(route)}: ${String(named[1])} "${String(named[2])}"`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const visited = asRecord(crawled?.result)['visited'];
|
|
36
|
+
if (Array.isArray(visited))
|
|
37
|
+
seen.push(...visited.filter((v) => 'string' === typeof v));
|
|
38
|
+
const names = [...new Set(seen)];
|
|
39
|
+
return 0 === names.length ? {} : { appControls: names.slice(0, MAX_APP_CONTROLS) };
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Harness id each lane of one drive runs under.
|
|
3
|
+
*
|
|
4
|
+
* Lanes in leased contexts used to share the drive's one id, so each lane's session, ending, synced
|
|
5
|
+
* a run under the same `harness-<id>` and the last lane to finish overwrote the others. A lane on
|
|
6
|
+
* its own session gets its own id; a drive that never leased keeps the one id it always had.
|
|
7
|
+
*/
|
|
8
|
+
export interface LaneIds {
|
|
9
|
+
/** The id for the lane on `sessionId`, minted once and reused. */
|
|
10
|
+
idFor(sessionId: string | undefined): string;
|
|
11
|
+
/** Every id this drive ran under, in the order the lanes started. */
|
|
12
|
+
all(): string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function laneIds(harness: string, leasing: boolean): LaneIds;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function laneIds(harness, leasing) {
|
|
2
|
+
const bySession = new Map();
|
|
3
|
+
return {
|
|
4
|
+
idFor(sessionId) {
|
|
5
|
+
if (!leasing || sessionId === undefined)
|
|
6
|
+
return harness;
|
|
7
|
+
const known = bySession.get(sessionId);
|
|
8
|
+
if (known !== undefined)
|
|
9
|
+
return known;
|
|
10
|
+
const id = `${harness}-L${String(bySession.size + 1)}`;
|
|
11
|
+
bySession.set(sessionId, id);
|
|
12
|
+
return id;
|
|
13
|
+
},
|
|
14
|
+
all: () => (0 < bySession.size ? [...bySession.values()] : [harness]),
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a drive script: lanes side by side, journeys in order, every step through Reticle's own tools.
|
|
3
|
+
*
|
|
4
|
+
* Replays, exact acts and asserts need no model and are judged by the tool's own verdict. Only an
|
|
5
|
+
* open goal reaches the model. The browser, the model and the clock of concurrency all arrive as
|
|
6
|
+
* ports, so the scheduling below is tested without either.
|
|
7
|
+
*
|
|
8
|
+
* Barriers cannot deadlock: a journey only waits on one in an EARLIER lane (`checkScript`), lanes
|
|
9
|
+
* are started in order, so whatever is waited on has already started.
|
|
10
|
+
*/
|
|
11
|
+
import { type PlanView } from '@reticlehq/core';
|
|
12
|
+
import { type DriveScript } from '@reticlehq/core/artifacts';
|
|
13
|
+
import { type HarnessResult, type HarnessToolset, type ToolOutcome } from '../harness.js';
|
|
14
|
+
export interface ScriptPorts {
|
|
15
|
+
/** A browser context for one lane. Without a pool this is the user's own tab, every time. */
|
|
16
|
+
lease(laneId: string): Promise<{
|
|
17
|
+
sessionId?: string;
|
|
18
|
+
release(): Promise<void>;
|
|
19
|
+
}>;
|
|
20
|
+
/** Reticle's tools on a lane's tab, as the journey's persona when it has one. */
|
|
21
|
+
toolset(sessionId: string | undefined, persona: string | undefined): HarnessToolset;
|
|
22
|
+
/** Hand an open goal to the model, on that toolset. */
|
|
23
|
+
drive(toolset: HarnessToolset, goal: string, maxSteps: number): Promise<HarnessResult>;
|
|
24
|
+
/** Lanes at once. 1 when there is no pool, so lanes take turns on the one tab. */
|
|
25
|
+
parallel: number;
|
|
26
|
+
/** A person switched autonomous driving off. */
|
|
27
|
+
stopped(): boolean;
|
|
28
|
+
}
|
|
29
|
+
export interface ScriptRun {
|
|
30
|
+
view: PlanView;
|
|
31
|
+
/** Every tool call the run made, in the order each lane made them. */
|
|
32
|
+
toolCalls: ToolOutcome[];
|
|
33
|
+
/** The model drives the open goals took. */
|
|
34
|
+
drives: HarnessResult[];
|
|
35
|
+
stopped: boolean;
|
|
36
|
+
/** One line per journey, for the drive's report. */
|
|
37
|
+
lines: string[];
|
|
38
|
+
}
|
|
39
|
+
export declare function runScript(script: DriveScript, ports: ScriptPorts, onChange?: (view: PlanView) => void): Promise<ScriptRun>;
|
|
40
|
+
/** The HUD's picture of a script before anything ran. */
|
|
41
|
+
export declare function planView(script: DriveScript, parallel: number): PlanView;
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run a drive script: lanes side by side, journeys in order, every step through Reticle's own tools.
|
|
3
|
+
*
|
|
4
|
+
* Replays, exact acts and asserts need no model and are judged by the tool's own verdict. Only an
|
|
5
|
+
* open goal reaches the model. The browser, the model and the clock of concurrency all arrive as
|
|
6
|
+
* ports, so the scheduling below is tested without either.
|
|
7
|
+
*
|
|
8
|
+
* Barriers cannot deadlock: a journey only waits on one in an EARLIER lane (`checkScript`), lanes
|
|
9
|
+
* are started in order, so whatever is waited on has already started.
|
|
10
|
+
*/
|
|
11
|
+
import { PredicateKind, ReplayStatus, ReticleTool, ScriptStatus, Verified, asRecord, } from '@reticlehq/core';
|
|
12
|
+
import { ScriptStepKind, barriersOf, stepLabel, } from '@reticlehq/core/artifacts';
|
|
13
|
+
import { checkTally } from '../drive-report.js';
|
|
14
|
+
import { goalsIn } from '../goals.js';
|
|
15
|
+
import { DEFAULT_MAX_STEPS, StopReason, } from '../harness.js';
|
|
16
|
+
/** Why a journey failed when its goal, not a check, is what was missing. */
|
|
17
|
+
const GOAL_NOT_REACHED = 'the goal was not reached, though its checks may have held';
|
|
18
|
+
/** A planned open journey gets the same budget a named persona drive gets. */
|
|
19
|
+
const OPEN_GOAL_STEPS = DEFAULT_MAX_STEPS;
|
|
20
|
+
export async function runScript(script, ports, onChange = () => undefined) {
|
|
21
|
+
const view = planView(script, Math.min(ports.parallel, script.maxParallel ?? ports.parallel));
|
|
22
|
+
const toolCalls = [];
|
|
23
|
+
const drives = [];
|
|
24
|
+
let stopped = false;
|
|
25
|
+
const tell = () => {
|
|
26
|
+
try {
|
|
27
|
+
onChange(structuredClone(view));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
/* the picture of a drive is never a reason to stop it */
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
// A journey that sits in several lanes is answered by the FIRST lane holding it; barriers wait on that.
|
|
34
|
+
const outcome = new Map();
|
|
35
|
+
const firstLane = new Map();
|
|
36
|
+
for (const lane of script.lanes) {
|
|
37
|
+
for (const id of lane.journeys) {
|
|
38
|
+
if (firstLane.has(id))
|
|
39
|
+
continue;
|
|
40
|
+
firstLane.set(id, lane.id);
|
|
41
|
+
let settle = () => undefined;
|
|
42
|
+
const promise = new Promise((resolve) => (settle = resolve));
|
|
43
|
+
outcome.set(id, { promise, settle });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const journeyById = new Map(script.journeys.map((j) => [j.id, j]));
|
|
47
|
+
/** Journeys whose checks may have held but whose goal the drive did not reach. */
|
|
48
|
+
const goalMissed = new Set();
|
|
49
|
+
const broken = new Map();
|
|
50
|
+
tell();
|
|
51
|
+
const runLane = async (lane, laneIndex) => {
|
|
52
|
+
const cards = view.lanes[laneIndex]?.journeys ?? [];
|
|
53
|
+
const settleOwned = (id, ok) => {
|
|
54
|
+
if (firstLane.get(id) === lane.id)
|
|
55
|
+
outcome.get(id)?.settle(ok);
|
|
56
|
+
};
|
|
57
|
+
let lease;
|
|
58
|
+
let blocked = false;
|
|
59
|
+
try {
|
|
60
|
+
for (const [position, id] of lane.journeys.entries()) {
|
|
61
|
+
const card = cards[position];
|
|
62
|
+
const journey = journeyById.get(id);
|
|
63
|
+
if (card === undefined || journey === undefined)
|
|
64
|
+
continue;
|
|
65
|
+
const waits = await Promise.all(barriersOf(script, lane.id, id).map((dep) => outcome.get(dep)?.promise ?? Promise.resolve(false)));
|
|
66
|
+
if (blocked || stopped || ports.stopped() || waits.includes(false)) {
|
|
67
|
+
blocked = true;
|
|
68
|
+
card.status = ScriptStatus.BLOCKED;
|
|
69
|
+
settleOwned(id, false);
|
|
70
|
+
tell();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
lease ??= await ports.lease(lane.id);
|
|
74
|
+
const tools = recording(ports.toolset(lease.sessionId, journey.persona), toolCalls);
|
|
75
|
+
card.status = ScriptStatus.RUNNING;
|
|
76
|
+
tell();
|
|
77
|
+
// This journey's own drives: lanes run side by side, so a slice of the shared list after the
|
|
78
|
+
// await could hold another lane's missed goal and blame it on this journey.
|
|
79
|
+
const own = [];
|
|
80
|
+
const ok = await runJourney(journey, card.steps, tools, ports, own, tell, () => {
|
|
81
|
+
stopped = true;
|
|
82
|
+
});
|
|
83
|
+
card.status = ok ? ScriptStatus.PASSED : ScriptStatus.FAILED;
|
|
84
|
+
drives.push(...own);
|
|
85
|
+
if (!ok && own.some((d) => false === d.goalMet))
|
|
86
|
+
goalMissed.add(card);
|
|
87
|
+
blocked = !ok;
|
|
88
|
+
settleOwned(id, ok);
|
|
89
|
+
tell();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
// The lease or a tool broke the lane: everything it had not finished is blocked, and says why.
|
|
94
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
95
|
+
broken.set(lane.id, message.split('\n')[0] ?? message);
|
|
96
|
+
for (const card of cards) {
|
|
97
|
+
if (ScriptStatus.PENDING === card.status || ScriptStatus.RUNNING === card.status) {
|
|
98
|
+
card.status = ScriptStatus.BLOCKED;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const id of lane.journeys)
|
|
102
|
+
settleOwned(id, false);
|
|
103
|
+
tell();
|
|
104
|
+
}
|
|
105
|
+
finally {
|
|
106
|
+
await lease?.release().catch(() => undefined);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
// Lanes start in order, `parallel` at a time: whatever a barrier waits on has already started.
|
|
110
|
+
let next = 0;
|
|
111
|
+
const worker = async () => {
|
|
112
|
+
for (let index = next++; index < script.lanes.length; index = next++) {
|
|
113
|
+
const lane = script.lanes[index];
|
|
114
|
+
if (lane !== undefined)
|
|
115
|
+
await runLane(lane, index);
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
await Promise.all(Array.from({ length: view.parallel }, worker));
|
|
119
|
+
const lines = view.lanes.flatMap((lane) => lane.journeys.map((card) => {
|
|
120
|
+
const why = ScriptStatus.BLOCKED === card.status
|
|
121
|
+
? broken.get(lane.id)
|
|
122
|
+
: goalMissed.has(card)
|
|
123
|
+
? GOAL_NOT_REACHED
|
|
124
|
+
: undefined;
|
|
125
|
+
return `${mark(card.status)} ${lane.id} · ${card.title} — ${card.status}${why === undefined ? '' : ` (${why.slice(0, 160)})`}`;
|
|
126
|
+
}));
|
|
127
|
+
return { view, toolCalls, drives, stopped: stopped || ports.stopped(), lines };
|
|
128
|
+
}
|
|
129
|
+
async function runJourney(journey, cards, tools, ports, drives, tell, stop) {
|
|
130
|
+
const checkpoints = new Map();
|
|
131
|
+
for (const [index, step] of journey.steps.entries()) {
|
|
132
|
+
const card = cards[index];
|
|
133
|
+
if (card === undefined)
|
|
134
|
+
continue;
|
|
135
|
+
if (ports.stopped()) {
|
|
136
|
+
card.status = ScriptStatus.BLOCKED;
|
|
137
|
+
tell();
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
card.status = ScriptStatus.RUNNING;
|
|
141
|
+
tell();
|
|
142
|
+
let ok;
|
|
143
|
+
if (ScriptStepKind.CHECKPOINT === step.kind) {
|
|
144
|
+
checkpoints.set(step.id, step.reenter);
|
|
145
|
+
ok = true;
|
|
146
|
+
}
|
|
147
|
+
else if (ScriptStepKind.BRANCH === step.kind) {
|
|
148
|
+
ok = await runBranch(step, checkpoints.get(step.at), tools, ports, drives, stop);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
ok = await runLeaf(step, tools, ports, drives, stop);
|
|
152
|
+
}
|
|
153
|
+
card.status = ok ? ScriptStatus.PASSED : ScriptStatus.FAILED;
|
|
154
|
+
tell();
|
|
155
|
+
if (!ok) {
|
|
156
|
+
for (const rest of cards.slice(index + 1))
|
|
157
|
+
rest.status = ScriptStatus.BLOCKED;
|
|
158
|
+
tell();
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Every case drives from the checkpoint. The first starts where the journey already is; each later
|
|
166
|
+
* one re-enters it first, so no case inherits the page the previous case left behind.
|
|
167
|
+
*/
|
|
168
|
+
async function runBranch(step, reenter, tools, ports, drives, stop) {
|
|
169
|
+
let all = true;
|
|
170
|
+
for (const [index, branchCase] of step.cases.entries()) {
|
|
171
|
+
if (ports.stopped())
|
|
172
|
+
return false;
|
|
173
|
+
if (0 < index) {
|
|
174
|
+
if (reenter === undefined)
|
|
175
|
+
return false;
|
|
176
|
+
const back = await replay(tools, { flowName: reenter.flow, to: reenter.to });
|
|
177
|
+
if (!back)
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (branchCase.when !== undefined && !(await holds(tools, branchCase.when)))
|
|
181
|
+
continue;
|
|
182
|
+
for (const leaf of branchCase.steps) {
|
|
183
|
+
if (!(await runLeaf(leaf, tools, ports, drives, stop))) {
|
|
184
|
+
all = false;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return all;
|
|
190
|
+
}
|
|
191
|
+
async function runLeaf(step, tools, ports, drives, stop) {
|
|
192
|
+
switch (step.kind) {
|
|
193
|
+
case ScriptStepKind.REPLAY:
|
|
194
|
+
return replay(tools, {
|
|
195
|
+
flowName: step.flow,
|
|
196
|
+
...(step.at === undefined ? {} : { at: step.at }),
|
|
197
|
+
...(step.to === undefined ? {} : { to: step.to }),
|
|
198
|
+
});
|
|
199
|
+
case ScriptStepKind.ASSERT:
|
|
200
|
+
return holds(tools, step.predicate);
|
|
201
|
+
case ScriptStepKind.ACT: {
|
|
202
|
+
if (step.goal !== undefined) {
|
|
203
|
+
const drive = await ports.drive(tools, step.goal, step.maxSteps ?? OPEN_GOAL_STEPS);
|
|
204
|
+
drives.push(drive);
|
|
205
|
+
if (StopReason.STOPPED === drive.stopReason)
|
|
206
|
+
stop();
|
|
207
|
+
// Passed means its checks held, not that it ran one: a refund journey whose two checks both
|
|
208
|
+
// failed was marked passed because a check had run.
|
|
209
|
+
const tally = checkTally(drive.toolCalls);
|
|
210
|
+
if (0 < tally.failed || false === drive.goalMet)
|
|
211
|
+
return false;
|
|
212
|
+
// A goal that quotes where it ends is judged there, by an assert kept as the run's check:
|
|
213
|
+
// the model clicked a counter with no `until` and the journey failed on a counter that worked.
|
|
214
|
+
// ponytail: the LAST quote is the end state ("from "count: 0" to "count: 1""); a goal that
|
|
215
|
+
// quotes its end first needs the planner to say which quote is the end.
|
|
216
|
+
const end = goalsIn(step.goal).at(-1);
|
|
217
|
+
if (end !== undefined)
|
|
218
|
+
return holds(tools, { kind: PredicateKind.TEXT, contains: end });
|
|
219
|
+
return 0 < tally.held;
|
|
220
|
+
}
|
|
221
|
+
const result = await call(tools, ReticleTool.ACT_AND_WAIT, {
|
|
222
|
+
...(step.target === undefined ? {} : { target: step.target }),
|
|
223
|
+
action: step.action,
|
|
224
|
+
...(step.args === undefined ? {} : { args: step.args }),
|
|
225
|
+
...(step.until === undefined ? {} : { until: step.until }),
|
|
226
|
+
});
|
|
227
|
+
return Verified.YES === asRecord(result)['verified'];
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async function replay(tools, args) {
|
|
232
|
+
return ReplayStatus.OK === asRecord(await call(tools, ReticleTool.FLOW_REPLAY, args))['status'];
|
|
233
|
+
}
|
|
234
|
+
async function holds(tools, predicate) {
|
|
235
|
+
return true === asRecord(await call(tools, ReticleTool.ASSERT, { predicate }))['pass'];
|
|
236
|
+
}
|
|
237
|
+
/** A tool that throws is a failed step, not a crashed run. */
|
|
238
|
+
async function call(tools, name, args) {
|
|
239
|
+
try {
|
|
240
|
+
return await tools.invoke(name, args);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
return { error: error instanceof Error ? error.message : String(error) };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
/** The toolset, with every call kept, so the run's report reads them like a model drive's. */
|
|
247
|
+
function recording(tools, into) {
|
|
248
|
+
return {
|
|
249
|
+
tools: tools.tools,
|
|
250
|
+
async invoke(name, args) {
|
|
251
|
+
const id = `script-${String(into.length + 1)}`;
|
|
252
|
+
try {
|
|
253
|
+
const result = await tools.invoke(name, args);
|
|
254
|
+
into.push({ id, name, args, result, isError: false });
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
259
|
+
into.push({ id, name, args, result: { error: message }, isError: true });
|
|
260
|
+
throw error;
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** The HUD's picture of a script before anything ran. */
|
|
266
|
+
export function planView(script, parallel) {
|
|
267
|
+
const byId = new Map(script.journeys.map((j) => [j.id, j]));
|
|
268
|
+
return {
|
|
269
|
+
parallel: Math.max(1, Math.min(parallel, script.lanes.length)),
|
|
270
|
+
lanes: script.lanes.map((lane) => ({
|
|
271
|
+
id: lane.id,
|
|
272
|
+
journeys: lane.journeys.flatMap((id) => {
|
|
273
|
+
const journey = byId.get(id);
|
|
274
|
+
if (journey === undefined)
|
|
275
|
+
return [];
|
|
276
|
+
return [
|
|
277
|
+
{
|
|
278
|
+
id,
|
|
279
|
+
title: journey.title,
|
|
280
|
+
...(journey.persona === undefined ? {} : { persona: journey.persona }),
|
|
281
|
+
waitsOn: barriersOf(script, lane.id, id),
|
|
282
|
+
status: ScriptStatus.PENDING,
|
|
283
|
+
steps: journey.steps.map((step) => ({
|
|
284
|
+
label: stepLabel(step),
|
|
285
|
+
status: ScriptStatus.PENDING,
|
|
286
|
+
})),
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
}),
|
|
290
|
+
})),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function mark(status) {
|
|
294
|
+
if (ScriptStatus.PASSED === status)
|
|
295
|
+
return '✓';
|
|
296
|
+
if (ScriptStatus.FAILED === status)
|
|
297
|
+
return '✗';
|
|
298
|
+
return '○';
|
|
299
|
+
}
|
|
@@ -39,15 +39,15 @@ declare const LicensePayloadSchema: z.ZodObject<{
|
|
|
39
39
|
exp: z.ZodNumber;
|
|
40
40
|
features: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
41
41
|
}, "strip", z.ZodTypeAny, {
|
|
42
|
-
lid: string;
|
|
43
42
|
org: string;
|
|
44
43
|
plan: string;
|
|
44
|
+
lid: string;
|
|
45
45
|
exp: number;
|
|
46
46
|
features?: string[] | undefined;
|
|
47
47
|
}, {
|
|
48
|
-
lid: string;
|
|
49
48
|
org: string;
|
|
50
49
|
plan: string;
|
|
50
|
+
lid: string;
|
|
51
51
|
exp: number;
|
|
52
52
|
features?: string[] | undefined;
|
|
53
53
|
}>;
|