@pouchy_ai/companion-sdk 0.12.1 → 0.13.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 +19 -1
- package/README.md +23 -3
- 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,24 @@ a protocol bump is always called out explicitly here.
|
|
|
12
12
|
|
|
13
13
|
## [Unreleased]
|
|
14
14
|
|
|
15
|
+
## [0.13.0] - 2026-07-06
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **Token streaming** — the reply can now render as it is generated instead
|
|
20
|
+
of arriving as one block. Register `onDelta((chunk, meta) => …)` and
|
|
21
|
+
`sendText` automatically streams: the POST response itself carries SSE
|
|
22
|
+
`delta` frames (raw text chunks), an optional `reset` (a hop that streamed
|
|
23
|
+
text turned out to be tool-call deliberation — clear the partial render),
|
|
24
|
+
and a final `done` frame. `onMessage` still fires exactly ONCE with the
|
|
25
|
+
authoritative final text (the done frame's envelope is emitted locally and
|
|
26
|
+
the event-stream replay of the same envelope id is deduplicated), so
|
|
27
|
+
existing render code keeps working unchanged — deltas are a pure add-on.
|
|
28
|
+
Pass `sendText(text, { stream: false })` to force the buffered path, or
|
|
29
|
+
`{ stream: true }` to stream without a subscriber. Requires a server with
|
|
30
|
+
streaming input support; on older servers the SDK transparently falls back
|
|
31
|
+
to the buffered response. No wire-protocol (envelope) change.
|
|
32
|
+
|
|
15
33
|
## [0.12.1] - 2026-07-05
|
|
16
34
|
|
|
17
35
|
### Added
|
|
@@ -127,7 +145,7 @@ self-hosted/CDN build; this entry captures the surface that ships publicly.
|
|
|
127
145
|
- **Public distribution.** Installable from the public npm registry as
|
|
128
146
|
`@pouchy_ai/companion-sdk`; also resolvable via `esm.sh` / `jsDelivr` for
|
|
129
147
|
no-bundler / import-map consumers, and self-hosted at
|
|
130
|
-
`https://
|
|
148
|
+
`https://pouchy.ai/sdk/companion-sdk.js`.
|
|
131
149
|
- **Chat.** Text and multimodal (text + images) messaging.
|
|
132
150
|
- **Voice.** Live WebRTC call (`connectCall`), provider-agnostic across OpenAI
|
|
133
151
|
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' },
|
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.13.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",
|