@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 +32 -7
- package/dist/providers/openai/speech.d.mts +3 -2
- package/dist/providers/openai/speech.d.mts.map +1 -1
- package/dist/providers/openai/speech.mjs +3 -2
- package/dist/providers/openai/speech.mjs.map +1 -1
- package/package.json +1 -1
- package/src/client/README.md +5 -0
- package/src/providers/openai/speech.ts +3 -2
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
|
|
170
|
-
|
|
171
|
-
|
|
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 `
|
|
355
|
-
`
|
|
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`
|
|
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.
|
|
29
|
-
*
|
|
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
|
|
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.
|
|
93
|
-
*
|
|
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":";;;;;AA0BA,MAAM,mBAAmB;AACzB,MAAM,0BAA0B;AAChC,MAAM,qBAAqB;AAC3B,MAAM,4BAA4B;AAClC,MAAM,eAAe;AAErB,MAAM,iBAAmD;CACvD,KAAK;CACL,MAAM;CACN,KAAK;CACL,MAAM;CACN,KAAK;CACL,KAAK;AACP;AAIA,MAAM,8BAAgE;CACpE,cAAc;CACd,aAAa;CACb,aAAa;CACb,aAAa;CACb,eAAe;CACf,aAAa;CACb,eAAe;CACf,cAAc;CACd,aAAa;CACb,cAAc;CACd,cAAc;CACd,cAAc;AAChB;AAEA,MAAM,yBAAyB,aAAqB;CAIlD,OAAO,SAFW,4BADG,SAAS,MAAM,GAAG,EAAE,IAAI,KAAK,EAAE,YAAY,KAAK,aACN;AAGjE;AAEA,MAAM,eAAe,aAAqB,UACxC,IAAI,iBAAiB;CAAE,MAAM;CAAkB,SAAS,GAAG,QAAQ,IAAI,MAAM;AAAU,CAAC;AAI1F,MAAM,iBAAiB,WAAmB,WACxC,IAAI,iBAAiB;CACnB,MAAM,WAAW,MAAM,iBAAiB;CACxC,SAAS,UAAU,UAAU,YAAY;AAC3C,CAAC;AAIH,MAAM,+BAA+B,UACnC,MAAM,WAAW,SAAS,IAAI,iBAAiB;AAEjD,MAAM,8BAA8B,OAAO,OAAO;CAChD,MAAM,OAAO;CACb,UAAU,OAAO,SAAS,OAAO,MAAM;CACvC,UAAU,OAAO,SAAS,OAAO,MAAM;CACvC,UAAU,OAAO,SACf,OAAO,MACL,OAAO,OAAO;EACZ,MAAM,OAAO;EACb,OAAO,OAAO,SAAS,OAAO,MAAM;EACpC,KAAK,OAAO,SAAS,OAAO,MAAM;CACpC,CAAC,CACH,CACF;AACF,CAAC;AAED,MAAM,8BAA8B,OAAO,oBAAoB,2BAA2B;AAE1F,MAAM,mBAAmB,OAAO,oBAAoB,OAAO,qBAAqB;;;;;;AAOhF,MAAa,oCAAoC,WAC/C,MAAM,OACJ,wBACA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,WAAW;CAEjC,OAAO,uBAAuB,GAAG,EAC/B,aAAY,YACV,OAAO,IAAI,aAAa;EACtB,MAAM,eAAe,QAAQ,gBAAgB;EAC7C,MAAM,eAAe,QAAQ,gBAAgB,OAAO;EACpD,MAAM,OAAO,OAAO,iBAAiB;GACnC,OAAO,QAAQ,SAAS,OAAO,sBAAsB;GACrD,OAAO,QAAQ;GACf,OAAO,QAAQ,SAAS,OAAO,gBAAgB;GAC/C,iBAAiB;GACjB,GAAI,iBAAiB,KAAA,IAAY,CAAC,IAAI,EAAE,aAAa;EACvD,CAAC,EAAE,KACD,OAAO,eAEH,IAAI,iBAAiB;GACnB,MAAM;GACN,SAAS;EACX,CAAC,CACL,CACF;EACA,MAAM,cAAc,kBAAkB,KAAK,OAAO,aAAa,gBAAgB,EAAE,KAC/E,kBAAkB,WAAW;GAC3B,eAAe,UAAU,SAAS,MAAM,OAAO,MAAM;GACrD,gBAAgB;EAClB,CAAC,GACD,kBAAkB,SAAS,MAAM,kBAAkB,CACrD;EACA,MAAM,WAAW,OAAO,OACrB,QAAQ,WAAW,EACnB,KAAK,OAAO,SAAS,YAAY,8BAA8B,CAAC,CAAC;EAEpE,IAAI,SAAS,SAAS,OAAO,SAAS,UAAU,KAC9C,OAAO,OAAO,OAAO,KAAK,cAAc,UAAU,SAAS,MAAM,CAAC;EAGpE,MAAM,QAAQ,OAAO,SAAS,YAAY,KACxC,OAAO,SAAS,YAAY,oCAAoC,CAAC,CACnE;EAEA,OAAO;GACL,OAAO,IAAI,WAAW,KAAK;GAC3B,UAAU,eAAe,iBAAiB;EAC5C;CACF,CAAC,EACL,CAAC;AACH,CAAC,CACH;;;;;;;AAQF,MAAa,8BAA8B,WACzC,MAAM,OACJ,kBACA,OAAO,IAAI,aAAa;CACtB,MAAM,SAAS,OAAO,WAAW;CAEjC,OAAO,iBAAiB,GAAG,EACzB,aAAY,YACV,OAAO,IAAI,aAAa;EACtB,MAAM,WAAW,IAAI,SAAS;EAC9B,SAAS,IACP,QACA,IAAI,KAAK,CAAC,QAAQ,MAAM,MAAM,EAAE,MAAM,GAAG,EAAE,MAAM,QAAQ,SAAS,CAAC,GACnE,sBAAsB,QAAQ,QAAQ,CACxC;EACA,MAAM,QACJ,QAAQ,SAAS,OAAO,6BAA6B;EACvD,SAAS,IAAI,SAAS,KAAK;EAC3B,SAAS,IAAI,mBAAmB,4BAA4B,KAAK,CAAC;EAElE,IAAI,QAAQ,aAAa,KAAA,GACvB,SAAS,IAAI,YAAY,QAAQ,QAAQ;EAG3C,IAAI,QAAQ,WAAW,KAAA,GACrB,SAAS,IAAI,UAAU,QAAQ,MAAM;EAGvC,MAAM,cAAc,kBAAkB,KACpC,OAAO,oBAAoB,uBAC7B,EAAE,KACA,kBAAkB,WAAW,EAC3B,eAAe,UAAU,SAAS,MAAM,OAAO,MAAM,IACvD,CAAC,GACD,kBAAkB,QAAQ,SAAS,SAAS,QAAQ,CAAC,CACvD;EACA,MAAM,WAAW,OAAO,OACrB,QAAQ,WAAW,EACnB,KAAK,OAAO,SAAS,YAAY,qCAAqC,CAAC,CAAC;EAE3E,IAAI,SAAS,SAAS,OAAO,SAAS,UAAU,KAC9C,OAAO,OAAO,OAAO,KAAK,cAAc,iBAAiB,SAAS,MAAM,CAAC;EAM3E,MAAM,UAAU,OAAO,4BAA4B,OAH5B,SAAS,KAAK,KACnC,OAAO,SAAS,YAAY,8CAA8C,CAAC,CAC7E,CAC0D,EAAE,KAC1D,OAAO,eAEH,IAAI,iBAAiB;GACnB,MAAM;GACN,SAAS;EACX,CAAC,CACL,CACF;EAEA,OAAO,yBAAyB,KAAK;GACnC,MAAM,QAAQ;GACd,UAAU,QAAQ;GAClB,iBAAiB,QAAQ;GACzB,UAAU,QAAQ,UAAU,KAAI,YAC9B,0BAA0B,KAAK;IAC7B,MAAM,QAAQ;IACd,cAAc,QAAQ;IACtB,YAAY,QAAQ;GACtB,CAAC,CACH;EACF,CAAC;CACH,CAAC,EACL,CAAC;AACH,CAAC,CACH"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yolk-sdk/agent",
|
|
3
|
-
"version": "0.1.0-canary.
|
|
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",
|
package/src/client/README.md
CHANGED
|
@@ -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.
|
|
163
|
-
*
|
|
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(
|