Skip to content

Commit 9aff61b

Browse files
committed
refactor(daemon): bind only the command families the daemon dispatches through
The daemon built its in-process AgentDevice with src/runtime.ts, which value-imports the all-family command barrel. Five daemon files therefore evaluated the management, recording, observability and system families they never call, and every change under src/commands became a daemon-restart-cost event. Split the runtime assembly (backend, artifacts, sessions, policy, cancellation) into src/runtime-factory.ts, which carries no command surface, and add src/runtime-command-surface.ts for the three families an in-process executor dispatches through: capture, selectors, interactions. src/runtime.ts keeps its public shape and now composes both, so createAgentDevice stays the one full-surface path and bindCaptureCommands is the single capture-family binding definition. Daemon value closure: 717 -> 705 files, 121 -> 108 src/commands files, 19,659 -> 17,434 commands LOC. The residue is the dispatch surface plus the two cli-schema/command-schema.ts edges owned by #2543, which take it to 34. Closes #2540
1 parent 3394d5b commit 9aff61b

11 files changed

Lines changed: 152 additions & 85 deletions

File tree

.fallowrc.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,11 @@
504504
"summarizeAgentBrowserProcesses",
505505
"summarizeManagedAgentBrowserProcesses"
506506
]
507+
},
508+
{
509+
"comment": "src/runtime.ts keeps its published type surface while its implementation moves to src/runtime-factory.ts (#2540). These two re-exports have no in-repo consumer; they stay so the module's exported shape is unchanged.",
510+
"file": "src/runtime.ts",
511+
"exports": ["CommandPolicy", "CommandSessionRecord"]
507512
}
508513
],
509514
"usedClassMembers": [

src/commands/capture/runtime/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { DiffSnapshotCommandResult } from '@agent-device/contracts/capture';
2+
import { bindRuntimeCommands } from '../../runtime-types.ts';
23
import type {
34
BoundOf,
45
DiffSnapshotCommandOptions,
56
RuntimeCommand,
67
ScreenshotCommandOptions,
78
SnapshotCommandOptions,
89
} from '../../runtime-types.ts';
10+
import type { AgentDeviceRuntime } from '../../../runtime-contract.ts';
911
import {
1012
diffScreenshotCommand,
1113
type DiffScreenshotCommandOptions,
@@ -29,3 +31,7 @@ export const captureCommands: CaptureCommands = {
2931
snapshot: snapshotCommand,
3032
diffSnapshot: diffSnapshotCommand,
3133
};
34+
35+
export function bindCaptureCommands(runtime: AgentDeviceRuntime): BoundCaptureCommands {
36+
return bindRuntimeCommands(captureCommands, runtime);
37+
}

src/commands/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { AgentDeviceRuntime } from '../runtime-contract.ts';
22
import { bindRuntimeCommands } from './runtime-types.ts';
33
import {
4+
bindCaptureCommands,
45
captureCommands,
56
type BoundCaptureCommands,
67
type CaptureCommands,
@@ -80,7 +81,7 @@ export const commands: AgentDeviceCommands = {
8081

8182
export function bindCommands(runtime: AgentDeviceRuntime): BoundAgentDeviceCommands {
8283
return {
83-
capture: bindRuntimeCommands(captureCommands, runtime),
84+
capture: bindCaptureCommands(runtime),
8485
selectors: bindSelectorCommands(runtime),
8586
interactions: bindInteractionCommands(runtime),
8687
system: bindRuntimeCommands(systemCommands, runtime),

src/daemon/interaction/internal/interaction-runtime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import type {
55
BackendActionResult,
66
BackendSnapshotResult,
77
} from '../../../backend.ts';
8-
import { createAgentDevice } from '../../../runtime.ts';
8+
import { createCommandSurfaceAgentDevice } from '../../../runtime-command-surface.ts';
99
import { getRequestSignal } from '@agent-device/host-kit/request';
1010
import type { Rect } from '@agent-device/kernel/snapshot';
1111
import type { DaemonCommandContext } from '../../context.ts';
@@ -112,7 +112,7 @@ export function finalizeTouchInteraction(params: FinalizeTouchInteractionInput):
112112
}
113113

114114
function createInteractionAgentDevice(params: InteractionRuntimeInput) {
115-
return createAgentDevice({
115+
return createCommandSurfaceAgentDevice({
116116
backend: createInteractionBackend(params),
117117
...createDaemonRuntimePolicy('interaction commands', { plural: true }),
118118
sessions: params.runtimeSessions,

src/daemon/runtime-policy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { AgentDeviceRuntimeConfig } from '../runtime-contract.ts';
2-
import { localCommandPolicy } from '../runtime.ts';
2+
import { localCommandPolicy } from '../runtime-factory.ts';
33
import { createUnsupportedArtifactAdapter } from './runtime-artifacts.ts';
44

55
export function createDaemonRuntimePolicy(

src/daemon/screenshot-runtime.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ import os from 'node:os';
1313
import path from 'node:path';
1414
import type { AgentDeviceBackend } from '../backend.ts';
1515
import type { ArtifactAdapter } from '../io.ts';
16-
import { createAgentDevice, localCommandPolicy } from '../runtime.ts';
16+
import { localCommandPolicy } from '../runtime-factory.ts';
17+
import { createCommandSurfaceAgentDevice } from '../runtime-command-surface.ts';
1718
import {
1819
assertSupportedScreenshotPixelDensity,
1920
readScreenshotResultMetadata,
@@ -116,7 +117,7 @@ export async function captureScreenshotArtifact(
116117
}>,
117118
): Promise<CapturedScreenshot> {
118119
const { session, sessionName, outPath, dispatchContext } = params;
119-
const runtime = createAgentDevice({
120+
const runtime = createCommandSurfaceAgentDevice({
120121
backend: createBoundScreenshotBackend(params),
121122
artifacts: createDaemonScreenshotArtifactAdapter(),
122123
sessions: createDaemonRuntimeSessionStore({

src/daemon/selector-runtime-backend.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type {
44
BackendSnapshotResult,
55
} from '../backend.ts';
66
import { resolveTargetDevice } from '@agent-device/device-selection/dispatch-resolve';
7-
import { createAgentDevice } from '../runtime.ts';
7+
import { createCommandSurfaceAgentDevice } from '../runtime-command-surface.ts';
88
import { publicPlatformString } from '@agent-device/kernel/device';
99
import { noActiveSessionError } from './response.ts';
1010
import type { SnapshotState, SnapshotNode } from '@agent-device/kernel/snapshot';
@@ -67,7 +67,7 @@ type ResolvedSelectorDevice =
6767
| { ok: false; response: DaemonResponse };
6868

6969
export function createSelectorRuntimeForDevice(params: SelectorRuntimeDeviceParams) {
70-
return createAgentDevice({
70+
return createCommandSurfaceAgentDevice({
7171
backend: createSelectorBackend(params),
7272
...createDaemonRuntimePolicy('selector commands', { plural: true }),
7373
sessions: createDaemonRuntimeSessionStore({

src/daemon/snapshot-command-runtime.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ import type { SnapshotResult } from '@agent-device/contracts/snapshot-runtime';
88
import { publicPlatformString } from '@agent-device/kernel/device';
99
import { AppError } from '@agent-device/kernel/errors';
1010
import type { AgentDeviceBackend, BackendSnapshotResult } from '../backend.ts';
11-
import { type CommandSessionRecord, createAgentDevice } from '../runtime.ts';
11+
import type { CommandSessionRecord } from '../runtime-contract.ts';
12+
import { createCommandSurfaceAgentDevice } from '../runtime-command-surface.ts';
1213
import { getRequestSignal } from '@agent-device/host-kit/request';
1314
import type { RuntimeAdmissionBindings } from './request-runtime-binding.ts';
1415
import { maybeBuildAndroidSnapshotTimeoutFailure } from './android-snapshot-timeout-evidence.ts';
@@ -128,7 +129,7 @@ function createSnapshotRuntime(
128129
} & RuntimeAdmissionBindings,
129130
) {
130131
const { req, sessionName, logPath, sessionStore, session, device, snapshotScope } = params;
131-
return createAgentDevice({
132+
return createCommandSurfaceAgentDevice({
132133
backend: createDaemonSnapshotBackend({
133134
req,
134135
logPath,

src/runtime-command-surface.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import {
2+
bindInteractionCommands,
3+
bindSelectorCommands,
4+
type BoundInteractionCommands,
5+
type BoundSelectorCommands,
6+
} from './commands/interaction/runtime/index.ts';
7+
import {
8+
bindCaptureCommands,
9+
type BoundCaptureCommands,
10+
} from './commands/capture/runtime/index.ts';
11+
import { createAgentDeviceRuntime } from './runtime-factory.ts';
12+
import type { AgentDeviceRuntime, AgentDeviceRuntimeConfig } from './runtime-contract.ts';
13+
14+
/**
15+
* The command surface an in-process executor dispatches through: capture, selector reads and
16+
* interactions. `src/runtime.ts` binds every family for the public SDK and CLI surface; a host
17+
* that only executes these three should not evaluate the management, recording, observability and
18+
* system families to get them. The runtime assembly is shared with `createAgentDevice`, so there
19+
* is one construction path and this differs only in the families it binds.
20+
*/
21+
export type CommandSurfaceAgentDevice = AgentDeviceRuntime & {
22+
capture: BoundCaptureCommands;
23+
selectors: BoundSelectorCommands;
24+
interactions: BoundInteractionCommands;
25+
};
26+
27+
export function createCommandSurfaceAgentDevice(
28+
config: AgentDeviceRuntimeConfig,
29+
): CommandSurfaceAgentDevice {
30+
const runtime = createAgentDeviceRuntime(config);
31+
return {
32+
...runtime,
33+
capture: bindCaptureCommands(runtime),
34+
selectors: bindSelectorCommands(runtime),
35+
interactions: bindInteractionCommands(runtime),
36+
};
37+
}

src/runtime-factory.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type {
2+
AgentDeviceRuntime,
3+
AgentDeviceRuntimeConfig,
4+
CommandPolicy,
5+
CommandSessionRecord,
6+
CommandSessionStore,
7+
} from './runtime-contract.ts';
8+
9+
/**
10+
* Assembles an in-process runtime from its backend, artifact adapter, session store, policy and
11+
* cancellation inputs. Carries no command surface: whoever needs bound commands composes them on
12+
* the result, so a consumer that only reads `backend`/`sessions`/`policy` does not evaluate the
13+
* command families it never dispatches.
14+
*/
15+
export function createAgentDeviceRuntime(config: AgentDeviceRuntimeConfig): AgentDeviceRuntime {
16+
return {
17+
backend: config.backend,
18+
artifacts: config.artifacts,
19+
sessions: config.sessions ?? createMemorySessionStore(),
20+
policy: config.policy ?? restrictedCommandPolicy(),
21+
diagnostics: config.diagnostics,
22+
clock: config.clock,
23+
signal: config.signal,
24+
};
25+
}
26+
27+
export function createMemorySessionStore(
28+
records: readonly CommandSessionRecord[] = [],
29+
): CommandSessionStore {
30+
const sessions = new Map(records.map((record) => [record.name, cloneSessionRecord(record)]));
31+
return {
32+
get: (name) => cloneSessionRecord(sessions.get(name)),
33+
set: (record) => {
34+
sessions.set(record.name, cloneSessionRecord(record));
35+
},
36+
delete: (name) => {
37+
sessions.delete(name);
38+
},
39+
list: () => Array.from(sessions.values(), (record) => cloneSessionRecord(record)),
40+
};
41+
}
42+
43+
function cloneSessionRecord(record: CommandSessionRecord): CommandSessionRecord;
44+
function cloneSessionRecord(record: undefined): undefined;
45+
function cloneSessionRecord(
46+
record: CommandSessionRecord | undefined,
47+
): CommandSessionRecord | undefined;
48+
function cloneSessionRecord(
49+
record: CommandSessionRecord | undefined,
50+
): CommandSessionRecord | undefined {
51+
if (!record) return undefined;
52+
return {
53+
...record,
54+
...(record.snapshot ? { snapshot: structuredClone(record.snapshot) } : {}),
55+
...(record.metadata ? { metadata: cloneMetadata(record.metadata) } : {}),
56+
};
57+
}
58+
59+
function cloneMetadata(metadata: Record<string, unknown>): Record<string, unknown> {
60+
try {
61+
return structuredClone(metadata) as Record<string, unknown>;
62+
} catch {
63+
return { ...metadata };
64+
}
65+
}
66+
67+
export function localCommandPolicy(overrides: Partial<CommandPolicy> = {}): CommandPolicy {
68+
return {
69+
allowLocalInputPaths: true,
70+
allowLocalOutputPaths: true,
71+
maxImagePixels: 20_000_000,
72+
...overrides,
73+
};
74+
}
75+
76+
export function restrictedCommandPolicy(overrides: Partial<CommandPolicy> = {}): CommandPolicy {
77+
return {
78+
allowLocalInputPaths: false,
79+
allowLocalOutputPaths: false,
80+
maxImagePixels: 20_000_000,
81+
...overrides,
82+
};
83+
}

0 commit comments

Comments
 (0)