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