@pouchy_ai/companion-sdk 0.12.1 → 0.14.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 +37 -1
- package/README.md +35 -3
- package/dist/call.d.ts +8 -0
- package/dist/call.js +15 -4
- package/dist/client.d.ts +31 -2
- package/dist/client.js +93 -3
- package/dist/index.js +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -12,6 +12,42 @@ a protocol bump is always called out explicitly here.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.14.0] - 2026-07-06
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Opt-in voice barge-in** — `connectCall({ bargeIn: true })` keeps the
|
|
20
|
+
microphone OPEN while the companion speaks, so the user can interrupt
|
|
21
|
+
mid-utterance and both providers' native VAD interruption takes over
|
|
22
|
+
(OpenAI Realtime server VAD; ElevenLabs Convai interruption handling).
|
|
23
|
+
Default remains half-duplex (mic muted during agent speech): on phone
|
|
24
|
+
speakers browser echo cancellation does not fully remove the companion's
|
|
25
|
+
own voice, and it would barge in on itself. Enable only where AEC holds —
|
|
26
|
+
headphones, desktop, or devices you have verified.
|
|
27
|
+
- The OpenAI Realtime microphone request now asks for explicit
|
|
28
|
+
`echoCancellation` / `noiseSuppression` / `autoGainControl` processing
|
|
29
|
+
constraints (harmless in half-duplex; required for barge-in to be viable).
|
|
30
|
+
|
|
31
|
+
No wire-protocol change (`PROTOCOL_VERSION` stays `1`).
|
|
32
|
+
|
|
33
|
+
## [0.13.0] - 2026-07-06
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **Token streaming** — the reply can now render as it is generated instead
|
|
38
|
+
of arriving as one block. Register `onDelta((chunk, meta) => …)` and
|
|
39
|
+
`sendText` automatically streams: the POST response itself carries SSE
|
|
40
|
+
`delta` frames (raw text chunks), an optional `reset` (a hop that streamed
|
|
41
|
+
text turned out to be tool-call deliberation — clear the partial render),
|
|
42
|
+
and a final `done` frame. `onMessage` still fires exactly ONCE with the
|
|
43
|
+
authoritative final text (the done frame's envelope is emitted locally and
|
|
44
|
+
the event-stream replay of the same envelope id is deduplicated), so
|
|
45
|
+
existing render code keeps working unchanged — deltas are a pure add-on.
|
|
46
|
+
Pass `sendText(text, { stream: false })` to force the buffered path, or
|
|
47
|
+
`{ stream: true }` to stream without a subscriber. Requires a server with
|
|
48
|
+
streaming input support; on older servers the SDK transparently falls back
|
|
49
|
+
to the buffered response. No wire-protocol (envelope) change.
|
|
50
|
+
|
|
15
51
|
## [0.12.1] - 2026-07-05
|
|
16
52
|
|
|
17
53
|
### Added
|
|
@@ -127,7 +163,7 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
|
|
|
127
163
|
- **Public distribution.** Installable from the public npm registry as
|
|
128
164
|
`@pouchy_ai/companion-sdk`; also resolvable via `esm.sh` / `jsDelivr` for
|
|
129
165
|
no-bundler / import-map consumers, and self-hosted at
|
|
130
|
-
`https://
|
|
166
|
+
`https://pouchy.ai/sdk/companion-sdk.js`.
|
|
131
167
|
- **Chat.** Text and multimodal (text + images) messaging.
|
|
132
168
|
- **Voice.** Live WebRTC call (`connectCall`), provider-agnostic across OpenAI
|
|
133
169
|
Realtime (zero extra deps) and ElevenLabs Convai (optional
|
package/README.md
CHANGED
|
@@ -18,10 +18,26 @@ No bundler? Pull it from a CDN via an import map instead — see
|
|
|
18
18
|
[`docs/companion-integration-faq.md`](../../docs/companion-integration-faq.md).
|
|
19
19
|
Changes per release are tracked in [`CHANGELOG.md`](./CHANGELOG.md).
|
|
20
20
|
|
|
21
|
+
## Quickstart
|
|
22
|
+
|
|
23
|
+
Two steps. **1)** Your backend exchanges the project **Secret Key** (create one
|
|
24
|
+
in the [dashboard](https://pouchy.ai/dashboard) → Keys) for a per-user
|
|
25
|
+
session token — first-seen `external_user_id`s are provisioned automatically:
|
|
26
|
+
|
|
27
|
+
```http
|
|
28
|
+
POST https://pouchy.ai/v1/sessions
|
|
29
|
+
Authorization: Bearer pchy_sk_…
|
|
30
|
+
{ "agent": "<agentId>", "external_user_id": "user_4211" }
|
|
31
|
+
|
|
32
|
+
→ { "session_token": "pchy_…", "expires_in": 3600, "instance": { … } }
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**2)** The client connects with the session token (never ship the Secret Key):
|
|
36
|
+
|
|
21
37
|
```ts
|
|
22
38
|
import { createCompanion } from '@pouchy_ai/companion-sdk';
|
|
23
39
|
|
|
24
|
-
const companion = createCompanion({ baseUrl: 'https://
|
|
40
|
+
const companion = createCompanion({ baseUrl: 'https://pouchy.ai', token: sessionToken });
|
|
25
41
|
await companion.connect();
|
|
26
42
|
companion.onMessage((t) => console.log(t));
|
|
27
43
|
companion.start();
|
|
@@ -30,6 +46,9 @@ await companion.connectCall(); // live voice (browser)
|
|
|
30
46
|
companion.sendWorldState({ type: 'game.player.hp', data: { hp: 12 }, retained: true });
|
|
31
47
|
```
|
|
32
48
|
|
|
49
|
+
(Personal/single-user integrations can pass a server-minted Personal Access
|
|
50
|
+
Token as `token` instead — same API. See the docs' authentication section.)
|
|
51
|
+
|
|
33
52
|
- **Guide:** [`docs/companion-quickstart.md`](../../docs/companion-quickstart.md)
|
|
34
53
|
- **Spec:** [`docs/companion-sdk-protocol.md`](../../docs/companion-sdk-protocol.md)
|
|
35
54
|
- **API reference:** [`docs/companion-api-reference.md`](../../docs/companion-api-reference.md)
|
|
@@ -42,7 +61,8 @@ Subscribe with `on(type, fn)` (or `'*'`), or these typed convenience helpers:
|
|
|
42
61
|
|
|
43
62
|
| Helper | Event | Use |
|
|
44
63
|
| --- | --- | --- |
|
|
45
|
-
| `onMessage(fn)` | `companion.message` |
|
|
64
|
+
| `onMessage(fn)` | `companion.message` | the assistant reply (fires exactly once per turn, streaming or not) |
|
|
65
|
+
| `onDelta(fn)` | *(POST-response SSE)* | **token streaming** — registering makes `sendText` stream the reply as it is generated: `fn(chunk, {reset?})` fires per text chunk (`reset` = clear the partial — that text was tool-call deliberation); `onMessage` then fires once with the authoritative final text (replay-deduplicated). Force per call with `sendText(text, { stream: true \| false })` |
|
|
46
66
|
| `onToolCall(fn)` | `companion.tool_call` | the companion asks your app to run a declared tool → `sendToolResult` |
|
|
47
67
|
| `onRender(fn)` | `companion.ui_action` | **Instant UI** — draw `payload.interface` (platform-neutral genui schema) with your own renderer (web / iOS / Android / CLI). Needs `ui.render` |
|
|
48
68
|
| `onInterfaceUpdate(fn)` | `companion.ui_update` | live `{key,value}` update to an already-rendered panel (no rebuild) |
|
|
@@ -75,7 +95,7 @@ memory, system prompt, or PII. Works over text and voice.
|
|
|
75
95
|
|
|
76
96
|
```ts
|
|
77
97
|
const c = createCompanion({
|
|
78
|
-
baseUrl: 'https://
|
|
98
|
+
baseUrl: 'https://pouchy.ai',
|
|
79
99
|
token: OWNER_PAT, // must hold the `represent` scope
|
|
80
100
|
surface: 'support-widget',
|
|
81
101
|
appContext: { name: 'AcmeShop', description: 'Order support' },
|
|
@@ -103,6 +123,18 @@ the [source README](../../src/lib/companion-sdk/README.md#representative-mode-
|
|
|
103
123
|
|
|
104
124
|
## Voice (optional dependency)
|
|
105
125
|
|
|
126
|
+
**Zero-code alternative:** if you just want a chat box, skip the SDK entirely —
|
|
127
|
+
`<iframe src="https://pouchy.ai/embed?token=…&theme=dark&accent=%23ff6b81">` is a
|
|
128
|
+
hosted drop-in widget with a postMessage control plane (docs:
|
|
129
|
+
`docs/companion-widget.md` in the repo / pouchy.ai/sdk → Drop-in widget).
|
|
130
|
+
|
|
131
|
+
`connectCall({ bargeIn: true })` opts into **full-duplex** voice: the mic stays
|
|
132
|
+
open while the companion speaks so the user can interrupt mid-utterance (the
|
|
133
|
+
providers' native interruption takes over). The default is half-duplex — on
|
|
134
|
+
phone speakers echo cancellation can't fully remove the companion's own voice
|
|
135
|
+
and it would interrupt itself. Enable it only where AEC holds (headphones,
|
|
136
|
+
desktop, or devices you've verified).
|
|
137
|
+
|
|
106
138
|
`connectCall()` for the **ElevenLabs Convai** provider needs the optional peer
|
|
107
139
|
dependency:
|
|
108
140
|
|
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
|
@@ -4,7 +4,7 @@ import type { AudioClipPayload, CompanionEnvelope, ConfirmRequestPayload, Expres
|
|
|
4
4
|
export type WorldStateInput<D = unknown> = Omit<WorldStateEvent<D>, 'specversion' | 'id' | 'source'> & Partial<Pick<WorldStateEvent<D>, 'specversion' | 'id' | 'source'>>;
|
|
5
5
|
import { type CompanionCall, type CompanionCallOptions } from './call.js';
|
|
6
6
|
export interface CompanionClientOptions {
|
|
7
|
-
/** Origin of the Pouchy deployment, e.g. "https://
|
|
7
|
+
/** Origin of the Pouchy deployment, e.g. "https://pouchy.ai". */
|
|
8
8
|
baseUrl: string;
|
|
9
9
|
/** A Pouchy access token (PAT). */
|
|
10
10
|
token: string;
|
|
@@ -126,6 +126,15 @@ export declare class CompanionError extends Error {
|
|
|
126
126
|
constructor(message: string, status: number);
|
|
127
127
|
}
|
|
128
128
|
type Handler = (envelope: CompanionEnvelope) => void;
|
|
129
|
+
/** Token-streaming subscriber (P1-2). `chunk` is a raw text delta of the reply
|
|
130
|
+
* being generated; `meta.reset` (with an empty chunk) tells the renderer to
|
|
131
|
+
* CLEAR the partial text (a hop that streamed text turned out to be tool-call
|
|
132
|
+
* deliberation, not the reply). Deltas are advisory — the final
|
|
133
|
+
* companion.message (fired through onMessage as usual) is authoritative and
|
|
134
|
+
* should replace whatever the deltas rendered. */
|
|
135
|
+
type DeltaHandler = (chunk: string, meta: {
|
|
136
|
+
reset?: boolean;
|
|
137
|
+
}) => void;
|
|
129
138
|
export declare class CompanionClient {
|
|
130
139
|
private readonly opts;
|
|
131
140
|
private readonly doFetch;
|
|
@@ -135,6 +144,7 @@ export declare class CompanionClient {
|
|
|
135
144
|
private abort;
|
|
136
145
|
private readonly handlers;
|
|
137
146
|
private readonly seen;
|
|
147
|
+
private readonly deltaHandlers;
|
|
138
148
|
private readonly pendingVoiceTools;
|
|
139
149
|
constructor(opts: CompanionClientOptions);
|
|
140
150
|
/** The active session id, or null before connect(). */
|
|
@@ -158,12 +168,31 @@ export declare class CompanionClient {
|
|
|
158
168
|
}>;
|
|
159
169
|
/** Send a user text turn, optionally with images (data URLs) for a multimodal
|
|
160
170
|
* turn — images need the `files` scope. The reply arrives on the event stream
|
|
161
|
-
* as a `companion.message` — subscribe via onMessage()/start() to receive it.
|
|
171
|
+
* as a `companion.message` — subscribe via onMessage()/start() to receive it.
|
|
172
|
+
*
|
|
173
|
+
* TOKEN STREAMING (P1-2): when any onDelta() subscriber is registered (or
|
|
174
|
+
* `opts.stream` is true), the reply streams token-by-token on this very
|
|
175
|
+
* request — delta subscribers fire as chunks arrive, and onMessage still
|
|
176
|
+
* fires exactly ONCE with the authoritative final text (the streamed copy
|
|
177
|
+
* is emitted locally; the event-stream replay of the same envelope is
|
|
178
|
+
* deduplicated by id). Pass `stream: false` to force the buffered path. */
|
|
162
179
|
sendText(text: string, opts?: {
|
|
163
180
|
images?: string[];
|
|
181
|
+
stream?: boolean;
|
|
164
182
|
}): Promise<{
|
|
165
183
|
seq: number | null;
|
|
166
184
|
}>;
|
|
185
|
+
/** The streaming leg of sendText: parse the POST response's SSE frames —
|
|
186
|
+
* `delta` / `reset` → delta subscribers, `done` → resolve (emitting the
|
|
187
|
+
* final envelope locally so onMessage fires immediately and the log replay
|
|
188
|
+
* dedups by id). Falls back to buffered semantics if the server ever
|
|
189
|
+
* responds with plain JSON (e.g. an old deployment). */
|
|
190
|
+
private sendTextStreaming;
|
|
191
|
+
/** Subscribe to token-streaming deltas of the reply (P1-2). Registering a
|
|
192
|
+
* subscriber makes sendText stream automatically. Returns an unsubscribe
|
|
193
|
+
* function. Render chunks as they arrive; on `meta.reset` clear the partial;
|
|
194
|
+
* when onMessage fires, replace the partial with the final text. */
|
|
195
|
+
onDelta(handler: DeltaHandler): () => void;
|
|
167
196
|
/** Push live world-state — a single event or a batch. Retained events
|
|
168
197
|
* (retained:true) coalesce per kind; transient ones carry a salience.
|
|
169
198
|
*
|
package/dist/client.js
CHANGED
|
@@ -37,6 +37,9 @@ export class CompanionClient {
|
|
|
37
37
|
abort = null;
|
|
38
38
|
handlers = new Map();
|
|
39
39
|
seen = new Set();
|
|
40
|
+
// Token-streaming delta subscribers (P1-2). When any are registered,
|
|
41
|
+
// sendText automatically streams the reply.
|
|
42
|
+
deltaHandlers = new Set();
|
|
40
43
|
// Voice tool-calls awaiting the app's sendToolResult, keyed by a synthetic id.
|
|
41
44
|
pendingVoiceTools = new Map();
|
|
42
45
|
constructor(opts) {
|
|
@@ -115,11 +118,98 @@ export class CompanionClient {
|
|
|
115
118
|
}
|
|
116
119
|
/** Send a user text turn, optionally with images (data URLs) for a multimodal
|
|
117
120
|
* turn — images need the `files` scope. The reply arrives on the event stream
|
|
118
|
-
* as a `companion.message` — subscribe via onMessage()/start() to receive it.
|
|
121
|
+
* as a `companion.message` — subscribe via onMessage()/start() to receive it.
|
|
122
|
+
*
|
|
123
|
+
* TOKEN STREAMING (P1-2): when any onDelta() subscriber is registered (or
|
|
124
|
+
* `opts.stream` is true), the reply streams token-by-token on this very
|
|
125
|
+
* request — delta subscribers fire as chunks arrive, and onMessage still
|
|
126
|
+
* fires exactly ONCE with the authoritative final text (the streamed copy
|
|
127
|
+
* is emitted locally; the event-stream replay of the same envelope is
|
|
128
|
+
* deduplicated by id). Pass `stream: false` to force the buffered path. */
|
|
119
129
|
async sendText(text, opts) {
|
|
120
130
|
const id = this.requireSession();
|
|
121
|
-
const
|
|
122
|
-
|
|
131
|
+
const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
|
|
132
|
+
const body = { text, ...(opts?.images?.length ? { images: opts.images } : {}) };
|
|
133
|
+
if (!wantStream) {
|
|
134
|
+
const json = await this.postJson(`/api/companion/session/${id}/input`, body);
|
|
135
|
+
return { seq: json.seq ?? null };
|
|
136
|
+
}
|
|
137
|
+
return this.sendTextStreaming(id, body);
|
|
138
|
+
}
|
|
139
|
+
/** The streaming leg of sendText: parse the POST response's SSE frames —
|
|
140
|
+
* `delta` / `reset` → delta subscribers, `done` → resolve (emitting the
|
|
141
|
+
* final envelope locally so onMessage fires immediately and the log replay
|
|
142
|
+
* dedups by id). Falls back to buffered semantics if the server ever
|
|
143
|
+
* responds with plain JSON (e.g. an old deployment). */
|
|
144
|
+
async sendTextStreaming(sessionId, body) {
|
|
145
|
+
const res = await this.doFetch(this.url(`/api/companion/session/${sessionId}/input`), {
|
|
146
|
+
method: 'POST',
|
|
147
|
+
headers: this.jsonHeaders(),
|
|
148
|
+
body: JSON.stringify({ ...body, stream: true })
|
|
149
|
+
});
|
|
150
|
+
const ctype = res.headers.get('content-type') ?? '';
|
|
151
|
+
if (!ctype.includes('text/event-stream')) {
|
|
152
|
+
// Old server / error before the stream opened — behave like postJson.
|
|
153
|
+
const json = (await res.json().catch(() => null));
|
|
154
|
+
if (!res.ok || !json || json.ok === false) {
|
|
155
|
+
throw new CompanionError(json?.error || `request failed (${res.status})`, res.status);
|
|
156
|
+
}
|
|
157
|
+
return { seq: json.seq ?? null };
|
|
158
|
+
}
|
|
159
|
+
if (!res.body)
|
|
160
|
+
throw new CompanionError('streaming response has no body', 502);
|
|
161
|
+
const reader = res.body.getReader();
|
|
162
|
+
const decoder = new TextDecoder();
|
|
163
|
+
let buf = '';
|
|
164
|
+
const fireDelta = (chunk, meta) => {
|
|
165
|
+
for (const h of this.deltaHandlers)
|
|
166
|
+
h(chunk, meta);
|
|
167
|
+
};
|
|
168
|
+
for (;;) {
|
|
169
|
+
const { done, value } = await reader.read();
|
|
170
|
+
if (value)
|
|
171
|
+
buf += decoder.decode(value, { stream: true });
|
|
172
|
+
const { events, rest } = parseSse(buf);
|
|
173
|
+
buf = rest;
|
|
174
|
+
for (const ev of events) {
|
|
175
|
+
if (ev.event === 'delta') {
|
|
176
|
+
const d = JSON.parse(ev.data);
|
|
177
|
+
if (typeof d.text === 'string' && d.text)
|
|
178
|
+
fireDelta(d.text, {});
|
|
179
|
+
}
|
|
180
|
+
else if (ev.event === 'reset') {
|
|
181
|
+
fireDelta('', { reset: true });
|
|
182
|
+
}
|
|
183
|
+
else if (ev.event === 'done') {
|
|
184
|
+
const d = JSON.parse(ev.data);
|
|
185
|
+
try {
|
|
186
|
+
await reader.cancel();
|
|
187
|
+
}
|
|
188
|
+
catch {
|
|
189
|
+
/* stream already finished */
|
|
190
|
+
}
|
|
191
|
+
if (d.ok === false) {
|
|
192
|
+
throw new CompanionError(d.error || 'turn failed', d.status ?? 500);
|
|
193
|
+
}
|
|
194
|
+
// Emit the final envelope NOW so onMessage fires with zero poll
|
|
195
|
+
// latency; the event-stream replay of the same id is deduped.
|
|
196
|
+
if (d.envelope && typeof d.envelope === 'object')
|
|
197
|
+
this.emit(d.envelope);
|
|
198
|
+
return { seq: d.seq ?? null };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
if (done)
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
throw new CompanionError('stream ended without a done frame', 502);
|
|
205
|
+
}
|
|
206
|
+
/** Subscribe to token-streaming deltas of the reply (P1-2). Registering a
|
|
207
|
+
* subscriber makes sendText stream automatically. Returns an unsubscribe
|
|
208
|
+
* function. Render chunks as they arrive; on `meta.reset` clear the partial;
|
|
209
|
+
* when onMessage fires, replace the partial with the final text. */
|
|
210
|
+
onDelta(handler) {
|
|
211
|
+
this.deltaHandlers.add(handler);
|
|
212
|
+
return () => this.deltaHandlers.delete(handler);
|
|
123
213
|
}
|
|
124
214
|
/** Push live world-state — a single event or a batch. Retained events
|
|
125
215
|
* (retained:true) coalesce per kind; transient ones carry a salience.
|
package/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// surface (web / app / game / hardware) over the REST/SSE plane.
|
|
3
3
|
//
|
|
4
4
|
// import { createCompanion } from '$lib/companion-sdk';
|
|
5
|
-
// const c = createCompanion({ baseUrl: 'https://
|
|
5
|
+
// const c = createCompanion({ baseUrl: 'https://pouchy.ai', token: PAT });
|
|
6
6
|
// await c.connect();
|
|
7
7
|
// c.onMessage((text) => console.log(text));
|
|
8
8
|
// c.start();
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/companion-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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",
|
|
7
|
-
"homepage": "https://
|
|
7
|
+
"homepage": "https://pouchy.ai",
|
|
8
8
|
"repository": {
|
|
9
9
|
"type": "git",
|
|
10
10
|
"url": "https://github.com/oviswang/Pouchy.git",
|