@effect-uai/core 0.13.0 → 0.15.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/{Audio-60lKKHxS.d.mts → Audio-_nGR4V0X.d.mts} +2 -2
- package/dist/{Audio-60lKKHxS.d.mts.map → Audio-_nGR4V0X.d.mts.map} +1 -1
- package/dist/Image-Cv6iBLTy.d.mts +99 -0
- package/dist/Image-Cv6iBLTy.d.mts.map +1 -0
- package/dist/{Items-SfUabNuB.d.mts → Items-C7MyrYPX.d.mts} +35 -4
- package/dist/Items-C7MyrYPX.d.mts.map +1 -0
- package/dist/Media-BbX7BAaO.d.mts +60 -0
- package/dist/Media-BbX7BAaO.d.mts.map +1 -0
- package/dist/MessengerError-CeK6VAcu.d.mts +64 -0
- package/dist/MessengerError-CeK6VAcu.d.mts.map +1 -0
- package/dist/{Tool-DE-rnvkw.d.mts → Tool-BF78QWtn.d.mts} +2 -2
- package/dist/{Tool-DE-rnvkw.d.mts.map → Tool-BF78QWtn.d.mts.map} +1 -1
- package/dist/{ToolEvent-D99MGQYH.d.mts → ToolEvent-Di4a723B.d.mts} +2 -2
- package/dist/{ToolEvent-D99MGQYH.d.mts.map → ToolEvent-Di4a723B.d.mts.map} +1 -1
- package/dist/{ToolResult-Cj8ChodL.d.mts → ToolResult-BsYQRjmW.d.mts} +2 -2
- package/dist/{ToolResult-Cj8ChodL.d.mts.map → ToolResult-BsYQRjmW.d.mts.map} +1 -1
- package/dist/{Toolkit-BEZrNPTI.d.mts → Toolkit-DAjJ9Tvl.d.mts} +5 -5
- package/dist/{Toolkit-BEZrNPTI.d.mts.map → Toolkit-DAjJ9Tvl.d.mts.map} +1 -1
- package/dist/browser/BrowserTool.d.mts +1 -1
- package/dist/capabilities/Capabilities.d.mts +26 -2
- package/dist/capabilities/Capabilities.d.mts.map +1 -1
- package/dist/capabilities/Capabilities.mjs +32 -1
- package/dist/capabilities/Capabilities.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Image.d.mts +3 -2
- package/dist/domain/Image.mjs +29 -3
- package/dist/domain/Image.mjs.map +1 -1
- package/dist/domain/Items.d.mts +2 -2
- package/dist/domain/Items.mjs +19 -1
- package/dist/domain/Items.mjs.map +1 -1
- package/dist/domain/Media.d.mts +2 -2
- package/dist/domain/Media.mjs +51 -2
- package/dist/domain/Media.mjs.map +1 -1
- package/dist/domain/Music.d.mts +3 -8
- package/dist/domain/Music.d.mts.map +1 -1
- package/dist/domain/Music.mjs.map +1 -1
- package/dist/domain/Research.d.mts +1 -1
- package/dist/domain/Turn.d.mts +73 -9
- package/dist/domain/Turn.d.mts.map +1 -1
- package/dist/domain/Turn.mjs +38 -2
- package/dist/domain/Turn.mjs.map +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/image-generator/ImageGenerator.d.mts +188 -0
- package/dist/image-generator/ImageGenerator.d.mts.map +1 -0
- package/dist/image-generator/ImageGenerator.mjs +50 -0
- package/dist/image-generator/ImageGenerator.mjs.map +1 -0
- package/dist/inbox/Inbox.d.mts +16 -0
- package/dist/inbox/Inbox.d.mts.map +1 -0
- package/dist/inbox/Inbox.mjs +28 -0
- package/dist/inbox/Inbox.mjs.map +1 -0
- package/dist/inbox/Inbox.test.d.mts +1 -0
- package/dist/inbox/Inbox.test.mjs +42 -0
- package/dist/inbox/Inbox.test.mjs.map +1 -0
- package/dist/index.d.mts +15 -9
- package/dist/index.mjs +8 -2
- package/dist/language-model/LanguageModel.d.mts +2 -2
- package/dist/messenger/Messenger.d.mts +472 -0
- package/dist/messenger/Messenger.d.mts.map +1 -0
- package/dist/messenger/Messenger.mjs +66 -0
- package/dist/messenger/Messenger.mjs.map +1 -0
- package/dist/messenger/MessengerAdapter.d.mts +35 -0
- package/dist/messenger/MessengerAdapter.d.mts.map +1 -0
- package/dist/messenger/MessengerAdapter.mjs +102 -0
- package/dist/messenger/MessengerAdapter.mjs.map +1 -0
- package/dist/messenger/MessengerAdapter.test.d.mts +1 -0
- package/dist/messenger/MessengerAdapter.test.mjs +138 -0
- package/dist/messenger/MessengerAdapter.test.mjs.map +1 -0
- package/dist/messenger/MessengerError.d.mts +2 -0
- package/dist/messenger/MessengerError.mjs +39 -0
- package/dist/messenger/MessengerError.mjs.map +1 -0
- package/dist/music-generator/MusicGenerator.d.mts +3 -2
- package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
- package/dist/observability/Metrics.d.mts +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +1 -1
- package/dist/streaming/Multipart.d.mts +21 -0
- package/dist/streaming/Multipart.d.mts.map +1 -0
- package/dist/streaming/Multipart.mjs +26 -0
- package/dist/streaming/Multipart.mjs.map +1 -0
- package/dist/testing/MockMessenger.d.mts +192 -0
- package/dist/testing/MockMessenger.d.mts.map +1 -0
- package/dist/testing/MockMessenger.mjs +76 -0
- package/dist/testing/MockMessenger.mjs.map +1 -0
- package/dist/testing/MockMusicGenerator.d.mts +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
- package/dist/tool/Approval.d.mts +3 -3
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +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/transcriber/Transcriber.d.mts +1 -1
- package/dist/web-read/WebReadTool.d.mts +1 -1
- package/dist/web-search/WebSearchTool.d.mts +1 -1
- package/package.json +29 -1
- package/src/capabilities/Capabilities.ts +38 -0
- package/src/domain/Image.ts +68 -2
- package/src/domain/Items.ts +18 -1
- package/src/domain/Media.ts +66 -0
- package/src/domain/Music.ts +4 -3
- package/src/domain/Turn.ts +56 -0
- package/src/image-generator/ImageGenerator.ts +165 -0
- package/src/inbox/Inbox.test.ts +52 -0
- package/src/inbox/Inbox.ts +33 -0
- package/src/index.ts +6 -0
- package/src/messenger/Messenger.ts +293 -0
- package/src/messenger/MessengerAdapter.test.ts +179 -0
- package/src/messenger/MessengerAdapter.ts +189 -0
- package/src/messenger/MessengerError.ts +67 -0
- package/src/streaming/Multipart.ts +29 -0
- package/src/testing/MockMessenger.ts +147 -0
- package/dist/Image-BK8-4H6s.d.mts +0 -59
- package/dist/Image-BK8-4H6s.d.mts.map +0 -1
- package/dist/Items-SfUabNuB.d.mts.map +0 -1
- package/dist/Media-DwgpifPD.d.mts +0 -57
- package/dist/Media-DwgpifPD.d.mts.map +0 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { describe, it } from "@effect/vitest"
|
|
2
|
+
import { Array as Arr, Duration, Effect, Option, Result, Stream } from "effect"
|
|
3
|
+
import { TestClock } from "effect/testing"
|
|
4
|
+
import { expect } from "vitest"
|
|
5
|
+
import * as MockMessenger from "../testing/MockMessenger.js"
|
|
6
|
+
import {
|
|
7
|
+
ChannelId,
|
|
8
|
+
type ConversationRef,
|
|
9
|
+
MessageId,
|
|
10
|
+
type Outbound,
|
|
11
|
+
type StreamOptions,
|
|
12
|
+
inConversation,
|
|
13
|
+
} from "./Messenger.js"
|
|
14
|
+
import { type StreamViaEditsOptions, splitForLimit } from "./MessengerAdapter.js"
|
|
15
|
+
import { MessengerRateLimited } from "./MessengerError.js"
|
|
16
|
+
|
|
17
|
+
const here: ConversationRef = { channel: ChannelId("c1") }
|
|
18
|
+
|
|
19
|
+
const bodyOf = (msg: Outbound): string => (msg.body._tag === "Text" ? msg.body.text : "")
|
|
20
|
+
|
|
21
|
+
const posts = (calls: ReadonlyArray<MockMessenger.Call>) =>
|
|
22
|
+
Arr.filterMap(calls, (c) =>
|
|
23
|
+
c._tag === "Post" ? Result.succeed(bodyOf(c.message)) : Result.failVoid,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const edits = (calls: ReadonlyArray<MockMessenger.Call>) =>
|
|
27
|
+
Arr.filterMap(calls, (c) =>
|
|
28
|
+
c._tag === "Edit" ? Result.succeed(bodyOf(c.next)) : Result.failVoid,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
/** Run `deltas` through the mock's `stream` and hand back the call log. */
|
|
32
|
+
const streamed = (
|
|
33
|
+
deltas: ReadonlyArray<string>,
|
|
34
|
+
script: MockMessenger.MockMessengerScript = {},
|
|
35
|
+
streamOptions?: StreamOptions,
|
|
36
|
+
) =>
|
|
37
|
+
Effect.gen(function* () {
|
|
38
|
+
const { service, recorder } = MockMessenger.make(script)
|
|
39
|
+
const id = yield* service.stream(Stream.fromIterable(deltas), streamOptions)
|
|
40
|
+
const { calls } = yield* recorder
|
|
41
|
+
return { id, calls }
|
|
42
|
+
}).pipe(inConversation(here))
|
|
43
|
+
|
|
44
|
+
const options = (o: StreamViaEditsOptions): MockMessenger.MockMessengerScript => ({
|
|
45
|
+
streamOptions: o,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// splitForLimit
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
describe("splitForLimit", () => {
|
|
53
|
+
it("prefers a paragraph break, then a line, then a word", () => {
|
|
54
|
+
expect(splitForLimit("aaa\n\nbbb ccc", 8)).toEqual(["aaa", "bbb ccc"])
|
|
55
|
+
expect(splitForLimit("aaa\nbbbb ccc", 8)).toEqual(["aaa", "bbbb ccc"])
|
|
56
|
+
expect(splitForLimit("aaaa bbbb ccc", 9)).toEqual(["aaaa", "bbbb ccc"])
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it("hard-cuts a run with no boundary inside the limit", () => {
|
|
60
|
+
expect(splitForLimit("aaaaaaaaaa", 4)).toEqual(["aaaa", "aaaa", "aa"])
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// streamViaEdits
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
describe("streamViaEdits", () => {
|
|
69
|
+
// TestClock keeps wall time frozen, so the `every` gate never opens and
|
|
70
|
+
// every delta after the first coalesces into the final flush.
|
|
71
|
+
it.effect("coalesces deltas arriving inside the `every` window", () =>
|
|
72
|
+
Effect.gen(function* () {
|
|
73
|
+
const { calls } = yield* streamed(["Hel", "lo ", "the", "re!"])
|
|
74
|
+
|
|
75
|
+
expect(posts(calls)).toEqual(["Hel"])
|
|
76
|
+
expect(edits(calls)).toEqual(["Hello there!"])
|
|
77
|
+
}),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
it.effect("waits for `minChars` of growth before spending an edit", () =>
|
|
81
|
+
Effect.gen(function* () {
|
|
82
|
+
const { calls } = yield* streamed(
|
|
83
|
+
["aaaa", "bbbb", "cccc", "dddd", "eeee", "ffff"],
|
|
84
|
+
options({ every: 0, minChars: 10 }),
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
// Edits land at 16 chars (12 of growth) and again on the final flush.
|
|
88
|
+
expect(posts(calls)).toEqual(["aaaa"])
|
|
89
|
+
expect(edits(calls).map((t) => t.length)).toEqual([16, 24])
|
|
90
|
+
}),
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
it.effect("skips the final flush when the message already shows the text", () =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const { calls } = yield* streamed(
|
|
96
|
+
["aaaaa", "bbbbb", "ccccc"],
|
|
97
|
+
options({ every: 0, minChars: 10 }),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
// The third delta opens the growth gate, so the tail is already sent.
|
|
101
|
+
expect(edits(calls)).toEqual(["aaaaabbbbbccccc"])
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
it.effect("rolls over to a new message past maxText", () =>
|
|
106
|
+
Effect.gen(function* () {
|
|
107
|
+
const { id, calls } = yield* streamed(["aaaa bbbb cccc dddd"], {
|
|
108
|
+
limits: { maxText: 10, maxCaption: 10 },
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
// Cut on the word boundary, not mid-word, and the id is the last message.
|
|
112
|
+
expect(posts(calls)).toEqual(["aaaa bbbb", "cccc dddd"])
|
|
113
|
+
expect(edits(calls)).toEqual([])
|
|
114
|
+
expect(id).toEqual(Option.some(MessageId("m2")))
|
|
115
|
+
}),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
it.effect("quotes the question on the first message only, never on a rollover", () =>
|
|
119
|
+
Effect.gen(function* () {
|
|
120
|
+
const { calls } = yield* streamed(
|
|
121
|
+
["aaaa bbbb cccc dddd"],
|
|
122
|
+
{ limits: { maxText: 10, maxCaption: 10 } },
|
|
123
|
+
{ replyTo: MessageId("q1") },
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const replies = Arr.filterMap(calls, (c) =>
|
|
127
|
+
c._tag === "Post" ? Result.succeed(c.message.replyTo) : Result.failVoid,
|
|
128
|
+
)
|
|
129
|
+
expect(replies).toEqual([MessageId("q1"), undefined])
|
|
130
|
+
}),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
it.effect("posts nothing for a stream with no text", () =>
|
|
134
|
+
Effect.gen(function* () {
|
|
135
|
+
const { id, calls } = yield* streamed(["", ""])
|
|
136
|
+
|
|
137
|
+
expect(calls).toEqual([])
|
|
138
|
+
expect(id).toEqual(Option.none())
|
|
139
|
+
}),
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
// A model opening a tool-calling turn with a newline, which every platform
|
|
143
|
+
// rejects as an empty message.
|
|
144
|
+
it.effect("treats whitespace-only deltas as no text at all", () =>
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
const { id, calls } = yield* streamed(["\n", " ", "\n\n"])
|
|
147
|
+
|
|
148
|
+
expect(calls).toEqual([])
|
|
149
|
+
expect(id).toEqual(Option.none())
|
|
150
|
+
}),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
it.live("waits out a rate limit and re-sends the same edit", () =>
|
|
154
|
+
Effect.gen(function* () {
|
|
155
|
+
const limited = new MessengerRateLimited({
|
|
156
|
+
provider: "mock",
|
|
157
|
+
retryAfter: Duration.millis(10),
|
|
158
|
+
})
|
|
159
|
+
const { calls } = yield* streamed(["abc", "defghij"], {
|
|
160
|
+
...options({ every: 0, minChars: 1 }),
|
|
161
|
+
failures: [undefined, limited],
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
expect(posts(calls)).toEqual(["abc"])
|
|
165
|
+
expect(edits(calls)).toEqual(["abcdefghij", "abcdefghij"])
|
|
166
|
+
}),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
it.effect("gives up once the rate-limit budget is spent", () =>
|
|
170
|
+
Effect.gen(function* () {
|
|
171
|
+
const limited = new MessengerRateLimited({ provider: "mock", retryAfter: Duration.zero })
|
|
172
|
+
const failed = yield* Effect.flip(
|
|
173
|
+
streamed(["abc"], { ...options({ rateLimitRetries: 1 }), failures: [limited, limited] }),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
expect(failed._tag).toBe("MessengerRateLimited")
|
|
177
|
+
}).pipe(TestClock.withLive),
|
|
178
|
+
)
|
|
179
|
+
})
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What an adapter author needs and a recipe author never sees: text
|
|
3
|
+
* splitting at a platform ceiling, and post-then-edit progressive delivery
|
|
4
|
+
* for platforms without a native streaming API.
|
|
5
|
+
*/
|
|
6
|
+
import { Array as Arr, Clock, Data, Duration, Effect, Option, Schedule, Stream } from "effect"
|
|
7
|
+
import {
|
|
8
|
+
CurrentConversation,
|
|
9
|
+
type MessageId,
|
|
10
|
+
type MessengerService,
|
|
11
|
+
type StreamOptions,
|
|
12
|
+
text,
|
|
13
|
+
} from "./Messenger.js"
|
|
14
|
+
import type * as MessengerError from "./MessengerError.js"
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Text splitting
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
// Paragraph break first, then line break, then space.
|
|
21
|
+
const boundaries = ["\n\n", "\n", " "] as const
|
|
22
|
+
|
|
23
|
+
/** Best split at or before `limit`; a hard cut only for an unbroken run. */
|
|
24
|
+
const cutAt = (body: string, limit: number): readonly [head: string, rest: string] =>
|
|
25
|
+
body.length <= limit
|
|
26
|
+
? [body, ""]
|
|
27
|
+
: Arr.findFirst(boundaries, (sep) =>
|
|
28
|
+
Option.map(
|
|
29
|
+
Option.liftPredicate(body.lastIndexOf(sep, limit - sep.length), (at) => at > 0),
|
|
30
|
+
(at) => [body.slice(0, at), body.slice(at + sep.length)] as const,
|
|
31
|
+
),
|
|
32
|
+
).pipe(Option.getOrElse(() => [body.slice(0, limit), body.slice(limit)] as const))
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Chunk `body` into pieces no longer than `limit`, breaking on paragraph,
|
|
36
|
+
* line, then word boundaries. Always yields at least one chunk, so callers
|
|
37
|
+
* have something to send even for empty input.
|
|
38
|
+
*/
|
|
39
|
+
export const splitForLimit = (body: string, limit: number): Arr.NonEmptyReadonlyArray<string> => {
|
|
40
|
+
const [head, rest] = cutAt(body, limit)
|
|
41
|
+
return rest.length === 0 ? [head] : [head, ...splitForLimit(rest, limit)]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// streamViaEdits
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
/** The slice of a messenger that {@link streamViaEdits} drives. */
|
|
49
|
+
export type EditableVerbs = Pick<MessengerService, "post" | "edit" | "limits">
|
|
50
|
+
|
|
51
|
+
export type StreamViaEditsOptions = {
|
|
52
|
+
/** Minimum wall time between edits. Default 1 second. */
|
|
53
|
+
readonly every?: Duration.Input
|
|
54
|
+
/** Minimum growth since the last edit before spending another. Default 40. */
|
|
55
|
+
readonly minChars?: number
|
|
56
|
+
/** How often to honour a `MessengerRateLimited` before giving up. Default 3. */
|
|
57
|
+
readonly rateLimitRetries?: number
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const isRateLimited = (e: MessengerError.MessengerError): boolean =>
|
|
61
|
+
e._tag === "MessengerRateLimited"
|
|
62
|
+
|
|
63
|
+
// A retry schedule's input is the error, so the delay is the platform's own
|
|
64
|
+
// `retry_after` rather than a guess. Bounded by `recurs`, so an unlucky call
|
|
65
|
+
// eventually surfaces the rate limit instead of stalling behind it.
|
|
66
|
+
const honourRetryAfter = (
|
|
67
|
+
times: number,
|
|
68
|
+
): Schedule.Schedule<number, MessengerError.MessengerError> =>
|
|
69
|
+
Schedule.recurs(times).pipe(
|
|
70
|
+
Schedule.setInputType<MessengerError.MessengerError>(),
|
|
71
|
+
Schedule.modifyDelay(({ input }) =>
|
|
72
|
+
Effect.succeed(input._tag === "MessengerRateLimited" ? input.retryAfter : Duration.zero),
|
|
73
|
+
),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The message being filled: not on the platform yet, or posted and edited
|
|
78
|
+
* since. Only the first draft carries the reply; rollover drafts do not.
|
|
79
|
+
*/
|
|
80
|
+
type Progress = Data.TaggedEnum<{
|
|
81
|
+
Draft: { readonly pending: string; readonly envelope: StreamOptions }
|
|
82
|
+
Sent: {
|
|
83
|
+
readonly id: MessageId
|
|
84
|
+
/** Text the message shows now. Never resent unchanged. */
|
|
85
|
+
readonly sent: string
|
|
86
|
+
/** Text it should show, deltas included. */
|
|
87
|
+
readonly pending: string
|
|
88
|
+
readonly lastFlush: number
|
|
89
|
+
}
|
|
90
|
+
}>
|
|
91
|
+
|
|
92
|
+
const Progress = Data.taggedEnum<Progress>()
|
|
93
|
+
|
|
94
|
+
type Sent = Data.TaggedEnum.Value<Progress, "Sent">
|
|
95
|
+
|
|
96
|
+
const appended = (s: Progress, delta: string): Progress => ({
|
|
97
|
+
...s,
|
|
98
|
+
pending: s.pending + delta,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Progressive delivery as post-then-edit, for adapters with no native
|
|
103
|
+
* streaming API. Coalesces deltas by time and growth, never resends
|
|
104
|
+
* unchanged text, waits out `MessengerRateLimited`, rolls over to a fresh
|
|
105
|
+
* message past `limits.maxText`, and always flushes the tail.
|
|
106
|
+
*
|
|
107
|
+
* The first chunk goes out as soon as any text arrives, so the message shows
|
|
108
|
+
* up immediately and fills in from there. A stream that produced no text
|
|
109
|
+
* posts nothing and yields `None`.
|
|
110
|
+
*/
|
|
111
|
+
export const streamViaEdits =
|
|
112
|
+
(verbs: EditableVerbs, options?: StreamViaEditsOptions): MessengerService["stream"] =>
|
|
113
|
+
(deltas, streamOptions = {}) =>
|
|
114
|
+
Effect.gen(function* () {
|
|
115
|
+
const every = Duration.toMillis(options?.every ?? "1 second")
|
|
116
|
+
const minChars = options?.minChars ?? 40
|
|
117
|
+
const schedule = honourRetryAfter(options?.rateLimitRetries ?? 3)
|
|
118
|
+
const conversation = yield* CurrentConversation
|
|
119
|
+
|
|
120
|
+
const patiently = <A, R2>(
|
|
121
|
+
effect: Effect.Effect<A, MessengerError.MessengerError, R2>,
|
|
122
|
+
): Effect.Effect<A, MessengerError.MessengerError, R2> =>
|
|
123
|
+
Effect.retry(effect, { schedule, while: isRateLimited })
|
|
124
|
+
|
|
125
|
+
// Post a draft, edit a sent message, and skip an edit that would resend
|
|
126
|
+
// what the message already shows.
|
|
127
|
+
const deliver = (
|
|
128
|
+
s: Progress,
|
|
129
|
+
body: string,
|
|
130
|
+
now: number,
|
|
131
|
+
): Effect.Effect<Sent, MessengerError.MessengerError, CurrentConversation> =>
|
|
132
|
+
Progress.$match(s, {
|
|
133
|
+
Draft: ({ pending, envelope }) =>
|
|
134
|
+
patiently(verbs.post(text(body, envelope))).pipe(
|
|
135
|
+
Effect.map((id) => Progress.Sent({ id, sent: body, pending, lastFlush: now })),
|
|
136
|
+
),
|
|
137
|
+
Sent: (sent) =>
|
|
138
|
+
body === sent.sent
|
|
139
|
+
? Effect.succeed(sent)
|
|
140
|
+
: patiently(verbs.edit({ conversation, id: sent.id }, text(body))).pipe(
|
|
141
|
+
Effect.as(Progress.Sent({ ...sent, sent: body, lastFlush: now })),
|
|
142
|
+
),
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
// Anything past the ceiling becomes the next message: finish the current
|
|
146
|
+
// one at a clean boundary, then start a draft on the remainder.
|
|
147
|
+
const rollover = (
|
|
148
|
+
s: Progress,
|
|
149
|
+
now: number,
|
|
150
|
+
): Effect.Effect<Progress, MessengerError.MessengerError, CurrentConversation> => {
|
|
151
|
+
const [head, rest] = cutAt(s.pending, verbs.limits.maxText)
|
|
152
|
+
return rest.length === 0
|
|
153
|
+
? Effect.succeed(s)
|
|
154
|
+
: deliver(s, head, now).pipe(
|
|
155
|
+
Effect.flatMap(() => rollover(Progress.Draft({ pending: rest, envelope: {} }), now)),
|
|
156
|
+
)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// A draft lands on its first real text; a sent message waits for both
|
|
160
|
+
// gates. Blank is not text: a model that opens a tool-calling turn with
|
|
161
|
+
// a newline would otherwise post it, and platforms reject a message
|
|
162
|
+
// that is only whitespace.
|
|
163
|
+
const due = (s: Progress, now: number): boolean =>
|
|
164
|
+
Progress.$match(s, {
|
|
165
|
+
Draft: ({ pending }) => pending.trim().length > 0,
|
|
166
|
+
Sent: ({ sent, pending, lastFlush }) =>
|
|
167
|
+
now - lastFlush >= every && pending.length - sent.length >= minChars,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
const step = (before: Progress, delta: string) =>
|
|
171
|
+
Effect.gen(function* () {
|
|
172
|
+
const now = yield* Clock.currentTimeMillis
|
|
173
|
+
const s = yield* rollover(appended(before, delta), now)
|
|
174
|
+
if (!due(s, now)) return s
|
|
175
|
+
return yield* deliver(s, s.pending, now)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const folded = yield* Stream.runFoldEffect(
|
|
179
|
+
deltas,
|
|
180
|
+
() => Progress.Draft({ pending: "", envelope: streamOptions }),
|
|
181
|
+
step,
|
|
182
|
+
)
|
|
183
|
+
const now = yield* Clock.currentTimeMillis
|
|
184
|
+
const s = yield* rollover(folded, now)
|
|
185
|
+
// The tail always lands; a draft that never held text is nothing to send.
|
|
186
|
+
if (s._tag === "Draft" && s.pending.trim().length === 0) return Option.none()
|
|
187
|
+
const final = yield* deliver(s, s.pending, now)
|
|
188
|
+
return Option.some(final.id)
|
|
189
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Data, type Duration, Match } from "effect"
|
|
2
|
+
|
|
3
|
+
// `reason` is required wherever one exists: the platform always says
|
|
4
|
+
// something, and an adapter raising `Unsupported` knows why it did.
|
|
5
|
+
|
|
6
|
+
/** Transport never came up: bad token, rejected handshake. Raised at layer build. */
|
|
7
|
+
export class MessengerConnectFailed extends Data.TaggedError("MessengerConnectFailed")<{
|
|
8
|
+
provider: string
|
|
9
|
+
reason: string
|
|
10
|
+
raw: unknown
|
|
11
|
+
}> {}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Inbound transport ended for good (reconnect budget spent, fatal close).
|
|
15
|
+
* Routine reconnects stay inside the adapter and never surface here.
|
|
16
|
+
*/
|
|
17
|
+
export class MessengerTransportClosed extends Data.TaggedError("MessengerTransportClosed")<{
|
|
18
|
+
provider: string
|
|
19
|
+
reason: string
|
|
20
|
+
raw?: unknown
|
|
21
|
+
}> {}
|
|
22
|
+
|
|
23
|
+
/** Which verb was rejected. */
|
|
24
|
+
export type MessengerOperation = "post" | "edit" | "react" | "typing"
|
|
25
|
+
|
|
26
|
+
/** An outbound verb was rejected: permission, unknown chat, text too long, no-op edit. */
|
|
27
|
+
export class MessengerRequestFailed extends Data.TaggedError("MessengerRequestFailed")<{
|
|
28
|
+
provider: string
|
|
29
|
+
operation: MessengerOperation
|
|
30
|
+
reason: string
|
|
31
|
+
raw: unknown
|
|
32
|
+
}> {}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Slow down, for this long. Typed rather than retried blindly so callers -
|
|
36
|
+
* `streamViaEdits` above all - decide whether waiting is still worth it.
|
|
37
|
+
*/
|
|
38
|
+
export class MessengerRateLimited extends Data.TaggedError("MessengerRateLimited")<{
|
|
39
|
+
provider: string
|
|
40
|
+
retryAfter: Duration.Duration
|
|
41
|
+
raw?: unknown
|
|
42
|
+
}> {}
|
|
43
|
+
|
|
44
|
+
/** The platform cannot express this at all: off-set reaction emoji, edits on WhatsApp. */
|
|
45
|
+
export class MessengerUnsupported extends Data.TaggedError("MessengerUnsupported")<{
|
|
46
|
+
provider: string
|
|
47
|
+
capability: string
|
|
48
|
+
reason: string
|
|
49
|
+
}> {}
|
|
50
|
+
|
|
51
|
+
export type MessengerError =
|
|
52
|
+
| MessengerConnectFailed
|
|
53
|
+
| MessengerTransportClosed
|
|
54
|
+
| MessengerRequestFailed
|
|
55
|
+
| MessengerRateLimited
|
|
56
|
+
| MessengerUnsupported
|
|
57
|
+
|
|
58
|
+
/** Human-readable description for logs. Prose, not a contract; branch on `_tag`. */
|
|
59
|
+
export const describe: (e: MessengerError) => string = Match.type<MessengerError>().pipe(
|
|
60
|
+
Match.discriminatorsExhaustive("_tag")({
|
|
61
|
+
MessengerConnectFailed: (e) => `the messenger could not connect: ${e.reason}`,
|
|
62
|
+
MessengerTransportClosed: (e) => `the messenger connection closed: ${e.reason}`,
|
|
63
|
+
MessengerRequestFailed: (e) => `the ${e.operation} was rejected: ${e.reason}`,
|
|
64
|
+
MessengerRateLimited: () => "the messenger is rate limited",
|
|
65
|
+
MessengerUnsupported: (e) => `this messenger does not support ${e.capability}: ${e.reason}`,
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import { HttpClientRequest } from "effect/unstable/http"
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multipart body for an `HttpClientRequest`, encoded to bytes.
|
|
6
|
+
*
|
|
7
|
+
* Use this over `HttpClientRequest.bodyFormData`: the Undici client
|
|
8
|
+
* passes the `FormData` object straight to `dispatcher.request`, which
|
|
9
|
+
* cannot serialise it, so the request is never sent and the effect
|
|
10
|
+
* hangs with no error. Encoding here produces the same bytes every
|
|
11
|
+
* other client derives internally, so it works on all of them.
|
|
12
|
+
*
|
|
13
|
+
* The failure is whatever `Response.arrayBuffer` rejects with; map it
|
|
14
|
+
* to your provider's transport error at the call site.
|
|
15
|
+
*/
|
|
16
|
+
export const bodyMultipart = (
|
|
17
|
+
form: FormData,
|
|
18
|
+
): Effect.Effect<
|
|
19
|
+
(request: HttpClientRequest.HttpClientRequest) => HttpClientRequest.HttpClientRequest,
|
|
20
|
+
unknown
|
|
21
|
+
> =>
|
|
22
|
+
Effect.gen(function* () {
|
|
23
|
+
const encoded = new Response(form)
|
|
24
|
+
const buffer = yield* Effect.tryPromise(() => encoded.arrayBuffer())
|
|
25
|
+
return HttpClientRequest.bodyUint8Array(
|
|
26
|
+
new Uint8Array(buffer),
|
|
27
|
+
encoded.headers.get("content-type") ?? "multipart/form-data",
|
|
28
|
+
)
|
|
29
|
+
})
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Array as Arr, Data, Effect, Layer, Option, Ref, Stream } from "effect"
|
|
2
|
+
import {
|
|
3
|
+
type ConversationRef,
|
|
4
|
+
CurrentConversation,
|
|
5
|
+
type InboundEvent,
|
|
6
|
+
MessageId,
|
|
7
|
+
type MessageRef,
|
|
8
|
+
Messenger,
|
|
9
|
+
type MessengerLimits,
|
|
10
|
+
type MessengerService,
|
|
11
|
+
type Outbound,
|
|
12
|
+
} from "../messenger/Messenger.js"
|
|
13
|
+
import {
|
|
14
|
+
type StreamViaEditsOptions,
|
|
15
|
+
splitForLimit,
|
|
16
|
+
streamViaEdits,
|
|
17
|
+
} from "../messenger/MessengerAdapter.js"
|
|
18
|
+
import * as MessengerError from "../messenger/MessengerError.js"
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Call log
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* One entry per outbound call. `Post` is logged once even when the text was
|
|
26
|
+
* long enough to split, so a caller that rolled over on its own is
|
|
27
|
+
* distinguishable from one that leaned on `post` to do it.
|
|
28
|
+
*/
|
|
29
|
+
export type Call = Data.TaggedEnum<{
|
|
30
|
+
Post: { readonly conversation: ConversationRef; readonly message: Outbound }
|
|
31
|
+
Edit: { readonly message: MessageRef; readonly next: Outbound }
|
|
32
|
+
React: { readonly message: MessageRef; readonly emoji: string }
|
|
33
|
+
TypingStart: { readonly conversation: ConversationRef }
|
|
34
|
+
TypingStop: { readonly conversation: ConversationRef }
|
|
35
|
+
}>
|
|
36
|
+
|
|
37
|
+
export const Call = Data.taggedEnum<Call>()
|
|
38
|
+
|
|
39
|
+
export type MockMessengerRecorder = {
|
|
40
|
+
readonly calls: ReadonlyArray<Call>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Script
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
export type MockMessengerScript = {
|
|
48
|
+
/** Inbound events to replay. Empty by default. */
|
|
49
|
+
readonly events?: Stream.Stream<InboundEvent, MessengerError.MessengerError>
|
|
50
|
+
readonly limits?: MessengerLimits
|
|
51
|
+
/**
|
|
52
|
+
* Failures handed back one per `post` / `edit` call, in order; `undefined`
|
|
53
|
+
* entries succeed, and everything past the end succeeds.
|
|
54
|
+
*/
|
|
55
|
+
readonly failures?: ReadonlyArray<MessengerError.MessengerError | undefined>
|
|
56
|
+
/** Passed through to `streamViaEdits`, which backs `stream`. */
|
|
57
|
+
readonly streamOptions?: StreamViaEditsOptions
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const defaultLimits: MessengerLimits = { maxText: 4096, maxCaption: 1024 }
|
|
61
|
+
|
|
62
|
+
const bodyText = (msg: Outbound): Option.Option<string> =>
|
|
63
|
+
msg.body._tag === "Text" ? Option.some(msg.body.text) : Option.none()
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Service builder
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
const buildService = (
|
|
70
|
+
script: MockMessengerScript,
|
|
71
|
+
record: (call: Call) => Effect.Effect<void>,
|
|
72
|
+
cursor: Ref.Ref<number>,
|
|
73
|
+
minted: Ref.Ref<number>,
|
|
74
|
+
): MessengerService => {
|
|
75
|
+
const limits = script.limits ?? defaultLimits
|
|
76
|
+
|
|
77
|
+
const nextFailure = Ref.getAndUpdate(cursor, (n) => n + 1).pipe(
|
|
78
|
+
Effect.flatMap((i) =>
|
|
79
|
+
Option.match(Option.fromNullishOr((script.failures ?? [])[i]), {
|
|
80
|
+
onNone: () => Effect.void,
|
|
81
|
+
onSome: Effect.fail<MessengerError.MessengerError>,
|
|
82
|
+
}),
|
|
83
|
+
),
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const mint = Ref.updateAndGet(minted, (n) => n + 1).pipe(Effect.map((n) => MessageId(`m${n}`)))
|
|
87
|
+
|
|
88
|
+
const post: MessengerService["post"] = (msg) =>
|
|
89
|
+
Effect.gen(function* () {
|
|
90
|
+
const conversation = yield* CurrentConversation
|
|
91
|
+
yield* record(Call.Post({ conversation, message: msg }))
|
|
92
|
+
yield* nextFailure
|
|
93
|
+
// One id per chunk, mirroring an adapter that splits at the ceiling.
|
|
94
|
+
const chunks = Option.match(bodyText(msg), {
|
|
95
|
+
onNone: () => 1,
|
|
96
|
+
onSome: (t) => splitForLimit(t, limits.maxText).length,
|
|
97
|
+
})
|
|
98
|
+
return yield* Effect.map(
|
|
99
|
+
Effect.forEach(Arr.range(1, chunks), () => mint),
|
|
100
|
+
Arr.lastNonEmpty,
|
|
101
|
+
)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
const edit: MessengerService["edit"] = (msg, next) =>
|
|
105
|
+
record(Call.Edit({ message: msg, next })).pipe(Effect.andThen(nextFailure))
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
events: script.events ?? Stream.empty,
|
|
109
|
+
post,
|
|
110
|
+
edit,
|
|
111
|
+
react: (msg, emoji) => record(Call.React({ message: msg, emoji })),
|
|
112
|
+
typing: Effect.gen(function* () {
|
|
113
|
+
const conversation = yield* CurrentConversation
|
|
114
|
+
yield* record(Call.TypingStart({ conversation }))
|
|
115
|
+
yield* Effect.addFinalizer(() => record(Call.TypingStop({ conversation })))
|
|
116
|
+
}),
|
|
117
|
+
stream: streamViaEdits({ post, edit, limits }, script.streamOptions),
|
|
118
|
+
limits,
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Public API
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
const makeUnsafe = (script: MockMessengerScript) => {
|
|
127
|
+
const calls = Ref.makeUnsafe<ReadonlyArray<Call>>([])
|
|
128
|
+
const service = buildService(
|
|
129
|
+
script,
|
|
130
|
+
(call) => Ref.update(calls, Arr.append(call)),
|
|
131
|
+
Ref.makeUnsafe(0),
|
|
132
|
+
Ref.makeUnsafe(0),
|
|
133
|
+
)
|
|
134
|
+
return {
|
|
135
|
+
service,
|
|
136
|
+
recorder: Ref.get(calls).pipe(Effect.map((c): MockMessengerRecorder => ({ calls: c }))),
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Layer registering the mock against the `Messenger` tag, plus its recorder. */
|
|
141
|
+
export const layer = (script: MockMessengerScript = {}) => {
|
|
142
|
+
const { service, recorder } = makeUnsafe(script)
|
|
143
|
+
return { layer: Layer.succeed(Messenger, service), recorder }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Bare service value + recorder. Use with `Effect.provideService`. */
|
|
147
|
+
export const make = (script: MockMessengerScript = {}) => makeUnsafe(script)
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import { i as MediaUrl, n as MediaBytes, r as MediaSource, t as MediaBase64 } from "./Media-DwgpifPD.mjs";
|
|
2
|
-
import { Schema } from "effect";
|
|
3
|
-
declare namespace Image_d_exports {
|
|
4
|
-
export { ImageBase64Source, ImageBytesSource, ImageMimeType, ImageSource, ImageUrlSource, imageBase64, imageBytes, imageUrl, isImageBase64, isImageBytes, isImageUrl };
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Image MIME types AI providers typically accept. The first four are the
|
|
8
|
-
* universal subset (Cohere v4, Voyage multimodal, Jina v4, Google
|
|
9
|
-
* `gemini-embedding-2`); HEIC / HEIF are Google-specific. The
|
|
10
|
-
* `(string & {})` tail keeps autocomplete on the literals while still
|
|
11
|
-
* accepting any string, so a newly-supported format works without an
|
|
12
|
-
* SDK update.
|
|
13
|
-
*/
|
|
14
|
-
type ImageMimeType = "image/png" | "image/jpeg" | "image/webp" | "image/gif" | "image/heic" | "image/heif" | (string & {});
|
|
15
|
-
type ImageUrlSource = MediaUrl<ImageMimeType>;
|
|
16
|
-
type ImageBase64Source = MediaBase64<ImageMimeType>;
|
|
17
|
-
type ImageBytesSource = MediaBytes<ImageMimeType>;
|
|
18
|
-
/**
|
|
19
|
-
* Where an image lives. Provider layers normalize across these:
|
|
20
|
-
* `bytes` becomes a base64 data URI for OpenAI / Anthropic, an
|
|
21
|
-
* `inlineData` part for Gemini, and a separate field for Cohere /
|
|
22
|
-
* Voyage. URL constraints (must be HTTPS, must be public, …) are
|
|
23
|
-
* provider-specific and validated at the layer, not in the type.
|
|
24
|
-
*/
|
|
25
|
-
type ImageSource = MediaSource<ImageMimeType>;
|
|
26
|
-
declare const ImageUrlSource: Schema.TaggedStruct<"url", {
|
|
27
|
-
readonly url: Schema.String;
|
|
28
|
-
readonly mimeType: Schema.optional<Schema.Schema<ImageMimeType>>;
|
|
29
|
-
}>;
|
|
30
|
-
declare const ImageBase64Source: Schema.TaggedStruct<"base64", {
|
|
31
|
-
readonly base64: Schema.String;
|
|
32
|
-
readonly mimeType: Schema.Schema<ImageMimeType>;
|
|
33
|
-
}>;
|
|
34
|
-
declare const ImageBytesSource: Schema.TaggedStruct<"bytes", {
|
|
35
|
-
readonly bytes: Schema.Uint8Array;
|
|
36
|
-
readonly mimeType: Schema.Schema<ImageMimeType>;
|
|
37
|
-
}>;
|
|
38
|
-
declare const ImageSource: Schema.Schema<ImageSource>;
|
|
39
|
-
declare const imageUrl: (url: string, mimeType?: ImageMimeType) => ImageUrlSource;
|
|
40
|
-
declare const imageBase64: (base64: string, mimeType: ImageMimeType) => ImageBase64Source;
|
|
41
|
-
declare const imageBytes: (bytes: Uint8Array, mimeType: ImageMimeType) => ImageBytesSource;
|
|
42
|
-
declare const isImageUrl: <I>(input: I) => input is I & {
|
|
43
|
-
readonly _tag: "url";
|
|
44
|
-
readonly url: string;
|
|
45
|
-
readonly mimeType?: ImageMimeType | undefined;
|
|
46
|
-
};
|
|
47
|
-
declare const isImageBase64: <I>(input: I) => input is I & {
|
|
48
|
-
readonly _tag: "base64";
|
|
49
|
-
readonly base64: string;
|
|
50
|
-
readonly mimeType: ImageMimeType;
|
|
51
|
-
};
|
|
52
|
-
declare const isImageBytes: <I>(input: I) => input is I & {
|
|
53
|
-
readonly _tag: "bytes";
|
|
54
|
-
readonly bytes: Uint8Array<ArrayBufferLike>;
|
|
55
|
-
readonly mimeType: ImageMimeType;
|
|
56
|
-
};
|
|
57
|
-
//#endregion
|
|
58
|
-
export { ImageUrlSource as a, imageBytes as c, isImageBytes as d, isImageUrl as f, ImageSource as i, imageUrl as l, ImageBytesSource as n, Image_d_exports as o, ImageMimeType as r, imageBase64 as s, ImageBase64Source as t, isImageBase64 as u };
|
|
59
|
-
//# sourceMappingURL=Image-BK8-4H6s.d.mts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Image-BK8-4H6s.d.mts","names":[],"sources":["../src/domain/Image.ts"],"mappings":";;;;;;;;;;;;;KAWY;KAYA,iBAAiB,SAAS;KAC1B,oBAAoB,YAAY;KAChC,mBAAmB,WAAW;;;;;;;;KAS9B,cAAc,YAAY;cAEzB,gBAAc,OAAA;;;;cAKd,mBAAiB,OAAA;;;;cAKjB,kBAAgB,OAAA;;;;cAKhB,aAAa,OAAO,OAAO;cAM3B,WAAQ,aAAe,WAAa,kBAAgB;cAGpD,cAAW,gBAAkB,UAAY,kBAAgB;cAMzD,aAAU,OAAW,YAAU,UAAY,kBAAgB;cAM3D,aAAU,GAAA,OAAA,MAAA,SAAA;;;;;cACV,gBAAa,GAAA,OAAA,MAAA,SAAA;;;;;cACb,eAAY,GAAA,OAAA,MAAA,SAAA"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"Items-SfUabNuB.d.mts","names":[],"sources":["../src/domain/Items.ts"],"mappings":";;;;;cAOa,WAAS,OAAA;;;;KAIV,mBAAmB,UAAU;;;;;;;;;cAU5B,YAAU,OAAA;;;;KAIX,oBAAoB,WAAW;;;;;;;;;cAgB9B,aAAW,OAAA;;;;;;;;;KASZ,qBAAqB,YAAY;cAEhC,cAAY,OAAA;;;;;KAKb,sBAAsB,aAAa;cAElC,uBAAqB,OAAA;;;;;;;KAOtB,+BAA+B,sBAAsB;cAEpD,UAAQ,OAAA;;;;;KAKT,kBAAkB,SAAS;cAE1B,YAAU,OAAA,gBAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;KACX,oBAAoB,WAAW;cAE9B,gBAAa,GAAA,OAAA,MAAA,SAAA;;;;;;;;;cACb,iBAAc,GAAA,OAAA,MAAA,SAAA;;;;;cACd,0BAAuB,GAAA,OAAA,MAAA,SAAA;;;;;;;cACvB,aAAU,GAAA,OAAA,MAAA,SAAA;;;;;cAEV,YAAU,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;KAKX,oBAAoB,WAAW;;;;;;;cAQ9B,SAAO,OAAA;;;;KAIR,iBAAiB,QAAQ;cAExB,cAAY,OAAA,gBAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KACb,sBAAsB,aAAa;cAElC,MAAI,OAAA;KACL,cAAc,KAAK;cAqBlB,SAAO,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAMR,iBAAiB,QAAQ;cAExB,UAAQ,OAAA;;;;;;;KAQT,kBAAkB,SAAS;cAE1B,gBAAc,OAAA;;;;;;KAMf,wBAAwB,eAAe;;;;;;;cAQtC,WAAS,OAAA;;;;;;;KAOV,mBAAmB,UAAU;cAE5B,aAAW,OAAA,gBAAA,OAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KACZ,qBAAqB,YAAY;cAMhC,cAAW,GAAA,OAAA,MAAA,SAAA;;;;cACX,eAAY,GAAA,OAAA,MAAA,SAAA;;;;cACZ,eAAY,GAAA,OAAA,MAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;cACZ,YAAS,GAAA,OAAA,MAAA,SAAA;;;;cAET,YAAS,GAAA,OAAA,MAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cACT,aAAU,GAAA,OAAA,MAAA,SAAA;;;;;;;cACV,mBAAgB,GAAA,OAAA,MAAA,SAAA;;;;;;cAChB,cAAW,GAAA,OAAA,MAAA,SAAA;;;;;;;;;;;;;;;;;;cAiBX,oBAAkB,OAAA;;;;KAInB,4BAA4B,mBAAmB;cAE9C,qBAAmB,OAAA;;;KAGpB,6BAA6B,oBAAoB;cAEhD,OAAK,OAAA;;;;;;;;;;;;KAON,eAAe,MAAM;cAEpB,YAAU,OAAA;KAUX,oBAAoB,WAAW;cAM9B,WAAQ,iBAAmB;cAM3B,aAAU,iBAAmB;cAM7B,gBAAa,iBAAmB;cAMhC,iBAAc,iBAAmB,mBAAmB"}
|