@ekairos/thread 1.22.1 → 1.22.2
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 +355 -273
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,45 +1,10 @@
|
|
|
1
|
-
# @ekairos/thread
|
|
1
|
+
# @ekairos/thread
|
|
2
2
|
|
|
3
|
-
Durable
|
|
3
|
+
Durable thread engine for Workflow-compatible AI agents.
|
|
4
4
|
|
|
5
|
-
`@ekairos/thread`
|
|
5
|
+
`@ekairos/thread` is the execution layer used by Ekairos agents. It persists context, items, steps, parts, and executions, while streaming UI chunks and enforcing transition contracts.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
- persistence-first,
|
|
9
|
-
- traceable by design,
|
|
10
|
-
- simple to embed in domain applications.
|
|
11
|
-
|
|
12
|
-
It is the runtime used by Ekairos coding agents and domain agents.
|
|
13
|
-
|
|
14
|
-
## Why Thread
|
|
15
|
-
|
|
16
|
-
Most chat abstractions stop at "messages in, text out".
|
|
17
|
-
Thread models the full lifecycle:
|
|
18
|
-
|
|
19
|
-
1. Persist trigger event.
|
|
20
|
-
2. Create execution.
|
|
21
|
-
3. Run model reaction.
|
|
22
|
-
4. Persist normalized parts.
|
|
23
|
-
5. Execute actions (tools).
|
|
24
|
-
6. Persist tool outcomes.
|
|
25
|
-
7. Decide continue or end.
|
|
26
|
-
8. Emit traces for every durable step.
|
|
27
|
-
|
|
28
|
-
This design supports long-running, resumable agent runs without losing state.
|
|
29
|
-
|
|
30
|
-
## Core Concepts
|
|
31
|
-
|
|
32
|
-
- `Thread`: durable loop orchestrator.
|
|
33
|
-
- `Reactor`: pluggable reaction implementation (`AI SDK`, `Codex`, `Claude`, `Cursor`, ...).
|
|
34
|
-
- `Thread Key`: stable public identifier (`thread.key`) for continuity.
|
|
35
|
-
- `Context`: typed persistent state attached to a thread.
|
|
36
|
-
- `Item`: normalized event (`input_text`, `output_text`, etc).
|
|
37
|
-
- `Execution`: one run for a trigger/reaction pair.
|
|
38
|
-
- `Step`: one loop iteration inside an execution.
|
|
39
|
-
- `Part`: normalized content fragment persisted by step.
|
|
40
|
-
- `Trace`: machine timeline (`thread.*`, `workflow.*`) for observability.
|
|
41
|
-
|
|
42
|
-
## Installation
|
|
7
|
+
## Install
|
|
43
8
|
|
|
44
9
|
```bash
|
|
45
10
|
pnpm add @ekairos/thread
|
|
@@ -54,310 +19,427 @@ Optional subpaths:
|
|
|
54
19
|
- `@ekairos/thread/mcp`
|
|
55
20
|
- `@ekairos/thread/oidc`
|
|
56
21
|
|
|
57
|
-
##
|
|
58
|
-
|
|
59
|
-
###
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
22
|
+
## Package Surface (from `src/index.ts`)
|
|
23
|
+
|
|
24
|
+
### Core builders and engine
|
|
25
|
+
|
|
26
|
+
- `createThread`
|
|
27
|
+
- `thread`
|
|
28
|
+
- `Thread`
|
|
29
|
+
- `type ThreadConfig`
|
|
30
|
+
- `type ThreadInstance`
|
|
31
|
+
- `type RegistrableThreadBuilder`
|
|
32
|
+
- `type ThreadOptions`
|
|
33
|
+
- `type ThreadStreamOptions`
|
|
34
|
+
|
|
35
|
+
### Reactors
|
|
36
|
+
|
|
37
|
+
- `createAiSdkReactor`
|
|
38
|
+
- `createScriptedReactor`
|
|
39
|
+
- `type ThreadReactor`
|
|
40
|
+
- `type ThreadReactorParams`
|
|
41
|
+
- `type ThreadReactionResult`
|
|
42
|
+
- `type ThreadReactionToolCall`
|
|
43
|
+
- `type ThreadReactionLLM`
|
|
44
|
+
- `type CreateAiSdkReactorOptions`
|
|
45
|
+
- `type CreateScriptedReactorOptions`
|
|
46
|
+
- `type ScriptedReactorStep`
|
|
47
|
+
|
|
48
|
+
### Contracts and transitions
|
|
49
|
+
|
|
50
|
+
- `THREAD_STATUSES`
|
|
51
|
+
- `THREAD_CONTEXT_STATUSES`
|
|
52
|
+
- `THREAD_EXECUTION_STATUSES`
|
|
53
|
+
- `THREAD_STEP_STATUSES`
|
|
54
|
+
- `THREAD_ITEM_STATUSES`
|
|
55
|
+
- `THREAD_ITEM_TYPES`
|
|
56
|
+
- `THREAD_CHANNELS`
|
|
57
|
+
- `THREAD_TRACE_EVENT_KINDS`
|
|
58
|
+
- `THREAD_STREAM_CHUNK_TYPES`
|
|
59
|
+
- `THREAD_CONTEXT_SUBSTATE_KEYS`
|
|
60
|
+
- `THREAD_THREAD_TRANSITIONS`
|
|
61
|
+
- `THREAD_CONTEXT_TRANSITIONS`
|
|
62
|
+
- `THREAD_EXECUTION_TRANSITIONS`
|
|
63
|
+
- `THREAD_STEP_TRANSITIONS`
|
|
64
|
+
- `THREAD_ITEM_TRANSITIONS`
|
|
65
|
+
- `can*Transition`, `assert*Transition`
|
|
66
|
+
- `assertThreadPartKey`
|
|
67
|
+
|
|
68
|
+
### Stream and parsing
|
|
69
|
+
|
|
70
|
+
- `parseThreadStreamEvent`
|
|
71
|
+
- `assertThreadStreamTransitions`
|
|
72
|
+
- `validateThreadStreamTimeline`
|
|
73
|
+
- `type ThreadStreamEvent`
|
|
74
|
+
- `type ContextCreatedEvent`
|
|
75
|
+
- `type ContextResolvedEvent`
|
|
76
|
+
- `type ContextStatusChangedEvent`
|
|
77
|
+
- `type ThreadCreatedEvent`
|
|
78
|
+
- `type ThreadResolvedEvent`
|
|
79
|
+
- `type ThreadStatusChangedEvent`
|
|
80
|
+
- `type ExecutionCreatedEvent`
|
|
81
|
+
- `type ExecutionStatusChangedEvent`
|
|
82
|
+
- `type ItemCreatedEvent`
|
|
83
|
+
- `type ItemStatusChangedEvent`
|
|
84
|
+
- `type StepCreatedEvent`
|
|
85
|
+
- `type StepStatusChangedEvent`
|
|
86
|
+
- `type PartCreatedEvent`
|
|
87
|
+
- `type PartUpdatedEvent`
|
|
88
|
+
- `type ChunkEmittedEvent`
|
|
89
|
+
- `type ThreadFinishedEvent`
|
|
90
|
+
|
|
91
|
+
### Event conversion helpers
|
|
92
|
+
|
|
93
|
+
- `createUserItemFromUIMessages`
|
|
94
|
+
- `createAssistantItemFromUIMessages`
|
|
95
|
+
- `convertToUIMessage`
|
|
96
|
+
- `convertItemToModelMessages`
|
|
97
|
+
- `convertItemsToModelMessages`
|
|
98
|
+
- `convertModelMessageToItem`
|
|
99
|
+
- `didToolExecute`
|
|
100
|
+
- `extractToolCallsFromParts`
|
|
101
|
+
|
|
102
|
+
### React hook
|
|
103
|
+
|
|
104
|
+
- `useThread`
|
|
105
|
+
- `type UseThreadOptions`
|
|
106
|
+
- `type ThreadSnapshot`
|
|
107
|
+
- `type ThreadStreamChunk`
|
|
108
|
+
|
|
109
|
+
### Registry / codex
|
|
110
|
+
|
|
111
|
+
- `registerThread`
|
|
112
|
+
- `getThread`
|
|
113
|
+
- `getThreadFactory`
|
|
114
|
+
- `hasThread`
|
|
115
|
+
- `listThreads`
|
|
116
|
+
- `createCodexThreadBuilder`
|
|
117
|
+
- codex defaults/types from `codex.ts`
|
|
118
|
+
|
|
119
|
+
## Thread API Specification
|
|
120
|
+
|
|
121
|
+
## `createThread`
|
|
63
122
|
|
|
64
123
|
```ts
|
|
65
|
-
|
|
66
|
-
import { configureRuntime } from "@ekairos/domain/runtime";
|
|
67
|
-
import { getOrgAdminDb } from "@/lib/admin-org-db";
|
|
68
|
-
import appDomain from "@/lib/domain";
|
|
69
|
-
|
|
70
|
-
export const runtimeConfig = configureRuntime({
|
|
71
|
-
runtime: async (env: { orgId: string }) => {
|
|
72
|
-
const db = await getOrgAdminDb(env.orgId, appDomain);
|
|
73
|
-
return { db };
|
|
74
|
-
},
|
|
75
|
-
domain: { domain: appDomain },
|
|
76
|
-
});
|
|
124
|
+
createThread<Env>(key: ThreadKey)
|
|
77
125
|
```
|
|
78
126
|
|
|
79
|
-
|
|
127
|
+
Builder stages:
|
|
80
128
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
1. `.context((storedContext, env) => context)` (required)
|
|
130
|
+
2. `.expandEvents((events, context, env) => events)` (optional)
|
|
131
|
+
3. `.narrative((context, env) => string)` (required)
|
|
132
|
+
4. `.actions((context, env) => Record<string, ThreadTool>)` (required)
|
|
133
|
+
5. `.model(modelInit | selector)` (optional)
|
|
134
|
+
6. `.reactor(reactor)` (optional, default is AI SDK reactor)
|
|
135
|
+
7. `.shouldContinue(({ reactionEvent, toolCalls, toolExecutionResults, ... }) => boolean)` (optional)
|
|
136
|
+
8. `.opts(threadOptions)` (optional)
|
|
85
137
|
|
|
86
|
-
|
|
87
|
-
type Ctx = { orgId: string; sessionId: string };
|
|
138
|
+
Builder terminals:
|
|
88
139
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}))
|
|
95
|
-
.narrative((ctx) => `You are a precise assistant. Session=${ctx.content?.sessionId}`)
|
|
96
|
-
.actions(() => ({
|
|
97
|
-
ping: tool({
|
|
98
|
-
description: "Return pong",
|
|
99
|
-
inputSchema: z.object({ text: z.string().optional() }),
|
|
100
|
-
execute: async ({ text }) => ({ pong: text ?? "ok" }),
|
|
101
|
-
}),
|
|
102
|
-
}))
|
|
103
|
-
.model("openai/gpt-5.2")
|
|
104
|
-
.build();
|
|
105
|
-
```
|
|
140
|
+
- `.build()` -> `ThreadInstance`
|
|
141
|
+
- `.react(triggerEvent, params)`
|
|
142
|
+
- `.stream(triggerEvent, params)` (deprecated alias)
|
|
143
|
+
- `.register()`
|
|
144
|
+
- `.config()`
|
|
106
145
|
|
|
107
|
-
###
|
|
146
|
+
### `ThreadConfig<Context, Env>`
|
|
108
147
|
|
|
109
|
-
|
|
148
|
+
Required keys:
|
|
110
149
|
|
|
111
|
-
-
|
|
112
|
-
-
|
|
150
|
+
- `context`
|
|
151
|
+
- `narrative`
|
|
152
|
+
- `actions` (or legacy `tools`)
|
|
113
153
|
|
|
114
|
-
|
|
115
|
-
import { createThread, createAiSdkReactor } from "@ekairos/thread";
|
|
154
|
+
Optional keys:
|
|
116
155
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
.build();
|
|
123
|
-
```
|
|
156
|
+
- `expandEvents`
|
|
157
|
+
- `model`
|
|
158
|
+
- `reactor`
|
|
159
|
+
- `shouldContinue`
|
|
160
|
+
- `opts`
|
|
124
161
|
|
|
125
|
-
`
|
|
162
|
+
### `Thread.react`
|
|
126
163
|
|
|
127
|
-
|
|
128
|
-
import { createAiSdkReactor } from "@ekairos/thread";
|
|
164
|
+
Primary form:
|
|
129
165
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
selectMaxModelSteps: ({ config, baseMaxModelSteps }) =>
|
|
137
|
-
typeof config.maxModelSteps === "number"
|
|
138
|
-
? config.maxModelSteps
|
|
139
|
-
: baseMaxModelSteps,
|
|
140
|
-
});
|
|
166
|
+
```ts
|
|
167
|
+
thread.react(triggerEvent, {
|
|
168
|
+
env,
|
|
169
|
+
context: { id } | { key } | null,
|
|
170
|
+
options,
|
|
171
|
+
})
|
|
141
172
|
```
|
|
142
173
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
- `@ekairos/openai-reactor` (`createOpenAIReactor`, `createCodexReactor`)
|
|
146
|
-
- `@ekairos/claude-reactor` (scaffold)
|
|
147
|
-
- `@ekairos/cursor-reactor` (scaffold)
|
|
148
|
-
|
|
149
|
-
### 3) Run from a workflow
|
|
174
|
+
Return shape:
|
|
150
175
|
|
|
151
176
|
```ts
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
env: { orgId: string; sessionId: string };
|
|
159
|
-
triggerEvent: ThreadItem;
|
|
160
|
-
threadKey?: string;
|
|
161
|
-
}) {
|
|
162
|
-
"use workflow";
|
|
163
|
-
|
|
164
|
-
const writable = getWritable<UIMessageChunk>();
|
|
165
|
-
return await helloThread.react(params.triggerEvent, {
|
|
166
|
-
env: params.env,
|
|
167
|
-
context: params.threadKey ? { key: params.threadKey } : null,
|
|
168
|
-
options: { writable, maxIterations: 2, maxModelSteps: 1 },
|
|
169
|
-
});
|
|
177
|
+
{
|
|
178
|
+
contextId: string;
|
|
179
|
+
context: StoredContext<Context>;
|
|
180
|
+
triggerEventId: string;
|
|
181
|
+
reactionEventId: string;
|
|
182
|
+
executionId: string;
|
|
170
183
|
}
|
|
171
184
|
```
|
|
172
185
|
|
|
173
|
-
|
|
186
|
+
### `ThreadStreamOptions`
|
|
174
187
|
|
|
175
|
-
|
|
188
|
+
- `maxIterations?: number` (default `20`)
|
|
189
|
+
- `maxModelSteps?: number` (default `1`)
|
|
190
|
+
- `preventClose?: boolean` (default `false`)
|
|
191
|
+
- `sendFinish?: boolean` (default `true`)
|
|
192
|
+
- `silent?: boolean` (default `false`)
|
|
193
|
+
- `writable?: WritableStream<UIMessageChunk>`
|
|
176
194
|
|
|
177
|
-
|
|
178
|
-
2. `saveTriggerAndCreateExecution` persists trigger and execution.
|
|
179
|
-
3. `createThreadStep` starts iteration record.
|
|
180
|
-
4. `buildSystemPrompt` and `buildTools` are evaluated.
|
|
181
|
-
5. `executeReaction` runs model + tool call planning.
|
|
182
|
-
6. `saveThreadPartsStep` persists normalized parts.
|
|
183
|
-
7. `saveReactionItem` or `updateItem` updates stable reaction item.
|
|
184
|
-
8. Tool executions run and are merged into persisted parts.
|
|
185
|
-
9. `shouldContinue(...)` decides next iteration or completion.
|
|
186
|
-
10. `completeExecution` closes run status.
|
|
195
|
+
### `ThreadOptions`
|
|
187
196
|
|
|
188
|
-
|
|
197
|
+
Lifecycle callbacks:
|
|
189
198
|
|
|
190
|
-
|
|
199
|
+
- `onContextCreated`
|
|
200
|
+
- `onContextUpdated`
|
|
201
|
+
- `onEventCreated`
|
|
202
|
+
- `onToolCallExecuted`
|
|
203
|
+
- `onEnd`
|
|
191
204
|
|
|
192
|
-
|
|
205
|
+
## Reactor Specification
|
|
193
206
|
|
|
194
|
-
|
|
195
|
-
- `createAssistantItemFromUIMessages(...)`
|
|
196
|
-
- `convertItemsToModelMessages(...)`
|
|
197
|
-
- `convertModelMessageToItem(...)`
|
|
198
|
-
- `didToolExecute(...)`
|
|
199
|
-
- `extractToolCallsFromParts(...)`
|
|
207
|
+
A reactor receives the full execution context for one iteration and returns normalized assistant output + tool calls.
|
|
200
208
|
|
|
201
|
-
|
|
209
|
+
### `ThreadReactorParams`
|
|
202
210
|
|
|
203
|
-
|
|
211
|
+
- `env`
|
|
212
|
+
- `context`
|
|
213
|
+
- `contextIdentifier`
|
|
214
|
+
- `triggerEvent`
|
|
215
|
+
- `model`
|
|
216
|
+
- `systemPrompt`
|
|
217
|
+
- `actions`
|
|
218
|
+
- `toolsForModel`
|
|
219
|
+
- `eventId`
|
|
220
|
+
- `executionId`
|
|
221
|
+
- `contextId`
|
|
222
|
+
- `stepId`
|
|
223
|
+
- `iteration`
|
|
224
|
+
- `maxModelSteps`
|
|
225
|
+
- `sendStart`
|
|
226
|
+
- `silent`
|
|
227
|
+
- `writable`
|
|
204
228
|
|
|
205
|
-
|
|
229
|
+
### `ThreadReactionResult`
|
|
206
230
|
|
|
207
|
-
|
|
231
|
+
- `assistantEvent: ThreadItem`
|
|
232
|
+
- `toolCalls: ThreadReactionToolCall[]`
|
|
233
|
+
- `messagesForModel: ModelMessage[]`
|
|
234
|
+
- `llm?: ThreadReactionLLM`
|
|
208
235
|
|
|
209
|
-
-
|
|
236
|
+
## Built-in Reactors
|
|
210
237
|
|
|
211
|
-
|
|
238
|
+
## `createAiSdkReactor` (production default)
|
|
212
239
|
|
|
213
|
-
|
|
214
|
-
- `thread_contexts`
|
|
215
|
-
- `thread_items`
|
|
216
|
-
- `thread_executions`
|
|
217
|
-
- `thread_steps`
|
|
218
|
-
- `thread_parts`
|
|
219
|
-
- `thread_trace_events`
|
|
220
|
-
- `thread_trace_runs`
|
|
221
|
-
- `thread_trace_spans`
|
|
222
|
-
|
|
223
|
-
Import domain schema:
|
|
240
|
+
Uses AI SDK streaming + tool extraction through engine steps.
|
|
224
241
|
|
|
225
242
|
```ts
|
|
226
|
-
import {
|
|
227
|
-
```
|
|
243
|
+
import { createAiSdkReactor } from "@ekairos/thread";
|
|
228
244
|
|
|
229
|
-
|
|
245
|
+
const reactor = createAiSdkReactor({
|
|
246
|
+
resolveConfig: async ({ env, context, iteration }) => {
|
|
247
|
+
"use step";
|
|
248
|
+
return {
|
|
249
|
+
model: env.model ?? "openai/gpt-5.2",
|
|
250
|
+
maxModelSteps: iteration === 0 ? 2 : 1,
|
|
251
|
+
tenant: context.content?.orgId,
|
|
252
|
+
};
|
|
253
|
+
},
|
|
254
|
+
selectModel: ({ baseModel, config }) => config.model ?? baseModel,
|
|
255
|
+
selectMaxModelSteps: ({ baseMaxModelSteps, config }) =>
|
|
256
|
+
typeof config.maxModelSteps === "number"
|
|
257
|
+
? config.maxModelSteps
|
|
258
|
+
: baseMaxModelSteps,
|
|
259
|
+
});
|
|
260
|
+
```
|
|
230
261
|
|
|
231
|
-
|
|
262
|
+
Use in thread:
|
|
232
263
|
|
|
233
|
-
|
|
264
|
+
```ts
|
|
265
|
+
createThread<{ orgId: string }>("support.agent")
|
|
266
|
+
.context((stored, env) => ({ ...stored.content, orgId: env.orgId }))
|
|
267
|
+
.narrative(() => "You are a precise assistant")
|
|
268
|
+
.actions(() => ({}))
|
|
269
|
+
.reactor(reactor)
|
|
270
|
+
.build();
|
|
271
|
+
```
|
|
234
272
|
|
|
235
|
-
|
|
236
|
-
- `silent`: disable stream writes, keep persistence.
|
|
237
|
-
- `preventClose`: do not close writer.
|
|
238
|
-
- `sendFinish`: control final `finish` chunk.
|
|
273
|
+
## `createScriptedReactor` (testing and deterministic local loops)
|
|
239
274
|
|
|
240
|
-
|
|
275
|
+
No network/model calls. Returns scripted payloads per iteration.
|
|
241
276
|
|
|
242
|
-
|
|
277
|
+
```ts
|
|
278
|
+
import { createScriptedReactor } from "@ekairos/thread";
|
|
279
|
+
|
|
280
|
+
const reactor = createScriptedReactor({
|
|
281
|
+
steps: [
|
|
282
|
+
{
|
|
283
|
+
assistantEvent: {
|
|
284
|
+
content: { parts: [{ type: "text", text: "Deterministic answer" }] },
|
|
285
|
+
},
|
|
286
|
+
toolCalls: [],
|
|
287
|
+
messagesForModel: [],
|
|
288
|
+
},
|
|
289
|
+
],
|
|
290
|
+
repeatLast: true,
|
|
291
|
+
});
|
|
292
|
+
```
|
|
243
293
|
|
|
244
|
-
|
|
245
|
-
- `context.id` is internal state id for typed context persistence.
|
|
246
|
-
- A thread can own one or more contexts; default runtime behavior is one active context per thread.
|
|
294
|
+
Rules:
|
|
247
295
|
|
|
248
|
-
|
|
296
|
+
- `steps` must contain at least 1 entry.
|
|
297
|
+
- If all steps are consumed and `repeatLast !== true`, reactor throws.
|
|
298
|
+
- `assistantEvent` is normalized with fallback fields:
|
|
299
|
+
- `id = params.eventId`
|
|
300
|
+
- `type = "output_text"`
|
|
301
|
+
- `channel = triggerEvent.channel`
|
|
302
|
+
- `createdAt = now`
|
|
249
303
|
|
|
250
|
-
|
|
251
|
-
through Workflow.
|
|
304
|
+
## Production Pattern
|
|
252
305
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
306
|
+
```ts
|
|
307
|
+
import { createThread, createAiSdkReactor } from "@ekairos/thread";
|
|
308
|
+
import { tool } from "ai";
|
|
309
|
+
import { z } from "zod";
|
|
256
310
|
|
|
257
|
-
|
|
311
|
+
type Env = { orgId: string; sessionId: string };
|
|
258
312
|
|
|
259
|
-
|
|
260
|
-
{
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
313
|
+
export const supportThread = createThread<Env>("support.agent")
|
|
314
|
+
.context((stored, env) => ({
|
|
315
|
+
orgId: env.orgId,
|
|
316
|
+
sessionId: env.sessionId,
|
|
317
|
+
...stored.content,
|
|
318
|
+
}))
|
|
319
|
+
.narrative((context) => `Assist session ${context.content?.sessionId}`)
|
|
320
|
+
.actions(() => ({
|
|
321
|
+
ping: tool({
|
|
322
|
+
description: "Health check",
|
|
323
|
+
inputSchema: z.object({ text: z.string().optional() }),
|
|
324
|
+
execute: async ({ text }) => ({ pong: text ?? "ok" }),
|
|
325
|
+
}),
|
|
326
|
+
}))
|
|
327
|
+
.reactor(createAiSdkReactor())
|
|
328
|
+
.shouldContinue(({ reactionEvent }) => {
|
|
329
|
+
const parts = reactionEvent.content?.parts ?? [];
|
|
330
|
+
const hasTool = parts.some((part: any) => part?.type === "tool-call");
|
|
331
|
+
return hasTool;
|
|
332
|
+
})
|
|
333
|
+
.build();
|
|
271
334
|
```
|
|
272
335
|
|
|
273
|
-
|
|
336
|
+
## Testing Pattern
|
|
274
337
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
Thread emits lifecycle traces by default through step operations.
|
|
278
|
-
|
|
279
|
-
Typical namespaces:
|
|
280
|
-
|
|
281
|
-
- `thread.run`
|
|
282
|
-
- `thread.context`
|
|
283
|
-
- `thread.execution`
|
|
284
|
-
- `thread.step`
|
|
285
|
-
- `thread.item`
|
|
286
|
-
- `thread.part`
|
|
287
|
-
- `thread.review`
|
|
288
|
-
- `thread.llm`
|
|
289
|
-
- `workflow.run`
|
|
290
|
-
|
|
291
|
-
These traces are intended for local persistence plus optional mirror ingestion to central collectors.
|
|
292
|
-
|
|
293
|
-
## Registry API
|
|
338
|
+
```ts
|
|
339
|
+
import { createThread, createScriptedReactor } from "@ekairos/thread";
|
|
294
340
|
|
|
295
|
-
|
|
341
|
+
type Env = { orgId: string };
|
|
296
342
|
|
|
297
|
-
|
|
298
|
-
|
|
343
|
+
const testThread = createThread<Env>("thread.test")
|
|
344
|
+
.context((stored, env) => ({ orgId: env.orgId, ...stored.content }))
|
|
345
|
+
.narrative(() => "Test narrative")
|
|
346
|
+
.actions(() => ({}))
|
|
347
|
+
.reactor(
|
|
348
|
+
createScriptedReactor({
|
|
349
|
+
steps: [
|
|
350
|
+
{
|
|
351
|
+
assistantEvent: {
|
|
352
|
+
content: { parts: [{ type: "text", text: "ok-1" }] },
|
|
353
|
+
},
|
|
354
|
+
toolCalls: [],
|
|
355
|
+
messagesForModel: [],
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
repeatLast: true,
|
|
359
|
+
}),
|
|
360
|
+
)
|
|
361
|
+
.build();
|
|
299
362
|
```
|
|
300
363
|
|
|
301
|
-
|
|
364
|
+
## Stream Contract
|
|
302
365
|
|
|
303
|
-
|
|
304
|
-
const builder = createThread<Env>("my.key").context(...).narrative(...).actions(...);
|
|
305
|
-
builder.register();
|
|
306
|
-
```
|
|
366
|
+
Thread stream events (`thread.stream.ts`) are entity-based.
|
|
307
367
|
|
|
308
|
-
|
|
368
|
+
Hierarchy:
|
|
309
369
|
|
|
310
|
-
|
|
370
|
+
1. context
|
|
371
|
+
2. thread
|
|
372
|
+
3. item
|
|
373
|
+
4. step
|
|
374
|
+
5. part
|
|
375
|
+
6. chunk
|
|
311
376
|
|
|
312
|
-
|
|
313
|
-
import { createCodexThreadBuilder } from "@ekairos/thread/codex";
|
|
377
|
+
Event types:
|
|
314
378
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
379
|
+
- `context.created`
|
|
380
|
+
- `context.resolved`
|
|
381
|
+
- `context.status.changed`
|
|
382
|
+
- `thread.created`
|
|
383
|
+
- `thread.resolved`
|
|
384
|
+
- `thread.status.changed`
|
|
385
|
+
- `execution.created`
|
|
386
|
+
- `execution.status.changed`
|
|
387
|
+
- `item.created`
|
|
388
|
+
- `item.status.changed`
|
|
389
|
+
- `step.created`
|
|
390
|
+
- `step.status.changed`
|
|
391
|
+
- `part.created`
|
|
392
|
+
- `part.updated`
|
|
393
|
+
- `chunk.emitted`
|
|
394
|
+
- `thread.finished`
|
|
331
395
|
|
|
332
|
-
|
|
396
|
+
Chunk types (`THREAD_STREAM_CHUNK_TYPES`):
|
|
333
397
|
|
|
334
|
-
- `
|
|
335
|
-
-
|
|
336
|
-
-
|
|
337
|
-
-
|
|
398
|
+
- `data-context-id`
|
|
399
|
+
- `data-context-substate`
|
|
400
|
+
- `data-thread-ping`
|
|
401
|
+
- `tool-output-available`
|
|
402
|
+
- `tool-output-error`
|
|
403
|
+
- `finish`
|
|
338
404
|
|
|
339
|
-
|
|
340
|
-
`@ekairos/openai-reactor` + `createCodexReactor(...)`.
|
|
405
|
+
Validation helpers:
|
|
341
406
|
|
|
342
|
-
|
|
407
|
+
- `parseThreadStreamEvent(event)`
|
|
408
|
+
- `assertThreadStreamTransitions(event)`
|
|
409
|
+
- `validateThreadStreamTimeline(events)`
|
|
343
410
|
|
|
344
|
-
|
|
411
|
+
## Transition Contract
|
|
345
412
|
|
|
346
|
-
|
|
347
|
-
|
|
413
|
+
Allowed status transitions are exported as constants and enforced by assertion helpers.
|
|
414
|
+
|
|
415
|
+
- Thread: `open -> streaming -> (open | closed | failed)`, `failed -> open`
|
|
416
|
+
- Context: `open <-> streaming`, `(open | streaming) -> closed`
|
|
417
|
+
- Execution: `executing -> (completed | failed)`
|
|
418
|
+
- Step: `running -> (completed | failed)`
|
|
419
|
+
- Item: `stored -> (pending | completed)`, `pending -> completed`
|
|
348
420
|
|
|
349
|
-
|
|
421
|
+
## Runtime and Schema
|
|
350
422
|
|
|
351
|
-
|
|
423
|
+
- Import schema with `threadDomain` from `@ekairos/thread/schema`
|
|
424
|
+
- Store integration defaults to `InstantStore`
|
|
425
|
+
- Runtime must be configured via `@ekairos/domain/runtime` in host app
|
|
352
426
|
|
|
353
|
-
|
|
354
|
-
- Keep thread definition declarative.
|
|
355
|
-
- Put DB/network side effects inside step functions.
|
|
356
|
-
- Prefer `context.id` for deterministic resume.
|
|
357
|
-
- Use explicit thread keys (`domain.agent.name` format).
|
|
427
|
+
Persisted entities:
|
|
358
428
|
|
|
359
|
-
|
|
429
|
+
- `thread_threads`
|
|
430
|
+
- `thread_contexts`
|
|
431
|
+
- `thread_items`
|
|
432
|
+
- `thread_executions`
|
|
433
|
+
- `thread_steps`
|
|
434
|
+
- `thread_parts`
|
|
435
|
+
- `thread_trace_events`
|
|
436
|
+
- `thread_trace_runs`
|
|
437
|
+
- `thread_trace_spans`
|
|
360
438
|
|
|
361
|
-
|
|
439
|
+
## Notes for Productive Usage
|
|
362
440
|
|
|
363
|
-
|
|
441
|
+
- Always pass explicit `env`.
|
|
442
|
+
- Prefer `context: { key }` for stable continuation and `context: { id }` for deterministic resume.
|
|
443
|
+
- Keep IO in workflow steps.
|
|
444
|
+
- Use `createScriptedReactor` for deterministic regression tests and component demos.
|
|
445
|
+
- Validate stream timelines with `validateThreadStreamTimeline` when consuming SSE externally.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ekairos/thread",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.2",
|
|
4
4
|
"description": "Pulzar Thread - Workflow-based AI Threads",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
},
|
|
111
111
|
"dependencies": {
|
|
112
112
|
"@ai-sdk/openai": "^2.0.52",
|
|
113
|
-
"@ekairos/domain": "^1.22.
|
|
113
|
+
"@ekairos/domain": "^1.22.2",
|
|
114
114
|
"@instantdb/admin": "0.22.126",
|
|
115
115
|
"@instantdb/core": "0.22.126",
|
|
116
116
|
"@vercel/mcp-adapter": "^1.0.0",
|