@orka-js/devtools 1.1.0 → 1.2.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/dist/index.cjs CHANGED
@@ -31,12 +31,17 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
31
31
  var src_exports = {};
32
32
  __export(src_exports, {
33
33
  DevToolsServer: () => DevToolsServer,
34
+ OpenTelemetryExporter: () => OpenTelemetryExporter,
35
+ ReplayDebugger: () => ReplayDebugger,
34
36
  Trace: () => Trace,
35
37
  TraceCollector: () => TraceCollector,
36
38
  createDevToolsHook: () => createDevToolsHook,
39
+ createOTLPExporter: () => createOTLPExporter,
40
+ createReplayDebugger: () => createReplayDebugger,
37
41
  createTracerWithDevTools: () => createTracerWithDevTools,
38
42
  devtools: () => devtools,
39
43
  getCollector: () => getCollector,
44
+ getReplayDebugger: () => getReplayDebugger,
40
45
  resetCollector: () => resetCollector,
41
46
  trace: () => trace,
42
47
  withTrace: () => withTrace
@@ -754,6 +759,358 @@ function createTracerWithDevTools(options = {}) {
754
759
  };
755
760
  }
756
761
 
762
+ // src/opentelemetry.ts
763
+ var OpenTelemetryExporter = class {
764
+ config;
765
+ spanBuffer = [];
766
+ flushTimer;
767
+ unsubscribe;
768
+ constructor(config) {
769
+ this.config = {
770
+ endpoint: config.endpoint,
771
+ serviceName: config.serviceName ?? "orkajs-app",
772
+ serviceVersion: config.serviceVersion ?? "1.0.0",
773
+ headers: config.headers ?? {},
774
+ batchSize: config.batchSize ?? 100,
775
+ flushIntervalMs: config.flushIntervalMs ?? 5e3,
776
+ enabled: config.enabled ?? true
777
+ };
778
+ }
779
+ /**
780
+ * Start the exporter - subscribes to trace events
781
+ */
782
+ start() {
783
+ if (!this.config.enabled) return;
784
+ const collector = getCollector();
785
+ this.unsubscribe = collector.subscribe((event) => {
786
+ if (event.type === "run:end" && event.run) {
787
+ this.addSpan(event.run, event.sessionId);
788
+ }
789
+ });
790
+ this.flushTimer = setInterval(() => {
791
+ this.flush();
792
+ }, this.config.flushIntervalMs);
793
+ }
794
+ /**
795
+ * Stop the exporter
796
+ */
797
+ async stop() {
798
+ if (this.flushTimer) {
799
+ clearInterval(this.flushTimer);
800
+ this.flushTimer = void 0;
801
+ }
802
+ if (this.unsubscribe) {
803
+ this.unsubscribe();
804
+ this.unsubscribe = void 0;
805
+ }
806
+ await this.flush();
807
+ }
808
+ /**
809
+ * Convert a TraceRun to OTLP span
810
+ */
811
+ addSpan(run, traceId) {
812
+ const span = this.runToSpan(run, traceId);
813
+ this.spanBuffer.push(span);
814
+ for (const child of run.children) {
815
+ this.addSpan(child, traceId);
816
+ }
817
+ if (this.spanBuffer.length >= this.config.batchSize) {
818
+ this.flush();
819
+ }
820
+ }
821
+ /**
822
+ * Convert TraceRun to OTLP Span format
823
+ */
824
+ runToSpan(run, traceId) {
825
+ const attributes = [
826
+ { key: "orka.run.type", value: { stringValue: run.type } },
827
+ { key: "orka.run.name", value: { stringValue: run.name } },
828
+ { key: "orka.run.status", value: { stringValue: run.status } }
829
+ ];
830
+ if (run.metadata) {
831
+ if (run.metadata.model) {
832
+ attributes.push({ key: "llm.model", value: { stringValue: run.metadata.model } });
833
+ }
834
+ if (run.metadata.provider) {
835
+ attributes.push({ key: "llm.provider", value: { stringValue: run.metadata.provider } });
836
+ }
837
+ if (run.metadata.totalTokens !== void 0) {
838
+ attributes.push({ key: "llm.tokens.total", value: { intValue: String(run.metadata.totalTokens) } });
839
+ }
840
+ if (run.metadata.promptTokens !== void 0) {
841
+ attributes.push({ key: "llm.tokens.prompt", value: { intValue: String(run.metadata.promptTokens) } });
842
+ }
843
+ if (run.metadata.completionTokens !== void 0) {
844
+ attributes.push({ key: "llm.tokens.completion", value: { intValue: String(run.metadata.completionTokens) } });
845
+ }
846
+ if (run.metadata.cost !== void 0) {
847
+ attributes.push({ key: "llm.cost", value: { doubleValue: run.metadata.cost } });
848
+ }
849
+ if (run.metadata.toolName) {
850
+ attributes.push({ key: "tool.name", value: { stringValue: run.metadata.toolName } });
851
+ }
852
+ }
853
+ if (run.latencyMs !== void 0) {
854
+ attributes.push({ key: "orka.latency_ms", value: { intValue: String(run.latencyMs) } });
855
+ }
856
+ if (run.error) {
857
+ attributes.push({ key: "error.message", value: { stringValue: run.error } });
858
+ }
859
+ return {
860
+ traceId: this.toHex(traceId, 32),
861
+ spanId: this.toHex(run.id, 16),
862
+ parentSpanId: run.parentId ? this.toHex(run.parentId, 16) : void 0,
863
+ name: `${run.type}/${run.name}`,
864
+ kind: this.getSpanKind(run.type),
865
+ startTimeUnixNano: String(run.startTime * 1e6),
866
+ endTimeUnixNano: String((run.endTime ?? run.startTime) * 1e6),
867
+ attributes,
868
+ status: {
869
+ code: run.status === "error" ? 2 : 1,
870
+ message: run.error
871
+ }
872
+ };
873
+ }
874
+ /**
875
+ * Get OTLP span kind based on run type
876
+ */
877
+ getSpanKind(type) {
878
+ switch (type) {
879
+ case "llm":
880
+ case "embedding":
881
+ return 3;
882
+ // CLIENT
883
+ case "tool":
884
+ return 3;
885
+ // CLIENT
886
+ case "agent":
887
+ case "chain":
888
+ case "workflow":
889
+ case "graph":
890
+ return 0;
891
+ // INTERNAL
892
+ default:
893
+ return 0;
894
+ }
895
+ }
896
+ /**
897
+ * Convert string ID to hex format
898
+ */
899
+ toHex(id, length) {
900
+ let hash = 0;
901
+ for (let i = 0; i < id.length; i++) {
902
+ const char = id.charCodeAt(i);
903
+ hash = (hash << 5) - hash + char;
904
+ hash = hash & hash;
905
+ }
906
+ const hex = Math.abs(hash).toString(16).padStart(length, "0");
907
+ return hex.slice(0, length);
908
+ }
909
+ /**
910
+ * Flush spans to OTLP endpoint
911
+ */
912
+ async flush() {
913
+ if (this.spanBuffer.length === 0) return;
914
+ const spans = [...this.spanBuffer];
915
+ this.spanBuffer = [];
916
+ const payload = {
917
+ resourceSpans: [{
918
+ resource: {
919
+ attributes: [
920
+ { key: "service.name", value: { stringValue: this.config.serviceName } },
921
+ { key: "service.version", value: { stringValue: this.config.serviceVersion } },
922
+ { key: "telemetry.sdk.name", value: { stringValue: "@orka-js/devtools" } },
923
+ { key: "telemetry.sdk.version", value: { stringValue: "1.1.0" } }
924
+ ]
925
+ },
926
+ scopeSpans: [{
927
+ scope: {
928
+ name: "@orka-js/devtools",
929
+ version: "1.1.0"
930
+ },
931
+ spans
932
+ }]
933
+ }]
934
+ };
935
+ try {
936
+ const response = await fetch(`${this.config.endpoint}/v1/traces`, {
937
+ method: "POST",
938
+ headers: {
939
+ "Content-Type": "application/json",
940
+ ...this.config.headers
941
+ },
942
+ body: JSON.stringify(payload)
943
+ });
944
+ if (!response.ok) {
945
+ console.error(`[DevTools] OTLP export failed: ${response.status} ${response.statusText}`);
946
+ this.spanBuffer.unshift(...spans);
947
+ }
948
+ } catch (error) {
949
+ console.error("[DevTools] OTLP export error:", error);
950
+ this.spanBuffer.unshift(...spans);
951
+ }
952
+ }
953
+ };
954
+ function createOTLPExporter(config) {
955
+ const exporter = new OpenTelemetryExporter(config);
956
+ exporter.start();
957
+ return exporter;
958
+ }
959
+
960
+ // src/replay.ts
961
+ var ReplayDebugger = class {
962
+ /**
963
+ * Replay a trace run with optionally modified input
964
+ */
965
+ async replay(options) {
966
+ const collector = getCollector();
967
+ const originalRun = collector.findRun(options.runId, options.sessionId);
968
+ if (!originalRun) {
969
+ throw new Error(`Run not found: ${options.runId}`);
970
+ }
971
+ const modifiedInput = options.modifyInput ? options.modifyInput(originalRun.input) : originalRun.input;
972
+ const replaySessionId = collector.startSession(`Replay: ${originalRun.name}`);
973
+ const replayRunId = collector.startRun(
974
+ originalRun.type,
975
+ `replay:${originalRun.name}`,
976
+ modifiedInput,
977
+ {
978
+ ...originalRun.metadata,
979
+ replayOf: originalRun.id,
980
+ originalSessionId: options.sessionId
981
+ }
982
+ );
983
+ const replayedOutput = originalRun.output;
984
+ collector.endRun(replayRunId, replayedOutput, originalRun.metadata);
985
+ collector.endSession(replaySessionId);
986
+ const replayedRun = collector.findRun(replayRunId);
987
+ if (!replayedRun) {
988
+ throw new Error("Failed to create replayed run");
989
+ }
990
+ const inputChanged = JSON.stringify(originalRun.input) !== JSON.stringify(modifiedInput);
991
+ const outputChanged = JSON.stringify(originalRun.output) !== JSON.stringify(replayedRun.output);
992
+ const latencyDiff = (replayedRun.latencyMs ?? 0) - (originalRun.latencyMs ?? 0);
993
+ return {
994
+ originalRun,
995
+ replayedRun,
996
+ diff: {
997
+ inputChanged,
998
+ outputChanged,
999
+ latencyDiff
1000
+ }
1001
+ };
1002
+ }
1003
+ /**
1004
+ * Fork a trace to create a new branch for experimentation
1005
+ */
1006
+ fork(runId, sessionId) {
1007
+ const collector = getCollector();
1008
+ const originalRun = collector.findRun(runId, sessionId);
1009
+ if (!originalRun) {
1010
+ throw new Error(`Run not found: ${runId}`);
1011
+ }
1012
+ const forkSessionId = collector.startSession(`Fork: ${originalRun.name}`);
1013
+ this.cloneRunTree(originalRun, forkSessionId);
1014
+ collector.endSession(forkSessionId);
1015
+ return forkSessionId;
1016
+ }
1017
+ /**
1018
+ * Clone a run and its children
1019
+ */
1020
+ cloneRunTree(run, _sessionId, parentId) {
1021
+ const collector = getCollector();
1022
+ const newRunId = collector.startRun(
1023
+ run.type,
1024
+ `fork:${run.name}`,
1025
+ run.input,
1026
+ {
1027
+ ...run.metadata,
1028
+ forkedFrom: run.id,
1029
+ originalParentId: parentId
1030
+ }
1031
+ );
1032
+ for (const child of run.children) {
1033
+ this.cloneRunTree(child, _sessionId, newRunId);
1034
+ }
1035
+ if (run.status === "error") {
1036
+ collector.errorRun(newRunId, run.error ?? "Unknown error");
1037
+ } else {
1038
+ collector.endRun(newRunId, run.output, run.metadata);
1039
+ }
1040
+ return newRunId;
1041
+ }
1042
+ /**
1043
+ * Compare two runs and return detailed diff
1044
+ */
1045
+ compare(runId1, runId2, sessionId) {
1046
+ const collector = getCollector();
1047
+ const run1 = collector.findRun(runId1, sessionId);
1048
+ const run2 = collector.findRun(runId2, sessionId);
1049
+ if (!run1 || !run2) {
1050
+ throw new Error("One or both runs not found");
1051
+ }
1052
+ return {
1053
+ run1: {
1054
+ id: run1.id,
1055
+ name: run1.name,
1056
+ type: run1.type,
1057
+ latencyMs: run1.latencyMs,
1058
+ status: run1.status,
1059
+ tokenCount: run1.metadata?.totalTokens
1060
+ },
1061
+ run2: {
1062
+ id: run2.id,
1063
+ name: run2.name,
1064
+ type: run2.type,
1065
+ latencyMs: run2.latencyMs,
1066
+ status: run2.status,
1067
+ tokenCount: run2.metadata?.totalTokens
1068
+ },
1069
+ diff: {
1070
+ latencyDiff: (run2.latencyMs ?? 0) - (run1.latencyMs ?? 0),
1071
+ latencyDiffPercent: run1.latencyMs ? ((run2.latencyMs ?? 0) - run1.latencyMs) / run1.latencyMs * 100 : 0,
1072
+ tokenDiff: (run2.metadata?.totalTokens ?? 0) - (run1.metadata?.totalTokens ?? 0),
1073
+ statusChanged: run1.status !== run2.status,
1074
+ inputChanged: JSON.stringify(run1.input) !== JSON.stringify(run2.input),
1075
+ outputChanged: JSON.stringify(run1.output) !== JSON.stringify(run2.output)
1076
+ }
1077
+ };
1078
+ }
1079
+ /**
1080
+ * Export a run as a reproducible test case
1081
+ */
1082
+ exportTestCase(runId, sessionId) {
1083
+ const collector = getCollector();
1084
+ const run = collector.findRun(runId, sessionId);
1085
+ if (!run) {
1086
+ throw new Error(`Run not found: ${runId}`);
1087
+ }
1088
+ return {
1089
+ name: `test_${run.name}_${run.id.slice(0, 8)}`,
1090
+ description: `Exported from DevTools trace ${run.id}`,
1091
+ type: run.type,
1092
+ input: run.input,
1093
+ expectedOutput: run.output,
1094
+ metadata: run.metadata,
1095
+ assertions: [
1096
+ { type: "status", expected: run.status },
1097
+ { type: "latency_max", expected: (run.latencyMs ?? 0) * 1.5 }
1098
+ ],
1099
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
1100
+ };
1101
+ }
1102
+ };
1103
+ function createReplayDebugger() {
1104
+ return new ReplayDebugger();
1105
+ }
1106
+ var replayDebugger;
1107
+ function getReplayDebugger() {
1108
+ if (!replayDebugger) {
1109
+ replayDebugger = new ReplayDebugger();
1110
+ }
1111
+ return replayDebugger;
1112
+ }
1113
+
757
1114
  // src/index.ts
758
1115
  async function devtools(config = {}) {
759
1116
  const collector = getCollector(config);
@@ -844,12 +1201,17 @@ var trace = {
844
1201
  // Annotate the CommonJS export names for ESM import in node:
845
1202
  0 && (module.exports = {
846
1203
  DevToolsServer,
1204
+ OpenTelemetryExporter,
1205
+ ReplayDebugger,
847
1206
  Trace,
848
1207
  TraceCollector,
849
1208
  createDevToolsHook,
1209
+ createOTLPExporter,
1210
+ createReplayDebugger,
850
1211
  createTracerWithDevTools,
851
1212
  devtools,
852
1213
  getCollector,
1214
+ getReplayDebugger,
853
1215
  resetCollector,
854
1216
  trace,
855
1217
  withTrace