@effect-uai/core 0.5.1 → 0.6.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/README.md +1 -1
- package/dist/{Items-BH8xUkoR.d.mts → Items-pXR5BAgW.d.mts} +12 -12
- package/dist/Items-pXR5BAgW.d.mts.map +1 -0
- package/dist/SandboxError-BvKypkJm.d.mts +158 -0
- package/dist/SandboxError-BvKypkJm.d.mts.map +1 -0
- package/dist/{Tool-87ViKCCO.d.mts → Tool-DP-AwKOf.d.mts} +11 -11
- package/dist/{Tool-87ViKCCO.d.mts.map → Tool-DP-AwKOf.d.mts.map} +1 -1
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +15 -15
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Turn.d.mts +388 -2
- package/dist/domain/Turn.d.mts.map +1 -0
- package/dist/domain/Turn.mjs +13 -13
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/index.d.mts +12 -6
- package/dist/index.mjs +9 -3
- package/dist/language-model/LanguageModel.d.mts +29 -27
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs +16 -25
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +10 -81
- package/dist/language-model/LanguageModel.test.mjs.map +1 -1
- package/dist/loop/Loop.d.mts +37 -90
- package/dist/loop/Loop.d.mts.map +1 -1
- package/dist/loop/Loop.mjs +39 -93
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +46 -54
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/retry/Retry.d.mts +64 -0
- package/dist/retry/Retry.d.mts.map +1 -0
- package/dist/retry/Retry.mjs +69 -0
- package/dist/retry/Retry.mjs.map +1 -0
- package/dist/retry/Retry.test.mjs +142 -0
- package/dist/retry/Retry.test.mjs.map +1 -0
- package/dist/sandbox/Memory.d.mts +53 -0
- package/dist/sandbox/Memory.d.mts.map +1 -0
- package/dist/sandbox/Memory.mjs +68 -0
- package/dist/sandbox/Memory.mjs.map +1 -0
- package/dist/sandbox/Sandbox.d.mts +717 -0
- package/dist/sandbox/Sandbox.d.mts.map +1 -0
- package/dist/sandbox/Sandbox.mjs +192 -0
- package/dist/sandbox/Sandbox.mjs.map +1 -0
- package/dist/sandbox/SandboxError.d.mts +2 -0
- package/dist/sandbox/SandboxError.mjs +89 -0
- package/dist/sandbox/SandboxError.mjs.map +1 -0
- package/dist/sandbox/SandboxImage.d.mts +28 -0
- package/dist/sandbox/SandboxImage.d.mts.map +1 -0
- package/dist/sandbox/SandboxImage.mjs +32 -0
- package/dist/sandbox/SandboxImage.mjs.map +1 -0
- package/dist/sandbox/SandboxNetwork.d.mts +34 -0
- package/dist/sandbox/SandboxNetwork.d.mts.map +1 -0
- package/dist/sandbox/SandboxNetwork.mjs +36 -0
- package/dist/sandbox/SandboxNetwork.mjs.map +1 -0
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +98 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs +32 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +56 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs.map +1 -1
- package/dist/testing/MockProvider.d.mts +3 -3
- package/dist/testing/MockProvider.d.mts.map +1 -1
- package/dist/testing/MockProvider.mjs +14 -8
- package/dist/testing/MockProvider.mjs.map +1 -1
- package/dist/testing/MockSandbox.d.mts +604 -0
- package/dist/testing/MockSandbox.d.mts.map +1 -0
- package/dist/testing/MockSandbox.mjs +160 -0
- package/dist/testing/MockSandbox.mjs.map +1 -0
- package/dist/testing/MockSpeechSynthesizer.d.mts +26 -11
- package/dist/testing/MockSpeechSynthesizer.d.mts.map +1 -1
- package/dist/testing/MockSpeechSynthesizer.mjs +75 -51
- package/dist/testing/MockSpeechSynthesizer.mjs.map +1 -1
- package/dist/tool/{Resolvers.d.mts → Approval.d.mts} +27 -27
- package/dist/tool/Approval.d.mts.map +1 -0
- package/dist/tool/{Resolvers.mjs → Approval.mjs} +18 -18
- package/dist/tool/Approval.mjs.map +1 -0
- package/dist/tool/Approval.test.d.mts +1 -0
- package/dist/tool/{Resolvers.test.mjs → Approval.test.mjs} +40 -40
- package/dist/tool/Approval.test.mjs.map +1 -0
- package/dist/tool/HistoryCheck.d.mts +6 -6
- package/dist/tool/HistoryCheck.d.mts.map +1 -1
- package/dist/tool/HistoryCheck.mjs +5 -5
- package/dist/tool/HistoryCheck.mjs.map +1 -1
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +4 -4
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +19 -19
- package/dist/tool/ToolEvent.d.mts.map +1 -1
- package/dist/tool/ToolEvent.mjs +8 -8
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/{Outcome.d.mts → ToolResult.d.mts} +29 -29
- package/dist/tool/ToolResult.d.mts.map +1 -0
- package/dist/tool/ToolResult.mjs +61 -0
- package/dist/tool/ToolResult.mjs.map +1 -0
- package/dist/tool/Toolkit.d.mts +58 -26
- package/dist/tool/Toolkit.d.mts.map +1 -1
- package/dist/tool/Toolkit.mjs +62 -34
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +13 -13
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/package.json +35 -7
- package/src/domain/Items.ts +9 -9
- package/src/domain/Turn.ts +16 -16
- package/src/index.ts +8 -2
- package/src/language-model/LanguageModel.test.ts +21 -102
- package/src/language-model/LanguageModel.ts +36 -59
- package/src/loop/Loop.test.ts +93 -91
- package/src/loop/Loop.ts +72 -170
- package/src/retry/Retry.test.ts +163 -0
- package/src/retry/Retry.ts +84 -0
- package/src/sandbox/Memory.ts +128 -0
- package/src/sandbox/Sandbox.ts +639 -0
- package/src/sandbox/SandboxError.ts +140 -0
- package/src/sandbox/SandboxImage.ts +26 -0
- package/src/sandbox/SandboxNetwork.ts +34 -0
- package/src/speech-synthesizer/SpeechSynthesizer.test.ts +89 -0
- package/src/speech-synthesizer/SpeechSynthesizer.ts +124 -0
- package/src/testing/MockProvider.ts +31 -25
- package/src/testing/MockSandbox.ts +342 -0
- package/src/testing/MockSpeechSynthesizer.ts +137 -77
- package/src/tool/{Resolvers.test.ts → Approval.test.ts} +45 -54
- package/src/tool/{Resolvers.ts → Approval.ts} +25 -25
- package/src/tool/HistoryCheck.ts +10 -13
- package/src/tool/Tool.ts +13 -15
- package/src/tool/ToolEvent.ts +7 -7
- package/src/tool/{Outcome.ts → ToolResult.ts} +20 -20
- package/src/tool/Toolkit.test.ts +13 -13
- package/src/tool/Toolkit.ts +97 -73
- package/dist/Items-BH8xUkoR.d.mts.map +0 -1
- package/dist/Turn-0CwCAyVe.d.mts +0 -388
- package/dist/Turn-0CwCAyVe.d.mts.map +0 -1
- package/dist/tool/Outcome.d.mts.map +0 -1
- package/dist/tool/Outcome.mjs +0 -61
- package/dist/tool/Outcome.mjs.map +0 -1
- package/dist/tool/Resolvers.d.mts.map +0 -1
- package/dist/tool/Resolvers.mjs.map +0 -1
- package/dist/tool/Resolvers.test.mjs.map +0 -1
- /package/dist/{tool/Resolvers.test.d.mts → retry/Retry.test.d.mts} +0 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { Array as Arr, Data, Effect, Layer, Ref, Stream } from "effect"
|
|
2
|
+
import {
|
|
3
|
+
ExecEvent,
|
|
4
|
+
Sandbox,
|
|
5
|
+
SandboxCustomImage,
|
|
6
|
+
SandboxHostnameAllowlist,
|
|
7
|
+
SandboxKernelSession,
|
|
8
|
+
SandboxPauseResume,
|
|
9
|
+
SandboxPortExposure,
|
|
10
|
+
SandboxPty,
|
|
11
|
+
SandboxSecretInjection,
|
|
12
|
+
SandboxSnapshots,
|
|
13
|
+
SandboxVolumes,
|
|
14
|
+
type CommonCreateRequest,
|
|
15
|
+
type CommonExecRequest,
|
|
16
|
+
type CommonSpawnRequest,
|
|
17
|
+
type ExecResult,
|
|
18
|
+
type FileEntry,
|
|
19
|
+
type ProcessHandle,
|
|
20
|
+
SandboxId,
|
|
21
|
+
type SandboxInstance,
|
|
22
|
+
type SandboxRef,
|
|
23
|
+
type SandboxService,
|
|
24
|
+
SnapshotId,
|
|
25
|
+
VolumeId,
|
|
26
|
+
} from "../sandbox/Sandbox.js"
|
|
27
|
+
import * as SandboxError from "../sandbox/SandboxError.js"
|
|
28
|
+
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Call log
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* One entry per side-effecting method call. Tests filter via
|
|
35
|
+
* `Call.$is(...)` / `Call.$match`.
|
|
36
|
+
*/
|
|
37
|
+
export type Call = Data.TaggedEnum<{
|
|
38
|
+
Create: { readonly request: CommonCreateRequest }
|
|
39
|
+
Attach: { readonly id: SandboxId }
|
|
40
|
+
Destroy: { readonly id: SandboxId }
|
|
41
|
+
Exec: { readonly id: SandboxId; readonly request: CommonExecRequest }
|
|
42
|
+
ExecStream: { readonly id: SandboxId; readonly request: CommonExecRequest }
|
|
43
|
+
Spawn: { readonly id: SandboxId; readonly request: CommonSpawnRequest }
|
|
44
|
+
FileRead: { readonly id: SandboxId; readonly path: string }
|
|
45
|
+
FileWrite: {
|
|
46
|
+
readonly id: SandboxId
|
|
47
|
+
readonly path: string
|
|
48
|
+
readonly contents: Uint8Array | string
|
|
49
|
+
}
|
|
50
|
+
FileRemove: { readonly id: SandboxId; readonly path: string }
|
|
51
|
+
FileMkdir: { readonly id: SandboxId; readonly path: string }
|
|
52
|
+
FileList: { readonly id: SandboxId; readonly path: string }
|
|
53
|
+
FileExists: { readonly id: SandboxId; readonly path: string }
|
|
54
|
+
ExposePort: { readonly id: SandboxId; readonly port: number }
|
|
55
|
+
SnapshotCreate: { readonly id: SandboxId; readonly name?: string }
|
|
56
|
+
SnapshotDestroy: { readonly id: SnapshotId }
|
|
57
|
+
VolumeCreate: { readonly name: string; readonly quotaBytes?: number }
|
|
58
|
+
VolumeDestroy: { readonly id: VolumeId }
|
|
59
|
+
}>
|
|
60
|
+
|
|
61
|
+
export const Call = Data.taggedEnum<Call>()
|
|
62
|
+
|
|
63
|
+
export type MockSandboxRecorder = {
|
|
64
|
+
readonly calls: ReadonlyArray<Call>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// Script
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* What this mock should hand back per call. Each method has its own
|
|
73
|
+
* scripted queue and a sensible default when the queue is empty.
|
|
74
|
+
* The filesystem is an in-memory mutable map seeded by `files`.
|
|
75
|
+
*/
|
|
76
|
+
export type MockSandboxScript = {
|
|
77
|
+
readonly id?: string
|
|
78
|
+
readonly execs?: ReadonlyArray<ExecResult>
|
|
79
|
+
readonly streams?: ReadonlyArray<ReadonlyArray<ExecEvent>>
|
|
80
|
+
readonly spawns?: ReadonlyArray<ProcessHandle>
|
|
81
|
+
readonly files?: Readonly<Record<string, Uint8Array | string>>
|
|
82
|
+
readonly sandboxes?: ReadonlyArray<SandboxRef>
|
|
83
|
+
readonly snapshots?: ReadonlyArray<{ readonly id: SnapshotId; readonly name?: string }>
|
|
84
|
+
readonly volumes?: ReadonlyArray<{ readonly id: VolumeId; readonly name: string }>
|
|
85
|
+
readonly portUrl?: (port: number) => string
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Helpers
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
const defaultExec: ExecResult = { exitCode: 0, stdout: "", stderr: "", durationMs: 0 }
|
|
93
|
+
|
|
94
|
+
const toBytes = (contents: Uint8Array | string) =>
|
|
95
|
+
typeof contents === "string" ? new TextEncoder().encode(contents) : contents
|
|
96
|
+
|
|
97
|
+
const seedFs = (files: MockSandboxScript["files"]) =>
|
|
98
|
+
new Map(Object.entries(files ?? {}).map(([k, v]) => [k, toBytes(v)]))
|
|
99
|
+
|
|
100
|
+
const exhausted = (method: string, scripted: number, attempt: number) =>
|
|
101
|
+
new SandboxError.SandboxInvalidRequest({
|
|
102
|
+
provider: "mock",
|
|
103
|
+
reason: `MockSandbox exhausted: ${scripted} ${method}(s) scripted, but call ${attempt} was made`,
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const consume = <A>(
|
|
107
|
+
cursor: Ref.Ref<number>,
|
|
108
|
+
scripted: ReadonlyArray<A>,
|
|
109
|
+
onMiss: (attempt: number) => Effect.Effect<A, SandboxError.SandboxError>,
|
|
110
|
+
) =>
|
|
111
|
+
Ref.getAndUpdate(cursor, (n) => n + 1).pipe(
|
|
112
|
+
Effect.flatMap((i) => (i < scripted.length ? Effect.succeed(scripted[i]!) : onMiss(i + 1))),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Service builder
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
|
|
119
|
+
const buildInstance = (
|
|
120
|
+
id: SandboxId,
|
|
121
|
+
script: MockSandboxScript,
|
|
122
|
+
fs: Ref.Ref<Map<string, Uint8Array>>,
|
|
123
|
+
execCursor: Ref.Ref<number>,
|
|
124
|
+
streamCursor: Ref.Ref<number>,
|
|
125
|
+
spawnCursor: Ref.Ref<number>,
|
|
126
|
+
record: (call: Call) => Effect.Effect<void>,
|
|
127
|
+
): SandboxInstance => ({
|
|
128
|
+
id,
|
|
129
|
+
|
|
130
|
+
exec: (request) =>
|
|
131
|
+
record(Call.Exec({ id, request })).pipe(
|
|
132
|
+
Effect.andThen(consume(execCursor, script.execs ?? [], () => Effect.succeed(defaultExec))),
|
|
133
|
+
),
|
|
134
|
+
|
|
135
|
+
execStream: (request) =>
|
|
136
|
+
Stream.unwrap(
|
|
137
|
+
record(Call.ExecStream({ id, request })).pipe(
|
|
138
|
+
Effect.andThen(
|
|
139
|
+
consume(streamCursor, script.streams ?? [], () =>
|
|
140
|
+
Effect.succeed<ReadonlyArray<ExecEvent>>([
|
|
141
|
+
ExecEvent.Complete({ exitCode: 0, durationMs: 0 }),
|
|
142
|
+
]),
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
Effect.map(
|
|
146
|
+
(events) =>
|
|
147
|
+
Stream.fromIterable(events) as Stream.Stream<ExecEvent, SandboxError.SandboxError>,
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
),
|
|
151
|
+
|
|
152
|
+
spawn: (request) =>
|
|
153
|
+
record(Call.Spawn({ id, request })).pipe(
|
|
154
|
+
Effect.andThen(
|
|
155
|
+
consume(spawnCursor, script.spawns ?? [], (n) =>
|
|
156
|
+
Effect.fail(exhausted("spawn", (script.spawns ?? []).length, n)),
|
|
157
|
+
),
|
|
158
|
+
),
|
|
159
|
+
Effect.tap((handle) => Effect.addFinalizer(() => Effect.orDie(handle.kill))),
|
|
160
|
+
),
|
|
161
|
+
|
|
162
|
+
files: {
|
|
163
|
+
read: (path) =>
|
|
164
|
+
record(Call.FileRead({ id, path })).pipe(
|
|
165
|
+
Effect.andThen(Ref.get(fs)),
|
|
166
|
+
Effect.flatMap((map) =>
|
|
167
|
+
map.has(path)
|
|
168
|
+
? Effect.succeed(map.get(path)!)
|
|
169
|
+
: Effect.fail(
|
|
170
|
+
new SandboxError.SandboxExecFailed({
|
|
171
|
+
provider: "mock",
|
|
172
|
+
reason: `ENOENT: ${path}`,
|
|
173
|
+
raw: undefined,
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
),
|
|
177
|
+
),
|
|
178
|
+
|
|
179
|
+
write: (path, contents) =>
|
|
180
|
+
record(Call.FileWrite({ id, path, contents })).pipe(
|
|
181
|
+
Effect.andThen(Ref.update(fs, (m) => new Map(m).set(path, toBytes(contents)))),
|
|
182
|
+
),
|
|
183
|
+
|
|
184
|
+
remove: (path) =>
|
|
185
|
+
record(Call.FileRemove({ id, path })).pipe(
|
|
186
|
+
Effect.andThen(
|
|
187
|
+
Ref.update(fs, (m) => {
|
|
188
|
+
const next = new Map(m)
|
|
189
|
+
next.delete(path)
|
|
190
|
+
return next
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
),
|
|
194
|
+
|
|
195
|
+
mkdir: (path) => record(Call.FileMkdir({ id, path })),
|
|
196
|
+
|
|
197
|
+
list: (path) =>
|
|
198
|
+
record(Call.FileList({ id, path })).pipe(
|
|
199
|
+
Effect.andThen(Ref.get(fs)),
|
|
200
|
+
Effect.map((map) => {
|
|
201
|
+
const prefix = path.endsWith("/") ? path : `${path}/`
|
|
202
|
+
const out: Array<FileEntry> = []
|
|
203
|
+
for (const k of map.keys()) {
|
|
204
|
+
if (k.startsWith(prefix)) out.push({ path: k, kind: "file" })
|
|
205
|
+
}
|
|
206
|
+
return out
|
|
207
|
+
}),
|
|
208
|
+
),
|
|
209
|
+
|
|
210
|
+
exists: (path) =>
|
|
211
|
+
record(Call.FileExists({ id, path })).pipe(
|
|
212
|
+
Effect.andThen(Ref.get(fs)),
|
|
213
|
+
Effect.map((map) => map.has(path)),
|
|
214
|
+
),
|
|
215
|
+
},
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
const buildService = (
|
|
219
|
+
script: MockSandboxScript,
|
|
220
|
+
record: (call: Call) => Effect.Effect<void>,
|
|
221
|
+
): SandboxService => {
|
|
222
|
+
const id = SandboxId(script.id ?? "mock-sandbox")
|
|
223
|
+
|
|
224
|
+
const acquireInstance = (sandboxId: SandboxId, onClose: Effect.Effect<void>) =>
|
|
225
|
+
Effect.gen(function* () {
|
|
226
|
+
const fs = yield* Ref.make(seedFs(script.files))
|
|
227
|
+
const execCursor = yield* Ref.make(0)
|
|
228
|
+
const streamCursor = yield* Ref.make(0)
|
|
229
|
+
const spawnCursor = yield* Ref.make(0)
|
|
230
|
+
yield* Effect.addFinalizer(() => onClose)
|
|
231
|
+
return buildInstance(sandboxId, script, fs, execCursor, streamCursor, spawnCursor, record)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
create: (request) =>
|
|
236
|
+
record(Call.Create({ request })).pipe(
|
|
237
|
+
Effect.andThen(acquireInstance(id, record(Call.Destroy({ id })))),
|
|
238
|
+
),
|
|
239
|
+
|
|
240
|
+
attach: (sandboxId) =>
|
|
241
|
+
record(Call.Attach({ id: sandboxId })).pipe(
|
|
242
|
+
// Attach detaches on scope close (no destroy).
|
|
243
|
+
Effect.andThen(acquireInstance(sandboxId, Effect.void)),
|
|
244
|
+
),
|
|
245
|
+
|
|
246
|
+
list: Effect.succeed(script.sandboxes ?? []),
|
|
247
|
+
|
|
248
|
+
destroy: (sandboxId) => record(Call.Destroy({ id: sandboxId })),
|
|
249
|
+
|
|
250
|
+
snapshots: {
|
|
251
|
+
create: (_from, name) =>
|
|
252
|
+
record(
|
|
253
|
+
name === undefined ? Call.SnapshotCreate({ id }) : Call.SnapshotCreate({ id, name }),
|
|
254
|
+
).pipe(
|
|
255
|
+
Effect.as(SnapshotId(`mock-snapshot-${name ?? Math.random().toString(36).slice(2, 8)}`)),
|
|
256
|
+
),
|
|
257
|
+
destroy: (sid) => record(Call.SnapshotDestroy({ id: sid })),
|
|
258
|
+
list: Effect.succeed(script.snapshots ?? []),
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
volumes: {
|
|
262
|
+
create: (name, options) =>
|
|
263
|
+
record(
|
|
264
|
+
options?.quotaBytes === undefined
|
|
265
|
+
? Call.VolumeCreate({ name })
|
|
266
|
+
: Call.VolumeCreate({ name, quotaBytes: options.quotaBytes }),
|
|
267
|
+
).pipe(Effect.as(VolumeId(`mock-volume-${name}`))),
|
|
268
|
+
destroy: (vid) => record(Call.VolumeDestroy({ id: vid })),
|
|
269
|
+
list: Effect.succeed(script.volumes ?? []),
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
ports: {
|
|
273
|
+
expose: (instance, port) =>
|
|
274
|
+
record(Call.ExposePort({ id: instance.id, port })).pipe(
|
|
275
|
+
Effect.as({ url: (script.portUrl ?? ((p) => `http://mock.local:${p}`))(port) }),
|
|
276
|
+
),
|
|
277
|
+
},
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Recorder
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
const makeRecorderUnsafe = () => {
|
|
286
|
+
const ref = Ref.makeUnsafe<ReadonlyArray<Call>>([])
|
|
287
|
+
return {
|
|
288
|
+
record: (call: Call) => Ref.update(ref, Arr.append(call)),
|
|
289
|
+
recorder: Ref.get(ref).pipe(Effect.map((calls): MockSandboxRecorder => ({ calls }))),
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
// Public API
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
|
|
297
|
+
const allCapabilities = Layer.mergeAll(
|
|
298
|
+
Layer.succeed(SandboxHostnameAllowlist, undefined),
|
|
299
|
+
Layer.succeed(SandboxSecretInjection, undefined),
|
|
300
|
+
Layer.succeed(SandboxSnapshots, undefined),
|
|
301
|
+
Layer.succeed(SandboxPauseResume, undefined),
|
|
302
|
+
Layer.succeed(SandboxVolumes, undefined),
|
|
303
|
+
Layer.succeed(SandboxKernelSession, undefined),
|
|
304
|
+
Layer.succeed(SandboxPty, undefined),
|
|
305
|
+
Layer.succeed(SandboxCustomImage, undefined),
|
|
306
|
+
Layer.succeed(SandboxPortExposure, undefined),
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Layer that registers a `MockSandbox` against the `Sandbox` tag, with
|
|
311
|
+
* every capability marker shipped — use when the code under test
|
|
312
|
+
* exercises any of them.
|
|
313
|
+
*/
|
|
314
|
+
export const layer = (script: MockSandboxScript = {}) => {
|
|
315
|
+
const { record, recorder } = makeRecorderUnsafe()
|
|
316
|
+
return {
|
|
317
|
+
layer: Layer.merge(Layer.succeed(Sandbox, buildService(script, record)), allCapabilities),
|
|
318
|
+
recorder,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Layer with the `Sandbox` service but no capability markers. Use to
|
|
324
|
+
* verify that consumers calling `Sandbox.snapshot` / `createVolume` /
|
|
325
|
+
* etc. fail at `Effect.provide` with a type error.
|
|
326
|
+
*/
|
|
327
|
+
export const layerWithoutCapabilities = (script: MockSandboxScript = {}) => {
|
|
328
|
+
const { record, recorder } = makeRecorderUnsafe()
|
|
329
|
+
return {
|
|
330
|
+
layer: Layer.succeed(Sandbox, buildService(script, record)),
|
|
331
|
+
recorder,
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** Bare service value + recorder. Use with `Effect.provideService`. */
|
|
336
|
+
export const make = (script: MockSandboxScript = {}) => {
|
|
337
|
+
const { record, recorder } = makeRecorderUnsafe()
|
|
338
|
+
return {
|
|
339
|
+
service: buildService(script, record),
|
|
340
|
+
recorder,
|
|
341
|
+
}
|
|
342
|
+
}
|
|
@@ -2,17 +2,21 @@ import { Effect, Layer, Ref, Stream } from "effect"
|
|
|
2
2
|
import type { AudioBlob, AudioChunk } from "../domain/Audio.js"
|
|
3
3
|
import * as AiError from "../domain/AiError.js"
|
|
4
4
|
import {
|
|
5
|
-
SpeechSynthesizer,
|
|
6
|
-
TtsIncrementalText,
|
|
7
5
|
type CommonStreamSynthesizeRequest,
|
|
6
|
+
type CommonSynthesizeDialogueRequest,
|
|
8
7
|
type CommonSynthesizeRequest,
|
|
8
|
+
MultiSpeakerTts,
|
|
9
|
+
SpeechSynthesizer,
|
|
9
10
|
type SpeechSynthesizerService,
|
|
11
|
+
TtsIncrementalText,
|
|
10
12
|
} from "../speech-synthesizer/SpeechSynthesizer.js"
|
|
11
13
|
|
|
12
14
|
export type MockSynthesizerRecorder = {
|
|
13
15
|
readonly synthesizeCalls: ReadonlyArray<CommonSynthesizeRequest>
|
|
14
16
|
readonly streamSynthesisCalls: ReadonlyArray<CommonSynthesizeRequest>
|
|
15
17
|
readonly streamSynthesisFromCalls: ReadonlyArray<CommonStreamSynthesizeRequest>
|
|
18
|
+
readonly synthesizeDialogueCalls: ReadonlyArray<CommonSynthesizeDialogueRequest>
|
|
19
|
+
readonly streamSynthesizeDialogueCalls: ReadonlyArray<CommonSynthesizeDialogueRequest>
|
|
16
20
|
}
|
|
17
21
|
|
|
18
22
|
export type MockSynthesizerScript = {
|
|
@@ -22,32 +26,40 @@ export type MockSynthesizerScript = {
|
|
|
22
26
|
readonly streamSynthesisChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>
|
|
23
27
|
/** One chunk-list per `streamSynthesisFrom` call, consumed in order. */
|
|
24
28
|
readonly streamSynthesisFromChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>
|
|
29
|
+
/** One blob per `synthesizeDialogue` call, consumed in order. */
|
|
30
|
+
readonly dialogueBlobs?: ReadonlyArray<AudioBlob>
|
|
31
|
+
/** One chunk-list per `streamSynthesizeDialogue` call, consumed in order. */
|
|
32
|
+
readonly streamSynthesizeDialogueChunks?: ReadonlyArray<ReadonlyArray<AudioChunk>>
|
|
25
33
|
}
|
|
26
34
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
+
type Record = {
|
|
36
|
+
readonly synthesize: (req: CommonSynthesizeRequest) => Effect.Effect<void>
|
|
37
|
+
readonly streamSynthesis: (req: CommonSynthesizeRequest) => Effect.Effect<void>
|
|
38
|
+
readonly streamSynthesisFrom: (req: CommonStreamSynthesizeRequest) => Effect.Effect<void>
|
|
39
|
+
readonly synthesizeDialogue: (req: CommonSynthesizeDialogueRequest) => Effect.Effect<void>
|
|
40
|
+
readonly streamSynthesizeDialogue: (req: CommonSynthesizeDialogueRequest) => Effect.Effect<void>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const exhausted = (label: string, scripted: number, call: number): AiError.AiError =>
|
|
44
|
+
new AiError.InvalidRequest({
|
|
45
|
+
provider: "mock",
|
|
46
|
+
raw: `MockSpeechSynthesizer exhausted: ${scripted} ${label} scripted, but call ${call} was made`,
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const makeService = (script: MockSynthesizerScript, record: Record) =>
|
|
35
50
|
Effect.gen(function* () {
|
|
36
51
|
const bCursor = yield* Ref.make(0)
|
|
37
52
|
const ssCursor = yield* Ref.make(0)
|
|
38
53
|
const ssfCursor = yield* Ref.make(0)
|
|
54
|
+
const sdCursor = yield* Ref.make(0)
|
|
55
|
+
const ssdCursor = yield* Ref.make(0)
|
|
39
56
|
const service: SpeechSynthesizerService = {
|
|
40
57
|
synthesize: (request) =>
|
|
41
58
|
Effect.gen(function* () {
|
|
42
59
|
yield* record.synthesize(request)
|
|
43
60
|
const i = yield* Ref.getAndUpdate(bCursor, (n) => n + 1)
|
|
44
61
|
const scripted = script.blobs ?? []
|
|
45
|
-
if (i >= scripted.length)
|
|
46
|
-
return yield* new AiError.InvalidRequest({
|
|
47
|
-
provider: "mock",
|
|
48
|
-
raw: `MockSpeechSynthesizer exhausted: ${scripted.length} blobs scripted, but call ${i + 1} was made`,
|
|
49
|
-
})
|
|
50
|
-
}
|
|
62
|
+
if (i >= scripted.length) return yield* exhausted("blobs", scripted.length, i + 1)
|
|
51
63
|
return scripted[i]!
|
|
52
64
|
}),
|
|
53
65
|
streamSynthesis: (request) =>
|
|
@@ -57,12 +69,7 @@ const makeService = (
|
|
|
57
69
|
const i = yield* Ref.getAndUpdate(ssCursor, (n) => n + 1)
|
|
58
70
|
const scripted = script.streamSynthesisChunks ?? []
|
|
59
71
|
if (i >= scripted.length) {
|
|
60
|
-
return Stream.fail(
|
|
61
|
-
new AiError.InvalidRequest({
|
|
62
|
-
provider: "mock",
|
|
63
|
-
raw: `MockSpeechSynthesizer exhausted: ${scripted.length} streamSynthesis lists scripted, but call ${i + 1} was made`,
|
|
64
|
-
}),
|
|
65
|
-
)
|
|
72
|
+
return Stream.fail(exhausted("streamSynthesis lists", scripted.length, i + 1))
|
|
66
73
|
}
|
|
67
74
|
return Stream.fromIterable(scripted[i]!)
|
|
68
75
|
}),
|
|
@@ -77,87 +84,140 @@ const makeService = (
|
|
|
77
84
|
const i = yield* Ref.getAndUpdate(ssfCursor, (n) => n + 1)
|
|
78
85
|
const scripted = script.streamSynthesisFromChunks ?? []
|
|
79
86
|
if (i >= scripted.length) {
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
provider: "mock",
|
|
83
|
-
raw: `MockSpeechSynthesizer exhausted: ${scripted.length} streamSynthesisFrom lists scripted, but call ${i + 1} was made`,
|
|
84
|
-
}),
|
|
87
|
+
const fail: Stream.Stream<AudioChunk, AiError.AiError | E, R> = Stream.fail(
|
|
88
|
+
exhausted("streamSynthesisFrom lists", scripted.length, i + 1),
|
|
85
89
|
)
|
|
86
|
-
return
|
|
90
|
+
return fail
|
|
87
91
|
}
|
|
88
|
-
// Drain the input text fully before emitting scripted audio chunks,
|
|
89
|
-
// so consumers can assert on what text was pushed.
|
|
90
92
|
return Stream.drain(textIn).pipe(Stream.concat(Stream.fromIterable(scripted[i]!)))
|
|
91
93
|
}),
|
|
92
94
|
),
|
|
95
|
+
synthesizeDialogue: (request) =>
|
|
96
|
+
Effect.gen(function* () {
|
|
97
|
+
yield* record.synthesizeDialogue(request)
|
|
98
|
+
const i = yield* Ref.getAndUpdate(sdCursor, (n) => n + 1)
|
|
99
|
+
const scripted = script.dialogueBlobs ?? []
|
|
100
|
+
if (i >= scripted.length) {
|
|
101
|
+
return yield* exhausted("dialogueBlobs", scripted.length, i + 1)
|
|
102
|
+
}
|
|
103
|
+
return scripted[i]!
|
|
104
|
+
}),
|
|
105
|
+
streamSynthesizeDialogue: (request) =>
|
|
106
|
+
Stream.unwrap(
|
|
107
|
+
Effect.gen(function* () {
|
|
108
|
+
yield* record.streamSynthesizeDialogue(request)
|
|
109
|
+
const i = yield* Ref.getAndUpdate(ssdCursor, (n) => n + 1)
|
|
110
|
+
const scripted = script.streamSynthesizeDialogueChunks ?? []
|
|
111
|
+
if (i >= scripted.length) {
|
|
112
|
+
return Stream.fail(
|
|
113
|
+
exhausted("streamSynthesizeDialogue lists", scripted.length, i + 1),
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
return Stream.fromIterable(scripted[i]!)
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
93
119
|
}
|
|
94
120
|
return service
|
|
95
121
|
})
|
|
96
122
|
|
|
123
|
+
type CallBuffers = {
|
|
124
|
+
readonly bCalls: Ref.Ref<ReadonlyArray<CommonSynthesizeRequest>>
|
|
125
|
+
readonly ssCalls: Ref.Ref<ReadonlyArray<CommonSynthesizeRequest>>
|
|
126
|
+
readonly ssfCalls: Ref.Ref<ReadonlyArray<CommonStreamSynthesizeRequest>>
|
|
127
|
+
readonly sdCalls: Ref.Ref<ReadonlyArray<CommonSynthesizeDialogueRequest>>
|
|
128
|
+
readonly ssdCalls: Ref.Ref<ReadonlyArray<CommonSynthesizeDialogueRequest>>
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const newBuffers = (): CallBuffers => ({
|
|
132
|
+
bCalls: Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeRequest>>([]),
|
|
133
|
+
ssCalls: Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeRequest>>([]),
|
|
134
|
+
ssfCalls: Ref.makeUnsafe<ReadonlyArray<CommonStreamSynthesizeRequest>>([]),
|
|
135
|
+
sdCalls: Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeDialogueRequest>>([]),
|
|
136
|
+
ssdCalls: Ref.makeUnsafe<ReadonlyArray<CommonSynthesizeDialogueRequest>>([]),
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
const recordFor = (b: CallBuffers): Record => ({
|
|
140
|
+
synthesize: (req) => Ref.update(b.bCalls, (xs) => [...xs, req]),
|
|
141
|
+
streamSynthesis: (req) => Ref.update(b.ssCalls, (xs) => [...xs, req]),
|
|
142
|
+
streamSynthesisFrom: (req) => Ref.update(b.ssfCalls, (xs) => [...xs, req]),
|
|
143
|
+
synthesizeDialogue: (req) => Ref.update(b.sdCalls, (xs) => [...xs, req]),
|
|
144
|
+
streamSynthesizeDialogue: (req) => Ref.update(b.ssdCalls, (xs) => [...xs, req]),
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const recorderEffect = (b: CallBuffers): Effect.Effect<MockSynthesizerRecorder> =>
|
|
148
|
+
Effect.gen(function* () {
|
|
149
|
+
const synthesizeCalls = yield* Ref.get(b.bCalls)
|
|
150
|
+
const streamSynthesisCalls = yield* Ref.get(b.ssCalls)
|
|
151
|
+
const streamSynthesisFromCalls = yield* Ref.get(b.ssfCalls)
|
|
152
|
+
const synthesizeDialogueCalls = yield* Ref.get(b.sdCalls)
|
|
153
|
+
const streamSynthesizeDialogueCalls = yield* Ref.get(b.ssdCalls)
|
|
154
|
+
return {
|
|
155
|
+
synthesizeCalls,
|
|
156
|
+
streamSynthesisCalls,
|
|
157
|
+
streamSynthesisFromCalls,
|
|
158
|
+
synthesizeDialogueCalls,
|
|
159
|
+
streamSynthesizeDialogueCalls,
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
97
163
|
/**
|
|
98
|
-
* Layer providing
|
|
99
|
-
* `TtsIncrementalText`
|
|
100
|
-
* where code under test exercises
|
|
164
|
+
* Layer providing `SpeechSynthesizer` plus both capability markers
|
|
165
|
+
* (`TtsIncrementalText` and `MultiSpeakerTts`). Use for the common case
|
|
166
|
+
* where code under test exercises any of the optional methods.
|
|
101
167
|
*/
|
|
102
168
|
export const layer = (
|
|
103
169
|
script: MockSynthesizerScript,
|
|
104
170
|
): {
|
|
105
|
-
readonly layer: Layer.Layer<SpeechSynthesizer | TtsIncrementalText>
|
|
171
|
+
readonly layer: Layer.Layer<SpeechSynthesizer | TtsIncrementalText | MultiSpeakerTts>
|
|
106
172
|
readonly recorder: Effect.Effect<MockSynthesizerRecorder>
|
|
107
173
|
} => {
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
synthesize: (req) => Ref.update(bCalls, (xs) => [...xs, req]),
|
|
115
|
-
streamSynthesis: (req) => Ref.update(ssCalls, (xs) => [...xs, req]),
|
|
116
|
-
streamSynthesisFrom: (req) => Ref.update(ssfCalls, (xs) => [...xs, req]),
|
|
117
|
-
}),
|
|
174
|
+
const buffers = newBuffers()
|
|
175
|
+
const synthesizerLayer = Layer.effect(SpeechSynthesizer, makeService(script, recordFor(buffers)))
|
|
176
|
+
const live = Layer.mergeAll(
|
|
177
|
+
synthesizerLayer,
|
|
178
|
+
Layer.succeed(TtsIncrementalText, undefined),
|
|
179
|
+
Layer.succeed(MultiSpeakerTts, undefined),
|
|
118
180
|
)
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
layer: live,
|
|
122
|
-
recorder: Effect.gen(function* () {
|
|
123
|
-
const synthesizeCalls = yield* Ref.get(bCalls)
|
|
124
|
-
const streamSynthesisCalls = yield* Ref.get(ssCalls)
|
|
125
|
-
const streamSynthesisFromCalls = yield* Ref.get(ssfCalls)
|
|
126
|
-
return { synthesizeCalls, streamSynthesisCalls, streamSynthesisFromCalls }
|
|
127
|
-
}),
|
|
128
|
-
}
|
|
181
|
+
return { layer: live, recorder: recorderEffect(buffers) }
|
|
129
182
|
}
|
|
130
183
|
|
|
131
184
|
/**
|
|
132
185
|
* Variant that omits the `TtsIncrementalText` marker — simulates a
|
|
133
|
-
* provider without incremental-text-in support (e.g. OpenAI
|
|
134
|
-
*
|
|
135
|
-
*
|
|
186
|
+
* provider without incremental-text-in support (e.g. OpenAI). Calls to
|
|
187
|
+
* `streamSynthesisFrom` in code under test should be a compile-time
|
|
188
|
+
* error. `MultiSpeakerTts` is still provided.
|
|
136
189
|
*/
|
|
137
190
|
export const layerWithoutIncremental = (
|
|
138
191
|
script: MockSynthesizerScript,
|
|
139
192
|
): {
|
|
140
|
-
readonly layer: Layer.Layer<SpeechSynthesizer>
|
|
193
|
+
readonly layer: Layer.Layer<SpeechSynthesizer | MultiSpeakerTts>
|
|
194
|
+
readonly recorder: Effect.Effect<MockSynthesizerRecorder>
|
|
195
|
+
} => {
|
|
196
|
+
const buffers = newBuffers()
|
|
197
|
+
const live = Layer.merge(
|
|
198
|
+
Layer.effect(SpeechSynthesizer, makeService(script, recordFor(buffers))),
|
|
199
|
+
Layer.succeed(MultiSpeakerTts, undefined),
|
|
200
|
+
)
|
|
201
|
+
return { layer: live, recorder: recorderEffect(buffers) }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Variant that omits the `MultiSpeakerTts` marker — simulates a
|
|
206
|
+
* provider without multi-speaker dialogue support (e.g. OpenAI,
|
|
207
|
+
* Inworld). Calls to `synthesizeDialogue` /
|
|
208
|
+
* `streamSynthesizeDialogue` in code under test should be a
|
|
209
|
+
* compile-time error.
|
|
210
|
+
*/
|
|
211
|
+
export const layerWithoutMultiSpeaker = (
|
|
212
|
+
script: MockSynthesizerScript,
|
|
213
|
+
): {
|
|
214
|
+
readonly layer: Layer.Layer<SpeechSynthesizer | TtsIncrementalText>
|
|
141
215
|
readonly recorder: Effect.Effect<MockSynthesizerRecorder>
|
|
142
216
|
} => {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
SpeechSynthesizer,
|
|
148
|
-
makeService(script, {
|
|
149
|
-
synthesize: (req) => Ref.update(bCalls, (xs) => [...xs, req]),
|
|
150
|
-
streamSynthesis: (req) => Ref.update(ssCalls, (xs) => [...xs, req]),
|
|
151
|
-
streamSynthesisFrom: (req) => Ref.update(ssfCalls, (xs) => [...xs, req]),
|
|
152
|
-
}),
|
|
217
|
+
const buffers = newBuffers()
|
|
218
|
+
const live = Layer.merge(
|
|
219
|
+
Layer.effect(SpeechSynthesizer, makeService(script, recordFor(buffers))),
|
|
220
|
+
Layer.succeed(TtsIncrementalText, undefined),
|
|
153
221
|
)
|
|
154
|
-
return {
|
|
155
|
-
layer: live,
|
|
156
|
-
recorder: Effect.gen(function* () {
|
|
157
|
-
const synthesizeCalls = yield* Ref.get(bCalls)
|
|
158
|
-
const streamSynthesisCalls = yield* Ref.get(ssCalls)
|
|
159
|
-
const streamSynthesisFromCalls = yield* Ref.get(ssfCalls)
|
|
160
|
-
return { synthesizeCalls, streamSynthesisCalls, streamSynthesisFromCalls }
|
|
161
|
-
}),
|
|
162
|
-
}
|
|
222
|
+
return { layer: live, recorder: recorderEffect(buffers) }
|
|
163
223
|
}
|