@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,145 @@
1
+ import { Data, Duration, Match } from "effect"
2
+
3
+ /**
4
+ * Failed to provision a browser session: the transport could not connect,
5
+ * a launched engine did not come up, or the provider refused to mint a
6
+ * session. Provider's structured reason on `reason`; the raw transport /
7
+ * SDK error on `raw`.
8
+ */
9
+ export class BrowserCreateFailed extends Data.TaggedError("BrowserCreateFailed")<{
10
+ provider: string
11
+ reason?: string
12
+ raw: unknown
13
+ }> {}
14
+
15
+ /**
16
+ * The requested session id does not exist. Raised by `attach` / `destroy`
17
+ * against a stale or never-known id. Distinct from
18
+ * {@link BrowserSessionExpired}, which is a session that existed and has
19
+ * since ended.
20
+ */
21
+ export class BrowserSessionNotFound extends Data.TaggedError("BrowserSessionNotFound")<{
22
+ provider: string
23
+ id: string
24
+ }> {}
25
+
26
+ /**
27
+ * An operation was issued against a session that has ended: it reached its
28
+ * max lifetime (the create-time `timeout`), was destroyed, or the provider
29
+ * reaped it. The handle is dead; `create` or `attach` a live session.
30
+ * Distinct from {@link BrowserTimeout} (a single live operation was too
31
+ * slow).
32
+ */
33
+ export class BrowserSessionExpired extends Data.TaggedError("BrowserSessionExpired")<{
34
+ provider: string
35
+ id: string
36
+ }> {}
37
+
38
+ /**
39
+ * A single operation exceeded its own time budget while the session was
40
+ * still alive (a navigation that never settled, a `waitFor` selector that
41
+ * never appeared). Carries the breached budget for observability.
42
+ */
43
+ export class BrowserTimeout extends Data.TaggedError("BrowserTimeout")<{
44
+ provider: string
45
+ operation: "create" | "attach" | "navigate" | "action" | "query"
46
+ budget?: Duration.Duration
47
+ raw?: unknown
48
+ }> {}
49
+
50
+ /**
51
+ * An interaction or observation failed at the transport / protocol layer:
52
+ * a selector resolved to nothing, an element was not actionable, an
53
+ * `evaluate` script threw, a navigation errored. `selector` and `reason`
54
+ * carry what is known.
55
+ */
56
+ export class BrowserActionFailed extends Data.TaggedError("BrowserActionFailed")<{
57
+ provider: string
58
+ operation: "navigate" | "interact" | "observe" | "evaluate" | "cookies"
59
+ selector?: string
60
+ reason?: string
61
+ raw: unknown
62
+ }> {}
63
+
64
+ /**
65
+ * Provider rejected the request because an account / project quota or
66
+ * concurrency limit was exceeded. Separate from {@link BrowserCreateFailed}
67
+ * so callers can back off and retry.
68
+ */
69
+ export class BrowserQuotaExceeded extends Data.TaggedError("BrowserQuotaExceeded")<{
70
+ provider: string
71
+ reason?: string
72
+ raw: unknown
73
+ }> {}
74
+
75
+ /**
76
+ * Caller's request shape is malformed for the wired provider. Adapters
77
+ * should reject at the request-schema layer where possible; this is the
78
+ * runtime fallback.
79
+ */
80
+ export class BrowserInvalidRequest extends Data.TaggedError("BrowserInvalidRequest")<{
81
+ provider: string
82
+ param?: string
83
+ reason?: string
84
+ raw?: unknown
85
+ }> {}
86
+
87
+ /**
88
+ * Transport could not authenticate against the provider. Subtype mirrors
89
+ * the model-side `AiError.AuthFailed` shape for consistency.
90
+ */
91
+ export type BrowserAuthSubtype = "auth" | "permission" | "billing" | "quota"
92
+
93
+ export class BrowserAuthFailed extends Data.TaggedError("BrowserAuthFailed")<{
94
+ provider: string
95
+ subtype: BrowserAuthSubtype
96
+ raw: unknown
97
+ }> {}
98
+
99
+ /**
100
+ * The wired provider does not implement the requested operation for these
101
+ * inputs. Distinct from {@link BrowserInvalidRequest} (malformed request)
102
+ * and from compile-time capability markers gating blanket provider gaps.
103
+ */
104
+ export class BrowserUnsupported extends Data.TaggedError("BrowserUnsupported")<{
105
+ provider: string
106
+ capability: string
107
+ reason?: string
108
+ }> {}
109
+
110
+ export type BrowserError =
111
+ | BrowserCreateFailed
112
+ | BrowserSessionNotFound
113
+ | BrowserSessionExpired
114
+ | BrowserTimeout
115
+ | BrowserActionFailed
116
+ | BrowserQuotaExceeded
117
+ | BrowserInvalidRequest
118
+ | BrowserAuthFailed
119
+ | BrowserUnsupported
120
+
121
+ const withReason = (base: string, reason: string | undefined): string =>
122
+ reason === undefined ? base : `${base}: ${reason}`
123
+
124
+ /**
125
+ * Short human-readable description of an error, for logs and model-facing
126
+ * failure messages. Prose, not a contract; branch on `_tag` instead.
127
+ */
128
+ export const describe: (e: BrowserError) => string = Match.type<BrowserError>().pipe(
129
+ Match.discriminatorsExhaustive("_tag")({
130
+ BrowserCreateFailed: (e) => withReason("the browser session could not be created", e.reason),
131
+ BrowserSessionNotFound: () => "the browser session does not exist",
132
+ BrowserSessionExpired: () => "the browser session has expired",
133
+ BrowserTimeout: (e) => `the ${e.operation} operation timed out`,
134
+ BrowserActionFailed: (e) =>
135
+ withReason(
136
+ `the ${e.operation} failed${e.selector === undefined ? "" : ` on ${e.selector}`}`,
137
+ e.reason,
138
+ ),
139
+ BrowserQuotaExceeded: (e) => withReason("the browser quota is exceeded", e.reason),
140
+ BrowserInvalidRequest: (e) => withReason("the request was invalid", e.reason),
141
+ BrowserAuthFailed: (e) => `browser authentication failed (${e.subtype})`,
142
+ BrowserUnsupported: (e) =>
143
+ withReason(`this browser does not support ${e.capability}`, e.reason),
144
+ }),
145
+ )
@@ -0,0 +1,161 @@
1
+ import { Effect, Schema } from "effect"
2
+ import * as Tool from "../tool/Tool.js"
3
+ import * as Toolkit from "../tool/Toolkit.js"
4
+ import type { BrowserSession } from "./Browser.js"
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Model-facing argument schemas. Annotated so the descriptions reach the
8
+ // model through the rendered JSON Schema (JSDoc does not).
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const GotoArgs = Schema.Struct({
12
+ url: Schema.String.annotate({ description: "Absolute URL to navigate to." }),
13
+ })
14
+
15
+ const ClickArgs = Schema.Struct({
16
+ ref: Schema.String.annotate({
17
+ description: "A CSS selector, or an element @ref if a page-reading tool provides one.",
18
+ }),
19
+ })
20
+
21
+ const FillArgs = Schema.Struct({
22
+ ref: Schema.String.annotate({
23
+ description: "A CSS selector (or element @ref) of the input to fill.",
24
+ }),
25
+ text: Schema.String.annotate({ description: "The text to put in the input." }),
26
+ })
27
+
28
+ const PressArgs = Schema.Struct({
29
+ key: Schema.String.annotate({
30
+ description: 'Key to press, e.g. "Enter" to submit the focused input, "Tab", "Escape".',
31
+ }),
32
+ })
33
+
34
+ const ScrollArgs = Schema.Struct({
35
+ direction: Schema.Literals(["up", "down"]).annotate({
36
+ description: "Scroll the viewport up or down.",
37
+ }),
38
+ })
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Uniform action outcome: the model always learns where the action left it.
42
+ // ---------------------------------------------------------------------------
43
+
44
+ const currentUrl = (session: BrowserSession): Effect.Effect<string> =>
45
+ session.evaluate("location.href").pipe(
46
+ Effect.flatMap(Schema.decodeUnknownEffect(Schema.String)),
47
+ Effect.orElseSucceed(() => "(unknown)"),
48
+ )
49
+
50
+ const okAt = (session: BrowserSession): Effect.Effect<string> =>
51
+ Effect.map(currentUrl(session), (url) => `ok (now at ${url})`)
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // The canonical browser action tools, each closed over a live `BrowserSession`
55
+ // and failing with the full `BrowserError`. `Output` is a compact outcome
56
+ // (`ok (now at <url>)`), not a page dump - a page-reading tool is the recipe's
57
+ // to define, so history does not accumulate a full page per action.
58
+ //
59
+ // A failed action (stale ref, dead control, timeout) fails typed on the error
60
+ // channel. Whether the model gets to recover from it is the loop's decision:
61
+ // wrap the toolkit in `Toolkit.describeFailures(BrowserError.describe)` to
62
+ // surface failures to the model, or map selectively to keep session/infra
63
+ // errors (e.g. `BrowserSessionExpired`) fatal.
64
+ //
65
+ // Browser actions are order-dependent (fill, then press Enter): run a turn's
66
+ // calls with `{ concurrency: 1 }`.
67
+ // ---------------------------------------------------------------------------
68
+
69
+ export const gotoTool = (session: BrowserSession) =>
70
+ Tool.make({
71
+ name: "browser_goto",
72
+ description: "Navigate the browser to an absolute URL.",
73
+ inputSchema: Tool.fromEffectSchema(GotoArgs),
74
+ run: (args) =>
75
+ Effect.andThen(session.goto(args.url), okAt(session)).pipe(
76
+ Effect.withSpan("browser_goto", {
77
+ kind: "client",
78
+ attributes: { "browser.url": args.url },
79
+ }),
80
+ ),
81
+ })
82
+
83
+ export const clickTool = (session: BrowserSession) =>
84
+ Tool.make({
85
+ name: "browser_click",
86
+ description: "Click the element named by ref. A click may navigate to a new page.",
87
+ inputSchema: Tool.fromEffectSchema(ClickArgs),
88
+ run: (args) =>
89
+ Effect.andThen(session.click(args.ref), okAt(session)).pipe(
90
+ Effect.withSpan("browser_click", {
91
+ kind: "client",
92
+ attributes: { "browser.ref": args.ref },
93
+ }),
94
+ ),
95
+ })
96
+
97
+ export const fillTool = (session: BrowserSession) =>
98
+ Tool.make({
99
+ name: "browser_fill",
100
+ description:
101
+ "Put text into the input named by ref. Filling alone does not submit; press Enter to submit the input.",
102
+ inputSchema: Tool.fromEffectSchema(FillArgs),
103
+ run: (args) =>
104
+ Effect.andThen(session.fill(args.ref, args.text), okAt(session)).pipe(
105
+ Effect.withSpan("browser_fill", {
106
+ kind: "client",
107
+ attributes: { "browser.ref": args.ref },
108
+ }),
109
+ ),
110
+ })
111
+
112
+ export const pressTool = (session: BrowserSession) =>
113
+ Tool.make({
114
+ name: "browser_press",
115
+ description: "Press a keyboard key against the focused element.",
116
+ inputSchema: Tool.fromEffectSchema(PressArgs),
117
+ run: (args) =>
118
+ Effect.andThen(session.press(args.key), okAt(session)).pipe(
119
+ Effect.withSpan("browser_press", {
120
+ kind: "client",
121
+ attributes: { "browser.key": args.key },
122
+ }),
123
+ ),
124
+ })
125
+
126
+ export const scrollTool = (session: BrowserSession) =>
127
+ Tool.make({
128
+ name: "browser_scroll",
129
+ description:
130
+ "Scroll the viewport. Only useful to bring an element into view - a page-reading tool's text is already scroll-independent.",
131
+ inputSchema: Tool.fromEffectSchema(ScrollArgs),
132
+ run: (args) =>
133
+ Effect.andThen(session.scroll({ direction: args.direction }), okAt(session)).pipe(
134
+ Effect.withSpan("browser_scroll", {
135
+ kind: "client",
136
+ attributes: { "browser.direction": args.direction },
137
+ }),
138
+ ),
139
+ })
140
+
141
+ /**
142
+ * The canonical browser action verbs bundled as a `Toolkit`, closed over a
143
+ * live `BrowserSession`. Compose it with a recipe-local page-reading tool and
144
+ * whatever finish/signal tools the agent needs:
145
+ *
146
+ * const toolkit = yield* Toolkit.compose(
147
+ * browserToolkit(session),
148
+ * Toolkit.make(readPageTool(session), finishTool),
149
+ * )
150
+ *
151
+ * For an à-la-carte subset, use the individual `gotoTool` / `clickTool` / …
152
+ * constructors directly. Run a turn's calls with `{ concurrency: 1 }`.
153
+ */
154
+ export const browserToolkit = (session: BrowserSession) =>
155
+ Toolkit.make(
156
+ gotoTool(session),
157
+ clickTool(session),
158
+ fillTool(session),
159
+ pressTool(session),
160
+ scrollTool(session),
161
+ )
@@ -1,4 +1,4 @@
1
- import { Data, Duration } from "effect"
1
+ import { Data, Duration, Match } from "effect"
2
2
 
3
3
  export type Scope = "rpm" | "tpm" | "rpd" | "tpd"
4
4
 
@@ -112,3 +112,29 @@ export type AiError =
112
112
  | IncompleteTurn
113
113
  | GenerationFailed
114
114
  | Unsupported
115
+
116
+ const withReason = (base: string, reason: string | undefined): string =>
117
+ reason === undefined ? base : `${base}: ${reason}`
118
+
119
+ /**
120
+ * Short human-readable description of an error, for logs and model-facing
121
+ * failure messages. Prose, not a contract; branch on `_tag` instead.
122
+ */
123
+ export const describe: (e: AiError) => string = Match.type<AiError>().pipe(
124
+ Match.discriminatorsExhaustive("_tag")({
125
+ RateLimited: (e) => `rate limited by ${e.provider}; try again later`,
126
+ Unavailable: (e) => `${e.provider} is temporarily unavailable`,
127
+ Timeout: (e) => `the ${e.provider} request timed out`,
128
+ ContentFiltered: (e) => withReason(`blocked by ${e.provider}'s content filter`, e.reason),
129
+ ContextLengthExceeded: (e) => `the request exceeded ${e.provider}'s context limit`,
130
+ InvalidRequest: (e) =>
131
+ e.param === undefined
132
+ ? `${e.provider} rejected the request`
133
+ : `${e.provider} rejected the request (param: ${e.param})`,
134
+ AuthFailed: (e) => `authentication against ${e.provider} failed (${e.subtype})`,
135
+ Cancelled: (e) => `the ${e.provider} request was cancelled`,
136
+ IncompleteTurn: () => "the provider stream ended without completing",
137
+ GenerationFailed: (e) => e.message ?? `${e.provider} failed mid-generation`,
138
+ Unsupported: (e) => withReason(`${e.provider} does not support ${e.capability}`, e.reason),
139
+ }),
140
+ )
@@ -0,0 +1,64 @@
1
+ import type { DateTime } from "effect"
2
+
3
+ // ---------------------------------------------------------------------------
4
+ // Source
5
+ // ---------------------------------------------------------------------------
6
+
7
+ /**
8
+ * A document the model consulted. `url` and `title` are the only
9
+ * near-universal fields; the rest is best-effort. `raw` round-trips
10
+ * provider-opaque tokens (encrypted indices, chunk handles, document ids) so
11
+ * nothing is lost.
12
+ */
13
+ export type Source = {
14
+ readonly url: string
15
+ readonly title?: string
16
+ readonly snippet?: string
17
+ readonly publishedDate?: DateTime.DateTime
18
+ readonly sourceType?: "web" | "x" | "news" | "file" | "document" | (string & {})
19
+ readonly raw?: unknown
20
+ }
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Span
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /**
27
+ * Where in the answer a claim is grounded, and which sources ground it.
28
+ * `sourceRefs` indexes into the sibling {@link Citations.sources} array
29
+ * (many-to-one). The four kinds normalize how providers link answer text to
30
+ * sources: a character / byte offset range, an exact source quote, a
31
+ * positional `[n]` marker, or `none` for a bare source with no anchor.
32
+ */
33
+ export type CitationSpan =
34
+ | {
35
+ readonly kind: "char"
36
+ readonly start: number
37
+ readonly end: number
38
+ readonly unit: "char" | "byte"
39
+ readonly sourceRefs: ReadonlyArray<number>
40
+ readonly confidence?: number
41
+ }
42
+ | { readonly kind: "quote"; readonly text: string; readonly sourceRefs: ReadonlyArray<number> }
43
+ | {
44
+ readonly kind: "marker"
45
+ readonly ordinal: number
46
+ readonly sourceRefs: ReadonlyArray<number>
47
+ }
48
+ | { readonly kind: "none"; readonly sourceRefs: ReadonlyArray<number> }
49
+
50
+ // ---------------------------------------------------------------------------
51
+ // Citations
52
+ // ---------------------------------------------------------------------------
53
+
54
+ /**
55
+ * The grounding for a piece of generated text: the sources consulted, and the
56
+ * spans that reference them. This is the canonical, provider-agnostic form.
57
+ * The flat `Items.Annotation` array is its degenerate view (one `char` span
58
+ * with one inlined source), used where a wire-attached, per-`output_text`
59
+ * shape is enough.
60
+ */
61
+ export type Citations = {
62
+ readonly sources: ReadonlyArray<Source>
63
+ readonly spans: ReadonlyArray<CitationSpan>
64
+ }
@@ -27,16 +27,26 @@ export type InputImage = typeof InputImage.Type
27
27
 
28
28
  // ---------------------------------------------------------------------------
29
29
  // Annotations - source / citation pointers attached to `output_text` blocks.
30
- // Mirrors OpenAI Responses API; other providers can omit or map onto these
31
- // shapes.
30
+ // Provider-agnostic: each provider populates the fields it has (character
31
+ // span, quote, or positional marker) and omits the rest.
32
32
  // ---------------------------------------------------------------------------
33
33
 
34
+ /**
35
+ * A cited source. `url` + `title` are the only near-universal fields; the
36
+ * link from a source to the answer text varies by provider, so it is optional
37
+ * and expressed three ways: `start_index`/`end_index` (a character span into
38
+ * the answer), `cited_text` (an exact quote of the source, when no offsets are
39
+ * given), or `marker` (a positional `[n]` reference into an ordered list). A
40
+ * provider populates whichever it has; a bare source list sets none.
41
+ */
34
42
  export const UrlCitation = Schema.Struct({
35
43
  type: Schema.Literal("url_citation"),
36
44
  url: Schema.String,
37
- start_index: Schema.Number,
38
- end_index: Schema.Number,
39
45
  title: Schema.String,
46
+ start_index: Schema.optional(Schema.Number),
47
+ end_index: Schema.optional(Schema.Number),
48
+ cited_text: Schema.optional(Schema.String),
49
+ marker: Schema.optional(Schema.Number),
40
50
  })
41
51
  export type UrlCitation = typeof UrlCitation.Type
42
52
 
@@ -0,0 +1,42 @@
1
+ import type * as Job from "../job/Job.js"
2
+ import type * as Items from "./Items.js"
3
+ import type { Turn } from "./Turn.js"
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Request
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Cross-provider deep-research request: a question in, a cited report out.
11
+ * Deliberately minimal, only the input every provider shares. Depth, search
12
+ * caps, and structured-output knobs vary too much to unify, so they live on
13
+ * each provider's typed request, which extends this and narrows `model`.
14
+ *
15
+ * `history` reuses the `LanguageModel` input primitive: the common case is a
16
+ * single user message with the question. There is no history threading. A
17
+ * research call is one background job, not a conversation, but the input
18
+ * encoding is shared.
19
+ */
20
+ export type ResearchRequest = {
21
+ readonly history: ReadonlyArray<Items.HistoryItem>
22
+ /** Model / agent id. Provider default if omitted; each provider narrows. */
23
+ readonly model?: string
24
+ }
25
+
26
+ // ---------------------------------------------------------------------------
27
+ // Result + job handle
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /**
31
+ * A completed research result is a `Turn`: one assistant message with the
32
+ * report text and its citations on `OutputText.annotations`, plus usage.
33
+ * Project it with `Turn.assistantText` / `Turn.citations` /
34
+ * `Turn.decodeStructured`. There is no bespoke report type; the streaming
35
+ * terminal (`TurnComplete.turn`) and the collected result are the same shape.
36
+ */
37
+
38
+ /** Provider-tagged handle to a running research job, branded to `Turn`. */
39
+ export type ResearchJobRef = Job.JobRef<Turn>
40
+
41
+ /** Current state of a research job. See {@link Job.JobState}. */
42
+ export type ResearchState = Job.JobState<Turn>
@@ -1,6 +1,7 @@
1
1
  import { Data, Effect, Result, Schema, Stream, pipe } from "effect"
2
2
  import * as StructuredFormat from "../structured-format/StructuredFormat.js"
3
3
  import {
4
+ type Annotation,
4
5
  HistoryItem,
5
6
  Message,
6
7
  Reasoning,
@@ -44,6 +45,14 @@ export type Turn = typeof Turn.Type
44
45
  * `UsageUpdate`: mid-stream cumulative usage. Anthropic emits this on
45
46
  * `message_start` and `message_delta`; other providers may only deliver
46
47
  * usage via `TurnComplete.turn.usage`.
48
+ *
49
+ * `WebSearchCall`: lifecycle of a provider-executed web search within the
50
+ * turn. Fires only when the turn grounds against search; absent otherwise.
51
+ *
52
+ * `CitationAdded`: a citation attached to the answer, emitted incrementally
53
+ * where the provider streams citations. Providers that bundle citations emit
54
+ * none of these; their citations still arrive on `TurnComplete.turn`
55
+ * (`OutputText.annotations`). Either way the final set is on the assembled turn.
47
56
  */
48
57
  export type TurnEvent = Data.TaggedEnum<{
49
58
  TextDelta: { readonly text: string }
@@ -52,6 +61,12 @@ export type TurnEvent = Data.TaggedEnum<{
52
61
  ToolCallStart: { readonly call_id: string; readonly name: string }
53
62
  ToolCallArgsDelta: { readonly call_id: string; readonly delta: string }
54
63
  UsageUpdate: { readonly usage: Usage }
64
+ WebSearchCall: {
65
+ readonly status: "started" | "searching" | "completed"
66
+ readonly query?: string
67
+ readonly action?: "search" | "open_page" | "find_in_page"
68
+ }
69
+ CitationAdded: { readonly annotation: Annotation }
55
70
  TurnComplete: { readonly turn: Turn }
56
71
  }>
57
72
 
@@ -94,6 +109,17 @@ export const assistantTexts = (turn: Turn): ReadonlyArray<string> =>
94
109
  */
95
110
  export const assistantText = (turn: Turn): string => assistantTexts(turn).join("")
96
111
 
112
+ /**
113
+ * Every citation annotation attached to the turn's assistant `output_text`
114
+ * blocks, in order. The flat view of a grounded turn's sources, whether the
115
+ * turn came from a normal generation with native search or a deep-research job.
116
+ */
117
+ export const citations = (turn: Turn): ReadonlyArray<Annotation> =>
118
+ assistantMessages(turn)
119
+ .flatMap((m) => m.content)
120
+ .filter(isOutputText)
121
+ .flatMap((b) => b.annotations ?? [])
122
+
97
123
  /**
98
124
  * Append a completed turn and optional follow-up items to a state record's
99
125
  * history. Recipes use this at the point where structured tool results are
@@ -108,13 +108,13 @@ export class EmbeddingModel extends Context.Service<EmbeddingModel, EmbeddingMod
108
108
  export const embed = <E extends EmbedEncoding | undefined = undefined>(
109
109
  request: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
110
110
  ): Effect.Effect<EmbedResponse<E>, AiError.AiError, EmbeddingModel> =>
111
- Effect.flatMap(EmbeddingModel.asEffect(), (m) => m.embed(request))
111
+ Effect.flatMap(EmbeddingModel, (m) => m.embed(request))
112
112
 
113
113
  /** Embed a batch in one provider call. Same `task` for every input. */
114
114
  export const embedMany = <E extends EmbedEncoding | undefined = undefined>(
115
115
  request: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
116
116
  ): Effect.Effect<EmbedManyResponse<E>, AiError.AiError, EmbeddingModel> =>
117
- Effect.flatMap(EmbeddingModel.asEffect(), (m) => m.embedMany(request))
117
+ Effect.flatMap(EmbeddingModel, (m) => m.embedMany(request))
118
118
 
119
119
  /**
120
120
  * Guard a requested `encoding` against a provider's supported set. Returns
package/src/index.ts CHANGED
@@ -1,10 +1,17 @@
1
1
  export * as AiError from "./domain/AiError.js"
2
2
  export * as Audio from "./domain/Audio.js"
3
+ export * as Browser from "./browser/Browser.js"
4
+ export * as BrowserError from "./browser/BrowserError.js"
5
+ export * as BrowserTool from "./browser/BrowserTool.js"
3
6
  export * as Capabilities from "./capabilities/Capabilities.js"
7
+ export * as Citation from "./domain/Citation.js"
8
+ export * as DeepResearch from "./research/DeepResearch.js"
4
9
  export * as Image from "./domain/Image.js"
5
10
  export * as Items from "./domain/Items.js"
11
+ export * as Job from "./job/Job.js"
6
12
  export * as Media from "./domain/Media.js"
7
13
  export * as Music from "./domain/Music.js"
14
+ export * as Research from "./domain/Research.js"
8
15
  export * as Transcript from "./domain/Transcript.js"
9
16
  export * as Turn from "./domain/Turn.js"
10
17
  export * as Embedding from "./embedding-model/Embedding.js"
@@ -18,6 +25,8 @@ export * as SandboxImage from "./sandbox/SandboxImage.js"
18
25
  export * as SandboxNetwork from "./sandbox/SandboxNetwork.js"
19
26
  export * as SpeechSynthesizer from "./speech-synthesizer/SpeechSynthesizer.js"
20
27
  export * as Transcriber from "./transcriber/Transcriber.js"
28
+ export * as WebRead from "./web-read/WebRead.js"
29
+ export * as WebReadTool from "./web-read/WebReadTool.js"
21
30
  export * as WebSearch from "./web-search/WebSearch.js"
22
31
  export * as WebSearchTool from "./web-search/WebSearchTool.js"
23
32
  export * as Vector from "./math/Vector.js"