@ekairos/thread 1.21.88-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +363 -0
- package/dist/codex.d.ts +95 -0
- package/dist/codex.js +91 -0
- package/dist/env.d.ts +12 -0
- package/dist/env.js +62 -0
- package/dist/events.d.ts +35 -0
- package/dist/events.js +102 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/mcp.d.ts +1 -0
- package/dist/mcp.js +1 -0
- package/dist/mirror.d.ts +41 -0
- package/dist/mirror.js +1 -0
- package/dist/oidc.d.ts +7 -0
- package/dist/oidc.js +25 -0
- package/dist/polyfills/dom-events.d.ts +1 -0
- package/dist/polyfills/dom-events.js +89 -0
- package/dist/react.d.ts +62 -0
- package/dist/react.js +101 -0
- package/dist/runtime.d.ts +17 -0
- package/dist/runtime.js +23 -0
- package/dist/runtime.step.d.ts +9 -0
- package/dist/runtime.step.js +7 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +200 -0
- package/dist/steps/do-story-stream-step.d.ts +29 -0
- package/dist/steps/do-story-stream-step.js +89 -0
- package/dist/steps/do-thread-stream-step.d.ts +29 -0
- package/dist/steps/do-thread-stream-step.js +90 -0
- package/dist/steps/mirror.steps.d.ts +6 -0
- package/dist/steps/mirror.steps.js +48 -0
- package/dist/steps/reaction.steps.d.ts +43 -0
- package/dist/steps/reaction.steps.js +354 -0
- package/dist/steps/store.steps.d.ts +98 -0
- package/dist/steps/store.steps.js +512 -0
- package/dist/steps/stream.steps.d.ts +41 -0
- package/dist/steps/stream.steps.js +99 -0
- package/dist/steps/trace.steps.d.ts +37 -0
- package/dist/steps/trace.steps.js +265 -0
- package/dist/stores/instant.document-parser.d.ts +6 -0
- package/dist/stores/instant.document-parser.js +210 -0
- package/dist/stores/instant.documents.d.ts +16 -0
- package/dist/stores/instant.documents.js +152 -0
- package/dist/stores/instant.store.d.ts +78 -0
- package/dist/stores/instant.store.js +530 -0
- package/dist/story.actions.d.ts +60 -0
- package/dist/story.actions.js +120 -0
- package/dist/story.builder.d.ts +115 -0
- package/dist/story.builder.js +130 -0
- package/dist/story.config.d.ts +54 -0
- package/dist/story.config.js +125 -0
- package/dist/story.d.ts +2 -0
- package/dist/story.engine.d.ts +224 -0
- package/dist/story.engine.js +464 -0
- package/dist/story.hooks.d.ts +21 -0
- package/dist/story.hooks.js +31 -0
- package/dist/story.js +6 -0
- package/dist/story.registry.d.ts +21 -0
- package/dist/story.registry.js +30 -0
- package/dist/story.store.d.ts +107 -0
- package/dist/story.store.js +1 -0
- package/dist/story.toolcalls.d.ts +60 -0
- package/dist/story.toolcalls.js +73 -0
- package/dist/thread.builder.d.ts +118 -0
- package/dist/thread.builder.js +134 -0
- package/dist/thread.config.d.ts +15 -0
- package/dist/thread.config.js +30 -0
- package/dist/thread.d.ts +3 -0
- package/dist/thread.engine.d.ts +229 -0
- package/dist/thread.engine.js +471 -0
- package/dist/thread.events.d.ts +35 -0
- package/dist/thread.events.js +105 -0
- package/dist/thread.hooks.d.ts +21 -0
- package/dist/thread.hooks.js +31 -0
- package/dist/thread.js +7 -0
- package/dist/thread.reactor.d.ts +82 -0
- package/dist/thread.reactor.js +65 -0
- package/dist/thread.registry.d.ts +21 -0
- package/dist/thread.registry.js +30 -0
- package/dist/thread.store.d.ts +121 -0
- package/dist/thread.store.js +1 -0
- package/dist/thread.toolcalls.d.ts +60 -0
- package/dist/thread.toolcalls.js +73 -0
- package/dist/tools-to-model-tools.d.ts +19 -0
- package/dist/tools-to-model-tools.js +21 -0
- package/package.json +133 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { getStory, hasStory, listStories } from "./story.registry.js";
|
|
2
|
+
function isContextEvent(value) {
|
|
3
|
+
return Boolean(value &&
|
|
4
|
+
typeof value === "object" &&
|
|
5
|
+
typeof value.type === "string" &&
|
|
6
|
+
typeof value.id === "string");
|
|
7
|
+
}
|
|
8
|
+
function resolveStoryKey(input, ctx) {
|
|
9
|
+
if (input && typeof input === "object") {
|
|
10
|
+
const key = String(input.storyKey ?? "").trim();
|
|
11
|
+
if (key)
|
|
12
|
+
return key;
|
|
13
|
+
const storyKey = String(input.story?.key ?? "").trim();
|
|
14
|
+
if (storyKey)
|
|
15
|
+
return storyKey;
|
|
16
|
+
}
|
|
17
|
+
if (typeof ctx.storyKey === "string" && ctx.storyKey.trim()) {
|
|
18
|
+
return ctx.storyKey.trim();
|
|
19
|
+
}
|
|
20
|
+
if (ctx.domain?.meta && typeof ctx.domain.meta.storyKey === "string") {
|
|
21
|
+
const key = String(ctx.domain.meta.storyKey ?? "").trim();
|
|
22
|
+
if (key)
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
if (typeof ctx.env?.storyKey === "string") {
|
|
26
|
+
const key = String(ctx.env.storyKey ?? "").trim();
|
|
27
|
+
if (key)
|
|
28
|
+
return key;
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function resolveStoryInstance(input, ctx) {
|
|
33
|
+
if (input && typeof input === "object") {
|
|
34
|
+
const story = input.story;
|
|
35
|
+
if (story && typeof story.react === "function")
|
|
36
|
+
return story;
|
|
37
|
+
}
|
|
38
|
+
if (ctx.story && typeof ctx.story.react === "function")
|
|
39
|
+
return ctx.story;
|
|
40
|
+
if (ctx.domain?.meta && ctx.domain.meta?.story) {
|
|
41
|
+
const story = ctx.domain.meta.story;
|
|
42
|
+
if (story && typeof story.react === "function")
|
|
43
|
+
return story;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
function resolveTriggerEvent(input) {
|
|
48
|
+
if (isContextEvent(input))
|
|
49
|
+
return input;
|
|
50
|
+
if (input && typeof input === "object") {
|
|
51
|
+
const triggerEvent = input.triggerEvent ?? input.incomingEvent;
|
|
52
|
+
if (isContextEvent(triggerEvent))
|
|
53
|
+
return triggerEvent;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
function resolveContextIdentifier(input) {
|
|
58
|
+
if (input && typeof input === "object") {
|
|
59
|
+
return input.context ?? input.contextIdentifier ?? null;
|
|
60
|
+
}
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
function resolveOptions(input) {
|
|
64
|
+
if (input && typeof input === "object") {
|
|
65
|
+
return input.options;
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
export const storyActions = {
|
|
70
|
+
react: {
|
|
71
|
+
description: "Run a registered story reaction for an incoming event.",
|
|
72
|
+
execute: async (input, ctx) => {
|
|
73
|
+
"use workflow";
|
|
74
|
+
const resolvedEnv = ctx?.env ??
|
|
75
|
+
(input && typeof input === "object" ? input.env : undefined);
|
|
76
|
+
if (!resolvedEnv) {
|
|
77
|
+
throw new Error("story.react: env is required (ctx.env or input.env).");
|
|
78
|
+
}
|
|
79
|
+
const triggerEvent = resolveTriggerEvent(input);
|
|
80
|
+
if (!triggerEvent) {
|
|
81
|
+
throw new Error("story.react: triggerEvent is required.");
|
|
82
|
+
}
|
|
83
|
+
const contextIdentifier = resolveContextIdentifier(input);
|
|
84
|
+
const options = resolveOptions(input);
|
|
85
|
+
const storyInstance = resolveStoryInstance(input, ctx ?? { env: resolvedEnv });
|
|
86
|
+
if (storyInstance) {
|
|
87
|
+
return await storyInstance.react(triggerEvent, {
|
|
88
|
+
env: resolvedEnv,
|
|
89
|
+
context: contextIdentifier ?? null,
|
|
90
|
+
options,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const storyKey = resolveStoryKey(input, ctx ?? { env: resolvedEnv });
|
|
94
|
+
if (!storyKey) {
|
|
95
|
+
throw new Error("story.react: storyKey is required (env.storyKey, input.storyKey, or registered story).");
|
|
96
|
+
}
|
|
97
|
+
const story = getStory(storyKey);
|
|
98
|
+
return await story.react(triggerEvent, {
|
|
99
|
+
env: resolvedEnv,
|
|
100
|
+
context: contextIdentifier ?? null,
|
|
101
|
+
options,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
listStories: {
|
|
106
|
+
description: "List registered story keys in the current runtime.",
|
|
107
|
+
execute: async () => {
|
|
108
|
+
return { ok: true, data: { stories: listStories() } };
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
hasStory: {
|
|
112
|
+
description: "Check if a story key is registered in the current runtime.",
|
|
113
|
+
execute: async (input) => {
|
|
114
|
+
const key = String(input?.storyKey ?? "").trim();
|
|
115
|
+
if (!key)
|
|
116
|
+
return { ok: false, error: "missing_story_key" };
|
|
117
|
+
return { ok: true, data: { storyKey: key, registered: hasStory(key) } };
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { StoryEnvironment } from "./story.config.js";
|
|
2
|
+
import { Story, type StoryModelInit, type StoryOptions, type StoryTool, type ShouldContinue, type StoryShouldContinueArgs, type StoryReactParams } from "./story.engine.js";
|
|
3
|
+
import type { ContextEvent, StoredContext } from "./story.store.js";
|
|
4
|
+
import { type StoryKey } from "./story.registry.js";
|
|
5
|
+
export interface StoryConfig<Context, Env extends StoryEnvironment = StoryEnvironment> {
|
|
6
|
+
context: (context: StoredContext<Context>, env: Env) => Promise<Context> | Context;
|
|
7
|
+
/**
|
|
8
|
+
* Event expansion stage (first-class; executed by the engine on every loop iteration).
|
|
9
|
+
*
|
|
10
|
+
* Runs inside the Story loop after events are loaded from the store and before
|
|
11
|
+
* they are converted into model messages.
|
|
12
|
+
*
|
|
13
|
+
* This is the intended place to do "document expansion" (turn file references
|
|
14
|
+
* into text parts) using LlamaCloud/Reducto/etc.
|
|
15
|
+
*
|
|
16
|
+
* If you do not provide an implementation, the default is an identity transform
|
|
17
|
+
* (events pass through unchanged).
|
|
18
|
+
*
|
|
19
|
+
* If you do I/O here, implement it as a `"use-step"` function.
|
|
20
|
+
*/
|
|
21
|
+
expandEvents?: (events: ContextEvent[], context: StoredContext<Context>, env: Env) => Promise<ContextEvent[]> | ContextEvent[];
|
|
22
|
+
/**
|
|
23
|
+
* Story-first "system" message for the model for this story run.
|
|
24
|
+
*/
|
|
25
|
+
narrative: (context: StoredContext<Context>, env: Env) => Promise<string> | string;
|
|
26
|
+
/**
|
|
27
|
+
* Actions available to the model (aka "tools" in AI SDK terminology).
|
|
28
|
+
*/
|
|
29
|
+
actions: (context: StoredContext<Context>, env: Env) => Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
|
|
30
|
+
/**
|
|
31
|
+
* @deprecated Use `actions()` instead.
|
|
32
|
+
*/
|
|
33
|
+
tools?: (context: StoredContext<Context>, env: Env) => Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
|
|
34
|
+
/**
|
|
35
|
+
* Model configuration (DurableAgent-style).
|
|
36
|
+
*
|
|
37
|
+
* - `string`: AI Gateway model id, resolved in the LLM step (e.g. `"openai/gpt-5.1-thinking"`).
|
|
38
|
+
* - `() => Promise<model>`: model factory. For Workflow compatibility, provide a `"use-step"` function
|
|
39
|
+
* so it can be serialized by reference.
|
|
40
|
+
* - `(context, env) => ...`: dynamic selection based on current context/env (runs in workflow context).
|
|
41
|
+
*/
|
|
42
|
+
model?: StoryModelInit | ((context: StoredContext<Context>, env: Env) => StoryModelInit);
|
|
43
|
+
/**
|
|
44
|
+
* Called after each streamed model "reaction" and subsequent tool executions.
|
|
45
|
+
*
|
|
46
|
+
* Use this to decide whether the story loop should continue.
|
|
47
|
+
*
|
|
48
|
+
* - `true` => continue looping
|
|
49
|
+
* - `false` => finalize the story run
|
|
50
|
+
*/
|
|
51
|
+
shouldContinue?: (args: StoryShouldContinueArgs<Context, Env>) => Promise<ShouldContinue> | ShouldContinue;
|
|
52
|
+
opts?: StoryOptions<Context, Env>;
|
|
53
|
+
}
|
|
54
|
+
export type StoryInstance<Context, Env extends StoryEnvironment = StoryEnvironment> = Story<Context, Env> & {
|
|
55
|
+
readonly __config: StoryConfig<Context, Env>;
|
|
56
|
+
};
|
|
57
|
+
export declare function story<Context, Env extends StoryEnvironment = StoryEnvironment>(config: StoryConfig<Context, Env>): StoryInstance<Context, Env>;
|
|
58
|
+
type AnyContextInitializer<Env extends StoryEnvironment> = (context: StoredContext<any>, env: Env) => Promise<any> | any;
|
|
59
|
+
type InferContextFromInitializer<I extends AnyContextInitializer<any>> = Awaited<ReturnType<I>>;
|
|
60
|
+
type BuilderSystemPrompt<Context, Env extends StoryEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<string> | string;
|
|
61
|
+
type BuilderTools<Context, Env extends StoryEnvironment> = (context: StoredContext<Context>, env: Env) => Promise<Record<string, StoryTool>> | Record<string, StoryTool>;
|
|
62
|
+
type BuilderExpandEvents<Context, Env extends StoryEnvironment> = (events: ContextEvent[], context: StoredContext<Context>, env: Env) => Promise<ContextEvent[]> | ContextEvent[];
|
|
63
|
+
type BuilderShouldContinue<Context, Env extends StoryEnvironment> = (args: StoryShouldContinueArgs<Context, Env>) => Promise<ShouldContinue> | ShouldContinue;
|
|
64
|
+
type BuilderModel<Context, Env extends StoryEnvironment> = StoryModelInit | ((context: StoredContext<Context>, env: Env) => StoryModelInit);
|
|
65
|
+
export type RegistrableStoryBuilder = {
|
|
66
|
+
key: StoryKey;
|
|
67
|
+
register: () => void;
|
|
68
|
+
};
|
|
69
|
+
type FluentStoryBuilder<Context, Env extends StoryEnvironment> = {
|
|
70
|
+
key: StoryKey;
|
|
71
|
+
expandEvents(fn: BuilderExpandEvents<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
72
|
+
narrative(fn: BuilderSystemPrompt<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
73
|
+
/**
|
|
74
|
+
* "System" facade (AI SDK terminology).
|
|
75
|
+
*/
|
|
76
|
+
system(fn: BuilderSystemPrompt<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
77
|
+
actions(fn: BuilderTools<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
78
|
+
/**
|
|
79
|
+
* @deprecated Use `actions()` instead.
|
|
80
|
+
*/
|
|
81
|
+
tools(fn: BuilderTools<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
82
|
+
model(model: BuilderModel<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
83
|
+
/**
|
|
84
|
+
* Stop/continue hook (DurableAgent-like stop condition).
|
|
85
|
+
*/
|
|
86
|
+
shouldContinue(fn: BuilderShouldContinue<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
87
|
+
opts(opts: StoryOptions<Context, Env>): FluentStoryBuilder<Context, Env>;
|
|
88
|
+
/**
|
|
89
|
+
* Convenience: react to an incoming event without requiring an explicit `.build()`.
|
|
90
|
+
*
|
|
91
|
+
* This still validates the config is complete (same as `.build()`), then delegates to the
|
|
92
|
+
* underlying `Story.react(...)`.
|
|
93
|
+
*/
|
|
94
|
+
react(triggerEvent: ContextEvent, params: StoryReactParams<Env>): ReturnType<Story<Context, Env>["react"]>;
|
|
95
|
+
/**
|
|
96
|
+
* @deprecated Use `react()` instead. Kept for backwards compatibility.
|
|
97
|
+
*/
|
|
98
|
+
stream(triggerEvent: ContextEvent, params: StoryReactParams<Env>): ReturnType<Story<Context, Env>["react"]>;
|
|
99
|
+
/**
|
|
100
|
+
* Registers this story definition in the global registry under `key`.
|
|
101
|
+
*
|
|
102
|
+
* This is intended to be called at module load / boot time so durable workflows
|
|
103
|
+
* can resume and still resolve the story by key without requiring an endpoint
|
|
104
|
+
* to call `.build()` again.
|
|
105
|
+
*/
|
|
106
|
+
register(): void;
|
|
107
|
+
config(): StoryConfig<Context, Env>;
|
|
108
|
+
build(): StoryInstance<Context, Env>;
|
|
109
|
+
};
|
|
110
|
+
type CreateStoryEntry<Env extends StoryEnvironment> = {
|
|
111
|
+
context<Initializer extends AnyContextInitializer<Env>>(initializer: Initializer): FluentStoryBuilder<InferContextFromInitializer<Initializer>, Env>;
|
|
112
|
+
initialize<Initializer extends AnyContextInitializer<Env>>(initializer: Initializer): FluentStoryBuilder<InferContextFromInitializer<Initializer>, Env>;
|
|
113
|
+
};
|
|
114
|
+
export declare function createStory<Env extends StoryEnvironment = StoryEnvironment>(key: StoryKey): CreateStoryEntry<Env>;
|
|
115
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { Story, } from "./story.engine.js";
|
|
2
|
+
import { registerStory } from "./story.registry.js";
|
|
3
|
+
export function story(config) {
|
|
4
|
+
class FunctionalStory extends Story {
|
|
5
|
+
constructor() {
|
|
6
|
+
super(config.opts);
|
|
7
|
+
this.__config = config;
|
|
8
|
+
}
|
|
9
|
+
async initialize(context, env) {
|
|
10
|
+
return config.context(context, env);
|
|
11
|
+
}
|
|
12
|
+
async expandEvents(events, context, env) {
|
|
13
|
+
if (config.expandEvents)
|
|
14
|
+
return config.expandEvents(events, context, env);
|
|
15
|
+
return super.expandEvents(events, context, env);
|
|
16
|
+
}
|
|
17
|
+
async buildSystemPrompt(context, env) {
|
|
18
|
+
if (config.narrative)
|
|
19
|
+
return config.narrative(context, env);
|
|
20
|
+
throw new Error("Story config is missing narrative()");
|
|
21
|
+
}
|
|
22
|
+
async buildTools(context, env) {
|
|
23
|
+
// Back-compat: accept `tools` in old configs.
|
|
24
|
+
if (config.actions)
|
|
25
|
+
return config.actions(context, env);
|
|
26
|
+
if (config.tools)
|
|
27
|
+
return config.tools(context, env);
|
|
28
|
+
throw new Error("Story config is missing actions()");
|
|
29
|
+
}
|
|
30
|
+
getModel(context, env) {
|
|
31
|
+
if (typeof config.model === "function")
|
|
32
|
+
return config.model(context, env);
|
|
33
|
+
return config.model ?? super.getModel(context, env);
|
|
34
|
+
}
|
|
35
|
+
async shouldContinue(args) {
|
|
36
|
+
if (config.shouldContinue)
|
|
37
|
+
return config.shouldContinue(args);
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const instance = new FunctionalStory();
|
|
42
|
+
return Object.assign(instance, { __config: config });
|
|
43
|
+
}
|
|
44
|
+
function assertConfigComplete(config) {
|
|
45
|
+
if (!config.context) {
|
|
46
|
+
throw new Error("createStory: you must define context() before building the story.");
|
|
47
|
+
}
|
|
48
|
+
if (!config.narrative) {
|
|
49
|
+
throw new Error("createStory: you must define narrative() before building the story.");
|
|
50
|
+
}
|
|
51
|
+
if (!config.actions && !config.tools) {
|
|
52
|
+
throw new Error("createStory: you must define actions() before building the story.");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
export function createStory(key) {
|
|
56
|
+
const initializeBuilder = (initializer) => {
|
|
57
|
+
const typedInitializer = (ctx, env) => initializer(ctx, env);
|
|
58
|
+
const fluentState = {
|
|
59
|
+
context: typedInitializer,
|
|
60
|
+
};
|
|
61
|
+
let cached = null;
|
|
62
|
+
const builder = {
|
|
63
|
+
key,
|
|
64
|
+
expandEvents(fn) {
|
|
65
|
+
fluentState.expandEvents = fn;
|
|
66
|
+
return builder;
|
|
67
|
+
},
|
|
68
|
+
narrative(narrative) {
|
|
69
|
+
fluentState.narrative = narrative;
|
|
70
|
+
return builder;
|
|
71
|
+
},
|
|
72
|
+
system(system) {
|
|
73
|
+
// Facade alias.
|
|
74
|
+
fluentState.narrative = system;
|
|
75
|
+
return builder;
|
|
76
|
+
},
|
|
77
|
+
actions(actionsFactory) {
|
|
78
|
+
fluentState.actions = actionsFactory;
|
|
79
|
+
return builder;
|
|
80
|
+
},
|
|
81
|
+
tools(toolsFactory) {
|
|
82
|
+
// Back-compat alias.
|
|
83
|
+
fluentState.actions = toolsFactory;
|
|
84
|
+
return builder;
|
|
85
|
+
},
|
|
86
|
+
model(model) {
|
|
87
|
+
fluentState.model = model;
|
|
88
|
+
return builder;
|
|
89
|
+
},
|
|
90
|
+
shouldContinue(fn) {
|
|
91
|
+
fluentState.shouldContinue = fn;
|
|
92
|
+
return builder;
|
|
93
|
+
},
|
|
94
|
+
opts(options) {
|
|
95
|
+
fluentState.opts = options;
|
|
96
|
+
return builder;
|
|
97
|
+
},
|
|
98
|
+
react(triggerEvent, params) {
|
|
99
|
+
assertConfigComplete(fluentState);
|
|
100
|
+
if (!cached) {
|
|
101
|
+
const config = fluentState;
|
|
102
|
+
cached = story(config);
|
|
103
|
+
}
|
|
104
|
+
return cached.react(triggerEvent, params);
|
|
105
|
+
},
|
|
106
|
+
stream(triggerEvent, params) {
|
|
107
|
+
return builder.react(triggerEvent, params);
|
|
108
|
+
},
|
|
109
|
+
register() {
|
|
110
|
+
assertConfigComplete(fluentState);
|
|
111
|
+
const config = fluentState;
|
|
112
|
+
registerStory(key, () => story(config));
|
|
113
|
+
},
|
|
114
|
+
config() {
|
|
115
|
+
assertConfigComplete(fluentState);
|
|
116
|
+
return fluentState;
|
|
117
|
+
},
|
|
118
|
+
build() {
|
|
119
|
+
assertConfigComplete(fluentState);
|
|
120
|
+
const config = fluentState;
|
|
121
|
+
return story(config);
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
return builder;
|
|
125
|
+
};
|
|
126
|
+
return {
|
|
127
|
+
context: initializeBuilder,
|
|
128
|
+
initialize: initializeBuilder,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { StoryStore } from "./story.store.js";
|
|
2
|
+
import type { ConcreteDomain } from "@ekairos/domain";
|
|
3
|
+
/**
|
|
4
|
+
* ## story.config.ts
|
|
5
|
+
*
|
|
6
|
+
* Workflow-first story execution means we **cannot** keep non-serializable runtime objects
|
|
7
|
+
* (database clients, network handles, SDK instances) inside workflow state.
|
|
8
|
+
*
|
|
9
|
+
* Instead, we centralize runtime construction behind a global resolver:
|
|
10
|
+
* - The workflow passes a **serializable** `StoryEnvironment` into steps.
|
|
11
|
+
* - Steps call `resolveStoryRuntime(env)` to instantiate the right store (Instant/Postgres/etc).
|
|
12
|
+
*
|
|
13
|
+
* This keeps `createStory(...)` purely declarative while still allowing per-tenant/per-org routing.
|
|
14
|
+
*/
|
|
15
|
+
export type StoryEnvironment = Record<string, unknown>;
|
|
16
|
+
export type StoryRuntime = {
|
|
17
|
+
store: StoryStore;
|
|
18
|
+
db: any;
|
|
19
|
+
domain?: ConcreteDomain<any, any>;
|
|
20
|
+
};
|
|
21
|
+
export type StoryRuntimeResolver<Env extends StoryEnvironment = StoryEnvironment> = (env: Env) => Promise<StoryRuntime | {
|
|
22
|
+
db?: any;
|
|
23
|
+
store?: StoryStore;
|
|
24
|
+
domain?: ConcreteDomain<any, any>;
|
|
25
|
+
} | any> | StoryRuntime | {
|
|
26
|
+
db?: any;
|
|
27
|
+
store?: StoryStore;
|
|
28
|
+
domain?: ConcreteDomain<any, any>;
|
|
29
|
+
} | any;
|
|
30
|
+
/**
|
|
31
|
+
* Optional global bootstrap hook for step runtimes.
|
|
32
|
+
*
|
|
33
|
+
* Motivation:
|
|
34
|
+
* - Workflow step runtimes may execute in a separate process/bundle.
|
|
35
|
+
* - If the app's `ekairos.ts` was not evaluated in that runtime yet, the story runtime
|
|
36
|
+
* resolver won't be configured and store steps will fail.
|
|
37
|
+
*
|
|
38
|
+
* Convention:
|
|
39
|
+
* - Apps should have a single `ekairos.ts` that calls `createEkairosConfig()` from `@ekairos/domain/runtime`.
|
|
40
|
+
* - That module can register this bootstrap function,
|
|
41
|
+
* so library steps can "pull" initialization on-demand before first store access.
|
|
42
|
+
*/
|
|
43
|
+
type RuntimeBootstrap = () => void | Promise<void>;
|
|
44
|
+
export declare function configureStoryRuntimeBootstrap(bootstrap: RuntimeBootstrap): void;
|
|
45
|
+
/**
|
|
46
|
+
* Configure the story runtime resolver (global).
|
|
47
|
+
*
|
|
48
|
+
* This should be called once during initialization (module load) in your app.
|
|
49
|
+
* The resolver itself will be invoked inside steps, where it can safely create DB clients.
|
|
50
|
+
*/
|
|
51
|
+
export declare function configureStoryRuntime<Env extends StoryEnvironment>(resolver: StoryRuntimeResolver<Env>): void;
|
|
52
|
+
export declare function isStoryRuntimeConfigured(): boolean;
|
|
53
|
+
export declare function resolveStoryRuntime(env: StoryEnvironment): Promise<StoryRuntime>;
|
|
54
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
const runtimeResolverSymbol = Symbol.for("ekairos.story.runtimeResolver");
|
|
4
|
+
const runtimeBootstrapSymbol = Symbol.for("ekairos.story.runtimeBootstrap");
|
|
5
|
+
function getGlobalRuntimeResolver() {
|
|
6
|
+
if (typeof globalThis === "undefined")
|
|
7
|
+
return null;
|
|
8
|
+
return globalThis[runtimeResolverSymbol] ?? null;
|
|
9
|
+
}
|
|
10
|
+
function setGlobalRuntimeResolver(resolver) {
|
|
11
|
+
if (typeof globalThis === "undefined")
|
|
12
|
+
return;
|
|
13
|
+
globalThis[runtimeResolverSymbol] = resolver;
|
|
14
|
+
}
|
|
15
|
+
let runtimeResolver = null;
|
|
16
|
+
const runtimeByDb = new WeakMap();
|
|
17
|
+
async function normalizeRuntime(value) {
|
|
18
|
+
if (!value) {
|
|
19
|
+
throw new Error("Story runtime resolver returned no value.");
|
|
20
|
+
}
|
|
21
|
+
if (typeof value === "object" && value.store) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
const dbCandidate = typeof value === "object" && value !== null && "db" in value
|
|
25
|
+
? value.db
|
|
26
|
+
: value;
|
|
27
|
+
if (!dbCandidate) {
|
|
28
|
+
throw new Error("Story runtime resolver did not provide a database or store.");
|
|
29
|
+
}
|
|
30
|
+
if (typeof dbCandidate === "object" && dbCandidate !== null) {
|
|
31
|
+
const cached = runtimeByDb.get(dbCandidate);
|
|
32
|
+
if (cached)
|
|
33
|
+
return cached;
|
|
34
|
+
}
|
|
35
|
+
const { InstantStore } = await import("./stores/instant.store.js");
|
|
36
|
+
const runtime = {
|
|
37
|
+
store: new InstantStore(dbCandidate),
|
|
38
|
+
db: dbCandidate,
|
|
39
|
+
domain: typeof value === "object" ? value.domain : undefined,
|
|
40
|
+
};
|
|
41
|
+
if (typeof dbCandidate === "object" && dbCandidate !== null) {
|
|
42
|
+
runtimeByDb.set(dbCandidate, runtime);
|
|
43
|
+
}
|
|
44
|
+
return runtime;
|
|
45
|
+
}
|
|
46
|
+
function getRuntimeResolver() {
|
|
47
|
+
return runtimeResolver ?? getGlobalRuntimeResolver();
|
|
48
|
+
}
|
|
49
|
+
let runtimeBootstrap = null;
|
|
50
|
+
function getGlobalRuntimeBootstrap() {
|
|
51
|
+
if (typeof globalThis === "undefined")
|
|
52
|
+
return null;
|
|
53
|
+
return globalThis[runtimeBootstrapSymbol] ?? null;
|
|
54
|
+
}
|
|
55
|
+
function setGlobalRuntimeBootstrap(bootstrap) {
|
|
56
|
+
if (typeof globalThis === "undefined")
|
|
57
|
+
return;
|
|
58
|
+
globalThis[runtimeBootstrapSymbol] = bootstrap;
|
|
59
|
+
}
|
|
60
|
+
function getRuntimeBootstrap() {
|
|
61
|
+
return runtimeBootstrap ?? getGlobalRuntimeBootstrap();
|
|
62
|
+
}
|
|
63
|
+
export function configureStoryRuntimeBootstrap(bootstrap) {
|
|
64
|
+
runtimeBootstrap = bootstrap;
|
|
65
|
+
setGlobalRuntimeBootstrap(bootstrap);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Configure the story runtime resolver (global).
|
|
69
|
+
*
|
|
70
|
+
* This should be called once during initialization (module load) in your app.
|
|
71
|
+
* The resolver itself will be invoked inside steps, where it can safely create DB clients.
|
|
72
|
+
*/
|
|
73
|
+
export function configureStoryRuntime(resolver) {
|
|
74
|
+
runtimeResolver = resolver;
|
|
75
|
+
setGlobalRuntimeResolver(runtimeResolver);
|
|
76
|
+
}
|
|
77
|
+
export function isStoryRuntimeConfigured() {
|
|
78
|
+
return Boolean(getRuntimeResolver());
|
|
79
|
+
}
|
|
80
|
+
export async function resolveStoryRuntime(env) {
|
|
81
|
+
if (!getRuntimeResolver()) {
|
|
82
|
+
// Best-effort: allow the step runtime to self-bootstrap once.
|
|
83
|
+
const bootstrap = getRuntimeBootstrap();
|
|
84
|
+
if (bootstrap) {
|
|
85
|
+
await bootstrap();
|
|
86
|
+
}
|
|
87
|
+
// Convention bootstrap (portable, runtime-resolvable):
|
|
88
|
+
// If the host app provides an `ekairos.bootstrap.js` at the project root, we can load it
|
|
89
|
+
// from the step runtime using a file URL. This avoids relying on bundler-only aliases.
|
|
90
|
+
if (!getRuntimeResolver()) {
|
|
91
|
+
const cwd = typeof process !== "undefined" && process.cwd ? process.cwd() : null;
|
|
92
|
+
if (cwd) {
|
|
93
|
+
const dynamicImport = new Function("specifier", "return import(specifier)");
|
|
94
|
+
const candidates = [
|
|
95
|
+
"ekairos.bootstrap.js",
|
|
96
|
+
"ekairos.bootstrap.cjs",
|
|
97
|
+
"ekairos.bootstrap.mjs",
|
|
98
|
+
];
|
|
99
|
+
for (const filename of candidates) {
|
|
100
|
+
try {
|
|
101
|
+
await dynamicImport(pathToFileURL(join(cwd, filename)).href);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// ignore
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// If bootstrap succeeded, proceed.
|
|
111
|
+
const resolver = getRuntimeResolver();
|
|
112
|
+
if (resolver)
|
|
113
|
+
return await normalizeRuntime(await resolver(env));
|
|
114
|
+
throw new Error([
|
|
115
|
+
"Story runtime is not configured.",
|
|
116
|
+
"",
|
|
117
|
+
"Convention:",
|
|
118
|
+
"- Create an app-level `ekairos.ts` that exports `ekairosConfig = createEkairosConfig({ runtime })` from @ekairos/domain/runtime",
|
|
119
|
+
"- Ensure the bootstrap module is evaluated in the step runtime (module load / worker boot).",
|
|
120
|
+
"",
|
|
121
|
+
"If you already have that file, ensure it is evaluated in the step runtime before calling story store steps.",
|
|
122
|
+
].join("\n"));
|
|
123
|
+
}
|
|
124
|
+
return await normalizeRuntime(await getRuntimeResolver()(env));
|
|
125
|
+
}
|
package/dist/story.d.ts
ADDED