@ekairos/thread 1.21.88-beta.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.
Files changed (86) hide show
  1. package/README.md +363 -0
  2. package/dist/codex.d.ts +95 -0
  3. package/dist/codex.js +91 -0
  4. package/dist/env.d.ts +12 -0
  5. package/dist/env.js +62 -0
  6. package/dist/events.d.ts +35 -0
  7. package/dist/events.js +102 -0
  8. package/dist/index.d.ts +9 -0
  9. package/dist/index.js +9 -0
  10. package/dist/mcp.d.ts +1 -0
  11. package/dist/mcp.js +1 -0
  12. package/dist/mirror.d.ts +41 -0
  13. package/dist/mirror.js +1 -0
  14. package/dist/oidc.d.ts +7 -0
  15. package/dist/oidc.js +25 -0
  16. package/dist/polyfills/dom-events.d.ts +1 -0
  17. package/dist/polyfills/dom-events.js +89 -0
  18. package/dist/react.d.ts +62 -0
  19. package/dist/react.js +101 -0
  20. package/dist/runtime.d.ts +17 -0
  21. package/dist/runtime.js +23 -0
  22. package/dist/runtime.step.d.ts +9 -0
  23. package/dist/runtime.step.js +7 -0
  24. package/dist/schema.d.ts +2 -0
  25. package/dist/schema.js +200 -0
  26. package/dist/steps/do-story-stream-step.d.ts +29 -0
  27. package/dist/steps/do-story-stream-step.js +89 -0
  28. package/dist/steps/do-thread-stream-step.d.ts +29 -0
  29. package/dist/steps/do-thread-stream-step.js +90 -0
  30. package/dist/steps/mirror.steps.d.ts +6 -0
  31. package/dist/steps/mirror.steps.js +48 -0
  32. package/dist/steps/reaction.steps.d.ts +43 -0
  33. package/dist/steps/reaction.steps.js +354 -0
  34. package/dist/steps/store.steps.d.ts +98 -0
  35. package/dist/steps/store.steps.js +512 -0
  36. package/dist/steps/stream.steps.d.ts +41 -0
  37. package/dist/steps/stream.steps.js +99 -0
  38. package/dist/steps/trace.steps.d.ts +37 -0
  39. package/dist/steps/trace.steps.js +265 -0
  40. package/dist/stores/instant.document-parser.d.ts +6 -0
  41. package/dist/stores/instant.document-parser.js +210 -0
  42. package/dist/stores/instant.documents.d.ts +16 -0
  43. package/dist/stores/instant.documents.js +152 -0
  44. package/dist/stores/instant.store.d.ts +78 -0
  45. package/dist/stores/instant.store.js +530 -0
  46. package/dist/story.actions.d.ts +60 -0
  47. package/dist/story.actions.js +120 -0
  48. package/dist/story.builder.d.ts +115 -0
  49. package/dist/story.builder.js +130 -0
  50. package/dist/story.config.d.ts +54 -0
  51. package/dist/story.config.js +125 -0
  52. package/dist/story.d.ts +2 -0
  53. package/dist/story.engine.d.ts +224 -0
  54. package/dist/story.engine.js +464 -0
  55. package/dist/story.hooks.d.ts +21 -0
  56. package/dist/story.hooks.js +31 -0
  57. package/dist/story.js +6 -0
  58. package/dist/story.registry.d.ts +21 -0
  59. package/dist/story.registry.js +30 -0
  60. package/dist/story.store.d.ts +107 -0
  61. package/dist/story.store.js +1 -0
  62. package/dist/story.toolcalls.d.ts +60 -0
  63. package/dist/story.toolcalls.js +73 -0
  64. package/dist/thread.builder.d.ts +118 -0
  65. package/dist/thread.builder.js +134 -0
  66. package/dist/thread.config.d.ts +15 -0
  67. package/dist/thread.config.js +30 -0
  68. package/dist/thread.d.ts +3 -0
  69. package/dist/thread.engine.d.ts +229 -0
  70. package/dist/thread.engine.js +471 -0
  71. package/dist/thread.events.d.ts +35 -0
  72. package/dist/thread.events.js +105 -0
  73. package/dist/thread.hooks.d.ts +21 -0
  74. package/dist/thread.hooks.js +31 -0
  75. package/dist/thread.js +7 -0
  76. package/dist/thread.reactor.d.ts +82 -0
  77. package/dist/thread.reactor.js +65 -0
  78. package/dist/thread.registry.d.ts +21 -0
  79. package/dist/thread.registry.js +30 -0
  80. package/dist/thread.store.d.ts +121 -0
  81. package/dist/thread.store.js +1 -0
  82. package/dist/thread.toolcalls.d.ts +60 -0
  83. package/dist/thread.toolcalls.js +73 -0
  84. package/dist/tools-to-model-tools.d.ts +19 -0
  85. package/dist/tools-to-model-tools.js +21 -0
  86. package/package.json +133 -0
@@ -0,0 +1,471 @@
1
+ import { getWritable } from "workflow";
2
+ import { registerThreadEnv } from "./env.js";
3
+ import { applyToolExecutionResultToParts } from "./thread.toolcalls.js";
4
+ import { toolsToModelTools } from "./tools-to-model-tools.js";
5
+ import { createAiSdkReactor, } from "./thread.reactor.js";
6
+ import { closeThreadStream, writeContextSubstate, writeThreadPing, writeToolOutputs, } from "./steps/stream.steps.js";
7
+ import { completeExecution, createThreadStep, emitContextIdChunk, initializeContext, saveReactionItem, saveTriggerAndCreateExecution, saveThreadPartsStep, updateThreadStep, updateContextContent, updateItem, } from "./steps/store.steps.js";
8
+ import { getClientResumeHookUrl, toolApprovalHookToken, toolApprovalWebhookToken, } from "./thread.hooks.js";
9
+ export { toolApprovalHookToken, toolApprovalWebhookToken, getClientResumeHookUrl };
10
+ export class Thread {
11
+ constructor(opts = {}, reactor) {
12
+ this.opts = opts;
13
+ this.reactor = reactor ?? createAiSdkReactor();
14
+ }
15
+ /**
16
+ * First-class event expansion stage (runs on every iteration of the durable loop).
17
+ *
18
+ * Use this to expand/normalize events before they are converted into model messages.
19
+ * Typical use-cases:
20
+ * - Expand file/document references into text (LlamaCloud/Reducto/…)
21
+ * - Token compaction / summarization of older parts
22
+ * - Attaching derived context snippets to the next model call
23
+ *
24
+ * IMPORTANT:
25
+ * - This stage is ALWAYS executed by the engine.
26
+ * - If you don't provide an implementation, the default behavior is an identity transform
27
+ * (events pass through unchanged).
28
+ * - If your implementation performs I/O, implement it as a `"use-step"` function (provided via
29
+ * the builder) so results are durable and replay-safe.
30
+ * - If it’s pure/deterministic, it can run in workflow context.
31
+ */
32
+ async expandEvents(events, _context, _env) {
33
+ return events;
34
+ }
35
+ getModel(_context, _env) {
36
+ return "openai/gpt-5";
37
+ }
38
+ getReactor(_context, _env) {
39
+ return this.reactor;
40
+ }
41
+ /**
42
+ * Thread stop/continue hook.
43
+ *
44
+ * After the model streamed and tools executed, the story can decide whether the loop should
45
+ * continue.
46
+ *
47
+ * Default: `true` (continue).
48
+ */
49
+ async shouldContinue(_args) {
50
+ return true;
51
+ }
52
+ async react(incomingEventOrParams, paramsMaybe) {
53
+ return await Thread.runLoop(this, incomingEventOrParams, paramsMaybe);
54
+ }
55
+ static async runLoop(story, incomingEventOrParams, paramsMaybe) {
56
+ const params = typeof incomingEventOrParams?.type === "string" && paramsMaybe
57
+ ? {
58
+ env: paramsMaybe.env,
59
+ triggerEvent: incomingEventOrParams,
60
+ contextIdentifier: paramsMaybe.context ?? null,
61
+ options: paramsMaybe.options,
62
+ }
63
+ : incomingEventOrParams;
64
+ const triggerEvent = params.triggerEvent ?? params.incomingEvent;
65
+ if (!triggerEvent) {
66
+ throw new Error("Thread.react: triggerEvent is required");
67
+ }
68
+ // Register env for step runtimes (workflow-friendly).
69
+ try {
70
+ const { getWorkflowMetadata } = await import("workflow");
71
+ const meta = getWorkflowMetadata?.();
72
+ const runId = meta?.workflowRunId ? String(meta.workflowRunId) : null;
73
+ registerThreadEnv(params.env, runId ?? undefined);
74
+ }
75
+ catch {
76
+ registerThreadEnv(params.env);
77
+ }
78
+ const maxIterations = params.options?.maxIterations ?? 20;
79
+ const maxModelSteps = params.options?.maxModelSteps ?? 1;
80
+ const preventClose = params.options?.preventClose ?? false;
81
+ const sendFinish = params.options?.sendFinish ?? true;
82
+ const silent = params.options?.silent ?? false;
83
+ let writable = params.options?.writable;
84
+ // 1) Ensure context exists (step)
85
+ const ctxResult = await initializeContext(params.env, params.contextIdentifier, { silent, writable });
86
+ const currentContext = ctxResult.context;
87
+ // If the caller didn't provide a writable, we still stream by default (unless silent),
88
+ // using a namespaced stream per context: `context:<contextId>`.
89
+ if (!silent && !writable) {
90
+ writable = getWritable({
91
+ namespace: `context:${String(currentContext.id)}`,
92
+ });
93
+ // If the context was created in `initializeContext` (which didn't have a writable yet),
94
+ // re-emit the context id chunk now so clients can subscribe to the right persisted thread.
95
+ if (ctxResult.isNew) {
96
+ await emitContextIdChunk({
97
+ env: params.env,
98
+ contextId: String(currentContext.id),
99
+ writable,
100
+ });
101
+ }
102
+ }
103
+ const contextSelector = params.contextIdentifier?.id
104
+ ? { id: String(params.contextIdentifier.id) }
105
+ : params.contextIdentifier?.key
106
+ ? { key: params.contextIdentifier.key }
107
+ : { id: String(currentContext.id) };
108
+ if (ctxResult.isNew) {
109
+ await story.opts.onContextCreated?.({ env: params.env, context: currentContext });
110
+ }
111
+ // 2) Persist trigger event + create execution shell (single step)
112
+ const { triggerEventId, reactionEventId, executionId } = await saveTriggerAndCreateExecution({
113
+ env: params.env,
114
+ contextIdentifier: contextSelector,
115
+ triggerEvent,
116
+ });
117
+ // Emit a simple ping chunk early so clients can validate that streaming works end-to-end.
118
+ // This should be ignored safely by clients that don't care about it.
119
+ if (!silent) {
120
+ await writeThreadPing({ label: "thread-start", writable });
121
+ }
122
+ let reactionEvent = null;
123
+ // Latest persisted context state for this run (we keep it in memory; store is updated via steps).
124
+ let updatedContext = currentContext;
125
+ let currentStepId = null;
126
+ const failExecution = async () => {
127
+ try {
128
+ await completeExecution(params.env, contextSelector, executionId, "failed");
129
+ }
130
+ catch {
131
+ // noop
132
+ }
133
+ try {
134
+ if (!silent) {
135
+ await closeThreadStream({ preventClose, sendFinish, writable });
136
+ }
137
+ }
138
+ catch {
139
+ // noop
140
+ }
141
+ };
142
+ try {
143
+ for (let iter = 0; iter < maxIterations; iter++) {
144
+ // Create a persisted step per iteration (IDs generated in step runtime for replay safety)
145
+ const stepCreate = await createThreadStep({
146
+ env: params.env,
147
+ executionId,
148
+ iteration: iter,
149
+ });
150
+ currentStepId = stepCreate.stepId;
151
+ // Hook: Thread DSL `context()` (implemented by subclasses via `initialize()`)
152
+ const nextContent = await story.initialize(updatedContext, params.env);
153
+ updatedContext = await updateContextContent(params.env, contextSelector, nextContent);
154
+ await story.opts.onContextUpdated?.({ env: params.env, context: updatedContext });
155
+ // Hook: Thread DSL `narrative()` (implemented by subclasses via `buildSystemPrompt()`)
156
+ const systemPrompt = await story.buildSystemPrompt(updatedContext, params.env);
157
+ // Hook: Thread DSL `actions()` (implemented by subclasses via `buildTools()`)
158
+ const toolsAll = await story.buildTools(updatedContext, params.env);
159
+ // IMPORTANT: step args must be serializable.
160
+ // Match DurableAgent behavior: convert tool input schemas to plain JSON Schema in workflow context.
161
+ const toolsForModel = toolsToModelTools(toolsAll);
162
+ // Execute model reaction for this iteration using the stable reaction event id.
163
+ //
164
+ // IMPORTANT:
165
+ // We expose a single visible `context_event` per story turn (`reactionEventId`).
166
+ // If we stream with a per-step id, the UI will render an optimistic assistant message
167
+ // (step id) and then a second persisted assistant message (reaction id) with the same
168
+ // content once InstantDB updates.
169
+ const reactor = story.getReactor(updatedContext, params.env);
170
+ const { assistantEvent, toolCalls, messagesForModel } = await reactor({
171
+ env: params.env,
172
+ context: updatedContext,
173
+ contextIdentifier: contextSelector,
174
+ triggerEvent,
175
+ model: story.getModel(updatedContext, params.env),
176
+ systemPrompt,
177
+ actions: toolsAll,
178
+ toolsForModel,
179
+ eventId: reactionEventId,
180
+ executionId,
181
+ contextId: String(currentContext.id),
182
+ stepId: String(stepCreate.stepId),
183
+ iteration: iter,
184
+ maxModelSteps,
185
+ // Only emit a `start` chunk once per story turn.
186
+ sendStart: !silent && iter === 0 && reactionEvent === null,
187
+ silent,
188
+ writable,
189
+ });
190
+ const reviewRequests = toolCalls.length > 0
191
+ ? toolCalls.flatMap((tc) => {
192
+ const toolDef = toolsAll[tc.toolName];
193
+ const auto = toolDef?.auto !== false;
194
+ tc.auto = auto;
195
+ if (auto)
196
+ return [];
197
+ return [
198
+ {
199
+ toolCallId: String(tc.toolCallId),
200
+ toolName: String(tc.toolName ?? ""),
201
+ },
202
+ ];
203
+ })
204
+ : [];
205
+ // Persist normalized parts hanging off the producing step (thread_parts).
206
+ // IMPORTANT:
207
+ // We intentionally do NOT persist the per-step LLM assistant event as a `context_event`.
208
+ // The story exposes a single visible `context_event` per turn (`reactionEventId`) so the UI
209
+ // doesn't render duplicate assistant messages (LLM-step + aggregated reaction).
210
+ const stepParts = (assistantEvent?.content?.parts ?? []);
211
+ await saveThreadPartsStep({
212
+ env: params.env,
213
+ stepId: stepCreate.stepId,
214
+ parts: stepParts,
215
+ executionId,
216
+ contextId: String(currentContext.id),
217
+ iteration: iter,
218
+ });
219
+ // Persist/append the aggregated reaction event (stable `reactionEventId` for the execution).
220
+ if (!reactionEvent) {
221
+ const reactionPayload = {
222
+ ...assistantEvent,
223
+ status: "pending",
224
+ };
225
+ reactionEvent = await saveReactionItem(params.env, contextSelector, reactionPayload, {
226
+ executionId,
227
+ contextId: String(currentContext.id),
228
+ reviewRequests,
229
+ });
230
+ }
231
+ else {
232
+ reactionEvent = await updateItem(params.env, reactionEvent.id, {
233
+ ...reactionEvent,
234
+ content: {
235
+ parts: [
236
+ ...(reactionEvent?.content?.parts ?? []),
237
+ ...(assistantEvent?.content?.parts ?? []),
238
+ ],
239
+ },
240
+ status: "pending",
241
+ }, { executionId, contextId: String(currentContext.id) });
242
+ }
243
+ story.opts.onEventCreated?.(assistantEvent);
244
+ // Done: no tool calls requested by the model
245
+ if (!toolCalls.length) {
246
+ const endResult = await story.callOnEnd(assistantEvent);
247
+ if (endResult) {
248
+ // Mark iteration step completed (no tools)
249
+ await updateThreadStep({
250
+ env: params.env,
251
+ stepId: stepCreate.stepId,
252
+ patch: {
253
+ status: "completed",
254
+ toolCalls: [],
255
+ toolExecutionResults: [],
256
+ continueLoop: false,
257
+ },
258
+ executionId,
259
+ contextId: String(currentContext.id),
260
+ iteration: iter,
261
+ });
262
+ // Mark reaction event completed
263
+ await updateItem(params.env, reactionEventId, {
264
+ ...reactionEvent,
265
+ status: "completed",
266
+ }, { executionId, contextId: String(currentContext.id) });
267
+ await completeExecution(params.env, contextSelector, executionId, "completed");
268
+ if (!silent) {
269
+ await closeThreadStream({ preventClose, sendFinish, writable });
270
+ }
271
+ return {
272
+ contextId: currentContext.id,
273
+ context: updatedContext,
274
+ triggerEventId,
275
+ reactionEventId,
276
+ executionId,
277
+ };
278
+ }
279
+ }
280
+ // Execute tool calls (workflow context; tool implementations decide step vs workflow)
281
+ if (!silent && toolCalls.length) {
282
+ await writeContextSubstate({ key: "actions", transient: true, writable });
283
+ }
284
+ const executionResults = await Promise.all(toolCalls.map(async (tc) => {
285
+ const toolDef = toolsAll[tc.toolName];
286
+ if (!toolDef || typeof toolDef.execute !== "function") {
287
+ return {
288
+ tc,
289
+ success: false,
290
+ output: null,
291
+ errorText: `Tool "${tc.toolName}" not found or has no execute().`,
292
+ };
293
+ }
294
+ try {
295
+ let toolArgs = tc.args;
296
+ if (toolDef?.auto === false) {
297
+ const { createHook, createWebhook } = await import("workflow");
298
+ const toolCallId = String(tc.toolCallId);
299
+ const hookToken = toolApprovalHookToken({ executionId, toolCallId });
300
+ const webhookToken = toolApprovalWebhookToken({ executionId, toolCallId });
301
+ const hook = createHook({ token: hookToken });
302
+ const webhook = createWebhook({ token: webhookToken });
303
+ const approvalOrRequest = await Promise.race([
304
+ hook.then((approval) => ({ source: "hook", approval })),
305
+ webhook.then((request) => ({ source: "webhook", request })),
306
+ ]);
307
+ const approval = approvalOrRequest.source === "hook"
308
+ ? approvalOrRequest.approval
309
+ : await approvalOrRequest.request.json().catch(() => null);
310
+ if (!approval || approval.approved !== true) {
311
+ return {
312
+ tc,
313
+ success: false,
314
+ output: null,
315
+ errorText: approval && "comment" in approval && approval.comment
316
+ ? `Tool execution not approved: ${approval.comment}`
317
+ : "Tool execution not approved",
318
+ };
319
+ }
320
+ if ("args" in approval && approval.args !== undefined) {
321
+ toolArgs = approval.args;
322
+ }
323
+ }
324
+ const output = await toolDef.execute(toolArgs, {
325
+ toolCallId: tc.toolCallId,
326
+ messages: messagesForModel,
327
+ eventId: reactionEventId,
328
+ executionId,
329
+ triggerEventId,
330
+ contextId: currentContext.id,
331
+ });
332
+ return { tc, success: true, output };
333
+ }
334
+ catch (e) {
335
+ return {
336
+ tc,
337
+ success: false,
338
+ output: null,
339
+ errorText: e instanceof Error ? e.message : String(e),
340
+ };
341
+ }
342
+ }));
343
+ // Emit tool outputs to the workflow stream (step)
344
+ if (!silent) {
345
+ await writeToolOutputs({
346
+ results: executionResults.map((r) => r.success
347
+ ? { toolCallId: r.tc.toolCallId, success: true, output: r.output }
348
+ : {
349
+ toolCallId: r.tc.toolCallId,
350
+ success: false,
351
+ errorText: r.errorText,
352
+ }),
353
+ writable,
354
+ });
355
+ }
356
+ // Clear action status once tool execution results have been emitted.
357
+ if (!silent && toolCalls.length) {
358
+ await writeContextSubstate({ key: null, transient: true, writable });
359
+ }
360
+ // Merge tool results into persisted parts (so next LLM call can see them)
361
+ if (reactionEvent) {
362
+ let parts = reactionEvent?.content?.parts ?? [];
363
+ for (const r of executionResults) {
364
+ parts = applyToolExecutionResultToParts(parts, r.tc, {
365
+ success: Boolean(r.success),
366
+ result: r.output,
367
+ message: r.errorText,
368
+ });
369
+ }
370
+ reactionEvent = await updateItem(params.env, reactionEventId, {
371
+ ...reactionEvent,
372
+ content: { parts },
373
+ status: "pending",
374
+ }, { executionId, contextId: String(currentContext.id) });
375
+ }
376
+ // Callback for observability/integration
377
+ for (const r of executionResults) {
378
+ await story.opts.onToolCallExecuted?.({
379
+ toolCall: r.tc,
380
+ success: r.success,
381
+ output: r.output,
382
+ errorText: r.errorText,
383
+ eventId: reactionEventId,
384
+ executionId,
385
+ });
386
+ }
387
+ // Stop/continue boundary: allow the Thread to decide if the loop should continue.
388
+ // IMPORTANT: we call this after tool results have been merged into the persisted `reactionEvent`,
389
+ // so stories can inspect `reactionEvent.content.parts` deterministically.
390
+ const continueLoop = await story.shouldContinue({
391
+ env: params.env,
392
+ context: updatedContext,
393
+ reactionEvent: reactionEvent ?? assistantEvent,
394
+ assistantEvent,
395
+ toolCalls,
396
+ toolExecutionResults: executionResults,
397
+ });
398
+ // Persist per-iteration step outcome (tools + continue signal)
399
+ await updateThreadStep({
400
+ env: params.env,
401
+ stepId: stepCreate.stepId,
402
+ patch: {
403
+ status: "completed",
404
+ toolCalls,
405
+ toolExecutionResults: executionResults,
406
+ continueLoop: continueLoop !== false,
407
+ },
408
+ executionId,
409
+ contextId: String(currentContext.id),
410
+ iteration: iter,
411
+ });
412
+ if (continueLoop === false) {
413
+ await updateItem(params.env, reactionEventId, {
414
+ ...reactionEvent,
415
+ status: "completed",
416
+ }, { executionId, contextId: String(currentContext.id) });
417
+ await completeExecution(params.env, contextSelector, executionId, "completed");
418
+ if (!silent) {
419
+ await closeThreadStream({ preventClose, sendFinish, writable });
420
+ }
421
+ return {
422
+ contextId: currentContext.id,
423
+ context: updatedContext,
424
+ triggerEventId,
425
+ reactionEventId,
426
+ executionId,
427
+ };
428
+ }
429
+ }
430
+ throw new Error(`Thread: maxIterations reached (${maxIterations}) without completion`);
431
+ }
432
+ catch (error) {
433
+ // Best-effort: persist failure on the current iteration step (if any)
434
+ if (currentStepId) {
435
+ try {
436
+ await updateThreadStep({
437
+ env: params.env,
438
+ stepId: currentStepId,
439
+ patch: {
440
+ status: "failed",
441
+ errorText: error instanceof Error ? error.message : String(error),
442
+ },
443
+ executionId,
444
+ contextId: String(currentContext.id),
445
+ });
446
+ }
447
+ catch {
448
+ // noop
449
+ }
450
+ }
451
+ await failExecution();
452
+ throw error;
453
+ }
454
+ }
455
+ /**
456
+ * @deprecated Use `react()` instead. Kept for backwards compatibility.
457
+ */
458
+ async stream(triggerEvent, params) {
459
+ return await this.react(triggerEvent, params);
460
+ }
461
+ async callOnEnd(lastEvent) {
462
+ if (!this.opts.onEnd)
463
+ return true;
464
+ const result = await this.opts.onEnd(lastEvent);
465
+ if (typeof result === "boolean")
466
+ return result;
467
+ if (result && typeof result === "object" && "end" in result)
468
+ return Boolean(result.end);
469
+ return true;
470
+ }
471
+ }
@@ -0,0 +1,35 @@
1
+ import { type ModelMessage, type UIMessage } from "ai";
2
+ import type { ThreadItem } from "./thread.store.js";
3
+ export declare const INPUT_TEXT_ITEM_TYPE = "input_text";
4
+ export declare const OUTPUT_TEXT_ITEM_TYPE = "output_text";
5
+ export declare const SYSTEM_TEXT_ITEM_TYPE = "ekairos:system";
6
+ export declare const WEB_CHANNEL = "web";
7
+ export declare const AGENT_CHANNEL = "whatsapp";
8
+ export declare const EMAIL_CHANNEL = "email";
9
+ export declare function createUserItemFromUIMessages(messages: UIMessage[]): ThreadItem;
10
+ export declare function createAssistantItemFromUIMessages(itemId: string, messages: UIMessage[]): ThreadItem;
11
+ export declare function convertToUIMessage(item: ThreadItem): UIMessage;
12
+ /**
13
+ * Converts stored ThreadItems to AI SDK ModelMessages.
14
+ *
15
+ * IMPORTANT:
16
+ * - Store-agnostic and workflow-safe.
17
+ * - Attachment/document handling MUST happen in the store boundary:
18
+ * `ThreadStore.itemsToModelMessages(items)`.
19
+ */
20
+ export declare function convertItemsToModelMessages(items: ThreadItem[]): Promise<ModelMessage[]>;
21
+ export declare function convertItemToModelMessages(item: ThreadItem): Promise<ModelMessage[]>;
22
+ export type AIMessage = {
23
+ id: string;
24
+ role: "user" | "assistant" | "system";
25
+ content: string;
26
+ createdAt: Date;
27
+ };
28
+ export type ResponseMessage = {
29
+ id: string;
30
+ timestamp: Date;
31
+ modelId: string;
32
+ headers?: Record<string, string>;
33
+ message: ModelMessage;
34
+ };
35
+ export declare function convertModelMessageToItem(itemId: string, message: ResponseMessage): ThreadItem;
@@ -0,0 +1,105 @@
1
+ import { convertToModelMessages } from "ai";
2
+ export const INPUT_TEXT_ITEM_TYPE = "input_text";
3
+ export const OUTPUT_TEXT_ITEM_TYPE = "output_text";
4
+ export const SYSTEM_TEXT_ITEM_TYPE = "ekairos:system";
5
+ export const WEB_CHANNEL = "web";
6
+ export const AGENT_CHANNEL = "whatsapp";
7
+ export const EMAIL_CHANNEL = "email";
8
+ export function createUserItemFromUIMessages(messages) {
9
+ if (!Array.isArray(messages) || messages.length === 0) {
10
+ throw new Error("Missing messages to create item");
11
+ }
12
+ const lastMessage = messages[messages.length - 1];
13
+ return {
14
+ id: lastMessage.id,
15
+ type: INPUT_TEXT_ITEM_TYPE,
16
+ channel: WEB_CHANNEL,
17
+ content: {
18
+ parts: lastMessage.parts,
19
+ },
20
+ createdAt: new Date().toISOString(),
21
+ };
22
+ }
23
+ export function createAssistantItemFromUIMessages(itemId, messages) {
24
+ if (!Array.isArray(messages) || messages.length === 0) {
25
+ throw new Error("Missing messages to create item");
26
+ }
27
+ const lastMessage = messages[messages.length - 1];
28
+ return {
29
+ id: itemId,
30
+ type: OUTPUT_TEXT_ITEM_TYPE,
31
+ channel: WEB_CHANNEL,
32
+ content: {
33
+ parts: lastMessage.parts,
34
+ },
35
+ createdAt: new Date().toISOString(),
36
+ };
37
+ }
38
+ export function convertToUIMessage(item) {
39
+ let role;
40
+ if (item.type === INPUT_TEXT_ITEM_TYPE) {
41
+ role = "user";
42
+ }
43
+ else if (item.type === SYSTEM_TEXT_ITEM_TYPE) {
44
+ role = "system";
45
+ }
46
+ else {
47
+ role = "assistant";
48
+ }
49
+ return {
50
+ id: item.id,
51
+ role: role,
52
+ parts: item.content.parts,
53
+ metadata: {
54
+ channel: item.channel,
55
+ type: item.type,
56
+ createdAt: item.createdAt,
57
+ }
58
+ };
59
+ }
60
+ /**
61
+ * Converts stored ThreadItems to AI SDK ModelMessages.
62
+ *
63
+ * IMPORTANT:
64
+ * - Store-agnostic and workflow-safe.
65
+ * - Attachment/document handling MUST happen in the store boundary:
66
+ * `ThreadStore.itemsToModelMessages(items)`.
67
+ */
68
+ export async function convertItemsToModelMessages(items) {
69
+ const results = [];
70
+ for (const item of items) {
71
+ results.push(await convertItemToModelMessages(item));
72
+ }
73
+ return results.flat();
74
+ }
75
+ export async function convertItemToModelMessages(item) {
76
+ const message = convertToUIMessage(item);
77
+ return convertToModelMessages([message]);
78
+ }
79
+ export function convertModelMessageToItem(itemId, message) {
80
+ let type;
81
+ switch (message.message.role) {
82
+ case "user":
83
+ type = INPUT_TEXT_ITEM_TYPE;
84
+ break;
85
+ case "assistant":
86
+ type = OUTPUT_TEXT_ITEM_TYPE;
87
+ break;
88
+ case "system":
89
+ type = SYSTEM_TEXT_ITEM_TYPE;
90
+ break;
91
+ default:
92
+ // Fallback for roles not mapped to our item types (e.g. tool).
93
+ type = OUTPUT_TEXT_ITEM_TYPE;
94
+ break;
95
+ }
96
+ return {
97
+ id: itemId,
98
+ type: type,
99
+ channel: WEB_CHANNEL,
100
+ content: {
101
+ parts: message.message.content,
102
+ },
103
+ createdAt: message.timestamp.toISOString(),
104
+ };
105
+ }
@@ -0,0 +1,21 @@
1
+ export declare function getClientResumeHookUrl(): string | undefined;
2
+ /**
3
+ * Deterministic hook token for approving an `auto: false` tool call.
4
+ *
5
+ * External systems can resume the hook with:
6
+ * `resumeHook(toolApprovalHookToken({ executionId, toolCallId }), { approved: true })`
7
+ */
8
+ export declare function toolApprovalHookToken(params: {
9
+ executionId: string;
10
+ toolCallId: string;
11
+ }): string;
12
+ /**
13
+ * Deterministic webhook token for approving an `auto: false` tool call.
14
+ *
15
+ * When using Workflow DevKit, the webhook is available at:
16
+ * `/.well-known/workflow/v1/webhook/:token`
17
+ */
18
+ export declare function toolApprovalWebhookToken(params: {
19
+ executionId: string;
20
+ toolCallId: string;
21
+ }): string;
@@ -0,0 +1,31 @@
1
+ function stripTrailingSlash(value) {
2
+ return value.replace(/\/$/, "");
3
+ }
4
+ export function getClientResumeHookUrl() {
5
+ const direct = process.env.EKAIROS_CLIENT_RESUME_HOOK_URL;
6
+ if (typeof direct === "string" && direct.trim())
7
+ return direct.trim();
8
+ const base = process.env.EKAIROS_CLIENT_BASE_URL;
9
+ if (typeof base === "string" && base.trim()) {
10
+ return `${stripTrailingSlash(base.trim())}/api/ekairos/resume-hook`;
11
+ }
12
+ return undefined;
13
+ }
14
+ /**
15
+ * Deterministic hook token for approving an `auto: false` tool call.
16
+ *
17
+ * External systems can resume the hook with:
18
+ * `resumeHook(toolApprovalHookToken({ executionId, toolCallId }), { approved: true })`
19
+ */
20
+ export function toolApprovalHookToken(params) {
21
+ return `ekairos_thread:tool-approval:${params.executionId}:${params.toolCallId}`;
22
+ }
23
+ /**
24
+ * Deterministic webhook token for approving an `auto: false` tool call.
25
+ *
26
+ * When using Workflow DevKit, the webhook is available at:
27
+ * `/.well-known/workflow/v1/webhook/:token`
28
+ */
29
+ export function toolApprovalWebhookToken(params) {
30
+ return `ekairos_thread:tool-approval-webhook:${params.executionId}:${params.toolCallId}`;
31
+ }
package/dist/thread.js ADDED
@@ -0,0 +1,7 @@
1
+ export {
2
+ // engine
3
+ Thread, } from "./thread.engine.js";
4
+ export {
5
+ // builder
6
+ thread, createThread, } from "./thread.builder.js";
7
+ export { createAiSdkReactor, } from "./thread.reactor.js";