@saadnvd1/agent-os 0.3.533 → 0.3.535
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 +74 -4
- package/app/api/features/pieces/[id]/release/route.ts +16 -0
- package/app/api/features/route.ts +8 -0
- package/app/api/recipes/[id]/carry/clear/route.ts +20 -0
- package/app/api/recipes/[id]/preview/route.ts +19 -0
- package/app/api/recipes/[id]/route.ts +51 -0
- package/app/api/recipes/[id]/run/route.ts +35 -0
- package/app/api/recipes/[id]/runs/route.ts +23 -0
- package/app/api/recipes/draft/route.ts +14 -0
- package/app/api/recipes/route.ts +35 -0
- package/app/api/tasks/[id]/plan-edits/route.ts +34 -0
- package/components/CommandPalette/useAppCommands.ts +20 -0
- package/components/Features/FeaturesSection.tsx +100 -0
- package/components/Features/index.ts +1 -0
- package/components/Recipes/RecipeForm.tsx +251 -0
- package/components/Recipes/RecipePage.tsx +127 -0
- package/components/Recipes/RecipesPanel.tsx +225 -0
- package/components/Recipes/RunChip.tsx +16 -0
- package/components/Recipes/RunHistory.tsx +88 -0
- package/components/Recipes/RunSheet.tsx +84 -0
- package/components/Recipes/TimerFields.tsx +193 -0
- package/components/Recipes/format.ts +51 -0
- package/components/Recipes/index.ts +1 -0
- package/components/Schedules/ScheduleForm.tsx +20 -2
- package/components/Schedules/SchedulesPanel.tsx +1 -0
- package/components/SessionList/RowSubtitle.tsx +16 -0
- package/components/SessionList/SessionRowMenu.tsx +29 -0
- package/components/SessionList/SidebarHeader.tsx +5 -0
- package/components/SessionList/WorkspaceSwitcher.tsx +20 -4
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Tasks/PlanReview.tsx +253 -0
- package/components/Tasks/TaskRow.tsx +19 -44
- package/components/Tasks/TasksDialog.tsx +2 -0
- package/data/features/index.ts +40 -0
- package/data/push/topics.ts +8 -0
- package/data/recipes/index.ts +129 -0
- package/data/tasks/index.ts +14 -0
- package/lib/away/run.ts +4 -1
- package/lib/db/changes.ts +8 -0
- package/lib/db/migrations.ts +143 -0
- package/lib/db/queries.ts +4 -1
- package/lib/db/schedules-move.ts +220 -0
- package/lib/db/types.ts +6 -0
- package/lib/models/plan-step.ts +67 -6
- package/lib/notify/copy.ts +28 -10
- package/lib/notify/push/events.ts +36 -2
- package/lib/notify/push/policy.ts +2 -0
- package/lib/notify/push-types.ts +7 -3
- package/lib/orchestrator/brief.ts +13 -0
- package/lib/orchestrator/conditions.ts +3 -1
- package/lib/orchestrator/deliver.ts +2 -0
- package/lib/orchestrator/digest.ts +27 -1
- package/lib/orchestrator/plan-pieces.ts +41 -0
- package/lib/orchestrator/recipe-tools.ts +159 -0
- package/lib/orchestrator/review.ts +2 -1
- package/lib/orchestrator/serve.ts +25 -0
- package/lib/orchestrator/tool-names.ts +5 -0
- package/lib/orchestrator/tool-schemas.ts +54 -0
- package/lib/orchestrator/tools.ts +10 -0
- package/lib/plans/features.ts +200 -0
- package/lib/plans/pieces.ts +366 -0
- package/lib/plans/release.ts +323 -0
- package/lib/plans/view.ts +85 -0
- package/lib/recipes/api.ts +19 -0
- package/lib/recipes/chip.ts +40 -0
- package/lib/recipes/compose.ts +78 -0
- package/lib/recipes/draft.ts +93 -0
- package/lib/recipes/index.ts +54 -0
- package/lib/recipes/result.ts +59 -0
- package/lib/recipes/run.ts +202 -0
- package/lib/recipes/settle.ts +465 -0
- package/lib/recipes/store.ts +378 -0
- package/lib/recipes/test-fixtures.ts +49 -0
- package/lib/recipes/timer.ts +107 -0
- package/lib/recipes/types.ts +116 -0
- package/lib/schedules/index.ts +4 -1
- package/lib/schedules/start.ts +33 -9
- package/lib/schedules/store.ts +6 -2
- package/lib/security/demo.ts +1 -0
- package/lib/settings-url.ts +1 -0
- package/lib/tasks/loop-types.ts +23 -0
- package/lib/tasks/plan-view.ts +38 -1
- package/package.json +1 -1
- package/scripts/revert-schedules-move.ts +24 -0
- package/server.ts +2 -0
- package/stores/recipesUi.ts +53 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The line a run ends with: `RESULT: ok build=143 version=1.4.2 — one sentence`.
|
|
2
|
+
|
|
3
|
+
export interface RunResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
outputs: Record<string, string>;
|
|
6
|
+
summary: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const RESULT_LINE = /^\s*[⏺●•]?\s*RESULT:\s*(ok|failed)\b(.*)$/gm;
|
|
10
|
+
const KEY = /^[a-z][a-z0-9_]{0,23}$/;
|
|
11
|
+
const VALUE = /^[A-Za-z0-9._:/#-]{1,64}$/;
|
|
12
|
+
export const MAX_OUTPUTS = 8;
|
|
13
|
+
export const MAX_SUMMARY = 300;
|
|
14
|
+
|
|
15
|
+
// Keys and values a next run can safely be shown: anything else is dropped.
|
|
16
|
+
export function cleanOutputs(raw: unknown): Record<string, string> {
|
|
17
|
+
const out: Record<string, string> = {};
|
|
18
|
+
if (!raw || typeof raw !== "object") return out;
|
|
19
|
+
for (const [k, v] of Object.entries(raw as Record<string, unknown>)) {
|
|
20
|
+
if (Object.keys(out).length >= MAX_OUTPUTS) break;
|
|
21
|
+
const value = typeof v === "number" ? String(v) : v;
|
|
22
|
+
if (KEY.test(k) && typeof value === "string" && VALUE.test(value))
|
|
23
|
+
out[k] = value;
|
|
24
|
+
}
|
|
25
|
+
return out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function cleanSummary(text: unknown): string {
|
|
29
|
+
if (typeof text !== "string") return "";
|
|
30
|
+
const one = text.replace(/\s+/g, " ").trim();
|
|
31
|
+
return one.length > MAX_SUMMARY ? `${one.slice(0, MAX_SUMMARY - 1)}…` : one;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// The last RESULT line in the text, or null.
|
|
35
|
+
export function parseResult(text: string | null | undefined): RunResult | null {
|
|
36
|
+
if (!text) return null;
|
|
37
|
+
let last: RegExpExecArray | null = null;
|
|
38
|
+
for (const m of text.matchAll(RESULT_LINE)) last = m;
|
|
39
|
+
if (!last) return null;
|
|
40
|
+
const rest = last[2];
|
|
41
|
+
const dash = rest.search(/\s(?:—|--)\s/);
|
|
42
|
+
const facts = dash === -1 ? rest : rest.slice(0, dash);
|
|
43
|
+
const summary =
|
|
44
|
+
dash === -1 ? "" : rest.slice(dash).replace(/^\s(?:—|--)\s/, "");
|
|
45
|
+
const raw: Record<string, string> = {};
|
|
46
|
+
for (const m of facts.matchAll(/(?:^|\s)([a-z][a-z0-9_]*)=(\S+)/g))
|
|
47
|
+
raw[m[1]] = m[2];
|
|
48
|
+
return {
|
|
49
|
+
ok: last[1] === "ok",
|
|
50
|
+
outputs: cleanOutputs(raw),
|
|
51
|
+
summary: cleanSummary(summary),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// One line for the next run's brief and the digest: `build=143 version=1.4.2`.
|
|
56
|
+
export const outputsText = (outputs: Record<string, string> | null): string =>
|
|
57
|
+
Object.entries(outputs ?? {})
|
|
58
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
59
|
+
.join(" ");
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
// Starting a run: claim the one live slot, compose the prompt, start a fresh
|
|
2
|
+
// task or session through the same gates and brakes as any other start.
|
|
3
|
+
|
|
4
|
+
import { randomUUID } from "crypto";
|
|
5
|
+
import { db } from "../db";
|
|
6
|
+
import { getProject } from "../projects";
|
|
7
|
+
import { isRemoteHost } from "../hosts";
|
|
8
|
+
import { createTask } from "../tasks";
|
|
9
|
+
import { startOrQueue } from "../tasks/queue";
|
|
10
|
+
import { launchSession } from "../sessions/launch";
|
|
11
|
+
import { braked, BrakeRefused } from "../orchestrator/brakes";
|
|
12
|
+
import { getOrchestrator } from "../orchestrator/home";
|
|
13
|
+
import { composePrompt } from "./compose";
|
|
14
|
+
import {
|
|
15
|
+
getRecipe,
|
|
16
|
+
getRun,
|
|
17
|
+
lastRevisionId,
|
|
18
|
+
liveRun,
|
|
19
|
+
modelOf,
|
|
20
|
+
toRun,
|
|
21
|
+
} from "./store";
|
|
22
|
+
import type { Recipe, RecipeRun, RunTriggerKind } from "./types";
|
|
23
|
+
|
|
24
|
+
export class AlreadyRunning extends Error {
|
|
25
|
+
constructor(readonly run: RecipeRun) {
|
|
26
|
+
super(`"${run.recipe_id}" already has run #${run.seq} going`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface StartRunOptions {
|
|
31
|
+
trigger: RunTriggerKind;
|
|
32
|
+
// The agent session that asked (the orchestrator, or one using aos).
|
|
33
|
+
actorSessionId?: string | null;
|
|
34
|
+
note?: string | null;
|
|
35
|
+
// A manual run's client key: a repeat gets the same run.
|
|
36
|
+
requestId?: string | null;
|
|
37
|
+
scheduleRunId?: number | null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Started {
|
|
41
|
+
run: RecipeRun;
|
|
42
|
+
queued: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const BRAKED: RunTriggerKind[] = ["tool", "cli", "timer", "catch-up"];
|
|
46
|
+
|
|
47
|
+
function claim(
|
|
48
|
+
recipe: Recipe,
|
|
49
|
+
o: StartRunOptions,
|
|
50
|
+
sessionId: string
|
|
51
|
+
): RecipeRun {
|
|
52
|
+
if (o.requestId) {
|
|
53
|
+
const again = db
|
|
54
|
+
.prepare(`SELECT * FROM recipe_runs WHERE request_id = ?`)
|
|
55
|
+
.get(o.requestId) as Parameters<typeof toRun>[0] | undefined;
|
|
56
|
+
if (again) return toRun(again);
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
return db.transaction(() => {
|
|
60
|
+
const seq = (
|
|
61
|
+
db
|
|
62
|
+
.prepare(
|
|
63
|
+
`SELECT COALESCE(MAX(seq), 0) + 1 AS n FROM recipe_runs WHERE recipe_id = ?`
|
|
64
|
+
)
|
|
65
|
+
.get(recipe.id) as { n: number }
|
|
66
|
+
).n;
|
|
67
|
+
const prompt = composePrompt(recipe, {
|
|
68
|
+
seq,
|
|
69
|
+
trigger: o.trigger,
|
|
70
|
+
note: o.note,
|
|
71
|
+
});
|
|
72
|
+
const id = Number(
|
|
73
|
+
db
|
|
74
|
+
.prepare(
|
|
75
|
+
`INSERT INTO recipe_runs (recipe_id, seq, trigger, triggered_by_session_id, schedule_run_id, request_id,
|
|
76
|
+
note, prompt, revision_id, status, session_id, started_at)
|
|
77
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'starting', ?, datetime('now'))`
|
|
78
|
+
)
|
|
79
|
+
.run(
|
|
80
|
+
recipe.id,
|
|
81
|
+
seq,
|
|
82
|
+
o.trigger,
|
|
83
|
+
o.actorSessionId ?? null,
|
|
84
|
+
o.scheduleRunId ?? null,
|
|
85
|
+
o.requestId ?? null,
|
|
86
|
+
o.note?.trim() || null,
|
|
87
|
+
prompt,
|
|
88
|
+
lastRevisionId(recipe.id),
|
|
89
|
+
sessionId
|
|
90
|
+
).lastInsertRowid
|
|
91
|
+
);
|
|
92
|
+
return getRun(id)!;
|
|
93
|
+
})();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if ((error as { code?: string }).code === "SQLITE_CONSTRAINT_UNIQUE") {
|
|
96
|
+
const live = liveRun(recipe.id);
|
|
97
|
+
if (live) throw new AlreadyRunning(live);
|
|
98
|
+
}
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function settleStart(
|
|
104
|
+
runId: number,
|
|
105
|
+
status: "running" | "queued" | "failed" | "skipped",
|
|
106
|
+
fields: { sessionId?: string; error?: string } = {}
|
|
107
|
+
) {
|
|
108
|
+
db.prepare(
|
|
109
|
+
`UPDATE recipe_runs SET status = ?, session_id = COALESCE(?, session_id), error = ?,
|
|
110
|
+
ended_at = CASE WHEN ? IN ('failed','skipped') THEN datetime('now') ELSE ended_at END
|
|
111
|
+
WHERE id = ? AND status IN ('starting','queued')`
|
|
112
|
+
).run(status, fields.sessionId ?? null, fields.error ?? null, status, runId);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function startRun(
|
|
116
|
+
recipeId: string,
|
|
117
|
+
o: StartRunOptions
|
|
118
|
+
): Promise<Started> {
|
|
119
|
+
const recipe = getRecipe(recipeId);
|
|
120
|
+
if (!recipe || recipe.archived_at) throw new Error("Recipe not found");
|
|
121
|
+
const project = getProject(recipe.project_id);
|
|
122
|
+
if (!project) throw new Error("Its project no longer exists");
|
|
123
|
+
if (isRemoteHost(project.host_id))
|
|
124
|
+
throw new Error(`${project.name} runs on another machine`);
|
|
125
|
+
const ws = recipe.workspace_id;
|
|
126
|
+
const sessionId = randomUUID();
|
|
127
|
+
const run = claim(recipe, o, sessionId);
|
|
128
|
+
// A repeat of a request that already started.
|
|
129
|
+
if (run.session_id !== sessionId)
|
|
130
|
+
return Promise.resolve({ run, queued: false });
|
|
131
|
+
const byOrchestrator = o.trigger === "tool";
|
|
132
|
+
const startedBy = byOrchestrator ? (getOrchestrator(ws)?.id ?? null) : null;
|
|
133
|
+
const name = `${recipe.name} #${run.seq}`;
|
|
134
|
+
const model = modelOf(recipe);
|
|
135
|
+
const gated = <T>(kind: "task" | "session", start: () => Promise<T>) =>
|
|
136
|
+
BRAKED.includes(o.trigger)
|
|
137
|
+
? braked(ws, kind, start, (r) => (r as { id?: string }).id ?? null, {
|
|
138
|
+
spendApproval: o.trigger === "tool" || o.trigger === "cli",
|
|
139
|
+
})
|
|
140
|
+
: start();
|
|
141
|
+
|
|
142
|
+
const go = async (): Promise<Started> => {
|
|
143
|
+
if (recipe.kind === "session") {
|
|
144
|
+
const { session } = await gated("session", () =>
|
|
145
|
+
launchSession({
|
|
146
|
+
id: sessionId,
|
|
147
|
+
projectId: recipe.project_id,
|
|
148
|
+
agentType: "claude",
|
|
149
|
+
prompt: run.prompt,
|
|
150
|
+
name,
|
|
151
|
+
view: recipe.view,
|
|
152
|
+
access: "full",
|
|
153
|
+
model: typeof model === "string" ? model : null,
|
|
154
|
+
startedBy,
|
|
155
|
+
}).then((l) => ({ ...l, id: l.session.id }))
|
|
156
|
+
);
|
|
157
|
+
settleStart(run.id, "running", { sessionId: session.id });
|
|
158
|
+
return { run: getRun(run.id)!, queued: false };
|
|
159
|
+
}
|
|
160
|
+
const out = await startOrQueue(
|
|
161
|
+
{
|
|
162
|
+
projectId: recipe.project_id,
|
|
163
|
+
prompt: run.prompt,
|
|
164
|
+
name,
|
|
165
|
+
view: recipe.view,
|
|
166
|
+
model: model ?? undefined,
|
|
167
|
+
plan: false,
|
|
168
|
+
planReason: "recipe run",
|
|
169
|
+
startedBy,
|
|
170
|
+
originWorkspaceId: byOrchestrator ? ws : undefined,
|
|
171
|
+
},
|
|
172
|
+
() =>
|
|
173
|
+
gated("task", () =>
|
|
174
|
+
createTask({
|
|
175
|
+
id: sessionId,
|
|
176
|
+
projectId: recipe.project_id,
|
|
177
|
+
prompt: run.prompt,
|
|
178
|
+
name,
|
|
179
|
+
view: recipe.view,
|
|
180
|
+
model: model ?? undefined,
|
|
181
|
+
plan: false,
|
|
182
|
+
planReason: "recipe run",
|
|
183
|
+
startedBy,
|
|
184
|
+
})
|
|
185
|
+
)
|
|
186
|
+
);
|
|
187
|
+
if ("queued" in out) {
|
|
188
|
+
settleStart(run.id, "queued", { sessionId: out.queued.id });
|
|
189
|
+
return { run: getRun(run.id)!, queued: true };
|
|
190
|
+
}
|
|
191
|
+
settleStart(run.id, "running", { sessionId: out.started.id });
|
|
192
|
+
return { run: getRun(run.id)!, queued: false };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
return go().catch((error) => {
|
|
196
|
+
const brake = error instanceof BrakeRefused;
|
|
197
|
+
settleStart(run.id, brake ? "skipped" : "failed", {
|
|
198
|
+
error: error instanceof Error ? error.message : String(error),
|
|
199
|
+
});
|
|
200
|
+
throw error;
|
|
201
|
+
});
|
|
202
|
+
}
|