@saadnvd1/agent-os 0.3.523 → 0.3.524
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 +57 -0
- package/app/api/sessions/[id]/simulator/route.ts +32 -0
- package/app/api/sessions/[id]/simulator/run/route.ts +37 -0
- package/lib/db/migrations.ts +19 -0
- package/lib/project-config/agentos.schema.json +19 -0
- package/lib/project-config/brief.ts +3 -0
- package/lib/project-config/schema.ts +10 -0
- package/lib/sessions/end.ts +3 -0
- package/lib/sessions/stop.ts +3 -0
- package/lib/simulator/agent-device.ts +258 -0
- package/lib/simulator/available.ts +5 -0
- package/lib/simulator/brief.ts +18 -0
- package/lib/simulator/http.ts +16 -0
- package/lib/simulator/index.ts +458 -0
- package/lib/simulator/lease.ts +169 -0
- package/lib/simulator/simctl.ts +167 -0
- package/package.json +1 -1
- package/server.ts +16 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The iOS simulators AgentOS makes for tasks, through `xcrun simctl`. Every
|
|
3
|
+
* device is named with this instance's prefix, and nothing destructive is
|
|
4
|
+
* ever done to a device without it: a project's own simulators and another
|
|
5
|
+
* AgentOS instance's (a dev one, say) are never touched.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFile } from "child_process";
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { promisify } from "util";
|
|
12
|
+
|
|
13
|
+
const execFileAsync = promisify(execFile);
|
|
14
|
+
|
|
15
|
+
export interface SimDevice {
|
|
16
|
+
udid: string;
|
|
17
|
+
name: string;
|
|
18
|
+
state: string;
|
|
19
|
+
runtime: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type SimctlRun = (args: string[]) => Promise<string>;
|
|
23
|
+
|
|
24
|
+
const realRun: SimctlRun = async (args) =>
|
|
25
|
+
(
|
|
26
|
+
await execFileAsync("xcrun", ["simctl", ...args], {
|
|
27
|
+
timeout: 180_000,
|
|
28
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
29
|
+
})
|
|
30
|
+
).stdout.trim();
|
|
31
|
+
|
|
32
|
+
let run: SimctlRun = realRun;
|
|
33
|
+
// Tests swap simctl for a fake; nothing else calls this.
|
|
34
|
+
export function setSimctlRunner(fake: SimctlRun | null): void {
|
|
35
|
+
run = fake ?? realRun;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Short and stable per database, so two instances on one Mac stay apart.
|
|
39
|
+
export function instancePrefix(): string {
|
|
40
|
+
const db = process.env.DB_PATH || path.join(process.cwd(), "agent-os.db");
|
|
41
|
+
return `aos-${createHash("sha1").update(db).digest("hex").slice(0, 6)}-`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const baseName = () => `${instancePrefix()}base`;
|
|
45
|
+
export const deviceNameFor = (sessionId: string) =>
|
|
46
|
+
`${instancePrefix()}${sessionId.replace(/-/g, "").slice(0, 8)}`;
|
|
47
|
+
export const ownsDevice = (name: string) => name.startsWith(instancePrefix());
|
|
48
|
+
|
|
49
|
+
export function parseDevices(json: string): SimDevice[] {
|
|
50
|
+
const parsed = JSON.parse(json) as {
|
|
51
|
+
devices?: Record<
|
|
52
|
+
string,
|
|
53
|
+
{ udid: string; name: string; state: string; isAvailable?: boolean }[]
|
|
54
|
+
>;
|
|
55
|
+
};
|
|
56
|
+
return Object.entries(parsed.devices ?? {}).flatMap(([runtime, list]) =>
|
|
57
|
+
list
|
|
58
|
+
.filter((d) => d.isAvailable !== false)
|
|
59
|
+
.map((d) => ({
|
|
60
|
+
udid: d.udid,
|
|
61
|
+
name: d.name,
|
|
62
|
+
state: d.state,
|
|
63
|
+
runtime,
|
|
64
|
+
}))
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function listDevices(): Promise<SimDevice[]> {
|
|
69
|
+
return parseDevices(await run(["list", "devices", "-j"]));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// The newest installed iOS runtime and the device type for a model name
|
|
73
|
+
// ("iPhone 17" is com.apple.CoreSimulator.SimDeviceType.iPhone-17).
|
|
74
|
+
async function newestRuntime(): Promise<string> {
|
|
75
|
+
const parsed = JSON.parse(await run(["list", "runtimes", "-j"])) as {
|
|
76
|
+
runtimes: { identifier: string; name: string; isAvailable: boolean }[];
|
|
77
|
+
};
|
|
78
|
+
const ios = parsed.runtimes.filter(
|
|
79
|
+
(r) => r.isAvailable && r.name.startsWith("iOS")
|
|
80
|
+
);
|
|
81
|
+
const newest = ios.sort((a, b) =>
|
|
82
|
+
a.name.localeCompare(b.name, undefined, { numeric: true })
|
|
83
|
+
)[ios.length - 1];
|
|
84
|
+
if (!newest) throw new Error("no iOS simulator runtime is installed");
|
|
85
|
+
return newest.identifier;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export const deviceTypeFor = (model: string) =>
|
|
89
|
+
`com.apple.CoreSimulator.SimDeviceType.${model.trim().replace(/\s+/g, "-")}`;
|
|
90
|
+
|
|
91
|
+
let baseBuilding: Promise<SimDevice> | null = null;
|
|
92
|
+
|
|
93
|
+
// The device every task's simulator is cloned from: made once, booted once so
|
|
94
|
+
// its first-run setup is done, then left shut down.
|
|
95
|
+
export function ensureBase(model: string): Promise<SimDevice> {
|
|
96
|
+
baseBuilding ??= buildBase(model).finally(() => {
|
|
97
|
+
baseBuilding = null;
|
|
98
|
+
});
|
|
99
|
+
return baseBuilding;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function buildBase(model: string): Promise<SimDevice> {
|
|
103
|
+
const runtime = await newestRuntime();
|
|
104
|
+
const existing = (await listDevices()).find(
|
|
105
|
+
(d) => d.name === baseName() && d.runtime === runtime
|
|
106
|
+
);
|
|
107
|
+
if (existing) return existing;
|
|
108
|
+
// A base on an older runtime is replaced; its clones keep working.
|
|
109
|
+
for (const d of (await listDevices()).filter((d) => d.name === baseName()))
|
|
110
|
+
await deleteDevice(d);
|
|
111
|
+
const udid = await run(["create", baseName(), deviceTypeFor(model), runtime]);
|
|
112
|
+
await run(["boot", udid]);
|
|
113
|
+
await run(["bootstatus", udid, "-b"]);
|
|
114
|
+
await run(["shutdown", udid]);
|
|
115
|
+
return { udid, name: baseName(), state: "Shutdown", runtime };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function cloneDevice(base: SimDevice, name: string) {
|
|
119
|
+
if (!ownsDevice(name)) throw new Error(`refusing to create ${name}`);
|
|
120
|
+
return run(["clone", base.udid, name]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function bootDevice(udid: string): Promise<void> {
|
|
124
|
+
try {
|
|
125
|
+
await run(["boot", udid]);
|
|
126
|
+
} catch (error) {
|
|
127
|
+
// Booting a booted device is not a failure.
|
|
128
|
+
if (!/current state: Booted/i.test(String(error))) throw error;
|
|
129
|
+
}
|
|
130
|
+
await run(["bootstatus", udid, "-b"]);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export async function shutdownDevice(device: {
|
|
134
|
+
udid: string;
|
|
135
|
+
name: string;
|
|
136
|
+
}): Promise<void> {
|
|
137
|
+
if (!ownsDevice(device.name)) return;
|
|
138
|
+
try {
|
|
139
|
+
await run(["shutdown", device.udid]);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (!/current state: Shutdown/i.test(String(error))) throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function deleteDevice(device: {
|
|
146
|
+
udid: string;
|
|
147
|
+
name: string;
|
|
148
|
+
}): Promise<void> {
|
|
149
|
+
if (!ownsDevice(device.name)) return;
|
|
150
|
+
await shutdownDevice(device).catch(() => {});
|
|
151
|
+
await run(["delete", device.udid]);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// What the device's app logged lately, newest last.
|
|
155
|
+
export const recentLog = (udid: string, process: string, last: string) =>
|
|
156
|
+
run([
|
|
157
|
+
"spawn",
|
|
158
|
+
udid,
|
|
159
|
+
"log",
|
|
160
|
+
"show",
|
|
161
|
+
"--last",
|
|
162
|
+
last,
|
|
163
|
+
"--predicate",
|
|
164
|
+
`process == "${process.replace(/[^\w .-]/g, "")}"`,
|
|
165
|
+
"--style",
|
|
166
|
+
"compact",
|
|
167
|
+
]);
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -71,6 +71,7 @@ import { claimAgentosEnv } from "./lib/agents/self-env";
|
|
|
71
71
|
import { wslNotes, wslVersion } from "./lib/wsl";
|
|
72
72
|
import { getAllProjects } from "./lib/projects";
|
|
73
73
|
import { isRemoteHost } from "./lib/hosts";
|
|
74
|
+
import { sweepSimulators } from "./lib/simulator";
|
|
74
75
|
|
|
75
76
|
const dev = process.env.NODE_ENV !== "production";
|
|
76
77
|
const hostname = "127.0.0.1";
|
|
@@ -369,6 +370,21 @@ app.prepare().then(async () => {
|
|
|
369
370
|
sweepOrphanTmux().catch((error) =>
|
|
370
371
|
console.error("Sweeping orphaned tmux sessions failed:", error)
|
|
371
372
|
);
|
|
373
|
+
// Simulators of tasks that ended or crashed, now and every few minutes.
|
|
374
|
+
let sweepingSims = false;
|
|
375
|
+
const sweepSims = async () => {
|
|
376
|
+
if (sweepingSims) return;
|
|
377
|
+
sweepingSims = true;
|
|
378
|
+
try {
|
|
379
|
+
await sweepSimulators();
|
|
380
|
+
} catch (error) {
|
|
381
|
+
console.error("Sweeping simulators failed:", error);
|
|
382
|
+
} finally {
|
|
383
|
+
sweepingSims = false;
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
void sweepSims();
|
|
387
|
+
setInterval(sweepSims, 5 * 60_000);
|
|
372
388
|
// Chat turns that kept running through a restart.
|
|
373
389
|
void reattachChats();
|
|
374
390
|
// Reviews this restart cut off run again and deliver their verdicts.
|