@cesarus777/opencode-cgp 0.1.0 → 0.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/core.js CHANGED
@@ -46,7 +46,16 @@ function threshold(v, path) {
46
46
  }
47
47
  function parseConfig(value = {}) {
48
48
  const o = object(value, "options");
49
- keys(o, ["enabled", "mode", "defaults", "policies", "safety", "telemetry"], "options");
49
+ keys(o, [
50
+ "enabled",
51
+ "mode",
52
+ "defaults",
53
+ "policies",
54
+ "safety",
55
+ "telemetry",
56
+ "metrics",
57
+ "history"
58
+ ], "options");
50
59
  if (o.mode !== undefined && o.mode !== "observe" && o.mode !== "enforce")
51
60
  throw new Error("mode: expected observe or enforce");
52
61
  const d = object(o.defaults ?? {}, "defaults");
@@ -62,6 +71,12 @@ function parseConfig(value = {}) {
62
71
  "sessionTtlMs",
63
72
  "outputReserveTokens"
64
73
  ], "safety");
74
+ const metrics = object(o.metrics ?? {}, "metrics");
75
+ keys(metrics, ["enabled", "sampleIntervalMs", "rollingWindowMs"], "metrics");
76
+ const history = object(o.history ?? {}, "history");
77
+ keys(history, ["enabled", "retentionDays", "maxBytes", "path"], "history");
78
+ if (history.path !== undefined && (typeof history.path !== "string" || !history.path.startsWith("/")))
79
+ throw new Error("history.path: expected absolute directory path");
65
80
  const t = object(o.telemetry ?? {}, "telemetry");
66
81
  keys(t, ["enabled", "jsonl", "path", "maxBytes"], "telemetry");
67
82
  if (t.path !== undefined && (typeof t.path !== "string" || !t.path.startsWith("/")))
@@ -109,6 +124,17 @@ function parseConfig(value = {}) {
109
124
  outputReserveTokens: number(s.outputReserveTokens, 0, "outputReserveTokens")
110
125
  }
111
126
  },
127
+ metrics: {
128
+ enabled: bool(metrics.enabled, true, "metrics.enabled"),
129
+ sampleIntervalMs: number(metrics.sampleIntervalMs, 500, "metrics.sampleIntervalMs", 100),
130
+ rollingWindowMs: number(metrics.rollingWindowMs, 2000, "metrics.rollingWindowMs", 100)
131
+ },
132
+ history: {
133
+ enabled: bool(history.enabled, true, "history.enabled"),
134
+ retentionDays: number(history.retentionDays, 7, "history.retentionDays", 1),
135
+ maxBytes: number(history.maxBytes, 64 * 1024 * 1024, "history.maxBytes", 4096),
136
+ ...typeof history.path === "string" ? { path: history.path } : {}
137
+ },
112
138
  telemetry: {
113
139
  enabled: bool(t.enabled, true, "telemetry.enabled"),
114
140
  jsonl: bool(t.jsonl, true, "telemetry.jsonl"),
@@ -116,6 +142,8 @@ function parseConfig(value = {}) {
116
142
  ...typeof t.path === "string" ? { path: t.path } : {}
117
143
  }
118
144
  };
145
+ if (config.metrics.rollingWindowMs < config.metrics.sampleIntervalMs || config.metrics.rollingWindowMs > 60000)
146
+ throw new Error("metrics.rollingWindowMs must be >= sampleIntervalMs and <= 60000");
119
147
  for (const policy of [config.defaults, ...config.policies]) {
120
148
  if (policy.threshold.type === "tokens" && config.safety.minReclaimTokens >= policy.threshold.value)
121
149
  throw new Error("minReclaimTokens must be smaller than each absolute threshold");
@@ -570,6 +598,7 @@ class SnapshotStore {
570
598
  config;
571
599
  sessions;
572
600
  now;
601
+ performanceSnapshot;
573
602
  epoch;
574
603
  sequence = 0;
575
604
  revision = 0;
@@ -579,10 +608,11 @@ class SnapshotStore {
579
608
  listeners = new Set;
580
609
  healthKey = "";
581
610
  reasons = new Set;
582
- constructor(config, sessions, now = Date.now, epoch = crypto.randomUUID()) {
611
+ constructor(config, sessions, now = Date.now, epoch = crypto.randomUUID(), performanceSnapshot) {
583
612
  this.config = config;
584
613
  this.sessions = sessions;
585
614
  this.now = now;
615
+ this.performanceSnapshot = performanceSnapshot;
586
616
  this.epoch = epoch;
587
617
  }
588
618
  health() {
@@ -643,7 +673,11 @@ class SnapshotStore {
643
673
  const s = resident.get(id);
644
674
  if (!s)
645
675
  continue;
646
- const next = project(s, this.config, this.now());
676
+ const performance = this.performanceSnapshot?.(id);
677
+ const next = freezeSnapshot({
678
+ ...project(s, this.config, this.now()),
679
+ ...performance ? { performance } : {}
680
+ });
647
681
  const old = this.rows.get(id);
648
682
  if (old && JSON.stringify({ ...old, revision: 0 }) === JSON.stringify(next))
649
683
  continue;
@@ -693,6 +727,8 @@ class SnapshotStore {
693
727
  }
694
728
  }
695
729
  function freezeSnapshot(row) {
730
+ if (row.performance)
731
+ Object.freeze(row.performance);
696
732
  if (row.measurement)
697
733
  Object.freeze(row.measurement);
698
734
  if (row.limits)
@@ -807,19 +843,404 @@ function executionStatus(known, nativeRunning, evidence) {
807
843
  return evidence.outcome === "succeeded" ? "completed" : evidence.outcome === "failed" ? "failed" : "unknown";
808
844
  return known ? "idle" : "unknown";
809
845
  }
846
+ // src/performance.ts
847
+ var valid = (n) => Number.isFinite(n) && n >= 0;
848
+ var rate = (tokens, ms) => valid(tokens) && ms > 0 ? tokens * 1000 / ms : undefined;
849
+ function outputTokens(tokens) {
850
+ const n = tokens.output + tokens.reasoning;
851
+ return Number.isSafeInteger(tokens.output) && tokens.output >= 0 && Number.isSafeInteger(tokens.reasoning) && tokens.reasoning >= 0 && Number.isSafeInteger(n) ? n : undefined;
852
+ }
853
+
854
+ class PerformanceTracker {
855
+ maxSessions;
856
+ windowMs;
857
+ now;
858
+ rows = new Map;
859
+ constructor(maxSessions, windowMs = 2000, now = Date.now) {
860
+ this.maxSessions = maxSessions;
861
+ this.windowMs = windowMs;
862
+ this.now = now;
863
+ }
864
+ observe(id, created, cumulative) {
865
+ let row = this.rows.get(id);
866
+ if (!row) {
867
+ if (this.rows.size >= this.maxSessions)
868
+ return;
869
+ row = {
870
+ observedSince: this.now(),
871
+ created,
872
+ partial: cumulative !== 0,
873
+ output: 0,
874
+ duration: 0,
875
+ completed: 0,
876
+ sequence: -1
877
+ };
878
+ this.rows.set(id, row);
879
+ }
880
+ if (valid(created))
881
+ row.created = created;
882
+ if (cumulative !== undefined && valid(cumulative))
883
+ row.cumulative = cumulative;
884
+ }
885
+ accept(id, sequence) {
886
+ const row = this.rows.get(id);
887
+ if (!row || sequence <= row.sequence)
888
+ return false;
889
+ row.sequence = sequence;
890
+ return true;
891
+ }
892
+ start(id, requestId, agent, model, started) {
893
+ const row = this.rows.get(id);
894
+ if (!row || !valid(started))
895
+ return;
896
+ const old = row.request;
897
+ if (old?.id === requestId && old.started === started)
898
+ return;
899
+ if (old && !old.settled)
900
+ row.partial = true;
901
+ row.request = {
902
+ id: requestId,
903
+ agent,
904
+ model,
905
+ started,
906
+ chars: 0,
907
+ settled: false,
908
+ events: new Set,
909
+ buckets: new Map
910
+ };
911
+ }
912
+ delta(id, requestId, eventId, at, characters) {
913
+ const row = this.rows.get(id), r = row?.request;
914
+ if (!row || !r || r.id !== requestId || r.settled || r.ended !== undefined || !valid(at) || at < r.started || !valid(characters) || !characters || r.events.has(eventId))
915
+ return;
916
+ if (r.events.size >= 256)
917
+ r.events.delete(r.events.values().next().value ?? "");
918
+ r.events.add(eventId);
919
+ r.first = r.first === undefined ? at : Math.min(r.first, at);
920
+ r.chars += characters;
921
+ const bucket = Math.floor(at / 100) * 100;
922
+ r.buckets.set(bucket, (r.buckets.get(bucket) ?? 0) + characters);
923
+ this.prune(r, Math.max(at, this.now()));
924
+ while (r.buckets.size > Math.ceil(this.windowMs / 100) + 2) {
925
+ r.buckets.delete(Math.min(...r.buckets.keys()));
926
+ }
927
+ }
928
+ streamed(id, requestId, at) {
929
+ const row = this.rows.get(id), r = row?.request;
930
+ if (!row || !r || r.id !== requestId || r.settled || r.ended !== undefined)
931
+ return;
932
+ if (!valid(at) || at <= r.started || r.first !== undefined && at < r.first) {
933
+ row.partial = true;
934
+ return;
935
+ }
936
+ r.ended = at;
937
+ }
938
+ settle(id, requestId, tokens) {
939
+ const row = this.rows.get(id), r = row?.request;
940
+ if (!row)
941
+ return;
942
+ if (!r || r.id !== requestId) {
943
+ row.partial = true;
944
+ return;
945
+ }
946
+ if (r.settled)
947
+ return;
948
+ r.settled = true;
949
+ if (tokens !== undefined && valid(tokens))
950
+ r.output = tokens;
951
+ if (tokens !== undefined && valid(tokens) && r.ended !== undefined) {
952
+ row.output += tokens;
953
+ row.duration += r.ended - r.started;
954
+ row.completed++;
955
+ } else
956
+ row.partial = true;
957
+ const snapshot = this.snapshot(id);
958
+ if (snapshot) {
959
+ const { latestRequestTps: _previousRate, ...current } = snapshot;
960
+ row.latest = {
961
+ ...current,
962
+ tokenSource: tokens === undefined ? "unavailable" : "reported",
963
+ ...tokens !== undefined && r.ended !== undefined ? { latestRequestTps: tokens * 1000 / (r.ended - r.started) } : {}
964
+ };
965
+ }
966
+ r.events.clear();
967
+ r.buckets.clear();
968
+ }
969
+ gap(id) {
970
+ for (const [key, row] of this.rows) {
971
+ if (id !== undefined && key !== id)
972
+ continue;
973
+ row.partial = true;
974
+ delete row.request;
975
+ }
976
+ }
977
+ prune(r, at) {
978
+ for (const time of r.buckets.keys())
979
+ if (time < at - this.windowMs)
980
+ r.buckets.delete(time);
981
+ }
982
+ snapshot(id) {
983
+ const row = this.rows.get(id);
984
+ if (!row)
985
+ return;
986
+ const at = this.now(), r = row.request;
987
+ const active = r && !r.settled && r.ended === undefined;
988
+ if (r)
989
+ this.prune(r, at);
990
+ const live = active ? rate([...r.buckets.values()].reduce((a, b) => a + b, 0) / 4, Math.min(this.windowMs, at - r.started)) : 0;
991
+ const averageModelTps = rate(row.output, row.duration);
992
+ const averageSessionTps = row.cumulative === undefined ? undefined : rate(row.cumulative, at - row.created);
993
+ return {
994
+ observedSince: row.observedSince,
995
+ at,
996
+ sessionCreatedAt: row.created,
997
+ ...row.cumulative === undefined ? {} : { sessionOutputTokens: row.cumulative },
998
+ status: active ? r.first === undefined ? "waiting" : "streaming" : !r && row.partial ? "unavailable" : "idle",
999
+ partial: row.partial,
1000
+ ...live === undefined || !r && row.partial ? {} : { liveTps: live },
1001
+ ...averageModelTps === undefined ? {} : { averageModelTps },
1002
+ ...averageSessionTps === undefined ? {} : { averageSessionTps },
1003
+ ...r ? {
1004
+ requestId: r.id,
1005
+ requestAgent: r.agent,
1006
+ requestModel: r.model,
1007
+ requestStartedAt: r.started,
1008
+ ...r.output === undefined ? {} : { requestOutputTokens: r.output },
1009
+ ...r.first === undefined ? {} : { firstOutputAt: r.first, ttftMs: r.first - r.started },
1010
+ ...r.ended === undefined ? {} : { streamEndedAt: r.ended }
1011
+ } : row.latest ? {
1012
+ ...row.latest.ttftMs === undefined ? {} : { ttftMs: row.latest.ttftMs }
1013
+ } : {},
1014
+ ...row.latest?.latestRequestTps === undefined ? {} : { latestRequestTps: row.latest.latestRequestTps },
1015
+ estimatedOutputTokens: r ? r.chars / 4 : 0,
1016
+ modelOutputTokens: row.output,
1017
+ modelDurationMs: row.duration,
1018
+ completedRequests: row.completed,
1019
+ tokenSource: r && !r.settled ? "estimated" : row.latest?.tokenSource ?? "unavailable"
1020
+ };
1021
+ }
1022
+ checkpoint() {
1023
+ return [...this.rows].map(([sessionId, r]) => ({
1024
+ sessionId,
1025
+ observedSince: r.observedSince,
1026
+ created: r.created,
1027
+ output: r.output,
1028
+ duration: r.duration,
1029
+ completed: r.completed,
1030
+ sequence: r.sequence
1031
+ }));
1032
+ }
1033
+ restore(value) {
1034
+ if (!Array.isArray(value))
1035
+ return;
1036
+ for (const item of value.slice(0, this.maxSessions)) {
1037
+ if (!item || typeof item !== "object")
1038
+ continue;
1039
+ const r = item;
1040
+ if (typeof r.sessionId !== "string" || !r.sessionId.length || r.sessionId.length > 512 || ![r.observedSince, r.created, r.output, r.duration, r.completed].every((n) => typeof n === "number" && Number.isSafeInteger(n) && n >= 0) || typeof r.sequence !== "number" || !Number.isSafeInteger(r.sequence) || r.sequence < -1)
1041
+ continue;
1042
+ const { observedSince, created, output, duration, completed, sequence } = r;
1043
+ if (typeof observedSince !== "number" || typeof created !== "number" || typeof output !== "number" || typeof duration !== "number" || typeof completed !== "number")
1044
+ continue;
1045
+ this.rows.set(r.sessionId, {
1046
+ observedSince,
1047
+ created,
1048
+ output,
1049
+ duration,
1050
+ completed,
1051
+ sequence,
1052
+ partial: true
1053
+ });
1054
+ }
1055
+ }
1056
+ remove(id) {
1057
+ this.rows.delete(id);
1058
+ }
1059
+ ids() {
1060
+ return this.rows.keys();
1061
+ }
1062
+ }
1063
+ // src/charts.ts
1064
+ var rateFields = [
1065
+ "liveTps",
1066
+ "averageModelTps",
1067
+ "averageSessionTps",
1068
+ "sessionOutputTokens"
1069
+ ];
1070
+ function downsamplePoints(points, limit) {
1071
+ if (!Number.isSafeInteger(limit) || limit < 16 || limit > 5000)
1072
+ throw new Error("maxPoints must be an integer from 16 to 5000");
1073
+ if (points.length <= limit)
1074
+ return [...points];
1075
+ const buckets = Math.max(1, Math.floor(limit / (2 + rateFields.length * 2)));
1076
+ const selected = new Set;
1077
+ for (let b = 0;b < buckets; b++) {
1078
+ const start = Math.floor(b * points.length / buckets), end = Math.floor((b + 1) * points.length / buckets);
1079
+ selected.add(start);
1080
+ selected.add(end - 1);
1081
+ for (const field of rateFields) {
1082
+ let low, high;
1083
+ for (let i = start;i < end; i++) {
1084
+ const value = points[i]?.[field];
1085
+ if (value === undefined)
1086
+ continue;
1087
+ if (low === undefined || value < (points[low]?.[field] ?? Infinity))
1088
+ low = i;
1089
+ if (high === undefined || value > (points[high]?.[field] ?? -Infinity))
1090
+ high = i;
1091
+ }
1092
+ if (low !== undefined)
1093
+ selected.add(low);
1094
+ if (high !== undefined)
1095
+ selected.add(high);
1096
+ }
1097
+ }
1098
+ let previous = -1;
1099
+ return [...selected].sort((a, b) => a - b).flatMap((index) => {
1100
+ const point = points[index];
1101
+ if (!point)
1102
+ return [];
1103
+ const skippedGap = points.slice(previous + 1, index + 1).some((p) => p.breakBefore);
1104
+ previous = index;
1105
+ return [{ ...point, breakBefore: point.breakBefore || skippedGap }];
1106
+ });
1107
+ }
1108
+ function buildChartData(records, options = {}) {
1109
+ const maxPoints = options.maxPoints ?? 1000, maxSessions = options.maxSessions ?? 128;
1110
+ if (!Number.isSafeInteger(maxSessions) || maxSessions < 1 || maxSessions > 1024)
1111
+ throw new Error("maxSessions must be from 1 to 1024");
1112
+ downsamplePoints([], maxPoints);
1113
+ const sorted = [...records].sort((a, b) => a.time - b.time || a.recordId.localeCompare(b.recordId));
1114
+ const lastSeen = new Map;
1115
+ for (const r of sorted)
1116
+ if (r.performance && (!options.sessionId || r.sessionId === options.sessionId))
1117
+ lastSeen.set(r.sessionId, r.time);
1118
+ const sessions = [...lastSeen].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
1119
+ const selected = new Set(sessions.slice(0, maxSessions).map(([id]) => id));
1120
+ const series = new Map, gaps = [];
1121
+ const seen = new Set, requestSeen = new Set;
1122
+ const pendingGaps = new Set;
1123
+ const observedSince = new Map;
1124
+ for (const r of sorted) {
1125
+ if (seen.has(r.recordId))
1126
+ continue;
1127
+ seen.add(r.recordId);
1128
+ if (r.type === "gap") {
1129
+ if (r.sessionId === "__history__" || selected.has(r.sessionId)) {
1130
+ gaps.push({
1131
+ time: r.time,
1132
+ sessionId: r.sessionId,
1133
+ reason: r.event ?? "observation-gap"
1134
+ });
1135
+ if (r.sessionId === "__history__")
1136
+ for (const id of selected)
1137
+ pendingGaps.add(id);
1138
+ else
1139
+ pendingGaps.add(r.sessionId);
1140
+ }
1141
+ continue;
1142
+ }
1143
+ const p = r.performance;
1144
+ if (!p || !selected.has(r.sessionId))
1145
+ continue;
1146
+ let s = series.get(r.sessionId);
1147
+ if (!s) {
1148
+ s = {
1149
+ sessionId: r.sessionId,
1150
+ ...r.parentId ? { parentId: r.parentId } : {},
1151
+ agents: [],
1152
+ models: [],
1153
+ points: [],
1154
+ requests: [],
1155
+ originalPoints: 0
1156
+ };
1157
+ series.set(r.sessionId, s);
1158
+ }
1159
+ const agent = p.requestAgent ?? r.agent, model = p.requestModel ?? r.model;
1160
+ if (agent && !s.agents.includes(agent))
1161
+ s.agents.push(agent);
1162
+ if (model && !s.models.includes(model))
1163
+ s.models.push(model);
1164
+ const previous = s.points.at(-1);
1165
+ const restart = observedSince.has(r.sessionId) && observedSince.get(r.sessionId) !== p.observedSince;
1166
+ const reset = previous?.sessionOutputTokens !== undefined && p.sessionOutputTokens !== undefined && p.sessionOutputTokens < previous.sessionOutputTokens;
1167
+ const discontinuity = pendingGaps.has(r.sessionId) || restart || reset || !previous || previous.partial !== p.partial || previous.liveTps === undefined || p.liveTps === undefined || r.time - previous.time > 2500 || previous.agent !== agent || previous.model !== model;
1168
+ const point = {
1169
+ time: r.time,
1170
+ ...agent ? { agent } : {},
1171
+ ...model ? { model } : {},
1172
+ breakBefore: discontinuity,
1173
+ partial: p.partial,
1174
+ ...p.liveTps === undefined ? {} : { liveTps: p.liveTps },
1175
+ ...p.averageModelTps === undefined ? {} : { averageModelTps: p.averageModelTps },
1176
+ ...p.averageSessionTps === undefined ? {} : { averageSessionTps: p.averageSessionTps },
1177
+ ...p.sessionOutputTokens === undefined ? {} : { sessionOutputTokens: p.sessionOutputTokens }
1178
+ };
1179
+ if (previous?.time === point.time)
1180
+ s.points[s.points.length - 1] = {
1181
+ ...point,
1182
+ breakBefore: point.breakBefore || previous.breakBefore
1183
+ };
1184
+ else
1185
+ s.points.push(point);
1186
+ observedSince.set(r.sessionId, p.observedSince);
1187
+ pendingGaps.delete(r.sessionId);
1188
+ if (r.type === "request" && p.requestId !== undefined && p.requestStartedAt !== undefined) {
1189
+ const key = JSON.stringify([
1190
+ r.sessionId,
1191
+ p.requestId,
1192
+ p.requestStartedAt
1193
+ ]);
1194
+ if (requestSeen.has(key))
1195
+ continue;
1196
+ requestSeen.add(key);
1197
+ const duration = p.streamEndedAt === undefined ? undefined : p.streamEndedAt - p.requestStartedAt;
1198
+ const validDuration = duration !== undefined && duration > 0;
1199
+ const output = p.tokenSource === "reported" ? p.requestOutputTokens : undefined;
1200
+ s.requests.push({
1201
+ time: r.time,
1202
+ requestId: p.requestId,
1203
+ started: p.requestStartedAt,
1204
+ ...agent ? { agent } : {},
1205
+ ...model ? { model } : {},
1206
+ partial: p.partial,
1207
+ ...p.ttftMs === undefined ? {} : { ttftMs: p.ttftMs },
1208
+ ...output === undefined ? {} : { outputTokens: output },
1209
+ ...validDuration ? { durationMs: duration } : {},
1210
+ ...output !== undefined && validDuration ? { tps: output * 1000 / duration } : {}
1211
+ });
1212
+ }
1213
+ }
1214
+ return {
1215
+ schema: 1,
1216
+ series: [...series.values()].map((s) => ({
1217
+ ...s,
1218
+ originalPoints: s.points.length,
1219
+ points: downsamplePoints(s.points, maxPoints)
1220
+ })),
1221
+ gaps: gaps.slice(-1024),
1222
+ omittedGaps: Math.max(0, gaps.length - 1024),
1223
+ omittedSessions: Math.max(0, sessions.length - selected.size),
1224
+ maxPoints
1225
+ };
1226
+ }
810
1227
  export {
811
1228
  usableInput,
812
1229
  promptTokens,
813
1230
  project,
814
1231
  parseConfig,
1232
+ outputTokens,
815
1233
  metric,
816
1234
  glob,
817
1235
  familyRows,
818
1236
  executionStatus,
819
1237
  evaluateThreshold,
820
1238
  estimate,
1239
+ downsamplePoints,
1240
+ buildChartData,
821
1241
  SnapshotStore,
822
1242
  PolicyResolver,
1243
+ PerformanceTracker,
823
1244
  NoopBudgetController,
824
1245
  ContextMonitor,
825
1246
  CompactionScheduler