@vellumai/assistant 0.10.0-dev.202606232234.a0ec2ee → 0.10.0-dev.202606232330.324e2b5

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/openapi.yaml CHANGED
@@ -25994,6 +25994,13 @@ paths:
25994
25994
  type: boolean
25995
25995
  messageId:
25996
25996
  type: string
25997
+ toolUseId:
25998
+ type: string
25999
+ input:
26000
+ type: object
26001
+ propertyNames:
26002
+ type: string
26003
+ additionalProperties: {}
25997
26004
  required:
25998
26005
  - type
25999
26006
  - content
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vellumai/assistant",
3
- "version": "0.10.0-dev.202606232234.a0ec2ee",
3
+ "version": "0.10.0-dev.202606232330.324e2b5",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "exports": {
@@ -130,6 +130,33 @@ describe("parseSubagentMessages", () => {
130
130
  expect(result.objective).toBe("Research vampire lore");
131
131
  });
132
132
 
133
+ test("emits toolUseId and raw input on tool_use, toolUseId on tool_result", () => {
134
+ const messages = [
135
+ msg("user", [{ type: "text", text: "Do something" }]),
136
+ msg("assistant", [
137
+ {
138
+ type: "tool_use",
139
+ id: "t-abc",
140
+ name: "bash",
141
+ input: { command: "ls -la" },
142
+ },
143
+ ]),
144
+ msg("user", [
145
+ { type: "tool_result", tool_use_id: "t-abc", content: "total 0" },
146
+ ]),
147
+ ];
148
+
149
+ const result = parseSubagentMessages("sub-1", messages);
150
+ const toolUse = result.events.find((e) => e.type === "tool_use");
151
+ expect(toolUse).toBeDefined();
152
+ expect(toolUse!.toolUseId).toBe("t-abc");
153
+ expect(toolUse!.input).toEqual({ command: "ls -la" });
154
+
155
+ const toolResult = result.events.find((e) => e.type === "tool_result");
156
+ expect(toolResult).toBeDefined();
157
+ expect(toolResult!.toolUseId).toBe("t-abc");
158
+ });
159
+
133
160
  test("includes messageId on text events from assistant messages", () => {
134
161
  const messages = [
135
162
  msg("user", [{ type: "text", text: "Do something" }]),
@@ -326,3 +326,68 @@ describe("SubagentManager terminal disposal", () => {
326
326
  asInternals(manager).stopSweep();
327
327
  });
328
328
  });
329
+
330
+ describe("SubagentManager.abort usage", () => {
331
+ test("emits the conversation's latest usage on abort, not zeros", () => {
332
+ const manager = new SubagentManager();
333
+ const sent: ServerMessage[] = [];
334
+ const sender = (msg: ServerMessage) => sent.push(msg);
335
+
336
+ const subagentId = "sa-abort-usage";
337
+ // state.usage starts at {0,0,0}; the live (fake) conversation has accrued
338
+ // usage (makeFakeConversation → {100, 50, 0.005}). Wire `sender` as the
339
+ // stored parent sender so `setStatus` routes the terminal event through it.
340
+ injectFakeSubagent(manager, subagentId, makeState(subagentId), sender);
341
+
342
+ const aborted = manager.abort(subagentId, sender, undefined, {
343
+ suppressNotification: true,
344
+ });
345
+ expect(aborted).toBe(true);
346
+
347
+ const statusMsg = sent.find(
348
+ (m): m is Extract<ServerMessage, { type: "subagent_status_changed" }> =>
349
+ m.type === "subagent_status_changed",
350
+ );
351
+ expect(statusMsg).toBeDefined();
352
+ expect(statusMsg!.status).toBe("aborted");
353
+ // The emitted usage is the conversation's accrued total — NOT the {0,0,0}
354
+ // init — so the client doesn't flush the token panel to zero on stop.
355
+ expect(statusMsg!.usage).toEqual({
356
+ inputTokens: 100,
357
+ outputTokens: 50,
358
+ estimatedCost: 0.005,
359
+ });
360
+
361
+ asInternals(manager).stopSweep();
362
+ });
363
+
364
+ test("keeps the last-known state.usage when the conversation was already released", () => {
365
+ const manager = new SubagentManager();
366
+ const sent: ServerMessage[] = [];
367
+ const sender = (msg: ServerMessage) => sent.push(msg);
368
+
369
+ const subagentId = "sa-abort-no-conv";
370
+ // No live conversation (released), but state carries a last-known usage —
371
+ // the abort must surface that, not overwrite it.
372
+ const state = makeState(subagentId, {
373
+ usage: { inputTokens: 320, outputTokens: 80, estimatedCost: 0.004 },
374
+ });
375
+ injectFakeSubagent(manager, subagentId, state, sender, null);
376
+
377
+ manager.abort(subagentId, sender, undefined, {
378
+ suppressNotification: true,
379
+ });
380
+
381
+ const statusMsg = sent.find(
382
+ (m): m is Extract<ServerMessage, { type: "subagent_status_changed" }> =>
383
+ m.type === "subagent_status_changed",
384
+ );
385
+ expect(statusMsg!.usage).toEqual({
386
+ inputTokens: 320,
387
+ outputTokens: 80,
388
+ estimatedCost: 0.004,
389
+ });
390
+
391
+ asInternals(manager).stopSweep();
392
+ });
393
+ });
@@ -31,6 +31,23 @@ export const SubagentDetailEventSchema = z.object({
31
31
  toolName: z.string().optional(),
32
32
  isError: z.boolean().optional(),
33
33
  messageId: z.string().optional(),
34
+ /**
35
+ * Tool-call id — the `tool_use.id` on a tool-call event and the referencing
36
+ * `tool_use_id` on its tool-result event, in the daemon's canonical
37
+ * content-block format. That format is provider-agnostic: every provider
38
+ * (Anthropic, OpenAI, Gemini, …) normalizes its native tool calls into these
39
+ * `tool_use`/`tool_result` blocks (see `providers/types.ts`), so this id is
40
+ * present regardless of which model produced the call. Lets the web client
41
+ * pair a result with its call and key the nested tool-detail view, so tool
42
+ * pills on reloaded/history subagents are clickable (not just live ones).
43
+ */
44
+ toolUseId: z.string().optional(),
45
+ /**
46
+ * Raw tool input object on tool-call events. (`content` also carries a
47
+ * JSON-stringified copy for back-compat / label derivation.) Surfaced in the
48
+ * tool-detail view's input section.
49
+ */
50
+ input: z.record(z.string(), z.unknown()).optional(),
34
51
  });
35
52
 
36
53
  export type SubagentDetailEvent = z.infer<typeof SubagentDetailEventSchema>;
@@ -39,6 +39,8 @@ export interface SubagentDetailResult {
39
39
  toolName?: string;
40
40
  isError?: boolean;
41
41
  messageId?: string;
42
+ toolUseId?: string;
43
+ input?: Record<string, unknown>;
42
44
  }>;
43
45
  }
44
46
 
@@ -112,6 +114,8 @@ export function parseSubagentMessages(
112
114
  type: "tool_use",
113
115
  content: JSON.stringify(input),
114
116
  toolName: name,
117
+ toolUseId: id || undefined,
118
+ input,
115
119
  });
116
120
  if (id) pendingTools.set(id, name);
117
121
  } else if (
@@ -147,6 +151,7 @@ export function parseSubagentMessages(
147
151
  content: resultContent,
148
152
  toolName: toolName ?? "unknown",
149
153
  isError,
154
+ toolUseId: toolUseId || undefined,
150
155
  });
151
156
  }
152
157
  }
@@ -553,6 +553,15 @@ export class SubagentManager {
553
553
  ),
554
554
  );
555
555
  managed.state.completedAt = Date.now();
556
+ // Capture the conversation's latest usage before emitting the terminal
557
+ // status. `subagent_status_changed` ships `state.usage`, and the abort path
558
+ // (unlike the completion/failure paths, which sync at agent-loop exit) would
559
+ // otherwise send the {0,0,0} init usage — zeroing the client's token counts
560
+ // even though those tokens were already spent. `usageStats` accrues per LLM
561
+ // turn (see conversation-usage.ts), so this is the most recent total.
562
+ if (managed.conversation) {
563
+ managed.state.usage = { ...managed.conversation.usageStats };
564
+ }
556
565
  if (parentSendToClient) {
557
566
  // Route the status update through the stored parent sender so the
558
567
  // owning conversation's UI chip updates, even when the abort comes from a