@guidekit/core 0.1.0-beta.2 → 0.1.0-beta.3

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/README.md CHANGED
@@ -63,7 +63,7 @@ All errors extend `GuideKitError` with structured metadata:
63
63
 
64
64
  - **DOM Scanner** — TreeWalker-based page model with `data-guidekit-ignore` support
65
65
  - **Context Manager** — Token budgeting and truncation for LLM context windows
66
- - **LLM Orchestrator** — Streaming responses with tool calling (Gemini adapter)
66
+ - **LLM Orchestrator** — Streaming responses with tool calling (Gemini (default), custom adapters via `LLMProviderAdapter`)
67
67
  - **Resource Manager** — AbortController pattern and lifecycle tracking
68
68
 
69
69
  ## Documentation
package/dist/index.cjs CHANGED
@@ -1094,7 +1094,7 @@ var DOMScanner = class {
1094
1094
  const style = window.getComputedStyle(el);
1095
1095
  const position = style.position;
1096
1096
  const zIndex = parseInt(style.zIndex, 10) || 0;
1097
- if ((position === "fixed" || position === "absolute") && !isNaN(zIndex) && zIndex >= 1e3) {
1097
+ if ((position === "fixed" || position === "absolute") && !Number.isNaN(zIndex) && zIndex >= 1e3) {
1098
1098
  const visible = isElementVisible(el);
1099
1099
  if (!visible) return;
1100
1100
  const overlayType = this.classifyOverlay(el, style);
@@ -1122,10 +1122,10 @@ var DOMScanner = class {
1122
1122
  return "dropdown";
1123
1123
  const width = parseFloat(style.width);
1124
1124
  const height = parseFloat(style.height);
1125
- if (typeof window !== "undefined" && !isNaN(width) && !isNaN(height) && width > window.innerWidth * 0.5 && height > window.innerHeight * 0.5) {
1125
+ if (typeof window !== "undefined" && !Number.isNaN(width) && !Number.isNaN(height) && width > window.innerWidth * 0.5 && height > window.innerHeight * 0.5) {
1126
1126
  return "modal";
1127
1127
  }
1128
- if (!isNaN(width) && width < 400) return "popover";
1128
+ if (!Number.isNaN(width) && width < 400) return "popover";
1129
1129
  return null;
1130
1130
  }
1131
1131
  // -------------------------------------------------------------------------
@@ -1841,424 +1841,9 @@ function isGuideKitError(error) {
1841
1841
  return error instanceof GuideKitError;
1842
1842
  }
1843
1843
 
1844
- // src/llm/openai-adapter.ts
1845
- var DEFAULT_OPENAI_MODEL = "gpt-4o";
1846
- var DEFAULT_TIMEOUT_MS = 15e3;
1847
- var OPENAI_CHAT_URL = "https://api.openai.com/v1/chat/completions";
1848
- function emptyUsage() {
1849
- return { prompt: 0, completion: 0, total: 0 };
1850
- }
1851
- var OpenAIAdapter = class {
1852
- apiKey;
1853
- model;
1854
- /** Tracks whether the last extractChunks call emitted a done chunk. */
1855
- lastExtractEmittedDone = false;
1856
- /**
1857
- * Token usage extracted from the most recent `parseResponse` call.
1858
- * Updated as each SSE chunk is parsed.
1859
- */
1860
- _lastUsage = emptyUsage();
1861
- constructor(config) {
1862
- this.apiKey = config.apiKey;
1863
- this.model = config.model ?? DEFAULT_OPENAI_MODEL;
1864
- }
1865
- /** Token usage from the most recent parseResponse call. */
1866
- get lastUsage() {
1867
- return this._lastUsage;
1868
- }
1869
- // -----------------------------------------------------------------------
1870
- // LLMProviderAdapter implementation
1871
- // -----------------------------------------------------------------------
1872
- /**
1873
- * Convert GuideKit tool definitions into OpenAI's `tools` format.
1874
- * Each tool is wrapped as `{ type: 'function', function: { name, description, parameters } }`.
1875
- */
1876
- formatTools(tools) {
1877
- if (tools.length === 0) return void 0;
1878
- return tools.map((tool) => ({
1879
- type: "function",
1880
- function: {
1881
- name: tool.name,
1882
- description: tool.description,
1883
- parameters: {
1884
- type: "object",
1885
- properties: { ...tool.parameters },
1886
- required: tool.required ?? []
1887
- }
1888
- }
1889
- }));
1890
- }
1891
- /**
1892
- * Convert an array of `ConversationTurn` objects into OpenAI's messages
1893
- * format with `role: 'user' | 'assistant'`.
1894
- */
1895
- formatConversation(history) {
1896
- return history.map((turn) => ({
1897
- role: turn.role,
1898
- content: turn.content
1899
- }));
1900
- }
1901
- /**
1902
- * Parse an OpenAI SSE streaming response into an async iterable of
1903
- * `TextChunk` and `ToolCall` objects.
1904
- *
1905
- * The OpenAI streaming endpoint sends each chunk as a JSON object
1906
- * prefixed by `data: `. The final line is `data: [DONE]`.
1907
- * Text content arrives in `choices[0].delta.content` and tool calls
1908
- * arrive in `choices[0].delta.tool_calls`.
1909
- *
1910
- * This method also:
1911
- * - Detects content filtering and throws `ContentFilterError`.
1912
- * - Tracks token usage (accessible via `lastUsage` after iteration).
1913
- */
1914
- async *parseResponse(stream) {
1915
- const reader = stream.getReader();
1916
- const decoder = new TextDecoder();
1917
- let buffer = "";
1918
- let doneEmitted = false;
1919
- this._lastUsage = emptyUsage();
1920
- const pendingToolCalls = /* @__PURE__ */ new Map();
1921
- try {
1922
- while (true) {
1923
- const { done, value } = await reader.read();
1924
- if (done) break;
1925
- buffer += decoder.decode(value, { stream: true });
1926
- const lines = buffer.split("\n");
1927
- buffer = lines.pop() ?? "";
1928
- for (const line of lines) {
1929
- const trimmed = line.trim();
1930
- if (!trimmed.startsWith("data:")) continue;
1931
- const jsonStr = trimmed.slice(5).trim();
1932
- if (jsonStr === "" || jsonStr === "[DONE]") {
1933
- if (jsonStr === "[DONE]") {
1934
- yield* this.flushPendingToolCalls(pendingToolCalls);
1935
- if (!doneEmitted) {
1936
- doneEmitted = true;
1937
- yield { text: "", done: true };
1938
- }
1939
- }
1940
- continue;
1941
- }
1942
- let parsed;
1943
- try {
1944
- parsed = JSON.parse(jsonStr);
1945
- } catch {
1946
- continue;
1947
- }
1948
- if (this.isContentFiltered(parsed)) {
1949
- throw new ContentFilterError({
1950
- code: ErrorCodes.CONTENT_FILTER_TRIGGERED,
1951
- message: "Response was blocked by provider content safety filter.",
1952
- provider: "openai",
1953
- suggestion: "Rephrase your question or adjust safety settings."
1954
- });
1955
- }
1956
- const chunkUsage = this.extractUsage(parsed);
1957
- if (chunkUsage) {
1958
- this._lastUsage = chunkUsage;
1959
- }
1960
- yield* this.extractChunks(parsed, pendingToolCalls, doneEmitted);
1961
- if (!doneEmitted && this.lastExtractEmittedDone) {
1962
- doneEmitted = true;
1963
- }
1964
- }
1965
- }
1966
- if (buffer.trim().startsWith("data:")) {
1967
- const jsonStr = buffer.trim().slice(5).trim();
1968
- if (jsonStr === "[DONE]") {
1969
- yield* this.flushPendingToolCalls(pendingToolCalls);
1970
- if (!doneEmitted) {
1971
- doneEmitted = true;
1972
- yield { text: "", done: true };
1973
- }
1974
- } else if (jsonStr !== "") {
1975
- try {
1976
- const parsed = JSON.parse(jsonStr);
1977
- if (this.isContentFiltered(parsed)) {
1978
- throw new ContentFilterError({
1979
- code: ErrorCodes.CONTENT_FILTER_TRIGGERED,
1980
- message: "Response was blocked by provider content safety filter.",
1981
- provider: "openai",
1982
- suggestion: "Rephrase your question or adjust safety settings."
1983
- });
1984
- }
1985
- const chunkUsage = this.extractUsage(parsed);
1986
- if (chunkUsage) {
1987
- this._lastUsage = chunkUsage;
1988
- }
1989
- yield* this.extractChunks(parsed, pendingToolCalls, doneEmitted);
1990
- if (!doneEmitted && this.lastExtractEmittedDone) {
1991
- doneEmitted = true;
1992
- }
1993
- } catch (error) {
1994
- if (error instanceof ContentFilterError) throw error;
1995
- }
1996
- }
1997
- }
1998
- yield* this.flushPendingToolCalls(pendingToolCalls);
1999
- } finally {
2000
- reader.releaseLock();
2001
- }
2002
- }
2003
- /**
2004
- * Format a tool result so it can be sent back to OpenAI as a
2005
- * `tool` role message with the `tool_call_id`.
2006
- */
2007
- formatToolResult(callId, result) {
2008
- return {
2009
- role: "tool",
2010
- tool_call_id: callId,
2011
- content: typeof result === "string" ? result : JSON.stringify(result)
2012
- };
2013
- }
2014
- // -----------------------------------------------------------------------
2015
- // Streaming request
2016
- // -----------------------------------------------------------------------
2017
- /**
2018
- * Build and execute a streaming request to the OpenAI Chat Completions API.
2019
- * Returns the raw `ReadableStream` for the response body together with
2020
- * the raw Response object.
2021
- */
2022
- async streamRequest(params) {
2023
- const contentsArray = params.contents;
2024
- const messages = [
2025
- { role: "system", content: params.systemPrompt },
2026
- ...contentsArray
2027
- ];
2028
- if (params.userMessage) {
2029
- messages.push({ role: "user", content: params.userMessage });
2030
- }
2031
- const body = {
2032
- model: this.model,
2033
- messages,
2034
- stream: true,
2035
- temperature: 0.7,
2036
- top_p: 0.95
2037
- };
2038
- if (params.tools) {
2039
- body.tools = params.tools;
2040
- }
2041
- const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2042
- const controller = new AbortController();
2043
- const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2044
- if (params.signal) {
2045
- params.signal.addEventListener(
2046
- "abort",
2047
- () => controller.abort(params.signal.reason),
2048
- { once: true }
2049
- );
2050
- }
2051
- let response;
2052
- try {
2053
- response = await fetch(OPENAI_CHAT_URL, {
2054
- method: "POST",
2055
- headers: {
2056
- "Content-Type": "application/json",
2057
- Authorization: `Bearer ${this.apiKey}`
2058
- },
2059
- body: JSON.stringify(body),
2060
- signal: controller.signal
2061
- });
2062
- } catch (error) {
2063
- clearTimeout(timeoutId);
2064
- if (error instanceof DOMException && error.name === "AbortError") {
2065
- if (params.signal?.aborted) {
2066
- throw error;
2067
- }
2068
- throw new TimeoutError({
2069
- code: ErrorCodes.TIMEOUT_LLM_RESPONSE,
2070
- message: `OpenAI request timed out after ${timeoutMs}ms`,
2071
- provider: "openai",
2072
- recoverable: true,
2073
- suggestion: "Try again or increase the timeout.",
2074
- operationName: "openai.chatCompletions",
2075
- timeoutMs
2076
- });
2077
- }
2078
- throw new NetworkError({
2079
- code: ErrorCodes.NETWORK_CONNECTION_LOST,
2080
- message: `Failed to connect to OpenAI API: ${error.message}`,
2081
- provider: "openai",
2082
- suggestion: "Check your network connection and try again.",
2083
- cause: error instanceof Error ? error : void 0
2084
- });
2085
- }
2086
- clearTimeout(timeoutId);
2087
- if (!response.ok) {
2088
- await this.handleHttpError(response);
2089
- }
2090
- if (!response.body) {
2091
- throw new NetworkError({
2092
- code: ErrorCodes.NETWORK_CONNECTION_LOST,
2093
- message: "OpenAI response body is null -- streaming unavailable.",
2094
- provider: "openai",
2095
- suggestion: "Retry the request."
2096
- });
2097
- }
2098
- return { stream: response.body, response };
2099
- }
2100
- // -----------------------------------------------------------------------
2101
- // Internal helpers
2102
- // -----------------------------------------------------------------------
2103
- /**
2104
- * Extract `TextChunk` and accumulate `ToolCall` data from a single parsed
2105
- * OpenAI SSE JSON object.
2106
- *
2107
- * OpenAI tool calls arrive incrementally: the first chunk for a tool call
2108
- * carries the `id` and `function.name`, while subsequent chunks append to
2109
- * `function.arguments`. We accumulate these in `pendingToolCalls` and only
2110
- * yield complete `ToolCall` objects when the finish_reason is 'tool_calls'
2111
- * or when flushed.
2112
- */
2113
- *extractChunks(parsed, pendingToolCalls, doneEmitted) {
2114
- this.lastExtractEmittedDone = false;
2115
- const choices = parsed.choices;
2116
- if (!choices || choices.length === 0) return;
2117
- for (const choice of choices) {
2118
- const delta = choice.delta;
2119
- const finishReason = choice.finish_reason;
2120
- if (delta) {
2121
- if (typeof delta.content === "string" && delta.content !== "") {
2122
- yield {
2123
- text: delta.content,
2124
- done: false
2125
- };
2126
- }
2127
- const toolCallDeltas = delta.tool_calls;
2128
- if (toolCallDeltas) {
2129
- for (const tc of toolCallDeltas) {
2130
- const existing = pendingToolCalls.get(tc.index);
2131
- if (existing) {
2132
- if (tc.function?.arguments) {
2133
- existing.argumentsJson += tc.function.arguments;
2134
- }
2135
- } else {
2136
- pendingToolCalls.set(tc.index, {
2137
- id: tc.id ?? "",
2138
- name: tc.function?.name ?? "",
2139
- argumentsJson: tc.function?.arguments ?? ""
2140
- });
2141
- }
2142
- }
2143
- }
2144
- }
2145
- if (finishReason === "tool_calls") {
2146
- yield* this.flushPendingToolCalls(pendingToolCalls);
2147
- }
2148
- if (finishReason === "stop" && !doneEmitted && !this.lastExtractEmittedDone) {
2149
- this.lastExtractEmittedDone = true;
2150
- yield { text: "", done: true };
2151
- }
2152
- }
2153
- }
2154
- /**
2155
- * Flush all accumulated pending tool calls as complete `ToolCall` objects.
2156
- */
2157
- *flushPendingToolCalls(pendingToolCalls) {
2158
- const sorted = [...pendingToolCalls.entries()].sort(
2159
- ([a], [b]) => a - b
2160
- );
2161
- for (const [, tc] of sorted) {
2162
- let args = {};
2163
- try {
2164
- args = JSON.parse(tc.argumentsJson);
2165
- } catch (_e) {
2166
- console.warn("[GuideKit:LLM] Failed to parse tool call arguments:", tc.argumentsJson);
2167
- }
2168
- yield {
2169
- id: tc.id,
2170
- name: tc.name,
2171
- arguments: args
2172
- };
2173
- }
2174
- pendingToolCalls.clear();
2175
- }
2176
- /**
2177
- * Extract token usage from a parsed OpenAI response chunk.
2178
- * Usage data typically appears in the final chunk when `stream_options`
2179
- * includes `include_usage`, or in the non-streaming response.
2180
- * Returns `null` if no usage data is present.
2181
- */
2182
- extractUsage(parsed) {
2183
- const usage = parsed.usage;
2184
- if (!usage) return null;
2185
- return {
2186
- prompt: usage.prompt_tokens ?? 0,
2187
- completion: usage.completion_tokens ?? 0,
2188
- total: usage.total_tokens ?? 0
2189
- };
2190
- }
2191
- /**
2192
- * Check whether a parsed OpenAI chunk indicates the response was
2193
- * blocked by a content filter.
2194
- *
2195
- * OpenAI signals content filtering through:
2196
- * - `choices[].finish_reason === 'content_filter'`
2197
- * - `choices[].content_filter_results` with `filtered: true`
2198
- */
2199
- isContentFiltered(parsed) {
2200
- const choices = parsed.choices;
2201
- if (!choices || choices.length === 0) return false;
2202
- return choices.some((choice) => {
2203
- if (choice.finish_reason === "content_filter") return true;
2204
- const filterResults = choice.content_filter_results;
2205
- if (filterResults) {
2206
- return Object.values(filterResults).some((r) => r.filtered === true);
2207
- }
2208
- return false;
2209
- });
2210
- }
2211
- /**
2212
- * Translate an HTTP error response from OpenAI into the appropriate
2213
- * GuideKit error class.
2214
- */
2215
- async handleHttpError(response) {
2216
- let errorBody = "";
2217
- try {
2218
- errorBody = await response.text();
2219
- } catch {
2220
- }
2221
- const status = response.status;
2222
- if (status === 401 || status === 403) {
2223
- throw new AuthenticationError({
2224
- code: ErrorCodes.AUTH_INVALID_KEY,
2225
- message: `OpenAI API authentication failed (${status}): ${errorBody}`,
2226
- provider: "openai",
2227
- suggestion: "Verify your OpenAI API key is correct and has not expired."
2228
- });
2229
- }
2230
- if (status === 429) {
2231
- const retryAfterHeader = response.headers.get("retry-after");
2232
- const retryAfterMs = retryAfterHeader ? parseInt(retryAfterHeader, 10) * 1e3 : 6e4;
2233
- throw new RateLimitError({
2234
- code: ErrorCodes.RATE_LIMIT_PROVIDER,
2235
- message: `OpenAI API rate limit exceeded (429): ${errorBody}`,
2236
- provider: "openai",
2237
- recoverable: true,
2238
- suggestion: `Rate limited by OpenAI. Retry after ${Math.ceil(retryAfterMs / 1e3)}s.`,
2239
- retryAfterMs
2240
- });
2241
- }
2242
- if (status >= 500) {
2243
- throw new NetworkError({
2244
- code: ErrorCodes.NETWORK_CONNECTION_LOST,
2245
- message: `OpenAI API server error (${status}): ${errorBody}`,
2246
- provider: "openai",
2247
- suggestion: "The OpenAI API is experiencing issues. Please try again later."
2248
- });
2249
- }
2250
- throw new NetworkError({
2251
- code: ErrorCodes.NETWORK_CONNECTION_LOST,
2252
- message: `OpenAI API request failed (${status}): ${errorBody}`,
2253
- provider: "openai",
2254
- suggestion: "Check the request parameters and try again."
2255
- });
2256
- }
2257
- };
2258
-
2259
1844
  // src/llm/index.ts
2260
1845
  var DEFAULT_GEMINI_MODEL = "gemini-2.5-flash";
2261
- var DEFAULT_TIMEOUT_MS2 = 15e3;
1846
+ var DEFAULT_TIMEOUT_MS = 15e3;
2262
1847
  var GEMINI_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/models";
2263
1848
  var DEFAULT_SAFETY_SETTINGS = [
2264
1849
  { category: "HARM_CATEGORY_HARASSMENT", threshold: "BLOCK_ONLY_HIGH" },
@@ -2266,7 +1851,7 @@ var DEFAULT_SAFETY_SETTINGS = [
2266
1851
  { category: "HARM_CATEGORY_SEXUALLY_EXPLICIT", threshold: "BLOCK_ONLY_HIGH" },
2267
1852
  { category: "HARM_CATEGORY_DANGEROUS_CONTENT", threshold: "BLOCK_ONLY_HIGH" }
2268
1853
  ];
2269
- function emptyUsage2() {
1854
+ function emptyUsage() {
2270
1855
  return { prompt: 0, completion: 0, total: 0 };
2271
1856
  }
2272
1857
  var GeminiAdapter = class {
@@ -2277,7 +1862,7 @@ var GeminiAdapter = class {
2277
1862
  * Updated as each SSE chunk is parsed; the final value reflects the
2278
1863
  * cumulative usage metadata sent by Gemini (typically in the last chunk).
2279
1864
  */
2280
- _lastUsage = emptyUsage2();
1865
+ _lastUsage = emptyUsage();
2281
1866
  constructor(config) {
2282
1867
  this.apiKey = config.apiKey;
2283
1868
  this.model = config.model ?? DEFAULT_GEMINI_MODEL;
@@ -2335,7 +1920,7 @@ var GeminiAdapter = class {
2335
1920
  const reader = stream.getReader();
2336
1921
  const decoder = new TextDecoder();
2337
1922
  let buffer = "";
2338
- this._lastUsage = emptyUsage2();
1923
+ this._lastUsage = emptyUsage();
2339
1924
  try {
2340
1925
  while (true) {
2341
1926
  const { done, value } = await reader.read();
@@ -2444,7 +2029,7 @@ var GeminiAdapter = class {
2444
2029
  if (params.tools) {
2445
2030
  body.tools = params.tools;
2446
2031
  }
2447
- const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
2032
+ const timeoutMs = params.timeoutMs ?? DEFAULT_TIMEOUT_MS;
2448
2033
  const controller = new AbortController();
2449
2034
  const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
2450
2035
  if (params.signal) {
@@ -2719,7 +2304,7 @@ var LLMOrchestrator = class {
2719
2304
  }
2720
2305
  }
2721
2306
  this.callbacks.onChunk?.({ text: "", done: true });
2722
- let usage = emptyUsage2();
2307
+ let usage = emptyUsage();
2723
2308
  if ("lastUsage" in adapter) {
2724
2309
  usage = adapter.lastUsage;
2725
2310
  }
@@ -2739,7 +2324,7 @@ var LLMOrchestrator = class {
2739
2324
  *
2740
2325
  * Custom adapters:
2741
2326
  * - Pass `{ adapter: myAdapter }` to use any `LLMProviderAdapter`.
2742
- * Example: `llm: { adapter: new OpenAIAdapter({ ... }) }`
2327
+ * Example: `llm: { adapter: myCustomAdapter }`
2743
2328
  */
2744
2329
  createAdapter(config) {
2745
2330
  if ("adapter" in config) {
@@ -9637,7 +9222,6 @@ exports.InitializationError = InitializationError;
9637
9222
  exports.LLMOrchestrator = LLMOrchestrator;
9638
9223
  exports.NavigationController = NavigationController;
9639
9224
  exports.NetworkError = NetworkError;
9640
- exports.OpenAIAdapter = OpenAIAdapter;
9641
9225
  exports.PermissionError = PermissionError;
9642
9226
  exports.ProactiveTriggerEngine = ProactiveTriggerEngine;
9643
9227
  exports.RateLimitError = RateLimitError;