@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 CHANGED
@@ -1,10 +1,45 @@
1
- # @ekairos/thread
1
+ # @ekairos/thread
2
2
 
3
- Durable thread engine for Workflow-compatible AI agents.
3
+ Durable AI threads for production apps.
4
4
 
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.
5
+ `@ekairos/thread` gives you an execution model that is:
6
6
 
7
- ## Install
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
- ## 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`
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
- - `expandEvents`
157
- - `model`
158
- - `reactor`
159
- - `shouldContinue`
160
- - `opts`
59
+ ### 1) Configure app runtime once
161
60
 
162
- ### `Thread.react`
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
- thread.react(triggerEvent, {
168
- env,
169
- context: { id } | { key } | null,
170
- options,
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
- Return shape:
79
+ ### 2) Define a thread
175
80
 
176
81
  ```ts
177
- {
178
- contextId: string;
179
- context: StoredContext<Context>;
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
- A reactor receives the full execution context for one iteration and returns normalized assistant output + tool calls.
86
+ type Env = { orgId: string; sessionId: string };
87
+ type Ctx = { orgId: string; sessionId: string };
208
88
 
209
- ### `ThreadReactorParams`
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
- - `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`
107
+ ### 2.1) Reactor model (new)
228
108
 
229
- ### `ThreadReactionResult`
109
+ Thread runs through a `reactor`:
230
110
 
231
- - `assistantEvent: ThreadItem`
232
- - `toolCalls: ThreadReactionToolCall[]`
233
- - `messagesForModel: ModelMessage[]`
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
- ## Built-in Reactors
115
+ ```ts
116
+ import { createThread, createAiSdkReactor } from "@ekairos/thread";
237
117
 
238
- ## `createAiSdkReactor` (production default)
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
- Uses AI SDK streaming + tool extraction through engine steps.
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, context, iteration }) => {
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: ({ baseModel, config }) => config.model ?? baseModel,
255
- selectMaxModelSteps: ({ baseMaxModelSteps, config }) =>
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
- Use in thread:
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 reactor = createScriptedReactor({
149
+ const scripted = createScriptedReactor({
281
150
  steps: [
282
151
  {
283
152
  assistantEvent: {
284
- content: { parts: [{ type: "text", text: "Deterministic answer" }] },
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
- Rules:
164
+ Provider reactors live in `packages/reactors/*`:
295
165
 
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`
166
+ - `@ekairos/openai-reactor` (`createCodexReactor`)
167
+ - `@ekairos/claude-reactor` (scaffold)
168
+ - `@ekairos/cursor-reactor` (scaffold)
303
169
 
304
- ## Production Pattern
170
+ ### 3) Run from a workflow
305
171
 
306
172
  ```ts
307
- import { createThread, createAiSdkReactor } from "@ekairos/thread";
308
- import { tool } from "ai";
309
- import { z } from "zod";
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
- type Env = { orgId: string; sessionId: string };
194
+ ## Thread Lifecycle (Detailed)
312
195
 
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();
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
- ## Testing Pattern
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 { createThread, createScriptedReactor } from "@ekairos/thread";
247
+ import { threadDomain } from "@ekairos/thread/schema";
248
+ ```
340
249
 
341
- type Env = { orgId: string };
250
+ ## Streaming
342
251
 
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();
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
- ## Stream Contract
294
+ This extension is additive and does not break Open Responses compatibility.
365
295
 
366
- Thread stream events (`thread.stream.ts`) are entity-based.
296
+ ## Tracing and Observability
367
297
 
368
- Hierarchy:
298
+ Thread emits lifecycle traces by default through step operations.
369
299
 
370
- 1. context
371
- 2. thread
372
- 3. item
373
- 4. step
374
- 5. part
375
- 6. chunk
300
+ Typical namespaces:
376
301
 
377
- Event types:
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
- - `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`
312
+ These traces are intended for local persistence plus optional mirror ingestion to central collectors.
395
313
 
396
- Chunk types (`THREAD_STREAM_CHUNK_TYPES`):
314
+ ## Registry API
397
315
 
398
- - `data-context-id`
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
- Validation helpers:
318
+ ```ts
319
+ import { registerThread, getThread } from "@ekairos/thread";
320
+ ```
406
321
 
407
- - `parseThreadStreamEvent(event)`
408
- - `assertThreadStreamTransitions(event)`
409
- - `validateThreadStreamTimeline(events)`
322
+ Builder convenience:
410
323
 
411
- ## Transition Contract
324
+ ```ts
325
+ const builder = createThread<Env>("my.key").context(...).narrative(...).actions(...);
326
+ builder.register();
327
+ ```
412
328
 
413
- Allowed status transitions are exported as constants and enforced by assertion helpers.
329
+ ## Preconfigured Codex Thread
414
330
 
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`
331
+ Use `@ekairos/thread/codex` to create coding threads with minimal wiring.
420
332
 
421
- ## Runtime and Schema
333
+ ```ts
334
+ import { createCodexThreadBuilder } from "@ekairos/thread/codex";
422
335
 
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
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
- Persisted entities:
353
+ What it configures for you:
428
354
 
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`
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
- ## Notes for Productive Usage
382
+ Thread prioritizes runtime correctness over implicit compatibility shims.
440
383
 
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.
384
+ When behavior conflicts with durability or protocol clarity, explicit configuration is preferred over hidden fallbacks.