@effect-uai/google 0.11.0 → 0.12.1

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 (63) hide show
  1. package/dist/Gemini.d.mts +8 -2
  2. package/dist/Gemini.d.mts.map +1 -1
  3. package/dist/Gemini.mjs +1 -1
  4. package/dist/Gemini.mjs.map +1 -1
  5. package/dist/GeminiEmbedding.d.mts +2 -3
  6. package/dist/GeminiEmbedding.d.mts.map +1 -1
  7. package/dist/GeminiEmbedding.mjs +1 -1
  8. package/dist/GeminiEmbedding.mjs.map +1 -1
  9. package/dist/GeminiSynthesizer.d.mts +14 -6
  10. package/dist/GeminiSynthesizer.d.mts.map +1 -1
  11. package/dist/GeminiSynthesizer.mjs +13 -12
  12. package/dist/GeminiSynthesizer.mjs.map +1 -1
  13. package/dist/GeminiSynthesizer.test.d.mts +1 -1
  14. package/dist/GeminiSynthesizer.test.mjs +1 -1
  15. package/dist/GeminiSynthesizer.test.mjs.map +1 -1
  16. package/dist/GeminiTools.d.mts +9 -5
  17. package/dist/GeminiTools.d.mts.map +1 -1
  18. package/dist/GeminiTools.mjs +1 -1
  19. package/dist/GeminiTools.mjs.map +1 -1
  20. package/dist/GoogleDeepResearch.d.mts +58 -7
  21. package/dist/GoogleDeepResearch.d.mts.map +1 -1
  22. package/dist/GoogleDeepResearch.mjs +77 -23
  23. package/dist/GoogleDeepResearch.mjs.map +1 -1
  24. package/dist/GoogleDeepResearch.test.d.mts +1 -0
  25. package/dist/GoogleDeepResearch.test.mjs +66 -0
  26. package/dist/GoogleDeepResearch.test.mjs.map +1 -0
  27. package/dist/LyriaGenerator.d.mts +0 -2
  28. package/dist/LyriaGenerator.d.mts.map +1 -1
  29. package/dist/LyriaGenerator.mjs +9 -6
  30. package/dist/LyriaGenerator.mjs.map +1 -1
  31. package/dist/LyriaGenerator.test.d.mts +1 -1
  32. package/dist/LyriaGenerator.test.mjs +153 -8
  33. package/dist/LyriaGenerator.test.mjs.map +1 -1
  34. package/dist/codec.d.mts +36 -11
  35. package/dist/codec.d.mts.map +1 -1
  36. package/dist/codec.mjs +27 -4
  37. package/dist/codec.mjs.map +1 -1
  38. package/dist/geminiSpeechCodec.d.mts +0 -1
  39. package/dist/geminiSpeechCodec.d.mts.map +1 -1
  40. package/dist/geminiSpeechCodec.mjs.map +1 -1
  41. package/dist/geminiSpeechCodec.test.d.mts +1 -1
  42. package/dist/geminiSpeechCodec.test.mjs +1 -1
  43. package/dist/geminiSpeechCodec.test.mjs.map +1 -1
  44. package/dist/geminiTools.test.d.mts +1 -1
  45. package/dist/geminiTools.test.mjs +44 -19
  46. package/dist/geminiTools.test.mjs.map +1 -1
  47. package/dist/{magic-string.es-CaWgF-3p.mjs → magic-string.es-CUAnjKYC.mjs} +14 -13
  48. package/dist/{magic-string.es-CaWgF-3p.mjs.map → magic-string.es-CUAnjKYC.mjs.map} +1 -1
  49. package/dist/models.d.mts.map +1 -1
  50. package/dist/{dist-CWoAuwS9.mjs → test.DNmyFkvJ-N3QqMRwZ.mjs} +257 -381
  51. package/dist/test.DNmyFkvJ-N3QqMRwZ.mjs.map +1 -0
  52. package/package.json +5 -5
  53. package/src/Gemini.ts +4 -7
  54. package/src/GeminiEmbedding.ts +36 -48
  55. package/src/GeminiSynthesizer.ts +20 -27
  56. package/src/GeminiTools.ts +4 -6
  57. package/src/GoogleDeepResearch.test.ts +80 -0
  58. package/src/GoogleDeepResearch.ts +127 -37
  59. package/src/LyriaGenerator.ts +19 -23
  60. package/src/codec.ts +98 -12
  61. package/src/geminiTools.test.ts +21 -3
  62. package/dist/dist-CWoAuwS9.mjs.map +0 -1
  63. /package/dist/{chunk-uyGKjUfl.mjs → rolldown-runtime-EAsCxrDo.mjs} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect-uai/google",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "Google Gemini provider for @effect-uai/core.",
5
5
  "keywords": [
6
6
  "ai",
@@ -59,13 +59,13 @@
59
59
  "access": "public"
60
60
  },
61
61
  "devDependencies": {
62
- "effect": "4.0.0-beta.94",
63
- "typescript": "^6.0.3",
64
- "@effect-uai/core": "0.11.0"
62
+ "effect": "4.0.0-rc.111",
63
+ "typescript": "^7.0.2",
64
+ "@effect-uai/core": "0.12.1"
65
65
  },
66
66
  "peerDependencies": {
67
67
  "@effect-uai/core": ">=0.2.0 <1",
68
- "effect": ">=4.0.0-beta.94 <5.0.0"
68
+ "effect": ">=4.0.0-rc.111 <5.0.0"
69
69
  },
70
70
  "scripts": {
71
71
  "build": "tsdown",
package/src/Gemini.ts CHANGED
@@ -330,13 +330,10 @@ export const layer = (
330
330
  const typed = Layer.effect(Gemini, make(cfg))
331
331
  const generic = Layer.effect(
332
332
  LanguageModel,
333
- Effect.map(
334
- make(cfg),
335
- (s): LanguageModelService => ({
336
- streamTurn: (request) => s.streamTurn(request as GeminiRequest),
337
- turn: (request) => s.turn(request as GeminiRequest),
338
- }),
339
- ),
333
+ Effect.map(make(cfg), (s): LanguageModelService => ({
334
+ streamTurn: (request) => s.streamTurn(request as GeminiRequest),
335
+ turn: (request) => s.turn(request as GeminiRequest),
336
+ })),
340
337
  )
341
338
  return Layer.merge(typed, generic)
342
339
  }
@@ -121,21 +121,16 @@ const urlRejected = (param: string): AiError.AiError =>
121
121
  */
122
122
  const imageSourceToPart = (param: string) =>
123
123
  Match.type<ImageSource>().pipe(
124
- Match.tag(
125
- "base64",
126
- (s): Effect.Effect<WirePart, AiError.AiError> =>
127
- Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),
124
+ Match.tag("base64", (s): Effect.Effect<WirePart, AiError.AiError> =>
125
+ Effect.succeed({ inlineData: { mimeType: s.mimeType, data: s.base64 } }),
128
126
  ),
129
- Match.tag(
130
- "bytes",
131
- (s): Effect.Effect<WirePart, AiError.AiError> =>
132
- Effect.succeed({
133
- inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },
134
- }),
127
+ Match.tag("bytes", (s): Effect.Effect<WirePart, AiError.AiError> =>
128
+ Effect.succeed({
129
+ inlineData: { mimeType: s.mimeType, data: Encoding.encodeBase64(s.bytes) },
130
+ }),
135
131
  ),
136
- Match.tag(
137
- "url",
138
- (): Effect.Effect<WirePart, AiError.AiError> => Effect.fail(urlRejected(param)),
132
+ Match.tag("url", (): Effect.Effect<WirePart, AiError.AiError> =>
133
+ Effect.fail(urlRejected(param)),
139
134
  ),
140
135
  Match.exhaustive,
141
136
  )
@@ -291,12 +286,10 @@ const embedImpl =
291
286
  postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:embedContent`, body),
292
287
  ),
293
288
  Effect.flatMap(decodeSingle),
294
- Effect.map(
295
- (decoded): EmbedResponse => ({
296
- embedding: valuesToEmbedding(decoded.embedding.values),
297
- usage: emptyUsage,
298
- }),
299
- ),
289
+ Effect.map((decoded): EmbedResponse => ({
290
+ embedding: valuesToEmbedding(decoded.embedding.values),
291
+ usage: emptyUsage,
292
+ })),
300
293
  )
301
294
 
302
295
  const embedManyImpl =
@@ -309,12 +302,10 @@ const embedManyImpl =
309
302
  postJson(cfg, `${baseUrl(cfg)}/models/${request.model}:batchEmbedContents`, body),
310
303
  ),
311
304
  Effect.flatMap(decodeBatch),
312
- Effect.map(
313
- (decoded): EmbedManyResponse => ({
314
- embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),
315
- usage: emptyUsage,
316
- }),
317
- ),
305
+ Effect.map((decoded): EmbedManyResponse => ({
306
+ embeddings: decoded.embeddings.map((e) => valuesToEmbedding(e.values)),
307
+ usage: emptyUsage,
308
+ })),
318
309
  )
319
310
 
320
311
  // ---------------------------------------------------------------------------
@@ -346,29 +337,26 @@ export const layer = (
346
337
  const typed = Layer.effect(GeminiEmbedding, make(cfg))
347
338
  const generic = Layer.effect(
348
339
  EmbeddingModel,
349
- Effect.map(
350
- make(cfg),
351
- (s): EmbeddingModelService => ({
352
- // Gemini's embed endpoints emit float32 only. Reject a non-float32
353
- // `encoding` (bucket 1) so a float32 vector isn't handed back
354
- // mislabeled as the requested type. `task` is left silent: it's a
355
- // per-model gap (honored by gemini-embedding-001, ignored by
356
- // gemini-embedding-2 server-side), and per capabilities.md §2.3 we
357
- // don't maintain per-model tables; there's no wire error to translate.
358
- embed: <E extends EmbedEncoding | undefined = undefined>(
359
- req: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
360
- ) =>
361
- assertEncoding(req.encoding, ["float32"], "gemini").pipe(
362
- Effect.andThen(s.embed(req as GeminiEmbedRequest)),
363
- ) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,
364
- embedMany: <E extends EmbedEncoding | undefined = undefined>(
365
- req: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
366
- ) =>
367
- assertEncoding(req.encoding, ["float32"], "gemini").pipe(
368
- Effect.andThen(s.embedMany(req as GeminiEmbedManyRequest)),
369
- ) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,
370
- }),
371
- ),
340
+ Effect.map(make(cfg), (s): EmbeddingModelService => ({
341
+ // Gemini's embed endpoints emit float32 only. Reject a non-float32
342
+ // `encoding` (bucket 1) so a float32 vector isn't handed back
343
+ // mislabeled as the requested type. `task` is left silent: it's a
344
+ // per-model gap (honored by gemini-embedding-001, ignored by
345
+ // gemini-embedding-2 server-side), and per capabilities.md §2.3 we
346
+ // don't maintain per-model tables; there's no wire error to translate.
347
+ embed: <E extends EmbedEncoding | undefined = undefined>(
348
+ req: Omit<CommonEmbedRequest, "encoding"> & { readonly encoding?: E },
349
+ ) =>
350
+ assertEncoding(req.encoding, ["float32"], "gemini").pipe(
351
+ Effect.andThen(s.embed(req as GeminiEmbedRequest)),
352
+ ) as Effect.Effect<EmbedResponse<E>, AiError.AiError>,
353
+ embedMany: <E extends EmbedEncoding | undefined = undefined>(
354
+ req: Omit<CommonEmbedManyRequest, "encoding"> & { readonly encoding?: E },
355
+ ) =>
356
+ assertEncoding(req.encoding, ["float32"], "gemini").pipe(
357
+ Effect.andThen(s.embedMany(req as GeminiEmbedManyRequest)),
358
+ ) as Effect.Effect<EmbedManyResponse<E>, AiError.AiError>,
359
+ })),
372
360
  )
373
361
  return Layer.merge(typed, generic)
374
362
  }
@@ -222,21 +222,18 @@ const unsupportedStreamDialogue: SpeechSynthesizerService["streamSynthesizeDialo
222
222
  // ---------------------------------------------------------------------------
223
223
 
224
224
  export const make = (cfg: Config) =>
225
- Effect.map(
226
- HttpClient.HttpClient,
227
- (client): GeminiSynthesizerService => ({
228
- synthesize: (r) =>
229
- synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
230
- streamSynthesis: (r) =>
231
- Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(
232
- Stream.map((b): AudioChunk => ({ bytes: b.bytes })),
233
- Stream.provideService(HttpClient.HttpClient, client),
234
- ),
235
- streamSynthesisFrom: unsupportedStreamFrom,
236
- synthesizeDialogue: unsupportedDialogue,
237
- streamSynthesizeDialogue: unsupportedStreamDialogue,
238
- }),
239
- )
225
+ Effect.map(HttpClient.HttpClient, (client): GeminiSynthesizerService => ({
226
+ synthesize: (r) =>
227
+ synthesizeImpl(cfg)(r).pipe(Effect.provideService(HttpClient.HttpClient, client)),
228
+ streamSynthesis: (r) =>
229
+ Stream.fromEffect(synthesizeImpl(cfg)(r)).pipe(
230
+ Stream.map((b): AudioChunk => ({ bytes: b.bytes })),
231
+ Stream.provideService(HttpClient.HttpClient, client),
232
+ ),
233
+ streamSynthesisFrom: unsupportedStreamFrom,
234
+ synthesizeDialogue: unsupportedDialogue,
235
+ streamSynthesizeDialogue: unsupportedStreamDialogue,
236
+ }))
240
237
 
241
238
  /**
242
239
  * Layer registers both `GeminiSynthesizer` and the generic
@@ -249,17 +246,13 @@ export const layer = (cfg: Config) =>
249
246
  Layer.effect(GeminiSynthesizer, make(cfg)),
250
247
  Layer.effect(
251
248
  SpeechSynthesizer,
252
- Effect.map(
253
- make(cfg),
254
- (s): SpeechSynthesizerService => ({
255
- synthesize: (req: CommonSynthesizeRequest) =>
256
- s.synthesize(req as GeminiSynthesizeRequest),
257
- streamSynthesis: (req: CommonSynthesizeRequest) =>
258
- s.streamSynthesis(req as GeminiSynthesizeRequest),
259
- streamSynthesisFrom: s.streamSynthesisFrom,
260
- synthesizeDialogue: s.synthesizeDialogue,
261
- streamSynthesizeDialogue: s.streamSynthesizeDialogue,
262
- }),
263
- ),
249
+ Effect.map(make(cfg), (s): SpeechSynthesizerService => ({
250
+ synthesize: (req: CommonSynthesizeRequest) => s.synthesize(req as GeminiSynthesizeRequest),
251
+ streamSynthesis: (req: CommonSynthesizeRequest) =>
252
+ s.streamSynthesis(req as GeminiSynthesizeRequest),
253
+ streamSynthesisFrom: s.streamSynthesisFrom,
254
+ synthesizeDialogue: s.synthesizeDialogue,
255
+ streamSynthesizeDialogue: s.streamSynthesizeDialogue,
256
+ })),
264
257
  ),
265
258
  )
@@ -77,12 +77,10 @@ const renderOne = (
77
77
  })
78
78
  : decodeConfig(tool.config).pipe(
79
79
  Result.map(configToWire),
80
- Result.mapError(
81
- (): ToolRenderError => ({
82
- capability: "providerTool",
83
- reason: `unrecognized Gemini provider tool config on "${tool.name}"`,
84
- }),
85
- ),
80
+ Result.mapError((): ToolRenderError => ({
81
+ capability: "providerTool",
82
+ reason: `unrecognized Gemini provider tool config on "${tool.name}"`,
83
+ })),
86
84
  )
87
85
 
88
86
  /**
@@ -0,0 +1,80 @@
1
+ import { Schema } from "effect"
2
+ import * as Turn from "@effect-uai/core/Turn"
3
+ import { describe, expect, it } from "vitest"
4
+ import { WireInteraction, jobStateOf } from "./GoogleDeepResearch.js"
5
+
6
+ const decode = Schema.decodeUnknownSync(WireInteraction)
7
+
8
+ // Extract the `Turn` from a completed interaction the way the service does.
9
+ const turnOf = (raw: unknown): Turn.Turn => {
10
+ const state = jobStateOf(decode(raw))
11
+ if (state._tag !== "Succeeded") throw new Error(`expected Succeeded, got ${state._tag}`)
12
+ return state.result
13
+ }
14
+
15
+ describe("GoogleDeepResearch grounding", () => {
16
+ it("maps grounding chunks on a completed interaction to url_citation annotations", () => {
17
+ const turn = turnOf({
18
+ id: "int_1",
19
+ status: "completed",
20
+ outputs: [
21
+ {
22
+ type: "text",
23
+ text: "Report body [1][2].",
24
+ groundingMetadata: {
25
+ groundingChunks: [
26
+ {
27
+ web: {
28
+ uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/abc",
29
+ title: "example.com",
30
+ },
31
+ },
32
+ {
33
+ web: { uri: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/def" },
34
+ },
35
+ ],
36
+ },
37
+ },
38
+ ],
39
+ })
40
+ expect(Turn.assistantText(turn)).toBe("Report body [1][2].")
41
+ expect(Turn.citations(turn)).toEqual([
42
+ {
43
+ type: "url_citation",
44
+ url: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/abc",
45
+ title: "example.com",
46
+ },
47
+ {
48
+ type: "url_citation",
49
+ url: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/def",
50
+ title: "https://vertexaisearch.cloud.google.com/grounding-api-redirect/def",
51
+ },
52
+ ])
53
+ })
54
+
55
+ it("gathers and de-dupes grounding across interaction / output / step levels", () => {
56
+ const turn = turnOf({
57
+ id: "int_2",
58
+ status: "completed",
59
+ groundingMetadata: {
60
+ groundingChunks: [{ web: { uri: "https://a.example", title: "a" } }],
61
+ },
62
+ outputs: [{ type: "text", text: "Body." }],
63
+ steps: [
64
+ {
65
+ type: "research",
66
+ groundingMetadata: {
67
+ groundingChunks: [
68
+ { web: { uri: "https://a.example", title: "a" } },
69
+ { web: { uri: "https://b.example", title: "b" } },
70
+ ],
71
+ },
72
+ },
73
+ ],
74
+ })
75
+ expect(Turn.citations(turn).map((c) => c.type === "url_citation" && c.url)).toEqual([
76
+ "https://a.example",
77
+ "https://b.example",
78
+ ])
79
+ })
80
+ })
@@ -1,6 +1,20 @@
1
- import { Context, Effect, Layer, Match, Redacted, Ref, Schema, Stream } from "effect"
1
+ import {
2
+ Array as Arr,
3
+ Context,
4
+ Effect,
5
+ Layer,
6
+ Match,
7
+ Option,
8
+ Redacted,
9
+ Ref,
10
+ Result,
11
+ Schema,
12
+ Stream,
13
+ pipe,
14
+ } from "effect"
2
15
  import { HttpClient, HttpClientRequest } from "effect/unstable/http"
3
16
  import * as AiError from "@effect-uai/core/AiError"
17
+ import type { Source } from "@effect-uai/core/Citation"
4
18
  import {
5
19
  DeepResearch,
6
20
  type DeepResearchService,
@@ -13,6 +27,7 @@ import * as Job from "@effect-uai/core/Job"
13
27
  import type { ResearchJobRef, ResearchRequest, ResearchState } from "@effect-uai/core/Research"
14
28
  import * as SSE from "@effect-uai/core/SSE"
15
29
  import { type Turn, TurnEvent } from "@effect-uai/core/Turn"
30
+ import { GroundingMetadata, groundingToAnnotations } from "./codec.js"
16
31
  import type { GoogleDeepResearchModel } from "./models.js"
17
32
 
18
33
  // ---------------------------------------------------------------------------
@@ -61,23 +76,28 @@ const decodeSubmit = Schema.decodeUnknownEffect(WireSubmit)
61
76
 
62
77
  // A completed interaction's text lives at `outputs[].text` (SDK `outputs`) or,
63
78
  // in some doc examples, `steps[].content[].text`. Decode both defensively and
64
- // extract from whichever is populated. Citations are undocumented on this
65
- // preview surface, so none are mapped yet (the report text carries inline
66
- // `[n]` markers); enrich once a live response confirms the shape.
79
+ // extract from whichever is populated. Grounding sources ride Google's
80
+ // `groundingMetadata.groundingChunks[]`; this preview surface is undocumented,
81
+ // so decode it at every plausible level (interaction / output / step) and map
82
+ // whichever is populated onto `url_citation` annotations. The report text also
83
+ // carries the sources as an inline `[n]` / `**Sources:**` list.
67
84
  const WireTextPart = Schema.Struct({
68
85
  type: Schema.optional(Schema.NullOr(Schema.String)),
69
86
  text: Schema.optional(Schema.NullOr(Schema.String)),
87
+ groundingMetadata: Schema.optional(Schema.NullOr(GroundingMetadata)),
70
88
  })
71
89
  const WireStep = Schema.Struct({
72
90
  type: Schema.optional(Schema.NullOr(Schema.String)),
73
91
  text: Schema.optional(Schema.NullOr(Schema.String)),
74
92
  content: Schema.optional(Schema.NullOr(Schema.Array(WireTextPart))),
93
+ groundingMetadata: Schema.optional(Schema.NullOr(GroundingMetadata)),
75
94
  })
76
95
  export const WireInteraction = Schema.Struct({
77
96
  id: Schema.optional(Schema.NullOr(Schema.String)),
78
97
  status: Schema.optional(Schema.NullOr(Schema.String)),
79
98
  outputs: Schema.optional(Schema.NullOr(Schema.Array(WireTextPart))),
80
99
  steps: Schema.optional(Schema.NullOr(Schema.Array(WireStep))),
100
+ groundingMetadata: Schema.optional(Schema.NullOr(GroundingMetadata)),
81
101
  error: Schema.optional(
82
102
  Schema.NullOr(
83
103
  Schema.Struct({
@@ -104,44 +124,111 @@ const reportText = (wire: WireInteraction): string => {
104
124
  return fromStep.length > 0 ? fromStep : (lastStep?.text ?? "")
105
125
  }
106
126
 
107
- const turnFromInteraction = (wire: WireInteraction): Turn => ({
108
- items: [
109
- {
110
- type: "message",
111
- role: "assistant",
112
- content: [{ type: "output_text", text: reportText(wire) }],
113
- providerData: wire,
114
- },
115
- ],
116
- usage: {},
117
- stop_reason: "stop",
127
+ /**
128
+ * The subset of the domain `Citation.Source` this provider can populate.
129
+ * Declared here rather than derived from the wire schema, so the exported
130
+ * shape is ours and a wire change cannot silently alter it.
131
+ */
132
+ const SourceSchema = Schema.Struct({
133
+ url: Schema.String,
134
+ title: Schema.optional(Schema.String),
135
+ })
136
+
137
+ /**
138
+ * The research trace: what the model did at each step and which sources it
139
+ * consulted there. The `Turn` keeps only the final report and the deduped
140
+ * union of all sources, so the per-step breakdown lives here or nowhere.
141
+ *
142
+ * Lands on `providerData.gemini`; read it with {@link researchDataOf}.
143
+ */
144
+ export const GeminiResearchData = Schema.Struct({
145
+ steps: Schema.Array(
146
+ Schema.Struct({
147
+ text: Schema.String,
148
+ sources: Schema.Array(SourceSchema),
149
+ }),
150
+ ),
118
151
  })
152
+ export type GeminiResearchData = typeof GeminiResearchData.Type
153
+
154
+ const decodeResearchData = Schema.decodeUnknownResult(Schema.Struct({ gemini: GeminiResearchData }))
155
+
156
+ /** Read this provider's data off an item, if it is there and ours. */
157
+ export const researchDataOf = (item: Items.HistoryItem): Option.Option<GeminiResearchData> =>
158
+ pipe(
159
+ decodeResearchData(item.providerData),
160
+ Result.match({
161
+ onSuccess: (d) => Option.some(d.gemini),
162
+ onFailure: () => Option.none<GeminiResearchData>(),
163
+ }),
164
+ )
165
+
166
+ const sourcesOf = (meta: GroundingMetadata | null | undefined): ReadonlyArray<Source> =>
167
+ (meta?.groundingChunks ?? []).flatMap((chunk) =>
168
+ chunk.web?.uri == null
169
+ ? []
170
+ : [{ url: chunk.web.uri, ...(chunk.web.title != null && { title: chunk.web.title }) }],
171
+ )
172
+
173
+ // A step contributes to the trace if it said something or consulted something.
174
+ const researchSteps = (wire: WireInteraction): GeminiResearchData["steps"] =>
175
+ (wire.steps ?? []).flatMap((step) => {
176
+ const text = textOfParts(step.content) || (step.text ?? "")
177
+ const sources = [
178
+ ...sourcesOf(step.groundingMetadata),
179
+ ...(step.content ?? []).flatMap((p) => sourcesOf(p.groundingMetadata)),
180
+ ]
181
+ return text.length === 0 && sources.length === 0 ? [] : [{ text, sources }]
182
+ })
183
+
184
+ // Grounding can live at any level of the interaction envelope; gather it from
185
+ // all three (interaction / output part / step) and let `groundingToAnnotations`
186
+ // de-dupe by url.
187
+ const interactionAnnotations = (wire: WireInteraction): ReadonlyArray<Items.Annotation> =>
188
+ groundingToAnnotations(
189
+ wire.groundingMetadata,
190
+ ...Arr.map(wire.outputs ?? [], (o) => o.groundingMetadata),
191
+ ...Arr.map(wire.steps ?? [], (s) => s.groundingMetadata),
192
+ )
193
+
194
+ const turnFromInteraction = (wire: WireInteraction): Turn => {
195
+ const annotations = interactionAnnotations(wire)
196
+ return {
197
+ items: [
198
+ {
199
+ type: "message",
200
+ role: "assistant",
201
+ content: [
202
+ {
203
+ type: "output_text",
204
+ text: reportText(wire),
205
+ ...(annotations.length > 0 && { annotations }),
206
+ },
207
+ ],
208
+ providerData: { gemini: { steps: researchSteps(wire) } },
209
+ },
210
+ ],
211
+ usage: {},
212
+ stop_reason: "stop",
213
+ }
214
+ }
119
215
 
120
216
  // Map the interaction status onto the generic `JobState`.
121
217
  export const jobStateOf = (wire: WireInteraction): ResearchState =>
122
218
  Match.value(wire.status ?? "").pipe(
123
219
  Match.whenOr("queued", "pending", (): ResearchState => ({ _tag: "Pending" })),
124
- Match.whenOr(
125
- "in_progress",
126
- "running",
127
- "processing",
128
- (): ResearchState => ({ _tag: "Running" }),
129
- ),
130
- Match.whenOr(
131
- "completed",
132
- "succeeded",
133
- (): ResearchState => ({ _tag: "Succeeded", result: turnFromInteraction(wire) }),
134
- ),
135
- Match.whenOr(
136
- "failed",
137
- "error",
138
- "cancelled",
139
- (): ResearchState => ({
140
- _tag: "Failed",
141
- ...(wire.error?.message != null && { reason: wire.error.message }),
142
- raw: wire,
143
- }),
144
- ),
220
+ Match.whenOr("in_progress", "running", "processing", (): ResearchState => ({
221
+ _tag: "Running",
222
+ })),
223
+ Match.whenOr("completed", "succeeded", (): ResearchState => ({
224
+ _tag: "Succeeded",
225
+ result: turnFromInteraction(wire),
226
+ })),
227
+ Match.whenOr("failed", "error", "cancelled", (): ResearchState => ({
228
+ _tag: "Failed",
229
+ ...(wire.error?.message != null && { reason: wire.error.message }),
230
+ raw: wire,
231
+ })),
145
232
  Match.orElse((): ResearchState => ({ _tag: "Running" })),
146
233
  )
147
234
 
@@ -226,6 +313,9 @@ const pollJob = (
226
313
  return yield* httpStatusError(response.status, text)
227
314
  }
228
315
  const json = yield* response.json.pipe(Effect.mapError(transportFailure))
316
+ // Preview surface: log the raw interaction so the (undocumented) grounding
317
+ // path can be confirmed against a live completed job at LOG_LEVEL=Debug.
318
+ yield* Effect.logDebug("google interactions poll response", { interaction: json })
229
319
  const wire = yield* decodeInteraction(json).pipe(Effect.mapError(transportFailure))
230
320
  return jobStateOf(wire)
231
321
  })
@@ -284,7 +374,7 @@ const completedTurn = (wire: WireInteraction | null | undefined, streamed: strin
284
374
  type: "message",
285
375
  role: "assistant",
286
376
  content: [{ type: "output_text", text: streamed }],
287
- ...(wire != null && { providerData: wire }),
377
+ ...(wire != null && { providerData: { gemini: { steps: researchSteps(wire) } } }),
288
378
  },
289
379
  ],
290
380
  usage: {},
@@ -105,19 +105,18 @@ const isClipModel = (model: string): boolean => model.includes("clip")
105
105
 
106
106
  export const realizedFormat: (mime: LyriaResponseMimeType) => AudioFormat =
107
107
  Match.type<LyriaResponseMimeType>().pipe(
108
- Match.when(
109
- "audio/mp3",
110
- (): AudioFormat => ({ container: "mp3", encoding: "mp3", sampleRate: 44100, channels: 2 }),
111
- ),
112
- Match.when(
113
- "audio/wav",
114
- (): AudioFormat => ({
115
- container: "wav",
116
- encoding: "pcm_s16le",
117
- sampleRate: 44100,
118
- channels: 2,
119
- }),
120
- ),
108
+ Match.when("audio/mp3", (): AudioFormat => ({
109
+ container: "mp3",
110
+ encoding: "mp3",
111
+ sampleRate: 44100,
112
+ channels: 2,
113
+ })),
114
+ Match.when("audio/wav", (): AudioFormat => ({
115
+ container: "wav",
116
+ encoding: "pcm_s16le",
117
+ sampleRate: 44100,
118
+ channels: 2,
119
+ })),
121
120
  Match.exhaustive,
122
121
  )
123
122
 
@@ -395,15 +394,12 @@ export const layer = (
395
394
  Layer.effect(LyriaGenerator, make(cfg)),
396
395
  Layer.effect(
397
396
  MusicGenerator,
398
- Effect.map(
399
- make(cfg),
400
- (s): MusicGeneratorService => ({
401
- generate: (req: CommonGenerateMusicRequest) =>
402
- s.generate({ ...req, model: req.model as LyriaModel }),
403
- streamGeneration: (req: CommonStreamGenerateMusicRequest) =>
404
- s.streamGeneration({ ...req, model: req.model as LyriaModel }),
405
- streamGenerationFrom: s.streamGenerationFrom,
406
- }),
407
- ),
397
+ Effect.map(make(cfg), (s): MusicGeneratorService => ({
398
+ generate: (req: CommonGenerateMusicRequest) =>
399
+ s.generate({ ...req, model: req.model as LyriaModel }),
400
+ streamGeneration: (req: CommonStreamGenerateMusicRequest) =>
401
+ s.streamGeneration({ ...req, model: req.model as LyriaModel }),
402
+ streamGenerationFrom: s.streamGenerationFrom,
403
+ })),
408
404
  ),
409
405
  )