@effect-uai/core 0.9.0 → 0.11.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 (196) hide show
  1. package/dist/{AiError-CrdR1EQi.d.mts → AiError-Bf1pq4BB.d.mts} +8 -3
  2. package/dist/{AiError-CrdR1EQi.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
  3. package/dist/{Audio-DDSKCkSe.d.mts → Audio-CR7CSljJ.d.mts} +2 -2
  4. package/dist/{Audio-DDSKCkSe.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
  5. package/dist/{Image-C_Zjpwhc.d.mts → Image-C4vuBmu0.d.mts} +12 -12
  6. package/dist/{Image-C_Zjpwhc.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
  7. package/dist/{Items-CgKD6Npw.d.mts → Items-BWNu6VGp.d.mts} +118 -92
  8. package/dist/Items-BWNu6VGp.d.mts.map +1 -0
  9. package/dist/{Loop-B5_cqumi.d.mts → Loop-D8nWpowc.d.mts} +2 -2
  10. package/dist/{Loop-B5_cqumi.d.mts.map → Loop-D8nWpowc.d.mts.map} +1 -1
  11. package/dist/{Media-DOLyaygI.d.mts → Media-D_CpcM1Z.d.mts} +1 -1
  12. package/dist/{Media-DOLyaygI.d.mts.map → Media-D_CpcM1Z.d.mts.map} +1 -1
  13. package/dist/{StructuredFormat-CMJFD3sn.d.mts → StructuredFormat-DHaUJBdd.d.mts} +1 -1
  14. package/dist/{StructuredFormat-CMJFD3sn.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
  15. package/dist/{Tool-0XUIT2A2.d.mts → Tool-CXFHCQrT.d.mts} +101 -16
  16. package/dist/Tool-CXFHCQrT.d.mts.map +1 -0
  17. package/dist/{ToolEvent-DYrAUJGs.d.mts → ToolEvent-nVijCrvm.d.mts} +2 -2
  18. package/dist/ToolEvent-nVijCrvm.d.mts.map +1 -0
  19. package/dist/{ToolResult-BZd-cEWW.d.mts → ToolResult-CQqKkcDa.d.mts} +10 -4
  20. package/dist/ToolResult-CQqKkcDa.d.mts.map +1 -0
  21. package/dist/{Toolkit-BKC8b8Xo.d.mts → Toolkit-BYPwwu-j.d.mts} +65 -24
  22. package/dist/Toolkit-BYPwwu-j.d.mts.map +1 -0
  23. package/dist/browser/Browser.d.mts +190 -0
  24. package/dist/browser/Browser.d.mts.map +1 -0
  25. package/dist/browser/Browser.mjs +34 -0
  26. package/dist/browser/Browser.mjs.map +1 -0
  27. package/dist/browser/BrowserError.d.mts +141 -0
  28. package/dist/browser/BrowserError.d.mts.map +1 -0
  29. package/dist/browser/BrowserError.mjs +89 -0
  30. package/dist/browser/BrowserError.mjs.map +1 -0
  31. package/dist/browser/BrowserTool.d.mts +59 -0
  32. package/dist/browser/BrowserTool.d.mts.map +1 -0
  33. package/dist/browser/BrowserTool.mjs +86 -0
  34. package/dist/browser/BrowserTool.mjs.map +1 -0
  35. package/dist/{dist-DWgRbDN3.mjs → dist-C0m0QnUP.mjs} +71 -30
  36. package/dist/dist-C0m0QnUP.mjs.map +1 -0
  37. package/dist/{dist-B04dlFHg.mjs → dist-CcRV3-A5.mjs} +8 -8
  38. package/dist/{dist-B04dlFHg.mjs.map → dist-CcRV3-A5.mjs.map} +1 -1
  39. package/dist/domain/AiError.d.mts +2 -2
  40. package/dist/domain/AiError.mjs +22 -3
  41. package/dist/domain/AiError.mjs.map +1 -1
  42. package/dist/domain/Audio.d.mts +1 -1
  43. package/dist/domain/Citation.d.mts +60 -0
  44. package/dist/domain/Citation.d.mts.map +1 -0
  45. package/dist/domain/Citation.mjs +7 -0
  46. package/dist/domain/Citation.mjs.map +1 -0
  47. package/dist/domain/Image.d.mts +1 -1
  48. package/dist/domain/Items.d.mts +1 -1
  49. package/dist/domain/Items.mjs +13 -3
  50. package/dist/domain/Items.mjs.map +1 -1
  51. package/dist/domain/Media.d.mts +1 -1
  52. package/dist/domain/Music.d.mts +1 -1
  53. package/dist/domain/Research.d.mts +37 -0
  54. package/dist/domain/Research.d.mts.map +1 -0
  55. package/dist/domain/Research.mjs +7 -0
  56. package/dist/domain/Research.mjs.map +1 -0
  57. package/dist/domain/Turn.d.mts +92 -10
  58. package/dist/domain/Turn.d.mts.map +1 -1
  59. package/dist/domain/Turn.mjs +8 -1
  60. package/dist/domain/Turn.mjs.map +1 -1
  61. package/dist/domain/Turn.test.mjs +1 -1
  62. package/dist/embedding-model/Embedding.d.mts +1 -1
  63. package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
  64. package/dist/embedding-model/EmbeddingModel.mjs +2 -2
  65. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  66. package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
  67. package/dist/index.d.mts +22 -13
  68. package/dist/index.mjs +18 -9
  69. package/dist/job/Job.d.mts +80 -0
  70. package/dist/job/Job.d.mts.map +1 -0
  71. package/dist/job/Job.mjs +49 -0
  72. package/dist/job/Job.mjs.map +1 -0
  73. package/dist/language-model/LanguageModel.d.mts +4 -4
  74. package/dist/language-model/LanguageModel.mjs +2 -2
  75. package/dist/language-model/LanguageModel.mjs.map +1 -1
  76. package/dist/language-model/LanguageModel.test.mjs +1 -1
  77. package/dist/loop/Loop.d.mts +1 -1
  78. package/dist/loop/Loop.test.mjs +1 -1
  79. package/dist/music-generator/MusicGenerator.d.mts +2 -2
  80. package/dist/music-generator/MusicGenerator.mjs +4 -4
  81. package/dist/music-generator/MusicGenerator.mjs.map +1 -1
  82. package/dist/music-generator/MusicGenerator.test.mjs +1 -1
  83. package/dist/observability/Metrics.d.mts +1 -1
  84. package/dist/observability/Metrics.test.mjs +2 -2
  85. package/dist/observability/Telemetry.test.mjs +2 -2
  86. package/dist/research/DeepResearch.d.mts +71 -0
  87. package/dist/research/DeepResearch.d.mts.map +1 -0
  88. package/dist/research/DeepResearch.mjs +61 -0
  89. package/dist/research/DeepResearch.mjs.map +1 -0
  90. package/dist/retry/Retry.d.mts +1 -1
  91. package/dist/retry/Retry.test.mjs +1 -1
  92. package/dist/sandbox/Sandbox.mjs +18 -18
  93. package/dist/sandbox/Sandbox.mjs.map +1 -1
  94. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
  95. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +8 -8
  96. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  97. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
  98. package/dist/streaming/JSONL.d.mts +3 -1
  99. package/dist/streaming/JSONL.d.mts.map +1 -1
  100. package/dist/streaming/JSONL.mjs +5 -11
  101. package/dist/streaming/JSONL.mjs.map +1 -1
  102. package/dist/streaming/JSONL.test.mjs +10 -1
  103. package/dist/streaming/JSONL.test.mjs.map +1 -1
  104. package/dist/streaming/SSE.d.mts +5 -3
  105. package/dist/streaming/SSE.d.mts.map +1 -1
  106. package/dist/streaming/SSE.mjs +28 -38
  107. package/dist/streaming/SSE.mjs.map +1 -1
  108. package/dist/streaming/SSE.test.mjs +54 -4
  109. package/dist/streaming/SSE.test.mjs.map +1 -1
  110. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  111. package/dist/structured-format/StructuredFormat.test.mjs +1 -1
  112. package/dist/testing/MockMusicGenerator.d.mts +1 -1
  113. package/dist/testing/MockProvider.d.mts +1 -1
  114. package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
  115. package/dist/tool/Approval.d.mts +19 -7
  116. package/dist/tool/Approval.d.mts.map +1 -1
  117. package/dist/tool/Approval.mjs +18 -11
  118. package/dist/tool/Approval.mjs.map +1 -1
  119. package/dist/tool/Approval.test.mjs +47 -8
  120. package/dist/tool/Approval.test.mjs.map +1 -1
  121. package/dist/tool/HistoryCheck.d.mts +2 -2
  122. package/dist/tool/Tool.d.mts +2 -2
  123. package/dist/tool/Tool.mjs +112 -28
  124. package/dist/tool/Tool.mjs.map +1 -1
  125. package/dist/tool/Tool.test.mjs +127 -3
  126. package/dist/tool/Tool.test.mjs.map +1 -1
  127. package/dist/tool/ToolEvent.d.mts +1 -1
  128. package/dist/tool/ToolEvent.mjs +5 -0
  129. package/dist/tool/ToolEvent.mjs.map +1 -1
  130. package/dist/tool/ToolResult.d.mts +2 -2
  131. package/dist/tool/ToolResult.mjs +15 -6
  132. package/dist/tool/ToolResult.mjs.map +1 -1
  133. package/dist/tool/Toolkit.d.mts +2 -2
  134. package/dist/tool/Toolkit.mjs +62 -22
  135. package/dist/tool/Toolkit.mjs.map +1 -1
  136. package/dist/tool/Toolkit.test.mjs +207 -9
  137. package/dist/tool/Toolkit.test.mjs.map +1 -1
  138. package/dist/transcriber/Transcriber.d.mts +2 -2
  139. package/dist/transcriber/Transcriber.mjs +3 -3
  140. package/dist/transcriber/Transcriber.mjs.map +1 -1
  141. package/dist/transcriber/Transcriber.test.mjs +1 -1
  142. package/dist/web-read/WebRead.d.mts +44 -0
  143. package/dist/web-read/WebRead.d.mts.map +1 -0
  144. package/dist/web-read/WebRead.mjs +18 -0
  145. package/dist/web-read/WebRead.mjs.map +1 -0
  146. package/dist/web-read/WebReadTool.d.mts +57 -0
  147. package/dist/web-read/WebReadTool.d.mts.map +1 -0
  148. package/dist/web-read/WebReadTool.mjs +59 -0
  149. package/dist/web-read/WebReadTool.mjs.map +1 -0
  150. package/dist/web-search/WebSearch.d.mts +1 -1
  151. package/dist/web-search/WebSearch.mjs +1 -1
  152. package/dist/web-search/WebSearch.mjs.map +1 -1
  153. package/dist/web-search/WebSearchTool.d.mts +12 -6
  154. package/dist/web-search/WebSearchTool.d.mts.map +1 -1
  155. package/dist/web-search/WebSearchTool.mjs +10 -5
  156. package/dist/web-search/WebSearchTool.mjs.map +1 -1
  157. package/package.json +39 -3
  158. package/src/browser/Browser.ts +268 -0
  159. package/src/browser/BrowserError.ts +145 -0
  160. package/src/browser/BrowserTool.ts +161 -0
  161. package/src/domain/AiError.ts +27 -1
  162. package/src/domain/Citation.ts +64 -0
  163. package/src/domain/Items.ts +14 -4
  164. package/src/domain/Research.ts +42 -0
  165. package/src/domain/Turn.ts +26 -0
  166. package/src/embedding-model/EmbeddingModel.ts +2 -2
  167. package/src/index.ts +9 -0
  168. package/src/job/Job.ts +125 -0
  169. package/src/language-model/LanguageModel.ts +2 -2
  170. package/src/music-generator/MusicGenerator.ts +4 -4
  171. package/src/research/DeepResearch.ts +137 -0
  172. package/src/sandbox/Sandbox.ts +18 -18
  173. package/src/speech-synthesizer/SpeechSynthesizer.ts +8 -8
  174. package/src/streaming/JSONL.test.ts +19 -0
  175. package/src/streaming/JSONL.ts +6 -38
  176. package/src/streaming/SSE.test.ts +86 -3
  177. package/src/streaming/SSE.ts +36 -80
  178. package/src/tool/Approval.test.ts +55 -5
  179. package/src/tool/Approval.ts +72 -27
  180. package/src/tool/Tool.test.ts +151 -1
  181. package/src/tool/Tool.ts +263 -66
  182. package/src/tool/ToolEvent.ts +5 -0
  183. package/src/tool/ToolResult.ts +14 -8
  184. package/src/tool/Toolkit.test.ts +231 -2
  185. package/src/tool/Toolkit.ts +175 -62
  186. package/src/transcriber/Transcriber.ts +3 -3
  187. package/src/web-read/WebRead.ts +70 -0
  188. package/src/web-read/WebReadTool.ts +96 -0
  189. package/src/web-search/WebSearch.ts +1 -1
  190. package/src/web-search/WebSearchTool.ts +11 -5
  191. package/dist/Items-CgKD6Npw.d.mts.map +0 -1
  192. package/dist/Tool-0XUIT2A2.d.mts.map +0 -1
  193. package/dist/ToolEvent-DYrAUJGs.d.mts.map +0 -1
  194. package/dist/ToolResult-BZd-cEWW.d.mts.map +0 -1
  195. package/dist/Toolkit-BKC8b8Xo.d.mts.map +0 -1
  196. package/dist/dist-DWgRbDN3.mjs.map +0 -1
@@ -0,0 +1,59 @@
1
+ import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
+ import { fromEffectSchema, make } from "../tool/Tool.mjs";
3
+ import { read } from "./WebRead.mjs";
4
+ import { Effect, Schema } from "effect";
5
+ //#region src/web-read/WebReadTool.ts
6
+ var WebReadTool_exports = /* @__PURE__ */ __exportAll({ webReadTool: () => webReadTool });
7
+ /**
8
+ * Model-facing argument schema. Deliberately narrow: the URL is the only
9
+ * thing that is genuinely the model's call. Output format and the content
10
+ * cost ceiling are app policy, pinned on the constructor instead, so they
11
+ * are guaranteed rather than left to the model to remember each call.
12
+ */
13
+ const WebReadToolArgs = Schema.Struct({ url: Schema.String.annotate({ description: "Absolute URL of the page to read." }) });
14
+ /** `title / url` header plus the extracted content - what a model reads best. */
15
+ const defaultRender = (response, maxChars) => {
16
+ return `${response.title === void 0 ? response.url : `${response.title}\n${response.url}`}\n\n${response.content.length > maxChars ? `${response.content.slice(0, maxChars)}\n[content truncated at ${maxChars} characters]` : response.content}`;
17
+ };
18
+ /**
19
+ * The canonical web-read tool. Its `R` is just `WebRead`, so providing any
20
+ * read-provider Layer satisfies it and the model-facing contract (name,
21
+ * description, schema) is identical no matter which backend answers - swap
22
+ * `FirecrawlRead.layer` for `JinaRead.layer` and the tool the model sees
23
+ * does not change. Drops straight into a `Toolkit` next to `webSearchTool`
24
+ * (search finds the URLs, read fetches the ones worth reading in full).
25
+ *
26
+ * `Output` is the rendered page; failures stay typed on the `AiError`
27
+ * channel. To let the model recover from a failed read, wrap the toolkit in
28
+ * `Toolkit.describeFailures(AiError.describe)`.
29
+ *
30
+ * This is a simple default implementation for quick use. For more elaborate
31
+ * cases (a different output contract, more model-facing knobs), build your
32
+ * own tool with `Tool.make` over the same `read` helper.
33
+ */
34
+ const webReadTool = (options) => {
35
+ const name = options?.name ?? "web_read";
36
+ const format = options?.format ?? "markdown";
37
+ const maxChars = options?.maxChars ?? 2e4;
38
+ const render = options?.render ?? ((response) => defaultRender(response, maxChars));
39
+ return make({
40
+ name,
41
+ description: "Read a web page and return its content as clean text. Use it to read a promising URL in full (e.g. one found via search).",
42
+ inputSchema: fromEffectSchema(WebReadToolArgs),
43
+ run: (args) => read({
44
+ url: args.url,
45
+ format
46
+ }).pipe(Effect.tap((r) => Effect.annotateCurrentSpan("web_read.content_chars", r.content.length)), Effect.map(render), Effect.withSpan(name, {
47
+ kind: "client",
48
+ attributes: {
49
+ "web_read.url": args.url,
50
+ "web_read.format": format,
51
+ "web_read.max_chars": maxChars
52
+ }
53
+ }))
54
+ });
55
+ };
56
+ //#endregion
57
+ export { WebReadTool_exports as t, webReadTool };
58
+
59
+ //# sourceMappingURL=WebReadTool.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"WebReadTool.mjs","names":["Tool.make","Tool.fromEffectSchema"],"sources":["../../src/web-read/WebReadTool.ts"],"sourcesContent":["import { Effect, Schema } from \"effect\"\nimport type * as AiError from \"../domain/AiError.js\"\nimport * as Tool from \"../tool/Tool.js\"\nimport { read, type ReadFormat, type ReadResponse, WebRead } from \"./WebRead.js\"\n\n/**\n * Model-facing argument schema. Deliberately narrow: the URL is the only\n * thing that is genuinely the model's call. Output format and the content\n * cost ceiling are app policy, pinned on the constructor instead, so they\n * are guaranteed rather than left to the model to remember each call.\n */\nconst WebReadToolArgs = Schema.Struct({\n url: Schema.String.annotate({\n description: \"Absolute URL of the page to read.\",\n }),\n})\n\nexport type WebReadToolArgs = typeof WebReadToolArgs.Type\n\n/** `title / url` header plus the extracted content - what a model reads best. */\nconst defaultRender = (response: ReadResponse, maxChars: number): string => {\n const header = response.title === undefined ? response.url : `${response.title}\\n${response.url}`\n const body =\n response.content.length > maxChars\n ? `${response.content.slice(0, maxChars)}\\n[content truncated at ${maxChars} characters]`\n : response.content\n return `${header}\\n\\n${body}`\n}\n\nexport type WebReadToolOptions = {\n /** Tool name the model sees. Default `\"web_read\"`. */\n readonly name?: string\n /**\n * App-fixed representation for every read - a policy choice, not a model\n * knob. Default `\"markdown\"` (what models read best).\n */\n readonly format?: ReadFormat\n /**\n * App-fixed ceiling on rendered content characters per call - a context\n * cost guard, not exposed to the model. Default `20000`.\n */\n readonly maxChars?: number\n /**\n * Override how a read response is rendered into the model-facing string.\n * Default: `title / url` header plus the content, truncated at `maxChars`.\n */\n readonly render?: (response: ReadResponse) => string\n}\n\n/**\n * The canonical web-read tool. Its `R` is just `WebRead`, so providing any\n * read-provider Layer satisfies it and the model-facing contract (name,\n * description, schema) is identical no matter which backend answers - swap\n * `FirecrawlRead.layer` for `JinaRead.layer` and the tool the model sees\n * does not change. Drops straight into a `Toolkit` next to `webSearchTool`\n * (search finds the URLs, read fetches the ones worth reading in full).\n *\n * `Output` is the rendered page; failures stay typed on the `AiError`\n * channel. To let the model recover from a failed read, wrap the toolkit in\n * `Toolkit.describeFailures(AiError.describe)`.\n *\n * This is a simple default implementation for quick use. For more elaborate\n * cases (a different output contract, more model-facing knobs), build your\n * own tool with `Tool.make` over the same `read` helper.\n */\nexport const webReadTool = (\n options?: WebReadToolOptions,\n): Tool.Tool<string, WebReadToolArgs, never, string, AiError.AiError, WebRead> => {\n const name = options?.name ?? \"web_read\"\n const format = options?.format ?? \"markdown\"\n const maxChars = options?.maxChars ?? 20_000\n const render = options?.render ?? ((response: ReadResponse) => defaultRender(response, maxChars))\n return Tool.make({\n name,\n description:\n \"Read a web page and return its content as clean text. Use it to read a promising URL in full (e.g. one found via search).\",\n inputSchema: Tool.fromEffectSchema(WebReadToolArgs),\n // The call is wrapped in a tracing span so agent traces show the tool\n // invocation (url, format, content length) nested under the model turn.\n // `withSpan` is a no-op until a Tracer is installed, so this is free by\n // default and adds nothing to the requirements.\n run: (args) =>\n read({ url: args.url, format }).pipe(\n Effect.tap((r) => Effect.annotateCurrentSpan(\"web_read.content_chars\", r.content.length)),\n Effect.map(render),\n Effect.withSpan(name, {\n kind: \"client\",\n attributes: {\n \"web_read.url\": args.url,\n \"web_read.format\": format,\n \"web_read.max_chars\": maxChars,\n },\n }),\n ),\n })\n}\n"],"mappings":";;;;;;;;;;;;AAWA,MAAM,kBAAkB,OAAO,OAAO,EACpC,KAAK,OAAO,OAAO,SAAS,EAC1B,aAAa,qCACd,CAAC,EACH,CAAC;;AAKF,MAAM,iBAAiB,UAAwB,aAA6B;AAM1E,QAAO,GALQ,SAAS,UAAU,KAAA,IAAY,SAAS,MAAM,GAAG,SAAS,MAAM,IAAI,SAAS,MAK3E,MAHf,SAAS,QAAQ,SAAS,WACtB,GAAG,SAAS,QAAQ,MAAM,GAAG,SAAS,CAAC,0BAA0B,SAAS,gBAC1E,SAAS;;;;;;;;;;;;;;;;;;AAwCjB,MAAa,eACX,YACgF;CAChF,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,WAAW,SAAS,YAAY;CACtC,MAAM,SAAS,SAAS,YAAY,aAA2B,cAAc,UAAU,SAAS;AAChG,QAAOA,KAAU;EACf;EACA,aACE;EACF,aAAaC,iBAAsB,gBAAgB;EAKnD,MAAM,SACJ,KAAK;GAAE,KAAK,KAAK;GAAK;GAAQ,CAAC,CAAC,KAC9B,OAAO,KAAK,MAAM,OAAO,oBAAoB,0BAA0B,EAAE,QAAQ,OAAO,CAAC,EACzF,OAAO,IAAI,OAAO,EAClB,OAAO,SAAS,MAAM;GACpB,MAAM;GACN,YAAY;IACV,gBAAgB,KAAK;IACrB,mBAAmB;IACnB,sBAAsB;IACvB;GACF,CAAC,CACH;EACJ,CAAC"}
@@ -1,4 +1,4 @@
1
- import { t as AiError } from "../AiError-CrdR1EQi.mjs";
1
+ import { t as AiError } from "../AiError-Bf1pq4BB.mjs";
2
2
  import { Context, DateTime, Effect, Schema } from "effect";
3
3
 
4
4
  //#region src/web-search/WebSearch.d.ts
@@ -35,7 +35,7 @@ const SearchRecency = Schema.Literals([
35
35
  */
36
36
  var WebSearch = class extends Context.Service()("@betalyra/effect-uai/WebSearch") {};
37
37
  /** Run one web search against whichever provider Layer is in scope. */
38
- const search = (request) => Effect.flatMap(WebSearch.asEffect(), (s) => s.search(request));
38
+ const search = (request) => Effect.flatMap(WebSearch, (s) => s.search(request));
39
39
  //#endregion
40
40
  export { SearchRecency, WebSearch, search, WebSearch_exports as t };
41
41
 
@@ -1 +1 @@
1
- {"version":3,"file":"WebSearch.mjs","names":[],"sources":["../../src/web-search/WebSearch.ts"],"sourcesContent":["import { Context, type DateTime, Effect, Schema } from \"effect\"\nimport type * as AiError from \"../domain/AiError.js\"\n\n// ---------------------------------------------------------------------------\n// Recency\n// ---------------------------------------------------------------------------\n\n/**\n * Convenience freshness enum shared by every provider. A coarse \"published\n * within the last …\" filter that maps onto each provider's native recency\n * knob (Perplexity `search_recency_filter`, Brave `freshness`, Tavily\n * `time_range`, You.com `freshness`), or onto a computed published-date\n * range where a provider has no enum (Exa).\n *\n * For a precise window use {@link CommonSearchRequest.startDate} /\n * {@link CommonSearchRequest.endDate} instead; `recency` is the sugar for\n * the common \"past week / past month\" case.\n *\n * Exposed as a `Schema` so {@link \"../web-search/WebSearchTool\".webSearchTool}\n * can hand it straight to a model as a tool parameter; the\n * {@link SearchRecency} type is derived from it.\n */\nexport const SearchRecency = Schema.Literals([\"hour\", \"day\", \"week\", \"month\", \"year\"])\n\nexport type SearchRecency = typeof SearchRecency.Type\n\n// ---------------------------------------------------------------------------\n// Request\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-provider web-search request. A field earns a place here only if\n * most of the supported providers honor it and a developer would expect it\n * from a generic search interface; everything provider-specific (Exa\n * `type`, Tavily `searchDepth`, Brave `goggles`, pagination `offset`,\n * `safeSearch`, `topic`, …) lives on that provider's typed request.\n *\n * Note the one structural difference from `EmbeddingModel` /\n * `Transcriber`: there is no `model`. None of the pure-`/search` endpoints\n * take one - what looks model-shaped on a provider (Exa `type`, Tavily\n * `searchDepth`) is a mode knob, so it stays on the provider-typed request.\n */\nexport type CommonSearchRequest = {\n /** The search query. The one field every provider requires. */\n readonly query: string\n /**\n * Upper bound on the number of results to return in this one call.\n * Providers cap differently (Perplexity / Tavily / Brave at ~20, Exa /\n * You.com at ~100); a value above a provider's cap is clamped by that\n * provider. There is no common pagination - only two providers paginate\n * and only shallowly, so `offset` is provider-typed.\n */\n readonly maxResults?: number\n /** Restrict results to these domains (e.g. `[\"docs.foo.com\"]`). */\n readonly includeDomains?: ReadonlyArray<string>\n /** Drop results from these domains. */\n readonly excludeDomains?: ReadonlyArray<string>\n /**\n * Coarse freshness filter - see {@link SearchRecency}. Sugar over an\n * explicit `startDate`; ignored where {@link startDate} is also set and\n * the provider takes only one.\n */\n readonly recency?: SearchRecency\n /** Published-after bound. The precise alternative to {@link recency}. */\n readonly startDate?: DateTime.DateTime\n /** Published-before bound. */\n readonly endDate?: DateTime.DateTime\n /** ISO 3166-1 alpha-2 country code to localize results (e.g. `\"us\"`). */\n readonly country?: string\n /** ISO 639-1 language code to bias results toward (e.g. `\"en\"`). */\n readonly language?: string\n}\n\n// ---------------------------------------------------------------------------\n// Response\n// ---------------------------------------------------------------------------\n\n/**\n * One normalized web-search hit. A flat record with optionals rather than a\n * tagged union: the providers differ by *which fields are present*, not by\n * *kind*. The provider's untouched result object is always on `raw`, so a\n * caller holding a typed provider tag can recover anything not promoted\n * here (`author`, `favicon`, multi-chunk `snippets[]`, …).\n */\nexport type SearchResult = {\n /** Canonical result URL. Always present. */\n readonly url: string\n /** Result title / headline, when the provider returns one. */\n readonly title?: string\n /**\n * Primary short excerpt - the one text field all providers return (under\n * various wire names: `snippet`, `content`, `description`). The richer\n * extract fields (`text`, `highlights`, `summary`) belong to the\n * out-of-scope extract feature and stay on the provider-typed result.\n */\n readonly snippet?: string\n /** Publication date, when the provider reports one. */\n readonly publishedDate?: DateTime.DateTime\n /**\n * Relevance score, only from providers that rank (Exa, Tavily);\n * `undefined` for providers that return an unscored list. Kept despite\n * being non-universal because ranking is core to what a \"search result\"\n * means - and `undefined` cleanly says \"this backend doesn't score.\"\n */\n readonly score?: number\n /** The provider-native result object, never lossy. */\n readonly raw: unknown\n}\n\nexport type SearchResponse = {\n readonly results: ReadonlyArray<SearchResult>\n /**\n * The provider-native top-level response object. Cost / usage reporting\n * is deliberately not modeled on this surface yet: providers disagree on\n * both what (Exa returns USD, Tavily credits, Brave nothing in-body) and\n * where (Brave reports cost in response headers), so it is deferred to the\n * unified usage design (see plans/usage-tracking.md). Anything a provider\n * reports survives here on `raw`.\n */\n readonly raw: unknown\n}\n\n// ---------------------------------------------------------------------------\n// Service + helper\n// ---------------------------------------------------------------------------\n\n/**\n * The portable web-search surface: one operation, `search`. Pure search is\n * the one thing every provider does, so - unlike `SpeechSynthesizer` /\n * `Transcriber` - this capability needs no marker tags; every provider\n * Layer can answer.\n */\nexport type WebSearchService = {\n readonly search: (request: CommonSearchRequest) => Effect.Effect<SearchResponse, AiError.AiError>\n}\n\n/**\n * Generic web-search service tag. Yield this for provider-portable code;\n * yield a provider tag (`PerplexitySearch`, `ExaSearch`, …) when you need\n * that provider's own knobs. A provider's `layer` registers both.\n */\nexport class WebSearch extends Context.Service<WebSearch, WebSearchService>()(\n \"@betalyra/effect-uai/WebSearch\",\n) {}\n\n/** Run one web search against whichever provider Layer is in scope. */\nexport const search = (\n request: CommonSearchRequest,\n): Effect.Effect<SearchResponse, AiError.AiError, WebSearch> =>\n Effect.flatMap(WebSearch.asEffect(), (s) => s.search(request))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,gBAAgB,OAAO,SAAS;CAAC;CAAQ;CAAO;CAAQ;CAAS;CAAO,CAAC;;;;;;AAuHtF,IAAa,YAAb,cAA+B,QAAQ,SAAsC,CAC3E,iCACD,CAAC;;AAGF,MAAa,UACX,YAEA,OAAO,QAAQ,UAAU,UAAU,GAAG,MAAM,EAAE,OAAO,QAAQ,CAAC"}
1
+ {"version":3,"file":"WebSearch.mjs","names":[],"sources":["../../src/web-search/WebSearch.ts"],"sourcesContent":["import { Context, type DateTime, Effect, Schema } from \"effect\"\nimport type * as AiError from \"../domain/AiError.js\"\n\n// ---------------------------------------------------------------------------\n// Recency\n// ---------------------------------------------------------------------------\n\n/**\n * Convenience freshness enum shared by every provider. A coarse \"published\n * within the last …\" filter that maps onto each provider's native recency\n * knob (Perplexity `search_recency_filter`, Brave `freshness`, Tavily\n * `time_range`, You.com `freshness`), or onto a computed published-date\n * range where a provider has no enum (Exa).\n *\n * For a precise window use {@link CommonSearchRequest.startDate} /\n * {@link CommonSearchRequest.endDate} instead; `recency` is the sugar for\n * the common \"past week / past month\" case.\n *\n * Exposed as a `Schema` so {@link \"../web-search/WebSearchTool\".webSearchTool}\n * can hand it straight to a model as a tool parameter; the\n * {@link SearchRecency} type is derived from it.\n */\nexport const SearchRecency = Schema.Literals([\"hour\", \"day\", \"week\", \"month\", \"year\"])\n\nexport type SearchRecency = typeof SearchRecency.Type\n\n// ---------------------------------------------------------------------------\n// Request\n// ---------------------------------------------------------------------------\n\n/**\n * Cross-provider web-search request. A field earns a place here only if\n * most of the supported providers honor it and a developer would expect it\n * from a generic search interface; everything provider-specific (Exa\n * `type`, Tavily `searchDepth`, Brave `goggles`, pagination `offset`,\n * `safeSearch`, `topic`, …) lives on that provider's typed request.\n *\n * Note the one structural difference from `EmbeddingModel` /\n * `Transcriber`: there is no `model`. None of the pure-`/search` endpoints\n * take one - what looks model-shaped on a provider (Exa `type`, Tavily\n * `searchDepth`) is a mode knob, so it stays on the provider-typed request.\n */\nexport type CommonSearchRequest = {\n /** The search query. The one field every provider requires. */\n readonly query: string\n /**\n * Upper bound on the number of results to return in this one call.\n * Providers cap differently (Perplexity / Tavily / Brave at ~20, Exa /\n * You.com at ~100); a value above a provider's cap is clamped by that\n * provider. There is no common pagination - only two providers paginate\n * and only shallowly, so `offset` is provider-typed.\n */\n readonly maxResults?: number\n /** Restrict results to these domains (e.g. `[\"docs.foo.com\"]`). */\n readonly includeDomains?: ReadonlyArray<string>\n /** Drop results from these domains. */\n readonly excludeDomains?: ReadonlyArray<string>\n /**\n * Coarse freshness filter - see {@link SearchRecency}. Sugar over an\n * explicit `startDate`; ignored where {@link startDate} is also set and\n * the provider takes only one.\n */\n readonly recency?: SearchRecency\n /** Published-after bound. The precise alternative to {@link recency}. */\n readonly startDate?: DateTime.DateTime\n /** Published-before bound. */\n readonly endDate?: DateTime.DateTime\n /** ISO 3166-1 alpha-2 country code to localize results (e.g. `\"us\"`). */\n readonly country?: string\n /** ISO 639-1 language code to bias results toward (e.g. `\"en\"`). */\n readonly language?: string\n}\n\n// ---------------------------------------------------------------------------\n// Response\n// ---------------------------------------------------------------------------\n\n/**\n * One normalized web-search hit. A flat record with optionals rather than a\n * tagged union: the providers differ by *which fields are present*, not by\n * *kind*. The provider's untouched result object is always on `raw`, so a\n * caller holding a typed provider tag can recover anything not promoted\n * here (`author`, `favicon`, multi-chunk `snippets[]`, …).\n */\nexport type SearchResult = {\n /** Canonical result URL. Always present. */\n readonly url: string\n /** Result title / headline, when the provider returns one. */\n readonly title?: string\n /**\n * Primary short excerpt - the one text field all providers return (under\n * various wire names: `snippet`, `content`, `description`). The richer\n * extract fields (`text`, `highlights`, `summary`) belong to the\n * out-of-scope extract feature and stay on the provider-typed result.\n */\n readonly snippet?: string\n /** Publication date, when the provider reports one. */\n readonly publishedDate?: DateTime.DateTime\n /**\n * Relevance score, only from providers that rank (Exa, Tavily);\n * `undefined` for providers that return an unscored list. Kept despite\n * being non-universal because ranking is core to what a \"search result\"\n * means - and `undefined` cleanly says \"this backend doesn't score.\"\n */\n readonly score?: number\n /** The provider-native result object, never lossy. */\n readonly raw: unknown\n}\n\nexport type SearchResponse = {\n readonly results: ReadonlyArray<SearchResult>\n /**\n * The provider-native top-level response object. Cost / usage reporting\n * is deliberately not modeled on this surface yet: providers disagree on\n * both what (Exa returns USD, Tavily credits, Brave nothing in-body) and\n * where (Brave reports cost in response headers), so it is deferred to the\n * unified usage design (see plans/usage-tracking.md). Anything a provider\n * reports survives here on `raw`.\n */\n readonly raw: unknown\n}\n\n// ---------------------------------------------------------------------------\n// Service + helper\n// ---------------------------------------------------------------------------\n\n/**\n * The portable web-search surface: one operation, `search`. Pure search is\n * the one thing every provider does, so - unlike `SpeechSynthesizer` /\n * `Transcriber` - this capability needs no marker tags; every provider\n * Layer can answer.\n */\nexport type WebSearchService = {\n readonly search: (request: CommonSearchRequest) => Effect.Effect<SearchResponse, AiError.AiError>\n}\n\n/**\n * Generic web-search service tag. Yield this for provider-portable code;\n * yield a provider tag (`PerplexitySearch`, `ExaSearch`, …) when you need\n * that provider's own knobs. A provider's `layer` registers both.\n */\nexport class WebSearch extends Context.Service<WebSearch, WebSearchService>()(\n \"@betalyra/effect-uai/WebSearch\",\n) {}\n\n/** Run one web search against whichever provider Layer is in scope. */\nexport const search = (\n request: CommonSearchRequest,\n): Effect.Effect<SearchResponse, AiError.AiError, WebSearch> =>\n Effect.flatMap(WebSearch, (s) => s.search(request))\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAsBA,MAAa,gBAAgB,OAAO,SAAS;CAAC;CAAQ;CAAO;CAAQ;CAAS;CAAO,CAAC;;;;;;AAuHtF,IAAa,YAAb,cAA+B,QAAQ,SAAsC,CAC3E,iCACD,CAAC;;AAGF,MAAa,UACX,YAEA,OAAO,QAAQ,YAAY,MAAM,EAAE,OAAO,QAAQ,CAAC"}
@@ -1,4 +1,5 @@
1
- import { c as Tool } from "../Tool-0XUIT2A2.mjs";
1
+ import { l as Tool } from "../Tool-CXFHCQrT.mjs";
2
+ import { t as AiError } from "../AiError-Bf1pq4BB.mjs";
2
3
  import { SearchResult, WebSearch } from "./WebSearch.mjs";
3
4
  import { Schema } from "effect";
4
5
 
@@ -47,12 +48,17 @@ type WebSearchToolOptions = {
47
48
  * `LanguageModel` tool-calling loop.
48
49
  *
49
50
  * The model controls only `query` and `recency`; domain scoping and the
50
- * result cap are app policy, fixed here on the constructor. `Output` is
51
- * rendered text, not raw `SearchResult[]`: the model reads the list, while
52
- * the app still gets the structured results through the normal tool-result
53
- * channel. Pass `render` to change the format.
51
+ * result cap are app policy, fixed here on the constructor. `Output` is the
52
+ * rendered text (the model reads a numbered list better than raw
53
+ * `SearchResult[]`; pass `render` to change the format); failures stay typed
54
+ * on the `AiError` channel. To let the model recover from a failed search,
55
+ * wrap the toolkit in `Toolkit.describeFailures(AiError.describe)`.
56
+ *
57
+ * This is a simple default implementation for quick use. For more elaborate
58
+ * cases (a different output contract, more model-facing knobs), build your
59
+ * own tool with `Tool.make` over the same `search` helper.
54
60
  */
55
- declare const webSearchTool: (options?: WebSearchToolOptions) => Tool<string, WebSearchToolArgs, never, string, WebSearch>;
61
+ declare const webSearchTool: (options?: WebSearchToolOptions) => Tool<string, WebSearchToolArgs, never, string, AiError, WebSearch>;
56
62
  //#endregion
57
63
  export { WebSearchToolArgs, WebSearchToolOptions, WebSearchTool_d_exports as t, webSearchTool };
58
64
  //# sourceMappingURL=WebSearchTool.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"WebSearchTool.d.mts","names":[],"sources":["../../src/web-search/WebSearchTool.ts"],"mappings":";;;;;;;;;;;;;;;;cAYM,iBAAA,EAAiB,MAAA,CAAA,MAAA;EAAA;;;KAKX,iBAAA,UAA2B,iBAAA,CAAkB,IAAA;AAAA,KAa7C,oBAAA;EAlBW,iEAoBZ,IAAA;EApBY;;;;EAAA,SAyBZ,UAAA;;;;;WAKA,cAAA,GAAiB,aAAA;WAEjB,cAAA,GAAiB,aAAA;;AA3B5B;;;WAgCW,MAAA,IAAU,OAAA,EAAS,aAAA,CAAc,YAAA;AAAA;AAnB5C;;;;;;;;;;;;;;AAAA,cAoCa,aAAA,GACX,OAAA,GAAU,oBAAA,KACT,IAAA,SAAkB,iBAAA,iBAAkC,SAAA"}
1
+ {"version":3,"file":"WebSearchTool.d.mts","names":[],"sources":["../../src/web-search/WebSearchTool.ts"],"mappings":";;;;;;;;;;;;;;;;;cAaM,iBAAA,EAAiB,MAAA,CAAA,MAAA;EAAA;;;KAKX,iBAAA,UAA2B,iBAAA,CAAkB,IAAA;AAAA,KAa7C,oBAAA;EAlBW,iEAoBZ,IAAA;EApBY;;;;EAAA,SAyBZ,UAAA;;;;;WAKA,cAAA,GAAiB,aAAA;WAEjB,cAAA,GAAiB,aAAA;;AA3B5B;;;WAgCW,MAAA,IAAU,OAAA,EAAS,aAAA,CAAc,YAAA;AAAA;AAnB5C;;;;;;;;;;;;;;;;;;;AAAA,cAyCa,aAAA,GACX,OAAA,GAAU,oBAAA,KACT,IAAA,SAAkB,iBAAA,iBAAkC,OAAA,EAAiB,SAAA"}
@@ -1,6 +1,6 @@
1
1
  import { n as __exportAll } from "../chunk-uyGKjUfl.mjs";
2
- import { SearchRecency, search } from "./WebSearch.mjs";
3
2
  import { fromEffectSchema, make } from "../tool/Tool.mjs";
3
+ import { SearchRecency, search } from "./WebSearch.mjs";
4
4
  import { Effect, Schema } from "effect";
5
5
  //#region src/web-search/WebSearchTool.ts
6
6
  var WebSearchTool_exports = /* @__PURE__ */ __exportAll({ webSearchTool: () => webSearchTool });
@@ -30,10 +30,15 @@ const defaultRender = (results) => results.length === 0 ? "No results found." :
30
30
  * `LanguageModel` tool-calling loop.
31
31
  *
32
32
  * The model controls only `query` and `recency`; domain scoping and the
33
- * result cap are app policy, fixed here on the constructor. `Output` is
34
- * rendered text, not raw `SearchResult[]`: the model reads the list, while
35
- * the app still gets the structured results through the normal tool-result
36
- * channel. Pass `render` to change the format.
33
+ * result cap are app policy, fixed here on the constructor. `Output` is the
34
+ * rendered text (the model reads a numbered list better than raw
35
+ * `SearchResult[]`; pass `render` to change the format); failures stay typed
36
+ * on the `AiError` channel. To let the model recover from a failed search,
37
+ * wrap the toolkit in `Toolkit.describeFailures(AiError.describe)`.
38
+ *
39
+ * This is a simple default implementation for quick use. For more elaborate
40
+ * cases (a different output contract, more model-facing knobs), build your
41
+ * own tool with `Tool.make` over the same `search` helper.
37
42
  */
38
43
  const webSearchTool = (options) => {
39
44
  const name = options?.name ?? "web_search";
@@ -1 +1 @@
1
- {"version":3,"file":"WebSearchTool.mjs","names":["Tool.make","Tool.fromEffectSchema"],"sources":["../../src/web-search/WebSearchTool.ts"],"sourcesContent":["import { Effect, Schema } from \"effect\"\nimport * as Tool from \"../tool/Tool.js\"\nimport { search, SearchRecency, type SearchResult, WebSearch } from \"./WebSearch.js\"\n\n/**\n * Model-facing argument schema. Deliberately narrow: `query` plus the one\n * filter that is genuinely the model's call - `recency` (it knows whether a\n * question is about current events or evergreen). App-policy scoping\n * (`includeDomains` / `excludeDomains`) and the `maxResults` cost ceiling\n * are pinned on the constructor instead, so they are guaranteed rather than\n * left to the model to remember each call.\n */\nconst WebSearchToolArgs = Schema.Struct({\n query: Schema.String,\n recency: Schema.optional(SearchRecency),\n})\n\nexport type WebSearchToolArgs = typeof WebSearchToolArgs.Type\n\n/** Numbered `title / url / snippet` list - what a model reads best. */\nconst defaultRender = (results: ReadonlyArray<SearchResult>): string =>\n results.length === 0\n ? \"No results found.\"\n : results\n .map((r, i) => {\n const head = `${i + 1}. ${r.title ?? r.url}\\n ${r.url}`\n return r.snippet === undefined ? head : `${head}\\n ${r.snippet}`\n })\n .join(\"\\n\\n\")\n\nexport type WebSearchToolOptions = {\n /** Tool name the model sees. Default `\"web_search\"`. */\n readonly name?: string\n /**\n * App-fixed ceiling on results per call - a cost guard, not exposed to\n * the model. Default `5`.\n */\n readonly maxResults?: number\n /**\n * App-fixed allowlist applied to every search. A policy scope (\"only\n * search our docs\"), not a model knob - so it is enforced, not requested.\n */\n readonly includeDomains?: ReadonlyArray<string>\n /** App-fixed denylist applied to every search (\"never return X\"). */\n readonly excludeDomains?: ReadonlyArray<string>\n /**\n * Override how results are rendered into the model-facing string.\n * Default: a numbered `title / url / snippet` list.\n */\n readonly render?: (results: ReadonlyArray<SearchResult>) => string\n}\n\n/**\n * The canonical web-search tool. Its `R` is just `WebSearch`, so providing\n * any search-provider Layer satisfies it and the model-facing contract\n * (name, description, schema) is identical no matter which backend answers\n * - swap `PerplexitySearch.layer` for `ExaSearch.layer` and the tool the\n * model sees does not change. Drops straight into a `Toolkit` and the\n * `LanguageModel` tool-calling loop.\n *\n * The model controls only `query` and `recency`; domain scoping and the\n * result cap are app policy, fixed here on the constructor. `Output` is\n * rendered text, not raw `SearchResult[]`: the model reads the list, while\n * the app still gets the structured results through the normal tool-result\n * channel. Pass `render` to change the format.\n */\nexport const webSearchTool = (\n options?: WebSearchToolOptions,\n): Tool.Tool<string, WebSearchToolArgs, never, string, WebSearch> => {\n const name = options?.name ?? \"web_search\"\n const render = options?.render ?? defaultRender\n const maxResults = options?.maxResults ?? 5\n const includeDomains = options?.includeDomains\n const excludeDomains = options?.excludeDomains\n return Tool.make({\n name,\n description:\n \"Search the web for current information. Returns ranked results with titles, URLs, and snippets.\",\n inputSchema: Tool.fromEffectSchema(WebSearchToolArgs),\n // The call is wrapped in a tracing span so agent traces show the tool\n // invocation (query, filters, result count) nested under the model turn.\n // `withSpan` is a no-op until a Tracer is installed, so this is free by\n // default and adds nothing to the requirements.\n run: (args) =>\n search({\n query: args.query,\n maxResults,\n ...(args.recency !== undefined && { recency: args.recency }),\n ...(includeDomains !== undefined && { includeDomains }),\n ...(excludeDomains !== undefined && { excludeDomains }),\n }).pipe(\n Effect.tap((r) => Effect.annotateCurrentSpan(\"web_search.result_count\", r.results.length)),\n Effect.map((r) => render(r.results)),\n Effect.withSpan(name, {\n kind: \"client\",\n attributes: {\n \"web_search.query\": args.query,\n \"web_search.max_results\": maxResults,\n ...(args.recency !== undefined && { \"web_search.recency\": args.recency }),\n ...(includeDomains !== undefined && { \"web_search.include_domains\": includeDomains }),\n ...(excludeDomains !== undefined && { \"web_search.exclude_domains\": excludeDomains }),\n },\n }),\n ),\n })\n}\n"],"mappings":";;;;;;;;;;;;;;AAYA,MAAM,oBAAoB,OAAO,OAAO;CACtC,OAAO,OAAO;CACd,SAAS,OAAO,SAAS,cAAc;CACxC,CAAC;;AAKF,MAAM,iBAAiB,YACrB,QAAQ,WAAW,IACf,sBACA,QACG,KAAK,GAAG,MAAM;CACb,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,OAAO,EAAE;AACpD,QAAO,EAAE,YAAY,KAAA,IAAY,OAAO,GAAG,KAAK,OAAO,EAAE;EACzD,CACD,KAAK,OAAO;;;;;;;;;;;;;;;AAsCrB,MAAa,iBACX,YACmE;CACnE,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,aAAa,SAAS,cAAc;CAC1C,MAAM,iBAAiB,SAAS;CAChC,MAAM,iBAAiB,SAAS;AAChC,QAAOA,KAAU;EACf;EACA,aACE;EACF,aAAaC,iBAAsB,kBAAkB;EAKrD,MAAM,SACJ,OAAO;GACL,OAAO,KAAK;GACZ;GACA,GAAI,KAAK,YAAY,KAAA,KAAa,EAAE,SAAS,KAAK,SAAS;GAC3D,GAAI,mBAAmB,KAAA,KAAa,EAAE,gBAAgB;GACtD,GAAI,mBAAmB,KAAA,KAAa,EAAE,gBAAgB;GACvD,CAAC,CAAC,KACD,OAAO,KAAK,MAAM,OAAO,oBAAoB,2BAA2B,EAAE,QAAQ,OAAO,CAAC,EAC1F,OAAO,KAAK,MAAM,OAAO,EAAE,QAAQ,CAAC,EACpC,OAAO,SAAS,MAAM;GACpB,MAAM;GACN,YAAY;IACV,oBAAoB,KAAK;IACzB,0BAA0B;IAC1B,GAAI,KAAK,YAAY,KAAA,KAAa,EAAE,sBAAsB,KAAK,SAAS;IACxE,GAAI,mBAAmB,KAAA,KAAa,EAAE,8BAA8B,gBAAgB;IACpF,GAAI,mBAAmB,KAAA,KAAa,EAAE,8BAA8B,gBAAgB;IACrF;GACF,CAAC,CACH;EACJ,CAAC"}
1
+ {"version":3,"file":"WebSearchTool.mjs","names":["Tool.make","Tool.fromEffectSchema"],"sources":["../../src/web-search/WebSearchTool.ts"],"sourcesContent":["import { Effect, Schema } from \"effect\"\nimport type * as AiError from \"../domain/AiError.js\"\nimport * as Tool from \"../tool/Tool.js\"\nimport { search, SearchRecency, type SearchResult, WebSearch } from \"./WebSearch.js\"\n\n/**\n * Model-facing argument schema. Deliberately narrow: `query` plus the one\n * filter that is genuinely the model's call - `recency` (it knows whether a\n * question is about current events or evergreen). App-policy scoping\n * (`includeDomains` / `excludeDomains`) and the `maxResults` cost ceiling\n * are pinned on the constructor instead, so they are guaranteed rather than\n * left to the model to remember each call.\n */\nconst WebSearchToolArgs = Schema.Struct({\n query: Schema.String,\n recency: Schema.optional(SearchRecency),\n})\n\nexport type WebSearchToolArgs = typeof WebSearchToolArgs.Type\n\n/** Numbered `title / url / snippet` list - what a model reads best. */\nconst defaultRender = (results: ReadonlyArray<SearchResult>): string =>\n results.length === 0\n ? \"No results found.\"\n : results\n .map((r, i) => {\n const head = `${i + 1}. ${r.title ?? r.url}\\n ${r.url}`\n return r.snippet === undefined ? head : `${head}\\n ${r.snippet}`\n })\n .join(\"\\n\\n\")\n\nexport type WebSearchToolOptions = {\n /** Tool name the model sees. Default `\"web_search\"`. */\n readonly name?: string\n /**\n * App-fixed ceiling on results per call - a cost guard, not exposed to\n * the model. Default `5`.\n */\n readonly maxResults?: number\n /**\n * App-fixed allowlist applied to every search. A policy scope (\"only\n * search our docs\"), not a model knob - so it is enforced, not requested.\n */\n readonly includeDomains?: ReadonlyArray<string>\n /** App-fixed denylist applied to every search (\"never return X\"). */\n readonly excludeDomains?: ReadonlyArray<string>\n /**\n * Override how results are rendered into the model-facing string.\n * Default: a numbered `title / url / snippet` list.\n */\n readonly render?: (results: ReadonlyArray<SearchResult>) => string\n}\n\n/**\n * The canonical web-search tool. Its `R` is just `WebSearch`, so providing\n * any search-provider Layer satisfies it and the model-facing contract\n * (name, description, schema) is identical no matter which backend answers\n * - swap `PerplexitySearch.layer` for `ExaSearch.layer` and the tool the\n * model sees does not change. Drops straight into a `Toolkit` and the\n * `LanguageModel` tool-calling loop.\n *\n * The model controls only `query` and `recency`; domain scoping and the\n * result cap are app policy, fixed here on the constructor. `Output` is the\n * rendered text (the model reads a numbered list better than raw\n * `SearchResult[]`; pass `render` to change the format); failures stay typed\n * on the `AiError` channel. To let the model recover from a failed search,\n * wrap the toolkit in `Toolkit.describeFailures(AiError.describe)`.\n *\n * This is a simple default implementation for quick use. For more elaborate\n * cases (a different output contract, more model-facing knobs), build your\n * own tool with `Tool.make` over the same `search` helper.\n */\nexport const webSearchTool = (\n options?: WebSearchToolOptions,\n): Tool.Tool<string, WebSearchToolArgs, never, string, AiError.AiError, WebSearch> => {\n const name = options?.name ?? \"web_search\"\n const render = options?.render ?? defaultRender\n const maxResults = options?.maxResults ?? 5\n const includeDomains = options?.includeDomains\n const excludeDomains = options?.excludeDomains\n return Tool.make({\n name,\n description:\n \"Search the web for current information. Returns ranked results with titles, URLs, and snippets.\",\n inputSchema: Tool.fromEffectSchema(WebSearchToolArgs),\n // The call is wrapped in a tracing span so agent traces show the tool\n // invocation (query, filters, result count) nested under the model turn.\n // `withSpan` is a no-op until a Tracer is installed, so this is free by\n // default and adds nothing to the requirements.\n run: (args) =>\n search({\n query: args.query,\n maxResults,\n ...(args.recency !== undefined && { recency: args.recency }),\n ...(includeDomains !== undefined && { includeDomains }),\n ...(excludeDomains !== undefined && { excludeDomains }),\n }).pipe(\n Effect.tap((r) => Effect.annotateCurrentSpan(\"web_search.result_count\", r.results.length)),\n Effect.map((r) => render(r.results)),\n Effect.withSpan(name, {\n kind: \"client\",\n attributes: {\n \"web_search.query\": args.query,\n \"web_search.max_results\": maxResults,\n ...(args.recency !== undefined && { \"web_search.recency\": args.recency }),\n ...(includeDomains !== undefined && { \"web_search.include_domains\": includeDomains }),\n ...(excludeDomains !== undefined && { \"web_search.exclude_domains\": excludeDomains }),\n },\n }),\n ),\n })\n}\n"],"mappings":";;;;;;;;;;;;;;AAaA,MAAM,oBAAoB,OAAO,OAAO;CACtC,OAAO,OAAO;CACd,SAAS,OAAO,SAAS,cAAc;CACxC,CAAC;;AAKF,MAAM,iBAAiB,YACrB,QAAQ,WAAW,IACf,sBACA,QACG,KAAK,GAAG,MAAM;CACb,MAAM,OAAO,GAAG,IAAI,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,OAAO,EAAE;AACpD,QAAO,EAAE,YAAY,KAAA,IAAY,OAAO,GAAG,KAAK,OAAO,EAAE;EACzD,CACD,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;AA2CrB,MAAa,iBACX,YACoF;CACpF,MAAM,OAAO,SAAS,QAAQ;CAC9B,MAAM,SAAS,SAAS,UAAU;CAClC,MAAM,aAAa,SAAS,cAAc;CAC1C,MAAM,iBAAiB,SAAS;CAChC,MAAM,iBAAiB,SAAS;AAChC,QAAOA,KAAU;EACf;EACA,aACE;EACF,aAAaC,iBAAsB,kBAAkB;EAKrD,MAAM,SACJ,OAAO;GACL,OAAO,KAAK;GACZ;GACA,GAAI,KAAK,YAAY,KAAA,KAAa,EAAE,SAAS,KAAK,SAAS;GAC3D,GAAI,mBAAmB,KAAA,KAAa,EAAE,gBAAgB;GACtD,GAAI,mBAAmB,KAAA,KAAa,EAAE,gBAAgB;GACvD,CAAC,CAAC,KACD,OAAO,KAAK,MAAM,OAAO,oBAAoB,2BAA2B,EAAE,QAAQ,OAAO,CAAC,EAC1F,OAAO,KAAK,MAAM,OAAO,EAAE,QAAQ,CAAC,EACpC,OAAO,SAAS,MAAM;GACpB,MAAM;GACN,YAAY;IACV,oBAAoB,KAAK;IACzB,0BAA0B;IAC1B,GAAI,KAAK,YAAY,KAAA,KAAa,EAAE,sBAAsB,KAAK,SAAS;IACxE,GAAI,mBAAmB,KAAA,KAAa,EAAE,8BAA8B,gBAAgB;IACpF,GAAI,mBAAmB,KAAA,KAAa,EAAE,8BAA8B,gBAAgB;IACrF;GACF,CAAC,CACH;EACJ,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/core",
3
- "version": "0.9.0",
3
+ "version": "0.11.0",
4
4
  "description": "Low-level primitives (loop, conversation, items, tools, streaming codecs) for building AI agents with Effect.",
5
5
  "keywords": [
6
6
  "agents",
@@ -42,6 +42,34 @@
42
42
  "types": "./dist/domain/Audio.d.mts",
43
43
  "import": "./dist/domain/Audio.mjs"
44
44
  },
45
+ "./Citation": {
46
+ "types": "./dist/domain/Citation.d.mts",
47
+ "import": "./dist/domain/Citation.mjs"
48
+ },
49
+ "./Research": {
50
+ "types": "./dist/domain/Research.d.mts",
51
+ "import": "./dist/domain/Research.mjs"
52
+ },
53
+ "./Job": {
54
+ "types": "./dist/job/Job.d.mts",
55
+ "import": "./dist/job/Job.mjs"
56
+ },
57
+ "./DeepResearch": {
58
+ "types": "./dist/research/DeepResearch.d.mts",
59
+ "import": "./dist/research/DeepResearch.mjs"
60
+ },
61
+ "./Browser": {
62
+ "types": "./dist/browser/Browser.d.mts",
63
+ "import": "./dist/browser/Browser.mjs"
64
+ },
65
+ "./BrowserError": {
66
+ "types": "./dist/browser/BrowserError.d.mts",
67
+ "import": "./dist/browser/BrowserError.mjs"
68
+ },
69
+ "./BrowserTool": {
70
+ "types": "./dist/browser/BrowserTool.d.mts",
71
+ "import": "./dist/browser/BrowserTool.mjs"
72
+ },
45
73
  "./Image": {
46
74
  "types": "./dist/domain/Image.d.mts",
47
75
  "import": "./dist/domain/Image.mjs"
@@ -110,6 +138,14 @@
110
138
  "types": "./dist/transcriber/Transcriber.d.mts",
111
139
  "import": "./dist/transcriber/Transcriber.mjs"
112
140
  },
141
+ "./WebRead": {
142
+ "types": "./dist/web-read/WebRead.d.mts",
143
+ "import": "./dist/web-read/WebRead.mjs"
144
+ },
145
+ "./WebReadTool": {
146
+ "types": "./dist/web-read/WebReadTool.d.mts",
147
+ "import": "./dist/web-read/WebReadTool.mjs"
148
+ },
113
149
  "./WebSearch": {
114
150
  "types": "./dist/web-search/WebSearch.d.mts",
115
151
  "import": "./dist/web-search/WebSearch.mjs"
@@ -206,11 +242,11 @@
206
242
  "@standard-schema/spec": "^1.1.0"
207
243
  },
208
244
  "devDependencies": {
209
- "effect": "4.0.0-beta.57",
245
+ "effect": "4.0.0-beta.94",
210
246
  "typescript": "^6.0.3"
211
247
  },
212
248
  "peerDependencies": {
213
- "effect": "4.0.0-beta.57"
249
+ "effect": ">=4.0.0-beta.94 <5.0.0"
214
250
  },
215
251
  "scripts": {
216
252
  "build": "tsdown",
@@ -0,0 +1,268 @@
1
+ import { Brand, Context, type Duration, Effect, Scope } from "effect"
2
+ import type * as BrowserError from "./BrowserError.js"
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Identity
6
+ // ---------------------------------------------------------------------------
7
+
8
+ /**
9
+ * Session identifier. Branded `string` so providers don't accidentally
10
+ * accept a raw user-supplied id, and so a session id can't be passed where
11
+ * another service's id is expected. Each provider mints these per its own
12
+ * scheme; core code treats them opaquely. Mint via `BrowserSessionId(...)`.
13
+ */
14
+ export type BrowserSessionId = Brand.Branded<string, "BrowserSessionId">
15
+ export const BrowserSessionId = Brand.nominal<BrowserSessionId>()
16
+
17
+ /** Reference returned by `list`; enough to `attach`. */
18
+ export type BrowserSessionRef = {
19
+ readonly id: BrowserSessionId
20
+ /** Current top-level URL, when the provider reports it. */
21
+ readonly url?: string
22
+ }
23
+
24
+ /**
25
+ * Output representation for {@link BrowserSession.content}. `markdown` is
26
+ * the default.
27
+ */
28
+ export type PageFormat = "markdown" | "html"
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Create request
32
+ // ---------------------------------------------------------------------------
33
+
34
+ /**
35
+ * Cross-provider session-create request. Provider-specific knobs (proxy,
36
+ * stealth, persistent profile, ...) live on each provider's narrowed
37
+ * request type which extends this.
38
+ */
39
+ export type CommonSessionRequest = {
40
+ /**
41
+ * Hard max lifetime of the session. Enforced locally (the handle is
42
+ * disposed and further calls fail with `BrowserSessionExpired`) and,
43
+ * where the provider supports it, passed through as a server-side cap so
44
+ * a crashed client still gets the session reclaimed. The provider cap
45
+ * must be >= the local reaper. Accepts millis, a string like
46
+ * `"5 minutes"`, or a `Duration`.
47
+ */
48
+ readonly timeout?: Duration.Input
49
+ /**
50
+ * Idle teardown: dispose the session after this much inactivity, the
51
+ * timer resetting on each operation. Opt-in; omit for no idle limit.
52
+ */
53
+ readonly idleTimeout?: Duration.Input
54
+ /** Initial viewport size. Providers default differently. */
55
+ readonly viewport?: { readonly width: number; readonly height: number }
56
+ }
57
+
58
+ // ---------------------------------------------------------------------------
59
+ // Observation shapes
60
+ // ---------------------------------------------------------------------------
61
+
62
+ export type BoundingBox = {
63
+ readonly x: number
64
+ readonly y: number
65
+ readonly width: number
66
+ readonly height: number
67
+ }
68
+
69
+ /**
70
+ * One element returned by {@link BrowserSession.query}. Serializable data,
71
+ * not a live handle (handles go stale across navigation and do not cross a
72
+ * provider boundary cleanly).
73
+ */
74
+ export type ElementInfo = {
75
+ /**
76
+ * Reference usable as a selector for a follow-up action. Valid until the
77
+ * next navigation; do not cache across `goto` or a page-changing click.
78
+ */
79
+ readonly ref: string
80
+ readonly tag: string
81
+ readonly text?: string
82
+ readonly attributes: Readonly<Record<string, string>>
83
+ readonly box?: BoundingBox
84
+ }
85
+
86
+ /** Accessibility-tree node returned by {@link BrowserSession.snapshot}. */
87
+ export type AxNode = {
88
+ readonly role: string
89
+ readonly name?: string
90
+ readonly value?: string
91
+ /**
92
+ * Reference usable as a selector for a follow-up action. Valid until the
93
+ * next navigation; do not cache across `goto` or a page-changing click.
94
+ */
95
+ readonly ref?: string
96
+ readonly children: ReadonlyArray<AxNode>
97
+ }
98
+
99
+ // ---------------------------------------------------------------------------
100
+ // Interaction shapes
101
+ // ---------------------------------------------------------------------------
102
+
103
+ export type ScrollDirection = "up" | "down" | "left" | "right"
104
+
105
+ export type ScrollOptions = {
106
+ readonly direction: ScrollDirection
107
+ /** Distance in CSS pixels. Provider default applies when omitted. */
108
+ readonly pixels?: number
109
+ }
110
+
111
+ export type ScreenshotOptions = {
112
+ /** Clip to the first element matching this selector. */
113
+ readonly selector?: string
114
+ /** Capture the full scrollable page rather than the viewport. */
115
+ readonly fullPage?: boolean
116
+ }
117
+
118
+ export type Cookie = {
119
+ readonly name: string
120
+ readonly value: string
121
+ readonly domain?: string
122
+ readonly path?: string
123
+ readonly expires?: number
124
+ readonly httpOnly?: boolean
125
+ readonly secure?: boolean
126
+ readonly sameSite?: "Strict" | "Lax" | "None"
127
+ }
128
+
129
+ /** Read / write the session's cookie jar. Needed for authenticated flows. */
130
+ export type CookieApi = {
131
+ readonly get: Effect.Effect<ReadonlyArray<Cookie>, BrowserError.BrowserError>
132
+ readonly set: (cookies: ReadonlyArray<Cookie>) => Effect.Effect<void, BrowserError.BrowserError>
133
+ }
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // Live session handle
137
+ // ---------------------------------------------------------------------------
138
+
139
+ /**
140
+ * Live session handle. Actions take a CSS selector string (so `#id` is
141
+ * just `#id`). Methods are plain `Effect` values; disposal is the scope
142
+ * finalizer, not a method here (see {@link BrowserService}). Anything not
143
+ * covered by a verb is reachable via `evaluate`. Per-operation time budgets
144
+ * are the provider's default; wrap any call in `Effect.timeout` for a
145
+ * tighter bound.
146
+ */
147
+ export type BrowserSession = {
148
+ readonly id: BrowserSessionId
149
+
150
+ // Navigation and waiting
151
+ readonly goto: (url: string) => Effect.Effect<void, BrowserError.BrowserError>
152
+ readonly waitFor: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
153
+
154
+ // Interaction
155
+ readonly click: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
156
+ readonly dblclick: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
157
+ readonly fill: (selector: string, text: string) => Effect.Effect<void, BrowserError.BrowserError>
158
+ readonly type: (selector: string, text: string) => Effect.Effect<void, BrowserError.BrowserError>
159
+ readonly press: (key: string) => Effect.Effect<void, BrowserError.BrowserError>
160
+ readonly hover: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
161
+ readonly focus: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
162
+ readonly select: (
163
+ selector: string,
164
+ value: string,
165
+ ) => Effect.Effect<void, BrowserError.BrowserError>
166
+ readonly check: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
167
+ readonly uncheck: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
168
+ readonly scroll: (options: ScrollOptions) => Effect.Effect<void, BrowserError.BrowserError>
169
+ readonly scrollIntoView: (selector: string) => Effect.Effect<void, BrowserError.BrowserError>
170
+
171
+ // Observation
172
+ readonly content: (format?: PageFormat) => Effect.Effect<string, BrowserError.BrowserError>
173
+ readonly screenshot: (
174
+ options?: ScreenshotOptions,
175
+ ) => Effect.Effect<Uint8Array, BrowserError.BrowserError>
176
+ readonly snapshot: Effect.Effect<AxNode, BrowserError.BrowserError>
177
+ readonly query: (
178
+ selector: string,
179
+ ) => Effect.Effect<ReadonlyArray<ElementInfo>, BrowserError.BrowserError>
180
+
181
+ // State and escape hatch
182
+ readonly cookies: CookieApi
183
+ readonly evaluate: (script: string) => Effect.Effect<unknown, BrowserError.BrowserError>
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // Service definition
188
+ // ---------------------------------------------------------------------------
189
+
190
+ /**
191
+ * Cross-provider browser service.
192
+ *
193
+ * Lifetime: `create` and `attach` return a handle in `Scope.Scope`. The
194
+ * scope finalizer disposes the session (close the transport, destroy or
195
+ * detach the remote session, kill the local engine). Bind with
196
+ * `Effect.scoped` for a single call, or hold a wider scope to keep a
197
+ * session warm across many calls. This is why there is no `dispose()` on
198
+ * {@link BrowserSession}; the scope is the lifecycle handle.
199
+ */
200
+ export type BrowserService = {
201
+ /**
202
+ * Provision a new session, bound to the caller's scope. Disposed on
203
+ * scope close.
204
+ */
205
+ readonly create: (
206
+ request: CommonSessionRequest,
207
+ ) => Effect.Effect<BrowserSession, BrowserError.BrowserError, Scope.Scope>
208
+
209
+ /**
210
+ * Re-acquire an existing session by id. Scope-bound, but the finalizer
211
+ * detaches rather than destroying, for sessions kept warm beyond a
212
+ * single run.
213
+ */
214
+ readonly attach: (
215
+ id: BrowserSessionId,
216
+ ) => Effect.Effect<BrowserSession, BrowserError.BrowserError, Scope.Scope>
217
+
218
+ /** Enumerate sessions visible to the configured account / project. */
219
+ readonly list: Effect.Effect<ReadonlyArray<BrowserSessionRef>, BrowserError.BrowserError>
220
+
221
+ /**
222
+ * Destroy a session from outside its owning scope. Most callers should
223
+ * let the scope finalizer handle this.
224
+ */
225
+ readonly destroy: (id: BrowserSessionId) => Effect.Effect<void, BrowserError.BrowserError>
226
+ }
227
+
228
+ export class Browser extends Context.Service<Browser, BrowserService>()(
229
+ "@betalyra/effect-uai/Browser",
230
+ ) {}
231
+
232
+ // ---------------------------------------------------------------------------
233
+ // Free helpers
234
+ // ---------------------------------------------------------------------------
235
+
236
+ /**
237
+ * Provision a session bound to the caller's scope. Drop the `Scope.Scope`
238
+ * requirement with `Effect.scoped`.
239
+ */
240
+ export const create = (
241
+ request: CommonSessionRequest,
242
+ ): Effect.Effect<BrowserSession, BrowserError.BrowserError, Browser | Scope.Scope> =>
243
+ Effect.flatMap(Browser, (s) => s.create(request))
244
+
245
+ /**
246
+ * Re-acquire an existing session by id. Same scope semantics as `create`,
247
+ * but the finalizer detaches rather than destroying.
248
+ */
249
+ export const attach = (
250
+ id: BrowserSessionId,
251
+ ): Effect.Effect<BrowserSession, BrowserError.BrowserError, Browser | Scope.Scope> =>
252
+ Effect.flatMap(Browser, (s) => s.attach(id))
253
+
254
+ /** Enumerate sessions for the configured account / project. */
255
+ export const list: Effect.Effect<
256
+ ReadonlyArray<BrowserSessionRef>,
257
+ BrowserError.BrowserError,
258
+ Browser
259
+ > = Effect.flatMap(Browser, (s) => s.list)
260
+
261
+ /**
262
+ * Destroy a session from outside its owning scope. Most callers should let
263
+ * the scope finalizer handle this automatically.
264
+ */
265
+ export const destroy = (
266
+ id: BrowserSessionId,
267
+ ): Effect.Effect<void, BrowserError.BrowserError, Browser> =>
268
+ Effect.flatMap(Browser, (s) => s.destroy(id))