@effect-uai/core 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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-BWNu6VGp.d.mts} +116 -90
- package/dist/Items-BWNu6VGp.d.mts.map +1 -0
- package/dist/{Tool-2lC_XMth.d.mts → Tool-CXFHCQrT.d.mts} +42 -9
- package/dist/Tool-CXFHCQrT.d.mts.map +1 -0
- package/dist/{ToolEvent-cWZ2BnbS.d.mts → ToolEvent-nVijCrvm.d.mts} +2 -2
- package/dist/{ToolEvent-cWZ2BnbS.d.mts.map → ToolEvent-nVijCrvm.d.mts.map} +1 -1
- package/dist/{ToolResult-CL4hK_9z.d.mts → ToolResult-CQqKkcDa.d.mts} +2 -2
- package/dist/{ToolResult-CL4hK_9z.d.mts.map → ToolResult-CQqKkcDa.d.mts.map} +1 -1
- package/dist/{Toolkit-Dr9l_hNy.d.mts → Toolkit-BYPwwu-j.d.mts} +6 -6
- package/dist/Toolkit-BYPwwu-j.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 +13 -3
- 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 +91 -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 +1 -1
- package/dist/observability/Metrics.test.mjs +2 -2
- 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.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/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 +14 -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/research/DeepResearch.ts +137 -0
- 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
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
|
|
|
@@ -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
|
),
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Context, Effect, Stream } from "effect"
|
|
2
|
+
import * as AiError from "../domain/AiError.js"
|
|
3
|
+
import * as Job from "../job/Job.js"
|
|
4
|
+
import type { ResearchJobRef, ResearchRequest, ResearchState } from "../domain/Research.js"
|
|
5
|
+
import type { Turn } from "../domain/Turn.js"
|
|
6
|
+
import { TurnEvent } from "../domain/Turn.js"
|
|
7
|
+
|
|
8
|
+
export type { ResearchJobRef, ResearchRequest, ResearchState } from "../domain/Research.js"
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Deep research: submit a question, watch a long-running background job, collect
|
|
12
|
+
* one cited report. Every provider is modeled as a job (`submit` / `poll` /
|
|
13
|
+
* `cancel`), so the surface is uniform. The convenience derivations
|
|
14
|
+
* (`research` / `researchStream` / `collect` / `status`) are the same for every
|
|
15
|
+
* provider and are built once by {@link fromJob}, so an implementor states only
|
|
16
|
+
* its wire calls, never the poll loop.
|
|
17
|
+
*
|
|
18
|
+
* `research` is the blocking one-liner (submit then poll to the terminal `Turn`);
|
|
19
|
+
* `researchStream` forwards live progress as `TurnEvent`s, terminating in
|
|
20
|
+
* `TurnComplete` whose `turn` is the value `research` / `collect` return. The job
|
|
21
|
+
* ref is plain serializable data, so it can be persisted and collected from a
|
|
22
|
+
* later process where the provider's job is server-backed.
|
|
23
|
+
*/
|
|
24
|
+
export type DeepResearchServiceShape<Req> = {
|
|
25
|
+
readonly submit: (request: Req) => Effect.Effect<ResearchJobRef, AiError.AiError>
|
|
26
|
+
readonly status: (ref: ResearchJobRef) => Effect.Effect<ResearchState, AiError.AiError>
|
|
27
|
+
readonly collect: (ref: ResearchJobRef) => Effect.Effect<Turn, AiError.AiError>
|
|
28
|
+
readonly streamFrom: (ref: ResearchJobRef) => Stream.Stream<TurnEvent, AiError.AiError>
|
|
29
|
+
readonly cancel: (ref: ResearchJobRef) => Effect.Effect<void, AiError.AiError>
|
|
30
|
+
readonly research: (request: Req) => Effect.Effect<Turn, AiError.AiError>
|
|
31
|
+
readonly researchStream: (request: Req) => Stream.Stream<TurnEvent, AiError.AiError>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type DeepResearchService = DeepResearchServiceShape<ResearchRequest>
|
|
35
|
+
|
|
36
|
+
export class DeepResearch extends Context.Service<DeepResearch, DeepResearchService>()(
|
|
37
|
+
"@betalyra/effect-uai/DeepResearch",
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
// fromJob: build the whole service from the three job ops (+ an optional real
|
|
42
|
+
// live stream). The derivations are identical for every provider.
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
/** The wire ops a provider supplies. `streamFrom` is optional: poll-only jobs
|
|
46
|
+
* get a synthesized stream ({@link synthesizedStream}) in its place. */
|
|
47
|
+
export type ResearchJobOps<Req> = {
|
|
48
|
+
readonly submit: (request: Req) => Effect.Effect<ResearchJobRef, AiError.AiError>
|
|
49
|
+
readonly poll: (ref: ResearchJobRef) => Effect.Effect<ResearchState, AiError.AiError>
|
|
50
|
+
readonly cancel: (ref: ResearchJobRef) => Effect.Effect<void, AiError.AiError>
|
|
51
|
+
readonly streamFrom?: (ref: ResearchJobRef) => Stream.Stream<TurnEvent, AiError.AiError>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// A poll-only job has no event stream, so synthesize one: a leading "searching"
|
|
55
|
+
// progress event, then the terminal `TurnComplete` off the poll loop. Honest
|
|
56
|
+
// low-fidelity progress, not a fabricated per-search feed.
|
|
57
|
+
const synthesizedStream = <Req>(
|
|
58
|
+
ops: ResearchJobOps<Req>,
|
|
59
|
+
ref: ResearchJobRef,
|
|
60
|
+
config?: Job.JobConfig,
|
|
61
|
+
): Stream.Stream<TurnEvent, AiError.AiError> =>
|
|
62
|
+
Stream.concat(
|
|
63
|
+
Stream.make(TurnEvent.WebSearchCall({ status: "searching" })),
|
|
64
|
+
Stream.map(Stream.fromEffect(Job.collect(ops.poll, ref, config)), (turn) =>
|
|
65
|
+
TurnEvent.TurnComplete({ turn }),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Assemble a {@link DeepResearchServiceShape} from the job ops. `research` is
|
|
71
|
+
* `submit` then poll-to-settle (best-effort cancel on interrupt, via
|
|
72
|
+
* {@link Job.run}); `collect` / `status` are the detached equivalents;
|
|
73
|
+
* `researchStream` submits then attaches. Generic over the request type so a
|
|
74
|
+
* provider-typed tag keeps its narrowed `submit`. `config` tunes the poll
|
|
75
|
+
* cadence and overall timeout of every derived poll loop (see
|
|
76
|
+
* {@link Job.JobConfig} for the defaults).
|
|
77
|
+
*/
|
|
78
|
+
export const fromJob = <Req extends ResearchRequest>(
|
|
79
|
+
ops: ResearchJobOps<Req>,
|
|
80
|
+
config?: Job.JobConfig,
|
|
81
|
+
): DeepResearchServiceShape<Req> => {
|
|
82
|
+
const streamFrom =
|
|
83
|
+
ops.streamFrom ?? ((ref: ResearchJobRef) => synthesizedStream(ops, ref, config))
|
|
84
|
+
return {
|
|
85
|
+
submit: ops.submit,
|
|
86
|
+
status: ops.poll,
|
|
87
|
+
collect: (ref) => Job.collect(ops.poll, ref, config),
|
|
88
|
+
cancel: ops.cancel,
|
|
89
|
+
streamFrom,
|
|
90
|
+
research: (request) =>
|
|
91
|
+
Job.run({ submit: ops.submit(request), poll: ops.poll, cancel: ops.cancel }, config),
|
|
92
|
+
researchStream: (request) => Stream.unwrap(Effect.map(ops.submit(request), streamFrom)),
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Top-level accessors over the generic tag. Portable code uses these; they
|
|
98
|
+
// require only `DeepResearch` in `R`.
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
/** Submit and poll to the terminal `Turn`. Cancels the job on interrupt. */
|
|
102
|
+
export const research = (
|
|
103
|
+
request: ResearchRequest,
|
|
104
|
+
): Effect.Effect<Turn, AiError.AiError, DeepResearch> =>
|
|
105
|
+
Effect.flatMap(DeepResearch, (s) => s.research(request))
|
|
106
|
+
|
|
107
|
+
/** Submit and forward live progress, terminating in `TurnComplete`. */
|
|
108
|
+
export const researchStream = (
|
|
109
|
+
request: ResearchRequest,
|
|
110
|
+
): Stream.Stream<TurnEvent, AiError.AiError, DeepResearch> =>
|
|
111
|
+
Stream.unwrap(Effect.map(DeepResearch, (s) => s.researchStream(request)))
|
|
112
|
+
|
|
113
|
+
/** Submit a detached job and return its ref. */
|
|
114
|
+
export const submit = (
|
|
115
|
+
request: ResearchRequest,
|
|
116
|
+
): Effect.Effect<ResearchJobRef, AiError.AiError, DeepResearch> =>
|
|
117
|
+
Effect.flatMap(DeepResearch, (s) => s.submit(request))
|
|
118
|
+
|
|
119
|
+
/** Current state of a job. */
|
|
120
|
+
export const status = (
|
|
121
|
+
ref: ResearchJobRef,
|
|
122
|
+
): Effect.Effect<ResearchState, AiError.AiError, DeepResearch> =>
|
|
123
|
+
Effect.flatMap(DeepResearch, (s) => s.status(ref))
|
|
124
|
+
|
|
125
|
+
/** Poll a detached job to completion. */
|
|
126
|
+
export const collect = (ref: ResearchJobRef): Effect.Effect<Turn, AiError.AiError, DeepResearch> =>
|
|
127
|
+
Effect.flatMap(DeepResearch, (s) => s.collect(ref))
|
|
128
|
+
|
|
129
|
+
/** Attach a live progress stream to a job (call again to re-attach). */
|
|
130
|
+
export const streamFrom = (
|
|
131
|
+
ref: ResearchJobRef,
|
|
132
|
+
): Stream.Stream<TurnEvent, AiError.AiError, DeepResearch> =>
|
|
133
|
+
Stream.unwrap(Effect.map(DeepResearch, (s) => s.streamFrom(ref)))
|
|
134
|
+
|
|
135
|
+
/** Cancel a running job. */
|
|
136
|
+
export const cancel = (ref: ResearchJobRef): Effect.Effect<void, AiError.AiError, DeepResearch> =>
|
|
137
|
+
Effect.flatMap(DeepResearch, (s) => s.cancel(ref))
|