@agentuity/core 3.0.0-alpha.2 → 3.0.0-alpha.6

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.
@@ -22,7 +22,7 @@ import { z } from 'zod/v4';
22
22
 
23
23
  /** Connection role assigned by the server in the init message */
24
24
  export const CoderHubInitRoleSchema = z
25
- .enum(['lead', 'sub_agent', 'controller'])
25
+ .enum(['lead', 'sub_agent', 'observer', 'controller'])
26
26
  .describe(
27
27
  'Role assigned to a connecting client, determining its permissions and message routing.'
28
28
  );
@@ -215,10 +215,20 @@ export const CoderHubControllerInitMessageSchema = BaseCoderHubInitMessageSchema
215
215
  );
216
216
  export type CoderHubControllerInitMessage = z.infer<typeof CoderHubControllerInitMessageSchema>;
217
217
 
218
+ export const CoderHubObserverInitMessageSchema = BaseCoderHubInitMessageSchema.extend({
219
+ role: z
220
+ .literal('observer')
221
+ .describe('Indicates this client is connecting as a read-only observer.'),
222
+ }).describe(
223
+ 'Initialization message sent by an observer client when the server chooses to send an explicit init frame.'
224
+ );
225
+ export type CoderHubObserverInitMessage = z.infer<typeof CoderHubObserverInitMessageSchema>;
226
+
218
227
  export const CoderHubInitMessageSchema = z
219
228
  .discriminatedUnion('role', [
220
229
  CoderHubLeadInitMessageSchema,
221
230
  CoderHubSubAgentInitMessageSchema,
231
+ CoderHubObserverInitMessageSchema,
222
232
  CoderHubControllerInitMessageSchema,
223
233
  ])
224
234
  .describe('Union of all initialization messages, discriminated by the client role.');
@@ -743,6 +753,22 @@ export const BootstrapReadyMessageSchema = z
743
753
  );
744
754
  export type BootstrapReadyMessage = z.infer<typeof BootstrapReadyMessageSchema>;
745
755
 
756
+ export const ObserverSubscribeMessageSchema = z
757
+ .object({
758
+ type: z
759
+ .literal('subscribe')
760
+ .describe('Discriminator indicating an observer subscription update.'),
761
+ patterns: z
762
+ .array(z.string())
763
+ .describe(
764
+ 'Event filters to receive, including categories, exact names, wildcard prefixes, or "*".'
765
+ ),
766
+ })
767
+ .describe(
768
+ 'Client message updating the observer event filters for a live Coder Hub WebSocket connection.'
769
+ );
770
+ export type ObserverSubscribeMessage = z.infer<typeof ObserverSubscribeMessageSchema>;
771
+
746
772
  export const SessionEntryMessageSchema = z
747
773
  .object({
748
774
  type: z
@@ -978,6 +1004,7 @@ export const ClientMessageSchema = z
978
1004
  SessionEntryMessageSchema,
979
1005
  SessionWriteMessageSchema,
980
1006
  BootstrapReadyMessageSchema,
1007
+ ObserverSubscribeMessageSchema,
981
1008
  RpcCommandMessageSchema,
982
1009
  RpcUiResponseMessageSchema,
983
1010
  PingMessageSchema,
@@ -993,7 +1020,7 @@ export type ClientMessage = z.infer<typeof ClientMessageSchema>;
993
1020
  * Messages the Coder Hub server can send to connected clients.
994
1021
  */
995
1022
  export const ServerMessageSchema = z
996
- .discriminatedUnion('type', [
1023
+ .union([
997
1024
  CoderHubInitMessageSchema,
998
1025
  CoderHubResponseSchema,
999
1026
  CoderHubHydrationMessageSchema,
@@ -1129,6 +1156,10 @@ export const ConnectionParamsSchema = z
1129
1156
  .enum(['lead', 'observer', 'controller'])
1130
1157
  .optional()
1131
1158
  .describe('Requested session role; the server may override based on permissions.'),
1159
+ subscribe: z
1160
+ .string()
1161
+ .optional()
1162
+ .describe('Comma-separated event filters requested during observer connection bootstrap.'),
1132
1163
  coordJobId: z
1133
1164
  .string()
1134
1165
  .optional()
@@ -322,6 +322,20 @@ export const CoderWorkspaceListResponseSchema = z
322
322
  .describe('Response payload for listing workspaces');
323
323
  export type CoderWorkspaceListResponse = z.infer<typeof CoderWorkspaceListResponseSchema>;
324
324
 
325
+ function hasWorkspaceSelections(input: {
326
+ repos?: unknown[];
327
+ savedSkillIds?: unknown[];
328
+ skillBucketIds?: unknown[];
329
+ enabledAgents?: unknown[];
330
+ }): boolean {
331
+ return (
332
+ (input.repos?.length ?? 0) > 0 ||
333
+ (input.savedSkillIds?.length ?? 0) > 0 ||
334
+ (input.skillBucketIds?.length ?? 0) > 0 ||
335
+ (input.enabledAgents?.length ?? 0) > 0
336
+ );
337
+ }
338
+
325
339
  export const CoderCreateWorkspaceRequestSchema = z
326
340
  .object({
327
341
  name: z.string().describe('Workspace name'),
@@ -335,6 +349,9 @@ export const CoderCreateWorkspaceRequestSchema = z
335
349
  .optional()
336
350
  .describe('Effective agent roster to store on the workspace'),
337
351
  })
352
+ .refine(hasWorkspaceSelections, {
353
+ message: 'A workspace needs at least one repo, saved skill, skill bucket, or agent',
354
+ })
338
355
  .describe('Request body for creating a workspace');
339
356
  export type CoderCreateWorkspaceRequest = z.infer<typeof CoderCreateWorkspaceRequestSchema>;
340
357
 
@@ -70,7 +70,7 @@ import type {
70
70
  ConnectionParams,
71
71
  ServerMessage,
72
72
  } from './protocol.ts';
73
- import { CoderHubInitMessageSchema } from './protocol.ts';
73
+ import { CoderHubInitMessageSchema, parseServerMessage } from './protocol.ts';
74
74
  import { normalizeCoderUrl } from './util.ts';
75
75
 
76
76
  /**
@@ -118,6 +118,11 @@ export const CoderHubWebSocketOptionsSchema = z.object({
118
118
  task: z.string().optional().describe('Initial task for driver mode'),
119
119
  /** Human-readable session label */
120
120
  label: z.string().optional().describe('Session label'),
121
+ /** Observer event filters to request during the initial connection. */
122
+ subscribe: z
123
+ .array(z.string())
124
+ .optional()
125
+ .describe('Observer event filters to request during connection setup'),
121
126
  /** Client origin (web, desktop, tui, sdk) */
122
127
  origin: z.enum(['web', 'desktop', 'tui', 'sdk']).optional().describe('Client origin'),
123
128
  /** Driver mode: 'rpc' for RPC bridge driver */
@@ -198,7 +203,13 @@ export const CoderHubWebSocketError = StructuredError('CoderHubWebSocketError')<
198
203
  | 'response_timeout'
199
204
  | 'invalid_response';
200
205
  sessionId?: string;
206
+ serverCode?: string;
207
+ serverMessage?: string;
208
+ serverMessageType?: 'connection_rejected' | 'protocol_error';
209
+ closeCode?: number;
210
+ closeReason?: string;
201
211
  }>();
212
+ export type CoderHubWebSocketErrorInstance = InstanceType<typeof CoderHubWebSocketError>;
202
213
 
203
214
  interface PendingRequest {
204
215
  resolve: (response: CoderHubResponse) => void;
@@ -270,6 +281,7 @@ export class CoderHubWebSocketClient {
270
281
  parentSessionId: string;
271
282
  task: string;
272
283
  label: string;
284
+ subscribe: string[];
273
285
  origin: 'web' | 'desktop' | 'tui' | 'sdk';
274
286
  driverMode: 'rpc' | undefined;
275
287
  driverInstanceId: string;
@@ -317,6 +329,7 @@ export class CoderHubWebSocketClient {
317
329
  parentSessionId: options.parentSessionId ?? '',
318
330
  task: options.task ?? '',
319
331
  label: options.label ?? '',
332
+ subscribe: options.subscribe ?? [],
320
333
  origin: options.origin ?? 'sdk',
321
334
  driverMode: options.driverMode,
322
335
  driverInstanceId: options.driverInstanceId ?? '',
@@ -522,6 +535,57 @@ export class CoderHubWebSocketClient {
522
535
  return `${Date.now()}-${++this.#messageId}`;
523
536
  }
524
537
 
538
+ #buildHandshakeError(input: {
539
+ code: 'auth_failed' | 'connection_error';
540
+ message: string;
541
+ serverCode?: string;
542
+ serverMessage?: string;
543
+ serverMessageType?: 'connection_rejected' | 'protocol_error';
544
+ closeCode?: number;
545
+ closeReason?: string;
546
+ }): CoderHubWebSocketErrorInstance {
547
+ return new CoderHubWebSocketError({
548
+ code: input.code,
549
+ message: input.message,
550
+ sessionId: this.sessionId,
551
+ serverCode: input.serverCode,
552
+ serverMessage: input.serverMessage,
553
+ serverMessageType: input.serverMessageType,
554
+ closeCode: input.closeCode,
555
+ closeReason: input.closeReason,
556
+ });
557
+ }
558
+
559
+ #markReady(input?: {
560
+ initMessage?: CoderHubInitMessage;
561
+ firstMessage?: ServerMessage;
562
+ sendBootstrapReady?: boolean;
563
+ }): void {
564
+ this.#authenticated = true;
565
+ this.#initMessage = input?.initMessage ?? null;
566
+ this.#sessionId =
567
+ input?.initMessage?.sessionId ??
568
+ (input?.firstMessage && 'sessionId' in input.firstMessage
569
+ ? input.firstMessage.sessionId
570
+ : undefined) ??
571
+ this.#options.sessionId ??
572
+ null;
573
+ this.#reconnectAttempts = 0;
574
+ this.#setState('connected');
575
+ this.#startHeartbeat();
576
+ if (input?.initMessage) {
577
+ this.#options.onInit(input.initMessage);
578
+ }
579
+ if (input?.sendBootstrapReady && this.#ws?.readyState === WebSocket.OPEN) {
580
+ this.#ws.send(JSON.stringify({ type: 'bootstrap_ready' }));
581
+ }
582
+ this.#flushMessageQueue();
583
+ this.#options.onOpen();
584
+ if (input?.firstMessage) {
585
+ this.#options.onMessage(input.firstMessage);
586
+ }
587
+ }
588
+
525
589
  #setState(state: CoderHubWebSocketState): void {
526
590
  if (this.#state !== state) {
527
591
  this.#state = state;
@@ -589,6 +653,8 @@ export class CoderHubWebSocketClient {
589
653
  parent: this.#options.parentSessionId || undefined,
590
654
  task: this.#options.task || undefined,
591
655
  label: this.#options.label || undefined,
656
+ subscribe:
657
+ this.#options.subscribe.length > 0 ? this.#options.subscribe.join(',') : undefined,
592
658
  orgId: this.#options.orgId || undefined,
593
659
  origin: this.#options.origin || undefined,
594
660
  driverMode: this.#options.driverMode || undefined,
@@ -601,6 +667,9 @@ export class CoderHubWebSocketClient {
601
667
  params.set(key, String(value));
602
668
  }
603
669
  }
670
+ if (this.#options.apiKey) {
671
+ params.set('api_key', this.#options.apiKey);
672
+ }
604
673
 
605
674
  const queryString = params.toString();
606
675
  return queryString ? `${wsUrl}?${queryString}` : wsUrl;
@@ -642,12 +711,16 @@ export class CoderHubWebSocketClient {
642
711
  ws.onopen = () => {
643
712
  if (ws !== this.#ws) return;
644
713
  this.#setState('authenticating');
645
- ws.send(
646
- JSON.stringify({
647
- authorization: this.#options.apiKey,
648
- org_id: this.#options.orgId,
649
- })
650
- );
714
+ if (this.#options.apiKey || this.#options.orgId) {
715
+ const bootstrapPayload: { authorization?: string; org_id?: string } = {};
716
+ if (this.#options.apiKey) {
717
+ bootstrapPayload.authorization = this.#options.apiKey;
718
+ }
719
+ if (this.#options.orgId) {
720
+ bootstrapPayload.org_id = this.#options.orgId;
721
+ }
722
+ ws.send(JSON.stringify(bootstrapPayload));
723
+ }
651
724
  };
652
725
 
653
726
  ws.onmessage = (event: MessageEvent) => {
@@ -685,10 +758,12 @@ export class CoderHubWebSocketClient {
685
758
  if (msg.type === 'connection_rejected' || msg.type === 'protocol_error') {
686
759
  this.#setState('closed');
687
760
  this.#options.onError(
688
- new CoderHubWebSocketError({
689
- message: `Connection rejected: ${msg.message ?? msg.code ?? 'Unknown error'}`,
761
+ this.#buildHandshakeError({
690
762
  code: 'auth_failed',
691
- sessionId: this.sessionId,
763
+ message: `Connection rejected: ${msg.message ?? msg.code ?? 'Unknown error'}`,
764
+ serverCode: msg.code,
765
+ serverMessage: msg.message,
766
+ serverMessageType: msg.type,
692
767
  })
693
768
  );
694
769
  this.#intentionallyClosed = true;
@@ -700,15 +775,18 @@ export class CoderHubWebSocketClient {
700
775
  const initResult = CoderHubInitMessageSchema.safeParse(parsed);
701
776
  if (initResult.success) {
702
777
  const initMsg = initResult.data;
703
- this.#authenticated = true;
704
- this.#initMessage = initMsg;
705
- this.#sessionId = initMsg.sessionId ?? this.#options.sessionId ?? null;
706
- this.#reconnectAttempts = 0;
707
- this.#setState('connected');
708
- this.#startHeartbeat();
709
- this.#flushMessageQueue();
710
- this.#options.onInit(initMsg);
711
- this.#options.onOpen();
778
+ this.#markReady({
779
+ initMessage: initMsg,
780
+ sendBootstrapReady: this.#options.role === 'controller',
781
+ });
782
+ return;
783
+ }
784
+
785
+ if (this.#options.role === 'observer') {
786
+ const firstObserverMessage = parseServerMessage(parsed);
787
+ if (firstObserverMessage) {
788
+ this.#markReady({ firstMessage: firstObserverMessage });
789
+ }
712
790
  }
713
791
  return;
714
792
  }
@@ -759,14 +837,31 @@ export class CoderHubWebSocketClient {
759
837
  this.#clearTimers();
760
838
  this.#setState('closed');
761
839
 
840
+ const wasAuthenticated = this.#authenticated;
841
+ const hadTerminalError = this.#intentionallyClosed;
842
+ const terminalClose = isTerminalCloseCode(event.code);
843
+
762
844
  // Clear auth state for clean reconnect
763
845
  this.#authenticated = false;
764
846
  this.#initMessage = null;
765
847
 
766
- if (isTerminalCloseCode(event.code)) {
848
+ if (terminalClose) {
767
849
  this.#intentionallyClosed = true;
768
850
  }
769
851
 
852
+ if (!wasAuthenticated && terminalClose && !hadTerminalError) {
853
+ this.#options.onError(
854
+ this.#buildHandshakeError({
855
+ code: 'connection_error',
856
+ message: `WebSocket closed before connection was ready (code ${event.code})${
857
+ event.reason ? `: ${event.reason}` : ''
858
+ }`,
859
+ closeCode: event.code,
860
+ closeReason: event.reason || undefined,
861
+ })
862
+ );
863
+ }
864
+
770
865
  this.#options.onClose(event.code, event.reason);
771
866
 
772
867
  if (!this.#intentionallyClosed) {
@@ -896,7 +991,11 @@ export async function* subscribeToCoderHub(
896
991
  onError: (error) => {
897
992
  if (
898
993
  error instanceof CoderHubWebSocketError &&
899
- (error.code === 'max_reconnects_exceeded' || error.code === 'auth_failed')
994
+ (error.code === 'max_reconnects_exceeded' ||
995
+ error.code === 'auth_failed' ||
996
+ (error.code === 'connection_error' &&
997
+ typeof error.closeCode === 'number' &&
998
+ isTerminalCloseCode(error.closeCode)))
900
999
  ) {
901
1000
  terminalError = error;
902
1001
  done = true;