@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.
Files changed (116) hide show
  1. package/dist/{Audio-60lKKHxS.d.mts → Audio-_nGR4V0X.d.mts} +2 -2
  2. package/dist/{Audio-60lKKHxS.d.mts.map → Audio-_nGR4V0X.d.mts.map} +1 -1
  3. package/dist/Image-Cv6iBLTy.d.mts +99 -0
  4. package/dist/Image-Cv6iBLTy.d.mts.map +1 -0
  5. package/dist/{Items-SfUabNuB.d.mts → Items-C7MyrYPX.d.mts} +35 -4
  6. package/dist/Items-C7MyrYPX.d.mts.map +1 -0
  7. package/dist/Media-BbX7BAaO.d.mts +60 -0
  8. package/dist/Media-BbX7BAaO.d.mts.map +1 -0
  9. package/dist/MessengerError-CeK6VAcu.d.mts +64 -0
  10. package/dist/MessengerError-CeK6VAcu.d.mts.map +1 -0
  11. package/dist/{Tool-DE-rnvkw.d.mts → Tool-BF78QWtn.d.mts} +2 -2
  12. package/dist/{Tool-DE-rnvkw.d.mts.map → Tool-BF78QWtn.d.mts.map} +1 -1
  13. package/dist/{ToolEvent-D99MGQYH.d.mts → ToolEvent-Di4a723B.d.mts} +2 -2
  14. package/dist/{ToolEvent-D99MGQYH.d.mts.map → ToolEvent-Di4a723B.d.mts.map} +1 -1
  15. package/dist/{ToolResult-Cj8ChodL.d.mts → ToolResult-BsYQRjmW.d.mts} +2 -2
  16. package/dist/{ToolResult-Cj8ChodL.d.mts.map → ToolResult-BsYQRjmW.d.mts.map} +1 -1
  17. package/dist/{Toolkit-BEZrNPTI.d.mts → Toolkit-DAjJ9Tvl.d.mts} +5 -5
  18. package/dist/{Toolkit-BEZrNPTI.d.mts.map → Toolkit-DAjJ9Tvl.d.mts.map} +1 -1
  19. package/dist/browser/BrowserTool.d.mts +1 -1
  20. package/dist/capabilities/Capabilities.d.mts +26 -2
  21. package/dist/capabilities/Capabilities.d.mts.map +1 -1
  22. package/dist/capabilities/Capabilities.mjs +32 -1
  23. package/dist/capabilities/Capabilities.mjs.map +1 -1
  24. package/dist/domain/Audio.d.mts +1 -1
  25. package/dist/domain/Image.d.mts +3 -2
  26. package/dist/domain/Image.mjs +29 -3
  27. package/dist/domain/Image.mjs.map +1 -1
  28. package/dist/domain/Items.d.mts +2 -2
  29. package/dist/domain/Items.mjs +19 -1
  30. package/dist/domain/Items.mjs.map +1 -1
  31. package/dist/domain/Media.d.mts +2 -2
  32. package/dist/domain/Media.mjs +51 -2
  33. package/dist/domain/Media.mjs.map +1 -1
  34. package/dist/domain/Music.d.mts +3 -8
  35. package/dist/domain/Music.d.mts.map +1 -1
  36. package/dist/domain/Music.mjs.map +1 -1
  37. package/dist/domain/Research.d.mts +1 -1
  38. package/dist/domain/Turn.d.mts +73 -9
  39. package/dist/domain/Turn.d.mts.map +1 -1
  40. package/dist/domain/Turn.mjs +38 -2
  41. package/dist/domain/Turn.mjs.map +1 -1
  42. package/dist/embedding-model/Embedding.d.mts +1 -1
  43. package/dist/image-generator/ImageGenerator.d.mts +188 -0
  44. package/dist/image-generator/ImageGenerator.d.mts.map +1 -0
  45. package/dist/image-generator/ImageGenerator.mjs +50 -0
  46. package/dist/image-generator/ImageGenerator.mjs.map +1 -0
  47. package/dist/inbox/Inbox.d.mts +16 -0
  48. package/dist/inbox/Inbox.d.mts.map +1 -0
  49. package/dist/inbox/Inbox.mjs +28 -0
  50. package/dist/inbox/Inbox.mjs.map +1 -0
  51. package/dist/inbox/Inbox.test.d.mts +1 -0
  52. package/dist/inbox/Inbox.test.mjs +42 -0
  53. package/dist/inbox/Inbox.test.mjs.map +1 -0
  54. package/dist/index.d.mts +15 -9
  55. package/dist/index.mjs +8 -2
  56. package/dist/language-model/LanguageModel.d.mts +2 -2
  57. package/dist/messenger/Messenger.d.mts +472 -0
  58. package/dist/messenger/Messenger.d.mts.map +1 -0
  59. package/dist/messenger/Messenger.mjs +66 -0
  60. package/dist/messenger/Messenger.mjs.map +1 -0
  61. package/dist/messenger/MessengerAdapter.d.mts +35 -0
  62. package/dist/messenger/MessengerAdapter.d.mts.map +1 -0
  63. package/dist/messenger/MessengerAdapter.mjs +102 -0
  64. package/dist/messenger/MessengerAdapter.mjs.map +1 -0
  65. package/dist/messenger/MessengerAdapter.test.d.mts +1 -0
  66. package/dist/messenger/MessengerAdapter.test.mjs +138 -0
  67. package/dist/messenger/MessengerAdapter.test.mjs.map +1 -0
  68. package/dist/messenger/MessengerError.d.mts +2 -0
  69. package/dist/messenger/MessengerError.mjs +39 -0
  70. package/dist/messenger/MessengerError.mjs.map +1 -0
  71. package/dist/music-generator/MusicGenerator.d.mts +3 -2
  72. package/dist/music-generator/MusicGenerator.d.mts.map +1 -1
  73. package/dist/observability/Metrics.d.mts +1 -1
  74. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +1 -1
  75. package/dist/streaming/Multipart.d.mts +21 -0
  76. package/dist/streaming/Multipart.d.mts.map +1 -0
  77. package/dist/streaming/Multipart.mjs +26 -0
  78. package/dist/streaming/Multipart.mjs.map +1 -0
  79. package/dist/testing/MockMessenger.d.mts +192 -0
  80. package/dist/testing/MockMessenger.d.mts.map +1 -0
  81. package/dist/testing/MockMessenger.mjs +76 -0
  82. package/dist/testing/MockMessenger.mjs.map +1 -0
  83. package/dist/testing/MockMusicGenerator.d.mts +1 -1
  84. package/dist/testing/MockProvider.d.mts +1 -1
  85. package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
  86. package/dist/tool/Approval.d.mts +3 -3
  87. package/dist/tool/HistoryCheck.d.mts +2 -2
  88. package/dist/tool/Tool.d.mts +1 -1
  89. package/dist/tool/ToolEvent.d.mts +1 -1
  90. package/dist/tool/ToolResult.d.mts +1 -1
  91. package/dist/tool/Toolkit.d.mts +1 -1
  92. package/dist/transcriber/Transcriber.d.mts +1 -1
  93. package/dist/web-read/WebReadTool.d.mts +1 -1
  94. package/dist/web-search/WebSearchTool.d.mts +1 -1
  95. package/package.json +29 -1
  96. package/src/capabilities/Capabilities.ts +38 -0
  97. package/src/domain/Image.ts +68 -2
  98. package/src/domain/Items.ts +18 -1
  99. package/src/domain/Media.ts +66 -0
  100. package/src/domain/Music.ts +4 -3
  101. package/src/domain/Turn.ts +56 -0
  102. package/src/image-generator/ImageGenerator.ts +165 -0
  103. package/src/inbox/Inbox.test.ts +52 -0
  104. package/src/inbox/Inbox.ts +33 -0
  105. package/src/index.ts +6 -0
  106. package/src/messenger/Messenger.ts +293 -0
  107. package/src/messenger/MessengerAdapter.test.ts +179 -0
  108. package/src/messenger/MessengerAdapter.ts +189 -0
  109. package/src/messenger/MessengerError.ts +67 -0
  110. package/src/streaming/Multipart.ts +29 -0
  111. package/src/testing/MockMessenger.ts +147 -0
  112. package/dist/Image-BK8-4H6s.d.mts +0 -59
  113. package/dist/Image-BK8-4H6s.d.mts.map +0 -1
  114. package/dist/Items-SfUabNuB.d.mts.map +0 -1
  115. package/dist/Media-DwgpifPD.d.mts +0 -57
  116. package/dist/Media-DwgpifPD.d.mts.map +0 -1
@@ -0,0 +1,76 @@
1
+ import { CurrentConversation, MessageId, Messenger } from "../messenger/Messenger.mjs";
2
+ import { splitForLimit, streamViaEdits } from "../messenger/MessengerAdapter.mjs";
3
+ import { Array, Data, Effect, Layer, Option, Ref, Stream } from "effect";
4
+ //#region src/testing/MockMessenger.ts
5
+ const Call = Data.taggedEnum();
6
+ const defaultLimits = {
7
+ maxText: 4096,
8
+ maxCaption: 1024
9
+ };
10
+ const bodyText = (msg) => msg.body._tag === "Text" ? Option.some(msg.body.text) : Option.none();
11
+ const buildService = (script, record, cursor, minted) => {
12
+ const limits = script.limits ?? defaultLimits;
13
+ const nextFailure = Ref.getAndUpdate(cursor, (n) => n + 1).pipe(Effect.flatMap((i) => Option.match(Option.fromNullishOr((script.failures ?? [])[i]), {
14
+ onNone: () => Effect.void,
15
+ onSome: Effect.fail
16
+ })));
17
+ const mint = Ref.updateAndGet(minted, (n) => n + 1).pipe(Effect.map((n) => MessageId(`m${n}`)));
18
+ const post = (msg) => Effect.gen(function* () {
19
+ const conversation = yield* CurrentConversation;
20
+ yield* record(Call.Post({
21
+ conversation,
22
+ message: msg
23
+ }));
24
+ yield* nextFailure;
25
+ const chunks = Option.match(bodyText(msg), {
26
+ onNone: () => 1,
27
+ onSome: (t) => splitForLimit(t, limits.maxText).length
28
+ });
29
+ return yield* Effect.map(Effect.forEach(Array.range(1, chunks), () => mint), Array.lastNonEmpty);
30
+ });
31
+ const edit = (msg, next) => record(Call.Edit({
32
+ message: msg,
33
+ next
34
+ })).pipe(Effect.andThen(nextFailure));
35
+ return {
36
+ events: script.events ?? Stream.empty,
37
+ post,
38
+ edit,
39
+ react: (msg, emoji) => record(Call.React({
40
+ message: msg,
41
+ emoji
42
+ })),
43
+ typing: Effect.gen(function* () {
44
+ const conversation = yield* CurrentConversation;
45
+ yield* record(Call.TypingStart({ conversation }));
46
+ yield* Effect.addFinalizer(() => record(Call.TypingStop({ conversation })));
47
+ }),
48
+ stream: streamViaEdits({
49
+ post,
50
+ edit,
51
+ limits
52
+ }, script.streamOptions),
53
+ limits
54
+ };
55
+ };
56
+ const makeUnsafe = (script) => {
57
+ const calls = Ref.makeUnsafe([]);
58
+ return {
59
+ service: buildService(script, (call) => Ref.update(calls, Array.append(call)), Ref.makeUnsafe(0), Ref.makeUnsafe(0)),
60
+ recorder: Ref.get(calls).pipe(Effect.map((c) => ({ calls: c })))
61
+ };
62
+ };
63
+ /** Layer registering the mock against the `Messenger` tag, plus its recorder. */
64
+ const layer = (script = {}) => {
65
+ const { service, recorder } = makeUnsafe(script);
66
+ return {
67
+ layer: Layer.succeed(Messenger, service),
68
+ recorder
69
+ };
70
+ };
71
+ /** Bare service value + recorder. Use with `Effect.provideService`. */
72
+ const make = (script = {}) => makeUnsafe(script);
73
+ //#endregion
74
+ export { Call, layer, make };
75
+
76
+ //# sourceMappingURL=MockMessenger.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MockMessenger.mjs","names":["Arr"],"sources":["../../src/testing/MockMessenger.ts"],"sourcesContent":["import { Array as Arr, Data, Effect, Layer, Option, Ref, Stream } from \"effect\"\nimport {\n type ConversationRef,\n CurrentConversation,\n type InboundEvent,\n MessageId,\n type MessageRef,\n Messenger,\n type MessengerLimits,\n type MessengerService,\n type Outbound,\n} from \"../messenger/Messenger.js\"\nimport {\n type StreamViaEditsOptions,\n splitForLimit,\n streamViaEdits,\n} from \"../messenger/MessengerAdapter.js\"\nimport * as MessengerError from \"../messenger/MessengerError.js\"\n\n// ---------------------------------------------------------------------------\n// Call log\n// ---------------------------------------------------------------------------\n\n/**\n * One entry per outbound call. `Post` is logged once even when the text was\n * long enough to split, so a caller that rolled over on its own is\n * distinguishable from one that leaned on `post` to do it.\n */\nexport type Call = Data.TaggedEnum<{\n Post: { readonly conversation: ConversationRef; readonly message: Outbound }\n Edit: { readonly message: MessageRef; readonly next: Outbound }\n React: { readonly message: MessageRef; readonly emoji: string }\n TypingStart: { readonly conversation: ConversationRef }\n TypingStop: { readonly conversation: ConversationRef }\n}>\n\nexport const Call = Data.taggedEnum<Call>()\n\nexport type MockMessengerRecorder = {\n readonly calls: ReadonlyArray<Call>\n}\n\n// ---------------------------------------------------------------------------\n// Script\n// ---------------------------------------------------------------------------\n\nexport type MockMessengerScript = {\n /** Inbound events to replay. Empty by default. */\n readonly events?: Stream.Stream<InboundEvent, MessengerError.MessengerError>\n readonly limits?: MessengerLimits\n /**\n * Failures handed back one per `post` / `edit` call, in order; `undefined`\n * entries succeed, and everything past the end succeeds.\n */\n readonly failures?: ReadonlyArray<MessengerError.MessengerError | undefined>\n /** Passed through to `streamViaEdits`, which backs `stream`. */\n readonly streamOptions?: StreamViaEditsOptions\n}\n\nconst defaultLimits: MessengerLimits = { maxText: 4096, maxCaption: 1024 }\n\nconst bodyText = (msg: Outbound): Option.Option<string> =>\n msg.body._tag === \"Text\" ? Option.some(msg.body.text) : Option.none()\n\n// ---------------------------------------------------------------------------\n// Service builder\n// ---------------------------------------------------------------------------\n\nconst buildService = (\n script: MockMessengerScript,\n record: (call: Call) => Effect.Effect<void>,\n cursor: Ref.Ref<number>,\n minted: Ref.Ref<number>,\n): MessengerService => {\n const limits = script.limits ?? defaultLimits\n\n const nextFailure = Ref.getAndUpdate(cursor, (n) => n + 1).pipe(\n Effect.flatMap((i) =>\n Option.match(Option.fromNullishOr((script.failures ?? [])[i]), {\n onNone: () => Effect.void,\n onSome: Effect.fail<MessengerError.MessengerError>,\n }),\n ),\n )\n\n const mint = Ref.updateAndGet(minted, (n) => n + 1).pipe(Effect.map((n) => MessageId(`m${n}`)))\n\n const post: MessengerService[\"post\"] = (msg) =>\n Effect.gen(function* () {\n const conversation = yield* CurrentConversation\n yield* record(Call.Post({ conversation, message: msg }))\n yield* nextFailure\n // One id per chunk, mirroring an adapter that splits at the ceiling.\n const chunks = Option.match(bodyText(msg), {\n onNone: () => 1,\n onSome: (t) => splitForLimit(t, limits.maxText).length,\n })\n return yield* Effect.map(\n Effect.forEach(Arr.range(1, chunks), () => mint),\n Arr.lastNonEmpty,\n )\n })\n\n const edit: MessengerService[\"edit\"] = (msg, next) =>\n record(Call.Edit({ message: msg, next })).pipe(Effect.andThen(nextFailure))\n\n return {\n events: script.events ?? Stream.empty,\n post,\n edit,\n react: (msg, emoji) => record(Call.React({ message: msg, emoji })),\n typing: Effect.gen(function* () {\n const conversation = yield* CurrentConversation\n yield* record(Call.TypingStart({ conversation }))\n yield* Effect.addFinalizer(() => record(Call.TypingStop({ conversation })))\n }),\n stream: streamViaEdits({ post, edit, limits }, script.streamOptions),\n limits,\n }\n}\n\n// ---------------------------------------------------------------------------\n// Public API\n// ---------------------------------------------------------------------------\n\nconst makeUnsafe = (script: MockMessengerScript) => {\n const calls = Ref.makeUnsafe<ReadonlyArray<Call>>([])\n const service = buildService(\n script,\n (call) => Ref.update(calls, Arr.append(call)),\n Ref.makeUnsafe(0),\n Ref.makeUnsafe(0),\n )\n return {\n service,\n recorder: Ref.get(calls).pipe(Effect.map((c): MockMessengerRecorder => ({ calls: c }))),\n }\n}\n\n/** Layer registering the mock against the `Messenger` tag, plus its recorder. */\nexport const layer = (script: MockMessengerScript = {}) => {\n const { service, recorder } = makeUnsafe(script)\n return { layer: Layer.succeed(Messenger, service), recorder }\n}\n\n/** Bare service value + recorder. Use with `Effect.provideService`. */\nexport const make = (script: MockMessengerScript = {}) => makeUnsafe(script)\n"],"mappings":";;;;AAoCA,MAAa,OAAO,KAAK,WAAiB;AAuB1C,MAAM,gBAAiC;CAAE,SAAS;CAAM,YAAY;AAAK;AAEzE,MAAM,YAAY,QAChB,IAAI,KAAK,SAAS,SAAS,OAAO,KAAK,IAAI,KAAK,IAAI,IAAI,OAAO,KAAK;AAMtE,MAAM,gBACJ,QACA,QACA,QACA,WACqB;CACrB,MAAM,SAAS,OAAO,UAAU;CAEhC,MAAM,cAAc,IAAI,aAAa,SAAS,MAAM,IAAI,CAAC,CAAC,CAAC,KACzD,OAAO,SAAS,MACd,OAAO,MAAM,OAAO,eAAe,OAAO,YAAY,CAAC,EAAA,CAAG,EAAE,GAAG;EAC7D,cAAc,OAAO;EACrB,QAAQ,OAAO;CACjB,CAAC,CACH,CACF;CAEA,MAAM,OAAO,IAAI,aAAa,SAAS,MAAM,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,KAAK,MAAM,UAAU,IAAI,GAAG,CAAC,CAAC;CAE9F,MAAM,QAAkC,QACtC,OAAO,IAAI,aAAa;EACtB,MAAM,eAAe,OAAO;EAC5B,OAAO,OAAO,KAAK,KAAK;GAAE;GAAc,SAAS;EAAI,CAAC,CAAC;EACvD,OAAO;EAEP,MAAM,SAAS,OAAO,MAAM,SAAS,GAAG,GAAG;GACzC,cAAc;GACd,SAAS,MAAM,cAAc,GAAG,OAAO,OAAO,CAAC,CAAC;EAClD,CAAC;EACD,OAAO,OAAO,OAAO,IACnB,OAAO,QAAQA,MAAI,MAAM,GAAG,MAAM,SAAS,IAAI,GAC/CA,MAAI,YACN;CACF,CAAC;CAEH,MAAM,QAAkC,KAAK,SAC3C,OAAO,KAAK,KAAK;EAAE,SAAS;EAAK;CAAK,CAAC,CAAC,CAAC,CAAC,KAAK,OAAO,QAAQ,WAAW,CAAC;CAE5E,OAAO;EACL,QAAQ,OAAO,UAAU,OAAO;EAChC;EACA;EACA,QAAQ,KAAK,UAAU,OAAO,KAAK,MAAM;GAAE,SAAS;GAAK;EAAM,CAAC,CAAC;EACjE,QAAQ,OAAO,IAAI,aAAa;GAC9B,MAAM,eAAe,OAAO;GAC5B,OAAO,OAAO,KAAK,YAAY,EAAE,aAAa,CAAC,CAAC;GAChD,OAAO,OAAO,mBAAmB,OAAO,KAAK,WAAW,EAAE,aAAa,CAAC,CAAC,CAAC;EAC5E,CAAC;EACD,QAAQ,eAAe;GAAE;GAAM;GAAM;EAAO,GAAG,OAAO,aAAa;EACnE;CACF;AACF;AAMA,MAAM,cAAc,WAAgC;CAClD,MAAM,QAAQ,IAAI,WAAgC,CAAC,CAAC;CAOpD,OAAO;EACL,SAPc,aACd,SACC,SAAS,IAAI,OAAO,OAAOA,MAAI,OAAO,IAAI,CAAC,GAC5C,IAAI,WAAW,CAAC,GAChB,IAAI,WAAW,CAAC,CAGV;EACN,UAAU,IAAI,IAAI,KAAK,CAAC,CAAC,KAAK,OAAO,KAAK,OAA8B,EAAE,OAAO,EAAE,EAAE,CAAC;CACxF;AACF;;AAGA,MAAa,SAAS,SAA8B,CAAC,MAAM;CACzD,MAAM,EAAE,SAAS,aAAa,WAAW,MAAM;CAC/C,OAAO;EAAE,OAAO,MAAM,QAAQ,WAAW,OAAO;EAAG;CAAS;AAC9D;;AAGA,MAAa,QAAQ,SAA8B,CAAC,MAAM,WAAW,MAAM"}
@@ -1,4 +1,4 @@
1
- import { n as AudioChunk } from "../Audio-60lKKHxS.mjs";
1
+ import { n as AudioChunk } from "../Audio-_nGR4V0X.mjs";
2
2
  import { CommonGenerateMusicRequest, CommonStreamGenerateMusicRequest, GenerateResult, MusicStreamEvent, audioEvent } from "../domain/Music.mjs";
3
3
  import { MusicGenerator, MusicInteractiveSession } from "../music-generator/MusicGenerator.mjs";
4
4
  import { Effect, Layer } from "effect";
@@ -1,4 +1,4 @@
1
- import { o as HistoryItem } from "../Items-SfUabNuB.mjs";
1
+ import { o as HistoryItem } from "../Items-C7MyrYPX.mjs";
2
2
  import { Turn } from "../domain/Turn.mjs";
3
3
  import { LanguageModel, LanguageModelService } from "../language-model/LanguageModel.mjs";
4
4
  import { Duration, Effect, Layer } from "effect";
@@ -1,4 +1,4 @@
1
- import { n as AudioChunk, t as AudioBlob } from "../Audio-60lKKHxS.mjs";
1
+ import { n as AudioChunk, t as AudioBlob } from "../Audio-_nGR4V0X.mjs";
2
2
  import { CommonStreamSynthesizeRequest, CommonSynthesizeDialogueRequest, CommonSynthesizeRequest, MultiSpeakerTts, SpeechSynthesizer, TtsIncrementalText } from "../speech-synthesizer/SpeechSynthesizer.mjs";
3
3
  import { Effect, Layer } from "effect";
4
4
  //#region src/testing/MockSpeechSynthesizer.d.ts
@@ -1,6 +1,6 @@
1
- import { v as ToolCall } from "../Items-SfUabNuB.mjs";
2
- import { t as ToolResult } from "../ToolResult-Cj8ChodL.mjs";
3
- import { t as ToolEvent } from "../ToolEvent-D99MGQYH.mjs";
1
+ import { y as ToolCall } from "../Items-C7MyrYPX.mjs";
2
+ import { t as ToolResult } from "../ToolResult-BsYQRjmW.mjs";
3
+ import { t as ToolEvent } from "../ToolEvent-Di4a723B.mjs";
4
4
  import { Data, Duration, Effect, Queue, Scope, Stream } from "effect";
5
5
  declare namespace Approval_d_exports {
6
6
  export { ApprovalDecision, ApprovalMapEntry, FromQueueOptions, ToolCallPlan, Verdict, approvalRequested, approve, fromMap, fromQueue, reject, splitApprovalDecisions };
@@ -1,5 +1,5 @@
1
- import { o as HistoryItem, v as ToolCall } from "../Items-SfUabNuB.mjs";
2
- import { t as ToolResult } from "../ToolResult-Cj8ChodL.mjs";
1
+ import { o as HistoryItem, y as ToolCall } from "../Items-C7MyrYPX.mjs";
2
+ import { t as ToolResult } from "../ToolResult-BsYQRjmW.mjs";
3
3
  declare namespace HistoryCheck_d_exports {
4
4
  export { cancelAllPending, findUnansweredCalls, isReconciled };
5
5
  }
@@ -1,2 +1,2 @@
1
- import { A as providerToolsOf, C as fromEffectSchema, D as make, E as isProviderTool, M as toDescriptors, N as withName, O as noInput, P as withRun, S as fail, T as interaction, a as Emit, b as descriptorsOf, c as SignalTool, d as ToolE, f as ToolError, g as ToolValidationError, h as ToolR, i as DecodeResult, j as signal, k as provider, l as Tool, m as ToolInputSchema, n as AnyTool, o as InteractionTool, p as ToolFailed, r as DecodableTool, s as ProviderTool, t as AnyLocalTool, u as ToolDescriptor, v as decodeArgs, w as fromStandardSchema, x as execute, y as decodeCallInput } from "../Tool-DE-rnvkw.mjs";
1
+ import { A as providerToolsOf, C as fromEffectSchema, D as make, E as isProviderTool, M as toDescriptors, N as withName, O as noInput, P as withRun, S as fail, T as interaction, a as Emit, b as descriptorsOf, c as SignalTool, d as ToolE, f as ToolError, g as ToolValidationError, h as ToolR, i as DecodeResult, j as signal, k as provider, l as Tool, m as ToolInputSchema, n as AnyTool, o as InteractionTool, p as ToolFailed, r as DecodableTool, s as ProviderTool, t as AnyLocalTool, u as ToolDescriptor, v as decodeArgs, w as fromStandardSchema, x as execute, y as decodeCallInput } from "../Tool-BF78QWtn.mjs";
2
2
  export { AnyLocalTool, AnyTool, DecodableTool, DecodeResult, Emit, InteractionTool, ProviderTool, SignalTool, Tool, ToolDescriptor, ToolE, ToolError, ToolFailed, ToolInputSchema, ToolR, ToolValidationError, decodeArgs, decodeCallInput, descriptorsOf, execute, fail, fromEffectSchema, fromStandardSchema, interaction, isProviderTool, make, noInput, provider, providerToolsOf, signal, toDescriptors, withName, withRun };
@@ -1,2 +1,2 @@
1
- import { a as isProgress, i as isOutput, r as isApprovalRequested, t as ToolEvent } from "../ToolEvent-D99MGQYH.mjs";
1
+ import { a as isProgress, i as isOutput, r as isApprovalRequested, t as ToolEvent } from "../ToolEvent-Di4a723B.mjs";
2
2
  export { ToolEvent, isApprovalRequested, isOutput, isProgress };
@@ -1,2 +1,2 @@
1
- import { a as executionError, c as isOk, d as toToolCallOutput, f as validationError, i as denied, l as nonLocalTool, o as failed, r as cancelled, s as isFailure, t as ToolResult, u as serializeValue } from "../ToolResult-Cj8ChodL.mjs";
1
+ import { a as executionError, c as isOk, d as toToolCallOutput, f as validationError, i as denied, l as nonLocalTool, o as failed, r as cancelled, s as isFailure, t as ToolResult, u as serializeValue } from "../ToolResult-BsYQRjmW.mjs";
2
2
  export { ToolResult, cancelled, denied, executionError, failed, isFailure, isOk, nonLocalTool, serializeValue, toToolCallOutput, validationError };
@@ -1,2 +1,2 @@
1
- import { S as wrap, _ as fromArray, a as ToolMap, b as namespace, c as ToolkitR, d as appendToolResults, f as collectResults, g as descriptors, h as describeFailures, i as NoDuplicateNames, m as continueWithResults, n as ExecuteOptions, o as Toolkit, p as compose, r as Middleware, s as ToolkitE, t as DuplicateToolName, u as UniqueTools, v as make, x as run, y as makeNamespaced } from "../Toolkit-BEZrNPTI.mjs";
1
+ import { S as wrap, _ as fromArray, a as ToolMap, b as namespace, c as ToolkitR, d as appendToolResults, f as collectResults, g as descriptors, h as describeFailures, i as NoDuplicateNames, m as continueWithResults, n as ExecuteOptions, o as Toolkit, p as compose, r as Middleware, s as ToolkitE, t as DuplicateToolName, u as UniqueTools, v as make, x as run, y as makeNamespaced } from "../Toolkit-DAjJ9Tvl.mjs";
2
2
  export { DuplicateToolName, ExecuteOptions, Middleware, NoDuplicateNames, ToolMap, Toolkit, ToolkitE, ToolkitR, UniqueTools, appendToolResults, collectResults, compose, continueWithResults, describeFailures, descriptors, fromArray, make, makeNamespaced, namespace, run, wrap };
@@ -1,5 +1,5 @@
1
1
  import { t as AiError } from "../AiError-eLV9t1lU.mjs";
2
- import { a as AudioSource, r as AudioFormat } from "../Audio-60lKKHxS.mjs";
2
+ import { a as AudioSource, r as AudioFormat } from "../Audio-_nGR4V0X.mjs";
3
3
  import { TranscriptEvent, TranscriptResult } from "../domain/Transcript.mjs";
4
4
  import { Context, Effect, Stream } from "effect";
5
5
  declare namespace Transcriber_d_exports {
@@ -1,4 +1,4 @@
1
- import { l as Tool } from "../Tool-DE-rnvkw.mjs";
1
+ import { l as Tool } from "../Tool-BF78QWtn.mjs";
2
2
  import { t as AiError } from "../AiError-eLV9t1lU.mjs";
3
3
  import { ReadFormat, ReadResponse, WebRead } from "./WebRead.mjs";
4
4
  import { Schema } from "effect";
@@ -1,4 +1,4 @@
1
- import { l as Tool } from "../Tool-DE-rnvkw.mjs";
1
+ import { l as Tool } from "../Tool-BF78QWtn.mjs";
2
2
  import { t as AiError } from "../AiError-eLV9t1lU.mjs";
3
3
  import { SearchResult, WebSearch } from "./WebSearch.mjs";
4
4
  import { Schema } from "effect";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/core",
3
- "version": "0.13.0",
3
+ "version": "0.15.0",
4
4
  "description": "Low-level primitives (loop, conversation, items, tools, streaming codecs) for building AI agents with Effect.",
5
5
  "keywords": [
6
6
  "agents",
@@ -106,10 +106,26 @@
106
106
  "types": "./dist/embedding-model/EmbeddingModel.d.mts",
107
107
  "import": "./dist/embedding-model/EmbeddingModel.mjs"
108
108
  },
109
+ "./ImageGenerator": {
110
+ "types": "./dist/image-generator/ImageGenerator.d.mts",
111
+ "import": "./dist/image-generator/ImageGenerator.mjs"
112
+ },
109
113
  "./LanguageModel": {
110
114
  "types": "./dist/language-model/LanguageModel.d.mts",
111
115
  "import": "./dist/language-model/LanguageModel.mjs"
112
116
  },
117
+ "./Messenger": {
118
+ "types": "./dist/messenger/Messenger.d.mts",
119
+ "import": "./dist/messenger/Messenger.mjs"
120
+ },
121
+ "./MessengerAdapter": {
122
+ "types": "./dist/messenger/MessengerAdapter.d.mts",
123
+ "import": "./dist/messenger/MessengerAdapter.mjs"
124
+ },
125
+ "./MessengerError": {
126
+ "types": "./dist/messenger/MessengerError.d.mts",
127
+ "import": "./dist/messenger/MessengerError.mjs"
128
+ },
113
129
  "./MusicGenerator": {
114
130
  "types": "./dist/music-generator/MusicGenerator.d.mts",
115
131
  "import": "./dist/music-generator/MusicGenerator.mjs"
@@ -162,6 +178,10 @@
162
178
  "types": "./dist/web-search/WebSearchTool.d.mts",
163
179
  "import": "./dist/web-search/WebSearchTool.mjs"
164
180
  },
181
+ "./Inbox": {
182
+ "types": "./dist/inbox/Inbox.d.mts",
183
+ "import": "./dist/inbox/Inbox.mjs"
184
+ },
165
185
  "./Loop": {
166
186
  "types": "./dist/loop/Loop.d.mts",
167
187
  "import": "./dist/loop/Loop.mjs"
@@ -214,6 +234,10 @@
214
234
  "types": "./dist/streaming/Lines.d.mts",
215
235
  "import": "./dist/streaming/Lines.mjs"
216
236
  },
237
+ "./Multipart": {
238
+ "types": "./dist/streaming/Multipart.d.mts",
239
+ "import": "./dist/streaming/Multipart.mjs"
240
+ },
217
241
  "./SSE": {
218
242
  "types": "./dist/streaming/SSE.d.mts",
219
243
  "import": "./dist/streaming/SSE.mjs"
@@ -230,6 +254,10 @@
230
254
  "types": "./dist/testing/MockProvider.d.mts",
231
255
  "import": "./dist/testing/MockProvider.mjs"
232
256
  },
257
+ "./testing/MockMessenger": {
258
+ "types": "./dist/testing/MockMessenger.d.mts",
259
+ "import": "./dist/testing/MockMessenger.mjs"
260
+ },
233
261
  "./testing/MockMusicGenerator": {
234
262
  "types": "./dist/testing/MockMusicGenerator.d.mts",
235
263
  "import": "./dist/testing/MockMusicGenerator.mjs"
@@ -1,4 +1,5 @@
1
1
  import { Effect } from "effect"
2
+ import type { ContentBlock, HistoryItem } from "../domain/Items.js"
2
3
 
3
4
  /**
4
5
  * Structured event emitted when a provider adapter drops a Common
@@ -79,3 +80,40 @@ export const warnDroppedWhen = <T>(
79
80
  value: T | undefined,
80
81
  warning: Omit<CapabilityWarning, "_tag" | "value">,
81
82
  ): Effect.Effect<void> => (value === undefined ? Effect.void : warnDropped({ ...warning, value }))
83
+
84
+ /**
85
+ * Warn when history carries a content block this provider's wire has no
86
+ * place for, which its encoder therefore omits.
87
+ *
88
+ * History is provider-agnostic, so a turn one provider produced can be
89
+ * replayed into another. A block only one provider can express
90
+ * (`output_image`, which needs an assistant-role image part) then reaches
91
+ * an encoder with nothing to map it to. Omitting is right, since failing
92
+ * would break replaying a mixed history just to continue the text, but it
93
+ * should not be silent.
94
+ *
95
+ * Call once from the effectful request path; the block encoders stay pure.
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * yield* warnDroppedBlocks(history, "output_image", {
100
+ * provider: "anthropic",
101
+ * capability: "output_image",
102
+ * reason: "Assistant messages carry no image here. Convert with `imagesAsInput` to send it as user content.",
103
+ * })
104
+ * ```
105
+ */
106
+ export const warnDroppedBlocks = (
107
+ history: ReadonlyArray<HistoryItem>,
108
+ type: ContentBlock["type"],
109
+ warning: Omit<CapabilityWarning, "_tag" | "field" | "value">,
110
+ ): Effect.Effect<void> => {
111
+ const count = history.reduce(
112
+ (n, item) =>
113
+ n + (item.type === "message" ? item.content.filter((b) => b.type === type).length : 0),
114
+ 0,
115
+ )
116
+ return count === 0
117
+ ? Effect.void
118
+ : warnDropped({ ...warning, field: type, value: `${count} block(s)` })
119
+ }
@@ -1,5 +1,14 @@
1
- import { Schema } from "effect"
2
- import type { MediaBase64, MediaBytes, MediaSource, MediaUrl } from "./Media.js"
1
+ import { Result, Schema } from "effect"
2
+ import type {
3
+ AspectRatio,
4
+ Dimensions,
5
+ MediaBase64,
6
+ MediaBytes,
7
+ MediaSource,
8
+ MediaUrl,
9
+ Watermark,
10
+ } from "./Media.js"
11
+ import { parseAspectRatio, round16 } from "./Media.js"
3
12
 
4
13
  /**
5
14
  * Image MIME types AI providers typically accept. The first four are the
@@ -70,6 +79,63 @@ export const imageBytes = (bytes: Uint8Array, mimeType: ImageMimeType): ImageByt
70
79
  mimeType,
71
80
  })
72
81
 
82
+ /** Cross-modality; lives in `Media.ts` because video generation shares it. */
83
+ export type { AspectRatio, Dimensions } from "./Media.js"
84
+
85
+ /**
86
+ * Resolution tier, roughly the short edge in pixels. A tier rather than
87
+ * a pixel pair: adapters whose wire takes exact dimensions derive them
88
+ * from the tier and the aspect ratio. Image-typed on purpose, video
89
+ * models tier by scan height instead.
90
+ */
91
+ export type ImageResolution = "1K" | "2K" | "4K"
92
+
93
+ const SHORT_EDGE: Record<ImageResolution, number> = { "1K": 1024, "2K": 2048, "4K": 4096 }
94
+
95
+ /**
96
+ * Turn the portable ratio and tier into pixels: the tier is the short
97
+ * edge, the long edge follows the ratio. `undefined` when neither was
98
+ * asked for, which the caller sends as "no size" so the endpoint picks.
99
+ *
100
+ * Adapters whose wire wants `"WxH"` format the pair; those wanting an
101
+ * object send it as is. Whether the result is in range for a given model
102
+ * is the server's call: per-model limits churn and we do not table them.
103
+ */
104
+ export const pixelsOf = (request: {
105
+ readonly aspectRatio?: AspectRatio
106
+ readonly resolution?: ImageResolution
107
+ }): Result.Result<Dimensions | undefined, string> =>
108
+ request.aspectRatio === undefined && request.resolution === undefined
109
+ ? Result.succeed(undefined)
110
+ : Result.map(parseAspectRatio(request.aspectRatio ?? "1:1"), ([w, h]) => {
111
+ const short = SHORT_EDGE[request.resolution ?? "1K"]
112
+ return w >= h
113
+ ? { width: round16((short * w) / h), height: short }
114
+ : { width: short, height: round16((short * h) / w) }
115
+ })
116
+
117
+ /**
118
+ * Per-image extras a provider reported and this type has no field for:
119
+ * pixel dimensions, a file name, a revised prompt. Opaque here, and a
120
+ * shared slot, so a provider keys its data under its own name
121
+ * (`{ fal: … }`, `{ openai: … }`) and reads only that key. Each provider
122
+ * package ships a typed reader; the framework never interprets it.
123
+ *
124
+ * Not promoted to real fields because the providers disagree on what
125
+ * they report. Only some fal endpoints return dimensions, and neither
126
+ * OpenAI's Images API nor Gemini returns them at all.
127
+ */
128
+ export type ProviderData = unknown
129
+
130
+ /** One image off a generation call, plus what the provider stamped into it. */
131
+ export type GeneratedImage = {
132
+ readonly image: ImageSource
133
+ /** Set only when the provider applies one. */
134
+ readonly watermark?: Watermark
135
+ /** {@link ProviderData}: per-image extras, keyed by provider name. */
136
+ readonly providerData?: ProviderData
137
+ }
138
+
73
139
  export const isImageUrl = Schema.is(ImageUrlSource)
74
140
  export const isImageBase64 = Schema.is(ImageBase64Source)
75
141
  export const isImageBytes = Schema.is(ImageBytesSource)
@@ -88,6 +88,22 @@ export const OutputText = Schema.Struct({
88
88
  })
89
89
  export type OutputText = typeof OutputText.Type
90
90
 
91
+ /**
92
+ * Model-emitted image. The counterpart to {@link InputImage}: same
93
+ * `ImageSource`, so an image the model drew goes straight back into the
94
+ * next turn with no conversion.
95
+ *
96
+ * How the pixels were produced is not modelled here. A provider may draw
97
+ * them itself, as an image part of the turn, or reach a hosted tool that
98
+ * draws them; either way the fact is the same, and enabling it is
99
+ * provider-specific.
100
+ */
101
+ export const OutputImage = Schema.Struct({
102
+ type: Schema.Literal("output_image"),
103
+ source: ImageSource,
104
+ })
105
+ export type OutputImage = typeof OutputImage.Type
106
+
91
107
  /**
92
108
  * Model-emitted refusal. Distinct from `output_text`: the model declined
93
109
  * to answer rather than producing normal output. Pair with
@@ -100,7 +116,7 @@ export const Refusal = Schema.Struct({
100
116
  })
101
117
  export type Refusal = typeof Refusal.Type
102
118
 
103
- export const ContentBlock = Schema.Union([InputText, InputImage, OutputText, Refusal])
119
+ export const ContentBlock = Schema.Union([InputText, InputImage, OutputText, OutputImage, Refusal])
104
120
  export type ContentBlock = typeof ContentBlock.Type
105
121
 
106
122
  export const Role = Schema.Literals(["user", "assistant", "system"])
@@ -176,6 +192,7 @@ export type HistoryItem = typeof HistoryItem.Type
176
192
  export const isInputText = Schema.is(InputText)
177
193
  export const isInputImage = Schema.is(InputImage)
178
194
  export const isOutputText = Schema.is(OutputText)
195
+ export const isOutputImage = Schema.is(OutputImage)
179
196
  export const isRefusal = Schema.is(Refusal)
180
197
 
181
198
  export const isMessage = Schema.is(Message)
@@ -31,6 +31,72 @@
31
31
  * they're a separate union (`FileRef`) added when needed.
32
32
  */
33
33
 
34
+ import { Result } from "effect"
35
+
36
+ /**
37
+ * Watermark kind a provider embedded in generated media. Shared across
38
+ * modalities (audio, image). No provider exposes metadata beyond the
39
+ * kind, so a bare literal union is enough.
40
+ */
41
+ export type Watermark = "synthid" | "c2pa" | (string & {})
42
+
43
+ /**
44
+ * Requested shape of generated visual media, width to height. Shared by
45
+ * image and video generation: the listed ratios are the ones providers
46
+ * accept as literals, and the `(string & {})` tail keeps a
47
+ * newly-supported ratio working without an SDK update. Which ratios a
48
+ * given model accepts is not checked here; the provider rejects what it
49
+ * cannot render and the adapter translates that error.
50
+ *
51
+ * Resolution is NOT shared: image models tier by short edge
52
+ * (`ImageResolution`), video models by scan height. Each modality types
53
+ * its own.
54
+ */
55
+ export type AspectRatio =
56
+ | "1:1"
57
+ | "2:3"
58
+ | "3:2"
59
+ | "3:4"
60
+ | "4:3"
61
+ | "4:5"
62
+ | "5:4"
63
+ | "9:16"
64
+ | "16:9"
65
+ | "21:9"
66
+ | "1:4"
67
+ | "4:1"
68
+ | "1:8"
69
+ | "8:1"
70
+ | (string & {})
71
+
72
+ /** Exact pixels, for adapters whose wire takes dimensions rather than a ratio. */
73
+ export type Dimensions = {
74
+ readonly width: number
75
+ readonly height: number
76
+ }
77
+
78
+ const positive = (n: number | undefined): n is number =>
79
+ n !== undefined && Number.isFinite(n) && n > 0
80
+
81
+ /**
82
+ * Read `"W:H"` as a pair. Fails when the ratio is not arithmetic, which
83
+ * the `(string & {})` tail on {@link AspectRatio} lets through: a caller
84
+ * asking for `"cinematic"` gets a message rather than NaN pixels. The
85
+ * failure is a plain string so this file stays free of provider
86
+ * vocabulary; each adapter wraps it in its own error.
87
+ */
88
+ export const parseAspectRatio = (
89
+ ratio: AspectRatio,
90
+ ): Result.Result<readonly [number, number], string> => {
91
+ const [w, h, ...rest] = ratio.split(":").map(Number)
92
+ return positive(w) && positive(h) && rest.length === 0
93
+ ? Result.succeed([w, h] as const)
94
+ : Result.fail(`aspectRatio "${ratio}" must read "W:H" with positive numbers.`)
95
+ }
96
+
97
+ /** Several image endpoints require both edges to be multiples of 16. */
98
+ export const round16 = (n: number): number => Math.max(16, Math.round(n / 16) * 16)
99
+
34
100
  export type MediaUrl<M extends string = string> = {
35
101
  readonly _tag: "url"
36
102
  readonly url: string
@@ -1,5 +1,6 @@
1
1
  import type { Duration } from "effect"
2
2
  import type { AudioBlob, AudioChunk, AudioFormat } from "./Audio.js"
3
+ import type { Watermark } from "./Media.js"
3
4
 
4
5
  /**
5
6
  * Prompt fragment with a relative weight. Native to Lyria RealTime
@@ -95,10 +96,10 @@ export const controlInput = (action: MusicSessionControl): MusicSessionInput =>
95
96
 
96
97
  /**
97
98
  * Watermark kind on the response. SynthID is mandatory on Lyria; C2PA
98
- * is opt-in on ElevenLabs. No provider exposes additional metadata
99
- * about the watermark itself, so a bare literal-union is enough.
99
+ * is opt-in on ElevenLabs. Lives in `Media.ts` since image providers
100
+ * report the same kinds; re-exported here unchanged.
100
101
  */
101
- export type Watermark = "synthid" | "c2pa" | (string & {})
102
+ export type { Watermark } from "./Media.js"
102
103
 
103
104
  /**
104
105
  * Labelled section with absolute timing. Populated when the provider
@@ -1,5 +1,6 @@
1
1
  import { Data, Effect, Result, Schema, Stream, pipe } from "effect"
2
2
  import * as StructuredFormat from "../structured-format/StructuredFormat.js"
3
+ import type { ImageSource } from "./Image.js"
3
4
  import {
4
5
  type Annotation,
5
6
  HistoryItem,
@@ -9,6 +10,7 @@ import {
9
10
  ToolCall,
10
11
  ToolCallOutput,
11
12
  Usage,
13
+ isOutputImage,
12
14
  isOutputText,
13
15
  isReasoning,
14
16
  isRefusal,
@@ -49,6 +51,12 @@ export type Turn = typeof Turn.Type
49
51
  * `WebSearchCall`: lifecycle of a provider-executed web search within the
50
52
  * turn. Fires only when the turn grounds against search; absent otherwise.
51
53
  *
54
+ * `ImageOutput`: an image the model produced, whether it drew it as part
55
+ * of its turn or reached a hosted tool for it. `partialIndex` marks a
56
+ * preview frame from providers that stream them; the finished image
57
+ * arrives without one and is also on `TurnComplete.turn` as an
58
+ * `OutputImage` block.
59
+ *
52
60
  * `CitationAdded`: a citation attached to the answer, emitted incrementally
53
61
  * where the provider streams citations. Providers that bundle citations emit
54
62
  * none of these; their citations still arrive on `TurnComplete.turn`
@@ -66,6 +74,11 @@ export type TurnEvent = Data.TaggedEnum<{
66
74
  readonly query?: string
67
75
  readonly action?: "search" | "open_page" | "find_in_page"
68
76
  }
77
+ ImageOutput: {
78
+ readonly image: ImageSource
79
+ /** Set only on a preview frame, counting from 0. Absent means finished. */
80
+ readonly partialIndex?: number
81
+ }
69
82
  CitationAdded: { readonly annotation: Annotation }
70
83
  TurnComplete: { readonly turn: Turn }
71
84
  }>
@@ -109,6 +122,49 @@ export const assistantTexts = (turn: Turn): ReadonlyArray<string> =>
109
122
  */
110
123
  export const assistantText = (turn: Turn): string => assistantTexts(turn).join("")
111
124
 
125
+ /**
126
+ * Every image the model produced this turn, in order. The counterpart to
127
+ * {@link assistantTexts}: each is an `ImageSource`, so it can be written
128
+ * to disk or passed straight back as an `input_image` on the next turn.
129
+ */
130
+ export const assistantImages = (turn: Turn): ReadonlyArray<ImageSource> =>
131
+ assistantMessages(turn)
132
+ .flatMap((m) => m.content)
133
+ .filter(isOutputImage)
134
+ .map((b) => b.source)
135
+
136
+ /**
137
+ * Rewrite assistant `output_image` blocks into a user message carrying
138
+ * the same pictures as `input_image`, so another provider can look at
139
+ * what this one drew.
140
+ *
141
+ * Only Gemini's wire has an assistant-role image part; every other
142
+ * adapter omits an `output_image` on replay and says so. Moving it to a
143
+ * user message is the conversion that makes it portable, and it is
144
+ * yours to make rather than the adapter's: "the assistant drew this" and
145
+ * "here is an image, look at it" are different claims, and silently
146
+ * swapping one for the other behind your back would be the wrong kind of
147
+ * helpful.
148
+ *
149
+ * Items with no images pass through untouched, and the new user message
150
+ * follows the assistant message it came from.
151
+ */
152
+ export const imagesAsInput = (history: ReadonlyArray<HistoryItem>): ReadonlyArray<HistoryItem> =>
153
+ history.flatMap((item) => {
154
+ if (item.type !== "message" || item.role !== "assistant") return [item]
155
+ const images = item.content.filter(isOutputImage)
156
+ return images.length === 0
157
+ ? [item]
158
+ : [
159
+ item,
160
+ {
161
+ type: "message" as const,
162
+ role: "user" as const,
163
+ content: images.map((b) => ({ type: "input_image" as const, source: b.source })),
164
+ },
165
+ ]
166
+ })
167
+
112
168
  /**
113
169
  * Every citation annotation attached to the turn's assistant `output_text`
114
170
  * blocks, in order. The flat view of a grounded turn's sources, whether the