@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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A drive somebody asked for in the platform's chat, run here, on their own machine.
|
|
3
|
+
*
|
|
4
|
+
* The platform cannot open `localhost`, and it never tries: the chat files the drive on the platform,
|
|
5
|
+
* and this daemon — which already calls out to the platform for every Harness turn — asks whether one
|
|
6
|
+
* is waiting for its project, drives the app it has connected, and reports how it ended. Every call
|
|
7
|
+
* goes OUT from this machine, so nothing listens for the platform and no port is opened.
|
|
8
|
+
*
|
|
9
|
+
* It only asks while an app is connected. A drive taken with nothing to drive would fail for a reason
|
|
10
|
+
* the person can fix in ten seconds (open the app), and leaving it unclaimed lets the chat tell them
|
|
11
|
+
* exactly that. One drive at a time: a second request waits for the first to finish.
|
|
12
|
+
*/
|
|
13
|
+
import { DriveMode, DriveTarget, LinkPath, PLATFORM_LINK_VERSION, PresenterTone, TOOL_SESSION_LIMITS, parseDriveSpec, parseToolSessionReply, sessionAskRetryable, } from '@reticlehq/core';
|
|
14
|
+
import { serverOptionsFromEnv } from './server-driver.js';
|
|
15
|
+
/**
|
|
16
|
+
* How often to ask while an app is connected.
|
|
17
|
+
*
|
|
18
|
+
* ponytail: a poll, one small GET per interval per connected daemon; a long-poll or a stream on the
|
|
19
|
+
* platform side is the upgrade if the chat's wait for a claim ever needs to be shorter than this.
|
|
20
|
+
*/
|
|
21
|
+
export const REMOTE_DRIVE_POLL_MS = 3_000;
|
|
22
|
+
const REQUEST_TIMEOUT_MS = 10_000;
|
|
23
|
+
/**
|
|
24
|
+
* How often the live picture is taken while a chat-requested drive runs. About one a second is what
|
|
25
|
+
* reads as video in the chat without turning a minute's drive into megabytes.
|
|
26
|
+
*/
|
|
27
|
+
export const REMOTE_DRIVE_FRAME_MS = 1_000;
|
|
28
|
+
/** The same JPEG quality the platform's own checks film at: about 40 KB a picture. */
|
|
29
|
+
export const REMOTE_DRIVE_JPEG_QUALITY = 50;
|
|
30
|
+
/** What the driven tab's HUD says once a chat-requested drive is over. */
|
|
31
|
+
export const REMOTE_DRIVE_ENDED = {
|
|
32
|
+
PROVED: 'Harness drive finished: proved',
|
|
33
|
+
NOT_PROVED: 'Harness drive finished: not proved',
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* End the driven tab's session once its drive is over, so its HUD stops counting "planning next
|
|
37
|
+
* action" for an agent that is not there. Auto-ended, not ended: the next agent or drive revives it.
|
|
38
|
+
*/
|
|
39
|
+
export function endDrivenTab(tab, outcome) {
|
|
40
|
+
tab?.autoEnd(outcome.ok ? REMOTE_DRIVE_ENDED.PROVED : REMOTE_DRIVE_ENDED.NOT_PROVED, outcome.ok ? PresenterTone.CALM : PresenterTone.WARN);
|
|
41
|
+
}
|
|
42
|
+
export function startRemoteDrives(deps) {
|
|
43
|
+
const doFetch = deps.fetch ?? ((url, init) => fetch(url, init));
|
|
44
|
+
let busy = false;
|
|
45
|
+
let stopped = false;
|
|
46
|
+
const tick = async (told) => {
|
|
47
|
+
if (busy || stopped || (undefined === told && !deps.connected()))
|
|
48
|
+
return;
|
|
49
|
+
const platform = told ?? serverOptionsFromEnv(await deps.env());
|
|
50
|
+
if (platform === undefined)
|
|
51
|
+
return;
|
|
52
|
+
const headers = {
|
|
53
|
+
'content-type': 'application/json',
|
|
54
|
+
authorization: `Bearer ${platform.apiKey}`,
|
|
55
|
+
};
|
|
56
|
+
busy = true;
|
|
57
|
+
try {
|
|
58
|
+
const res = await doFetch(`${platform.url}${LinkPath.NEXT_DRIVE}`, {
|
|
59
|
+
method: 'GET',
|
|
60
|
+
headers,
|
|
61
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok)
|
|
64
|
+
return;
|
|
65
|
+
const drive = (await res.json()).drive;
|
|
66
|
+
if (null === drive || undefined === drive)
|
|
67
|
+
return;
|
|
68
|
+
if ('string' !== typeof drive.id || 'string' !== typeof drive.goal)
|
|
69
|
+
return;
|
|
70
|
+
deps.log?.(`reticle: driving a request from the platform chat: ${drive.goal}`);
|
|
71
|
+
const driveId = drive.id;
|
|
72
|
+
const driveUrl = `${platform.url}${LinkPath.driveResult(driveId)}`;
|
|
73
|
+
const app = 'string' === typeof drive.appKey
|
|
74
|
+
? { key: drive.appKey, url: 'string' === typeof drive.appUrl ? drive.appUrl : null }
|
|
75
|
+
: undefined;
|
|
76
|
+
// Read field by field: a spec from a newer platform still drives, minus what is unknown here.
|
|
77
|
+
const parsed = parseDriveSpec(drive.spec);
|
|
78
|
+
const prepared = deps.prepare === undefined
|
|
79
|
+
? {
|
|
80
|
+
sessionId: await deps.pick?.(drive.goal, platform.apiKey, app),
|
|
81
|
+
applied: { target: DriveTarget.TAB, mode: DriveMode.LOCAL },
|
|
82
|
+
ignored: [],
|
|
83
|
+
}
|
|
84
|
+
: await deps.prepare({
|
|
85
|
+
goal: drive.goal,
|
|
86
|
+
apiKey: platform.apiKey,
|
|
87
|
+
...(app === undefined ? {} : { app }),
|
|
88
|
+
spec: parsed.spec,
|
|
89
|
+
});
|
|
90
|
+
const ignored = [...parsed.ignored, ...prepared.ignored];
|
|
91
|
+
const picked = prepared.sessionId;
|
|
92
|
+
if (null === picked) {
|
|
93
|
+
await doFetch(driveUrl, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers,
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
ok: false,
|
|
98
|
+
summary: ('refusal' in prepared ? prepared.refusal : undefined) ?? NO_OWN_TAB,
|
|
99
|
+
verdict: 'unknown',
|
|
100
|
+
filmed: false,
|
|
101
|
+
applied: prepared.applied,
|
|
102
|
+
ignored,
|
|
103
|
+
}),
|
|
104
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const sessionId = picked;
|
|
109
|
+
const frame = deps.frame;
|
|
110
|
+
// Only when the person asked to watch: a picture of their app leaves this machine for it.
|
|
111
|
+
const filming = false === drive.record || frame === undefined
|
|
112
|
+
? undefined
|
|
113
|
+
: film(() => frame(sessionId), deps.frameIntervalMs ?? REMOTE_DRIVE_FRAME_MS, (jpeg) => doFetch(`${platform.url}${LinkPath.driveFrames(driveId)}`, {
|
|
114
|
+
method: 'POST',
|
|
115
|
+
headers,
|
|
116
|
+
body: JSON.stringify({ jpeg }),
|
|
117
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
118
|
+
}));
|
|
119
|
+
// The platform decides every step; this daemon only runs the tools it is told to.
|
|
120
|
+
const session = deps.session;
|
|
121
|
+
if (DriveMode.PLATFORM === prepared.applied.mode && session !== undefined) {
|
|
122
|
+
try {
|
|
123
|
+
const tools = session(sessionId);
|
|
124
|
+
await runToolSession((body) => doFetch(`${platform.url}${LinkPath.driveSession(driveId)}`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers,
|
|
127
|
+
body: JSON.stringify(body),
|
|
128
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
129
|
+
}), tools, {
|
|
130
|
+
applied: prepared.applied,
|
|
131
|
+
ignored,
|
|
132
|
+
...(tools.tools === undefined ? {} : { tools: tools.tools }),
|
|
133
|
+
}, deps.now ?? (() => Date.now()), deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))));
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await filming?.stop();
|
|
137
|
+
deps.settle?.(sessionId, { ok: true, summary: '' });
|
|
138
|
+
}
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
let outcome;
|
|
142
|
+
let framesSent = 0;
|
|
143
|
+
try {
|
|
144
|
+
outcome = await deps.drive(drive.goal, sessionId);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
outcome = { ok: false, summary: error instanceof Error ? error.message : String(error) };
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
framesSent = (await filming?.stop()) ?? 0;
|
|
151
|
+
}
|
|
152
|
+
deps.settle?.(sessionId, outcome);
|
|
153
|
+
await doFetch(driveUrl, {
|
|
154
|
+
method: 'POST',
|
|
155
|
+
headers,
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
...outcome,
|
|
158
|
+
filmed: 0 < framesSent,
|
|
159
|
+
applied: prepared.applied,
|
|
160
|
+
ignored,
|
|
161
|
+
}),
|
|
162
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
// Offline, or the platform is down: the next tick asks again. The local daemon never fails for it.
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
busy = false;
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
const timer = setInterval(() => void tick(), deps.intervalMs ?? REMOTE_DRIVE_POLL_MS);
|
|
173
|
+
timer.unref();
|
|
174
|
+
return {
|
|
175
|
+
tick,
|
|
176
|
+
stop: () => {
|
|
177
|
+
stopped = true;
|
|
178
|
+
clearInterval(timer);
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* A drive the platform orchestrates: post what the last calls did, run the next batch, until the
|
|
184
|
+
* platform says the drive is over, stops answering, or the drive reaches its call budget. Every call
|
|
185
|
+
* is answered, failed ones included: the platform decides what a failure means.
|
|
186
|
+
*/
|
|
187
|
+
async function runToolSession(post, tools, first, now, sleep) {
|
|
188
|
+
let results = [];
|
|
189
|
+
let made = 0;
|
|
190
|
+
// Numbered, so an ask sent again after a failure is the SAME ask: the platform answers it once.
|
|
191
|
+
for (let ask = 0;; ask += 1) {
|
|
192
|
+
const body = {
|
|
193
|
+
protocol: PLATFORM_LINK_VERSION,
|
|
194
|
+
ask,
|
|
195
|
+
results,
|
|
196
|
+
...(0 === ask ? first : {}),
|
|
197
|
+
};
|
|
198
|
+
const res = await askWithRetries(() => post(body), sleep);
|
|
199
|
+
// Refused, or still failing after every retry: stop. The platform ends a drive that stops asking.
|
|
200
|
+
if (res === undefined || !res.ok)
|
|
201
|
+
return;
|
|
202
|
+
const reply = parseToolSessionReply(await res.json());
|
|
203
|
+
if (reply.done)
|
|
204
|
+
return;
|
|
205
|
+
results = [];
|
|
206
|
+
for (const call of reply.calls) {
|
|
207
|
+
if (made >= TOOL_SESSION_LIMITS.MAX_CALLS)
|
|
208
|
+
return;
|
|
209
|
+
made += 1;
|
|
210
|
+
const at = now();
|
|
211
|
+
try {
|
|
212
|
+
const result = await tools.invoke(call.tool, call.args);
|
|
213
|
+
results.push({ seq: call.seq, ok: true, result, ms: now() - at });
|
|
214
|
+
}
|
|
215
|
+
catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
results.push({ seq: call.seq, ok: false, error: message, ms: now() - at });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* One ask, sent again while the network or the platform fails it (a dropped connection, a 5xx, a
|
|
224
|
+
* 429), waiting longer each time. A refusal is the platform's answer and is returned as it is.
|
|
225
|
+
* Undefined when every attempt failed to get any answer at all.
|
|
226
|
+
*/
|
|
227
|
+
async function askWithRetries(send, sleep) {
|
|
228
|
+
for (let attempt = 0;; attempt += 1) {
|
|
229
|
+
let res;
|
|
230
|
+
try {
|
|
231
|
+
res = await send();
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
res = undefined;
|
|
235
|
+
}
|
|
236
|
+
if (res !== undefined && !sessionAskRetryable(res.status))
|
|
237
|
+
return res;
|
|
238
|
+
if (attempt >= TOOL_SESSION_LIMITS.RETRIES)
|
|
239
|
+
return res;
|
|
240
|
+
await sleep(TOOL_SESSION_LIMITS.RETRY_BASE_MS * 2 ** attempt);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Take a picture every `everyMs` and send it, until stopped. One capture at a time, and a picture
|
|
245
|
+
* identical to the last one sent is skipped: a page standing still is not worth a request a second.
|
|
246
|
+
* A failed capture or send is skipped too, never fatal: the drive matters, the picture of it less.
|
|
247
|
+
* `stop` answers how many pictures were sent.
|
|
248
|
+
*/
|
|
249
|
+
function film(frame, everyMs, send) {
|
|
250
|
+
let last;
|
|
251
|
+
let sent = 0;
|
|
252
|
+
let busy;
|
|
253
|
+
const shoot = async () => {
|
|
254
|
+
try {
|
|
255
|
+
const shot = await frame();
|
|
256
|
+
if (shot === undefined)
|
|
257
|
+
return;
|
|
258
|
+
const jpeg = Buffer.from(shot).toString('base64');
|
|
259
|
+
if (jpeg === last)
|
|
260
|
+
return;
|
|
261
|
+
last = jpeg;
|
|
262
|
+
// Counted only once the platform took it: a refused picture is not a filmed drive.
|
|
263
|
+
if ((await send(jpeg)).ok)
|
|
264
|
+
sent += 1;
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
// The next tick tries again.
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const timer = setInterval(() => {
|
|
271
|
+
if (busy === undefined)
|
|
272
|
+
busy = shoot().finally(() => (busy = undefined));
|
|
273
|
+
}, everyMs);
|
|
274
|
+
timer.unref();
|
|
275
|
+
return {
|
|
276
|
+
stop: async () => {
|
|
277
|
+
clearInterval(timer);
|
|
278
|
+
await busy;
|
|
279
|
+
return sent;
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
/** What a connected tab says about itself, as far as picking one goes. */
|
|
284
|
+
/** The refusal a drive gets when every connected tab belongs to some other project. */
|
|
285
|
+
export const NO_OWN_TAB = "No tab of this project is connected to Reticle on this machine, and a request is never run in another project's app. Open this project's app, then ask again.";
|
|
286
|
+
/**
|
|
287
|
+
* `pickDriveSession` over the tabs of the project `apiKey` is for, or null when there are none. One
|
|
288
|
+
* daemon serves every project on the machine but polls with ONE credential, so without this a
|
|
289
|
+
* request made in project A drove project B's tab and sent pictures of it to A's chat.
|
|
290
|
+
*/
|
|
291
|
+
export async function pickOwnDriveSession(tabs, goal, apiKey, keyOf) {
|
|
292
|
+
const own = [];
|
|
293
|
+
for (const tab of tabs) {
|
|
294
|
+
const key = await keyOf(tab.sessionId).catch(() => undefined);
|
|
295
|
+
if (key === apiKey)
|
|
296
|
+
own.push(tab);
|
|
297
|
+
}
|
|
298
|
+
return pickDriveSession(own, goal) ?? null;
|
|
299
|
+
}
|
|
300
|
+
/** The address of an http(s) URL, `host:port`, for matching a tab to the app a request names. */
|
|
301
|
+
const hostOf = (url) => {
|
|
302
|
+
try {
|
|
303
|
+
return new URL(url).host;
|
|
304
|
+
}
|
|
305
|
+
catch {
|
|
306
|
+
return undefined;
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
const URLS_IN_TEXT = /https?:\/\/[^\s"'<>,]+/gi;
|
|
310
|
+
/**
|
|
311
|
+
* The tab a chat-requested drive should use: one whose address the request names, then a visible
|
|
312
|
+
* one over a hidden (throttled) one, then the one heard from most recently.
|
|
313
|
+
*/
|
|
314
|
+
export function pickDriveSession(tabs, goal) {
|
|
315
|
+
const named = new Set((goal.match(URLS_IN_TEXT) ?? []).flatMap((url) => hostOf(url) ?? []));
|
|
316
|
+
const rank = (tab) => [
|
|
317
|
+
named.has(hostOf(tab.url) ?? '') ? 0 : 1,
|
|
318
|
+
tab.hidden ? 1 : 0,
|
|
319
|
+
tab.lastSeenMs,
|
|
320
|
+
];
|
|
321
|
+
const better = (a, b) => {
|
|
322
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
323
|
+
const x = a[i] ?? 0;
|
|
324
|
+
const y = b[i] ?? 0;
|
|
325
|
+
if (x !== y)
|
|
326
|
+
return x < y;
|
|
327
|
+
}
|
|
328
|
+
return false;
|
|
329
|
+
};
|
|
330
|
+
let best;
|
|
331
|
+
for (const tab of tabs)
|
|
332
|
+
if (best === undefined || better(rank(tab), rank(best)))
|
|
333
|
+
best = tab;
|
|
334
|
+
return best?.sessionId;
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* The verdict on a drive's goal: the platform's judgement of the goal when it made one, else the
|
|
338
|
+
* goals the harness checked itself. A drive that broke, proved nothing, or whose goal nobody judged
|
|
339
|
+
* is `unknown`: "it ran and nothing threw" is not "the goal was reached".
|
|
340
|
+
*/
|
|
341
|
+
export function driveVerdict(out) {
|
|
342
|
+
if (out.error !== undefined)
|
|
343
|
+
return 'unknown';
|
|
344
|
+
// A goal judged unmet, or a check that came back "no", is a failed drive whatever else held.
|
|
345
|
+
if (false === out.goalMet)
|
|
346
|
+
return 'no';
|
|
347
|
+
if (0 < (out.checks?.failed ?? 0))
|
|
348
|
+
return 'no';
|
|
349
|
+
// The quoted texts are looked for on the page the drive ENDED on. None there refutes the goal;
|
|
350
|
+
// some missing does not, because a goal that quotes where it starts ("from "Count is 0" to
|
|
351
|
+
// "Count is 1"") loses that text by working. Then the goal's own judgement decides, as below.
|
|
352
|
+
const goals = out.goals ?? [];
|
|
353
|
+
if (0 < goals.length && goals.every((g) => 'no' === g.verified))
|
|
354
|
+
return 'no';
|
|
355
|
+
if (0 < goals.length && !goals.some((g) => 'yes' === g.verified))
|
|
356
|
+
return 'unknown';
|
|
357
|
+
// A yes needs a check that held behind it: the model saying the goal was met is not evidence.
|
|
358
|
+
if (!out.proved || 0 === (out.checks?.held ?? 0))
|
|
359
|
+
return 'unknown';
|
|
360
|
+
if (true === out.goalMet)
|
|
361
|
+
return 'yes';
|
|
362
|
+
if (0 < goals.length && goals.every((g) => 'yes' === g.verified))
|
|
363
|
+
return 'yes';
|
|
364
|
+
return 'unknown';
|
|
365
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ScriptStatus, type FlowFile, type FlowStep, type PlanView } from '@reticlehq/core';
|
|
2
|
+
import { type DriveScript } from '@reticlehq/core/artifacts';
|
|
3
|
+
type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
|
|
4
|
+
export interface ScriptAsk {
|
|
5
|
+
about: string;
|
|
6
|
+
flows: readonly FlowFile[];
|
|
7
|
+
replay: readonly string[];
|
|
8
|
+
goals: readonly string[];
|
|
9
|
+
gaps: readonly string[];
|
|
10
|
+
rules: readonly string[];
|
|
11
|
+
/** Personas the project already drove; the platform proposes new ones only when there are none. */
|
|
12
|
+
personas?: readonly {
|
|
13
|
+
name: string;
|
|
14
|
+
journey: string;
|
|
15
|
+
}[];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* The personas earlier drives used, read from their flows' intents (`Name: journey`). Proposed
|
|
19
|
+
* afresh on every run, they were different people each time, so each run's flows had new names
|
|
20
|
+
* and the old ones were never replayed or updated.
|
|
21
|
+
*/
|
|
22
|
+
export declare function personasIn(flows: readonly FlowFile[]): {
|
|
23
|
+
name: string;
|
|
24
|
+
journey: string;
|
|
25
|
+
}[];
|
|
26
|
+
/** Equal for two steps that drive the same control the same way; the values never leave. */
|
|
27
|
+
export declare function stepHash(step: FlowStep): string;
|
|
28
|
+
export declare function proposeScript(platform: {
|
|
29
|
+
url: string;
|
|
30
|
+
apiKey: string;
|
|
31
|
+
}, ask: ScriptAsk, doFetch?: FetchLike): Promise<{
|
|
32
|
+
script: DriveScript;
|
|
33
|
+
planId?: string;
|
|
34
|
+
} | undefined>;
|
|
35
|
+
/** One journey's outcome, as the platform keeps it beside the plan. */
|
|
36
|
+
export interface JourneyResult {
|
|
37
|
+
id: string;
|
|
38
|
+
title: string;
|
|
39
|
+
status: ScriptStatus;
|
|
40
|
+
}
|
|
41
|
+
/** How each journey of a run plan went, once each. */
|
|
42
|
+
export declare function journeyResults(view: PlanView): JourneyResult[];
|
|
43
|
+
/** Tell the platform how its plan went. A failure costs the next plan its memory, never this run. */
|
|
44
|
+
export declare function reportPlanResults(platform: {
|
|
45
|
+
url: string;
|
|
46
|
+
apiKey: string;
|
|
47
|
+
}, planId: string, results: readonly JourneyResult[], doFetch?: FetchLike): Promise<boolean>;
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The drive plan, asked of the platform.
|
|
3
|
+
*
|
|
4
|
+
* The platform plans the whole drive: it proposes the personas, orders the saved flows, finds the
|
|
5
|
+
* shared starts to branch from and writes the product's rules into each open journey. What it is
|
|
6
|
+
* sent is what the project already knows, and each saved step goes as a short hash, never its values,
|
|
7
|
+
* which is all a plan needs to tell two steps apart.
|
|
8
|
+
*
|
|
9
|
+
* The answer is not trusted: it must parse as a drive script and pass the same checks a local plan
|
|
10
|
+
* does, or it is dropped and the daemon plans locally.
|
|
11
|
+
*
|
|
12
|
+
* The platform keeps every plan under an id, and is told afterwards how each journey went: that is
|
|
13
|
+
* what the next plan learns from (what failed is driven first).
|
|
14
|
+
*/
|
|
15
|
+
import { createHash } from 'node:crypto';
|
|
16
|
+
import { ScriptStatus, StepEffect, } from '@reticlehq/core';
|
|
17
|
+
import { DriveScriptSchema, checkScript } from '@reticlehq/core/artifacts';
|
|
18
|
+
const SCRIPTS_PATH = '/v1/harness/scripts';
|
|
19
|
+
const RESULTS_PATH = '/results';
|
|
20
|
+
const TIMEOUT_MS = 90_000;
|
|
21
|
+
/** Characters of a step hash: enough to tell a flow's steps apart, too few to carry anything. */
|
|
22
|
+
const STEP_HASH_CHARS = 12;
|
|
23
|
+
/** How long a persona's name may be, read back out of a flow's intent. */
|
|
24
|
+
const MAX_PERSONA_NAME = 80;
|
|
25
|
+
const MAX_PERSONAS = 5;
|
|
26
|
+
/**
|
|
27
|
+
* The personas earlier drives used, read from their flows' intents (`Name: journey`). Proposed
|
|
28
|
+
* afresh on every run, they were different people each time, so each run's flows had new names
|
|
29
|
+
* and the old ones were never replayed or updated.
|
|
30
|
+
*/
|
|
31
|
+
export function personasIn(flows) {
|
|
32
|
+
const seen = new Map();
|
|
33
|
+
for (const flow of flows) {
|
|
34
|
+
// The first line only: an older drive saved the product's rules below the journey.
|
|
35
|
+
const intent = (flow.intent ?? '').split('\n')[0] ?? '';
|
|
36
|
+
const at = intent.indexOf(':');
|
|
37
|
+
if (at <= 0 || MAX_PERSONA_NAME < at)
|
|
38
|
+
continue;
|
|
39
|
+
const name = intent.slice(0, at).trim();
|
|
40
|
+
const journey = intent.slice(at + 1).trim();
|
|
41
|
+
if (0 < name.length && 0 < journey.length && !seen.has(name))
|
|
42
|
+
seen.set(name, journey);
|
|
43
|
+
}
|
|
44
|
+
return [...seen].slice(0, MAX_PERSONAS).map(([name, journey]) => ({ name, journey }));
|
|
45
|
+
}
|
|
46
|
+
/** Equal for two steps that drive the same control the same way; the values never leave. */
|
|
47
|
+
export function stepHash(step) {
|
|
48
|
+
return createHash('sha256')
|
|
49
|
+
.update(JSON.stringify([step.tool, step.anchor, step.action, step.args, step.steps, step.invoke]))
|
|
50
|
+
.digest('hex')
|
|
51
|
+
.slice(0, STEP_HASH_CHARS);
|
|
52
|
+
}
|
|
53
|
+
export async function proposeScript(platform, ask, doFetch = (url, init) => fetch(url, init)) {
|
|
54
|
+
try {
|
|
55
|
+
const res = await doFetch(`${platform.url}${SCRIPTS_PATH}`, {
|
|
56
|
+
method: 'POST',
|
|
57
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${platform.apiKey}` },
|
|
58
|
+
body: JSON.stringify({
|
|
59
|
+
about: ask.about,
|
|
60
|
+
flows: ask.flows.map((flow) => ({
|
|
61
|
+
name: flow.name,
|
|
62
|
+
...(flow.intent === undefined ? {} : { intent: flow.intent }),
|
|
63
|
+
needs: flow.needs ?? [],
|
|
64
|
+
steps: flow.steps.map(stepHash),
|
|
65
|
+
commits: flow.steps.flatMap((step, i) => (StepEffect.COMMITS === step.effect ? [i] : [])),
|
|
66
|
+
// How much it proves: a journey whose flows assert nothing is driven again, not replayed.
|
|
67
|
+
checks: flow.steps.filter((step) => step.expect !== undefined || true === step.steps?.some((s) => s.expect !== undefined)).length,
|
|
68
|
+
})),
|
|
69
|
+
replay: ask.replay,
|
|
70
|
+
goals: ask.goals,
|
|
71
|
+
gaps: ask.gaps,
|
|
72
|
+
rules: ask.rules,
|
|
73
|
+
...(ask.personas === undefined || 0 === ask.personas.length
|
|
74
|
+
? {}
|
|
75
|
+
: { personas: ask.personas }),
|
|
76
|
+
}),
|
|
77
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok)
|
|
80
|
+
return undefined;
|
|
81
|
+
const body = (await res.json());
|
|
82
|
+
const parsed = DriveScriptSchema.safeParse(body.script);
|
|
83
|
+
if (!parsed.success || 0 < checkScript(parsed.data).length)
|
|
84
|
+
return undefined;
|
|
85
|
+
return {
|
|
86
|
+
script: parsed.data,
|
|
87
|
+
...('string' === typeof body.planId ? { planId: body.planId } : {}),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** Worst first: a journey that ran in two lanes reports the worse of the two. */
|
|
95
|
+
const SEVERITY = [
|
|
96
|
+
ScriptStatus.FAILED,
|
|
97
|
+
ScriptStatus.BLOCKED,
|
|
98
|
+
ScriptStatus.RUNNING,
|
|
99
|
+
ScriptStatus.PENDING,
|
|
100
|
+
ScriptStatus.NOT_TAKEN,
|
|
101
|
+
ScriptStatus.PASSED,
|
|
102
|
+
];
|
|
103
|
+
/** How each journey of a run plan went, once each. */
|
|
104
|
+
export function journeyResults(view) {
|
|
105
|
+
const byId = new Map();
|
|
106
|
+
for (const lane of view.lanes)
|
|
107
|
+
for (const card of lane.journeys) {
|
|
108
|
+
const seen = byId.get(card.id);
|
|
109
|
+
if (seen === undefined || SEVERITY.indexOf(card.status) < SEVERITY.indexOf(seen.status))
|
|
110
|
+
byId.set(card.id, { id: card.id, title: card.title, status: card.status });
|
|
111
|
+
}
|
|
112
|
+
return [...byId.values()];
|
|
113
|
+
}
|
|
114
|
+
/** Tell the platform how its plan went. A failure costs the next plan its memory, never this run. */
|
|
115
|
+
export async function reportPlanResults(platform, planId, results, doFetch = (url, init) => fetch(url, init)) {
|
|
116
|
+
try {
|
|
117
|
+
const res = await doFetch(`${platform.url}${SCRIPTS_PATH}/${encodeURIComponent(planId)}${RESULTS_PATH}`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
headers: { 'content-type': 'application/json', authorization: `Bearer ${platform.apiKey}` },
|
|
120
|
+
body: JSON.stringify({ results }),
|
|
121
|
+
signal: AbortSignal.timeout(TIMEOUT_MS),
|
|
122
|
+
});
|
|
123
|
+
return res.ok;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ModelDriver } from '../harness.js';
|
|
2
|
+
export declare const SERVER_DRIVER_NAME = "server";
|
|
3
|
+
type FetchLike = (url: string, init: RequestInit) => Promise<Response>;
|
|
4
|
+
export interface ServerDriverOptions {
|
|
5
|
+
url: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
persona?: string;
|
|
8
|
+
/** The goal a drive with no persona is for: the platform names its flows after it. */
|
|
9
|
+
journey?: string;
|
|
10
|
+
/** journey | brute-force | stress | madman | security. The platform defaults to journey. */
|
|
11
|
+
mode?: string;
|
|
12
|
+
/** What `.reticle` already knows, as text: saved journeys and open intents. */
|
|
13
|
+
plan?: string;
|
|
14
|
+
/** The same plan, structured, for the platform's Jev engine. */
|
|
15
|
+
planSteps?: readonly {
|
|
16
|
+
kind: string;
|
|
17
|
+
target: string;
|
|
18
|
+
why: string;
|
|
19
|
+
}[];
|
|
20
|
+
/** The signal names the app declares, so the engine can claim one by name. */
|
|
21
|
+
vocabulary?: readonly string[];
|
|
22
|
+
/** Where secret field values live. Only their NAMES go to the platform. Defaults to `process.env`. */
|
|
23
|
+
env?: Record<string, string | undefined>;
|
|
24
|
+
maxSteps?: number;
|
|
25
|
+
fetch?: FetchLike;
|
|
26
|
+
}
|
|
27
|
+
/** The platform's url and this project's key, when the machine is linked. */
|
|
28
|
+
export declare function serverOptionsFromEnv(env: Record<string, string | undefined>): {
|
|
29
|
+
url: string;
|
|
30
|
+
apiKey: string;
|
|
31
|
+
} | undefined;
|
|
32
|
+
export declare class ServerHarnessError extends Error {
|
|
33
|
+
readonly status: number;
|
|
34
|
+
constructor(message: string, status: number);
|
|
35
|
+
}
|
|
36
|
+
export declare function serverDriver(options: ServerDriverOptions): ModelDriver;
|
|
37
|
+
/** The secret fields this machine can fill, by name only. */
|
|
38
|
+
export declare function secretNames(env: Record<string, string | undefined>): string[];
|
|
39
|
+
/** The call's arguments with every secret placeholder replaced by its value from this machine. */
|
|
40
|
+
export declare function withSecrets(args: Record<string, unknown>, env: Record<string, string | undefined>): Record<string, unknown>;
|
|
41
|
+
export {};
|