@effect-uai/core 0.10.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/{Image-BZmKfIdq.d.mts → Image-C4vuBmu0.d.mts} +11 -11
- package/dist/{Image-BZmKfIdq.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
- package/dist/{Items-BnIduEV2.d.mts → Items-BKQgqJ8A.d.mts} +136 -97
- package/dist/Items-BKQgqJ8A.d.mts.map +1 -0
- package/dist/{Tool-2lC_XMth.d.mts → Tool-u5bag8SO.d.mts} +42 -9
- package/dist/Tool-u5bag8SO.d.mts.map +1 -0
- package/dist/{ToolEvent-cWZ2BnbS.d.mts → ToolEvent-BBfOdhF_.d.mts} +2 -2
- package/dist/{ToolEvent-cWZ2BnbS.d.mts.map → ToolEvent-BBfOdhF_.d.mts.map} +1 -1
- package/dist/{ToolResult-CL4hK_9z.d.mts → ToolResult-CRpmkBXR.d.mts} +2 -2
- package/dist/{ToolResult-CL4hK_9z.d.mts.map → ToolResult-CRpmkBXR.d.mts.map} +1 -1
- package/dist/{Toolkit-Dr9l_hNy.d.mts → Toolkit-D4mQvhjq.d.mts} +6 -6
- package/dist/Toolkit-D4mQvhjq.d.mts.map +1 -0
- package/dist/browser/Browser.mjs +4 -4
- package/dist/browser/Browser.mjs.map +1 -1
- package/dist/browser/BrowserTool.d.mts +34 -33
- package/dist/browser/BrowserTool.d.mts.map +1 -1
- package/dist/{dist-DWgRbDN3.mjs → dist-C0m0QnUP.mjs} +71 -30
- package/dist/dist-C0m0QnUP.mjs.map +1 -0
- package/dist/{dist-B04dlFHg.mjs → dist-CcRV3-A5.mjs} +8 -8
- package/dist/{dist-B04dlFHg.mjs.map → dist-CcRV3-A5.mjs.map} +1 -1
- package/dist/domain/Citation.d.mts +60 -0
- package/dist/domain/Citation.d.mts.map +1 -0
- package/dist/domain/Citation.mjs +7 -0
- package/dist/domain/Citation.mjs.map +1 -0
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Items.mjs +28 -4
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Research.d.mts +37 -0
- package/dist/domain/Research.d.mts.map +1 -0
- package/dist/domain/Research.mjs +7 -0
- package/dist/domain/Research.mjs.map +1 -0
- package/dist/domain/Turn.d.mts +92 -9
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +8 -1
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/domain/Turn.test.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.mjs +2 -2
- package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
- package/dist/index.d.mts +12 -8
- package/dist/index.mjs +5 -1
- package/dist/job/Job.d.mts +80 -0
- package/dist/job/Job.d.mts.map +1 -0
- package/dist/job/Job.mjs +49 -0
- package/dist/job/Job.mjs.map +1 -0
- package/dist/language-model/LanguageModel.d.mts +2 -2
- package/dist/language-model/LanguageModel.mjs +2 -2
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +1 -1
- package/dist/loop/Loop.test.mjs +1 -1
- package/dist/music-generator/MusicGenerator.mjs +4 -4
- package/dist/music-generator/MusicGenerator.mjs.map +1 -1
- package/dist/music-generator/MusicGenerator.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +13 -5
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +30 -7
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/observability/Metrics.test.mjs +44 -2
- package/dist/observability/Metrics.test.mjs.map +1 -1
- package/dist/observability/Telemetry.test.mjs +2 -2
- package/dist/research/DeepResearch.d.mts +71 -0
- package/dist/research/DeepResearch.d.mts.map +1 -0
- package/dist/research/DeepResearch.mjs +61 -0
- package/dist/research/DeepResearch.mjs.map +1 -0
- package/dist/retry/Retry.d.mts +3 -3
- package/dist/retry/Retry.d.mts.map +1 -1
- package/dist/retry/Retry.mjs +30 -1
- package/dist/retry/Retry.mjs.map +1 -1
- package/dist/retry/Retry.test.mjs +1 -1
- package/dist/sandbox/Sandbox.mjs +18 -18
- package/dist/sandbox/Sandbox.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +8 -8
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
- package/dist/streaming/JSONL.d.mts +3 -1
- package/dist/streaming/JSONL.d.mts.map +1 -1
- package/dist/streaming/JSONL.mjs +5 -11
- package/dist/streaming/JSONL.mjs.map +1 -1
- package/dist/streaming/JSONL.test.mjs +10 -1
- package/dist/streaming/JSONL.test.mjs.map +1 -1
- package/dist/streaming/SSE.d.mts +5 -3
- package/dist/streaming/SSE.d.mts.map +1 -1
- package/dist/streaming/SSE.mjs +28 -38
- package/dist/streaming/SSE.mjs.map +1 -1
- package/dist/streaming/SSE.test.mjs +54 -4
- package/dist/streaming/SSE.test.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSandbox.d.mts +1 -1
- package/dist/tool/Approval.d.mts +3 -3
- package/dist/tool/Approval.test.mjs +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +27 -7
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +127 -3
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +1 -1
- package/dist/tool/ToolResult.d.mts +1 -1
- package/dist/tool/Toolkit.d.mts +1 -1
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +46 -2
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.mjs +3 -3
- package/dist/transcriber/Transcriber.mjs.map +1 -1
- package/dist/transcriber/Transcriber.test.mjs +1 -1
- package/dist/web-read/WebRead.mjs +1 -1
- package/dist/web-read/WebRead.mjs.map +1 -1
- package/dist/web-read/WebReadTool.d.mts +1 -1
- package/dist/web-search/WebSearch.mjs +1 -1
- package/dist/web-search/WebSearch.mjs.map +1 -1
- package/dist/web-search/WebSearchTool.d.mts +1 -1
- package/package.json +19 -3
- package/src/browser/Browser.ts +4 -4
- package/src/domain/Citation.ts +64 -0
- package/src/domain/Items.ts +32 -4
- package/src/domain/Research.ts +42 -0
- package/src/domain/Turn.ts +26 -0
- package/src/embedding-model/EmbeddingModel.ts +2 -2
- package/src/index.ts +4 -0
- package/src/job/Job.ts +125 -0
- package/src/language-model/LanguageModel.ts +2 -2
- package/src/music-generator/MusicGenerator.ts +4 -4
- package/src/observability/Metrics.test.ts +65 -0
- package/src/observability/Metrics.ts +53 -11
- package/src/research/DeepResearch.ts +137 -0
- package/src/retry/Retry.ts +44 -3
- package/src/sandbox/Sandbox.ts +18 -18
- package/src/speech-synthesizer/SpeechSynthesizer.ts +8 -8
- package/src/streaming/JSONL.test.ts +19 -0
- package/src/streaming/JSONL.ts +6 -38
- package/src/streaming/SSE.test.ts +86 -3
- package/src/streaming/SSE.ts +36 -80
- package/src/tool/Tool.test.ts +151 -1
- package/src/tool/Tool.ts +120 -22
- package/src/tool/Toolkit.test.ts +60 -0
- package/src/tool/Toolkit.ts +2 -2
- package/src/transcriber/Transcriber.ts +3 -3
- package/src/web-read/WebRead.ts +1 -1
- package/src/web-search/WebSearch.ts +1 -1
- package/dist/Items-BnIduEV2.d.mts.map +0 -1
- package/dist/Tool-2lC_XMth.d.mts.map +0 -1
- package/dist/Toolkit-Dr9l_hNy.d.mts.map +0 -1
- package/dist/dist-DWgRbDN3.mjs.map +0 -1
|
@@ -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
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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,22 @@
|
|
|
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
|
+
},
|
|
45
61
|
"./Browser": {
|
|
46
62
|
"types": "./dist/browser/Browser.d.mts",
|
|
47
63
|
"import": "./dist/browser/Browser.mjs"
|
|
@@ -226,11 +242,11 @@
|
|
|
226
242
|
"@standard-schema/spec": "^1.1.0"
|
|
227
243
|
},
|
|
228
244
|
"devDependencies": {
|
|
229
|
-
"effect": "4.0.0-beta.
|
|
245
|
+
"effect": "4.0.0-beta.94",
|
|
230
246
|
"typescript": "^6.0.3"
|
|
231
247
|
},
|
|
232
248
|
"peerDependencies": {
|
|
233
|
-
"effect": "4.0.0-beta.
|
|
249
|
+
"effect": ">=4.0.0-beta.94 <5.0.0"
|
|
234
250
|
},
|
|
235
251
|
"scripts": {
|
|
236
252
|
"build": "tsdown",
|
package/src/browser/Browser.ts
CHANGED
|
@@ -240,7 +240,7 @@ export class Browser extends Context.Service<Browser, BrowserService>()(
|
|
|
240
240
|
export const create = (
|
|
241
241
|
request: CommonSessionRequest,
|
|
242
242
|
): Effect.Effect<BrowserSession, BrowserError.BrowserError, Browser | Scope.Scope> =>
|
|
243
|
-
Effect.flatMap(Browser
|
|
243
|
+
Effect.flatMap(Browser, (s) => s.create(request))
|
|
244
244
|
|
|
245
245
|
/**
|
|
246
246
|
* Re-acquire an existing session by id. Same scope semantics as `create`,
|
|
@@ -249,14 +249,14 @@ export const create = (
|
|
|
249
249
|
export const attach = (
|
|
250
250
|
id: BrowserSessionId,
|
|
251
251
|
): Effect.Effect<BrowserSession, BrowserError.BrowserError, Browser | Scope.Scope> =>
|
|
252
|
-
Effect.flatMap(Browser
|
|
252
|
+
Effect.flatMap(Browser, (s) => s.attach(id))
|
|
253
253
|
|
|
254
254
|
/** Enumerate sessions for the configured account / project. */
|
|
255
255
|
export const list: Effect.Effect<
|
|
256
256
|
ReadonlyArray<BrowserSessionRef>,
|
|
257
257
|
BrowserError.BrowserError,
|
|
258
258
|
Browser
|
|
259
|
-
> = Effect.flatMap(Browser
|
|
259
|
+
> = Effect.flatMap(Browser, (s) => s.list)
|
|
260
260
|
|
|
261
261
|
/**
|
|
262
262
|
* Destroy a session from outside its owning scope. Most callers should let
|
|
@@ -265,4 +265,4 @@ export const list: Effect.Effect<
|
|
|
265
265
|
export const destroy = (
|
|
266
266
|
id: BrowserSessionId,
|
|
267
267
|
): Effect.Effect<void, BrowserError.BrowserError, Browser> =>
|
|
268
|
-
Effect.flatMap(Browser
|
|
268
|
+
Effect.flatMap(Browser, (s) => s.destroy(id))
|
|
@@ -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
|
+
}
|
package/src/domain/Items.ts
CHANGED
|
@@ -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
|
-
//
|
|
31
|
-
//
|
|
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
|
|
|
@@ -101,6 +111,12 @@ export type Role = typeof Role.Type
|
|
|
101
111
|
// The framework never reads or interprets it; provider modules decode
|
|
102
112
|
// their own data via their own typed readers (see e.g.
|
|
103
113
|
// the `@effect-uai/responses` package).
|
|
114
|
+
//
|
|
115
|
+
// The slot is shared, so a provider keys its data under its own name
|
|
116
|
+
// (`{ responses: ... }`, `{ gemini: ... }`) and reads only that key. One item
|
|
117
|
+
// can pass through several providers - falling back from one to another
|
|
118
|
+
// leaves both keys on it - and a provider that finds no key of its own must
|
|
119
|
+
// encode the item normally rather than assume the slot is its own.
|
|
104
120
|
// ---------------------------------------------------------------------------
|
|
105
121
|
|
|
106
122
|
const ProviderData = Schema.optional(Schema.Unknown)
|
|
@@ -171,8 +187,20 @@ export const isReasoning = Schema.is(Reasoning)
|
|
|
171
187
|
// Usage and stop reason
|
|
172
188
|
// ---------------------------------------------------------------------------
|
|
173
189
|
|
|
190
|
+
/**
|
|
191
|
+
* Prompt-cache token breakdown. `cached_tokens` is cache reads;
|
|
192
|
+
* `cache_write_tokens` is tokens written to the cache this turn (billed at a
|
|
193
|
+
* premium). Field names mirror the OpenAI Responses usage shape.
|
|
194
|
+
*
|
|
195
|
+
* Whether these are a subset of `input_tokens` or a separate bucket is
|
|
196
|
+
* provider-specific: OpenAI counts cache reads inside `input_tokens`, while
|
|
197
|
+
* Anthropic reports `input_tokens` as post-breakpoint tokens only, with reads
|
|
198
|
+
* and writes as separate additive buckets. Read a provider's usage in its own
|
|
199
|
+
* terms; do not assume one relationship holds everywhere.
|
|
200
|
+
*/
|
|
174
201
|
export const InputTokensDetails = Schema.Struct({
|
|
175
202
|
cached_tokens: Schema.optional(Schema.Number),
|
|
203
|
+
cache_write_tokens: Schema.optional(Schema.Number),
|
|
176
204
|
})
|
|
177
205
|
export type InputTokensDetails = typeof InputTokensDetails.Type
|
|
178
206
|
|
|
@@ -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>
|
package/src/domain/Turn.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
@@ -4,10 +4,14 @@ export * as Browser from "./browser/Browser.js"
|
|
|
4
4
|
export * as BrowserError from "./browser/BrowserError.js"
|
|
5
5
|
export * as BrowserTool from "./browser/BrowserTool.js"
|
|
6
6
|
export * as Capabilities from "./capabilities/Capabilities.js"
|
|
7
|
+
export * as Citation from "./domain/Citation.js"
|
|
8
|
+
export * as DeepResearch from "./research/DeepResearch.js"
|
|
7
9
|
export * as Image from "./domain/Image.js"
|
|
8
10
|
export * as Items from "./domain/Items.js"
|
|
11
|
+
export * as Job from "./job/Job.js"
|
|
9
12
|
export * as Media from "./domain/Media.js"
|
|
10
13
|
export * as Music from "./domain/Music.js"
|
|
14
|
+
export * as Research from "./domain/Research.js"
|
|
11
15
|
export * as Transcript from "./domain/Transcript.js"
|
|
12
16
|
export * as Turn from "./domain/Turn.js"
|
|
13
17
|
export * as Embedding from "./embedding-model/Embedding.js"
|
package/src/job/Job.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { Duration, Effect, Schedule } from "effect"
|
|
2
|
+
import * as AiError from "../domain/AiError.js"
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Status + handle
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalized state of a provider-hosted background job. Implementors map their
|
|
10
|
+
* provider's status vocabulary onto these four states. The terminal states
|
|
11
|
+
* carry their payload: `Succeeded` the collected result, `Failed` an optional
|
|
12
|
+
* reason and the raw provider error. This mirrors the wire, where a single
|
|
13
|
+
* poll response returns both status and (when done) the result.
|
|
14
|
+
*/
|
|
15
|
+
export type JobState<A> =
|
|
16
|
+
| { readonly _tag: "Pending" }
|
|
17
|
+
| { readonly _tag: "Running" }
|
|
18
|
+
| { readonly _tag: "Succeeded"; readonly result: A }
|
|
19
|
+
| { readonly _tag: "Failed"; readonly reason?: string; readonly raw?: unknown }
|
|
20
|
+
|
|
21
|
+
/** The two states that end a poll loop. */
|
|
22
|
+
export const isSettled = <A>(
|
|
23
|
+
state: JobState<A>,
|
|
24
|
+
): state is Extract<JobState<A>, { _tag: "Succeeded" | "Failed" }> =>
|
|
25
|
+
state._tag === "Succeeded" || state._tag === "Failed"
|
|
26
|
+
|
|
27
|
+
declare const ResultType: unique symbol
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Opaque, provider-tagged handle to a running background job. Jobs can run for
|
|
31
|
+
* minutes and outlive a single process, so the handle is detachable: submit
|
|
32
|
+
* now, collect later. `provider` tags who owns `id`.
|
|
33
|
+
*
|
|
34
|
+
* Parameterized by the result type `A` it collects to, so a ref cannot be
|
|
35
|
+
* crossed between capabilities (a `JobRef<Turn>` is not a `JobRef<VideoResult>`).
|
|
36
|
+
* The brand is phantom: the runtime value is just `{ _tag, provider, id }`, so
|
|
37
|
+
* a ref stays plain serializable data for persistence across restarts.
|
|
38
|
+
*/
|
|
39
|
+
export type JobRef<A = unknown> = {
|
|
40
|
+
readonly _tag: "JobRef"
|
|
41
|
+
readonly provider: string
|
|
42
|
+
readonly id: string
|
|
43
|
+
readonly [ResultType]?: A
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const jobRef = <A = unknown>(provider: string, id: string): JobRef<A> => ({
|
|
47
|
+
_tag: "JobRef",
|
|
48
|
+
provider,
|
|
49
|
+
id,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Poll driver
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The three wire operations a poll-based async job supplies. `run` / `collect`
|
|
58
|
+
* drive the submit -> poll -> settle cadence around them, so an implementor
|
|
59
|
+
* states only its provider calls, not the loop. `poll` returns the current
|
|
60
|
+
* `JobState`, carrying the result in `Succeeded`. Generic over the collected
|
|
61
|
+
* `Result`.
|
|
62
|
+
*/
|
|
63
|
+
export type JobOps<Result> = {
|
|
64
|
+
readonly submit: Effect.Effect<JobRef<Result>, AiError.AiError>
|
|
65
|
+
readonly poll: (ref: JobRef<Result>) => Effect.Effect<JobState<Result>, AiError.AiError>
|
|
66
|
+
readonly cancel: (ref: JobRef<Result>) => Effect.Effect<void, AiError.AiError>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type JobConfig = {
|
|
70
|
+
/** Cadence between status fetches. Default 10 seconds, jittered. */
|
|
71
|
+
readonly pollInterval?: Duration.Input
|
|
72
|
+
/** Overall wait cap; exceeding it fails `Timeout`. Default 45 minutes. */
|
|
73
|
+
readonly timeout?: Duration.Input
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const DEFAULT_POLL_INTERVAL: Duration.Input = "10 seconds"
|
|
77
|
+
const DEFAULT_TIMEOUT: Duration.Input = "45 minutes"
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Poll `poll(ref)` on a jittered fixed cadence until the job settles, then
|
|
81
|
+
* return the result or fail `GenerationFailed`. Bounded by `timeout`, which
|
|
82
|
+
* fails `Timeout` if the job never settles. Takes the poll op alone (not the
|
|
83
|
+
* full `JobOps`) so a detached `collect(ref)` needs no submit-bound request.
|
|
84
|
+
*/
|
|
85
|
+
export const collect = <Result>(
|
|
86
|
+
poll: (ref: JobRef<Result>) => Effect.Effect<JobState<Result>, AiError.AiError>,
|
|
87
|
+
ref: JobRef<Result>,
|
|
88
|
+
config?: JobConfig,
|
|
89
|
+
): Effect.Effect<Result, AiError.AiError> =>
|
|
90
|
+
poll(ref).pipe(
|
|
91
|
+
Effect.repeat({
|
|
92
|
+
schedule: Schedule.jittered(Schedule.spaced(config?.pollInterval ?? DEFAULT_POLL_INTERVAL)),
|
|
93
|
+
until: isSettled,
|
|
94
|
+
}),
|
|
95
|
+
Effect.flatMap((state) =>
|
|
96
|
+
state._tag === "Succeeded"
|
|
97
|
+
? Effect.succeed(state.result)
|
|
98
|
+
: Effect.fail(
|
|
99
|
+
new AiError.GenerationFailed({
|
|
100
|
+
provider: ref.provider,
|
|
101
|
+
message: state._tag === "Failed" ? (state.reason ?? "job failed") : "job failed",
|
|
102
|
+
raw: state._tag === "Failed" ? (state.raw ?? ref) : ref,
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
),
|
|
106
|
+
Effect.timeoutOrElse({
|
|
107
|
+
duration: config?.timeout ?? DEFAULT_TIMEOUT,
|
|
108
|
+
orElse: () => Effect.fail(new AiError.Timeout({ provider: ref.provider, raw: ref })),
|
|
109
|
+
}),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Submit, then poll to completion. Best-effort cancels the server job if the
|
|
114
|
+
* caller interrupts (where the provider supports it). The one-call path; use
|
|
115
|
+
* `ops.submit` + `collect` directly for the detached case.
|
|
116
|
+
*/
|
|
117
|
+
export const run = <Result>(
|
|
118
|
+
ops: JobOps<Result>,
|
|
119
|
+
config?: JobConfig,
|
|
120
|
+
): Effect.Effect<Result, AiError.AiError> =>
|
|
121
|
+
ops.submit.pipe(
|
|
122
|
+
Effect.flatMap((ref) =>
|
|
123
|
+
collect(ops.poll, ref, config).pipe(Effect.onInterrupt(() => Effect.ignore(ops.cancel(ref)))),
|
|
124
|
+
),
|
|
125
|
+
)
|
|
@@ -65,7 +65,7 @@ export class LanguageModel extends Context.Service<LanguageModel, LanguageModelS
|
|
|
65
65
|
export const streamTurn = (
|
|
66
66
|
request: CommonRequest,
|
|
67
67
|
): Stream.Stream<TurnEvent, AiError.AiError, LanguageModel> =>
|
|
68
|
-
Stream.unwrap(Effect.map(LanguageModel
|
|
68
|
+
Stream.unwrap(Effect.map(LanguageModel, (m) => m.streamTurn(request)))
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
71
|
* Drain a single turn and return the assembled `Turn`. Delegates to the
|
|
@@ -74,7 +74,7 @@ export const streamTurn = (
|
|
|
74
74
|
* {@link turnFromStream}.
|
|
75
75
|
*/
|
|
76
76
|
export const turn = (request: CommonRequest): Effect.Effect<Turn, AiError.AiError, LanguageModel> =>
|
|
77
|
-
Effect.flatMap(LanguageModel
|
|
77
|
+
Effect.flatMap(LanguageModel, (m) => m.turn(request))
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Build a `turn` implementation from a `streamTurn` implementation.
|
|
@@ -94,13 +94,13 @@ export class MusicInteractiveSession extends Context.Service<MusicInteractiveSes
|
|
|
94
94
|
export const generate = (
|
|
95
95
|
request: CommonGenerateMusicRequest,
|
|
96
96
|
): Effect.Effect<GenerateResult, AiError.AiError, MusicGenerator> =>
|
|
97
|
-
Effect.flatMap(MusicGenerator
|
|
97
|
+
Effect.flatMap(MusicGenerator, (s) => s.generate(request))
|
|
98
98
|
|
|
99
99
|
/** Prompt in, audio chunks out. */
|
|
100
100
|
export const streamGeneration = (
|
|
101
101
|
request: CommonStreamGenerateMusicRequest,
|
|
102
102
|
): Stream.Stream<AudioChunk, AiError.AiError, MusicGenerator> =>
|
|
103
|
-
Stream.unwrap(Effect.map(MusicGenerator
|
|
103
|
+
Stream.unwrap(Effect.map(MusicGenerator, (s) => s.streamGeneration(request)))
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
106
|
* Bidirectional generation. Dual-arity: pipeable (data-last) and
|
|
@@ -156,8 +156,8 @@ export const streamGenerationFrom: {
|
|
|
156
156
|
> =>
|
|
157
157
|
Stream.unwrap(
|
|
158
158
|
Effect.gen(function* () {
|
|
159
|
-
const s = yield* MusicGenerator
|
|
160
|
-
yield* MusicInteractiveSession
|
|
159
|
+
const s = yield* MusicGenerator
|
|
160
|
+
yield* MusicInteractiveSession
|
|
161
161
|
return s.streamGenerationFrom(input, request)
|
|
162
162
|
}),
|
|
163
163
|
),
|
|
@@ -25,6 +25,9 @@ const turnComplete = (usage: Usage): TurnEvent =>
|
|
|
25
25
|
|
|
26
26
|
const text = (s: string): TurnEvent => TurnEvent.TextDelta({ text: s })
|
|
27
27
|
|
|
28
|
+
const toolArgs = (s: string): TurnEvent =>
|
|
29
|
+
TurnEvent.ToolCallArgsDelta({ call_id: "call_1", delta: s })
|
|
30
|
+
|
|
28
31
|
const samples = (events: ReadonlyArray<unknown>): ReadonlyArray<MetricEvent> =>
|
|
29
32
|
events.filter(isMetricEvent)
|
|
30
33
|
|
|
@@ -94,6 +97,30 @@ describe("tokenTotals", () => {
|
|
|
94
97
|
expect(m).toEqual({ name: "effect_uai_output_tokens", kind: "counter", value: 7 })
|
|
95
98
|
}),
|
|
96
99
|
)
|
|
100
|
+
|
|
101
|
+
it.effect("carries cache read and write tokens through usage and measurements", () =>
|
|
102
|
+
Effect.gen(function* () {
|
|
103
|
+
const stream = Stream.make(
|
|
104
|
+
turnComplete({
|
|
105
|
+
input_tokens: 94,
|
|
106
|
+
input_tokens_details: { cached_tokens: 31851, cache_write_tokens: 604 },
|
|
107
|
+
}),
|
|
108
|
+
turnComplete({ input_tokens: 6, input_tokens_details: { cached_tokens: 32000 } }),
|
|
109
|
+
)
|
|
110
|
+
const out = yield* Stream.runCollect(stream.pipe(tokenTotals))
|
|
111
|
+
const totals = tagged(out, "TokenTotals") as ReadonlyArray<TokenTotals>
|
|
112
|
+
expect(totals.map((t) => t.cumulative.input_tokens_details?.cache_write_tokens)).toEqual([
|
|
113
|
+
604, 604,
|
|
114
|
+
])
|
|
115
|
+
expect(totals.map((t) => t.cumulative.input_tokens_details?.cached_tokens)).toEqual([
|
|
116
|
+
31851, 63851,
|
|
117
|
+
])
|
|
118
|
+
const write = head(totals).measurements.find(
|
|
119
|
+
(m) => m.name === "effect_uai_cache_write_tokens",
|
|
120
|
+
)
|
|
121
|
+
expect(write).toEqual({ name: "effect_uai_cache_write_tokens", kind: "counter", value: 604 })
|
|
122
|
+
}),
|
|
123
|
+
)
|
|
97
124
|
})
|
|
98
125
|
|
|
99
126
|
describe("timeToFirstToken", () => {
|
|
@@ -238,6 +265,44 @@ describe("throughput (metronome)", () => {
|
|
|
238
265
|
expect(head(rates).ratePerSecond).toBeGreaterThan(0)
|
|
239
266
|
}),
|
|
240
267
|
)
|
|
268
|
+
|
|
269
|
+
it.effect("counts tool-call arguments: a turn with no prose still reports a rate", () =>
|
|
270
|
+
Effect.gen(function* () {
|
|
271
|
+
// The shape of an agent turn that only calls tools. Before output deltas
|
|
272
|
+
// were counted beyond TextDelta this measured 0 for the whole run.
|
|
273
|
+
const source = Stream.make(toolArgs('{"city":'), toolArgs('"Lisbon"}')).pipe(
|
|
274
|
+
Stream.concat(timed<TurnEvent>([["10 seconds", turnComplete({})]])),
|
|
275
|
+
)
|
|
276
|
+
const stream = source.pipe(
|
|
277
|
+
throughput({ every: "1 second", unit: "char" }),
|
|
278
|
+
metricEvents,
|
|
279
|
+
Stream.take(1),
|
|
280
|
+
)
|
|
281
|
+
const out = yield* runTimed(stream, ["1 second"])
|
|
282
|
+
const rates = out.filter((e): e is Throughput => e._tag === "Throughput")
|
|
283
|
+
expect(rates).toHaveLength(1)
|
|
284
|
+
expect(head(rates).ratePerSecond).toBeGreaterThan(0)
|
|
285
|
+
}),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
// Runs on the real clock: this asserts an absence, so there is no sample to
|
|
289
|
+
// wait for and the stream has to end by the source closing. Under TestClock
|
|
290
|
+
// that path does not settle, because the metronome re-arms a sleep that no
|
|
291
|
+
// remaining `adjust` resolves.
|
|
292
|
+
it.live("ignores events that carry no generated output", () =>
|
|
293
|
+
Effect.gen(function* () {
|
|
294
|
+
// ToolCallStart and UsageUpdate are turn bookkeeping, not output.
|
|
295
|
+
const source = Stream.make(
|
|
296
|
+
TurnEvent.ToolCallStart({ call_id: "call_1", name: "get_weather" }),
|
|
297
|
+
TurnEvent.UsageUpdate({ usage: { output_tokens: 5 } }),
|
|
298
|
+
).pipe(Stream.concat(timed<TurnEvent>([["30 millis", turnComplete({})]])))
|
|
299
|
+
const stream = source.pipe(throughput({ every: "10 millis", unit: "char" }), metricEvents)
|
|
300
|
+
const out = yield* Stream.runCollect(stream)
|
|
301
|
+
// No output delta ever landed, so the accumulator never started and the
|
|
302
|
+
// metronome ticks produce nothing.
|
|
303
|
+
expect(out.filter((e): e is Throughput => e._tag === "Throughput")).toHaveLength(0)
|
|
304
|
+
}),
|
|
305
|
+
)
|
|
241
306
|
})
|
|
242
307
|
|
|
243
308
|
describe("custom events and helpers", () => {
|