@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
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
Stream,
|
|
13
13
|
} from "effect"
|
|
14
14
|
import type { Usage } from "../domain/Items.js"
|
|
15
|
-
import type
|
|
15
|
+
import { type Turn, TurnEvent } from "../domain/Turn.js"
|
|
16
16
|
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
18
|
// Self-describing measurements (the exporter-facing payload)
|
|
@@ -126,6 +126,7 @@ const MetricName = {
|
|
|
126
126
|
totalTokens: "effect_uai_total_tokens",
|
|
127
127
|
reasoningTokens: "effect_uai_reasoning_tokens",
|
|
128
128
|
cachedInputTokens: "effect_uai_cached_input_tokens",
|
|
129
|
+
cacheWriteTokens: "effect_uai_cache_write_tokens",
|
|
129
130
|
} as const
|
|
130
131
|
|
|
131
132
|
/** Read a structural `_tag` off any value without narrowing to a closed union. */
|
|
@@ -141,6 +142,34 @@ const contentDeltaKind = (ev: unknown): Option.Option<"text" | "reasoning"> => {
|
|
|
141
142
|
: Option.none()
|
|
142
143
|
}
|
|
143
144
|
|
|
145
|
+
const isTextDelta = TurnEvent.$is("TextDelta")
|
|
146
|
+
const isReasoningDelta = TurnEvent.$is("ReasoningDelta")
|
|
147
|
+
const isRefusalDelta = TurnEvent.$is("RefusalDelta")
|
|
148
|
+
const isToolCallArgsDelta = TurnEvent.$is("ToolCallArgsDelta")
|
|
149
|
+
|
|
150
|
+
/** A delta carrying generated output, whatever the model was producing. */
|
|
151
|
+
export type OutputDelta = Extract<
|
|
152
|
+
TurnEvent,
|
|
153
|
+
{ readonly _tag: "TextDelta" | "ReasoningDelta" | "RefusalDelta" | "ToolCallArgsDelta" }
|
|
154
|
+
>
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Narrow to a delta that contributes to the model's output stream. Tool-call
|
|
158
|
+
* arguments count: an agent turn can be almost entirely `ToolCallArgsDelta`,
|
|
159
|
+
* and measuring only prose reports a near-zero rate for it.
|
|
160
|
+
*
|
|
161
|
+
* The single place the output-delta tag list lives — the throughput
|
|
162
|
+
* accumulator and `unitCount` both read it. Distinct from `contentDeltaKind`,
|
|
163
|
+
* which answers a narrower question for first-token latency.
|
|
164
|
+
*/
|
|
165
|
+
const outputDelta = (ev: unknown): Option.Option<OutputDelta> =>
|
|
166
|
+
isTextDelta(ev) || isReasoningDelta(ev) || isRefusalDelta(ev) || isToolCallArgsDelta(ev)
|
|
167
|
+
? Option.some(ev)
|
|
168
|
+
: Option.none()
|
|
169
|
+
|
|
170
|
+
const outputTextOf = (delta: OutputDelta): string =>
|
|
171
|
+
isToolCallArgsDelta(delta) ? delta.delta : delta.text
|
|
172
|
+
|
|
144
173
|
const isTurnCompleteEvent = (ev: unknown): boolean => tagOf(ev) === "TurnComplete"
|
|
145
174
|
|
|
146
175
|
const turnOf = (ev: unknown): Turn =>
|
|
@@ -154,6 +183,10 @@ const addUsage = (a: Usage, b: Usage): Usage => {
|
|
|
154
183
|
a.input_tokens_details?.cached_tokens,
|
|
155
184
|
b.input_tokens_details?.cached_tokens,
|
|
156
185
|
)
|
|
186
|
+
const cacheWrite = sumOptional(
|
|
187
|
+
a.input_tokens_details?.cache_write_tokens,
|
|
188
|
+
b.input_tokens_details?.cache_write_tokens,
|
|
189
|
+
)
|
|
157
190
|
const reasoning = sumOptional(
|
|
158
191
|
a.output_tokens_details?.reasoning_tokens,
|
|
159
192
|
b.output_tokens_details?.reasoning_tokens,
|
|
@@ -162,7 +195,13 @@ const addUsage = (a: Usage, b: Usage): Usage => {
|
|
|
162
195
|
input_tokens: sumOptional(a.input_tokens, b.input_tokens),
|
|
163
196
|
output_tokens: sumOptional(a.output_tokens, b.output_tokens),
|
|
164
197
|
total_tokens: sumOptional(a.total_tokens, b.total_tokens),
|
|
165
|
-
input_tokens_details:
|
|
198
|
+
input_tokens_details:
|
|
199
|
+
cached === undefined && cacheWrite === undefined
|
|
200
|
+
? undefined
|
|
201
|
+
: {
|
|
202
|
+
...(cached !== undefined && { cached_tokens: cached }),
|
|
203
|
+
...(cacheWrite !== undefined && { cache_write_tokens: cacheWrite }),
|
|
204
|
+
},
|
|
166
205
|
output_tokens_details: reasoning === undefined ? undefined : { reasoning_tokens: reasoning },
|
|
167
206
|
}
|
|
168
207
|
}
|
|
@@ -175,6 +214,7 @@ const usageMeasurements = (usage: Usage): ReadonlyArray<Measurement> => {
|
|
|
175
214
|
[MetricName.totalTokens, usage.total_tokens],
|
|
176
215
|
[MetricName.reasoningTokens, usage.output_tokens_details?.reasoning_tokens],
|
|
177
216
|
[MetricName.cachedInputTokens, usage.input_tokens_details?.cached_tokens],
|
|
217
|
+
[MetricName.cacheWriteTokens, usage.input_tokens_details?.cache_write_tokens],
|
|
178
218
|
]
|
|
179
219
|
return Arr.filterMap(pairs, ([name, value]) =>
|
|
180
220
|
value === undefined
|
|
@@ -349,8 +389,11 @@ export type ThroughputOptions = {
|
|
|
349
389
|
readonly every?: Duration.Input
|
|
350
390
|
/** What a "unit" is. Default `"char"` (exact and chunking-independent). */
|
|
351
391
|
readonly unit?: "char" | "token" | "event"
|
|
352
|
-
/**
|
|
353
|
-
|
|
392
|
+
/**
|
|
393
|
+
* Token counter for `unit: "token"`. Called only for deltas that carry
|
|
394
|
+
* generated output. Absent => approximate 1 per such delta.
|
|
395
|
+
*/
|
|
396
|
+
readonly tokenizer?: (event: OutputDelta) => Effect.Effect<number>
|
|
354
397
|
/** Rate definition. Default `"windowed"` (current speed, no cold-start bias). */
|
|
355
398
|
readonly mode?: "windowed" | "cumulative"
|
|
356
399
|
/** EWMA smoothing. Omitted => off; `"default"` => a sensible factor. */
|
|
@@ -434,14 +477,13 @@ const unitCount = (
|
|
|
434
477
|
unit: "char" | "token" | "event",
|
|
435
478
|
tokenizer: ThroughputOptions["tokenizer"],
|
|
436
479
|
): Effect.Effect<number> => {
|
|
437
|
-
|
|
438
|
-
|
|
480
|
+
const output = outputDelta(ev)
|
|
481
|
+
if (Option.isNone(output)) return Effect.succeed(0)
|
|
482
|
+
const delta = output.value
|
|
439
483
|
return Match.value(unit).pipe(
|
|
440
|
-
Match.when("char", () => Effect.succeed([...
|
|
484
|
+
Match.when("char", () => Effect.succeed([...outputTextOf(delta)].length)),
|
|
441
485
|
Match.when("event", () => Effect.succeed(1)),
|
|
442
|
-
Match.when("token", () =>
|
|
443
|
-
tokenizer === undefined ? Effect.succeed(1) : tokenizer(ev as TurnEvent),
|
|
444
|
-
),
|
|
486
|
+
Match.when("token", () => (tokenizer === undefined ? Effect.succeed(1) : tokenizer(delta))),
|
|
445
487
|
Match.exhaustive,
|
|
446
488
|
)
|
|
447
489
|
}
|
|
@@ -465,7 +507,7 @@ export const throughput =
|
|
|
465
507
|
Stream.tap((ev) =>
|
|
466
508
|
isTurnCompleteEvent(ev)
|
|
467
509
|
? Ref.update(ref, resetTurn)
|
|
468
|
-
:
|
|
510
|
+
: Option.isSome(outputDelta(ev))
|
|
469
511
|
? Effect.flatMap(Clock.currentTimeMillis, (now) =>
|
|
470
512
|
Effect.flatMap(unitCount(ev, unit, tokenizer), (n) =>
|
|
471
513
|
Ref.update(ref, (state) => addUnits(state, n, now)),
|
|
@@ -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))
|
package/src/retry/Retry.ts
CHANGED
|
@@ -29,14 +29,55 @@ import type * as AiError from "../domain/AiError.js"
|
|
|
29
29
|
* variants if needed (e.g. a custom schedule that observes `cause`).
|
|
30
30
|
*/
|
|
31
31
|
export class Retryable extends Data.TaggedError("RetryableAi")<{
|
|
32
|
-
readonly cause:
|
|
32
|
+
readonly cause:
|
|
33
|
+
| AiError.RateLimited
|
|
34
|
+
| AiError.Unavailable
|
|
35
|
+
| AiError.Timeout
|
|
36
|
+
| AiError.InvalidRequest
|
|
33
37
|
}> {}
|
|
34
38
|
|
|
39
|
+
/**
|
|
40
|
+
* `InvalidRequest` payloads that are a transient fault on the provider's side
|
|
41
|
+
* rather than a defect in the request.
|
|
42
|
+
*
|
|
43
|
+
* The class exists because a provider can only answer within its own error
|
|
44
|
+
* vocabulary: a constrained-decoding grammar that times out while compiling is
|
|
45
|
+
* a cold-start timeout, but it arrives as `invalid_request_error` because that
|
|
46
|
+
* is where the vendor files schema problems. Replaying it succeeds — the same
|
|
47
|
+
* schema compiles on the next call.
|
|
48
|
+
*
|
|
49
|
+
* An allow-list rather than a general softening of `InvalidRequest`, and it
|
|
50
|
+
* must stay one. The whole point of that tag being fatal is that a genuinely
|
|
51
|
+
* malformed request retried on a schedule burns quota to arrive at the same
|
|
52
|
+
* rejection, slowly. An entry earns its place by having been observed to
|
|
53
|
+
* succeed on replay.
|
|
54
|
+
*/
|
|
55
|
+
const TRANSIENT_INVALID_REQUEST: ReadonlyArray<RegExp> = [/grammar compilation timed out/i]
|
|
56
|
+
|
|
57
|
+
/** The payload as text, for matching. `raw` is whatever the provider handed
|
|
58
|
+
* back — usually its response body as a string, sometimes a parsed object,
|
|
59
|
+
* and occasionally something that will not serialise. */
|
|
60
|
+
const rawText = (raw: unknown): string => {
|
|
61
|
+
if (typeof raw === "string") return raw
|
|
62
|
+
try {
|
|
63
|
+
return JSON.stringify(raw) ?? ""
|
|
64
|
+
} catch {
|
|
65
|
+
return String(raw)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isTransientInvalidRequest = (e: AiError.AiError): boolean =>
|
|
70
|
+
e._tag === "InvalidRequest" &&
|
|
71
|
+
TRANSIENT_INVALID_REQUEST.some((pattern) => pattern.test(rawText(e.raw)))
|
|
72
|
+
|
|
35
73
|
/** Type-narrowing predicate for the retryable subset. */
|
|
36
74
|
export const isRetryable = (
|
|
37
75
|
e: AiError.AiError,
|
|
38
|
-
): e is AiError.RateLimited | AiError.Unavailable | AiError.Timeout =>
|
|
39
|
-
e._tag === "RateLimited" ||
|
|
76
|
+
): e is AiError.RateLimited | AiError.Unavailable | AiError.Timeout | AiError.InvalidRequest =>
|
|
77
|
+
e._tag === "RateLimited" ||
|
|
78
|
+
e._tag === "Unavailable" ||
|
|
79
|
+
e._tag === "Timeout" ||
|
|
80
|
+
isTransientInvalidRequest(e)
|
|
40
81
|
|
|
41
82
|
// Lift events to Items, non-retryable failures to Terminal values
|
|
42
83
|
// (escape retry), retryable failures to wrapped errors (only thing
|
package/src/sandbox/Sandbox.ts
CHANGED
|
@@ -520,7 +520,7 @@ export class SandboxPortExposure extends Context.Service<SandboxPortExposure, vo
|
|
|
520
520
|
export const create = (
|
|
521
521
|
request: CommonCreateRequest,
|
|
522
522
|
): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>
|
|
523
|
-
Effect.flatMap(Sandbox
|
|
523
|
+
Effect.flatMap(Sandbox, (s) => s.create(request))
|
|
524
524
|
|
|
525
525
|
/**
|
|
526
526
|
* Re-acquire an existing sandbox by id. Same scope semantics as
|
|
@@ -529,14 +529,14 @@ export const create = (
|
|
|
529
529
|
export const attach = (
|
|
530
530
|
id: SandboxId,
|
|
531
531
|
): Effect.Effect<SandboxInstance, SandboxError.SandboxError, Sandbox | Scope.Scope> =>
|
|
532
|
-
Effect.flatMap(Sandbox
|
|
532
|
+
Effect.flatMap(Sandbox, (s) => s.attach(id))
|
|
533
533
|
|
|
534
534
|
/** Enumerate sandboxes for the configured account / project. */
|
|
535
535
|
export const list: Effect.Effect<
|
|
536
536
|
ReadonlyArray<SandboxRef>,
|
|
537
537
|
SandboxError.SandboxError,
|
|
538
538
|
Sandbox
|
|
539
|
-
> = Effect.flatMap(Sandbox
|
|
539
|
+
> = Effect.flatMap(Sandbox, (s) => s.list)
|
|
540
540
|
|
|
541
541
|
/**
|
|
542
542
|
* Escape hatch for destroying a sandbox from outside its owning
|
|
@@ -544,7 +544,7 @@ export const list: Effect.Effect<
|
|
|
544
544
|
* automatically.
|
|
545
545
|
*/
|
|
546
546
|
export const destroy = (id: SandboxId): Effect.Effect<void, SandboxError.SandboxError, Sandbox> =>
|
|
547
|
-
Effect.flatMap(Sandbox
|
|
547
|
+
Effect.flatMap(Sandbox, (s) => s.destroy(id))
|
|
548
548
|
|
|
549
549
|
/**
|
|
550
550
|
* Create a snapshot of a live sandbox. Requires
|
|
@@ -557,8 +557,8 @@ export const snapshot = (
|
|
|
557
557
|
name?: string,
|
|
558
558
|
): Effect.Effect<SnapshotId, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>
|
|
559
559
|
Effect.gen(function* () {
|
|
560
|
-
const s = yield* Sandbox
|
|
561
|
-
yield* SandboxSnapshots
|
|
560
|
+
const s = yield* Sandbox
|
|
561
|
+
yield* SandboxSnapshots
|
|
562
562
|
return yield* s.snapshots.create(from, name)
|
|
563
563
|
})
|
|
564
564
|
|
|
@@ -570,8 +570,8 @@ export const createVolume = (
|
|
|
570
570
|
options?: { readonly quotaBytes?: number },
|
|
571
571
|
): Effect.Effect<VolumeId, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>
|
|
572
572
|
Effect.gen(function* () {
|
|
573
|
-
const s = yield* Sandbox
|
|
574
|
-
yield* SandboxVolumes
|
|
573
|
+
const s = yield* Sandbox
|
|
574
|
+
yield* SandboxVolumes
|
|
575
575
|
return yield* s.volumes.create(name, options)
|
|
576
576
|
})
|
|
577
577
|
|
|
@@ -580,8 +580,8 @@ export const destroyVolume = (
|
|
|
580
580
|
id: VolumeId,
|
|
581
581
|
): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxVolumes> =>
|
|
582
582
|
Effect.gen(function* () {
|
|
583
|
-
const s = yield* Sandbox
|
|
584
|
-
yield* SandboxVolumes
|
|
583
|
+
const s = yield* Sandbox
|
|
584
|
+
yield* SandboxVolumes
|
|
585
585
|
return yield* s.volumes.destroy(id)
|
|
586
586
|
})
|
|
587
587
|
|
|
@@ -591,8 +591,8 @@ export const listVolumes: Effect.Effect<
|
|
|
591
591
|
SandboxError.SandboxError,
|
|
592
592
|
Sandbox | SandboxVolumes
|
|
593
593
|
> = Effect.gen(function* () {
|
|
594
|
-
const s = yield* Sandbox
|
|
595
|
-
yield* SandboxVolumes
|
|
594
|
+
const s = yield* Sandbox
|
|
595
|
+
yield* SandboxVolumes
|
|
596
596
|
return yield* s.volumes.list
|
|
597
597
|
})
|
|
598
598
|
|
|
@@ -601,8 +601,8 @@ export const destroySnapshot = (
|
|
|
601
601
|
id: SnapshotId,
|
|
602
602
|
): Effect.Effect<void, SandboxError.SandboxError, Sandbox | SandboxSnapshots> =>
|
|
603
603
|
Effect.gen(function* () {
|
|
604
|
-
const s = yield* Sandbox
|
|
605
|
-
yield* SandboxSnapshots
|
|
604
|
+
const s = yield* Sandbox
|
|
605
|
+
yield* SandboxSnapshots
|
|
606
606
|
return yield* s.snapshots.destroy(id)
|
|
607
607
|
})
|
|
608
608
|
|
|
@@ -612,8 +612,8 @@ export const listSnapshots: Effect.Effect<
|
|
|
612
612
|
SandboxError.SandboxError,
|
|
613
613
|
Sandbox | SandboxSnapshots
|
|
614
614
|
> = Effect.gen(function* () {
|
|
615
|
-
const s = yield* Sandbox
|
|
616
|
-
yield* SandboxSnapshots
|
|
615
|
+
const s = yield* Sandbox
|
|
616
|
+
yield* SandboxSnapshots
|
|
617
617
|
return yield* s.snapshots.list
|
|
618
618
|
})
|
|
619
619
|
|
|
@@ -633,7 +633,7 @@ export const exposePort = (
|
|
|
633
633
|
Sandbox | SandboxPortExposure
|
|
634
634
|
> =>
|
|
635
635
|
Effect.gen(function* () {
|
|
636
|
-
const s = yield* Sandbox
|
|
637
|
-
yield* SandboxPortExposure
|
|
636
|
+
const s = yield* Sandbox
|
|
637
|
+
yield* SandboxPortExposure
|
|
638
638
|
return yield* s.ports.expose(instance, port)
|
|
639
639
|
})
|
|
@@ -169,13 +169,13 @@ export class MultiSpeakerTts extends Context.Service<MultiSpeakerTts, void>()(
|
|
|
169
169
|
export const synthesize = (
|
|
170
170
|
request: CommonSynthesizeRequest,
|
|
171
171
|
): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer> =>
|
|
172
|
-
Effect.flatMap(SpeechSynthesizer
|
|
172
|
+
Effect.flatMap(SpeechSynthesizer, (s) => s.synthesize(request))
|
|
173
173
|
|
|
174
174
|
/** Full text in, audio chunks out. */
|
|
175
175
|
export const streamSynthesis = (
|
|
176
176
|
request: CommonSynthesizeRequest,
|
|
177
177
|
): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer> =>
|
|
178
|
-
Stream.unwrap(Effect.map(SpeechSynthesizer
|
|
178
|
+
Stream.unwrap(Effect.map(SpeechSynthesizer, (s) => s.streamSynthesis(request)))
|
|
179
179
|
|
|
180
180
|
/**
|
|
181
181
|
* Incremental synthesis. Dual-arity: pipeable (data-last) and direct
|
|
@@ -205,8 +205,8 @@ export const streamSynthesisFrom: {
|
|
|
205
205
|
<E, R>(textIn: Stream.Stream<string, E, R>, request: CommonStreamSynthesizeRequest) =>
|
|
206
206
|
Stream.unwrap(
|
|
207
207
|
Effect.gen(function* () {
|
|
208
|
-
const s = yield* SpeechSynthesizer
|
|
209
|
-
yield* TtsIncrementalText
|
|
208
|
+
const s = yield* SpeechSynthesizer
|
|
209
|
+
yield* TtsIncrementalText
|
|
210
210
|
return s.streamSynthesisFrom(textIn, request)
|
|
211
211
|
}),
|
|
212
212
|
),
|
|
@@ -220,8 +220,8 @@ export const synthesizeDialogue = (
|
|
|
220
220
|
request: CommonSynthesizeDialogueRequest,
|
|
221
221
|
): Effect.Effect<AudioBlob, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>
|
|
222
222
|
Effect.gen(function* () {
|
|
223
|
-
const s = yield* SpeechSynthesizer
|
|
224
|
-
yield* MultiSpeakerTts
|
|
223
|
+
const s = yield* SpeechSynthesizer
|
|
224
|
+
yield* MultiSpeakerTts
|
|
225
225
|
return yield* s.synthesizeDialogue(request)
|
|
226
226
|
})
|
|
227
227
|
|
|
@@ -233,8 +233,8 @@ export const streamSynthesizeDialogue = (
|
|
|
233
233
|
): Stream.Stream<AudioChunk, AiError.AiError, SpeechSynthesizer | MultiSpeakerTts> =>
|
|
234
234
|
Stream.unwrap(
|
|
235
235
|
Effect.gen(function* () {
|
|
236
|
-
const s = yield* SpeechSynthesizer
|
|
237
|
-
yield* MultiSpeakerTts
|
|
236
|
+
const s = yield* SpeechSynthesizer
|
|
237
|
+
yield* MultiSpeakerTts
|
|
238
238
|
return s.streamSynthesizeDialogue(request)
|
|
239
239
|
}),
|
|
240
240
|
)
|
|
@@ -8,6 +8,18 @@ const bytesOf = (...chunks: ReadonlyArray<string>) =>
|
|
|
8
8
|
|
|
9
9
|
const collect = <A, E>(s: Stream.Stream<A, E>) => Effect.runPromise(Stream.runCollect(s))
|
|
10
10
|
|
|
11
|
+
const runUntilEnd = <A, E>(s: Stream.Stream<A, E>) =>
|
|
12
|
+
Effect.runPromise(
|
|
13
|
+
s.pipe(
|
|
14
|
+
Stream.result,
|
|
15
|
+
Stream.runCollect,
|
|
16
|
+
Effect.map((results) => ({
|
|
17
|
+
seen: results.filter(Result.isSuccess).map((r) => r.success),
|
|
18
|
+
failures: results.filter(Result.isFailure).map((r) => r.failure),
|
|
19
|
+
})),
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
11
23
|
const collectResult = <A, E>(s: Stream.Stream<A, E>) =>
|
|
12
24
|
Effect.runPromise(Effect.result(Stream.runCollect(s)))
|
|
13
25
|
|
|
@@ -33,6 +45,13 @@ describe("JSONL.fromBytes", () => {
|
|
|
33
45
|
const out = await collect(JSONL.fromBytes(bytesOf("a\n\n\nb\n")))
|
|
34
46
|
expect(out).toEqual(["a", "b"])
|
|
35
47
|
})
|
|
48
|
+
|
|
49
|
+
it("discards a buffered partial line when the upstream fails mid-stream", async () => {
|
|
50
|
+
const failing = Stream.concat(bytesOf('{"n":1}\n{"n":2'), Stream.fail("boom" as const))
|
|
51
|
+
const { seen, failures } = await runUntilEnd(JSONL.fromBytes(failing))
|
|
52
|
+
expect(seen).toEqual(['{"n":1}'])
|
|
53
|
+
expect(failures).toEqual(["boom"])
|
|
54
|
+
})
|
|
36
55
|
})
|
|
37
56
|
|
|
38
57
|
describe("JSONL.parse", () => {
|
package/src/streaming/JSONL.ts
CHANGED
|
@@ -1,58 +1,26 @@
|
|
|
1
|
-
import { Data, Effect, Schema, Stream } from "effect"
|
|
1
|
+
import { Channel, Data, Effect, Schema, Stream } from "effect"
|
|
2
2
|
|
|
3
3
|
export class JsonParseError extends Data.TaggedError("JsonParseError")<{
|
|
4
4
|
readonly line: string
|
|
5
5
|
readonly cause: unknown
|
|
6
6
|
}> {}
|
|
7
7
|
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Generic stream helpers (kept module-local; see SSE.ts for the same shape).
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
const decodeText = <E, R>(self: Stream.Stream<Uint8Array, E, R>): Stream.Stream<string, E, R> =>
|
|
13
|
-
self.pipe(
|
|
14
|
-
Stream.mapAccum(
|
|
15
|
-
(): TextDecoder => new TextDecoder("utf-8"),
|
|
16
|
-
(decoder, chunk: Uint8Array) => [decoder, [decoder.decode(chunk, { stream: true })]] as const,
|
|
17
|
-
{
|
|
18
|
-
onHalt: (decoder: TextDecoder) => {
|
|
19
|
-
const tail = decoder.decode()
|
|
20
|
-
return tail.length > 0 ? [tail] : []
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
),
|
|
24
|
-
)
|
|
25
|
-
|
|
26
|
-
const splitOn =
|
|
27
|
-
(separator: string) =>
|
|
28
|
-
<E, R>(self: Stream.Stream<string, E, R>): Stream.Stream<string, E, R> =>
|
|
29
|
-
self.pipe(
|
|
30
|
-
Stream.mapAccum(
|
|
31
|
-
(): string => "",
|
|
32
|
-
(buffer, chunk: string) => {
|
|
33
|
-
const parts = (buffer + chunk).split(separator)
|
|
34
|
-
const tail = parts[parts.length - 1] ?? ""
|
|
35
|
-
return [tail, parts.slice(0, -1)] as const
|
|
36
|
-
},
|
|
37
|
-
{ onHalt: (tail: string) => (tail.length > 0 ? [tail] : []) },
|
|
38
|
-
),
|
|
39
|
-
)
|
|
40
|
-
|
|
41
8
|
// ---------------------------------------------------------------------------
|
|
42
9
|
// Public API
|
|
43
10
|
// ---------------------------------------------------------------------------
|
|
44
11
|
|
|
45
12
|
/**
|
|
46
13
|
* Decode a `Stream<Uint8Array>` into a `Stream<string>` of newline-delimited
|
|
47
|
-
* lines. Empty lines are skipped. Buffers across chunk boundaries
|
|
14
|
+
* lines. Empty lines are skipped. Buffers across chunk boundaries; a final
|
|
15
|
+
* line without a trailing newline is emitted at clean stream end, while a
|
|
16
|
+
* buffered partial line is discarded if the stream fails.
|
|
48
17
|
*/
|
|
49
18
|
export const fromBytes = <E, R>(
|
|
50
19
|
self: Stream.Stream<Uint8Array, E, R>,
|
|
51
20
|
): Stream.Stream<string, E, R> =>
|
|
52
21
|
self.pipe(
|
|
53
|
-
decodeText,
|
|
54
|
-
Stream.
|
|
55
|
-
splitOn("\n"),
|
|
22
|
+
Stream.decodeText,
|
|
23
|
+
Stream.pipeThroughChannel(Channel.splitLines()),
|
|
56
24
|
Stream.filter((line) => line.length > 0),
|
|
57
25
|
)
|
|
58
26
|
|