@saadnvd1/agent-os 0.3.556 → 0.3.558
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 -0
- package/app/api/experiments/[id]/route.ts +39 -0
- package/app/api/experiments/route.test.ts +51 -0
- package/app/api/experiments/route.ts +7 -0
- package/app/api/learning-cards/[id]/route.test.ts +62 -0
- package/app/api/learning-cards/[id]/route.ts +36 -0
- package/app/api/recipes/draft/route.ts +6 -1
- package/app/api/recipes/route.ts +3 -0
- package/app/api/sessions/[id]/learning-cards/route.test.ts +35 -0
- package/app/api/sessions/[id]/learning-cards/route.ts +10 -0
- package/components/Chat/ChatPanel.tsx +2 -0
- package/components/LearningCards/LearningCard.test.ts +156 -0
- package/components/LearningCards/LearningCard.tsx +210 -0
- package/components/Settings/ExperimentsSection.test.ts +89 -0
- package/components/Settings/ExperimentsSection.tsx +225 -0
- package/components/Settings/SettingsDialog.test.ts +3 -0
- package/components/Settings/SettingsDialog.tsx +9 -0
- package/components/Settings/commands.ts +9 -0
- package/components/Settings/entry-points.test.ts +1 -0
- package/data/experiments/index.ts +67 -0
- package/data/learning-cards/index.ts +79 -0
- package/lib/chat/store.ts +16 -0
- package/lib/db/migrations.ts +59 -0
- package/lib/done/index.ts +2 -0
- package/lib/experiments/hooks.ts +39 -0
- package/lib/experiments/index.ts +19 -0
- package/lib/experiments/meter.ts +327 -0
- package/lib/experiments/registry.ts +37 -0
- package/lib/experiments/view.ts +19 -0
- package/lib/learning-cards/__fixtures__/db.ts +52 -0
- package/lib/learning-cards/__fixtures__/sessions.ts +63 -0
- package/lib/learning-cards/analyze.ts +228 -0
- package/lib/learning-cards/apply.ts +196 -0
- package/lib/learning-cards/index.ts +29 -0
- package/lib/learning-cards/input.ts +116 -0
- package/lib/learning-cards/pairs.ts +111 -0
- package/lib/learning-cards/signals.ts +98 -0
- package/lib/learning-cards/store.ts +103 -0
- package/lib/learning-cards/types.ts +79 -0
- package/lib/orchestrator/quick.ts +14 -1
- package/lib/recipes/draft.ts +62 -33
- package/lib/recipes/settle.ts +4 -1
- package/lib/settings-store.ts +22 -0
- package/lib/settings-url.ts +1 -0
- package/lib/tasks/finish.ts +2 -0
- package/lib/tasks/session.ts +2 -0
- package/lib/updates/settings.ts +2 -20
- package/package.json +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { getSetting, putSetting } from "../settings-store";
|
|
2
|
+
import { type ExperimentId } from "./registry";
|
|
3
|
+
|
|
4
|
+
type Env = Record<string, string | undefined>;
|
|
5
|
+
|
|
6
|
+
const key = (id: ExperimentId) => `experiments.${id}`;
|
|
7
|
+
|
|
8
|
+
// AGENTOS_EXPERIMENTS=off pins every experiment off, like the update banner.
|
|
9
|
+
export const experimentsLocked = (env: Env = process.env) =>
|
|
10
|
+
env.AGENTOS_EXPERIMENTS === "off";
|
|
11
|
+
|
|
12
|
+
// The one check, on the server. Off by default: no row means off.
|
|
13
|
+
export function experimentOn(id: ExperimentId, env: Env = process.env) {
|
|
14
|
+
if (experimentsLocked(env)) return false;
|
|
15
|
+
return getSetting(key(id)) === "1";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const setExperiment = (id: ExperimentId, on: boolean) =>
|
|
19
|
+
putSetting(key(id), on ? "1" : null);
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What experiments cost and whether they earn it. Every model call an
|
|
3
|
+
* experiment makes goes through experimentRun, which records it here and in
|
|
4
|
+
* the usage ledger; non-model work (scripts, suggestions shown, accepted,
|
|
5
|
+
* dismissed) is counted with countEvent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { db } from "../db";
|
|
9
|
+
import {
|
|
10
|
+
runClaude,
|
|
11
|
+
type ClaudeRun,
|
|
12
|
+
type ClaudeRunner,
|
|
13
|
+
} from "../orchestrator/claude-cli";
|
|
14
|
+
import type { RunUsage } from "../usage/check-runs";
|
|
15
|
+
import { recordLooseCall } from "../usage/calls";
|
|
16
|
+
import { dayKey } from "../usage/aggregate";
|
|
17
|
+
import { experimentOn } from ".";
|
|
18
|
+
import { getExperiment, type ExperimentId } from "./registry";
|
|
19
|
+
|
|
20
|
+
export type ExperimentEvent =
|
|
21
|
+
| "model"
|
|
22
|
+
| "script"
|
|
23
|
+
| "shown"
|
|
24
|
+
| "accepted"
|
|
25
|
+
| "dismissed"
|
|
26
|
+
| "applied"
|
|
27
|
+
| "error";
|
|
28
|
+
|
|
29
|
+
export interface MeterContext {
|
|
30
|
+
trigger: string;
|
|
31
|
+
sessionId?: string | null;
|
|
32
|
+
workspaceId?: string | null;
|
|
33
|
+
detail?: string | null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const clip = (s: string | null | undefined, n = 120) =>
|
|
37
|
+
s ? s.slice(0, n) : null;
|
|
38
|
+
|
|
39
|
+
function insert(
|
|
40
|
+
id: ExperimentId,
|
|
41
|
+
event: ExperimentEvent,
|
|
42
|
+
ctx: MeterContext,
|
|
43
|
+
u?: RunUsage,
|
|
44
|
+
ok = true,
|
|
45
|
+
at = Date.now()
|
|
46
|
+
): number {
|
|
47
|
+
const info = db
|
|
48
|
+
.prepare(
|
|
49
|
+
`INSERT INTO experiment_usage (experiment_id, event, trigger, session_id, workspace_id, model, provider,
|
|
50
|
+
input_tokens, cache_write_tokens, cache_read_tokens, output_tokens, cost_usd, duration_ms, ok, detail, at)
|
|
51
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
52
|
+
)
|
|
53
|
+
.run(
|
|
54
|
+
id,
|
|
55
|
+
event,
|
|
56
|
+
ctx.trigger,
|
|
57
|
+
ctx.sessionId ?? null,
|
|
58
|
+
ctx.workspaceId ?? null,
|
|
59
|
+
u?.model ?? null,
|
|
60
|
+
u ? (u.provider ?? "anthropic") : null,
|
|
61
|
+
u?.inputTokens ?? 0,
|
|
62
|
+
u?.cacheWriteTokens ?? 0,
|
|
63
|
+
u?.cacheReadTokens ?? 0,
|
|
64
|
+
u?.outputTokens ?? 0,
|
|
65
|
+
u?.costUsd ?? 0,
|
|
66
|
+
u?.durationMs ?? null,
|
|
67
|
+
ok ? 1 : 0,
|
|
68
|
+
clip(ctx.detail),
|
|
69
|
+
at
|
|
70
|
+
);
|
|
71
|
+
return Number(info.lastInsertRowid);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function recordModel(id: ExperimentId, ctx: MeterContext, u: RunUsage) {
|
|
75
|
+
try {
|
|
76
|
+
const rowId = insert(id, "model", ctx, u);
|
|
77
|
+
recordLooseCall({
|
|
78
|
+
callKey: `experiment:${rowId}`,
|
|
79
|
+
at: Date.now(),
|
|
80
|
+
sessionId: ctx.sessionId ?? null,
|
|
81
|
+
taskId: null,
|
|
82
|
+
workspaceId: ctx.workspaceId ?? null,
|
|
83
|
+
projectId: null,
|
|
84
|
+
sessionName: getExperiment(id).name,
|
|
85
|
+
role: `experiment:${id}`,
|
|
86
|
+
kind: "helper",
|
|
87
|
+
agentType: null,
|
|
88
|
+
model: u.model,
|
|
89
|
+
provider: u.provider ?? "anthropic",
|
|
90
|
+
input: u.inputTokens,
|
|
91
|
+
cacheRead: u.cacheReadTokens,
|
|
92
|
+
cacheWrite5m: u.cacheWriteTokens,
|
|
93
|
+
cacheWrite1h: 0,
|
|
94
|
+
output: u.outputTokens,
|
|
95
|
+
source: "helper",
|
|
96
|
+
});
|
|
97
|
+
} catch (e) {
|
|
98
|
+
console.error(`[experiments] recording ${id}:`, e);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* The only way an experiment calls a model. Returns null, having started
|
|
104
|
+
* nothing, when the experiment is off. A failed run is recorded too (its
|
|
105
|
+
* usage, or an error row when it reported none) and then thrown.
|
|
106
|
+
*/
|
|
107
|
+
export async function experimentRun(
|
|
108
|
+
id: ExperimentId,
|
|
109
|
+
ctx: MeterContext,
|
|
110
|
+
run: ClaudeRun,
|
|
111
|
+
runner: ClaudeRunner = runClaude
|
|
112
|
+
): Promise<unknown | null> {
|
|
113
|
+
if (!experimentOn(id)) return null;
|
|
114
|
+
let usage: RunUsage | null = null;
|
|
115
|
+
try {
|
|
116
|
+
const out = await runner({
|
|
117
|
+
...run,
|
|
118
|
+
onUsage: (u) => {
|
|
119
|
+
usage = u;
|
|
120
|
+
run.onUsage?.(u);
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
if (usage) recordModel(id, ctx, usage);
|
|
124
|
+
return out;
|
|
125
|
+
} catch (error) {
|
|
126
|
+
try {
|
|
127
|
+
if (usage) recordModel(id, ctx, usage);
|
|
128
|
+
insert(
|
|
129
|
+
id,
|
|
130
|
+
"error",
|
|
131
|
+
{
|
|
132
|
+
...ctx,
|
|
133
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
134
|
+
},
|
|
135
|
+
undefined,
|
|
136
|
+
false
|
|
137
|
+
);
|
|
138
|
+
} catch (e) {
|
|
139
|
+
console.error(`[experiments] recording ${id}:`, e);
|
|
140
|
+
}
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Non-model work. Does nothing when the experiment is off, unless `force`:
|
|
147
|
+
* a person deciding on a card that is already on screen is still counted.
|
|
148
|
+
*/
|
|
149
|
+
export function countEvent(
|
|
150
|
+
id: ExperimentId,
|
|
151
|
+
event: Exclude<ExperimentEvent, "model">,
|
|
152
|
+
ctx: MeterContext,
|
|
153
|
+
force = false
|
|
154
|
+
): void {
|
|
155
|
+
if (!force && !experimentOn(id)) return;
|
|
156
|
+
try {
|
|
157
|
+
insert(id, event, ctx);
|
|
158
|
+
} catch (e) {
|
|
159
|
+
console.error(`[experiments] counting ${id}:`, e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Whether this session already has this event, so a hook runs once.
|
|
164
|
+
export function hasEvent(
|
|
165
|
+
id: ExperimentId,
|
|
166
|
+
sessionId: string,
|
|
167
|
+
event: ExperimentEvent,
|
|
168
|
+
trigger?: string
|
|
169
|
+
): boolean {
|
|
170
|
+
return !!db
|
|
171
|
+
.prepare(
|
|
172
|
+
`SELECT 1 FROM experiment_usage WHERE experiment_id = ? AND session_id = ? AND event = ?
|
|
173
|
+
AND (? IS NULL OR trigger = ?) LIMIT 1`
|
|
174
|
+
)
|
|
175
|
+
.get(id, sessionId, event, trigger ?? null, trigger ?? null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface WindowTotals {
|
|
179
|
+
calls: number;
|
|
180
|
+
tokens: number;
|
|
181
|
+
costUsd: number;
|
|
182
|
+
scripts: number;
|
|
183
|
+
shown: number;
|
|
184
|
+
accepted: number;
|
|
185
|
+
dismissed: number;
|
|
186
|
+
// accepted / (accepted + dismissed); null with no decisions yet.
|
|
187
|
+
acceptRate: number | null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface UsageSummary {
|
|
191
|
+
today: WindowTotals;
|
|
192
|
+
d7: WindowTotals;
|
|
193
|
+
d30: WindowTotals;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const DAY = 86_400_000;
|
|
197
|
+
|
|
198
|
+
export function startOfDay(now: number): number {
|
|
199
|
+
const d = new Date(now);
|
|
200
|
+
d.setHours(0, 0, 0, 0);
|
|
201
|
+
return d.getTime();
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function windowTotals(id: ExperimentId, since: number): WindowTotals {
|
|
205
|
+
const r = db
|
|
206
|
+
.prepare(
|
|
207
|
+
`SELECT
|
|
208
|
+
coalesce(sum(event = 'model'), 0) AS calls,
|
|
209
|
+
coalesce(sum(input_tokens + cache_write_tokens + cache_read_tokens + output_tokens), 0) AS tokens,
|
|
210
|
+
coalesce(sum(cost_usd), 0) AS costUsd,
|
|
211
|
+
coalesce(sum(event = 'script'), 0) AS scripts,
|
|
212
|
+
coalesce(sum(event = 'shown'), 0) AS shown,
|
|
213
|
+
coalesce(sum(event = 'accepted'), 0) AS accepted,
|
|
214
|
+
coalesce(sum(event = 'dismissed'), 0) AS dismissed
|
|
215
|
+
FROM experiment_usage WHERE experiment_id = ? AND at >= ?`
|
|
216
|
+
)
|
|
217
|
+
.get(id, since) as Omit<WindowTotals, "acceptRate">;
|
|
218
|
+
const decided = r.accepted + r.dismissed;
|
|
219
|
+
return { ...r, acceptRate: decided ? r.accepted / decided : null };
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function usageSummary(id: ExperimentId, now = Date.now()): UsageSummary {
|
|
223
|
+
const today = startOfDay(now);
|
|
224
|
+
return {
|
|
225
|
+
today: windowTotals(id, today),
|
|
226
|
+
d7: windowTotals(id, today - 6 * DAY),
|
|
227
|
+
d30: windowTotals(id, today - 29 * DAY),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export interface DayRow {
|
|
232
|
+
day: string;
|
|
233
|
+
calls: number;
|
|
234
|
+
tokens: number;
|
|
235
|
+
costUsd: number;
|
|
236
|
+
accepted: number;
|
|
237
|
+
dismissed: number;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export interface Breakdown {
|
|
241
|
+
key: string;
|
|
242
|
+
calls: number;
|
|
243
|
+
tokens: number;
|
|
244
|
+
costUsd: number;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface RecentEvent {
|
|
248
|
+
at: number;
|
|
249
|
+
event: string;
|
|
250
|
+
trigger: string;
|
|
251
|
+
model: string | null;
|
|
252
|
+
tokens: number;
|
|
253
|
+
costUsd: number;
|
|
254
|
+
ok: boolean;
|
|
255
|
+
detail: string | null;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface UsageDetail {
|
|
259
|
+
days: DayRow[];
|
|
260
|
+
byTrigger: Breakdown[];
|
|
261
|
+
byModel: Breakdown[];
|
|
262
|
+
recent: RecentEvent[];
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export function usageDetail(id: ExperimentId, now = Date.now()): UsageDetail {
|
|
266
|
+
const since = startOfDay(now) - 29 * DAY;
|
|
267
|
+
const rows = db
|
|
268
|
+
.prepare(
|
|
269
|
+
`SELECT at, event, trigger, model, cost_usd AS costUsd,
|
|
270
|
+
input_tokens + cache_write_tokens + cache_read_tokens + output_tokens AS tokens
|
|
271
|
+
FROM experiment_usage WHERE experiment_id = ? AND at >= ?`
|
|
272
|
+
)
|
|
273
|
+
.all(id, since) as {
|
|
274
|
+
at: number;
|
|
275
|
+
event: string;
|
|
276
|
+
trigger: string;
|
|
277
|
+
model: string | null;
|
|
278
|
+
costUsd: number;
|
|
279
|
+
tokens: number;
|
|
280
|
+
}[];
|
|
281
|
+
const days = new Map<string, DayRow>();
|
|
282
|
+
for (let i = 29; i >= 0; i--) {
|
|
283
|
+
const day = dayKey(startOfDay(now) - i * DAY);
|
|
284
|
+
days.set(day, {
|
|
285
|
+
day,
|
|
286
|
+
calls: 0,
|
|
287
|
+
tokens: 0,
|
|
288
|
+
costUsd: 0,
|
|
289
|
+
accepted: 0,
|
|
290
|
+
dismissed: 0,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
const trig = new Map<string, Breakdown>();
|
|
294
|
+
const mod = new Map<string, Breakdown>();
|
|
295
|
+
const add = (m: Map<string, Breakdown>, key: string, r: (typeof rows)[0]) => {
|
|
296
|
+
const b = m.get(key) ?? { key, calls: 0, tokens: 0, costUsd: 0 };
|
|
297
|
+
b.calls += r.event === "model" ? 1 : 0;
|
|
298
|
+
b.tokens += r.tokens;
|
|
299
|
+
b.costUsd += r.costUsd;
|
|
300
|
+
m.set(key, b);
|
|
301
|
+
};
|
|
302
|
+
for (const r of rows) {
|
|
303
|
+
const d = days.get(dayKey(r.at));
|
|
304
|
+
if (d) {
|
|
305
|
+
d.calls += r.event === "model" ? 1 : 0;
|
|
306
|
+
d.tokens += r.tokens;
|
|
307
|
+
d.costUsd += r.costUsd;
|
|
308
|
+
d.accepted += r.event === "accepted" ? 1 : 0;
|
|
309
|
+
d.dismissed += r.event === "dismissed" ? 1 : 0;
|
|
310
|
+
}
|
|
311
|
+
add(trig, r.trigger, r);
|
|
312
|
+
if (r.event === "model") add(mod, r.model ?? "unrecorded", r);
|
|
313
|
+
}
|
|
314
|
+
const recent = db
|
|
315
|
+
.prepare(
|
|
316
|
+
`SELECT at, event, trigger, model, input_tokens + cache_write_tokens + cache_read_tokens + output_tokens AS tokens,
|
|
317
|
+
cost_usd AS costUsd, ok, detail
|
|
318
|
+
FROM experiment_usage WHERE experiment_id = ? ORDER BY at DESC, id DESC LIMIT 50`
|
|
319
|
+
)
|
|
320
|
+
.all(id) as (Omit<RecentEvent, "ok"> & { ok: number })[];
|
|
321
|
+
return {
|
|
322
|
+
days: [...days.values()],
|
|
323
|
+
byTrigger: [...trig.values()],
|
|
324
|
+
byModel: [...mod.values()],
|
|
325
|
+
recent: recent.map((r) => ({ ...r, ok: !!r.ok })),
|
|
326
|
+
};
|
|
327
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Every experiment AgentOS knows about. All are off until someone turns them
|
|
2
|
+
// on. To add one: an entry here, `experimentOn(id)` where it does its work,
|
|
3
|
+
// and `experimentRun(id, ...)` for any model call it makes.
|
|
4
|
+
|
|
5
|
+
export type ExperimentStatus = "experimental" | "beta";
|
|
6
|
+
|
|
7
|
+
export interface Experiment {
|
|
8
|
+
// Kebab-case and stable: it names the setting and keys the usage rows.
|
|
9
|
+
id: string;
|
|
10
|
+
name: string;
|
|
11
|
+
description: string;
|
|
12
|
+
// Who owns it: "sessions", "tasks", "orchestrator"...
|
|
13
|
+
area: string;
|
|
14
|
+
status: ExperimentStatus;
|
|
15
|
+
// It shows suggestions, so an accept rate means something for it.
|
|
16
|
+
suggests?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const EXPERIMENTS = [
|
|
20
|
+
{
|
|
21
|
+
id: "learning-cards",
|
|
22
|
+
name: "Learning cards",
|
|
23
|
+
description:
|
|
24
|
+
"When a session ends, suggest saving it as a recipe or keeping a lesson, in the one place it belongs.",
|
|
25
|
+
area: "sessions",
|
|
26
|
+
status: "experimental",
|
|
27
|
+
suggests: true,
|
|
28
|
+
},
|
|
29
|
+
] as const satisfies readonly Experiment[];
|
|
30
|
+
|
|
31
|
+
export type ExperimentId = (typeof EXPERIMENTS)[number]["id"];
|
|
32
|
+
|
|
33
|
+
export const isExperimentId = (v: unknown): v is ExperimentId =>
|
|
34
|
+
EXPERIMENTS.some((e) => e.id === v);
|
|
35
|
+
|
|
36
|
+
export const getExperiment = (id: ExperimentId): Experiment =>
|
|
37
|
+
EXPERIMENTS.find((e) => e.id === id)!;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { experimentOn, experimentsLocked } from ".";
|
|
2
|
+
import { EXPERIMENTS, type Experiment, type ExperimentId } from "./registry";
|
|
3
|
+
import { usageSummary, type UsageSummary } from "./meter";
|
|
4
|
+
|
|
5
|
+
export interface ExperimentView extends Experiment {
|
|
6
|
+
on: boolean;
|
|
7
|
+
usage: UsageSummary;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const experimentView = (id: ExperimentId): ExperimentView => ({
|
|
11
|
+
...EXPERIMENTS.find((e) => e.id === id)!,
|
|
12
|
+
on: experimentOn(id),
|
|
13
|
+
usage: usageSummary(id),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export const experimentsView = () => ({
|
|
17
|
+
locked: experimentsLocked(),
|
|
18
|
+
experiments: EXPERIMENTS.map((e) => experimentView(e.id)),
|
|
19
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { randomUUID } from "crypto";
|
|
2
|
+
import { db } from "../../db";
|
|
3
|
+
import { saveItem } from "../../chat/store";
|
|
4
|
+
import { makeSession, workspaceWithProject } from "../../recipes/test-fixtures";
|
|
5
|
+
import type { Step } from "../types";
|
|
6
|
+
|
|
7
|
+
// A finished session in a fresh workspace, its steps written as chat items.
|
|
8
|
+
export function seedSession(
|
|
9
|
+
o: {
|
|
10
|
+
prompt?: string;
|
|
11
|
+
steps?: Step[];
|
|
12
|
+
final?: string;
|
|
13
|
+
orchestrator?: boolean;
|
|
14
|
+
fields?: Record<string, string | number | null>;
|
|
15
|
+
} = {}
|
|
16
|
+
) {
|
|
17
|
+
const { ws, project } = workspaceWithProject();
|
|
18
|
+
const id = makeSession({
|
|
19
|
+
project_id: project.id,
|
|
20
|
+
task_prompt: o.prompt ?? "Send the latest build to the beta testers",
|
|
21
|
+
name: "Beta build",
|
|
22
|
+
...o.fields,
|
|
23
|
+
});
|
|
24
|
+
let n = 0;
|
|
25
|
+
const at = () => ({ id: `i${n++}`, createdAt: Date.now() + n });
|
|
26
|
+
for (const s of o.steps ?? [])
|
|
27
|
+
saveItem(
|
|
28
|
+
id,
|
|
29
|
+
s.kind === "user"
|
|
30
|
+
? { ...at(), kind: "user", text: s.text }
|
|
31
|
+
: {
|
|
32
|
+
...at(),
|
|
33
|
+
kind: "tool",
|
|
34
|
+
name: s.name,
|
|
35
|
+
title: s.name,
|
|
36
|
+
input: s.command ? { command: s.command } : { file_path: s.path },
|
|
37
|
+
status: s.failed ? "error" : "done",
|
|
38
|
+
output: s.output,
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
if (o.final) saveItem(id, { ...at(), kind: "assistant", text: o.final });
|
|
42
|
+
if (o.orchestrator)
|
|
43
|
+
makeSession({ role: "orchestrator", workspace_id: ws.id });
|
|
44
|
+
return { id, ws, project };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const clearCards = () =>
|
|
48
|
+
db.exec(
|
|
49
|
+
`DELETE FROM learning_cards; DELETE FROM experiment_usage; DELETE FROM orchestrator_notes`
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const uid = () => randomUUID();
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { SessionInput, Step } from "../types";
|
|
2
|
+
|
|
3
|
+
export const bash = (
|
|
4
|
+
command: string,
|
|
5
|
+
o: { ok?: boolean; output?: string } = {}
|
|
6
|
+
): Step => ({
|
|
7
|
+
kind: "tool",
|
|
8
|
+
name: "Bash",
|
|
9
|
+
ok: o.ok ?? true,
|
|
10
|
+
failed: !(o.ok ?? true),
|
|
11
|
+
command,
|
|
12
|
+
path: "",
|
|
13
|
+
input: command,
|
|
14
|
+
output: o.output ?? "",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export const edit = (path: string, change = "x"): Step => ({
|
|
18
|
+
kind: "tool",
|
|
19
|
+
name: "Edit",
|
|
20
|
+
ok: true,
|
|
21
|
+
failed: false,
|
|
22
|
+
command: "",
|
|
23
|
+
path,
|
|
24
|
+
input: `${path} ${change}`,
|
|
25
|
+
output: "",
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const said = (text: string, byPerson = true): Step => ({
|
|
29
|
+
kind: "user",
|
|
30
|
+
text,
|
|
31
|
+
byPerson,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const input = (o: Partial<SessionInput> = {}): SessionInput => ({
|
|
35
|
+
id: "s1",
|
|
36
|
+
name: "A session",
|
|
37
|
+
workspaceId: "w1",
|
|
38
|
+
projectId: "p1",
|
|
39
|
+
skip: null,
|
|
40
|
+
brief: "Refactor the parser module",
|
|
41
|
+
final: "Done. The parser is split in two files.",
|
|
42
|
+
steps: [],
|
|
43
|
+
reviews: [],
|
|
44
|
+
inRepo: true,
|
|
45
|
+
run: null,
|
|
46
|
+
previousRun: null,
|
|
47
|
+
recentBriefs: [],
|
|
48
|
+
...o,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Sends a build to testers: external CLIs, a report of facts, no repo.
|
|
52
|
+
export const releaseToTesters = (): SessionInput =>
|
|
53
|
+
input({
|
|
54
|
+
brief: "Send the latest build to the beta testers",
|
|
55
|
+
final:
|
|
56
|
+
"Build 57 (1.4.2) sent to 3 testers. Invite id 6a1f2c9d, group Beta.",
|
|
57
|
+
inRepo: false,
|
|
58
|
+
steps: [
|
|
59
|
+
said("Send the latest build to the beta testers"),
|
|
60
|
+
bash("asc builds list --app 123"),
|
|
61
|
+
bash("cd app && eas submit --platform ios"),
|
|
62
|
+
],
|
|
63
|
+
});
|