@yolk-sdk/agent 0.1.0-canary.48 → 0.1.0-canary.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -17,7 +17,7 @@ Canary APIs are unstable. Keep all `@yolk-sdk/*` packages on the same version.
17
17
  ## Subpaths
18
18
 
19
19
  | Subpath | Purpose |
20
- | ----------------------------------------------------- | --------------------------------------------------------------- |
20
+ | ----------------------------------------------------- | -------------------------------------------------------------- |
21
21
  | `@yolk-sdk/agent/protocol` | Wire messages, events, content, usage, tool schemas |
22
22
  | `@yolk-sdk/agent/loop` | Stateless LLM/tool loop |
23
23
  | `@yolk-sdk/agent/loop/testing` | Faux provider and tool executor test helpers |
@@ -107,6 +107,21 @@ const program = run({
107
107
  // Provide LLM provider, loop config, context transformer, and tool executor layers in the host app.
108
108
  ```
109
109
 
110
+ ## Provider configuration
111
+
112
+ Provider output limits are required and host-owned. Yolk does not infer model limits or apply hidden
113
+ fallbacks.
114
+
115
+ | Provider factory | Required output-limit field |
116
+ | ---------------------------------- | --------------------------- |
117
+ | `makeOpenAiProviderLayer` | `maxCompletionTokens` |
118
+ | `makeOpenAiCodexProviderLayer` | `maxOutputTokens` |
119
+ | `makeAnthropicClaudeProviderLayer` | `maxTokens` |
120
+
121
+ The public `toOpenAiRequestBody`, `toOpenAiCodexRequestBody`, and
122
+ `toAnthropicClaudeRequestBody` helpers require the same limit configuration. `OpenAiProviderLayer`
123
+ reads both `OPENAI_API_KEY` and integer `OPENAI_MAX_COMPLETION_TOKENS` through Effect Config.
124
+
110
125
  ## Provider failures and retries
111
126
 
112
127
  Provider adapters classify safe failure metadata at the boundary. The loop owns bounded retry
@@ -166,9 +181,11 @@ const ContextLayer = makeWindowCompactionTransformer({
166
181
  ```
167
182
 
168
183
  The default estimator uses provider-neutral character and media heuristics. Pass
169
- `TokenEstimateOptions.countTextTokens` to the estimation helpers when the host has a synchronous
170
- tokenizer, or pass a whole-transcript `estimateTokens` to planners and transformers. Reuse the same
171
- estimator for warnings, planning, and before/after checks; tokenizer dependencies remain host-owned.
184
+ `TokenEstimateOptions.countTextTokens` to improve estimates for selected message text, reasoning,
185
+ and host tool-call identifiers, or pass a whole-transcript `estimateTokens` to planners and
186
+ transformers. Exact provider-request accounting must also include system prompts, tool definitions,
187
+ vendor framing, and a safety margin. Reuse one estimator for warnings, planning, and before/after
188
+ checks; tokenizer dependencies remain host-owned.
172
189
 
173
190
  ## Protocol content
174
191
 
@@ -351,8 +368,9 @@ handler, approval HITL, transcript projection, and one-shot TTS/STT contracts.
351
368
  after reconnect, optionally prefixing user seeds with author display names via
352
369
  `{ includeAuthors: true }` for multi-user transcripts.
353
370
  - `makeWebSocketVoiceTransport` covers Node/server realtime sessions;
354
- `@yolk-sdk/agent/providers/openai/speech` provides `VoiceSpeechSynthesizer` /
355
- `VoiceTranscriber` layers. `VoiceSpeechRequest.instructions` steers delivery style only, and
371
+ `@yolk-sdk/agent/providers/openai/speech` provides `makeOpenAiSpeechSynthesizerLayer` and
372
+ `makeOpenAiTranscriberLayer` for the provider-neutral voice services.
373
+ `VoiceSpeechRequest.instructions` steers delivery style only, and
356
374
  provider 429s (rate limit or exhausted credits) surface as `VoiceSpeechError` code
357
375
  `rate_limited` so hosts can distinguish quota from outage.
358
376
 
@@ -388,6 +406,7 @@ aborts, and implementation bugs outside typed tool execution.
388
406
  ## Host responsibilities
389
407
 
390
408
  - Choose models/providers and provide an LLM provider layer, using SDK provider subpaths or host adapters.
409
+ - Configure model-specific provider output-token limits.
391
410
  - Persist sessions, transcripts, and append logs.
392
411
  - Persist/return one `ToolResultMessage` for every host tool call, including `isError` failures.
393
412
  - Persist terminal provider failures and clear active run ids where applicable.
@@ -397,11 +416,17 @@ aborts, and implementation bugs outside typed tool execution.
397
416
  ## Boundaries
398
417
 
399
418
  - Core loop/protocol/runtime/tools have no React, Next.js, provider SDKs, auth, storage drivers, or app concepts.
400
- - `@yolk-sdk/agent/compaction` is pure helper scaffolding; hosts own thresholds, summaries, checkpoints, and durable compactor policy.
419
+ - `@yolk-sdk/agent/compaction` combines pure planning/formatting helpers with Effect-native transformer and retry adapters; hosts own thresholds, summaries, compaction payloads, and durable compactor policy.
401
420
  - `@yolk-sdk/agent/react` is headless and uses React as an optional peer.
402
421
  - Provider subpaths own vendor wire/auth mechanics only; hosts own token storage, refresh, routing, and policy.
422
+ - `@yolk-sdk/agent/providers/openai/speech` is server integration requiring runtime
423
+ `FormData`/`Blob`, a host `HttpClient` layer, and a secret API key; do not invoke it from browser
424
+ code.
403
425
  - Loop stays stateless: transcript in, events out.
404
426
  - Runtime owns generic session orchestration only; host apps own persistence adapters and policy.
427
+ - Client HTTP helpers are runtime-portable with a host `HttpClient` layer. Attachment helpers need
428
+ `Blob`/`File` and may use `FileReader`; the Cloudflare WebSocket transport needs the global
429
+ `WebSocket` constructor when its stream runs. None read browser globals at import time.
405
430
  - Tools model generic metadata/execution; host apps own concrete tool catalogs.
406
431
  - `task` is the standard subagent delegation tool. Packages define the schema; host apps execute subagents and omit `task` from subagent toolsets in v1.
407
432
 
@@ -25,8 +25,9 @@ type OpenAiSpeechConfig = {
25
25
  declare const makeOpenAiSpeechSynthesizerLayer: (config: OpenAiSpeechConfig) => Layer.Layer<VoiceSpeechSynthesizer, never, HttpClient.HttpClient>;
26
26
  /**
27
27
  * OpenAI speech-to-text adapter for the provider-neutral `VoiceTranscriber`
28
- * service. Uses `verbose_json` so language/duration/segments survive when the
29
- * model provides them.
28
+ * service. Whisper models use `verbose_json`; GPT-4o transcribe models use
29
+ * plain `json`. Optional language/duration/segments survive only when the
30
+ * selected model returns them.
30
31
  */
31
32
  declare const makeOpenAiTranscriberLayer: (config: OpenAiSpeechConfig) => Layer.Layer<VoiceTranscriber, never, HttpClient.HttpClient>;
32
33
  //#endregion
@@ -1 +1 @@
1
- {"version":3,"file":"speech.d.mts","names":[],"sources":["../../../src/providers/openai/speech.ts"],"mappings":";;;;;KAWY,kBAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAQ;EAAA,SACzB,SAAA;EAAA,SACA,gBAAA;EAAA,SACA,kBAAA;EAAA,SACA,yBAAA;EAAA,SACA,YAAA;EALQ;;;;;EAAA,SAWR,mBAAA;AAAA;;;AAAmB;AAkF9B;;cAAa,gCAAA,GAAoC,MAAA,EAAQ,kBAAA,KAAkB,KAAA,CAAA,KAAA,CAAA,sBAAA,SAAA,UAAA,CAAA,UAAA;;;;;;cA2D9D,0BAAA,GAA8B,MAAA,EAAQ,kBAAA,KAAkB,KAAA,CAAA,KAAA,CAAA,gBAAA,SAAA,UAAA,CAAA,UAAA"}
1
+ {"version":3,"file":"speech.d.mts","names":[],"sources":["../../../src/providers/openai/speech.ts"],"mappings":";;;;;KAWY,kBAAA;EAAA,SACD,MAAA,EAAQ,QAAA,CAAS,QAAQ;EAAA,SACzB,SAAA;EAAA,SACA,gBAAA;EAAA,SACA,kBAAA;EAAA,SACA,yBAAA;EAAA,SACA,YAAA;EALQ;;;;;EAAA,SAWR,mBAAA;AAAA;;;AAAmB;AAkF9B;;cAAa,gCAAA,GAAoC,MAAA,EAAQ,kBAAA,KAAkB,KAAA,CAAA,KAAA,CAAA,sBAAA,SAAA,UAAA,CAAA,UAAA;;;;;;;cA4D9D,0BAAA,GAA8B,MAAA,EAAQ,kBAAA,KAAkB,KAAA,CAAA,KAAA,CAAA,gBAAA,SAAA,UAAA,CAAA,UAAA"}
@@ -89,8 +89,9 @@ const makeOpenAiSpeechSynthesizerLayer = (config) => Layer.effect(VoiceSpeechSyn
89
89
  }));
90
90
  /**
91
91
  * OpenAI speech-to-text adapter for the provider-neutral `VoiceTranscriber`
92
- * service. Uses `verbose_json` so language/duration/segments survive when the
93
- * model provides them.
92
+ * service. Whisper models use `verbose_json`; GPT-4o transcribe models use
93
+ * plain `json`. Optional language/duration/segments survive only when the
94
+ * selected model returns them.
94
95
  */
95
96
  const makeOpenAiTranscriberLayer = (config) => Layer.effect(VoiceTranscriber, Effect.gen(function* () {
96
97
  const client = yield* HttpClient.HttpClient;
@@ -1 +1 @@
1
- {"version":3,"file":"speech.mjs","names":[],"sources":["../../../src/providers/openai/speech.ts"],"sourcesContent":["import { Effect, Layer, Redacted } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport { HttpBody, HttpClient, HttpClientRequest } from 'effect/unstable/http'\nimport {\n VoiceSpeechError,\n VoiceSpeechSynthesizer,\n VoiceTranscriber,\n VoiceTranscriptionResult,\n VoiceTranscriptionSegment\n} from '@yolk-sdk/agent/voice'\n\nexport type OpenAiSpeechConfig = {\n readonly apiKey: Redacted.Redacted<string>\n readonly speechUrl?: string\n readonly transcriptionUrl?: string\n readonly defaultSpeechModel?: string\n readonly defaultTranscriptionModel?: string\n readonly defaultVoice?: string\n /**\n * Default delivery-style instructions for synthesis. Only sent when\n * resolved (request overrides config); `tts-1`/`tts-1-hd` reject\n * instructions, so leave unset when using those models.\n */\n readonly defaultInstructions?: string\n}\n\nconst defaultSpeechUrl = 'https://api.openai.com/v1/audio/speech'\nconst defaultTranscriptionUrl = 'https://api.openai.com/v1/audio/transcriptions'\nconst defaultSpeechModel = 'gpt-4o-mini-tts'\nconst defaultTranscriptionModel = 'gpt-4o-mini-transcribe'\nconst defaultVoice = 'alloy'\n\nconst audioMimeTypes: Readonly<Record<string, string>> = {\n mp3: 'audio/mpeg',\n opus: 'audio/opus',\n aac: 'audio/aac',\n flac: 'audio/flac',\n wav: 'audio/wav',\n pcm: 'audio/pcm'\n}\n\n// OpenAI transcription detects the container format from the uploaded file\n// extension, so the multipart filename must match the audio MIME type.\nconst transcriptionFileExtensions: Readonly<Record<string, string>> = {\n 'audio/mpeg': 'mp3',\n 'audio/mp3': 'mp3',\n 'audio/mp4': 'mp4',\n 'audio/m4a': 'm4a',\n 'audio/x-m4a': 'm4a',\n 'audio/wav': 'wav',\n 'audio/x-wav': 'wav',\n 'audio/webm': 'webm',\n 'audio/ogg': 'ogg',\n 'audio/opus': 'ogg',\n 'audio/flac': 'flac',\n 'audio/mpga': 'mpga'\n}\n\nconst transcriptionFilename = (mimeType: string) => {\n const bareMimeType = mimeType.split(';')[0]?.trim().toLowerCase() ?? mimeType\n const extension = transcriptionFileExtensions[bareMimeType] ?? 'mp3'\n\n return `audio.${extension}`\n}\n\nconst httpFailure = (message: string) => (error: { readonly message: string }) =>\n new VoiceSpeechError({ code: 'provider_error', message: `${message}: ${error.message}` })\n\n// 429 covers both rate limits and exhausted credits (`insufficient_quota`);\n// hosts surface it distinctly so users don't read quota exhaustion as an outage.\nconst statusFailure = (operation: string, status: number) =>\n new VoiceSpeechError({\n code: status === 429 ? 'rate_limited' : 'provider_error',\n message: `OpenAI ${operation} returned ${status}`\n })\n\n// `verbose_json` (language/duration/segments) is only supported by whisper\n// models; gpt-4o transcribe models reject it and support plain `json`.\nconst transcriptionResponseFormat = (model: string) =>\n model.startsWith('whisper') ? 'verbose_json' : 'json'\n\nconst OpenAiTranscriptionResponse = Schema.Struct({\n text: Schema.String,\n language: Schema.optional(Schema.String),\n duration: Schema.optional(Schema.Number),\n segments: Schema.optional(\n Schema.Array(\n Schema.Struct({\n text: Schema.String,\n start: Schema.optional(Schema.Number),\n end: Schema.optional(Schema.Number)\n })\n )\n )\n})\n\nconst decodeTranscriptionResponse = Schema.decodeUnknownEffect(OpenAiTranscriptionResponse)\n\nconst encodeSpeechBody = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString)\n\n/**\n * OpenAI text-to-speech adapter for the provider-neutral\n * `VoiceSpeechSynthesizer` service. Hosts provide the `HttpClient` layer and\n * API key config; no env reads happen in the package.\n */\nexport const makeOpenAiSpeechSynthesizerLayer = (config: OpenAiSpeechConfig) =>\n Layer.effect(\n VoiceSpeechSynthesizer,\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return VoiceSpeechSynthesizer.of({\n synthesize: request =>\n Effect.gen(function* () {\n const outputFormat = request.outputFormat ?? 'mp3'\n const instructions = request.instructions ?? config.defaultInstructions\n const body = yield* encodeSpeechBody({\n model: request.model ?? config.defaultSpeechModel ?? defaultSpeechModel,\n input: request.text,\n voice: request.voice ?? config.defaultVoice ?? defaultVoice,\n response_format: outputFormat,\n ...(instructions === undefined ? {} : { instructions })\n }).pipe(\n Effect.mapError(\n () =>\n new VoiceSpeechError({\n code: 'invalid_request',\n message: 'Could not encode speech request'\n })\n )\n )\n const httpRequest = HttpClientRequest.post(config.speechUrl ?? defaultSpeechUrl).pipe(\n HttpClientRequest.setHeaders({\n authorization: `Bearer ${Redacted.value(config.apiKey)}`,\n 'content-type': 'application/json'\n }),\n HttpClientRequest.bodyText(body, 'application/json')\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(Effect.mapError(httpFailure('OpenAI speech request failed')))\n\n if (response.status < 200 || response.status >= 300) {\n return yield* Effect.fail(statusFailure('speech', response.status))\n }\n\n const audio = yield* response.arrayBuffer.pipe(\n Effect.mapError(httpFailure('Could not read OpenAI speech audio'))\n )\n\n return {\n audio: new Uint8Array(audio),\n mimeType: audioMimeTypes[outputFormat] ?? 'application/octet-stream'\n }\n })\n })\n })\n )\n\n/**\n * OpenAI speech-to-text adapter for the provider-neutral `VoiceTranscriber`\n * service. Uses `verbose_json` so language/duration/segments survive when the\n * model provides them.\n */\nexport const makeOpenAiTranscriberLayer = (config: OpenAiSpeechConfig) =>\n Layer.effect(\n VoiceTranscriber,\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return VoiceTranscriber.of({\n transcribe: request =>\n Effect.gen(function* () {\n const formData = new FormData()\n formData.set(\n 'file',\n new Blob([request.audio.slice().buffer], { type: request.mimeType }),\n transcriptionFilename(request.mimeType)\n )\n const model =\n request.model ?? config.defaultTranscriptionModel ?? defaultTranscriptionModel\n formData.set('model', model)\n formData.set('response_format', transcriptionResponseFormat(model))\n\n if (request.language !== undefined) {\n formData.set('language', request.language)\n }\n\n if (request.prompt !== undefined) {\n formData.set('prompt', request.prompt)\n }\n\n const httpRequest = HttpClientRequest.post(\n config.transcriptionUrl ?? defaultTranscriptionUrl\n ).pipe(\n HttpClientRequest.setHeaders({\n authorization: `Bearer ${Redacted.value(config.apiKey)}`\n }),\n HttpClientRequest.setBody(HttpBody.formData(formData))\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(Effect.mapError(httpFailure('OpenAI transcription request failed')))\n\n if (response.status < 200 || response.status >= 300) {\n return yield* Effect.fail(statusFailure('transcription', response.status))\n }\n\n const payload = yield* response.json.pipe(\n Effect.mapError(httpFailure('Could not read OpenAI transcription response'))\n )\n const decoded = yield* decodeTranscriptionResponse(payload).pipe(\n Effect.mapError(\n () =>\n new VoiceSpeechError({\n code: 'provider_error',\n message: 'OpenAI transcription response had an unexpected shape'\n })\n )\n )\n\n return VoiceTranscriptionResult.make({\n text: decoded.text,\n language: decoded.language,\n durationSeconds: decoded.duration,\n segments: decoded.segments?.map(segment =>\n VoiceTranscriptionSegment.make({\n text: segment.text,\n startSeconds: segment.start,\n endSeconds: segment.end\n })\n )\n })\n })\n })\n })\n )\n"],"mappings":"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"}
1
+ {"version":3,"file":"speech.mjs","names":[],"sources":["../../../src/providers/openai/speech.ts"],"sourcesContent":["import { Effect, Layer, Redacted } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport { HttpBody, HttpClient, HttpClientRequest } from 'effect/unstable/http'\nimport {\n VoiceSpeechError,\n VoiceSpeechSynthesizer,\n VoiceTranscriber,\n VoiceTranscriptionResult,\n VoiceTranscriptionSegment\n} from '@yolk-sdk/agent/voice'\n\nexport type OpenAiSpeechConfig = {\n readonly apiKey: Redacted.Redacted<string>\n readonly speechUrl?: string\n readonly transcriptionUrl?: string\n readonly defaultSpeechModel?: string\n readonly defaultTranscriptionModel?: string\n readonly defaultVoice?: string\n /**\n * Default delivery-style instructions for synthesis. Only sent when\n * resolved (request overrides config); `tts-1`/`tts-1-hd` reject\n * instructions, so leave unset when using those models.\n */\n readonly defaultInstructions?: string\n}\n\nconst defaultSpeechUrl = 'https://api.openai.com/v1/audio/speech'\nconst defaultTranscriptionUrl = 'https://api.openai.com/v1/audio/transcriptions'\nconst defaultSpeechModel = 'gpt-4o-mini-tts'\nconst defaultTranscriptionModel = 'gpt-4o-mini-transcribe'\nconst defaultVoice = 'alloy'\n\nconst audioMimeTypes: Readonly<Record<string, string>> = {\n mp3: 'audio/mpeg',\n opus: 'audio/opus',\n aac: 'audio/aac',\n flac: 'audio/flac',\n wav: 'audio/wav',\n pcm: 'audio/pcm'\n}\n\n// OpenAI transcription detects the container format from the uploaded file\n// extension, so the multipart filename must match the audio MIME type.\nconst transcriptionFileExtensions: Readonly<Record<string, string>> = {\n 'audio/mpeg': 'mp3',\n 'audio/mp3': 'mp3',\n 'audio/mp4': 'mp4',\n 'audio/m4a': 'm4a',\n 'audio/x-m4a': 'm4a',\n 'audio/wav': 'wav',\n 'audio/x-wav': 'wav',\n 'audio/webm': 'webm',\n 'audio/ogg': 'ogg',\n 'audio/opus': 'ogg',\n 'audio/flac': 'flac',\n 'audio/mpga': 'mpga'\n}\n\nconst transcriptionFilename = (mimeType: string) => {\n const bareMimeType = mimeType.split(';')[0]?.trim().toLowerCase() ?? mimeType\n const extension = transcriptionFileExtensions[bareMimeType] ?? 'mp3'\n\n return `audio.${extension}`\n}\n\nconst httpFailure = (message: string) => (error: { readonly message: string }) =>\n new VoiceSpeechError({ code: 'provider_error', message: `${message}: ${error.message}` })\n\n// 429 covers both rate limits and exhausted credits (`insufficient_quota`);\n// hosts surface it distinctly so users don't read quota exhaustion as an outage.\nconst statusFailure = (operation: string, status: number) =>\n new VoiceSpeechError({\n code: status === 429 ? 'rate_limited' : 'provider_error',\n message: `OpenAI ${operation} returned ${status}`\n })\n\n// `verbose_json` (language/duration/segments) is only supported by whisper\n// models; gpt-4o transcribe models reject it and support plain `json`.\nconst transcriptionResponseFormat = (model: string) =>\n model.startsWith('whisper') ? 'verbose_json' : 'json'\n\nconst OpenAiTranscriptionResponse = Schema.Struct({\n text: Schema.String,\n language: Schema.optional(Schema.String),\n duration: Schema.optional(Schema.Number),\n segments: Schema.optional(\n Schema.Array(\n Schema.Struct({\n text: Schema.String,\n start: Schema.optional(Schema.Number),\n end: Schema.optional(Schema.Number)\n })\n )\n )\n})\n\nconst decodeTranscriptionResponse = Schema.decodeUnknownEffect(OpenAiTranscriptionResponse)\n\nconst encodeSpeechBody = Schema.encodeUnknownEffect(Schema.UnknownFromJsonString)\n\n/**\n * OpenAI text-to-speech adapter for the provider-neutral\n * `VoiceSpeechSynthesizer` service. Hosts provide the `HttpClient` layer and\n * API key config; no env reads happen in the package.\n */\nexport const makeOpenAiSpeechSynthesizerLayer = (config: OpenAiSpeechConfig) =>\n Layer.effect(\n VoiceSpeechSynthesizer,\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return VoiceSpeechSynthesizer.of({\n synthesize: request =>\n Effect.gen(function* () {\n const outputFormat = request.outputFormat ?? 'mp3'\n const instructions = request.instructions ?? config.defaultInstructions\n const body = yield* encodeSpeechBody({\n model: request.model ?? config.defaultSpeechModel ?? defaultSpeechModel,\n input: request.text,\n voice: request.voice ?? config.defaultVoice ?? defaultVoice,\n response_format: outputFormat,\n ...(instructions === undefined ? {} : { instructions })\n }).pipe(\n Effect.mapError(\n () =>\n new VoiceSpeechError({\n code: 'invalid_request',\n message: 'Could not encode speech request'\n })\n )\n )\n const httpRequest = HttpClientRequest.post(config.speechUrl ?? defaultSpeechUrl).pipe(\n HttpClientRequest.setHeaders({\n authorization: `Bearer ${Redacted.value(config.apiKey)}`,\n 'content-type': 'application/json'\n }),\n HttpClientRequest.bodyText(body, 'application/json')\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(Effect.mapError(httpFailure('OpenAI speech request failed')))\n\n if (response.status < 200 || response.status >= 300) {\n return yield* Effect.fail(statusFailure('speech', response.status))\n }\n\n const audio = yield* response.arrayBuffer.pipe(\n Effect.mapError(httpFailure('Could not read OpenAI speech audio'))\n )\n\n return {\n audio: new Uint8Array(audio),\n mimeType: audioMimeTypes[outputFormat] ?? 'application/octet-stream'\n }\n })\n })\n })\n )\n\n/**\n * OpenAI speech-to-text adapter for the provider-neutral `VoiceTranscriber`\n * service. Whisper models use `verbose_json`; GPT-4o transcribe models use\n * plain `json`. Optional language/duration/segments survive only when the\n * selected model returns them.\n */\nexport const makeOpenAiTranscriberLayer = (config: OpenAiSpeechConfig) =>\n Layer.effect(\n VoiceTranscriber,\n Effect.gen(function* () {\n const client = yield* HttpClient.HttpClient\n\n return VoiceTranscriber.of({\n transcribe: request =>\n Effect.gen(function* () {\n const formData = new FormData()\n formData.set(\n 'file',\n new Blob([request.audio.slice().buffer], { type: request.mimeType }),\n transcriptionFilename(request.mimeType)\n )\n const model =\n request.model ?? config.defaultTranscriptionModel ?? defaultTranscriptionModel\n formData.set('model', model)\n formData.set('response_format', transcriptionResponseFormat(model))\n\n if (request.language !== undefined) {\n formData.set('language', request.language)\n }\n\n if (request.prompt !== undefined) {\n formData.set('prompt', request.prompt)\n }\n\n const httpRequest = HttpClientRequest.post(\n config.transcriptionUrl ?? defaultTranscriptionUrl\n ).pipe(\n HttpClientRequest.setHeaders({\n authorization: `Bearer ${Redacted.value(config.apiKey)}`\n }),\n HttpClientRequest.setBody(HttpBody.formData(formData))\n )\n const response = yield* client\n .execute(httpRequest)\n .pipe(Effect.mapError(httpFailure('OpenAI transcription request failed')))\n\n if (response.status < 200 || response.status >= 300) {\n return yield* Effect.fail(statusFailure('transcription', response.status))\n }\n\n const payload = yield* response.json.pipe(\n Effect.mapError(httpFailure('Could not read OpenAI transcription response'))\n )\n const decoded = yield* decodeTranscriptionResponse(payload).pipe(\n Effect.mapError(\n () =>\n new VoiceSpeechError({\n code: 'provider_error',\n message: 'OpenAI transcription response had an unexpected shape'\n })\n )\n )\n\n return VoiceTranscriptionResult.make({\n text: decoded.text,\n language: decoded.language,\n durationSeconds: decoded.duration,\n segments: decoded.segments?.map(segment =>\n VoiceTranscriptionSegment.make({\n text: segment.text,\n startSeconds: segment.start,\n endSeconds: segment.end\n })\n )\n })\n })\n })\n })\n )\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yolk-sdk/agent",
3
- "version": "0.1.0-canary.48",
3
+ "version": "0.1.0-canary.49",
4
4
  "description": "Protocol, loop, runtime, client, compaction, tools, React, providers, OAuth, skillset, and voice primitives for building Yolk agents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -28,4 +28,9 @@ Framework-agnostic client transport and state helpers for Yolk agents.
28
28
  - No React.
29
29
  - No UI components.
30
30
  - No auth chrome, provider defaults, or app routes.
31
+ - HTTP helpers are runtime-portable when the host provides an Effect `HttpClient` layer.
32
+ - `documentPartFromTextFile`/`textFromBlob` require `File`/`Blob`; the fallback reader requires
33
+ `FileReader`.
34
+ - Cloudflare WebSocket transport requires the global `WebSocket` constructor when its stream runs.
35
+ - Browser globals are not read at module import time.
31
36
  - Pre-terminal cancellation aborts active response body readers; terminal draining does not.
@@ -159,8 +159,9 @@ export const makeOpenAiSpeechSynthesizerLayer = (config: OpenAiSpeechConfig) =>
159
159
 
160
160
  /**
161
161
  * OpenAI speech-to-text adapter for the provider-neutral `VoiceTranscriber`
162
- * service. Uses `verbose_json` so language/duration/segments survive when the
163
- * model provides them.
162
+ * service. Whisper models use `verbose_json`; GPT-4o transcribe models use
163
+ * plain `json`. Optional language/duration/segments survive only when the
164
+ * selected model returns them.
164
165
  */
165
166
  export const makeOpenAiTranscriberLayer = (config: OpenAiSpeechConfig) =>
166
167
  Layer.effect(