eve-hub 0.14.12 → 0.15.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/CHANGELOG.md +10 -0
- package/bot-model.d.mts +2 -0
- package/bot-model.mjs +22 -3
- package/fanout/SKILL.md +59 -0
- package/kit-modules.json +1 -0
- package/package.json +2 -1
- package/prepare.mjs +7 -6
- package/roster-model.mjs +12 -2
- package/skills/fanout.d.ts +2 -0
- package/skills/fanout.gen.mjs +2 -0
- package/skills/fanout.js +5 -0
- package/stubs.d.mts +1 -0
- package/stubs.mjs +29 -0
- package/subagent-cap.mjs +2 -2
- package/subagent-tiers.d.mts +5 -0
- package/subagent-tiers.mjs +66 -0
- package/sync-kit.d.mts +1 -1
- package/sync-kit.mjs +15 -6
- package/templates/execution.md +3 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# eve-hub
|
|
2
2
|
|
|
3
|
+
## 0.15.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Worker tiers: each base-team lead declares `reader`, `editor` and `verifier` subagents (read-only bots get no `editor`), scaffolded under `agents/<bot>/agent/subagents/<tier>/` in the parent's sandbox with a short prompt and no team prompt. Each tier's model is `agents[].subagents.<tier>` in hub.workspace.json, read with `bindRoster(workspace).subagentModel(bot, tier)`; the template starts `reader` and `editor` on DeepSeek V4 Pro and `verifier` on Sonnet 5.5.
|
|
8
|
+
- An existing workspace gets a tier by declaring it in hub.workspace.json (`agents[].subagents.<tier>`): the kit sync (`hub start`, the hosted build, the hosted kit bump) writes the folder of each declared tier a bot lacks and leaves one already there. The version bump alone adds no tier, and local bots lose `kit__worker`, so declare the tiers in the same commit as the bump.
|
|
9
|
+
- The default subagent cap is Opus 5.5 · low, allowing Opus 5.5, Sonnet 5.5, DeepSeek V4 Pro and MiMo V2.6 Pro. Fable is no longer allowed by default.
|
|
10
|
+
- `kit__worker` and the `eve-hub/worker-model` export are removed. DeepSeek's gateway host order (full-precision hosts, then baseten's FP4) moves into `modelOptionsFor` (`eve-hub/bot-model`).
|
|
11
|
+
- A hosted build mounts the git extension in bots only, never in a declared subagent's folder.
|
|
12
|
+
|
|
3
13
|
## 0.14.12
|
|
4
14
|
|
|
5
15
|
### Patch Changes
|
package/bot-model.d.mts
CHANGED
|
@@ -13,5 +13,7 @@ export function ownModel(agents: readonly RosterAgent[], name: string): Selectio
|
|
|
13
13
|
export function copyModel(agents: readonly RosterAgent[], cap: SubagentCap, name: string): Clamped;
|
|
14
14
|
export function subagentModels(agents: readonly RosterAgent[], cap: SubagentCap, name: string): SubagentModels;
|
|
15
15
|
export function isAgentCopy(ctx: unknown): boolean;
|
|
16
|
+
export const GATEWAY_PROVIDERS: Readonly<Record<string, readonly string[]>>;
|
|
16
17
|
export function modelOptionsFor(model: string): { providerOptions: Record<string, unknown> } | undefined;
|
|
18
|
+
export function subagentModel(agents: readonly RosterAgent[], cap: SubagentCap, bot: string, name: string): { model: string; reasoning: string; modelContextWindowTokens: number; modelOptions?: { providerOptions: Record<string, unknown> } };
|
|
17
19
|
export function selectModel(agents: readonly RosterAgent[], cap: SubagentCap, name: string, ctx: unknown): Selection & { modelContextWindowTokens: number; modelOptions?: unknown; copy: boolean };
|
package/bot-model.mjs
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Built-in `agent` copies of a bot (eve's root-copy tool) run under the same
|
|
7
7
|
// subagent cap as declared subagents: the bot's own model and reasoning,
|
|
8
8
|
// clamped (never rejected) to the cap.
|
|
9
|
-
import { capFor, clampSubagent, resolveSubagents } from "./subagent-cap.mjs";
|
|
9
|
+
import { capFor, clampSubagent, resolveSubagent, resolveSubagents } from "./subagent-cap.mjs";
|
|
10
10
|
|
|
11
11
|
export const CONTEXT_WINDOW = 200_000;
|
|
12
12
|
|
|
@@ -48,12 +48,31 @@ export function subagentModels(agents, cap, name) {
|
|
|
48
48
|
// populated in dynamic-model resolvers, so the channel is the signal.)
|
|
49
49
|
export const isAgentCopy = (ctx) => ctx?.channel?.kind === "subagent";
|
|
50
50
|
|
|
51
|
-
//
|
|
51
|
+
// AI Gateway hosts an open-weights model tries first: full-precision ones,
|
|
52
|
+
// then baseten's FP4 before the gateway's own pick. From
|
|
53
|
+
// https://ai-gateway.vercel.sh/v1/models/<id>/endpoints.
|
|
54
|
+
export const GATEWAY_PROVIDERS = Object.freeze({
|
|
55
|
+
"deepseek/deepseek-v4-pro": Object.freeze(["deepseek", "azure", "alibaba", "baseten"]),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Provider options that fit `model`, and only those. Reasoning text is opt-in
|
|
59
|
+
// on current Anthropic and OpenAI models; ask for summaries so the UI can
|
|
52
60
|
// interleave them with tool calls.
|
|
53
61
|
export function modelOptionsFor(model) {
|
|
54
62
|
if (model.startsWith("anthropic/")) return { providerOptions: { anthropic: { thinking: { type: "adaptive", display: "summarized" } } } };
|
|
55
63
|
if (model.startsWith("openai/")) return { providerOptions: { openai: { reasoningSummary: "auto" } } };
|
|
56
|
-
|
|
64
|
+
const order = GATEWAY_PROVIDERS[model];
|
|
65
|
+
return order ? { providerOptions: { gateway: { order: [...order] } } } : undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// What `bot`'s declared subagent `name` passes to defineAgent: its
|
|
69
|
+
// hub.workspace.json model, clamped to the bot's cap, with options that fit
|
|
70
|
+
// it. A subagent the roster doesn't declare is a config error, not the cap.
|
|
71
|
+
export function subagentModel(agents, cap, botName, name) {
|
|
72
|
+
if (!bot(agents, botName).subagents?.[name]) throw new Error(`"${botName}" declares no subagent "${name}" in hub.workspace.json`);
|
|
73
|
+
const { model, reasoning } = resolveSubagent(agents, cap, botName, name);
|
|
74
|
+
const options = modelOptionsFor(model);
|
|
75
|
+
return { model, reasoning, modelContextWindowTokens: CONTEXT_WINDOW, ...(options ? { modelOptions: options } : {}) };
|
|
57
76
|
}
|
|
58
77
|
|
|
59
78
|
// The selection eve's dynamic model resolver returns for one of `name`'s
|
package/fanout/SKILL.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: fanout
|
|
3
|
+
description: How to hand work to your reader, editor and verifier subagents. Load before you send any task to one, so each gets an unambiguous, non-overlapping, checkable task and you can check what comes back.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Fanning out work
|
|
7
|
+
|
|
8
|
+
Your `reader`, `editor` and `verifier` run cheaper models than you and never see your conversation. Each one gets only the task you write, so the task has to stand on its own.
|
|
9
|
+
|
|
10
|
+
## 1. The contract
|
|
11
|
+
|
|
12
|
+
Every task you send has all of these fields. If you can't fill one in, don't send the task: do it yourself or split it further.
|
|
13
|
+
|
|
14
|
+
- `goal`: one sentence describing the result.
|
|
15
|
+
- `owns`: the files the worker may change. It is empty for a reader and a verifier.
|
|
16
|
+
- `facts`: what is already decided, so the worker doesn't reopen it.
|
|
17
|
+
- `pointers`: where to look, as `file:line` or a URL. Never paste file contents.
|
|
18
|
+
- `check`: the command that proves the task is done.
|
|
19
|
+
- `budget`: a turn count. Allow 2 to 3 for setup, 1 to 2 for reading, 2 to 4 for implementing and 2 to 3 for verifying, 12 at most.
|
|
20
|
+
- `stop`: when to give up and report instead of pushing on.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
goal: Make listThreads skip archived threads unless asked.
|
|
24
|
+
owns: /workspace/wt-a/src/threads.ts, /workspace/wt-a/test/threads.test.ts
|
|
25
|
+
facts: The flag is `archived: "include"`; the default excludes them.
|
|
26
|
+
pointers: src/threads.ts:88 (the query), test/threads.test.ts:40
|
|
27
|
+
check: cd /workspace/wt-a && node --test test/threads.test.ts
|
|
28
|
+
budget: 5 turns
|
|
29
|
+
stop: if the query is built anywhere else, report where and stop.
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## 2. Disjoint ownership
|
|
33
|
+
|
|
34
|
+
- No two tasks running at the same time share a file in `owns`.
|
|
35
|
+
- Your workers share your sandbox. Give each editor running in parallel its own worktree (`git worktree add /workspace/wt-a`) and name that directory in its task.
|
|
36
|
+
- You apply their diffs to your branch one at a time.
|
|
37
|
+
- A change that cuts across files several workers own runs serially, or goes to an `agent` copy (rung 2 below).
|
|
38
|
+
|
|
39
|
+
## 3. What comes back
|
|
40
|
+
|
|
41
|
+
Each tier replies with exactly this report, inside its turn budget:
|
|
42
|
+
|
|
43
|
+
<!-- tier-reports -->
|
|
44
|
+
|
|
45
|
+
Review the diff together with the module around it, not just the changed lines. Rerun every `check` yourself before you take the change.
|
|
46
|
+
|
|
47
|
+
After a reader or verifier returns, run `git status --porcelain` in its worktree. If anything changed, discard its result and count the attempt as a failure on the ladder below: `bash` can still write files, so read-only is a prompt rule, not a guarantee.
|
|
48
|
+
|
|
49
|
+
## 4. When it fails
|
|
50
|
+
|
|
51
|
+
1. Retry once on the same tier with a fresh worker. Give it the original task, the failed diff and the output of the check.
|
|
52
|
+
2. Then give the same task to an `agent` copy of yourself.
|
|
53
|
+
3. Then take the task back and split it differently.
|
|
54
|
+
|
|
55
|
+
Never try a rung a second time. If two workers given the same task return substantively different diffs, the task is under-specified: rewrite it before you send it again.
|
|
56
|
+
|
|
57
|
+
## 5. The cap
|
|
58
|
+
|
|
59
|
+
Run at most 3 workers in parallel.
|
package/kit-modules.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eve-hub",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.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",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"./workspace-roster": "./workspace-roster.mjs",
|
|
23
23
|
"./bot-model": "./bot-model.mjs",
|
|
24
24
|
"./subagent-cap": "./subagent-cap.mjs",
|
|
25
|
+
"./subagent-tiers": "./subagent-tiers.mjs",
|
|
25
26
|
"./session-limits": "./session-limits.mjs",
|
|
26
27
|
"./main-instructions": "./main-instructions.mjs",
|
|
27
28
|
"./execution-context": "./execution-context.mjs",
|
package/prepare.mjs
CHANGED
|
@@ -125,7 +125,8 @@ export function prepareDeployment(dir) {
|
|
|
125
125
|
const dirOf = (name) => join(dir, "agents", name, "agent");
|
|
126
126
|
const local = new Set(resolved.agents.filter((a) => a.runsOn === "local").map((a) => a.name));
|
|
127
127
|
const folders = readdirSync(join(dir, "agents")).filter((name) => !local.has(name) && existsSync(join(dirOf(name), "agent.ts")));
|
|
128
|
-
const
|
|
128
|
+
const tiersOf = (name) => Object.keys(definition.agents.find((a) => a.name === name)?.subagents ?? {});
|
|
129
|
+
const change = syncKit({ bots: folders.map((name) => ({ name, runsOn: "hosted", files: readBotFiles(dirOf(name)), subagents: tiersOf(name) })), modules: KIT_MODULES });
|
|
129
130
|
if (change.report.length) throw new Error(`these files can't ship to a hosted deployment; update or delete them:\n${change.report.map((r) => ` agents/${r.bot}/agent/${r.file} ${r.reason}`).join("\n")}`);
|
|
130
131
|
writeFileSync(join(dir, MANIFEST_FILE), `${JSON.stringify(resolved, null, 2)}\n`);
|
|
131
132
|
for (const { name } of definition.agents) {
|
|
@@ -137,10 +138,10 @@ export function prepareDeployment(dir) {
|
|
|
137
138
|
writeFileSync(join(dir, "agents", "main", "agent", "channels", "manifest.ts"), MANIFEST_CHANNEL);
|
|
138
139
|
applyKit(change, dirOf);
|
|
139
140
|
const deployment = deploymentFacts();
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const bot =
|
|
143
|
-
|
|
141
|
+
// Bots only: a declared subagent's agent.ts (agent/subagents/<tier>/) is not a bot and mounts none of these.
|
|
142
|
+
for (const name of readdirSync(join(dir, "agents"))) {
|
|
143
|
+
const bot = dirOf(name);
|
|
144
|
+
if (!existsSync(join(bot, "agent.ts"))) continue;
|
|
144
145
|
const code = CODE_BOTS.includes(name);
|
|
145
146
|
for (const [stub, content] of Object.entries({ ...(deployment.githubToken ? gitExtensionFiles() : {}), ...(code ? codeExtensionFiles(name) : {}) })) {
|
|
146
147
|
mkdirSync(dirname(join(bot, stub)), { recursive: true });
|
|
@@ -152,7 +153,7 @@ export function prepareDeployment(dir) {
|
|
|
152
153
|
for (const name of readdirSync(join(dir, "agents"))) {
|
|
153
154
|
const bot = join(dir, "agents", name, "agent");
|
|
154
155
|
writeExecutionContext(bot, { bot: name, deployment });
|
|
155
|
-
// The kit's extension
|
|
156
|
+
// The kit's extension mounts only on the hub's machine and is excluded from the kit tarball.
|
|
156
157
|
// Other extensions, such as eve-code, stay.
|
|
157
158
|
for (const item of readdirSync(bot, { recursive: true })) {
|
|
158
159
|
if (item.endsWith(join("extensions", "kit.ts"))) removeStub(bot, item);
|
package/roster-model.mjs
CHANGED
|
@@ -10,11 +10,13 @@
|
|
|
10
10
|
// function has no workspace on disk).
|
|
11
11
|
// - `rosterModel(name, …)` / `rosterLimits(name)`: the same, read from disk
|
|
12
12
|
// (see workspaceRoster below), for bots scaffolded before bindRoster.
|
|
13
|
+
// - `subagentModel(bot, name, { mock })`: the model fields of `bot`'s
|
|
14
|
+
// declared subagent `name` (bot-model.mjs), or its mock in mock mode.
|
|
13
15
|
// - `createRosterModel({ agents, subagentCap })`: bound to a roster given
|
|
14
16
|
// explicitly (e.g. a test).
|
|
15
17
|
import { defineDynamic } from "eve";
|
|
16
18
|
import evePackage from "eve/package.json" with { type: "json" };
|
|
17
|
-
import { CONTEXT_WINDOW, selectModel } from "./bot-model.mjs";
|
|
19
|
+
import { CONTEXT_WINDOW, selectModel, subagentModel } from "./bot-model.mjs";
|
|
18
20
|
import { DEFAULT_SUBAGENT_CAP } from "./subagent-cap.mjs";
|
|
19
21
|
import { sessionLimits } from "./session-limits.mjs";
|
|
20
22
|
import { installModelCallTimeout } from "./model-timeout.mjs";
|
|
@@ -58,7 +60,15 @@ const rosterOf = ({ agents, humans, subagentCap, limits }) => ({ agents, humans,
|
|
|
58
60
|
// `subagentCap`) for `data`, validated the same way as the file.
|
|
59
61
|
export function bindRoster(data) {
|
|
60
62
|
const roster = rosterOf(parseWorkspace(data));
|
|
61
|
-
return {
|
|
63
|
+
return {
|
|
64
|
+
...roster,
|
|
65
|
+
rosterModel: createRosterModel(roster),
|
|
66
|
+
rosterLimits: (name) => sessionLimits(roster, name, evePackage.version),
|
|
67
|
+
subagentModel: (bot, name, { mock } = {}) => {
|
|
68
|
+
const selection = subagentModel(roster.agents, roster.subagentCap, bot, name);
|
|
69
|
+
return real ? selection : { model: mock, modelContextWindowTokens: CONTEXT_WINDOW };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
62
72
|
}
|
|
63
73
|
|
|
64
74
|
// Bots scaffolded before bindRoster read the roster from disk instead: the
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
// Generated by scripts/build-onboarding.mjs from fanout/SKILL.md. Do not edit.
|
|
2
|
+
export default {"description":"How to hand work to your reader, editor and verifier subagents. Load before you send any task to one, so each gets an unambiguous, non-overlapping, checkable task and you can check what comes back.","markdown":"# Fanning out work\n\nYour `reader`, `editor` and `verifier` run cheaper models than you and never see your conversation. Each one gets only the task you write, so the task has to stand on its own.\n\n## 1. The contract\n\nEvery task you send has all of these fields. If you can't fill one in, don't send the task: do it yourself or split it further.\n\n- `goal`: one sentence describing the result.\n- `owns`: the files the worker may change. It is empty for a reader and a verifier.\n- `facts`: what is already decided, so the worker doesn't reopen it.\n- `pointers`: where to look, as `file:line` or a URL. Never paste file contents.\n- `check`: the command that proves the task is done.\n- `budget`: a turn count. Allow 2 to 3 for setup, 1 to 2 for reading, 2 to 4 for implementing and 2 to 3 for verifying, 12 at most.\n- `stop`: when to give up and report instead of pushing on.\n\n```\ngoal: Make listThreads skip archived threads unless asked.\nowns: /workspace/wt-a/src/threads.ts, /workspace/wt-a/test/threads.test.ts\nfacts: The flag is `archived: \"include\"`; the default excludes them.\npointers: src/threads.ts:88 (the query), test/threads.test.ts:40\ncheck: cd /workspace/wt-a && node --test test/threads.test.ts\nbudget: 5 turns\nstop: if the query is built anywhere else, report where and stop.\n```\n\n## 2. Disjoint ownership\n\n- No two tasks running at the same time share a file in `owns`.\n- Your workers share your sandbox. Give each editor running in parallel its own worktree (`git worktree add /workspace/wt-a`) and name that directory in its task.\n- You apply their diffs to your branch one at a time.\n- A change that cuts across files several workers own runs serially, or goes to an `agent` copy (rung 2 below).\n\n## 3. What comes back\n\nEach tier replies with exactly this report, inside its turn budget:\n\n- `reader`, 8 turns:\n\n ```\n answer: <1-2 sentences>\n evidence: <file:line | command + output | URL>, one per line\n open: <what it couldn't settle, or \"none\">\n ```\n\n- `editor`, 12 turns:\n\n ```\n diff: <git diff of owns>\n check: <command> -> exit <n>, last 20 lines\n outside: <files that also need a change but aren't in owns, or \"none\">\n notes: <=5 lines\n ```\n\n- `verifier`, 10 turns:\n\n ```\n verdict: holds | fails | couldn't check\n runs: <command> -> exit <n>, the deciding output lines\n seen: <what is on screen, for a UI>\n other: <anything wrong outside the claim>\n ```\n\nReview the diff together with the module around it, not just the changed lines. Rerun every `check` yourself before you take the change.\n\nAfter a reader or verifier returns, run `git status --porcelain` in its worktree. If anything changed, discard its result and count the attempt as a failure on the ladder below: `bash` can still write files, so read-only is a prompt rule, not a guarantee.\n\n## 4. When it fails\n\n1. Retry once on the same tier with a fresh worker. Give it the original task, the failed diff and the output of the check.\n2. Then give the same task to an `agent` copy of yourself.\n3. Then take the task back and split it differently.\n\nNever try a rung a second time. If two workers given the same task return substantively different diffs, the task is under-specified: rewrite it before you send it again.\n\n## 5. The cap\n\nRun at most 3 workers in parallel.\n"};
|
package/skills/fanout.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { defineSkill } from "eve/skills";
|
|
2
|
+
import skill from "./fanout.gen.mjs";
|
|
3
|
+
// The fanout skill: fanout/SKILL.md, embedded at build time (scripts/build-onboarding.mjs) like
|
|
4
|
+
// the forms skill. A lead loads it before handing work to its reader, editor or verifier tiers.
|
|
5
|
+
export default defineSkill(skill);
|
package/stubs.d.mts
CHANGED
|
@@ -8,3 +8,4 @@ export function kitStub(file: string, options?: { files?: Record<string, string[
|
|
|
8
8
|
export const ROLE_FILE: string;
|
|
9
9
|
export const ROLE_DIR: string;
|
|
10
10
|
export function botInstructionsStub(name: string): string;
|
|
11
|
+
export function tierFiles(bot: string, tier: string): Record<string, string>;
|
package/stubs.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ROLE_DIR, ROLE_FILE } from "./role-files.mjs";
|
|
2
|
+
import { tierOf } from "./subagent-tiers.mjs";
|
|
2
3
|
|
|
3
4
|
// eve finds tools, hooks, and channels by file (the tool name is the file
|
|
4
5
|
// name), so each bot keeps a one-line stub per kit module. Bot file -> what it
|
|
@@ -18,6 +19,7 @@ export const KIT_FILES = {
|
|
|
18
19
|
"tools/list_agents.ts": ["The team right now: kind, place, tools, models, who's busy."],
|
|
19
20
|
"tools/post_message.ts": ["Post into an existing thread or a bot's DM; with `status`, report the thread's phase."],
|
|
20
21
|
"skills/forms.ts": ["The forms skill (packages/agent-kit/forms/SKILL.md): how to draw a flow, sequence, chart or ask the app renders."],
|
|
22
|
+
"skills/fanout.ts": ["The fanout skill (packages/agent-kit/fanout/SKILL.md): the contract for handing work to the reader, editor and verifier subagents."],
|
|
21
23
|
"skills/local-onboarding.ts": ["The local-onboarding skill (packages/agent-kit/local-onboarding/SKILL.md): the Mac half of onboarding, the live work to port and the sandbox setup."],
|
|
22
24
|
};
|
|
23
25
|
|
|
@@ -61,6 +63,7 @@ export const DEPLOYED_KIT_FILES = new Set([
|
|
|
61
63
|
...["react", "show", "fetch_upload", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
|
|
62
64
|
"memory/team.ts",
|
|
63
65
|
"skills/forms.ts",
|
|
66
|
+
"skills/fanout.ts",
|
|
64
67
|
...["create_thread", "set_summary", "move_path", "archive_thread", "unarchive_thread", "list_agents", "create_agent", "remove_agent", "restart_agent", "tag_threads", "dash_update", "apply_tags"].map((t) => `tools/${t}.ts`),
|
|
65
68
|
]);
|
|
66
69
|
|
|
@@ -167,3 +170,29 @@ export function instructionsSource(files, main) {
|
|
|
167
170
|
if (copy) return "conflict";
|
|
168
171
|
return isKitReexport(files[PROTOCOL_STUB_FILE], "instructions/protocol") ? "kit" : "custom";
|
|
169
172
|
}
|
|
173
|
+
|
|
174
|
+
// The files of `bot`'s `tier` folder, relative to the bot's agent/ folder. The text is imported, never copied.
|
|
175
|
+
export function tierFiles(bot, tier) {
|
|
176
|
+
const spec = tierOf(tier);
|
|
177
|
+
const dir = `subagents/${tier}`;
|
|
178
|
+
const name = JSON.stringify(tier);
|
|
179
|
+
return {
|
|
180
|
+
[`${dir}/agent.ts`]: `import { defineAgent } from "eve";
|
|
181
|
+
import { mockModel } from "eve/evals";
|
|
182
|
+
import { bindRoster } from "eve-hub/roster-model";
|
|
183
|
+
import { tierDescription } from "eve-hub/subagent-tiers";
|
|
184
|
+
import workspace from "../../../../../hub.workspace.json" with { type: "json" };
|
|
185
|
+
|
|
186
|
+
// Model and reasoning come from hub.workspace.json agents[${JSON.stringify(bot)}].subagents.${tier}, clamped to the bot's subagent cap.
|
|
187
|
+
const { subagentModel } = bindRoster(workspace);
|
|
188
|
+
|
|
189
|
+
export default defineAgent({
|
|
190
|
+
description: tierDescription(${name}),
|
|
191
|
+
...subagentModel(${JSON.stringify(bot)}, ${name}, { mock: mockModel(${JSON.stringify(`${tier} here (mock).`)}) }),
|
|
192
|
+
});
|
|
193
|
+
`,
|
|
194
|
+
[`${dir}/sandbox.ts`]: `import { defineParentSandbox } from "eve/sandbox";\n\nexport default defineParentSandbox();\n`,
|
|
195
|
+
[`${dir}/instructions.ts`]: `import { defineInstructions } from "eve/instructions";\nimport { tierInstructions } from "eve-hub/subagent-tiers";\n\n// The ${tier}'s prompt, from eve-hub (packages/agent-kit/subagent-tiers.mjs).\nexport default defineInstructions({ content: tierInstructions(${name}) });\n`,
|
|
196
|
+
...(spec.readOnly ? { [`${dir}/tools/write_file.ts`]: `import { disableTool } from "eve/tools";\n\n// A ${tier} never edits files.\nexport default disableTool();\n` } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
package/subagent-cap.mjs
CHANGED
|
@@ -10,9 +10,9 @@ export const REASONING_LEVELS = ["none", "minimal", "low", "medium", "high", "xh
|
|
|
10
10
|
// `models` lists what a subagent may pick at or under the cap; any other
|
|
11
11
|
// model is clamped to `model`.
|
|
12
12
|
export const DEFAULT_SUBAGENT_CAP = Object.freeze({
|
|
13
|
-
model: "anthropic/claude-
|
|
13
|
+
model: "anthropic/claude-opus-5.5",
|
|
14
14
|
reasoning: "low",
|
|
15
|
-
models: Object.freeze(["anthropic/claude-
|
|
15
|
+
models: Object.freeze(["anthropic/claude-opus-5.5", "anthropic/claude-sonnet-5.5", "deepseek/deepseek-v4-pro", "xiaomi/mimo-v2.6-pro"]),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
// Returns what the subagent actually runs on, plus a note for each field
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type SubagentTier = { readOnly: boolean; use: string; refuse: string; example: string; budget: number; stop: string; report: string; prompt: string };
|
|
2
|
+
export const SUBAGENT_TIERS: Readonly<Record<string, Readonly<SubagentTier>>>;
|
|
3
|
+
export function tierOf(tier: string): Readonly<SubagentTier>;
|
|
4
|
+
export function tierDescription(tier: string): string;
|
|
5
|
+
export function tierInstructions(tier: string): string;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// The worker tiers a lead bot fans work out to, as declared subagents (eve: agent/subagents/<tier>/).
|
|
2
|
+
// A bot gets a tier by declaring it in hub.workspace.json agents[].subagents.<tier>; syncKit
|
|
3
|
+
// (sync-kit.mjs) writes the folder of each declared tier the bot lacks, so a workspace made
|
|
4
|
+
// before a tier existed gets it the same way a new one does. Each shares its parent's sandbox,
|
|
5
|
+
// so it works on the parent's files; its model is the roster's, clamped to the bot's cap.
|
|
6
|
+
// `readOnly` tiers can't write files. A worker has no team prompt: it never posts or hands off.
|
|
7
|
+
//
|
|
8
|
+
// This module is the one source of each tier's text. The folder's agent.ts and instructions.ts
|
|
9
|
+
// import it from eve-hub instead of holding a copy, so a kit bump changes what every existing
|
|
10
|
+
// workspace's lead and workers read. The lead chooses a tier from its tool description alone
|
|
11
|
+
// (tierDescription); the fanout skill's report table is generated from `report`. The folder's
|
|
12
|
+
// files themselves are stubs.mjs tierFiles: this module holds only text, so a deployed bot imports nothing else.
|
|
13
|
+
// When the lead hands work to tiers: to run its pieces at once, never to offload one task. The execution template says the same sentence (test/fanout-skill.test.mjs).
|
|
14
|
+
export const ROUTING_RULE = "Fan out work that splits into independent pieces: one tier call per piece, all in one step. A single task stays with you, however long. Judgment always stays with you.";
|
|
15
|
+
|
|
16
|
+
// Null prototype: a lookup by a roster name sees only the tiers, never `constructor` or `__proto__`.
|
|
17
|
+
export const SUBAGENT_TIERS = Object.freeze(Object.assign(Object.create(null), {
|
|
18
|
+
reader: Object.freeze({
|
|
19
|
+
readOnly: true,
|
|
20
|
+
use: "you can state the question without your conversation, and the answer is a fact.",
|
|
21
|
+
refuse: "judgment calls, or Kit or GitHub data not yet in the sandbox (workers have no connections).",
|
|
22
|
+
example: "\"What installs tier folders into an existing workspace on a kit bump?\" (a file:line).",
|
|
23
|
+
budget: 8,
|
|
24
|
+
stop: "the question is ambiguous, the evidence runs out, or answering would mean changing a file.",
|
|
25
|
+
report: "answer: <1-2 sentences>\nevidence: <file:line | command + output | URL>, one per line\nopen: <what it couldn't settle, or \"none\">",
|
|
26
|
+
prompt: "You are a reader: a worker that a lead bot delegates reading to. You share its sandbox, so you see the same files it does. You get one question with everything you need; you never see the caller's conversation.\n\n- Answer exactly the question. Read code, history (git log, blame) and docs; search the web only when the question needs it.\n- Never change a file, commit, push or install anything. Shell commands are for reading.\n- Back every claim with evidence: a file:line, a command and its output, or a link. Keep facts apart from guesses, and say which is which.",
|
|
27
|
+
}),
|
|
28
|
+
editor: Object.freeze({
|
|
29
|
+
readOnly: false,
|
|
30
|
+
use: "two editors given your spec would make the same diff, and a command decides whether it's done.",
|
|
31
|
+
refuse: "design across files (SQL, migration and tests together: you or an `agent` copy), or Swift layout (judgment, and no Swift compiler here).",
|
|
32
|
+
example: "replace a named sentence in a prompt and assert it in a named test file.",
|
|
33
|
+
budget: 12,
|
|
34
|
+
stop: "a file outside `owns` must change (name it under `outside:` instead of editing it), the check fails twice, or the spec is ambiguous.",
|
|
35
|
+
report: "diff: <git diff of owns>\ncheck: <command> -> exit <n>, last 20 lines\noutside: <files that also need a change but aren't in owns, or \"none\">\nnotes: <=5 lines",
|
|
36
|
+
prompt: "You are an editor: a worker that a lead bot delegates file edits to. You share its sandbox, so you edit the same files it reviews. You get one task with everything you need; you never see the caller's conversation.\n\n- Do exactly the task. Touch only the files in `owns`; if another file must change, stop and report which one and why. No drive-by refactors, renames, reformatting, or new dependencies.\n- Work in the worktree the task names, with absolute paths. Read a file before you change it. Match the surrounding style.\n- Run the task's `check`. Don't commit, push, or install anything unless the task says to.",
|
|
37
|
+
}),
|
|
38
|
+
verifier: Object.freeze({
|
|
39
|
+
readOnly: true,
|
|
40
|
+
use: "you can name the command or screen that decides the claim.",
|
|
41
|
+
refuse: "native or Swift checks in a hosted sandbox (CI or a Mac bot), or whether a design is right (a reviewer's call).",
|
|
42
|
+
example: "\"The sync and tier tests pass at the PR head\" (it runs them).",
|
|
43
|
+
budget: 10,
|
|
44
|
+
stop: "the check can't run as given: say why instead of running a weaker check.",
|
|
45
|
+
report: "verdict: holds | fails | couldn't check\nruns: <command> -> exit <n>, the deciding output lines\nseen: <what is on screen, for a UI>\nother: <anything wrong outside the claim>",
|
|
46
|
+
prompt: "You are a verifier: a worker that a lead bot delegates checks to. You share its sandbox, so you check the same files and builds it has. You get one claim to check and how to check it; you never see the caller's conversation.\n\n- Check the claim on the real path: run the tests, the build, or the app itself, with real inputs. Reading the diff is not a check. To see a screenshot, open the image with read_file.\n- Never change a file, commit, push or install anything beyond what the check itself needs (a dependency install, a dev server). Stop any server you start.\n- A suite can take minutes. When bash says a command is still running, check on it at the paths bash named for that command, never another job's, until its exit file is written. Decide from that exit code and the output it names, never from a `tail` of it or a timeout of your own.\n- Report what you saw, not what you expected.",
|
|
47
|
+
}),
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
export const tierOf = (tier) => {
|
|
51
|
+
const spec = SUBAGENT_TIERS[tier];
|
|
52
|
+
if (!spec) throw new Error(`unknown subagent tier ${JSON.stringify(tier)} (expected ${Object.keys(SUBAGENT_TIERS).join(", ")})`);
|
|
53
|
+
return spec;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// What the lead's model reads when it picks a tool: that the tier costs less, when to use it, when to do it itself, one example.
|
|
57
|
+
export function tierDescription(tier) {
|
|
58
|
+
const { use, refuse, example } = tierOf(tier);
|
|
59
|
+
return `Use when ${use} Not for ${refuse} ${ROUTING_RULE} Example: ${example}`;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// The worker's whole prompt: who it is, its rules, its turn budget, when it stops, and the report it returns.
|
|
63
|
+
export function tierInstructions(tier) {
|
|
64
|
+
const { prompt, budget, stop, report } = tierOf(tier);
|
|
65
|
+
return `${prompt}\n- You have ${budget} turns. Stop and report when ${stop}\n\nYour task follows the fanout contract. Reply with exactly this report, no preamble:\n\n\`\`\`\n${report}\n\`\`\`\n`;
|
|
66
|
+
}
|
package/sync-kit.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type KitBot = { name: string; runsOn: 'local' | 'hosted'; files: Record<string, string
|
|
1
|
+
export type KitBot = { name: string; runsOn: 'local' | 'hosted'; files: Record<string, string>; subagents?: string[] };
|
|
2
2
|
export type KitChange = {
|
|
3
3
|
write: { bot: string; file: string; content: string }[];
|
|
4
4
|
delete: { bot: string; file: string }[];
|
package/sync-kit.mjs
CHANGED
|
@@ -6,8 +6,9 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, rmdirSync, unlinkSync
|
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
import {
|
|
8
8
|
DEPLOYED_KIT_FILES, ROLE_DIR, EXECUTION_CONTEXT_STUB_FILE, ROLE_FILE, KIT_FILES, MAIN_INSTRUCTIONS, MAIN_INSTRUCTIONS_STUB, MAIN_KIT_FILES, MEMORY_STUBS, PROTOCOL_STUB, PROTOCOL_STUB_FILE,
|
|
9
|
-
botInstructionsStub, executionContextStub, instructionsSource, isKitReexport, kitPath, kitStub,
|
|
9
|
+
botInstructionsStub, executionContextStub, instructionsSource, isKitReexport, kitPath, kitStub, tierFiles,
|
|
10
10
|
} from "./stubs.mjs";
|
|
11
|
+
import { SUBAGENT_TIERS } from "./subagent-tiers.mjs";
|
|
11
12
|
|
|
12
13
|
const KIT_DIRS = ["tools", "skills", "hooks", "channels", "schedules", "instructions", "memory", "connections"];
|
|
13
14
|
const SUBAGENT = /^(?:subagents\/[^/]+\/)+/;
|
|
@@ -25,7 +26,8 @@ const HOST_ONLY_MODULES = new Set(HOST_ONLY.map(moduleOf));
|
|
|
25
26
|
/** Whether `file` (relative to a bot's agent/ folder, subagents included) is one syncKit reads: a stub, or any source that holds a stub's slot. */
|
|
26
27
|
const ROLE_ADDITION = /^role\/[^/]+\.md$/;
|
|
27
28
|
const LEGACY_ADDITION = /^instructions\/[^/]+\.md$/;
|
|
28
|
-
|
|
29
|
+
const SUBAGENT_DEFINITION = /^(?:subagents\/[^/]+\/)+agent\.ts$/;
|
|
30
|
+
export const isKitFile = (file) => file === "instructions.md" || file === MAIN_INSTRUCTIONS || file === ROLE_FILE || ROLE_ADDITION.test(file) || SUBAGENT_DEFINITION.test(file) || isSource(file);
|
|
29
31
|
|
|
30
32
|
// Every kit module the file imports, from its import and export lines only.
|
|
31
33
|
function kitImports(text) {
|
|
@@ -34,8 +36,11 @@ function kitImports(text) {
|
|
|
34
36
|
}
|
|
35
37
|
|
|
36
38
|
/**
|
|
37
|
-
* bots: [{ name, runsOn, files }], files being { "<path under agents/<name>/agent>": text } for the files
|
|
38
|
-
* isKitFile accepts (a subagent's are under "subagents/<x>/")
|
|
39
|
+
* bots: [{ name, runsOn, files, subagents }], files being { "<path under agents/<name>/agent>": text } for the files
|
|
40
|
+
* isKitFile accepts (a subagent's are under "subagents/<x>/"), subagents the names the bot declares in
|
|
41
|
+
* hub.workspace.json (agents[].subagents). A declared worker tier (subagent-tiers.mjs) with no
|
|
42
|
+
* subagents/<tier>/agent.ts gets its folder written; one that has it is the workspace's and left.
|
|
43
|
+
* modules: every kit module a stub may import
|
|
39
44
|
* ("tools/post_message"). runsOn "local" gets the whole kit, execution-context and memory stubs included;
|
|
40
45
|
* "hosted" only the stubs a Vercel function can run (DEPLOYED_KIT_FILES) and loses the others, which the
|
|
41
46
|
* build writes itself (memory, execution context) or cannot use.
|
|
@@ -50,7 +55,7 @@ function kitImports(text) {
|
|
|
50
55
|
export function syncKit({ bots, modules }) {
|
|
51
56
|
const kit = new Set(modules);
|
|
52
57
|
const change = { write: [], delete: [], report: [], instructions: {} };
|
|
53
|
-
for (const { name: bot, runsOn, files: found } of bots) {
|
|
58
|
+
for (const { name: bot, runsOn, files: found, subagents = [] } of bots) {
|
|
54
59
|
if (runsOn !== "local" && runsOn !== "hosted") throw new Error(`${bot}: runsOn must be "local" or "hosted", not ${JSON.stringify(runsOn)}`);
|
|
55
60
|
const local = runsOn === "local";
|
|
56
61
|
const main = bot === "main";
|
|
@@ -61,6 +66,10 @@ export function syncKit({ bots, modules }) {
|
|
|
61
66
|
files[file] = content;
|
|
62
67
|
change.write.push({ bot, file, content });
|
|
63
68
|
};
|
|
69
|
+
for (const tier of subagents) {
|
|
70
|
+
if (!SUBAGENT_TIERS[tier] || has(`subagents/${tier}/agent.ts`)) continue;
|
|
71
|
+
for (const [file, content] of Object.entries(tierFiles(bot, tier))) write(file, content);
|
|
72
|
+
}
|
|
64
73
|
// A bot with a role.md gets its whole prompt from instructions.ts; the shared-sections stub would repeat it,
|
|
65
74
|
// and an instructions.md beside it would put the old text first, so that is a conflict, found before
|
|
66
75
|
// anything is changed: the bot keeps what it has.
|
|
@@ -132,7 +141,7 @@ export function readBotFiles(agentDir, prefix = "") {
|
|
|
132
141
|
read(MAIN_INSTRUCTIONS);
|
|
133
142
|
read(ROLE_FILE);
|
|
134
143
|
if (existsSync(join(agentDir, ROLE_DIR))) for (const entry of readdirSync(join(agentDir, ROLE_DIR), { withFileTypes: true })) if (entry.isFile() && ROLE_ADDITION.test(`${ROLE_DIR}/${entry.name}`)) read(`${ROLE_DIR}/${entry.name}`);
|
|
135
|
-
}
|
|
144
|
+
} else read("agent.ts");
|
|
136
145
|
for (const dir of KIT_DIRS) {
|
|
137
146
|
if (!existsSync(join(agentDir, dir))) continue;
|
|
138
147
|
for (const entry of readdirSync(join(agentDir, dir), { withFileTypes: true })) if (entry.isFile() && isKitFile(`${dir}/${entry.name}`)) read(`${dir}/${entry.name}`);
|
package/templates/execution.md
CHANGED
|
@@ -15,11 +15,9 @@
|
|
|
15
15
|
- Why: every question costs the human a context switch, and most answers are in the code or the history.
|
|
16
16
|
- **Split the work**
|
|
17
17
|
- Do the part your role owns; hand the rest to the role that owns it, because skipping a role skips its check.
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
- A delegation carries the goal, what's decided, what to return and what not to touch; the receiver starts with none of your context.
|
|
22
|
-
- A subagent with no commit, push or verdict in 30 minutes is stuck: cancel it and finish the work yourself.
|
|
18
|
+
- Your reader, editor and verifier run on models far cheaper than yours: reading goes to reader, specified edits to editor, pass/fail checks (running tests included) to verifier. When work splits, count the pieces before you route: three files to trace is three readers, two test runs to check is two verifiers. Never merge pieces, and never keep one of them yourself. One question, one edit or one test run, with nothing beside it, is a single task, not a piece. Fan out work that splits into independent pieces: one tier call per piece, all in one step. A single task stays with you, however long. Judgment always stays with you.
|
|
19
|
+
- Before you hand work to a reader, editor, verifier or `agent` copy, load the `fanout` skill and follow it: the receiver starts with none of your context.
|
|
20
|
+
- A subagent with no commit, push, report or verdict in 30 minutes is stuck: cancel it and finish the work yourself.
|
|
23
21
|
- **Waiting**
|
|
24
22
|
- Never sleep or poll more than 60 seconds in one command. Check CI or a deploy once, report, and say what you're waiting on.
|
|
25
23
|
- While work runs, post at least every 30 minutes: a result, or the one thing blocking you.
|