@effect-uai/exa 0.11.0 → 0.12.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.
package/dist/index.d.mts CHANGED
@@ -1,5 +1,4 @@
1
- import { ExaCategory, ExaLivecrawl, ExaResearchModel, ExaSearchType } from "./models.mjs";
1
+ import { ExaCategory, ExaLivecrawl, ExaSearchType } from "./models.mjs";
2
2
  import { t as ExaContents_d_exports } from "./ExaContents.mjs";
3
- import { t as ExaDeepResearch_d_exports } from "./ExaDeepResearch.mjs";
4
3
  import { t as ExaSearch_d_exports } from "./ExaSearch.mjs";
5
- export { ExaCategory, ExaContents_d_exports as ExaContents, ExaDeepResearch_d_exports as ExaDeepResearch, ExaLivecrawl, ExaResearchModel, ExaSearch_d_exports as ExaSearch, ExaSearchType };
4
+ export { ExaCategory, ExaContents_d_exports as ExaContents, ExaLivecrawl, ExaSearch_d_exports as ExaSearch, ExaSearchType };
package/dist/index.mjs CHANGED
@@ -1,5 +1,4 @@
1
1
  import { t as ExaContents_exports } from "./ExaContents.mjs";
2
- import { t as ExaDeepResearch_exports } from "./ExaDeepResearch.mjs";
3
2
  import { t as ExaSearch_exports } from "./ExaSearch.mjs";
4
3
  import "./models.mjs";
5
- export { ExaContents_exports as ExaContents, ExaDeepResearch_exports as ExaDeepResearch, ExaSearch_exports as ExaSearch };
4
+ export { ExaContents_exports as ExaContents, ExaSearch_exports as ExaSearch };
package/dist/models.d.mts CHANGED
@@ -14,18 +14,6 @@
14
14
  * cross-provider `CommonSearchRequest`.
15
15
  */
16
16
  type ExaSearchType = "auto" | "fast" | "instant" | "deep-lite" | "deep" | "deep-reasoning" | "neural" | "keyword" | (string & {});
17
- /**
18
- * Exa research model for the async `/research/v1` task API (create + poll).
19
- * `exa-research-fast` / `exa-research` / `exa-research-pro` trade latency for
20
- * depth. The `(string & {})` tail accepts any string without an SDK bump.
21
- *
22
- * Note (2026-07): Exa announced `/research/v1` deprecation in favour of
23
- * `/search` with `type: "deep-reasoning"`. Verify the endpoint and the
24
- * completed-task response shape against live docs before relying on this.
25
- *
26
- * Reference: https://docs.exa.ai/reference/research/create-a-task
27
- */
28
- type ExaResearchModel = "exa-research" | "exa-research-pro" | "exa-research-fast" | (string & {});
29
17
  /**
30
18
  * Exa's result category filter. Narrows the index to a content vertical.
31
19
  * Provider-specific, so it lives on `ExaSearchRequest`.
@@ -38,5 +26,5 @@ type ExaCategory = "company" | "research paper" | "news" | "personal site" | "fi
38
26
  */
39
27
  type ExaLivecrawl = "never" | "fallback" | "preferred" | "always" | (string & {});
40
28
  //#endregion
41
- export { ExaCategory, ExaLivecrawl, ExaResearchModel, ExaSearchType };
29
+ export { ExaCategory, ExaLivecrawl, ExaSearchType };
42
30
  //# sourceMappingURL=models.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAcA;;;;;AAuBA;;;;;AAWA;;;KAlCY,aAAA;;AAiDZ;;;;;;;;;;KA1BY,gBAAA;;;;;KAWA,WAAA;;;;;;KAeA,YAAA"}
1
+ {"version":3,"file":"models.d.mts","names":[],"sources":["../src/models.ts"],"mappings":";;AAcA;;;;;AAgBA;;;;;AAeA;;;KA/BY,aAAA;;;;;KAgBA,WAAA;;;;;;KAeA,YAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/exa",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "Exa provider for @effect-uai/core (web search, web read).",
5
5
  "keywords": [
6
6
  "ai",
@@ -39,10 +39,6 @@
39
39
  "types": "./dist/ExaContents.d.mts",
40
40
  "import": "./dist/ExaContents.mjs"
41
41
  },
42
- "./ExaDeepResearch": {
43
- "types": "./dist/ExaDeepResearch.d.mts",
44
- "import": "./dist/ExaDeepResearch.mjs"
45
- },
46
42
  "./ExaSearch": {
47
43
  "types": "./dist/ExaSearch.d.mts",
48
44
  "import": "./dist/ExaSearch.mjs"
@@ -54,7 +50,7 @@
54
50
  "devDependencies": {
55
51
  "effect": "4.0.0-beta.94",
56
52
  "typescript": "^6.0.3",
57
- "@effect-uai/core": "0.11.0"
53
+ "@effect-uai/core": "0.12.0"
58
54
  },
59
55
  "peerDependencies": {
60
56
  "@effect-uai/core": ">=0.2.0 <1",
package/src/index.ts CHANGED
@@ -1,4 +1,3 @@
1
1
  export * as ExaContents from "./ExaContents.js"
2
- export * as ExaDeepResearch from "./ExaDeepResearch.js"
3
2
  export * as ExaSearch from "./ExaSearch.js"
4
3
  export * from "./models.js"
package/src/models.ts CHANGED
@@ -24,24 +24,6 @@ export type ExaSearchType =
24
24
  // eslint-disable-next-line @typescript-eslint/ban-types
25
25
  | (string & {})
26
26
 
27
- /**
28
- * Exa research model for the async `/research/v1` task API (create + poll).
29
- * `exa-research-fast` / `exa-research` / `exa-research-pro` trade latency for
30
- * depth. The `(string & {})` tail accepts any string without an SDK bump.
31
- *
32
- * Note (2026-07): Exa announced `/research/v1` deprecation in favour of
33
- * `/search` with `type: "deep-reasoning"`. Verify the endpoint and the
34
- * completed-task response shape against live docs before relying on this.
35
- *
36
- * Reference: https://docs.exa.ai/reference/research/create-a-task
37
- */
38
- export type ExaResearchModel =
39
- | "exa-research"
40
- | "exa-research-pro"
41
- | "exa-research-fast"
42
- // eslint-disable-next-line @typescript-eslint/ban-types
43
- | (string & {})
44
-
45
27
  /**
46
28
  * Exa's result category filter. Narrows the index to a content vertical.
47
29
  * Provider-specific, so it lives on `ExaSearchRequest`.
@@ -1,70 +0,0 @@
1
- import { ExaResearchModel } from "./models.mjs";
2
- import { Context, Effect, Layer, Redacted, Schema } from "effect";
3
- import { HttpClient } from "effect/unstable/http";
4
- import { DeepResearch, DeepResearchServiceShape } from "@effect-uai/core/DeepResearch";
5
- import * as Job from "@effect-uai/core/Job";
6
- import * as StructuredFormat from "@effect-uai/core/StructuredFormat";
7
- import { ResearchRequest, ResearchState } from "@effect-uai/core/Research";
8
-
9
- //#region src/ExaDeepResearch.d.ts
10
- declare namespace ExaDeepResearch_d_exports {
11
- export { Config, ExaDeepResearch, ExaDeepResearchService, ExaResearchRequest, WireResearch, jobStateOf, layer, make };
12
- }
13
- /**
14
- * Exa-typed research request. Narrows `model` and adds `outputSchema`.
15
- * Provider-portable code uses the generic `DeepResearch` tag instead.
16
- */
17
- type ExaResearchRequest = Omit<ResearchRequest, "model"> & {
18
- readonly model?: ExaResearchModel;
19
- /**
20
- * Optional structured output. When set, Exa researches the web and returns
21
- * JSON matching the schema (Draft 7) instead of a prose report, with
22
- * field-level citations. The completed `Turn`'s text is that JSON, so decode
23
- * it with `Turn.decodeStructured(turn, outputSchema)` — the same path as
24
- * `LanguageModel` structured output.
25
- */
26
- readonly outputSchema?: StructuredFormat.StructuredFormat<unknown>;
27
- };
28
- type ExaDeepResearchService = DeepResearchServiceShape<ExaResearchRequest>;
29
- declare const ExaDeepResearch_base: Context.ServiceClass<ExaDeepResearch, "@betalyra/effect-uai/providers/exa/ExaDeepResearch", ExaDeepResearchService>;
30
- /**
31
- * Provider-typed tag for Exa deep research over the async task API
32
- * (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield
33
- * this for the model / `outputSchema` knobs; yield the generic `DeepResearch`
34
- * tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the
35
- * synthesized default {@link fromJob} builds.
36
- */
37
- declare class ExaDeepResearch extends ExaDeepResearch_base {}
38
- type Config = {
39
- readonly apiKey: Redacted.Redacted;
40
- readonly baseUrl?: string; /** Poll cadence / overall timeout for the derived poll loops. */
41
- readonly job?: Job.JobConfig;
42
- };
43
- declare const WireResearch: Schema.Struct<{
44
- readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
45
- readonly status: Schema.optional<Schema.NullOr<Schema.String>>;
46
- readonly schema: Schema.optional<Schema.NullOr<Schema.Unknown>>;
47
- readonly data: Schema.optional<Schema.NullOr<Schema.$Record<Schema.String, Schema.Unknown>>>;
48
- readonly citations: Schema.optional<Schema.NullOr<Schema.$Record<Schema.String, Schema.$Array<Schema.Struct<{
49
- readonly id: Schema.optional<Schema.NullOr<Schema.String>>;
50
- readonly url: Schema.optional<Schema.NullOr<Schema.String>>;
51
- readonly title: Schema.optional<Schema.NullOr<Schema.String>>;
52
- readonly snippet: Schema.optional<Schema.NullOr<Schema.String>>;
53
- }>>>>>;
54
- readonly error: Schema.optional<Schema.NullOr<Schema.Struct<{
55
- readonly message: Schema.optional<Schema.NullOr<Schema.String>>;
56
- }>>>;
57
- }>;
58
- type WireResearch = typeof WireResearch.Type;
59
- declare const jobStateOf: (wire: WireResearch) => ResearchState;
60
- /** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */
61
- declare const make: (cfg: Config) => Effect.Effect<ExaDeepResearchService, never, HttpClient.HttpClient>;
62
- /**
63
- * Layer registering the provider-typed `ExaDeepResearch` tag and the generic
64
- * `DeepResearch` tag. No capability markers: the surface (poll, collect,
65
- * synthesized stream) is universal.
66
- */
67
- declare const layer: (cfg: Config) => Layer.Layer<ExaDeepResearch | DeepResearch, never, HttpClient.HttpClient>;
68
- //#endregion
69
- export { Config, ExaDeepResearch, ExaDeepResearchService, ExaResearchRequest, WireResearch, jobStateOf, layer, make, ExaDeepResearch_d_exports as t };
70
- //# sourceMappingURL=ExaDeepResearch.d.mts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ExaDeepResearch.d.mts","names":[],"sources":["../src/ExaDeepResearch.ts"],"mappings":";;;;;;;;;;;;;;;;KAyBY,kBAAA,GAAqB,IAAA,CAAK,eAAA;EAAA,SAC3B,KAAA,GAAQ,gBAAA;;;;;;;;WAQR,YAAA,GAAe,gBAAA,CAAiB,gBAAA;AAAA;AAAA,KAG/B,sBAAA,GAAyB,wBAAA,CAAyB,kBAAA;AAAA,cAAmB,oBAAA;;;;;;;;cASpE,eAAA,SAAwB,oBAAA;AAAA,KAIzB,MAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAA;EAAA,SACjB,OAAA,WAlBgD;EAAA,SAoBhD,GAAA,GAAM,GAAA,CAAI,SAAA;AAAA;AAAA,cAwBR,YAAA,EAAY,MAAA,CAAA,MAAA;EAAA;;;;;;;;;;;;;;KAQb,YAAA,UAAsB,YAAA,CAAa,IAAA;AAAA,cAoDlC,UAAA,GAAc,IAAA,EAAM,YAAA,KAAe,aAAA;;cA8HnC,IAAA,GACX,GAAA,EAAK,MAAA,KACJ,MAAA,CAAO,MAAA,CAAO,sBAAA,SAA+B,UAAA,CAAW,UAAA;;;;;AAxN3D;cAgOa,KAAA,GACX,GAAA,EAAK,MAAA,KACJ,KAAA,CAAM,KAAA,CAAM,eAAA,GAAkB,YAAA,SAAqB,UAAA,CAAW,UAAA"}
@@ -1,183 +0,0 @@
1
- import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
2
- import { Context, Effect, Layer, Match, Redacted, Schema } from "effect";
3
- import { HttpClient, HttpClientRequest } from "effect/unstable/http";
4
- import * as AiError from "@effect-uai/core/AiError";
5
- import { DeepResearch, fromJob } from "@effect-uai/core/DeepResearch";
6
- import * as Items from "@effect-uai/core/Items";
7
- import * as Job from "@effect-uai/core/Job";
8
- //#region src/ExaDeepResearch.ts
9
- var ExaDeepResearch_exports = /* @__PURE__ */ __exportAll({
10
- ExaDeepResearch: () => ExaDeepResearch,
11
- WireResearch: () => WireResearch,
12
- jobStateOf: () => jobStateOf,
13
- layer: () => layer,
14
- make: () => make
15
- });
16
- /**
17
- * Provider-typed tag for Exa deep research over the async task API
18
- * (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield
19
- * this for the model / `outputSchema` knobs; yield the generic `DeepResearch`
20
- * tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the
21
- * synthesized default {@link fromJob} builds.
22
- */
23
- var ExaDeepResearch = class extends Context.Service()("@betalyra/effect-uai/providers/exa/ExaDeepResearch") {};
24
- const WireSubmit = Schema.Struct({ id: Schema.String });
25
- const decodeSubmit = Schema.decodeUnknownEffect(WireSubmit);
26
- const OptionalText = Schema.optional(Schema.NullOr(Schema.String));
27
- const WireSource = Schema.Struct({
28
- id: OptionalText,
29
- url: OptionalText,
30
- title: OptionalText,
31
- snippet: OptionalText
32
- });
33
- const WireResearch = Schema.Struct({
34
- id: OptionalText,
35
- status: OptionalText,
36
- schema: Schema.optional(Schema.NullOr(Schema.Unknown)),
37
- data: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown))),
38
- citations: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Array(WireSource)))),
39
- error: Schema.optional(Schema.NullOr(Schema.Struct({ message: OptionalText })))
40
- });
41
- const decodeResearch = Schema.decodeUnknownEffect(WireResearch);
42
- const reportText = (wire) => {
43
- if (wire.schema != null) return JSON.stringify(wire.data ?? {});
44
- const answer = wire.data?.answer;
45
- return typeof answer === "string" ? answer : "";
46
- };
47
- const citationsToAnnotations = (wire) => Object.values(wire.citations ?? {}).flat().flatMap((c, i) => c.url == null ? [] : [{
48
- type: "url_citation",
49
- url: c.url,
50
- title: c.title ?? c.url,
51
- marker: i + 1,
52
- ...c.snippet != null && { cited_text: c.snippet }
53
- }]);
54
- const turnFromResearch = (wire) => {
55
- const annotations = citationsToAnnotations(wire);
56
- return {
57
- items: [{
58
- type: "message",
59
- role: "assistant",
60
- content: [{
61
- type: "output_text",
62
- text: reportText(wire),
63
- ...annotations.length > 0 && { annotations }
64
- }],
65
- providerData: wire
66
- }],
67
- usage: {},
68
- stop_reason: "stop"
69
- };
70
- };
71
- const jobStateOf = (wire) => Match.value(wire.status ?? "").pipe(Match.whenOr("pending", "queued", () => ({ _tag: "Pending" })), Match.whenOr("running", "in_progress", () => ({ _tag: "Running" })), Match.when("completed", () => ({
72
- _tag: "Succeeded",
73
- result: turnFromResearch(wire)
74
- })), Match.whenOr("failed", "canceled", "cancelled", () => ({
75
- _tag: "Failed",
76
- ...wire.error?.message != null && { reason: wire.error.message },
77
- raw: wire
78
- })), Match.orElse(() => ({ _tag: "Running" })));
79
- const baseUrl = (cfg) => cfg.baseUrl ?? "https://api.exa.ai";
80
- const authHeader = (cfg) => HttpClientRequest.setHeader("x-api-key", Redacted.value(cfg.apiKey));
81
- const transportFailure = (cause) => new AiError.Unavailable({
82
- provider: "exa",
83
- raw: cause
84
- });
85
- const httpStatusError = (status, body) => {
86
- const provider = "exa";
87
- const raw = body;
88
- if (status === 429) return new AiError.RateLimited({
89
- provider,
90
- raw
91
- });
92
- if (status === 408 || status === 504) return new AiError.Timeout({
93
- provider,
94
- raw
95
- });
96
- if (status === 401) return new AiError.AuthFailed({
97
- provider,
98
- subtype: "auth",
99
- raw
100
- });
101
- if (status === 403) return new AiError.AuthFailed({
102
- provider,
103
- subtype: "permission",
104
- raw
105
- });
106
- if (status === 402) return new AiError.AuthFailed({
107
- provider,
108
- subtype: "billing",
109
- raw
110
- });
111
- if (status >= 500) return new AiError.Unavailable({
112
- provider,
113
- status,
114
- raw
115
- });
116
- return new AiError.InvalidRequest({
117
- provider,
118
- raw
119
- });
120
- };
121
- const instructions = (history) => history.filter(Items.isMessage).filter((m) => m.role === "user").flatMap((m) => m.content).filter(Items.isInputText).map((b) => b.text).join("\n");
122
- const submitBody = (request) => ({
123
- instructions: instructions(request.history),
124
- model: request.model ?? "exa-research",
125
- ...request.outputSchema !== void 0 && { output: { schema: request.outputSchema.schema["~standard"].jsonSchema.input({ target: "draft-07" }) } }
126
- });
127
- const submitJob = (cfg, request) => Effect.gen(function* () {
128
- const client = yield* HttpClient.HttpClient;
129
- const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/research/v0/tasks`).pipe(authHeader(cfg), HttpClientRequest.bodyJsonUnsafe(submitBody(request)));
130
- const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
131
- if (response.status >= 400) {
132
- const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
133
- return yield* httpStatusError(response.status, text);
134
- }
135
- const wire = yield* decodeSubmit(yield* response.json.pipe(Effect.mapError(transportFailure))).pipe(Effect.mapError(transportFailure));
136
- return Job.jobRef("exa", wire.id);
137
- });
138
- const pollJob = (cfg, ref) => Effect.gen(function* () {
139
- const client = yield* HttpClient.HttpClient;
140
- const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/research/v0/tasks/${ref.id}`).pipe(authHeader(cfg));
141
- const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure));
142
- if (response.status >= 400) {
143
- const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""));
144
- return yield* httpStatusError(response.status, text);
145
- }
146
- return jobStateOf(yield* decodeResearch(yield* response.json.pipe(Effect.mapError(transportFailure))).pipe(Effect.mapError(transportFailure)));
147
- });
148
- const unsupported = (capability) => new AiError.Unsupported({
149
- provider: "exa",
150
- capability,
151
- reason: "Exa's research task API exposes no verified cancel endpoint."
152
- });
153
- const jobOps = (cfg, client) => {
154
- const withClient = (e) => Effect.provideService(e, HttpClient.HttpClient, client);
155
- return {
156
- submit: (request) => withClient(submitJob(cfg, request)),
157
- poll: (ref) => withClient(pollJob(cfg, ref)),
158
- cancel: () => Effect.fail(unsupported("cancel"))
159
- };
160
- };
161
- /** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */
162
- const make = (cfg) => Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job));
163
- /**
164
- * Layer registering the provider-typed `ExaDeepResearch` tag and the generic
165
- * `DeepResearch` tag. No capability markers: the surface (poll, collect,
166
- * synthesized stream) is universal.
167
- */
168
- const layer = (cfg) => {
169
- const typed = Layer.effect(ExaDeepResearch, make(cfg));
170
- const generic = Layer.effect(DeepResearch, Effect.map(HttpClient.HttpClient, (client) => {
171
- const ops = jobOps(cfg, client);
172
- return fromJob({
173
- submit: (request) => ops.submit(request),
174
- poll: ops.poll,
175
- cancel: ops.cancel
176
- }, cfg.job);
177
- }));
178
- return Layer.merge(typed, generic);
179
- };
180
- //#endregion
181
- export { ExaDeepResearch, WireResearch, jobStateOf, layer, make, ExaDeepResearch_exports as t };
182
-
183
- //# sourceMappingURL=ExaDeepResearch.mjs.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"ExaDeepResearch.mjs","names":[],"sources":["../src/ExaDeepResearch.ts"],"sourcesContent":["import { Context, Effect, Layer, Match, Redacted, Schema } from \"effect\"\nimport { HttpClient, HttpClientRequest } from \"effect/unstable/http\"\nimport * as AiError from \"@effect-uai/core/AiError\"\nimport {\n DeepResearch,\n type DeepResearchService,\n type DeepResearchServiceShape,\n fromJob,\n type ResearchJobOps,\n} from \"@effect-uai/core/DeepResearch\"\nimport * as Items from \"@effect-uai/core/Items\"\nimport * as Job from \"@effect-uai/core/Job\"\nimport * as StructuredFormat from \"@effect-uai/core/StructuredFormat\"\nimport type { ResearchJobRef, ResearchRequest, ResearchState } from \"@effect-uai/core/Research\"\nimport type { Turn } from \"@effect-uai/core/Turn\"\nimport type { ExaResearchModel } from \"./models.js\"\n\n// ---------------------------------------------------------------------------\n// Public types\n// ---------------------------------------------------------------------------\n\n/**\n * Exa-typed research request. Narrows `model` and adds `outputSchema`.\n * Provider-portable code uses the generic `DeepResearch` tag instead.\n */\nexport type ExaResearchRequest = Omit<ResearchRequest, \"model\"> & {\n readonly model?: ExaResearchModel\n /**\n * Optional structured output. When set, Exa researches the web and returns\n * JSON matching the schema (Draft 7) instead of a prose report, with\n * field-level citations. The completed `Turn`'s text is that JSON, so decode\n * it with `Turn.decodeStructured(turn, outputSchema)` — the same path as\n * `LanguageModel` structured output.\n */\n readonly outputSchema?: StructuredFormat.StructuredFormat<unknown>\n}\n\nexport type ExaDeepResearchService = DeepResearchServiceShape<ExaResearchRequest>\n\n/**\n * Provider-typed tag for Exa deep research over the async task API\n * (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield\n * this for the model / `outputSchema` knobs; yield the generic `DeepResearch`\n * tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the\n * synthesized default {@link fromJob} builds.\n */\nexport class ExaDeepResearch extends Context.Service<ExaDeepResearch, ExaDeepResearchService>()(\n \"@betalyra/effect-uai/providers/exa/ExaDeepResearch\",\n) {}\n\nexport type Config = {\n readonly apiKey: Redacted.Redacted\n readonly baseUrl?: string\n /** Poll cadence / overall timeout for the derived poll loops. */\n readonly job?: Job.JobConfig\n}\n\n// ---------------------------------------------------------------------------\n// Wire codec\n// ---------------------------------------------------------------------------\n\nconst WireSubmit = Schema.Struct({ id: Schema.String })\nconst decodeSubmit = Schema.decodeUnknownEffect(WireSubmit)\n\nconst OptionalText = Schema.optional(Schema.NullOr(Schema.String))\n\n// A source backing one data field. `id` is Exa's dedup key (usually the url).\nconst WireSource = Schema.Struct({\n id: OptionalText,\n url: OptionalText,\n title: OptionalText,\n snippet: OptionalText,\n})\n\n// `data` holds the results: `{ answer }` for a prose task, or the schema-shaped\n// object when an `outputSchema` was given. `schema` (non-null) is the reliable\n// discriminator. `citations` is keyed by data-field path, each a list of\n// sources. Reference: exa-labs/openapi-spec `/research/v0/tasks`.\nexport const WireResearch = Schema.Struct({\n id: OptionalText,\n status: OptionalText,\n schema: Schema.optional(Schema.NullOr(Schema.Unknown)),\n data: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown))),\n citations: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Array(WireSource)))),\n error: Schema.optional(Schema.NullOr(Schema.Struct({ message: OptionalText }))),\n})\nexport type WireResearch = typeof WireResearch.Type\nconst decodeResearch = Schema.decodeUnknownEffect(WireResearch)\n\n// Structured → the whole `data` object serialized (decoded outside via\n// `Turn.decodeStructured`). Prose → `data.answer`.\nconst reportText = (wire: WireResearch): string => {\n if (wire.schema != null) return JSON.stringify(wire.data ?? {})\n const answer = wire.data?.answer\n return typeof answer === \"string\" ? answer : \"\"\n}\n\n// Flatten the field-keyed citations into the flat `Annotation` list. The\n// per-field grouping is lost in this view; the raw wire stays on `providerData`.\nconst citationsToAnnotations = (wire: WireResearch): ReadonlyArray<Items.Annotation> =>\n Object.values(wire.citations ?? {})\n .flat()\n .flatMap((c, i) =>\n c.url == null\n ? []\n : [\n {\n type: \"url_citation\" as const,\n url: c.url,\n title: c.title ?? c.url,\n marker: i + 1,\n ...(c.snippet != null && { cited_text: c.snippet }),\n },\n ],\n )\n\nconst turnFromResearch = (wire: WireResearch): Turn => {\n const annotations = citationsToAnnotations(wire)\n return {\n items: [\n {\n type: \"message\",\n role: \"assistant\",\n content: [\n {\n type: \"output_text\",\n text: reportText(wire),\n ...(annotations.length > 0 && { annotations }),\n },\n ],\n providerData: wire,\n },\n ],\n usage: {},\n stop_reason: \"stop\",\n }\n}\n\nexport const jobStateOf = (wire: WireResearch): ResearchState =>\n Match.value(wire.status ?? \"\").pipe(\n Match.whenOr(\"pending\", \"queued\", (): ResearchState => ({ _tag: \"Pending\" })),\n Match.whenOr(\"running\", \"in_progress\", (): ResearchState => ({ _tag: \"Running\" })),\n Match.when(\n \"completed\",\n (): ResearchState => ({ _tag: \"Succeeded\", result: turnFromResearch(wire) }),\n ),\n Match.whenOr(\n \"failed\",\n \"canceled\",\n \"cancelled\",\n (): ResearchState => ({\n _tag: \"Failed\",\n ...(wire.error?.message != null && { reason: wire.error.message }),\n raw: wire,\n }),\n ),\n Match.orElse((): ResearchState => ({ _tag: \"Running\" })),\n )\n\n// ---------------------------------------------------------------------------\n// HTTP\n// ---------------------------------------------------------------------------\n\nconst baseUrl = (cfg: Config): string => cfg.baseUrl ?? \"https://api.exa.ai\"\n\nconst authHeader = (cfg: Config) =>\n HttpClientRequest.setHeader(\"x-api-key\", Redacted.value(cfg.apiKey))\n\nconst transportFailure = (cause: unknown): AiError.AiError =>\n new AiError.Unavailable({ provider: \"exa\", raw: cause })\n\nconst httpStatusError = (status: number, body: string): AiError.AiError => {\n const provider = \"exa\"\n const raw = body\n if (status === 429) return new AiError.RateLimited({ provider, raw })\n if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })\n if (status === 401) return new AiError.AuthFailed({ provider, subtype: \"auth\", raw })\n if (status === 403) return new AiError.AuthFailed({ provider, subtype: \"permission\", raw })\n if (status === 402) return new AiError.AuthFailed({ provider, subtype: \"billing\", raw })\n if (status >= 500) return new AiError.Unavailable({ provider, status, raw })\n return new AiError.InvalidRequest({ provider, raw })\n}\n\n// Deep research takes one instruction. Concatenate the user turns' text.\nconst instructions = (history: ReadonlyArray<Items.HistoryItem>): string =>\n history\n .filter(Items.isMessage)\n .filter((m) => m.role === \"user\")\n .flatMap((m) => m.content)\n .filter(Items.isInputText)\n .map((b) => b.text)\n .join(\"\\n\")\n\nconst submitBody = (request: ExaResearchRequest): Record<string, unknown> => ({\n instructions: instructions(request.history),\n model: request.model ?? \"exa-research\",\n ...(request.outputSchema !== undefined && {\n output: {\n schema: request.outputSchema.schema[\"~standard\"].jsonSchema.input({ target: \"draft-07\" }),\n },\n }),\n})\n\nconst submitJob = (\n cfg: Config,\n request: ExaResearchRequest,\n): Effect.Effect<ResearchJobRef, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/research/v0/tasks`).pipe(\n authHeader(cfg),\n HttpClientRequest.bodyJsonUnsafe(submitBody(request)),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeSubmit(json).pipe(Effect.mapError(transportFailure))\n return Job.jobRef<Turn>(\"exa\", wire.id)\n })\n\nconst pollJob = (\n cfg: Config,\n ref: ResearchJobRef,\n): Effect.Effect<ResearchState, AiError.AiError, HttpClient.HttpClient> =>\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/research/v0/tasks/${ref.id}`).pipe(\n authHeader(cfg),\n )\n const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))\n if (response.status >= 400) {\n const text = yield* response.text.pipe(Effect.orElseSucceed(() => \"\"))\n return yield* httpStatusError(response.status, text)\n }\n const json = yield* response.json.pipe(Effect.mapError(transportFailure))\n const wire = yield* decodeResearch(json).pipe(Effect.mapError(transportFailure))\n return jobStateOf(wire)\n })\n\nconst unsupported = (capability: string): AiError.Unsupported =>\n new AiError.Unsupported({\n provider: \"exa\",\n capability,\n reason: \"Exa's research task API exposes no verified cancel endpoint.\",\n })\n\n// ---------------------------------------------------------------------------\n// Constructors\n// ---------------------------------------------------------------------------\n\nconst jobOps = (cfg: Config, client: HttpClient.HttpClient): ResearchJobOps<ExaResearchRequest> => {\n const withClient = <A>(e: Effect.Effect<A, AiError.AiError, HttpClient.HttpClient>) =>\n Effect.provideService(e, HttpClient.HttpClient, client)\n return {\n submit: (request) => withClient(submitJob(cfg, request)),\n poll: (ref) => withClient(pollJob(cfg, ref)),\n cancel: () => Effect.fail(unsupported(\"cancel\")),\n }\n}\n\n/** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */\nexport const make = (\n cfg: Config,\n): Effect.Effect<ExaDeepResearchService, never, HttpClient.HttpClient> =>\n Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job))\n\n/**\n * Layer registering the provider-typed `ExaDeepResearch` tag and the generic\n * `DeepResearch` tag. No capability markers: the surface (poll, collect,\n * synthesized stream) is universal.\n */\nexport const layer = (\n cfg: Config,\n): Layer.Layer<ExaDeepResearch | DeepResearch, never, HttpClient.HttpClient> => {\n const typed = Layer.effect(ExaDeepResearch, make(cfg))\n const generic = Layer.effect(\n DeepResearch,\n Effect.map(HttpClient.HttpClient, (client): DeepResearchService => {\n const ops = jobOps(cfg, client)\n return fromJob<ResearchRequest>(\n {\n submit: (request) => ops.submit(request as ExaResearchRequest),\n poll: ops.poll,\n cancel: ops.cancel,\n },\n cfg.job,\n )\n }),\n )\n return Layer.merge(typed, generic)\n}\n"],"mappings":"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"}
@@ -1,294 +0,0 @@
1
- import { Context, Effect, Layer, Match, Redacted, Schema } from "effect"
2
- import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
- import * as AiError from "@effect-uai/core/AiError"
4
- import {
5
- DeepResearch,
6
- type DeepResearchService,
7
- type DeepResearchServiceShape,
8
- fromJob,
9
- type ResearchJobOps,
10
- } from "@effect-uai/core/DeepResearch"
11
- import * as Items from "@effect-uai/core/Items"
12
- import * as Job from "@effect-uai/core/Job"
13
- import * as StructuredFormat from "@effect-uai/core/StructuredFormat"
14
- import type { ResearchJobRef, ResearchRequest, ResearchState } from "@effect-uai/core/Research"
15
- import type { Turn } from "@effect-uai/core/Turn"
16
- import type { ExaResearchModel } from "./models.js"
17
-
18
- // ---------------------------------------------------------------------------
19
- // Public types
20
- // ---------------------------------------------------------------------------
21
-
22
- /**
23
- * Exa-typed research request. Narrows `model` and adds `outputSchema`.
24
- * Provider-portable code uses the generic `DeepResearch` tag instead.
25
- */
26
- export type ExaResearchRequest = Omit<ResearchRequest, "model"> & {
27
- readonly model?: ExaResearchModel
28
- /**
29
- * Optional structured output. When set, Exa researches the web and returns
30
- * JSON matching the schema (Draft 7) instead of a prose report, with
31
- * field-level citations. The completed `Turn`'s text is that JSON, so decode
32
- * it with `Turn.decodeStructured(turn, outputSchema)` — the same path as
33
- * `LanguageModel` structured output.
34
- */
35
- readonly outputSchema?: StructuredFormat.StructuredFormat<unknown>
36
- }
37
-
38
- export type ExaDeepResearchService = DeepResearchServiceShape<ExaResearchRequest>
39
-
40
- /**
41
- * Provider-typed tag for Exa deep research over the async task API
42
- * (`POST /research/v0/tasks` create + `GET /research/v0/tasks/{id}` poll). Yield
43
- * this for the model / `outputSchema` knobs; yield the generic `DeepResearch`
44
- * tag for portable code. Poll-based, so `researchStream` / `streamFrom` are the
45
- * synthesized default {@link fromJob} builds.
46
- */
47
- export class ExaDeepResearch extends Context.Service<ExaDeepResearch, ExaDeepResearchService>()(
48
- "@betalyra/effect-uai/providers/exa/ExaDeepResearch",
49
- ) {}
50
-
51
- export type Config = {
52
- readonly apiKey: Redacted.Redacted
53
- readonly baseUrl?: string
54
- /** Poll cadence / overall timeout for the derived poll loops. */
55
- readonly job?: Job.JobConfig
56
- }
57
-
58
- // ---------------------------------------------------------------------------
59
- // Wire codec
60
- // ---------------------------------------------------------------------------
61
-
62
- const WireSubmit = Schema.Struct({ id: Schema.String })
63
- const decodeSubmit = Schema.decodeUnknownEffect(WireSubmit)
64
-
65
- const OptionalText = Schema.optional(Schema.NullOr(Schema.String))
66
-
67
- // A source backing one data field. `id` is Exa's dedup key (usually the url).
68
- const WireSource = Schema.Struct({
69
- id: OptionalText,
70
- url: OptionalText,
71
- title: OptionalText,
72
- snippet: OptionalText,
73
- })
74
-
75
- // `data` holds the results: `{ answer }` for a prose task, or the schema-shaped
76
- // object when an `outputSchema` was given. `schema` (non-null) is the reliable
77
- // discriminator. `citations` is keyed by data-field path, each a list of
78
- // sources. Reference: exa-labs/openapi-spec `/research/v0/tasks`.
79
- export const WireResearch = Schema.Struct({
80
- id: OptionalText,
81
- status: OptionalText,
82
- schema: Schema.optional(Schema.NullOr(Schema.Unknown)),
83
- data: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Unknown))),
84
- citations: Schema.optional(Schema.NullOr(Schema.Record(Schema.String, Schema.Array(WireSource)))),
85
- error: Schema.optional(Schema.NullOr(Schema.Struct({ message: OptionalText }))),
86
- })
87
- export type WireResearch = typeof WireResearch.Type
88
- const decodeResearch = Schema.decodeUnknownEffect(WireResearch)
89
-
90
- // Structured → the whole `data` object serialized (decoded outside via
91
- // `Turn.decodeStructured`). Prose → `data.answer`.
92
- const reportText = (wire: WireResearch): string => {
93
- if (wire.schema != null) return JSON.stringify(wire.data ?? {})
94
- const answer = wire.data?.answer
95
- return typeof answer === "string" ? answer : ""
96
- }
97
-
98
- // Flatten the field-keyed citations into the flat `Annotation` list. The
99
- // per-field grouping is lost in this view; the raw wire stays on `providerData`.
100
- const citationsToAnnotations = (wire: WireResearch): ReadonlyArray<Items.Annotation> =>
101
- Object.values(wire.citations ?? {})
102
- .flat()
103
- .flatMap((c, i) =>
104
- c.url == null
105
- ? []
106
- : [
107
- {
108
- type: "url_citation" as const,
109
- url: c.url,
110
- title: c.title ?? c.url,
111
- marker: i + 1,
112
- ...(c.snippet != null && { cited_text: c.snippet }),
113
- },
114
- ],
115
- )
116
-
117
- const turnFromResearch = (wire: WireResearch): Turn => {
118
- const annotations = citationsToAnnotations(wire)
119
- return {
120
- items: [
121
- {
122
- type: "message",
123
- role: "assistant",
124
- content: [
125
- {
126
- type: "output_text",
127
- text: reportText(wire),
128
- ...(annotations.length > 0 && { annotations }),
129
- },
130
- ],
131
- providerData: wire,
132
- },
133
- ],
134
- usage: {},
135
- stop_reason: "stop",
136
- }
137
- }
138
-
139
- export const jobStateOf = (wire: WireResearch): ResearchState =>
140
- Match.value(wire.status ?? "").pipe(
141
- Match.whenOr("pending", "queued", (): ResearchState => ({ _tag: "Pending" })),
142
- Match.whenOr("running", "in_progress", (): ResearchState => ({ _tag: "Running" })),
143
- Match.when(
144
- "completed",
145
- (): ResearchState => ({ _tag: "Succeeded", result: turnFromResearch(wire) }),
146
- ),
147
- Match.whenOr(
148
- "failed",
149
- "canceled",
150
- "cancelled",
151
- (): ResearchState => ({
152
- _tag: "Failed",
153
- ...(wire.error?.message != null && { reason: wire.error.message }),
154
- raw: wire,
155
- }),
156
- ),
157
- Match.orElse((): ResearchState => ({ _tag: "Running" })),
158
- )
159
-
160
- // ---------------------------------------------------------------------------
161
- // HTTP
162
- // ---------------------------------------------------------------------------
163
-
164
- const baseUrl = (cfg: Config): string => cfg.baseUrl ?? "https://api.exa.ai"
165
-
166
- const authHeader = (cfg: Config) =>
167
- HttpClientRequest.setHeader("x-api-key", Redacted.value(cfg.apiKey))
168
-
169
- const transportFailure = (cause: unknown): AiError.AiError =>
170
- new AiError.Unavailable({ provider: "exa", raw: cause })
171
-
172
- const httpStatusError = (status: number, body: string): AiError.AiError => {
173
- const provider = "exa"
174
- const raw = body
175
- if (status === 429) return new AiError.RateLimited({ provider, raw })
176
- if (status === 408 || status === 504) return new AiError.Timeout({ provider, raw })
177
- if (status === 401) return new AiError.AuthFailed({ provider, subtype: "auth", raw })
178
- if (status === 403) return new AiError.AuthFailed({ provider, subtype: "permission", raw })
179
- if (status === 402) return new AiError.AuthFailed({ provider, subtype: "billing", raw })
180
- if (status >= 500) return new AiError.Unavailable({ provider, status, raw })
181
- return new AiError.InvalidRequest({ provider, raw })
182
- }
183
-
184
- // Deep research takes one instruction. Concatenate the user turns' text.
185
- const instructions = (history: ReadonlyArray<Items.HistoryItem>): string =>
186
- history
187
- .filter(Items.isMessage)
188
- .filter((m) => m.role === "user")
189
- .flatMap((m) => m.content)
190
- .filter(Items.isInputText)
191
- .map((b) => b.text)
192
- .join("\n")
193
-
194
- const submitBody = (request: ExaResearchRequest): Record<string, unknown> => ({
195
- instructions: instructions(request.history),
196
- model: request.model ?? "exa-research",
197
- ...(request.outputSchema !== undefined && {
198
- output: {
199
- schema: request.outputSchema.schema["~standard"].jsonSchema.input({ target: "draft-07" }),
200
- },
201
- }),
202
- })
203
-
204
- const submitJob = (
205
- cfg: Config,
206
- request: ExaResearchRequest,
207
- ): Effect.Effect<ResearchJobRef, AiError.AiError, HttpClient.HttpClient> =>
208
- Effect.gen(function* () {
209
- const client = yield* HttpClient.HttpClient
210
- const httpRequest = HttpClientRequest.post(`${baseUrl(cfg)}/research/v0/tasks`).pipe(
211
- authHeader(cfg),
212
- HttpClientRequest.bodyJsonUnsafe(submitBody(request)),
213
- )
214
- const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
215
- if (response.status >= 400) {
216
- const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
217
- return yield* httpStatusError(response.status, text)
218
- }
219
- const json = yield* response.json.pipe(Effect.mapError(transportFailure))
220
- const wire = yield* decodeSubmit(json).pipe(Effect.mapError(transportFailure))
221
- return Job.jobRef<Turn>("exa", wire.id)
222
- })
223
-
224
- const pollJob = (
225
- cfg: Config,
226
- ref: ResearchJobRef,
227
- ): Effect.Effect<ResearchState, AiError.AiError, HttpClient.HttpClient> =>
228
- Effect.gen(function* () {
229
- const client = yield* HttpClient.HttpClient
230
- const httpRequest = HttpClientRequest.get(`${baseUrl(cfg)}/research/v0/tasks/${ref.id}`).pipe(
231
- authHeader(cfg),
232
- )
233
- const response = yield* client.execute(httpRequest).pipe(Effect.mapError(transportFailure))
234
- if (response.status >= 400) {
235
- const text = yield* response.text.pipe(Effect.orElseSucceed(() => ""))
236
- return yield* httpStatusError(response.status, text)
237
- }
238
- const json = yield* response.json.pipe(Effect.mapError(transportFailure))
239
- const wire = yield* decodeResearch(json).pipe(Effect.mapError(transportFailure))
240
- return jobStateOf(wire)
241
- })
242
-
243
- const unsupported = (capability: string): AiError.Unsupported =>
244
- new AiError.Unsupported({
245
- provider: "exa",
246
- capability,
247
- reason: "Exa's research task API exposes no verified cancel endpoint.",
248
- })
249
-
250
- // ---------------------------------------------------------------------------
251
- // Constructors
252
- // ---------------------------------------------------------------------------
253
-
254
- const jobOps = (cfg: Config, client: HttpClient.HttpClient): ResearchJobOps<ExaResearchRequest> => {
255
- const withClient = <A>(e: Effect.Effect<A, AiError.AiError, HttpClient.HttpClient>) =>
256
- Effect.provideService(e, HttpClient.HttpClient, client)
257
- return {
258
- submit: (request) => withClient(submitJob(cfg, request)),
259
- poll: (ref) => withClient(pollJob(cfg, ref)),
260
- cancel: () => Effect.fail(unsupported("cancel")),
261
- }
262
- }
263
-
264
- /** Build an `ExaDeepResearchService`. For Layer setup, prefer {@link layer}. */
265
- export const make = (
266
- cfg: Config,
267
- ): Effect.Effect<ExaDeepResearchService, never, HttpClient.HttpClient> =>
268
- Effect.map(HttpClient.HttpClient, (client) => fromJob(jobOps(cfg, client), cfg.job))
269
-
270
- /**
271
- * Layer registering the provider-typed `ExaDeepResearch` tag and the generic
272
- * `DeepResearch` tag. No capability markers: the surface (poll, collect,
273
- * synthesized stream) is universal.
274
- */
275
- export const layer = (
276
- cfg: Config,
277
- ): Layer.Layer<ExaDeepResearch | DeepResearch, never, HttpClient.HttpClient> => {
278
- const typed = Layer.effect(ExaDeepResearch, make(cfg))
279
- const generic = Layer.effect(
280
- DeepResearch,
281
- Effect.map(HttpClient.HttpClient, (client): DeepResearchService => {
282
- const ops = jobOps(cfg, client)
283
- return fromJob<ResearchRequest>(
284
- {
285
- submit: (request) => ops.submit(request as ExaResearchRequest),
286
- poll: ops.poll,
287
- cancel: ops.cancel,
288
- },
289
- cfg.job,
290
- )
291
- }),
292
- )
293
- return Layer.merge(typed, generic)
294
- }