@pouchy_ai/companion-sdk 0.35.0 → 0.37.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 +59 -1
- package/README.md +74 -16
- package/dist/call.d.ts +18 -0
- package/dist/call.d.ts.map +1 -1
- package/dist/call.js +54 -0
- package/dist/call.js.map +1 -1
- package/dist/client.d.ts +27 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +42 -2
- package/dist/client.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/protocol.d.ts +30 -0
- package/dist/protocol.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/call.ts +70 -0
- package/src/client.ts +75 -4
- package/src/index.ts +1 -0
- package/src/protocol.ts +31 -0
package/src/client.ts
CHANGED
|
@@ -22,6 +22,7 @@ import type {
|
|
|
22
22
|
OutboundPayloadMap,
|
|
23
23
|
OutboundType,
|
|
24
24
|
PendingConfirm,
|
|
25
|
+
PendingToolCall,
|
|
25
26
|
RenderInterfacePayload,
|
|
26
27
|
SocialMessagePayload,
|
|
27
28
|
ToolCallPayload,
|
|
@@ -62,7 +63,11 @@ export interface CompanionClientOptions {
|
|
|
62
63
|
surface?: string;
|
|
63
64
|
/** Requested I/O modalities (intersected with the token's grant server-side). */
|
|
64
65
|
modalities?: string[];
|
|
65
|
-
/** Action types this surface can perform (declared at handshake).
|
|
66
|
+
/** Action types this surface can perform (declared at handshake). Stored but
|
|
67
|
+
* never consumed server-side today — a client-side capability hint whose one
|
|
68
|
+
* SDK effect is opting a tool-less embed into the auto-offered host-control
|
|
69
|
+
* verbs on connectCall() voice sessions. It does NOT register tools for text
|
|
70
|
+
* turns: a tool you want called on a text turn must be declared in `tools`. */
|
|
66
71
|
handles?: string[];
|
|
67
72
|
/** World-state kinds this surface emits (declared at handshake). */
|
|
68
73
|
contextKinds?: string[];
|
|
@@ -117,6 +122,19 @@ export interface CompanionClientOptions {
|
|
|
117
122
|
* error responses (4xx/5xx incl. 429) are NEVER queued — the server saw
|
|
118
123
|
* those; they keep the normal throw semantics. Default false. */
|
|
119
124
|
queueOffline?: boolean;
|
|
125
|
+
/** Replay of pause-orphaned tool calls (0.37.0). When `connect()` resumes a
|
|
126
|
+
* session whose turn is paused on tool calls THIS surface never answered
|
|
127
|
+
* (the page reloaded / crashed mid-pause), the handshake carries them as
|
|
128
|
+
* `pendingToolCalls` and the client re-emits each to your onToolCall
|
|
129
|
+
* handler with `replayed: true` — post the results as usual and the paused
|
|
130
|
+
* turn completes instead of being abandoned. Register onToolCall BEFORE
|
|
131
|
+
* calling connect()/start() to receive them. The result apply is
|
|
132
|
+
* idempotent per call id, but the APP-side effect may not be: if a tool
|
|
133
|
+
* has side effects, treat `call.id` as an idempotency key when
|
|
134
|
+
* `replayed` is set. Pass false to disable the re-emit — the calls stay
|
|
135
|
+
* readable on the `HelloAck.pendingToolCalls` you get from connect().
|
|
136
|
+
* Default true. */
|
|
137
|
+
replayPendingToolCalls?: boolean;
|
|
120
138
|
/** Persistence for the offline queue (see `queueOffline`). Defaults to
|
|
121
139
|
* localStorage when available (privacy-mode / quota failures fall back
|
|
122
140
|
* silently), else an in-memory page-lifetime store. The key is
|
|
@@ -146,6 +164,11 @@ export interface HelloAck {
|
|
|
146
164
|
* the owner (their two companions are friends). Lets you skip offering a
|
|
147
165
|
* "pair" affordance to an already-connected visitor. */
|
|
148
166
|
visitorPaired: boolean;
|
|
167
|
+
/** Outstanding tool calls of a turn that paused before this handshake —
|
|
168
|
+
* non-empty only when resuming mid-pause (0.37.0). Unless
|
|
169
|
+
* `replayPendingToolCalls: false`, each is also re-emitted to onToolCall
|
|
170
|
+
* with `replayed: true` right after connect() resolves. */
|
|
171
|
+
pendingToolCalls: PendingToolCall[];
|
|
149
172
|
}
|
|
150
173
|
|
|
151
174
|
/** A tool the embedding declares the companion may ask it to perform. */
|
|
@@ -393,6 +416,9 @@ export class CompanionClient {
|
|
|
393
416
|
private readonly doFetch: typeof fetch;
|
|
394
417
|
private _session: string | null = null;
|
|
395
418
|
private cursor = 0;
|
|
419
|
+
/** Pause-orphaned call replay fired (0.37.0) — once per client instance, so
|
|
420
|
+
* a manual second connect() can't double-deliver to a live handler. */
|
|
421
|
+
private replayedPendingToolCalls = false;
|
|
396
422
|
private streaming = false;
|
|
397
423
|
private abort: AbortController | null = null;
|
|
398
424
|
// Bumped on every start()/stop(): a loop whose generation is stale exits
|
|
@@ -734,6 +760,7 @@ export class CompanionClient {
|
|
|
734
760
|
resumeCursor: number;
|
|
735
761
|
representative?: boolean;
|
|
736
762
|
visitorPaired?: boolean;
|
|
763
|
+
pendingToolCalls?: PendingToolCall[];
|
|
737
764
|
}>('/api/companion/session', {
|
|
738
765
|
surface: this.opts.surface ?? 'default',
|
|
739
766
|
modalities: this.opts.modalities,
|
|
@@ -748,10 +775,44 @@ export class CompanionClient {
|
|
|
748
775
|
// A successful handshake proves reachability — replay any sends queued
|
|
749
776
|
// while offline (0.34.0; no-op when the queue is empty).
|
|
750
777
|
if (this.opts.queueOffline) void this.flushOutbox().catch(() => {});
|
|
778
|
+
const pendingToolCalls = Array.isArray(json.pendingToolCalls)
|
|
779
|
+
? json.pendingToolCalls.filter(
|
|
780
|
+
(c): c is PendingToolCall => !!c && typeof c.id === 'string' && typeof c.name === 'string'
|
|
781
|
+
)
|
|
782
|
+
: [];
|
|
783
|
+
// Pause-orphaned call replay (0.37.0): re-emit each outstanding call to
|
|
784
|
+
// onToolCall so a reloaded embed completes the paused turn. Deferred a
|
|
785
|
+
// tick so `await connect()` callers can still register handlers first;
|
|
786
|
+
// once-per-client so a second connect() (manual re-handshake) can't
|
|
787
|
+
// double-deliver alongside a live handler that already got them. The
|
|
788
|
+
// synthetic envelope id is derived from the callId, so the stream-replay
|
|
789
|
+
// of the ORIGINAL envelope (a client resuming with a persisted cursor)
|
|
790
|
+
// carries a different id and the app-facing `replayed` flag is the
|
|
791
|
+
// dedup signal — see the option's jsdoc on idempotency.
|
|
792
|
+
if (
|
|
793
|
+
pendingToolCalls.length &&
|
|
794
|
+
this.opts.replayPendingToolCalls !== false &&
|
|
795
|
+
!this.replayedPendingToolCalls
|
|
796
|
+
) {
|
|
797
|
+
this.replayedPendingToolCalls = true;
|
|
798
|
+
setTimeout(() => {
|
|
799
|
+
for (const c of pendingToolCalls) {
|
|
800
|
+
this.emit({
|
|
801
|
+
v: PROTOCOL_VERSION,
|
|
802
|
+
id: `replay_${c.id}`,
|
|
803
|
+
ts: Date.now(),
|
|
804
|
+
session: json.session,
|
|
805
|
+
type: 'companion.tool_call',
|
|
806
|
+
payload: { id: c.id, name: c.name, args: c.args ?? '', replayed: true }
|
|
807
|
+
} as CompanionEnvelope);
|
|
808
|
+
}
|
|
809
|
+
}, 0);
|
|
810
|
+
}
|
|
751
811
|
return {
|
|
752
812
|
...json,
|
|
753
813
|
representative: json.representative ?? false,
|
|
754
|
-
visitorPaired: json.visitorPaired ?? false
|
|
814
|
+
visitorPaired: json.visitorPaired ?? false,
|
|
815
|
+
pendingToolCalls
|
|
755
816
|
};
|
|
756
817
|
}
|
|
757
818
|
|
|
@@ -1576,10 +1637,20 @@ export class CompanionClient {
|
|
|
1576
1637
|
handler: (call: ToolCallEvent, envelope: CompanionEnvelope) => void
|
|
1577
1638
|
): () => void {
|
|
1578
1639
|
return this.on('companion.tool_call', (env) => {
|
|
1579
|
-
const p = env.payload as { id?: unknown; name?: unknown; args?: unknown };
|
|
1640
|
+
const p = env.payload as { id?: unknown; name?: unknown; args?: unknown; replayed?: unknown };
|
|
1580
1641
|
if (typeof p?.id === 'string' && typeof p?.name === 'string') {
|
|
1581
1642
|
const args = typeof p.args === 'string' ? p.args : '';
|
|
1582
|
-
handler(
|
|
1643
|
+
handler(
|
|
1644
|
+
{
|
|
1645
|
+
id: p.id,
|
|
1646
|
+
name: p.name,
|
|
1647
|
+
args,
|
|
1648
|
+
argsJson: parseArgsJson(args),
|
|
1649
|
+
// Pause-orphaned replay (0.37.0) — see replayPendingToolCalls.
|
|
1650
|
+
...(p.replayed === true ? { replayed: true } : {})
|
|
1651
|
+
},
|
|
1652
|
+
env
|
|
1653
|
+
);
|
|
1583
1654
|
}
|
|
1584
1655
|
});
|
|
1585
1656
|
}
|
package/src/index.ts
CHANGED
package/src/protocol.ts
CHANGED
|
@@ -161,6 +161,32 @@ export interface ToolCallPayload {
|
|
|
161
161
|
name: string;
|
|
162
162
|
/** The model's arguments as a raw JSON string. */
|
|
163
163
|
args: string;
|
|
164
|
+
/** True when this delivery is a REPLAY of a still-outstanding call from a
|
|
165
|
+
* turn that paused before a reconnect (see hello.ack `pendingToolCalls`) —
|
|
166
|
+
* the app may have already performed it and crashed before posting the
|
|
167
|
+
* result. Side-effecting tools should treat `id` as an idempotency key.
|
|
168
|
+
* Absent on the original live delivery. */
|
|
169
|
+
replayed?: boolean;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** One outstanding tool call from a turn that paused BEFORE this handshake —
|
|
173
|
+
* carried on `hello.ack` (0.37.0) so an embed that lost its state (tab
|
|
174
|
+
* reload / crash mid-pause) can rediscover what it owes and COMPLETE the
|
|
175
|
+
* paused turn instead of abandoning it via `/end`. Post each result with
|
|
176
|
+
* `sendToolResult(id, …)` exactly like a live `companion.tool_call`; the
|
|
177
|
+
* server's apply is idempotent per id, so double-posting is safe. */
|
|
178
|
+
export interface PendingToolCall {
|
|
179
|
+
/** Correlation id — echo it back on the tool result. */
|
|
180
|
+
id: string;
|
|
181
|
+
/** The declared tool name the model chose. */
|
|
182
|
+
name: string;
|
|
183
|
+
/** The model's arguments as a raw JSON string. */
|
|
184
|
+
args: string;
|
|
185
|
+
/** Correlation id of the paused turn (when the pausing client minted one). */
|
|
186
|
+
turnId?: string;
|
|
187
|
+
/** Epoch ms the turn paused / last progressed — judge staleness before
|
|
188
|
+
* replaying a side-effecting call. */
|
|
189
|
+
pausedAt?: number;
|
|
164
190
|
}
|
|
165
191
|
|
|
166
192
|
/** Payload of a `companion.ui_update` event — a live change to an
|
|
@@ -283,6 +309,11 @@ export interface HelloAckPayload {
|
|
|
283
309
|
resumeCursor?: number;
|
|
284
310
|
representative?: boolean;
|
|
285
311
|
visitorPaired?: boolean;
|
|
312
|
+
/** Outstanding tool calls from a turn that paused before this handshake —
|
|
313
|
+
* present (non-empty) only when resuming a session mid-pause. See
|
|
314
|
+
* PendingToolCall; the SDK re-emits them to onToolCall with
|
|
315
|
+
* `replayed: true` unless `replayPendingToolCalls: false`. */
|
|
316
|
+
pendingToolCalls?: PendingToolCall[];
|
|
286
317
|
}
|
|
287
318
|
|
|
288
319
|
/** Payload of a `control.call_ready` event — the stream echo of `start_call`'s
|