@pouchy_ai/companion-sdk 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +13 -0
- package/dist/call.d.ts +8 -0
- package/dist/call.js +15 -4
- package/dist/client.d.ts +7 -1
- package/dist/client.js +12 -0
- package/dist/index.d.ts +1 -1
- package/dist/protocol.d.ts +11 -1
- package/dist/protocol.js +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,37 @@ a protocol bump is always called out explicitly here.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.15.0] - 2026-07-08
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Typing / activity indicator** — a new `companion.typing` event and a
|
|
20
|
+
`client.onTyping(({ active }) => …)` convenience. The server emits
|
|
21
|
+
`active:true` when a turn starts working and `active:false` when it emits
|
|
22
|
+
its reply or pauses for an app tool result, so it spans the whole turn —
|
|
23
|
+
including the tool-loop / thinking phase BEFORE the first text delta, which
|
|
24
|
+
`onDelta` can't observe. Drive a "typing…" affordance uniformly across
|
|
25
|
+
streaming and non-streaming replies. Additive and non-breaking (receivers
|
|
26
|
+
ignore unknown types); the terminal `companion.message` stays authoritative.
|
|
27
|
+
Wire protocol stays `PROTOCOL_VERSION 1` (an added outbound type is
|
|
28
|
+
backwards-compatible). New payload type: `TypingPayload { active: boolean }`.
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- **Opt-in voice barge-in** — `connectCall({ bargeIn: true })` keeps the
|
|
33
|
+
microphone OPEN while the companion speaks, so the user can interrupt
|
|
34
|
+
mid-utterance and both providers' native VAD interruption takes over
|
|
35
|
+
(OpenAI Realtime server VAD; ElevenLabs Convai interruption handling).
|
|
36
|
+
Default remains half-duplex (mic muted during agent speech): on phone
|
|
37
|
+
speakers browser echo cancellation does not fully remove the companion's
|
|
38
|
+
own voice, and it would barge in on itself. Enable only where AEC holds —
|
|
39
|
+
headphones, desktop, or devices you have verified.
|
|
40
|
+
- The OpenAI Realtime microphone request now asks for explicit
|
|
41
|
+
`echoCancellation` / `noiseSuppression` / `autoGainControl` processing
|
|
42
|
+
constraints (harmless in half-duplex; required for barge-in to be viable).
|
|
43
|
+
|
|
44
|
+
No wire-protocol change (`PROTOCOL_VERSION` stays `1`).
|
|
45
|
+
|
|
15
46
|
## [0.13.0] - 2026-07-06
|
|
16
47
|
|
|
17
48
|
### Added
|
package/README.md
CHANGED
|
@@ -70,6 +70,7 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
|
|
|
70
70
|
| `onConfirmRequest(fn)` | `companion.confirm_request` | a sensitive-op approval request. **Platform session tokens** (your end users): show your own confirm card and resolve it with `confirmAction` — this is how confirm-gated custom skills (POST / credentialed) run. First-party user tokens: observe-only — approval stays first-party (where the `stepUp:true` biometric gate lives) |
|
|
71
71
|
| `onAudio(fn)` | `companion.audio` | TTS clip (non-call modality) |
|
|
72
72
|
| `onExpression(fn)` | `companion.expression` | avatar viseme / expression / gesture |
|
|
73
|
+
| `onTyping(fn)` | `companion.typing` | activity indicator — `fn({active})` fires `true` when a turn starts working and `false` when it finishes / pauses, spanning the tool-loop / thinking phase before the first text delta. Drive a "typing…" state |
|
|
73
74
|
| `onUsage(fn)` | `control.usage` | per-token metering echo |
|
|
74
75
|
| `onError(fn)` | `control.error` | agent / stream errors |
|
|
75
76
|
|
|
@@ -123,6 +124,18 @@ the [source README](../../src/lib/companion-sdk/README.md#representative-mode-
|
|
|
123
124
|
|
|
124
125
|
## Voice (optional dependency)
|
|
125
126
|
|
|
127
|
+
**Zero-code alternative:** if you just want a chat box, skip the SDK entirely —
|
|
128
|
+
`<iframe src="https://pouchy.ai/embed?token=…&theme=dark&accent=%23ff6b81">` is a
|
|
129
|
+
hosted drop-in widget with a postMessage control plane (docs:
|
|
130
|
+
`docs/companion-widget.md` in the repo / pouchy.ai/sdk → Drop-in widget).
|
|
131
|
+
|
|
132
|
+
`connectCall({ bargeIn: true })` opts into **full-duplex** voice: the mic stays
|
|
133
|
+
open while the companion speaks so the user can interrupt mid-utterance (the
|
|
134
|
+
providers' native interruption takes over). The default is half-duplex — on
|
|
135
|
+
phone speakers echo cancellation can't fully remove the companion's own voice
|
|
136
|
+
and it would interrupt itself. Enable it only where AEC holds (headphones,
|
|
137
|
+
desktop, or devices you've verified).
|
|
138
|
+
|
|
126
139
|
`connectCall()` for the **ElevenLabs Convai** provider needs the optional peer
|
|
127
140
|
dependency:
|
|
128
141
|
|
package/dist/call.d.ts
CHANGED
|
@@ -46,6 +46,14 @@ export interface CompanionCallOptions {
|
|
|
46
46
|
}) => void;
|
|
47
47
|
onSpeakingChange?: (speaking: boolean) => void;
|
|
48
48
|
onError?: (err: Error) => void;
|
|
49
|
+
/** OPT-IN full duplex: keep the mic OPEN while the companion speaks so the
|
|
50
|
+
* user can interrupt mid-utterance (both providers' native VAD interruption
|
|
51
|
+
* then works). Default false = half-duplex — the mic is muted during agent
|
|
52
|
+
* speech, because on phone SPEAKERS browser echo cancellation does not fully
|
|
53
|
+
* remove the companion's own voice and it barges in on itself (the SDK-voice
|
|
54
|
+
* "说一句就断了" field report). Enable only where AEC actually holds:
|
|
55
|
+
* headphones, desktop, or devices you have verified. */
|
|
56
|
+
bargeIn?: boolean;
|
|
49
57
|
}
|
|
50
58
|
export interface CompanionCall {
|
|
51
59
|
/** Hang up. Idempotent. */
|
package/dist/call.js
CHANGED
|
@@ -163,7 +163,11 @@ const SDP_TIMEOUT_MS = 8_000;
|
|
|
163
163
|
* we've already given up on the call. */
|
|
164
164
|
async function getMicWithTimeout() {
|
|
165
165
|
let timedOut = false;
|
|
166
|
-
const micPromise = navigator.mediaDevices.getUserMedia({
|
|
166
|
+
const micPromise = navigator.mediaDevices.getUserMedia({
|
|
167
|
+
// Explicit processing constraints: echoCancellation is what makes the
|
|
168
|
+
// opt-in barge-in mode viable at all, and it is harmless in half-duplex.
|
|
169
|
+
audio: { echoCancellation: true, noiseSuppression: true, autoGainControl: true }
|
|
170
|
+
});
|
|
167
171
|
micPromise
|
|
168
172
|
.then((m) => {
|
|
169
173
|
if (timedOut)
|
|
@@ -299,8 +303,10 @@ async function openOpenAICall(creds, opts, bridge) {
|
|
|
299
303
|
else if (msg.type === 'output_audio_buffer.started') {
|
|
300
304
|
opts.onSpeakingChange?.(true);
|
|
301
305
|
// Model started speaking → mute the mic so its own audio can't
|
|
302
|
-
// echo-transcribe into a self-barge-in.
|
|
303
|
-
|
|
306
|
+
// echo-transcribe into a self-barge-in. bargeIn opts out (B wave):
|
|
307
|
+
// the mic stays open and OpenAI's server VAD handles interruption.
|
|
308
|
+
if (opts.bargeIn !== true)
|
|
309
|
+
muteMicForSpeech();
|
|
304
310
|
}
|
|
305
311
|
else if (msg.type === 'output_audio_buffer.stopped' ||
|
|
306
312
|
msg.type === 'output_audio_buffer.cleared' ||
|
|
@@ -311,7 +317,8 @@ async function openOpenAICall(creds, opts, bridge) {
|
|
|
311
317
|
// audio doesn't get captured the instant we unmute. Several event names
|
|
312
318
|
// can end a turn; any of them re-arms the mic.
|
|
313
319
|
opts.onSpeakingChange?.(false);
|
|
314
|
-
|
|
320
|
+
if (opts.bargeIn !== true)
|
|
321
|
+
scheduleMicUnmute();
|
|
315
322
|
}
|
|
316
323
|
};
|
|
317
324
|
const offer = await pc.createOffer();
|
|
@@ -497,6 +504,10 @@ async function openConvaiCall(creds, opts, bridge) {
|
|
|
497
504
|
return;
|
|
498
505
|
const speaking = mode === 'speaking';
|
|
499
506
|
opts.onSpeakingChange?.(speaking);
|
|
507
|
+
// bargeIn opts out of half-duplex (B wave): the mic stays open and
|
|
508
|
+
// ElevenLabs' native interruption handling takes over.
|
|
509
|
+
if (opts.bargeIn === true)
|
|
510
|
+
return;
|
|
500
511
|
if (speaking) {
|
|
501
512
|
if (micUnmuteTimer) {
|
|
502
513
|
clearTimeout(micUnmuteTimer);
|
package/dist/client.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, UsagePayload, WorldStateEvent } from './protocol.js';
|
|
1
|
+
import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, ExpressionPayload, InterfaceUpdatePayload, OutboundType, PendingConfirm, RenderInterfacePayload, SocialMessagePayload, TypingPayload, UsagePayload, WorldStateEvent } from './protocol.js';
|
|
2
2
|
/** Ergonomic world-state input: a WorldStateEvent with the CloudEvents plumbing
|
|
3
3
|
* (specversion / id / source) optional — sendWorldState fills them. */
|
|
4
4
|
export type WorldStateInput<D = unknown> = Omit<WorldStateEvent<D>, 'specversion' | 'id' | 'source'> & Partial<Pick<WorldStateEvent<D>, 'specversion' | 'id' | 'source'>>;
|
|
@@ -401,6 +401,12 @@ export declare class CompanionClient {
|
|
|
401
401
|
* friends (companion.social_message), delivered cross-app to any embed whose
|
|
402
402
|
* token holds `social.message`. Surface them in your own UI / notify the user. */
|
|
403
403
|
onSocialMessage(handler: (payload: SocialMessagePayload, envelope: CompanionEnvelope) => void): () => void;
|
|
404
|
+
/** Convenience: subscribe to the companion's activity indicator
|
|
405
|
+
* (companion.typing). Fires `active:true` when a turn starts working and
|
|
406
|
+
* `active:false` when it finishes or pauses for a tool result — spanning the
|
|
407
|
+
* tool-loop / thinking phase before the first text delta. Drive a "typing…"
|
|
408
|
+
* affordance from it: `client.onTyping(({ active }) => setTyping(active))`. */
|
|
409
|
+
onTyping(handler: (payload: TypingPayload, envelope: CompanionEnvelope) => void): () => void;
|
|
404
410
|
/** Open the event stream (auto-reconnecting with the resume cursor). Uses the
|
|
405
411
|
* WebSocket plane when opted in + available, else SSE. */
|
|
406
412
|
start(): void;
|
package/dist/client.js
CHANGED
|
@@ -643,6 +643,18 @@ export class CompanionClient {
|
|
|
643
643
|
handler(p, env);
|
|
644
644
|
});
|
|
645
645
|
}
|
|
646
|
+
/** Convenience: subscribe to the companion's activity indicator
|
|
647
|
+
* (companion.typing). Fires `active:true` when a turn starts working and
|
|
648
|
+
* `active:false` when it finishes or pauses for a tool result — spanning the
|
|
649
|
+
* tool-loop / thinking phase before the first text delta. Drive a "typing…"
|
|
650
|
+
* affordance from it: `client.onTyping(({ active }) => setTyping(active))`. */
|
|
651
|
+
onTyping(handler) {
|
|
652
|
+
return this.on('companion.typing', (env) => {
|
|
653
|
+
const p = env.payload;
|
|
654
|
+
if (p && typeof p === 'object' && typeof p.active === 'boolean')
|
|
655
|
+
handler(p, env);
|
|
656
|
+
});
|
|
657
|
+
}
|
|
646
658
|
/** Open the event stream (auto-reconnecting with the resume cursor). Uses the
|
|
647
659
|
* WebSocket plane when opted in + available, else SSE. */
|
|
648
660
|
start() {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,6 @@ export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js
|
|
|
3
3
|
export type { CompanionClientOptions, HelloAck, RecalledMemory, CallCredentials, CompanionToolDecl, WorldStateInput, CompanionAvatar, BrandIconSize } from './client.js';
|
|
4
4
|
export { openCompanionCall, HOST_CONTROL_TOOLS, HOST_CONTROL_TOOL_NAMES, AVATAR_VISUAL_TOOLS, AVATAR_VISUAL_TOOL_NAMES } from './call.js';
|
|
5
5
|
export type { CompanionCall, CompanionCallOptions, VoiceToolBridge } from './call.js';
|
|
6
|
-
export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, UsagePayload } from './protocol.js';
|
|
6
|
+
export type { CompanionEnvelope, OutboundType, InboundType, WorldStateEvent, RenderInterfacePayload, InterfaceUpdatePayload, SocialMessagePayload, ConfirmRequestPayload, PendingConfirm, AudioClipPayload, ExpressionPayload, TypingPayload, UsagePayload } from './protocol.js';
|
|
7
7
|
/** Create a companion client. */
|
|
8
8
|
export declare function createCompanion(opts: CompanionClientOptions): CompanionClient;
|
package/dist/protocol.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export declare const PROTOCOL_VERSION: 1;
|
|
|
3
3
|
export declare const INBOUND_TYPES: readonly ["hello", "input.text", "context.event", "context.snapshot", "tool.result", "control.start_call", "control.end_call", "control.set_modalities", "control.ping"];
|
|
4
4
|
export type InboundType = (typeof INBOUND_TYPES)[number];
|
|
5
5
|
/** Pouchy → app (control/data plane). */
|
|
6
|
-
export declare const OUTBOUND_TYPES: readonly ["hello.ack", "companion.message", "companion.audio", "companion.tool_call", "companion.ui_action", "companion.ui_update", "companion.expression", "companion.voice_inject", "companion.confirm_request", "companion.social_message", "control.call_ready", "control.error", "control.usage"];
|
|
6
|
+
export declare const OUTBOUND_TYPES: readonly ["hello.ack", "companion.message", "companion.audio", "companion.tool_call", "companion.ui_action", "companion.ui_update", "companion.expression", "companion.voice_inject", "companion.confirm_request", "companion.social_message", "companion.typing", "control.call_ready", "control.error", "control.usage"];
|
|
7
7
|
export type OutboundType = (typeof OUTBOUND_TYPES)[number];
|
|
8
8
|
export type MessageType = InboundType | OutboundType;
|
|
9
9
|
/** The single envelope every control/data-plane message shares. */
|
|
@@ -120,6 +120,16 @@ export interface SocialMessagePayload {
|
|
|
120
120
|
/** ISO timestamp of delivery. */
|
|
121
121
|
createdAt: string;
|
|
122
122
|
}
|
|
123
|
+
/** Payload of a `companion.typing` event — an activity indicator bracketing the
|
|
124
|
+
* model work of a turn: `active:true` when the companion starts working,
|
|
125
|
+
* `active:false` when it emits its reply or pauses for an app tool result. It
|
|
126
|
+
* spans the whole turn — the tool-loop / thinking phase BEFORE the first text
|
|
127
|
+
* delta, which `onDelta` can't observe — so a host can show a "typing…" state
|
|
128
|
+
* uniformly across streaming and non-streaming replies. Advisory: a lost event
|
|
129
|
+
* never affects the turn; the terminal `companion.message` is authoritative. */
|
|
130
|
+
export interface TypingPayload {
|
|
131
|
+
active: boolean;
|
|
132
|
+
}
|
|
123
133
|
/** A live world-state event the app streams in. `retained:true` = latest-value
|
|
124
134
|
* state (coalesced); `retained:false` = a transient moment carrying `salience`
|
|
125
135
|
* and optional `voiceRelevant`. */
|
package/dist/protocol.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/companion-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.0",
|
|
4
4
|
"description": "Embed the Pouchy companion — chat, voice, tools, memory, live world-state, instant UI, and agent-to-agent messaging — in any app, game, or site.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE",
|