@pouchy_ai/companion-sdk 0.40.1 → 0.42.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/src/client.ts CHANGED
@@ -221,6 +221,12 @@ export type CallCredentials =
221
221
  /** First-message override ('' = suppress the canned greeting so the call
222
222
  * opens in-character / context-aware). Pass as overrides.agent.firstMessage. */
223
223
  firstMessage?: string;
224
+ /** Generation handle of the call-active window this mint stamped (0.41.0).
225
+ * Self-plumbed integrations: pass it to endSession({ callGen }) at call
226
+ * end so the server clears exactly this call's window — a stale handle's
227
+ * late teardown then can't clear a newer call's window. connectCall()
228
+ * handles this automatically. */
229
+ callGen?: string;
224
230
  }
225
231
  | {
226
232
  provider: 'openai-realtime';
@@ -229,6 +235,8 @@ export type CallCredentials =
229
235
  model: string;
230
236
  voice: string;
231
237
  expiresAt: number | null;
238
+ /** See the elevenlabs-convai branch — same window-generation handle. */
239
+ callGen?: string;
232
240
  };
233
241
 
234
242
  export interface RecalledMemory {
@@ -460,6 +468,12 @@ export class CompanionClient {
460
468
  private readonly doFetch: typeof fetch;
461
469
  private _session: string | null = null;
462
470
  private cursor = 0;
471
+ /** Generation handle of the last call-active window THIS client minted
472
+ * (startCall → server callGen, 0.41.0). endSession sends it by default so
473
+ * the server's window clear is fenced to our own call — and sends an
474
+ * explicit null when this client never minted one, so a text client's
475
+ * close() can no longer kill a live call's window on the same session. */
476
+ private lastCallGen: string | null = null;
463
477
  /** Pause-orphaned call replay fired (0.37.0) — once per client instance, so
464
478
  * a manual second connect() can't double-deliver to a live handler. */
465
479
  private replayedPendingToolCalls = false;
@@ -1442,7 +1456,13 @@ export class CompanionClient {
1442
1456
  async sendWorldState(
1443
1457
  event: WorldStateInput | WorldStateInput[],
1444
1458
  opts?: { signal?: AbortSignal }
1445
- ): Promise<{ accepted: number; dropped: number; injected: number; reacted: boolean }> {
1459
+ ): Promise<{
1460
+ accepted: number;
1461
+ dropped: number;
1462
+ injected: number;
1463
+ reacted: boolean;
1464
+ duplicates: number;
1465
+ }> {
1446
1466
  const id = this.requireSession();
1447
1467
  const fill = (e: WorldStateInput): WorldStateEvent => ({
1448
1468
  ...e,
@@ -1456,14 +1476,20 @@ export class CompanionClient {
1456
1476
  dropped: number;
1457
1477
  injected?: number;
1458
1478
  reacted?: boolean;
1479
+ duplicates?: number;
1459
1480
  }>(`/api/companion/session/${id}/context`, body, opts?.signal);
1460
1481
  // injected/reacted tell the host a live-call inject or a proactive text
1461
1482
  // reaction was triggered by this batch (0.28.0 — previously discarded).
1483
+ // duplicates (0.41.0) is how much of `accepted` was replay dedup: the
1484
+ // server is envelope-id idempotent, so stamp stable `id`s on host-level
1485
+ // retries and a re-sent batch folds nothing while reporting the same
1486
+ // accepted count.
1462
1487
  return {
1463
1488
  accepted: json.accepted ?? 0,
1464
1489
  dropped: json.dropped ?? 0,
1465
1490
  injected: json.injected ?? 0,
1466
- reacted: json.reacted === true
1491
+ reacted: json.reacted === true,
1492
+ duplicates: json.duplicates ?? 0
1467
1493
  };
1468
1494
  }
1469
1495
 
@@ -1487,11 +1513,15 @@ export class CompanionClient {
1487
1513
  opts?: { voice?: string; locale?: string; signal?: AbortSignal }
1488
1514
  ): Promise<CallCredentials> {
1489
1515
  const id = this.requireSession();
1490
- return this.postJson<CallCredentials>(
1516
+ const creds = await this.postJson<CallCredentials>(
1491
1517
  `/api/companion/session/${id}/call`,
1492
1518
  { voice: opts?.voice, locale: opts?.locale },
1493
1519
  opts?.signal
1494
1520
  );
1521
+ // Remember the minted window's generation so a later endSession()/close()
1522
+ // (without an explicit callGen) fences its clear to OUR call (0.41.0).
1523
+ this.lastCallGen = creds.callGen ?? null;
1524
+ return creds;
1495
1525
  }
1496
1526
 
1497
1527
  /** Open a live voice call end-to-end: mints credentials (startCall) and opens a
@@ -1515,12 +1545,17 @@ export class CompanionClient {
1515
1545
  // assigned after the call opens; a connect-phase onError finds the no-op.
1516
1546
  let unsub: () => void = () => {};
1517
1547
  let cleaned = false;
1548
+ // Captured PER HANDLE at mint time (0.41.0): a second connectCall on this
1549
+ // client overwrites lastCallGen, so the first handle's (possibly late)
1550
+ // teardown must carry the gen of ITS OWN mint — the server then no-ops the
1551
+ // clear instead of silencing the new call's window (CR-142 leg d).
1552
+ let mintedGen: string | null = null;
1518
1553
  const cleanup = () => {
1519
1554
  if (cleaned) return;
1520
1555
  cleaned = true;
1521
1556
  unsub();
1522
1557
  // Consolidate this call into durable memory (best-effort, keepalive).
1523
- void this.endSession({ transcript });
1558
+ void this.endSession({ transcript, callGen: mintedGen });
1524
1559
  };
1525
1560
  const wrappedOpts: CompanionCallOptions = {
1526
1561
  ...opts,
@@ -1563,6 +1598,7 @@ export class CompanionClient {
1563
1598
  : undefined;
1564
1599
 
1565
1600
  const creds = await this.startCall({ voice: opts?.voice, locale: opts?.locale });
1601
+ mintedGen = creds.callGen ?? null;
1566
1602
  let call: CompanionCall;
1567
1603
  try {
1568
1604
  call = await openCompanionCall(creds, wrappedOpts, bridge);
@@ -1612,9 +1648,17 @@ export class CompanionClient {
1612
1648
  * beacon — harmless). `facts` is how many memories were consolidated. */
1613
1649
  async endSession(opts?: {
1614
1650
  transcript?: { role: string; text: string }[];
1651
+ /** Which call-active window this teardown owns (0.41.0). A string fences
1652
+ * the server's window clear to that call's mint; `null` says "I hold no
1653
+ * call — leave any live window alone". Defaults to the gen of the last
1654
+ * call THIS client minted (null if none), so a text client's close() no
1655
+ * longer kills a live call's window on the same session. Self-plumbed
1656
+ * integrations pass startCall's `creds.callGen` here at call end. */
1657
+ callGen?: string | null;
1615
1658
  }): Promise<EndSessionResult | null> {
1616
1659
  const id = this._session;
1617
1660
  if (!id) return null;
1661
+ const callGen = opts?.callGen !== undefined ? opts.callGen : this.lastCallGen;
1618
1662
  // Cap the payload (fetch keepalive bodies are size-limited): last 60 lines.
1619
1663
  const transcript = (opts?.transcript ?? [])
1620
1664
  .filter((t) => t && typeof t.text === 'string' && t.text.trim())
@@ -1628,7 +1672,7 @@ export class CompanionClient {
1628
1672
  const res = await this.fetchAuthed(this.url(`/api/companion/session/${id}/end`), {
1629
1673
  method: 'POST',
1630
1674
  headers: this.jsonHeaders(),
1631
- body: JSON.stringify({ transcript }),
1675
+ body: JSON.stringify({ transcript, callGen }),
1632
1676
  keepalive: true
1633
1677
  });
1634
1678
  return (await res.json().catch(() => null)) as EndSessionResult | null;
package/src/index.ts CHANGED
@@ -91,6 +91,21 @@ export {
91
91
  } from './protocol';
92
92
  export type { CompanionErrorCode, ControlErrorCode } from './protocol';
93
93
 
94
+ // Instant UI ⇄ Google A2UI v0.9 projection (0.42.0) — put `toA2UI` in front of
95
+ // any renderer built against the A2UI catalog and it draws Pouchy panels;
96
+ // `fromA2UI` ingests an A2UI surface back into the Instant UI panel shape.
97
+ // The node-type catalog rides along as data for renderer capability checks.
98
+ export { toA2UI, fromA2UI, INSTANT_UI_NODE_TYPES } from './a2ui';
99
+ export type {
100
+ A2UISurface,
101
+ A2UIComponent,
102
+ A2UIValue,
103
+ InstantUIInput,
104
+ DynamicInterface,
105
+ GenuiNode,
106
+ EmbeddedAction
107
+ } from './a2ui';
108
+
94
109
  // The framework-agnostic view controller behind the /svelte and /vue subpath
95
110
  // adapters (and the separate `@pouchy_ai/react` package) — exported from the
96
111
  // root too, so a host on any OTHER framework (or none) can reuse the same