@yolk-sdk/agent 0.1.0-canary.35 → 0.1.0-canary.36

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
@@ -322,7 +322,8 @@ handler, approval HITL, transcript projection, and one-shot TTS/STT contracts.
322
322
  back-to-back responses, multi-item responses, and duplicate final transcript event families
323
323
  never concatenate, wipe, or duplicate messages. `sequenceVoiceEvent`/`dedupeStoredVoiceEvents`
324
324
  give replay-safe durable event ids; `voiceSeedTextsFromMessages` seeds new provider sessions
325
- after reconnect.
325
+ after reconnect, optionally prefixing user seeds with author display names via
326
+ `{ includeAuthors: true }` for multi-user transcripts.
326
327
  - `makeWebSocketVoiceTransport` covers Node/server realtime sessions;
327
328
  `@yolk-sdk/agent/providers/openai/speech` provides `VoiceSpeechSynthesizer` /
328
329
  `VoiceTranscriber` layers. `VoiceSpeechRequest.instructions` steers delivery style only, and
@@ -6,7 +6,7 @@ import { VoiceToolBridgeError, VoiceToolCallRequest, VoiceToolExecutionResult, e
6
6
  import { WebSocketVoiceTransportOptions, makeWebSocketVoiceTransport } from "./websocket.mjs";
7
7
  import { VoiceSpeechError, VoiceSpeechErrorCode, VoiceSpeechRequest, VoiceSpeechResult, VoiceSpeechSynthesizer, VoiceTranscriber, VoiceTranscriptionRequest, VoiceTranscriptionResult, VoiceTranscriptionSegment, speechResultToAudioPart } from "./speech.mjs";
8
8
  import { VoiceToolCallDecision, decideVoiceToolCall, handleVoiceToolCall, voiceApprovalRequestId, voiceToolDenialOutput } from "./tool-server.mjs";
9
- import { StoredVoiceEvent, VoiceEventSequencerState, VoiceProjectionResult, VoiceProjectionState, VoiceSeedText, dedupeStoredVoiceEvents, emptyVoiceProjectionState, initialVoiceEventSequencerState, makeVoiceEventId, projectVoiceEvent, protocolToolCallFromVoice, sequenceVoiceEvent, voiceSeedTextsFromMessages } from "./projection.mjs";
9
+ import { StoredVoiceEvent, VoiceEventSequencerState, VoiceProjectionResult, VoiceProjectionState, VoiceSeedText, VoiceSeedTextOptions, dedupeStoredVoiceEvents, emptyVoiceProjectionState, initialVoiceEventSequencerState, makeVoiceEventId, projectVoiceEvent, protocolToolCallFromVoice, sequenceVoiceEvent, voiceSeedTextsFromMessages } from "./projection.mjs";
10
10
  import { VOICE_SESSION_LOG_STATE_VERSION, VoiceAssistantDraftState, VoiceSessionLogFoldResult, VoiceSessionLogState, VoiceToolEventPhase, emptyVoiceSessionLogState, foldStoredVoiceEvents, storedToolEventsFromOutcome, storedVoiceToolEvents, voiceToolEventId } from "./session-log.mjs";
11
11
  import { VoiceEventOutboxApi, VoiceEventOutboxOptions, makeVoiceEventOutbox } from "./outbox.mjs";
12
- export { StoredVoiceEvent, VOICE_SESSION_LOG_STATE_VERSION, VoiceAssistantAudioStarted, VoiceAssistantAudioStopped, VoiceAssistantDraftState, VoiceAssistantTranscriptDelta, VoiceAssistantTranscriptFinal, VoiceAudioInputStarted, VoiceAudioInputStopped, VoiceAwaitingInput, type VoiceClientCodec, VoiceCommand, VoiceConnect, type VoiceControllerApi, type VoiceControllerOptions, VoiceDisconnect, VoiceErrorEvent, VoiceEvent, type VoiceEventOutboxApi, type VoiceEventOutboxOptions, type VoiceEventSequencerState, VoiceInputTranscription, VoiceInterrupted, VoiceNoTurnDetection, type VoiceProjectionResult, type VoiceProjectionState, type VoiceSeedText, VoiceSendText, VoiceServerVadTurnDetection, VoiceSessionClosed, VoiceSessionConfig, VoiceSessionError, VoiceSessionErrorCode, type VoiceSessionLogFoldResult, VoiceSessionLogState, VoiceSessionOpened, VoiceSessionOpening, VoiceSessionToolCallRequest, VoiceSpeechError, VoiceSpeechErrorCode, VoiceSpeechRequest, type VoiceSpeechResult, VoiceSpeechSynthesizer, VoiceStartAudioInput, VoiceStopAudioInput, VoiceSubmitHitlResponse, VoiceSubmitToolOutput, VoiceToolBridgeError, VoiceToolCall, VoiceToolCallApprovalRequiredOutcome, VoiceToolCallCompleted, type VoiceToolCallDecision, VoiceToolCallDeniedOutcome, VoiceToolCallExecutedOutcome, VoiceToolCallExecuting, VoiceToolCallFailed, VoiceToolCallOutcome, VoiceToolCallRequest, VoiceToolCallsRequested, type VoiceToolEventPhase, VoiceToolExecutionResult, VoiceTranscriber, type VoiceTranscriptionRequest, VoiceTranscriptionResult, VoiceTranscriptionSegment, VoiceTransport, type VoiceTransportApi, VoiceTurnDetection, VoiceUserTranscriptDelta, VoiceUserTranscriptFinal, type WebSocketVoiceTransportOptions, decideVoiceToolCall, dedupeStoredVoiceEvents, emptyVoiceProjectionState, emptyVoiceSessionLogState, executeVoiceToolCall, foldStoredVoiceEvents, handleVoiceToolCall, initialVoiceEventSequencerState, makeVoiceController, makeVoiceEventId, makeVoiceEventOutbox, makeWebSocketVoiceTransport, projectVoiceEvent, protocolToolCallFromVoice, sequenceVoiceEvent, speechResultToAudioPart, storedToolEventsFromOutcome, storedVoiceToolEvents, voiceApprovalRequestId, voiceSeedTextsFromMessages, voiceToolDenialOutput, voiceToolEventId };
12
+ export { StoredVoiceEvent, VOICE_SESSION_LOG_STATE_VERSION, VoiceAssistantAudioStarted, VoiceAssistantAudioStopped, VoiceAssistantDraftState, VoiceAssistantTranscriptDelta, VoiceAssistantTranscriptFinal, VoiceAudioInputStarted, VoiceAudioInputStopped, VoiceAwaitingInput, type VoiceClientCodec, VoiceCommand, VoiceConnect, type VoiceControllerApi, type VoiceControllerOptions, VoiceDisconnect, VoiceErrorEvent, VoiceEvent, type VoiceEventOutboxApi, type VoiceEventOutboxOptions, type VoiceEventSequencerState, VoiceInputTranscription, VoiceInterrupted, VoiceNoTurnDetection, type VoiceProjectionResult, type VoiceProjectionState, type VoiceSeedText, type VoiceSeedTextOptions, VoiceSendText, VoiceServerVadTurnDetection, VoiceSessionClosed, VoiceSessionConfig, VoiceSessionError, VoiceSessionErrorCode, type VoiceSessionLogFoldResult, VoiceSessionLogState, VoiceSessionOpened, VoiceSessionOpening, VoiceSessionToolCallRequest, VoiceSpeechError, VoiceSpeechErrorCode, VoiceSpeechRequest, type VoiceSpeechResult, VoiceSpeechSynthesizer, VoiceStartAudioInput, VoiceStopAudioInput, VoiceSubmitHitlResponse, VoiceSubmitToolOutput, VoiceToolBridgeError, VoiceToolCall, VoiceToolCallApprovalRequiredOutcome, VoiceToolCallCompleted, type VoiceToolCallDecision, VoiceToolCallDeniedOutcome, VoiceToolCallExecutedOutcome, VoiceToolCallExecuting, VoiceToolCallFailed, VoiceToolCallOutcome, VoiceToolCallRequest, VoiceToolCallsRequested, type VoiceToolEventPhase, VoiceToolExecutionResult, VoiceTranscriber, type VoiceTranscriptionRequest, VoiceTranscriptionResult, VoiceTranscriptionSegment, VoiceTransport, type VoiceTransportApi, VoiceTurnDetection, VoiceUserTranscriptDelta, VoiceUserTranscriptFinal, type WebSocketVoiceTransportOptions, decideVoiceToolCall, dedupeStoredVoiceEvents, emptyVoiceProjectionState, emptyVoiceSessionLogState, executeVoiceToolCall, foldStoredVoiceEvents, handleVoiceToolCall, initialVoiceEventSequencerState, makeVoiceController, makeVoiceEventId, makeVoiceEventOutbox, makeWebSocketVoiceTransport, projectVoiceEvent, protocolToolCallFromVoice, sequenceVoiceEvent, speechResultToAudioPart, storedToolEventsFromOutcome, storedVoiceToolEvents, voiceApprovalRequestId, voiceSeedTextsFromMessages, voiceToolDenialOutput, voiceToolEventId };
@@ -59,13 +59,23 @@ type VoiceSeedText = {
59
59
  readonly role: 'user' | 'assistant';
60
60
  readonly text: string;
61
61
  };
62
+ type VoiceSeedTextOptions = {
63
+ /**
64
+ * Prefix user seeds with the message author's display name
65
+ * (`"Mike: ..."`). Text runtimes render per-message author envelopes into
66
+ * model-visible context; seeds drop envelopes, so multi-user transcripts
67
+ * lose who said what unless this is enabled. Assistant seeds are never
68
+ * prefixed — the assistant is the speaking agent itself.
69
+ */
70
+ readonly includeAuthors?: boolean;
71
+ };
62
72
  /**
63
73
  * Map a replayed protocol transcript into text seeds for a new provider
64
74
  * session. Tool results are omitted; assistant messages seed their visible
65
75
  * text only. Reconnect never resumes a provider session: hosts replay these
66
76
  * seeds through the controller before the first user turn.
67
77
  */
68
- declare const voiceSeedTextsFromMessages: (messages: ReadonlyArray<AgentMessage>) => ReadonlyArray<VoiceSeedText>;
78
+ declare const voiceSeedTextsFromMessages: (messages: ReadonlyArray<AgentMessage>, options?: VoiceSeedTextOptions) => ReadonlyArray<VoiceSeedText>;
69
79
  declare const StoredVoiceEvent_base: Schema.Class<StoredVoiceEvent, Schema.Struct<{
70
80
  readonly eventId: Schema.String;
71
81
  readonly event: Schema.Union<readonly [typeof VoiceSessionOpening, typeof VoiceSessionOpened, typeof VoiceSessionClosed, typeof VoiceAudioInputStarted, typeof VoiceAudioInputStopped, typeof VoiceUserTranscriptDelta, typeof VoiceUserTranscriptFinal, typeof VoiceAssistantTranscriptDelta, typeof VoiceAssistantTranscriptFinal, typeof VoiceAssistantAudioStarted, typeof VoiceAssistantAudioStopped, typeof VoiceInterrupted, typeof VoiceToolCallsRequested, typeof VoiceToolCallExecuting, typeof VoiceToolCallCompleted, typeof VoiceToolCallFailed, typeof VoiceAwaitingInput, typeof VoiceErrorEvent]>;
@@ -88,5 +98,5 @@ declare const sequenceVoiceEvent: (streamId: string, state: VoiceEventSequencerS
88
98
  /** De-dupe replayed stored voice events by `eventId`. */
89
99
  declare const dedupeStoredVoiceEvents: (events: ReadonlyArray<StoredVoiceEvent>) => ReadonlyArray<StoredVoiceEvent>;
90
100
  //#endregion
91
- export { StoredVoiceEvent, VoiceAssistantDraft, VoiceEventSequencerState, VoiceProjectionResult, VoiceProjectionState, VoiceSeedText, dedupeStoredVoiceEvents, emptyVoiceProjectionState, initialVoiceEventSequencerState, makeVoiceEventId, projectVoiceEvent, protocolToolCallFromVoice, sequenceVoiceEvent, voiceSeedTextsFromMessages };
101
+ export { StoredVoiceEvent, VoiceAssistantDraft, VoiceEventSequencerState, VoiceProjectionResult, VoiceProjectionState, VoiceSeedText, VoiceSeedTextOptions, dedupeStoredVoiceEvents, emptyVoiceProjectionState, initialVoiceEventSequencerState, makeVoiceEventId, projectVoiceEvent, protocolToolCallFromVoice, sequenceVoiceEvent, voiceSeedTextsFromMessages };
92
102
  //# sourceMappingURL=projection.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"projection.d.mts","names":[],"sources":["../../src/voice/projection.ts"],"mappings":";;;;;KAeY,mBAAA;+EAED,GAAA;EAAA,SACA,IAAI;AAAA;;AAAA;AAaf;;;;;;;;KAAY,oBAAA;EAYgB,uEAVjB,eAAA,EAAiB,aAAA,CAAc,mBAAA;EAUD;;;;;EAAA,SAJ9B,aAAA,EAAe,aAAA,UAEf;EAAA,SAAA,aAAA,EAAe,aAAA,CAAc,QAAA,GAAA;EAAA,SAE7B,eAAA,EAAiB,aAAA,CAAc,iBAAA;AAAA;AAAA,KAG9B,qBAAA;EAAA,SACD,KAAA,EAAO,oBAAA,EAJyC;EAAA,SAMhD,QAAA,EAAU,aAAA,CAAc,YAAA;AAAA;AAAA,cAGtB,yBAAA,EAA2B,oBAKvC;AAAA,cAkBY,yBAAA,GAA6B,IAAA,EAAM,aAAA,KAAgB,QAK5D;;;;;;;;;;;AA/B2C;AAG/C;;;;AAKC;AAkBD;;;;cAoIa,iBAAA,GACX,KAAA,EAAO,oBAAA,EACP,KAAA,EAAO,UAAA,KACN,qBAAA;AAAA,KAoES,aAAA;EAAA,SACD,IAAA;EAAA,SACA,IAAI;AAAA;AAzEf;;;;;;AAAA,cAkFa,0BAAA,GACX,QAAA,EAAU,aAAA,CAAc,YAAA,MACvB,aAAA,CAAc,aAAA;AAAA,cAgBb,qBAAA;;;;;;;AAjCH;AAID;cAsCa,gBAAA,SAAyB,qBAGpC;AAAA,cAEW,gBAAA,GAAoB,QAAA,UAAkB,QAAgB;AAAA,KAEvD,wBAAA;EAAA,SACD,YAAY;AAAA;AAAA,cAGV,+BAAA,EAAiC,wBAA8C;AAAA,cAE/E,kBAAA,GACX,QAAA,UACA,KAAA,EAAO,wBAAA,EACP,KAAA,EAAO,UAAA;EAAA,SACK,MAAA,EAAQ,gBAAA;EAAA,SAA2B,KAAA,EAAO,wBAAA;AAAA;;cAS3C,uBAAA,GACX,MAAA,EAAQ,aAAA,CAAc,gBAAA,MACrB,aAAA,CAAc,gBAAA"}
1
+ {"version":3,"file":"projection.d.mts","names":[],"sources":["../../src/voice/projection.ts"],"mappings":";;;;;KAeY,mBAAA;+EAED,GAAA;EAAA,SACA,IAAI;AAAA;;AAAA;AAaf;;;;;;;;KAAY,oBAAA;EAYgB,uEAVjB,eAAA,EAAiB,aAAA,CAAc,mBAAA;EAUD;;;;;EAAA,SAJ9B,aAAA,EAAe,aAAA,UAEf;EAAA,SAAA,aAAA,EAAe,aAAA,CAAc,QAAA,GAAA;EAAA,SAE7B,eAAA,EAAiB,aAAA,CAAc,iBAAA;AAAA;AAAA,KAG9B,qBAAA;EAAA,SACD,KAAA,EAAO,oBAAA,EAJyC;EAAA,SAMhD,QAAA,EAAU,aAAA,CAAc,YAAA;AAAA;AAAA,cAGtB,yBAAA,EAA2B,oBAKvC;AAAA,cAkBY,yBAAA,GAA6B,IAAA,EAAM,aAAA,KAAgB,QAK5D;;;;;;;;;;;AA/B2C;AAG/C;;;;AAKC;AAkBD;;;;cAoIa,iBAAA,GACX,KAAA,EAAO,oBAAA,EACP,KAAA,EAAO,UAAA,KACN,qBAAA;AAAA,KAoES,aAAA;EAAA,SACD,IAAA;EAAA,SACA,IAAI;AAAA;AAAA,KAGH,oBAAA;EATX;;;;;;;EAAA,SAiBU,cAAc;AAAA;;;;;AAjBxB;AAID;cAuCa,0BAAA,GACX,QAAA,EAAU,aAAA,CAAc,YAAA,GACxB,OAAA,GAAS,oBAAA,KACR,aAAA,CAAc,aAAA;AAAA,cAgBb,qBAAA;;;;;;;AA7CqB;AA0BzB;cA4Ba,gBAAA,SAAyB,qBAGpC;AAAA,cAEW,gBAAA,GAAoB,QAAA,UAAkB,QAAgB;AAAA,KAEvD,wBAAA;EAAA,SACD,YAAY;AAAA;AAAA,cAGV,+BAAA,EAAiC,wBAA8C;AAAA,cAE/E,kBAAA,GACX,QAAA,UACA,KAAA,EAAO,wBAAA,EACP,KAAA,EAAO,UAAA;EAAA,SACK,MAAA,EAAQ,gBAAA;EAAA,SAA2B,KAAA,EAAO,wBAAA;AAAA;;cAS3C,uBAAA,GACX,MAAA,EAAQ,aAAA,CAAc,gBAAA,MACrB,aAAA,CAAc,gBAAA"}
@@ -159,16 +159,22 @@ const projectVoiceEvent = (state, event) => {
159
159
  };
160
160
  }
161
161
  };
162
+ const userSeedText = (message, options) => {
163
+ const text = contentPreview(message.content);
164
+ if (text.length === 0) return text;
165
+ const displayName = message.author?.displayName?.trim();
166
+ return options.includeAuthors === true && displayName !== void 0 && displayName.length > 0 ? `${displayName}: ${text}` : text;
167
+ };
162
168
  /**
163
169
  * Map a replayed protocol transcript into text seeds for a new provider
164
170
  * session. Tool results are omitted; assistant messages seed their visible
165
171
  * text only. Reconnect never resumes a provider session: hosts replay these
166
172
  * seeds through the controller before the first user turn.
167
173
  */
168
- const voiceSeedTextsFromMessages = (messages) => messages.flatMap((message) => {
174
+ const voiceSeedTextsFromMessages = (messages, options = {}) => messages.flatMap((message) => {
169
175
  switch (message._tag) {
170
176
  case "User": {
171
- const text = contentPreview(message.content);
177
+ const text = userSeedText(message, options);
172
178
  return text.length === 0 ? [] : [{
173
179
  role: "user",
174
180
  text
@@ -1 +1 @@
1
- {"version":3,"file":"projection.mjs","names":[],"sources":["../../src/voice/projection.ts"],"sourcesContent":["import { Option } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n AssistantAgentMessage,\n AssistantTextPart,\n assistantContent,\n contentPreview,\n HostToolCallPart,\n ToolCall,\n ToolResultMessage,\n UserMessage,\n type AgentMessage\n} from '@yolk-sdk/agent/protocol'\nimport { VoiceEvent, type VoiceToolCall } from './protocol.ts'\n\nexport type VoiceAssistantDraft = {\n /** Segment key: provider item id when present, else response id. */\n readonly key: string | null\n readonly text: string\n}\n\n/**\n * Pure projection state for turning a voice event stream into protocol\n * messages. Assistant text accumulates as per-segment drafts — keyed by\n * provider item id (finals fire per output item, and one response can carry\n * several items), falling back to response id — so overlapping or\n * back-to-back segments never concatenate into or wipe one another; tool\n * call/result pairs accumulate per turn and flush together with the\n * assistant transcript so projected transcripts never contain dangling host\n * tool calls.\n */\nexport type VoiceProjectionState = {\n /** Open assistant drafts by segment key, in arrival order. */\n readonly assistantDrafts: ReadonlyArray<VoiceAssistantDraft>\n /**\n * Segment keys already flushed. Providers can emit multiple final\n * transcript event families (legacy + current names) for one segment;\n * replays of a finalized key project no messages.\n */\n readonly finalizedKeys: ReadonlyArray<string>\n /** Tool calls of the current turn, in provider order. */\n readonly turnToolCalls: ReadonlyArray<ToolCall>\n /** One result message per settled tool call of the current turn. */\n readonly turnToolResults: ReadonlyArray<ToolResultMessage>\n}\n\nexport type VoiceProjectionResult = {\n readonly state: VoiceProjectionState\n /** Durable messages produced by this event, in append order. */\n readonly messages: ReadonlyArray<AgentMessage>\n}\n\nexport const emptyVoiceProjectionState: VoiceProjectionState = {\n assistantDrafts: [],\n finalizedKeys: [],\n turnToolCalls: [],\n turnToolResults: []\n}\n\n/**\n * Providers emit transcript finals per output item (`itemId`), and one\n * response can carry several items; keying by item id keeps a mid-response\n * item final from wiping later items of the same response. Deltas/finals of\n * the same segment must carry the same ids for keys to match.\n */\nconst segmentKey = (event: {\n readonly itemId: string | null\n readonly responseId: string | null\n}): string | null => event.itemId ?? event.responseId\n\nconst decodeParamsOption = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)\n\nconst toolCallParams = (argumentsJson: string): unknown =>\n Option.getOrElse(decodeParamsOption(argumentsJson), () => argumentsJson)\n\nexport const protocolToolCallFromVoice = (call: VoiceToolCall): ToolCall =>\n ToolCall.make({\n id: call.callId,\n name: call.name,\n params: toolCallParams(call.argumentsJson)\n })\n\nconst settledToolMessages = (state: VoiceProjectionState): ReadonlyArray<AgentMessage> => {\n const settledIds = new Set(state.turnToolResults.map(result => result.toolCallId))\n const settledCalls = state.turnToolCalls.filter(call => settledIds.has(call.id))\n\n if (settledCalls.length === 0) {\n return []\n }\n\n const orderedResults = settledCalls.flatMap(call => {\n const result = state.turnToolResults.find(entry => entry.toolCallId === call.id)\n\n return result === undefined ? [] : [result]\n })\n\n return [\n AssistantAgentMessage.make({\n parts: [\n AssistantTextPart.make({ content: '' }),\n ...settledCalls.map(call => HostToolCallPart.make({ call }))\n ]\n }),\n ...orderedResults\n ]\n}\n\nconst assistantTextMessages = (texts: ReadonlyArray<string>): ReadonlyArray<AgentMessage> =>\n texts.flatMap(text =>\n text.trim().length === 0\n ? []\n : [AssistantAgentMessage.make({ parts: [AssistantTextPart.make({ content: text })] })]\n )\n\nconst markFinalized = (\n finalizedKeys: ReadonlyArray<string>,\n drafts: ReadonlyArray<VoiceAssistantDraft>\n): ReadonlyArray<string> => [\n ...finalizedKeys,\n ...drafts.flatMap(draft => (draft.key === null ? [] : [draft.key]))\n]\n\nconst isFinalized = (state: VoiceProjectionState, key: string | null) =>\n key !== null && state.finalizedKeys.includes(key)\n\n/** Flush every open draft plus settled tool pairs; ends the turn. */\nconst flushAllDrafts = (state: VoiceProjectionState): VoiceProjectionResult => ({\n state: {\n ...emptyVoiceProjectionState,\n finalizedKeys: markFinalized(state.finalizedKeys, state.assistantDrafts)\n },\n messages: [\n ...settledToolMessages(state),\n ...assistantTextMessages(state.assistantDrafts.map(draft => draft.text))\n ]\n})\n\n/** Flush one segment's transcript plus settled tool pairs; keeps other drafts open. */\nconst flushSegment = (\n state: VoiceProjectionState,\n key: string | null,\n transcript: string | null\n): VoiceProjectionResult => {\n const draft = state.assistantDrafts.find(entry => entry.key === key)\n const remainingDrafts = state.assistantDrafts.filter(entry => entry.key !== key)\n // Truncated/interrupted segments can arrive as finals with an empty\n // transcript; never lose text the user already heard streaming.\n const text =\n transcript === null || transcript.trim().length === 0\n ? (draft?.text ?? transcript ?? '')\n : transcript\n\n return {\n state: {\n assistantDrafts: remainingDrafts,\n finalizedKeys: key === null ? state.finalizedKeys : [...state.finalizedKeys, key],\n turnToolCalls: [],\n turnToolResults: []\n },\n messages: [...settledToolMessages(state), ...assistantTextMessages([text])]\n }\n}\n\n/**\n * Deltas for a new segment key close every other open draft first: the\n * previous segment is complete from the transcript's perspective even if its\n * final event has not arrived (or never arrives). Flushed keys are marked\n * finalized so their late final events project nothing.\n */\nconst appendDraftDelta = (\n state: VoiceProjectionState,\n key: string | null,\n delta: string\n): VoiceProjectionResult => {\n const otherDrafts = state.assistantDrafts.filter(entry => entry.key !== key)\n const currentDraft = state.assistantDrafts.find(entry => entry.key === key)\n\n return {\n state: {\n ...state,\n assistantDrafts: [{ key, text: `${currentDraft?.text ?? ''}${delta}` }],\n finalizedKeys: markFinalized(state.finalizedKeys, otherDrafts)\n },\n messages: assistantTextMessages(otherDrafts.map(draft => draft.text))\n }\n}\n\n/**\n * Project one voice event into durable protocol messages.\n *\n * - `UserTranscriptFinal` becomes a `UserMessage`.\n * - `AssistantTranscriptDelta` accumulates per segment key (item id, falling\n * back to response id); a delta for a new segment flushes the previous\n * segment's draft first so back-to-back segments never concatenate into\n * one message.\n * - `AssistantTranscriptFinal` flushes its segment: settled tool call/result\n * pairs first, then the assistant text message. Finals for already-flushed\n * segments project nothing (providers can emit duplicate final event\n * families for one segment).\n * - `Interrupted` and `SessionClosed` flush all partial drafts so nothing is\n * lost and no dangling host tool calls are produced.\n * - Other lifecycle events only update state.\n *\n * Unsettled tool calls (for example approvals still pending when the session\n * ends) are intentionally dropped from the durable transcript; pending HITL\n * requests live in the host session log, not the message transcript.\n */\nexport const projectVoiceEvent = (\n state: VoiceProjectionState,\n event: VoiceEvent\n): VoiceProjectionResult => {\n switch (event._tag) {\n case 'UserTranscriptFinal':\n return { state, messages: [UserMessage.make({ content: event.text })] }\n case 'AssistantTranscriptDelta':\n return isFinalized(state, segmentKey(event))\n ? { state, messages: [] }\n : appendDraftDelta(state, segmentKey(event), event.delta)\n case 'AssistantTranscriptFinal':\n return isFinalized(state, segmentKey(event))\n ? { state, messages: [] }\n : flushSegment(state, segmentKey(event), event.text)\n case 'Interrupted':\n case 'SessionClosed':\n return flushAllDrafts(state)\n case 'ToolCallsRequested':\n return {\n state: {\n ...state,\n turnToolCalls: [\n ...state.turnToolCalls,\n ...event.calls.map(call => protocolToolCallFromVoice(call))\n ]\n },\n messages: []\n }\n case 'ToolCallCompleted':\n return {\n state: {\n ...state,\n turnToolResults: [\n ...state.turnToolResults,\n ToolResultMessage.make({ toolCallId: event.callId, content: event.output })\n ]\n },\n messages: []\n }\n case 'ToolCallFailed':\n return {\n state: {\n ...state,\n turnToolResults: [\n ...state.turnToolResults,\n ToolResultMessage.make({\n toolCallId: event.callId,\n content: event.message,\n isError: true\n })\n ]\n },\n messages: []\n }\n case 'SessionOpening':\n case 'SessionOpened':\n case 'AudioInputStarted':\n case 'AudioInputStopped':\n case 'UserTranscriptDelta':\n case 'AssistantAudioStarted':\n case 'AssistantAudioStopped':\n case 'ToolCallExecuting':\n case 'AwaitingInput':\n case 'Error':\n return { state, messages: [] }\n }\n}\n\n// --- Reconnect seeding -------------------------------------------------------\n\nexport type VoiceSeedText = {\n readonly role: 'user' | 'assistant'\n readonly text: string\n}\n\n/**\n * Map a replayed protocol transcript into text seeds for a new provider\n * session. Tool results are omitted; assistant messages seed their visible\n * text only. Reconnect never resumes a provider session: hosts replay these\n * seeds through the controller before the first user turn.\n */\nexport const voiceSeedTextsFromMessages = (\n messages: ReadonlyArray<AgentMessage>\n): ReadonlyArray<VoiceSeedText> =>\n messages.flatMap((message): ReadonlyArray<VoiceSeedText> => {\n switch (message._tag) {\n case 'User': {\n const text = contentPreview(message.content)\n\n return text.length === 0 ? [] : [{ role: 'user' as const, text }]\n }\n case 'Assistant': {\n const text = contentPreview(assistantContent(message))\n\n return text.length === 0 ? [] : [{ role: 'assistant' as const, text }]\n }\n case 'ToolResult':\n return []\n }\n })\n\n// --- Durable event ids -------------------------------------------------------\n\n/**\n * Replay-safe durable envelope for voice events. Hosts append these to their\n * session log; clients de-dupe replays by `eventId`, matching text runtime\n * durable stream rules.\n */\nexport class StoredVoiceEvent extends Schema.Class<StoredVoiceEvent>('StoredVoiceEvent')({\n eventId: Schema.String,\n event: VoiceEvent\n}) {}\n\nexport const makeVoiceEventId = (streamId: string, sequence: number) => `${streamId}:${sequence}`\n\nexport type VoiceEventSequencerState = {\n readonly nextSequence: number\n}\n\nexport const initialVoiceEventSequencerState: VoiceEventSequencerState = { nextSequence: 0 }\n\nexport const sequenceVoiceEvent = (\n streamId: string,\n state: VoiceEventSequencerState,\n event: VoiceEvent\n): { readonly stored: StoredVoiceEvent; readonly state: VoiceEventSequencerState } => ({\n stored: StoredVoiceEvent.make({\n eventId: makeVoiceEventId(streamId, state.nextSequence),\n event\n }),\n state: { nextSequence: state.nextSequence + 1 }\n})\n\n/** De-dupe replayed stored voice events by `eventId`. */\nexport const dedupeStoredVoiceEvents = (\n events: ReadonlyArray<StoredVoiceEvent>\n): ReadonlyArray<StoredVoiceEvent> => {\n const seen = new Set<string>()\n\n return events.filter(stored => {\n if (seen.has(stored.eventId)) {\n return false\n }\n\n seen.add(stored.eventId)\n\n return true\n })\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"projection.mjs","names":[],"sources":["../../src/voice/projection.ts"],"sourcesContent":["import { Option } from 'effect'\nimport * as Schema from 'effect/Schema'\nimport {\n AssistantAgentMessage,\n AssistantTextPart,\n assistantContent,\n contentPreview,\n HostToolCallPart,\n ToolCall,\n ToolResultMessage,\n UserMessage,\n type AgentMessage\n} from '@yolk-sdk/agent/protocol'\nimport { VoiceEvent, type VoiceToolCall } from './protocol.ts'\n\nexport type VoiceAssistantDraft = {\n /** Segment key: provider item id when present, else response id. */\n readonly key: string | null\n readonly text: string\n}\n\n/**\n * Pure projection state for turning a voice event stream into protocol\n * messages. Assistant text accumulates as per-segment drafts — keyed by\n * provider item id (finals fire per output item, and one response can carry\n * several items), falling back to response id — so overlapping or\n * back-to-back segments never concatenate into or wipe one another; tool\n * call/result pairs accumulate per turn and flush together with the\n * assistant transcript so projected transcripts never contain dangling host\n * tool calls.\n */\nexport type VoiceProjectionState = {\n /** Open assistant drafts by segment key, in arrival order. */\n readonly assistantDrafts: ReadonlyArray<VoiceAssistantDraft>\n /**\n * Segment keys already flushed. Providers can emit multiple final\n * transcript event families (legacy + current names) for one segment;\n * replays of a finalized key project no messages.\n */\n readonly finalizedKeys: ReadonlyArray<string>\n /** Tool calls of the current turn, in provider order. */\n readonly turnToolCalls: ReadonlyArray<ToolCall>\n /** One result message per settled tool call of the current turn. */\n readonly turnToolResults: ReadonlyArray<ToolResultMessage>\n}\n\nexport type VoiceProjectionResult = {\n readonly state: VoiceProjectionState\n /** Durable messages produced by this event, in append order. */\n readonly messages: ReadonlyArray<AgentMessage>\n}\n\nexport const emptyVoiceProjectionState: VoiceProjectionState = {\n assistantDrafts: [],\n finalizedKeys: [],\n turnToolCalls: [],\n turnToolResults: []\n}\n\n/**\n * Providers emit transcript finals per output item (`itemId`), and one\n * response can carry several items; keying by item id keeps a mid-response\n * item final from wiping later items of the same response. Deltas/finals of\n * the same segment must carry the same ids for keys to match.\n */\nconst segmentKey = (event: {\n readonly itemId: string | null\n readonly responseId: string | null\n}): string | null => event.itemId ?? event.responseId\n\nconst decodeParamsOption = Schema.decodeUnknownOption(Schema.UnknownFromJsonString)\n\nconst toolCallParams = (argumentsJson: string): unknown =>\n Option.getOrElse(decodeParamsOption(argumentsJson), () => argumentsJson)\n\nexport const protocolToolCallFromVoice = (call: VoiceToolCall): ToolCall =>\n ToolCall.make({\n id: call.callId,\n name: call.name,\n params: toolCallParams(call.argumentsJson)\n })\n\nconst settledToolMessages = (state: VoiceProjectionState): ReadonlyArray<AgentMessage> => {\n const settledIds = new Set(state.turnToolResults.map(result => result.toolCallId))\n const settledCalls = state.turnToolCalls.filter(call => settledIds.has(call.id))\n\n if (settledCalls.length === 0) {\n return []\n }\n\n const orderedResults = settledCalls.flatMap(call => {\n const result = state.turnToolResults.find(entry => entry.toolCallId === call.id)\n\n return result === undefined ? [] : [result]\n })\n\n return [\n AssistantAgentMessage.make({\n parts: [\n AssistantTextPart.make({ content: '' }),\n ...settledCalls.map(call => HostToolCallPart.make({ call }))\n ]\n }),\n ...orderedResults\n ]\n}\n\nconst assistantTextMessages = (texts: ReadonlyArray<string>): ReadonlyArray<AgentMessage> =>\n texts.flatMap(text =>\n text.trim().length === 0\n ? []\n : [AssistantAgentMessage.make({ parts: [AssistantTextPart.make({ content: text })] })]\n )\n\nconst markFinalized = (\n finalizedKeys: ReadonlyArray<string>,\n drafts: ReadonlyArray<VoiceAssistantDraft>\n): ReadonlyArray<string> => [\n ...finalizedKeys,\n ...drafts.flatMap(draft => (draft.key === null ? [] : [draft.key]))\n]\n\nconst isFinalized = (state: VoiceProjectionState, key: string | null) =>\n key !== null && state.finalizedKeys.includes(key)\n\n/** Flush every open draft plus settled tool pairs; ends the turn. */\nconst flushAllDrafts = (state: VoiceProjectionState): VoiceProjectionResult => ({\n state: {\n ...emptyVoiceProjectionState,\n finalizedKeys: markFinalized(state.finalizedKeys, state.assistantDrafts)\n },\n messages: [\n ...settledToolMessages(state),\n ...assistantTextMessages(state.assistantDrafts.map(draft => draft.text))\n ]\n})\n\n/** Flush one segment's transcript plus settled tool pairs; keeps other drafts open. */\nconst flushSegment = (\n state: VoiceProjectionState,\n key: string | null,\n transcript: string | null\n): VoiceProjectionResult => {\n const draft = state.assistantDrafts.find(entry => entry.key === key)\n const remainingDrafts = state.assistantDrafts.filter(entry => entry.key !== key)\n // Truncated/interrupted segments can arrive as finals with an empty\n // transcript; never lose text the user already heard streaming.\n const text =\n transcript === null || transcript.trim().length === 0\n ? (draft?.text ?? transcript ?? '')\n : transcript\n\n return {\n state: {\n assistantDrafts: remainingDrafts,\n finalizedKeys: key === null ? state.finalizedKeys : [...state.finalizedKeys, key],\n turnToolCalls: [],\n turnToolResults: []\n },\n messages: [...settledToolMessages(state), ...assistantTextMessages([text])]\n }\n}\n\n/**\n * Deltas for a new segment key close every other open draft first: the\n * previous segment is complete from the transcript's perspective even if its\n * final event has not arrived (or never arrives). Flushed keys are marked\n * finalized so their late final events project nothing.\n */\nconst appendDraftDelta = (\n state: VoiceProjectionState,\n key: string | null,\n delta: string\n): VoiceProjectionResult => {\n const otherDrafts = state.assistantDrafts.filter(entry => entry.key !== key)\n const currentDraft = state.assistantDrafts.find(entry => entry.key === key)\n\n return {\n state: {\n ...state,\n assistantDrafts: [{ key, text: `${currentDraft?.text ?? ''}${delta}` }],\n finalizedKeys: markFinalized(state.finalizedKeys, otherDrafts)\n },\n messages: assistantTextMessages(otherDrafts.map(draft => draft.text))\n }\n}\n\n/**\n * Project one voice event into durable protocol messages.\n *\n * - `UserTranscriptFinal` becomes a `UserMessage`.\n * - `AssistantTranscriptDelta` accumulates per segment key (item id, falling\n * back to response id); a delta for a new segment flushes the previous\n * segment's draft first so back-to-back segments never concatenate into\n * one message.\n * - `AssistantTranscriptFinal` flushes its segment: settled tool call/result\n * pairs first, then the assistant text message. Finals for already-flushed\n * segments project nothing (providers can emit duplicate final event\n * families for one segment).\n * - `Interrupted` and `SessionClosed` flush all partial drafts so nothing is\n * lost and no dangling host tool calls are produced.\n * - Other lifecycle events only update state.\n *\n * Unsettled tool calls (for example approvals still pending when the session\n * ends) are intentionally dropped from the durable transcript; pending HITL\n * requests live in the host session log, not the message transcript.\n */\nexport const projectVoiceEvent = (\n state: VoiceProjectionState,\n event: VoiceEvent\n): VoiceProjectionResult => {\n switch (event._tag) {\n case 'UserTranscriptFinal':\n return { state, messages: [UserMessage.make({ content: event.text })] }\n case 'AssistantTranscriptDelta':\n return isFinalized(state, segmentKey(event))\n ? { state, messages: [] }\n : appendDraftDelta(state, segmentKey(event), event.delta)\n case 'AssistantTranscriptFinal':\n return isFinalized(state, segmentKey(event))\n ? { state, messages: [] }\n : flushSegment(state, segmentKey(event), event.text)\n case 'Interrupted':\n case 'SessionClosed':\n return flushAllDrafts(state)\n case 'ToolCallsRequested':\n return {\n state: {\n ...state,\n turnToolCalls: [\n ...state.turnToolCalls,\n ...event.calls.map(call => protocolToolCallFromVoice(call))\n ]\n },\n messages: []\n }\n case 'ToolCallCompleted':\n return {\n state: {\n ...state,\n turnToolResults: [\n ...state.turnToolResults,\n ToolResultMessage.make({ toolCallId: event.callId, content: event.output })\n ]\n },\n messages: []\n }\n case 'ToolCallFailed':\n return {\n state: {\n ...state,\n turnToolResults: [\n ...state.turnToolResults,\n ToolResultMessage.make({\n toolCallId: event.callId,\n content: event.message,\n isError: true\n })\n ]\n },\n messages: []\n }\n case 'SessionOpening':\n case 'SessionOpened':\n case 'AudioInputStarted':\n case 'AudioInputStopped':\n case 'UserTranscriptDelta':\n case 'AssistantAudioStarted':\n case 'AssistantAudioStopped':\n case 'ToolCallExecuting':\n case 'AwaitingInput':\n case 'Error':\n return { state, messages: [] }\n }\n}\n\n// --- Reconnect seeding -------------------------------------------------------\n\nexport type VoiceSeedText = {\n readonly role: 'user' | 'assistant'\n readonly text: string\n}\n\nexport type VoiceSeedTextOptions = {\n /**\n * Prefix user seeds with the message author's display name\n * (`\"Mike: ...\"`). Text runtimes render per-message author envelopes into\n * model-visible context; seeds drop envelopes, so multi-user transcripts\n * lose who said what unless this is enabled. Assistant seeds are never\n * prefixed — the assistant is the speaking agent itself.\n */\n readonly includeAuthors?: boolean\n}\n\nconst userSeedText = (\n message: Extract<AgentMessage, { readonly _tag: 'User' }>,\n options: VoiceSeedTextOptions\n) => {\n const text = contentPreview(message.content)\n\n if (text.length === 0) {\n return text\n }\n\n const displayName = message.author?.displayName?.trim()\n\n return options.includeAuthors === true && displayName !== undefined && displayName.length > 0\n ? `${displayName}: ${text}`\n : text\n}\n\n/**\n * Map a replayed protocol transcript into text seeds for a new provider\n * session. Tool results are omitted; assistant messages seed their visible\n * text only. Reconnect never resumes a provider session: hosts replay these\n * seeds through the controller before the first user turn.\n */\nexport const voiceSeedTextsFromMessages = (\n messages: ReadonlyArray<AgentMessage>,\n options: VoiceSeedTextOptions = {}\n): ReadonlyArray<VoiceSeedText> =>\n messages.flatMap((message): ReadonlyArray<VoiceSeedText> => {\n switch (message._tag) {\n case 'User': {\n const text = userSeedText(message, options)\n\n return text.length === 0 ? [] : [{ role: 'user' as const, text }]\n }\n case 'Assistant': {\n const text = contentPreview(assistantContent(message))\n\n return text.length === 0 ? [] : [{ role: 'assistant' as const, text }]\n }\n case 'ToolResult':\n return []\n }\n })\n\n// --- Durable event ids -------------------------------------------------------\n\n/**\n * Replay-safe durable envelope for voice events. Hosts append these to their\n * session log; clients de-dupe replays by `eventId`, matching text runtime\n * durable stream rules.\n */\nexport class StoredVoiceEvent extends Schema.Class<StoredVoiceEvent>('StoredVoiceEvent')({\n eventId: Schema.String,\n event: VoiceEvent\n}) {}\n\nexport const makeVoiceEventId = (streamId: string, sequence: number) => `${streamId}:${sequence}`\n\nexport type VoiceEventSequencerState = {\n readonly nextSequence: number\n}\n\nexport const initialVoiceEventSequencerState: VoiceEventSequencerState = { nextSequence: 0 }\n\nexport const sequenceVoiceEvent = (\n streamId: string,\n state: VoiceEventSequencerState,\n event: VoiceEvent\n): { readonly stored: StoredVoiceEvent; readonly state: VoiceEventSequencerState } => ({\n stored: StoredVoiceEvent.make({\n eventId: makeVoiceEventId(streamId, state.nextSequence),\n event\n }),\n state: { nextSequence: state.nextSequence + 1 }\n})\n\n/** De-dupe replayed stored voice events by `eventId`. */\nexport const dedupeStoredVoiceEvents = (\n events: ReadonlyArray<StoredVoiceEvent>\n): ReadonlyArray<StoredVoiceEvent> => {\n const seen = new Set<string>()\n\n return events.filter(stored => {\n if (seen.has(stored.eventId)) {\n return false\n }\n\n seen.add(stored.eventId)\n\n return true\n })\n}\n"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
1
  import { VoiceEvent, VoiceSessionError, VoiceToolCall, VoiceToolCallOutcome } from "./protocol.mjs";
2
2
  import { WebRtcVoiceRuntime } from "./browser/webrtc.mjs";
3
3
  import { VoiceClientCodec } from "./client-codec.mjs";
4
- import { VoiceSeedText, voiceSeedTextsFromMessages } from "./projection.mjs";
4
+ import { VoiceSeedText, VoiceSeedTextOptions, voiceSeedTextsFromMessages } from "./projection.mjs";
5
5
  import { VoiceEventOutboxOptions } from "./outbox.mjs";
6
6
  import { HitlResponse, ToolApprovalRequest, ToolApprovalResponse } from "@yolk-sdk/agent/protocol";
7
7
  import { Effect } from "effect";
@@ -53,5 +53,5 @@ type YolkVoiceApi = {
53
53
  */
54
54
  declare const useYolkVoice: (options: UseYolkVoiceOptions) => YolkVoiceApi;
55
55
  //#endregion
56
- export { UseYolkVoiceOptions, type VoiceSeedText, YolkVoiceApi, YolkVoiceStatus, useYolkVoice, voiceSeedTextsFromMessages };
56
+ export { UseYolkVoiceOptions, type VoiceSeedText, type VoiceSeedTextOptions, YolkVoiceApi, YolkVoiceStatus, useYolkVoice, voiceSeedTextsFromMessages };
57
57
  //# sourceMappingURL=react.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.mts","names":[],"sources":["../../src/voice/react.ts"],"mappings":";;;;;;;;;KA0BY,eAAA;AAAA,KAEA,mBAAA;uFAED,SAAA,GAAY,QAAA,aAAqB,MAAA,CAAO,MAAA,SAAe,iBAAA,GAJvC;EAAA,SAMhB,eAAA,GACP,IAAA,EAAM,aAAA,EACN,QAAA,GAAW,oBAAA,KACR,MAAA,CAAO,MAAA,CAAO,oBAAA,EAAsB,iBAAA,GAThB;EAAA,SAWhB,KAAA,EAAO,gBAAA,EATN;EAAA,SAWD,aAAA,GAAgB,GAAA,aAAgB,aAAA,CAAc,UAAA;WAE9C,gBAAA,UAXiC;EAAA,SAajC,KAAA,SAAc,aAAA,CAAc,aAAA;EATxB;;;;;;;EAAA,SAiBJ,QAAA,GAAW,uBAAA;EAAA,SACX,OAAA,IAAW,KAAA,EAAO,UAAA;EAAA,SAClB,OAAA,IAAW,KAAA,EAAO,iBAAA;EAAA,SAClB,cAAA,WADkB;EAAA,SAGlB,OAAA,GAAU,kBAAA;AAAA;AAAA,KAGT,YAAA;EAAA,SACD,MAAA,EAAQ,eAAA;EAAA,SACR,KAAA,EAAO,iBAAA;EAAA,SACP,YAAA;EAAA,SACA,MAAA,WAjCuD;EAAA,SAmCvD,SAAA,UAhCD;EAAA,SAkCC,gBAAA,EAAkB,aAAA,CAAc,mBAAA;EAAA,SAChC,KAAA;EAAA,SACA,IAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA,GAAW,IAAA;EAAA,SACX,kBAAA,GAAqB,QAAA,EAAU,YAAA;EAAA,SAC/B,WAAA,GAAc,SAAA,UAAmB,UAAA;EAAA,SACjC,QAAA,GAAW,SAAA,UAAmB,UAAA,UAAoB,MAAA,oBArC3C;EAAA,SAuCP,kBAAA,GAAqB,OAAA,EAAS,gBAAA;AAAA;;;;;;;;cA6F5B,YAAA,GAAgB,OAAA,EAAS,mBAAA,KAAsB,YAgP3D"}
1
+ {"version":3,"file":"react.d.mts","names":[],"sources":["../../src/voice/react.ts"],"mappings":";;;;;;;;;KA8BY,eAAA;AAAA,KAEA,mBAAA;uFAED,SAAA,GAAY,QAAA,aAAqB,MAAA,CAAO,MAAA,SAAe,iBAAA,GAJvC;EAAA,SAMhB,eAAA,GACP,IAAA,EAAM,aAAA,EACN,QAAA,GAAW,oBAAA,KACR,MAAA,CAAO,MAAA,CAAO,oBAAA,EAAsB,iBAAA,GAThB;EAAA,SAWhB,KAAA,EAAO,gBAAA,EATN;EAAA,SAWD,aAAA,GAAgB,GAAA,aAAgB,aAAA,CAAc,UAAA;WAE9C,gBAAA,UAXiC;EAAA,SAajC,KAAA,SAAc,aAAA,CAAc,aAAA;EATxB;;;;;;;EAAA,SAiBJ,QAAA,GAAW,uBAAA;EAAA,SACX,OAAA,IAAW,KAAA,EAAO,UAAA;EAAA,SAClB,OAAA,IAAW,KAAA,EAAO,iBAAA;EAAA,SAClB,cAAA,WADkB;EAAA,SAGlB,OAAA,GAAU,kBAAA;AAAA;AAAA,KAGT,YAAA;EAAA,SACD,MAAA,EAAQ,eAAA;EAAA,SACR,KAAA,EAAO,iBAAA;EAAA,SACP,YAAA;EAAA,SACA,MAAA,WAjCuD;EAAA,SAmCvD,SAAA,UAhCD;EAAA,SAkCC,gBAAA,EAAkB,aAAA,CAAc,mBAAA;EAAA,SAChC,KAAA;EAAA,SACA,IAAA;EAAA,SACA,MAAA;EAAA,SACA,QAAA,GAAW,IAAA;EAAA,SACX,kBAAA,GAAqB,QAAA,EAAU,YAAA;EAAA,SAC/B,WAAA,GAAc,SAAA,UAAmB,UAAA;EAAA,SACjC,QAAA,GAAW,SAAA,UAAmB,UAAA,UAAoB,MAAA,oBArC3C;EAAA,SAuCP,kBAAA,GAAqB,OAAA,EAAS,gBAAA;AAAA;;;;;;;;cA6F5B,YAAA,GAAgB,OAAA,EAAS,mBAAA,KAAsB,YAgP3D"}
@@ -1 +1 @@
1
- {"version":3,"file":"react.mjs","names":["ToolApprovalResponseClass"],"sources":["../../src/voice/react.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useReducer, useRef } from 'react'\nimport { Effect, Exit, Scope, Stream } from 'effect'\nimport type {\n HitlResponse,\n ToolApprovalRequest,\n ToolApprovalResponse\n} from '@yolk-sdk/agent/protocol'\nimport { ToolApprovalResponse as ToolApprovalResponseClass } from '@yolk-sdk/agent/protocol'\nimport {\n makeWebRtcVoiceTransport,\n type WebRtcMediaStreamLike,\n type WebRtcVoiceRuntime\n} from './browser/index.ts'\nimport type { VoiceClientCodec } from './client-codec.ts'\nimport { makeVoiceController, type VoiceControllerApi } from './controller.ts'\nimport { makeVoiceEventOutbox, type VoiceEventOutboxOptions } from './outbox.ts'\nimport {\n VoiceSessionError,\n type VoiceEvent,\n type VoiceToolCall,\n type VoiceToolCallOutcome\n} from './protocol.ts'\nimport { voiceSeedTextsFromMessages, type VoiceSeedText } from './projection.ts'\n\nexport type YolkVoiceStatus = 'idle' | 'connecting' | 'live' | 'error'\n\nexport type UseYolkVoiceOptions = {\n /** Host SDP exchange: POST the offer SDP to an app route, return answer. */\n readonly negotiate: (offerSdp: string) => Effect.Effect<string, VoiceSessionError>\n /** Host server tool endpoint call; never executes tools in the browser. */\n readonly executeToolCall: (\n call: VoiceToolCall,\n approval?: ToolApprovalResponse\n ) => Effect.Effect<VoiceToolCallOutcome, VoiceSessionError>\n /** Provider client codec, e.g. `openAiRealtimeVoiceClientCodec`. */\n readonly codec: VoiceClientCodec\n /** Provider server-event decoder to provider-neutral voice events. */\n readonly decodeMessage: (raw: string) => ReadonlyArray<VoiceEvent>\n /** Provider data channel label, e.g. `oai-events`. */\n readonly dataChannelLabel: string\n /** Conversation seeds replayed into each new provider session. */\n readonly seeds?: () => ReadonlyArray<VoiceSeedText>\n /**\n * Durable session-log outbox: every session event is buffered as a\n * replay-safe `StoredVoiceEvent` and batch-flushed to the host endpoint\n * (see `makeVoiceEventOutbox`). Use a keepalive-capable `flush` so the\n * final batch survives page unload; the host folds batches with\n * `foldStoredVoiceEvents`.\n */\n readonly eventLog?: VoiceEventOutboxOptions\n readonly onEvent?: (event: VoiceEvent) => void\n readonly onError?: (error: VoiceSessionError) => void\n readonly readyTimeoutMs?: number\n /** Test seam; defaults to real browser WebRTC APIs. */\n readonly runtime?: WebRtcVoiceRuntime\n}\n\nexport type YolkVoiceApi = {\n readonly status: YolkVoiceStatus\n readonly error: VoiceSessionError | null\n readonly isConnecting: boolean\n readonly isLive: boolean\n /** Streaming user transcript draft; cleared on each final transcript. */\n readonly userDraft: string\n /** Pending voice tool approvals awaiting a HITL response. */\n readonly pendingApprovals: ReadonlyArray<ToolApprovalRequest>\n readonly start: () => void\n readonly stop: () => void\n readonly toggle: () => void\n readonly sendText: (text: string) => void\n readonly submitHitlResponse: (response: HitlResponse) => void\n readonly approveTool: (requestId: string, toolCallId: string) => void\n readonly denyTool: (requestId: string, toolCallId: string, reason?: string) => void\n /** Attach the audio element that plays the assistant's voice. */\n readonly attachAudioElement: (element: HTMLAudioElement | null) => void\n}\n\ntype VoiceHookState = {\n readonly status: YolkVoiceStatus\n readonly error: VoiceSessionError | null\n readonly userDraft: string\n readonly pendingApprovals: ReadonlyArray<ToolApprovalRequest>\n}\n\ntype VoiceHookAction =\n | { readonly _tag: 'Connecting' }\n | { readonly _tag: 'Live' }\n | { readonly _tag: 'Stopped' }\n | { readonly _tag: 'Errored'; readonly error: VoiceSessionError }\n | { readonly _tag: 'Event'; readonly event: VoiceEvent }\n | { readonly _tag: 'ApprovalSubmitted'; readonly requestId: string }\n\nconst initialVoiceHookState: VoiceHookState = {\n status: 'idle',\n error: null,\n userDraft: '',\n pendingApprovals: []\n}\n\nconst applyEvent = (state: VoiceHookState, event: VoiceEvent): VoiceHookState => {\n switch (event._tag) {\n case 'UserTranscriptDelta':\n return { ...state, userDraft: `${state.userDraft}${event.delta}` }\n case 'UserTranscriptFinal':\n return { ...state, userDraft: '' }\n case 'AwaitingInput': {\n const approvals = event.requests.filter(request => request._tag === 'ToolApprovalRequest')\n\n return approvals.length === 0\n ? state\n : { ...state, pendingApprovals: [...state.pendingApprovals, ...approvals] }\n }\n case 'ToolCallCompleted':\n case 'ToolCallFailed':\n return {\n ...state,\n pendingApprovals: state.pendingApprovals.filter(\n request => request.toolCallId !== event.callId\n )\n }\n case 'Error':\n return {\n ...state,\n status: 'error',\n error: new VoiceSessionError({ code: event.code, message: event.message })\n }\n case 'SessionClosed':\n return state.status === 'error' ? state : { ...state, status: 'idle', userDraft: '' }\n default:\n return state\n }\n}\n\nconst reduceVoiceHookState = (state: VoiceHookState, action: VoiceHookAction): VoiceHookState => {\n switch (action._tag) {\n case 'Connecting':\n return { ...initialVoiceHookState, status: 'connecting' }\n case 'Live':\n return { ...state, status: 'live', error: null }\n case 'Stopped':\n return { ...state, status: 'idle', userDraft: '' }\n case 'Errored':\n return { ...state, status: 'error', error: action.error }\n case 'Event':\n return applyEvent(state, action.event)\n case 'ApprovalSubmitted':\n return {\n ...state,\n pendingApprovals: state.pendingApprovals.filter(\n request => request.requestId !== action.requestId\n )\n }\n }\n}\n\nconst isDomMediaStream = (\n stream: WebRtcMediaStreamLike\n): stream is WebRtcMediaStreamLike & MediaStream =>\n typeof MediaStream !== 'undefined' && stream instanceof MediaStream\n\n/**\n * Headless browser voice hook over the Yolk WebRTC transport and voice\n * controller. Owns connection lifecycle, event-derived UI state, and HITL\n * approval submission. Rendering, chat projection, and product policy stay\n * host-owned; subscribe with `onEvent` to project transcripts into chat\n * state.\n */\nexport const useYolkVoice = (options: UseYolkVoiceOptions): YolkVoiceApi => {\n const [state, dispatch] = useReducer(reduceVoiceHookState, initialVoiceHookState)\n const scopeRef = useRef<Scope.Closeable | null>(null)\n const controllerRef = useRef<VoiceControllerApi | null>(null)\n const audioElementRef = useRef<HTMLAudioElement | null>(null)\n const attemptIdRef = useRef(0)\n const optionsRef = useRef(options)\n\n useEffect(() => {\n optionsRef.current = options\n }, [options])\n\n const isConnecting = state.status === 'connecting'\n const isLive = state.status === 'live'\n\n const closeSession = useCallback(() => {\n const scope = scopeRef.current\n scopeRef.current = null\n controllerRef.current = null\n\n if (audioElementRef.current !== null) {\n audioElementRef.current.srcObject = null\n }\n\n if (scope !== null) {\n Effect.runFork(Scope.close(scope, Exit.void))\n }\n }, [])\n\n const stop = useCallback(() => {\n attemptIdRef.current += 1\n closeSession()\n dispatch({ _tag: 'Stopped' })\n }, [closeSession])\n\n const start = useCallback(() => {\n if (scopeRef.current !== null) {\n return\n }\n\n const attemptId = attemptIdRef.current + 1\n attemptIdRef.current = attemptId\n dispatch({ _tag: 'Connecting' })\n\n const program = Effect.gen(function* () {\n const scope = yield* Scope.make()\n\n if (attemptIdRef.current !== attemptId) {\n yield* Scope.close(scope, Exit.void)\n return\n }\n\n scopeRef.current = scope\n\n const opts = optionsRef.current\n const { session, outbox } = yield* Scope.provide(\n Effect.gen(function* () {\n const transport = yield* makeWebRtcVoiceTransport({\n negotiate: opts.negotiate,\n decodeMessage: opts.decodeMessage,\n dataChannelLabel: opts.dataChannelLabel,\n readyTimeoutMs: opts.readyTimeoutMs,\n runtime: opts.runtime,\n onRemoteAudioStream: stream => {\n const element = audioElementRef.current\n\n if (element !== null && isDomMediaStream(stream)) {\n element.srcObject = stream\n }\n }\n })\n const controller = yield* makeVoiceController({\n transport,\n codec: opts.codec,\n executeToolCall: opts.executeToolCall\n })\n const eventOutbox =\n opts.eventLog === undefined ? null : yield* makeVoiceEventOutbox(opts.eventLog)\n\n return { session: controller, outbox: eventOutbox }\n }),\n scope\n )\n\n controllerRef.current = session\n\n const seeds = optionsRef.current.seeds?.() ?? []\n yield* Effect.forEach(\n seeds,\n seed =>\n seed.role === 'user'\n ? session.seedUserText(seed.text)\n : session.seedAssistantText(seed.text),\n { discard: true }\n )\n\n if (attemptIdRef.current === attemptId) {\n dispatch({ _tag: 'Live' })\n }\n\n yield* Stream.runForEach(session.events, event =>\n Effect.gen(function* () {\n if (attemptIdRef.current !== attemptId) {\n return\n }\n\n if (outbox !== null) {\n yield* outbox.offer(event)\n }\n\n optionsRef.current.onEvent?.(event)\n dispatch({ _tag: 'Event', event })\n\n if (event._tag === 'Error') {\n optionsRef.current.onError?.(\n new VoiceSessionError({ code: event.code, message: event.message })\n )\n }\n })\n )\n })\n\n Effect.runFork(\n program.pipe(\n Effect.matchEffect({\n onFailure: error =>\n Effect.sync(() => {\n closeSession()\n\n if (attemptIdRef.current === attemptId) {\n dispatch({ _tag: 'Errored', error })\n optionsRef.current.onError?.(error)\n }\n }),\n onSuccess: () =>\n Effect.sync(() => {\n if (attemptIdRef.current === attemptId) {\n closeSession()\n dispatch({ _tag: 'Stopped' })\n }\n })\n })\n )\n )\n }, [closeSession])\n\n const toggle = useCallback(() => {\n if (isConnecting || isLive) {\n stop()\n return\n }\n\n start()\n }, [isConnecting, isLive, start, stop])\n\n const sendText = useCallback((text: string) => {\n const controller = controllerRef.current\n\n if (controller === null || text.trim().length === 0) {\n return\n }\n\n Effect.runFork(\n controller\n .sendText(text)\n .pipe(Effect.catch(error => Effect.sync(() => optionsRef.current.onError?.(error))))\n )\n }, [])\n\n const submitHitlResponse = useCallback((response: HitlResponse) => {\n const controller = controllerRef.current\n\n if (controller === null) {\n return\n }\n\n if (response._tag === 'ToolApprovalResponse') {\n dispatch({ _tag: 'ApprovalSubmitted', requestId: response.requestId })\n }\n\n Effect.runFork(controller.submitHitlResponse(response))\n }, [])\n\n const approveTool = useCallback(\n (requestId: string, toolCallId: string) => {\n submitHitlResponse(\n ToolApprovalResponseClass.make({\n requestId,\n toolCallId,\n decision: 'approved',\n source: 'user'\n })\n )\n },\n [submitHitlResponse]\n )\n\n const denyTool = useCallback(\n (requestId: string, toolCallId: string, reason?: string) => {\n submitHitlResponse(\n ToolApprovalResponseClass.make({\n requestId,\n toolCallId,\n decision: 'denied',\n source: 'user',\n ...(reason === undefined ? {} : { reason })\n })\n )\n },\n [submitHitlResponse]\n )\n\n const attachAudioElement = useCallback((element: HTMLAudioElement | null) => {\n audioElementRef.current = element\n }, [])\n\n useEffect(\n () => () => {\n attemptIdRef.current += 1\n closeSession()\n },\n [closeSession]\n )\n\n return {\n status: state.status,\n error: state.error,\n isConnecting,\n isLive,\n userDraft: state.userDraft,\n pendingApprovals: state.pendingApprovals,\n start,\n stop,\n toggle,\n sendText,\n submitHitlResponse,\n approveTool,\n denyTool,\n attachAudioElement\n }\n}\n\nexport { voiceSeedTextsFromMessages }\nexport type { VoiceSeedText }\n"],"mappings":"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"}
1
+ {"version":3,"file":"react.mjs","names":["ToolApprovalResponseClass"],"sources":["../../src/voice/react.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useEffect, useReducer, useRef } from 'react'\nimport { Effect, Exit, Scope, Stream } from 'effect'\nimport type {\n HitlResponse,\n ToolApprovalRequest,\n ToolApprovalResponse\n} from '@yolk-sdk/agent/protocol'\nimport { ToolApprovalResponse as ToolApprovalResponseClass } from '@yolk-sdk/agent/protocol'\nimport {\n makeWebRtcVoiceTransport,\n type WebRtcMediaStreamLike,\n type WebRtcVoiceRuntime\n} from './browser/index.ts'\nimport type { VoiceClientCodec } from './client-codec.ts'\nimport { makeVoiceController, type VoiceControllerApi } from './controller.ts'\nimport { makeVoiceEventOutbox, type VoiceEventOutboxOptions } from './outbox.ts'\nimport {\n VoiceSessionError,\n type VoiceEvent,\n type VoiceToolCall,\n type VoiceToolCallOutcome\n} from './protocol.ts'\nimport {\n voiceSeedTextsFromMessages,\n type VoiceSeedText,\n type VoiceSeedTextOptions\n} from './projection.ts'\n\nexport type YolkVoiceStatus = 'idle' | 'connecting' | 'live' | 'error'\n\nexport type UseYolkVoiceOptions = {\n /** Host SDP exchange: POST the offer SDP to an app route, return answer. */\n readonly negotiate: (offerSdp: string) => Effect.Effect<string, VoiceSessionError>\n /** Host server tool endpoint call; never executes tools in the browser. */\n readonly executeToolCall: (\n call: VoiceToolCall,\n approval?: ToolApprovalResponse\n ) => Effect.Effect<VoiceToolCallOutcome, VoiceSessionError>\n /** Provider client codec, e.g. `openAiRealtimeVoiceClientCodec`. */\n readonly codec: VoiceClientCodec\n /** Provider server-event decoder to provider-neutral voice events. */\n readonly decodeMessage: (raw: string) => ReadonlyArray<VoiceEvent>\n /** Provider data channel label, e.g. `oai-events`. */\n readonly dataChannelLabel: string\n /** Conversation seeds replayed into each new provider session. */\n readonly seeds?: () => ReadonlyArray<VoiceSeedText>\n /**\n * Durable session-log outbox: every session event is buffered as a\n * replay-safe `StoredVoiceEvent` and batch-flushed to the host endpoint\n * (see `makeVoiceEventOutbox`). Use a keepalive-capable `flush` so the\n * final batch survives page unload; the host folds batches with\n * `foldStoredVoiceEvents`.\n */\n readonly eventLog?: VoiceEventOutboxOptions\n readonly onEvent?: (event: VoiceEvent) => void\n readonly onError?: (error: VoiceSessionError) => void\n readonly readyTimeoutMs?: number\n /** Test seam; defaults to real browser WebRTC APIs. */\n readonly runtime?: WebRtcVoiceRuntime\n}\n\nexport type YolkVoiceApi = {\n readonly status: YolkVoiceStatus\n readonly error: VoiceSessionError | null\n readonly isConnecting: boolean\n readonly isLive: boolean\n /** Streaming user transcript draft; cleared on each final transcript. */\n readonly userDraft: string\n /** Pending voice tool approvals awaiting a HITL response. */\n readonly pendingApprovals: ReadonlyArray<ToolApprovalRequest>\n readonly start: () => void\n readonly stop: () => void\n readonly toggle: () => void\n readonly sendText: (text: string) => void\n readonly submitHitlResponse: (response: HitlResponse) => void\n readonly approveTool: (requestId: string, toolCallId: string) => void\n readonly denyTool: (requestId: string, toolCallId: string, reason?: string) => void\n /** Attach the audio element that plays the assistant's voice. */\n readonly attachAudioElement: (element: HTMLAudioElement | null) => void\n}\n\ntype VoiceHookState = {\n readonly status: YolkVoiceStatus\n readonly error: VoiceSessionError | null\n readonly userDraft: string\n readonly pendingApprovals: ReadonlyArray<ToolApprovalRequest>\n}\n\ntype VoiceHookAction =\n | { readonly _tag: 'Connecting' }\n | { readonly _tag: 'Live' }\n | { readonly _tag: 'Stopped' }\n | { readonly _tag: 'Errored'; readonly error: VoiceSessionError }\n | { readonly _tag: 'Event'; readonly event: VoiceEvent }\n | { readonly _tag: 'ApprovalSubmitted'; readonly requestId: string }\n\nconst initialVoiceHookState: VoiceHookState = {\n status: 'idle',\n error: null,\n userDraft: '',\n pendingApprovals: []\n}\n\nconst applyEvent = (state: VoiceHookState, event: VoiceEvent): VoiceHookState => {\n switch (event._tag) {\n case 'UserTranscriptDelta':\n return { ...state, userDraft: `${state.userDraft}${event.delta}` }\n case 'UserTranscriptFinal':\n return { ...state, userDraft: '' }\n case 'AwaitingInput': {\n const approvals = event.requests.filter(request => request._tag === 'ToolApprovalRequest')\n\n return approvals.length === 0\n ? state\n : { ...state, pendingApprovals: [...state.pendingApprovals, ...approvals] }\n }\n case 'ToolCallCompleted':\n case 'ToolCallFailed':\n return {\n ...state,\n pendingApprovals: state.pendingApprovals.filter(\n request => request.toolCallId !== event.callId\n )\n }\n case 'Error':\n return {\n ...state,\n status: 'error',\n error: new VoiceSessionError({ code: event.code, message: event.message })\n }\n case 'SessionClosed':\n return state.status === 'error' ? state : { ...state, status: 'idle', userDraft: '' }\n default:\n return state\n }\n}\n\nconst reduceVoiceHookState = (state: VoiceHookState, action: VoiceHookAction): VoiceHookState => {\n switch (action._tag) {\n case 'Connecting':\n return { ...initialVoiceHookState, status: 'connecting' }\n case 'Live':\n return { ...state, status: 'live', error: null }\n case 'Stopped':\n return { ...state, status: 'idle', userDraft: '' }\n case 'Errored':\n return { ...state, status: 'error', error: action.error }\n case 'Event':\n return applyEvent(state, action.event)\n case 'ApprovalSubmitted':\n return {\n ...state,\n pendingApprovals: state.pendingApprovals.filter(\n request => request.requestId !== action.requestId\n )\n }\n }\n}\n\nconst isDomMediaStream = (\n stream: WebRtcMediaStreamLike\n): stream is WebRtcMediaStreamLike & MediaStream =>\n typeof MediaStream !== 'undefined' && stream instanceof MediaStream\n\n/**\n * Headless browser voice hook over the Yolk WebRTC transport and voice\n * controller. Owns connection lifecycle, event-derived UI state, and HITL\n * approval submission. Rendering, chat projection, and product policy stay\n * host-owned; subscribe with `onEvent` to project transcripts into chat\n * state.\n */\nexport const useYolkVoice = (options: UseYolkVoiceOptions): YolkVoiceApi => {\n const [state, dispatch] = useReducer(reduceVoiceHookState, initialVoiceHookState)\n const scopeRef = useRef<Scope.Closeable | null>(null)\n const controllerRef = useRef<VoiceControllerApi | null>(null)\n const audioElementRef = useRef<HTMLAudioElement | null>(null)\n const attemptIdRef = useRef(0)\n const optionsRef = useRef(options)\n\n useEffect(() => {\n optionsRef.current = options\n }, [options])\n\n const isConnecting = state.status === 'connecting'\n const isLive = state.status === 'live'\n\n const closeSession = useCallback(() => {\n const scope = scopeRef.current\n scopeRef.current = null\n controllerRef.current = null\n\n if (audioElementRef.current !== null) {\n audioElementRef.current.srcObject = null\n }\n\n if (scope !== null) {\n Effect.runFork(Scope.close(scope, Exit.void))\n }\n }, [])\n\n const stop = useCallback(() => {\n attemptIdRef.current += 1\n closeSession()\n dispatch({ _tag: 'Stopped' })\n }, [closeSession])\n\n const start = useCallback(() => {\n if (scopeRef.current !== null) {\n return\n }\n\n const attemptId = attemptIdRef.current + 1\n attemptIdRef.current = attemptId\n dispatch({ _tag: 'Connecting' })\n\n const program = Effect.gen(function* () {\n const scope = yield* Scope.make()\n\n if (attemptIdRef.current !== attemptId) {\n yield* Scope.close(scope, Exit.void)\n return\n }\n\n scopeRef.current = scope\n\n const opts = optionsRef.current\n const { session, outbox } = yield* Scope.provide(\n Effect.gen(function* () {\n const transport = yield* makeWebRtcVoiceTransport({\n negotiate: opts.negotiate,\n decodeMessage: opts.decodeMessage,\n dataChannelLabel: opts.dataChannelLabel,\n readyTimeoutMs: opts.readyTimeoutMs,\n runtime: opts.runtime,\n onRemoteAudioStream: stream => {\n const element = audioElementRef.current\n\n if (element !== null && isDomMediaStream(stream)) {\n element.srcObject = stream\n }\n }\n })\n const controller = yield* makeVoiceController({\n transport,\n codec: opts.codec,\n executeToolCall: opts.executeToolCall\n })\n const eventOutbox =\n opts.eventLog === undefined ? null : yield* makeVoiceEventOutbox(opts.eventLog)\n\n return { session: controller, outbox: eventOutbox }\n }),\n scope\n )\n\n controllerRef.current = session\n\n const seeds = optionsRef.current.seeds?.() ?? []\n yield* Effect.forEach(\n seeds,\n seed =>\n seed.role === 'user'\n ? session.seedUserText(seed.text)\n : session.seedAssistantText(seed.text),\n { discard: true }\n )\n\n if (attemptIdRef.current === attemptId) {\n dispatch({ _tag: 'Live' })\n }\n\n yield* Stream.runForEach(session.events, event =>\n Effect.gen(function* () {\n if (attemptIdRef.current !== attemptId) {\n return\n }\n\n if (outbox !== null) {\n yield* outbox.offer(event)\n }\n\n optionsRef.current.onEvent?.(event)\n dispatch({ _tag: 'Event', event })\n\n if (event._tag === 'Error') {\n optionsRef.current.onError?.(\n new VoiceSessionError({ code: event.code, message: event.message })\n )\n }\n })\n )\n })\n\n Effect.runFork(\n program.pipe(\n Effect.matchEffect({\n onFailure: error =>\n Effect.sync(() => {\n closeSession()\n\n if (attemptIdRef.current === attemptId) {\n dispatch({ _tag: 'Errored', error })\n optionsRef.current.onError?.(error)\n }\n }),\n onSuccess: () =>\n Effect.sync(() => {\n if (attemptIdRef.current === attemptId) {\n closeSession()\n dispatch({ _tag: 'Stopped' })\n }\n })\n })\n )\n )\n }, [closeSession])\n\n const toggle = useCallback(() => {\n if (isConnecting || isLive) {\n stop()\n return\n }\n\n start()\n }, [isConnecting, isLive, start, stop])\n\n const sendText = useCallback((text: string) => {\n const controller = controllerRef.current\n\n if (controller === null || text.trim().length === 0) {\n return\n }\n\n Effect.runFork(\n controller\n .sendText(text)\n .pipe(Effect.catch(error => Effect.sync(() => optionsRef.current.onError?.(error))))\n )\n }, [])\n\n const submitHitlResponse = useCallback((response: HitlResponse) => {\n const controller = controllerRef.current\n\n if (controller === null) {\n return\n }\n\n if (response._tag === 'ToolApprovalResponse') {\n dispatch({ _tag: 'ApprovalSubmitted', requestId: response.requestId })\n }\n\n Effect.runFork(controller.submitHitlResponse(response))\n }, [])\n\n const approveTool = useCallback(\n (requestId: string, toolCallId: string) => {\n submitHitlResponse(\n ToolApprovalResponseClass.make({\n requestId,\n toolCallId,\n decision: 'approved',\n source: 'user'\n })\n )\n },\n [submitHitlResponse]\n )\n\n const denyTool = useCallback(\n (requestId: string, toolCallId: string, reason?: string) => {\n submitHitlResponse(\n ToolApprovalResponseClass.make({\n requestId,\n toolCallId,\n decision: 'denied',\n source: 'user',\n ...(reason === undefined ? {} : { reason })\n })\n )\n },\n [submitHitlResponse]\n )\n\n const attachAudioElement = useCallback((element: HTMLAudioElement | null) => {\n audioElementRef.current = element\n }, [])\n\n useEffect(\n () => () => {\n attemptIdRef.current += 1\n closeSession()\n },\n [closeSession]\n )\n\n return {\n status: state.status,\n error: state.error,\n isConnecting,\n isLive,\n userDraft: state.userDraft,\n pendingApprovals: state.pendingApprovals,\n start,\n stop,\n toggle,\n sendText,\n submitHitlResponse,\n approveTool,\n denyTool,\n attachAudioElement\n }\n}\n\nexport { voiceSeedTextsFromMessages }\nexport type { VoiceSeedText, VoiceSeedTextOptions }\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.35",
3
+ "version": "0.1.0-canary.36",
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",
@@ -103,5 +103,6 @@ export {
103
103
  type VoiceEventSequencerState,
104
104
  type VoiceProjectionResult,
105
105
  type VoiceProjectionState,
106
- type VoiceSeedText
106
+ type VoiceSeedText,
107
+ type VoiceSeedTextOptions
107
108
  } from './projection.ts'
@@ -281,6 +281,34 @@ export type VoiceSeedText = {
281
281
  readonly text: string
282
282
  }
283
283
 
284
+ export type VoiceSeedTextOptions = {
285
+ /**
286
+ * Prefix user seeds with the message author's display name
287
+ * (`"Mike: ..."`). Text runtimes render per-message author envelopes into
288
+ * model-visible context; seeds drop envelopes, so multi-user transcripts
289
+ * lose who said what unless this is enabled. Assistant seeds are never
290
+ * prefixed — the assistant is the speaking agent itself.
291
+ */
292
+ readonly includeAuthors?: boolean
293
+ }
294
+
295
+ const userSeedText = (
296
+ message: Extract<AgentMessage, { readonly _tag: 'User' }>,
297
+ options: VoiceSeedTextOptions
298
+ ) => {
299
+ const text = contentPreview(message.content)
300
+
301
+ if (text.length === 0) {
302
+ return text
303
+ }
304
+
305
+ const displayName = message.author?.displayName?.trim()
306
+
307
+ return options.includeAuthors === true && displayName !== undefined && displayName.length > 0
308
+ ? `${displayName}: ${text}`
309
+ : text
310
+ }
311
+
284
312
  /**
285
313
  * Map a replayed protocol transcript into text seeds for a new provider
286
314
  * session. Tool results are omitted; assistant messages seed their visible
@@ -288,12 +316,13 @@ export type VoiceSeedText = {
288
316
  * seeds through the controller before the first user turn.
289
317
  */
290
318
  export const voiceSeedTextsFromMessages = (
291
- messages: ReadonlyArray<AgentMessage>
319
+ messages: ReadonlyArray<AgentMessage>,
320
+ options: VoiceSeedTextOptions = {}
292
321
  ): ReadonlyArray<VoiceSeedText> =>
293
322
  messages.flatMap((message): ReadonlyArray<VoiceSeedText> => {
294
323
  switch (message._tag) {
295
324
  case 'User': {
296
- const text = contentPreview(message.content)
325
+ const text = userSeedText(message, options)
297
326
 
298
327
  return text.length === 0 ? [] : [{ role: 'user' as const, text }]
299
328
  }
@@ -22,7 +22,11 @@ import {
22
22
  type VoiceToolCall,
23
23
  type VoiceToolCallOutcome
24
24
  } from './protocol.ts'
25
- import { voiceSeedTextsFromMessages, type VoiceSeedText } from './projection.ts'
25
+ import {
26
+ voiceSeedTextsFromMessages,
27
+ type VoiceSeedText,
28
+ type VoiceSeedTextOptions
29
+ } from './projection.ts'
26
30
 
27
31
  export type YolkVoiceStatus = 'idle' | 'connecting' | 'live' | 'error'
28
32
 
@@ -410,4 +414,4 @@ export const useYolkVoice = (options: UseYolkVoiceOptions): YolkVoiceApi => {
410
414
  }
411
415
 
412
416
  export { voiceSeedTextsFromMessages }
413
- export type { VoiceSeedText }
417
+ export type { VoiceSeedText, VoiceSeedTextOptions }