@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.
- package/.turbo/turbo-build.log +22 -0
- package/CHANGELOG.md +7 -0
- package/dist/chunk-G4LOF3MT.js +957 -0
- package/dist/chunk-G4LOF3MT.js.map +1 -0
- package/dist/factory.d.ts +59 -0
- package/dist/factory.js +18 -0
- package/dist/factory.js.map +1 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +41 -0
- package/dist/index.js.map +1 -0
- package/dist/reactor-Brv_eUBE.d.ts +742 -0
- package/eslint.config.js +33 -0
- package/package.json +30 -0
- package/src/adapters/interfaces.ts +248 -0
- package/src/engine/context-builder.ts +151 -0
- package/src/engine/execution-log.ts +73 -0
- package/src/engine/primitives/actions.ts +364 -0
- package/src/engine/primitives/agent-meta.ts +37 -0
- package/src/engine/primitives/ai.ts +86 -0
- package/src/engine/primitives/date.ts +144 -0
- package/src/engine/primitives/http.ts +56 -0
- package/src/engine/primitives/math.ts +173 -0
- package/src/engine/primitives/mcp.ts +59 -0
- package/src/engine/primitives/storage.ts +91 -0
- package/src/engine/reactor.ts +308 -0
- package/src/engine/types.ts +304 -0
- package/src/env.d.ts +142 -0
- package/src/factory.ts +69 -0
- package/src/index.ts +61 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +9 -0
|
@@ -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
|
+
}
|