eve-hub 0.7.0 → 0.8.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 CHANGED
@@ -1,5 +1,17 @@
1
1
  # eve-hub
2
2
 
3
+ ## 0.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `agent_send` is gone. `post_message` takes a `policy` (`queue`, `steer` or `abort`) that says how a post reaches the one bot it @-mentions: queue behind its turn (the default), fold into its running turn, or stop its turn and start a fresh one on the text. Every bot has it. The team model no longer draws a `steer` edge or carries the sentence about who can steer.
8
+
9
+ ## 0.7.1
10
+
11
+ ### Patch Changes
12
+
13
+ - A subagent's spawn call stays running through the parent's next step until its task settles, so a working card keeps its subagent orbs.
14
+
3
15
  ## 0.7.0
4
16
 
5
17
  ### Minor Changes
package/README.md CHANGED
@@ -5,7 +5,7 @@ The bot-side code every hub agent shares (milestone M3 in [`docs/architecture`](
5
5
  | Module | What it is | Bots use it as |
6
6
  |---|---|---|
7
7
  | `tools/update_thread.ts`, `list_threads.ts`, `read_thread.ts`, `react.ts`, `show.ts`, `attach_image.ts`, `browser.ts` | Registry and workspace tools (`defineTool`) | `agent/tools/<same name>.ts` |
8
- | `tools/create_thread.ts`, `post_message.ts`, `archive_thread.ts`, `unarchive_thread.ts`, `list_agents.ts`, `create_agent.ts`, `remove_agent.ts`, `restart_agent.ts`, `agent_send.ts`, `tag_threads.ts`, `apply_tags.ts` | The orchestrator's tools | `main`'s `agent/tools/<same name>.ts` |
8
+ | `tools/create_thread.ts`, `post_message.ts`, `archive_thread.ts`, `unarchive_thread.ts`, `list_agents.ts`, `create_agent.ts`, `remove_agent.ts`, `restart_agent.ts`, `tag_threads.ts`, `apply_tags.ts` | The orchestrator's tools | `main`'s `agent/tools/<same name>.ts` |
9
9
  | `hooks/sandbox.ts` | Migrates legacy Docker sessions to the host sandbox (`defineHook`) | `agent/hooks/sandbox.ts` |
10
10
  | `instructions/main.ts`, `main-instructions.mjs` | main's instructions (`defineInstructions`), rendered at build time from the kit's `templates/main-instructions.md` + `workspace-protocol.md`, so no workspace carries a copy | `main`'s `agent/instructions.ts`; workspace-only additions in `agent/instructions/*.md` |
11
11
  | `instructions/protocol.ts` | The shared protocol alone, rendered from `templates/workspace-protocol.md` followed by `templates/engineering.md` | every other bot's `agent/instructions/protocol.ts`, appended after its own `instructions.md` (who the bot is) |
package/kit-modules.json CHANGED
@@ -13,7 +13,6 @@
13
13
  "schedules/dash",
14
14
  "schedules/prune",
15
15
  "skills/onboarding-profile",
16
- "tools/agent_send",
17
16
  "tools/apply_tags",
18
17
  "tools/archive_thread",
19
18
  "tools/attach_image",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eve-hub",
3
- "version": "0.7.0",
3
+ "version": "0.8.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",
package/stubs.mjs CHANGED
@@ -33,7 +33,6 @@ export const MAIN_KIT_FILES = {
33
33
  "tools/remove_agent.ts": ["Remove a bot from the workspace."],
34
34
  "tools/restart_agent.ts": ["Restart one bot's server."],
35
35
  "tools/restart_hub.ts": ["Restart the whole hub; this turn is re-delivered once it's back."],
36
- "tools/agent_send.ts": ["Queue for, steer, or abort another bot's session."],
37
36
  "tools/tag_threads.ts": ["Thread tags: the vocabulary, and staged backfill / merge proposals."],
38
37
  "tools/dash_update.ts": ["Set Dash entries: importance, lane, reason and deferred per thread."],
39
38
  "tools/apply_tags.ts": ["Retag threads, merge or rename tags: applies a staged proposal or a new batch directly."],
@@ -59,7 +58,7 @@ export const DEPLOYED_KIT_FILES = new Set([
59
58
  "tools/onboarding_scripts.ts",
60
59
  ...["react", "show", "update_thread", "list_threads", "read_thread", "post_message"].map((t) => `tools/${t}.ts`),
61
60
  "memory/team.ts",
62
- ...["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`),
61
+ ...["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`),
63
62
  ]);
64
63
 
65
64
  // ---- stub text: what each kit stub file says ----
package/team.mjs CHANGED
@@ -16,9 +16,8 @@ export const NODE_KINDS = [...KINDS, "other", "human"];
16
16
 
17
17
  const sha = (text, length) => createHash("sha256").update(text).digest("hex").slice(0, length);
18
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.
19
+ // Selectors: "human" or { kind: [...] }, both picking nodes by kind. `text` is the sentence the team prompt carries when the section
20
+ // has at least one edge.
22
21
  export const SECTIONS = [
23
22
  { id: "partner", edge: { kind: "partner", from: "human", to: { kind: ["coordinator"] } }, text: "The coordinator works with the human directly." },
24
23
  { id: "route", edge: { kind: "route", from: { kind: ["coordinator"] }, to: { kind: ["planner", "builder"] } }, text: "The coordinator routes work to planners and builders." },
@@ -27,7 +26,6 @@ export const SECTIONS = [
27
26
  { id: "fixfirst", edge: { kind: "fixfirst", from: { kind: ["reviewer"] }, to: { kind: ["builder"] } }, text: "Reviewers send fixes back to the builder that posted the build." },
28
27
  { id: "ship", edge: { kind: "ship", from: { kind: ["reviewer"] }, to: "human" }, text: "A reviewer's ship verdict goes to the human." },
29
28
  { 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
29
  ];
32
30
  export const EDGE_KINDS = SECTIONS.map((section) => section.edge.kind);
33
31
  export const SECTIONS_HASH = sha(JSON.stringify(SECTIONS), 8);
@@ -36,7 +34,7 @@ const pick = (selector, nodes) =>
36
34
  nodes.filter((n) => {
37
35
  if (selector === "human") return n.kind === "human";
38
36
  if (n.kind === "human") return false;
39
- return selector === "bots" || (selector.kind ? selector.kind.includes(n.kind) : Boolean(n.tools?.includes(selector.tool)));
37
+ return selector.kind.includes(n.kind);
40
38
  });
41
39
 
42
40
  export function teamEdges(nodes) {
@@ -86,9 +84,8 @@ export function teamText(model) {
86
84
  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
85
  }
88
86
  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)]))}`] : [];
87
+ const rules = SECTIONS.flatMap(({ edge, text }) => {
88
+ return model.edges.some((e) => e.kind === edge.kind) ? [`- ${text}`] : [];
92
89
  });
93
90
  if (rules.length) lines.push("", "Handoffs:", ...rules);
94
91
  return lines.join("\n");
@@ -1,7 +1,7 @@
1
1
  You are **main**, the configured human's long-lived partner and the orchestrator of this workspace. The human talks to you first.
2
2
 
3
3
  ## Know the team at runtime
4
- You don't know in advance which bots exist or what they're good at, and the team changes. Every message you get lists the current team with each bot's role; `list_agents` also tells you each bot's model, what its subagents and `agent` copies are capped to, and what it's doing right now. Check before you route. `list_threads` / `read_thread` show every thread in the workspace, whichever session you're in. `agent_send` reaches a bot's session in a thread: `queue` (default) posts your input as its next turn, `steer` lands it in the turn that's running now, and `abort` stops the turn (nothing re-delivered, no restart); abort with input means "stop, do this instead".
4
+ You don't know in advance which bots exist or what they're good at, and the team changes. Every message you get lists the current team with each bot's role; `list_agents` also tells you each bot's model, what its subagents and `agent` copies are capped to, and what it's doing right now. Check before you route. `list_threads` / `read_thread` show every thread in the workspace, whichever session you're in.
5
5
 
6
6
  ## Route like a world-class lead
7
7
  - Send each piece of work straight to the bot whose stated role fits it best. There is no fixed pipeline; match the work, not a habit.
@@ -2,6 +2,7 @@ declare const _default: import("eve/tools").ToolDefinition<{
2
2
  threadId?: string | undefined;
3
3
  agent?: string | undefined;
4
4
  text: string;
5
+ policy?: "abort" | "queue" | "steer" | undefined;
5
6
  status?: "done" | "planning" | "reviewing" | "working" | undefined;
6
7
  metadata?: {
7
8
  pr?: string | undefined;
@@ -27,6 +28,7 @@ declare const _default: import("eve/tools").ToolDefinition<{
27
28
  threadId?: string | undefined;
28
29
  agent?: string | undefined;
29
30
  text: string;
31
+ policy?: "abort" | "queue" | "steer" | undefined;
30
32
  status?: "done" | "planning" | "reviewing" | "working" | undefined;
31
33
  metadata?: {
32
34
  pr?: string | undefined;
@@ -10,15 +10,20 @@ import { registryFetch } from "../registry-fetch.mjs";
10
10
  // target thread, if any, isn't woken; it sees the message as yours the next
11
11
  // time it's addressed.
12
12
  //
13
+ // `policy` is how a post reaches the one bot it @-mentions: queue (default)
14
+ // waits for that bot's turn to end, steer folds into its running turn, abort
15
+ // stops its turn and the text starts its next one.
16
+ //
13
17
  // A status post: `status` is the thread's phase after it (the hub checks the
14
18
  // move), the text is its summary, `metadata` its PR and sha. The hub keeps the
15
19
  // latest one per bot and thread for the workspace state and the Dash.
16
20
  export default defineTool({
17
- description: "Post a message as you. Leave out threadId and agent to post into the thread you're in, while you work: posting doesn't end your turn, and when a post is your turn's last act your end-of-turn text isn't posted. Or post into another workspace thread or a bot's DM. @-mentions wake bots the same way your replies do (at most one, never yourself); `@all` wakes every participant; each bot's gate decides whether it replies. Posting into a bot's DM wakes that bot. Archived threads are refused. A status post sets the thread's phase: planning, working, reviewing or done; allowed moves are start → planning or working, planning → working, working → reviewing, reviewing → working (fix first), planning (rethink) or done. Any other move is refused with the allowed next phases. The text is a phase marker, not a report: at most 8 plain words on what is happening now (\"Building the input-count fix\"), with no bot or role names, no restating the phase, no parentheticals. Test results, branch names and caveats go in a separate message. The PR URL and sha go in metadata, never in the text.",
21
+ description: "Post a message as you. Leave out threadId and agent to post into the thread you're in, while you work: posting doesn't end your turn, and when a post is your turn's last act your end-of-turn text isn't posted. Or post into another workspace thread or a bot's DM. @-mentions wake bots the same way your replies do (at most one, never yourself); `@all` wakes every participant; each bot's gate decides whether it replies. Posting into a bot's DM wakes that bot. Archived threads are refused. `policy` says how a post reaches the one bot it @-mentions: queue (default) runs as its next turn, or starts one now if it is idle; steer lands in its RUNNING turn at the next step boundary and the bot folds it in without a restart, to correct course mid-turn; abort stops its turn in that thread (running commands are killed, the process keeps running) and the text starts its fresh turn, \"stop, do this instead\". Steer and abort need exactly one @-mention, not `@all`, and cut into work already under way: use them only to change what a running turn is doing. A status post sets the thread's phase: planning, working, reviewing or done; allowed moves are start → planning or working, planning → working, working → reviewing, reviewing → working (fix first), planning (rethink) or done. Any other move is refused with the allowed next phases. The text is a phase marker, not a report: at most 8 plain words on what is happening now (\"Building the input-count fix\"), with no bot or role names, no restating the phase, no parentheticals. Test results, branch names and caveats go in a separate message. The PR URL and sha go in metadata, never in the text.",
18
22
  inputSchema: z.object({
19
23
  threadId: z.string().optional().describe("Another thread's id, e.g. thr_0002, or a DM id, dm-<bot>. Omit it to post into the thread you're in."),
20
24
  agent: z.string().optional().describe("Post into this bot's DM instead of a thread."),
21
25
  text: z.string().describe("The message. @-mention the one bot who should act. On a status post: at most 8 plain words on what is happening now."),
26
+ policy: z.enum(["queue", "steer", "abort"]).optional().describe("How the post reaches the one bot it @-mentions: queue (default), steer into its running turn, or abort its turn and start a fresh one on this text."),
22
27
  status: z.enum(["planning", "working", "reviewing", "done"]).optional().describe("The thread's phase after this post. Makes it a status post."),
23
28
  metadata: z
24
29
  .object({
@@ -34,12 +39,12 @@ export default defineTool({
34
39
  .describe("With a verdict: what the human has to pick. [] when nothing; required with ship."),
35
40
  tested: z.string().optional().describe("With a verdict: what you ran to check it, in a sentence (shown on the Dash row)."),
36
41
  }),
37
- async execute({ threadId, agent, text, status, metadata, verdict, decisions, tested }, ctx) {
42
+ async execute({ threadId, agent, text, policy, status, metadata, verdict, decisions, tested }, ctx) {
38
43
  const target = threadId ?? (agent ? `dm-${agent.replace(/^@/, "")}` : "here");
39
44
  const res = await registryFetch(`/api/threads/${encodeURIComponent(target)}/messages`, {
40
45
  method: "POST",
41
46
  headers: { "content-type": "application/json" },
42
- body: JSON.stringify({ text, sessionId: ctx.session.id, ...(status ? { status } : {}), ...(metadata ? { metadata } : {}), ...(verdict ? { verdict } : {}), ...(decisions ? { decisions } : {}), ...(tested ? { tested } : {}) }),
47
+ body: JSON.stringify({ text, sessionId: ctx.session.id, ...(policy ? { policy } : {}), ...(status ? { status } : {}), ...(metadata ? { metadata } : {}), ...(verdict ? { verdict } : {}), ...(decisions ? { decisions } : {}), ...(tested ? { tested } : {}) }),
43
48
  });
44
49
  if (!res.ok)
45
50
  throw new Error(`post_message failed: ${res.status} ${await res.text()}`);
package/world.mjs CHANGED
@@ -10925,7 +10925,7 @@ function reduceActivity(turn, { type, data = {} }, at, onCutOff) {
10925
10925
  changed = trackTaskEvent(turn.card, { type, data }, new Date(at).toISOString()) || changed;
10926
10926
  if (changed) turn.taskCard = taskCardView(turn.card);
10927
10927
  if (type === "step.started") {
10928
- for (const tool of turn.tools) if (tool.status === "running" && tool.step !== void 0 && tool.step <= data.stepIndex) {
10928
+ for (const tool of turn.tools) if (!tool.task && tool.status === "running" && tool.step !== void 0 && tool.step <= data.stepIndex) {
10929
10929
  tool.status = "interrupted";
10930
10930
  changed = true;
10931
10931
  }
@@ -1,16 +0,0 @@
1
- declare const _default: import("eve/tools").ToolDefinition<{
2
- agent: string;
3
- threadId?: string | undefined;
4
- input?: string | undefined;
5
- policy?: "abort" | "queue" | "steer" | undefined;
6
- why?: string | undefined;
7
- }, any> & {
8
- execute(input: {
9
- agent: string;
10
- threadId?: string | undefined;
11
- input?: string | undefined;
12
- policy?: "abort" | "queue" | "steer" | undefined;
13
- why?: string | undefined;
14
- }, ctx: import("eve/tools").ToolContext): Promise<any>;
15
- };
16
- export default _default;
@@ -1,36 +0,0 @@
1
- import { defineTool } from "eve/tools";
2
- import { z } from "zod";
3
- import { registryFetch } from "../registry-fetch.mjs";
4
- export default defineTool({
5
- description: [
6
- "Send to another bot's session in a thread, choosing how it lands:",
7
- "- queue (default): `input` becomes a message from you to the bot in `threadId`, posted like any other message. If the bot is busy there it runs as its next turn; if idle, it starts now.",
8
- "- steer: `input` lands in the bot's RUNNING turn at its next step boundary; it folds it in and keeps going (no cancel, no restart). Use it to correct course mid-turn.",
9
- "- abort: stop the bot's turn in `threadId` (or, without threadId and input, every busy turn it has). Its running commands are killed and it's left idle; its process keeps running (unlike restart_agent) and the aborted turn is never re-delivered. With `input`, that message is posted right after and becomes its fresh turn: \"stop, do this instead\" in one call.",
10
- 'Returns { result: "queued" | "steered" | "aborted" | "not busy there", ... }. On "not busy there" from steer, send with queue instead.',
11
- ].join("\n"),
12
- inputSchema: z.object({
13
- agent: z.string().describe("The bot, a bot's name, as the workspace state lists it."),
14
- threadId: z.string().optional().describe("The thread, e.g. thr_006b. Needed for queue and steer, and for abort with input."),
15
- input: z.string().optional().describe("What to tell the bot, written to it. Needed for queue and steer; optional for abort."),
16
- policy: z.enum(["queue", "steer", "abort"]).optional().describe("queue (default), steer, or abort."),
17
- why: z.string().optional().describe("abort only: one line shown in the thread, e.g. 'wrong branch'."),
18
- }),
19
- async execute({ agent, threadId, input, policy = "queue", why }, ctx) {
20
- const res = await registryFetch(`/api/agents/${encodeURIComponent(agent)}/send`, {
21
- method: "POST",
22
- headers: { "content-type": "application/json" },
23
- body: JSON.stringify({ policy, threadId, input, why, sessionId: ctx.session.id }),
24
- });
25
- if (!res.ok) {
26
- const text = await res.text();
27
- // The registry's catch-all 404: it has no /send route at all, i.e. the
28
- // hub is still running a build from before agent_send. Say so, rather
29
- // than "not found", which reads like the bot or turn is missing.
30
- if (res.status === 404 && /^\{"error":"not found"\}$/.test(text.trim()))
31
- throw new Error("agent_send failed: the hub registry has no /send route (it's running an older build); restart the hub, then retry");
32
- throw new Error(`agent_send failed: ${res.status} ${text}`);
33
- }
34
- return await res.json();
35
- },
36
- });