ai.pouchy.companion 0.1.0-preview.10
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 +346 -0
- package/LICENSE +42 -0
- package/README.md +269 -0
- package/Runtime/CompanionClient.cs +856 -0
- package/Runtime/CompanionError.cs +48 -0
- package/Runtime/EventStreamPump.cs +171 -0
- package/Runtime/Models.cs +476 -0
- package/Runtime/Pouchy.Companion.asmdef +14 -0
- package/Runtime/PouchyCompanionBehaviour.cs +153 -0
- package/Runtime/Protocol.cs +139 -0
- package/package.json +35 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to the Pouchy Companion Unity/C# SDK are documented here.
|
|
4
|
+
This project adheres to [Semantic Versioning](https://semver.org).
|
|
5
|
+
|
|
6
|
+
> Parity note — **CORRECTED in 0.1.0-preview.6 (audit round 490)**. This note
|
|
7
|
+
> used to read: "the JS SDK 0.35.0 per-request header deadline
|
|
8
|
+
> (`requestTimeoutMs`) needs no port here — every request already runs under
|
|
9
|
+
> `CompanionClientOptions.Timeout`". That stated the wrong conclusion as
|
|
10
|
+
> settled. Running under *a* deadline was never the question; the point of the
|
|
11
|
+
> JS work is that the flat default is RAISED on the routes whose server handler
|
|
12
|
+
> declares `maxDuration: 300`, and this client's flat 90s was BELOW that ceiling
|
|
13
|
+
> on three of them. Ported in 0.1.0-preview.6 as
|
|
14
|
+
> `CompanionClientOptions.LongWorkTimeout`. Recorded here rather than rewritten
|
|
15
|
+
> away, because a parity ledger that quietly edits its own wrong entries is not
|
|
16
|
+
> auditable.
|
|
17
|
+
|
|
18
|
+
> Parity ledger — JS surfaces not yet ported (recorded audit round 26, so the
|
|
19
|
+
> gap is a decision, not drift): `handles`/`contextKinds` hello options (the
|
|
20
|
+
> game track's core play-along declaration — the natural NEXT port here),
|
|
21
|
+
> representative mode (`visitor` + `pairVisitor`), `GetAvatar` (a Unity host
|
|
22
|
+
> rendering the companion is a primary consumer), and binary `IngestFile`
|
|
23
|
+
> (`IngestKnowledgeAsync` text is covered). All are single REST calls — port
|
|
24
|
+
> on demand.
|
|
25
|
+
>
|
|
26
|
+
> Added audit round 32: JS `setToken` (swap the bearer token on a live client
|
|
27
|
+
> after ~1h session-token expiry) is also unported — `_token` is readonly
|
|
28
|
+
> here, so a Unity host currently reconnects with a fresh client instead (the
|
|
29
|
+
> session survives: a fresh token for the same user resumes the same session).
|
|
30
|
+
> Python gained `set_token()` this round; port here on demand.
|
|
31
|
+
>
|
|
32
|
+
> Added audit round 361: `EndSessionAsync()` cannot carry the VOICE `transcript`
|
|
33
|
+
> (nor `callGen`) — it posts an empty object. Voice audio is provider-direct, so
|
|
34
|
+
> that field is the only path from anything said aloud into long-term memory. A
|
|
35
|
+
> Unity host is a PRIMARY consumer here (a game that runs its own voice call and
|
|
36
|
+
> uses this client for the session plane loses the entire call at hangup — the
|
|
37
|
+
> server answers `skipped: 'no_content'`). The Dart SDK closed this in
|
|
38
|
+
> `pouchy_companion` 0.1.1; port here on demand — one optional argument, and the
|
|
39
|
+
> wire field has always existed.
|
|
40
|
+
|
|
41
|
+
## 0.1.0-preview.10 — 2026-08-21
|
|
42
|
+
|
|
43
|
+
- **The RAW client's documented reconnect loop no longer hot-loops against a
|
|
44
|
+
gateway that accepts and instantly closes.** preview.9 clamped this shape in
|
|
45
|
+
`EventStreamPump` — and the README's raw-client idiom one paragraph earlier
|
|
46
|
+
("call `StreamEventsAsync` again"; the Go/Python caller-owned-loop contract)
|
|
47
|
+
stayed un-clamped, exactly the carrier class #523 closed for Python/Go/Dart.
|
|
48
|
+
`StreamEventsAsync` now sleeps an escalating 1→2→4→8s before opening a
|
|
49
|
+
window whenever the previous window ended cleanly, delivered **zero
|
|
50
|
+
envelopes**, and lived under 2s from headers-received — a shape a healthy
|
|
51
|
+
origin cannot produce (every real window opens with `hello.ack` and closes
|
|
52
|
+
at ~45s). Any delivered envelope clears the ladder at delivery; an errored
|
|
53
|
+
window leaves it alone (errors still throw immediately, and backing off on
|
|
54
|
+
errors is already the caller's documented job). The bound is 2s
|
|
55
|
+
post-headers — the JS client's stamp — which preview.9's pump could not
|
|
56
|
+
use from outside the client; the pump keeps its coarser 5s
|
|
57
|
+
handshake-inclusive guard, and when both fire the delays add (bounded).
|
|
58
|
+
|
|
59
|
+
## 0.1.0-preview.9 — 2026-08-21
|
|
60
|
+
|
|
61
|
+
- **`EventStreamPump` no longer hot-loops against a gateway that accepts and
|
|
62
|
+
instantly closes.** preview.8's policy — "clean window close → reconnect
|
|
63
|
+
immediately" — was written for the server's designed ~45s cadence, but it
|
|
64
|
+
also fired on a clean close a gateway produced in milliseconds
|
|
65
|
+
(auth-as-close, an LB idle policy, a misconfigured proxy). Every reconnect
|
|
66
|
+
got the same instant close, so the pump reconnected in a zero-delay hot
|
|
67
|
+
loop: unbounded requests against the gateway and a pinned core on the
|
|
68
|
+
device. This is the exact shape the JS client's WS leg has guarded since
|
|
69
|
+
its 0.35.0 short-lived-connection clamp; the pump claimed to mirror that
|
|
70
|
+
clamp and ported only the error half. Now a clean close that delivered
|
|
71
|
+
**zero events** in under 5s climbs the existing 1→30s ladder (silently —
|
|
72
|
+
a clean close is not an error, so `onError` stays quiet). A window that
|
|
73
|
+
delivered an event, however short, and a zero-event window that ran the
|
|
74
|
+
designed cadence both keep the immediate reconnect, so idle-but-healthy
|
|
75
|
+
subscriptions pay nothing. The bound is 5s where the JS clamp is 2s
|
|
76
|
+
because this measurement includes the handshake (the client exposes no
|
|
77
|
+
headers-received instant); both sit far below the 45s cadence.
|
|
78
|
+
|
|
79
|
+
## 0.1.0-preview.8 — 2026-08-21
|
|
80
|
+
|
|
81
|
+
- **`PouchyCompanionBehaviour` is now an always-on subscription —
|
|
82
|
+
previously it went silent after exactly one stream window (~45s).**
|
|
83
|
+
The raw `StreamEventsAsync` consumes ONE window and returns when the
|
|
84
|
+
server closes it, which it does cleanly every ~45s by design (the
|
|
85
|
+
go/python idiom: the caller owns the loop). The behaviour called it
|
|
86
|
+
once, so a Unity host received events for 45 seconds and then silently
|
|
87
|
+
nothing, forever — the same user-visible shape as the Go cursor defect
|
|
88
|
+
(#492), reached through a missing loop instead of a poisoned cursor,
|
|
89
|
+
in the one component whose whole point is that a designer wires it in
|
|
90
|
+
the Inspector and never writes the loop themselves.
|
|
91
|
+
- **New: `EventStreamPump.RunAsync(client, onEvent, onError, ct)`** —
|
|
92
|
+
the engine-agnostic reconnect loop the behaviour now rides, public so
|
|
93
|
+
a pure .NET host gets the same always-on subscription without owning
|
|
94
|
+
the policy. Clean window close → immediate reconnect (the cursor has
|
|
95
|
+
advanced; nothing is lost or replayed) and the backoff ladder resets;
|
|
96
|
+
errors surface via `onError` and retry after
|
|
97
|
+
`max(ladder, min(RetryAfter, 60s))` with a 1,2,4,8,16,30s ladder — the
|
|
98
|
+
server can only ever slow the loop down, and a window that delivered
|
|
99
|
+
events resets the ladder (a working-then-dying link is a fresh
|
|
100
|
+
incident, not an escalation). `not_connected` stops the pump: the
|
|
101
|
+
session was ended on purpose and a retry can never mint it back.
|
|
102
|
+
Cancellation returns quietly, teardown noise included; `RunAsync`
|
|
103
|
+
never throws, which is what makes it safe to fire-and-forget from a
|
|
104
|
+
component lifecycle method. Handshake failures in the behaviour remain
|
|
105
|
+
fatal (a bad/expired token cannot be retried into working).
|
|
106
|
+
- Proven in the package's own runner: 6 new `dotnet test` cases
|
|
107
|
+
(reconnect-and-resume-from-cursor, RetryAfter-honouring backoff, the
|
|
108
|
+
60s cap, ladder reset, `not_connected` stop, ladder shape), and four
|
|
109
|
+
hand-run mutations each fail exactly the intended case — removing the
|
|
110
|
+
loop (the defect itself), `min` for `max` on RetryAfter, dropping the
|
|
111
|
+
delivered-reset, dropping the `not_connected` stop. The repo-side
|
|
112
|
+
cross-SDK gate pins the pump policy and pins that the behaviour rides
|
|
113
|
+
the pump rather than the bare one-shot call.
|
|
114
|
+
|
|
115
|
+
## 0.1.0-preview.7 — 2026-08-21
|
|
116
|
+
|
|
117
|
+
- **The event stream bounds byte-SILENCE —
|
|
118
|
+
`CompanionClientOptions.StreamStallTimeout` (default 60s).** A half-open
|
|
119
|
+
TCP drop (a NAT/LB idle-kill eats the FIN; no close ever arrives) left
|
|
120
|
+
`StreamEventsAsync` blocked forever on a socket that would never speak
|
|
121
|
+
again, with every later event silently lost — reproduced against the
|
|
122
|
+
pre-change client (one event delivered, then unbounded silence). The
|
|
123
|
+
server pings every 1.5s and closes each stream window at 45s, so a
|
|
124
|
+
HEALTHY connection is never byte-silent for a minute; 60s clears both
|
|
125
|
+
numbers, the same default and reasoning as the Go SDK's
|
|
126
|
+
`DefaultStreamStallTimeout`, Dart's `kStreamStallTimeout` and Python's
|
|
127
|
+
`read=60.0`. Armed per READ, not per stream — a healthy subscription is
|
|
128
|
+
never cut, and the host's own `onEvent` latency never counts as
|
|
129
|
+
silence. On a stall the call throws `CompanionError` (`Status` 0, code
|
|
130
|
+
`aborted`) whose message says what to do: call `StreamEventsAsync`
|
|
131
|
+
again, it resumes from `Cursor`. Zero/negative
|
|
132
|
+
(`Timeout.InfiniteTimeSpan`) disables the bound — the pre-preview.7
|
|
133
|
+
behaviour. Closes the C# half of CR-600 (the last SDK outside this
|
|
134
|
+
contract).
|
|
135
|
+
- **`CompanionError.RetryAfter` now has a DOMAIN: finite and
|
|
136
|
+
non-negative, on both carriers.** The header parse was
|
|
137
|
+
`Math.Max(0.0, secs)` over a `double.TryParse` that accepts `"NaN"`
|
|
138
|
+
and `"Infinity"` — and `Math.Max` PROPAGATES NaN by specification, so
|
|
139
|
+
a broken upstream's header reached the caller as NaN/Infinity, where
|
|
140
|
+
the README's own handler
|
|
141
|
+
(`Task.Delay(TimeSpan.FromSeconds(e.RetryAfter ?? 1))`) throws inside
|
|
142
|
+
the caller's catch block; a negative header clamped to 0, an instant
|
|
143
|
+
retry into the same 429. The body's `retryAfterSec` had NO domain at
|
|
144
|
+
all — and non-finite is reachable there from a fully COMPLIANT
|
|
145
|
+
producer, because the JSON overflow literal (`1e400`) is legal JSON
|
|
146
|
+
and Json.NET decodes it to `double.PositiveInfinity` (measured,
|
|
147
|
+
Newtonsoft 13.0.3). Both carriers now route through one `Sleepable`
|
|
148
|
+
predicate (the C# spelling of Go's `sleepableSeconds` / Dart's
|
|
149
|
+
`_sleepableSeconds` / Python's `_sleepable_seconds`): an out-of-domain
|
|
150
|
+
body value falls THROUGH to the header instead of suppressing it, an
|
|
151
|
+
out-of-domain header becomes `null`, and an HTTP-date already in the
|
|
152
|
+
past still clamps to 0 (that is clock skew from a compliant origin,
|
|
153
|
+
not corruption). Closes the C# halves of CR-613 and CR-615.
|
|
154
|
+
- **The package gains a behavioural test suite**
|
|
155
|
+
(`dotnet test packages/unity-sdk/ci/tests/Tests.csproj` — net8, xunit,
|
|
156
|
+
no Unity editor needed): 28 cases driving the pure .NET client through
|
|
157
|
+
an injected `HttpClient` stub. Both fixes were reproduced there FIRST
|
|
158
|
+
(12 of 23 Retry-After cases fail against the preview.6 sources; the
|
|
159
|
+
stall hang reproduced at >6s), and four hand-run mutations each fail
|
|
160
|
+
exactly the intended case. The repo-side cross-SDK gates
|
|
161
|
+
(`retry-after.drift.test.ts`, `stream-cursor.drift.test.ts`) now pin
|
|
162
|
+
these tests by name; both of their C#-shaped `unbound`/exception books
|
|
163
|
+
are EMPTY for the first time.
|
|
164
|
+
|
|
165
|
+
## 0.1.0-preview.6 — 2026-08-18
|
|
166
|
+
|
|
167
|
+
- **Route-aware deadlines — `CompanionClientOptions.LongWorkTimeout`
|
|
168
|
+
(default 310s).** The client ran every request under one flat 90s
|
|
169
|
+
`Timeout`, armed as a single `CancellationTokenSource` under
|
|
170
|
+
`HttpCompletionOption.ResponseContentRead`, so it bounded the WHOLE
|
|
171
|
+
exchange. Three companion routes declare `maxDuration: 300` and are
|
|
172
|
+
reached by this client: `POST …/input` on the buffered (non-streaming)
|
|
173
|
+
leg, `POST …/tool-result` (the handler awaits the full billed
|
|
174
|
+
continuation inline), and `POST /api/companion/knowledge` (fetch +
|
|
175
|
+
chunk + embed all happen before the headers). The drive budget alone
|
|
176
|
+
is 240s, so 90s was exceeded by ordinary long turns rather than by
|
|
177
|
+
pathology — a succeeding, BILLED operation surfaced as
|
|
178
|
+
`request_timeout`, and the natural retry then raced the still-running
|
|
179
|
+
first request. `IsLongWork(method, path)` now selects
|
|
180
|
+
`LongWorkTimeout` for exactly those three; everything else keeps the
|
|
181
|
+
90s `Timeout`, including `POST …/confirm`, whose response is honestly
|
|
182
|
+
bounded at ~47s (35s tool box + 12s phrasing hop) and already fits.
|
|
183
|
+
**Additive and non-breaking**: `Timeout` keeps its name, type and
|
|
184
|
+
default, and lowering it for fast failure on ordinary reads no longer
|
|
185
|
+
re-introduces the cut on the three long routes. Same number and same
|
|
186
|
+
reasoning as the Go SDK's `LongWorkTimeout` and the JS SDK's
|
|
187
|
+
`BUFFERED_TURN_TIMEOUT_MS`.
|
|
188
|
+
- **The streaming POST's deadline is disarmed once an SSE body is
|
|
189
|
+
confirmed**, so the options doc's claim about it holds by construction
|
|
190
|
+
rather than by whichever `HttpClientHandler` is underneath. Measured
|
|
191
|
+
on .NET 8 / `SocketsHttpHandler`: the fired token did NOT cut the body
|
|
192
|
+
(the read never receives it), so this was not an observed break there
|
|
193
|
+
— but Unity ships Mono's handler, which is a different implementation,
|
|
194
|
+
and the buffered fallback branch on the same code path deliberately
|
|
195
|
+
keeps its deadline because it reads a whole `maxDuration: 300` body.
|
|
196
|
+
- **A `request_timeout` now names the deadline that actually fired**
|
|
197
|
+
rather than always quoting `Timeout` — a long-work timeout blamed on
|
|
198
|
+
the ordinary knob sends the integrator to the wrong setting.
|
|
199
|
+
- Cross-SDK gate: `packages/unity-sdk` moves from `unbound` to `bound`
|
|
200
|
+
in `sdk-deadlines.drift.test.ts` and gains a block that binds this
|
|
201
|
+
client's predicate to the server's own `maxDuration` declarations in
|
|
202
|
+
both directions, so a new long route cannot land without a decision
|
|
203
|
+
here. Compiled and sentinel-checked before publishing
|
|
204
|
+
(`dotnet build packages/unity-sdk/ci/Compile.csproj`).
|
|
205
|
+
|
|
206
|
+
- **`quota_exhausted` joined the wire vocabulary** (`Protocol.cs` —
|
|
207
|
+
`ErrorCodes.QuotaExhausted`, 402, NOT retryable; contrast `RateLimited`).
|
|
208
|
+
Arrived via the drift-gated regeneration when the JS SDK shipped it in
|
|
209
|
+
0.45.0 (2026-08-06); recorded here so the next preview release names it
|
|
210
|
+
instead of shipping it silently.
|
|
211
|
+
- **The C# is compiled now — CR-169 closed.** The package went to npm having
|
|
212
|
+
never been read by a compiler; the wire vocabulary was drift-tested, the code
|
|
213
|
+
itself was not. It compiles clean: every `Runtime/*.cs` including
|
|
214
|
+
`PouchyCompanionBehaviour`, under Unity 2021.3's own profile (.NET Standard
|
|
215
|
+
2.1, C# 9, `UNITY_2021_1_OR_NEWER`) against the UnityEngine 2021.3.33
|
|
216
|
+
reference assemblies — **0 errors, 0 warnings**, with the .NET analyzers on.
|
|
217
|
+
The run is committed as `ci/Compile.csproj` and wired to
|
|
218
|
+
`.github/workflows/unity-sdk.yml`, so it is a gate rather than a one-time
|
|
219
|
+
result, and it needs no Unity install or licence on the runner.
|
|
220
|
+
Paired with a sentinel build that must FAIL: a green compile of code behind
|
|
221
|
+
`#if UNITY_2021_1_OR_NEWER` means nothing if the define or the engine
|
|
222
|
+
references stopped resolving, and the sentinel is what tells those apart.
|
|
223
|
+
**Still not proven, and stated plainly rather than implied away:** runtime
|
|
224
|
+
behaviour inside a real editor or player. No source or API change — this
|
|
225
|
+
release is the corrected status on the README, the package description and
|
|
226
|
+
the public `/sdk` reference, which all still said the compile pass was
|
|
227
|
+
pending.
|
|
228
|
+
|
|
229
|
+
## 0.1.0-preview.5 — 2026-07-26
|
|
230
|
+
|
|
231
|
+
- **`SummaryKey` / `SummaryVars` on `ConfirmRequestPayload` and `PendingConfirm`**
|
|
232
|
+
(JS SDK 0.43.0 parity). The confirmation sentence now also travels as data — an
|
|
233
|
+
i18n key plus its placeholder values — so a Unity host that knows the player's
|
|
234
|
+
language can render the approval in it. `Summary` is unchanged and still always
|
|
235
|
+
present; both new fields are null for a tool with no keyed summary and on
|
|
236
|
+
confirmations recorded before this release, so fall back to `Summary`.
|
|
237
|
+
|
|
238
|
+
## 0.1.0-preview.4 — 2026-07-17
|
|
239
|
+
|
|
240
|
+
### Added
|
|
241
|
+
|
|
242
|
+
- **Paused-turn recovery fields (read-only), mirroring JS SDK 0.37.0:**
|
|
243
|
+
`HelloAckPayload.PendingToolCalls` (new `PendingToolCall` model —
|
|
244
|
+
`Id`, `Name`, `Args`, `TurnId?`, `PausedAt?`) lists the outstanding tool
|
|
245
|
+
calls of a turn paused before the handshake (non-empty only when the
|
|
246
|
+
session was resumed mid-pause), and `ToolCallPayload.Replayed` marks the
|
|
247
|
+
re-delivery of such a pause-orphaned call. A restarted host can complete
|
|
248
|
+
the paused turn by posting each result via `SendToolResultAsync` (server
|
|
249
|
+
apply is idempotent per call id) instead of abandoning it via
|
|
250
|
+
`EndSessionAsync`.
|
|
251
|
+
|
|
252
|
+
> Parity note (preview.4): the JS SDK's `replayPendingToolCalls` AUTO-replay
|
|
253
|
+
> is deliberately not ported — same decision as the Python SDK 0.1.6: this
|
|
254
|
+
> client has no tool-call handler registry to replay into (integrators
|
|
255
|
+
> consume the event stream themselves), so recovery stays the caller's
|
|
256
|
+
> explicit `PendingToolCalls` → perform → `SendToolResultAsync` walk.
|
|
257
|
+
> Recorded so the cross-SDK parity ledger stays auditable.
|
|
258
|
+
|
|
259
|
+
## 0.1.0-preview.3 — 2026-07-17
|
|
260
|
+
|
|
261
|
+
### Added
|
|
262
|
+
|
|
263
|
+
- `SendTextResult.Duplicate` — the server has flagged replayed turnIds with
|
|
264
|
+
`duplicate: true` all along, but the client dropped the field on both send
|
|
265
|
+
paths, so a caller could not tell a replay from a fresh billed turn. Now
|
|
266
|
+
populated on the plain POST and the streaming `done` frame (null for a fresh
|
|
267
|
+
turn). The `SendTextAsync` doc also no longer claims a plain duplicate
|
|
268
|
+
"returns the recorded reply" — the recorded TEXT only rides back on the
|
|
269
|
+
streaming path.
|
|
270
|
+
- `CompanionError(message, status, code, retryAfter, innerException)` overload,
|
|
271
|
+
used by the transport wrapping below to preserve the original exception.
|
|
272
|
+
|
|
273
|
+
### Fixed
|
|
274
|
+
|
|
275
|
+
- **Transport failures now throw `CompanionError`** (the README's "every failed
|
|
276
|
+
call throws CompanionError" was false for them): `HttpRequestException` and
|
|
277
|
+
the per-request deadline's `TaskCanceledException` escaped raw from
|
|
278
|
+
`RequestJsonAsync`, `SendTextStreamingAsync`, and `StreamEventsAsync`. They
|
|
279
|
+
are wrapped with `Status=0` and `Code="request_timeout"` (deadline elapsed) /
|
|
280
|
+
`"aborted"` (other transport failures, incl. a dropped body stream) — the JS
|
|
281
|
+
SDK's synthesized-code vocabulary — with the cause as `InnerException`.
|
|
282
|
+
Caller-token cancellation on the event stream is deliberately NOT wrapped
|
|
283
|
+
(it stays an `OperationCanceledException`, the behaviour's teardown signal).
|
|
284
|
+
- **No more spurious `OnError` on teardown**: `PouchyCompanionBehaviour.OnDestroy`
|
|
285
|
+
cancels the CTS and disposes the response, which surfaced the resulting
|
|
286
|
+
`ObjectDisposedException`/`IOException` as an `OnError` on every normal
|
|
287
|
+
exit. `RunStreamAsync` now swallows any exception once cancellation was
|
|
288
|
+
requested.
|
|
289
|
+
- `RequireSession` tags its error `Code="not_connected"` (the JS SDK's code for
|
|
290
|
+
the same misuse) instead of null, so switch-on-`Code` handling covers it.
|
|
291
|
+
- `RetryAfter` also parses the RFC 9110 HTTP-date form of the `Retry-After`
|
|
292
|
+
header (delta from now, clamped at 0); only numeric seconds parsed before.
|
|
293
|
+
- `CompanionClientOptions.Timeout` doc no longer overpromises: it reliably
|
|
294
|
+
bounds time-to-response-headers on the streaming input POST, not whole-body
|
|
295
|
+
reads.
|
|
296
|
+
- README: escaped the `|` in the `SendWorldStateAsync(event \| events)` table
|
|
297
|
+
row (it split the GFM table cell); documented the transport-error codes.
|
|
298
|
+
- UPM manifest: added the `license` field and shipped the SDK LICENSE file
|
|
299
|
+
(the companion-sdk license text, UPM-adapted).
|
|
300
|
+
|
|
301
|
+
> Parity note (preview.3): `request_timeout` / `aborted` / `not_connected`
|
|
302
|
+
> mirror the JS SDK's SDK_SYNTHESIZED_ERROR_CODES and Python 0.1.5 — one
|
|
303
|
+
> cross-SDK error vocabulary. As in Python, `aborted` here covers any
|
|
304
|
+
> non-timeout transport failure (there is no AbortSignal equivalent).
|
|
305
|
+
|
|
306
|
+
## 0.1.0-preview.2 — 2026-07-15
|
|
307
|
+
|
|
308
|
+
### Fixed
|
|
309
|
+
|
|
310
|
+
- `IngestKnowledgeAsync` sent `{ "source": ... }`, but the server reads `name`
|
|
311
|
+
(title), `kind`, and `locale` and ignored `source` — so every ingested
|
|
312
|
+
document was titled "an uploaded file". The signature is now
|
|
313
|
+
`IngestKnowledgeAsync(text, name, kind, locale)` matching the server contract
|
|
314
|
+
and the JS/Python SDKs.
|
|
315
|
+
|
|
316
|
+
## 0.1.0-preview.1 — 2026-07-14
|
|
317
|
+
|
|
318
|
+
Initial preview release — the C# counterpart of the Python and JS companion
|
|
319
|
+
SDKs, targeting Unity 2021.3+ (netstandard2.1) and plain .NET.
|
|
320
|
+
|
|
321
|
+
### Added
|
|
322
|
+
|
|
323
|
+
- `CompanionClient` (pure .NET, no UnityEngine dependency): `ConnectAsync`,
|
|
324
|
+
`SendTextAsync` (plain + streaming await-reply with `onDelta`),
|
|
325
|
+
`StreamEventsAsync` (SSE with typed payloads + auto-advancing cursor),
|
|
326
|
+
`PendingConfirmsAsync`, `ConfirmActionAsync`, `SendToolResultAsync`,
|
|
327
|
+
`SendWorldStateAsync` (single + batch), `HistoryAsync`, `RecallAsync`,
|
|
328
|
+
`RememberAsync`, `IngestKnowledgeAsync`, `GetWalletAsync`,
|
|
329
|
+
`SetModalitiesAsync`, `PingAsync`, `EndSessionAsync`.
|
|
330
|
+
- `PouchyCompanionBehaviour` — optional Unity MonoBehaviour that owns a client,
|
|
331
|
+
streams events on a background Task, and marshals `OnMessage` / `OnTyping` /
|
|
332
|
+
`OnConfirmRequest` / `OnToolCall` / `OnError` onto the main thread.
|
|
333
|
+
- Typed models (`Runtime/Models.cs`) for every payload in the wire protocol,
|
|
334
|
+
with `[JsonExtensionData]` forward-compat and an `OutboundPayloadTypes` map.
|
|
335
|
+
- `CompanionError` with `Status` / `Code` / `RetryAfter`.
|
|
336
|
+
- `Runtime/Protocol.cs` — the message-type and error-code vocabulary, generated
|
|
337
|
+
from the TypeScript source of truth.
|
|
338
|
+
- UPM manifest (`ai.pouchy.companion`), Newtonsoft.Json dependency, asmdef.
|
|
339
|
+
|
|
340
|
+
### Notes
|
|
341
|
+
|
|
342
|
+
- **Wire protocol is drift-guarded** against `src/lib/companion-sdk/protocol.ts`
|
|
343
|
+
(Protocol.cs byte-equality + Models.cs field-name pins in CI).
|
|
344
|
+
- **Compile verification pending** — this preview was authored without a local
|
|
345
|
+
.NET toolchain; the first Unity/CI compile pass will follow. Report issues.
|
|
346
|
+
- Voice/WebRTC calls are intentionally out of scope (REST/SSE only).
|
package/LICENSE
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
Pouchy Companion SDK License
|
|
2
|
+
Copyright (c) 2026 Pouchy.ai. All Rights Reserved.
|
|
3
|
+
|
|
4
|
+
This license governs use of the "ai.pouchy.companion" software package (the
|
|
5
|
+
"SDK") as distributed for the Unity Package Manager. The SDK is proprietary
|
|
6
|
+
software owned by Pouchy.ai. By installing, copying, or using the SDK you agree
|
|
7
|
+
to these terms.
|
|
8
|
+
|
|
9
|
+
1. GRANT. Subject to these terms, Pouchy.ai grants you a worldwide,
|
|
10
|
+
non-exclusive, non-transferable, royalty-free, revocable license to install
|
|
11
|
+
and use the SDK, as distributed, solely to build and operate applications
|
|
12
|
+
that integrate with Pouchy.ai's official services and APIs.
|
|
13
|
+
|
|
14
|
+
2. RESTRICTIONS. Except to the extent a restriction below is prohibited by
|
|
15
|
+
applicable law, you may not:
|
|
16
|
+
(a) modify, adapt, or create derivative works of the SDK's source for
|
|
17
|
+
redistribution;
|
|
18
|
+
(b) redistribute, sublicense, sell, rent, or lease the SDK as a standalone
|
|
19
|
+
product, or republish it under a different name;
|
|
20
|
+
(c) reverse engineer, decompile, or disassemble the SDK except to the extent
|
|
21
|
+
necessary for interoperability and expressly permitted by law;
|
|
22
|
+
(d) use the SDK to build or operate a service that competes with Pouchy.ai's
|
|
23
|
+
services, or to access Pouchy.ai's services in violation of their terms;
|
|
24
|
+
(e) remove or alter any copyright, trademark, or other proprietary notices.
|
|
25
|
+
|
|
26
|
+
3. RESERVATION OF RIGHTS. All rights not expressly granted are reserved by
|
|
27
|
+
Pouchy.ai. No rights are granted in Pouchy.ai's source repositories,
|
|
28
|
+
trademarks, or services beyond what is needed to use the SDK as described.
|
|
29
|
+
|
|
30
|
+
4. TERMINATION. This license terminates automatically if you breach it. On
|
|
31
|
+
termination you must stop using and delete all copies of the SDK.
|
|
32
|
+
|
|
33
|
+
5. NO WARRANTY. THE SDK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
34
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
35
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
36
|
+
|
|
37
|
+
6. LIMITATION OF LIABILITY. IN NO EVENT SHALL POUCHY.AI BE LIABLE FOR ANY CLAIM,
|
|
38
|
+
DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT, OR
|
|
39
|
+
OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SDK OR THE USE OR
|
|
40
|
+
OTHER DEALINGS IN THE SDK.
|
|
41
|
+
|
|
42
|
+
For any other use, or for written permission, contact legal@pouchy.ai.
|
package/README.md
ADDED
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Pouchy Companion — Unity / C# SDK
|
|
2
|
+
|
|
3
|
+
A thin, typed C# client for the [Pouchy](https://pouchy.ai) companion REST/SSE
|
|
4
|
+
plane — the same wire contract the JS (`@pouchy_ai/companion-sdk`) and Python
|
|
5
|
+
(`pouchy-companion`) SDKs speak, aimed at **Unity games and .NET integrators**.
|
|
6
|
+
|
|
7
|
+
> **⚠️ 0.1.0-preview** — the **wire protocol** is drift-tested against the
|
|
8
|
+
> TypeScript source of truth (`Runtime/Protocol.cs` is generated; `Models.cs`
|
|
9
|
+
> field names are pinned in CI), so the vocabulary cannot silently diverge.
|
|
10
|
+
> The **C# compiles clean** — every `Runtime/*.cs`, `PouchyCompanionBehaviour`
|
|
11
|
+
> included, under Unity 2021.3's own profile (.NET Standard 2.1, C# 9) against
|
|
12
|
+
> the UnityEngine 2021.3 reference assemblies: 0 errors, 0 warnings — and the
|
|
13
|
+
> pure .NET client is **behaviour-tested** (`dotnet test`, no editor needed:
|
|
14
|
+
> error/backoff contract, event-stream stall handling). What is **still
|
|
15
|
+
> unproven is runtime behaviour inside a real editor or player**. Please
|
|
16
|
+
> report anything you hit to <support@pouchy.ai>.
|
|
17
|
+
|
|
18
|
+
- `System.Net.Http.HttpClient` + async/await + **Newtonsoft.Json** — the pure
|
|
19
|
+
`CompanionClient` has **no UnityEngine dependency**, so it unit-tests outside
|
|
20
|
+
the editor. `PouchyCompanionBehaviour` is the optional Unity glue.
|
|
21
|
+
- Typed models for every envelope/payload; unknown/newer server fields are
|
|
22
|
+
preserved (`[JsonExtensionData]`) instead of failing to parse.
|
|
23
|
+
- Voice/WebRTC and browser-only surfaces are **deliberately out of scope**
|
|
24
|
+
(REST/SSE only) — they need a native WebRTC plugin. Use the JS SDK for embeds.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
The package is published on npm and installs through a **UPM scoped registry**
|
|
29
|
+
(Unity's Package Manager speaks the npm protocol natively). Add both blocks to
|
|
30
|
+
your project's `Packages/manifest.json`:
|
|
31
|
+
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"scopedRegistries": [
|
|
35
|
+
{
|
|
36
|
+
"name": "Pouchy",
|
|
37
|
+
"url": "https://registry.npmjs.org",
|
|
38
|
+
"scopes": ["ai.pouchy"]
|
|
39
|
+
}
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"ai.pouchy.companion": "0.1.0-preview.10"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Unity resolves it on the next editor focus; after that the package shows up in
|
|
48
|
+
**Window → Package Manager → My Registries** with the normal version/upgrade UI.
|
|
49
|
+
|
|
50
|
+
This package depends on **Newtonsoft.Json** via
|
|
51
|
+
`com.unity.nuget.newtonsoft-json` (declared in `package.json`). If your project
|
|
52
|
+
doesn't already resolve it, install *Newtonsoft Json* from the Package Manager
|
|
53
|
+
(or add `"com.unity.nuget.newtonsoft-json": "3.2.1"` to your manifest).
|
|
54
|
+
|
|
55
|
+
**Player settings:** the client uses `System.Net.Http`, so set **Project
|
|
56
|
+
Settings → Player → Api Compatibility Level = .NET Standard 2.1** (Unity's
|
|
57
|
+
default). On IL2CPP builds keep managed stripping at Low, or add a `link.xml`
|
|
58
|
+
preserving `Pouchy.Companion` and `Newtonsoft.Json`, so the reflection-based
|
|
59
|
+
JSON binding survives the AOT strip. (These are the two most likely
|
|
60
|
+
first-compile/runtime snags — see the preview note above.)
|
|
61
|
+
|
|
62
|
+
## Quickstart
|
|
63
|
+
|
|
64
|
+
Mint a session token from **your backend** — one per end user. Never ship a
|
|
65
|
+
secret key (`pchy_sk_…`) in a client build; the token is the client credential.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
curl -X POST https://pouchy.ai/v1/sessions \
|
|
69
|
+
-H "Authorization: Bearer pchy_sk_…" \
|
|
70
|
+
-H "Content-Type: application/json" \
|
|
71
|
+
-d '{ "agent": "<agentId>", "external_user_id": "player_42" }'
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Plain .NET (`CompanionClient`)
|
|
75
|
+
|
|
76
|
+
```csharp
|
|
77
|
+
using Pouchy.Companion;
|
|
78
|
+
|
|
79
|
+
using var client = new CompanionClient("https://pouchy.ai", sessionToken);
|
|
80
|
+
var ack = await client.ConnectAsync();
|
|
81
|
+
Console.WriteLine("scopes: " + string.Join(",", ack.GrantedScopes ?? new string[0]));
|
|
82
|
+
|
|
83
|
+
// Request/response in one call — streams server-side, returns the reply.
|
|
84
|
+
var reply = await client.SendTextAsync("what about the boss on floor 3?", awaitReply: true);
|
|
85
|
+
Console.WriteLine(reply.Text);
|
|
86
|
+
|
|
87
|
+
// Streaming deltas:
|
|
88
|
+
await client.SendTextAsync("tell me a story", awaitReply: true,
|
|
89
|
+
onDelta: (chunk, reset) => Console.Write(chunk));
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The event stream (proactive messages, confirms, tool calls) resumes from the
|
|
93
|
+
session cursor and gives each envelope a typed `TypedPayload` (every outbound
|
|
94
|
+
event has a class — e.g. `DataActivityPayload` for `companion.data_activity`,
|
|
95
|
+
the metadata-only Data-plane activity frame; needs the `data.activity` scope):
|
|
96
|
+
|
|
97
|
+
```csharp
|
|
98
|
+
var cts = new CancellationTokenSource();
|
|
99
|
+
await client.StreamEventsAsync(async env =>
|
|
100
|
+
{
|
|
101
|
+
switch (env.TypedPayload)
|
|
102
|
+
{
|
|
103
|
+
case MessagePayload m:
|
|
104
|
+
Debug.Log("companion: " + m.Text);
|
|
105
|
+
break;
|
|
106
|
+
case ConfirmRequestPayload c:
|
|
107
|
+
// Platform session tokens may resolve confirms directly — your end
|
|
108
|
+
// user IS this instance's human. Show your own UI, then:
|
|
109
|
+
var res = await client.ConfirmActionAsync(c.ConfirmId, approve: true);
|
|
110
|
+
Debug.Log("outcome: " + res.Outcome);
|
|
111
|
+
break;
|
|
112
|
+
case ToolCallPayload t:
|
|
113
|
+
await client.SendToolResultAsync(t.Id, ok: true, result: new { answer = 42 });
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
}, cts.Token);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The server closes each stream window after ~45s (a clean end — call
|
|
120
|
+
`StreamEventsAsync` again; it resumes from `client.Cursor`, so nothing is lost
|
|
121
|
+
or repeated). That loop is storm-safe by construction: a clean instantly-closed
|
|
122
|
+
**empty** window (a gateway accepting and immediately closing — never the
|
|
123
|
+
designed cadence, which always delivers at least `hello.ack`) makes the next
|
|
124
|
+
open back off 1→8s (anti-storm, preview.10); any delivered envelope resets the
|
|
125
|
+
ladder, and errors still throw immediately. Byte-**silence** is bounded too (`StreamStallTimeout`, default
|
|
126
|
+
60s): on a half-open TCP drop — a NAT idle-kill eats the FIN and no close ever
|
|
127
|
+
arrives — the call throws a `CompanionError` (`Status` 0) naming the stall
|
|
128
|
+
instead of blocking forever, and the same reconnect resumes. The bound is armed
|
|
129
|
+
per read, so a healthy subscription is never cut, however long it lives. Set
|
|
130
|
+
`StreamStallTimeout = Timeout.InfiniteTimeSpan` to disable it (then only your
|
|
131
|
+
CancellationToken can unblock a dead read).
|
|
132
|
+
|
|
133
|
+
Don't want to own that loop? **`EventStreamPump.RunAsync(client, onEvent,
|
|
134
|
+
onError, ct)`** runs the stream as a permanent subscription: it reconnects
|
|
135
|
+
immediately after every clean window close (except a zero-event close well
|
|
136
|
+
under the server's ~45s cadence — a gateway accepting and instantly closing —
|
|
137
|
+
which climbs the backoff ladder instead of hot-looping), backs off on errors
|
|
138
|
+
(1→30s, honouring the server's `RetryAfter`, capped at 60s), stops on
|
|
139
|
+
cancellation or a deliberately ended session, and never throws — errors reach
|
|
140
|
+
your `onError` callback instead. It is what `PouchyCompanionBehaviour` rides
|
|
141
|
+
internally.
|
|
142
|
+
|
|
143
|
+
### Unity (`PouchyCompanionBehaviour`)
|
|
144
|
+
|
|
145
|
+
Add the component, set **Base Url** + **Token** in the Inspector, wire the
|
|
146
|
+
UnityEvents, and call `Connect()`. Every event is raised on the **main thread**,
|
|
147
|
+
so handlers can touch Unity APIs safely. The subscription is **always-on**: the
|
|
148
|
+
component rides `EventStreamPump`, so it reconnects through the server's ~45s
|
|
149
|
+
window closes and transient failures for the lifetime of the GameObject
|
|
150
|
+
(stream errors surface on `OnError` and are retried with backoff; a session
|
|
151
|
+
ended with `EndSessionAsync` stops it).
|
|
152
|
+
|
|
153
|
+
```csharp
|
|
154
|
+
var companion = gameObject.AddComponent<PouchyCompanionBehaviour>();
|
|
155
|
+
companion.Token = sessionToken;
|
|
156
|
+
companion.OnMessage.AddListener(text => chatUI.Append(text));
|
|
157
|
+
companion.OnTyping.AddListener(active => typingDot.SetActive(active));
|
|
158
|
+
companion.Connect();
|
|
159
|
+
// ...later, from a send button:
|
|
160
|
+
companion.SendText("hi!");
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### World state (play-along context)
|
|
164
|
+
|
|
165
|
+
```csharp
|
|
166
|
+
await client.SendWorldStateAsync(new WorldStateEvent {
|
|
167
|
+
Type = "game.event.boss_spawned",
|
|
168
|
+
Data = Newtonsoft.Json.Linq.JToken.FromObject(new { name = "Ashen Knight", floor = 3 }),
|
|
169
|
+
Salience = 0.9,
|
|
170
|
+
});
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Methods
|
|
174
|
+
|
|
175
|
+
| Method | REST | Notes |
|
|
176
|
+
| --- | --- | --- |
|
|
177
|
+
| `ConnectAsync()` | `POST /api/companion/session` | handshake → `HelloAckPayload`; sets `SessionId`, `Cursor` |
|
|
178
|
+
| `SendTextAsync(text, awaitReply, onDelta, images, turnId)` | `POST …/session/{id}/input` | plain or streaming; always returns the `TurnId` (retry-idempotent — a replayed turn sets `Duplicate`) |
|
|
179
|
+
| `StreamEventsAsync(onEvent, ct)` | `GET …/session/{id}/stream` | SSE; typed `TypedPayload`, cursor auto-advances; byte-silence bounded (`StreamStallTimeout` 60s) |
|
|
180
|
+
| `PendingConfirmsAsync()` | `GET …/session/{id}/confirm` | `List<PendingConfirm>` |
|
|
181
|
+
| `ConfirmActionAsync(confirmId, approve)` | `POST …/session/{id}/confirm` | `ConfirmResolution` |
|
|
182
|
+
| `SendToolResultAsync(callId, ok, result)` | `POST …/session/{id}/tool-result` | returns `allDone` |
|
|
183
|
+
| `SendWorldStateAsync(event \| events)` | `POST …/session/{id}/context` | fills specversion/id/source; `WorldStateAccepted` |
|
|
184
|
+
| `HistoryAsync(limit)` | `GET …/session/{id}/history` | `List<HistoryTurn>` |
|
|
185
|
+
| `RecallAsync(query, limit)` | `GET /api/companion/memory` | `List<RecalledMemory>` |
|
|
186
|
+
| `RememberAsync(fact)` | `POST /api/companion/memory` | needs `memory.write:app` / `:core` |
|
|
187
|
+
| `IngestKnowledgeAsync(text, name, kind, locale)` | `POST /api/companion/knowledge` | — |
|
|
188
|
+
| `GetWalletAsync()` | `GET /api/companion/wallet` | read-only (`wallet.read`) |
|
|
189
|
+
| `SetModalitiesAsync(modalities)` | `POST …/session/{id}/modalities` | — |
|
|
190
|
+
| `PingAsync()` | `POST …/session/{id}/ping` | keep-alive |
|
|
191
|
+
| `EndSessionAsync()` | `POST …/session/{id}/end` | clears `SessionId` |
|
|
192
|
+
|
|
193
|
+
## Tokens & scopes
|
|
194
|
+
|
|
195
|
+
A **platform session token** (minted via `POST /v1/sessions`) represents one of
|
|
196
|
+
your end users and may resolve confirms directly. The token's granted scopes
|
|
197
|
+
(`ack.GrantedScopes`) gate memory writes, wallet reads, social messages, etc.
|
|
198
|
+
The scope model, confirm flow, world-state salience, and limits are identical
|
|
199
|
+
across SDKs — see the [Python SDK](https://pypi.org/project/pouchy-companion/)
|
|
200
|
+
for the same token/scopes note, and the full reference at
|
|
201
|
+
[pouchy.ai/sdk](https://pouchy.ai/sdk) and
|
|
202
|
+
[companion-api-reference](https://pouchy.ai/docs/companion-api-reference).
|
|
203
|
+
|
|
204
|
+
## Errors
|
|
205
|
+
|
|
206
|
+
Every failed call throws `CompanionError` with `.Status`, `.Code`
|
|
207
|
+
(machine-switchable — `rate_limited`, `turn_pending`, `confirm_resolved`, … see
|
|
208
|
+
`Protocol.ErrorCodes`) and `.RetryAfter` seconds when the server said when to
|
|
209
|
+
come back. `.RetryAfter` is guaranteed **finite and non-negative** — it is read
|
|
210
|
+
from the body's `retryAfterSec` first (it survives a header-stripping proxy),
|
|
211
|
+
then the `Retry-After` header (numeric or RFC 9110 HTTP-date), and anything a
|
|
212
|
+
broken upstream sends outside that domain (negative, `NaN`, `Infinity` — the
|
|
213
|
+
last is reachable from legal JSON via the `1e400` overflow literal) becomes
|
|
214
|
+
`null` rather than a value the handler below cannot sleep. Transport failures
|
|
215
|
+
are wrapped too — `Status` is `0` and `Code` is
|
|
216
|
+
`request_timeout` (the per-request deadline elapsed) or `aborted` (any other
|
|
217
|
+
network failure), with the original exception preserved as `InnerException`.
|
|
218
|
+
|
|
219
|
+
```csharp
|
|
220
|
+
try { await client.SendTextAsync("hi"); }
|
|
221
|
+
catch (CompanionError e) when (e.Code == "rate_limited")
|
|
222
|
+
{
|
|
223
|
+
await Task.Delay(TimeSpan.FromSeconds(e.RetryAfter ?? 1));
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Not every refusal is worth retrying. `quota_exhausted` (402) means the account's
|
|
228
|
+
monthly allowance is spent — credits, or realtime voice minutes — and nothing
|
|
229
|
+
changes until the plan is upgraded or the 1st (UTC) arrives, so a retry loop on
|
|
230
|
+
it never terminates. Handle it separately from `rate_limited`:
|
|
231
|
+
|
|
232
|
+
```csharp
|
|
233
|
+
catch (CompanionError e) when (e.Code == Protocol.ErrorCodes.QuotaExhausted)
|
|
234
|
+
{
|
|
235
|
+
ShowUpgradePrompt(e.Message); // do NOT retry
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## Voice / WebRTC
|
|
240
|
+
|
|
241
|
+
Not included. This SDK is REST/SSE only; live voice calls need a native WebRTC
|
|
242
|
+
plugin the game supplies. `control.call_ready` / `CallReadyPayload` are part of
|
|
243
|
+
the typed vocabulary, but `startCall` (which returns the WebRTC credentials) is
|
|
244
|
+
not implemented here.
|
|
245
|
+
|
|
246
|
+
## Not yet ported from the JS SDK
|
|
247
|
+
|
|
248
|
+
A few JS-SDK surfaces are deliberately unported (recorded decisions, not
|
|
249
|
+
drift — the full ledger with rationale lives at the top of `CHANGELOG.md`):
|
|
250
|
+
`handles`/`contextKinds` hello options, representative mode
|
|
251
|
+
(`visitor`/`pairVisitor`), `GetAvatar`, binary `IngestFile`
|
|
252
|
+
(`IngestKnowledgeAsync` text is covered), and `setToken` live-token swap (a
|
|
253
|
+
Unity host reconnects with a fresh client after ~1h token expiry — the
|
|
254
|
+
session survives the reconnect). Ask (or port) on demand.
|
|
255
|
+
|
|
256
|
+
## Protocol parity
|
|
257
|
+
|
|
258
|
+
`Runtime/Protocol.cs` is **generated from the TypeScript source of truth**
|
|
259
|
+
(`src/lib/companion-sdk/protocol.ts`) and drift-tested in CI; the payload models
|
|
260
|
+
in `Runtime/Models.cs` have their field names pinned to the same interfaces, so
|
|
261
|
+
the three SDKs cannot silently diverge on the wire.
|
|
262
|
+
|
|
263
|
+
The C# is compiled in CI as well, under Unity 2021.3's profile and against the
|
|
264
|
+
UnityEngine 2021.3 reference assemblies — no Unity install or licence required
|
|
265
|
+
on the runner, so the check runs on every change rather than at release time.
|
|
266
|
+
The run pairs with a deliberately-broken sentinel that **must** fail: a compile
|
|
267
|
+
of `#if UNITY_2021_1_OR_NEWER` code proves nothing if the define or the engine
|
|
268
|
+
references quietly stopped resolving, and the sentinel is what makes that
|
|
269
|
+
visible instead of green.
|