@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.
- package/README.md +1 -1
- package/app/api/bus/spawn/route.ts +21 -3
- package/app/api/load/route.test.ts +32 -0
- package/app/api/load/route.ts +20 -0
- package/app/api/sessions/[id]/pause/route.ts +97 -0
- package/app/api/tasks/[id]/priority/route.ts +29 -0
- package/app/api/tasks/route.ts +17 -2
- package/components/Chat/ChatPanel.tsx +2 -0
- package/components/Chat/PausedBanner.tsx +54 -0
- package/components/Draft/DraftChips.tsx +25 -0
- package/components/Draft/DraftPanel.tsx +1 -0
- package/components/SessionList/QueuedRow.tsx +46 -2
- package/components/SessionList/RowSubtitle.tsx +21 -3
- package/components/SessionList/SessionRowMenu.tsx +49 -1
- package/components/SessionList/useShelfActions.ts +39 -2
- package/data/sessions/shelf.ts +50 -0
- package/data/tasks/index.ts +2 -0
- package/data/tasks/queue.ts +38 -1
- package/lib/bus/index.ts +21 -2
- package/lib/chat/hold.ts +11 -2
- package/lib/db/migrations.ts +56 -1
- package/lib/db/types.ts +30 -0
- package/lib/done/index.ts +2 -0
- package/lib/drafts.ts +3 -0
- package/lib/hosts/peer-load.ts +120 -0
- package/lib/load/machines.ts +124 -0
- package/lib/load/monitor.ts +17 -2
- package/lib/load/start-gate.ts +95 -0
- package/lib/orchestrator/act.ts +78 -0
- package/lib/orchestrator/brakes.ts +2 -0
- package/lib/orchestrator/brief.ts +4 -1
- package/lib/orchestrator/digest.ts +15 -4
- package/lib/orchestrator/serve.ts +18 -0
- package/lib/orchestrator/tool-names.ts +3 -0
- package/lib/orchestrator/tool-schemas.ts +33 -0
- package/lib/orchestrator/tools.ts +7 -1
- package/lib/project-config/agentos.schema.json +13 -0
- package/lib/project-config/schema.ts +9 -0
- package/lib/sidebar/paused.ts +13 -0
- package/lib/sidebar/queued.ts +34 -1
- package/lib/sidebar/shelves.ts +21 -1
- package/lib/sidebar/snooze.ts +26 -0
- package/lib/tasks/finish.ts +7 -0
- package/lib/tasks/move-carry.ts +1 -0
- package/lib/tasks/move-flow.ts +6 -0
- package/lib/tasks/pause-hold.ts +70 -0
- package/lib/tasks/pause-policy.ts +130 -0
- package/lib/tasks/pause.ts +718 -0
- package/lib/tasks/placement.ts +219 -0
- package/lib/tasks/queue.ts +522 -64
- package/lib/tasks/schedule-types.ts +64 -0
- package/lib/tasks/schedule.ts +271 -0
- package/package.json +1 -1
- 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
|
+
}
|