@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,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Actions primitive — intercepted orchestration intents with event emission.
|
|
3
|
+
*
|
|
4
|
+
* This is the core of the event-sourced runtime model. Every action is an
|
|
5
|
+
* **intent**, not a direct execution. The handler emits the intent, and the
|
|
6
|
+
* reactor resolves it.
|
|
7
|
+
*
|
|
8
|
+
* - `actions.run(step, input)` → emits `action.run`, suspends, reactor enqueues handler, resolves with result
|
|
9
|
+
* - `actions.wait(duration)` → emits `action.wait`, schedules alarm, terminates execution
|
|
10
|
+
* - `actions.loop(body)` → emits `action.loop.start`, reactor drives iterations
|
|
11
|
+
* - `actions.fetch(url, init)` → emits `action.fetch`, delegates to http primitive
|
|
12
|
+
*
|
|
13
|
+
* @module
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
KalpActions,
|
|
18
|
+
ExecutableNode,
|
|
19
|
+
InputOf,
|
|
20
|
+
OutputOf,
|
|
21
|
+
WakeReason,
|
|
22
|
+
AskOptions,
|
|
23
|
+
EmitOptions,
|
|
24
|
+
AgentContract,
|
|
25
|
+
} from "@kalphq/sdk";
|
|
26
|
+
import { z } from "@kalphq/sdk";
|
|
27
|
+
import type { ExecutionLog } from "@/engine/execution-log";
|
|
28
|
+
import type { SchedulerAdapter } from "@/adapters/interfaces";
|
|
29
|
+
import type { ExecutionContext } from "@/engine/types";
|
|
30
|
+
|
|
31
|
+
/** Callback to dispatch a handler task into the reactor queue. */
|
|
32
|
+
export type DispatchAction = (
|
|
33
|
+
moduleRef: string,
|
|
34
|
+
input: unknown,
|
|
35
|
+
) => Promise<unknown>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Parses a human-readable duration string (e.g. "5m", "1h", "30s") to milliseconds.
|
|
39
|
+
*
|
|
40
|
+
* @param duration - A string like "5m", "1h", "30s", "2d", or a raw number of ms.
|
|
41
|
+
* @returns Duration in milliseconds.
|
|
42
|
+
*/
|
|
43
|
+
function parseDuration(duration: string | number): number {
|
|
44
|
+
if (typeof duration === "number") return duration;
|
|
45
|
+
|
|
46
|
+
const match = duration.match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)$/i);
|
|
47
|
+
if (!match) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
`Invalid duration format: "${duration}". Expected e.g. "5m", "30s", "1h".`,
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const value = parseFloat(match[1]!);
|
|
54
|
+
const unit = match[2]!.toLowerCase();
|
|
55
|
+
|
|
56
|
+
const multipliers: Record<string, number> = {
|
|
57
|
+
ms: 1,
|
|
58
|
+
s: 1_000,
|
|
59
|
+
m: 60_000,
|
|
60
|
+
h: 3_600_000,
|
|
61
|
+
d: 86_400_000,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return value * (multipliers[unit] ?? 1);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Creates the intercepted actions primitive for handler context.
|
|
69
|
+
*
|
|
70
|
+
* Each method emits structured events and interacts with the reactor
|
|
71
|
+
* via the `dispatch` callback (for run) and the `scheduler` adapter
|
|
72
|
+
* (for wait).
|
|
73
|
+
*
|
|
74
|
+
* @param log - The execution log for event emission.
|
|
75
|
+
* @param scheduler - The scheduler adapter for deferred execution.
|
|
76
|
+
* @param dispatch - Callback to enqueue a handler task in the reactor.
|
|
77
|
+
* @param execCtx - Optional execution context for event identity.
|
|
78
|
+
* @returns A {@link KalpActions} matching the SDK interface exactly.
|
|
79
|
+
*/
|
|
80
|
+
export function createActionsPrimitive(
|
|
81
|
+
log: ExecutionLog,
|
|
82
|
+
scheduler: SchedulerAdapter,
|
|
83
|
+
dispatch: DispatchAction,
|
|
84
|
+
execCtx?: ExecutionContext,
|
|
85
|
+
): KalpActions {
|
|
86
|
+
let loopCounter = 0;
|
|
87
|
+
const ids = {
|
|
88
|
+
executionId: execCtx?.executionId ?? "",
|
|
89
|
+
traceId: execCtx?.traceId ?? "",
|
|
90
|
+
threadId: execCtx?.threadId ?? "",
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
/**
|
|
95
|
+
* Emits an `action.run` intent and suspends until the reactor resolves it.
|
|
96
|
+
* The handler receives a Promise that resolves with the step/tool result.
|
|
97
|
+
*/
|
|
98
|
+
async run<T extends ExecutableNode>(
|
|
99
|
+
node: T,
|
|
100
|
+
...args: InputOf<T> extends never ? [] : [input: InputOf<T>]
|
|
101
|
+
): Promise<OutputOf<T>> {
|
|
102
|
+
const target = `${node.kind}s.${node.id}`;
|
|
103
|
+
const input = args[0];
|
|
104
|
+
|
|
105
|
+
await log.emit({
|
|
106
|
+
type: "action.run",
|
|
107
|
+
target,
|
|
108
|
+
input,
|
|
109
|
+
...ids,
|
|
110
|
+
timestamp: Date.now(),
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
const result = await dispatch(target, input);
|
|
114
|
+
|
|
115
|
+
await log.emit({
|
|
116
|
+
type: "action.run.completed",
|
|
117
|
+
target,
|
|
118
|
+
result,
|
|
119
|
+
...ids,
|
|
120
|
+
timestamp: Date.now(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return result as OutputOf<T>;
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Emits an `action.wait` event, schedules an alarm, and returns.
|
|
128
|
+
* The host adapter is responsible for re-entering the reactor when the alarm fires.
|
|
129
|
+
*/
|
|
130
|
+
async wait(duration: string | number): Promise<WakeReason> {
|
|
131
|
+
const ms = parseDuration(duration);
|
|
132
|
+
|
|
133
|
+
await log.emit({
|
|
134
|
+
type: "action.wait",
|
|
135
|
+
duration,
|
|
136
|
+
...ids,
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
await scheduler.schedule(Date.now() + ms);
|
|
141
|
+
|
|
142
|
+
// In the event-sourced model, wait terminates the current execution.
|
|
143
|
+
// The alarm fires and re-enters the reactor via handleEvent("onTick").
|
|
144
|
+
// The WakeReason is resolved by the reactor on rehydration.
|
|
145
|
+
return { type: "timeout" };
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Runtime-driven loop. Each iteration is a separate event, enabling
|
|
150
|
+
* persistence, rehydration, cut, pause, and per-iteration billing.
|
|
151
|
+
*/
|
|
152
|
+
loop(body: () => Promise<void>): void {
|
|
153
|
+
const loopId = `loop_${++loopCounter}_${Date.now()}`;
|
|
154
|
+
|
|
155
|
+
// Fire-and-forget — the reactor drives the loop asynchronously.
|
|
156
|
+
// The handler does NOT await the loop; it registers the intent.
|
|
157
|
+
void (async () => {
|
|
158
|
+
await log.emit({
|
|
159
|
+
type: "action.loop.start",
|
|
160
|
+
loopId,
|
|
161
|
+
...ids,
|
|
162
|
+
timestamp: Date.now(),
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
let iteration = 0;
|
|
166
|
+
|
|
167
|
+
while (true) {
|
|
168
|
+
await log.emit({
|
|
169
|
+
type: "action.loop.iteration",
|
|
170
|
+
loopId,
|
|
171
|
+
iteration,
|
|
172
|
+
...ids,
|
|
173
|
+
timestamp: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
await body();
|
|
178
|
+
} catch (err) {
|
|
179
|
+
await log.emit({
|
|
180
|
+
type: "action.loop.end",
|
|
181
|
+
loopId,
|
|
182
|
+
reason: err instanceof Error ? err.message : "error",
|
|
183
|
+
...ids,
|
|
184
|
+
timestamp: Date.now(),
|
|
185
|
+
});
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
iteration++;
|
|
190
|
+
}
|
|
191
|
+
})();
|
|
192
|
+
},
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Intercepted fetch — logs the request and delegates to globalThis.fetch.
|
|
196
|
+
*/
|
|
197
|
+
async fetch(
|
|
198
|
+
input: string | URL | Request,
|
|
199
|
+
init?: RequestInit,
|
|
200
|
+
): Promise<Response> {
|
|
201
|
+
const url =
|
|
202
|
+
typeof input === "string"
|
|
203
|
+
? input
|
|
204
|
+
: input instanceof URL
|
|
205
|
+
? input.href
|
|
206
|
+
: input.url;
|
|
207
|
+
const method =
|
|
208
|
+
init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
209
|
+
|
|
210
|
+
const start = Date.now();
|
|
211
|
+
const response = await globalThis.fetch(input, init);
|
|
212
|
+
const durationMs = Date.now() - start;
|
|
213
|
+
|
|
214
|
+
await log.emit({
|
|
215
|
+
type: "action.fetch",
|
|
216
|
+
url,
|
|
217
|
+
method,
|
|
218
|
+
status: response.status,
|
|
219
|
+
durationMs,
|
|
220
|
+
...ids,
|
|
221
|
+
timestamp: Date.now(),
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
return response;
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
async ask<T extends z.ZodTypeAny>(
|
|
228
|
+
_prompt: string,
|
|
229
|
+
_schema: T,
|
|
230
|
+
_options?: AskOptions,
|
|
231
|
+
): Promise<z.infer<T>> {
|
|
232
|
+
await log.emit({
|
|
233
|
+
type: "action.ask",
|
|
234
|
+
...ids,
|
|
235
|
+
timestamp: Date.now(),
|
|
236
|
+
});
|
|
237
|
+
return { text: "stub response" } as z.infer<T>;
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async requestApproval(
|
|
241
|
+
_reason: string,
|
|
242
|
+
_options?: AskOptions,
|
|
243
|
+
): Promise<boolean> {
|
|
244
|
+
await log.emit({
|
|
245
|
+
type: "action.approval",
|
|
246
|
+
...ids,
|
|
247
|
+
timestamp: Date.now(),
|
|
248
|
+
});
|
|
249
|
+
return true;
|
|
250
|
+
},
|
|
251
|
+
|
|
252
|
+
emit(eventName: string, payload: unknown, _options?: EmitOptions): void {
|
|
253
|
+
void log.emit({
|
|
254
|
+
type: "action.emit",
|
|
255
|
+
event: eventName,
|
|
256
|
+
payload,
|
|
257
|
+
...ids,
|
|
258
|
+
timestamp: Date.now(),
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
async callAgent<TContract extends AgentContract<any, any>>(
|
|
263
|
+
contract: TContract,
|
|
264
|
+
input: z.infer<TContract["inputSchema"]>,
|
|
265
|
+
): Promise<z.infer<TContract["outputSchema"]>> {
|
|
266
|
+
await log.emit({
|
|
267
|
+
type: "action.call",
|
|
268
|
+
contract: contract.agentId,
|
|
269
|
+
input,
|
|
270
|
+
...ids,
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
});
|
|
273
|
+
return {} as z.infer<TContract["outputSchema"]>;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Pauses the agent until an external event occurs.
|
|
278
|
+
* Emits an action.wait event with duration and schedules a timeout alarm if specified.
|
|
279
|
+
*/
|
|
280
|
+
async waitForEvent(
|
|
281
|
+
eventName: string,
|
|
282
|
+
timeout?: string | number,
|
|
283
|
+
): Promise<WakeReason> {
|
|
284
|
+
const duration = timeout ? parseDuration(timeout) : 0;
|
|
285
|
+
|
|
286
|
+
await log.emit({
|
|
287
|
+
type: "action.wait",
|
|
288
|
+
duration,
|
|
289
|
+
...ids,
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (timeout) {
|
|
294
|
+
await scheduler.schedule(Date.now() + duration);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The reactor will resolve this when the event arrives or timeout fires
|
|
298
|
+
return { type: "event", eventName, payload: undefined };
|
|
299
|
+
},
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Pauses the agent until a specific date/time.
|
|
303
|
+
* Emits an action.wait event with the duration and schedules an alarm for the target time.
|
|
304
|
+
*/
|
|
305
|
+
async waitUntil(date: Date | string | number): Promise<WakeReason> {
|
|
306
|
+
const targetTime =
|
|
307
|
+
typeof date === "number"
|
|
308
|
+
? date
|
|
309
|
+
: typeof date === "string"
|
|
310
|
+
? new Date(date).getTime()
|
|
311
|
+
: date.getTime();
|
|
312
|
+
|
|
313
|
+
const now = Date.now();
|
|
314
|
+
const duration = Math.max(0, targetTime - now);
|
|
315
|
+
|
|
316
|
+
await log.emit({
|
|
317
|
+
type: "action.wait",
|
|
318
|
+
duration,
|
|
319
|
+
...ids,
|
|
320
|
+
timestamp: Date.now(),
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
await scheduler.schedule(targetTime);
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
type: "scheduled_time_reached",
|
|
327
|
+
scheduledAt: new Date(targetTime).toISOString(),
|
|
328
|
+
};
|
|
329
|
+
},
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Schedules a node for future non-blocking execution.
|
|
333
|
+
* Emits an action.schedule event and schedules the execution via the scheduler.
|
|
334
|
+
*/
|
|
335
|
+
async schedule<T extends ExecutableNode>(
|
|
336
|
+
node: T,
|
|
337
|
+
date: Date | string | number,
|
|
338
|
+
...args: InputOf<T> extends never ? [] : [input: InputOf<T>]
|
|
339
|
+
): Promise<{ scheduleId: string }> {
|
|
340
|
+
const targetTime =
|
|
341
|
+
typeof date === "number"
|
|
342
|
+
? date
|
|
343
|
+
: typeof date === "string"
|
|
344
|
+
? new Date(date).getTime()
|
|
345
|
+
: date.getTime();
|
|
346
|
+
|
|
347
|
+
const target = `${node.kind}s.${node.id}`;
|
|
348
|
+
const input = args[0];
|
|
349
|
+
const scheduleId = `schedule_${target}_${targetTime}`;
|
|
350
|
+
|
|
351
|
+
await log.emit({
|
|
352
|
+
type: "action.schedule",
|
|
353
|
+
at: targetTime,
|
|
354
|
+
payload: { scheduleId, target, input },
|
|
355
|
+
...ids,
|
|
356
|
+
timestamp: Date.now(),
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
await scheduler.schedule(targetTime);
|
|
360
|
+
|
|
361
|
+
return { scheduleId };
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent introspection primitive.
|
|
3
|
+
*
|
|
4
|
+
* Provides read-only metadata about the current agent execution.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExecutionLog } from "@/engine/execution-log";
|
|
10
|
+
import type { AgentIntrospection } from "@kalphq/sdk";
|
|
11
|
+
import type { ExecutionContext } from "@/engine/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates an agent introspection primitive.
|
|
15
|
+
*
|
|
16
|
+
* @param log - The execution log for event emission.
|
|
17
|
+
* @param execCtx - Optional execution context for event identity.
|
|
18
|
+
* @param agentConfig - Agent configuration metadata.
|
|
19
|
+
* @returns An {@link AgentIntrospection} instance.
|
|
20
|
+
*/
|
|
21
|
+
export function createAgentMetaPrimitive(
|
|
22
|
+
log: ExecutionLog,
|
|
23
|
+
execCtx?: ExecutionContext,
|
|
24
|
+
agentConfig?: {
|
|
25
|
+
name: string;
|
|
26
|
+
systemPrompt: string;
|
|
27
|
+
metadata?: Record<string, unknown>;
|
|
28
|
+
},
|
|
29
|
+
): AgentIntrospection {
|
|
30
|
+
return {
|
|
31
|
+
agentId: execCtx?.executionId ?? "unknown",
|
|
32
|
+
runId: execCtx?.traceId ?? "unknown",
|
|
33
|
+
name: agentConfig?.name ?? "unknown",
|
|
34
|
+
systemPrompt: agentConfig?.systemPrompt ?? "",
|
|
35
|
+
metadata: agentConfig?.metadata,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI primitive — intercepted LLM calls with event emission.
|
|
3
|
+
*
|
|
4
|
+
* Every generate/stream/classify call is logged to the execution log.
|
|
5
|
+
* The actual LLM provider call is delegated to the injected implementation.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { KalpAI } from "@kalphq/sdk";
|
|
11
|
+
import type { ExecutionLog } from "@/engine/execution-log";
|
|
12
|
+
import type { ExecutionContext } from "@/engine/types";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A concrete AI provider implementation that performs the actual LLM calls.
|
|
16
|
+
* This is injected by the host adapter (e.g. Cloudflare AI, OpenAI fetch, etc.).
|
|
17
|
+
*/
|
|
18
|
+
export type AIProvider = KalpAI;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Creates an intercepted AI primitive that wraps every call with event emission.
|
|
22
|
+
*
|
|
23
|
+
* The returned object matches the SDK's {@link KalpAI} interface exactly.
|
|
24
|
+
* Internally, each method:
|
|
25
|
+
* 1. Emits a `primitive.invoked` event before calling the provider.
|
|
26
|
+
* 2. Calls the actual provider implementation.
|
|
27
|
+
* 3. Emits a `primitive.invoked` event with the result.
|
|
28
|
+
*
|
|
29
|
+
* @param provider - The concrete AI provider implementation.
|
|
30
|
+
* @param log - The execution log for event emission.
|
|
31
|
+
* @param execCtx - Optional execution context for event identity.
|
|
32
|
+
* @returns An intercepted {@link KalpAI} matching the SDK interface.
|
|
33
|
+
*/
|
|
34
|
+
export function createAIPrimitive(
|
|
35
|
+
provider: AIProvider,
|
|
36
|
+
log: ExecutionLog,
|
|
37
|
+
execCtx?: ExecutionContext,
|
|
38
|
+
): KalpAI {
|
|
39
|
+
const ids = {
|
|
40
|
+
executionId: execCtx?.executionId ?? "",
|
|
41
|
+
traceId: execCtx?.traceId ?? "",
|
|
42
|
+
threadId: execCtx?.threadId ?? "",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
async generate(params) {
|
|
47
|
+
const result = await provider.generate(params);
|
|
48
|
+
await log.emit({
|
|
49
|
+
type: "primitive.invoked",
|
|
50
|
+
name: "ai.generate",
|
|
51
|
+
params: { model: params.model, prompt: params.prompt },
|
|
52
|
+
result,
|
|
53
|
+
...ids,
|
|
54
|
+
timestamp: Date.now(),
|
|
55
|
+
});
|
|
56
|
+
return result;
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
stream(params) {
|
|
60
|
+
// Stream is special — we log the invocation but return the iterable.
|
|
61
|
+
// Individual chunks are NOT logged (too noisy). The caller consumes them.
|
|
62
|
+
void log.emit({
|
|
63
|
+
type: "primitive.invoked",
|
|
64
|
+
name: "ai.stream",
|
|
65
|
+
params: { model: params.model, prompt: params.prompt },
|
|
66
|
+
result: "[stream]",
|
|
67
|
+
...ids,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
return provider.stream(params);
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
async classify(params) {
|
|
74
|
+
const result = await provider.classify(params);
|
|
75
|
+
await log.emit({
|
|
76
|
+
type: "primitive.invoked",
|
|
77
|
+
name: "ai.classify",
|
|
78
|
+
params: { input: params.input, labels: params.labels },
|
|
79
|
+
result,
|
|
80
|
+
...ids,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
});
|
|
83
|
+
return result;
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic date/time primitive.
|
|
3
|
+
*
|
|
4
|
+
* Provides deterministic time operations using the event timestamp.
|
|
5
|
+
*
|
|
6
|
+
* @module
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ExecutionLog } from "@/engine/execution-log";
|
|
10
|
+
import type { KalpDate, TimezoneFormatter } from "@kalphq/sdk";
|
|
11
|
+
import type { ExecutionContext } from "@/engine/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates a date 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 KalpDate} instance.
|
|
19
|
+
*/
|
|
20
|
+
export function createDatePrimitive(
|
|
21
|
+
log: ExecutionLog,
|
|
22
|
+
execCtx?: ExecutionContext,
|
|
23
|
+
): KalpDate {
|
|
24
|
+
const ids = {
|
|
25
|
+
executionId: execCtx?.executionId ?? "",
|
|
26
|
+
traceId: execCtx?.traceId ?? "",
|
|
27
|
+
threadId: execCtx?.threadId ?? "",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Use current time as base (in production, use event timestamp)
|
|
31
|
+
const baseTime = Date.now();
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
/**
|
|
35
|
+
* Get the current time (event timestamp).
|
|
36
|
+
*/
|
|
37
|
+
now(): number {
|
|
38
|
+
return baseTime;
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Format the current time as ISO string.
|
|
43
|
+
*/
|
|
44
|
+
toISOString(): string {
|
|
45
|
+
return new Date(baseTime).toISOString();
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if the original event occurred after the given date.
|
|
50
|
+
*
|
|
51
|
+
* @param isoString - ISO 8601 date string to compare against.
|
|
52
|
+
*/
|
|
53
|
+
isAfter(isoString: string): boolean {
|
|
54
|
+
const timestamp = Date.now();
|
|
55
|
+
void log.emit({
|
|
56
|
+
type: "primitive.invoked",
|
|
57
|
+
name: "date.isAfter",
|
|
58
|
+
params: isoString,
|
|
59
|
+
result: undefined,
|
|
60
|
+
...ids,
|
|
61
|
+
timestamp,
|
|
62
|
+
});
|
|
63
|
+
return baseTime > new Date(isoString).getTime();
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Add a duration to the current time.
|
|
68
|
+
*
|
|
69
|
+
* @param duration - Duration string (e.g., "1h", "30m", "24h") or milliseconds.
|
|
70
|
+
*/
|
|
71
|
+
add(duration: string | number): number {
|
|
72
|
+
const timestamp = Date.now();
|
|
73
|
+
void log.emit({
|
|
74
|
+
type: "primitive.invoked",
|
|
75
|
+
name: "date.add",
|
|
76
|
+
params: duration,
|
|
77
|
+
result: undefined,
|
|
78
|
+
...ids,
|
|
79
|
+
timestamp,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// If duration is a number, treat as milliseconds
|
|
83
|
+
if (typeof duration === "number") {
|
|
84
|
+
return baseTime + duration;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Parse duration string
|
|
88
|
+
const match = duration.match(/^(\d+)([smhd])$/);
|
|
89
|
+
if (!match) {
|
|
90
|
+
throw new Error(`Invalid duration format: ${duration}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const value = parseInt(match[1]!, 10);
|
|
94
|
+
const unit = match[2]!;
|
|
95
|
+
if (!unit) {
|
|
96
|
+
throw new Error(`Invalid duration format: ${duration}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const multipliers: Record<string, number> = {
|
|
100
|
+
s: 1000,
|
|
101
|
+
m: 60000,
|
|
102
|
+
h: 3600000,
|
|
103
|
+
d: 86400000,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const multiplier = multipliers[unit];
|
|
107
|
+
if (!multiplier) {
|
|
108
|
+
throw new Error(`Invalid duration unit: ${unit}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return baseTime + value * multiplier;
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convert the event timestamp to a specific timezone.
|
|
116
|
+
*
|
|
117
|
+
* @param tz - IANA timezone identifier.
|
|
118
|
+
* @returns TimezoneFormatter for the converted time.
|
|
119
|
+
*/
|
|
120
|
+
timezone(tz: string): TimezoneFormatter {
|
|
121
|
+
const formatter: TimezoneFormatter = {
|
|
122
|
+
format(format: string): string {
|
|
123
|
+
const timestamp = Date.now();
|
|
124
|
+
void log.emit({
|
|
125
|
+
type: "primitive.invoked",
|
|
126
|
+
name: "date.timezone.format",
|
|
127
|
+
params: { tz, format },
|
|
128
|
+
result: undefined,
|
|
129
|
+
...ids,
|
|
130
|
+
timestamp,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Simple formatting - in production use a proper date library
|
|
134
|
+
const date = new Date(baseTime);
|
|
135
|
+
return date.toISOString().replace("T", " ").slice(0, 19);
|
|
136
|
+
},
|
|
137
|
+
toISOString(): string {
|
|
138
|
+
return new Date(baseTime).toISOString();
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
return formatter;
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP primitive — intercepted fetch with event emission.
|
|
3
|
+
*
|
|
4
|
+
* Every outbound HTTP request is logged to the execution log.
|
|
5
|
+
* The actual fetch is delegated to the runtime's fetch implementation.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { ExecutionLog } from "@/engine/execution-log";
|
|
11
|
+
import type { ExecutionContext } from "@/engine/types";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Creates an intercepted fetch function that emits events for every call.
|
|
15
|
+
*
|
|
16
|
+
* @param log - The execution log for event emission.
|
|
17
|
+
* @param execCtx - Optional execution context for event identity.
|
|
18
|
+
* @returns A fetch function matching the standard `fetch` signature.
|
|
19
|
+
*/
|
|
20
|
+
export function createHttpPrimitive(
|
|
21
|
+
log: ExecutionLog,
|
|
22
|
+
execCtx?: ExecutionContext,
|
|
23
|
+
): (input: string | URL | Request, init?: RequestInit) => Promise<Response> {
|
|
24
|
+
const ids = {
|
|
25
|
+
executionId: execCtx?.executionId ?? "",
|
|
26
|
+
traceId: execCtx?.traceId ?? "",
|
|
27
|
+
threadId: execCtx?.threadId ?? "",
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return async (
|
|
31
|
+
input: string | URL | Request,
|
|
32
|
+
init?: RequestInit,
|
|
33
|
+
): Promise<Response> => {
|
|
34
|
+
const url =
|
|
35
|
+
typeof input === "string"
|
|
36
|
+
? input
|
|
37
|
+
: input instanceof URL
|
|
38
|
+
? input.href
|
|
39
|
+
: input.url;
|
|
40
|
+
const method =
|
|
41
|
+
init?.method ?? (input instanceof Request ? input.method : "GET");
|
|
42
|
+
|
|
43
|
+
const response = await globalThis.fetch(input, init);
|
|
44
|
+
|
|
45
|
+
await log.emit({
|
|
46
|
+
type: "primitive.invoked",
|
|
47
|
+
name: "http.fetch",
|
|
48
|
+
params: { url, method },
|
|
49
|
+
result: { status: response.status, statusText: response.statusText },
|
|
50
|
+
...ids,
|
|
51
|
+
timestamp: Date.now(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
return response;
|
|
55
|
+
};
|
|
56
|
+
}
|