eve-hub 0.6.42 → 0.6.44
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/CHANGELOG.md +12 -0
- package/execution-context.mjs +2 -4
- package/kit-modules.json +1 -0
- package/memory/team.d.ts +20 -0
- package/memory/team.js +38 -0
- package/mention-gate.mjs +3 -63
- package/mention-tokens.mjs +64 -1
- package/package.json +1 -1
- package/prepare.mjs +3 -4
- package/stubs.d.mts +1 -0
- package/stubs.mjs +7 -4
- package/sync-kit.mjs +1 -1
- package/team.d.mts +24 -0
- package/team.mjs +113 -0
- package/tools/list_agents.d.ts +6 -0
- package/tools/list_agents.js +1 -1
- package/workspace.mjs +4 -1
- package/world.mjs +381 -382
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# eve-hub
|
|
2
2
|
|
|
3
|
+
## 0.6.44
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- The mention rule (mentionTokens and codeRanges) lives in mention-tokens.mjs with no imports, so the composer's fuzz harness loads the server's own source; mention-gate re-exports codeRanges.
|
|
8
|
+
|
|
9
|
+
## 0.6.43
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- Every bot is given the team model (who is on the team, where each runs, each bot's real tools from its eve runtime, who hands work to whom) as a memory slot that re-sends only when it changes, hosted bots included; `list_agents` is on every bot and reports each bot's kind, place and tools.
|
|
14
|
+
|
|
3
15
|
## 0.6.42
|
|
4
16
|
|
|
5
17
|
### Patch Changes
|
package/execution-context.mjs
CHANGED
|
@@ -39,15 +39,13 @@ export function deploymentFacts(env = process.env) {
|
|
|
39
39
|
|
|
40
40
|
const list = (items) => items.map((item) => `\`${item}\``).join(", ");
|
|
41
41
|
|
|
42
|
-
export function hostedContext({ bot,
|
|
43
|
-
const mac = roster.filter((agent) => agent.runsOn === "local").map((agent) => agent.name);
|
|
44
|
-
const cloud = roster.filter((agent) => agent.runsOn !== "local").map((agent) => agent.name);
|
|
42
|
+
export function hostedContext({ bot, kit, removed, deployment: d }, options) {
|
|
45
43
|
const dashboard = d.team && d.project ? `https://vercel.com/${d.team}/${d.project}` : undefined;
|
|
46
44
|
const lines = [
|
|
47
45
|
"## Execution context",
|
|
48
46
|
`You are \`${bot}\`, an eve agent deployed on Vercel, not on a laptop: \`hub.workspace.json\` defines you as hosted.`,
|
|
49
47
|
"",
|
|
50
|
-
|
|
48
|
+
"- **Workspace:** one hosted hub holds the threads and routes messages to each bot. Who is on the team, where each bot runs and what each can do come live from the hub: the team model message at the top of your turn (when it changes) and `list_agents`.",
|
|
51
49
|
`- **Package:** your tools, hooks, channel and instructions come from the \`eve-hub\` package, version ${kit}.`,
|
|
52
50
|
`- **Deployment:** ${[
|
|
53
51
|
(d.project ?? d.projectId) && `project \`${d.project ?? d.projectId}\``,
|
package/kit-modules.json
CHANGED
package/memory/team.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
declare const recall: ({ messages }: {
|
|
2
|
+
messages: readonly unknown[];
|
|
3
|
+
}) => Promise<{
|
|
4
|
+
messages: {
|
|
5
|
+
id: string;
|
|
6
|
+
content: string;
|
|
7
|
+
}[];
|
|
8
|
+
}>;
|
|
9
|
+
declare const _default: import("eve/memory").DefinedMemory<{
|
|
10
|
+
readonly description: "The team: who is on it, what each bot can do, and who hands work to whom.";
|
|
11
|
+
readonly namespace: "hub-team-memory";
|
|
12
|
+
readonly scope: "workspace";
|
|
13
|
+
readonly provider: {
|
|
14
|
+
readonly recall: {
|
|
15
|
+
"turn.started": typeof recall;
|
|
16
|
+
"compaction.completed": typeof recall;
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
export default _default;
|
package/memory/team.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { defineMemory, defineMemoryProvider } from "eve/memory";
|
|
2
|
+
import { registryFetch } from "../registry-fetch.mjs";
|
|
3
|
+
import { teamRecall } from "../team.mjs";
|
|
4
|
+
// The team model (team.mjs) as an eve memory slot: roster, each bot's tools
|
|
5
|
+
// and the handoff rules, from the hub's GET /api/team. Recall runs at the start
|
|
6
|
+
// of every turn and after compaction, and adds one message only when the team
|
|
7
|
+
// differs from what this session was last given, so a turn where nothing
|
|
8
|
+
// changed carries nothing. A hub that can't answer within 5 s fails the recall, not the turn.
|
|
9
|
+
const warned = new Set();
|
|
10
|
+
async function fetchTeam(since) {
|
|
11
|
+
const res = await registryFetch(`/api/team${since ? `?since=${since}` : ""}`, { signal: AbortSignal.timeout(5_000) });
|
|
12
|
+
if (res.status === 304)
|
|
13
|
+
return null;
|
|
14
|
+
if (!res.ok)
|
|
15
|
+
throw new Error(`GET /api/team: HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
16
|
+
return (await res.json());
|
|
17
|
+
}
|
|
18
|
+
const recall = async ({ messages }) => {
|
|
19
|
+
try {
|
|
20
|
+
return await teamRecall(messages, fetchTeam);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
const note = error.message;
|
|
24
|
+
if (!warned.has(note)) {
|
|
25
|
+
warned.add(note);
|
|
26
|
+
console.warn(`team: ${note}`);
|
|
27
|
+
}
|
|
28
|
+
return { messages: [] };
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
export default defineMemory({
|
|
32
|
+
description: "The team: who is on it, what each bot can do, and who hands work to whom.",
|
|
33
|
+
namespace: "hub-team-memory",
|
|
34
|
+
scope: "workspace",
|
|
35
|
+
provider: defineMemoryProvider({
|
|
36
|
+
recall: { "turn.started": recall, "compaction.completed": recall },
|
|
37
|
+
}),
|
|
38
|
+
});
|
package/mention-gate.mjs
CHANGED
|
@@ -20,11 +20,14 @@
|
|
|
20
20
|
// overridden by HUB_MENTION_GATE (on|off), HUB_MENTION_GATE_MODEL,
|
|
21
21
|
// HUB_MENTION_GATE_REASONING, HUB_MENTION_GATE_TIMEOUT_MS and
|
|
22
22
|
// HUB_MENTION_GATE_URL (tests).
|
|
23
|
+
import { codeRanges } from "./mention-tokens.mjs";
|
|
23
24
|
import { registryHeaders } from "./registry-fetch.mjs";
|
|
24
25
|
import { existsSync, readFileSync } from "node:fs";
|
|
25
26
|
import { join } from "node:path";
|
|
26
27
|
import { pathToFileURL } from "node:url";
|
|
27
28
|
|
|
29
|
+
export { codeRanges };
|
|
30
|
+
|
|
28
31
|
export const GATE_DEFAULTS = {
|
|
29
32
|
enabled: true,
|
|
30
33
|
model: "openai/gpt-6-luna-decisions",
|
|
@@ -194,69 +197,6 @@ export function triggerText(envelope) {
|
|
|
194
197
|
return next < 0 ? rest : rest.slice(0, next);
|
|
195
198
|
}
|
|
196
199
|
|
|
197
|
-
// The character ranges of a message that are code, by CommonMark's rules:
|
|
198
|
-
// fenced blocks (``` or ~~~, closed by the same character, at least as
|
|
199
|
-
// long), and code spans (a run of n backticks closed by the next run of
|
|
200
|
-
// exactly n on the same line; an opener can't be backslash-escaped).
|
|
201
|
-
// Anything ambiguous stays text: an unclosed fence or an unmatched run is
|
|
202
|
-
// literal, so a handle next to it counts as live.
|
|
203
|
-
export function codeRanges(text) {
|
|
204
|
-
const s = String(text ?? "");
|
|
205
|
-
const ranges = [];
|
|
206
|
-
const lines = [];
|
|
207
|
-
for (let at = 0; at <= s.length; ) {
|
|
208
|
-
const nl = s.indexOf("\n", at);
|
|
209
|
-
const end = nl < 0 ? s.length : nl;
|
|
210
|
-
lines.push({ start: at, end, text: s.slice(at, end) });
|
|
211
|
-
if (nl < 0) break;
|
|
212
|
-
at = nl + 1;
|
|
213
|
-
}
|
|
214
|
-
// Fences first: only closed ones are code.
|
|
215
|
-
const fenced = new Set();
|
|
216
|
-
for (let i = 0; i < lines.length; i++) {
|
|
217
|
-
const open = lines[i].text.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
218
|
-
if (!open || (open[1][0] === "`" && open[2].includes("`"))) continue;
|
|
219
|
-
const close = new RegExp(`^ {0,3}${open[1][0] === "`" ? "`" : "~"}{${open[1].length},}\\s*$`);
|
|
220
|
-
const j = lines.findIndex((l, k) => k > i && close.test(l.text));
|
|
221
|
-
if (j < 0) continue;
|
|
222
|
-
ranges.push([lines[i].start, lines[j].end]);
|
|
223
|
-
for (let k = i; k <= j; k++) fenced.add(k);
|
|
224
|
-
i = j;
|
|
225
|
-
}
|
|
226
|
-
// Code spans.
|
|
227
|
-
let para = [];
|
|
228
|
-
const flush = () => {
|
|
229
|
-
if (!para.length) return;
|
|
230
|
-
const from = para[0].start;
|
|
231
|
-
const t = s.slice(from, para.at(-1).end);
|
|
232
|
-
const runs = [...t.matchAll(/`+/g)].map((m) => ({ at: m.index, n: m[0].length }));
|
|
233
|
-
for (let i = 0; i < runs.length; i++) {
|
|
234
|
-
let { at, n } = runs[i];
|
|
235
|
-
let slash = 0;
|
|
236
|
-
for (let b = at - 1; b >= 0 && t[b] === "\\"; b--) slash++;
|
|
237
|
-
if (slash % 2 === 1) {
|
|
238
|
-
// An escaped backtick is literal; the rest of the run can still open.
|
|
239
|
-
at++;
|
|
240
|
-
n--;
|
|
241
|
-
if (!n) continue;
|
|
242
|
-
}
|
|
243
|
-
const j = runs.findIndex((r, k) => k > i && r.n === n);
|
|
244
|
-
if (j < 0) continue;
|
|
245
|
-
ranges.push([from + at, from + runs[j].at + n]);
|
|
246
|
-
i = j;
|
|
247
|
-
}
|
|
248
|
-
para = [];
|
|
249
|
-
};
|
|
250
|
-
// One line at a time: a span that would cross lines might cross a block
|
|
251
|
-
// boundary (a heading or a list item interrupts a paragraph without a
|
|
252
|
-
// blank line), so it stays text.
|
|
253
|
-
lines.forEach((l, i) => {
|
|
254
|
-
if (!fenced.has(i) && l.text.trim()) para.push(l);
|
|
255
|
-
flush();
|
|
256
|
-
});
|
|
257
|
-
return ranges;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
200
|
// Where the bot's @handle sits in a message, by parsing its markdown: in
|
|
261
201
|
// code (codeRanges), on a > quote line, or live (anywhere else).
|
|
262
202
|
export function mentionSites(text, agent) {
|
package/mention-tokens.mjs
CHANGED
|
@@ -1,4 +1,67 @@
|
|
|
1
|
-
|
|
1
|
+
// The mention rule: what counts as `@handle` in a message. No imports, so the
|
|
2
|
+
// native editor checks load this one file as is.
|
|
3
|
+
// The character ranges of a message that are code, by CommonMark's rules:
|
|
4
|
+
// fenced blocks (``` or ~~~, closed by the same character, at least as
|
|
5
|
+
// long), and code spans (a run of n backticks closed by the next run of
|
|
6
|
+
// exactly n on the same line; an opener can't be backslash-escaped).
|
|
7
|
+
// Anything ambiguous stays text: an unclosed fence or an unmatched run is
|
|
8
|
+
// literal, so a handle next to it counts as live.
|
|
9
|
+
export function codeRanges(text) {
|
|
10
|
+
const s = String(text ?? "");
|
|
11
|
+
const ranges = [];
|
|
12
|
+
const lines = [];
|
|
13
|
+
for (let at = 0; at <= s.length; ) {
|
|
14
|
+
const nl = s.indexOf("\n", at);
|
|
15
|
+
const end = nl < 0 ? s.length : nl;
|
|
16
|
+
lines.push({ start: at, end, text: s.slice(at, end) });
|
|
17
|
+
if (nl < 0) break;
|
|
18
|
+
at = nl + 1;
|
|
19
|
+
}
|
|
20
|
+
// Fences first: only closed ones are code.
|
|
21
|
+
const fenced = new Set();
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const open = lines[i].text.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
24
|
+
if (!open || (open[1][0] === "`" && open[2].includes("`"))) continue;
|
|
25
|
+
const close = new RegExp(`^ {0,3}${open[1][0] === "`" ? "`" : "~"}{${open[1].length},}\\s*$`);
|
|
26
|
+
const j = lines.findIndex((l, k) => k > i && close.test(l.text));
|
|
27
|
+
if (j < 0) continue;
|
|
28
|
+
ranges.push([lines[i].start, lines[j].end]);
|
|
29
|
+
for (let k = i; k <= j; k++) fenced.add(k);
|
|
30
|
+
i = j;
|
|
31
|
+
}
|
|
32
|
+
// Code spans.
|
|
33
|
+
let para = [];
|
|
34
|
+
const flush = () => {
|
|
35
|
+
if (!para.length) return;
|
|
36
|
+
const from = para[0].start;
|
|
37
|
+
const t = s.slice(from, para.at(-1).end);
|
|
38
|
+
const runs = [...t.matchAll(/`+/g)].map((m) => ({ at: m.index, n: m[0].length }));
|
|
39
|
+
for (let i = 0; i < runs.length; i++) {
|
|
40
|
+
let { at, n } = runs[i];
|
|
41
|
+
let slash = 0;
|
|
42
|
+
for (let b = at - 1; b >= 0 && t[b] === "\\"; b--) slash++;
|
|
43
|
+
if (slash % 2 === 1) {
|
|
44
|
+
// An escaped backtick is literal; the rest of the run can still open.
|
|
45
|
+
at++;
|
|
46
|
+
n--;
|
|
47
|
+
if (!n) continue;
|
|
48
|
+
}
|
|
49
|
+
const j = runs.findIndex((r, k) => k > i && r.n === n);
|
|
50
|
+
if (j < 0) continue;
|
|
51
|
+
ranges.push([from + at, from + runs[j].at + n]);
|
|
52
|
+
i = j;
|
|
53
|
+
}
|
|
54
|
+
para = [];
|
|
55
|
+
};
|
|
56
|
+
// One line at a time: a span that would cross lines might cross a block
|
|
57
|
+
// boundary (a heading or a list item interrupts a paragraph without a
|
|
58
|
+
// blank line), so it stays text.
|
|
59
|
+
lines.forEach((l, i) => {
|
|
60
|
+
if (!fenced.has(i) && l.text.trim()) para.push(l);
|
|
61
|
+
flush();
|
|
62
|
+
});
|
|
63
|
+
return ranges;
|
|
64
|
+
}
|
|
2
65
|
|
|
3
66
|
// The one definition of an actual mention: `@handle` at a word boundary (not
|
|
4
67
|
// foo@bar, @@x, \@x or a URL's /@x) and outside code (codeRanges). Every reader of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.44",
|
|
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",
|
package/prepare.mjs
CHANGED
|
@@ -101,10 +101,10 @@ export default defineMemory({ namespace: ${JSON.stringify(`hub-${slot}-memory`)}
|
|
|
101
101
|
// Replaces the kit's execution-context stub (the local page) with the hosted
|
|
102
102
|
// page, for a bot the definition says is hosted. Its removed-tools list is read
|
|
103
103
|
// from the bot's folder as pruned.
|
|
104
|
-
function writeExecutionContext(bot, { bot: name,
|
|
104
|
+
function writeExecutionContext(bot, { bot: name, deployment }) {
|
|
105
105
|
const kitTools = Object.keys(name === "main" ? MAIN_KIT_FILES : KIT_FILES).filter((file) => file.startsWith("tools/") && file !== "tools/bash.ts").map((file) => basename(file, ".ts"));
|
|
106
106
|
const removed = kitTools.filter((tool) => !existsSync(join(bot, "tools", `${tool}.ts`)));
|
|
107
|
-
const section = hostedContext({ bot: name,
|
|
107
|
+
const section = hostedContext({ bot: name, kit: KIT_VERSION, removed, deployment });
|
|
108
108
|
mkdirSync(join(bot, "instructions"), { recursive: true });
|
|
109
109
|
writeFileSync(join(bot, "instructions", "execution-context.ts"), `import { defineInstructions } from "eve/instructions";\n\nexport default defineInstructions({ content: ${JSON.stringify(section)} });\n`);
|
|
110
110
|
}
|
|
@@ -141,11 +141,10 @@ export function prepareDeployment(dir) {
|
|
|
141
141
|
writeFileSync(join(bot, stub), content);
|
|
142
142
|
}
|
|
143
143
|
}
|
|
144
|
-
const roster = resolved.agents;
|
|
145
144
|
const deployment = deploymentFacts();
|
|
146
145
|
for (const name of readdirSync(join(dir, "agents"))) {
|
|
147
146
|
const bot = join(dir, "agents", name, "agent");
|
|
148
|
-
writeExecutionContext(bot, { bot: name,
|
|
147
|
+
writeExecutionContext(bot, { bot: name, deployment });
|
|
149
148
|
// The kit's extension (kit__worker) runs host-only workers and is excluded from the kit tarball.
|
|
150
149
|
// Other extensions, such as eve-code, stay.
|
|
151
150
|
for (const item of readdirSync(bot, { recursive: true })) {
|
package/stubs.d.mts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const KIT_FILES: Record<string, string[]>;
|
|
2
2
|
export const MAIN_KIT_FILES: Record<string, string[]>;
|
|
3
3
|
export const DEPLOYED_KIT_FILES: Set<string>;
|
|
4
|
+
export const MEMORY_STUBS: Record<string, string>;
|
|
4
5
|
export const PROTOCOL_STUB_FILE: string;
|
|
5
6
|
export const PROTOCOL_STUB: string;
|
|
6
7
|
export function kitStub(file: string, options?: { files?: Record<string, string[]> }): string;
|
package/stubs.mjs
CHANGED
|
@@ -14,6 +14,7 @@ export const KIT_FILES = {
|
|
|
14
14
|
"tools/update_thread.ts": ["Update a thread this bot is in: title, track, domain and summary."],
|
|
15
15
|
"tools/list_threads.ts": ["Every workspace thread, from any session."],
|
|
16
16
|
"tools/read_thread.ts": ["Read any workspace thread."],
|
|
17
|
+
"tools/list_agents.ts": ["The team right now: kind, place, tools, models, who's busy."],
|
|
17
18
|
"tools/post_message.ts": ["Post into an existing thread or a bot's DM; with `status`, report the thread's phase."],
|
|
18
19
|
};
|
|
19
20
|
|
|
@@ -28,7 +29,6 @@ export const MAIN_KIT_FILES = {
|
|
|
28
29
|
"tools/move_path.ts": ["Rename or merge a path (project/track): every thread under it moves."],
|
|
29
30
|
"tools/archive_thread.ts": ["Archive a thread whose work is done."],
|
|
30
31
|
"tools/unarchive_thread.ts": ["Bring an archived thread back."],
|
|
31
|
-
"tools/list_agents.ts": ["The team right now: roles, models, who's busy."],
|
|
32
32
|
"tools/create_agent.ts": ["Add a bot to the workspace."],
|
|
33
33
|
"tools/remove_agent.ts": ["Remove a bot from the workspace."],
|
|
34
34
|
"tools/restart_agent.ts": ["Restart one bot's server."],
|
|
@@ -61,6 +61,7 @@ export const DEPLOYED_KIT_FILES = new Set([
|
|
|
61
61
|
"skills/onboarding-profile.ts",
|
|
62
62
|
"tools/onboarding_scripts.ts",
|
|
63
63
|
...["react", "show", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
|
|
64
|
+
"memory/team.ts",
|
|
64
65
|
...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "agent_send", "create_agent", "remove_agent", "restart_agent", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
|
|
65
66
|
]);
|
|
66
67
|
|
|
@@ -97,9 +98,10 @@ import workspace from "../../../../hub.workspace.json" with { type: "json" };
|
|
|
97
98
|
export default defineInstructions({ content: executionContext(${JSON.stringify(name)}, workspace) });
|
|
98
99
|
`;
|
|
99
100
|
|
|
100
|
-
// Every bot (main too) has
|
|
101
|
-
// workspace's memory/ entries
|
|
102
|
-
//
|
|
101
|
+
// Every bot (main too) has three memory slots: memory/workspace.ts recalls the
|
|
102
|
+
// workspace's memory/ entries, memory/bot.ts the bot's own agents/<bot>/memory/
|
|
103
|
+
// entries, and memory/team.ts the team model from the hub, every turn
|
|
104
|
+
// (packages/agent-kit/memory/).
|
|
103
105
|
const memoryStub = (slot, what) => `// ${what}, recalled every turn. From eve-hub
|
|
104
106
|
// (packages/agent-kit/memory/${slot}.ts); add or supersede entries there, not here.
|
|
105
107
|
export { default } from "eve-hub/memory/${slot}";
|
|
@@ -107,6 +109,7 @@ export { default } from "eve-hub/memory/${slot}";
|
|
|
107
109
|
export const MEMORY_STUBS = {
|
|
108
110
|
"memory/workspace.ts": memoryStub("workspace", "The workspace's memory/ entries"),
|
|
109
111
|
"memory/bot.ts": memoryStub("bot", "This bot's own agents/<bot>/memory/ entries"),
|
|
112
|
+
"memory/team.ts": memoryStub("team", "The team model (roster, tools, handoffs) from the hub"),
|
|
110
113
|
};
|
|
111
114
|
|
|
112
115
|
// main's instructions aren't copied into a workspace: agent/instructions.ts
|
package/sync-kit.mjs
CHANGED
|
@@ -65,8 +65,8 @@ export function syncKit({ bots, modules }) {
|
|
|
65
65
|
const wanted = new Set(Object.keys(stubs).filter((file) => local || DEPLOYED_KIT_FILES.has(file)));
|
|
66
66
|
if (local) {
|
|
67
67
|
wanted.add(EXECUTION_CONTEXT_STUB_FILE);
|
|
68
|
-
for (const file of Object.keys(MEMORY_STUBS)) wanted.add(file);
|
|
69
68
|
}
|
|
69
|
+
for (const file of Object.keys(MEMORY_STUBS)) if (local || DEPLOYED_KIT_FILES.has(file)) wanted.add(file);
|
|
70
70
|
const content = (file) => (file === EXECUTION_CONTEXT_STUB_FILE ? executionContextStub(bot) : MEMORY_STUBS[file] ?? kitStub(file, { files: stubs }));
|
|
71
71
|
for (const file of Object.keys(files).sort()) {
|
|
72
72
|
const rel = file.replace(SUBAGENT, "");
|
package/team.d.mts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type TeamNode = {
|
|
2
|
+
id: string;
|
|
3
|
+
kind: "human" | "coordinator" | "planner" | "builder" | "reviewer" | "git-ops" | "demo" | "other";
|
|
4
|
+
label: string;
|
|
5
|
+
color?: string;
|
|
6
|
+
role?: string;
|
|
7
|
+
model?: string;
|
|
8
|
+
runsOn?: "hosted" | "local";
|
|
9
|
+
tools?: string[] | null;
|
|
10
|
+
subagents?: string[];
|
|
11
|
+
};
|
|
12
|
+
export type TeamEdge = { from: string; to: string; kind: string };
|
|
13
|
+
export type TeamModel = { version: 1; hash: string; nodes: TeamNode[]; edges: TeamEdge[] };
|
|
14
|
+
export type TeamAgent = { name: string; role: string; model: string; color?: string; kind?: string; runsOn?: "hosted" | "local"; subagents?: Record<string, unknown> };
|
|
15
|
+
export const KINDS: readonly string[];
|
|
16
|
+
export const MODEL_VERSION: 1;
|
|
17
|
+
export const NODE_KINDS: readonly [string, ...string[]];
|
|
18
|
+
export const EDGE_KINDS: readonly [string, ...string[]];
|
|
19
|
+
export const SECTIONS: readonly { id: string; edge: { kind: string; from: unknown; to: unknown }; text: string }[];
|
|
20
|
+
export const SECTIONS_HASH: string;
|
|
21
|
+
export function teamEdges(nodes: readonly TeamNode[]): TeamEdge[];
|
|
22
|
+
export function teamModel(input: { agents: readonly TeamAgent[]; humans?: readonly { name: string; color?: string }[]; tools?: Record<string, readonly string[] | null | undefined> }): TeamModel;
|
|
23
|
+
export function teamText(model: TeamModel): string;
|
|
24
|
+
export function teamRecall(history: readonly unknown[], fetchTeam: (since?: string) => Promise<TeamModel | null>): Promise<{ messages: { id: string; content: string }[] }>;
|
package/team.mjs
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// The team model: who is on the team, what each bot can do, and who hands work
|
|
2
|
+
// to whom. One derivation feeds every reader: the hub's GET /api/team (the
|
|
3
|
+
// native Team window, list_agents) and the prompt's team slot (memory/team.ts),
|
|
4
|
+
// which delivers it to a session once and again only when it changes.
|
|
5
|
+
//
|
|
6
|
+
// A bot's `kind` is its place in the team (hub.workspace.json). Its tools are
|
|
7
|
+
// what its running eve reports (/v1/info), never read from files. The edges
|
|
8
|
+
// come from SECTIONS: each protocol section names the edge it draws between
|
|
9
|
+
// kinds (or a tool), and the same entry renders the sentence the bots read.
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { KINDS } from "./workspace.mjs";
|
|
12
|
+
|
|
13
|
+
export { KINDS };
|
|
14
|
+
export const MODEL_VERSION = 1;
|
|
15
|
+
export const NODE_KINDS = [...KINDS, "other", "human"];
|
|
16
|
+
|
|
17
|
+
const sha = (text, length) => createHash("sha256").update(text).digest("hex").slice(0, length);
|
|
18
|
+
|
|
19
|
+
// Selectors: "human", "bots" (every bot), { kind: [...] } or { tool: "agent_send" };
|
|
20
|
+
// all but the first pick bots. `text` is the sentence the team prompt carries when the section
|
|
21
|
+
// has at least one edge; `{from}` is the names behind the edge's from side.
|
|
22
|
+
export const SECTIONS = [
|
|
23
|
+
{ id: "partner", edge: { kind: "partner", from: "human", to: { kind: ["coordinator"] } }, text: "The coordinator works with the human directly." },
|
|
24
|
+
{ id: "route", edge: { kind: "route", from: { kind: ["coordinator"] }, to: { kind: ["planner", "builder"] } }, text: "The coordinator routes work to planners and builders." },
|
|
25
|
+
{ id: "plan", edge: { kind: "plan", from: { kind: ["planner"] }, to: { kind: ["builder"] } }, text: "Planners hand plans to builders." },
|
|
26
|
+
{ id: "build", edge: { kind: "build", from: { kind: ["builder"] }, to: { kind: ["reviewer"] } }, text: "Builders hand finished work to a reviewer." },
|
|
27
|
+
{ id: "fixfirst", edge: { kind: "fixfirst", from: { kind: ["reviewer"] }, to: { kind: ["builder"] } }, text: "Reviewers send fixes back to the builder that posted the build." },
|
|
28
|
+
{ id: "ship", edge: { kind: "ship", from: { kind: ["reviewer"] }, to: "human" }, text: "A reviewer's ship verdict goes to the human." },
|
|
29
|
+
{ id: "escalate", edge: { kind: "escalate", from: { kind: ["git-ops"] }, to: { kind: ["builder"] } }, text: "Git-ops bots report what they can't fix to a builder." },
|
|
30
|
+
{ id: "steer", edge: { kind: "steer", from: { tool: "agent_send" }, to: "bots" }, text: "Only {from} can queue work for or steer another bot (`agent_send`)." },
|
|
31
|
+
];
|
|
32
|
+
export const EDGE_KINDS = SECTIONS.map((section) => section.edge.kind);
|
|
33
|
+
export const SECTIONS_HASH = sha(JSON.stringify(SECTIONS), 8);
|
|
34
|
+
|
|
35
|
+
const pick = (selector, nodes) =>
|
|
36
|
+
nodes.filter((n) => {
|
|
37
|
+
if (selector === "human") return n.kind === "human";
|
|
38
|
+
if (n.kind === "human") return false;
|
|
39
|
+
return selector === "bots" || (selector.kind ? selector.kind.includes(n.kind) : Boolean(n.tools?.includes(selector.tool)));
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export function teamEdges(nodes) {
|
|
43
|
+
const edges = [];
|
|
44
|
+
for (const { edge } of SECTIONS) {
|
|
45
|
+
const from = pick(edge.from, nodes);
|
|
46
|
+
const to = pick(edge.to, nodes);
|
|
47
|
+
for (const f of from) for (const t of to) if (f.id !== t.id) edges.push({ from: f.id, to: t.id, kind: edge.kind });
|
|
48
|
+
}
|
|
49
|
+
return edges;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// `agents`: hub.workspace.json's agents plus `runsOn` resolved; `tools`: bot
|
|
53
|
+
// name -> the tool names its eve reports, absent or null when not reported.
|
|
54
|
+
export function teamModel({ agents, humans = [], tools = {} }) {
|
|
55
|
+
const nodes = [
|
|
56
|
+
...humans.map((h) => ({ id: h.name, kind: "human", label: h.name, ...(h.color ? { color: h.color } : {}) })),
|
|
57
|
+
...agents.map((a) => ({
|
|
58
|
+
id: a.name,
|
|
59
|
+
kind: a.kind ?? "other",
|
|
60
|
+
label: a.name,
|
|
61
|
+
...(a.color ? { color: a.color } : {}),
|
|
62
|
+
role: a.role,
|
|
63
|
+
model: a.model,
|
|
64
|
+
runsOn: a.runsOn ?? "hosted",
|
|
65
|
+
tools: tools[a.name] ? [...new Set(tools[a.name])].sort() : null,
|
|
66
|
+
subagents: Object.keys(a.subagents ?? {}),
|
|
67
|
+
})),
|
|
68
|
+
];
|
|
69
|
+
const edges = teamEdges(nodes);
|
|
70
|
+
return { version: MODEL_VERSION, hash: sha(JSON.stringify({ nodes, edges }), 12), nodes, edges };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const list = (items) => items.join(", ");
|
|
74
|
+
|
|
75
|
+
// What a session reads: the roster with each bot's kind, place and tools, then
|
|
76
|
+
// the handoff sentences. Identical for every bot, so one slot text serves the
|
|
77
|
+
// whole team. The header carries both hashes: the model's and the sections'.
|
|
78
|
+
export function teamText(model) {
|
|
79
|
+
const bots = model.nodes.filter((n) => n.kind !== "human");
|
|
80
|
+
const reported = bots.filter((b) => b.tools);
|
|
81
|
+
const shared = reported.length ? reported[0].tools.filter((t) => reported.every((b) => b.tools.includes(t))) : [];
|
|
82
|
+
const lines = [`Team model ${model.hash}/${SECTIONS_HASH}: the team right now, from the hub. It replaces any earlier team model above.`, "", "Bots:"];
|
|
83
|
+
for (const b of bots) {
|
|
84
|
+
const own = b.tools?.filter((t) => !shared.includes(t));
|
|
85
|
+
const where = b.runsOn === "local" ? "on the owner's Mac" : "on Vercel";
|
|
86
|
+
lines.push(`- ${b.id} (${b.kind}, ${where}): ${b.role}${own?.length ? ` Also: ${list(own)}.` : ""}${b.tools ? "" : " Tools not reported."}${b.subagents.length ? ` Subagents: ${list(b.subagents)}.` : ""}`);
|
|
87
|
+
}
|
|
88
|
+
if (shared.length) lines.push("", `Every bot with reported tools has: ${list(shared)}.`);
|
|
89
|
+
const rules = SECTIONS.flatMap(({ id, edge, text }) => {
|
|
90
|
+
const from = model.edges.filter((e) => e.kind === edge.kind).map((e) => e.from);
|
|
91
|
+
return from.length ? [`- ${text.replace("{from}", list([...new Set(from)]))}`] : [];
|
|
92
|
+
});
|
|
93
|
+
if (rules.length) lines.push("", "Handoffs:", ...rules);
|
|
94
|
+
return lines.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const textOf = (m) => (typeof m.content === "string" ? m.content : Array.isArray(m.content) ? m.content.map((p) => (p?.type === "text" ? p.text : "")).join("") : "");
|
|
98
|
+
const HEADER = /^Team model ([0-9a-f]{12})\/([0-9a-f]{8}):/;
|
|
99
|
+
|
|
100
|
+
// The team slot's recall, given the session's history: the team model as one
|
|
101
|
+
// message when it differs from the last one this session was given, nothing
|
|
102
|
+
// otherwise. `fetchTeam(since)` asks the hub, with the model hash the session
|
|
103
|
+
// holds (only while these sections are the ones that rendered it), and answers
|
|
104
|
+
// the model, or null when the hub says nothing changed.
|
|
105
|
+
export async function teamRecall(history, fetchTeam) {
|
|
106
|
+
const given = history.filter((m) => m?.role === "user" && m.kind === "memory.load" && HEADER.test(textOf(m))).map(textOf);
|
|
107
|
+
const last = HEADER.exec(given.at(-1) ?? "");
|
|
108
|
+
const model = await fetchTeam(last && last[2] === SECTIONS_HASH ? last[1] : undefined);
|
|
109
|
+
if (!model) return { messages: [] };
|
|
110
|
+
const text = teamText(model);
|
|
111
|
+
if (given.at(-1) === text) return { messages: [] };
|
|
112
|
+
return { messages: [{ id: `team#${given.length + 1}@${sha(text, 8)}`, content: text }] };
|
|
113
|
+
}
|
package/tools/list_agents.d.ts
CHANGED
|
@@ -2,6 +2,9 @@ declare const _default: import("eve/tools").ToolDefinition<Record<string, never>
|
|
|
2
2
|
name: string;
|
|
3
3
|
role: string;
|
|
4
4
|
model: string;
|
|
5
|
+
kind?: string;
|
|
6
|
+
runsOn?: "hosted" | "local";
|
|
7
|
+
tools?: string[] | null;
|
|
5
8
|
subagentCap: string;
|
|
6
9
|
subagents: Array<{
|
|
7
10
|
name: string;
|
|
@@ -31,6 +34,9 @@ declare const _default: import("eve/tools").ToolDefinition<Record<string, never>
|
|
|
31
34
|
name: string;
|
|
32
35
|
role: string;
|
|
33
36
|
model: string;
|
|
37
|
+
kind?: string;
|
|
38
|
+
runsOn?: "hosted" | "local";
|
|
39
|
+
tools?: string[] | null;
|
|
34
40
|
subagentCap: string;
|
|
35
41
|
subagents: Array<{
|
|
36
42
|
name: string;
|
package/tools/list_agents.js
CHANGED
|
@@ -2,7 +2,7 @@ import { defineTool } from "eve/tools";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { registryFetch } from "../registry-fetch.mjs";
|
|
4
4
|
export default defineTool({
|
|
5
|
-
description: "List the workspace's bots right now: each one's role, model, the model and reasoning each of its declared subagents and its built-in `agent` copies resolve to under the workspace subagent cap (clamped from hub.workspace.json, never rejected), whether it's idle or busy (and where; a bot's threads run in parallel, so busy elsewhere doesn't mean it can't start work in a new thread), its server's memory (process.rssMb) and uptime (process.uptimeS; only on a local hub, since a hosted hub runs no bot process to measure), and any restart in progress (restart_agent, or the registry recycling a bot that grew too big or ran too long). The team changes; check it before routing work.",
|
|
5
|
+
description: "List the workspace's bots right now: each one's role, kind (its place in the team: coordinator, planner, builder, reviewer, git-ops, demo, or other), where it runs (hosted or local), the tools its runtime reports (null when not yet reported), its model, the model and reasoning each of its declared subagents and its built-in `agent` copies resolve to under the workspace subagent cap (clamped from hub.workspace.json, never rejected), whether it's idle or busy (and where; a bot's threads run in parallel, so busy elsewhere doesn't mean it can't start work in a new thread), its server's memory (process.rssMb) and uptime (process.uptimeS; only on a local hub, since a hosted hub runs no bot process to measure), and any restart in progress (restart_agent, or the registry recycling a bot that grew too big or ran too long). The team changes; check it before routing work.",
|
|
6
6
|
inputSchema: z.object({}),
|
|
7
7
|
async execute() {
|
|
8
8
|
// Anything but a 2xx list of bots is an error naming the status and the
|
package/workspace.mjs
CHANGED
|
@@ -47,9 +47,11 @@ export const DOMAIN = /^[a-z][a-z0-9-]*(\/[a-z][a-z0-9-]*)*$/;
|
|
|
47
47
|
export const NAME = /^[a-z][a-z0-9-]{1,30}$/;
|
|
48
48
|
export const HUMAN_NAME = /^[a-z][a-z0-9-]{0,30}$/;
|
|
49
49
|
const COLOR = /^#[0-9a-f]{3}([0-9a-f]{3})?$/i;
|
|
50
|
-
const AGENT_KEYS = new Set(["name", "model", "role", "color", "subagents", "subagentCap", "limits", "port", "runsOn", "domains"]);
|
|
50
|
+
const AGENT_KEYS = new Set(["name", "model", "role", "color", "subagents", "subagentCap", "limits", "port", "runsOn", "domains", "kind"]);
|
|
51
51
|
|
|
52
52
|
export const RUNS_ON = ["hosted", "local"];
|
|
53
|
+
// The bot's place in the team (team.mjs draws the handoffs between kinds).
|
|
54
|
+
export const KINDS = ["coordinator", "planner", "builder", "reviewer", "git-ops", "demo"];
|
|
53
55
|
|
|
54
56
|
export class WorkspaceError extends Error {}
|
|
55
57
|
|
|
@@ -102,6 +104,7 @@ export function parseWorkspace(data, file = WORKSPACE_FILE) {
|
|
|
102
104
|
// Where the bot runs: on the hosted bots project (default), or on the human's Mac, started by
|
|
103
105
|
// the app's local runner. Fixed at definition.
|
|
104
106
|
if (agent.runsOn !== undefined && !RUNS_ON.includes(agent.runsOn)) fail(file, `${label}.runsOn must be "hosted" or "local"`);
|
|
107
|
+
if (agent.kind !== undefined && !KINDS.includes(agent.kind)) fail(file, `${label}.kind must be one of ${KINDS.join(", ")}`);
|
|
105
108
|
if (agent.domains !== undefined && (!Array.isArray(agent.domains) || agent.domains.some((d) => typeof d !== "string" || !DOMAIN.test(d)))) fail(file, `${label}.domains must be an array of memory folders like "ui" or "ui/motion"`);
|
|
106
109
|
const unknown = Object.keys(agent).filter((k) => !AGENT_KEYS.has(k));
|
|
107
110
|
if (unknown.length) fail(file, `${label}: unknown field(s) ${unknown.join(", ")}`);
|