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