@roll-agent/relay-client 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -103,3 +103,7 @@ error.
103
103
  `@roll-agent/relay-client/testing` exports `createRelayClientForTesting()` and the minimal WebSocket
104
104
  and scheduler interfaces. Inject a deterministic transport without adding Node APIs to production
105
105
  Browser code.
106
+
107
+ ### Context recovery and Wire 1.3
108
+
109
+ `RelayThreadView.contextRecovery` carries the latest bounded recovery gate state from snapshots and `context.recovery.changed` events. It is `null` for older peers. The default session advertisement remains `["1.2", "1.1"]` for existing Relay services with strict version allowlists. After upgrading the Relay service and Companion together, pass `relayProtocolVersions: ["1.3", "1.2", "1.1"]` to `createRelayClient`. Companion's bridge accepts `protocolVersion: "1.3"`; the Core host session factory accepts `relayProtocolVersion: "1.3"`. The deployed Relay service must support 1.3 ticket negotiation and routing before enabling either option.
package/dist/client.d.ts CHANGED
@@ -1,17 +1,17 @@
1
- import { relayRequestMethodSchemasV12, type RelayInteractionRequestV11, type RelayRequestParamsForVersion, type RelayRequestResultForVersion } from "@roll-agent/relay-protocol";
1
+ import { relayRequestMethodSchemasV13, type RelayInteractionRequestV11, type RelayRequestParamsForVersion, type RelayRequestResultForVersion } from "@roll-agent/relay-protocol";
2
2
  import { z } from "zod/v4";
3
3
  import { type RelayClientErrorDetails, type RelayConnectionState, type RelayThreadView } from "./schemas.ts";
4
4
  import { type RelayClientRuntimeDependencies } from "./transport.ts";
5
- type ThreadListWireInput = z.input<(typeof relayRequestMethodSchemasV12)["thread.list"]["params"]>;
6
- type ThreadCreateWireInput = z.input<(typeof relayRequestMethodSchemasV12)["thread.create"]["params"]>;
7
- type ThreadListWireResult = RelayRequestResultForVersion<"1.2", "thread.list">;
8
- type ThreadOpenWireResult = RelayRequestResultForVersion<"1.2", "thread.open">;
9
- type ThreadSnapshotWireResult = RelayRequestResultForVersion<"1.2", "thread.snapshot">;
10
- type TurnStartWireResult = RelayRequestResultForVersion<"1.2", "turn.start">;
11
- type TurnCancelWireResult = RelayRequestResultForVersion<"1.2", "turn.cancel">;
12
- type InteractionCandidateWireResult = RelayRequestResultForVersion<"1.2", "interaction.candidate">;
13
- export type RelayOperationResult = RelayRequestResultForVersion<"1.2", "operation.result.get">;
14
- export type RelayThreadCapabilities = RelayRequestResultForVersion<"1.2", "thread.capabilities">;
5
+ type ThreadListWireInput = z.input<(typeof relayRequestMethodSchemasV13)["thread.list"]["params"]>;
6
+ type ThreadCreateWireInput = z.input<(typeof relayRequestMethodSchemasV13)["thread.create"]["params"]>;
7
+ type ThreadListWireResult = RelayRequestResultForVersion<"1.3", "thread.list">;
8
+ type ThreadOpenWireResult = RelayRequestResultForVersion<"1.3", "thread.open"> | RelayRequestResultForVersion<"1.2", "thread.open">;
9
+ type ThreadSnapshotWireResult = RelayRequestResultForVersion<"1.3", "thread.snapshot"> | RelayRequestResultForVersion<"1.2", "thread.snapshot">;
10
+ type TurnStartWireResult = RelayRequestResultForVersion<"1.3", "turn.start">;
11
+ type TurnCancelWireResult = RelayRequestResultForVersion<"1.3", "turn.cancel">;
12
+ type InteractionCandidateWireResult = RelayRequestResultForVersion<"1.3", "interaction.candidate">;
13
+ export type RelayOperationResult = RelayRequestResultForVersion<"1.3", "operation.result.get">;
14
+ export type RelayThreadCapabilities = RelayRequestResultForVersion<"1.3", "thread.capabilities">;
15
15
  export type RelayThreadId = ThreadOpenWireResult["thread"]["id"];
16
16
  export type RelayTurnId = RelayRequestParamsForVersion<"1.2", "turn.cancel">["turnId"];
17
17
  export type RelayInteractionId = RelayInteractionRequestV11["interactionId"];
@@ -27,7 +27,7 @@ export type RelayTurnCancelResult = TurnCancelWireResult;
27
27
  export type RelayInteractionCandidateResult = InteractionCandidateWireResult;
28
28
  export interface RelaySessionProviderInput {
29
29
  readonly signal: AbortSignal;
30
- readonly supportedRelayProtocolVersions: readonly ["1.2", "1.1"];
30
+ readonly supportedRelayProtocolVersions: readonly ("1.3" | "1.2" | "1.1")[];
31
31
  }
32
32
  export type RelaySessionProvider = (input: RelaySessionProviderInput) => Promise<unknown>;
33
33
  export interface RelayRequestOptions {
@@ -36,6 +36,8 @@ export interface RelayRequestOptions {
36
36
  }
37
37
  export interface CreateRelayClientOptions {
38
38
  readonly getSession: RelaySessionProvider;
39
+ /** Opt into 1.3 only after the Relay service and Companion support it. */
40
+ readonly relayProtocolVersions?: readonly ("1.3" | "1.2" | "1.1")[];
39
41
  readonly requestTimeoutMs?: number;
40
42
  }
41
43
  export type RelayConnectionListener = (state: RelayConnectionState) => void;
package/dist/client.js CHANGED
@@ -1 +1 @@
1
- import{RELAY_ERROR_CODES_V11 as e,RELAY_MESSAGE_TYPES_V11 as t,RELAY_REQUEST_METHODS_V11 as s,RELAY_REQUEST_METHOD_DISPOSITIONS_V12 as n,parseRelayInteractionCandidateForRequestV11 as i,parseRelayRequestParamsForVersion as r,parseRelayRequestResultForVersion as o,relayAckSchemaV11 as a,relayMessageSchemaV12 as c,relayRequestIdSchema as l,relayRequestMethodSchemasV12 as d,relayRuntimeRequestSchemaV12 as h}from"@roll-agent/relay-protocol";import{parseRelayBrowserFirstControlFrameAny as u,relayBrowserControlMessageSchemaAny as p,relaySessionDescriptorSchema as y}from"@roll-agent/relay-protocol/control";import{z as m}from"zod/v4";import{RELAY_CLIENT_TRANSPORT_ERROR_CODES as f,getPendingInteractionRequest as g,relayClientErrorDetailsSchema as v,relayConnectionStateSchema as w,relayThreadViewSchema as R}from"./schemas.js";import{browserRelayRuntimeDependencies as S}from"./transport.js";export class RelayClientError extends Error{details;constructor(e){super(e.message),this.name="RelayClientError",this.details=v.parse(e)}}const b=3e4,I=1,q=-1,k={normal:1e3,invalidFrame:4002,sessionFatal:4008,sessionRetryable:4012},C=new Set(["completed","cancelled","failed"]);function T(e,t,s){return new RelayClientError({kind:"transport",code:e,message:t,retryable:s})}function E(e){return e instanceof RelayClientError?e:T(f.transportError,e instanceof Error?e.message:"Relay transport failed",!0)}function P(e,t){try{e(t)}catch(e){if("function"==typeof globalThis.reportError)try{globalThis.reportError(e)}catch{}}}function M(e){if("string"!=typeof e)throw T(f.protocolError,"Relay sent a non-text frame",!1);try{return JSON.parse(e)}catch{throw T(f.protocolError,"Relay sent invalid JSON",!1)}}function A(e){try{return u(e)}catch{throw T(f.protocolError,"Relay first frame must be Control 1.0 session.ready for Wire 1.1",!1)}}function B(e){const t=e.activeTurn;return void 0===t?{status:"idle"}:"cancelling"===t.status?{status:"cancelling",turnId:t.id}:{status:"running",turnId:t.id}}function O(e,t){const s=e.findIndex(e=>e.streamId===t.streamId);return-1===s?[...e,t]:e.map((e,n)=>n===s?t:e)}function x(e,t){return{status:e,interactionId:t.interactionId,threadId:t.threadId,turnId:t.turnId,method:t.method}}class RelayThreadImpl{id;#e;#t=new Set;#s;constructor(e,t){this.#e=e,this.id=t,this.#s=R.parse({status:"loading",threadId:t,snapshot:null,liveAssistantMessages:[],interactions:[],turn:{status:"idle"}})}getSnapshot(){return this.#s}subscribe(e){return this.#t.add(e),P(e,this.#s),()=>{this.#t.delete(e)}}async send(e,t){const s=await this.#e.startTurn(this.id,e,t);return this.#n({status:"running",turnId:s.turnId}),s}async cancel(e,t){const s=await this.#e.cancelTurn(this.id,e,t);return s.cancelling&&this.#n({status:"cancelling",turnId:e}),s}async respond(e,t,s){const n=this.#s.interactions.find(t=>{const s=g(t);return s?.interactionId===e}),i=void 0===n?void 0:g(n);if(void 0===i)throw T(f.connectionUnavailable,"Interaction is no longer pending in this Browser session",!1);this.#i(e,"responding");try{return await this.#e.respondToInteraction(i,t,s)}catch(t){throw this.#i(e,"pending"),t}}getResult(e,t){return this.#e.getOperationResult(this.id,e,t)}capabilities(e){return this.#e.getThreadCapabilities(this.id,e)}async refresh(e){try{const t=await this.#e.getThreadSnapshot(this.id,e);return this.applySnapshot(t),t}catch(e){throw this.applyError(E(e).details),e}}applySnapshot(e){this.#r({status:"ready",threadId:this.id,snapshot:e,liveAssistantMessages:[],interactions:this.#s.interactions,turn:B(e)})}applyError(e){this.#r({status:"error",threadId:this.id,snapshot:this.#s.snapshot,liveAssistantMessages:this.#s.liveAssistantMessages,interactions:this.#s.interactions,turn:this.#s.turn,error:e})}applyRuntimeEvent(e){const t=e.event,s=t.event,n=t.turnId;let i=this.#s.liveAssistantMessages,r=this.#s.turn;if("message.started"===s.type)i=O(i,{status:"streaming",streamId:s.streamId,text:""});else if("message.delta"===s.type){const e=i.find(e=>e.streamId===s.streamId);i=O(i,{status:"streaming",streamId:s.streamId,text:`${e?.text??""}${s.delta}`})}else"message.completed"===s.type?i=O(i,{status:"completed",streamId:s.streamId,text:s.text}):"turn.started"===s.type&&void 0!==n?r={status:"running",turnId:n}:"turn.completed"===s.type&&void 0!==n?r={status:"completed",turnId:n}:"turn.cancelled"===s.type&&void 0!==n?r={status:"cancelled",turnId:n}:"turn.failed"===s.type&&void 0!==n&&(r={status:"failed",turnId:n,stage:s.stage});this.#o({liveAssistantMessages:i,turn:r}),"turn.completed"!==s.type&&"turn.cancelled"!==s.type&&"turn.failed"!==s.type||this.refresh().catch(()=>{})}applyInteractionRequest(e){const t=this.#s.interactions.findIndex(t=>{const s=g(t);return s?.interactionId===e.interactionId}),s={status:"pending",request:e},n=-1===t?[...this.#s.interactions,s]:this.#s.interactions.map((e,n)=>n===t?s:e);this.#o({interactions:n})}applyInteractionTerminal(e){const s=x(e.type===t.interactionResolved?"resolved":"cancelled",e),n=this.#s.interactions.findIndex(t=>{const s=g(t);return s?.interactionId===e.interactionId}),i=-1===n?[...this.#s.interactions,s]:this.#s.interactions.map((e,t)=>t===n?s:e);this.#o({interactions:i})}#n(e){const t=this.#s.turn;"idle"!==t.status&&t.turnId===e.turnId&&C.has(t.status)||this.#o({turn:e})}#i(e,t){const s=this.#s.interactions.map(s=>{const n=g(s);return n?.interactionId===e?{status:t,request:n}:s});this.#o({interactions:s})}#o(e){const t={threadId:this.id,snapshot:this.#s.snapshot,liveAssistantMessages:e.liveAssistantMessages??this.#s.liveAssistantMessages,interactions:e.interactions??this.#s.interactions,turn:e.turn??this.#s.turn};this.#r(null===this.#s.snapshot?{status:"loading",...t,snapshot:null}:{status:"ready",...t,snapshot:this.#s.snapshot})}#r(e){this.#s=R.parse(e);for(const e of this.#t)P(e,this.#s)}}export class RelayClientImpl{#a;#c;#l=new Set;#d=new Map;#h=new Map;#u=new Map;#p={status:"idle"};#y;#m;#f="1.1";#g=new Map;#v;#w;#R;#S=0;#b=!1;#I=!1;#q=q;#k=!1;#C=0;#T=!1;#E;constructor(e,t){if(this.#a=e,this.#c=t,this.#E=e.requestTimeoutMs??b,!Number.isFinite(this.#E)||this.#E<=0)throw new TypeError("requestTimeoutMs must be a positive finite number")}async connect(e){if("connected"===this.#p.status)return;if(this.#b)throw T(f.closed,"Relay client is closed",!1);this.#P();const t=this.#v;return void 0!==t?t:(this.#I=!1,this.#S=0,this.#M({status:"connecting",attempt:1}),this.#A(e?.signal,e?.timeoutMs).catch(e=>{const t=E(e);throw this.#b||this.#I||this.#M({status:"closed",reason:"session"===t.details.kind?"session":"transport"}),t}))}close(){this.#b||(this.#b=!0,this.#I=!0,this.#w?.abort(),this.#P(),this.#B("client closed"),this.#O(!0),this.#M({status:"closed",reason:"client"}))}getConnectionState(){return this.#p}subscribeConnection(e){return this.#l.add(e),P(e,this.#p),()=>{this.#l.delete(e)}}async listThreads(e={},t){return this.#x(s.threadList,e,t)}async createThread(e={},t){const n=await this.#x(s.threadCreate,{...e,requestId:this.#c.createUuid()},t),i=this.#F(n.thread.id);return await i.refresh(t).catch(()=>{}),i}async openThread(e,t){const n=await this.#x(s.threadOpen,{threadId:e},t),i=this.#F(e);return i.applySnapshot(n),i}async startTurn(e,t,n){return this.#x(s.turnStart,{requestId:this.#c.createUuid(),threadId:e,turnId:this.#c.createUuid(),input:{text:t}},n)}async cancelTurn(e,t,n){return this.#x(s.turnCancel,{requestId:this.#c.createUuid(),threadId:e,turnId:t},n)}async respondToInteraction(e,t,n){const r=i(e,t);return this.#x(s.interactionCandidate,r,n)}async getThreadCapabilities(e,t){return this.#x("thread.capabilities",{threadId:e},t)}getOperationResult(e,t,s){if("1.2"!==this.#f)return Promise.reject(T(f.protocolError,"App results require Relay Wire 1.2",!1));const n=`${this.#C}:${this.#m}:${e}:${t}`;if(void 0===s){const e=this.#g.get(n);if(e)return e}const i=this.#x("operation.result.get",{threadId:e,operationId:t},s);return void 0===s&&(this.#g.set(n,i),i.finally(()=>{this.#g.get(n)===i&&this.#g.delete(n)}).catch(()=>{})),i}async getThreadSnapshot(e,t){return this.#x(s.threadSnapshot,{threadId:e,limit:100},t)}async#W(e,t){const s=t??this.#E;if(!Number.isFinite(s)||s<=0)throw new TypeError("timeoutMs must be a positive finite number");const n=new AbortController;this.#w=n;let i=!1;const r=()=>{n.abort()};e?.addEventListener("abort",r,{once:!0}),!0===e?.aborted&&n.abort();const o=this.#c.scheduler.setTimer(()=>{i=!0,n.abort()},s),a=new Promise((e,t)=>{const s=()=>{t(T(i?f.requestTimeout:f.aborted,i?"Relay session connection timed out":"Relay session request was aborted",i)),this.#y?.close(k.normal,"session attempt cancelled")};n.signal.aborted?s():n.signal.addEventListener("abort",s,{once:!0})});try{await Promise.race([this.#j(n.signal),a])}finally{this.#c.scheduler.clearTimer(o),e?.removeEventListener("abort",r),this.#w===n&&(this.#w=void 0)}}async#j(e){try{let t;try{t=await this.#a.getSession({signal:e,supportedRelayProtocolVersions:["1.2","1.1"]})}catch(t){if(e.aborted)throw T(f.aborted,"Relay session request was aborted",!1);throw E(t)}if(e.aborted)throw T(f.aborted,"Relay session request was aborted",!1);const s=y.safeParse(t);if(!s.success)throw T(f.invalidSession,"Session endpoint returned an invalid Relay session",!1);if(Date.parse(s.data.expiresAt)<=this.#c.scheduler.now())throw T(f.invalidSession,"Session endpoint returned an expired Relay session",!1);this.#B("superseded relay session");const n=this.#c.createWebSocket(s.data.connectUrl);this.#y=n,await this.#L(n)}catch(e){throw E(e)}}#L(e){return new Promise((t,s)=>{const n={ready:!1,settled:!1},i=e=>{n.settled||(n.settled=!0,s(e))};e.setHandlers({onOpen:()=>{},onMessage:s=>{if(!n.ready||this.#y===e)try{const e=M(s);if(!n.ready){const s=A(e);if(void 0!==this.#m&&this.#m!==s.workspaceId)throw T(f.protocolError,"Relay session changed the bound Workspace",!1);return this.#m=s.workspaceId,this.#f=s.relayProtocolVersion,this.#g.clear(),n.ready=!0,n.settled=!0,this.#S=0,this.#T=!0,this.#M({status:"connected",workspaceStatus:s.workspaceStatus}),this.#U(),"online"===s.workspaceStatus&&this.#z(),void t()}this.#$(e)}catch(t){const s=E(t);n.ready?this.#V(s):i(s),e.close(k.invalidFrame,"invalid relay frame")}},onClose:()=>{const t=this.#y===e;t&&(this.#y=void 0),n.ready?t&&this.#N():i(T(f.transportError,"Relay connection closed before session.ready",!0))},onError:()=>{n.ready||i(T(f.transportError,"Relay WebSocket failed before session.ready",!0))}})})}#$(e){const t=p.safeParse(e);if(t.success)return void this.#G(t.data);const s=c.safeParse(e);if(!s.success)throw T(f.protocolError,"Relay sent an invalid Control 1.0 or Wire 1.1 frame",!1);this.#H(s.data)}#G(e){if("session.ready"===e.type)throw T(f.protocolError,"session.ready may only be the first frame",!1);if("workspace.status"===e.type){this.#D(e.workspaceId);const t=this.#p,s="connected"===t.status&&"offline"===t.workspaceStatus&&"online"===e.status;return this.#M({status:"connected",workspaceStatus:e.status}),s&&(this.#T=!0,this.#J()),void("online"===e.status&&this.#z())}const t=new RelayClientError({kind:"session",code:e.code,message:`Relay session failed: ${e.code}`,retryable:e.retryable});if(e.retryable)this.#y?.close(k.sessionRetryable,"retryable relay session error");else{this.#I=!0,this.#O(!0);for(const e of this.#d.values())e.applyError(t.details);this.#M({status:"closed",reason:"session"}),this.#y?.close(k.sessionFatal,"non-retryable relay session error")}}#H(e){if(e.type!==t.runtimeResponse){if(e.type===t.runtimeGap)return this.#D(e.workspaceId),this.#Q(e.throughRelaySequence),void this.#K(e.throughRelaySequence);if(e.type!==t.runtimeEvent&&e.type!==t.interactionRequest&&e.type!==t.interactionResolved&&e.type!==t.interactionCancelled)throw T(f.protocolError,`Relay sent disallowed Browser inbound frame: ${e.type}`,!1);this.#X({relaySequence:e.relaySequence,message:e})}else this.#Y(e)}#Y(t){this.#D(t.workspaceId);const s=this.#h.get(t.requestId);if(void 0!==s){if(void 0!==t.error){const n=t.error.code===e.remoteRequestDenied?"Remote request denied":t.error.message;return void this.#Z(s,new RelayClientError({kind:"remote",code:t.error.code,message:n,retryable:t.error.retryable}))}if(void 0!==t.result)try{const e=o(this.#f,s.method,t.result);this.#Z(s,void 0,e)}catch{this.#Z(s,T(f.protocolError,`Relay returned an invalid ${s.method} result`,!1))}else this.#Z(s,T(f.protocolError,"runtime.response contains neither result nor error",!1))}}#X(e){if(this.#D(e.message.workspaceId),this.#Q(e.relaySequence),e.relaySequence<=this.#q)this.#U();else if(this.#k)this.#u.set(e.relaySequence,e);else{if(e.relaySequence!==this.#q+1)return this.#u.set(e.relaySequence,e),void this.#K(e.relaySequence-1);this.#_(e.message),this.#q=e.relaySequence,this.#U()}}#_(e){const s=e.type===t.runtimeEvent?e.event.threadId:e.threadId,n=this.#F(s);e.type===t.runtimeEvent?n.applyRuntimeEvent(e):e.type===t.interactionRequest?n.applyInteractionRequest(e):n.applyInteractionTerminal(e)}#K(e){if(e<=this.#q)return void this.#U();if(this.#k)return;this.#k=!0;const t=e,s=this.#C,n=[...this.#d.values()].map(async e=>e.refresh());Promise.all(n).then(()=>{if(this.#C===s){this.#q=t;for(const e of[...this.#u.keys()])e<=t&&this.#u.delete(e);this.#k=!1,this.#U(),this.#ee()}}).catch(()=>{this.#C===s&&(this.#k=!1,this.#y?.close(k.sessionRetryable,"snapshot recovery failed"))})}#Q(e){this.#T&&(this.#T=!1,e>this.#q||(this.#C+=1,this.#q=q,this.#u.clear(),this.#k=!1,this.#J()))}#J(){for(const e of this.#d.values())e.refresh().catch(()=>{})}#ee(){for(;!this.#k;){const e=this.#q+1,t=this.#u.get(e);if(void 0===t){const t=[...this.#u.keys()].sort((e,t)=>e-t)[0];return void(void 0!==t&&t>e&&this.#K(t-1))}this.#u.delete(e),this.#_(t.message),this.#q=e,this.#U()}}#U(){const e=this.#m,s=this.#y;if(void 0===e||1!==s?.readyState)return;const n=a.parse({type:t.runtimeAck,workspaceId:e,throughRelaySequence:this.#q});s.send(JSON.stringify(n))}#N(){this.#b||this.#I||(this.#te(),this.#se())}#se(){if(void 0!==this.#R||this.#b||this.#I)return;this.#S+=1;const e=this.#c.reconnectDelayMs(this.#S),t=new Date(this.#c.scheduler.now()+e).toISOString();this.#M({status:"reconnecting",attempt:this.#S,retryAt:t}),this.#R=this.#c.scheduler.setTimer(()=>{this.#R=void 0,this.#b||this.#I||void 0!==this.#v||"connected"===this.#p.status||this.#A().catch(e=>{if(this.#b)return;if(!E(e).details.retryable)return this.#I=!0,this.#O(!0),void this.#M({status:"closed",reason:"session"});this.#se()})},e)}#A(e,t){const s=this.#W(e,t).finally(()=>{this.#v===s&&(this.#v=void 0)});return this.#v=s,s}#B(e){const t=this.#y;void 0!==t&&(this.#y=void 0,t.close(k.normal,e))}#P(){void 0!==this.#R&&(this.#c.scheduler.clearTimer(this.#R),this.#R=void 0)}#z(){for(const e of this.#h.values())e.mutation&&this.#ne(e)}#x(e,s,i){if("connected"!==this.#p.status||"online"!==this.#p.workspaceStatus||void 0===this.#m){const e="connected"===this.#p.status&&"offline"===this.#p.workspaceStatus;return Promise.reject(T(e?f.workspaceOffline:f.connectionUnavailable,e?"The local Workspace is offline":"Relay is not connected",!0))}if(!0===i?.signal?.aborted)return Promise.reject(T(f.aborted,"Relay request was aborted",!1));const o=r("1.2",e,s),a=l.parse(this.#c.createUuid()),c=h.parse({type:t.runtimeRequest,requestId:a,workspaceId:this.#m,method:e,params:o}),d=i?.timeoutMs??this.#E;if(!Number.isFinite(d)||d<=0)return Promise.reject(new TypeError("timeoutMs must be a positive finite number"));const u="mutation"===n[e];return new Promise((t,s)=>{const n=i?.signal?()=>{const e=this.#h.get(a);if(void 0===e)return;const t=e.mutation&&e.sent;this.#Z(e,T(t?f.outcomeUnknown:f.aborted,t?"Mutation outcome is unknown after Browser cancellation":"Relay request was aborted",!1))}:void 0,r={requestId:a,method:e,frame:JSON.stringify(c),mutation:u,sent:!1,resolve:e=>{t(e)},reject:s,signal:i?.signal,abortListener:n,timeoutHandle:void 0};r.timeoutHandle=this.#c.scheduler.setTimer(()=>{const e=this.#h.get(a);if(void 0===e)return;const t=e.mutation&&e.sent;this.#Z(e,T(t?f.outcomeUnknown:f.requestTimeout,t?"Mutation outcome is unknown after Relay timeout":"Relay request timed out",!t))},d),i?.signal?.addEventListener("abort",n??(()=>{}),{once:!0}),this.#h.set(a,r),this.#ne(r)})}#ne(e){const t=this.#y;1===t?.readyState&&"connected"===this.#p.status&&"online"===this.#p.workspaceStatus&&(t.send(e.frame),e.sent=!0)}#Z(e,t,s){this.#h.delete(e.requestId)&&(void 0!==e.timeoutHandle&&this.#c.scheduler.clearTimer(e.timeoutHandle),void 0!==e.signal&&void 0!==e.abortListener&&e.signal.removeEventListener("abort",e.abortListener),void 0!==t?e.reject(t):e.resolve(s))}#te(){for(const e of[...this.#h.values()])e.mutation||this.#Z(e,T(f.transportError,"Relay disconnected before the query completed",!0))}#O(e){for(const t of[...this.#h.values()]){const s=e&&t.mutation&&t.sent;this.#Z(t,T(s?f.outcomeUnknown:f.closed,s?"Mutation outcome is unknown after Relay closed":"Relay client is closed",!1))}}#D(e){if(this.#m!==e)throw T(f.protocolError,"Relay frame does not match the bound Workspace",!1)}#F(e){const t=this.#d.get(e);if(void 0!==t)return t;const s=new RelayThreadImpl(this,e);return this.#d.set(e,s),s}#M(e){this.#p=w.parse(e);for(const e of this.#l)P(e,this.#p)}#V(e){this.#I=!0,this.#O(!0);for(const t of this.#d.values())t.applyError(e.details);this.#M({status:"closed",reason:"protocol"})}}export function createRelayClientWithRuntime(e,t){return new RelayClientImpl(e,t)}export function createRelayClient(e){return createRelayClientWithRuntime(e,S)}
1
+ import{RELAY_ERROR_CODES_V11 as e,RELAY_MESSAGE_TYPES_V11 as t,RELAY_REQUEST_METHODS_V11 as s,RELAY_REQUEST_METHOD_DISPOSITIONS_V12 as n,parseRelayInteractionCandidateForRequestV11 as i,parseRelayRequestParamsForVersion as r,parseRelayRequestResultForVersion as o,relayAckSchemaV11 as a,relayMessageSchemaV13 as c,relayRequestIdSchema as l,relayRequestMethodSchemasV13 as d,relayRuntimeRequestSchemaV12 as h}from"@roll-agent/relay-protocol";import{parseRelayBrowserFirstControlFrameAny as u,relayBrowserControlMessageSchemaAny as p,relaySessionDescriptorSchema as y}from"@roll-agent/relay-protocol/control";import{z as m}from"zod/v4";import{RELAY_CLIENT_TRANSPORT_ERROR_CODES as f,getPendingInteractionRequest as v,relayClientErrorDetailsSchema as g,relayConnectionStateSchema as w,relayThreadViewSchema as R}from"./schemas.js";import{browserRelayRuntimeDependencies as S}from"./transport.js";export class RelayClientError extends Error{details;constructor(e){super(e.message),this.name="RelayClientError",this.details=g.parse(e)}}const b=3e4,I=1,q=-1,k={normal:1e3,invalidFrame:4002,sessionFatal:4008,sessionRetryable:4012},C=new Set(["completed","cancelled","failed"]);function T(e,t,s){return new RelayClientError({kind:"transport",code:e,message:t,retryable:s})}function E(e){return e instanceof RelayClientError?e:T(f.transportError,e instanceof Error?e.message:"Relay transport failed",!0)}function P(e,t){try{e(t)}catch(e){if("function"==typeof globalThis.reportError)try{globalThis.reportError(e)}catch{}}}function M(e){if("string"!=typeof e)throw T(f.protocolError,"Relay sent a non-text frame",!1);try{return JSON.parse(e)}catch{throw T(f.protocolError,"Relay sent invalid JSON",!1)}}function A(e){try{return u(e)}catch{throw T(f.protocolError,"Relay first frame must be Control 1.0 session.ready for Wire 1.1",!1)}}function x(e){const t=e.activeTurn;return void 0===t?{status:"idle"}:"cancelling"===t.status?{status:"cancelling",turnId:t.id}:{status:"running",turnId:t.id}}function B(e,t){const s=e.findIndex(e=>e.streamId===t.streamId);return-1===s?[...e,t]:e.map((e,n)=>n===s?t:e)}function O(e,t){return{status:e,interactionId:t.interactionId,threadId:t.threadId,turnId:t.turnId,method:t.method}}class RelayThreadImpl{id;#e;#t=new Set;#s;constructor(e,t){this.#e=e,this.id=t,this.#s=R.parse({status:"loading",threadId:t,snapshot:null,liveAssistantMessages:[],interactions:[],turn:{status:"idle"}})}getSnapshot(){return this.#s}subscribe(e){return this.#t.add(e),P(e,this.#s),()=>{this.#t.delete(e)}}async send(e,t){const s=await this.#e.startTurn(this.id,e,t);return this.#n({status:"running",turnId:s.turnId}),s}async cancel(e,t){const s=await this.#e.cancelTurn(this.id,e,t);return s.cancelling&&this.#n({status:"cancelling",turnId:e}),s}async respond(e,t,s){const n=this.#s.interactions.find(t=>{const s=v(t);return s?.interactionId===e}),i=void 0===n?void 0:v(n);if(void 0===i)throw T(f.connectionUnavailable,"Interaction is no longer pending in this Browser session",!1);this.#i(e,"responding");try{return await this.#e.respondToInteraction(i,t,s)}catch(t){throw this.#i(e,"pending"),t}}getResult(e,t){return this.#e.getOperationResult(this.id,e,t)}capabilities(e){return this.#e.getThreadCapabilities(this.id,e)}async refresh(e){try{const t=await this.#e.getThreadSnapshot(this.id,e);return this.applySnapshot(t),t}catch(e){throw this.applyError(E(e).details),e}}applySnapshot(e){this.#r({status:"ready",threadId:this.id,snapshot:e,liveAssistantMessages:[],interactions:this.#s.interactions,turn:x(e),contextRecovery:"contextRecovery"in e?e.contextRecovery:null})}applyError(e){this.#r({status:"error",threadId:this.id,snapshot:this.#s.snapshot,liveAssistantMessages:this.#s.liveAssistantMessages,interactions:this.#s.interactions,turn:this.#s.turn,contextRecovery:this.#s.contextRecovery,error:e})}applyRuntimeEvent(e){const t=e.event,s=t.event,n=t.turnId;let i=this.#s.liveAssistantMessages,r=this.#s.turn;if("message.started"===s.type)i=B(i,{status:"streaming",streamId:s.streamId,text:""});else if("message.delta"===s.type){const e=i.find(e=>e.streamId===s.streamId);i=B(i,{status:"streaming",streamId:s.streamId,text:`${e?.text??""}${s.delta}`})}else"message.completed"===s.type?i=B(i,{status:"completed",streamId:s.streamId,text:s.text}):"turn.started"===s.type&&void 0!==n?r={status:"running",turnId:n}:"turn.completed"===s.type&&void 0!==n?r={status:"completed",turnId:n}:"turn.cancelled"===s.type&&void 0!==n?r={status:"cancelled",turnId:n}:"turn.failed"===s.type&&void 0!==n&&(r={status:"failed",turnId:n,stage:s.stage});this.#o({liveAssistantMessages:i,turn:r,..."context.recovery.changed"===s.type?{contextRecovery:s.contextRecovery}:{}}),"turn.completed"!==s.type&&"turn.cancelled"!==s.type&&"turn.failed"!==s.type||this.refresh().catch(()=>{})}applyInteractionRequest(e){const t=this.#s.interactions.findIndex(t=>{const s=v(t);return s?.interactionId===e.interactionId}),s={status:"pending",request:e},n=-1===t?[...this.#s.interactions,s]:this.#s.interactions.map((e,n)=>n===t?s:e);this.#o({interactions:n})}applyInteractionTerminal(e){const s=O(e.type===t.interactionResolved?"resolved":"cancelled",e),n=this.#s.interactions.findIndex(t=>{const s=v(t);return s?.interactionId===e.interactionId}),i=-1===n?[...this.#s.interactions,s]:this.#s.interactions.map((e,t)=>t===n?s:e);this.#o({interactions:i})}#n(e){const t=this.#s.turn;"idle"!==t.status&&t.turnId===e.turnId&&C.has(t.status)||this.#o({turn:e})}#i(e,t){const s=this.#s.interactions.map(s=>{const n=v(s);return n?.interactionId===e?{status:t,request:n}:s});this.#o({interactions:s})}#o(e){const t={threadId:this.id,snapshot:this.#s.snapshot,liveAssistantMessages:e.liveAssistantMessages??this.#s.liveAssistantMessages,interactions:e.interactions??this.#s.interactions,turn:e.turn??this.#s.turn,contextRecovery:void 0===e.contextRecovery?this.#s.contextRecovery:e.contextRecovery};this.#r(null===this.#s.snapshot?{status:"loading",...t,snapshot:null}:{status:"ready",...t,snapshot:this.#s.snapshot})}#r(e){this.#s=R.parse(e);for(const e of this.#t)P(e,this.#s)}}export class RelayClientImpl{#a;#c;#l=new Set;#d=new Map;#h=new Map;#u=new Map;#p={status:"idle"};#y;#m;#f="1.1";#v=new Map;#g;#w;#R;#S=0;#b=!1;#I=!1;#q=q;#k=!1;#C=0;#T=!1;#E;constructor(e,t){if(this.#a=e,this.#c=t,this.#E=e.requestTimeoutMs??b,!Number.isFinite(this.#E)||this.#E<=0)throw new TypeError("requestTimeoutMs must be a positive finite number")}async connect(e){if("connected"===this.#p.status)return;if(this.#b)throw T(f.closed,"Relay client is closed",!1);this.#P();const t=this.#g;return void 0!==t?t:(this.#I=!1,this.#S=0,this.#M({status:"connecting",attempt:1}),this.#A(e?.signal,e?.timeoutMs).catch(e=>{const t=E(e);throw this.#b||this.#I||this.#M({status:"closed",reason:"session"===t.details.kind?"session":"transport"}),t}))}close(){this.#b||(this.#b=!0,this.#I=!0,this.#w?.abort(),this.#P(),this.#x("client closed"),this.#B(!0),this.#M({status:"closed",reason:"client"}))}getConnectionState(){return this.#p}subscribeConnection(e){return this.#l.add(e),P(e,this.#p),()=>{this.#l.delete(e)}}async listThreads(e={},t){return this.#O(s.threadList,e,t)}async createThread(e={},t){const n=await this.#O(s.threadCreate,{...e,requestId:this.#c.createUuid()},t),i=this.#F(n.thread.id);return await i.refresh(t).catch(()=>{}),i}async openThread(e,t){const n=await this.#O(s.threadOpen,{threadId:e},t),i=this.#F(e);return i.applySnapshot(n),i}async startTurn(e,t,n){return this.#O(s.turnStart,{requestId:this.#c.createUuid(),threadId:e,turnId:this.#c.createUuid(),input:{text:t}},n)}async cancelTurn(e,t,n){return this.#O(s.turnCancel,{requestId:this.#c.createUuid(),threadId:e,turnId:t},n)}async respondToInteraction(e,t,n){const r=i(e,t);return this.#O(s.interactionCandidate,r,n)}async getThreadCapabilities(e,t){return this.#O("thread.capabilities",{threadId:e},t)}getOperationResult(e,t,s){if("1.1"===this.#f)return Promise.reject(T(f.protocolError,"App results require Relay Wire 1.2",!1));const n=`${this.#C}:${this.#m}:${e}:${t}`;if(void 0===s){const e=this.#v.get(n);if(e)return e}const i=this.#O("operation.result.get",{threadId:e,operationId:t},s);return void 0===s&&(this.#v.set(n,i),i.finally(()=>{this.#v.get(n)===i&&this.#v.delete(n)}).catch(()=>{})),i}async getThreadSnapshot(e,t){return this.#O(s.threadSnapshot,{threadId:e,limit:100},t)}async#W(e,t){const s=t??this.#E;if(!Number.isFinite(s)||s<=0)throw new TypeError("timeoutMs must be a positive finite number");const n=new AbortController;this.#w=n;let i=!1;const r=()=>{n.abort()};e?.addEventListener("abort",r,{once:!0}),!0===e?.aborted&&n.abort();const o=this.#c.scheduler.setTimer(()=>{i=!0,n.abort()},s),a=new Promise((e,t)=>{const s=()=>{t(T(i?f.requestTimeout:f.aborted,i?"Relay session connection timed out":"Relay session request was aborted",i)),this.#y?.close(k.normal,"session attempt cancelled")};n.signal.aborted?s():n.signal.addEventListener("abort",s,{once:!0})});try{await Promise.race([this.#j(n.signal),a])}finally{this.#c.scheduler.clearTimer(o),e?.removeEventListener("abort",r),this.#w===n&&(this.#w=void 0)}}async#j(e){try{let t;try{t=await this.#a.getSession({signal:e,supportedRelayProtocolVersions:this.#a.relayProtocolVersions??["1.2","1.1"]})}catch(t){if(e.aborted)throw T(f.aborted,"Relay session request was aborted",!1);throw E(t)}if(e.aborted)throw T(f.aborted,"Relay session request was aborted",!1);const s=y.safeParse(t);if(!s.success)throw T(f.invalidSession,"Session endpoint returned an invalid Relay session",!1);if(Date.parse(s.data.expiresAt)<=this.#c.scheduler.now())throw T(f.invalidSession,"Session endpoint returned an expired Relay session",!1);this.#x("superseded relay session");const n=this.#c.createWebSocket(s.data.connectUrl);this.#y=n,await this.#L(n)}catch(e){throw E(e)}}#L(e){return new Promise((t,s)=>{const n={ready:!1,settled:!1},i=e=>{n.settled||(n.settled=!0,s(e))};e.setHandlers({onOpen:()=>{},onMessage:s=>{if(!n.ready||this.#y===e)try{const e=M(s);if(!n.ready){const s=A(e);if(void 0!==this.#m&&this.#m!==s.workspaceId)throw T(f.protocolError,"Relay session changed the bound Workspace",!1);return this.#m=s.workspaceId,this.#f=s.relayProtocolVersion,this.#v.clear(),n.ready=!0,n.settled=!0,this.#S=0,this.#T=!0,this.#M({status:"connected",workspaceStatus:s.workspaceStatus}),this.#U(),"online"===s.workspaceStatus&&this.#z(),void t()}this.#V(e)}catch(t){const s=E(t);n.ready?this.#$(s):i(s),e.close(k.invalidFrame,"invalid relay frame")}},onClose:()=>{const t=this.#y===e;t&&(this.#y=void 0),n.ready?t&&this.#N():i(T(f.transportError,"Relay connection closed before session.ready",!0))},onError:()=>{n.ready||i(T(f.transportError,"Relay WebSocket failed before session.ready",!0))}})})}#V(e){const t=p.safeParse(e);if(t.success)return void this.#G(t.data);const s=c.safeParse(e);if(!s.success)throw T(f.protocolError,"Relay sent an invalid Control 1.0 or Wire 1.1 frame",!1);this.#H(s.data)}#G(e){if("session.ready"===e.type)throw T(f.protocolError,"session.ready may only be the first frame",!1);if("workspace.status"===e.type){this.#D(e.workspaceId);const t=this.#p,s="connected"===t.status&&"offline"===t.workspaceStatus&&"online"===e.status;return this.#M({status:"connected",workspaceStatus:e.status}),s&&(this.#T=!0,this.#J()),void("online"===e.status&&this.#z())}const t=new RelayClientError({kind:"session",code:e.code,message:`Relay session failed: ${e.code}`,retryable:e.retryable});if(e.retryable)this.#y?.close(k.sessionRetryable,"retryable relay session error");else{this.#I=!0,this.#B(!0);for(const e of this.#d.values())e.applyError(t.details);this.#M({status:"closed",reason:"session"}),this.#y?.close(k.sessionFatal,"non-retryable relay session error")}}#H(e){if(e.type!==t.runtimeResponse){if(e.type===t.runtimeGap)return this.#D(e.workspaceId),this.#Q(e.throughRelaySequence),void this.#K(e.throughRelaySequence);if(e.type!==t.runtimeEvent&&e.type!==t.interactionRequest&&e.type!==t.interactionResolved&&e.type!==t.interactionCancelled)throw T(f.protocolError,`Relay sent disallowed Browser inbound frame: ${e.type}`,!1);this.#X({relaySequence:e.relaySequence,message:e})}else this.#Y(e)}#Y(t){this.#D(t.workspaceId);const s=this.#h.get(t.requestId);if(void 0!==s){if(void 0!==t.error){const n=t.error.code===e.remoteRequestDenied?"Remote request denied":t.error.message;return void this.#Z(s,new RelayClientError({kind:"remote",code:t.error.code,message:n,retryable:t.error.retryable}))}if(void 0!==t.result)try{const e=o(this.#f,s.method,t.result);this.#Z(s,void 0,e)}catch{this.#Z(s,T(f.protocolError,`Relay returned an invalid ${s.method} result`,!1))}else this.#Z(s,T(f.protocolError,"runtime.response contains neither result nor error",!1))}}#X(e){if(this.#D(e.message.workspaceId),this.#Q(e.relaySequence),e.relaySequence<=this.#q)this.#U();else if(this.#k)this.#u.set(e.relaySequence,e);else{if(e.relaySequence!==this.#q+1)return this.#u.set(e.relaySequence,e),void this.#K(e.relaySequence-1);this.#_(e.message),this.#q=e.relaySequence,this.#U()}}#_(e){const s=e.type===t.runtimeEvent?e.event.threadId:e.threadId,n=this.#F(s);e.type===t.runtimeEvent?n.applyRuntimeEvent(e):e.type===t.interactionRequest?n.applyInteractionRequest(e):n.applyInteractionTerminal(e)}#K(e){if(e<=this.#q)return void this.#U();if(this.#k)return;this.#k=!0;const t=e,s=this.#C,n=[...this.#d.values()].map(async e=>e.refresh());Promise.all(n).then(()=>{if(this.#C===s){this.#q=t;for(const e of[...this.#u.keys()])e<=t&&this.#u.delete(e);this.#k=!1,this.#U(),this.#ee()}}).catch(()=>{this.#C===s&&(this.#k=!1,this.#y?.close(k.sessionRetryable,"snapshot recovery failed"))})}#Q(e){this.#T&&(this.#T=!1,e>this.#q||(this.#C+=1,this.#q=q,this.#u.clear(),this.#k=!1,this.#J()))}#J(){for(const e of this.#d.values())e.refresh().catch(()=>{})}#ee(){for(;!this.#k;){const e=this.#q+1,t=this.#u.get(e);if(void 0===t){const t=[...this.#u.keys()].sort((e,t)=>e-t)[0];return void(void 0!==t&&t>e&&this.#K(t-1))}this.#u.delete(e),this.#_(t.message),this.#q=e,this.#U()}}#U(){const e=this.#m,s=this.#y;if(void 0===e||1!==s?.readyState)return;const n=a.parse({type:t.runtimeAck,workspaceId:e,throughRelaySequence:this.#q});s.send(JSON.stringify(n))}#N(){this.#b||this.#I||(this.#te(),this.#se())}#se(){if(void 0!==this.#R||this.#b||this.#I)return;this.#S+=1;const e=this.#c.reconnectDelayMs(this.#S),t=new Date(this.#c.scheduler.now()+e).toISOString();this.#M({status:"reconnecting",attempt:this.#S,retryAt:t}),this.#R=this.#c.scheduler.setTimer(()=>{this.#R=void 0,this.#b||this.#I||void 0!==this.#g||"connected"===this.#p.status||this.#A().catch(e=>{if(this.#b)return;if(!E(e).details.retryable)return this.#I=!0,this.#B(!0),void this.#M({status:"closed",reason:"session"});this.#se()})},e)}#A(e,t){const s=this.#W(e,t).finally(()=>{this.#g===s&&(this.#g=void 0)});return this.#g=s,s}#x(e){const t=this.#y;void 0!==t&&(this.#y=void 0,t.close(k.normal,e))}#P(){void 0!==this.#R&&(this.#c.scheduler.clearTimer(this.#R),this.#R=void 0)}#z(){for(const e of this.#h.values())e.mutation&&this.#ne(e)}#O(e,s,i){if("connected"!==this.#p.status||"online"!==this.#p.workspaceStatus||void 0===this.#m){const e="connected"===this.#p.status&&"offline"===this.#p.workspaceStatus;return Promise.reject(T(e?f.workspaceOffline:f.connectionUnavailable,e?"The local Workspace is offline":"Relay is not connected",!0))}if(!0===i?.signal?.aborted)return Promise.reject(T(f.aborted,"Relay request was aborted",!1));const o=r("1.2",e,s),a=l.parse(this.#c.createUuid()),c=h.parse({type:t.runtimeRequest,requestId:a,workspaceId:this.#m,method:e,params:o}),d=i?.timeoutMs??this.#E;if(!Number.isFinite(d)||d<=0)return Promise.reject(new TypeError("timeoutMs must be a positive finite number"));const u="mutation"===n[e];return new Promise((t,s)=>{const n=i?.signal?()=>{const e=this.#h.get(a);if(void 0===e)return;const t=e.mutation&&e.sent;this.#Z(e,T(t?f.outcomeUnknown:f.aborted,t?"Mutation outcome is unknown after Browser cancellation":"Relay request was aborted",!1))}:void 0,r={requestId:a,method:e,frame:JSON.stringify(c),mutation:u,sent:!1,resolve:e=>{t(e)},reject:s,signal:i?.signal,abortListener:n,timeoutHandle:void 0};r.timeoutHandle=this.#c.scheduler.setTimer(()=>{const e=this.#h.get(a);if(void 0===e)return;const t=e.mutation&&e.sent;this.#Z(e,T(t?f.outcomeUnknown:f.requestTimeout,t?"Mutation outcome is unknown after Relay timeout":"Relay request timed out",!t))},d),i?.signal?.addEventListener("abort",n??(()=>{}),{once:!0}),this.#h.set(a,r),this.#ne(r)})}#ne(e){const t=this.#y;1===t?.readyState&&"connected"===this.#p.status&&"online"===this.#p.workspaceStatus&&(t.send(e.frame),e.sent=!0)}#Z(e,t,s){this.#h.delete(e.requestId)&&(void 0!==e.timeoutHandle&&this.#c.scheduler.clearTimer(e.timeoutHandle),void 0!==e.signal&&void 0!==e.abortListener&&e.signal.removeEventListener("abort",e.abortListener),void 0!==t?e.reject(t):e.resolve(s))}#te(){for(const e of[...this.#h.values()])e.mutation||this.#Z(e,T(f.transportError,"Relay disconnected before the query completed",!0))}#B(e){for(const t of[...this.#h.values()]){const s=e&&t.mutation&&t.sent;this.#Z(t,T(s?f.outcomeUnknown:f.closed,s?"Mutation outcome is unknown after Relay closed":"Relay client is closed",!1))}}#D(e){if(this.#m!==e)throw T(f.protocolError,"Relay frame does not match the bound Workspace",!1)}#F(e){const t=this.#d.get(e);if(void 0!==t)return t;const s=new RelayThreadImpl(this,e);return this.#d.set(e,s),s}#M(e){this.#p=w.parse(e);for(const e of this.#l)P(e,this.#p)}#$(e){this.#I=!0,this.#B(!0);for(const t of this.#d.values())t.applyError(e.details);this.#M({status:"closed",reason:"protocol"})}}export function createRelayClientWithRuntime(e,t){return new RelayClientImpl(e,t)}export function createRelayClient(e){return createRelayClientWithRuntime(e,S)}
package/dist/schemas.d.ts CHANGED
@@ -291,6 +291,12 @@ export declare const relayTurnStateSchema: z.ZodDiscriminatedUnion<[z.ZodReadonl
291
291
  }, z.core.$strict>>]>;
292
292
  export declare const relayThreadViewSchema: z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
293
293
  snapshot: z.ZodNull;
294
+ contextRecovery: z.ZodDefault<z.ZodNullable<z.ZodReadonly<z.ZodObject<{
295
+ checkpointId: z.ZodString;
296
+ required: z.ZodBoolean;
297
+ unextracted: z.ZodBoolean;
298
+ requiredItemCount: z.ZodNumber;
299
+ }, z.core.$strict>>>>;
294
300
  threadId: z.core.$ZodBranded<z.ZodString, "ThreadId">;
295
301
  liveAssistantMessages: z.ZodReadonly<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
296
302
  status: z.ZodLiteral<"streaming">;
@@ -497,7 +503,7 @@ export declare const relayThreadViewSchema: z.ZodDiscriminatedUnion<[z.ZodReadon
497
503
  }, z.core.$strict>>]>;
498
504
  status: z.ZodLiteral<"loading">;
499
505
  }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
500
- snapshot: z.ZodReadonly<z.ZodObject<{
506
+ snapshot: z.ZodUnion<readonly [z.ZodReadonly<z.ZodObject<{
501
507
  thread: z.ZodReadonly<z.ZodObject<{
502
508
  id: z.core.$ZodBranded<z.ZodString, "ThreadId">;
503
509
  title: z.ZodOptional<z.ZodString>;
@@ -584,7 +590,106 @@ export declare const relayThreadViewSchema: z.ZodDiscriminatedUnion<[z.ZodReadon
584
590
  }, z.core.$strict>>>;
585
591
  nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
586
592
  }, z.core.$strict>>;
587
- }, z.core.$strict>>;
593
+ contextRecovery: z.ZodNullable<z.ZodReadonly<z.ZodObject<{
594
+ checkpointId: z.ZodString;
595
+ required: z.ZodBoolean;
596
+ unextracted: z.ZodBoolean;
597
+ requiredItemCount: z.ZodNumber;
598
+ }, z.core.$strict>>>;
599
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
600
+ thread: z.ZodReadonly<z.ZodObject<{
601
+ id: z.core.$ZodBranded<z.ZodString, "ThreadId">;
602
+ title: z.ZodOptional<z.ZodString>;
603
+ model: z.ZodOptional<z.ZodString>;
604
+ createdAt: z.ZodString;
605
+ updatedAt: z.ZodString;
606
+ messageCount: z.ZodNumber;
607
+ }, z.core.$strict>>;
608
+ messages: z.ZodReadonly<z.ZodObject<{
609
+ items: z.ZodArray<z.ZodReadonly<z.ZodObject<{
610
+ sequence: z.ZodNumber;
611
+ role: z.ZodEnum<{
612
+ user: "user";
613
+ assistant: "assistant";
614
+ }>;
615
+ createdAt: z.ZodString;
616
+ parts: z.ZodArray<z.ZodReadonly<z.ZodObject<{
617
+ type: z.ZodLiteral<"text">;
618
+ text: z.ZodString;
619
+ }, z.core.$strict>>>;
620
+ }, z.core.$strict>>>;
621
+ nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
622
+ }, z.core.$strict>>;
623
+ activeTurn: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
624
+ id: z.core.$ZodBranded<z.ZodString, "TurnId">;
625
+ status: z.ZodEnum<{
626
+ running: "running";
627
+ cancelling: "cancelling";
628
+ }>;
629
+ startedAt: z.ZodString;
630
+ }, z.core.$strict>>>;
631
+ pendingApprovals: z.ZodArray<z.ZodReadonly<z.ZodObject<{
632
+ id: z.core.$ZodBranded<z.ZodString, "ApprovalId">;
633
+ turnId: z.core.$ZodBranded<z.ZodString, "TurnId">;
634
+ agentName: z.ZodString;
635
+ toolName: z.ZodString;
636
+ preview: z.ZodJSONSchema;
637
+ reason: z.ZodOptional<z.ZodString>;
638
+ }, z.core.$strict>>>;
639
+ transcriptCompleteness: z.ZodEnum<{
640
+ complete: "complete";
641
+ legacy_snapshot: "legacy_snapshot";
642
+ }>;
643
+ operations: z.ZodReadonly<z.ZodObject<{
644
+ items: z.ZodArray<z.ZodReadonly<z.ZodObject<{
645
+ id: z.core.$ZodBranded<z.ZodString, "OperationId">;
646
+ sequence: z.ZodNumber;
647
+ toolCallId: z.ZodString;
648
+ agentName: z.ZodString;
649
+ toolName: z.ZodString;
650
+ createdAt: z.ZodString;
651
+ outcome: z.ZodReadonly<z.ZodObject<{
652
+ kind: z.ZodEnum<{
653
+ success: "success";
654
+ user_rejected: "user_rejected";
655
+ policy_denied: "policy_denied";
656
+ invalid_input: "invalid_input";
657
+ cancelled: "cancelled";
658
+ tool_failed: "tool_failed";
659
+ }>;
660
+ reason: z.ZodOptional<z.ZodString>;
661
+ }, z.core.$strict>>;
662
+ display: z.ZodJSONSchema;
663
+ appOutput: z.ZodOptional<z.ZodReadonly<z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
664
+ status: z.ZodLiteral<"rejected">;
665
+ reason: z.ZodEnum<{
666
+ credential_field: "credential_field";
667
+ credential_value: "credential_value";
668
+ }>;
669
+ field: z.ZodOptional<z.ZodString>;
670
+ }, z.core.$strict>>, z.ZodObject<{
671
+ status: z.ZodLiteral<"available">;
672
+ schemaId: z.ZodString;
673
+ schemaVersion: z.ZodNumber;
674
+ }, z.core.$strict>, z.ZodObject<{
675
+ status: z.ZodEnum<{
676
+ not_provided: "not_provided";
677
+ invalid: "invalid";
678
+ too_large: "too_large";
679
+ expired: "expired";
680
+ denied: "denied";
681
+ }>;
682
+ }, z.core.$strict>]>>>;
683
+ }, z.core.$strict>>>;
684
+ nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
685
+ }, z.core.$strict>>;
686
+ }, z.core.$strict>>]>;
687
+ contextRecovery: z.ZodDefault<z.ZodNullable<z.ZodReadonly<z.ZodObject<{
688
+ checkpointId: z.ZodString;
689
+ required: z.ZodBoolean;
690
+ unextracted: z.ZodBoolean;
691
+ requiredItemCount: z.ZodNumber;
692
+ }, z.core.$strict>>>>;
588
693
  threadId: z.core.$ZodBranded<z.ZodString, "ThreadId">;
589
694
  liveAssistantMessages: z.ZodReadonly<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
590
695
  status: z.ZodLiteral<"streaming">;
@@ -791,7 +896,100 @@ export declare const relayThreadViewSchema: z.ZodDiscriminatedUnion<[z.ZodReadon
791
896
  }, z.core.$strict>>]>;
792
897
  status: z.ZodLiteral<"ready">;
793
898
  }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
794
- snapshot: z.ZodNullable<z.ZodReadonly<z.ZodObject<{
899
+ snapshot: z.ZodNullable<z.ZodUnion<readonly [z.ZodReadonly<z.ZodObject<{
900
+ thread: z.ZodReadonly<z.ZodObject<{
901
+ id: z.core.$ZodBranded<z.ZodString, "ThreadId">;
902
+ title: z.ZodOptional<z.ZodString>;
903
+ model: z.ZodOptional<z.ZodString>;
904
+ createdAt: z.ZodString;
905
+ updatedAt: z.ZodString;
906
+ messageCount: z.ZodNumber;
907
+ }, z.core.$strict>>;
908
+ messages: z.ZodReadonly<z.ZodObject<{
909
+ items: z.ZodArray<z.ZodReadonly<z.ZodObject<{
910
+ sequence: z.ZodNumber;
911
+ role: z.ZodEnum<{
912
+ user: "user";
913
+ assistant: "assistant";
914
+ }>;
915
+ createdAt: z.ZodString;
916
+ parts: z.ZodArray<z.ZodReadonly<z.ZodObject<{
917
+ type: z.ZodLiteral<"text">;
918
+ text: z.ZodString;
919
+ }, z.core.$strict>>>;
920
+ }, z.core.$strict>>>;
921
+ nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
922
+ }, z.core.$strict>>;
923
+ activeTurn: z.ZodOptional<z.ZodReadonly<z.ZodObject<{
924
+ id: z.core.$ZodBranded<z.ZodString, "TurnId">;
925
+ status: z.ZodEnum<{
926
+ running: "running";
927
+ cancelling: "cancelling";
928
+ }>;
929
+ startedAt: z.ZodString;
930
+ }, z.core.$strict>>>;
931
+ pendingApprovals: z.ZodArray<z.ZodReadonly<z.ZodObject<{
932
+ id: z.core.$ZodBranded<z.ZodString, "ApprovalId">;
933
+ turnId: z.core.$ZodBranded<z.ZodString, "TurnId">;
934
+ agentName: z.ZodString;
935
+ toolName: z.ZodString;
936
+ preview: z.ZodJSONSchema;
937
+ reason: z.ZodOptional<z.ZodString>;
938
+ }, z.core.$strict>>>;
939
+ transcriptCompleteness: z.ZodEnum<{
940
+ complete: "complete";
941
+ legacy_snapshot: "legacy_snapshot";
942
+ }>;
943
+ operations: z.ZodReadonly<z.ZodObject<{
944
+ items: z.ZodArray<z.ZodReadonly<z.ZodObject<{
945
+ id: z.core.$ZodBranded<z.ZodString, "OperationId">;
946
+ sequence: z.ZodNumber;
947
+ toolCallId: z.ZodString;
948
+ agentName: z.ZodString;
949
+ toolName: z.ZodString;
950
+ createdAt: z.ZodString;
951
+ outcome: z.ZodReadonly<z.ZodObject<{
952
+ kind: z.ZodEnum<{
953
+ success: "success";
954
+ user_rejected: "user_rejected";
955
+ policy_denied: "policy_denied";
956
+ invalid_input: "invalid_input";
957
+ cancelled: "cancelled";
958
+ tool_failed: "tool_failed";
959
+ }>;
960
+ reason: z.ZodOptional<z.ZodString>;
961
+ }, z.core.$strict>>;
962
+ display: z.ZodJSONSchema;
963
+ appOutput: z.ZodOptional<z.ZodReadonly<z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
964
+ status: z.ZodLiteral<"rejected">;
965
+ reason: z.ZodEnum<{
966
+ credential_field: "credential_field";
967
+ credential_value: "credential_value";
968
+ }>;
969
+ field: z.ZodOptional<z.ZodString>;
970
+ }, z.core.$strict>>, z.ZodObject<{
971
+ status: z.ZodLiteral<"available">;
972
+ schemaId: z.ZodString;
973
+ schemaVersion: z.ZodNumber;
974
+ }, z.core.$strict>, z.ZodObject<{
975
+ status: z.ZodEnum<{
976
+ not_provided: "not_provided";
977
+ invalid: "invalid";
978
+ too_large: "too_large";
979
+ expired: "expired";
980
+ denied: "denied";
981
+ }>;
982
+ }, z.core.$strict>]>>>;
983
+ }, z.core.$strict>>>;
984
+ nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
985
+ }, z.core.$strict>>;
986
+ contextRecovery: z.ZodNullable<z.ZodReadonly<z.ZodObject<{
987
+ checkpointId: z.ZodString;
988
+ required: z.ZodBoolean;
989
+ unextracted: z.ZodBoolean;
990
+ requiredItemCount: z.ZodNumber;
991
+ }, z.core.$strict>>>;
992
+ }, z.core.$strict>>, z.ZodReadonly<z.ZodObject<{
795
993
  thread: z.ZodReadonly<z.ZodObject<{
796
994
  id: z.core.$ZodBranded<z.ZodString, "ThreadId">;
797
995
  title: z.ZodOptional<z.ZodString>;
@@ -878,7 +1076,7 @@ export declare const relayThreadViewSchema: z.ZodDiscriminatedUnion<[z.ZodReadon
878
1076
  }, z.core.$strict>>>;
879
1077
  nextBeforeSequence: z.ZodNullable<z.ZodNumber>;
880
1078
  }, z.core.$strict>>;
881
- }, z.core.$strict>>>;
1079
+ }, z.core.$strict>>]>>;
882
1080
  error: z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
883
1081
  code: z.ZodEnum<{
884
1082
  readonly aborted: "ABORTED";
@@ -912,6 +1110,12 @@ export declare const relayThreadViewSchema: z.ZodDiscriminatedUnion<[z.ZodReadon
912
1110
  retryable: z.ZodBoolean;
913
1111
  kind: z.ZodLiteral<"remote">;
914
1112
  }, z.core.$strict>>]>;
1113
+ contextRecovery: z.ZodDefault<z.ZodNullable<z.ZodReadonly<z.ZodObject<{
1114
+ checkpointId: z.ZodString;
1115
+ required: z.ZodBoolean;
1116
+ unextracted: z.ZodBoolean;
1117
+ requiredItemCount: z.ZodNumber;
1118
+ }, z.core.$strict>>>>;
915
1119
  threadId: z.core.$ZodBranded<z.ZodString, "ThreadId">;
916
1120
  liveAssistantMessages: z.ZodReadonly<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodReadonly<z.ZodObject<{
917
1121
  status: z.ZodLiteral<"streaming">;
package/dist/schemas.js CHANGED
@@ -1 +1 @@
1
- import{relayInteractionRequestSchemaV11 as t,relayRequestMethodSchemasV12 as e}from"@roll-agent/relay-protocol";import{relayClientErrorCodeSchema as r}from"@roll-agent/relay-protocol/control";import{z as n}from"zod/v4";const a=e["thread.open"].params.def.innerType.shape.threadId,o=e["turn.cancel"].params.def.innerType.shape.turnId,s=e["thread.snapshot"].result;export const RELAY_CONNECTION_STATUSES={idle:"idle",connecting:"connecting",connected:"connected",reconnecting:"reconnecting",closed:"closed"};export const relayConnectionStateSchema=n.discriminatedUnion("status",[n.object({status:n.literal(RELAY_CONNECTION_STATUSES.idle)}).strict().readonly(),n.object({status:n.literal(RELAY_CONNECTION_STATUSES.connecting),attempt:n.number().int().positive()}).strict().readonly(),n.object({status:n.literal(RELAY_CONNECTION_STATUSES.connected),workspaceStatus:n.enum(["online","offline"])}).strict().readonly(),n.object({status:n.literal(RELAY_CONNECTION_STATUSES.reconnecting),attempt:n.number().int().positive(),retryAt:n.string().datetime({offset:!0})}).strict().readonly(),n.object({status:n.literal(RELAY_CONNECTION_STATUSES.closed),reason:n.enum(["client","session","transport","protocol"])}).strict().readonly()]);export const RELAY_CLIENT_TRANSPORT_ERROR_CODES={aborted:"ABORTED",closed:"CLIENT_CLOSED",connectionUnavailable:"CONNECTION_UNAVAILABLE",invalidSession:"INVALID_SESSION",outcomeUnknown:"OUTCOME_UNKNOWN",protocolError:"PROTOCOL_ERROR",requestTimeout:"REQUEST_TIMEOUT",transportError:"TRANSPORT_ERROR",workspaceOffline:"WORKSPACE_OFFLINE"};export const relayClientTransportErrorCodeSchema=n.enum(RELAY_CLIENT_TRANSPORT_ERROR_CODES);const i={message:n.string(),retryable:n.boolean()};export const relayClientErrorDetailsSchema=n.discriminatedUnion("kind",[n.object({kind:n.literal("transport"),...i,code:relayClientTransportErrorCodeSchema}).strict().readonly(),n.object({kind:n.literal("session"),...i,code:r}).strict().readonly(),n.object({kind:n.literal("remote"),...i,code:n.string().min(1)}).strict().readonly()]);export const relayLiveAssistantMessageSchema=n.discriminatedUnion("status",[n.object({status:n.literal("streaming"),streamId:n.string().uuid(),text:n.string()}).strict().readonly(),n.object({status:n.literal("completed"),streamId:n.string().uuid(),text:n.string()}).strict().readonly()]);const l={interactionId:t.options[0].def.innerType.shape.interactionId,threadId:a,turnId:o,method:t.options[0].def.innerType.shape.method.or(t.options[1].def.innerType.shape.method)};export const relayInteractionStateSchema=n.discriminatedUnion("status",[n.object({status:n.literal("pending"),request:t}).strict().readonly(),n.object({status:n.literal("responding"),request:t}).strict().readonly(),n.object({status:n.literal("resolved"),...l}).strict().readonly(),n.object({status:n.literal("cancelled"),...l}).strict().readonly()]);export const relayTurnStateSchema=n.discriminatedUnion("status",[n.object({status:n.literal("idle")}).strict().readonly(),n.object({status:n.literal("running"),turnId:o}).strict().readonly(),n.object({status:n.literal("cancelling"),turnId:o}).strict().readonly(),n.object({status:n.literal("completed"),turnId:o}).strict().readonly(),n.object({status:n.literal("cancelled"),turnId:o}).strict().readonly(),n.object({status:n.literal("failed"),turnId:o,stage:n.enum(["bootstrap","plan","execute"])}).strict().readonly()]);const c={threadId:a,liveAssistantMessages:n.array(relayLiveAssistantMessageSchema).readonly(),interactions:n.array(relayInteractionStateSchema).readonly(),turn:relayTurnStateSchema};export const relayThreadViewSchema=n.discriminatedUnion("status",[n.object({status:n.literal("loading"),...c,snapshot:n.null()}).strict().readonly(),n.object({status:n.literal("ready"),...c,snapshot:s}).strict().readonly(),n.object({status:n.literal("error"),...c,snapshot:s.nullable(),error:relayClientErrorDetailsSchema}).strict().readonly()]);export function getPendingInteractionRequest(t){return"pending"===t.status||"responding"===t.status?t.request:void 0}
1
+ import{relayInteractionRequestSchemaV11 as t,relayRequestMethodSchemasV12 as e,relayThreadSnapshotSchemaV13 as r}from"@roll-agent/relay-protocol";import{relayClientErrorCodeSchema as n}from"@roll-agent/relay-protocol/control";import{z as o}from"zod/v4";const a=e["thread.open"].params.def.innerType.shape.threadId,s=e["turn.cancel"].params.def.innerType.shape.turnId,i=o.union([r,e["thread.snapshot"].result]);export const RELAY_CONNECTION_STATUSES={idle:"idle",connecting:"connecting",connected:"connected",reconnecting:"reconnecting",closed:"closed"};export const relayConnectionStateSchema=o.discriminatedUnion("status",[o.object({status:o.literal(RELAY_CONNECTION_STATUSES.idle)}).strict().readonly(),o.object({status:o.literal(RELAY_CONNECTION_STATUSES.connecting),attempt:o.number().int().positive()}).strict().readonly(),o.object({status:o.literal(RELAY_CONNECTION_STATUSES.connected),workspaceStatus:o.enum(["online","offline"])}).strict().readonly(),o.object({status:o.literal(RELAY_CONNECTION_STATUSES.reconnecting),attempt:o.number().int().positive(),retryAt:o.string().datetime({offset:!0})}).strict().readonly(),o.object({status:o.literal(RELAY_CONNECTION_STATUSES.closed),reason:o.enum(["client","session","transport","protocol"])}).strict().readonly()]);export const RELAY_CLIENT_TRANSPORT_ERROR_CODES={aborted:"ABORTED",closed:"CLIENT_CLOSED",connectionUnavailable:"CONNECTION_UNAVAILABLE",invalidSession:"INVALID_SESSION",outcomeUnknown:"OUTCOME_UNKNOWN",protocolError:"PROTOCOL_ERROR",requestTimeout:"REQUEST_TIMEOUT",transportError:"TRANSPORT_ERROR",workspaceOffline:"WORKSPACE_OFFLINE"};export const relayClientTransportErrorCodeSchema=o.enum(RELAY_CLIENT_TRANSPORT_ERROR_CODES);const l={message:o.string(),retryable:o.boolean()};export const relayClientErrorDetailsSchema=o.discriminatedUnion("kind",[o.object({kind:o.literal("transport"),...l,code:relayClientTransportErrorCodeSchema}).strict().readonly(),o.object({kind:o.literal("session"),...l,code:n}).strict().readonly(),o.object({kind:o.literal("remote"),...l,code:o.string().min(1)}).strict().readonly()]);export const relayLiveAssistantMessageSchema=o.discriminatedUnion("status",[o.object({status:o.literal("streaming"),streamId:o.string().uuid(),text:o.string()}).strict().readonly(),o.object({status:o.literal("completed"),streamId:o.string().uuid(),text:o.string()}).strict().readonly()]);const c={interactionId:t.options[0].def.innerType.shape.interactionId,threadId:a,turnId:s,method:t.options[0].def.innerType.shape.method.or(t.options[1].def.innerType.shape.method)};export const relayInteractionStateSchema=o.discriminatedUnion("status",[o.object({status:o.literal("pending"),request:t}).strict().readonly(),o.object({status:o.literal("responding"),request:t}).strict().readonly(),o.object({status:o.literal("resolved"),...c}).strict().readonly(),o.object({status:o.literal("cancelled"),...c}).strict().readonly()]);export const relayTurnStateSchema=o.discriminatedUnion("status",[o.object({status:o.literal("idle")}).strict().readonly(),o.object({status:o.literal("running"),turnId:s}).strict().readonly(),o.object({status:o.literal("cancelling"),turnId:s}).strict().readonly(),o.object({status:o.literal("completed"),turnId:s}).strict().readonly(),o.object({status:o.literal("cancelled"),turnId:s}).strict().readonly(),o.object({status:o.literal("failed"),turnId:s,stage:o.enum(["bootstrap","plan","execute"])}).strict().readonly()]);const d={contextRecovery:r.def.innerType.shape.contextRecovery.default(null),threadId:a,liveAssistantMessages:o.array(relayLiveAssistantMessageSchema).readonly(),interactions:o.array(relayInteractionStateSchema).readonly(),turn:relayTurnStateSchema};export const relayThreadViewSchema=o.discriminatedUnion("status",[o.object({status:o.literal("loading"),...d,snapshot:o.null()}).strict().readonly(),o.object({status:o.literal("ready"),...d,snapshot:i}).strict().readonly(),o.object({status:o.literal("error"),...d,snapshot:i.nullable(),error:relayClientErrorDetailsSchema}).strict().readonly()]);export function getPendingInteractionRequest(t){return"pending"===t.status||"responding"===t.status?t.request:void 0}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@roll-agent/relay-client",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "repository": {
@@ -31,7 +31,7 @@
31
31
  "node": ">=22.6.0"
32
32
  },
33
33
  "dependencies": {
34
- "@roll-agent/relay-protocol": "0.4.1",
34
+ "@roll-agent/relay-protocol": "0.5.0",
35
35
  "zod": "3.25.76"
36
36
  },
37
37
  "devDependencies": {