diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 123e828a63..ba42c2c275 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -436,6 +436,10 @@ "types": "./src/facades/replay.ts", "default": "./src/facades/replay.ts" }, + "./replay-backend": { + "types": "./src/replay-backend.ts", + "default": "./src/replay-backend.ts" + }, "./runner-lease-context": { "types": "./src/runner-lease-context.ts", "default": "./src/runner-lease-context.ts" diff --git a/packages/contracts/src/replay-backend.ts b/packages/contracts/src/replay-backend.ts new file mode 100644 index 0000000000..1b8a0e35a8 --- /dev/null +++ b/packages/contracts/src/replay-backend.ts @@ -0,0 +1,207 @@ +import type { DaemonError } from '@agent-device/kernel/errors'; +import type { DaemonResponse, SessionRuntimeHints } from '@agent-device/kernel/contracts'; +import type { DeviceInfo } from '@agent-device/kernel/device'; +import type { SnapshotNode, SnapshotState } from '@agent-device/kernel/snapshot'; +import type { DaemonWireRequest } from './daemon-wire-request.ts'; +import type { + ReplayDivergenceResume, + ReplayDivergenceSuggestionBasis, +} from './replay-divergence.ts'; +import type { ReplayDispatchOptions, ReplayObservationAuthorityBinder } from './replay.ts'; +import type { SessionAction } from './session-action.ts'; +import type { + SnapshotDiagnosticsState, + SnapshotDiagnosticsSummary, +} from './snapshot-diagnostics.ts'; + +/** + * The replay backend contract (#3377): the session and command shapes a non-native replay engine + * is handed, the capabilities it implements, and the failure descriptor the daemon reports for a + * failed step. Everything here is type-only, so an engine can depend on this module without + * evaluating any host code. + */ + +/** + * The slice of the daemon's live session record replay reads. The daemon passes its full + * `SessionState`; replay never names the record itself, so the port depends on these fields and + * nothing else the daemon keeps on a session. + */ +export type ReplaySessionState = Readonly<{ + name: string; + device: DeviceInfo; + appBundleId?: string; + actions: SessionAction[]; + trace?: Readonly<{ outPath: string }>; + /** Read for presence only: an active recording means replay video must not start another. */ + screenRecording?: object; + snapshotDiagnostics?: Pick; +}>; + +export type ReplaySessionStore = Readonly<{ + get: () => ReplaySessionState | undefined; + lookup: () => Readonly<{ address: string; session: ReplaySessionState }> | undefined; + getRuntimeHints: () => SessionRuntimeHints | undefined; + ensureSessionDir: () => string; + /** Rejects the request when its device/platform selectors contradict the session's binding. */ + assertSelectorMatches: (flags: DaemonWireRequest['flags']) => void; + /** + * The runtime hints an `open` dispatched from this session would run with: the request's own, + * the session's persisted ones, and the platform defaults the daemon owns. + */ + resolveOpenRuntimeHints: (params: { + request: DaemonWireRequest; + device?: DeviceInfo; + platform?: 'ios' | 'android'; + }) => SessionRuntimeHints | undefined; +}>; + +/** + * The replay side of the ref-publication owner: the operational capture it reads, a binder for + * the daemon-side authority that records a capture and publishes exactly its own projection, and + * the daemon's snapshot capture over this session. The session-store pair the authority is drawn + * from stays in the daemon. + */ +export type ReplaySessionObservation = Readonly<{ + get: () => ReplaySessionState | undefined; + bindAuthority: (signal?: AbortSignal) => ReturnType & + Readonly<{ + capture: (params: { + flags: DaemonWireRequest['flags']; + logPath: string; + }) => Promise<{ snapshot: SnapshotState }>; + }>; +}>; + +export type ReplayDispatchRequest = DaemonWireRequest & + Readonly<{ dispatch?: ReplayDispatchOptions }>; + +export type ReplayInvoke = (request: ReplayDispatchRequest) => Promise; + +/** + * The dispatch options a backend may ask the daemon to honor on a nested public command. A key + * a backend adds without a counterpart in `ReplayDispatchOptions` fails to compile here. + */ +export type ReplayBackendDispatchOptions = Pick< + ReplayDispatchOptions, + | 'closeAppOnly' + | 'killApp' + | 'observationOnly' + | 'gestureViewport' + | 'gestureExecutionProfile' + | 'settingsAppBundleId' +>; + +/** One execution step a backend reports while an attempt runs. */ +export type ReplayBackendStep = Readonly<{ + index: number; + total: number; + command?: string; + value?: string; +}>; + +export type ReplayBackendSession = Readonly<{ + /** The effective SessionStore key selected by request binding. */ + name: string; + logPath: string; + store: ReplaySessionStore; + /** Bound observation reads and writes used by the ref-publication owner and divergence capture. */ + observationStore: ReplaySessionObservation; +}>; + +/** One replay run as a backend sees it: the prepared request, its session, and the dispatch port. */ +export type ReplayBackendCommand = Readonly<{ + request: DaemonWireRequest; + /** + * True when the request reached the daemon over its public HTTP surface only: flow scripts are + * then untrusted and `runScript` HTTP calls may not reach private addresses. Absent means a + * local caller, which trusts its own scripts. + */ + publicNetworkOnly?: true; + /** Dispatch options every request this command issues carries, before its own. */ + dispatch?: ReplayDispatchOptions; + session: ReplayBackendSession; + invoke: ReplayInvoke; + tracePath?: string; + onStep?: (step: ReplayBackendStep) => void; +}>; + +/** What a backend reports about one source before any run: the shared scheduling vocabulary. */ +export type ReplaySourceInspection = Readonly<{ + /** The flow's authored title, when the format carries one. */ + title: string | undefined; + /** A static app target the entry resolves to, for advisory device binding. */ + appTarget: string | undefined; +}>; + +/** Reads one flow file, or reports that it is unavailable (#1802 readers). */ +export type ReplayOptionalSourceReader = (resolvedPath: string) => string | undefined; + +export type ReplayBackendExportWarning = Readonly<{ + line: number; + action: string; + message: string; +}>; + +export type ReplayBackendExportResult = Readonly<{ + yaml: string; + warnings: ReplayBackendExportWarning[]; +}>; + +/** + * `TId` is the format grammar's backend vocabulary (`ReplayBackendId` in `@agent-device/ad-script`, + * which sits above this package), so the registry's keys and the `--maestro` flag's accepted + * values cannot drift apart. + */ +export type ReplayBackend = Readonly<{ + id: TId; + /** Expands the entry flow's include closure into a path-to-text map (best effort, #1802). */ + collectSourceFiles(params: { + entryPath: string; + entrySource: string; + env?: Readonly>; + readSource: ReplayOptionalSourceReader; + }): Record; + /** Reads title/app-target metadata from one source, without running it. Throws on an invalid flow. */ + inspectSource(source: string, sourcePath: string): ReplaySourceInspection; + /** Runs one replay through the backend's engine, owning no session or wire state. */ + runReplay(command: ReplayBackendCommand): Promise; + /** Converts a native `.ad` action list into the backend's own script format. */ + exportReplayScript( + actions: SessionAction[], + options: { actionLines?: number[]; metadata?: { env?: Record } }, + ): ReplayBackendExportResult; +}>; + +/** One failed step of a backend run, in the vocabulary the divergence report publishes. */ +export type ReplayBackendActionFailure = Readonly<{ + error: DaemonError; + req: DaemonWireRequest; + sessionStore: ReplaySessionStore; + observationStore: ReplaySessionObservation; + logPath: string; + replayPath: string; + stepIndex: number; + source: Readonly<{ path?: string; line: number }>; + /** The authored step name, shown in the report and recorded as the failed action. */ + action: string; + /** The step's display label: its name plus its rendered argument. */ + actionLabel: string; + /** The `.ad`-shaped action the post-failure capture and suggestion ranking read. */ + captureAction: Readonly<{ + command: string; + positionals: readonly string[]; + flags: SessionAction['flags']; + }>; + /** Control-flow steps fail without suggestions; the post-failure screen is still captured. */ + isControl: boolean; + /** Values scrubbed from every published string. */ + redactions: readonly Readonly<{ name: string; value: string }>[]; + resume: ReplayDivergenceResume; + artifactPaths: readonly string[]; + warnings: readonly string[]; + snapshotDiagnostics?: SnapshotDiagnosticsSummary; + /** Candidate nodes the engine proposes for the failed step, before host ranking and bounding. */ + suggestNodes( + snapshot: Readonly<{ createdAt: number; nodes: SnapshotNode[] }>, + ): readonly Readonly<{ node: SnapshotNode; basis: ReplayDivergenceSuggestionBasis }>[]; +}>; diff --git a/packages/daemon/src/server/replay-device-selection.ts b/packages/daemon/src/server/replay-device-selection.ts index b0d37a510f..86ea2d5edc 100644 --- a/packages/daemon/src/server/replay-device-selection.ts +++ b/packages/daemon/src/server/replay-device-selection.ts @@ -6,7 +6,7 @@ import { import type { ResolveTargetDeviceOptions } from '@agent-device/device-selection/dispatch-resolve'; import type { CommandFlags } from '@agent-device/contracts/command'; import { readReplayScriptSourceFile } from '@agent-device/replay-port/script-source'; -import type { ReplayBackend } from '@agent-device/replay-port/replay-backend-registry'; +import type { ReplayBackend } from '@agent-device/contracts/replay-backend'; import type { DaemonRequest } from '../core/daemon-request.ts'; import { appTargetResolutionOptions, diff --git a/packages/maestro/src/daemon-port/daemon-runtime-public-operation.ts b/packages/maestro/src/daemon-port/daemon-runtime-public-operation.ts index d30915bf52..579e697fab 100644 --- a/packages/maestro/src/daemon-port/daemon-runtime-public-operation.ts +++ b/packages/maestro/src/daemon-port/daemon-runtime-public-operation.ts @@ -4,7 +4,7 @@ import type { MaestroDispatchSelector, MaestroSinglePointerGestureInput, } from '@agent-device/maestro'; -import type { GestureExecutionProfile } from '@agent-device/contracts/gesture-plan-types'; +import type { ReplayBackendDispatchOptions } from '@agent-device/contracts/replay-backend'; import type { Point, Rect } from '@agent-device/kernel/snapshot'; export type MaestroClickOptions = Pick< @@ -51,20 +51,22 @@ export type MaestroPublicOperation = * each option into its own request-private vocabulary before dispatch. Nothing here names live * session state, which is what lets the port live outside the daemon. */ -export type MaestroDaemonDispatchOptions = Readonly<{ - /** Terminate the targeted app without ending the owning daemon session. */ - closeAppOnly?: true; - /** With `closeAppOnly`: Maestro `killApp` process death (`am kill` on Android) instead of `stop`. */ - killApp?: true; - /** A hierarchy capture used as operational evidence only; it issues no client ref authority. */ - observationOnly?: true; - /** Provider-owned viewport already resolved for a nested gesture command. */ - gestureViewport?: Rect; - /** Execution profile for timed coordinate swipes projected to `gesture pan`. */ - gestureExecutionProfile?: GestureExecutionProfile; - /** Maestro `setPermissions` app targeting; the settings handler prefers it over the session app. */ - settingsAppBundleId?: string; -}>; +export type MaestroDaemonDispatchOptions = Omit< + ReplayBackendDispatchOptions, + 'closeAppOnly' | 'killApp' | 'observationOnly' +> & + Readonly<{ + /** + * Terminate the targeted app without ending the owning daemon session. Presence-only (as are + * `killApp` and `observationOnly`): the daemon treats `false` like omission, so the port never + * sends it. + */ + closeAppOnly?: true; + /** With `closeAppOnly`: Maestro `killApp` process death (`am kill` on Android) instead of `stop`. */ + killApp?: true; + /** A hierarchy capture used as operational evidence only; it issues no client ref authority. */ + observationOnly?: true; + }>; /** * One public daemon command the port asks the daemon to run on the replay's behalf. The daemon diff --git a/packages/replay-port/src/daemon-port/__tests__/session-replay-maestro-request.test.ts b/packages/replay-port/src/daemon-port/__tests__/session-replay-maestro-request.test.ts index 234c2770e1..66be7d8697 100644 --- a/packages/replay-port/src/daemon-port/__tests__/session-replay-maestro-request.test.ts +++ b/packages/replay-port/src/daemon-port/__tests__/session-replay-maestro-request.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import type { ReplayDispatchRequest } from '@agent-device/replay-port/command-types'; +import type { ReplayDispatchRequest } from '@agent-device/contracts/replay-backend'; import { maestroOperationDispatchRequest } from '../session-replay-maestro-request.ts'; const replay: ReplayDispatchRequest = { diff --git a/packages/replay-port/src/daemon-port/command-types.ts b/packages/replay-port/src/daemon-port/command-types.ts index 59e8633dcc..2d3f586276 100644 --- a/packages/replay-port/src/daemon-port/command-types.ts +++ b/packages/replay-port/src/daemon-port/command-types.ts @@ -1,66 +1,11 @@ import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { ReplayDivergenceResume, ReplayRepairHint } from '@agent-device/contracts/divergence'; import type { - ReplayDispatchOptions, - ReplayObservationAuthorityBinder, -} from '@agent-device/contracts/replay'; + ReplayBackendCommand, + ReplayBackendSession, +} from '@agent-device/contracts/replay-backend'; import type { SessionAction, SessionScope } from '@agent-device/contracts/session'; -import type { SnapshotDiagnosticsState } from '@agent-device/contracts/capture'; -import type { ReplayTestAttemptStepSink } from '@agent-device/replay-test'; -import type { DaemonResponse, SessionRuntimeHints } from '@agent-device/kernel/contracts'; -import type { DeviceInfo } from '@agent-device/kernel/device'; -import type { SnapshotState } from '@agent-device/kernel/snapshot'; - -/** - * The slice of the daemon's live session record replay reads. The daemon passes its full - * `SessionState`; replay never names the record itself, so the port depends on these fields and - * nothing else the daemon keeps on a session. - */ -export type ReplaySessionState = Readonly<{ - name: string; - device: DeviceInfo; - appBundleId?: string; - actions: SessionAction[]; - trace?: Readonly<{ outPath: string }>; - /** Read for presence only: an active recording means replay video must not start another. */ - screenRecording?: object; - snapshotDiagnostics?: Pick; -}>; - -export type ReplaySessionStore = Readonly<{ - get: () => ReplaySessionState | undefined; - lookup: () => Readonly<{ address: string; session: ReplaySessionState }> | undefined; - getRuntimeHints: () => SessionRuntimeHints | undefined; - ensureSessionDir: () => string; - /** Rejects the request when its device/platform selectors contradict the session's binding. */ - assertSelectorMatches: (flags: DaemonWireRequest['flags']) => void; - /** - * The runtime hints an `open` dispatched from this session would run with: the request's own, - * the session's persisted ones, and the platform defaults the daemon owns. - */ - resolveOpenRuntimeHints: (params: { - request: DaemonWireRequest; - device?: DeviceInfo; - platform?: 'ios' | 'android'; - }) => SessionRuntimeHints | undefined; -}>; - -/** - * The replay side of the ref-publication owner: the operational capture it reads, a binder for - * the daemon-side authority that records a capture and publishes exactly its own projection, and - * the daemon's snapshot capture over this session. The session-store pair the authority is drawn - * from stays in the daemon. - */ -export type ReplaySessionObservation = Readonly<{ - get: () => ReplaySessionState | undefined; - bindAuthority: (signal?: AbortSignal) => ReturnType & - Readonly<{ - capture: (params: { - flags: DaemonWireRequest['flags']; - logPath: string; - }) => Promise<{ snapshot: SnapshotState }>; - }>; -}>; +import type { DaemonResponse } from '@agent-device/kernel/contracts'; /** Immutable read projection of the repair-transaction fields the coordinator's writers touch. */ export type ReplaySessionView = Readonly<{ @@ -109,21 +54,11 @@ export type ReplayCoordinator = { export type ReplayTestSessionFactory = (sessionName: string, logPath: string) => ReplaySession; -export type ReplaySession = Readonly<{ - /** The effective SessionStore key selected by request binding. */ - name: string; - logPath: string; - store: ReplaySessionStore; - /** Bound observation reads and writes used by the ref-publication owner and divergence capture. */ - observationStore: ReplaySessionObservation; - /** The repair-transaction gateway the daemon bound over this session. */ - coordinator: ReplayCoordinator; -}>; - -export type ReplayDispatchRequest = DaemonWireRequest & - Readonly<{ dispatch?: ReplayDispatchOptions }>; - -export type ReplayInvoke = (request: ReplayDispatchRequest) => Promise; +export type ReplaySession = ReplayBackendSession & + Readonly<{ + /** The repair-transaction gateway the daemon bound over this session. */ + coordinator: ReplayCoordinator; + }>; /** Daemon policy replay consults but does not own. */ export type ReplayDaemonDependencies = Readonly<{ @@ -131,25 +66,13 @@ export type ReplayDaemonDependencies = Readonly<{ resolveSessionScope: (request: DaemonWireRequest) => SessionScope; }>; -export type ReplayCommand = Readonly<{ - request: DaemonWireRequest; - /** - * True when the request reached the daemon over its public HTTP surface only: flow scripts are - * then untrusted, `runScript` HTTP calls may not reach private addresses, and the Maestro engine - * decides script trust from this input instead of reading `req.internal`. Absent means a local - * caller, which trusts its own scripts. - */ - publicNetworkOnly?: true; - /** The isolation scope the daemon already resolved for this request, when it did. */ - resolvedSessionScope?: SessionScope; - /** Dispatch options every request this command issues carries, before its own. */ - dispatch?: ReplayDispatchOptions; - session: ReplaySession; - invoke: ReplayInvoke; - dependencies: ReplayDaemonDependencies; - tracePath?: string; - onStep?: ReplayTestAttemptStepSink; -}>; +export type ReplayCommand = Omit & + Readonly<{ + /** The isolation scope the daemon already resolved for this request, when it did. */ + resolvedSessionScope?: SessionScope; + session: ReplaySession; + dependencies: ReplayDaemonDependencies; + }>; type ReplayRequestContext = Readonly<{ token: DaemonWireRequest['token']; diff --git a/packages/replay-port/src/daemon-port/native-command.ts b/packages/replay-port/src/daemon-port/native-command.ts index 95a863dab4..1f587cd08f 100644 --- a/packages/replay-port/src/daemon-port/native-command.ts +++ b/packages/replay-port/src/daemon-port/native-command.ts @@ -15,12 +15,11 @@ import { REPLAY_SCRIPT_SOURCE_REQUIRED_MESSAGE, } from '@agent-device/replay-port/script-source'; import { prepareReplaySession } from './session-replay-runtime-session.ts'; +import type { ReplayCommand, ReplayCoordinator } from '@agent-device/replay-port/command-types'; import type { - ReplayCommand, - ReplayCoordinator, ReplaySessionState, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; import { errorResponse, type DaemonResponse } from '@agent-device/kernel/contracts'; /** diff --git a/packages/replay-port/src/daemon-port/replay-backend-failure.ts b/packages/replay-port/src/daemon-port/replay-backend-failure.ts new file mode 100644 index 0000000000..d03bd2710d --- /dev/null +++ b/packages/replay-port/src/daemon-port/replay-backend-failure.ts @@ -0,0 +1,119 @@ +import type { ReplayDivergence } from '@agent-device/contracts/divergence'; +import type { ReplayBackendActionFailure } from '@agent-device/contracts/replay-backend'; +import { + REPLAY_DIVERGENCE_SUGGESTION_LIMIT, + createReplayDivergenceSanitizer, + type ReplayVarScrubEntry, +} from '@agent-device/ad-replay/divergence'; +import { getRequestSignal } from '@agent-device/host-kit/request'; +import type { DaemonResponse } from '@agent-device/kernel/contracts'; +import { computeReplayRepairHint } from './repair-hint.ts'; +import { + buildReplayDivergenceSuggestionForNode, + buildDivergenceScreen, + captureDivergenceObservation, + toReplayRepairHintCapture, +} from './session-replay-divergence.ts'; +import { boundReplayDivergenceForSession } from './session-replay-divergence-publication.ts'; +import { + buildReplayDivergenceFailureResponseFromDescriptor, + hoistReplayFailureCauseDiagnosticMeta, +} from './session-replay-runtime-failure-response.ts'; +import { rankAndDedupeReplaySuggestions } from './session-replay-suggestion-ranking.ts'; + +/** + * Builds the REPLAY_DIVERGENCE response for one failed backend step: the post-failure screen + * capture, the engine's suggested nodes ranked and deduped, the bounded divergence report, and + * the resume contract. The backend names what failed; this owns how the daemon reports it. + */ +export async function buildReplayBackendActionFailureResponse( + failure: ReplayBackendActionFailure, +): Promise { + const { req, sessionStore, observationStore, logPath, captureAction } = failure; + const requestSignal = getRequestSignal(req.meta?.requestId); + const cause = hoistReplayFailureCauseDiagnosticMeta(failure.error); + const scrubVars: ReplayVarScrubEntry[] = [...failure.redactions]; + const sanitize = createReplayDivergenceSanitizer(scrubVars); + const safeCause = { + ...cause, + message: sanitize(cause.message), + ...(cause.hint ? { hint: sanitize(cause.hint) } : {}), + }; + const session = observationStore.get(); + const observation = session + ? await captureDivergenceObservation({ + session, + observationStore, + logPath, + action: captureAction, + }) + : { + state: 'unavailable' as const, + reason: 'no-session', + hint: 'The session closed before a post-failure screen could be captured.', + }; + const suggestions = + session && observation.state === 'available' && !failure.isControl + ? rankAndDedupeReplaySuggestions( + failure + .suggestNodes({ createdAt: Date.now(), nodes: observation.nodes }) + .map(({ node, basis }) => ({ node, nodeIndex: node.index, basis })), + ).map(({ node, basis }) => + buildReplayDivergenceSuggestionForNode({ + node, + nodes: observation.nodes, + session, + action: captureAction, + basis, + sanitize, + }), + ) + : []; + const divergence: ReplayDivergence = { + version: 1, + kind: 'action-failure', + step: { + index: failure.stepIndex, + source: { + path: sanitize(failure.source.path ?? failure.replayPath), + line: failure.source.line, + }, + }, + action: sanitize(failure.actionLabel), + cause: { + code: safeCause.code, + message: safeCause.message, + ...(safeCause.hint ? { hint: safeCause.hint } : {}), + }, + screen: buildDivergenceScreen(observation, sanitize), + suggestions: suggestions.slice(0, REPLAY_DIVERGENCE_SUGGESTION_LIMIT), + suggestionCount: suggestions.length, + resume: failure.resume, + repairHint: computeReplayRepairHint({ + kind: 'action-failure', + targetEvidence: undefined, + capture: toReplayRepairHintCapture(observation), + }), + }; + const bounded = boundReplayDivergenceForSession({ + sessionStore, + observationStore, + divergence, + responseLevel: req.meta?.responseLevel, + evidence: observation.state === 'available' ? observation.evidence : undefined, + ...(requestSignal ? { signal: requestSignal } : {}), + }); + return buildReplayDivergenceFailureResponseFromDescriptor({ + error: safeCause, + actionLabel: failure.actionLabel, + action: failure.action, + positionals: [...captureAction.positionals], + step: failure.stepIndex, + replayPath: failure.replayPath, + artifactPaths: [...failure.artifactPaths], + snapshotDiagnostics: failure.snapshotDiagnostics, + divergence: bounded, + scrubVars, + warnings: failure.warnings, + }); +} diff --git a/packages/replay-port/src/daemon-port/replay-backend-registry.ts b/packages/replay-port/src/daemon-port/replay-backend-registry.ts index 36a62f44b2..cf84fc9151 100644 --- a/packages/replay-port/src/daemon-port/replay-backend-registry.ts +++ b/packages/replay-port/src/daemon-port/replay-backend-registry.ts @@ -1,7 +1,5 @@ -import type { SessionAction } from '@agent-device/contracts/session'; import type { ReplayBackendId } from '@agent-device/ad-script'; -import type { ReplayCommand } from './command-types.ts'; -import type { DaemonResponse } from '@agent-device/kernel/contracts'; +import type { ReplayBackend } from '@agent-device/contracts/replay-backend'; /** * The replay backend registry (#3377). @@ -24,64 +22,19 @@ import type { DaemonResponse } from '@agent-device/kernel/contracts'; * the adapter and engine behind it are reached the same way. Type imports are free. */ -/** - * Backend ids come from the format grammar's vocabulary (`@agent-device/ad-script`), so the - * registry's keys and the `--maestro` flag's accepted values can never drift apart. - */ - -/** What a backend reports about one source before any run: the shared scheduling vocabulary. */ -export type ReplaySourceInspection = Readonly<{ - /** The flow's authored title, when the format carries one. */ - title: string | undefined; - /** A static app target the entry resolves to, for advisory device binding. */ - appTarget: string | undefined; -}>; - -/** Reads one flow file, or reports that it is unavailable (#1802 readers). */ -export type ReplayOptionalSourceReader = (resolvedPath: string) => string | undefined; - -export type ReplayBackendExportWarning = Readonly<{ - line: number; - action: string; - message: string; -}>; - -export type ReplayBackendExportResult = Readonly<{ - yaml: string; - warnings: ReplayBackendExportWarning[]; -}>; - -export type ReplayBackend = Readonly<{ - id: ReplayBackendId; - /** Expands the entry flow's include closure into a path-to-text map (best effort, #1802). */ - collectSourceFiles(params: { - entryPath: string; - entrySource: string; - env?: Readonly>; - readSource: ReplayOptionalSourceReader; - }): Record; - /** Reads title/app-target metadata from one source, without running it. Throws on an invalid flow. */ - inspectSource(source: string, sourcePath: string): ReplaySourceInspection; - /** Runs one replay through the backend's engine, owning no session or wire state. */ - runReplay(command: ReplayCommand): Promise; - /** Converts a native `.ad` action list into the backend's own script format. */ - exportReplayScript( - actions: SessionAction[], - options: { actionLines?: number[]; metadata?: { env?: Record } }, - ): ReplayBackendExportResult; -}>; - /** * Builtin registrations: the ONE extraction seam. Each thunk is the only place its backend's * package is named outside the backend's own adapter, and stays a function-scoped `import()` so * no host entry evaluates an engine eagerly (the `cli-startup-import-closure` pins). Extraction * swaps this table for the installed-plugin lookup; the host call sites never change. */ -const BUILTIN_REPLAY_BACKENDS: Readonly Promise>> = { +const BUILTIN_REPLAY_BACKENDS: Readonly< + Record Promise>> +> = { maestro: async () => (await import('./replay-maestro-backend.ts')).maestroBackend, }; -const loadedBackends = new Map>(); +const loadedBackends = new Map>>(); /** * Resolves a backend once per process, memoizing the engine's single evaluation. A load failure @@ -90,7 +43,7 @@ const loadedBackends = new Map>(); * an unregistered value is rejected by the caller before it reaches here; engines never fall back * to one another. */ -export function getReplayBackend(id: ReplayBackendId): Promise { +export function getReplayBackend(id: ReplayBackendId): Promise> { const cached = loadedBackends.get(id); if (cached) return cached; const loading = BUILTIN_REPLAY_BACKENDS[id]().catch((error: unknown) => { diff --git a/packages/replay-port/src/daemon-port/replay-dispatch-envelope.ts b/packages/replay-port/src/daemon-port/replay-dispatch-envelope.ts index 653b60890d..2ec98cb8f1 100644 --- a/packages/replay-port/src/daemon-port/replay-dispatch-envelope.ts +++ b/packages/replay-port/src/daemon-port/replay-dispatch-envelope.ts @@ -3,11 +3,8 @@ import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { ReplayDispatchOptions } from '@agent-device/contracts/replay'; import { stripUndefined } from '@agent-device/kernel/record'; import type { DaemonResponse } from '@agent-device/kernel/contracts'; -import type { - ReplayCommand, - ReplayDispatchRequest, - ReplayInvoke, -} from '@agent-device/replay-port/command-types'; +import type { ReplayCommand } from '@agent-device/replay-port/command-types'; +import type { ReplayDispatchRequest, ReplayInvoke } from '@agent-device/contracts/replay-backend'; /** The request-private facts replay's own decisions read, as the daemon resolves them. */ export type ReplayPrivateAdmission = Readonly<{ diff --git a/packages/replay-port/src/daemon-port/replay-maestro-backend.ts b/packages/replay-port/src/daemon-port/replay-maestro-backend.ts index 3b8604ee7b..3a00164b4f 100644 --- a/packages/replay-port/src/daemon-port/replay-maestro-backend.ts +++ b/packages/replay-port/src/daemon-port/replay-maestro-backend.ts @@ -5,7 +5,7 @@ import { MAESTRO_SELECTOR_PROJECTION, } from '@agent-device/maestro'; import { projectSelectorExpression } from '@agent-device/selectors'; -import type { ReplayBackend } from './replay-backend-registry.ts'; +import type { ReplayBackend } from '@agent-device/contracts/replay-backend'; import { runTypedMaestroReplay } from './session-replay-maestro-runtime.ts'; /** @@ -21,7 +21,7 @@ import { runTypedMaestroReplay } from './session-replay-maestro-runtime.ts'; * ordinary public commands (ADR 0015), so the engine's own request folding stays in * `session-replay-maestro-request.ts` against the daemon's `ReplayDispatchOptions` contract. */ -export const maestroBackend: ReplayBackend = { +export const maestroBackend: ReplayBackend<'maestro'> = { id: 'maestro', collectSourceFiles(params) { return collectMaestroFlowSources(params); diff --git a/packages/replay-port/src/daemon-port/replay-session-binding.ts b/packages/replay-port/src/daemon-port/replay-session-binding.ts index 51345c93fc..b30a85ef7d 100644 --- a/packages/replay-port/src/daemon-port/replay-session-binding.ts +++ b/packages/replay-port/src/daemon-port/replay-session-binding.ts @@ -1,10 +1,9 @@ +import type { ReplayCoordinator, ReplaySession } from '@agent-device/replay-port/command-types'; import type { - ReplayCoordinator, - ReplaySession, ReplaySessionObservation, ReplaySessionState, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; /** * The live session record reached through the reads replay binds over. The daemon implements it diff --git a/packages/replay-port/src/daemon-port/session-replay-action-runtime.ts b/packages/replay-port/src/daemon-port/session-replay-action-runtime.ts index 80c688ee94..8858dff041 100644 --- a/packages/replay-port/src/daemon-port/session-replay-action-runtime.ts +++ b/packages/replay-port/src/daemon-port/session-replay-action-runtime.ts @@ -1,9 +1,6 @@ import type { SessionAction, SessionScope } from '@agent-device/contracts/session'; -import type { - ReplayDaemonDependencies, - ReplayDispatchRequest, - ReplayInvoke, -} from '@agent-device/replay-port/command-types'; +import type { ReplayDaemonDependencies } from '@agent-device/replay-port/command-types'; +import type { ReplayDispatchRequest, ReplayInvoke } from '@agent-device/contracts/replay-backend'; import { mergeParentFlags } from '@agent-device/command-registry/batch'; import { AppError, normalizeError } from '@agent-device/kernel/errors'; import { diff --git a/packages/replay-port/src/daemon-port/session-replay-dispatch-narrowing.ts b/packages/replay-port/src/daemon-port/session-replay-dispatch-narrowing.ts index 41393349fd..a6f7dacd1e 100644 --- a/packages/replay-port/src/daemon-port/session-replay-dispatch-narrowing.ts +++ b/packages/replay-port/src/daemon-port/session-replay-dispatch-narrowing.ts @@ -1,4 +1,4 @@ -import type { ReplayDispatchRequest } from '@agent-device/replay-port/command-types'; +import type { ReplayDispatchRequest } from '@agent-device/contracts/replay-backend'; import type { AdReplayDispatchGuard, AdReplayDispatchOutcome, diff --git a/packages/replay-port/src/daemon-port/session-replay-divergence-publication.ts b/packages/replay-port/src/daemon-port/session-replay-divergence-publication.ts index a0af9451c0..fb88050bf5 100644 --- a/packages/replay-port/src/daemon-port/session-replay-divergence-publication.ts +++ b/packages/replay-port/src/daemon-port/session-replay-divergence-publication.ts @@ -8,7 +8,7 @@ import type { ReplayObservationEvidence } from '@agent-device/contracts/replay'; import type { ReplaySessionObservation, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; /** * Daemon-owned replay projection and publication boundary. The response or diff --git a/packages/replay-port/src/daemon-port/session-replay-divergence.ts b/packages/replay-port/src/daemon-port/session-replay-divergence.ts index 6551d8fd67..7878e59232 100644 --- a/packages/replay-port/src/daemon-port/session-replay-divergence.ts +++ b/packages/replay-port/src/daemon-port/session-replay-divergence.ts @@ -16,12 +16,12 @@ import { computeReplayRepairHint, type ReplayRepairHintCapture, } from '@agent-device/replay-port/repair-hint'; +import type { ReplayResumeStamper } from '@agent-device/replay-port/command-types'; import type { - ReplayResumeStamper, ReplaySessionObservation, ReplaySessionState, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; import type { ReplayObservationEvidence } from '@agent-device/contracts/replay'; import { boundReplayDivergenceForSession } from './session-replay-divergence-publication.ts'; diff --git a/packages/replay-port/src/daemon-port/session-replay-maestro-failure.ts b/packages/replay-port/src/daemon-port/session-replay-maestro-failure.ts index 84e76a235a..fd0c3902c0 100644 --- a/packages/replay-port/src/daemon-port/session-replay-maestro-failure.ts +++ b/packages/replay-port/src/daemon-port/session-replay-maestro-failure.ts @@ -1,42 +1,17 @@ import { collectMaestroFailureSuggestions, type MaestroFailedAction } from '@agent-device/maestro'; -import type { SnapshotNode } from '@agent-device/kernel/snapshot'; import type { DaemonError } from '@agent-device/kernel/errors'; import type { SnapshotDiagnosticsSummary } from '@agent-device/contracts/capture'; -import { type ReplayDivergence } from '@agent-device/contracts/divergence'; -import { - REPLAY_DIVERGENCE_SUGGESTION_LIMIT, - createReplayDivergenceSanitizer, - type ReplayVarScrubEntry, -} from '@agent-device/ad-replay/divergence'; import { formatScriptArg } from '@agent-device/ad-script'; -import { getRequestSignal } from '@agent-device/host-kit/request'; import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { + ReplayBackendActionFailure, ReplaySessionObservation, - ReplaySessionState, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; -import type { ReplayReportAction } from '@agent-device/replay-port/session-replay-report-action'; -import { rankAndDedupeReplaySuggestions } from '@agent-device/replay-port/session-replay-suggestion-ranking'; -import { - buildReplayDivergenceSuggestionForNode, - buildDivergenceScreen, - captureDivergenceObservation, - toReplayRepairHintCapture, - type DivergenceFieldSanitizer, -} from './session-replay-divergence.ts'; -import { boundReplayDivergenceForSession } from './session-replay-divergence-publication.ts'; -import { computeReplayRepairHint } from '@agent-device/replay-port/repair-hint'; -import { - buildReplayDivergenceFailureResponseFromDescriptor, - hoistReplayFailureCauseDiagnosticMeta, -} from '@agent-device/replay-port/session-replay-runtime-failure-response'; +} from '@agent-device/contracts/replay-backend'; import { type DaemonResponse } from '@agent-device/kernel/contracts'; +import { buildReplayBackendActionFailureResponse } from './replay-backend-failure.ts'; -export type MaestroFailureReportAction = Pick< - ReplayReportAction, - 'command' | 'positionals' | 'flags' ->; +export type MaestroFailureReportAction = ReplayBackendActionFailure['captureAction']; export type MaestroFailureReportProjection = { readonly command: { readonly kind: string }; @@ -75,118 +50,32 @@ export async function buildTypedMaestroFailureResponse(params: { readonly logPath: string; readonly snapshotDiagnostics?: SnapshotDiagnosticsSummary; }): Promise { - const { failure, replayPath, req, sessionStore, observationStore, logPath } = params; - const requestSignal = getRequestSignal(req.meta?.requestId); + const { failure, req } = params; const report = buildTypedMaestroFailureReportProjection(failure, req); - const cause = hoistReplayFailureCauseDiagnosticMeta(params.error); - const scrubVars: ReplayVarScrubEntry[] = [...failure.redactions]; - const sanitize = createReplayDivergenceSanitizer(scrubVars); - const safeCause = { - ...cause, - message: sanitize(cause.message), - ...(cause.hint ? { hint: sanitize(cause.hint) } : {}), - }; - const session = observationStore.get(); - const observation = session - ? await captureDivergenceObservation({ - session, - observationStore, - logPath, - action: report.action, - }) - : { - state: 'unavailable' as const, - reason: 'no-session', - hint: 'The session closed before a post-failure screen could be captured.', - }; - const suggestions = - session && observation.state === 'available' && !failure.isControl - ? collectTypedMaestroSuggestions({ - failure, - action: report.action, - session, - nodes: observation.nodes, - sanitize, - }) - : []; - const actionLabel = [failure.action, formatMaestroActionValue(failure.value)] - .filter(Boolean) - .join(' '); - const divergence: ReplayDivergence = { - version: 1, - kind: 'action-failure', - step: { - index: failure.stepIndex, - source: { - path: sanitize(failure.source.path ?? replayPath), - line: failure.source.line, - }, - }, - action: sanitize(actionLabel), - cause: { - code: safeCause.code, - message: safeCause.message, - ...(safeCause.hint ? { hint: safeCause.hint } : {}), - }, - screen: buildDivergenceScreen(observation, sanitize), - suggestions: suggestions.slice(0, REPLAY_DIVERGENCE_SUGGESTION_LIMIT), - suggestionCount: suggestions.length, - resume: failure.resume, - repairHint: computeReplayRepairHint({ - kind: 'action-failure', - targetEvidence: undefined, - capture: toReplayRepairHintCapture(observation), - }), - }; - const bounded = boundReplayDivergenceForSession({ - sessionStore, - observationStore, - divergence, - responseLevel: req.meta?.responseLevel, - evidence: observation.state === 'available' ? observation.evidence : undefined, - ...(requestSignal ? { signal: requestSignal } : {}), - }); - return buildReplayDivergenceFailureResponseFromDescriptor({ - error: safeCause, - actionLabel, + return await buildReplayBackendActionFailureResponse({ + error: params.error, + req, + sessionStore: params.sessionStore, + observationStore: params.observationStore, + logPath: params.logPath, + replayPath: params.replayPath, + stepIndex: failure.stepIndex, + source: failure.source, action: failure.action, - positionals: [...report.action.positionals], - step: failure.stepIndex, - replayPath, - artifactPaths: [...failure.artifactPaths], - snapshotDiagnostics: params.snapshotDiagnostics, - divergence: bounded, - scrubVars, + actionLabel: [failure.action, formatMaestroActionValue(failure.value)] + .filter(Boolean) + .join(' '), + captureAction: report.action, + isControl: failure.isControl, + redactions: failure.redactions, + resume: failure.resume, + artifactPaths: failure.artifactPaths, warnings: failure.warnings, + snapshotDiagnostics: params.snapshotDiagnostics, + suggestNodes: (snapshot) => collectMaestroFailureSuggestions(failure, snapshot), }); } -function collectTypedMaestroSuggestions(params: { - failure: MaestroFailedAction; - action: MaestroFailureReportAction; - session: ReplaySessionState; - nodes: SnapshotNode[]; - sanitize: DivergenceFieldSanitizer; -}) { - const snapshot = { createdAt: Date.now(), nodes: params.nodes }; - return rankAndDedupeReplaySuggestions( - collectMaestroFailureSuggestions(params.failure, snapshot).map(({ node, basis }) => ({ - node, - nodeIndex: node.index, - basis, - })), - ).map(({ node, basis }) => - buildReplayDivergenceSuggestionForNode({ - node, - nodes: params.nodes, - session: params.session, - action: params.action, - basis, - sanitize: params.sanitize, - }), - ); -} - function formatMaestroActionValue(value: string | undefined): string { if (!value || value === '') return value ?? ''; return formatScriptArg(value); diff --git a/packages/replay-port/src/daemon-port/session-replay-maestro-request.ts b/packages/replay-port/src/daemon-port/session-replay-maestro-request.ts index 51854e9221..122ef12d37 100644 --- a/packages/replay-port/src/daemon-port/session-replay-maestro-request.ts +++ b/packages/replay-port/src/daemon-port/session-replay-maestro-request.ts @@ -2,9 +2,11 @@ import type { MaestroDaemonDispatchOptions, MaestroDaemonOperationRequest, } from '@agent-device/maestro/daemon-runtime-port'; -import type { ReplayDispatchOptions } from '@agent-device/contracts/replay'; import { stripUndefined } from '@agent-device/kernel/record'; -import type { ReplayDispatchRequest } from '@agent-device/replay-port/command-types'; +import type { + ReplayBackendDispatchOptions, + ReplayDispatchRequest, +} from '@agent-device/contracts/replay-backend'; /** * The daemon half of the Maestro runtime port. The port projects a flow step onto one public @@ -36,7 +38,7 @@ export function maestroOperationDispatchRequest( */ function maestroDispatchOptions( dispatch: MaestroDaemonDispatchOptions | undefined, -): Pick { +): ReplayBackendDispatchOptions { return stripUndefined({ closeAppOnly: dispatch?.closeAppOnly, killApp: dispatch?.killApp, diff --git a/packages/replay-port/src/daemon-port/session-replay-maestro-response.ts b/packages/replay-port/src/daemon-port/session-replay-maestro-response.ts index a72ee9bbfd..4df917c03a 100644 --- a/packages/replay-port/src/daemon-port/session-replay-maestro-response.ts +++ b/packages/replay-port/src/daemon-port/session-replay-maestro-response.ts @@ -3,11 +3,14 @@ import type { MaestroExecutionOutcome } from '@agent-device/maestro'; import { normalizeError } from '@agent-device/kernel/errors'; import { summarizeSnapshotTimingSamples } from '@agent-device/contracts/capture'; import { buildTypedMaestroFailureResponse } from './session-replay-maestro-failure.ts'; -import type { ReplayCommand, ReplaySessionStore } from '@agent-device/replay-port/command-types'; +import type { + ReplayBackendCommand, + ReplaySessionStore, +} from '@agent-device/contracts/replay-backend'; import { type DaemonResponse } from '@agent-device/kernel/contracts'; type TypedMaestroSuccessResponseParams = Readonly<{ - command: ReplayCommand; + command: ReplayBackendCommand; outcome: Extract; startedAt: number; snapshotStart: number; @@ -38,7 +41,7 @@ export function buildTypedMaestroSuccessResponse( } type TypedMaestroErrorResponseParams = Readonly<{ - command: ReplayCommand; + command: ReplayBackendCommand; /** The entry flow's caller-resolved display path (the replay script source bundle's entry). */ replayPath: string; state: { diff --git a/packages/replay-port/src/daemon-port/session-replay-maestro-runtime.ts b/packages/replay-port/src/daemon-port/session-replay-maestro-runtime.ts index a4a32614d7..c3eae4070d 100644 --- a/packages/replay-port/src/daemon-port/session-replay-maestro-runtime.ts +++ b/packages/replay-port/src/daemon-port/session-replay-maestro-runtime.ts @@ -31,10 +31,10 @@ import { } from '@agent-device/replay-port/script-source'; import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; import type { - ReplayCommand, + ReplayBackendCommand, ReplayDispatchRequest, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; import { errorResponse, type DaemonResponse, @@ -46,7 +46,7 @@ type TypedMaestroReplayState = { }; type TypedMaestroReplayPreparation = Readonly<{ - command: ReplayCommand; + command: ReplayBackendCommand; bundle: ReplayScriptSourceBundle; }>; @@ -73,7 +73,9 @@ type MaestroReplayBinding = Pick< 'device' | 'platform' | 'target' | 'runtimeHints' >; -export async function runTypedMaestroReplay(command: ReplayCommand): Promise { +export async function runTypedMaestroReplay( + command: ReplayBackendCommand, +): Promise { const { request: req } = command; const bundle = req.flags?.replayScriptSource; if (!bundle) return errorResponse('INVALID_ARGS', REPLAY_SCRIPT_SOURCE_REQUIRED_MESSAGE); @@ -311,7 +313,7 @@ function buildTypedMaestroEnv(req: DaemonWireRequest): Record { } function createMaestroReplayPort(params: { - command: ReplayCommand; + command: ReplayBackendCommand; device: DeviceInfo | undefined; platform: Extract; runtimeHints: SessionRuntimeHints | undefined; diff --git a/packages/replay-port/src/daemon-port/session-replay-runtime-engine-adapter.ts b/packages/replay-port/src/daemon-port/session-replay-runtime-engine-adapter.ts index d016a5f38b..00870c63d4 100644 --- a/packages/replay-port/src/daemon-port/session-replay-runtime-engine-adapter.ts +++ b/packages/replay-port/src/daemon-port/session-replay-runtime-engine-adapter.ts @@ -3,11 +3,13 @@ import type { DaemonWireRequest } from '@agent-device/contracts/command'; import type { ReplayCoordinator, ReplayDaemonDependencies, +} from '@agent-device/replay-port/command-types'; +import type { ReplayDispatchRequest, ReplayInvoke, ReplaySessionObservation, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; import { readReplaySelectorDisplayValue } from '@agent-device/selectors'; import { type DaemonResponse, diff --git a/packages/replay-port/src/daemon-port/session-replay-runtime-failure.ts b/packages/replay-port/src/daemon-port/session-replay-runtime-failure.ts index e885fb21c9..eb9d0d225e 100644 --- a/packages/replay-port/src/daemon-port/session-replay-runtime-failure.ts +++ b/packages/replay-port/src/daemon-port/session-replay-runtime-failure.ts @@ -12,11 +12,11 @@ import { hoistReplayFailureCauseDiagnosticMeta, } from '@agent-device/replay-port/session-replay-runtime-failure-response'; import { getRequestSignal } from '@agent-device/host-kit/request'; +import type { ReplayResumeStamper } from '@agent-device/replay-port/command-types'; import type { - ReplayResumeStamper, ReplaySessionObservation, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; import { type DaemonResponse } from '@agent-device/kernel/contracts'; export async function withReplayFailureDiagnostics(params: { diff --git a/packages/replay-port/src/daemon-port/session-replay-runtime-plan.ts b/packages/replay-port/src/daemon-port/session-replay-runtime-plan.ts index aa7e528f62..690ffb5199 100644 --- a/packages/replay-port/src/daemon-port/session-replay-runtime-plan.ts +++ b/packages/replay-port/src/daemon-port/session-replay-runtime-plan.ts @@ -1,13 +1,12 @@ import type { SessionAction } from '@agent-device/contracts/session'; import type { ReplayDispatchOptions } from '@agent-device/contracts/replay'; import type { CommandFlags, DaemonWireRequest } from '@agent-device/contracts/command'; +import type { ReplayCommand, ReplayCoordinator } from '@agent-device/replay-port/command-types'; import type { - ReplayCommand, - ReplayCoordinator, ReplayDispatchRequest, ReplaySessionState, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; import { buildReplayScriptPlatformFlags } from '@agent-device/replay-port/replay-script-selection'; import { inspectAdReplay, diff --git a/packages/replay-port/src/daemon-port/session-replay-runtime-session.ts b/packages/replay-port/src/daemon-port/session-replay-runtime-session.ts index 325abf3eab..3d4979b719 100644 --- a/packages/replay-port/src/daemon-port/session-replay-runtime-session.ts +++ b/packages/replay-port/src/daemon-port/session-replay-runtime-session.ts @@ -1,10 +1,7 @@ import fs from 'node:fs'; import type { DaemonWireRequest } from '@agent-device/contracts/command'; -import type { - ReplayCoordinator, - ReplaySessionStore, - ReplaySessionView, -} from '@agent-device/replay-port/command-types'; +import type { ReplayCoordinator, ReplaySessionView } from '@agent-device/replay-port/command-types'; +import type { ReplaySessionStore } from '@agent-device/contracts/replay-backend'; import { expandSessionPath } from '@agent-device/host-kit/session-paths'; import { healedScriptSiblingPath } from '@agent-device/replay-port/session-replay-heal'; import { diff --git a/packages/replay-port/src/daemon-port/session-replay-target-verification.ts b/packages/replay-port/src/daemon-port/session-replay-target-verification.ts index 2da5832542..26fe88a5f4 100644 --- a/packages/replay-port/src/daemon-port/session-replay-target-verification.ts +++ b/packages/replay-port/src/daemon-port/session-replay-target-verification.ts @@ -40,12 +40,12 @@ import { type DivergenceObservation, } from './session-replay-divergence.ts'; import { boundReplayDivergenceForSession } from './session-replay-divergence-publication.ts'; +import type { ReplayResumeStamper } from '@agent-device/replay-port/command-types'; import type { - ReplayResumeStamper, ReplaySessionObservation, ReplaySessionState, ReplaySessionStore, -} from '@agent-device/replay-port/command-types'; +} from '@agent-device/contracts/replay-backend'; import { computeReplayRepairHint, type ReplayRepairHintCapture, diff --git a/packages/replay-port/src/daemon-port/session-replay-video-recording.ts b/packages/replay-port/src/daemon-port/session-replay-video-recording.ts index 02b80f75bd..2a424c0763 100644 --- a/packages/replay-port/src/daemon-port/session-replay-video-recording.ts +++ b/packages/replay-port/src/daemon-port/session-replay-video-recording.ts @@ -7,9 +7,9 @@ import { sleep } from '@agent-device/host-kit/retry'; import { collectReplayActionArtifactPaths } from '@agent-device/replay-port/session-replay-runtime-artifacts'; import type { ReplayRecordVideoRequest, - ReplaySessionStore, ReplayTestVideoOwner, } from '@agent-device/replay-port/command-types'; +import type { ReplaySessionStore } from '@agent-device/contracts/replay-backend'; import { defaultRecordingPath, recordingExtensionForPlatform, diff --git a/packages/replay-port/src/daemon-port/session-test-source-discovery.ts b/packages/replay-port/src/daemon-port/session-test-source-discovery.ts index c78ace3e87..6ef93fe530 100644 --- a/packages/replay-port/src/daemon-port/session-test-source-discovery.ts +++ b/packages/replay-port/src/daemon-port/session-test-source-discovery.ts @@ -10,7 +10,8 @@ import type { ReplayTestManifest, ReplayTestSource, } from '@agent-device/replay-test'; -import { getReplayBackend, type ReplayBackend } from './replay-backend-registry.ts'; +import type { ReplayBackend } from '@agent-device/contracts/replay-backend'; +import { getReplayBackend } from './replay-backend-registry.ts'; /** * The daemon adapter's source-inspection capability (#1478 P3b). diff --git a/packages/replay-port/src/script-source-bundle.ts b/packages/replay-port/src/script-source-bundle.ts index ddff524d97..57cf3b05c0 100644 --- a/packages/replay-port/src/script-source-bundle.ts +++ b/packages/replay-port/src/script-source-bundle.ts @@ -3,7 +3,7 @@ import { AppError } from '@agent-device/kernel/errors'; import type { ReplayScriptSourceBundle } from '@agent-device/contracts/replay'; import { resolveUserPath } from '@agent-device/host-kit/file'; import { resolveReplayFormat } from '@agent-device/ad-script'; -import type { ReplayBackend } from '@agent-device/replay-port/replay-backend-registry'; +import type { ReplayBackend } from '@agent-device/contracts/replay-backend'; export const REPLAY_SCRIPT_SOURCE_BUNDLE_MAX_BYTES = 2 * 1024 * 1024; diff --git a/scripts/layering/contracts-exports.snapshot.json b/scripts/layering/contracts-exports.snapshot.json index c0313775b4..c2834535cf 100644 --- a/scripts/layering/contracts-exports.snapshot.json +++ b/scripts/layering/contracts-exports.snapshot.json @@ -103,6 +103,7 @@ "@agent-device/contracts/recording-stop-progress", "@agent-device/contracts/remote", "@agent-device/contracts/replay", + "@agent-device/contracts/replay-backend", "@agent-device/contracts/runner-lease-context", "@agent-device/contracts/runtime-contract", "@agent-device/contracts/runtime-contract-error", diff --git a/src/cli/commands/replay.ts b/src/cli/commands/replay.ts index 5ed9fa53ff..d50c4f79c5 100644 --- a/src/cli/commands/replay.ts +++ b/src/cli/commands/replay.ts @@ -3,7 +3,7 @@ import path from 'node:path'; import { AppError } from '@agent-device/kernel/errors'; import { parseReplayScriptDetailed, readReplayScriptMetadata } from '@agent-device/ad-script'; import { resolveUserPath } from '@agent-device/host-kit/file'; -import type { ReplayBackend } from '@agent-device/replay-port/replay-backend-registry'; +import type { ReplayBackend } from '@agent-device/contracts/replay-backend'; import { writeCommandOutput } from './shared.ts'; import type { ClientCommandHandler } from './router-types.ts';