@ekairos/thread 1.22.3 → 1.22.4-beta.feature-thread-unify.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 +292 -353
- package/dist/index.d.ts +5 -1
- package/dist/index.js +3 -1
- package/dist/react.d.ts +4 -3
- package/dist/reactors/ai-sdk.reactor.d.ts +38 -0
- package/dist/reactors/ai-sdk.reactor.js +65 -0
- package/dist/reactors/scripted.reactor.d.ts +23 -0
- package/dist/reactors/scripted.reactor.js +56 -0
- package/dist/reactors/types.d.ts +48 -0
- package/dist/reactors/types.js +1 -0
- package/dist/steps/do-thread-stream-step.d.ts +2 -1
- package/dist/steps/do-thread-stream-step.js +4 -2
- package/dist/steps/reaction.steps.d.ts +3 -2
- package/dist/steps/reaction.steps.js +7 -66
- package/dist/steps/store.steps.js +1 -2
- package/dist/steps/trace.steps.d.ts +2 -1
- package/dist/steps/trace.steps.js +9 -4
- package/dist/stores/instant.documents.d.ts +2 -2
- package/dist/stores/instant.documents.js +1 -1
- package/dist/stores/instant.store.js +57 -4
- package/dist/thread.builder.js +4 -1
- package/dist/thread.contract.d.ts +45 -0
- package/dist/thread.contract.js +98 -0
- package/dist/thread.d.ts +1 -1
- package/dist/thread.engine.d.ts +1 -1
- package/dist/thread.engine.js +44 -17
- package/dist/thread.events.js +17 -2
- package/dist/thread.js +1 -1
- package/dist/thread.reactor.d.ts +3 -82
- package/dist/thread.reactor.js +2 -65
- package/dist/thread.store.d.ts +12 -8
- package/dist/thread.stream.d.ts +119 -0
- package/dist/thread.stream.js +152 -0
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -1,10 +1,45 @@
|
|
|
1
|
-
|
|
1
|
+
# @ekairos/thread
|
|
2
2
|
|
|
3
|
-
Durable
|
|
3
|
+
Durable AI threads for production apps.
|
|
4
4
|
|
|
5
|
-
`@ekairos/thread`
|
|
5
|
+
`@ekairos/thread` gives you an execution model that is:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- workflow-compatible,
|
|
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
|
|
8
43
|
|
|
9
44
|
```bash
|
|
10
45
|
pnpm add @ekairos/thread
|
|
@@ -19,427 +54,331 @@ Optional subpaths:
|
|
|
19
54
|
- `@ekairos/thread/mcp`
|
|
20
55
|
- `@ekairos/thread/oidc`
|
|
21
56
|
|
|
22
|
-
##
|
|
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`
|
|
122
|
-
|
|
123
|
-
```ts
|
|
124
|
-
createThread<Env>(key: ThreadKey)
|
|
125
|
-
```
|
|
126
|
-
|
|
127
|
-
Builder stages:
|
|
128
|
-
|
|
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)
|
|
137
|
-
|
|
138
|
-
Builder terminals:
|
|
139
|
-
|
|
140
|
-
- `.build()` -> `ThreadInstance`
|
|
141
|
-
- `.react(triggerEvent, params)`
|
|
142
|
-
- `.stream(triggerEvent, params)` (deprecated alias)
|
|
143
|
-
- `.register()`
|
|
144
|
-
- `.config()`
|
|
145
|
-
|
|
146
|
-
### `ThreadConfig<Context, Env>`
|
|
147
|
-
|
|
148
|
-
Required keys:
|
|
149
|
-
|
|
150
|
-
- `context`
|
|
151
|
-
- `narrative`
|
|
152
|
-
- `actions` (or legacy `tools`)
|
|
153
|
-
|
|
154
|
-
Optional keys:
|
|
57
|
+
## Quick Start
|
|
155
58
|
|
|
156
|
-
|
|
157
|
-
- `model`
|
|
158
|
-
- `reactor`
|
|
159
|
-
- `shouldContinue`
|
|
160
|
-
- `opts`
|
|
59
|
+
### 1) Configure app runtime once
|
|
161
60
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
Primary form:
|
|
61
|
+
Thread resolves persistence through runtime.
|
|
62
|
+
Do this once in app bootstrap (`src/ekairos.ts`):
|
|
165
63
|
|
|
166
64
|
```ts
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
65
|
+
import "server-only";
|
|
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
|
+
});
|
|
172
77
|
```
|
|
173
78
|
|
|
174
|
-
|
|
79
|
+
### 2) Define a thread
|
|
175
80
|
|
|
176
81
|
```ts
|
|
177
|
-
{
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
triggerEventId: string;
|
|
181
|
-
reactionEventId: string;
|
|
182
|
-
executionId: string;
|
|
183
|
-
}
|
|
184
|
-
```
|
|
185
|
-
|
|
186
|
-
### `ThreadStreamOptions`
|
|
187
|
-
|
|
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>`
|
|
194
|
-
|
|
195
|
-
### `ThreadOptions`
|
|
196
|
-
|
|
197
|
-
Lifecycle callbacks:
|
|
198
|
-
|
|
199
|
-
- `onContextCreated`
|
|
200
|
-
- `onContextUpdated`
|
|
201
|
-
- `onEventCreated`
|
|
202
|
-
- `onToolCallExecuted`
|
|
203
|
-
- `onEnd`
|
|
204
|
-
|
|
205
|
-
## Reactor Specification
|
|
82
|
+
import { createThread } from "@ekairos/thread";
|
|
83
|
+
import { tool } from "ai";
|
|
84
|
+
import { z } from "zod";
|
|
206
85
|
|
|
207
|
-
|
|
86
|
+
type Env = { orgId: string; sessionId: string };
|
|
87
|
+
type Ctx = { orgId: string; sessionId: string };
|
|
208
88
|
|
|
209
|
-
|
|
89
|
+
export const helloThread = createThread<Env>("hello.thread")
|
|
90
|
+
.context(async (stored, env) => ({
|
|
91
|
+
orgId: env.orgId,
|
|
92
|
+
sessionId: env.sessionId,
|
|
93
|
+
...(stored.content ?? {}),
|
|
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
|
+
```
|
|
210
106
|
|
|
211
|
-
|
|
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`
|
|
107
|
+
### 2.1) Reactor model (new)
|
|
228
108
|
|
|
229
|
-
|
|
109
|
+
Thread runs through a `reactor`:
|
|
230
110
|
|
|
231
|
-
-
|
|
232
|
-
-
|
|
233
|
-
-
|
|
234
|
-
- `llm?: ThreadReactionLLM`
|
|
111
|
+
- default: `createAiSdkReactor()` (included in `@ekairos/thread`)
|
|
112
|
+
- deterministic/local testing: `createScriptedReactor({ steps })`
|
|
113
|
+
- optional: custom/provider reactor via `.reactor(...)`
|
|
235
114
|
|
|
236
|
-
|
|
115
|
+
```ts
|
|
116
|
+
import { createThread, createAiSdkReactor } from "@ekairos/thread";
|
|
237
117
|
|
|
238
|
-
|
|
118
|
+
const thread = createThread<{ orgId: string }>("my.thread")
|
|
119
|
+
.context((stored, env) => ({ ...(stored.content ?? {}), orgId: env.orgId }))
|
|
120
|
+
.narrative(() => "System prompt")
|
|
121
|
+
.actions(() => ({}))
|
|
122
|
+
.reactor(createAiSdkReactor())
|
|
123
|
+
.build();
|
|
124
|
+
```
|
|
239
125
|
|
|
240
|
-
|
|
126
|
+
`createAiSdkReactor` also accepts optional per-turn config hooks:
|
|
241
127
|
|
|
242
128
|
```ts
|
|
243
129
|
import { createAiSdkReactor } from "@ekairos/thread";
|
|
244
130
|
|
|
245
131
|
const reactor = createAiSdkReactor({
|
|
246
|
-
resolveConfig: async ({ env
|
|
132
|
+
resolveConfig: async ({ env }) => {
|
|
247
133
|
"use step";
|
|
248
|
-
return {
|
|
249
|
-
model: env.model ?? "openai/gpt-5.2",
|
|
250
|
-
maxModelSteps: iteration === 0 ? 2 : 1,
|
|
251
|
-
tenant: context.content?.orgId,
|
|
252
|
-
};
|
|
134
|
+
return { model: env.model ?? "openai/gpt-5.2", maxModelSteps: 2 };
|
|
253
135
|
},
|
|
254
|
-
selectModel: ({
|
|
255
|
-
selectMaxModelSteps: ({
|
|
136
|
+
selectModel: ({ config, baseModel }) => config.model ?? baseModel,
|
|
137
|
+
selectMaxModelSteps: ({ config, baseMaxModelSteps }) =>
|
|
256
138
|
typeof config.maxModelSteps === "number"
|
|
257
139
|
? config.maxModelSteps
|
|
258
140
|
: baseMaxModelSteps,
|
|
259
141
|
});
|
|
260
142
|
```
|
|
261
143
|
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
```
|
|
272
|
-
|
|
273
|
-
## `createScriptedReactor` (testing and deterministic local loops)
|
|
274
|
-
|
|
275
|
-
No network/model calls. Returns scripted payloads per iteration.
|
|
144
|
+
For deterministic tests and local iteration loops without LLM/network calls:
|
|
276
145
|
|
|
277
146
|
```ts
|
|
278
147
|
import { createScriptedReactor } from "@ekairos/thread";
|
|
279
148
|
|
|
280
|
-
const
|
|
149
|
+
const scripted = createScriptedReactor({
|
|
281
150
|
steps: [
|
|
282
151
|
{
|
|
283
152
|
assistantEvent: {
|
|
284
|
-
content: {
|
|
153
|
+
content: {
|
|
154
|
+
parts: [{ type: "text", text: "deterministic response" }],
|
|
155
|
+
},
|
|
285
156
|
},
|
|
286
157
|
toolCalls: [],
|
|
287
158
|
messagesForModel: [],
|
|
288
159
|
},
|
|
289
160
|
],
|
|
290
|
-
repeatLast: true,
|
|
291
161
|
});
|
|
292
162
|
```
|
|
293
163
|
|
|
294
|
-
|
|
164
|
+
Provider reactors live in `packages/reactors/*`:
|
|
295
165
|
|
|
296
|
-
- `
|
|
297
|
-
-
|
|
298
|
-
- `
|
|
299
|
-
- `id = params.eventId`
|
|
300
|
-
- `type = "output_text"`
|
|
301
|
-
- `channel = triggerEvent.channel`
|
|
302
|
-
- `createdAt = now`
|
|
166
|
+
- `@ekairos/openai-reactor` (`createCodexReactor`)
|
|
167
|
+
- `@ekairos/claude-reactor` (scaffold)
|
|
168
|
+
- `@ekairos/cursor-reactor` (scaffold)
|
|
303
169
|
|
|
304
|
-
|
|
170
|
+
### 3) Run from a workflow
|
|
305
171
|
|
|
306
172
|
```ts
|
|
307
|
-
import {
|
|
308
|
-
import {
|
|
309
|
-
import {
|
|
173
|
+
import { getWritable } from "workflow";
|
|
174
|
+
import type { UIMessageChunk } from "ai";
|
|
175
|
+
import type { ThreadItem } from "@ekairos/thread";
|
|
176
|
+
import { helloThread } from "./hello.thread";
|
|
177
|
+
|
|
178
|
+
export async function helloWorkflow(params: {
|
|
179
|
+
env: { orgId: string; sessionId: string };
|
|
180
|
+
triggerEvent: ThreadItem;
|
|
181
|
+
threadKey?: string;
|
|
182
|
+
}) {
|
|
183
|
+
"use workflow";
|
|
184
|
+
|
|
185
|
+
const writable = getWritable<UIMessageChunk>();
|
|
186
|
+
return await helloThread.react(params.triggerEvent, {
|
|
187
|
+
env: params.env,
|
|
188
|
+
context: params.threadKey ? { key: params.threadKey } : null,
|
|
189
|
+
options: { writable, maxIterations: 2, maxModelSteps: 1 },
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
```
|
|
310
193
|
|
|
311
|
-
|
|
194
|
+
## Thread Lifecycle (Detailed)
|
|
312
195
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
return hasTool;
|
|
332
|
-
})
|
|
333
|
-
.build();
|
|
334
|
-
```
|
|
196
|
+
For each `react(...)` call:
|
|
197
|
+
|
|
198
|
+
1. `initializeContext` creates or loads context.
|
|
199
|
+
2. `saveTriggerAndCreateExecution` persists trigger and execution.
|
|
200
|
+
3. `createThreadStep` starts iteration record.
|
|
201
|
+
4. `buildSystemPrompt` and `buildTools` are evaluated.
|
|
202
|
+
5. `executeReaction` runs model + tool call planning.
|
|
203
|
+
6. `saveThreadPartsStep` persists normalized parts.
|
|
204
|
+
7. `saveReactionItem` or `updateItem` updates stable reaction item.
|
|
205
|
+
8. Tool executions run and are merged into persisted parts.
|
|
206
|
+
9. `shouldContinue(...)` decides next iteration or completion.
|
|
207
|
+
10. `completeExecution` closes run status.
|
|
208
|
+
|
|
209
|
+
All side effects are executed through workflow-safe steps.
|
|
210
|
+
|
|
211
|
+
## Event and Item Model
|
|
212
|
+
|
|
213
|
+
Key utilities:
|
|
335
214
|
|
|
336
|
-
|
|
215
|
+
- `createUserItemFromUIMessages(...)`
|
|
216
|
+
- `createAssistantItemFromUIMessages(...)`
|
|
217
|
+
- `convertItemsToModelMessages(...)`
|
|
218
|
+
- `convertModelMessageToItem(...)`
|
|
219
|
+
- `didToolExecute(...)`
|
|
220
|
+
- `extractToolCallsFromParts(...)`
|
|
221
|
+
|
|
222
|
+
This keeps a stable internal representation while remaining compatible with UI/model formats.
|
|
223
|
+
|
|
224
|
+
## Runtime and Persistence
|
|
225
|
+
|
|
226
|
+
Thread runtime resolves from `@ekairos/domain/runtime` bootstrap.
|
|
227
|
+
|
|
228
|
+
Default persistence adapter:
|
|
229
|
+
|
|
230
|
+
- `InstantStore` (`@ekairos/thread/instant`)
|
|
231
|
+
|
|
232
|
+
Schema:
|
|
233
|
+
|
|
234
|
+
- `thread_threads`
|
|
235
|
+
- `thread_contexts`
|
|
236
|
+
- `thread_items`
|
|
237
|
+
- `thread_executions`
|
|
238
|
+
- `thread_steps`
|
|
239
|
+
- `thread_parts`
|
|
240
|
+
- `thread_trace_events`
|
|
241
|
+
- `thread_trace_runs`
|
|
242
|
+
- `thread_trace_spans`
|
|
243
|
+
|
|
244
|
+
Import domain schema:
|
|
337
245
|
|
|
338
246
|
```ts
|
|
339
|
-
import {
|
|
247
|
+
import { threadDomain } from "@ekairos/thread/schema";
|
|
248
|
+
```
|
|
340
249
|
|
|
341
|
-
|
|
250
|
+
## Streaming
|
|
342
251
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
252
|
+
Thread writes `UIMessageChunk` to workflow writable streams.
|
|
253
|
+
|
|
254
|
+
Options:
|
|
255
|
+
|
|
256
|
+
- `writable`: custom stream.
|
|
257
|
+
- `silent`: disable stream writes, keep persistence.
|
|
258
|
+
- `preventClose`: do not close writer.
|
|
259
|
+
- `sendFinish`: control final `finish` chunk.
|
|
260
|
+
|
|
261
|
+
Namespaced streams are supported using `context:<contextId>`.
|
|
262
|
+
|
|
263
|
+
## Identity Model
|
|
264
|
+
|
|
265
|
+
- `thread.key` is the functional continuity id.
|
|
266
|
+
- `context.id` is internal state id for typed context persistence.
|
|
267
|
+
- A thread can own one or more contexts; default runtime behavior is one active context per thread.
|
|
268
|
+
|
|
269
|
+
### Open Responses alignment
|
|
270
|
+
|
|
271
|
+
Thread is protocol-aligned with Open Responses item/event semantics and keeps durable execution
|
|
272
|
+
through Workflow.
|
|
273
|
+
|
|
274
|
+
- Public continuity id should be `thread.key`.
|
|
275
|
+
- Context remains internal typed state, but can be exposed as an extension field in thread query APIs.
|
|
276
|
+
- Safe extension pattern: include `context` object in thread payload while preserving standard fields.
|
|
277
|
+
|
|
278
|
+
Example shape for a thread query response:
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"object": "conversation",
|
|
283
|
+
"id": "thread-key-or-id",
|
|
284
|
+
"status": "completed",
|
|
285
|
+
"context": {
|
|
286
|
+
"id": "ctx_123",
|
|
287
|
+
"key": "code.agent.session.abc",
|
|
288
|
+
"status": "completed",
|
|
289
|
+
"content": {}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
362
292
|
```
|
|
363
293
|
|
|
364
|
-
|
|
294
|
+
This extension is additive and does not break Open Responses compatibility.
|
|
365
295
|
|
|
366
|
-
|
|
296
|
+
## Tracing and Observability
|
|
367
297
|
|
|
368
|
-
|
|
298
|
+
Thread emits lifecycle traces by default through step operations.
|
|
369
299
|
|
|
370
|
-
|
|
371
|
-
2. thread
|
|
372
|
-
3. item
|
|
373
|
-
4. step
|
|
374
|
-
5. part
|
|
375
|
-
6. chunk
|
|
300
|
+
Typical namespaces:
|
|
376
301
|
|
|
377
|
-
|
|
302
|
+
- `thread.run`
|
|
303
|
+
- `thread.context`
|
|
304
|
+
- `thread.execution`
|
|
305
|
+
- `thread.step`
|
|
306
|
+
- `thread.item`
|
|
307
|
+
- `thread.part`
|
|
308
|
+
- `thread.review`
|
|
309
|
+
- `thread.llm`
|
|
310
|
+
- `workflow.run`
|
|
378
311
|
|
|
379
|
-
|
|
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`
|
|
312
|
+
These traces are intended for local persistence plus optional mirror ingestion to central collectors.
|
|
395
313
|
|
|
396
|
-
|
|
314
|
+
## Registry API
|
|
397
315
|
|
|
398
|
-
|
|
399
|
-
- `data-context-substate`
|
|
400
|
-
- `data-thread-ping`
|
|
401
|
-
- `tool-output-available`
|
|
402
|
-
- `tool-output-error`
|
|
403
|
-
- `finish`
|
|
316
|
+
Register and resolve threads by key:
|
|
404
317
|
|
|
405
|
-
|
|
318
|
+
```ts
|
|
319
|
+
import { registerThread, getThread } from "@ekairos/thread";
|
|
320
|
+
```
|
|
406
321
|
|
|
407
|
-
|
|
408
|
-
- `assertThreadStreamTransitions(event)`
|
|
409
|
-
- `validateThreadStreamTimeline(events)`
|
|
322
|
+
Builder convenience:
|
|
410
323
|
|
|
411
|
-
|
|
324
|
+
```ts
|
|
325
|
+
const builder = createThread<Env>("my.key").context(...).narrative(...).actions(...);
|
|
326
|
+
builder.register();
|
|
327
|
+
```
|
|
412
328
|
|
|
413
|
-
|
|
329
|
+
## Preconfigured Codex Thread
|
|
414
330
|
|
|
415
|
-
|
|
416
|
-
- Context: `open <-> streaming`, `(open | streaming) -> closed`
|
|
417
|
-
- Execution: `executing -> (completed | failed)`
|
|
418
|
-
- Step: `running -> (completed | failed)`
|
|
419
|
-
- Item: `stored -> (pending | completed)`, `pending -> completed`
|
|
331
|
+
Use `@ekairos/thread/codex` to create coding threads with minimal wiring.
|
|
420
332
|
|
|
421
|
-
|
|
333
|
+
```ts
|
|
334
|
+
import { createCodexThreadBuilder } from "@ekairos/thread/codex";
|
|
422
335
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
336
|
+
const builder = createCodexThreadBuilder({
|
|
337
|
+
key: "code.agent",
|
|
338
|
+
context: async (stored, env) => ({ ...(stored.content ?? {}), ...env }),
|
|
339
|
+
executeCodex: async ({ input, env }) => {
|
|
340
|
+
// Call Codex app server here (usually in a use-step function)
|
|
341
|
+
return {
|
|
342
|
+
threadId: "t_123",
|
|
343
|
+
turnId: "turn_123",
|
|
344
|
+
assistantText: "done",
|
|
345
|
+
reasoningText: "",
|
|
346
|
+
diff: "",
|
|
347
|
+
toolParts: [],
|
|
348
|
+
};
|
|
349
|
+
},
|
|
350
|
+
});
|
|
351
|
+
```
|
|
426
352
|
|
|
427
|
-
|
|
353
|
+
What it configures for you:
|
|
428
354
|
|
|
429
|
-
- `
|
|
430
|
-
- `
|
|
431
|
-
- `
|
|
432
|
-
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
- `
|
|
436
|
-
|
|
437
|
-
|
|
355
|
+
- `codex` action schema,
|
|
356
|
+
- default model selection (`openai/gpt-5.2`),
|
|
357
|
+
- default continue rule (`stop after codex action executes`),
|
|
358
|
+
- default narrative fallback.
|
|
359
|
+
|
|
360
|
+
For direct Codex runtime (without "tool indirection"), use
|
|
361
|
+
`@ekairos/openai-reactor` + `createCodexReactor(...)`.
|
|
362
|
+
|
|
363
|
+
## MCP and OIDC
|
|
364
|
+
|
|
365
|
+
Utilities are exposed for protocol integration:
|
|
366
|
+
|
|
367
|
+
- `@ekairos/thread/mcp`
|
|
368
|
+
- `@ekairos/thread/oidc`
|
|
369
|
+
|
|
370
|
+
Use these from server-side API routes when exposing thread-driven tools via MCP.
|
|
371
|
+
|
|
372
|
+
## DX Guidelines
|
|
373
|
+
|
|
374
|
+
- Keep `env` serializable.
|
|
375
|
+
- Keep thread definition declarative.
|
|
376
|
+
- Put DB/network side effects inside step functions.
|
|
377
|
+
- Prefer `context.id` for deterministic resume.
|
|
378
|
+
- Use explicit thread keys (`domain.agent.name` format).
|
|
379
|
+
|
|
380
|
+
## Breaking-Change Policy
|
|
438
381
|
|
|
439
|
-
|
|
382
|
+
Thread prioritizes runtime correctness over implicit compatibility shims.
|
|
440
383
|
|
|
441
|
-
|
|
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.
|
|
384
|
+
When behavior conflicts with durability or protocol clarity, explicit configuration is preferred over hidden fallbacks.
|