@stackbone/sdk 0.1.0-alpha.1 → 0.1.0-alpha.3

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,587 @@
1
+ import { appendFile, mkdir } from 'fs/promises';
2
+ import { homedir } from 'os';
3
+ import { resolve, dirname } from 'path';
4
+ import { randomUUID } from 'crypto';
5
+
6
+ var __defProp = Object.defineProperty;
7
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+
9
+ // package.json
10
+ var version = "0.1.0-alpha.2";
11
+
12
+ // src/observability/logger.ts
13
+ var LOG_LEVELS = {
14
+ trace: 10,
15
+ debug: 20,
16
+ info: 30,
17
+ warn: 40,
18
+ error: 50,
19
+ fatal: 60
20
+ };
21
+ var DEFAULT_BATCH_SIZE = 50;
22
+ var DEFAULT_INTERVAL_MS = 1e3;
23
+ var DEFAULT_HTTP_TIMEOUT_MS = 5e3;
24
+ function createPlatformLogger(options) {
25
+ const otelEndpoint = options.otelEndpoint ?? void 0;
26
+ const mode = options.mode ?? (otelEndpoint ? "cloud" : "local");
27
+ const baseBindings = {
28
+ run_id: options.runId
29
+ };
30
+ if (options.installationId !== void 0) baseBindings["installation_id"] = options.installationId;
31
+ if (options.agentId !== void 0) baseBindings["agent_id"] = options.agentId;
32
+ const destination = mode === "cloud" ? new OtlpHttpDestination({
33
+ endpoint: otelEndpoint ?? "",
34
+ resourceAttributes: options.resourceAttributes ?? {},
35
+ httpTimeoutMs: options.httpTimeoutMs ?? DEFAULT_HTTP_TIMEOUT_MS,
36
+ flushBatchSize: options.flushBatchSize ?? DEFAULT_BATCH_SIZE,
37
+ flushIntervalMs: options.flushIntervalMs ?? DEFAULT_INTERVAL_MS,
38
+ ...options.fetchImpl !== void 0 ? {
39
+ fetchImpl: options.fetchImpl
40
+ } : {}
41
+ }) : new JsonlFileDestination({
42
+ runId: options.runId,
43
+ runsDir: options.runsDir ?? defaultRunsDir(),
44
+ ...options.appendFileImpl !== void 0 ? {
45
+ appendFileImpl: options.appendFileImpl
46
+ } : {},
47
+ ...options.mkdirImpl !== void 0 ? {
48
+ mkdirImpl: options.mkdirImpl
49
+ } : {}
50
+ });
51
+ const now = options.now ?? Date.now;
52
+ return buildLogger({
53
+ destination,
54
+ baseBindings,
55
+ mode,
56
+ now
57
+ });
58
+ }
59
+ __name(createPlatformLogger, "createPlatformLogger");
60
+ function buildLogger(opts) {
61
+ const writeAt = /* @__PURE__ */ __name((level, msgOrObj, msg) => {
62
+ const time = opts.now();
63
+ const levelNumber = LOG_LEVELS[level];
64
+ const record = {
65
+ level: levelNumber,
66
+ time,
67
+ msg: "",
68
+ run_id: String(opts.baseBindings["run_id"] ?? ""),
69
+ ...opts.baseBindings
70
+ };
71
+ if (typeof msgOrObj === "string") {
72
+ record.msg = msgOrObj;
73
+ } else if (msgOrObj && typeof msgOrObj === "object") {
74
+ Object.assign(record, msgOrObj);
75
+ if (typeof msg === "string") record.msg = msg;
76
+ else if (typeof record.msg !== "string") record.msg = "";
77
+ }
78
+ opts.destination.write(record);
79
+ }, "writeAt");
80
+ const child = /* @__PURE__ */ __name((bindings) => buildLogger({
81
+ ...opts,
82
+ baseBindings: {
83
+ ...opts.baseBindings,
84
+ ...bindings
85
+ }
86
+ }), "child");
87
+ return {
88
+ mode: opts.mode,
89
+ trace: /* @__PURE__ */ __name((msgOrObj, msg) => writeAt("trace", msgOrObj, msg), "trace"),
90
+ debug: /* @__PURE__ */ __name((msgOrObj, msg) => writeAt("debug", msgOrObj, msg), "debug"),
91
+ info: /* @__PURE__ */ __name((msgOrObj, msg) => writeAt("info", msgOrObj, msg), "info"),
92
+ warn: /* @__PURE__ */ __name((msgOrObj, msg) => writeAt("warn", msgOrObj, msg), "warn"),
93
+ error: /* @__PURE__ */ __name((msgOrObj, msg) => writeAt("error", msgOrObj, msg), "error"),
94
+ fatal: /* @__PURE__ */ __name((msgOrObj, msg) => writeAt("fatal", msgOrObj, msg), "fatal"),
95
+ child,
96
+ flush: /* @__PURE__ */ __name(() => opts.destination.flush(), "flush"),
97
+ close: /* @__PURE__ */ __name(() => opts.destination.close(), "close")
98
+ };
99
+ }
100
+ __name(buildLogger, "buildLogger");
101
+ var JsonlFileDestination = class JsonlFileDestination2 {
102
+ static {
103
+ __name(this, "JsonlFileDestination");
104
+ }
105
+ path;
106
+ appendFile;
107
+ mkdir;
108
+ inFlight = Promise.resolve();
109
+ dirReady = false;
110
+ constructor(opts) {
111
+ this.path = resolve(opts.runsDir, `${opts.runId}.jsonl`);
112
+ this.appendFile = opts.appendFileImpl ?? ((p, d) => appendFile(p, d, "utf8"));
113
+ this.mkdir = opts.mkdirImpl ?? (async (p) => {
114
+ await mkdir(p, {
115
+ recursive: true
116
+ });
117
+ });
118
+ }
119
+ write(record) {
120
+ const line = `${JSON.stringify(record)}
121
+ `;
122
+ this.inFlight = this.inFlight.then(async () => {
123
+ if (!this.dirReady) {
124
+ await this.mkdir(dirname(this.path));
125
+ this.dirReady = true;
126
+ }
127
+ try {
128
+ await this.appendFile(this.path, line);
129
+ } catch (error) {
130
+ process.stderr.write(`[stackbone/sdk] PlatformLogger JSONL write failed: ${error instanceof Error ? error.message : String(error)}
131
+ `);
132
+ }
133
+ });
134
+ }
135
+ async flush() {
136
+ await this.inFlight;
137
+ }
138
+ async close() {
139
+ await this.flush();
140
+ }
141
+ };
142
+ var OtlpHttpDestination = class OtlpHttpDestination2 {
143
+ static {
144
+ __name(this, "OtlpHttpDestination");
145
+ }
146
+ url;
147
+ resourceAttributes;
148
+ httpTimeoutMs;
149
+ flushBatchSize;
150
+ flushIntervalMs;
151
+ fetchImpl;
152
+ buffer = [];
153
+ timer = null;
154
+ inFlight = Promise.resolve();
155
+ closed = false;
156
+ constructor(opts) {
157
+ this.url = `${opts.endpoint.replace(/\/+$/, "")}/v1/logs`;
158
+ this.resourceAttributes = opts.resourceAttributes;
159
+ this.httpTimeoutMs = opts.httpTimeoutMs;
160
+ this.flushBatchSize = opts.flushBatchSize;
161
+ this.flushIntervalMs = opts.flushIntervalMs;
162
+ this.fetchImpl = opts.fetchImpl ?? fetch;
163
+ }
164
+ write(record) {
165
+ if (this.closed) return;
166
+ this.buffer.push(record);
167
+ if (this.buffer.length >= this.flushBatchSize) {
168
+ void this.flush();
169
+ } else {
170
+ this.armTimer();
171
+ }
172
+ }
173
+ async flush() {
174
+ await this.inFlight;
175
+ if (this.buffer.length === 0) return;
176
+ const drained = this.buffer.splice(0, this.buffer.length);
177
+ this.disarmTimer();
178
+ this.inFlight = this.post(drained).catch((error) => {
179
+ process.stderr.write(`[stackbone/sdk] PlatformLogger OTLP POST failed: ${error instanceof Error ? error.message : String(error)}
180
+ `);
181
+ });
182
+ await this.inFlight;
183
+ }
184
+ async close() {
185
+ this.closed = true;
186
+ await this.flush();
187
+ }
188
+ armTimer() {
189
+ if (this.timer) return;
190
+ this.timer = setTimeout(() => {
191
+ this.timer = null;
192
+ void this.flush();
193
+ }, this.flushIntervalMs);
194
+ this.timer.unref?.();
195
+ }
196
+ disarmTimer() {
197
+ if (this.timer) {
198
+ clearTimeout(this.timer);
199
+ this.timer = null;
200
+ }
201
+ }
202
+ async post(records) {
203
+ const body = JSON.stringify(buildOtlpLogsBody(records, this.resourceAttributes));
204
+ const controller = new AbortController();
205
+ const timeout = setTimeout(() => controller.abort(), this.httpTimeoutMs);
206
+ try {
207
+ const resp = await this.fetchImpl(this.url, {
208
+ method: "POST",
209
+ headers: {
210
+ "content-type": "application/json"
211
+ },
212
+ body,
213
+ signal: controller.signal
214
+ });
215
+ if (!resp.ok) {
216
+ const text = await resp.text().catch(() => "");
217
+ throw new Error(`HTTP ${resp.status}${text ? `: ${text.slice(0, 200)}` : ""}`);
218
+ }
219
+ } finally {
220
+ clearTimeout(timeout);
221
+ }
222
+ }
223
+ };
224
+ var SDK_SCOPE = {
225
+ name: "@stackbone/sdk",
226
+ version
227
+ };
228
+ function buildOtlpLogsBody(records, resourceAttributes) {
229
+ return {
230
+ resourceLogs: [
231
+ {
232
+ resource: {
233
+ attributes: toOtlpAttributes(resourceAttributes)
234
+ },
235
+ scopeLogs: [
236
+ {
237
+ scope: SDK_SCOPE,
238
+ logRecords: records.map(toOtlpLogRecord)
239
+ }
240
+ ]
241
+ }
242
+ ]
243
+ };
244
+ }
245
+ __name(buildOtlpLogsBody, "buildOtlpLogsBody");
246
+ function toOtlpLogRecord(record) {
247
+ const { level, time, msg, ...rest } = record;
248
+ return {
249
+ timeUnixNano: `${time * 1e6}`,
250
+ severityNumber: level,
251
+ severityText: severityText(level),
252
+ body: {
253
+ stringValue: msg
254
+ },
255
+ attributes: toOtlpAttributesFromUnknown(rest)
256
+ };
257
+ }
258
+ __name(toOtlpLogRecord, "toOtlpLogRecord");
259
+ var SEVERITY_TEXT_BY_LEVEL = Object.fromEntries(Object.entries(LOG_LEVELS).map(([name, value]) => [
260
+ value,
261
+ name.toUpperCase()
262
+ ]));
263
+ function severityText(level) {
264
+ return SEVERITY_TEXT_BY_LEVEL[level] ?? "INFO";
265
+ }
266
+ __name(severityText, "severityText");
267
+ function toOtlpAttributes(attrs) {
268
+ return Object.entries(attrs).map(([key, value]) => ({
269
+ key,
270
+ value: {
271
+ stringValue: value
272
+ }
273
+ }));
274
+ }
275
+ __name(toOtlpAttributes, "toOtlpAttributes");
276
+ function toOtlpAttributesFromUnknown(attrs) {
277
+ const out = [];
278
+ for (const [key, value] of Object.entries(attrs)) {
279
+ if (value === null || value === void 0) continue;
280
+ if (typeof value === "string") {
281
+ out.push({
282
+ key,
283
+ value: {
284
+ stringValue: value
285
+ }
286
+ });
287
+ } else if (typeof value === "number") {
288
+ if (Number.isInteger(value)) out.push({
289
+ key,
290
+ value: {
291
+ intValue: String(value)
292
+ }
293
+ });
294
+ else out.push({
295
+ key,
296
+ value: {
297
+ doubleValue: value
298
+ }
299
+ });
300
+ } else if (typeof value === "boolean") {
301
+ out.push({
302
+ key,
303
+ value: {
304
+ boolValue: value
305
+ }
306
+ });
307
+ } else {
308
+ out.push({
309
+ key,
310
+ value: {
311
+ stringValue: JSON.stringify(value)
312
+ }
313
+ });
314
+ }
315
+ }
316
+ return out;
317
+ }
318
+ __name(toOtlpAttributesFromUnknown, "toOtlpAttributesFromUnknown");
319
+ function defaultRunsDir() {
320
+ return resolve(homedir(), ".stackbone", "dev", "runs");
321
+ }
322
+ __name(defaultRunsDir, "defaultRunsDir");
323
+ var RUN_STEP_TYPES = [
324
+ "agent",
325
+ "llm_call",
326
+ "db_query",
327
+ "http_fetch",
328
+ "queue_publish",
329
+ "hitl_pause",
330
+ "tool_call",
331
+ "rag_query",
332
+ "storage_op"
333
+ ];
334
+ var STEP_TYPE_ATTRIBUTE = "stackbone.step.type";
335
+ var RUN_ID_ATTRIBUTE = "stackbone.run.id";
336
+ var NAME_PREFIX_MAP = [
337
+ [
338
+ /^stackbone\.ai\./,
339
+ "llm_call"
340
+ ],
341
+ [
342
+ /^stackbone\.db\./,
343
+ "db_query"
344
+ ],
345
+ [
346
+ /^stackbone\.http\./,
347
+ "http_fetch"
348
+ ],
349
+ [
350
+ /^stackbone\.queues\./,
351
+ "queue_publish"
352
+ ],
353
+ [
354
+ /^stackbone\.approval\./,
355
+ "hitl_pause"
356
+ ],
357
+ [
358
+ /^stackbone\.tool\./,
359
+ "tool_call"
360
+ ],
361
+ [
362
+ /^stackbone\.rag\./,
363
+ "rag_query"
364
+ ],
365
+ [
366
+ /^stackbone\.storage\./,
367
+ "storage_op"
368
+ ]
369
+ ];
370
+ var DEFAULT_BATCH_SIZE2 = 50;
371
+ var DEFAULT_INTERVAL_MS2 = 500;
372
+ var RunStepsSpanProcessor = class {
373
+ static {
374
+ __name(this, "RunStepsSpanProcessor");
375
+ }
376
+ buffer = [];
377
+ stepIdBySpanId = /* @__PURE__ */ new Map();
378
+ batchSize;
379
+ intervalMs;
380
+ resolveStepType;
381
+ newId;
382
+ connectionString;
383
+ ownsSql;
384
+ sql;
385
+ timer = null;
386
+ inFlight = null;
387
+ constructor(options = {}) {
388
+ this.batchSize = options.flushBatchSize ?? DEFAULT_BATCH_SIZE2;
389
+ this.intervalMs = options.flushIntervalMs ?? DEFAULT_INTERVAL_MS2;
390
+ this.resolveStepType = options.resolveStepType ?? defaultStepTypeResolver;
391
+ this.newId = options.newId ?? randomUUID;
392
+ this.connectionString = options.connectionString;
393
+ if (options.sql) {
394
+ this.sql = options.sql;
395
+ this.ownsSql = false;
396
+ } else {
397
+ this.sql = null;
398
+ this.ownsSql = true;
399
+ }
400
+ }
401
+ // OTel calls onStart for the parent before any child, and onEnd for
402
+ // children before the parent. Reserving the UUID up-front lets the child's
403
+ // onEnd resolve `parent_step_id` synchronously — the alternative (looking
404
+ // up the parent at write time) would race the buffer flush.
405
+ onStart(span) {
406
+ const spanId = span.spanContext().spanId;
407
+ if (!this.stepIdBySpanId.has(spanId)) {
408
+ this.stepIdBySpanId.set(spanId, this.newId());
409
+ }
410
+ }
411
+ onEnd(span) {
412
+ const row = this.buildRow(span);
413
+ if (!row) return;
414
+ this.buffer.push(row);
415
+ if (this.buffer.length >= this.batchSize) {
416
+ void this.flush();
417
+ } else {
418
+ this.armTimer();
419
+ }
420
+ }
421
+ async forceFlush() {
422
+ await this.flush();
423
+ }
424
+ async shutdown() {
425
+ this.disarmTimer();
426
+ await this.flush();
427
+ if (this.ownsSql && this.sql?.end) {
428
+ await this.sql.end().catch(() => void 0);
429
+ }
430
+ }
431
+ buildRow(span) {
432
+ const spanId = span.spanContext().spanId;
433
+ const stepId = this.stepIdBySpanId.get(spanId) ?? this.newId();
434
+ this.stepIdBySpanId.delete(spanId);
435
+ const runId = readString(span.attributes[RUN_ID_ATTRIBUTE]);
436
+ if (!runId) return null;
437
+ const parentSpanId = span.parentSpanContext?.spanId ?? span.parentSpanId ?? null;
438
+ const parentStepId = parentSpanId ? this.stepIdBySpanId.get(parentSpanId) ?? null : null;
439
+ const startedAt = formatHrTime(span.startTime);
440
+ const finishedAt = formatHrTime(span.endTime ?? span.startTime);
441
+ const durationMs = hrTimeToMs(span.duration ?? diffHrTime(span.endTime, span.startTime));
442
+ const isError = span.status?.code === 2;
443
+ return {
444
+ id: stepId,
445
+ run_id: runId,
446
+ parent_step_id: parentStepId,
447
+ type: this.resolveStepType(span),
448
+ name: span.name,
449
+ status: isError ? "error" : "ok",
450
+ payload: serialisablePayload(span.attributes),
451
+ error: isError ? {
452
+ message: span.status?.message ?? "span ended with ERROR status"
453
+ } : null,
454
+ started_at: startedAt,
455
+ finished_at: finishedAt,
456
+ duration_ms: durationMs
457
+ };
458
+ }
459
+ armTimer() {
460
+ if (this.timer) return;
461
+ this.timer = setTimeout(() => {
462
+ this.timer = null;
463
+ void this.flush();
464
+ }, this.intervalMs);
465
+ this.timer.unref?.();
466
+ }
467
+ disarmTimer() {
468
+ if (this.timer) {
469
+ clearTimeout(this.timer);
470
+ this.timer = null;
471
+ }
472
+ }
473
+ async flush() {
474
+ if (this.inFlight) {
475
+ await this.inFlight;
476
+ }
477
+ if (this.buffer.length === 0) return;
478
+ const drained = this.buffer.splice(0, this.buffer.length);
479
+ this.disarmTimer();
480
+ this.inFlight = this.writeBatch(drained).finally(() => {
481
+ this.inFlight = null;
482
+ });
483
+ await this.inFlight;
484
+ }
485
+ async ensureSql() {
486
+ if (this.sql) return this.sql;
487
+ if (!this.connectionString) return null;
488
+ const mod = await import('postgres');
489
+ this.sql = mod.default(this.connectionString);
490
+ return this.sql;
491
+ }
492
+ async writeBatch(rows) {
493
+ try {
494
+ const sql = await this.ensureSql();
495
+ if (!sql) {
496
+ process.stderr.write("[stackbone/sdk] RunStepsSpanProcessor dropped batch: no DATABASE_URL configured.\n");
497
+ return;
498
+ }
499
+ const placeholders = [];
500
+ const params = [];
501
+ for (const row of rows) {
502
+ const base = params.length;
503
+ placeholders.push(`($${base + 1}::uuid, $${base + 2}::uuid, $${base + 3}::uuid, $${base + 4}, $${base + 5}, $${base + 6}, $${base + 7}::jsonb, $${base + 8}::jsonb, $${base + 9}::timestamptz, $${base + 10}::timestamptz, $${base + 11}::int)`);
504
+ params.push(row.id, row.run_id, row.parent_step_id, row.type, row.name, row.status, JSON.stringify(row.payload), row.error ? JSON.stringify(row.error) : null, row.started_at, row.finished_at, row.duration_ms);
505
+ }
506
+ const text = `INSERT INTO stackbone_platform.run_steps (
507
+ id, run_id, parent_step_id, type, name, status, payload, error,
508
+ started_at, finished_at, duration_ms
509
+ ) VALUES ${placeholders.join(", ")}`;
510
+ await sql.unsafe(text, params);
511
+ } catch (error) {
512
+ process.stderr.write(`[stackbone/sdk] RunStepsSpanProcessor write failed: ${error instanceof Error ? error.message : String(error)}
513
+ `);
514
+ }
515
+ }
516
+ };
517
+ var defaultStepTypeResolver = /* @__PURE__ */ __name((span) => {
518
+ const explicit = readString(span.attributes[STEP_TYPE_ATTRIBUTE]);
519
+ if (explicit && RUN_STEP_TYPES.includes(explicit)) {
520
+ return explicit;
521
+ }
522
+ for (const [pattern, type] of NAME_PREFIX_MAP) {
523
+ if (pattern.test(span.name)) return type;
524
+ }
525
+ return "agent";
526
+ }, "defaultStepTypeResolver");
527
+ var readString = /* @__PURE__ */ __name((value) => typeof value === "string" && value.length > 0 ? value : void 0, "readString");
528
+ var formatHrTime = /* @__PURE__ */ __name((hr) => new Date(hr ? hrTimeToMs(hr) : Date.now()).toISOString(), "formatHrTime");
529
+ var hrTimeToMs = /* @__PURE__ */ __name((hr) => {
530
+ if (!hr) return 0;
531
+ const [seconds, nanos] = hr;
532
+ return seconds * 1e3 + Math.floor(nanos / 1e6);
533
+ }, "hrTimeToMs");
534
+ var diffHrTime = /* @__PURE__ */ __name((end, start) => {
535
+ if (!end || !start) return void 0;
536
+ const seconds = end[0] - start[0];
537
+ const nanos = end[1] - start[1];
538
+ return [
539
+ seconds,
540
+ nanos
541
+ ];
542
+ }, "diffHrTime");
543
+ var serialisablePayload = /* @__PURE__ */ __name((attributes) => {
544
+ const out = {};
545
+ for (const [key, value] of Object.entries(attributes)) {
546
+ if (key === RUN_ID_ATTRIBUTE) continue;
547
+ if (key === STEP_TYPE_ATTRIBUTE) continue;
548
+ out[key] = value;
549
+ }
550
+ return out;
551
+ }, "serialisablePayload");
552
+
553
+ // src/observability/aggregate-run-cost.ts
554
+ var SQL = `WITH llm_costs AS (
555
+ SELECT COALESCE(SUM((payload->>'cost_usd')::numeric), 0) AS total
556
+ FROM stackbone_platform.run_steps
557
+ WHERE run_id = $1::uuid
558
+ AND type = 'llm_call'
559
+ )
560
+ UPDATE stackbone_platform.runs
561
+ SET cost_estimated_usd = (SELECT total FROM llm_costs)
562
+ WHERE id = $1::uuid
563
+ RETURNING cost_estimated_usd`;
564
+ async function aggregateRunCost(runId, options) {
565
+ const result = await options.sql.unsafe(SQL, [
566
+ runId
567
+ ]);
568
+ const row = result[0];
569
+ if (!row) {
570
+ return {
571
+ costEstimatedUsd: 0
572
+ };
573
+ }
574
+ const raw = row.cost_estimated_usd;
575
+ if (raw === null || raw === void 0) return {
576
+ costEstimatedUsd: 0
577
+ };
578
+ const num = typeof raw === "string" ? Number(raw) : raw;
579
+ return {
580
+ costEstimatedUsd: Number.isFinite(num) ? num : 0
581
+ };
582
+ }
583
+ __name(aggregateRunCost, "aggregateRunCost");
584
+
585
+ export { LOG_LEVELS, RUN_ID_ATTRIBUTE, RUN_STEP_TYPES, RunStepsSpanProcessor, STEP_TYPE_ATTRIBUTE, aggregateRunCost, createPlatformLogger, defaultRunsDir };
586
+ //# sourceMappingURL=index.js.map
587
+ //# sourceMappingURL=index.js.map