@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,64 @@
|
|
|
1
|
+
// What the queue's scheduler and timed pause show clients. No imports, so any
|
|
2
|
+
// client (the web app, a native one) can share it.
|
|
3
|
+
|
|
4
|
+
export const PRIORITIES = [
|
|
5
|
+
"urgent",
|
|
6
|
+
"blocking",
|
|
7
|
+
"normal",
|
|
8
|
+
"background",
|
|
9
|
+
] as const;
|
|
10
|
+
export type Priority = (typeof PRIORITIES)[number];
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_PRIORITY: Priority = "normal";
|
|
13
|
+
|
|
14
|
+
export const PRIORITY_LABEL: Record<Priority, string> = {
|
|
15
|
+
urgent: "Urgent",
|
|
16
|
+
blocking: "Blocking",
|
|
17
|
+
normal: "Normal",
|
|
18
|
+
background: "Background",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// One line under each choice in the start form.
|
|
22
|
+
export const PRIORITY_HINT: Record<Priority, string> = {
|
|
23
|
+
urgent: "A bug you hit: starts first, even on a busy machine",
|
|
24
|
+
blocking: "Something else waits on it",
|
|
25
|
+
normal: "In arrival order",
|
|
26
|
+
background: "Starts last; waits for quiet",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const isPriority = (v: unknown): v is Priority =>
|
|
30
|
+
typeof v === "string" && (PRIORITIES as readonly string[]).includes(v);
|
|
31
|
+
|
|
32
|
+
// Why a queued task isn't starting; null on the view means it is next.
|
|
33
|
+
export type WaitCode =
|
|
34
|
+
| "after"
|
|
35
|
+
| "after_any"
|
|
36
|
+
| "orch_paused"
|
|
37
|
+
| "window"
|
|
38
|
+
| "load_red"
|
|
39
|
+
| "load_amber"
|
|
40
|
+
| "heavy"
|
|
41
|
+
| "spacing"
|
|
42
|
+
| "needs"
|
|
43
|
+
| "slot";
|
|
44
|
+
|
|
45
|
+
export type PauseState = "pausing" | "paused" | "resuming";
|
|
46
|
+
export type PauseBy = "person" | "orchestrator" | "scheduler";
|
|
47
|
+
|
|
48
|
+
// A paused (or pausing) session as lists show it.
|
|
49
|
+
export interface PauseView {
|
|
50
|
+
state: PauseState;
|
|
51
|
+
// When it resumes by itself; null: until someone resumes it.
|
|
52
|
+
until: string | null;
|
|
53
|
+
by: PauseBy;
|
|
54
|
+
reason: string | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface QueueExplainView {
|
|
58
|
+
code: WaitCode | null;
|
|
59
|
+
text: string | null;
|
|
60
|
+
score: number;
|
|
61
|
+
rank: number;
|
|
62
|
+
of: number;
|
|
63
|
+
priority: Priority;
|
|
64
|
+
}
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The queue's scheduler, as pure functions: candidates and what we know about
|
|
3
|
+
* the machine in, who starts and why the others wait out. Rules only, no model
|
|
4
|
+
* and no clock of its own, so every case is a plain test.
|
|
5
|
+
*
|
|
6
|
+
* A candidate (a queued task, or a paused one that came due) first meets a
|
|
7
|
+
* fixed list of holds; the first that applies is the reason shown. The ones
|
|
8
|
+
* that pass are ordered by score: priority + age (capped) + a resume bonus.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_PRIORITY,
|
|
13
|
+
PRIORITY_LABEL,
|
|
14
|
+
type Priority,
|
|
15
|
+
type QueueExplainView,
|
|
16
|
+
type WaitCode,
|
|
17
|
+
} from "./schedule-types";
|
|
18
|
+
|
|
19
|
+
export { PRIORITIES, DEFAULT_PRIORITY } from "./schedule-types";
|
|
20
|
+
export type { Priority, WaitCode } from "./schedule-types";
|
|
21
|
+
|
|
22
|
+
export const PRIORITY_POINTS: Record<Priority, number> = {
|
|
23
|
+
urgent: 400,
|
|
24
|
+
blocking: 300,
|
|
25
|
+
normal: 200,
|
|
26
|
+
background: 100,
|
|
27
|
+
};
|
|
28
|
+
export const AGE_POINTS_PER_MIN = 0.5;
|
|
29
|
+
// Reached after five hours of waiting.
|
|
30
|
+
export const AGE_CAP = 150;
|
|
31
|
+
export const RESUME_BONUS = 150;
|
|
32
|
+
// Out of red this long before automatic starts resume.
|
|
33
|
+
export const LOAD_SETTLE_MS = 120_000;
|
|
34
|
+
// On amber, only this priority and above start.
|
|
35
|
+
export const AMBER_MIN_PRIORITY: Priority = "blocking";
|
|
36
|
+
// Between automatic starts on one machine.
|
|
37
|
+
export const START_GAP_MS = 60_000;
|
|
38
|
+
// Between starts of tasks that boot a simulator.
|
|
39
|
+
export const HEAVY_START_GAP_MS = 5 * 60_000;
|
|
40
|
+
|
|
41
|
+
export type MachineLevel = "green" | "amber" | "red";
|
|
42
|
+
|
|
43
|
+
// What the machine a candidate would start on looks like right now.
|
|
44
|
+
export interface MachineView {
|
|
45
|
+
// Shown in hold texts, e.g. "this machine".
|
|
46
|
+
label: string;
|
|
47
|
+
// False: no load holds at all (AGENTOS_LOAD=off).
|
|
48
|
+
enabled: boolean;
|
|
49
|
+
// No sample read yet since AgentOS started.
|
|
50
|
+
reading: boolean;
|
|
51
|
+
level: MachineLevel;
|
|
52
|
+
// Not red, and calm for LOAD_SETTLE_MS since it last was.
|
|
53
|
+
settled: boolean;
|
|
54
|
+
// Seconds of calm still needed, when not settled and not red.
|
|
55
|
+
settleLeftMs: number;
|
|
56
|
+
booted: number;
|
|
57
|
+
maxBooted: number;
|
|
58
|
+
// Heavy tasks started within HEAVY_START_GAP_MS that hold no lease yet.
|
|
59
|
+
heavyStarting: number;
|
|
60
|
+
lastAutoStartAt: number | null;
|
|
61
|
+
lastHeavyStartAt: number | null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface Hold {
|
|
65
|
+
code: WaitCode;
|
|
66
|
+
text: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface Candidate {
|
|
70
|
+
id: string;
|
|
71
|
+
kind: "start" | "resume";
|
|
72
|
+
// The line it is in: a workspace, or the project without one.
|
|
73
|
+
group: string;
|
|
74
|
+
priority: Priority;
|
|
75
|
+
// The existing line order; breaks ties.
|
|
76
|
+
position: number;
|
|
77
|
+
// When it started waiting (queued, or came due to resume).
|
|
78
|
+
waitingSinceMs: number;
|
|
79
|
+
// Boots a simulator.
|
|
80
|
+
heavy: boolean;
|
|
81
|
+
// Needs something the machine it would start on can't give (a Mac).
|
|
82
|
+
needsUnmet: boolean;
|
|
83
|
+
// Starts on another machine: this one's load doesn't matter to it.
|
|
84
|
+
remote: boolean;
|
|
85
|
+
// `after X` hold, from the caller (it needs the database).
|
|
86
|
+
before: Hold | null;
|
|
87
|
+
// Orchestrator Pause and the usage window, which hold after `after`.
|
|
88
|
+
auto: Hold | null;
|
|
89
|
+
// "After any": its set, the ones that have finished, the ones other waiters
|
|
90
|
+
// already used, and whether it was already released (a retry).
|
|
91
|
+
any: {
|
|
92
|
+
ids: string[];
|
|
93
|
+
finished: string[];
|
|
94
|
+
used: string[];
|
|
95
|
+
released: boolean;
|
|
96
|
+
label: string;
|
|
97
|
+
} | null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export interface Group {
|
|
101
|
+
inFlight: number;
|
|
102
|
+
limit: number;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface Plan {
|
|
106
|
+
start: string[];
|
|
107
|
+
// The finish an "after any" waiter takes, by candidate id (null: none left).
|
|
108
|
+
consume: Map<string, string | null>;
|
|
109
|
+
explain: Map<string, QueueExplainView>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export function scoreParts(
|
|
113
|
+
c: Pick<Candidate, "kind" | "priority" | "waitingSinceMs">,
|
|
114
|
+
now: number
|
|
115
|
+
) {
|
|
116
|
+
const minutes = Math.max(0, (now - c.waitingSinceMs) / 60_000);
|
|
117
|
+
const priority =
|
|
118
|
+
PRIORITY_POINTS[c.priority] ?? PRIORITY_POINTS[DEFAULT_PRIORITY];
|
|
119
|
+
const age = Math.min(AGE_CAP, AGE_POINTS_PER_MIN * minutes);
|
|
120
|
+
const resume = c.kind === "resume" ? RESUME_BONUS : 0;
|
|
121
|
+
return { priority, age, resume, score: priority + age + resume };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const priorityRank = (p: Priority) => PRIORITY_POINTS[p] ?? 0;
|
|
125
|
+
|
|
126
|
+
const seconds = (ms: number) => Math.max(1, Math.ceil(ms / 1000));
|
|
127
|
+
|
|
128
|
+
// The machine holds, in order. Null: it may start here now.
|
|
129
|
+
export function machineHold(
|
|
130
|
+
c: Pick<Candidate, "heavy" | "needsUnmet" | "remote" | "priority">,
|
|
131
|
+
m: MachineView,
|
|
132
|
+
now: number
|
|
133
|
+
): Hold | null {
|
|
134
|
+
if (c.remote) return null;
|
|
135
|
+
if (c.needsUnmet) return { code: "needs", text: `Waiting: needs a Mac` };
|
|
136
|
+
if (!m.enabled) return null;
|
|
137
|
+
if (m.reading)
|
|
138
|
+
return { code: "load_red", text: "Waiting: reading machine load" };
|
|
139
|
+
if (m.level === "red")
|
|
140
|
+
return { code: "load_red", text: `Waiting: ${m.label} load red` };
|
|
141
|
+
if (!m.settled)
|
|
142
|
+
return {
|
|
143
|
+
code: "load_red",
|
|
144
|
+
text: `Waiting: ${m.label} load was red; calm for ${seconds(m.settleLeftMs)}s more`,
|
|
145
|
+
};
|
|
146
|
+
if (
|
|
147
|
+
m.level === "amber" &&
|
|
148
|
+
priorityRank(c.priority) < priorityRank(AMBER_MIN_PRIORITY)
|
|
149
|
+
)
|
|
150
|
+
return {
|
|
151
|
+
code: "load_amber",
|
|
152
|
+
text: `Waiting: ${m.label} load amber; ${c.priority} work starts on green`,
|
|
153
|
+
};
|
|
154
|
+
if (c.heavy) {
|
|
155
|
+
if (m.level !== "green")
|
|
156
|
+
return {
|
|
157
|
+
code: "heavy",
|
|
158
|
+
text: `Waiting: ${m.label} load amber; simulator work starts on green`,
|
|
159
|
+
};
|
|
160
|
+
if (m.heavyStarting > 0)
|
|
161
|
+
return {
|
|
162
|
+
code: "heavy",
|
|
163
|
+
text: `Waiting: a simulator is booting on ${m.label}`,
|
|
164
|
+
};
|
|
165
|
+
if (m.booted >= m.maxBooted)
|
|
166
|
+
return {
|
|
167
|
+
code: "heavy",
|
|
168
|
+
text: `Waiting: ${m.booted} of ${m.maxBooted} simulators in use`,
|
|
169
|
+
};
|
|
170
|
+
if (
|
|
171
|
+
m.lastHeavyStartAt !== null &&
|
|
172
|
+
now - m.lastHeavyStartAt < HEAVY_START_GAP_MS
|
|
173
|
+
)
|
|
174
|
+
return {
|
|
175
|
+
code: "heavy",
|
|
176
|
+
text: `Waiting: a simulator task just started on ${m.label} (next in ${seconds(HEAVY_START_GAP_MS - (now - m.lastHeavyStartAt))}s)`,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
if (m.lastAutoStartAt !== null && now - m.lastAutoStartAt < START_GAP_MS)
|
|
180
|
+
return {
|
|
181
|
+
code: "spacing",
|
|
182
|
+
text: `Waiting: spacing starts (next in ${seconds(START_GAP_MS - (now - m.lastAutoStartAt))}s)`,
|
|
183
|
+
};
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export function slotText(rank: number, of: number, priority: Priority): string {
|
|
188
|
+
const word = priority === DEFAULT_PRIORITY ? "" : ` · ${priority}`;
|
|
189
|
+
return `#${rank} of ${of}${word}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function priorityWord(priority: Priority): string {
|
|
193
|
+
return PRIORITY_LABEL[priority];
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// One look at the line. Candidates in, who starts and every row's reason out.
|
|
197
|
+
export function planStarts(
|
|
198
|
+
candidates: Candidate[],
|
|
199
|
+
groups: Map<string, Group>,
|
|
200
|
+
machine: MachineView,
|
|
201
|
+
now: number
|
|
202
|
+
): Plan {
|
|
203
|
+
const plan: Plan = { start: [], consume: new Map(), explain: new Map() };
|
|
204
|
+
const m: MachineView = { ...machine };
|
|
205
|
+
const byGroup = new Map<string, Candidate[]>();
|
|
206
|
+
for (const c of candidates)
|
|
207
|
+
byGroup.set(c.group, [...(byGroup.get(c.group) ?? []), c]);
|
|
208
|
+
const used = new Set<string>();
|
|
209
|
+
for (const c of candidates) for (const id of c.any?.used ?? []) used.add(id);
|
|
210
|
+
|
|
211
|
+
for (const [key, list] of byGroup) {
|
|
212
|
+
const group = groups.get(key) ?? { inFlight: 0, limit: Infinity };
|
|
213
|
+
let inFlight = group.inFlight;
|
|
214
|
+
const scored = list
|
|
215
|
+
.map((c) => ({ c, ...scoreParts(c, now) }))
|
|
216
|
+
.sort((a, b) => b.score - a.score || a.c.position - b.c.position);
|
|
217
|
+
scored.forEach(({ c, score, priority: _p }, index) => {
|
|
218
|
+
const rank = index + 1;
|
|
219
|
+
const hold = holdOf(c);
|
|
220
|
+
const view = (h: Hold | null): QueueExplainView => ({
|
|
221
|
+
code: h?.code ?? null,
|
|
222
|
+
text: h?.text ?? null,
|
|
223
|
+
score: Math.round(score * 10) / 10,
|
|
224
|
+
rank,
|
|
225
|
+
of: scored.length,
|
|
226
|
+
priority: c.priority,
|
|
227
|
+
});
|
|
228
|
+
if (hold) return void plan.explain.set(c.id, view(hold));
|
|
229
|
+
if (inFlight >= group.limit)
|
|
230
|
+
return void plan.explain.set(
|
|
231
|
+
c.id,
|
|
232
|
+
view({
|
|
233
|
+
code: "slot",
|
|
234
|
+
text: slotText(rank, scored.length, c.priority),
|
|
235
|
+
})
|
|
236
|
+
);
|
|
237
|
+
// Passed every hold: it starts.
|
|
238
|
+
if (c.any && !c.any.released) {
|
|
239
|
+
const free = c.any.finished.find((id) => !used.has(id)) ?? null;
|
|
240
|
+
if (free) used.add(free);
|
|
241
|
+
plan.consume.set(c.id, free);
|
|
242
|
+
}
|
|
243
|
+
inFlight += 1;
|
|
244
|
+
if (!c.remote) {
|
|
245
|
+
m.lastAutoStartAt = now;
|
|
246
|
+
if (c.heavy) {
|
|
247
|
+
m.lastHeavyStartAt = now;
|
|
248
|
+
m.heavyStarting += 1;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
plan.start.push(c.id);
|
|
252
|
+
plan.explain.set(c.id, view(null));
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return plan;
|
|
256
|
+
|
|
257
|
+
function holdOf(c: Candidate): Hold | null {
|
|
258
|
+
if (c.before) return c.before;
|
|
259
|
+
if (c.any && !c.any.released) {
|
|
260
|
+
const free = c.any.finished.some((id) => !used.has(id));
|
|
261
|
+
const spent =
|
|
262
|
+
c.any.ids.length > 0 && c.any.finished.length === c.any.ids.length;
|
|
263
|
+
if (!free && !spent)
|
|
264
|
+
return {
|
|
265
|
+
code: "after_any",
|
|
266
|
+
text: `Waiting: after any running task (${c.any.label})`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
return c.auto ?? machineHold(c, m, now);
|
|
270
|
+
}
|
|
271
|
+
}
|
package/package.json
CHANGED
package/server.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { ensureBusBrief } from "./lib/agents/launch";
|
|
|
8
8
|
import { reattachChats } from "./lib/chat/runner";
|
|
9
9
|
import { serveChatSocket } from "./lib/chat/socket";
|
|
10
10
|
import { startStackWatcher } from "./lib/stacks";
|
|
11
|
+
import { startPauseWatcher } from "./lib/tasks/pause";
|
|
11
12
|
import { setStartGate } from "./lib/stacks/tick";
|
|
12
13
|
import { stackStartGate } from "./lib/orchestrator/brakes";
|
|
13
14
|
import { buildId } from "./lib/build";
|
|
@@ -393,7 +394,7 @@ app.prepare().then(async () => {
|
|
|
393
394
|
void sweepSims();
|
|
394
395
|
setInterval(sweepSims, 5 * 60_000);
|
|
395
396
|
// Chat turns that kept running through a restart.
|
|
396
|
-
void reattachChats();
|
|
397
|
+
void reattachChats().finally(() => startPauseWatcher());
|
|
397
398
|
// Reviews this restart cut off run again and deliver their verdicts.
|
|
398
399
|
if (process.env.AGENTOS_ORCHESTRATOR !== "off")
|
|
399
400
|
resumeReviews().catch((error) =>
|