eve-hub 0.4.2 → 0.5.0
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/artifacts/core.mjs +41 -190
- package/artifacts/types/change-explainer.mjs +36 -60
- package/artifacts/types/explainer-assets.gen.mjs +3 -0
- package/package.json +1 -3
- package/roster-model.mjs +0 -9
- package/schedules/classify.d.ts +2 -0
- package/schedules/classify.js +20 -0
- package/schedules/dash-classify.mjs +119 -84
- package/schedules/dash-rules.mjs +1 -1
- package/schedules/dash.js +3 -13
- package/tools/artifact_list.js +8 -10
- package/tools/artifact_publish.js +9 -13
- package/tools/artifact_status.js +8 -10
- package/artifacts/state.d.ts +0 -1
- package/artifacts/state.js +0 -5
- package/model-credential.mjs +0 -44
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "Apache-2.0",
|
|
5
5
|
"description": "Bot-side code shared by every hub agent: registry tools, the roster hook, the eve channel, the host sandbox and browser, and model helpers.",
|
|
6
6
|
"type": "module",
|
|
@@ -28,8 +28,6 @@
|
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"eve": "*",
|
|
31
|
-
"ai": "^7.0.105",
|
|
32
|
-
"@ai-sdk/gateway": "^4.0.100",
|
|
33
31
|
"zod": "4.5.4",
|
|
34
32
|
"@vercel/connect": "^2.4.1",
|
|
35
33
|
"@vercel/oidc": "4.0.0"
|
package/roster-model.mjs
CHANGED
|
@@ -17,15 +17,6 @@ import { CONTEXT_WINDOW, selectModel } from "./bot-model.mjs";
|
|
|
17
17
|
import { DEFAULT_SUBAGENT_CAP } from "./subagent-cap.mjs";
|
|
18
18
|
import { sessionLimits } from "./session-limits.mjs";
|
|
19
19
|
import { findWorkspaceRoot, loadWorkspace, parseWorkspace } from "./workspace.mjs";
|
|
20
|
-
import { createGateway } from "ai";
|
|
21
|
-
import { hubGatewayFetch } from "./model-credential.mjs";
|
|
22
|
-
import { registryBase } from "./registry-fetch.mjs";
|
|
23
|
-
|
|
24
|
-
// Hosted, a bot's model ids resolve through the owner's sign-in from the hub
|
|
25
|
-
// (model-credential.mjs); elsewhere through the default gateway.
|
|
26
|
-
if (process.env.HUB_TOKEN)
|
|
27
|
-
globalThis.AI_SDK_DEFAULT_PROVIDER = createGateway({ apiKey: "hub-owner-sign-in", fetch: hubGatewayFetch(`${registryBase().replace(/\/$/, "")}/api/model-credential`) });
|
|
28
|
-
|
|
29
20
|
const real = process.env.ROSTER_MODELS !== "mock";
|
|
30
21
|
|
|
31
22
|
export function createRosterModel({ agents, subagentCap }) {
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// main's classify schedule: gives each idle, non-deferred thread that moved
|
|
2
|
+
// on a Dash entry (importance, delay) with openai/gpt-6-luna-decisions
|
|
3
|
+
// through eve's `evaluate` (eve's gateway credentials). The pass: runClassify
|
|
4
|
+
// in ./dash-classify.mjs; a thread whose basis hasn't changed costs nothing.
|
|
5
|
+
// The hub fires it off-cron as soon as a bot's turn ends (registry/server.mjs,
|
|
6
|
+
// schedules.runSoon), so a question shows on the sidebar within seconds; the
|
|
7
|
+
// cron catches what that missed (a failed call, a hub restart). classifyPass
|
|
8
|
+
// keeps one pass at a time in this process, whatever the hub's dispatches;
|
|
9
|
+
// the pass's path filing (`filing`) runs on after it, unawaited, so neither
|
|
10
|
+
// this dispatch nor the next pass waits on it.
|
|
11
|
+
import { defineSchedule } from "eve/schedules";
|
|
12
|
+
import { evaluate } from "eve/ai";
|
|
13
|
+
import { createHub } from "./hub.mjs";
|
|
14
|
+
import { classifyPass, modelClassify, runClassify } from "./dash-classify.mjs";
|
|
15
|
+
export default defineSchedule({
|
|
16
|
+
cron: "*/10 * * * *",
|
|
17
|
+
async run() {
|
|
18
|
+
await classifyPass(() => runClassify(createHub(), { classify: modelClassify({ evaluate }) }));
|
|
19
|
+
},
|
|
20
|
+
});
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
//
|
|
1
|
+
// main's classify schedule (schedules/classify.ts): gives
|
|
2
2
|
// every idle thread a Dash entry (registry/dash.mjs: importance, delay) and
|
|
3
3
|
// writes them back through POST /api/dash/entries.
|
|
4
4
|
//
|
|
5
5
|
// targets open threads (not DMs, schedule threads or archived) where no
|
|
6
6
|
// bot is working, that aren't deferred, and whose basis changed
|
|
7
7
|
// since their entry was written. An unchanged hub costs nothing.
|
|
8
|
-
// classify (
|
|
8
|
+
// classify (target, snap) -> { threadId, importance, delay } or null, all
|
|
9
|
+
// targets at once, each written as soon as it's decided.
|
|
9
10
|
// modelClassify: one evaluation per thread on the AI Gateway
|
|
10
11
|
// (openai/gpt-6-luna-decisions), two choice questions. Its
|
|
11
12
|
// answer is the classification. No model, a failed call or an
|
|
@@ -66,8 +67,11 @@ export function pathQuestion(paths) {
|
|
|
66
67
|
return { type: "choice", instructions: "Which path covers this thread's work? Paths group threads by feature or effort at any depth. A bug, polish or follow-up belongs to its area's existing path. Prefer an existing path where it fits; choose new only when none fits.", criteria };
|
|
67
68
|
}
|
|
68
69
|
|
|
69
|
-
// The threads to (re)classify this
|
|
70
|
-
// (no entry, or the thread moved on) first,
|
|
70
|
+
// The threads to (re)classify this pass, at most `max`: those the Dash needs
|
|
71
|
+
// (no entry, or the thread moved on) first, newest change first, so the
|
|
72
|
+
// thread whose turn just ended (what fired this pass) is selected under the
|
|
73
|
+
// cap whatever the backlog, and a thread whose answers keep failing sinks behind
|
|
74
|
+
// fresh ones instead of holding the front; later passes drain the rest. Then those
|
|
71
75
|
// asked only for a path, least recently classified first, so threads still
|
|
72
76
|
// waiting on main's filing take turns and never crowd out the Dash.
|
|
73
77
|
export function classifyTargets(snap, { max = CLASSIFY_MAX } = {}) {
|
|
@@ -87,7 +91,7 @@ export function classifyTargets(snap, { max = CLASSIFY_MAX } = {}) {
|
|
|
87
91
|
(due ? out : pathOnly).push({ id: t.id, title: t.title, tags: t.tags ?? {}, status: t.status ?? {}, last, brief: threadBrief(t, snap), basis, entry: e ?? null, paths: path ? pathsInProject(t, snap) : null });
|
|
88
92
|
}
|
|
89
93
|
const at = (x) => String(x ?? "");
|
|
90
|
-
out.sort((a, b) => at(
|
|
94
|
+
out.sort((a, b) => at(b.last.at).localeCompare(at(a.last.at)));
|
|
91
95
|
pathOnly.sort((a, b) => at(a.entry?.updatedAt).localeCompare(at(b.entry?.updatedAt)) || at(a.last.at).localeCompare(at(b.last.at)));
|
|
92
96
|
return [...out, ...pathOnly].slice(0, max);
|
|
93
97
|
}
|
|
@@ -150,20 +154,16 @@ export async function decideThread(t, snap, { evaluate, model = DECISION_MODEL,
|
|
|
150
154
|
return out;
|
|
151
155
|
}
|
|
152
156
|
|
|
153
|
-
// The schedule's classifier
|
|
154
|
-
//
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
};
|
|
164
|
-
const out = [];
|
|
165
|
-
for (let i = 0; i < targets.length; i += concurrency) out.push(...(await Promise.all(targets.slice(i, i + concurrency).map(one))));
|
|
166
|
-
return out.filter(Boolean);
|
|
157
|
+
// The schedule's classifier, one thread: the model's answer as is
|
|
158
|
+
// ({ threadId, importance, delay, path? }), or null when there's no
|
|
159
|
+
// `evaluate`, the call failed or the answer is invalid (no entry for it).
|
|
160
|
+
/** @param {{ evaluate?: (options: any) => Promise<any>, log?: any, model?: string, timeoutMs?: number }} [options] */
|
|
161
|
+
export function modelClassify({ evaluate, log = console, ...opts } = {}) {
|
|
162
|
+
return async (t, snap) => {
|
|
163
|
+
if (!evaluate) return null;
|
|
164
|
+
const a = await decideThread(t, snap, { evaluate, ...opts }).catch((err) => (log?.warn?.(`dash: decide ${t.id} failed, skipped: ${err?.message ?? err}`), undefined));
|
|
165
|
+
if (a === null) log?.warn?.(`dash: decide ${t.id}: invalid answer, skipped`);
|
|
166
|
+
return a ? { threadId: t.id, ...a } : null;
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
|
|
@@ -203,82 +203,117 @@ export function newPathNotice(threads, snap) {
|
|
|
203
203
|
// not. The model's verdict on the brief decides; nothing here reads the
|
|
204
204
|
// messages. (A pending request is posted by the rule scan, dash-rules.mjs.)
|
|
205
205
|
const WAITS = { blocking: "waiting on your decision", land: "waiting on your sign-off" };
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
// The notice for `entries`, or null: committed with them in one write
|
|
219
|
-
// (hub.dashEntries), so a verdict and its notice are kept together or not
|
|
220
|
-
// at all, and a failed write is judged afresh next pass against the entry
|
|
221
|
-
// as it still is.
|
|
222
|
-
export function asksNotice(entries, byId) {
|
|
223
|
-
const asks = askLines(entries, byId);
|
|
224
|
-
if (!asks.length) return null;
|
|
225
|
-
const lines = asks.map((a) => a.line);
|
|
226
|
-
const key = `asks-${createHash("sha256").update(asks.map(({ entry: e }) => `${e.threadId}:${e.delay}:${e.basis}`).sort().join(",")).digest("hex").slice(0, 32)}`;
|
|
227
|
-
return { schedule: "dash", text: `@main dash: ${lines.length === 1 ? lines[0] : `${lines.length} changes\n${lines.map((l) => `- ${l}`).join("\n")}`}`, clientId: key };
|
|
206
|
+
// The notice for `e`, the new entry of target `t`, or null: committed with
|
|
207
|
+
// it in one write (hub.dashEntries), so a verdict and its notice are kept
|
|
208
|
+
// together or not at all, and a failed write is judged afresh next pass
|
|
209
|
+
// against the entry as it still is.
|
|
210
|
+
export function asksNotice(e, t) {
|
|
211
|
+
const was = t.entry?.delay;
|
|
212
|
+
if (was === e.delay) return null;
|
|
213
|
+
const link = `[${String(t.title ?? t.id).replace(/[[\]]/g, "")}](${t.id})`;
|
|
214
|
+
const line = WAITS[e.delay] ? `asks you: ${link}, ${WAITS[e.delay]}` : WAITS[was] ? `no longer asking you: ${link}` : null;
|
|
215
|
+
if (!line) return null;
|
|
216
|
+
const key = `asks-${createHash("sha256").update(`${e.threadId}:${e.delay}:${e.basis}`).digest("hex").slice(0, 32)}`;
|
|
217
|
+
return { schedule: "dash", text: `@main dash: ${line}`, clientId: key };
|
|
228
218
|
}
|
|
229
219
|
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
220
|
+
// The `new` answers of a pass go to main in one notice per project (deduped
|
|
221
|
+
// by the set of threads): not just these, every unfiled thread in their
|
|
222
|
+
// projects, so main groups a project once instead of 30 at a time. Which
|
|
223
|
+
// threads are unfiled is read from the hub now, after this pass's filing,
|
|
224
|
+
// not from the pass's snapshot: another pass may have filed some meanwhile.
|
|
225
|
+
// A failed read or post is logged; the threads are still unfiled, so they're
|
|
226
|
+
// asked again.
|
|
227
|
+
async function noticeNewPaths(hub, fresh, log) {
|
|
228
|
+
if (!fresh.length || !hub.post) return;
|
|
229
|
+
let snap;
|
|
230
|
+
try {
|
|
231
|
+
snap = await hub.snapshot();
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return log?.warn?.(`dash: new-path notice skipped, no snapshot: ${err?.message ?? err}`);
|
|
240
234
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
const threads = all.filter((t) => (t.tags?.project ?? "") === project);
|
|
247
|
-
const key = `paths-new-${createHash("sha256").update(threads.map((t) => t.id).sort().join(",")).digest("hex").slice(0, 32)}`;
|
|
248
|
-
await hub.post("dash", newPathNotice(threads, snap), key).catch((err) => log?.warn?.(`dash: new-path notice failed: ${err?.message ?? err}`));
|
|
249
|
-
}
|
|
235
|
+
const all = unfiledIn(snap, new Set(fresh.map((t) => t.tags?.project ?? "")));
|
|
236
|
+
for (const project of new Set(all.map((t) => t.tags?.project ?? ""))) {
|
|
237
|
+
const threads = all.filter((t) => (t.tags?.project ?? "") === project);
|
|
238
|
+
const key = `paths-new-${createHash("sha256").update(threads.map((t) => t.id).sort().join(",")).digest("hex").slice(0, 32)}`;
|
|
239
|
+
await hub.post("dash", newPathNotice(threads, snap), key).catch((err) => log?.warn?.(`dash: new-path notice failed: ${err?.message ?? err}`));
|
|
250
240
|
}
|
|
251
|
-
return fresh.length;
|
|
252
241
|
}
|
|
253
242
|
|
|
254
|
-
// One classifier pass.
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
243
|
+
// One classifier pass. Every target is decided at once, and each is written
|
|
244
|
+
// (with its "asks you" notice) the moment its answer is in, so a thread's
|
|
245
|
+
// entry never waits on another thread's call: the pass takes as long as its
|
|
246
|
+
// slowest call (decideThread's timeoutMs), whatever the backlog, and the
|
|
247
|
+
// thread whose turn just ended shows within its own call. Path picks are
|
|
248
|
+
// filed after the pass (`filing`, see filePaths), outside it. Each entry
|
|
249
|
+
// carries the basis its target was classified from, so a thread that moved
|
|
250
|
+
// on meanwhile is looked at again next pass, and clears any old reason line.
|
|
251
|
+
// Importance a human set (entry.importanceBy) is never sent. classifiedAt is
|
|
252
|
+
// stamped when there was nothing to do or every target was written; a
|
|
253
|
+
// skipped target or a failed write leaves it, so "never classified" and
|
|
254
|
+
// "checked" stay distinct. Returns { written, filing }: `filing` settles when
|
|
255
|
+
// the pass's path maintenance has (the schedule doesn't wait for it).
|
|
256
|
+
/** @param {any} hub @param {{ classify?: (target: any, snap: any) => Promise<any>, max?: number, snap?: any, log?: any }} [opts] */
|
|
257
|
+
export async function runClassify(hub, { classify = async () => null, max = CLASSIFY_MAX, snap = null, log = console } = {}) {
|
|
262
258
|
const s = snap ?? (await hub.snapshot());
|
|
263
259
|
const targets = classifyTargets(s, { max });
|
|
264
|
-
const byId = new Map(targets.map((t) => [t.id, t]));
|
|
265
260
|
const humans = new Set((s?.humans ?? []).map((h) => h.name));
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
const
|
|
269
|
-
const
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const notice = asksNotice(
|
|
261
|
+
if (targets.length) log?.log?.(`dash: classifying ${targets.length} thread${targets.length === 1 ? "" : "s"}: ${targets.map((t) => t.id).join(", ")}`);
|
|
262
|
+
const picks = [];
|
|
263
|
+
const one = async (t) => {
|
|
264
|
+
const d = await classify(t, s);
|
|
265
|
+
if (!d) return 0;
|
|
266
|
+
if (d.path) picks.push([t, d.path]);
|
|
267
|
+
const own = humans.has(t.entry?.importanceBy);
|
|
268
|
+
const entry = { threadId: t.id, ...(own || d.importance === undefined ? {} : { importance: d.importance }), delay: d.delay, reason: null, basis: t.basis };
|
|
269
|
+
const notice = asksNotice(entry, t);
|
|
275
270
|
try {
|
|
276
|
-
await (notice ? hub.dashEntries(
|
|
271
|
+
await (notice ? hub.dashEntries([entry], [], notice) : hub.dashEntries([entry]));
|
|
272
|
+
return 1;
|
|
277
273
|
} catch (err) {
|
|
278
|
-
log?.warn?.(`dash: classify write failed: ${err?.message ?? err}`);
|
|
279
|
-
return
|
|
274
|
+
log?.warn?.(`dash: classify write for ${t.id} failed: ${err?.message ?? err}`);
|
|
275
|
+
return 0;
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
const written = (await Promise.all(targets.map(one))).reduce((a, b) => a + b, 0);
|
|
279
|
+
if (written === targets.length) await hub.dashClassified?.().catch((err) => log?.warn?.(`dash: classifiedAt not stamped: ${err?.message ?? err}`));
|
|
280
|
+
return { written, filing: filePaths(hub, picks, log) };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Path maintenance after a pass, never part of it: the pass is over when its
|
|
284
|
+
// entries are written and classifiedAt stamped, so the next one (a fresh
|
|
285
|
+
// question) never waits on a filing request. Files each path pick, then sends the pass's `new` answers.
|
|
286
|
+
// Never rejects; a failure is logged and the thread, still unfiled, is
|
|
287
|
+
// asked again. A pass that starts meanwhile may ask the same thread again:
|
|
288
|
+
// a second filing is refused (already filed), and the notice reads the hub's
|
|
289
|
+
// state when it's built, so it never names a thread filed meanwhile.
|
|
290
|
+
async function filePaths(hub, picks, log) {
|
|
291
|
+
await Promise.all(picks.filter(([, path]) => path !== NEW_PATH).map(([t, path]) => hub.fileThread?.(t.id, path).catch((err) => log?.warn?.(`dash: file ${t.id} into ${path} failed: ${err?.message ?? err}`))));
|
|
292
|
+
await noticeNewPaths(hub, picks.filter(([, path]) => path === NEW_PATH).map(([t]) => t), log);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// One pass at a time in this bot process, and none lost: the schedule runs
|
|
296
|
+
// its pass through this. The hub can't know when a pass ends (a dispatch it
|
|
297
|
+
// gave up on keeps running in eve; aborting the request doesn't stop it),
|
|
298
|
+
// so the process that runs passes is the one that serializes them. A
|
|
299
|
+
// dispatch that arrives during a pass returns at once and asks that pass for
|
|
300
|
+
// one more round when it ends, which reads a fresh snapshot. The flag lives
|
|
301
|
+
// on globalThis, which eve keeps across dispatches, and goes with the process.
|
|
302
|
+
const RUNNING = Symbol.for("hub.dash.classify.running");
|
|
303
|
+
/** @param {() => Promise<unknown>} pass */
|
|
304
|
+
export async function classifyPass(pass) {
|
|
305
|
+
const g = /** @type {any} */ (globalThis);
|
|
306
|
+
if (g[RUNNING]) {
|
|
307
|
+
g[RUNNING].again = true;
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const run = (g[RUNNING] = { again: true });
|
|
311
|
+
try {
|
|
312
|
+
while (run.again) {
|
|
313
|
+
run.again = false;
|
|
314
|
+
await pass();
|
|
280
315
|
}
|
|
316
|
+
} finally {
|
|
317
|
+
g[RUNNING] = undefined;
|
|
281
318
|
}
|
|
282
|
-
if (complete) await hub.dashClassified?.().catch((err) => log?.warn?.(`dash: classifiedAt not stamped: ${err?.message ?? err}`));
|
|
283
|
-
return { written: entries.length };
|
|
284
319
|
}
|
package/schedules/dash-rules.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { said, lastSaid } from "./thread-brief.mjs";
|
|
|
24
24
|
// approval a bot asked for). { threadId: "w:" + the pending request
|
|
25
25
|
// ids }. A bot's `waiting` status alone isn't one: it also means
|
|
26
26
|
// waiting on its own tasks. A thread that waits on the human in what was said is
|
|
27
|
-
// the classifier's call (the Dash model's verdict, see
|
|
27
|
+
// the classifier's call (the Dash model's verdict, see asksNotice in
|
|
28
28
|
// ./dash-classify.mjs), not this scan's. While a bot works there the
|
|
29
29
|
// previous entry is kept, so a request made meanwhile posts once the
|
|
30
30
|
// thread settles.
|
package/schedules/dash.js
CHANGED
|
@@ -9,24 +9,14 @@
|
|
|
9
9
|
// with a note of what was skipped. Every scan whose PRs came back stamps the
|
|
10
10
|
// Dash's checkedAt, so the sidebar shows "stale" while GitHub is unreachable
|
|
11
11
|
// (gh logged out, offline). The tick: runDash in ./dash-run.mjs; the
|
|
12
|
-
// decisions: dashScan in ./dash-rules.mjs.
|
|
13
|
-
// (./
|
|
14
|
-
// Dash entry (importance, delay) with openai/gpt-6-luna-decisions through
|
|
15
|
-
// eve's `evaluate` (eve's gateway credentials); its failure never fails the tick.
|
|
12
|
+
// decisions: dashScan in ./dash-rules.mjs. The threads' Dash entries are
|
|
13
|
+
// main's classify schedule (./classify.ts).
|
|
16
14
|
import { defineSchedule } from "eve/schedules";
|
|
17
15
|
import { createHub } from "./hub.mjs";
|
|
18
16
|
import { runDash } from "./dash-run.mjs";
|
|
19
|
-
import { evaluate } from "eve/ai";
|
|
20
|
-
import { modelClassify, runClassify } from "./dash-classify.mjs";
|
|
21
17
|
export default defineSchedule({
|
|
22
18
|
cron: "*/10 * * * *",
|
|
23
19
|
async run() {
|
|
24
|
-
|
|
25
|
-
try {
|
|
26
|
-
await runDash(hub);
|
|
27
|
-
}
|
|
28
|
-
finally {
|
|
29
|
-
await runClassify(hub, { classify: modelClassify({ evaluate }) }).catch((err) => console.warn(`dash: classify skipped: ${err?.message ?? err}`));
|
|
30
|
-
}
|
|
20
|
+
await runDash(createHub());
|
|
31
21
|
},
|
|
32
22
|
});
|
package/tools/artifact_list.js
CHANGED
|
@@ -1,19 +1,17 @@
|
|
|
1
|
-
// The `artifact_list` tool: the artifacts this session
|
|
2
|
-
//
|
|
1
|
+
// The `artifact_list` tool: the artifacts this session published, or, with
|
|
2
|
+
// all: true, every artifact the hub knows and who owns each.
|
|
3
3
|
import { defineTool } from "eve/tools";
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { registryFetch } from "../registry-fetch.mjs";
|
|
6
|
-
import { artifacts } from "../artifacts/state.js";
|
|
7
6
|
const brief = (a) => ({ id: a.id, version: a.version, status: a.status, title: a.card?.title, alias: a.alias, updatedAt: a.updatedAt, ...(a.owner ? { owner: a.owner } : {}) });
|
|
8
7
|
export default defineTool({
|
|
9
8
|
description: "List the artifacts you own (id, version, status, stable URL). all: true lists every artifact in the workspace with its owner.",
|
|
10
9
|
inputSchema: z.object({ all: z.boolean().optional().describe("Every artifact the hub knows, not just yours.") }),
|
|
11
|
-
async execute({ all }) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return { artifacts: ((await res.json()).artifacts ?? []).map(brief) };
|
|
10
|
+
async execute({ all }, ctx) {
|
|
11
|
+
const res = await registryFetch(`/api/artifacts`);
|
|
12
|
+
if (!res.ok)
|
|
13
|
+
throw new Error(`GET /api/artifacts: ${(await res.json().catch(() => ({}))).error ?? res.status}`);
|
|
14
|
+
const every = ((await res.json()).artifacts ?? []);
|
|
15
|
+
return { artifacts: (all ? every : every.filter((a) => a.owner?.sessionId === ctx.session.id)).map(brief) };
|
|
18
16
|
},
|
|
19
17
|
});
|
|
@@ -1,23 +1,19 @@
|
|
|
1
|
-
// The `artifact_publish` tool: builds
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// The `artifact_publish` tool: builds the next version of an artifact in this
|
|
2
|
+
// session's sandbox and has the hub deploy it and move its stable link
|
|
3
|
+
// (artifacts/core.mjs). The result carries the hub's record; the hub puts its
|
|
4
|
+
// card on this turn's reply.
|
|
5
5
|
import { defineTool } from "eve/tools";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
-
import {
|
|
8
|
-
import { artifacts } from "../artifacts/state.js";
|
|
7
|
+
import { publish, TYPES } from "../artifacts/core.mjs";
|
|
9
8
|
export default defineTool({
|
|
10
|
-
description: "Publish the next version of an artifact: a versioned static site you own and keep current, such as a change explainer for a PR or stack. Builds it from `source
|
|
9
|
+
description: "Publish the next version of an artifact: a versioned static site you own and keep current, such as a change explainer for a PR or stack. Builds it from `source` in your sandbox, deploys it, moves its stable URL to the new version, and shows a card in this thread (the same card updates on every later version). Takes a few minutes. Types: change-explainer (source: { fixture: <fixture dir>, repo: <git checkout>, scip?: <index.scip>, branch?: <ref> }).",
|
|
11
10
|
inputSchema: z.object({
|
|
12
11
|
type: z.enum(Object.keys(TYPES)).describe("Artifact type."),
|
|
13
12
|
key: z.string().regex(/^[\w.-]{1,40}$/).describe("Stable key within the type, e.g. the fixture id `thr_05a3` or `eve-pr-4342`. Same key = same artifact, next version."),
|
|
14
13
|
source: z.record(z.string(), z.unknown()).describe("What the type builds from (see the description)."),
|
|
15
14
|
}),
|
|
16
|
-
async execute({ type, key, source }) {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const record = await publish({ type, key, source }, prior);
|
|
20
|
-
artifacts.update((all) => ({ ...all, [id]: record }));
|
|
21
|
-
return { artifact: record, note: `published ${id} v${record.version}: ${record.alias}` };
|
|
15
|
+
async execute({ type, key, source }, ctx) {
|
|
16
|
+
const record = await publish({ type, key, source }, { sandbox: await ctx.getSandbox(), sessionId: ctx.session.id });
|
|
17
|
+
return { artifact: record, note: `published ${record.id} v${record.version}: ${record.alias}` };
|
|
22
18
|
},
|
|
23
19
|
});
|
package/tools/artifact_status.js
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
// The `artifact_status` tool: whether a published artifact still describes its
|
|
2
|
-
// source (the type decides
|
|
3
|
-
//
|
|
2
|
+
// source (the type decides, reading the repo in this session's sandbox). The
|
|
3
|
+
// hub applies the result to the version it checked, so the card shows it too.
|
|
4
4
|
import { defineTool } from "eve/tools";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import {
|
|
7
|
-
import { artifacts } from "../artifacts/state.js";
|
|
6
|
+
import { registryRecord, status } from "../artifacts/core.mjs";
|
|
8
7
|
export default defineTool({
|
|
9
8
|
description: "Check whether an artifact you published is still current: after you push, rebase or force-push, when its base or a stacked parent moves, and before asking for review. Returns fresh or stale with reasons; when stale, publish again with artifact_publish.",
|
|
10
9
|
inputSchema: z.object({
|
|
11
10
|
id: z.string().describe("Artifact id, `<type>:<key>`, e.g. change-explainer:thr_05a3 (artifact_list shows yours)."),
|
|
12
11
|
}),
|
|
13
|
-
async execute({ id }) {
|
|
14
|
-
const record = await
|
|
12
|
+
async execute({ id }, ctx) {
|
|
13
|
+
const record = await registryRecord(id);
|
|
15
14
|
if (!record)
|
|
16
15
|
throw new Error(`no artifact ${id}; artifact_list shows the ones this session owns`);
|
|
17
|
-
const result = await status(record);
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
return { artifact: next, status: result.status, reasons: result.reasons };
|
|
16
|
+
const result = await status(record, { sandbox: await ctx.getSandbox() });
|
|
17
|
+
const checked = { id: record.id, version: record.version, status: result.status, checkedAt: new Date().toISOString() };
|
|
18
|
+
return { artifact: checked, status: result.status, reasons: result.reasons };
|
|
21
19
|
},
|
|
22
20
|
});
|
package/artifacts/state.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const artifacts: any;
|
package/artifacts/state.js
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
// The owning session's artifact records, id -> latest record. Durable eve
|
|
2
|
-
// session state: it survives restarts like the rest of the session.
|
|
3
|
-
import { defineState } from "eve/context";
|
|
4
|
-
import { STATE_NAME } from "./core.mjs";
|
|
5
|
-
export const artifacts = defineState(STATE_NAME, () => ({}));
|
package/model-credential.mjs
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
// Hosted bots resolve model ids through the owner's refreshable Vercel sign-in.
|
|
2
|
-
// HUB_TOKEN identifies a hosted bot; the credential endpoint is on its registry.
|
|
3
|
-
// The hub owns expiration and refresh; Gateway rejection invalidates our cache.
|
|
4
|
-
import { createHash } from "node:crypto";
|
|
5
|
-
import { GatewayAuthenticationError } from "@ai-sdk/gateway";
|
|
6
|
-
import { registryHeaders } from "./registry-fetch.mjs";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The fetch the gateway sends through: the hub's current token and team on
|
|
10
|
-
* each request, and on a 401, one more try with the token the hub hands out
|
|
11
|
-
* after it is told (by sha256) which token Gateway refused.
|
|
12
|
-
*/
|
|
13
|
-
export function hubGatewayFetch(credentialUrl, fetchFn = (...a) => globalThis.fetch(...a), now = Date.now) {
|
|
14
|
-
let current;
|
|
15
|
-
const credential = async (rejected) => {
|
|
16
|
-
if (!rejected && current && now() < current.expiresAt) return current;
|
|
17
|
-
const headers = registryHeaders();
|
|
18
|
-
if (rejected) headers["x-rejected-token-sha256"] = createHash("sha256").update(rejected).digest("hex");
|
|
19
|
-
const response = await fetchFn(credentialUrl, { headers });
|
|
20
|
-
if (response.status === 401) {
|
|
21
|
-
const { error } = await response.json();
|
|
22
|
-
throw new GatewayAuthenticationError({ message: error });
|
|
23
|
-
}
|
|
24
|
-
if (!response.ok) throw new Error(`model credential from the hub: HTTP ${response.status} ${await response.text()}`);
|
|
25
|
-
return (current = await response.json());
|
|
26
|
-
};
|
|
27
|
-
return async (input, init) => {
|
|
28
|
-
const headers = new Headers(init?.headers);
|
|
29
|
-
const send = async (c) => {
|
|
30
|
-
headers.set("authorization", `Bearer ${c.token}`);
|
|
31
|
-
headers.set("x-vercel-ai-gateway-team", c.teamId);
|
|
32
|
-
const response = await fetchFn(input, { ...init, headers });
|
|
33
|
-
if (response.status === 401) current = undefined;
|
|
34
|
-
return response;
|
|
35
|
-
};
|
|
36
|
-
const first = await credential();
|
|
37
|
-
const response = await send(first);
|
|
38
|
-
if (response.status !== 401) return response;
|
|
39
|
-
// A streamed body can't be sent twice, but its rejected token is dropped.
|
|
40
|
-
if (init?.body != null && typeof init.body !== "string") return response;
|
|
41
|
-
await response.body?.cancel();
|
|
42
|
-
return send(await credential(first.token));
|
|
43
|
-
};
|
|
44
|
-
}
|