@saadnvd1/agent-os 0.3.571 → 0.3.573

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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/app/api/bus/spawn/route.ts +21 -3
  3. package/app/api/load/route.test.ts +32 -0
  4. package/app/api/load/route.ts +20 -0
  5. package/app/api/sessions/[id]/pause/route.ts +97 -0
  6. package/app/api/tasks/[id]/priority/route.ts +29 -0
  7. package/app/api/tasks/route.ts +17 -2
  8. package/components/Chat/ChatPanel.tsx +2 -0
  9. package/components/Chat/PausedBanner.tsx +54 -0
  10. package/components/Draft/DraftChips.tsx +25 -0
  11. package/components/Draft/DraftPanel.tsx +1 -0
  12. package/components/SessionList/QueuedRow.tsx +46 -2
  13. package/components/SessionList/RowSubtitle.tsx +21 -3
  14. package/components/SessionList/SessionRowMenu.tsx +49 -1
  15. package/components/SessionList/useShelfActions.ts +39 -2
  16. package/data/sessions/shelf.ts +50 -0
  17. package/data/tasks/index.ts +2 -0
  18. package/data/tasks/queue.ts +38 -1
  19. package/lib/bus/index.ts +21 -2
  20. package/lib/chat/hold.ts +11 -2
  21. package/lib/db/migrations.ts +56 -1
  22. package/lib/db/types.ts +30 -0
  23. package/lib/done/index.ts +2 -0
  24. package/lib/drafts.ts +3 -0
  25. package/lib/hosts/peer-load.ts +120 -0
  26. package/lib/load/machines.ts +124 -0
  27. package/lib/load/monitor.ts +17 -2
  28. package/lib/load/start-gate.ts +95 -0
  29. package/lib/orchestrator/act.ts +78 -0
  30. package/lib/orchestrator/brakes.ts +2 -0
  31. package/lib/orchestrator/brief.ts +4 -1
  32. package/lib/orchestrator/digest.ts +15 -4
  33. package/lib/orchestrator/serve.ts +18 -0
  34. package/lib/orchestrator/tool-names.ts +3 -0
  35. package/lib/orchestrator/tool-schemas.ts +33 -0
  36. package/lib/orchestrator/tools.ts +7 -1
  37. package/lib/project-config/agentos.schema.json +13 -0
  38. package/lib/project-config/schema.ts +9 -0
  39. package/lib/sidebar/paused.ts +13 -0
  40. package/lib/sidebar/queued.ts +34 -1
  41. package/lib/sidebar/shelves.ts +21 -1
  42. package/lib/sidebar/snooze.ts +26 -0
  43. package/lib/tasks/finish.ts +7 -0
  44. package/lib/tasks/move-carry.ts +1 -0
  45. package/lib/tasks/move-flow.ts +6 -0
  46. package/lib/tasks/pause-hold.ts +70 -0
  47. package/lib/tasks/pause-policy.ts +130 -0
  48. package/lib/tasks/pause.ts +718 -0
  49. package/lib/tasks/placement.ts +219 -0
  50. package/lib/tasks/queue.ts +522 -64
  51. package/lib/tasks/schedule-types.ts +64 -0
  52. package/lib/tasks/schedule.ts +271 -0
  53. package/package.json +1 -1
  54. package/server.ts +2 -1
@@ -0,0 +1,219 @@
1
+ /**
2
+ * Where a task may run. A task's needs are a small set (a Mac, Xcode, a
3
+ * simulator); a machine reports what it has. Needs come from, in order: the
4
+ * task's own setting, a `machine:` line in its brief, the project's
5
+ * agentos.json, and what the repository's files imply. `place` is pure: the
6
+ * machines' readings in, the machine to start it on out. iOS work never
7
+ * leaves a Mac, because no other machine reports those capabilities.
8
+ */
9
+
10
+ import fs from "fs";
11
+ import path from "path";
12
+ import { db } from "../db";
13
+ import { loadProjectConfig } from "../project-config";
14
+ import { MACHINE_NEEDS } from "../project-config/schema";
15
+ import {
16
+ machineHold,
17
+ type Candidate,
18
+ type Hold,
19
+ type MachineView,
20
+ } from "./schedule";
21
+
22
+ export type Need = (typeof MACHINE_NEEDS)[number];
23
+ export type NeedsSource = "task" | "brief" | "config" | "inferred" | "none";
24
+
25
+ const ALL_MAC: Need[] = ["mac", "xcode", "simulator"];
26
+
27
+ const isNeed = (v: unknown): v is Need =>
28
+ typeof v === "string" && (MACHINE_NEEDS as readonly string[]).includes(v);
29
+
30
+ export const heavyNeeds = (needs: Need[]) => needs.includes("simulator");
31
+
32
+ export const meetsNeeds = (needs: Need[], capabilities: string[]) =>
33
+ needs.every((n) => capabilities.includes(n));
34
+
35
+ // What a request's `machine` setting means: "mac" pins it to a Mac, "any"
36
+ // says it needs nothing.
37
+ export function needsFromSetting(setting: unknown): Need[] | null {
38
+ if (setting === "any") return [];
39
+ if (setting === "mac") return ["mac"];
40
+ if (Array.isArray(setting)) return setting.filter(isNeed);
41
+ return null;
42
+ }
43
+
44
+ // `machine: mac` or `needs: simulator, xcode` on a line of its own.
45
+ export function needsFromBrief(prompt: string): Need[] | null {
46
+ const m = /^[ \t]*(?:machine|needs)[ \t]*:[ \t]*([^\n]+)$/im.exec(prompt);
47
+ if (!m) return null;
48
+ const words = m[1]
49
+ .toLowerCase()
50
+ .split(/[,\s]+/)
51
+ .filter(Boolean);
52
+ if (words.includes("any")) return [];
53
+ const out = new Set<Need>();
54
+ for (const w of words) {
55
+ if (isNeed(w)) out.add(w);
56
+ if (w === "ios") ALL_MAC.forEach((n) => out.add(n));
57
+ }
58
+ return out.size ? [...out] : null;
59
+ }
60
+
61
+ // An Xcode project or workspace one level down (ios/App.xcodeproj), or an
62
+ // Expo app with an ios section.
63
+ export function inferNeeds(projectPath: string): Need[] {
64
+ const ios = path.join(projectPath, "ios");
65
+ try {
66
+ if (
67
+ fs
68
+ .readdirSync(ios)
69
+ .some((f) => f.endsWith(".xcodeproj") || f.endsWith(".xcworkspace"))
70
+ )
71
+ return ALL_MAC;
72
+ } catch {
73
+ // no ios folder
74
+ }
75
+ try {
76
+ const app = JSON.parse(
77
+ fs.readFileSync(path.join(projectPath, "app.json"), "utf-8")
78
+ ) as { ios?: unknown; expo?: { ios?: unknown } };
79
+ if (app.ios || app.expo?.ios) return ALL_MAC;
80
+ } catch {
81
+ // no app.json
82
+ }
83
+ return [];
84
+ }
85
+
86
+ export function needsOf(input: {
87
+ projectId: string;
88
+ projectPath: string;
89
+ prompt: string;
90
+ setting?: unknown;
91
+ }): { needs: Need[]; source: NeedsSource } {
92
+ const task = needsFromSetting(input.setting);
93
+ if (task) return { needs: task, source: "task" };
94
+ const brief = needsFromBrief(input.prompt);
95
+ if (brief) return { needs: brief, source: "brief" };
96
+ const loaded = loadProjectConfig(input.projectPath);
97
+ const declared = loaded.config.machine?.needs;
98
+ if (declared) return { needs: declared, source: "config" };
99
+ if (loaded.config.simulator) return remember(input.projectId, ALL_MAC);
100
+ const inferred = inferNeeds(input.projectPath);
101
+ return remember(input.projectId, inferred);
102
+ }
103
+
104
+ // Kept on the project so a list can show it without reading its files.
105
+ function remember(
106
+ projectId: string,
107
+ needs: Need[]
108
+ ): { needs: Need[]; source: NeedsSource } {
109
+ const json = JSON.stringify(needs);
110
+ try {
111
+ db.prepare(
112
+ `UPDATE projects SET machine_needs = ?, machine_needs_source = 'inferred'
113
+ WHERE id = ? AND (machine_needs IS NOT ?)`
114
+ ).run(json, projectId, json);
115
+ } catch {
116
+ // a read-only caller: the answer stands without the note
117
+ }
118
+ return { needs, source: needs.length ? "inferred" : "none" };
119
+ }
120
+
121
+ export const parseNeeds = (json: string | null): Need[] | null => {
122
+ if (!json) return null;
123
+ try {
124
+ const v = JSON.parse(json) as unknown;
125
+ return Array.isArray(v) ? v.filter(isNeed) : null;
126
+ } catch {
127
+ return null;
128
+ }
129
+ };
130
+
131
+ // Why a queued task can't be started on another machine, or null. These are
132
+ // the kinds of task `startRemoteTask` refuses or would change on the way.
133
+ export function unplaceable(r: {
134
+ plan_only: number | null;
135
+ plan: number | null;
136
+ agent_type: string | null;
137
+ model_overrides: string | null;
138
+ host_id: string | null;
139
+ }): string | null {
140
+ if (r.host_id) return "it was pinned to a machine";
141
+ if (r.plan_only) return "plan-only tasks run on this machine";
142
+ if (r.plan) return "plan-first tasks run on this machine";
143
+ if (r.agent_type && r.agent_type !== "claude")
144
+ return "only Claude tasks run on another machine";
145
+ if (r.model_overrides) return "per-role models run on this machine";
146
+ return null;
147
+ }
148
+
149
+ export interface PeerOption {
150
+ hostId: string;
151
+ name: string;
152
+ view: MachineView;
153
+ capabilities: string[];
154
+ // The project exists there (projectMachines said null).
155
+ hasProject: boolean;
156
+ loadPerCore: number;
157
+ }
158
+
159
+ export interface Placement {
160
+ hostId: string;
161
+ name: string;
162
+ // Shown in the row and told to the orchestrator: "this machine load red".
163
+ why: string;
164
+ }
165
+
166
+ const LOAD_HOLDS = new Set<Hold["code"]>([
167
+ "load_red",
168
+ "load_amber",
169
+ "needs",
170
+ "heavy",
171
+ ]);
172
+
173
+ // True when the home machine's load or needs rule this start out, which is
174
+ // the only time a linked machine is worth asking.
175
+ export const wantsPlacing = (
176
+ c: Pick<Candidate, "heavy" | "needsUnmet" | "remote" | "priority">,
177
+ home: MachineView,
178
+ now: number
179
+ ): boolean => {
180
+ if (c.remote || home.reading) return false;
181
+ const hold = machineHold(c, home, now);
182
+ return !!hold && LOAD_HOLDS.has(hold.code);
183
+ };
184
+
185
+ // Null: start it at home (or hold it there for a reason placing doesn't
186
+ // change). Otherwise the best linked machine that is green, fresh, has the
187
+ // project and meets the needs.
188
+ export function place(
189
+ c: Pick<Candidate, "heavy" | "needsUnmet" | "remote" | "priority">,
190
+ home: MachineView,
191
+ peers: PeerOption[],
192
+ needs: Need[],
193
+ now: number
194
+ ): Placement | null {
195
+ if (!wantsPlacing(c, home, now)) return null;
196
+ const hold = machineHold(c, home, now)!;
197
+ const options = peers
198
+ .filter(
199
+ (p) =>
200
+ p.hasProject &&
201
+ meetsNeeds(needs, p.capabilities) &&
202
+ machineHold({ ...c, needsUnmet: false, heavy: false }, p.view, now) ===
203
+ null
204
+ )
205
+ .sort(
206
+ (a, b) =>
207
+ a.loadPerCore - b.loadPerCore || a.hostId.localeCompare(b.hostId)
208
+ );
209
+ const best = options[0];
210
+ if (!best) return null;
211
+ return {
212
+ hostId: best.hostId,
213
+ name: best.name,
214
+ why:
215
+ hold.code === "needs"
216
+ ? "needs not met here"
217
+ : `${home.label} load ${home.level}`,
218
+ };
219
+ }