negotium 0.2.14 → 0.2.16

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.
Files changed (34) hide show
  1. package/dist/agent-helpers.js +178 -26
  2. package/dist/agent-helpers.js.map +8 -8
  3. package/dist/{chunk-5g5a63vv.js → chunk-5cpdbbeh.js} +56 -6
  4. package/dist/{chunk-5g5a63vv.js.map → chunk-5cpdbbeh.js.map} +4 -4
  5. package/dist/hosted-agent.js +118 -9
  6. package/dist/hosted-agent.js.map +6 -6
  7. package/dist/main.js +389 -50
  8. package/dist/main.js.map +15 -15
  9. package/dist/mcp-factories.js +247 -36
  10. package/dist/mcp-factories.js.map +9 -9
  11. package/dist/registry.js +3 -3
  12. package/dist/registry.js.map +2 -2
  13. package/dist/rollout.js +1 -1
  14. package/dist/runtime/src/agents/codex-provider.ts +53 -2
  15. package/dist/runtime/src/agents/rollout/codex.ts +62 -0
  16. package/dist/runtime/src/mcp/factories/token-stats.ts +33 -11
  17. package/dist/runtime/src/mcp/runtime-spec.ts +58 -1
  18. package/dist/runtime/src/node-host.ts +1 -0
  19. package/dist/runtime/src/runtime/public-helpers.ts +8 -0
  20. package/dist/runtime/src/runtime/turn-event-stream.ts +17 -2
  21. package/dist/runtime/src/storage/token-stats.ts +200 -19
  22. package/dist/runtime/src/version.ts +1 -1
  23. package/dist/runtime-helpers.js +443 -30
  24. package/dist/runtime-helpers.js.map +9 -3
  25. package/dist/storage.js +112 -11
  26. package/dist/storage.js.map +3 -3
  27. package/dist/types/packages/core/src/agents/codex-provider.d.ts +13 -0
  28. package/dist/types/packages/core/src/agents/rollout/codex.d.ts +9 -0
  29. package/dist/types/packages/core/src/mcp/factories/token-stats.d.ts +4 -2
  30. package/dist/types/packages/core/src/runtime/bashrs-completions.d.ts +50 -0
  31. package/dist/types/packages/core/src/runtime/public-helpers.d.ts +2 -0
  32. package/dist/types/packages/core/src/storage/token-stats.d.ts +35 -6
  33. package/dist/types/packages/core/src/version.d.ts +1 -1
  34. package/package.json +1 -1
@@ -4,14 +4,24 @@ import { join } from "node:path";
4
4
  import { appendJsonlEntry, readJsonlLines } from "#platform/jsonl";
5
5
  import { logger } from "#platform/logger";
6
6
  import { resolveStorageLogDir } from "#storage/storage-host";
7
+ import type { AgentKind, TokenUsage } from "#types";
7
8
 
8
9
  export interface QueryRecord {
10
+ schemaVersion: 2;
9
11
  timestamp: string; // ISO 8601 UTC
10
- session: string; // topic name
12
+ session: string; // topic title (legacy output name)
13
+ topicId: string;
14
+ providerSessionId?: string;
15
+ agent: AgentKind;
16
+ model: string;
17
+ /** Cache-miss input only. */
11
18
  inputTokens: number;
12
19
  outputTokens: number;
13
20
  cacheCreationInputTokens: number;
14
21
  cacheReadInputTokens: number;
22
+ contextTokens?: number;
23
+ contextWindow?: number;
24
+ estimatedCostUsd: number;
15
25
  }
16
26
 
17
27
  export interface Bucket {
@@ -20,6 +30,23 @@ export interface Bucket {
20
30
  cacheCreationInputTokens: number;
21
31
  cacheReadInputTokens: number;
22
32
  queries: number;
33
+ estimatedCostUsd: number;
34
+ }
35
+
36
+ export interface CurrentSessionUsage {
37
+ timestamp: string;
38
+ topicId: string;
39
+ topicTitle: string;
40
+ providerSessionId?: string;
41
+ agent: AgentKind;
42
+ model: string;
43
+ contextTokens: number;
44
+ contextWindow: number;
45
+ }
46
+
47
+ export interface TopicUsageSummary extends Bucket {
48
+ topicId: string;
49
+ currentSession?: CurrentSessionUsage;
23
50
  }
24
51
 
25
52
  function emptyBucket(): Bucket {
@@ -29,6 +56,7 @@ function emptyBucket(): Bucket {
29
56
  cacheCreationInputTokens: 0,
30
57
  cacheReadInputTokens: 0,
31
58
  queries: 0,
59
+ estimatedCostUsd: 0,
32
60
  };
33
61
  }
34
62
 
@@ -48,11 +76,11 @@ function queriesPath(userId: number | string): string {
48
76
  return join(logDir, `token-queries-${fileId}.jsonl`);
49
77
  }
50
78
 
51
- function loadRecords(userId: number | string): QueryRecord[] {
79
+ function loadRecords(userId: number | string): unknown[] {
52
80
  try {
53
81
  return readJsonlLines(queriesPath(userId)).flatMap((line) => {
54
82
  try {
55
- return [JSON.parse(line) as QueryRecord];
83
+ return [JSON.parse(line) as unknown];
56
84
  } catch {
57
85
  return [];
58
86
  }
@@ -65,14 +93,72 @@ function loadRecords(userId: number | string): QueryRecord[] {
65
93
  export function calcCost(
66
94
  b: Pick<
67
95
  Bucket,
96
+ | "inputTokens"
97
+ | "outputTokens"
98
+ | "cacheCreationInputTokens"
99
+ | "cacheReadInputTokens"
100
+ | "estimatedCostUsd"
101
+ >,
102
+ ): number {
103
+ return b.estimatedCostUsd;
104
+ }
105
+
106
+ type TokenPrices = {
107
+ input: number;
108
+ output: number;
109
+ cacheWrite?: number;
110
+ cacheRead: number;
111
+ };
112
+
113
+ const TOKEN_PRICES: Record<string, TokenPrices> = {
114
+ "codex:gpt-5.6-sol": { input: 5, cacheRead: 0.5, output: 30 },
115
+ "codex:gpt-5.6-terra": { input: 2.5, cacheRead: 0.25, output: 15 },
116
+ "codex:gpt-5.6-luna": { input: 1, cacheRead: 0.1, output: 6 },
117
+ "claude:fable": { input: 10, cacheWrite: 12.5, cacheRead: 1, output: 50 },
118
+ "claude:opus": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
119
+ "claude:sonnet": { input: 2, cacheWrite: 2.5, cacheRead: 0.2, output: 10 },
120
+ "maestro:kimi-k3": { input: 3, cacheRead: 0.3, output: 15 },
121
+ "maestro:kimi-k2.7-code": { input: 0.95, cacheRead: 0.19, output: 4 },
122
+ "maestro:deepseek-pro": { input: 0.435, cacheRead: 0.003625, output: 0.87 },
123
+ "maestro:deepseek-flash": { input: 0.14, cacheRead: 0.0028, output: 0.28 },
124
+ };
125
+
126
+ function estimateUsageCost(
127
+ agent: AgentKind,
128
+ model: string,
129
+ usage: Pick<
130
+ QueryRecord,
68
131
  "inputTokens" | "outputTokens" | "cacheCreationInputTokens" | "cacheReadInputTokens"
69
132
  >,
70
133
  ): number {
134
+ const prices = TOKEN_PRICES[`${agent}:${model}`];
135
+ if (!prices) return 0;
71
136
  return (
72
- (b.inputTokens / 1_000_000) * 3.0 +
73
- (b.outputTokens / 1_000_000) * 15.0 +
74
- (b.cacheCreationInputTokens / 1_000_000) * 3.75 +
75
- (b.cacheReadInputTokens / 1_000_000) * 0.3
137
+ (usage.inputTokens * prices.input +
138
+ usage.outputTokens * prices.output +
139
+ (agent === "claude"
140
+ ? usage.cacheCreationInputTokens * (prices.cacheWrite ?? prices.input)
141
+ : 0) +
142
+ usage.cacheReadInputTokens * prices.cacheRead) /
143
+ 1_000_000
144
+ );
145
+ }
146
+
147
+ function isQueryRecord(value: unknown): value is QueryRecord {
148
+ if (!value || typeof value !== "object") return false;
149
+ const record = value as Partial<QueryRecord>;
150
+ return (
151
+ record.schemaVersion === 2 &&
152
+ typeof record.timestamp === "string" &&
153
+ typeof record.session === "string" &&
154
+ typeof record.topicId === "string" &&
155
+ typeof record.agent === "string" &&
156
+ typeof record.model === "string" &&
157
+ typeof record.inputTokens === "number" &&
158
+ typeof record.outputTokens === "number" &&
159
+ typeof record.cacheCreationInputTokens === "number" &&
160
+ typeof record.cacheReadInputTokens === "number" &&
161
+ typeof record.estimatedCostUsd === "number"
76
162
  );
77
163
  }
78
164
 
@@ -80,20 +166,39 @@ export function calcCost(
80
166
  export function recordUsage(
81
167
  userId: number | string,
82
168
  session: string,
83
- usage: {
84
- inputTokens: number;
85
- outputTokens: number;
86
- cacheCreationInputTokens?: number;
87
- cacheReadInputTokens?: number;
169
+ usage: TokenUsage,
170
+ context: {
171
+ topicId: string;
172
+ providerSessionId?: string;
173
+ agent: AgentKind;
174
+ model: string;
88
175
  },
89
176
  ) {
177
+ const cacheReadInputTokens = usage.cacheReadInputTokens ?? 0;
178
+ // Claude reports cache buckets separately; Codex and Maestro include cache
179
+ // hits in their provider input total.
180
+ const inputTokens =
181
+ context.agent === "claude"
182
+ ? usage.inputTokens
183
+ : Math.max(0, usage.inputTokens - cacheReadInputTokens);
184
+ const normalized = {
185
+ inputTokens,
186
+ outputTokens: usage.outputTokens,
187
+ cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
188
+ cacheReadInputTokens,
189
+ };
90
190
  const record: QueryRecord = {
191
+ schemaVersion: 2,
91
192
  timestamp: new Date().toISOString(),
92
193
  session,
93
- inputTokens: usage.inputTokens,
94
- outputTokens: usage.outputTokens,
95
- cacheCreationInputTokens: usage.cacheCreationInputTokens ?? 0,
96
- cacheReadInputTokens: usage.cacheReadInputTokens ?? 0,
194
+ topicId: context.topicId,
195
+ ...(context.providerSessionId ? { providerSessionId: context.providerSessionId } : {}),
196
+ agent: context.agent,
197
+ model: context.model,
198
+ ...normalized,
199
+ ...(usage.contextTokens !== undefined ? { contextTokens: usage.contextTokens } : {}),
200
+ ...(usage.contextWindow !== undefined ? { contextWindow: usage.contextWindow } : {}),
201
+ estimatedCostUsd: usage.costUsd ?? estimateUsageCost(context.agent, context.model, normalized),
97
202
  };
98
203
  try {
99
204
  appendJsonlEntry(queriesPath(userId), record);
@@ -110,6 +215,8 @@ export function getStats(
110
215
  total: Bucket;
111
216
  byHour: Record<string, Bucket>;
112
217
  bySession: Record<string, Bucket>;
218
+ currentSessions: CurrentSessionUsage[];
219
+ ignoredLegacyRecords: number;
113
220
  estimatedCostUsd: number;
114
221
  } {
115
222
  const records = loadRecords(userId);
@@ -119,14 +226,28 @@ export function getStats(
119
226
 
120
227
  if ((from && Number.isNaN(fromTs)) || (to && Number.isNaN(toTs))) {
121
228
  logger.warn({ from, to }, "token-stats: Invalid date range, returning empty");
122
- return { total: emptyBucket(), byHour: {}, bySession: {}, estimatedCostUsd: 0 };
229
+ return {
230
+ total: emptyBucket(),
231
+ byHour: {},
232
+ bySession: {},
233
+ currentSessions: [],
234
+ ignoredLegacyRecords: 0,
235
+ estimatedCostUsd: 0,
236
+ };
123
237
  }
124
238
 
125
239
  const total = emptyBucket();
126
240
  const byHour: Record<string, Bucket> = {};
127
241
  const bySession: Record<string, Bucket> = {};
242
+ const currentSessions = new Map<string, CurrentSessionUsage>();
243
+ let ignoredLegacyRecords = 0;
128
244
 
129
- for (const r of records) {
245
+ for (const raw of records) {
246
+ if (!isQueryRecord(raw)) {
247
+ ignoredLegacyRecords += 1;
248
+ continue;
249
+ }
250
+ const r = raw;
130
251
  const ts = new Date(r.timestamp).getTime();
131
252
  if (ts < fromTs || ts > toTs) continue;
132
253
 
@@ -141,8 +262,68 @@ export function getStats(
141
262
  bucket.cacheCreationInputTokens += r.cacheCreationInputTokens;
142
263
  bucket.cacheReadInputTokens += r.cacheReadInputTokens;
143
264
  bucket.queries += 1;
265
+ bucket.estimatedCostUsd += r.estimatedCostUsd;
266
+ }
267
+
268
+ if (r.contextTokens !== undefined && r.contextWindow !== undefined && r.contextWindow > 0) {
269
+ currentSessions.set(r.topicId, {
270
+ timestamp: r.timestamp,
271
+ topicId: r.topicId,
272
+ topicTitle: r.session,
273
+ ...(r.providerSessionId ? { providerSessionId: r.providerSessionId } : {}),
274
+ agent: r.agent,
275
+ model: r.model,
276
+ contextTokens: r.contextTokens,
277
+ contextWindow: r.contextWindow,
278
+ });
279
+ }
280
+ }
281
+
282
+ return {
283
+ total,
284
+ byHour,
285
+ bySession,
286
+ currentSessions: [...currentSessions.values()].sort((a, b) =>
287
+ b.timestamp.localeCompare(a.timestamp),
288
+ ),
289
+ ignoredLegacyRecords,
290
+ estimatedCostUsd: calcCost(total),
291
+ };
292
+ }
293
+
294
+ /** Exact all-time usage for one topic, independent of transcript pagination. */
295
+ export function getTopicStats(userId: number | string, topicId: string): TopicUsageSummary {
296
+ const total = emptyBucket();
297
+ let currentSession: CurrentSessionUsage | undefined;
298
+
299
+ for (const raw of loadRecords(userId)) {
300
+ if (!isQueryRecord(raw) || raw.topicId !== topicId) continue;
301
+
302
+ total.inputTokens += raw.inputTokens;
303
+ total.outputTokens += raw.outputTokens;
304
+ total.cacheCreationInputTokens += raw.cacheCreationInputTokens;
305
+ total.cacheReadInputTokens += raw.cacheReadInputTokens;
306
+ total.queries += 1;
307
+ total.estimatedCostUsd += raw.estimatedCostUsd;
308
+
309
+ if (
310
+ raw.contextTokens !== undefined &&
311
+ raw.contextWindow !== undefined &&
312
+ raw.contextWindow > 0 &&
313
+ (!currentSession || raw.timestamp > currentSession.timestamp)
314
+ ) {
315
+ currentSession = {
316
+ timestamp: raw.timestamp,
317
+ topicId: raw.topicId,
318
+ topicTitle: raw.session,
319
+ ...(raw.providerSessionId ? { providerSessionId: raw.providerSessionId } : {}),
320
+ agent: raw.agent,
321
+ model: raw.model,
322
+ contextTokens: raw.contextTokens,
323
+ contextWindow: raw.contextWindow,
324
+ };
144
325
  }
145
326
  }
146
327
 
147
- return { total, byHour, bySession, estimatedCostUsd: calcCost(total) };
328
+ return { topicId, ...total, ...(currentSession ? { currentSession } : {}) };
148
329
  }
@@ -1 +1 @@
1
- export const NEGOTIUM_VERSION = "0.2.14";
1
+ export const NEGOTIUM_VERSION = "0.2.16";