@kalphq/core 0.1.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.
@@ -0,0 +1,173 @@
1
+ /**
2
+ * Deterministic math primitive.
3
+ *
4
+ * Provides deterministic random number generation and standard math operations.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import type { ExecutionLog } from "@/engine/execution-log";
10
+ import type { KalpMath } from "@kalphq/sdk";
11
+ import type { ExecutionContext } from "@/engine/types";
12
+
13
+ /**
14
+ * Creates a math primitive that emits events to the execution log.
15
+ *
16
+ * @param log - The execution log for event emission.
17
+ * @param execCtx - Optional execution context for event identity.
18
+ * @returns A {@link KalpMath} instance.
19
+ */
20
+ export function createMathPrimitive(
21
+ log: ExecutionLog,
22
+ execCtx?: ExecutionContext,
23
+ ): KalpMath {
24
+ const ids = {
25
+ executionId: execCtx?.executionId ?? "",
26
+ traceId: execCtx?.traceId ?? "",
27
+ threadId: execCtx?.threadId ?? "",
28
+ };
29
+
30
+ // Simple seeded random for deterministic behavior
31
+ let seed = execCtx?.executionId.split("").reduce((a, c) => a + c.charCodeAt(0), 0) ?? Date.now();
32
+
33
+ return {
34
+ /**
35
+ * Deterministic pseudo-random number generator.
36
+ * Seeded by the execution runId (ULID) for replay consistency.
37
+ */
38
+ random(): number {
39
+ const timestamp = Date.now();
40
+ seed = (seed * 9301 + 49297) % 233280;
41
+ const result = seed / 233280;
42
+ void log.emit({
43
+ type: "primitive.invoked",
44
+ name: "math.random",
45
+ params: undefined,
46
+ result,
47
+ ...ids,
48
+ timestamp,
49
+ });
50
+ return result;
51
+ },
52
+
53
+ /** Round down to nearest integer. */
54
+ floor(x: number): number {
55
+ const result = Math.floor(x);
56
+ const timestamp = Date.now();
57
+ void log.emit({
58
+ type: "primitive.invoked",
59
+ name: "math.floor",
60
+ params: x,
61
+ result,
62
+ ...ids,
63
+ timestamp,
64
+ });
65
+ return result;
66
+ },
67
+
68
+ /** Round up to nearest integer. */
69
+ ceil(x: number): number {
70
+ const result = Math.ceil(x);
71
+ const timestamp = Date.now();
72
+ void log.emit({
73
+ type: "primitive.invoked",
74
+ name: "math.ceil",
75
+ params: x,
76
+ result,
77
+ ...ids,
78
+ timestamp,
79
+ });
80
+ return result;
81
+ },
82
+
83
+ /** Round to nearest integer. */
84
+ round(x: number): number {
85
+ const result = Math.round(x);
86
+ const timestamp = Date.now();
87
+ void log.emit({
88
+ type: "primitive.invoked",
89
+ name: "math.round",
90
+ params: x,
91
+ result,
92
+ ...ids,
93
+ timestamp,
94
+ });
95
+ return result;
96
+ },
97
+
98
+ /** Return smallest of provided values. */
99
+ min(...values: number[]): number {
100
+ const result = Math.min(...values);
101
+ const timestamp = Date.now();
102
+ void log.emit({
103
+ type: "primitive.invoked",
104
+ name: "math.min",
105
+ params: values,
106
+ result,
107
+ ...ids,
108
+ timestamp,
109
+ });
110
+ return result;
111
+ },
112
+
113
+ /** Return largest of provided values. */
114
+ max(...values: number[]): number {
115
+ const result = Math.max(...values);
116
+ const timestamp = Date.now();
117
+ void log.emit({
118
+ type: "primitive.invoked",
119
+ name: "math.max",
120
+ params: values,
121
+ result,
122
+ ...ids,
123
+ timestamp,
124
+ });
125
+ return result;
126
+ },
127
+
128
+ /** Return absolute value. */
129
+ abs(x: number): number {
130
+ const result = Math.abs(x);
131
+ const timestamp = Date.now();
132
+ void log.emit({
133
+ type: "primitive.invoked",
134
+ name: "math.abs",
135
+ params: x,
136
+ result,
137
+ ...ids,
138
+ timestamp,
139
+ });
140
+ return result;
141
+ },
142
+
143
+ /** Return base to the power of exponent. */
144
+ pow(base: number, exponent: number): number {
145
+ const result = Math.pow(base, exponent);
146
+ const timestamp = Date.now();
147
+ void log.emit({
148
+ type: "primitive.invoked",
149
+ name: "math.pow",
150
+ params: { base, exponent },
151
+ result,
152
+ ...ids,
153
+ timestamp,
154
+ });
155
+ return result;
156
+ },
157
+
158
+ /** Return square root. */
159
+ sqrt(x: number): number {
160
+ const result = Math.sqrt(x);
161
+ const timestamp = Date.now();
162
+ void log.emit({
163
+ type: "primitive.invoked",
164
+ name: "math.sqrt",
165
+ params: x,
166
+ result,
167
+ ...ids,
168
+ timestamp,
169
+ });
170
+ return result;
171
+ },
172
+ };
173
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Model Context Protocol (MCP) primitive.
3
+ *
4
+ * Provides access to MCP servers configured in the project.
5
+ *
6
+ * @module
7
+ */
8
+
9
+ import type { ExecutionLog } from "@/engine/execution-log";
10
+ import type { KalpMcp } from "@kalphq/sdk";
11
+ import type { ExecutionContext } from "@/engine/types";
12
+
13
+ /**
14
+ * Creates an MCP primitive that emits events to the execution log.
15
+ *
16
+ * @param log - The execution log for event emission.
17
+ * @param execCtx - Optional execution context for event identity.
18
+ * @returns A {@link KalpMcp} instance.
19
+ */
20
+ export function createMcpPrimitive(
21
+ log: ExecutionLog,
22
+ execCtx?: ExecutionContext,
23
+ ): KalpMcp {
24
+ const ids = {
25
+ executionId: execCtx?.executionId ?? "",
26
+ traceId: execCtx?.traceId ?? "",
27
+ threadId: execCtx?.threadId ?? "",
28
+ };
29
+
30
+ // Return a Proxy that dynamically handles MCP server calls
31
+ return new Proxy({} as KalpMcp, {
32
+ get(_target, serverName: string) {
33
+ // Return a Proxy for the server's tools
34
+ return new Proxy(
35
+ {},
36
+ {
37
+ get(_target, toolName: string) {
38
+ // Return the tool function
39
+ return async (input: unknown): Promise<unknown> => {
40
+ const timestamp = Date.now();
41
+ void log.emit({
42
+ type: "primitive.invoked",
43
+ name: "mcp." + serverName + "." + toolName,
44
+ params: input,
45
+ result: undefined,
46
+ ...ids,
47
+ timestamp,
48
+ });
49
+
50
+ // TODO: Implement actual MCP server connection
51
+ // For now, return a stub response
52
+ return { error: "MCP not implemented in core yet" };
53
+ };
54
+ },
55
+ },
56
+ );
57
+ },
58
+ });
59
+ }
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Storage primitive — intercepted key-value state with event emission.
3
+ *
4
+ * Every read/write is logged to the execution log. The actual storage
5
+ * is delegated to the {@link StateStore}.
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import type { StoragePrimitive } from "@kalphq/sdk";
11
+ import type { StateStore } from "@/adapters/interfaces";
12
+ import type { ExecutionLog } from "@/engine/execution-log";
13
+ import type { ExecutionContext } from "@/engine/types";
14
+
15
+ /**
16
+ * Creates an intercepted storage primitive that emits events for every operation.
17
+ *
18
+ * @param stateStore - The state sub-store for KV operations.
19
+ * @param log - The execution log for event emission.
20
+ * @param execCtx - The current execution context (identity for events).
21
+ * @returns A {@link StoragePrimitive} matching the SDK's storage API.
22
+ */
23
+ export function createStoragePrimitive(
24
+ stateStore: StateStore,
25
+ log: ExecutionLog,
26
+ execCtx?: ExecutionContext,
27
+ ): StoragePrimitive {
28
+ const ids = {
29
+ executionId: execCtx?.executionId ?? "",
30
+ traceId: execCtx?.traceId ?? "",
31
+ threadId: execCtx?.threadId ?? "",
32
+ };
33
+
34
+ return {
35
+ async get<T = unknown>(key: string): Promise<T | null> {
36
+ const value = (await stateStore.get(key)) as T | null;
37
+ await log.emit({
38
+ type: "state.read",
39
+ key,
40
+ value,
41
+ ...ids,
42
+ timestamp: Date.now(),
43
+ });
44
+ return value;
45
+ },
46
+
47
+ async put(key: string, value: unknown): Promise<void> {
48
+ await stateStore.set(key, value);
49
+ await log.emit({
50
+ type: "state.write",
51
+ key,
52
+ value,
53
+ ...ids,
54
+ timestamp: Date.now(),
55
+ });
56
+ },
57
+
58
+ async delete(key: string): Promise<void> {
59
+ await stateStore.delete(key);
60
+ await log.emit({
61
+ type: "state.write",
62
+ key,
63
+ value: undefined,
64
+ ...ids,
65
+ timestamp: Date.now(),
66
+ });
67
+ },
68
+
69
+ async increment(key: string, amount: number = 1): Promise<number> {
70
+ const newValue = await stateStore.increment(key, amount);
71
+ await log.emit({
72
+ type: "state.write",
73
+ key,
74
+ value: newValue,
75
+ ...ids,
76
+ timestamp: Date.now(),
77
+ });
78
+ return newValue;
79
+ },
80
+
81
+ async transaction<T>(
82
+ callback: (tx: any) => Promise<T>,
83
+ _options?: any,
84
+ ): Promise<T> {
85
+ // Basic transaction wrapping (without full interceptor for tx context yet)
86
+ return stateStore.transaction(async (tx) => {
87
+ return callback(tx as any);
88
+ });
89
+ },
90
+ };
91
+ }
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Orchestration Reactor — the Kalp v2 event-sourced execution engine.
3
+ *
4
+ * The reactor is **infrastructure-agnostic**. It receives events, processes
5
+ * tasks from an internal queue, and delegates all IO to adapter interfaces.
6
+ * When the queue is empty, control returns to the host adapter (Durable Object,
7
+ * Node process, test harness, etc.) which waits for the next external event.
8
+ *
9
+ * Key invariants:
10
+ * - The reactor never imports Cloudflare, Node, or any platform API.
11
+ * - Every operation emits structured events to the {@link ExecutionLog}.
12
+ * - Handler execution is sandboxed via intercepted primitives.
13
+ * - `actions.run` is an intent + suspend, not a direct call.
14
+ *
15
+ * @module
16
+ */
17
+
18
+ import type { IRGraph, IRNodeId } from "@kalphq/sdk";
19
+ import type {
20
+ PersistenceAdapter,
21
+ SchedulerAdapter,
22
+ } from "@/adapters/interfaces";
23
+ import type {
24
+ ExecutionTask,
25
+ RuntimeEvent,
26
+ HandlerModule,
27
+ } from "@/engine/types";
28
+ import type { RuntimeProviders } from "@/engine/context-builder";
29
+ import { ExecutionLog } from "@/engine/execution-log";
30
+ import { buildHandlerContext } from "@/engine/context-builder";
31
+
32
+ /**
33
+ * The Kalp v2 Orchestration Reactor.
34
+ *
35
+ * Processes external events by looking up the corresponding IR entry,
36
+ * executing the handler chain, and emitting structured events for every
37
+ * operation. The reactor is portable — it works in Durable Objects, Node,
38
+ * or any environment that provides the adapter interfaces.
39
+ */
40
+ export class OrchestrationReactor {
41
+ /** The execution log (single source of truth). */
42
+ public readonly log: ExecutionLog;
43
+ /** The IR graph (structural index only). */
44
+ public readonly ir: IRGraph;
45
+
46
+ /** Internal task queue. */
47
+ private queue: ExecutionTask[] = [];
48
+ /** Bundled handler modules keyed by moduleRef. */
49
+ private bundles: Map<string, HandlerModule>;
50
+ /** Composite persistence adapter (state, events, idempotency, threads). */
51
+ private persistence: PersistenceAdapter;
52
+ /** Scheduler adapter (alarms). */
53
+ private scheduler: SchedulerAdapter;
54
+ /** External providers (ai, auth, memory, vault). */
55
+ private providers: RuntimeProviders;
56
+ /** Result of the last completed handler. */
57
+ private lastResult: unknown = undefined;
58
+ /** Opaque thread identifier (set by adapter via RuntimeEvent.threadId). */
59
+ private threadId: string = "";
60
+
61
+ /**
62
+ * Creates a new reactor instance.
63
+ *
64
+ * @param ir - The compiled IR graph.
65
+ * @param bundles - Map from moduleRef to bundled handler module.
66
+ * @param persistence - Persistence adapter for state and event log.
67
+ * @param scheduler - Scheduler adapter for deferred wake-ups.
68
+ * @param providers - External providers for ai, auth, memory, vault.
69
+ */
70
+ constructor(
71
+ ir: IRGraph,
72
+ bundles: Map<string, HandlerModule>,
73
+ persistence: PersistenceAdapter,
74
+ scheduler: SchedulerAdapter,
75
+ providers: RuntimeProviders,
76
+ ) {
77
+ this.ir = ir;
78
+ this.bundles = bundles;
79
+ this.persistence = persistence;
80
+ this.scheduler = scheduler;
81
+ this.providers = providers;
82
+ this.log = new ExecutionLog(persistence.events);
83
+ }
84
+
85
+ // ──────────────────────────────────────────────────────────────────────────
86
+ // Public API (called by host adapter)
87
+ // ──────────────────────────────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Handles an external event by resolving the IR entry and processing
91
+ * the handler chain until the queue is empty.
92
+ *
93
+ * @param event - The external runtime event.
94
+ * @returns The result of the last completed handler.
95
+ * @throws If no entry exists for the event type.
96
+ */
97
+ async handleEvent(event: RuntimeEvent): Promise<unknown> {
98
+ const entryId = this.ir.entries[event.type];
99
+ if (!entryId) {
100
+ throw new Error(`No entry for event: ${event.type}`);
101
+ }
102
+
103
+ // Set thread identity from adapter (opaque — Core never parses it)
104
+ if (event.threadId) {
105
+ this.threadId = event.threadId;
106
+ }
107
+
108
+ const traceId = crypto.randomUUID();
109
+
110
+ await this.log.emit({
111
+ type: "node.started",
112
+ nodeId: entryId,
113
+ executionId: "",
114
+ traceId,
115
+ threadId: this.threadId,
116
+ timestamp: Date.now(),
117
+ });
118
+
119
+ this.queue.push({ nodeId: entryId, context: event.payload });
120
+ await this.processUntilIdle();
121
+
122
+ return this.lastResult;
123
+ }
124
+
125
+ /**
126
+ * Drains the task queue. Called internally after handleEvent and recursively
127
+ * after actions.run dispatches. When the queue is empty, control returns to
128
+ * the host adapter.
129
+ */
130
+ async processUntilIdle(): Promise<void> {
131
+ while (this.queue.length > 0) {
132
+ const task = this.queue.shift()!;
133
+ await this.processTask(task);
134
+ }
135
+ }
136
+
137
+ // ──────────────────────────────────────────────────────────────────────────
138
+ // Internal task processing
139
+ // ──────────────────────────────────────────────────────────────────────────
140
+
141
+ /**
142
+ * Processes a single task from the queue.
143
+ *
144
+ * - **Entry nodes**: traverse sequential edges to find the handler.
145
+ * - **Handler nodes**: build context, execute handler, emit events.
146
+ */
147
+ private async processTask(task: ExecutionTask): Promise<void> {
148
+ const node = this.ir.nodes[task.nodeId];
149
+ if (!node) {
150
+ await this.log.emit({
151
+ type: "error",
152
+ nodeId: task.nodeId,
153
+ error: `Node "${task.nodeId}" not found in IR.`,
154
+ executionId: "",
155
+ traceId: "",
156
+ threadId: this.threadId,
157
+ timestamp: Date.now(),
158
+ });
159
+ return;
160
+ }
161
+
162
+ switch (node.kind) {
163
+ case "entry": {
164
+ // Entry nodes are pass-through — enqueue successors
165
+ this.enqueueSuccessors(task.nodeId, task.context);
166
+ break;
167
+ }
168
+
169
+ case "handler": {
170
+ const executionId = crypto.randomUUID();
171
+
172
+ await this.log.emit({
173
+ type: "node.started",
174
+ nodeId: node.id,
175
+ executionId,
176
+ traceId: "",
177
+ threadId: this.threadId,
178
+ timestamp: Date.now(),
179
+ });
180
+
181
+ const handlerModule = this.bundles.get(node.moduleRef);
182
+ if (!handlerModule) {
183
+ await this.log.emit({
184
+ type: "error",
185
+ nodeId: node.id,
186
+ error: `Handler module "${node.moduleRef}" not found in bundles.`,
187
+ executionId,
188
+ traceId: "",
189
+ threadId: this.threadId,
190
+ timestamp: Date.now(),
191
+ });
192
+ return;
193
+ }
194
+
195
+ // Build intercepted context with dispatch callback
196
+ const dispatch = this.createDispatch();
197
+ const ctx = buildHandlerContext(
198
+ this.log,
199
+ this.persistence.state,
200
+ this.scheduler,
201
+ dispatch,
202
+ this.providers,
203
+ {
204
+ executionId,
205
+ traceId: "",
206
+ threadId: this.threadId,
207
+ untrackedIOCount: 0,
208
+ untrackedIOByType: { network: 0, timer: 0, fs: 0, unknown: 0 },
209
+ hasUntrustedPlugins: false
210
+ },
211
+ );
212
+
213
+ // Execute handler in sandboxed context
214
+ try {
215
+ const result = await handlerModule.default(ctx, task.context);
216
+
217
+ await this.log.emit({
218
+ type: "node.completed",
219
+ nodeId: node.id,
220
+ result,
221
+ executionId,
222
+ traceId: "",
223
+ threadId: this.threadId,
224
+ timestamp: Date.now(),
225
+ });
226
+
227
+ this.lastResult = result;
228
+
229
+ // If this task was dispatched by actions.run, resolve the caller's promise
230
+ if (task.resolve) {
231
+ task.resolve(result);
232
+ }
233
+
234
+ // Continue to sequential successors
235
+ this.enqueueSuccessors(task.nodeId, {
236
+ ...(task.context as object),
237
+ result,
238
+ });
239
+ } catch (err) {
240
+ const message = err instanceof Error ? err.message : String(err);
241
+ await this.log.emit({
242
+ type: "error",
243
+ nodeId: node.id,
244
+ error: message,
245
+ executionId,
246
+ traceId: "",
247
+ threadId: this.threadId,
248
+ timestamp: Date.now(),
249
+ });
250
+
251
+ // Re-throw to let the host adapter handle it
252
+ throw err;
253
+ }
254
+ break;
255
+ }
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Enqueues all successor nodes connected by outgoing edges from the given node.
261
+ *
262
+ * @param nodeId - The source node ID.
263
+ * @param context - The context to pass to successor tasks.
264
+ */
265
+ private enqueueSuccessors(nodeId: IRNodeId, context: unknown): void {
266
+ const outEdges = this.ir.edges.filter((e) => e.from === nodeId);
267
+ for (const edge of outEdges) {
268
+ this.queue.push({ nodeId: edge.to, context });
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Creates a dispatch function for actions.run.
274
+ *
275
+ * When a handler calls `actions.run(step, input)`, this function:
276
+ * 1. Looks up the handler node by moduleRef in the IR.
277
+ * 2. Creates a task with a resolve callback.
278
+ * 3. Returns a Promise that resolves when the task completes.
279
+ */
280
+ private createDispatch(): (
281
+ moduleRef: string,
282
+ input: unknown,
283
+ ) => Promise<unknown> {
284
+ return (moduleRef: string, input: unknown): Promise<unknown> => {
285
+ // O(1) lookup via handlerIndex (built by compiler)
286
+ const handlerNodeId = this.ir.handlerIndex[moduleRef] as
287
+ | IRNodeId
288
+ | undefined;
289
+
290
+ if (!handlerNodeId) {
291
+ return Promise.reject(
292
+ new Error(
293
+ `Cannot dispatch: handler "${moduleRef}" not found in IR. ` +
294
+ `Ensure it is statically imported at the top level.`,
295
+ ),
296
+ );
297
+ }
298
+
299
+ return new Promise<unknown>((resolve) => {
300
+ this.queue.push({
301
+ nodeId: handlerNodeId,
302
+ context: input,
303
+ resolve,
304
+ });
305
+ });
306
+ };
307
+ }
308
+ }