assistant-stream 0.3.20 → 0.3.22

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.
Files changed (36) hide show
  1. package/dist/core/accumulators/assistant-message-accumulator.d.ts.map +1 -1
  2. package/dist/core/accumulators/assistant-message-accumulator.js +5 -0
  3. package/dist/core/accumulators/assistant-message-accumulator.js.map +1 -1
  4. package/dist/core/converters/toGenericMessages.js.map +1 -1
  5. package/dist/core/object/ObjectStreamResponse.js.map +1 -1
  6. package/dist/core/object/types.d.ts +1 -0
  7. package/dist/core/object/types.d.ts.map +1 -1
  8. package/dist/core/serialization/PlainText.js.map +1 -1
  9. package/dist/core/serialization/assistant-transport/AssistantTransport.js.map +1 -1
  10. package/dist/core/serialization/data-stream/DataStream.js.map +1 -1
  11. package/dist/core/serialization/ui-message-stream/UIMessageStream.js.map +1 -1
  12. package/dist/core/tool/ToolCallReader.d.ts +1 -0
  13. package/dist/core/tool/ToolCallReader.d.ts.map +1 -1
  14. package/dist/core/tool/ToolCallReader.js.map +1 -1
  15. package/dist/core/tool/ToolExecutionStream.js.map +1 -1
  16. package/dist/core/tool/schema-utils.js.map +1 -1
  17. package/dist/core/tool/tool-types.d.ts.map +1 -1
  18. package/dist/core/tool/toolResultStream.js.map +1 -1
  19. package/dist/core/utils/stream/AssistantTransformStream.js.map +1 -1
  20. package/dist/core/utils/stream/SSE.js.map +1 -1
  21. package/dist/core/utils/stream/merge.js.map +1 -1
  22. package/dist/core/utils/types.d.ts +12 -1
  23. package/dist/core/utils/types.d.ts.map +1 -1
  24. package/dist/index.d.ts +2 -2
  25. package/dist/resumable/ResumableStreamContext.js.map +1 -1
  26. package/dist/resumable/createResumableAssistantStreamResponse.js.map +1 -1
  27. package/dist/resumable/stores/InMemoryResumableStreamStore.js.map +1 -1
  28. package/dist/resumable/stores/ioredis.js.map +1 -1
  29. package/dist/utils/json/fix-json.js.map +1 -1
  30. package/dist/utils/json/is-json.js.map +1 -1
  31. package/package.json +4 -4
  32. package/src/core/accumulators/assistant-message-accumulator.test.ts +51 -0
  33. package/src/core/accumulators/assistant-message-accumulator.ts +9 -0
  34. package/src/core/utils/types.ts +14 -0
  35. package/src/index.ts +1 -0
  36. package/src/resumable/__tests__/integration.test.ts +12 -1
@@ -1 +1 @@
1
- {"version":3,"file":"is-json.js","names":[],"sources":["../../../src/utils/json/is-json.ts"],"sourcesContent":["import type {\n ReadonlyJSONArray,\n ReadonlyJSONObject,\n ReadonlyJSONValue,\n} from \"./json-value\";\n\nexport function isJSONValue(value: unknown): value is ReadonlyJSONValue {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return true;\n }\n\n if (Array.isArray(value)) {\n return value.every(isJSONValue);\n }\n\n if (typeof value === \"object\") {\n return Object.entries(value).every(\n ([key, val]) => typeof key === \"string\" && isJSONValue(val),\n );\n }\n\n return false;\n}\n\nexport function isJSONArray(value: unknown): value is ReadonlyJSONArray {\n return Array.isArray(value) && value.every(isJSONValue);\n}\n\nexport function isJSONObject(value: unknown): value is ReadonlyJSONObject {\n return (\n value != null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.entries(value).every(\n ([key, val]) => typeof key === \"string\" && isJSONValue(val),\n )\n );\n}\n"],"mappings":";AAMA,SAAgB,YAAY,OAA4C;CACtE,IACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU,WAEjB,OAAO;CAGT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,MAAM,WAAW;CAGhC,IAAI,OAAO,UAAU,UACnB,OAAO,OAAO,QAAQ,KAAK,EAAE,OAC1B,CAAC,KAAK,SAAS,OAAO,QAAQ,YAAY,YAAY,GAAG,CAC5D;CAGF,OAAO;AACT;AAEA,SAAgB,YAAY,OAA4C;CACtE,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,WAAW;AACxD;AAEA,SAAgB,aAAa,OAA6C;CACxE,OACE,SAAS,QACT,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,QAAQ,KAAK,EAAE,OACnB,CAAC,KAAK,SAAS,OAAO,QAAQ,YAAY,YAAY,GAAG,CAC5D;AAEJ"}
1
+ {"version":3,"file":"is-json.js","names":[],"sources":["../../../src/utils/json/is-json.ts"],"sourcesContent":["import type {\n ReadonlyJSONArray,\n ReadonlyJSONObject,\n ReadonlyJSONValue,\n} from \"./json-value\";\n\nexport function isJSONValue(value: unknown): value is ReadonlyJSONValue {\n if (\n value === null ||\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n typeof value === \"boolean\"\n ) {\n return true;\n }\n\n if (Array.isArray(value)) {\n return value.every(isJSONValue);\n }\n\n if (typeof value === \"object\") {\n return Object.entries(value).every(\n ([key, val]) => typeof key === \"string\" && isJSONValue(val),\n );\n }\n\n return false;\n}\n\nexport function isJSONArray(value: unknown): value is ReadonlyJSONArray {\n return Array.isArray(value) && value.every(isJSONValue);\n}\n\nexport function isJSONObject(value: unknown): value is ReadonlyJSONObject {\n return (\n value != null &&\n typeof value === \"object\" &&\n !Array.isArray(value) &&\n Object.entries(value).every(\n ([key, val]) => typeof key === \"string\" && isJSONValue(val),\n )\n );\n}\n"],"mappings":";AAMA,SAAgB,YAAY,OAA4C;CACtE,IACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU,WAEjB,OAAO;CAGT,IAAI,MAAM,QAAQ,KAAK,GACrB,OAAO,MAAM,MAAM,WAAW;CAGhC,IAAI,OAAO,UAAU,UACnB,OAAO,OAAO,QAAQ,KAAK,CAAC,CAAC,OAC1B,CAAC,KAAK,SAAS,OAAO,QAAQ,YAAY,YAAY,GAAG,CAC5D;CAGF,OAAO;AACT;AAEA,SAAgB,YAAY,OAA4C;CACtE,OAAO,MAAM,QAAQ,KAAK,KAAK,MAAM,MAAM,WAAW;AACxD;AAEA,SAAgB,aAAa,OAA6C;CACxE,OACE,SAAS,QACT,OAAO,UAAU,YACjB,CAAC,MAAM,QAAQ,KAAK,KACpB,OAAO,QAAQ,KAAK,CAAC,CAAC,OACnB,CAAC,KAAK,SAAS,OAAO,QAAQ,YAAY,YAAY,GAAG,CAC5D;AAEJ"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "assistant-stream",
3
- "version": "0.3.20",
3
+ "version": "0.3.22",
4
4
  "description": "Streaming utilities for AI assistants",
5
5
  "keywords": [
6
6
  "ai",
@@ -64,10 +64,10 @@
64
64
  },
65
65
  "devDependencies": {
66
66
  "@types/json-schema": "^7.0.15",
67
- "ioredis": "^5.11.0",
67
+ "ioredis": "^5.11.1",
68
68
  "redis": "^6.0.0",
69
- "vitest": "^4.1.7",
70
- "@assistant-ui/x-buildutils": "0.0.11"
69
+ "vitest": "^4.1.8",
70
+ "@assistant-ui/x-buildutils": "0.0.13"
71
71
  },
72
72
  "publishConfig": {
73
73
  "access": "public",
@@ -101,6 +101,57 @@ describe("AssistantMessageAccumulator timing", () => {
101
101
  expect(last.metadata.timing!.toolCallCount).toBe(1);
102
102
  });
103
103
 
104
+ it("should record per-tool-call timing on the part", async () => {
105
+ const before = Date.now();
106
+ const chunks: AssistantStreamChunk[] = [
107
+ {
108
+ type: "part-start",
109
+ path: [0],
110
+ part: {
111
+ type: "tool-call",
112
+ toolCallId: "tc-1",
113
+ toolName: "search",
114
+ },
115
+ },
116
+ { type: "text-delta", path: [0], textDelta: '{"q":"test"}' },
117
+ { type: "tool-call-args-text-finish", path: [0] },
118
+ {
119
+ type: "result",
120
+ path: [0],
121
+ result: "found",
122
+ isError: false,
123
+ },
124
+ { type: "part-finish", path: [0] },
125
+ {
126
+ type: "message-finish",
127
+ path: [],
128
+ finishReason: "stop",
129
+ usage: { inputTokens: 0, outputTokens: 0 },
130
+ },
131
+ ];
132
+
133
+ const messages = await collectStream(chunks);
134
+ const after = Date.now();
135
+
136
+ const runningPart = messages
137
+ .find((m) =>
138
+ m.parts.some((p) => p.type === "tool-call" && p.state !== "result"),
139
+ )!
140
+ .parts.find((p) => p.type === "tool-call")!;
141
+ expect(runningPart.timing).toBeDefined();
142
+ expect(runningPart.timing!.startedAt).toBeGreaterThanOrEqual(before);
143
+ expect(runningPart.timing!.completedAt).toBeUndefined();
144
+
145
+ const settledPart = messages
146
+ .at(-1)!
147
+ .parts.find((p) => p.type === "tool-call")!;
148
+ expect(settledPart.timing).toBeDefined();
149
+ expect(settledPart.timing!.completedAt).toBeGreaterThanOrEqual(
150
+ settledPart.timing!.startedAt,
151
+ );
152
+ expect(settledPart.timing!.completedAt).toBeLessThanOrEqual(after);
153
+ });
154
+
104
155
  it("should include timing on flush when stream closes without message-finish", async () => {
105
156
  const chunks: AssistantStreamChunk[] = [
106
157
  { type: "part-start", path: [0], part: { type: "text" } },
@@ -92,6 +92,7 @@ const handlePartStart = (
92
92
  toolName: partInit.toolName,
93
93
  argsText: "",
94
94
  args: {},
95
+ timing: { startedAt: Date.now() },
95
96
  ...(partInit.parentId && { parentId: partInit.parentId }),
96
97
  };
97
98
  return {
@@ -213,6 +214,14 @@ const handleResult = (
213
214
  return {
214
215
  ...part,
215
216
  state: "result",
217
+ ...(part.timing !== undefined
218
+ ? {
219
+ timing: {
220
+ ...part.timing,
221
+ completedAt: part.timing.completedAt ?? Date.now(),
222
+ },
223
+ }
224
+ : {}),
216
225
  ...(chunk.artifact !== undefined ? { artifact: chunk.artifact } : {}),
217
226
  result: chunk.result,
218
227
  isError: chunk.isError ?? false,
@@ -53,6 +53,19 @@ type ToolCallStatus =
53
53
  reason: "cancelled" | "length" | "content-filter" | "other";
54
54
  };
55
55
 
56
+ /**
57
+ * Wall-clock timing of a tool call. Accumulator-populated timings are
58
+ * measured by the consuming accumulator, so resumed or replayed streams
59
+ * re-measure them; hosts that need authoritative timings supply the field
60
+ * themselves.
61
+ */
62
+ export type ToolCallTiming = {
63
+ /** Epoch milliseconds when the tool call started streaming or executing. */
64
+ readonly startedAt: number;
65
+ /** Epoch milliseconds when the result landed. Absent while the call runs. */
66
+ readonly completedAt?: number;
67
+ };
68
+
56
69
  type ToolCallPartBase = {
57
70
  type: "tool-call";
58
71
  status: ToolCallStatus;
@@ -60,6 +73,7 @@ type ToolCallPartBase = {
60
73
  toolName: string;
61
74
  argsText: string;
62
75
  args: ReadonlyJSONObject;
76
+ timing?: ToolCallTiming;
63
77
  artifact?: ReadonlyJSONValue;
64
78
  result?: ReadonlyJSONValue;
65
79
  modelContent?: readonly ToolModelContentPart[];
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export type {
33
33
  AssistantMessage,
34
34
  AssistantMessageTiming,
35
35
  DataPart,
36
+ ToolCallTiming,
36
37
  } from "./core/utils/types";
37
38
 
38
39
  export type {
@@ -145,7 +145,17 @@ describe("resumable integration", () => {
145
145
  new AssistantTransportDecoder(),
146
146
  );
147
147
 
148
- expect(replayMessage.parts).toEqual(producerMessage.parts);
148
+ // Tool-call timing is measured by the consuming accumulator, so a replay
149
+ // re-measures it; compare parts modulo timing.
150
+ const withoutTiming = (parts: typeof replayMessage.parts) =>
151
+ parts.map((part) =>
152
+ part.type === "tool-call"
153
+ ? (({ timing: _timing, ...rest }) => rest)(part)
154
+ : part,
155
+ );
156
+ expect(withoutTiming(replayMessage.parts)).toEqual(
157
+ withoutTiming(producerMessage.parts),
158
+ );
149
159
  expect(replayMessage.status).toEqual(producerMessage.status);
150
160
  const toolPart = replayMessage.parts.find((p) => p.type === "tool-call");
151
161
  expect(toolPart).toBeDefined();
@@ -154,6 +164,7 @@ describe("resumable integration", () => {
154
164
  toolCallId: "tool-1",
155
165
  args: { query: "weather" },
156
166
  result: { temperature: 72 },
167
+ timing: { startedAt: expect.any(Number) },
157
168
  });
158
169
  });
159
170
  });