dsh-realtime-agent 0.2.5 → 0.2.7

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/README.md CHANGED
@@ -75,6 +75,44 @@ A session is opened only when `autoStart` is set. `maxTranscriptChars` bounds th
75
75
  on each request: it is the **oldest** lines that are dropped, and the most recent line is always kept,
76
76
  because a buffer that can hold nothing can answer nothing.
77
77
 
78
+ ## What can be changed while it runs
79
+
80
+ `docs/control-plane-fields.md` is the gate, and it classifies this plugin's fields in three ways.
81
+ `delegationTimeoutMs` and `maxTranscriptChars` are **live**: read at the moment they are used, so the
82
+ seam's settings surface changes them without a restart, and a change lands on the next delegation.
83
+
84
+ ```
85
+ set realtime-agent.delegationTimeoutMs=90000
86
+ set realtime-agent.maxTranscriptChars=12000
87
+ ```
88
+
89
+ `provider`, `model`, `voice` and `instructions` are **session-bound** — they travelled in the
90
+ provider's `session.start`, so only a new session can carry new ones, and the UI that offers them must
91
+ say *reconnect* rather than pretending an instant change is possible.
92
+
93
+ `autoStart` is **restart-bound**, and this is a correction rather than a convenience: its only read site
94
+ is the boot, so nothing a running process could do would honour a change. A `set` on it is refused with
95
+ the restart it needs — `realtime-agent.autoStart` is claimed when the plugin loads, so restart to change
96
+ it — rather than accepted and quietly ignored.
97
+
98
+ ## Asking it what it is doing
99
+
100
+ Two of this plugin's three session events **return** what they did, so a caller that is waiting can have
101
+ the answer while a caller that only emits is unaffected:
102
+
103
+ - `realtime-agent/status` — a query. `{open, provider, model, voice?, sessionId?}`, read from the session
104
+ the provider actually accepted while one is open, and from what a start *would* use while none is. A
105
+ mounted agent always answers, so `undefined` from the dispatch means the row is absent — which is a
106
+ different fact from a session that is merely closed.
107
+ - `realtime-agent/start` / `realtime-agent/stop` — the transport emits these and ignores the result; the
108
+ control channel dispatches them with `serial` and answers with the **outcome**: `{ok, voice, refusal?}`
109
+ rather than an acknowledgement that the request was made. A `start` that replied *requested* while the
110
+ open silently failed is exactly the collapse this project has already paid for once.
111
+
112
+ A failed request carries a **structured refusal** — the seam's machine code and the `remedy` written to be
113
+ relayed verbatim — and never the failure's message. This plugin holds no credential to redact against; the
114
+ adapter does, and that is why its journal records the class of a session failure rather than the text.
115
+
78
116
  ## How answers are kept deliverable
79
117
 
80
118
  - **A long answer is cut, not rejected.** The seam bounds an append at 2000 characters and *throws*
package/lib/bridge.d.ts CHANGED
@@ -26,6 +26,8 @@ export interface DelegationAsker {
26
26
  * @returns the text, or a marked prefix of it no longer than `maxChars`.
27
27
  */
28
28
  export declare function boundAppend(text: string, maxChars?: number): string;
29
+ /** Which channel an append went out on. What an acknowledgement names, and what it does not. */
30
+ export type DelegationAppend = 'commentary' | 'thinking';
29
31
  /**
30
32
  * Answer one delegation, or tell the model plainly that it could not be answered.
31
33
  *
@@ -35,7 +37,8 @@ export declare function boundAppend(text: string, maxChars?: number): string;
35
37
  * @param transcript - the conversation so far, as the responder will see it.
36
38
  * @param ask - how to reach a responder.
37
39
  * @param timeoutMs - bound on waiting for one.
40
+ * @param onAcknowledged - called once per accepted append, which is not once per thing heard.
38
41
  * @returns a promise settling once the session has been answered.
39
42
  */
40
- export declare function answerDelegation(session: Pick<RealtimeSession, 'id' | 'appendCommentary' | 'appendThinking'>, delegation: RealtimeDelegation, transcript: readonly AgentTranscriptLine[], ask: DelegationAsker, timeoutMs: number): Promise<void>;
43
+ export declare function answerDelegation(session: Pick<RealtimeSession, 'id' | 'appendCommentary' | 'appendThinking'>, delegation: RealtimeDelegation, transcript: readonly AgentTranscriptLine[], ask: DelegationAsker, timeoutMs: number, onAcknowledged: (append: DelegationAppend, delegationId: string) => void): Promise<void>;
41
44
  //# sourceMappingURL=bridge.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9F,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE1F;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,sDAAiD,CAAA;AAK/E;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAA;CACnG;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAyB,GAAG,MAAM,CAKrF;AAmBD;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,kBAAkB,GAAG,gBAAgB,CAAC,EAC5E,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,SAAS,mBAAmB,EAAE,EAC1C,GAAG,EAAE,eAAe,EACpB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA0Bf"}
1
+ {"version":3,"file":"bridge.d.ts","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAoB,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9F,OAAO,KAAK,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAE1F;;;;;;GAMG;AACH,eAAO,MAAM,iBAAiB,sDAAiD,CAAA;AAK/E;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAA;CACnG;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAyB,GAAG,MAAM,CAKrF;AAmBD,gGAAgG;AAChG,MAAM,MAAM,gBAAgB,GAAG,YAAY,GAAG,UAAU,CAAA;AAExD;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,kBAAkB,GAAG,gBAAgB,CAAC,EAC5E,UAAU,EAAE,kBAAkB,EAC9B,UAAU,EAAE,SAAS,mBAAmB,EAAE,EAC1C,GAAG,EAAE,eAAe,EACpB,SAAS,EAAE,MAAM,EACjB,cAAc,EAAE,CAAC,MAAM,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,KAAK,IAAI,GACvE,OAAO,CAAC,IAAI,CAAC,CAkCf"}
package/lib/bridge.js CHANGED
@@ -52,9 +52,10 @@ function settled(ask, request) {
52
52
  * @param transcript - the conversation so far, as the responder will see it.
53
53
  * @param ask - how to reach a responder.
54
54
  * @param timeoutMs - bound on waiting for one.
55
+ * @param onAcknowledged - called once per accepted append, which is not once per thing heard.
55
56
  * @returns a promise settling once the session has been answered.
56
57
  */
57
- export async function answerDelegation(session, delegation, transcript, ask, timeoutMs) {
58
+ export async function answerDelegation(session, delegation, transcript, ask, timeoutMs, onAcknowledged) {
58
59
  const request = {
59
60
  id: delegation.id,
60
61
  offsetMs: delegation.offsetMs,
@@ -68,14 +69,22 @@ export async function answerDelegation(session, delegation, transcript, ask, tim
68
69
  });
69
70
  const answer = await Promise.race([settled(ask, request), expired]);
70
71
  const text = typeof answer?.text === 'string' ? answer.text.trim() : '';
72
+ // Every append below awaits the provider's acknowledgement rather than the send — that is the seam's
73
+ // session contract, written that way for a measured reason. So a resolved await **is** the
74
+ // acknowledgement, and this is the only honest place to record one. What it is not is delivery:
75
+ // nothing here says a speaker rendered anything, and keeping those two apart is the whole of
76
+ // invariant 6 — and the reason the fault-injection matrix has a row for exactly this pair.
71
77
  if (text.length === 0) {
72
78
  await session.appendCommentary(UNANSWERED_NOTICE, delegation.id);
79
+ onAcknowledged('commentary', delegation.id);
73
80
  return;
74
81
  }
75
82
  if (answer?.mode === 'spoken') {
76
83
  await session.appendCommentary(boundAppend(text), delegation.id);
84
+ onAcknowledged('commentary', delegation.id);
77
85
  return;
78
86
  }
79
87
  await session.appendThinking(boundAppend(text), delegation.id);
88
+ onAcknowledged('thinking', delegation.id);
80
89
  }
81
90
  //# sourceMappingURL=bridge.js.map
package/lib/bridge.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAiD,MAAM,cAAc,CAAA;AAG9F;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,8CAA8C,CAAA;AAE/E,iGAAiG;AACjG,MAAM,iBAAiB,GAAG,cAAc,CAAA;AAWxC;;;;;;;;GAQG;AACH,MAAM,UAAU,WAAW,CAAC,IAAY,EAAE,QAAQ,GAAW,gBAAgB;IAC3E,IAAI,IAAI,CAAC,MAAM,IAAI,QAAQ;QAAE,OAAO,IAAI,CAAA;IACxC,mFAAmF;IACnF,IAAI,QAAQ,IAAI,iBAAiB,CAAC,MAAM;QAAE,OAAO,iBAAiB,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAA;IACrF,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,GAAG,iBAAiB,CAAC,MAAM,CAAC,GAAG,iBAAiB,CAAA;AAC/E,CAAC;AAED;;;;;;;;GAQG;AACH,SAAS,OAAO,CAAC,GAAoB,EAAE,OAA0B;IAC/D,IAAI,CAAC;QACH,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAA;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,CAAA;IACnC,CAAC;AACH,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,OAA4E,EAC5E,UAA8B,EAC9B,UAA0C,EAC1C,GAAoB,EACpB,SAAiB;IAEjB,MAAM,OAAO,GAAsB;QACjC,EAAE,EAAE,UAAU,CAAC,EAAE;QACjB,QAAQ,EAAE,UAAU,CAAC,QAAQ;QAC7B,UAAU;QACV,SAAS,EAAE,OAAO,CAAC,EAAE;KACtB,CAAA;IAED,kGAAkG;IAClG,4DAA4D;IAC5D,MAAM,OAAO,GAAG,IAAI,OAAO,CAAY,CAAC,OAAO,EAAE,EAAE;QACjD,UAAU,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAAA,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC,KAAK,EAAE,CAAA;IAC7D,CAAC,CAAC,CAAA;IAEF,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,CAAC,CAAA;IACnE,MAAM,IAAI,GAAG,OAAO,MAAM,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;IAEvE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtB,MAAM,OAAO,CAAC,gBAAgB,CAAC,iBAAiB,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;QAChE,OAAM;IACR,CAAC;IACD,IAAI,MAAM,EAAE,IAAI,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,OAAO,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;QAChE,OAAM;IACR,CAAC;IACD,MAAM,OAAO,CAAC,cAAc,CAAC,WAAW,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC,EAAE,CAAC,CAAA;AAChE,CAAC"}
1
+ {"version":3,"file":"bridge.js","sourceRoot":"","sources":["../src/bridge.ts"],"names":[],"mappings":"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"}
package/lib/index.d.ts CHANGED
@@ -23,9 +23,9 @@
23
23
  import Schema from '@deepseek-ai/schemastery';
24
24
  import type { Context } from '@deepseek-ai/cordis';
25
25
  import type { RealtimeDelegationSettlement, RealtimeSession, RealtimeSessionHandlers } from 'dsh-realtime';
26
- import { type DelegationAsker } from './bridge.ts';
26
+ import { type DelegationAppend, type DelegationAsker } from './bridge.ts';
27
27
  import { TranscriptBuffer } from './transcript.ts';
28
- import type { DelegationAnswer, DelegationRequest, RealtimeAgentConfig } from './types.ts';
28
+ import type { DelegationAnswer, DelegationRequest, RealtimeAgentConfig, RealtimeSessionRequestOutcome, RealtimeVoiceStatus } from './types.ts';
29
29
  declare module '@deepseek-ai/cordis' {
30
30
  interface Events {
31
31
  /**
@@ -74,15 +74,27 @@ declare module '@deepseek-ai/cordis' {
74
74
  * Emitted by a transport when an authenticated client arrives, so that connecting a microphone is
75
75
  * enough to be heard — with no profile option and no dependence on a model choosing to call
76
76
  * `voice_start`. The session belongs to the agent, so the transport asks rather than opens one itself.
77
+ *
78
+ * Returns the request's **outcome** so a caller that is waiting for the answer can have it —
79
+ * `ctx.serial` from the control channel — while the transport, which only *emits*, is unaffected.
77
80
  */
78
- 'realtime-agent/start'(): void;
81
+ 'realtime-agent/start'(): RealtimeSessionRequestOutcome | undefined | Promise<RealtimeSessionRequestOutcome | undefined>;
79
82
  /**
80
83
  * Close the voice session.
81
84
  *
82
85
  * Emitted by a transport when its last client goes away, including when the transport itself is
83
86
  * disposed — a session outliving the microphone that asked for it is a socket nobody is listening to.
87
+ * Returns the outcome, for the same reason `realtime-agent/start` does.
88
+ */
89
+ 'realtime-agent/stop'(): RealtimeSessionRequestOutcome | undefined | Promise<RealtimeSessionRequestOutcome | undefined>;
90
+ /**
91
+ * The voice session's state — a **query**, so nothing emits it.
92
+ *
93
+ * `undefined` means no listener answered, which is the honest answer for a composition with no agent
94
+ * row: it is distinguishable from a session that is merely closed, because the agent that is mounted
95
+ * always answers.
84
96
  */
85
- 'realtime-agent/stop'(): void;
97
+ 'realtime-agent/status'(): RealtimeVoiceStatus | undefined | Promise<RealtimeVoiceStatus | undefined>;
86
98
  }
87
99
  }
88
100
  export * from './types.ts';
@@ -125,14 +137,27 @@ export interface HandlerDeps {
125
137
  readonly session: () => RealtimeSession | undefined;
126
138
  /** How to reach a responder. */
127
139
  readonly ask: DelegationAsker;
128
- /** Bound on waiting for one. */
129
- readonly timeoutMs: number;
140
+ /**
141
+ * Bound on waiting for one, read at the moment the delegation is answered.
142
+ *
143
+ * An accessor because `delegationTimeoutMs` is a **live** field: the window the voice model waits in
144
+ * can be changed while the plugin runs, and the change must apply to the next delegation rather than
145
+ * to the next boot.
146
+ */
147
+ readonly timeoutMs: () => number;
130
148
  /** Called when the session ended, so the caller can drop its reference. */
131
149
  readonly onClosed: () => void;
132
150
  /** Where a session-scoped failure is reported. */
133
151
  readonly onSessionError: (error: Error) => void;
134
152
  /** Where output audio is delivered. Called once per provider delta. */
135
153
  readonly onAudio: (pcm16: Uint8Array) => void;
154
+ /**
155
+ * Where an accepted append is reported.
156
+ *
157
+ * An acknowledgement, not a delivery: it says the provider took the append, and nothing about
158
+ * whether a speaker ever rendered it. See the seam's session contract and invariant 6.
159
+ */
160
+ readonly onAcknowledged: (append: DelegationAppend, delegationId: string) => void;
136
161
  }
137
162
  /**
138
163
  * Build the session handlers.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAsB,4BAA4B,EAAE,eAAe,EAAE,uBAAuB,EAAsB,MAAM,cAAc,CAAA;AAClJ,OAAO,EAAoB,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AAEpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAE1F,OAAO,QAAQ,qBAAqB,CAAC;IACnC,UAAU,MAAM;QACd;;;;;;WAMG;QACH,2BAA2B,CAAC,OAAO,EAAE,iBAAiB,GAAG,gBAAgB,GAAG,SAAS,GAAG,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAAA;QAC7H,2EAA2E;QAC3E,sBAAsB,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAA;QAC1C;;;;;;;;;;;;WAYG;QACH,mCAAmC,CAAC,UAAU,EAAE,4BAA4B,GAAG,IAAI,CAAA;QACnF;;;;;;WAMG;QACH,sBAAsB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;QAC/C;;;;;;WAMG;QACH,oBAAoB,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAA;QAC7C;;;;;;WAMG;QACH,sBAAsB,IAAI,IAAI,CAAA;QAC9B;;;;;WAKG;QACH,qBAAqB,IAAI,IAAI,CAAA;KAC9B;CACF;AAED,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,iBAAiB,EAAE,KAAK,eAAe,EAAE,MAAM,aAAa,CAAA;AACpG,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAA;AAClD,OAAO,EAAE,oBAAoB,EAAE,KAAK,aAAa,EAAE,MAAM,YAAY,CAAA;AAErE,wDAAwD;AACxD,eAAO,MAAM,IAAI,mBAAmB,CAAA;AAEpC,sFAAsF;AACtF,eAAO,MAAM,MAAM,UAAwB,CAAA;AAE3C;;;;;;GAMG;AACH,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;aAQjB,CAAA;AAEF,yGAAyG;AACzG,MAAM,WAAW,WAAW;IAC1B,wCAAwC;IACxC,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAA;IACrC,mFAAmF;IACnF,QAAQ,CAAC,OAAO,EAAE,MAAM,eAAe,GAAG,SAAS,CAAA;IACnD,gCAAgC;IAChC,QAAQ,CAAC,GAAG,EAAE,eAAe,CAAA;IAC7B,gCAAgC;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,IAAI,CAAA;IAC7B,kDAAkD;IAClD,QAAQ,CAAC,cAAc,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAA;IAC/C,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,UAAU,KAAK,IAAI,CAAA;CAC9C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,WAAW,GAAG,uBAAuB,CAmBzE;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,mBAAmB,GAAG,IAAI,CAgGrE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/lib/index.js CHANGED
@@ -21,6 +21,7 @@
21
21
  * @module dsh-realtime-agent
22
22
  */
23
23
  import Schema from '@deepseek-ai/schemastery';
24
+ import { REALTIME_ERROR_CODES, RealtimeError } from 'dsh-realtime';
24
25
  import { answerDelegation } from './bridge.js';
25
26
  import { voiceToolDefinitions } from './tools.js';
26
27
  import { TranscriptBuffer } from './transcript.js';
@@ -66,30 +67,105 @@ export function createHandlers(deps) {
66
67
  return;
67
68
  // Handlers are synchronous, so the answer is dispatched rather than awaited. A rejection is
68
69
  // reported rather than thrown: an unanswered delegation is already the failure path.
69
- void answerDelegation(session, delegation, deps.transcript.lines(), deps.ask, deps.timeoutMs)
70
- .catch(deps.onSessionError);
70
+ void answerDelegation(session, delegation, deps.transcript.lines(), deps.ask, deps.timeoutMs(), deps.onAcknowledged).catch(deps.onSessionError);
71
71
  },
72
72
  onAudio: (pcm16) => { deps.onAudio(pcm16); },
73
73
  onClosed: () => { deps.onClosed(); },
74
74
  onError: (error) => { deps.onSessionError(error); },
75
75
  };
76
76
  }
77
+ /**
78
+ * A count this plugin can actually be run with: a whole, positive number.
79
+ *
80
+ * Local to the plugin rather than shared from the seam, deliberately: the reason text is part of this
81
+ * plugin's own onboarding, and the seam has no business knowing that this plugin measures a timeout in
82
+ * milliseconds and a transcript in characters.
83
+ * @param field - the setting's own name, for the reason.
84
+ * @param value - the proposed value.
85
+ * @returns the value, when it is usable.
86
+ */
87
+ function requireCount(field, value) {
88
+ if (!Number.isInteger(value) || value < 1) {
89
+ throw new RealtimeError(`${field} must be a positive whole number`, REALTIME_ERROR_CODES.INVALID_SETTING);
90
+ }
91
+ return value;
92
+ }
77
93
  /**
78
94
  * Hold a session and bridge what the voice model delegates.
79
95
  * @param ctx - the Cordis context, which must already provide the `realtime` service.
80
96
  * @param config - validated configuration.
81
97
  */
82
98
  export function apply(ctx, config) {
83
- const transcript = new TranscriptBuffer(config.maxTranscriptChars);
99
+ const journal = ctx.realtime.journal;
100
+ /**
101
+ * The two fields this plugin reads at the moment of use.
102
+ *
103
+ * `autoStart` is deliberately **not** here as a changeable value. Its only read site is the boot
104
+ * below, so a running process has nothing that could honour a change; the gate was corrected to say
105
+ * so, and it is registered below as restart-bound — which is what makes that correction bite rather
106
+ * than merely being written down.
107
+ */
108
+ const live = { delegationTimeoutMs: config.delegationTimeoutMs, maxTranscriptChars: config.maxTranscriptChars };
109
+ const transcript = new TranscriptBuffer(() => live.maxTranscriptChars);
84
110
  let session;
111
+ // The live fields `docs/control-plane-fields.md` lists for this plugin, plus the one it reclassified:
112
+ // `autoStart` is registered without a setter, so a change to it is refused with the restart it needs
113
+ // instead of being accepted and quietly ignored.
114
+ ctx.effect(function* () {
115
+ const release = ctx.realtime.settings.register(name, [
116
+ {
117
+ field: 'delegationTimeoutMs',
118
+ kind: 'number',
119
+ scope: 'live',
120
+ describe: 'How long the voice model waits for a responder',
121
+ get: () => live.delegationTimeoutMs,
122
+ set: (value) => { live.delegationTimeoutMs = requireCount('delegationTimeoutMs', value); },
123
+ },
124
+ {
125
+ field: 'maxTranscriptChars',
126
+ kind: 'number',
127
+ scope: 'live',
128
+ describe: 'Character budget for the transcript carried on a delegation',
129
+ get: () => live.maxTranscriptChars,
130
+ set: (value) => { live.maxTranscriptChars = requireCount('maxTranscriptChars', value); },
131
+ },
132
+ {
133
+ field: 'autoStart',
134
+ kind: 'boolean',
135
+ scope: 'restart',
136
+ describe: 'Open a session as soon as the plugin mounts',
137
+ get: () => config.autoStart,
138
+ },
139
+ ]);
140
+ yield () => { release(); };
141
+ }, 'realtime-agent.settings');
85
142
  const handlers = createHandlers({
86
143
  transcript,
87
144
  session: () => session,
88
145
  ask: request => ctx.serial('realtime-agent/delegation', request),
89
- timeoutMs: config.delegationTimeoutMs,
90
- onClosed: () => { session = undefined; },
91
- onSessionError: (error) => { ctx.emit('realtime-agent/error', error); },
92
- onAudio: (pcm16) => { ctx.emit('realtime-agent/audio', pcm16); },
146
+ timeoutMs: () => live.delegationTimeoutMs,
147
+ onClosed: () => {
148
+ session = undefined;
149
+ journal.record('session.closed', {});
150
+ },
151
+ onSessionError: (error) => {
152
+ ctx.emit('realtime-agent/error', error);
153
+ // The class, never the message. A provider error can carry the key it refused, and this plugin
154
+ // holds no credential to redact against — so it records the category and leaves the text to the
155
+ // plugin that does. Naming the setting instead of repeating the value, applied to a log line.
156
+ journal.record('session.failed', { class: error.name });
157
+ },
158
+ onAudio: (pcm16) => {
159
+ ctx.emit('realtime-agent/audio', pcm16);
160
+ // Handed to the transport: all the host can observe, and no more (invariant 6).
161
+ journal.record('speech.sent', { bytes: String(pcm16.byteLength) });
162
+ },
163
+ onAcknowledged: (append, delegationId) => {
164
+ // The one place an acknowledgement can honestly be recorded: the seam's appends resolve on the
165
+ // provider's confirmation, not on the send. Note what is deliberately absent beside it — no entry
166
+ // anywhere claims the audio was heard, which is the pair invariant 6 exists to keep apart.
167
+ journal.record('append.acknowledged', { append, delegationId });
168
+ },
93
169
  });
94
170
  /**
95
171
  * Open a session, or return the one already open.
@@ -109,6 +185,7 @@ export function apply(ctx, config) {
109
185
  handlers,
110
186
  });
111
187
  session = opened;
188
+ journal.record('session.opened', { provider: config.provider, model: config.model });
112
189
  return opened;
113
190
  };
114
191
  /**
@@ -122,6 +199,70 @@ export function apply(ctx, config) {
122
199
  session = undefined;
123
200
  await current?.close();
124
201
  };
202
+ /**
203
+ * The session's state, as only this plugin can report it.
204
+ *
205
+ * With nothing open it answers with what a start *would* use, rather than with nothing: "no session,
206
+ * and it would be `openai-live`/`gpt-live-1`" is a different — and more useful — answer than an
207
+ * absence, and it is the one a status surface needs to render a sensible control.
208
+ */
209
+ const status = () => {
210
+ const current = session;
211
+ if (current === undefined) {
212
+ return {
213
+ open: false,
214
+ provider: config.provider,
215
+ model: config.model,
216
+ ...config.voice === undefined ? {} : { voice: config.voice },
217
+ };
218
+ }
219
+ return {
220
+ open: true,
221
+ provider: current.started.provider,
222
+ model: current.started.model,
223
+ ...current.started.voice === undefined ? {} : { voice: current.started.voice },
224
+ sessionId: current.id,
225
+ };
226
+ };
227
+ /**
228
+ * Classify a failed request, carrying what a caller can act on and never the message.
229
+ *
230
+ * A seam failure carries its machine code and the remedy written to be relayed; anything else carries
231
+ * its **class** alone. The message is deliberately dropped: a provider error is exactly where a key
232
+ * turns up, and this plugin holds no credential to redact against — the same reason its journal
233
+ * records the class of a session failure rather than the text.
234
+ * @param error - whatever the attempt threw.
235
+ * @returns the structured refusal.
236
+ */
237
+ const refusalFor = (error) => {
238
+ if (error instanceof RealtimeError) {
239
+ return {
240
+ code: error.code,
241
+ ...error.detail?.remedy === undefined ? {} : { remedy: error.detail.remedy },
242
+ };
243
+ }
244
+ return { class: error instanceof Error ? error.name : typeof error };
245
+ };
246
+ /**
247
+ * Run a session request and report what it produced.
248
+ *
249
+ * The result is **returned as well as** reported on the bus, because a caller may be waiting for it:
250
+ * the control channel dispatches these with `serial`, so `start` can answer with the session that
251
+ * opened rather than with an acknowledgement that it asked. The failure still goes onto the bus, so a
252
+ * transport that merely emits keeps the behaviour it always had.
253
+ * @param attempt - the request to run.
254
+ * @returns whether it achieved what it asked for, and the state afterwards.
255
+ */
256
+ const requestSession = async (attempt) => {
257
+ try {
258
+ await attempt();
259
+ return { ok: true, voice: status() };
260
+ }
261
+ catch (error) {
262
+ ctx.emit('realtime-agent/error', error);
263
+ return { ok: false, voice: status(), refusal: refusalFor(error) };
264
+ }
265
+ };
125
266
  // Tools are an effect, like every other contribution this plugin makes: the fiber that mounted them
126
267
  // releases them, so there is no separate teardown path to forget.
127
268
  ctx.effect(function* () {
@@ -153,17 +294,16 @@ export function apply(ctx, config) {
153
294
  yield () => { dispose(); };
154
295
  }, 'realtime-agent.mic');
155
296
  // Session requests from the transport. The route knows when an authenticated client connects and the
156
- // agent owns the session, so one event is the whole of the wiring between them.
297
+ // agent owns the session, so one event is the whole of the wiring between them. Each listener returns
298
+ // its outcome: `emit` ignores it, `serial` waits for it, and that is how the control channel can
299
+ // answer *what happened* rather than *that it was asked*.
157
300
  ctx.effect(function* () {
158
301
  const disposers = [
159
- ctx.on('realtime-agent/start', () => {
160
- // Fire-and-forget for the same reason `autoStart` is: a listener has no caller to catch a
161
- // rejection, and the failure is reported on the bus rather than thrown into one.
162
- void open().catch((error) => { ctx.emit('realtime-agent/error', error); });
163
- }),
164
- ctx.on('realtime-agent/stop', () => {
165
- void stop().catch((error) => { ctx.emit('realtime-agent/error', error); });
166
- }),
302
+ ctx.on('realtime-agent/start', () => requestSession(() => open())),
303
+ ctx.on('realtime-agent/stop', () => requestSession(() => stop())),
304
+ // A query, and the only listener that answers one. A mounted agent always answers, so a caller
305
+ // that gets `undefined` from the dispatch learns the agent row is absent rather than guessing.
306
+ ctx.on('realtime-agent/status', () => status()),
167
307
  ];
168
308
  yield () => { for (const dispose of disposers)
169
309
  dispose(); };
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -12,11 +12,16 @@ export declare class TranscriptBuffer {
12
12
  private readonly buffered;
13
13
  private chars;
14
14
  /**
15
- * @param maxChars - character budget. Every value is accepted: a budget smaller than one line
16
- * degrades to keeping exactly the most recent line rather than to keeping nothing, because a
17
- * buffer that can hold nothing cannot answer any delegation at all.
15
+ * @param maxChars - character budget, read at every eviction.
16
+ *
17
+ * An accessor rather than a number because `maxTranscriptChars` is a **live** field
18
+ * (`docs/control-plane-fields.md`): the budget can change while the conversation runs, and a copy
19
+ * taken at construction would make the change look applied while the buffer kept the old one.
20
+ * Every value is accepted: a budget smaller than one line degrades to keeping exactly the most recent
21
+ * line rather than to keeping nothing, because a buffer that can hold nothing cannot answer any
22
+ * delegation at all.
18
23
  */
19
- constructor(maxChars: number);
24
+ constructor(maxChars: () => number);
20
25
  /**
21
26
  * Record one fragment.
22
27
  * @param kind - which side spoke.
@@ -1 +1 @@
1
- {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAQrD;;;;;;;GAOG;AACH,qBAAa,gBAAgB;IASf,OAAO,CAAC,QAAQ,CAAC,QAAQ;IARrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,KAAK,CAAI;IAEjB;;;;OAIG;IACH,YAA6B,QAAQ,EAAE,MAAM,EAAI;IAEjD;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAanE;IAED;;;OAGG;IACH,KAAK,IAAI,mBAAmB,EAAE,CAE7B;IAED,yFAAyF;IACzF,OAAO,CAAC,KAAK;CAOd"}
1
+ {"version":3,"file":"transcript.d.ts","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAQrD;;;;;;;GAOG;AACH,qBAAa,gBAAgB;IAcf,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAbrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAqB;IAC9C,OAAO,CAAC,KAAK,CAAI;IAEjB;;;;;;;;;OASG;IACH,YAA6B,QAAQ,EAAE,MAAM,MAAM,EAAI;IAEvD;;;;;OAKG;IACH,MAAM,CAAC,IAAI,EAAE,OAAO,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAanE;IAED;;;OAGG;IACH,KAAK,IAAI,mBAAmB,EAAE,CAE7B;IAED,yFAAyF;IACzF,OAAO,CAAC,KAAK;CAQd"}
package/lib/transcript.js CHANGED
@@ -11,9 +11,14 @@ export class TranscriptBuffer {
11
11
  buffered = [];
12
12
  chars = 0;
13
13
  /**
14
- * @param maxChars - character budget. Every value is accepted: a budget smaller than one line
15
- * degrades to keeping exactly the most recent line rather than to keeping nothing, because a
16
- * buffer that can hold nothing cannot answer any delegation at all.
14
+ * @param maxChars - character budget, read at every eviction.
15
+ *
16
+ * An accessor rather than a number because `maxTranscriptChars` is a **live** field
17
+ * (`docs/control-plane-fields.md`): the budget can change while the conversation runs, and a copy
18
+ * taken at construction would make the change look applied while the buffer kept the old one.
19
+ * Every value is accepted: a budget smaller than one line degrades to keeping exactly the most recent
20
+ * line rather than to keeping nothing, because a buffer that can hold nothing cannot answer any
21
+ * delegation at all.
17
22
  */
18
23
  constructor(maxChars) {
19
24
  this.maxChars = maxChars;
@@ -47,7 +52,8 @@ export class TranscriptBuffer {
47
52
  }
48
53
  /** Drop the oldest lines until the budget is met, never dropping the most recent one. */
49
54
  evict() {
50
- while (this.chars > this.maxChars && this.buffered.length > 1) {
55
+ const budget = this.maxChars();
56
+ while (this.chars > budget && this.buffered.length > 1) {
51
57
  // The loop condition proves a first element exists, so this assertion is an invariant rather
52
58
  // than a hope — and it leaves no unreachable branch for the coverage gate to flag.
53
59
  this.chars -= this.buffered.shift().text.length;
@@ -1 +1 @@
1
- {"version":3,"file":"transcript.js","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAgB;IASE,QAAQ;IARpB,QAAQ,GAAmB,EAAE,CAAA;IACtC,KAAK,GAAG,CAAC,CAAA;IAEjB;;;;OAIG;IACH,YAA6B,QAAgB;wBAAhB,QAAQ;IAAW,CAAC;IAEjD;;;;;OAKG;IACH,MAAM,CAAC,IAAwB,EAAE,IAAY,EAAE,KAAc;QAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAC7B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAA;QAEzB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QACpD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC7D,IAAI,CAAC,IAAI,IAAI,IAAI,CAAA;YACjB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACrB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAED;;;OAGG;IACH,KAAK;QACH,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAC1E,CAAC;IAED,yFAAyF;IACjF,KAAK;QACX,OAAO,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9D,6FAA6F;YAC7F,mFAAmF;YACnF,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAG,CAAC,IAAI,CAAC,MAAM,CAAA;QAClD,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"transcript.js","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"AAQA;;;;;;;GAOG;AACH,MAAM,OAAO,gBAAgB;IAcE,QAAQ;IAbpB,QAAQ,GAAmB,EAAE,CAAA;IACtC,KAAK,GAAG,CAAC,CAAA;IAEjB;;;;;;;;;OASG;IACH,YAA6B,QAAsB;wBAAtB,QAAQ;IAAiB,CAAC;IAEvD;;;;;OAKG;IACH,MAAM,CAAC,IAAwB,EAAE,IAAY,EAAE,KAAc;QAC3D,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAM;QAC7B,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,CAAA;QAEzB,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAA;QACpD,IAAI,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;YAC7D,IAAI,CAAC,IAAI,IAAI,IAAI,CAAA;YACjB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QACrB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAA;QACnD,CAAC;QAED,IAAI,CAAC,KAAK,EAAE,CAAA;IACd,CAAC;IAED;;;OAGG;IACH,KAAK;QACH,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;IAC1E,CAAC;IAED,yFAAyF;IACjF,KAAK;QACX,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAA;QAC9B,OAAO,IAAI,CAAC,KAAK,GAAG,MAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvD,6FAA6F;YAC7F,mFAAmF;YACnF,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAG,CAAC,IAAI,CAAC,MAAM,CAAA;QAClD,CAAC;IACH,CAAC;CACF"}
package/lib/types.d.ts CHANGED
@@ -68,4 +68,60 @@ export interface RealtimeAgentConfig {
68
68
  /** Character budget for the transcript carried on a delegation request. */
69
69
  maxTranscriptChars: number;
70
70
  }
71
+ /**
72
+ * The voice session's state, as the plugin that owns it is the only one able to report it.
73
+ *
74
+ * A **query's** answer rather than an event's payload: nothing emits this, and a caller that needs it
75
+ * asks through `realtime-agent/status` — which is also what makes "is voice live" answerable without
76
+ * opening anything to find out.
77
+ */
78
+ export interface RealtimeVoiceStatus {
79
+ /** Whether a provider session is open now. */
80
+ readonly open: boolean;
81
+ /**
82
+ * The registered route the session is on — or, when none is open, the one a start *would* use.
83
+ *
84
+ * Read from the session the provider actually accepted when there is one, because a provider may
85
+ * alias what was asked for.
86
+ */
87
+ readonly provider: string;
88
+ /** The model the provider accepted, or the one configured when nothing is open. */
89
+ readonly model: string;
90
+ /** The output voice, when one has been settled. */
91
+ readonly voice?: string;
92
+ /** The provider's own session id. Present only while a session is open. */
93
+ readonly sessionId?: string;
94
+ }
95
+ /**
96
+ * Why a session request did not succeed, in the shape a caller can act on.
97
+ *
98
+ * **Never the failure's message**, and that is a deliberate limitation rather than an oversight: the
99
+ * plugin that holds the credential is the adapter, and this one holds none to redact against — which is
100
+ * the same rule its journal follows, where a session failure is recorded as its *class*. What it can
101
+ * carry instead is more useful than the message: the seam's own machine code, and the `remedy` written
102
+ * to be relayed verbatim, both of which name a *setting* rather than its value.
103
+ */
104
+ export interface RealtimeSessionRefusal {
105
+ /** The seam's machine code, when the failure was one of its classified ones (`NOT_CONFIGURED`, `RATE_LIMITED`, …). */
106
+ readonly code?: string;
107
+ /** What to do about it, written to be relayed verbatim to whoever is trying to use the feature. */
108
+ readonly remedy?: string;
109
+ /** The failing class, when there was no code to carry. A class, never an instance's message. */
110
+ readonly class?: string;
111
+ }
112
+ /**
113
+ * What a request to open or close the voice session produced.
114
+ *
115
+ * The **outcome**, not an acknowledgement, because the two are the difference between "asked" and "it
116
+ * worked" — and collapsing them is the failure this project has already paid for once: a `start` that
117
+ * replied *requested* while the open silently failed teaches a user that the plugin is broken.
118
+ */
119
+ export interface RealtimeSessionRequestOutcome {
120
+ /** Whether the request achieved what it asked for. */
121
+ readonly ok: boolean;
122
+ /** The state **after** the attempt. */
123
+ readonly voice: RealtimeVoiceStatus;
124
+ /** Present exactly when `ok` is false. */
125
+ readonly refusal?: RealtimeSessionRefusal;
126
+ }
71
127
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iFAAiF;AACjF,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;IACxB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,EAAE,EAAE,MAAM,CAAA;IACV,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAA;IAChB,0FAA0F;IAC1F,UAAU,EAAE,SAAS,mBAAmB,EAAE,CAAA;IAC1C,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAA;IAChB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAA;IAClB,wGAAwG;IACxG,mBAAmB,EAAE,MAAM,CAAA;IAC3B,2EAA2E;IAC3E,kBAAkB,EAAE,MAAM,CAAA;CAC3B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,iFAAiF;AACjF,MAAM,WAAW,mBAAmB;IAClC,wBAAwB;IACxB,IAAI,EAAE,OAAO,GAAG,QAAQ,CAAA;IACxB,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAA;CACb;AAED;;;;;;GAMG;AACH,MAAM,WAAW,iBAAiB;IAChC,qFAAqF;IACrF,EAAE,EAAE,MAAM,CAAA;IACV,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAA;IAChB,0FAA0F;IAC1F,UAAU,EAAE,SAAS,mBAAmB,EAAE,CAAA;IAC1C,2DAA2D;IAC3D,SAAS,EAAE,MAAM,CAAA;CAClB;AAED;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAA;IACZ;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAC3B;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,sDAAsD;IACtD,QAAQ,EAAE,MAAM,CAAA;IAChB,8BAA8B;IAC9B,KAAK,EAAE,MAAM,CAAA;IACb,yDAAyD;IACzD,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,iDAAiD;IACjD,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAA;IAClB,wGAAwG;IACxG,mBAAmB,EAAE,MAAM,CAAA;IAC3B,2EAA2E;IAC3E,kBAAkB,EAAE,MAAM,CAAA;CAC3B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,8CAA8C;IAC9C,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAA;IACtB;;;;;OAKG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,mFAAmF;IACnF,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;IACtB,mDAAmD;IACnD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,sBAAsB;IACrC,sHAAsH;IACtH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAA;IACtB,mGAAmG;IACnG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,gGAAgG;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CACxB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,6BAA6B;IAC5C,sDAAsD;IACtD,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAA;IACpB,uCAAuC;IACvC,QAAQ,CAAC,KAAK,EAAE,mBAAmB,CAAA;IACnC,0CAA0C;IAC1C,QAAQ,CAAC,OAAO,CAAC,EAAE,sBAAsB,CAAA;CAC1C"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-realtime-agent",
3
- "version": "0.2.5",
3
+ "version": "0.2.7",
4
4
  "description": "Delegation bridge for the dsh-realtime seam: answers what the voice model delegates, or says plainly that it cannot.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -42,7 +42,7 @@
42
42
  "registry": "https://registry.npmjs.org"
43
43
  },
44
44
  "dependencies": {
45
- "dsh-realtime": "^0.2.1"
45
+ "dsh-realtime": "^0.2.3"
46
46
  },
47
47
  "peerDependencies": {
48
48
  "@deepseek-ai/cordis": "^4.0.3",
package/src/bridge.ts CHANGED
@@ -55,6 +55,9 @@ function settled(ask: DelegationAsker, request: DelegationRequest): Promise<Dele
55
55
  }
56
56
  }
57
57
 
58
+ /** Which channel an append went out on. What an acknowledgement names, and what it does not. */
59
+ export type DelegationAppend = 'commentary' | 'thinking'
60
+
58
61
  /**
59
62
  * Answer one delegation, or tell the model plainly that it could not be answered.
60
63
  *
@@ -64,6 +67,7 @@ function settled(ask: DelegationAsker, request: DelegationRequest): Promise<Dele
64
67
  * @param transcript - the conversation so far, as the responder will see it.
65
68
  * @param ask - how to reach a responder.
66
69
  * @param timeoutMs - bound on waiting for one.
70
+ * @param onAcknowledged - called once per accepted append, which is not once per thing heard.
67
71
  * @returns a promise settling once the session has been answered.
68
72
  */
69
73
  export async function answerDelegation(
@@ -72,6 +76,7 @@ export async function answerDelegation(
72
76
  transcript: readonly AgentTranscriptLine[],
73
77
  ask: DelegationAsker,
74
78
  timeoutMs: number,
79
+ onAcknowledged: (append: DelegationAppend, delegationId: string) => void,
75
80
  ): Promise<void> {
76
81
  const request: DelegationRequest = {
77
82
  id: delegation.id,
@@ -89,13 +94,21 @@ export async function answerDelegation(
89
94
  const answer = await Promise.race([settled(ask, request), expired])
90
95
  const text = typeof answer?.text === 'string' ? answer.text.trim() : ''
91
96
 
97
+ // Every append below awaits the provider's acknowledgement rather than the send — that is the seam's
98
+ // session contract, written that way for a measured reason. So a resolved await **is** the
99
+ // acknowledgement, and this is the only honest place to record one. What it is not is delivery:
100
+ // nothing here says a speaker rendered anything, and keeping those two apart is the whole of
101
+ // invariant 6 — and the reason the fault-injection matrix has a row for exactly this pair.
92
102
  if (text.length === 0) {
93
103
  await session.appendCommentary(UNANSWERED_NOTICE, delegation.id)
104
+ onAcknowledged('commentary', delegation.id)
94
105
  return
95
106
  }
96
107
  if (answer?.mode === 'spoken') {
97
108
  await session.appendCommentary(boundAppend(text), delegation.id)
109
+ onAcknowledged('commentary', delegation.id)
98
110
  return
99
111
  }
100
112
  await session.appendThinking(boundAppend(text), delegation.id)
113
+ onAcknowledged('thinking', delegation.id)
101
114
  }
package/src/index.ts CHANGED
@@ -24,10 +24,11 @@
24
24
  import Schema from '@deepseek-ai/schemastery'
25
25
  import type { Context } from '@deepseek-ai/cordis'
26
26
  import type { RealtimeDelegation, RealtimeDelegationSettlement, RealtimeSession, RealtimeSessionHandlers, RealtimeTranscript } from 'dsh-realtime'
27
- import { answerDelegation, type DelegationAsker } from './bridge.ts'
27
+ import { REALTIME_ERROR_CODES, RealtimeError } from 'dsh-realtime'
28
+ import { answerDelegation, type DelegationAppend, type DelegationAsker } from './bridge.ts'
28
29
  import { voiceToolDefinitions } from './tools.ts'
29
30
  import { TranscriptBuffer } from './transcript.ts'
30
- import type { DelegationAnswer, DelegationRequest, RealtimeAgentConfig } from './types.ts'
31
+ import type { DelegationAnswer, DelegationRequest, RealtimeAgentConfig, RealtimeSessionRefusal, RealtimeSessionRequestOutcome, RealtimeVoiceStatus } from './types.ts'
31
32
 
32
33
  declare module '@deepseek-ai/cordis' {
33
34
  interface Events {
@@ -77,15 +78,27 @@ declare module '@deepseek-ai/cordis' {
77
78
  * Emitted by a transport when an authenticated client arrives, so that connecting a microphone is
78
79
  * enough to be heard — with no profile option and no dependence on a model choosing to call
79
80
  * `voice_start`. The session belongs to the agent, so the transport asks rather than opens one itself.
81
+ *
82
+ * Returns the request's **outcome** so a caller that is waiting for the answer can have it —
83
+ * `ctx.serial` from the control channel — while the transport, which only *emits*, is unaffected.
80
84
  */
81
- 'realtime-agent/start'(): void
85
+ 'realtime-agent/start'(): RealtimeSessionRequestOutcome | undefined | Promise<RealtimeSessionRequestOutcome | undefined>
82
86
  /**
83
87
  * Close the voice session.
84
88
  *
85
89
  * Emitted by a transport when its last client goes away, including when the transport itself is
86
90
  * disposed — a session outliving the microphone that asked for it is a socket nobody is listening to.
91
+ * Returns the outcome, for the same reason `realtime-agent/start` does.
92
+ */
93
+ 'realtime-agent/stop'(): RealtimeSessionRequestOutcome | undefined | Promise<RealtimeSessionRequestOutcome | undefined>
94
+ /**
95
+ * The voice session's state — a **query**, so nothing emits it.
96
+ *
97
+ * `undefined` means no listener answered, which is the honest answer for a composition with no agent
98
+ * row: it is distinguishable from a session that is merely closed, because the agent that is mounted
99
+ * always answers.
87
100
  */
88
- 'realtime-agent/stop'(): void
101
+ 'realtime-agent/status'(): RealtimeVoiceStatus | undefined | Promise<RealtimeVoiceStatus | undefined>
89
102
  }
90
103
  }
91
104
 
@@ -125,14 +138,27 @@ export interface HandlerDeps {
125
138
  readonly session: () => RealtimeSession | undefined
126
139
  /** How to reach a responder. */
127
140
  readonly ask: DelegationAsker
128
- /** Bound on waiting for one. */
129
- readonly timeoutMs: number
141
+ /**
142
+ * Bound on waiting for one, read at the moment the delegation is answered.
143
+ *
144
+ * An accessor because `delegationTimeoutMs` is a **live** field: the window the voice model waits in
145
+ * can be changed while the plugin runs, and the change must apply to the next delegation rather than
146
+ * to the next boot.
147
+ */
148
+ readonly timeoutMs: () => number
130
149
  /** Called when the session ended, so the caller can drop its reference. */
131
150
  readonly onClosed: () => void
132
151
  /** Where a session-scoped failure is reported. */
133
152
  readonly onSessionError: (error: Error) => void
134
153
  /** Where output audio is delivered. Called once per provider delta. */
135
154
  readonly onAudio: (pcm16: Uint8Array) => void
155
+ /**
156
+ * Where an accepted append is reported.
157
+ *
158
+ * An acknowledgement, not a delivery: it says the provider took the append, and nothing about
159
+ * whether a speaker ever rendered it. See the seam's session contract and invariant 6.
160
+ */
161
+ readonly onAcknowledged: (append: DelegationAppend, delegationId: string) => void
136
162
  }
137
163
 
138
164
  /**
@@ -152,8 +178,14 @@ export function createHandlers(deps: HandlerDeps): RealtimeSessionHandlers {
152
178
  if (session === undefined) return
153
179
  // Handlers are synchronous, so the answer is dispatched rather than awaited. A rejection is
154
180
  // reported rather than thrown: an unanswered delegation is already the failure path.
155
- void answerDelegation(session, delegation, deps.transcript.lines(), deps.ask, deps.timeoutMs)
156
- .catch(deps.onSessionError)
181
+ void answerDelegation(
182
+ session,
183
+ delegation,
184
+ deps.transcript.lines(),
185
+ deps.ask,
186
+ deps.timeoutMs(),
187
+ deps.onAcknowledged,
188
+ ).catch(deps.onSessionError)
157
189
  },
158
190
  onAudio: (pcm16: Uint8Array): void => { deps.onAudio(pcm16) },
159
191
  onClosed: (): void => { deps.onClosed() },
@@ -161,23 +193,103 @@ export function createHandlers(deps: HandlerDeps): RealtimeSessionHandlers {
161
193
  }
162
194
  }
163
195
 
196
+ /**
197
+ * A count this plugin can actually be run with: a whole, positive number.
198
+ *
199
+ * Local to the plugin rather than shared from the seam, deliberately: the reason text is part of this
200
+ * plugin's own onboarding, and the seam has no business knowing that this plugin measures a timeout in
201
+ * milliseconds and a transcript in characters.
202
+ * @param field - the setting's own name, for the reason.
203
+ * @param value - the proposed value.
204
+ * @returns the value, when it is usable.
205
+ */
206
+ function requireCount(field: string, value: number): number {
207
+ if (!Number.isInteger(value) || value < 1) {
208
+ throw new RealtimeError(`${field} must be a positive whole number`, REALTIME_ERROR_CODES.INVALID_SETTING)
209
+ }
210
+ return value
211
+ }
212
+
164
213
  /**
165
214
  * Hold a session and bridge what the voice model delegates.
166
215
  * @param ctx - the Cordis context, which must already provide the `realtime` service.
167
216
  * @param config - validated configuration.
168
217
  */
169
218
  export function apply(ctx: Context, config: RealtimeAgentConfig): void {
170
- const transcript = new TranscriptBuffer(config.maxTranscriptChars)
219
+ const journal = ctx.realtime.journal
220
+
221
+ /**
222
+ * The two fields this plugin reads at the moment of use.
223
+ *
224
+ * `autoStart` is deliberately **not** here as a changeable value. Its only read site is the boot
225
+ * below, so a running process has nothing that could honour a change; the gate was corrected to say
226
+ * so, and it is registered below as restart-bound — which is what makes that correction bite rather
227
+ * than merely being written down.
228
+ */
229
+ const live = { delegationTimeoutMs: config.delegationTimeoutMs, maxTranscriptChars: config.maxTranscriptChars }
230
+
231
+ const transcript = new TranscriptBuffer(() => live.maxTranscriptChars)
171
232
  let session: RealtimeSession | undefined
172
233
 
234
+ // The live fields `docs/control-plane-fields.md` lists for this plugin, plus the one it reclassified:
235
+ // `autoStart` is registered without a setter, so a change to it is refused with the restart it needs
236
+ // instead of being accepted and quietly ignored.
237
+ ctx.effect(function* () {
238
+ const release = ctx.realtime.settings.register(name, [
239
+ {
240
+ field: 'delegationTimeoutMs',
241
+ kind: 'number',
242
+ scope: 'live',
243
+ describe: 'How long the voice model waits for a responder',
244
+ get: () => live.delegationTimeoutMs,
245
+ set: (value: number) => { live.delegationTimeoutMs = requireCount('delegationTimeoutMs', value) },
246
+ },
247
+ {
248
+ field: 'maxTranscriptChars',
249
+ kind: 'number',
250
+ scope: 'live',
251
+ describe: 'Character budget for the transcript carried on a delegation',
252
+ get: () => live.maxTranscriptChars,
253
+ set: (value: number) => { live.maxTranscriptChars = requireCount('maxTranscriptChars', value) },
254
+ },
255
+ {
256
+ field: 'autoStart',
257
+ kind: 'boolean',
258
+ scope: 'restart',
259
+ describe: 'Open a session as soon as the plugin mounts',
260
+ get: () => config.autoStart,
261
+ },
262
+ ])
263
+ yield () => { release() }
264
+ }, 'realtime-agent.settings')
265
+
173
266
  const handlers = createHandlers({
174
267
  transcript,
175
268
  session: () => session,
176
269
  ask: request => ctx.serial('realtime-agent/delegation', request),
177
- timeoutMs: config.delegationTimeoutMs,
178
- onClosed: () => { session = undefined },
179
- onSessionError: (error) => { ctx.emit('realtime-agent/error', error) },
180
- onAudio: (pcm16) => { ctx.emit('realtime-agent/audio', pcm16) },
270
+ timeoutMs: () => live.delegationTimeoutMs,
271
+ onClosed: () => {
272
+ session = undefined
273
+ journal.record('session.closed', {})
274
+ },
275
+ onSessionError: (error) => {
276
+ ctx.emit('realtime-agent/error', error)
277
+ // The class, never the message. A provider error can carry the key it refused, and this plugin
278
+ // holds no credential to redact against — so it records the category and leaves the text to the
279
+ // plugin that does. Naming the setting instead of repeating the value, applied to a log line.
280
+ journal.record('session.failed', { class: error.name })
281
+ },
282
+ onAudio: (pcm16) => {
283
+ ctx.emit('realtime-agent/audio', pcm16)
284
+ // Handed to the transport: all the host can observe, and no more (invariant 6).
285
+ journal.record('speech.sent', { bytes: String(pcm16.byteLength) })
286
+ },
287
+ onAcknowledged: (append, delegationId) => {
288
+ // The one place an acknowledgement can honestly be recorded: the seam's appends resolve on the
289
+ // provider's confirmation, not on the send. Note what is deliberately absent beside it — no entry
290
+ // anywhere claims the audio was heard, which is the pair invariant 6 exists to keep apart.
291
+ journal.record('append.acknowledged', { append, delegationId })
292
+ },
181
293
  })
182
294
 
183
295
  /**
@@ -197,6 +309,7 @@ export function apply(ctx: Context, config: RealtimeAgentConfig): void {
197
309
  handlers,
198
310
  })
199
311
  session = opened
312
+ journal.record('session.opened', { provider: config.provider, model: config.model })
200
313
  return opened
201
314
  }
202
315
 
@@ -212,6 +325,72 @@ export function apply(ctx: Context, config: RealtimeAgentConfig): void {
212
325
  await current?.close()
213
326
  }
214
327
 
328
+ /**
329
+ * The session's state, as only this plugin can report it.
330
+ *
331
+ * With nothing open it answers with what a start *would* use, rather than with nothing: "no session,
332
+ * and it would be `openai-live`/`gpt-live-1`" is a different — and more useful — answer than an
333
+ * absence, and it is the one a status surface needs to render a sensible control.
334
+ */
335
+ const status = (): RealtimeVoiceStatus => {
336
+ const current = session
337
+ if (current === undefined) {
338
+ return {
339
+ open: false,
340
+ provider: config.provider,
341
+ model: config.model,
342
+ ...config.voice === undefined ? {} : { voice: config.voice },
343
+ }
344
+ }
345
+ return {
346
+ open: true,
347
+ provider: current.started.provider,
348
+ model: current.started.model,
349
+ ...current.started.voice === undefined ? {} : { voice: current.started.voice },
350
+ sessionId: current.id,
351
+ }
352
+ }
353
+
354
+ /**
355
+ * Classify a failed request, carrying what a caller can act on and never the message.
356
+ *
357
+ * A seam failure carries its machine code and the remedy written to be relayed; anything else carries
358
+ * its **class** alone. The message is deliberately dropped: a provider error is exactly where a key
359
+ * turns up, and this plugin holds no credential to redact against — the same reason its journal
360
+ * records the class of a session failure rather than the text.
361
+ * @param error - whatever the attempt threw.
362
+ * @returns the structured refusal.
363
+ */
364
+ const refusalFor = (error: unknown): RealtimeSessionRefusal => {
365
+ if (error instanceof RealtimeError) {
366
+ return {
367
+ code: error.code,
368
+ ...error.detail?.remedy === undefined ? {} : { remedy: error.detail.remedy },
369
+ }
370
+ }
371
+ return { class: error instanceof Error ? error.name : typeof error }
372
+ }
373
+
374
+ /**
375
+ * Run a session request and report what it produced.
376
+ *
377
+ * The result is **returned as well as** reported on the bus, because a caller may be waiting for it:
378
+ * the control channel dispatches these with `serial`, so `start` can answer with the session that
379
+ * opened rather than with an acknowledgement that it asked. The failure still goes onto the bus, so a
380
+ * transport that merely emits keeps the behaviour it always had.
381
+ * @param attempt - the request to run.
382
+ * @returns whether it achieved what it asked for, and the state afterwards.
383
+ */
384
+ const requestSession = async (attempt: () => Promise<unknown>): Promise<RealtimeSessionRequestOutcome> => {
385
+ try {
386
+ await attempt()
387
+ return { ok: true, voice: status() }
388
+ } catch (error) {
389
+ ctx.emit('realtime-agent/error', error as Error)
390
+ return { ok: false, voice: status(), refusal: refusalFor(error) }
391
+ }
392
+ }
393
+
215
394
  // Tools are an effect, like every other contribution this plugin makes: the fiber that mounted them
216
395
  // releases them, so there is no separate teardown path to forget.
217
396
  ctx.effect(function* () {
@@ -242,17 +421,16 @@ export function apply(ctx: Context, config: RealtimeAgentConfig): void {
242
421
  }, 'realtime-agent.mic')
243
422
 
244
423
  // Session requests from the transport. The route knows when an authenticated client connects and the
245
- // agent owns the session, so one event is the whole of the wiring between them.
424
+ // agent owns the session, so one event is the whole of the wiring between them. Each listener returns
425
+ // its outcome: `emit` ignores it, `serial` waits for it, and that is how the control channel can
426
+ // answer *what happened* rather than *that it was asked*.
246
427
  ctx.effect(function* () {
247
428
  const disposers = [
248
- ctx.on('realtime-agent/start', () => {
249
- // Fire-and-forget for the same reason `autoStart` is: a listener has no caller to catch a
250
- // rejection, and the failure is reported on the bus rather than thrown into one.
251
- void open().catch((error: Error) => { ctx.emit('realtime-agent/error', error) })
252
- }),
253
- ctx.on('realtime-agent/stop', () => {
254
- void stop().catch((error: Error) => { ctx.emit('realtime-agent/error', error) })
255
- }),
429
+ ctx.on('realtime-agent/start', () => requestSession(() => open())),
430
+ ctx.on('realtime-agent/stop', () => requestSession(() => stop())),
431
+ // A query, and the only listener that answers one. A mounted agent always answers, so a caller
432
+ // that gets `undefined` from the dispatch learns the agent row is absent rather than guessing.
433
+ ctx.on('realtime-agent/status', () => status()),
256
434
  ]
257
435
  yield () => { for (const dispose of disposers) dispose() }
258
436
  }, 'realtime-agent.session-requests')
package/src/transcript.ts CHANGED
@@ -19,11 +19,16 @@ export class TranscriptBuffer {
19
19
  private chars = 0
20
20
 
21
21
  /**
22
- * @param maxChars - character budget. Every value is accepted: a budget smaller than one line
23
- * degrades to keeping exactly the most recent line rather than to keeping nothing, because a
24
- * buffer that can hold nothing cannot answer any delegation at all.
22
+ * @param maxChars - character budget, read at every eviction.
23
+ *
24
+ * An accessor rather than a number because `maxTranscriptChars` is a **live** field
25
+ * (`docs/control-plane-fields.md`): the budget can change while the conversation runs, and a copy
26
+ * taken at construction would make the change look applied while the buffer kept the old one.
27
+ * Every value is accepted: a budget smaller than one line degrades to keeping exactly the most recent
28
+ * line rather than to keeping nothing, because a buffer that can hold nothing cannot answer any
29
+ * delegation at all.
25
30
  */
26
- constructor(private readonly maxChars: number) {}
31
+ constructor(private readonly maxChars: () => number) {}
27
32
 
28
33
  /**
29
34
  * Record one fragment.
@@ -56,7 +61,8 @@ export class TranscriptBuffer {
56
61
 
57
62
  /** Drop the oldest lines until the budget is met, never dropping the most recent one. */
58
63
  private evict(): void {
59
- while (this.chars > this.maxChars && this.buffered.length > 1) {
64
+ const budget = this.maxChars()
65
+ while (this.chars > budget && this.buffered.length > 1) {
60
66
  // The loop condition proves a first element exists, so this assertion is an invariant rather
61
67
  // than a hope — and it leaves no unreachable branch for the coverage gate to flag.
62
68
  this.chars -= this.buffered.shift()!.text.length
package/src/types.ts CHANGED
@@ -72,3 +72,62 @@ export interface RealtimeAgentConfig {
72
72
  /** Character budget for the transcript carried on a delegation request. */
73
73
  maxTranscriptChars: number
74
74
  }
75
+
76
+ /**
77
+ * The voice session's state, as the plugin that owns it is the only one able to report it.
78
+ *
79
+ * A **query's** answer rather than an event's payload: nothing emits this, and a caller that needs it
80
+ * asks through `realtime-agent/status` — which is also what makes "is voice live" answerable without
81
+ * opening anything to find out.
82
+ */
83
+ export interface RealtimeVoiceStatus {
84
+ /** Whether a provider session is open now. */
85
+ readonly open: boolean
86
+ /**
87
+ * The registered route the session is on — or, when none is open, the one a start *would* use.
88
+ *
89
+ * Read from the session the provider actually accepted when there is one, because a provider may
90
+ * alias what was asked for.
91
+ */
92
+ readonly provider: string
93
+ /** The model the provider accepted, or the one configured when nothing is open. */
94
+ readonly model: string
95
+ /** The output voice, when one has been settled. */
96
+ readonly voice?: string
97
+ /** The provider's own session id. Present only while a session is open. */
98
+ readonly sessionId?: string
99
+ }
100
+
101
+ /**
102
+ * Why a session request did not succeed, in the shape a caller can act on.
103
+ *
104
+ * **Never the failure's message**, and that is a deliberate limitation rather than an oversight: the
105
+ * plugin that holds the credential is the adapter, and this one holds none to redact against — which is
106
+ * the same rule its journal follows, where a session failure is recorded as its *class*. What it can
107
+ * carry instead is more useful than the message: the seam's own machine code, and the `remedy` written
108
+ * to be relayed verbatim, both of which name a *setting* rather than its value.
109
+ */
110
+ export interface RealtimeSessionRefusal {
111
+ /** The seam's machine code, when the failure was one of its classified ones (`NOT_CONFIGURED`, `RATE_LIMITED`, …). */
112
+ readonly code?: string
113
+ /** What to do about it, written to be relayed verbatim to whoever is trying to use the feature. */
114
+ readonly remedy?: string
115
+ /** The failing class, when there was no code to carry. A class, never an instance's message. */
116
+ readonly class?: string
117
+ }
118
+
119
+ /**
120
+ * What a request to open or close the voice session produced.
121
+ *
122
+ * The **outcome**, not an acknowledgement, because the two are the difference between "asked" and "it
123
+ * worked" — and collapsing them is the failure this project has already paid for once: a `start` that
124
+ * replied *requested* while the open silently failed teaches a user that the plugin is broken.
125
+ */
126
+ export interface RealtimeSessionRequestOutcome {
127
+ /** Whether the request achieved what it asked for. */
128
+ readonly ok: boolean
129
+ /** The state **after** the attempt. */
130
+ readonly voice: RealtimeVoiceStatus
131
+ /** Present exactly when `ok` is false. */
132
+ readonly refusal?: RealtimeSessionRefusal
133
+ }