unbrowse 2.12.2 → 2.12.7

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 (64) hide show
  1. package/README.md +8 -44
  2. package/dist/cli.js +514 -20723
  3. package/package.json +4 -10
  4. package/runtime-src/api/routes.ts +15 -801
  5. package/runtime-src/auth/index.ts +32 -142
  6. package/runtime-src/capture/index.ts +101 -436
  7. package/runtime-src/cli.ts +371 -956
  8. package/runtime-src/client/index.ts +29 -622
  9. package/runtime-src/execution/index.ts +85 -345
  10. package/runtime-src/graph/index.ts +10 -128
  11. package/runtime-src/intent-match.ts +27 -27
  12. package/runtime-src/kuri/client.ts +82 -543
  13. package/runtime-src/orchestrator/index.ts +462 -2246
  14. package/runtime-src/reverse-engineer/index.ts +22 -220
  15. package/runtime-src/runtime/local-server.ts +16 -149
  16. package/runtime-src/runtime/paths.ts +5 -9
  17. package/runtime-src/runtime/setup.ts +1 -52
  18. package/runtime-src/server.ts +11 -6
  19. package/runtime-src/transform/schema-hints.ts +358 -0
  20. package/runtime-src/types/skill.ts +2 -49
  21. package/runtime-src/verification/index.ts +0 -15
  22. package/runtime-src/version.ts +13 -13
  23. package/vendor/kuri/darwin-arm64/kuri +0 -0
  24. package/vendor/kuri/darwin-x64/kuri +0 -0
  25. package/vendor/kuri/linux-arm64/kuri +0 -0
  26. package/vendor/kuri/linux-x64/kuri +0 -0
  27. package/bin/unbrowse-wrapper.mjs +0 -39
  28. package/bin/unbrowse.js +0 -38
  29. package/runtime-src/analytics-session.ts +0 -33
  30. package/runtime-src/api/browse-index.ts +0 -254
  31. package/runtime-src/api/browse-session.ts +0 -179
  32. package/runtime-src/api/browse-submit.ts +0 -455
  33. package/runtime-src/auth/runtime.ts +0 -116
  34. package/runtime-src/browser/index.ts +0 -635
  35. package/runtime-src/browser/types.ts +0 -41
  36. package/runtime-src/capture/prefetch.ts +0 -122
  37. package/runtime-src/capture/rsc.ts +0 -45
  38. package/runtime-src/cli/shortcuts.ts +0 -273
  39. package/runtime-src/client/graph-client.ts +0 -99
  40. package/runtime-src/execution/robots.ts +0 -167
  41. package/runtime-src/execution/search-forms.ts +0 -188
  42. package/runtime-src/graph/planner.ts +0 -411
  43. package/runtime-src/graph/session.ts +0 -294
  44. package/runtime-src/graph/trace-store.ts +0 -136
  45. package/runtime-src/indexer/index.ts +0 -480
  46. package/runtime-src/orchestrator/browser-agent.ts +0 -374
  47. package/runtime-src/orchestrator/dag-advisor.ts +0 -59
  48. package/runtime-src/orchestrator/dag-feedback.ts +0 -256
  49. package/runtime-src/orchestrator/first-pass-action.ts +0 -362
  50. package/runtime-src/orchestrator/passive-publish.ts +0 -152
  51. package/runtime-src/orchestrator/timing-economics.ts +0 -80
  52. package/runtime-src/payments/cascade.ts +0 -137
  53. package/runtime-src/payments/index.ts +0 -268
  54. package/runtime-src/payments/wallet.ts +0 -33
  55. package/runtime-src/reverse-engineer/description-prompt.ts +0 -132
  56. package/runtime-src/router.ts +0 -17
  57. package/runtime-src/runtime/browser-access.ts +0 -11
  58. package/runtime-src/runtime/browser-host.ts +0 -48
  59. package/runtime-src/runtime/lifecycle.ts +0 -17
  60. package/runtime-src/runtime/supervisor.ts +0 -69
  61. package/runtime-src/single-binary.ts +0 -141
  62. package/runtime-src/telemetry.ts +0 -253
  63. package/runtime-src/verification/matrix.ts +0 -30
  64. package/scripts/postinstall.mjs +0 -81
@@ -1,362 +0,0 @@
1
- import * as kuri from "../kuri/client.js";
2
- import type { KuriHarEntry } from "../kuri/client.js";
3
- import { createHash } from "node:crypto";
4
- import type { EndpointDescriptor, SkillManifest } from "../types/skill.js";
5
-
6
- export type IntentClass = "search" | "navigate" | "click" | "submit" | "read" | "unknown";
7
-
8
- export interface FirstPassResult {
9
- intentClass: IntentClass;
10
- actionTaken: string;
11
- hit: boolean;
12
- interceptedEntries: KuriHarEntry[];
13
- miniSkill?: SkillManifest;
14
- result?: unknown;
15
- timeMs: number;
16
- /** Tab ID kept alive on miss for Phase 4 agentic loop to reuse */
17
- tabId?: string;
18
- }
19
-
20
- interface FirstPassOptions {
21
- signal?: AbortSignal;
22
- clientScope?: string;
23
- }
24
-
25
- /** Classify an intent string into an action category. */
26
- export function classifyIntent(intent: string): IntentClass {
27
- if (/\b(search|find|discover|look\s+for|browse)\b/i.test(intent)) return "search";
28
- if (/\b(go\s+to|open|visit|navigate)\b/i.test(intent)) return "navigate";
29
- if (/\b(click|tap|press|select)\b/i.test(intent)) return "click";
30
- if (/\b(submit|register|sign\s+up|rsvp|book|apply)\b/i.test(intent)) return "submit";
31
- if (/\b(read|get|fetch|show|list|view)\b/i.test(intent)) return "read";
32
- return "unknown";
33
- }
34
-
35
- /** Map an intent class to a browser action descriptor. */
36
- export function classifyAction(
37
- intentClass: IntentClass,
38
- _url: string,
39
- ): { type: "search" | "click" | "navigate" | "wait"; selector?: string } {
40
- switch (intentClass) {
41
- case "search":
42
- return {
43
- type: "search",
44
- selector: "input[type=search],input[name=q],input[name=query],input[name=search]",
45
- };
46
- case "submit":
47
- return {
48
- type: "click",
49
- selector: "button[type=submit],input[type=submit],[role=button]",
50
- };
51
- case "click":
52
- return { type: "click", selector: "[role=button],button,a" };
53
- case "navigate":
54
- case "read":
55
- case "unknown":
56
- default:
57
- return { type: "navigate" };
58
- }
59
- }
60
-
61
- /** Filter HAR entries to only JSON API responses with 2xx status. */
62
- function filterJsonApiEntries(entries: KuriHarEntry[]): KuriHarEntry[] {
63
- return entries.filter((entry) => {
64
- const status = entry.response?.status;
65
- if (!status || status < 200 || status > 299) return false;
66
- const ct =
67
- (entry.response.headers ?? []).find(
68
- (h) => h.name.toLowerCase() === "content-type",
69
- )?.value ?? "";
70
- return ct.includes("application/json") || ct.includes("+json");
71
- });
72
- }
73
-
74
- /** Build a lightweight skill manifest from intercepted HAR entries. */
75
- export function synthesizeSkillFromIntercepted(
76
- interceptedEntries: KuriHarEntry[],
77
- domain: string,
78
- intent: string,
79
- ): SkillManifest | undefined {
80
- if (interceptedEntries.length === 0) return undefined;
81
-
82
- const hash = createHash("sha1")
83
- .update(interceptedEntries.map((e) => e.request.url).join("|"))
84
- .digest("hex")
85
- .slice(0, 8);
86
-
87
- const endpoints: EndpointDescriptor[] = interceptedEntries.map((entry, i) => {
88
- const parsed = new URL(entry.request.url);
89
- return {
90
- endpoint_id: `fp-ep-${i}`,
91
- method: entry.request.method.toUpperCase() as EndpointDescriptor["method"],
92
- url_template: parsed.origin + parsed.pathname,
93
- idempotency: "safe" as const,
94
- verification_status: "unverified" as const,
95
- reliability_score: 0.5,
96
- description: `First-pass intercepted endpoint for intent: ${intent}`,
97
- };
98
- });
99
-
100
- return {
101
- skill_id: `first-pass-${domain}-${hash}`,
102
- version: "0.0.1",
103
- schema_version: "2",
104
- name: `First-pass: ${domain}`,
105
- intent_signature: intent,
106
- domain,
107
- description: "Lightweight first-pass skill synthesized from intercepted network requests",
108
- owner_type: "agent",
109
- execution_type: "http",
110
- lifecycle: "active",
111
- endpoints,
112
- created_at: new Date().toISOString(),
113
- updated_at: new Date().toISOString(),
114
- };
115
- }
116
-
117
- /**
118
- * Lightweight first-pass browser action: navigate to contextUrl, optionally
119
- * perform an intent-driven action (search / click), collect intercepted
120
- * JSON API requests via HAR recording, and return them for passive indexing.
121
- *
122
- * Designed to complete within ~5-8s. On any error, returns hit:false
123
- * and never throws.
124
- */
125
- export async function tryFirstPassBrowserAction(
126
- intent: string,
127
- params: Record<string, unknown>,
128
- contextUrl: string,
129
- options?: FirstPassOptions,
130
- ): Promise<FirstPassResult> {
131
- const t0 = Date.now();
132
- const intentClass = classifyIntent(intent);
133
- const action = classifyAction(intentClass, contextUrl);
134
-
135
- let domain = "";
136
- try {
137
- domain = new URL(contextUrl).hostname;
138
- } catch {
139
- domain = "unknown";
140
- }
141
-
142
- // Hard 8s timeout for the entire operation
143
- const timeoutMs = 8_000;
144
- const deadline = t0 + timeoutMs;
145
- const externalSignal = options?.signal;
146
-
147
- function isAborted(): boolean {
148
- if (externalSignal?.aborted) return true;
149
- return Date.now() >= deadline;
150
- }
151
-
152
- async function sleepMs(ms: number): Promise<void> {
153
- const remaining = deadline - Date.now();
154
- const actual = Math.min(ms, Math.max(0, remaining));
155
- if (actual <= 0) return;
156
- return new Promise<void>((resolve) => {
157
- const tid = setTimeout(resolve, actual);
158
- if (externalSignal) {
159
- const cleanup = () => {
160
- clearTimeout(tid);
161
- resolve();
162
- };
163
- externalSignal.addEventListener("abort", cleanup, { once: true });
164
- }
165
- });
166
- }
167
-
168
- let tabId: string | undefined;
169
- let createdFreshTab = false;
170
-
171
- try {
172
- if (isAborted()) {
173
- return miss("aborted-before-start", intentClass, t0);
174
- }
175
-
176
- // Ensure kuri is running
177
- await kuri.start();
178
- await kuri.discoverTabs();
179
-
180
- if (isAborted()) {
181
- return miss("aborted-after-start", intentClass, t0);
182
- }
183
-
184
- // Open a fresh tab (fall back to default)
185
- try {
186
- tabId = await kuri.newTab("about:blank");
187
- createdFreshTab = !!tabId;
188
- if (!tabId) tabId = await kuri.getDefaultTab();
189
- } catch {
190
- tabId = await kuri.getDefaultTab();
191
- }
192
-
193
- if (!tabId) {
194
- return miss("no-tab", intentClass, t0);
195
- }
196
-
197
- if (isAborted()) {
198
- await cleanupTab(tabId, createdFreshTab);
199
- return miss("aborted-before-navigate", intentClass, t0);
200
- }
201
-
202
- // Start HAR recording before navigation to capture all requests
203
- await kuri.harStart(tabId);
204
-
205
- // Navigate to the target URL
206
- await kuri.navigate(tabId, contextUrl);
207
- await sleepMs(1_500);
208
-
209
- if (isAborted()) {
210
- await safeHarStop(tabId);
211
- await cleanupTab(tabId, createdFreshTab);
212
- return miss("aborted-after-navigate", intentClass, t0);
213
- }
214
-
215
- // Perform intent-driven action
216
- let actionTaken = "navigate";
217
- if (action.type === "search" && action.selector) {
218
- const searchTerm =
219
- typeof params.query === "string"
220
- ? params.query
221
- : typeof params.q === "string"
222
- ? params.q
223
- : intent;
224
- try {
225
- const searchResult = await kuri.evaluate(
226
- tabId,
227
- `(function(){
228
- var sel = ${JSON.stringify(action.selector)};
229
- var el = document.querySelector(sel);
230
- if (!el) return 'not-found';
231
- el.focus();
232
- el.value = ${JSON.stringify(searchTerm)};
233
- el.dispatchEvent(new Event('input', {bubbles:true}));
234
- el.dispatchEvent(new Event('change', {bubbles:true}));
235
- var form = el.closest('form');
236
- if (form) { form.dispatchEvent(new Event('submit', {bubbles:true, cancelable:true})); return 'form-submit'; }
237
- el.dispatchEvent(new KeyboardEvent('keydown', {key:'Enter',keyCode:13,bubbles:true}));
238
- el.dispatchEvent(new KeyboardEvent('keypress', {key:'Enter',keyCode:13,bubbles:true}));
239
- el.dispatchEvent(new KeyboardEvent('keyup', {key:'Enter',keyCode:13,bubbles:true}));
240
- return 'search-enter';
241
- })()`,
242
- );
243
- actionTaken =
244
- typeof searchResult === "string" ? searchResult : "search-attempted";
245
- } catch {
246
- actionTaken = "search-failed";
247
- }
248
- } else if (action.type === "click" && action.selector) {
249
- try {
250
- const clickResult = await kuri.evaluate(
251
- tabId,
252
- `(function(){
253
- var el = document.querySelector(${JSON.stringify(action.selector)});
254
- if (!el) return 'not-found';
255
- el.click();
256
- return 'clicked';
257
- })()`,
258
- );
259
- actionTaken =
260
- typeof clickResult === "string" ? clickResult : "click-attempted";
261
- } catch {
262
- actionTaken = "click-failed";
263
- }
264
- }
265
-
266
- // Wait for network activity to settle after the action
267
- await sleepMs(2_000);
268
-
269
- if (isAborted()) {
270
- await safeHarStop(tabId);
271
- await cleanupTab(tabId, createdFreshTab);
272
- return miss(actionTaken, intentClass, t0);
273
- }
274
-
275
- // Stop HAR recording and collect entries
276
- let harEntries: KuriHarEntry[] = [];
277
- try {
278
- const harResult = await kuri.harStop(tabId);
279
- harEntries = harResult.entries ?? [];
280
- } catch {
281
- // non-fatal — HAR collection failed
282
- }
283
-
284
- // On hit: cleanup tab. On miss: keep tab alive for Phase 4 agentic loop.
285
- if (filterJsonApiEntries(harEntries).length > 0) {
286
- await cleanupTab(tabId, createdFreshTab);
287
- }
288
-
289
- // Filter to JSON API responses only
290
- const jsonEntries = filterJsonApiEntries(harEntries);
291
- const hit = jsonEntries.length > 0;
292
-
293
- let result: unknown;
294
- let miniSkill: SkillManifest | undefined;
295
-
296
- if (hit) {
297
- try {
298
- const firstEntry = jsonEntries[0];
299
- const text = firstEntry?.response.content?.text;
300
- if (text) result = JSON.parse(text);
301
- } catch {
302
- result = undefined;
303
- }
304
- miniSkill = synthesizeSkillFromIntercepted(jsonEntries, domain, intent);
305
- }
306
-
307
- return {
308
- hit,
309
- result,
310
- interceptedEntries: jsonEntries,
311
- miniSkill,
312
- actionTaken,
313
- intentClass,
314
- timeMs: Date.now() - t0,
315
- tabId: hit ? undefined : tabId, // keep tab alive on miss
316
- };
317
- } catch (err) {
318
- // On any error, cleanup and return miss — never throw
319
- if (tabId) {
320
- try {
321
- await safeHarStop(tabId);
322
- } catch {
323
- /* ignore */
324
- }
325
- try {
326
- await cleanupTab(tabId, createdFreshTab);
327
- } catch {
328
- /* ignore */
329
- }
330
- }
331
- const isAbortError =
332
- err instanceof Error &&
333
- (err.name === "AbortError" || err.name === "TimeoutError");
334
- return {
335
- hit: false,
336
- interceptedEntries: [],
337
- actionTaken: isAbortError ? "aborted" : "error",
338
- intentClass,
339
- timeMs: Date.now() - t0,
340
- };
341
- }
342
- }
343
-
344
- async function safeHarStop(tabId: string): Promise<void> {
345
- try {
346
- await kuri.harStop(tabId);
347
- } catch {
348
- /* best-effort */
349
- }
350
- }
351
-
352
- async function cleanupTab(tabId: string, close: boolean): Promise<void> {
353
- try {
354
- if (close) {
355
- await kuri.closeTab(tabId);
356
- } else {
357
- await kuri.navigate(tabId, "about:blank");
358
- }
359
- } catch {
360
- /* best-effort */
361
- }
362
- }
@@ -1,152 +0,0 @@
1
- import { cachePublishedSkill, validateManifest, publishGraphEdges } from "../client/index.js";
2
- import { attributeLifecycle, type LifecycleEvent } from "../runtime/lifecycle.js";
3
- import { publishSkill } from "../marketplace/index.js";
4
- import type { SkillManifest } from "../types/index.js";
5
-
6
- type PassivePublishDeps = {
7
- cachePublishedSkill: typeof cachePublishedSkill;
8
- publishSkill: typeof publishSkill;
9
- validateManifest: typeof validateManifest;
10
- };
11
-
12
- export type PassiveParityVerdict = "pass" | "fail" | "skip";
13
-
14
- type PassivePublishOptions = {
15
- deps?: PassivePublishDeps;
16
- parity?: PassiveParityVerdict | Promise<PassiveParityVerdict> | (() => Promise<PassiveParityVerdict>);
17
- };
18
-
19
- const defaultDeps: PassivePublishDeps = {
20
- cachePublishedSkill,
21
- publishSkill,
22
- validateManifest,
23
- };
24
-
25
- const passivePublishInFlight = new Map<string, Promise<void>>();
26
-
27
- function mergeBackendDescriptions(
28
- localSkill: SkillManifest,
29
- publishedSkill: SkillManifest,
30
- ): SkillManifest["endpoints"] {
31
- return localSkill.endpoints.map((endpoint) => {
32
- const backendEndpoint = publishedSkill.endpoints.find(
33
- (candidate) =>
34
- candidate.endpoint_id === endpoint.endpoint_id ||
35
- (candidate.method === endpoint.method && candidate.url_template === endpoint.url_template),
36
- );
37
- if (!backendEndpoint?.description) return endpoint;
38
- return {
39
- ...endpoint,
40
- description: backendEndpoint.description,
41
- };
42
- });
43
- }
44
-
45
- export function queuePassiveSkillPublish(
46
- skill: SkillManifest,
47
- options: PassivePublishOptions = {},
48
- ): Promise<void> {
49
- const deps = options.deps ?? defaultDeps;
50
- const existing = passivePublishInFlight.get(skill.skill_id);
51
- if (existing) return existing;
52
-
53
- const job = (async () => {
54
- if (skill.execution_type !== "http") return;
55
-
56
- const parityVerdict =
57
- typeof options.parity === "function"
58
- ? await options.parity()
59
- : options.parity instanceof Promise
60
- ? await options.parity
61
- : options.parity;
62
- if (parityVerdict === "fail") {
63
- console.warn(`[publish] passive publish skipped for ${skill.skill_id}: parity_failed`);
64
- return;
65
- }
66
-
67
- const publishableEndpoints = skill.endpoints.filter((endpoint) => endpoint.method !== "WS");
68
- if (publishableEndpoints.length === 0) return;
69
-
70
- const { operation_graph: _graph, ...publishBase } = skill;
71
- const publishDraft: SkillManifest = {
72
- ...publishBase,
73
- endpoints: publishableEndpoints,
74
- };
75
-
76
- const validation = await deps.validateManifest({ ...publishDraft, skill_id: "__validate__" });
77
- if (!validation.valid) {
78
- console.warn(
79
- `[publish] passive publish skipped for ${skill.skill_id}: ${validation.hardErrors.join("; ") || "validation failed"}`,
80
- );
81
- return;
82
- }
83
-
84
- const publishStart = Date.now();
85
- const published = await deps.publishSkill(publishDraft);
86
- const publishMs = Date.now() - publishStart;
87
- deps.cachePublishedSkill({
88
- ...skill,
89
- ...published,
90
- endpoints: mergeBackendDescriptions(skill, published),
91
- operation_graph: skill.operation_graph,
92
- ...(skill.auth_profile_ref ? { auth_profile_ref: skill.auth_profile_ref } : {}),
93
- });
94
-
95
- // Publish graph edges via dedicated endpoint (fire-and-forget)
96
- if (skill.operation_graph?.operations) {
97
- for (const op of skill.operation_graph.operations) {
98
- const opEdges = (skill.operation_graph.edges ?? [])
99
- .filter(e => e.from_operation_id === op.operation_id)
100
- .map(e => ({
101
- target_endpoint_id: skill.operation_graph!.operations.find(
102
- t => t.operation_id === e.to_operation_id
103
- )?.endpoint_id ?? e.to_operation_id,
104
- kind: e.kind,
105
- confidence: e.confidence,
106
- }));
107
- if (opEdges.length > 0) {
108
- publishGraphEdges(skill.domain, {
109
- endpoint_id: op.endpoint_id,
110
- method: op.method,
111
- url_template: op.url_template,
112
- }, opEdges).catch(() => {});
113
- }
114
- }
115
- }
116
-
117
- const publishEvent: LifecycleEvent = {
118
- phase: "publish",
119
- skill_id: skill.skill_id,
120
- timestamp: new Date().toISOString(),
121
- duration_ms: publishMs,
122
- source: "marketplace",
123
- };
124
- const totals = attributeLifecycle([publishEvent]);
125
- console.log(`[lifecycle] publish=${totals.get("publish")}ms for ${skill.skill_id}`);
126
- console.log(`[publish] passive publish succeeded for ${skill.skill_id}`);
127
- })()
128
- .catch((err) => {
129
- console.error(
130
- `[publish] passive publish failed for ${skill.skill_id}: ${err instanceof Error ? err.message : String(err)}`,
131
- );
132
- })
133
- .finally(() => {
134
- passivePublishInFlight.delete(skill.skill_id);
135
- });
136
-
137
- passivePublishInFlight.set(skill.skill_id, job);
138
- return job;
139
- }
140
-
141
- /** Await all in-flight passive publish jobs. Call before process exit. */
142
- export async function drainPendingPassivePublishes(): Promise<void> {
143
- const pending = [...passivePublishInFlight.values()];
144
- if (pending.length === 0) return;
145
- console.log(`[publish] draining ${pending.length} pending passive publish(es)...`);
146
- await Promise.allSettled(pending);
147
- console.log(`[publish] all passive publishes drained`);
148
- }
149
-
150
- export function resetPassivePublishQueueForTests(): void {
151
- passivePublishInFlight.clear();
152
- }
@@ -1,80 +0,0 @@
1
- import type { SkillManifest } from "../types/index.js";
2
-
3
- export const DEFAULT_CAPTURE_MS = 22_000;
4
- export const DEFAULT_CAPTURE_TOKENS = 30_000;
5
- export const CHARS_PER_TOKEN = 4;
6
- export const TOKEN_COST_PER_MILLION_USD = 3;
7
- export const TOKEN_COST_UC = Math.round(TOKEN_COST_PER_MILLION_USD);
8
-
9
- const SAVINGS_SOURCES = new Set([
10
- "marketplace",
11
- "route-cache",
12
- "first-pass",
13
- "direct-fetch",
14
- "browser-action",
15
- ]);
16
-
17
- export interface TimingEconomics {
18
- response_bytes: number;
19
- response_tokens: number;
20
- tokens_saved: number;
21
- tokens_saved_pct: number;
22
- time_saved_pct: number;
23
- baseline_total_ms?: number;
24
- time_saved_ms?: number;
25
- baseline_cost_uc?: number;
26
- actual_cost_uc: number;
27
- cost_saved_uc?: number;
28
- baseline_source?: "real" | "estimated";
29
- }
30
-
31
- export function computeTimingEconomics({
32
- source,
33
- totalMs,
34
- result,
35
- skill,
36
- paidSearchUc = 0,
37
- paidExecutionUc = 0,
38
- }: {
39
- source: string;
40
- totalMs: number;
41
- result: unknown;
42
- skill?: Pick<SkillManifest, "discovery_cost">;
43
- paidSearchUc?: number;
44
- paidExecutionUc?: number;
45
- }): TimingEconomics {
46
- const resultStr = typeof result === "string" ? result : JSON.stringify(result ?? "");
47
- const responseBytes = resultStr.length;
48
- const responseTokens = Math.ceil(responseBytes / CHARS_PER_TOKEN);
49
- const actualCostUc = (responseTokens * TOKEN_COST_UC) + paidSearchUc + paidExecutionUc;
50
-
51
- const economics: TimingEconomics = {
52
- response_bytes: responseBytes,
53
- response_tokens: responseTokens,
54
- tokens_saved: 0,
55
- tokens_saved_pct: 0,
56
- time_saved_pct: 0,
57
- actual_cost_uc: actualCostUc,
58
- };
59
-
60
- if (!SAVINGS_SOURCES.has(source)) return economics;
61
-
62
- const baselineTokens = skill?.discovery_cost?.capture_tokens ?? DEFAULT_CAPTURE_TOKENS;
63
- const baselineMs = skill?.discovery_cost?.capture_ms ?? DEFAULT_CAPTURE_MS;
64
- const baselineCostUc = baselineTokens * TOKEN_COST_UC;
65
-
66
- economics.tokens_saved = Math.max(0, baselineTokens - responseTokens);
67
- economics.tokens_saved_pct = baselineTokens > 0
68
- ? Math.round((economics.tokens_saved / baselineTokens) * 100)
69
- : 0;
70
- economics.time_saved_pct = baselineMs > 0
71
- ? Math.round((Math.max(0, baselineMs - totalMs) / baselineMs) * 100)
72
- : 0;
73
- economics.baseline_total_ms = baselineMs;
74
- economics.time_saved_ms = Math.max(0, baselineMs - totalMs);
75
- economics.baseline_cost_uc = baselineCostUc;
76
- economics.cost_saved_uc = Math.max(0, baselineCostUc - actualCostUc);
77
- economics.baseline_source = skill?.discovery_cost ? "real" : "estimated";
78
-
79
- return economics;
80
- }