@ekairos/events 1.22.32-beta.development.0 → 1.22.33-beta.development.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/README.md CHANGED
@@ -23,6 +23,67 @@ Context-first durable execution runtime for Ekairos.
23
23
 
24
24
  The aggregate is `context`. Executions, steps, parts, and items are scoped to a context.
25
25
 
26
+ ## Canonical Parts
27
+
28
+ `event_parts` is the canonical content model for produced output.
29
+
30
+ Rules:
31
+
32
+ - `event_parts.part` is the source of truth for replay and inspection.
33
+ - `event_items.content.parts` on output items is maintained as a compatibility mirror and is deprecated as a replay source.
34
+ - Provider/model-specific values must live under `metadata`, never as first-class semantic fields.
35
+
36
+ Canonical part kinds:
37
+
38
+ - `content`
39
+ - `reasoning`
40
+ - `source`
41
+ - `tool-call`
42
+ - `tool-result`
43
+
44
+ Each canonical part stores a `content` array. The entries inside that array define the payload type:
45
+
46
+ - `text`
47
+ - `file`
48
+ - `json`
49
+ - `source-url`
50
+ - `source-document`
51
+
52
+ Example tool result:
53
+
54
+ ```ts
55
+ {
56
+ type: "tool-result",
57
+ toolCallId: "call_123",
58
+ toolName: "inspectCanvasRegion",
59
+ state: "output-available",
60
+ content: [
61
+ {
62
+ type: "text",
63
+ text: "Zoomed crop of the requested region.",
64
+ },
65
+ {
66
+ type: "file",
67
+ mediaType: "image/png",
68
+ filename: "inspect-region.png",
69
+ data: "iVBORw0KGgoAAAANSUhEUgAA...",
70
+ },
71
+ ],
72
+ metadata: {
73
+ provider: {
74
+ itemId: "fc_041cb...",
75
+ },
76
+ },
77
+ }
78
+ ```
79
+
80
+ The AI SDK bridge projects canonical parts to:
81
+
82
+ - assistant messages with text/file/reasoning/source/tool-call parts
83
+ - tool messages with `tool-result` or `tool-error`
84
+
85
+ That means multipart tool outputs are replayed from `event_parts` instead of relying on the deprecated output-item mirror.
86
+
26
87
  ## Install
27
88
 
28
89
  ```bash
@@ -1,6 +1,7 @@
1
1
  import { registerContextEnv } from "./env.js";
2
2
  import { OUTPUT_ITEM_TYPE, WEB_CHANNEL } from "./context.events.js";
3
3
  import { applyToolExecutionResultToParts } from "./context.toolcalls.js";
4
+ import { isContextPartEnvelope, normalizePartsForPersistence, } from "./context.parts.js";
4
5
  import { toolsToModelTools } from "./tools-to-model-tools.js";
5
6
  import { createAiSdkReactor, } from "./context.reactor.js";
6
7
  import { abortPersistedContextStepStream, closePersistedContextStepStream, createPersistedContextStepStream, closeContextStream, } from "./steps/stream.steps.js";
@@ -22,6 +23,20 @@ function clipPreview(value, max = 240) {
22
23
  function summarizePartPreview(part) {
23
24
  if (!part || typeof part !== "object")
24
25
  return {};
26
+ if (isContextPartEnvelope(part)) {
27
+ const preview = part.content[0]?.type === "text"
28
+ ? part.content[0].text
29
+ : JSON.stringify(part.content[0] ?? part);
30
+ const state = "state" in part && typeof part.state === "string" ? part.state : undefined;
31
+ const toolCallId = "toolCallId" in part && typeof part.toolCallId === "string"
32
+ ? part.toolCallId
33
+ : undefined;
34
+ return {
35
+ partPreview: preview ? clipPreview(preview) : undefined,
36
+ partState: state,
37
+ partToolCallId: toolCallId,
38
+ };
39
+ }
25
40
  const row = part;
26
41
  const partType = typeof row.type === "string" ? row.type : "";
27
42
  const partState = typeof row.state === "string" ? row.state : undefined;
@@ -527,7 +542,7 @@ export class ContextEngine {
527
542
  : [];
528
543
  let persistedReactionPartsSignature = "";
529
544
  const persistReactionParts = async (nextParts) => {
530
- const normalizedParts = Array.isArray(nextParts) ? nextParts : [];
545
+ const normalizedParts = normalizePartsForPersistence(Array.isArray(nextParts) ? nextParts : []);
531
546
  const nextSignature = JSON.stringify(normalizedParts);
532
547
  if (nextSignature === persistedReactionPartsSignature)
533
548
  return;
@@ -591,7 +606,7 @@ export class ContextEngine {
591
606
  // We intentionally do NOT persist the per-step LLM assistant event as a `context_event`.
592
607
  // The story exposes a single visible `context_event` per turn (`reactionEventId`) so the UI
593
608
  // doesn't render duplicate assistant messages (LLM-step + aggregated reaction).
594
- const stepParts = (assistantEvent?.content?.parts ?? []);
609
+ const stepParts = normalizePartsForPersistence((assistantEvent?.content?.parts ?? []));
595
610
  const assistantEventEffective = {
596
611
  ...assistantEvent,
597
612
  content: {
@@ -836,11 +851,9 @@ export class ContextEngine {
836
851
  }
837
852
  })));
838
853
  // Merge action results into persisted parts (so next LLM call can see them)
839
- let parts = Array.isArray(reactionEvent.content?.parts)
840
- ? [...reactionEvent.content.parts]
841
- : [];
854
+ let finalizedStepParts = Array.isArray(stepParts) ? [...stepParts] : [];
842
855
  for (const r of actionResults) {
843
- parts = applyToolExecutionResultToParts(parts, {
856
+ finalizedStepParts = applyToolExecutionResultToParts(finalizedStepParts, {
844
857
  toolCallId: r.actionRequest.actionRef,
845
858
  toolName: r.actionRequest.actionName,
846
859
  }, {
@@ -849,11 +862,20 @@ export class ContextEngine {
849
862
  message: r.errorText,
850
863
  });
851
864
  }
865
+ await measureBenchmark(params.__benchmark, `${stagePrefix}.saveFinalStepPartsMs`, async () => await ops.saveContextPartsStep({
866
+ stepId: stepCreate.stepId,
867
+ parts: finalizedStepParts,
868
+ executionId,
869
+ contextId: String(currentContext.id),
870
+ iteration: iter,
871
+ }));
852
872
  reactionEvent = {
853
873
  ...reactionEvent,
854
874
  content: {
855
875
  ...reactionEvent.content,
856
- parts,
876
+ // Deprecated mirror for compatibility. `event_parts` are the
877
+ // source of truth for replay and step inspection.
878
+ parts: [...reactionPartsBeforeStep, ...finalizedStepParts],
857
879
  },
858
880
  status: "pending",
859
881
  };
@@ -6,6 +6,26 @@ export declare const INPUT_TEXT_ITEM_TYPE = "input";
6
6
  export declare const WEB_CHANNEL = "web";
7
7
  export declare const AGENT_CHANNEL = "whatsapp";
8
8
  export declare const EMAIL_CHANNEL = "email";
9
+ export type ContextOutputContentPart = {
10
+ type: "text";
11
+ text: string;
12
+ } | ({
13
+ type: "image-data";
14
+ data: string;
15
+ mediaType: string;
16
+ filename?: string;
17
+ } & Record<string, unknown>) | ({
18
+ type: string;
19
+ } & Record<string, unknown>);
20
+ export type ContextOutputPart = {
21
+ type: "json";
22
+ value: unknown;
23
+ } | {
24
+ type: "content";
25
+ value: ContextOutputContentPart[];
26
+ };
27
+ export declare function isContextOutputPart(value: unknown): value is ContextOutputPart;
28
+ export declare function normalizeContextOutputPart(value: unknown): ContextOutputPart;
9
29
  export declare function createUserItemFromUIMessages(messages: UIMessage[]): ContextItem;
10
30
  export declare function createAssistantItemFromUIMessages(itemId: string, messages: UIMessage[]): ContextItem;
11
31
  export declare function convertToUIMessage(item: ContextItem): UIMessage;
@@ -1,10 +1,322 @@
1
1
  import { convertToModelMessages } from "ai";
2
+ import { isContextPartEnvelope, normalizePartsForPersistence, } from "./context.parts.js";
2
3
  export const INPUT_ITEM_TYPE = "input";
3
4
  export const OUTPUT_ITEM_TYPE = "output";
4
5
  export const INPUT_TEXT_ITEM_TYPE = INPUT_ITEM_TYPE;
5
6
  export const WEB_CHANNEL = "web";
6
7
  export const AGENT_CHANNEL = "whatsapp";
7
8
  export const EMAIL_CHANNEL = "email";
9
+ function asRecord(value) {
10
+ if (!value || typeof value !== "object")
11
+ return null;
12
+ return value;
13
+ }
14
+ function isContextOutputContentPart(value) {
15
+ const record = asRecord(value);
16
+ return Boolean(record && typeof record.type === "string");
17
+ }
18
+ export function isContextOutputPart(value) {
19
+ const record = asRecord(value);
20
+ if (!record || typeof record.type !== "string") {
21
+ return false;
22
+ }
23
+ if (record.type === "json") {
24
+ return "value" in record;
25
+ }
26
+ if (record.type === "content") {
27
+ return Array.isArray(record.value) && record.value.every(isContextOutputContentPart);
28
+ }
29
+ return false;
30
+ }
31
+ export function normalizeContextOutputPart(value) {
32
+ if (isContextOutputPart(value)) {
33
+ return value;
34
+ }
35
+ return {
36
+ type: "json",
37
+ value,
38
+ };
39
+ }
40
+ function isToolUIPart(value) {
41
+ const record = asRecord(value);
42
+ return Boolean(record && typeof record.type === "string" && record.type.startsWith("tool-"));
43
+ }
44
+ function readToolNameFromPart(part) {
45
+ return String(part.type).split("-").slice(1).join("-");
46
+ }
47
+ function stripDataUrlPrefix(value) {
48
+ return value.replace(/^data:[^;]+;base64,/i, "");
49
+ }
50
+ function asCanonicalParts(parts) {
51
+ return normalizePartsForPersistence(parts);
52
+ }
53
+ function contentBlockToPrimaryUiParts(block) {
54
+ if (block.type === "text") {
55
+ return [{ type: "text", text: block.text }];
56
+ }
57
+ if (block.type === "file") {
58
+ const url = typeof block.url === "string" && block.url.length > 0
59
+ ? block.url
60
+ : typeof block.data === "string" && block.data.length > 0
61
+ ? block.data.startsWith("data:")
62
+ ? block.data
63
+ : `data:${block.mediaType};base64,${block.data}`
64
+ : typeof block.fileId === "string" && block.fileId.length > 0
65
+ ? block.fileId
66
+ : "";
67
+ if (!url) {
68
+ return [];
69
+ }
70
+ return [
71
+ {
72
+ type: "file",
73
+ mediaType: block.mediaType,
74
+ filename: block.filename,
75
+ url,
76
+ },
77
+ ];
78
+ }
79
+ if (block.type === "json") {
80
+ return [
81
+ {
82
+ type: "text",
83
+ text: JSON.stringify(block.value, null, 2),
84
+ },
85
+ ];
86
+ }
87
+ if (block.type === "source-url") {
88
+ return [
89
+ {
90
+ type: "source-url",
91
+ sourceId: block.sourceId,
92
+ url: block.url,
93
+ title: block.title,
94
+ },
95
+ ];
96
+ }
97
+ if (block.type === "source-document") {
98
+ return [
99
+ {
100
+ type: "source-document",
101
+ sourceId: block.sourceId,
102
+ mediaType: block.mediaType,
103
+ title: block.title,
104
+ filename: block.filename,
105
+ },
106
+ ];
107
+ }
108
+ return [];
109
+ }
110
+ function toolCallContentToInput(content) {
111
+ if (content.length === 0)
112
+ return undefined;
113
+ if (content.length === 1) {
114
+ const first = content[0];
115
+ if (first.type === "json")
116
+ return first.value;
117
+ if (first.type === "text")
118
+ return first.text;
119
+ return first;
120
+ }
121
+ return content;
122
+ }
123
+ function canonicalPartsToPrimaryUiParts(parts) {
124
+ const uiParts = [];
125
+ for (const part of parts) {
126
+ if (part.type === "content") {
127
+ uiParts.push(...part.content.flatMap((block) => contentBlockToPrimaryUiParts(block)));
128
+ continue;
129
+ }
130
+ if (part.type === "reasoning") {
131
+ const text = part.content
132
+ .filter((block) => block.type === "text")
133
+ .map((block) => block.text)
134
+ .join("\n\n");
135
+ if (text.trim()) {
136
+ uiParts.push({
137
+ type: "reasoning",
138
+ text,
139
+ state: part.state,
140
+ });
141
+ }
142
+ continue;
143
+ }
144
+ if (part.type === "source") {
145
+ uiParts.push(...part.content.flatMap((block) => contentBlockToPrimaryUiParts(block)));
146
+ continue;
147
+ }
148
+ if (part.type === "tool-call") {
149
+ uiParts.push({
150
+ type: `tool-${part.toolName}`,
151
+ toolCallId: part.toolCallId,
152
+ state: part.state ?? "input-available",
153
+ input: toolCallContentToInput(part.content),
154
+ });
155
+ }
156
+ }
157
+ return uiParts;
158
+ }
159
+ function canonicalToolResultContentToOutput(content) {
160
+ if (content.length === 1 && content[0]?.type === "json") {
161
+ return {
162
+ type: "json",
163
+ value: content[0].value,
164
+ };
165
+ }
166
+ return {
167
+ type: "content",
168
+ value: content.map((block) => {
169
+ if (block.type === "text") {
170
+ return {
171
+ type: "text",
172
+ text: block.text,
173
+ };
174
+ }
175
+ if (block.type === "file") {
176
+ if (block.mediaType.startsWith("image/") &&
177
+ typeof block.data === "string" &&
178
+ block.data.length > 0) {
179
+ return {
180
+ type: "image-data",
181
+ data: stripDataUrlPrefix(block.data),
182
+ mediaType: block.mediaType,
183
+ filename: block.filename,
184
+ };
185
+ }
186
+ return {
187
+ type: "file",
188
+ mediaType: block.mediaType,
189
+ filename: block.filename,
190
+ data: typeof block.data === "string" && block.data.length > 0
191
+ ? block.data
192
+ : typeof block.url === "string" && block.url.length > 0
193
+ ? block.url
194
+ : block.fileId,
195
+ };
196
+ }
197
+ if (block.type === "json") {
198
+ return {
199
+ type: "text",
200
+ text: JSON.stringify(block.value, null, 2),
201
+ };
202
+ }
203
+ return {
204
+ type: "text",
205
+ text: JSON.stringify(block),
206
+ };
207
+ }),
208
+ };
209
+ }
210
+ function canonicalToolPartsToModelMessages(parts) {
211
+ const toolInputs = new Map();
212
+ const toolResults = [];
213
+ for (const part of parts) {
214
+ if (part.type === "tool-call") {
215
+ toolInputs.set(part.toolCallId, toolCallContentToInput(part.content));
216
+ continue;
217
+ }
218
+ if (part.type !== "tool-result") {
219
+ continue;
220
+ }
221
+ if (part.state === "output-error") {
222
+ const text = part.content
223
+ .filter((block) => block.type === "text")
224
+ .map((block) => block.text)
225
+ .join("\n\n");
226
+ toolResults.push({
227
+ type: "tool-error",
228
+ toolCallId: part.toolCallId,
229
+ toolName: part.toolName,
230
+ input: toolInputs.get(part.toolCallId),
231
+ error: text || "Tool execution failed.",
232
+ });
233
+ continue;
234
+ }
235
+ toolResults.push({
236
+ type: "tool-result",
237
+ toolCallId: part.toolCallId,
238
+ toolName: part.toolName,
239
+ output: canonicalToolResultContentToOutput(part.content),
240
+ });
241
+ }
242
+ if (toolResults.length === 0) {
243
+ return [];
244
+ }
245
+ return [
246
+ {
247
+ role: "tool",
248
+ content: toolResults,
249
+ },
250
+ ];
251
+ }
252
+ function canonicalPartsToModelMessages(role, parts) {
253
+ const uiMessage = {
254
+ id: "canonical-item",
255
+ role,
256
+ parts: canonicalPartsToPrimaryUiParts(parts),
257
+ };
258
+ return removeEmptyToolMessages(convertToModelMessages([uiMessage]));
259
+ }
260
+ function normalizeAssistantPartsForModel(parts) {
261
+ return parts
262
+ .map((part) => {
263
+ if (!isToolUIPart(part)) {
264
+ return part;
265
+ }
266
+ const next = {
267
+ ...part,
268
+ state: part.state === "output-available" || part.state === "output-error"
269
+ ? "input-available"
270
+ : part.state,
271
+ };
272
+ delete next.output;
273
+ delete next.errorText;
274
+ return next;
275
+ })
276
+ .filter(Boolean);
277
+ }
278
+ function buildToolResultContent(parts) {
279
+ const toolContent = [];
280
+ for (const part of parts) {
281
+ if (!isToolUIPart(part)) {
282
+ continue;
283
+ }
284
+ const toolCallId = typeof part.toolCallId === "string" ? part.toolCallId : "";
285
+ const toolName = readToolNameFromPart(part);
286
+ if (!toolCallId || !toolName) {
287
+ continue;
288
+ }
289
+ if (part.state === "output-available") {
290
+ toolContent.push({
291
+ type: "tool-result",
292
+ toolCallId,
293
+ toolName,
294
+ output: normalizeContextOutputPart(part.output),
295
+ });
296
+ continue;
297
+ }
298
+ if (part.state === "output-error") {
299
+ toolContent.push({
300
+ type: "tool-error",
301
+ toolCallId,
302
+ toolName,
303
+ input: part.input,
304
+ error: typeof part.errorText === "string" && part.errorText.trim().length > 0
305
+ ? part.errorText
306
+ : "Tool execution failed.",
307
+ });
308
+ }
309
+ }
310
+ return toolContent;
311
+ }
312
+ function removeEmptyToolMessages(messages) {
313
+ return messages.filter((message) => {
314
+ if (message.role !== "tool") {
315
+ return true;
316
+ }
317
+ return Array.isArray(message.content) ? message.content.length > 0 : true;
318
+ });
319
+ }
8
320
  export function createUserItemFromUIMessages(messages) {
9
321
  if (!Array.isArray(messages) || messages.length === 0) {
10
322
  throw new Error("Missing messages to create item");
@@ -15,7 +327,7 @@ export function createUserItemFromUIMessages(messages) {
15
327
  type: INPUT_ITEM_TYPE,
16
328
  channel: WEB_CHANNEL,
17
329
  content: {
18
- parts: lastMessage.parts,
330
+ parts: asCanonicalParts(lastMessage.parts),
19
331
  },
20
332
  createdAt: new Date().toISOString(),
21
333
  };
@@ -30,16 +342,17 @@ export function createAssistantItemFromUIMessages(itemId, messages) {
30
342
  type: OUTPUT_ITEM_TYPE,
31
343
  channel: WEB_CHANNEL,
32
344
  content: {
33
- parts: lastMessage.parts,
345
+ parts: asCanonicalParts(lastMessage.parts),
34
346
  },
35
347
  createdAt: new Date().toISOString(),
36
348
  };
37
349
  }
38
350
  export function convertToUIMessage(item) {
39
351
  const role = item.type === INPUT_ITEM_TYPE ? "user" : "assistant";
40
- const parts = Array.isArray(item.content.parts)
41
- ? item.content.parts
42
- : [];
352
+ const rawParts = Array.isArray(item.content.parts) ? item.content.parts : [];
353
+ const parts = rawParts.every(isContextPartEnvelope)
354
+ ? canonicalPartsToPrimaryUiParts(rawParts)
355
+ : rawParts;
43
356
  return {
44
357
  id: item.id,
45
358
  role,
@@ -67,8 +380,40 @@ export async function convertItemsToModelMessages(items) {
67
380
  return results.flat();
68
381
  }
69
382
  export async function convertItemToModelMessages(item) {
70
- const message = convertToUIMessage(item);
71
- return convertToModelMessages([message]);
383
+ const role = item.type === INPUT_ITEM_TYPE ? "user" : "assistant";
384
+ const rawParts = Array.isArray(item.content.parts) ? item.content.parts : [];
385
+ const canonicalParts = asCanonicalParts(rawParts);
386
+ if (canonicalParts.length > 0) {
387
+ const primary = await canonicalPartsToModelMessages(role, canonicalParts);
388
+ if (role !== "assistant") {
389
+ return primary;
390
+ }
391
+ return [
392
+ ...primary,
393
+ ...canonicalToolPartsToModelMessages(canonicalParts),
394
+ ];
395
+ }
396
+ const assistantParts = normalizeAssistantPartsForModel(rawParts);
397
+ const message = {
398
+ id: item.id,
399
+ role,
400
+ parts: assistantParts,
401
+ };
402
+ const modelMessages = removeEmptyToolMessages(await convertToModelMessages([message]));
403
+ if (role !== "assistant") {
404
+ return modelMessages;
405
+ }
406
+ const toolContent = buildToolResultContent(rawParts);
407
+ if (toolContent.length === 0) {
408
+ return modelMessages;
409
+ }
410
+ return [
411
+ ...modelMessages,
412
+ {
413
+ role: "tool",
414
+ content: toolContent,
415
+ },
416
+ ];
72
417
  }
73
418
  function normalizeModelMessageContentToParts(content) {
74
419
  if (Array.isArray(content))