@kevin5251984/guild 0.2.17 → 0.2.19

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/src/tools.ts CHANGED
@@ -75,8 +75,25 @@ export type ToolContext = {
75
75
  args: Record<string, unknown>,
76
76
  ctx: ToolContext,
77
77
  ) => Promise<ToolOutcome>;
78
+ /** Devin-style background spawn handles for this turn. Same Map across dispatch clones. */
79
+ spawnHandles?: Map<string, SpawnHandle>;
78
80
  };
79
81
 
82
+ export type SpawnHandle = {
83
+ id: string;
84
+ title: string;
85
+ profile: string;
86
+ done: Promise<ToolOutcome>;
87
+ outcome?: ToolOutcome;
88
+ abort?: AbortController;
89
+ };
90
+
91
+ /** Pin the turn's handle Map before dispatch spreads a rest clone. */
92
+ export function attachSpawnHandles(ctx: ToolContext): Map<string, SpawnHandle> {
93
+ if (!ctx.spawnHandles) ctx.spawnHandles = new Map();
94
+ return ctx.spawnHandles;
95
+ }
96
+
80
97
  const BASE_TOOLS: Tool[] = [
81
98
  {
82
99
  name: "run",
@@ -178,7 +195,7 @@ export function guildTools(
178
195
  name: Type.String({ description: "Skill name or slug" }),
179
196
  }),
180
197
  });
181
- if ((ctx.spawnDepth ?? 0) < 1 && sandbox !== "read_only") {
198
+ if ((ctx.spawnDepth ?? 0) < 1) {
182
199
  const agents = ctx.subagents ?? [];
183
200
  const listed = agents
184
201
  .slice(0, 40)
@@ -190,14 +207,28 @@ export function guildTools(
190
207
  const catalog = listed ? ` Available: ${listed}.` : "";
191
208
  tools.push({
192
209
  name: "spawn",
193
- description: `Spawn a subagent with a fresh context. It returns a summary, not a transcript. Use for parallel exploration, review, or a bounded implementation slice.${catalog} Subagents cannot spawn children.`,
210
+ description: `Delegate to a specialist (Devin run_subagent / Pi subagent / Codex spawn_agent). Fresh context; returns a summary, not a transcript. You stay coordinator. Single: title + task + profile (aliases: description/prompt, name/agent). Profiles: explorer (read-only survey), reviewer (read-only critique), worker (bounded patch). luna-explore maps to explorer, luna-general to worker. Independent surveys: background=true (is_background), then read_spawn with the agent_id before the final reply. Parallel: several spawn calls this round, or tasks: [{title, task, profile}, ...] (max 8, 4 at a time). Do not spawn for one known file or a one-line change.${catalog} A read_only parent still spawns; the child stays read_only. Subagents cannot spawn children.`,
194
211
  parameters: Type.Object({
195
- prompt: Type.String({
196
- description: "Self-contained task for the subagent. Include paths, constraints, and the deliverable.",
197
- }),
212
+ prompt: Type.Optional(
213
+ Type.String({
214
+ description: "Self-contained task. Same as task.",
215
+ }),
216
+ ),
217
+ task: Type.Optional(
218
+ Type.String({ description: "Alias of prompt (Devin/Pi)" }),
219
+ ),
198
220
  name: Type.Optional(
199
221
  Type.String({
200
- description: "Subagent name or slug from the library. Default worker.",
222
+ description: "Subagent name or slug. Default worker.",
223
+ }),
224
+ ),
225
+ agent: Type.Optional(
226
+ Type.String({ description: "Alias of name (Pi)" }),
227
+ ),
228
+ profile: Type.Optional(
229
+ Type.String({
230
+ description:
231
+ "Devin profile: explorer | reviewer | worker. luna-explore → explorer, luna-general → worker.",
201
232
  }),
202
233
  ),
203
234
  description: Type.Optional(
@@ -205,6 +236,51 @@ export function guildTools(
205
236
  description: "Short 3–8 word label for the chat UI",
206
237
  }),
207
238
  ),
239
+ title: Type.Optional(
240
+ Type.String({ description: "Alias of description (Devin title)" }),
241
+ ),
242
+ background: Type.Optional(
243
+ Type.Boolean({
244
+ description:
245
+ "If true, return agent_id immediately and keep working. Then call read_spawn.",
246
+ }),
247
+ ),
248
+ is_background: Type.Optional(
249
+ Type.Boolean({ description: "Alias of background (Devin)" }),
250
+ ),
251
+ tasks: Type.Optional(
252
+ Type.Array(
253
+ Type.Object({
254
+ prompt: Type.Optional(Type.String()),
255
+ task: Type.Optional(Type.String()),
256
+ name: Type.Optional(Type.String()),
257
+ agent: Type.Optional(Type.String()),
258
+ profile: Type.Optional(Type.String()),
259
+ description: Type.Optional(Type.String()),
260
+ title: Type.Optional(Type.String()),
261
+ }),
262
+ {
263
+ description:
264
+ "Pi parallel: run these subagents concurrently (max 8, 4 at a time).",
265
+ },
266
+ ),
267
+ ),
268
+ }),
269
+ });
270
+ tools.push({
271
+ name: "read_spawn",
272
+ description:
273
+ "Read a background spawn started with background=true (Devin read_subagent). Pass agent_id from spawn. block=true (default) waits; block=false returns running or the summary.",
274
+ parameters: Type.Object({
275
+ agent_id: Type.Optional(
276
+ Type.String({ description: "Id returned by background spawn" }),
277
+ ),
278
+ id: Type.Optional(Type.String({ description: "Alias of agent_id" })),
279
+ block: Type.Optional(
280
+ Type.Boolean({
281
+ description: "Wait for the child. Default true.",
282
+ }),
283
+ ),
208
284
  }),
209
285
  });
210
286
  }
@@ -222,7 +298,7 @@ export const GUILD_TOOLS: Tool[] = guildTools();
222
298
 
223
299
  function openaiParameters(name: string): {
224
300
  type: "object";
225
- properties: Record<string, { type: "string"; description?: string }>;
301
+ properties: Record<string, unknown>;
226
302
  required: string[];
227
303
  } {
228
304
  if (name === "run") {
@@ -257,14 +333,42 @@ function openaiParameters(name: string): {
257
333
  };
258
334
  }
259
335
  if (name === "spawn") {
260
- return {
336
+ const job = {
261
337
  type: "object",
262
338
  properties: {
263
- prompt: { type: "string", description: "Self-contained task" },
339
+ prompt: { type: "string", description: "Self-contained task. Same as task." },
340
+ task: { type: "string", description: "Alias of prompt" },
264
341
  name: { type: "string", description: "Subagent name or slug" },
342
+ agent: { type: "string", description: "Alias of name" },
343
+ profile: { type: "string", description: "explorer | reviewer | worker" },
265
344
  description: { type: "string", description: "Short UI label" },
345
+ title: { type: "string", description: "Alias of description" },
266
346
  },
267
- required: ["prompt"],
347
+ };
348
+ return {
349
+ type: "object",
350
+ properties: {
351
+ ...job.properties,
352
+ background: { type: "boolean", description: "Return agent_id immediately" },
353
+ is_background: { type: "boolean", description: "Alias of background" },
354
+ tasks: {
355
+ type: "array",
356
+ description: "Pi parallel: [{name, prompt}, ...] max 8, 4 at a time",
357
+ items: job,
358
+ },
359
+ },
360
+ required: [],
361
+ };
362
+ }
363
+ if (name === "read_spawn") {
364
+ return {
365
+ type: "object",
366
+ properties: {
367
+ agent_id: { type: "string", description: "Id from background spawn" },
368
+ id: { type: "string", description: "Alias of agent_id" },
369
+ block: { type: "boolean", description: "Wait. Default true." },
370
+ },
371
+ required: [],
268
372
  };
269
373
  }
270
374
  if (name === "image_gen") {
@@ -327,6 +431,7 @@ export const BUILTIN_TOOL_NAMES = [
327
431
  "list",
328
432
  "skill",
329
433
  "spawn",
434
+ "read_spawn",
330
435
  "image_gen",
331
436
  "browser",
332
437
  ] as const;
@@ -339,6 +444,7 @@ export async function executeTool(
339
444
  try {
340
445
  const refused = gateTool(name, args, ctx);
341
446
  if (refused) return refused;
447
+ attachSpawnHandles(ctx);
342
448
  if (ctx.dispatch) {
343
449
  const { dispatch, ...rest } = ctx;
344
450
  return await dispatch(name, args, rest);
@@ -384,13 +490,12 @@ export async function builtinExecute(
384
490
  if (name === "list") return listDir(asString(args.path), pathBase);
385
491
  if (name === "skill") return loadSkill(asString(args.name), ctx.skills ?? []);
386
492
  if (name === "spawn") {
387
- const { spawnSubagent } = await import("./subagent.ts");
388
- return spawnSubagent({
389
- prompt: asString(args.prompt),
390
- name: typeof args.name === "string" ? args.name : "",
391
- description: typeof args.description === "string" ? args.description : "",
392
- ctx,
393
- });
493
+ const { runSpawnJobs } = await import("./subagent.ts");
494
+ return runSpawnJobs(args, ctx);
495
+ }
496
+ if (name === "read_spawn") {
497
+ const { readSpawn } = await import("./subagent.ts");
498
+ return readSpawn(args, ctx);
394
499
  }
395
500
  if (name === "image_gen") {
396
501
  const { generateImage } = await import("./image-gen.ts");
@@ -713,7 +818,8 @@ Never say you cannot access this machine. Never tell the user to run the command
713
818
  When the question is about this computer, call tools first, then answer with evidence from the output.
714
819
  To generate an image, call image_gen with a prompt. Do not search the disk or load skills looking for Imagine. After it returns, include the markdown image in your reply.
715
820
  To use a real website in a browser, call browser with action=open and a url, then snapshot/click/type using refs like @e1. Default is a Hermes-shaped snapshot of the user's last_used Chrome profile (never the live profile). Set GUILD_BROWSER_REAL_PROFILE=0 for a throwaway empty profile.
716
- To delegate a bounded slice (explore, review, implement) to a specialist with its own context, call spawn. Pass a self-contained prompt. The subagent returns a summary. Subagents cannot spawn children.
821
+ You stay coordinator. Spawn is the specialist, not a last resort (Devin run_subagent / Pi subagent / Codex spawn_agent). Call spawn for a survey (explorer / luna-explore), a critique (reviewer), or a bounded patch (worker / luna-general) instead of stuffing that work into this turn with list/read/run. Do not spawn for one known file or a one-line change. Independent surveys: spawn with background=true, keep working, then read_spawn {agent_id, block:true} before the final reply. Or several spawn calls / tasks: [{title, task, profile}] this round. Task must be self-contained (child has a fresh context). Do not let a child commit, push, or decide architecture. A read_only seat can still spawn; the child stays read_only. Subagents cannot spawn children.
822
+ Independent tool calls in one round also run in parallel — fire several reads/searches together.
717
823
  Check the [exit code: N] marker on every run result; investigate failures before moving on. Prefer the workdir argument over cd.
718
824
  To follow a staffed skill, call skill with its exact name (or slug) before applying it. Relative paths in a skill resolve against that skill's base directory.
719
825
  Prefer small commands. macOS RAM: sysctl hw.memsize ; memory_pressure. Disk: df -h.`;
package/src/trajectory.ts CHANGED
@@ -38,6 +38,49 @@ export function clip(text: string, n = CLIP): string {
38
38
  return one.slice(0, n - 1) + "…";
39
39
  }
40
40
 
41
+ function recordOf(value: unknown): Record<string, unknown> | null {
42
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
43
+ return value as Record<string, unknown>;
44
+ }
45
+
46
+ /** Old rows logged spawn as TOOL "spawn spawn". Present them as spawn. */
47
+ export function promoteSpawnEvent<T extends { kind: string; summary: string; payload?: unknown }>(
48
+ event: T,
49
+ ): T & { kind: TrajectoryKind; summary: string } {
50
+ if (event.kind === "spawn") return event as T & { kind: TrajectoryKind; summary: string };
51
+ const payload = recordOf(event.payload);
52
+ const toolName = String(payload?.name || "");
53
+ const looksSpawn =
54
+ event.kind === "tool" &&
55
+ (toolName === "spawn" ||
56
+ toolName === "read_spawn" ||
57
+ /^\s*spawn(\s+spawn)?\s*$/i.test(event.summary || ""));
58
+ if (!looksSpawn) return event as T & { kind: TrajectoryKind; summary: string };
59
+ const args = recordOf(payload?.args) || payload || {};
60
+ if (toolName === "read_spawn") {
61
+ const waitId = String(args.agent_id || args.id || "").trim();
62
+ return {
63
+ ...event,
64
+ kind: "spawn",
65
+ summary: clip(`read ${waitId}`),
66
+ };
67
+ }
68
+ const agent = String(args.name || args.profile || "worker").trim() || "worker";
69
+ const label = String(args.title || args.description || "")
70
+ .replace(/\s+/g, " ")
71
+ .trim();
72
+ const prompt = String(args.prompt || args.task || "")
73
+ .replace(/\s+/g, " ")
74
+ .trim();
75
+ return {
76
+ ...event,
77
+ kind: "spawn",
78
+ summary: clip(
79
+ `${agent}${label ? " · " + label : ""}${prompt ? " — " + prompt : ""}`,
80
+ ),
81
+ };
82
+ }
83
+
41
84
  function cap(value: unknown): unknown {
42
85
  if (typeof value === "string") {
43
86
  return value.length > FIELD_CAP ? value.slice(0, FIELD_CAP) : value;
@@ -140,17 +183,34 @@ export function turnTrajectoryEvents(input: {
140
183
  });
141
184
  continue;
142
185
  }
143
- if (trace.name === "spawn") {
144
- const agent = String(trace.args.name || "worker").trim() || "worker";
145
- const label = String(trace.args.description || "").replace(/\s+/g, " ").trim();
146
- const prompt = String(trace.args.prompt || "").replace(/\s+/g, " ").trim();
186
+ if (trace.name === "spawn" || trace.name === "read_spawn") {
187
+ const agent =
188
+ String(
189
+ trace.args.profile ||
190
+ trace.args.name ||
191
+ trace.args.agent ||
192
+ "worker",
193
+ ).trim() || "worker";
194
+ const label = String(trace.args.title || trace.args.description || "")
195
+ .replace(/\s+/g, " ")
196
+ .trim();
197
+ const prompt = String(trace.args.prompt || trace.args.task || "")
198
+ .replace(/\s+/g, " ")
199
+ .trim();
200
+ const bg =
201
+ trace.args.background === true || trace.args.is_background === true
202
+ ? " (bg)"
203
+ : "";
204
+ const waitId = String(trace.args.agent_id || trace.args.id || "").trim();
147
205
  events.push({
148
206
  ts,
149
207
  turnId,
150
208
  botId,
151
209
  kind: "spawn",
152
210
  summary: clip(
153
- `${agent}${label ? " · " + label : ""}${prompt ? " — " + prompt : ""}${trace.running ? "" : ""}`,
211
+ trace.name === "read_spawn"
212
+ ? `read ${waitId || agent}${trace.running ? " …" : ""}`
213
+ : `${agent}${bg}${label ? " · " + label : ""}${prompt ? " — " + prompt : ""}${trace.running ? " …" : ""}`,
154
214
  ),
155
215
  payload: cap(trace.args),
156
216
  result: String(cap(trace.text ?? "")),
@@ -261,16 +321,19 @@ export function synthesizeTrajectory(messages: ChatMessage[]): TrajectoryEvent[]
261
321
  continue;
262
322
  }
263
323
  if (part.type === "tool") {
324
+ const spawnish = part.name === "spawn" || part.name === "read_spawn";
264
325
  events.push({
265
326
  seq: seq++,
266
327
  ts,
267
328
  turnId,
268
329
  botId: msg.author,
269
- kind: part.name === "spawn" ? "spawn" : "tool",
330
+ kind: spawnish ? "spawn" : "tool",
270
331
  summary: clip(
271
- part.name === "spawn"
272
- ? part.detail || part.label || "spawn"
273
- : `${part.name} ${part.detail}`,
332
+ part.name === "read_spawn"
333
+ ? `read ${part.detail || ""}`
334
+ : part.name === "spawn"
335
+ ? part.detail || part.label || "spawn"
336
+ : `${part.name} ${part.detail}`,
274
337
  ),
275
338
  payload: { name: part.name, detail: part.detail },
276
339
  result: part.output,
package/src/usage.ts CHANGED
@@ -59,13 +59,19 @@ export function fromOpenAiUsage(usage?: {
59
59
  prompt_tokens?: number;
60
60
  completion_tokens?: number;
61
61
  total_tokens?: number;
62
+ prompt_tokens_details?: { cached_tokens?: number };
63
+ input_tokens_details?: { cached_tokens?: number };
62
64
  } | null): ChatUsage {
63
65
  if (!usage) return { rounds: 1 };
64
66
  const input = num(usage.prompt_tokens);
65
67
  const output = num(usage.completion_tokens);
68
+ const cacheRead =
69
+ num(usage.prompt_tokens_details?.cached_tokens) ??
70
+ num(usage.input_tokens_details?.cached_tokens);
66
71
  return {
67
72
  input,
68
73
  output,
74
+ cacheRead,
69
75
  totalTokens: num(usage.total_tokens) || sum(input, output),
70
76
  rounds: 1,
71
77
  };
@@ -74,6 +80,8 @@ export function fromOpenAiUsage(usage?: {
74
80
  export function fromAnthropicUsage(usage?: {
75
81
  input_tokens?: number;
76
82
  output_tokens?: number;
83
+ cache_read_input_tokens?: number;
84
+ cache_creation_input_tokens?: number;
77
85
  } | null): ChatUsage {
78
86
  if (!usage) return { rounds: 1 };
79
87
  const input = num(usage.input_tokens);
@@ -81,6 +89,8 @@ export function fromAnthropicUsage(usage?: {
81
89
  return {
82
90
  input,
83
91
  output,
92
+ cacheRead: num(usage.cache_read_input_tokens),
93
+ cacheWrite: num(usage.cache_creation_input_tokens),
84
94
  totalTokens: sum(input, output),
85
95
  rounds: 1,
86
96
  };
@@ -37,6 +37,8 @@ export type Bot = {
37
37
  skillIds: string[];
38
38
  defaultPositionId: string;
39
39
  oneLiner?: string;
40
+ /** Public /generated/… sprite. Missing → procedural tavern buddy. */
41
+ portrait?: string;
40
42
  /** Chat model for this bot. Missing → guild default. */
41
43
  model?: ModelRef | null;
42
44
  createdAt: string;
@@ -53,6 +55,10 @@ export type Room = {
53
55
  name: string;
54
56
  memberIds: string[];
55
57
  createdAt: string;
58
+ /** Parent channel id when this is a branched side quest. */
59
+ parentId?: string;
60
+ /** Message id in the parent room that opened this branch. */
61
+ branchFromId?: string;
56
62
  };
57
63
 
58
64
  export type ChatPart =
@@ -114,6 +120,12 @@ export type ChatMessage = {
114
120
  steer?: boolean;
115
121
  /** Live bot this steer was aimed at. */
116
122
  steerBotId?: string;
123
+ /**
124
+ * Bot ids this message dispatches.
125
+ * User @mentions and bot handoffs. Missing on older rows (parse the body).
126
+ * Empty array means nobody — do not fall back to parsing.
127
+ */
128
+ mentions?: string[];
117
129
  };
118
130
 
119
131
  export type LlmApi =
@@ -141,7 +153,8 @@ export type AuxRole =
141
153
  | "skills"
142
154
  | "approval"
143
155
  | "title"
144
- | "generate";
156
+ | "generate"
157
+ | "spawn";
145
158
 
146
159
  export type ModelsFile = {
147
160
  default?: ModelRef | null;