@pouchy_ai/companion-sdk 0.24.0 → 0.25.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 +1 -0
- package/dist/client.d.ts +35 -1
- package/dist/client.js +44 -12
- package/dist/index.d.ts +1 -1
- package/package.json +2 -2
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.25.0] - 2026-07-11
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`sendText(text, { awaitReply: true })` — await the reply as the return
|
|
20
|
+
value.** Resolves with `{ seq, text, envelope }` (a new exported
|
|
21
|
+
`SendTextReply` type) once the turn completes: `text` is the authoritative
|
|
22
|
+
final assistant text (the same value `onMessage` receives), `envelope` the
|
|
23
|
+
full `companion.message` envelope. Request/response hosts — a CLI REPL, an
|
|
24
|
+
HTTP handler, a test — no longer hand-roll the resolve-on-message +
|
|
25
|
+
timeout gate every integration was writing. It rides the same streaming
|
|
26
|
+
request (the terminal `done` frame carries the envelope), so it works
|
|
27
|
+
without `start()`; `onMessage`/`onDelta` subscribers still fire as usual.
|
|
28
|
+
With `stream: false` (or an old server that answers plain JSON) it falls
|
|
29
|
+
back to the event stream — which then does need `start()` — resolving with
|
|
30
|
+
the next `companion.message` or rejecting with a client-side
|
|
31
|
+
`CompanionError` `code: 'reply_timeout'` after `replyTimeoutMs`
|
|
32
|
+
(default 60 000 ms).
|
|
33
|
+
|
|
34
|
+
### Docs
|
|
35
|
+
|
|
36
|
+
- OpenAPI spec corrections (served at `/api/companion/openapi.json`): the
|
|
37
|
+
memory-write body field is `content` (was wrongly documented as `text`),
|
|
38
|
+
knowledge ingest takes `name`/`kind`/`locale` (was `title`), the
|
|
39
|
+
modalities enum is `text|voice|call|files` (`avatar` never existed),
|
|
40
|
+
`GET …/confirm` (pending confirmations) is now documented, the session
|
|
41
|
+
handshake body documents `handles`/`contextKinds`/`visitor`, and memory
|
|
42
|
+
scopes use the real namespaced strings (`memory.read:app` …).
|
|
43
|
+
- The `/sdk` reference described the call handle as having `.end()` — the
|
|
44
|
+
method is `close()`.
|
|
45
|
+
|
|
15
46
|
## [0.24.0] - 2026-07-11
|
|
16
47
|
|
|
17
48
|
### Added
|
package/README.md
CHANGED
|
@@ -96,6 +96,7 @@ local/device skills).
|
|
|
96
96
|
|
|
97
97
|
| Method | What it does |
|
|
98
98
|
|---|---|
|
|
99
|
+
| `sendText(text, { awaitReply: true, replyTimeoutMs? })` | Request/response mode: the promise resolves with the completed turn's reply `{ seq, text, envelope }` (`SendTextReply`) — no `onMessage` wiring or hand-rolled turn gate. Works without `start()` (the reply rides the same streaming request); only the `stream: false` / old-server fallback needs the event stream, and it rejects with `code: 'reply_timeout'` after `replyTimeoutMs` (default 60s). |
|
|
99
100
|
| `recall({ query?, limit? })` / `remember({ content, … })` | Read / write the token-scoped memory namespace. `query` runs SEMANTIC search over the facts server-side (embedding-based, ranked fallback); omit it for the plain importance/recency ranking. |
|
|
100
101
|
| `ingestKnowledge({ text, name, … })` / `ingestFile(file)` | Push documents into the user's knowledge base ("My materials"); needs `memory.write:core`. |
|
|
101
102
|
| `history({ limit? })` | Fetch the session transcript — restore chat on reload. |
|
package/dist/client.d.ts
CHANGED
|
@@ -181,6 +181,15 @@ export interface EndSessionResult {
|
|
|
181
181
|
facts?: number;
|
|
182
182
|
skipped?: string;
|
|
183
183
|
}
|
|
184
|
+
/** What `sendText(text, { awaitReply: true })` resolves with: the completed
|
|
185
|
+
* turn's reply. `text` is the authoritative final assistant text (same value
|
|
186
|
+
* onMessage receives); `envelope` is the full `companion.message` envelope
|
|
187
|
+
* for hosts that want the id/ts. */
|
|
188
|
+
export interface SendTextReply {
|
|
189
|
+
seq: number | null;
|
|
190
|
+
text: string;
|
|
191
|
+
envelope: CompanionEnvelope;
|
|
192
|
+
}
|
|
184
193
|
type Handler = (envelope: CompanionEnvelope) => void;
|
|
185
194
|
/** Token-streaming subscriber (P1-2). `chunk` is a raw text delta of the reply
|
|
186
195
|
* being generated; `meta.reset` (with an empty chunk) tells the renderer to
|
|
@@ -244,13 +253,38 @@ export declare class CompanionClient {
|
|
|
244
253
|
* request — delta subscribers fire as chunks arrive, and onMessage still
|
|
245
254
|
* fires exactly ONCE with the authoritative final text (the streamed copy
|
|
246
255
|
* is emitted locally; the event-stream replay of the same envelope is
|
|
247
|
-
* deduplicated by id). Pass `stream: false` to force the buffered path.
|
|
256
|
+
* deduplicated by id). Pass `stream: false` to force the buffered path.
|
|
257
|
+
*
|
|
258
|
+
* AWAIT THE REPLY (request/response hosts — a REPL, an HTTP handler, a
|
|
259
|
+
* test): pass `awaitReply: true` and the promise resolves with the final
|
|
260
|
+
* reply `{ seq, text, envelope }` — no onMessage wiring, no hand-rolled
|
|
261
|
+
* turn gate. It rides the same streaming request (the terminal `done`
|
|
262
|
+
* frame carries the authoritative envelope), so it works without start();
|
|
263
|
+
* onMessage/onDelta subscribers still fire as usual. Only when the reply
|
|
264
|
+
* can't ride the request (`stream: false`, or an old server) does it fall
|
|
265
|
+
* back to the event stream — which then DOES need start() — resolving with
|
|
266
|
+
* the next `companion.message`, or rejecting with code `reply_timeout`
|
|
267
|
+
* after `replyTimeoutMs` (default 60s). */
|
|
268
|
+
sendText(text: string, opts: {
|
|
269
|
+
awaitReply: true;
|
|
270
|
+
images?: string[];
|
|
271
|
+
stream?: boolean;
|
|
272
|
+
replyTimeoutMs?: number;
|
|
273
|
+
}): Promise<SendTextReply>;
|
|
248
274
|
sendText(text: string, opts?: {
|
|
249
275
|
images?: string[];
|
|
250
276
|
stream?: boolean;
|
|
277
|
+
awaitReply?: boolean;
|
|
278
|
+
replyTimeoutMs?: number;
|
|
251
279
|
}): Promise<{
|
|
252
280
|
seq: number | null;
|
|
253
281
|
}>;
|
|
282
|
+
/** The awaitReply leg of sendText. Prefers the in-request answer (the
|
|
283
|
+
* streaming `done` frame's envelope); falls back to the next
|
|
284
|
+
* `companion.message` off the event stream, bounded by a timeout. The
|
|
285
|
+
* fallback subscription opens BEFORE the POST so a fast event-stream reply
|
|
286
|
+
* can't slip through the gap. */
|
|
287
|
+
private sendTextAwaitingReply;
|
|
254
288
|
/** The streaming leg of sendText: parse the POST response's SSE frames —
|
|
255
289
|
* `delta` / `reset` → delta subscribers, `done` → resolve (emitting the
|
|
256
290
|
* final envelope locally so onMessage fires immediately and the log replay
|
package/dist/client.js
CHANGED
|
@@ -177,26 +177,58 @@ export class CompanionClient {
|
|
|
177
177
|
});
|
|
178
178
|
return { pairId: json.pairId ?? null };
|
|
179
179
|
}
|
|
180
|
-
/** Send a user text turn, optionally with images (data URLs) for a multimodal
|
|
181
|
-
* turn — images need the `files` scope. The reply arrives on the event stream
|
|
182
|
-
* as a `companion.message` — subscribe via onMessage()/start() to receive it.
|
|
183
|
-
*
|
|
184
|
-
* TOKEN STREAMING (P1-2): when any onDelta() subscriber is registered (or
|
|
185
|
-
* `opts.stream` is true), the reply streams token-by-token on this very
|
|
186
|
-
* request — delta subscribers fire as chunks arrive, and onMessage still
|
|
187
|
-
* fires exactly ONCE with the authoritative final text (the streamed copy
|
|
188
|
-
* is emitted locally; the event-stream replay of the same envelope is
|
|
189
|
-
* deduplicated by id). Pass `stream: false` to force the buffered path. */
|
|
190
180
|
async sendText(text, opts) {
|
|
191
181
|
const id = this.requireSession();
|
|
192
|
-
const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
|
|
193
182
|
const body = { text, ...(opts?.images?.length ? { images: opts.images } : {}) };
|
|
183
|
+
if (opts?.awaitReply)
|
|
184
|
+
return this.sendTextAwaitingReply(id, body, opts);
|
|
185
|
+
const wantStream = opts?.stream ?? this.deltaHandlers.size > 0;
|
|
194
186
|
if (!wantStream) {
|
|
195
187
|
const json = await this.postJson(`/api/companion/session/${id}/input`, body);
|
|
196
188
|
return { seq: json.seq ?? null };
|
|
197
189
|
}
|
|
198
190
|
return this.sendTextStreaming(id, body);
|
|
199
191
|
}
|
|
192
|
+
/** The awaitReply leg of sendText. Prefers the in-request answer (the
|
|
193
|
+
* streaming `done` frame's envelope); falls back to the next
|
|
194
|
+
* `companion.message` off the event stream, bounded by a timeout. The
|
|
195
|
+
* fallback subscription opens BEFORE the POST so a fast event-stream reply
|
|
196
|
+
* can't slip through the gap. */
|
|
197
|
+
async sendTextAwaitingReply(sessionId, body, opts) {
|
|
198
|
+
const timeoutMs = opts?.replyTimeoutMs ?? 60_000;
|
|
199
|
+
let resolveFallback;
|
|
200
|
+
const fallback = new Promise((res) => (resolveFallback = res));
|
|
201
|
+
const off = this.on('companion.message', (env) => resolveFallback(env));
|
|
202
|
+
let timer;
|
|
203
|
+
try {
|
|
204
|
+
let seq = null;
|
|
205
|
+
let envelope;
|
|
206
|
+
if (opts?.stream === false) {
|
|
207
|
+
const json = await this.postJson(`/api/companion/session/${sessionId}/input`, body);
|
|
208
|
+
seq = json.seq ?? null;
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const r = await this.sendTextStreaming(sessionId, body);
|
|
212
|
+
seq = r.seq;
|
|
213
|
+
envelope = r.envelope;
|
|
214
|
+
}
|
|
215
|
+
if (!envelope) {
|
|
216
|
+
envelope = await Promise.race([
|
|
217
|
+
fallback,
|
|
218
|
+
new Promise((_, reject) => {
|
|
219
|
+
timer = setTimeout(() => reject(new CompanionError(`timed out after ${timeoutMs}ms waiting for the reply — buffered mode needs start() polling the event stream`, 0, 'reply_timeout')), timeoutMs);
|
|
220
|
+
})
|
|
221
|
+
]);
|
|
222
|
+
}
|
|
223
|
+
const replyText = envelope.payload?.text;
|
|
224
|
+
return { seq, text: typeof replyText === 'string' ? replyText : '', envelope };
|
|
225
|
+
}
|
|
226
|
+
finally {
|
|
227
|
+
off();
|
|
228
|
+
if (timer !== undefined)
|
|
229
|
+
clearTimeout(timer);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
200
232
|
/** The streaming leg of sendText: parse the POST response's SSE frames —
|
|
201
233
|
* `delta` / `reset` → delta subscribers, `done` → resolve (emitting the
|
|
202
234
|
* final envelope locally so onMessage fires immediately and the log replay
|
|
@@ -256,7 +288,7 @@ export class CompanionClient {
|
|
|
256
288
|
// latency; the event-stream replay of the same id is deduped.
|
|
257
289
|
if (d.envelope && typeof d.envelope === 'object')
|
|
258
290
|
this.emit(d.envelope);
|
|
259
|
-
return { seq: d.seq ?? null };
|
|
291
|
+
return { seq: d.seq ?? null, envelope: d.envelope ?? undefined };
|
|
260
292
|
}
|
|
261
293
|
}
|
|
262
294
|
if (done)
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CompanionClient, type CompanionClientOptions } from './client.js';
|
|
2
2
|
export { CompanionClient, CompanionError, pouchyBrandIconUrl } from './client.js';
|
|
3
|
-
export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult } from './client.js';
|
|
3
|
+
export type { CompanionClientOptions, HelloAck, RecalledMemory, CompanionTurn, CallCredentials, CompanionToolDecl, ToolCallEvent, WorldStateInput, CompanionAvatar, CompanionWallet, WalletBalance, BrandIconSize, EndSessionResult, SendTextReply } 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
6
|
export { COMPANION_SCOPES, COMPANION_MODALITIES, SENSITIVE_SCOPES, REPRESENT_SCOPES, DEFAULT_SCOPES, DEFAULT_MODALITIES, isSensitiveScope, isRepresentScope, hasScope } from './scopes.js';
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pouchy_ai/companion-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Embed the Pouchy companion
|
|
3
|
+
"version": "0.25.0",
|
|
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
7
|
"homepage": "https://pouchy.ai",
|