@orka-js/devtools 1.1.0 → 1.2.1
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 +362 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -1
- package/dist/index.d.ts +129 -1
- package/dist/index.js +357 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -709,6 +709,358 @@ function createTracerWithDevTools(options = {}) {
|
|
|
709
709
|
};
|
|
710
710
|
}
|
|
711
711
|
|
|
712
|
+
// src/opentelemetry.ts
|
|
713
|
+
var OpenTelemetryExporter = class {
|
|
714
|
+
config;
|
|
715
|
+
spanBuffer = [];
|
|
716
|
+
flushTimer;
|
|
717
|
+
unsubscribe;
|
|
718
|
+
constructor(config) {
|
|
719
|
+
this.config = {
|
|
720
|
+
endpoint: config.endpoint,
|
|
721
|
+
serviceName: config.serviceName ?? "orkajs-app",
|
|
722
|
+
serviceVersion: config.serviceVersion ?? "1.0.0",
|
|
723
|
+
headers: config.headers ?? {},
|
|
724
|
+
batchSize: config.batchSize ?? 100,
|
|
725
|
+
flushIntervalMs: config.flushIntervalMs ?? 5e3,
|
|
726
|
+
enabled: config.enabled ?? true
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Start the exporter - subscribes to trace events
|
|
731
|
+
*/
|
|
732
|
+
start() {
|
|
733
|
+
if (!this.config.enabled) return;
|
|
734
|
+
const collector = getCollector();
|
|
735
|
+
this.unsubscribe = collector.subscribe((event) => {
|
|
736
|
+
if (event.type === "run:end" && event.run) {
|
|
737
|
+
this.addSpan(event.run, event.sessionId);
|
|
738
|
+
}
|
|
739
|
+
});
|
|
740
|
+
this.flushTimer = setInterval(() => {
|
|
741
|
+
this.flush();
|
|
742
|
+
}, this.config.flushIntervalMs);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Stop the exporter
|
|
746
|
+
*/
|
|
747
|
+
async stop() {
|
|
748
|
+
if (this.flushTimer) {
|
|
749
|
+
clearInterval(this.flushTimer);
|
|
750
|
+
this.flushTimer = void 0;
|
|
751
|
+
}
|
|
752
|
+
if (this.unsubscribe) {
|
|
753
|
+
this.unsubscribe();
|
|
754
|
+
this.unsubscribe = void 0;
|
|
755
|
+
}
|
|
756
|
+
await this.flush();
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* Convert a TraceRun to OTLP span
|
|
760
|
+
*/
|
|
761
|
+
addSpan(run, traceId) {
|
|
762
|
+
const span = this.runToSpan(run, traceId);
|
|
763
|
+
this.spanBuffer.push(span);
|
|
764
|
+
for (const child of run.children) {
|
|
765
|
+
this.addSpan(child, traceId);
|
|
766
|
+
}
|
|
767
|
+
if (this.spanBuffer.length >= this.config.batchSize) {
|
|
768
|
+
this.flush();
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Convert TraceRun to OTLP Span format
|
|
773
|
+
*/
|
|
774
|
+
runToSpan(run, traceId) {
|
|
775
|
+
const attributes = [
|
|
776
|
+
{ key: "orka.run.type", value: { stringValue: run.type } },
|
|
777
|
+
{ key: "orka.run.name", value: { stringValue: run.name } },
|
|
778
|
+
{ key: "orka.run.status", value: { stringValue: run.status } }
|
|
779
|
+
];
|
|
780
|
+
if (run.metadata) {
|
|
781
|
+
if (run.metadata.model) {
|
|
782
|
+
attributes.push({ key: "llm.model", value: { stringValue: run.metadata.model } });
|
|
783
|
+
}
|
|
784
|
+
if (run.metadata.provider) {
|
|
785
|
+
attributes.push({ key: "llm.provider", value: { stringValue: run.metadata.provider } });
|
|
786
|
+
}
|
|
787
|
+
if (run.metadata.totalTokens !== void 0) {
|
|
788
|
+
attributes.push({ key: "llm.tokens.total", value: { intValue: String(run.metadata.totalTokens) } });
|
|
789
|
+
}
|
|
790
|
+
if (run.metadata.promptTokens !== void 0) {
|
|
791
|
+
attributes.push({ key: "llm.tokens.prompt", value: { intValue: String(run.metadata.promptTokens) } });
|
|
792
|
+
}
|
|
793
|
+
if (run.metadata.completionTokens !== void 0) {
|
|
794
|
+
attributes.push({ key: "llm.tokens.completion", value: { intValue: String(run.metadata.completionTokens) } });
|
|
795
|
+
}
|
|
796
|
+
if (run.metadata.cost !== void 0) {
|
|
797
|
+
attributes.push({ key: "llm.cost", value: { doubleValue: run.metadata.cost } });
|
|
798
|
+
}
|
|
799
|
+
if (run.metadata.toolName) {
|
|
800
|
+
attributes.push({ key: "tool.name", value: { stringValue: run.metadata.toolName } });
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (run.latencyMs !== void 0) {
|
|
804
|
+
attributes.push({ key: "orka.latency_ms", value: { intValue: String(run.latencyMs) } });
|
|
805
|
+
}
|
|
806
|
+
if (run.error) {
|
|
807
|
+
attributes.push({ key: "error.message", value: { stringValue: run.error } });
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
traceId: this.toHex(traceId, 32),
|
|
811
|
+
spanId: this.toHex(run.id, 16),
|
|
812
|
+
parentSpanId: run.parentId ? this.toHex(run.parentId, 16) : void 0,
|
|
813
|
+
name: `${run.type}/${run.name}`,
|
|
814
|
+
kind: this.getSpanKind(run.type),
|
|
815
|
+
startTimeUnixNano: String(run.startTime * 1e6),
|
|
816
|
+
endTimeUnixNano: String((run.endTime ?? run.startTime) * 1e6),
|
|
817
|
+
attributes,
|
|
818
|
+
status: {
|
|
819
|
+
code: run.status === "error" ? 2 : 1,
|
|
820
|
+
message: run.error
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
/**
|
|
825
|
+
* Get OTLP span kind based on run type
|
|
826
|
+
*/
|
|
827
|
+
getSpanKind(type) {
|
|
828
|
+
switch (type) {
|
|
829
|
+
case "llm":
|
|
830
|
+
case "embedding":
|
|
831
|
+
return 3;
|
|
832
|
+
// CLIENT
|
|
833
|
+
case "tool":
|
|
834
|
+
return 3;
|
|
835
|
+
// CLIENT
|
|
836
|
+
case "agent":
|
|
837
|
+
case "chain":
|
|
838
|
+
case "workflow":
|
|
839
|
+
case "graph":
|
|
840
|
+
return 0;
|
|
841
|
+
// INTERNAL
|
|
842
|
+
default:
|
|
843
|
+
return 0;
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Convert string ID to hex format
|
|
848
|
+
*/
|
|
849
|
+
toHex(id, length) {
|
|
850
|
+
let hash = 0;
|
|
851
|
+
for (let i = 0; i < id.length; i++) {
|
|
852
|
+
const char = id.charCodeAt(i);
|
|
853
|
+
hash = (hash << 5) - hash + char;
|
|
854
|
+
hash = hash & hash;
|
|
855
|
+
}
|
|
856
|
+
const hex = Math.abs(hash).toString(16).padStart(length, "0");
|
|
857
|
+
return hex.slice(0, length);
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Flush spans to OTLP endpoint
|
|
861
|
+
*/
|
|
862
|
+
async flush() {
|
|
863
|
+
if (this.spanBuffer.length === 0) return;
|
|
864
|
+
const spans = [...this.spanBuffer];
|
|
865
|
+
this.spanBuffer = [];
|
|
866
|
+
const payload = {
|
|
867
|
+
resourceSpans: [{
|
|
868
|
+
resource: {
|
|
869
|
+
attributes: [
|
|
870
|
+
{ key: "service.name", value: { stringValue: this.config.serviceName } },
|
|
871
|
+
{ key: "service.version", value: { stringValue: this.config.serviceVersion } },
|
|
872
|
+
{ key: "telemetry.sdk.name", value: { stringValue: "@orka-js/devtools" } },
|
|
873
|
+
{ key: "telemetry.sdk.version", value: { stringValue: "1.1.0" } }
|
|
874
|
+
]
|
|
875
|
+
},
|
|
876
|
+
scopeSpans: [{
|
|
877
|
+
scope: {
|
|
878
|
+
name: "@orka-js/devtools",
|
|
879
|
+
version: "1.1.0"
|
|
880
|
+
},
|
|
881
|
+
spans
|
|
882
|
+
}]
|
|
883
|
+
}]
|
|
884
|
+
};
|
|
885
|
+
try {
|
|
886
|
+
const response = await fetch(`${this.config.endpoint}/v1/traces`, {
|
|
887
|
+
method: "POST",
|
|
888
|
+
headers: {
|
|
889
|
+
"Content-Type": "application/json",
|
|
890
|
+
...this.config.headers
|
|
891
|
+
},
|
|
892
|
+
body: JSON.stringify(payload)
|
|
893
|
+
});
|
|
894
|
+
if (!response.ok) {
|
|
895
|
+
console.error(`[DevTools] OTLP export failed: ${response.status} ${response.statusText}`);
|
|
896
|
+
this.spanBuffer.unshift(...spans);
|
|
897
|
+
}
|
|
898
|
+
} catch (error) {
|
|
899
|
+
console.error("[DevTools] OTLP export error:", error);
|
|
900
|
+
this.spanBuffer.unshift(...spans);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
function createOTLPExporter(config) {
|
|
905
|
+
const exporter = new OpenTelemetryExporter(config);
|
|
906
|
+
exporter.start();
|
|
907
|
+
return exporter;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// src/replay.ts
|
|
911
|
+
var ReplayDebugger = class {
|
|
912
|
+
/**
|
|
913
|
+
* Replay a trace run with optionally modified input
|
|
914
|
+
*/
|
|
915
|
+
async replay(options) {
|
|
916
|
+
const collector = getCollector();
|
|
917
|
+
const originalRun = collector.findRun(options.runId, options.sessionId);
|
|
918
|
+
if (!originalRun) {
|
|
919
|
+
throw new Error(`Run not found: ${options.runId}`);
|
|
920
|
+
}
|
|
921
|
+
const modifiedInput = options.modifyInput ? options.modifyInput(originalRun.input) : originalRun.input;
|
|
922
|
+
const replaySessionId = collector.startSession(`Replay: ${originalRun.name}`);
|
|
923
|
+
const replayRunId = collector.startRun(
|
|
924
|
+
originalRun.type,
|
|
925
|
+
`replay:${originalRun.name}`,
|
|
926
|
+
modifiedInput,
|
|
927
|
+
{
|
|
928
|
+
...originalRun.metadata,
|
|
929
|
+
replayOf: originalRun.id,
|
|
930
|
+
originalSessionId: options.sessionId
|
|
931
|
+
}
|
|
932
|
+
);
|
|
933
|
+
const replayedOutput = originalRun.output;
|
|
934
|
+
collector.endRun(replayRunId, replayedOutput, originalRun.metadata);
|
|
935
|
+
collector.endSession(replaySessionId);
|
|
936
|
+
const replayedRun = collector.findRun(replayRunId);
|
|
937
|
+
if (!replayedRun) {
|
|
938
|
+
throw new Error("Failed to create replayed run");
|
|
939
|
+
}
|
|
940
|
+
const inputChanged = JSON.stringify(originalRun.input) !== JSON.stringify(modifiedInput);
|
|
941
|
+
const outputChanged = JSON.stringify(originalRun.output) !== JSON.stringify(replayedRun.output);
|
|
942
|
+
const latencyDiff = (replayedRun.latencyMs ?? 0) - (originalRun.latencyMs ?? 0);
|
|
943
|
+
return {
|
|
944
|
+
originalRun,
|
|
945
|
+
replayedRun,
|
|
946
|
+
diff: {
|
|
947
|
+
inputChanged,
|
|
948
|
+
outputChanged,
|
|
949
|
+
latencyDiff
|
|
950
|
+
}
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
/**
|
|
954
|
+
* Fork a trace to create a new branch for experimentation
|
|
955
|
+
*/
|
|
956
|
+
fork(runId, sessionId) {
|
|
957
|
+
const collector = getCollector();
|
|
958
|
+
const originalRun = collector.findRun(runId, sessionId);
|
|
959
|
+
if (!originalRun) {
|
|
960
|
+
throw new Error(`Run not found: ${runId}`);
|
|
961
|
+
}
|
|
962
|
+
const forkSessionId = collector.startSession(`Fork: ${originalRun.name}`);
|
|
963
|
+
this.cloneRunTree(originalRun, forkSessionId);
|
|
964
|
+
collector.endSession(forkSessionId);
|
|
965
|
+
return forkSessionId;
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Clone a run and its children
|
|
969
|
+
*/
|
|
970
|
+
cloneRunTree(run, _sessionId, parentId) {
|
|
971
|
+
const collector = getCollector();
|
|
972
|
+
const newRunId = collector.startRun(
|
|
973
|
+
run.type,
|
|
974
|
+
`fork:${run.name}`,
|
|
975
|
+
run.input,
|
|
976
|
+
{
|
|
977
|
+
...run.metadata,
|
|
978
|
+
forkedFrom: run.id,
|
|
979
|
+
originalParentId: parentId
|
|
980
|
+
}
|
|
981
|
+
);
|
|
982
|
+
for (const child of run.children) {
|
|
983
|
+
this.cloneRunTree(child, _sessionId, newRunId);
|
|
984
|
+
}
|
|
985
|
+
if (run.status === "error") {
|
|
986
|
+
collector.errorRun(newRunId, run.error ?? "Unknown error");
|
|
987
|
+
} else {
|
|
988
|
+
collector.endRun(newRunId, run.output, run.metadata);
|
|
989
|
+
}
|
|
990
|
+
return newRunId;
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Compare two runs and return detailed diff
|
|
994
|
+
*/
|
|
995
|
+
compare(runId1, runId2, sessionId) {
|
|
996
|
+
const collector = getCollector();
|
|
997
|
+
const run1 = collector.findRun(runId1, sessionId);
|
|
998
|
+
const run2 = collector.findRun(runId2, sessionId);
|
|
999
|
+
if (!run1 || !run2) {
|
|
1000
|
+
throw new Error("One or both runs not found");
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
run1: {
|
|
1004
|
+
id: run1.id,
|
|
1005
|
+
name: run1.name,
|
|
1006
|
+
type: run1.type,
|
|
1007
|
+
latencyMs: run1.latencyMs,
|
|
1008
|
+
status: run1.status,
|
|
1009
|
+
tokenCount: run1.metadata?.totalTokens
|
|
1010
|
+
},
|
|
1011
|
+
run2: {
|
|
1012
|
+
id: run2.id,
|
|
1013
|
+
name: run2.name,
|
|
1014
|
+
type: run2.type,
|
|
1015
|
+
latencyMs: run2.latencyMs,
|
|
1016
|
+
status: run2.status,
|
|
1017
|
+
tokenCount: run2.metadata?.totalTokens
|
|
1018
|
+
},
|
|
1019
|
+
diff: {
|
|
1020
|
+
latencyDiff: (run2.latencyMs ?? 0) - (run1.latencyMs ?? 0),
|
|
1021
|
+
latencyDiffPercent: run1.latencyMs ? ((run2.latencyMs ?? 0) - run1.latencyMs) / run1.latencyMs * 100 : 0,
|
|
1022
|
+
tokenDiff: (run2.metadata?.totalTokens ?? 0) - (run1.metadata?.totalTokens ?? 0),
|
|
1023
|
+
statusChanged: run1.status !== run2.status,
|
|
1024
|
+
inputChanged: JSON.stringify(run1.input) !== JSON.stringify(run2.input),
|
|
1025
|
+
outputChanged: JSON.stringify(run1.output) !== JSON.stringify(run2.output)
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Export a run as a reproducible test case
|
|
1031
|
+
*/
|
|
1032
|
+
exportTestCase(runId, sessionId) {
|
|
1033
|
+
const collector = getCollector();
|
|
1034
|
+
const run = collector.findRun(runId, sessionId);
|
|
1035
|
+
if (!run) {
|
|
1036
|
+
throw new Error(`Run not found: ${runId}`);
|
|
1037
|
+
}
|
|
1038
|
+
return {
|
|
1039
|
+
name: `test_${run.name}_${run.id.slice(0, 8)}`,
|
|
1040
|
+
description: `Exported from DevTools trace ${run.id}`,
|
|
1041
|
+
type: run.type,
|
|
1042
|
+
input: run.input,
|
|
1043
|
+
expectedOutput: run.output,
|
|
1044
|
+
metadata: run.metadata,
|
|
1045
|
+
assertions: [
|
|
1046
|
+
{ type: "status", expected: run.status },
|
|
1047
|
+
{ type: "latency_max", expected: (run.latencyMs ?? 0) * 1.5 }
|
|
1048
|
+
],
|
|
1049
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
function createReplayDebugger() {
|
|
1054
|
+
return new ReplayDebugger();
|
|
1055
|
+
}
|
|
1056
|
+
var replayDebugger;
|
|
1057
|
+
function getReplayDebugger() {
|
|
1058
|
+
if (!replayDebugger) {
|
|
1059
|
+
replayDebugger = new ReplayDebugger();
|
|
1060
|
+
}
|
|
1061
|
+
return replayDebugger;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
712
1064
|
// src/index.ts
|
|
713
1065
|
async function devtools(config = {}) {
|
|
714
1066
|
const collector = getCollector(config);
|
|
@@ -798,12 +1150,17 @@ var trace = {
|
|
|
798
1150
|
};
|
|
799
1151
|
export {
|
|
800
1152
|
DevToolsServer,
|
|
1153
|
+
OpenTelemetryExporter,
|
|
1154
|
+
ReplayDebugger,
|
|
801
1155
|
Trace,
|
|
802
1156
|
TraceCollector,
|
|
803
1157
|
createDevToolsHook,
|
|
1158
|
+
createOTLPExporter,
|
|
1159
|
+
createReplayDebugger,
|
|
804
1160
|
createTracerWithDevTools,
|
|
805
1161
|
devtools,
|
|
806
1162
|
getCollector,
|
|
1163
|
+
getReplayDebugger,
|
|
807
1164
|
resetCollector,
|
|
808
1165
|
trace,
|
|
809
1166
|
withTrace
|