imprint-mcp 0.5.0 → 0.6.0

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 (202) hide show
  1. package/README.md +6 -4
  2. package/examples/discoverandgo/README.md +1 -1
  3. package/examples/discoverandgo/book_discoverandgo_museum_pass/workflow.json +7 -0
  4. package/examples/google-flights/README.md +41 -14
  5. package/examples/google-flights/_shared/google_batchexecute_parser.ts +165 -0
  6. package/examples/google-flights/_shared/google_flights_transport.ts +70 -0
  7. package/examples/google-flights/_shared/google_flights_types.ts +56 -0
  8. package/examples/google-flights/get_flight_booking_options/backends.json +43 -0
  9. package/examples/google-flights/get_flight_booking_options/index.ts +129 -0
  10. package/examples/google-flights/get_flight_booking_options/parser.ts +209 -0
  11. package/examples/google-flights/get_flight_booking_options/playbook.yaml +46 -0
  12. package/examples/google-flights/get_flight_booking_options/request-transform.ts +210 -0
  13. package/examples/google-flights/get_flight_booking_options/workflow.json +85 -0
  14. package/examples/google-flights/get_flight_calendar_prices/backends.json +27 -0
  15. package/examples/google-flights/get_flight_calendar_prices/index.ts +44 -29
  16. package/examples/google-flights/get_flight_calendar_prices/parser.ts +76 -69
  17. package/examples/google-flights/get_flight_calendar_prices/playbook.yaml +67 -40
  18. package/examples/google-flights/get_flight_calendar_prices/workflow.json +29 -17
  19. package/examples/google-flights/get_flight_location_details/backends.json +27 -0
  20. package/examples/google-flights/{lookup_airport → get_flight_location_details}/index.ts +30 -20
  21. package/examples/google-flights/get_flight_location_details/parser.ts +103 -0
  22. package/examples/google-flights/get_flight_location_details/playbook.yaml +59 -0
  23. package/examples/google-flights/{lookup_airport → get_flight_location_details}/workflow.json +19 -12
  24. package/examples/google-flights/lookup_flight_locations/backends.json +27 -0
  25. package/examples/google-flights/lookup_flight_locations/index.ts +111 -0
  26. package/examples/google-flights/lookup_flight_locations/package.json +9 -0
  27. package/examples/google-flights/lookup_flight_locations/parser.ts +135 -0
  28. package/examples/google-flights/lookup_flight_locations/playbook.yaml +47 -0
  29. package/examples/google-flights/lookup_flight_locations/request-transform.ts +53 -0
  30. package/examples/google-flights/lookup_flight_locations/workflow.json +64 -0
  31. package/examples/google-flights/search_flights/backends.json +42 -0
  32. package/examples/google-flights/search_flights/index.ts +105 -69
  33. package/examples/google-flights/search_flights/parser.ts +186 -183
  34. package/examples/google-flights/search_flights/playbook.yaml +295 -88
  35. package/examples/google-flights/search_flights/request-transform.ts +247 -92
  36. package/examples/google-flights/search_flights/workflow.json +66 -40
  37. package/examples/google-flights/validate_flight_itinerary/backends.json +27 -0
  38. package/examples/google-flights/validate_flight_itinerary/index.ts +103 -0
  39. package/examples/google-flights/validate_flight_itinerary/package.json +9 -0
  40. package/examples/google-flights/validate_flight_itinerary/parser.ts +101 -0
  41. package/examples/google-flights/validate_flight_itinerary/playbook.yaml +84 -0
  42. package/examples/google-flights/validate_flight_itinerary/request-transform.ts +92 -0
  43. package/examples/google-flights/validate_flight_itinerary/workflow.json +59 -0
  44. package/examples/google-hotels/README.md +40 -15
  45. package/examples/google-hotels/get_hotel_details/backends.json +16 -0
  46. package/examples/google-hotels/get_hotel_details/index.ts +122 -0
  47. package/examples/google-hotels/get_hotel_details/parser.ts +275 -0
  48. package/examples/google-hotels/get_hotel_details/playbook.yaml +40 -0
  49. package/examples/google-hotels/get_hotel_details/request-transform.ts +67 -0
  50. package/examples/google-hotels/get_hotel_details/workflow.json +67 -0
  51. package/examples/google-hotels/get_hotel_photos/backends.json +16 -0
  52. package/examples/google-hotels/get_hotel_photos/index.ts +90 -0
  53. package/examples/google-hotels/get_hotel_photos/parser.ts +75 -0
  54. package/examples/google-hotels/get_hotel_photos/playbook.yaml +23 -0
  55. package/examples/google-hotels/get_hotel_photos/request-transform.ts +27 -0
  56. package/examples/google-hotels/get_hotel_photos/workflow.json +44 -0
  57. package/examples/google-hotels/get_hotel_reviews/backends.json +16 -0
  58. package/examples/google-hotels/get_hotel_reviews/index.ts +14 -17
  59. package/examples/google-hotels/get_hotel_reviews/parser.ts +177 -105
  60. package/examples/google-hotels/get_hotel_reviews/playbook.yaml +9 -54
  61. package/examples/google-hotels/get_hotel_reviews/workflow.json +10 -13
  62. package/examples/google-hotels/get_travel_area_boundaries/backends.json +16 -0
  63. package/examples/google-hotels/get_travel_area_boundaries/index.ts +104 -0
  64. package/examples/google-hotels/get_travel_area_boundaries/package.json +9 -0
  65. package/examples/google-hotels/get_travel_area_boundaries/parser.ts +127 -0
  66. package/examples/google-hotels/get_travel_area_boundaries/playbook.yaml +38 -0
  67. package/examples/google-hotels/get_travel_area_boundaries/request-transform.ts +52 -0
  68. package/examples/google-hotels/get_travel_area_boundaries/workflow.json +54 -0
  69. package/examples/google-hotels/list_hotel_booking_options/backends.json +16 -0
  70. package/examples/google-hotels/list_hotel_booking_options/index.ts +123 -0
  71. package/examples/google-hotels/list_hotel_booking_options/package.json +9 -0
  72. package/examples/google-hotels/list_hotel_booking_options/parser.ts +308 -0
  73. package/examples/google-hotels/list_hotel_booking_options/playbook.yaml +95 -0
  74. package/examples/google-hotels/list_hotel_booking_options/request-transform.ts +77 -0
  75. package/examples/google-hotels/list_hotel_booking_options/workflow.json +67 -0
  76. package/examples/google-hotels/search_hotel_web_links/backends.json +16 -0
  77. package/examples/google-hotels/search_hotel_web_links/index.ts +90 -0
  78. package/examples/google-hotels/search_hotel_web_links/package.json +9 -0
  79. package/examples/google-hotels/search_hotel_web_links/parser.ts +96 -0
  80. package/examples/google-hotels/search_hotel_web_links/playbook.yaml +53 -0
  81. package/examples/google-hotels/search_hotel_web_links/request-transform.ts +17 -0
  82. package/examples/google-hotels/search_hotel_web_links/workflow.json +43 -0
  83. package/examples/google-hotels/search_hotels/backends.json +16 -0
  84. package/examples/google-hotels/search_hotels/index.ts +32 -102
  85. package/examples/google-hotels/search_hotels/parser.ts +237 -216
  86. package/examples/google-hotels/search_hotels/playbook.yaml +177 -42
  87. package/examples/google-hotels/search_hotels/request-transform.ts +101 -170
  88. package/examples/google-hotels/search_hotels/workflow.json +24 -73
  89. package/examples/google-hotels/suggest_hotel_destinations/backends.json +16 -0
  90. package/examples/google-hotels/{autocomplete_hotel_location → suggest_hotel_destinations}/index.ts +30 -19
  91. package/examples/google-hotels/suggest_hotel_destinations/package.json +9 -0
  92. package/examples/google-hotels/suggest_hotel_destinations/parser.ts +138 -0
  93. package/examples/google-hotels/suggest_hotel_destinations/playbook.yaml +47 -0
  94. package/examples/google-hotels/suggest_hotel_destinations/request-transform.ts +33 -0
  95. package/examples/google-hotels/suggest_hotel_destinations/workflow.json +44 -0
  96. package/examples/southwest/README.md +31 -70
  97. package/examples/southwest/get_account_details/backends.json +22 -0
  98. package/examples/southwest/get_account_details/credentials.manifest.json +17 -0
  99. package/examples/southwest/get_account_details/index.ts +138 -0
  100. package/examples/{google-hotels/_shared → southwest/get_account_details}/package.json +2 -2
  101. package/examples/southwest/get_account_details/parser.ts +136 -0
  102. package/examples/southwest/get_account_details/playbook.yaml +19 -0
  103. package/examples/southwest/get_account_details/workflow.json +93 -0
  104. package/examples/southwest/get_flight_status/backends.json +21 -0
  105. package/examples/southwest/get_flight_status/index.ts +130 -0
  106. package/examples/southwest/get_flight_status/package.json +9 -0
  107. package/examples/southwest/get_flight_status/parser.ts +138 -0
  108. package/examples/southwest/get_flight_status/playbook.yaml +58 -0
  109. package/examples/southwest/get_flight_status/workflow.json +77 -0
  110. package/examples/southwest/get_low_fare_calendar/backends.json +21 -0
  111. package/examples/southwest/get_low_fare_calendar/index.ts +134 -0
  112. package/examples/southwest/get_low_fare_calendar/package.json +9 -0
  113. package/examples/southwest/get_low_fare_calendar/parser.ts +107 -0
  114. package/examples/southwest/get_low_fare_calendar/playbook.yaml +41 -0
  115. package/examples/southwest/get_low_fare_calendar/workflow.json +81 -0
  116. package/examples/southwest/list_upcoming_trips/backends.json +22 -0
  117. package/examples/southwest/list_upcoming_trips/credentials.manifest.json +17 -0
  118. package/examples/southwest/list_upcoming_trips/index.ts +123 -0
  119. package/examples/southwest/list_upcoming_trips/package.json +9 -0
  120. package/examples/southwest/list_upcoming_trips/parser.ts +160 -0
  121. package/examples/southwest/list_upcoming_trips/playbook.yaml +24 -0
  122. package/examples/southwest/list_upcoming_trips/workflow.json +78 -0
  123. package/examples/southwest/search_flights/backends.json +21 -0
  124. package/examples/southwest/search_flights/index.ts +150 -0
  125. package/examples/southwest/search_flights/package.json +9 -0
  126. package/examples/southwest/search_flights/parser.ts +214 -0
  127. package/examples/southwest/search_flights/playbook.yaml +52 -0
  128. package/examples/southwest/search_flights/workflow.json +88 -0
  129. package/package.json +1 -1
  130. package/prompts/audit-agent.md +2 -1
  131. package/prompts/build-planning.md +22 -1
  132. package/prompts/compile-agent.md +66 -53
  133. package/prompts/prereq-builder.md +3 -1
  134. package/prompts/tool-planning.md +2 -0
  135. package/src/cli.ts +8 -3
  136. package/src/imprint/audit.ts +337 -44
  137. package/src/imprint/backend-cache.ts +114 -0
  138. package/src/imprint/backend-ladder.ts +74 -24
  139. package/src/imprint/bot-defense.ts +12 -0
  140. package/src/imprint/build-plan.ts +346 -53
  141. package/src/imprint/cdp-browser-fetch.ts +96 -43
  142. package/src/imprint/cdp-jar-cache.ts +6 -5
  143. package/src/imprint/chromium.ts +34 -0
  144. package/src/imprint/codex-cli-compile.ts +13 -2
  145. package/src/imprint/compile-agent.ts +3 -1
  146. package/src/imprint/compile-tools.ts +715 -138
  147. package/src/imprint/compile.ts +41 -9
  148. package/src/imprint/concurrency.ts +26 -3
  149. package/src/imprint/cron.ts +0 -3
  150. package/src/imprint/endpoint-key.ts +23 -0
  151. package/src/imprint/llm-url.ts +69 -0
  152. package/src/imprint/llm.ts +366 -47
  153. package/src/imprint/login.ts +83 -75
  154. package/src/imprint/mcp-compile-server.ts +4 -1
  155. package/src/imprint/mcp-server.ts +51 -16
  156. package/src/imprint/param-grounding.ts +61 -52
  157. package/src/imprint/probe-backends.ts +21 -115
  158. package/src/imprint/redact.ts +17 -15
  159. package/src/imprint/request-capture.ts +94 -0
  160. package/src/imprint/runtime.ts +1 -78
  161. package/src/imprint/session-diff.ts +14 -1
  162. package/src/imprint/stealth-fetch.ts +30 -20
  163. package/src/imprint/stealth-token-cache.ts +2 -2
  164. package/src/imprint/teach-plan.ts +2 -0
  165. package/src/imprint/teach-state.ts +14 -9
  166. package/src/imprint/teach.ts +41 -7
  167. package/src/imprint/telemetry.ts +3 -0
  168. package/src/imprint/tool-candidates.ts +8 -7
  169. package/src/imprint/tool-plan.ts +29 -3
  170. package/src/imprint/tracing.ts +18 -2
  171. package/examples/google-flights/_shared/batchexecute.ts +0 -63
  172. package/examples/google-flights/_shared/flights_request.ts +0 -97
  173. package/examples/google-flights/get_flight_booking_details/index.ts +0 -159
  174. package/examples/google-flights/get_flight_booking_details/parser.ts +0 -182
  175. package/examples/google-flights/get_flight_booking_details/playbook.yaml +0 -138
  176. package/examples/google-flights/get_flight_booking_details/request-transform.ts +0 -86
  177. package/examples/google-flights/get_flight_booking_details/workflow.json +0 -98
  178. package/examples/google-flights/get_flight_calendar_prices/request-transform.ts +0 -31
  179. package/examples/google-flights/lookup_airport/parser.ts +0 -66
  180. package/examples/google-flights/lookup_airport/playbook.yaml +0 -47
  181. package/examples/google-flights/lookup_airport/request-transform.ts +0 -20
  182. package/examples/google-hotels/_shared/batchexecute.ts +0 -73
  183. package/examples/google-hotels/_shared/freq.ts +0 -158
  184. package/examples/google-hotels/autocomplete_hotel_location/parser.ts +0 -71
  185. package/examples/google-hotels/autocomplete_hotel_location/playbook.yaml +0 -36
  186. package/examples/google-hotels/autocomplete_hotel_location/request-transform.ts +0 -37
  187. package/examples/google-hotels/autocomplete_hotel_location/workflow.json +0 -36
  188. package/examples/google-hotels/get_hotel_booking_options/index.ts +0 -143
  189. package/examples/google-hotels/get_hotel_booking_options/parser.ts +0 -271
  190. package/examples/google-hotels/get_hotel_booking_options/playbook.yaml +0 -154
  191. package/examples/google-hotels/get_hotel_booking_options/request-transform.ts +0 -154
  192. package/examples/google-hotels/get_hotel_booking_options/workflow.json +0 -84
  193. package/examples/google-hotels/get_hotel_reviews/request-transform.ts +0 -42
  194. package/examples/southwest/search_southwest_flights/backends.json +0 -23
  195. package/examples/southwest/search_southwest_flights/cron.json +0 -19
  196. package/examples/southwest/search_southwest_flights/index.ts +0 -127
  197. package/examples/southwest/search_southwest_flights/playbook.yaml +0 -46
  198. package/examples/southwest/search_southwest_flights/workflow.json +0 -71
  199. /package/examples/google-flights/{get_flight_booking_details → get_flight_booking_options}/package.json +0 -0
  200. /package/examples/google-flights/{lookup_airport → get_flight_location_details}/package.json +0 -0
  201. /package/examples/google-hotels/{autocomplete_hotel_location → get_hotel_details}/package.json +0 -0
  202. /package/examples/google-hotels/{get_hotel_booking_options → get_hotel_photos}/package.json +0 -0
@@ -126,16 +126,29 @@ export {
126
126
 
127
127
  /**
128
128
  * How many compile agents run in parallel when more than one tool is selected.
129
- * Kept at 2 (not 3): bursts of near-identical reverse-engineering requests in a
130
- * short window raise the model's usage-policy safety-filter false-positive rate,
131
- * so we trade a little wall-clock for fewer spurious refusals. Single-tool runs
132
- * still use concurrency 1.
129
+ * Single-tool runs still use concurrency 1.
133
130
  */
134
- const COMPILE_CONCURRENCY = 2;
131
+ const COMPILE_CONCURRENCY = 3;
135
132
 
136
133
  /** Module logger — suppressed during teach's spinner phases via muteLog(). */
137
134
  const log = createLog('teach');
138
135
 
136
+ export function isProviderUsageLimitedError(err: unknown): boolean {
137
+ const text = err instanceof Error ? `${err.name}\n${err.message}` : String(err);
138
+ return (
139
+ /\berror_code=rate_limited\b/i.test(text) ||
140
+ /\byou['’]?ve hit your usage limit\b/i.test(text) ||
141
+ /\btry again at \d{1,2}:\d{2}\s*(?:am|pm)\b/i.test(text)
142
+ );
143
+ }
144
+
145
+ function skippedAfterProviderLimitError(err: unknown): Error {
146
+ const message = err instanceof Error ? err.message : String(err);
147
+ return new Error(
148
+ `skipped because the compile provider hit a usage limit before this tool started. Retry the teach run after the provider reset; root cause: ${message.split('\n')[0] ?? message}`,
149
+ );
150
+ }
151
+
139
152
  // ─── Types ──────────────────────────────────────────────────────────────────
140
153
 
141
154
  interface TeachOptions {
@@ -1932,13 +1945,34 @@ async function compileCandidatePlans(opts: {
1932
1945
  buildPlan,
1933
1946
  );
1934
1947
  const outcomeByKey = new Map<string, CompileOutcome>();
1935
- for (const level of levels) {
1948
+ for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
1949
+ const level = levels[levelIndex];
1950
+ if (!level) continue;
1936
1951
  const levelPlans = level.map((k) => k.plan);
1937
- const levelOutcomes = await mapLimitSettled(levelPlans, concurrency, compileOne);
1952
+ const levelOutcomes = await mapLimitSettled(levelPlans, concurrency, compileOne, {
1953
+ stopOnError: isProviderUsageLimitedError,
1954
+ });
1938
1955
  levelPlans.forEach((plan, i) => {
1939
1956
  const outcome = levelOutcomes[i];
1940
1957
  if (outcome) outcomeByKey.set(plan.workflowKey, outcome);
1941
1958
  });
1959
+ const usageLimitOutcome = levelOutcomes.find(
1960
+ (outcome) => outcome && !outcome.ok && isProviderUsageLimitedError(outcome.error),
1961
+ );
1962
+ if (usageLimitOutcome && !usageLimitOutcome.ok) {
1963
+ const skippedError = skippedAfterProviderLimitError(usageLimitOutcome.error);
1964
+ for (const plan of levelPlans) {
1965
+ if (!outcomeByKey.has(plan.workflowKey)) {
1966
+ outcomeByKey.set(plan.workflowKey, { ok: false, error: skippedError });
1967
+ }
1968
+ }
1969
+ for (const remainingLevel of levels.slice(levelIndex + 1)) {
1970
+ for (const { plan } of remainingLevel) {
1971
+ outcomeByKey.set(plan.workflowKey, { ok: false, error: skippedError });
1972
+ }
1973
+ }
1974
+ break;
1975
+ }
1942
1976
  }
1943
1977
  const outcomes: CompileOutcome[] = opts.plans.map(
1944
1978
  (plan) =>
@@ -9,6 +9,8 @@ interface TelemetryRequestLike {
9
9
 
10
10
  const HARD_TELEMETRY_PATH_PATTERN =
11
11
  /\/(log|gen_204|jserror|ping|beacon|csi|batchlog|metrics|stats|collect|analytics|adsct|pagead|ccm)(?=$|[/?])/i;
12
+ const HARD_TELEMETRY_HOST_PATTERN =
13
+ /(^|[.-])(analytics|telemetry|metrics?|quantummetric|smetrics|demdex|qualtrics|liveramp)([.-]|$)/i;
12
14
  const TERMINAL_EVENT_PATH_PATTERN = /\/events?\/?$/i;
13
15
  const EVENT_COLLECTOR_BODY_PATTERNS = [
14
16
  /"app_(?:version|build)"/i,
@@ -31,6 +33,7 @@ export function isTelemetryRequest(request: TelemetryRequestLike): boolean {
31
33
  return false;
32
34
  }
33
35
 
36
+ if (HARD_TELEMETRY_HOST_PATTERN.test(url.hostname)) return true;
34
37
  if (isTelemetryPath(url.pathname)) return true;
35
38
  if (!TERMINAL_EVENT_PATH_PATTERN.test(url.pathname)) return false;
36
39
  if (request.method.toUpperCase() !== 'POST') return false;
@@ -10,7 +10,9 @@ import { existsSync, readFileSync } from 'node:fs';
10
10
  import { join as pathJoin } from 'node:path';
11
11
  import { z } from 'zod';
12
12
  import { inferAppApiHosts } from './app-api-hosts.ts';
13
+ import { getEndpointKey } from './endpoint-key.ts';
13
14
  import { isSameRegistrableDomain, registrableDomain } from './etld.ts';
15
+ import { compactUrlForLlm } from './llm-url.ts';
14
16
  import { type LLMOptions, extractJsonObject, resolveProvider } from './llm.ts';
15
17
  import { createLog } from './log.ts';
16
18
  import { compactRequestContexts, requestContextDigest } from './request-context.ts';
@@ -205,10 +207,12 @@ export async function detectToolCandidates(
205
207
  const payload = buildToolCandidatePayload(session, {
206
208
  trustSessionScope: opts.trustSessionScope,
207
209
  });
210
+ const payloadChars = JSON.stringify(payload).length;
208
211
 
209
212
  setSpanAttributes(span, {
210
213
  'imprint.events_considered': payload.events.length,
211
214
  'imprint.requests_considered': payload.requests.length,
215
+ 'imprint.detect.payload_chars': payloadChars,
212
216
  });
213
217
 
214
218
  if (payload.requests.length === 0) {
@@ -222,7 +226,7 @@ export async function detectToolCandidates(
222
226
  }
223
227
 
224
228
  log(
225
- `detecting candidate tools from ${payload.events.length} event(s), ${payload.requests.length} request(s)…`,
229
+ `detecting candidate tools from ${payload.events.length} event(s), ${payload.requests.length} request(s); ${Math.round(payloadChars / 1024)} KB payload…`,
226
230
  );
227
231
  const llm = resolveProvider(llmConfig ?? {});
228
232
  const runOnce = async (): Promise<{
@@ -388,7 +392,7 @@ export function buildToolCandidatePayload(
388
392
  seq: request.seq,
389
393
  timestamp: request.timestamp,
390
394
  method: request.method,
391
- url: request.url,
395
+ url: compactUrlForLlm(request.url),
392
396
  resourceType: request.resourceType,
393
397
  status: request.response?.status,
394
398
  mimeType: request.response?.mimeType,
@@ -408,7 +412,7 @@ export function buildToolCandidatePayload(
408
412
 
409
413
  return {
410
414
  site: session.site,
411
- url: session.url,
415
+ url: compactUrlForLlm(session.url),
412
416
  narration: session.narration.map((n) => ({
413
417
  seq: n.seq,
414
418
  timestamp: n.timestamp,
@@ -473,10 +477,7 @@ function candidateRequestGroupKey(request: CandidateRequestPayload): unknown[] {
473
477
  function distinctEndpointFamilies(payload: ToolCandidatePayload): number {
474
478
  const counts = new Map<string, number>();
475
479
  for (const r of payload.requests) {
476
- const url = safeUrl(r.url);
477
- if (!url) continue;
478
- const rpc = /[?&]rpcids?=([^&]+)/.exec(url.search)?.[1];
479
- const key = rpc ? `rpc:${decodeURIComponent(rpc)}` : `${r.method} ${url.pathname}`;
480
+ const key = getEndpointKey(r as unknown as CapturedRequest);
480
481
  counts.set(key, (counts.get(key) ?? 0) + 1);
481
482
  }
482
483
  let families = 0;
@@ -22,6 +22,7 @@ import {
22
22
  resolveAssignedModules,
23
23
  } from './build-plan.ts';
24
24
  import { withTimeout } from './concurrency.ts';
25
+ import { compactUrlForLlm } from './llm-url.ts';
25
26
  import { type ProviderName, resolveProvider } from './llm.ts';
26
27
  import { loadJsonFile } from './load-json.ts';
27
28
  import { createLog } from './log.ts';
@@ -148,7 +149,7 @@ export function buildToolPlanPayload(opts: {
148
149
  seq: r.seq,
149
150
  timestamp: r.timestamp,
150
151
  method: r.method,
151
- url: r.url,
152
+ url: compactUrlForLlm(r.url),
152
153
  status: r.response?.status,
153
154
  mimeType: r.response?.mimeType,
154
155
  headers: truncate(JSON.stringify(r.headers), HEADER_LIMIT) ?? '{}',
@@ -164,7 +165,7 @@ export function buildToolPlanPayload(opts: {
164
165
 
165
166
  return {
166
167
  site: session.site,
167
- url: session.url,
168
+ url: compactUrlForLlm(session.url),
168
169
  tool: {
169
170
  toolName: candidate.toolName,
170
171
  description: candidate.description,
@@ -266,8 +267,12 @@ export async function planToolCompile(opts: {
266
267
  'tool planner',
267
268
  );
268
269
  const plan = stripCodeFences(result.text).trim();
269
- if (plan.length === 0) {
270
+ const validation = validateToolPlan(plan);
271
+ if (!validation.valid) {
270
272
  setSpanAttributes(span, { 'imprint.tool_plan.skipped': true });
273
+ log(
274
+ `tool planning skipped for ${opts.toolName} (${validation.reason}) — compiling without a plan`,
275
+ );
271
276
  return undefined;
272
277
  }
273
278
 
@@ -292,6 +297,27 @@ export async function planToolCompile(opts: {
292
297
  );
293
298
  }
294
299
 
300
+ export function validateToolPlan(plan: string): { valid: true } | { valid: false; reason: string } {
301
+ const trimmed = plan.trim();
302
+ if (trimmed.length === 0) return { valid: false, reason: 'empty plan' };
303
+ if (/^(?:\$\{credential\.[A-Za-z0-9_]+\}|\{credential\.[A-Za-z0-9_]+\})$/.test(trimmed)) {
304
+ return { valid: false, reason: 'looks like a credential placeholder, not a plan' };
305
+ }
306
+ if (/^\{[\s\S]*\}$/.test(trimmed) || /^\[[\s\S]*\]$/.test(trimmed)) {
307
+ return { valid: false, reason: 'looks like JSON, not markdown plan' };
308
+ }
309
+
310
+ const requiredSections = ['### Parameters', '### Requests', '### Response parsing'];
311
+ const missing = requiredSections.filter((section) => !trimmed.includes(section));
312
+ if (missing.length > 0) {
313
+ return { valid: false, reason: `missing required section(s): ${missing.join(', ')}` };
314
+ }
315
+ if (trimmed.length < 200)
316
+ return { valid: false, reason: 'too short to be an implementation plan' };
317
+
318
+ return { valid: true };
319
+ }
320
+
295
321
  function toolPlanDisabled(): boolean {
296
322
  const v = process.env.IMPRINT_NO_TOOL_PLAN;
297
323
  return !!v && !['0', 'false', 'no', 'off'].includes(v.toLowerCase());
@@ -48,6 +48,7 @@ export function suppressTracingInit(): void {
48
48
  suppressInit = true;
49
49
  }
50
50
  const DEFAULT_TRACE_IO_MAX_CHARS = 50_000;
51
+ const DEFAULT_TRACE_ERROR_MAX_CHARS = 2000;
51
52
  const CACHE_READ_MULTIPLIER = 0.1;
52
53
  const CACHE_WRITE_MULTIPLIER = 1.25;
53
54
 
@@ -473,8 +474,23 @@ function cleanAttributeValue(value: unknown): AttributeValue | undefined {
473
474
 
474
475
  function recordSpanError(span: Span, err: unknown): void {
475
476
  const error = err instanceof Error ? err : new Error(String(err));
476
- span.recordException(error);
477
- span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
477
+ const message = sanitizeTraceErrorMessage(error.message);
478
+ const stack = error.stack ? sanitizeTraceErrorMessage(error.stack) : undefined;
479
+ span.recordException({
480
+ name: error.name,
481
+ message,
482
+ stack,
483
+ });
484
+ span.setStatus({ code: SpanStatusCode.ERROR, message });
485
+ }
486
+
487
+ export function sanitizeTraceErrorMessage(
488
+ message: string,
489
+ maxChars = DEFAULT_TRACE_ERROR_MAX_CHARS,
490
+ ): string {
491
+ if (message.length <= maxChars) return message;
492
+ if (maxChars <= 0) return `...[truncated ${message.length} chars]`;
493
+ return `${message.slice(0, maxChars)}\n...[truncated ${message.length - maxChars} chars]`;
478
494
  }
479
495
 
480
496
  function openInferenceProvider(provider: string): string {
@@ -1,63 +0,0 @@
1
- // Decode Google's batchexecute streaming envelope used by every FlightsFrontendUi RPC.
2
- //
3
- // Wire format (verified against recorded seq 69/97/111/667):
4
- // )]}'\n\n <- anti-XSSI magic prefix
5
- // <decimal length>\n <- length line (jsonChars + 2; counts bounding newlines)
6
- // [[...rows...]]\n <- one chunk = single-line JSON array of rows
7
- // ...repeats...
8
- //
9
- // We DELIBERATELY do not slice by the length lines: the stated length is
10
- // `jsonChars + 2`, so naive slice(pos, pos+len) overshoots into the next token.
11
- // Because every chunk is single-line JSON (all interior newlines are escaped as
12
- // \n), splitting on "\n" is exact and robust. Each real RPC result is a row
13
- // ["wrb.fr", <rpcid|null>, "<doubly-encoded JSON payload>", ...]; row[2] must be
14
- // JSON.parse'd a SECOND time. Sidecar rows ("di", "af.httprm", "e") are ignored.
15
-
16
- export function decodeBatchExecute(raw: string): Array<{ rpcid: string | null; payload: any }> {
17
- let text = raw;
18
- if (text.startsWith(")]}'")) {
19
- text = text.slice(4);
20
- }
21
-
22
- const out: Array<{ rpcid: string | null; payload: any }> = [];
23
- const lines = text.split('\n');
24
-
25
- for (const line of lines) {
26
- const trimmed = line.trim();
27
- if (trimmed.length === 0) continue;
28
- // Skip the decimal length marker lines.
29
- if (/^\d+$/.test(trimmed)) continue;
30
- // Chunks are JSON arrays; anything else is noise / partial.
31
- if (trimmed[0] !== '[') continue;
32
-
33
- let chunk: any;
34
- try {
35
- chunk = JSON.parse(trimmed);
36
- } catch {
37
- // Tolerate truncated / partial trailing lines.
38
- continue;
39
- }
40
- if (!Array.isArray(chunk)) continue;
41
-
42
- for (const row of chunk) {
43
- if (!Array.isArray(row) || row[0] !== 'wrb.fr') continue;
44
- const rpcid = typeof row[1] === 'string' ? row[1] : null;
45
- if (typeof row[2] !== 'string') continue;
46
- let payload: any;
47
- try {
48
- payload = JSON.parse(row[2]);
49
- } catch {
50
- continue;
51
- }
52
- out.push({ rpcid, payload });
53
- }
54
- }
55
-
56
- return out;
57
- }
58
-
59
- export function extractRpcPayload(raw: string, rpcid?: string): any {
60
- const frames = decodeBatchExecute(raw);
61
- const frame = rpcid != null ? frames.find((f) => f.rpcid === rpcid) : frames[0];
62
- return frame?.payload;
63
- }
@@ -1,97 +0,0 @@
1
- // Builds the double-encoded `f.req` form body for FlightsFrontendService RPCs
2
- // (GetShoppingResults / GetCalendarPicker / GetBookingResults). Body construction
3
- // is session-independent; f.sid/bl/_reqid/X-Goog-BatchExecute-Bgr are runtime state
4
- // and are intentionally left untouched (transform returns the input url verbatim).
5
- //
6
- // NOTE (correction to spec): the leg encoding is NOT byte-for-byte identical across
7
- // all three RPCs. GetCalendarPicker uses 4-slot legs ([ORIGIN,DEST,TIMES|null,STOPS])
8
- // with the date range living in the outer wrapper; Shopping/Booking use the full
9
- // 15-slot leg with DATE at [6]. Verified by decoding seq 97 vs seq 111.
10
-
11
- // Fresh searches emit wrapper `...,0,0,0,1]` and leg[14]=3 (proven seq 111/140).
12
- // In-page-refined searches use `...,0,1,0,1]` with return-leg[14]=1 (seq 194/425) —
13
- // a UI freshness flag, not a user param; we always emit the fresh form for shopping.
14
- // Booking outbound legs use [14]=3, return legs [14]=1 (seq 764/811).
15
-
16
- function buildLeg(leg: any): any[] {
17
- const out: any[] = new Array(15).fill(null);
18
- out[0] = [[[leg?.origin, 0]]];
19
- out[1] = [[[leg?.dest, 0]]];
20
- out[2] = leg?.times ?? null;
21
- out[3] = leg?.stops ?? 0;
22
- // Google uses slot 4 for included alliances and carrier codes. Slot 5 is an
23
- // exclusion list; putting carrier codes there inverts the filter.
24
- out[4] = leg?.includeAirlines ?? leg?.alliances ?? null;
25
- out[5] = leg?.excludeAirlines ?? null;
26
- out[6] = leg?.date ?? null;
27
- out[7] = leg?.duration ?? null;
28
- out[8] = Array.isArray(leg?.selected)
29
- ? leg.selected.map((s: any) => [s?.origin, s?.date, s?.dest, null, s?.carrier, s?.flightNumber])
30
- : null;
31
- out[14] = 3;
32
- return out;
33
- }
34
-
35
- export function buildFlightSearchParams(params: Record<string, any>): any[] {
36
- const p: Record<string, any> = params ?? {};
37
- // 18-slot positional search array shared by every body (proven seq 111).
38
- const sp: any[] = new Array(18).fill(null);
39
- sp[2] = p.tripType ?? 1; // 1=round, 2=one-way, 3=multi-city
40
- sp[4] = [];
41
- sp[5] = 1;
42
- sp[6] = [p.adults ?? 1, p.children ?? 0, p.infantsSeat ?? 0, p.infantsLap ?? 0];
43
- sp[7] = p.maxPrice != null ? [null, p.maxPrice] : null;
44
- sp[10] = p.bags ? [p.bags.carryOn ?? 0, p.bags.checked ?? 0] : null;
45
- const legs: any[] = Array.isArray(p.legs) ? p.legs : [];
46
- sp[13] = legs.map((l: any) => buildLeg(l));
47
- sp[17] = 1;
48
- return sp;
49
- }
50
-
51
- export function encodeFreq(payload: any): string {
52
- // payload -> inner json string -> embedded in [null, inner] -> x-www-form-urlencoded.
53
- // Verified byte-for-byte against seq 111: `\"`->%5C%22, `[`->%5B, `,`->%2C, `=`->%3D.
54
- const inner = JSON.stringify(payload);
55
- const outer = JSON.stringify([null, inner]);
56
- return 'f.req=' + encodeURIComponent(outer) + '&';
57
- }
58
-
59
- export function transform(
60
- method: string,
61
- url: string,
62
- responses: Record<string, any>,
63
- params?: Record<string, any>,
64
- ): { url: string; body: string } {
65
- // method/responses are part of the contract but unused for body construction
66
- // (the booking token arrives via params.flight_token). Reference to satisfy strict.
67
- void method;
68
- void responses;
69
-
70
- const p: Record<string, any> = params ?? {};
71
- const m = /FlightsFrontendService\/(\w+)/.exec(url);
72
- if (!m || !m[1]) throw new Error(`unrecognized FlightsFrontendService rpc in url: ${url}`);
73
- const rpc: string = m[1];
74
-
75
- const sp = buildFlightSearchParams(p);
76
- let payload: any;
77
-
78
- if (rpc === 'GetShoppingResults') {
79
- payload = [[], sp, 0, 0, 0, 1];
80
- } else if (rpc === 'GetCalendarPicker') {
81
- const legs = sp[13];
82
- if (Array.isArray(legs)) sp[13] = legs.map((l: any) => (Array.isArray(l) ? l.slice(0, 4) : l));
83
- payload = [null, sp, [p.startDate ?? null, p.endDate ?? null], null, [7, 7]];
84
- } else if (rpc === 'GetBookingResults') {
85
- const legs = sp[13];
86
- if (Array.isArray(legs)) {
87
- legs.forEach((l: any, i: number) => {
88
- if (i >= 1 && Array.isArray(l)) l[14] = 1; // return leg(s)
89
- });
90
- }
91
- payload = [[null, p.flight_token ?? null], sp, null, p.tripType === 2 ? 0 : 1];
92
- } else {
93
- throw new Error(`unsupported FlightsFrontendService rpc: ${rpc}`);
94
- }
95
-
96
- return { url, body: encodeFreq(payload) };
97
- }
@@ -1,159 +0,0 @@
1
- /**
2
- * GENERATED by `imprint emit` — DO NOT EDIT BY HAND.
3
- *
4
- * Tool: get_flight_booking_details
5
- * Site: google-flights
6
- * Intent: Get booking/fare details and bookable options for a specific selected flight itinerary on Google Flights.
7
- *
8
- * To regenerate: imprint emit ~/.imprint/google-flights/get_flight_booking_details/workflow.json --force
9
- */
10
-
11
- import { fileURLToPath } from 'node:url';
12
- import { dirname, join } from 'node:path';
13
- import {
14
- executeWorkflow,
15
- type CredentialStore,
16
- } from 'imprint/runtime';
17
- import type { ToolResult, Workflow } from 'imprint/types';
18
-
19
- const WORKFLOW: Workflow = {
20
- "toolName": "get_flight_booking_details",
21
- "intent": {
22
- "description": "Get booking/fare details and bookable options for a specific selected flight itinerary on Google Flights.",
23
- "userSaid": "clicked one of the one-way fares to find more details and booking details; saw the options for another flight; chose round trip fares; kept exploring round trip fare details"
24
- },
25
- "parameters": [
26
- {
27
- "name": "origin",
28
- "type": "string",
29
- "description": "Origin airport IATA code for the outbound leg, e.g. SJC",
30
- "verified": false,
31
- "verifyNote": "annotated"
32
- },
33
- {
34
- "name": "destination",
35
- "type": "string",
36
- "description": "Destination airport IATA code for the outbound leg, e.g. SAN",
37
- "verified": false,
38
- "verifyNote": "annotated"
39
- },
40
- {
41
- "name": "departure_date",
42
- "type": "string",
43
- "description": "Outbound date in YYYY-MM-DD",
44
- "verified": false,
45
- "verifyNote": "annotated"
46
- },
47
- {
48
- "name": "return_date",
49
- "type": "string",
50
- "description": "Return date in YYYY-MM-DD. Empty/omit for a one-way booking.",
51
- "default": "",
52
- "verified": true
53
- },
54
- {
55
- "name": "outbound_flight",
56
- "type": "string",
57
- "description": "Selected outbound flight as 'carrier number', e.g. 'WN 3489'",
58
- "verified": false,
59
- "verifyNote": "annotated"
60
- },
61
- {
62
- "name": "return_flight",
63
- "type": "string",
64
- "description": "Selected return flight as 'carrier number' (round trip only), e.g. 'WN 3540'. Empty for one-way.",
65
- "default": "",
66
- "verified": false,
67
- "verifyNote": "annotated"
68
- },
69
- {
70
- "name": "flight_token",
71
- "type": "string",
72
- "description": "Opaque per-itinerary booking token minted by the search_flights tool's flight_token output for the selected itinerary."
73
- }
74
- ],
75
- "requests": [
76
- {
77
- "method": "POST",
78
- "url": "https://www.google.com/_/FlightsFrontendUi/data/travel.frontend.flights.FlightsFrontendService/GetBookingResults?f.sid=${state.f_sid}&bl=${state.bl}&hl=en-US&soc-app=162&soc-platform=1&soc-device=1&_reqid=1659189&rt=c",
79
- "headers": {
80
- "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8",
81
- "X-Same-Domain": "1",
82
- "Referer": "https://www.google.com/travel/flights",
83
- "x-goog-ext-259736195-jspb": "[\"en-US\",\"US\",\"USD\",2,null,[420],null,null,7,[]]"
84
- },
85
- "body": "f.req=${param.origin}|${param.destination}|${param.departure_date}|${param.return_date}|${param.outbound_flight}|${param.return_flight}|${param.flight_token}&",
86
- "effect": "safe"
87
- }
88
- ],
89
- "site": "google-flights",
90
- "bootstrap": {
91
- "url": "https://www.google.com/travel/flights",
92
- "waitUntil": "domcontentloaded",
93
- "timeoutMs": 30000,
94
- "captures": [
95
- {
96
- "name": "f_sid",
97
- "required": false,
98
- "capability": "browser_bootstrap",
99
- "source": "html_regex",
100
- "pattern": "\"FdrFJe\":\"([^\"]+)\"",
101
- "group": 1
102
- },
103
- {
104
- "name": "bl",
105
- "required": false,
106
- "capability": "browser_bootstrap",
107
- "source": "html_regex",
108
- "pattern": "\"cfb2h\":\"([^\"]+)\"",
109
- "group": 1
110
- }
111
- ]
112
- },
113
- "parserModule": "./parser.ts",
114
- "requestTransformModule": "./request-transform.ts",
115
- "liveVerified": true
116
- };
117
-
118
- export interface GetFlightBookingDetailsInput {
119
- /** Origin airport IATA code for the outbound leg, e.g. SJC */
120
- origin: string;
121
- /** Destination airport IATA code for the outbound leg, e.g. SAN */
122
- destination: string;
123
- /** Outbound date in YYYY-MM-DD */
124
- departure_date: string;
125
- /** Return date in YYYY-MM-DD. Empty/omit for a one-way booking. */
126
- return_date?: string;
127
- /** Selected outbound flight as 'carrier number', e.g. 'WN 3489' */
128
- outbound_flight: string;
129
- /** Selected return flight as 'carrier number' (round trip only), e.g. 'WN 3540'. Empty for one-way. */
130
- return_flight?: string;
131
- /** Opaque per-itinerary booking token minted by the search_flights tool's flight_token output for the selected itinerary. */
132
- flight_token: string;
133
- }
134
-
135
- export async function getFlightBookingDetails(
136
- input: GetFlightBookingDetailsInput,
137
- opts: { credentials?: CredentialStore; fetchImpl?: typeof fetch; initialState?: Record<string, unknown> } = {},
138
- ): Promise<ToolResult> {
139
- const __dirname = dirname(fileURLToPath(import.meta.url));
140
- const params: Record<string, string | number | boolean> = {
141
- return_date: input.return_date ?? "",
142
- return_flight: input.return_flight ?? "",
143
- origin: input.origin,
144
- destination: input.destination,
145
- departure_date: input.departure_date,
146
- outbound_flight: input.outbound_flight,
147
- flight_token: input.flight_token,
148
- };
149
- return executeWorkflow({
150
- workflow: WORKFLOW,
151
- params,
152
- credentials: opts.credentials,
153
- fetchImpl: opts.fetchImpl,
154
- initialState: opts.initialState,
155
- workflowPath: join(__dirname, 'workflow.json'),
156
- });
157
- }
158
-
159
- export { WORKFLOW };