@saadnvd1/agent-os 0.3.523 → 0.3.527
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 +82 -3
- package/app/api/doctor/route.ts +8 -0
- package/app/api/sessions/[id]/simulator/route.ts +32 -0
- package/app/api/sessions/[id]/simulator/run/route.ts +37 -0
- package/app/api/workspaces/[id]/orchestrator/needs-you/dismiss/route.ts +31 -0
- package/app/api/workspaces/[id]/orchestrator/questions/[questionId]/route.ts +15 -0
- package/app/page.tsx +8 -3
- package/components/Chat/ChatPanel.tsx +96 -8
- package/components/Chat/EventRow.tsx +2 -2
- package/components/Chat/EventTurns.test.ts +56 -0
- package/components/Chat/EventTurns.tsx +141 -0
- package/components/Chat/useFind.ts +5 -3
- package/components/Chat/useGoToItem.ts +71 -0
- package/components/Orchestrator/AsksList.tsx +16 -11
- package/components/Orchestrator/NeedsYouStrip.tsx +148 -0
- package/components/Orchestrator/OrchestratorBar.tsx +27 -2
- package/components/Settings/SettingsDialog.tsx +2 -2
- package/components/Settings/SetupSection.tsx +6 -1
- package/components/Simulator/ScreenshotStrip.test.ts +91 -0
- package/components/Simulator/ScreenshotStrip.tsx +102 -0
- package/components/Simulator/index.ts +1 -0
- package/components/Tasks/TaskRow.tsx +2 -0
- package/components/Welcome/DevicesStep.tsx +90 -0
- package/components/Welcome/HealthList.tsx +69 -0
- package/components/Welcome/ProjectStep.tsx +2 -8
- package/components/Welcome/WelcomeDialog.tsx +31 -7
- package/data/doctor/index.ts +29 -0
- package/data/orchestrators/index.ts +59 -0
- package/data/push/topics.ts +2 -0
- package/data/simulator/index.test.ts +14 -0
- package/data/simulator/index.ts +33 -0
- package/data/simulator/keys.ts +5 -0
- package/hooks/useNeedsYou.ts +53 -0
- package/lib/chat/events.ts +2 -0
- package/lib/chat/find.ts +25 -4
- package/lib/chat/group.ts +50 -2
- package/lib/chat/turns.ts +111 -0
- package/lib/db/changes.ts +6 -0
- package/lib/db/migrations.ts +51 -0
- package/lib/doctor/checks.ts +222 -0
- package/lib/orchestrator/ask-view.ts +13 -0
- package/lib/orchestrator/brief.ts +5 -1
- package/lib/orchestrator/deliver.ts +3 -0
- package/lib/orchestrator/events.ts +19 -0
- package/lib/orchestrator/needs-you-items.ts +104 -0
- package/lib/orchestrator/overview.ts +3 -0
- package/lib/orchestrator/questions.ts +120 -0
- package/lib/orchestrator/serve.ts +11 -0
- package/lib/orchestrator/tool-names.ts +2 -0
- package/lib/orchestrator/tool-schemas.ts +12 -0
- package/lib/orchestrator/tools.ts +4 -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/lib/welcome-url.ts +6 -1
- package/package.json +1 -1
- package/server.ts +16 -0
- package/stores/needsYouFocus.ts +13 -0
- package/stores/welcomeUi.test.ts +15 -0
- package/stores/welcomeUi.ts +6 -1
|
@@ -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/lib/welcome-url.ts
CHANGED
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
export const WELCOME_PARAM = "welcome";
|
|
5
5
|
|
|
6
|
-
export const WELCOME_STEPS = [
|
|
6
|
+
export const WELCOME_STEPS = [
|
|
7
|
+
"agents",
|
|
8
|
+
"project",
|
|
9
|
+
"devices",
|
|
10
|
+
"finish",
|
|
11
|
+
] as const;
|
|
7
12
|
|
|
8
13
|
export type WelcomeStep = (typeof WELCOME_STEPS)[number];
|
|
9
14
|
|
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.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { proxy } from "valtio";
|
|
2
|
+
|
|
3
|
+
// "Show this ask", raised by the Needs-you strip and handled by the asks list
|
|
4
|
+
// above the chat, which opens itself and scrolls the card into view.
|
|
5
|
+
export const needsYouFocus = proxy<{
|
|
6
|
+
ask: { id: number; at: number } | null;
|
|
7
|
+
}>({ ask: null });
|
|
8
|
+
|
|
9
|
+
export const needsYouFocusActions = {
|
|
10
|
+
ask: (id: number) => {
|
|
11
|
+
needsYouFocus.ask = { id, at: Date.now() };
|
|
12
|
+
},
|
|
13
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { welcomeUi, welcomeUiActions } from "./welcomeUi";
|
|
3
|
+
|
|
4
|
+
describe("choosing a project in the welcome", () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
welcomeUiActions.go("project");
|
|
7
|
+
welcomeUiActions.setProject(null);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
it("remembers it and lands on the devices step, not the finish", () => {
|
|
11
|
+
welcomeUiActions.chooseProject("p1");
|
|
12
|
+
expect(welcomeUi.projectId).toBe("p1");
|
|
13
|
+
expect(welcomeUi.step).toBe("devices");
|
|
14
|
+
});
|
|
15
|
+
});
|
package/stores/welcomeUi.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { proxy } from "valtio";
|
|
2
|
-
import type
|
|
2
|
+
import { WELCOME_STEPS, type WelcomeStep } from "@/lib/welcome-url";
|
|
3
3
|
|
|
4
4
|
// The first-run welcome: whether it is on show, the step, and the project
|
|
5
5
|
// the user made (or picked) to start in.
|
|
@@ -20,6 +20,11 @@ export const welcomeUiActions = {
|
|
|
20
20
|
setProject: (projectId: string | null) => {
|
|
21
21
|
welcomeUi.projectId = projectId;
|
|
22
22
|
},
|
|
23
|
+
// A project picked or added: remember it and move to the step after "project".
|
|
24
|
+
chooseProject: (projectId: string) => {
|
|
25
|
+
welcomeUi.projectId = projectId;
|
|
26
|
+
welcomeUi.step = WELCOME_STEPS[WELCOME_STEPS.indexOf("project") + 1];
|
|
27
|
+
},
|
|
23
28
|
close: () => {
|
|
24
29
|
welcomeUi.open = false;
|
|
25
30
|
},
|