1import {
2 OrchestratorMcpFailure,
3 DeviceToolCloseInput,
4 DeviceToolError,
5 DeviceToolListResult,
6 DeviceToolOpenInput,
7 DeviceToolOpenResult,
8 DeviceToolScreenshotResult,
9 DeviceToolTargetInput,
10} from "@t3tools/contracts";
11import * as Schema from "effect/Schema";
12import * as FileSystem from "effect/FileSystem";
13import * as Path from "effect/Path";
14import * as ServerConfig from "../../../config.ts";
15import { Tool, Toolkit } from "effect/ai";
16
17import * as McpInvocationContext from "../../McpInvocationContext.ts";
18import * as DeviceService from "../../../device/DeviceService.ts";
19import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts";
20
21const dependencies = [
22 McpInvocationContext.McpInvocationContext,
23 ThreadManagementService.ThreadManagementService,
24 DeviceService.DeviceService,
25];
26
27/** What a device tool fails with, including the access gate's refusal. */
28const DeviceToolFailure = Schema.Union([DeviceToolError, OrchestratorMcpFailure]);
29
30/**
31 * Deliberately a small surface: lifecycle, visibility for the user, and one
32 * image-returning verb. Driving the device (taps, typing, install, logs)
33 * happens through the preconfigured `agent-device` CLI, which has the
34 * semantic snapshot model agents need and stays current with its own
35 * releases. Wrapping its commands here would only lag behind it.
36 */
37const DeviceListTool = Tool.make("device_list", {
38 description:
39 "List iOS Simulators and Android Emulators on this environment's device hosts, which platforms each host can run, and which devices are already open in this thread's Device panel. Call this before device_open when you do not know a device id.",
40 // An empty struct serializes as `anyOf [object, array]`, which some
41 // providers reject and then drop every tool on the server with it.
42 parameters: Schema.Struct({
43 hostId: Schema.optional(
44 Schema.String.annotate({ description: "Limit to one device host. Defaults to all hosts." }),
45 ),
46 }),
47 success: DeviceToolListResult,
48 failure: DeviceToolFailure,
49 dependencies,
50})
51 .annotate(Tool.Title, "List devices")
52 .annotate(Tool.Readonly, true)
53 .annotate(Tool.Destructive, false)
54 .annotate(Tool.Idempotent, true)
55 .annotate(Tool.OpenWorld, false);
56
57const DeviceOpenTool = Tool.make("device_open", {
58 description:
59 "Open a simulator or emulator for this thread: boots it if needed, starts its live stream, and shows it in the user's Device panel so they can watch. Returns the agent-device CLI invocation pinned to the device; drive the device with that CLI afterwards.",
60 parameters: DeviceToolOpenInput,
61 success: DeviceToolOpenResult,
62 failure: DeviceToolFailure,
63 dependencies: [...dependencies, FileSystem.FileSystem, Path.Path, ServerConfig.ServerConfig],
64})
65 .annotate(Tool.Title, "Open device")
66 .annotate(Tool.Readonly, false)
67 .annotate(Tool.Destructive, false)
68 .annotate(Tool.Idempotent, true)
69 .annotate(Tool.OpenWorld, true);
70
71export const DeviceScreenshotTool = Tool.make("device_screenshot", {
72 description:
73 "Capture the current screen of an open device as a PNG image. Use it to see what the user sees; for taps and text use the agent-device CLI.",
74 parameters: DeviceToolTargetInput,
75 success: DeviceToolScreenshotResult,
76 failure: DeviceToolFailure,
77 dependencies,
78})
79 .annotate(Tool.Title, "Screenshot device")
80 .annotate(Tool.Readonly, true)
81 .annotate(Tool.Destructive, false)
82 .annotate(Tool.Idempotent, true)
83 .annotate(Tool.OpenWorld, true);
84
85const DeviceCloseTool = Tool.make("device_close", {
86 description:
87 "Remove a device from this thread's Device panel. Pass shutdown=true to also power the simulator or emulator off.",
88 parameters: DeviceToolCloseInput,
89 success: Schema.Record(Schema.String, Schema.Never).annotate({
90 description: "The device was closed.",
91 }),
92 failure: DeviceToolFailure,
93 dependencies,
94})
95 .annotate(Tool.Title, "Close device")
96 .annotate(Tool.Readonly, false)
97 .annotate(Tool.Destructive, true)
98 .annotate(Tool.Idempotent, true)
99 .annotate(Tool.OpenWorld, true);
100
101export const DeviceStandardToolkit = Toolkit.make(DeviceListTool, DeviceOpenTool, DeviceCloseTool);
102
103export const DeviceScreenshotToolkit = Toolkit.make(DeviceScreenshotTool);
104
105export const DeviceToolkit = Toolkit.make(
106 DeviceListTool,
107 DeviceOpenTool,
108 DeviceScreenshotTool,
109 DeviceCloseTool,
110);