@agentuity/runtime 0.0.95 → 0.0.96
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/AGENTS.md +3 -1
- package/dist/_events.d.ts +64 -0
- package/dist/_events.d.ts.map +1 -0
- package/dist/_events.js +92 -0
- package/dist/_events.js.map +1 -0
- package/dist/_idle.d.ts +1 -1
- package/dist/_idle.d.ts.map +1 -1
- package/dist/_idle.js +2 -16
- package/dist/_idle.js.map +1 -1
- package/dist/_server.d.ts +30 -13
- package/dist/_server.d.ts.map +1 -1
- package/dist/_server.js +39 -572
- package/dist/_server.js.map +1 -1
- package/dist/_services.d.ts.map +1 -1
- package/dist/_services.js +4 -2
- package/dist/_services.js.map +1 -1
- package/dist/_standalone.d.ts.map +1 -1
- package/dist/_standalone.js +2 -1
- package/dist/_standalone.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +13 -17
- package/dist/agent.js.map +1 -1
- package/dist/app.d.ts +58 -171
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +119 -218
- package/dist/app.js.map +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -3
- package/dist/index.js.map +1 -1
- package/dist/middleware.d.ts +29 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +200 -0
- package/dist/middleware.js.map +1 -0
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +5 -2
- package/dist/router.js.map +1 -1
- package/dist/services/local/vector.d.ts.map +1 -1
- package/dist/services/local/vector.js +3 -2
- package/dist/services/local/vector.js.map +1 -1
- package/dist/services/thread/local.d.ts +20 -0
- package/dist/services/thread/local.d.ts.map +1 -0
- package/dist/services/thread/local.js +76 -0
- package/dist/services/thread/local.js.map +1 -0
- package/dist/session.d.ts +60 -8
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +186 -54
- package/dist/session.js.map +1 -1
- package/dist/web.d.ts +8 -0
- package/dist/web.d.ts.map +1 -0
- package/dist/web.js +66 -0
- package/dist/web.js.map +1 -0
- package/dist/workbench.d.ts +2 -0
- package/dist/workbench.d.ts.map +1 -1
- package/dist/workbench.js +192 -39
- package/dist/workbench.js.map +1 -1
- package/package.json +10 -10
- package/src/_events.ts +142 -0
- package/src/_idle.ts +2 -18
- package/src/_server.ts +48 -681
- package/src/_services.ts +4 -2
- package/src/_standalone.ts +2 -1
- package/src/agent.ts +11 -14
- package/src/app.ts +164 -246
- package/src/index.ts +42 -4
- package/src/middleware.ts +252 -0
- package/src/router.ts +6 -2
- package/src/services/local/vector.ts +3 -2
- package/src/services/thread/local.ts +106 -0
- package/src/session.ts +238 -59
- package/src/web.ts +75 -0
- package/src/workbench.ts +226 -38
package/src/session.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
/** biome-ignore-all lint/suspicious/noExplicitAny: anys are great */
|
|
3
3
|
import type { Context } from 'hono';
|
|
4
|
-
import {
|
|
4
|
+
import { getSignedCookie, setSignedCookie } from 'hono/cookie';
|
|
5
5
|
import { type Env, fireEvent } from './app';
|
|
6
6
|
import type { AppState } from './index';
|
|
7
7
|
import { getServiceUrls } from '@agentuity/server';
|
|
8
|
-
import { WebSocket } from 'ws';
|
|
9
8
|
import { internal } from './logger/internal';
|
|
9
|
+
import { timingSafeEqual } from 'node:crypto';
|
|
10
10
|
|
|
11
11
|
export type ThreadEventName = 'destroyed';
|
|
12
12
|
export type SessionEventName = 'completed';
|
|
@@ -229,13 +229,13 @@ export interface ThreadIDProvider {
|
|
|
229
229
|
* thrd_ such as `thrd_212c16896b974ffeb21a748f0eeba620`. The max length of the
|
|
230
230
|
* string is 64 characters and the min length is 32 characters long
|
|
231
231
|
* (including the prefix). The characters after the prefix must match the
|
|
232
|
-
* regular expression [a-zA-Z0-9
|
|
232
|
+
* regular expression [a-zA-Z0-9].
|
|
233
233
|
*
|
|
234
234
|
* @param appState - The app state from createApp setup function
|
|
235
235
|
* @param ctx - Hono request context
|
|
236
|
-
* @returns The thread id to use
|
|
236
|
+
* @returns The thread id to use (can be async for signed cookies)
|
|
237
237
|
*/
|
|
238
|
-
getThreadId(appState: AppState, ctx: Context<Env>): string
|
|
238
|
+
getThreadId(appState: AppState, ctx: Context<Env>): string | Promise<string>;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
/**
|
|
@@ -255,7 +255,7 @@ export interface ThreadIDProvider {
|
|
|
255
255
|
* }
|
|
256
256
|
*
|
|
257
257
|
* async restore(ctx: Context<Env>): Promise<Thread> {
|
|
258
|
-
* const threadId = getCookie(ctx, 'atid') || generateId('thrd');
|
|
258
|
+
* const threadId = ctx.req.header('x-thread-id') || getCookie(ctx, 'atid') || generateId('thrd');
|
|
259
259
|
* const data = await this.redis.get(`thread:${threadId}`);
|
|
260
260
|
* const thread = new DefaultThread(this, threadId);
|
|
261
261
|
* if (data) {
|
|
@@ -454,21 +454,197 @@ export function generateId(prefix?: string): string {
|
|
|
454
454
|
}
|
|
455
455
|
|
|
456
456
|
/**
|
|
457
|
-
*
|
|
458
|
-
*
|
|
457
|
+
* Validates a thread ID against runtime constraints:
|
|
458
|
+
* - Must start with 'thrd_'
|
|
459
|
+
* - Must be at least 32 characters long (including prefix)
|
|
460
|
+
* - Must be less than 64 characters long
|
|
461
|
+
* - Must contain only [a-zA-Z0-9] after 'thrd_' prefix (no dashes for maximum randomness)
|
|
462
|
+
*/
|
|
463
|
+
export function isValidThreadId(threadId: string): boolean {
|
|
464
|
+
if (!threadId.startsWith('thrd_')) {
|
|
465
|
+
return false;
|
|
466
|
+
}
|
|
467
|
+
if (threadId.length < 32 || threadId.length > 64) {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
|
|
471
|
+
if (!validThreadIdCharacters.test(threadId.substring(5))) {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Validates a thread ID and throws detailed error messages for debugging.
|
|
479
|
+
* @param threadId The thread ID to validate
|
|
480
|
+
* @throws Error with detailed message if validation fails
|
|
481
|
+
*/
|
|
482
|
+
export function validateThreadIdOrThrow(threadId: string): void {
|
|
483
|
+
if (!threadId) {
|
|
484
|
+
throw new Error(`the ThreadIDProvider returned an empty thread id for getThreadId`);
|
|
485
|
+
}
|
|
486
|
+
if (!threadId.startsWith('thrd_')) {
|
|
487
|
+
throw new Error(
|
|
488
|
+
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must start with the prefix 'thrd_'.`
|
|
489
|
+
);
|
|
490
|
+
}
|
|
491
|
+
if (threadId.length > 64) {
|
|
492
|
+
throw new Error(
|
|
493
|
+
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be less than 64 characters long.`
|
|
494
|
+
);
|
|
495
|
+
}
|
|
496
|
+
if (threadId.length < 32) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be at least 32 characters long.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
const validThreadIdCharacters = /^[a-zA-Z0-9]+$/;
|
|
502
|
+
if (!validThreadIdCharacters.test(threadId.substring(5))) {
|
|
503
|
+
throw new Error(
|
|
504
|
+
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must contain only characters that match the regular expression [a-zA-Z0-9].`
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Determines if the connection is secure (HTTPS) by checking the request protocol
|
|
511
|
+
* and x-forwarded-proto header (for reverse proxy scenarios).
|
|
512
|
+
* Defaults to false (HTTP) if unable to determine.
|
|
513
|
+
*/
|
|
514
|
+
export function isSecureConnection(ctx: Context<Env>): boolean {
|
|
515
|
+
// Check x-forwarded-proto header first (reverse proxy)
|
|
516
|
+
const forwardedProto = ctx.req.header('x-forwarded-proto');
|
|
517
|
+
if (forwardedProto) {
|
|
518
|
+
return forwardedProto === 'https';
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Check the request URL protocol if available
|
|
522
|
+
try {
|
|
523
|
+
if (ctx.req.url) {
|
|
524
|
+
const url = new URL(ctx.req.url);
|
|
525
|
+
return url.protocol === 'https:';
|
|
526
|
+
}
|
|
527
|
+
} catch {
|
|
528
|
+
// Fall through to default
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Default to HTTP (e.g., for localhost development)
|
|
532
|
+
return false;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/**
|
|
536
|
+
* Signs a thread ID using HMAC SHA-256 and returns it in the format: threadId;signature
|
|
537
|
+
* Format: thrd_abc123;base64signature
|
|
538
|
+
*/
|
|
539
|
+
export async function signThreadId(threadId: string, secret: string): Promise<string> {
|
|
540
|
+
const hasher = new Bun.CryptoHasher('sha256', secret);
|
|
541
|
+
hasher.update(threadId);
|
|
542
|
+
const signatureBase64 = hasher.digest('base64');
|
|
543
|
+
|
|
544
|
+
return `${threadId};${signatureBase64}`;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* Verifies a signed thread ID header and returns the thread ID if valid, or undefined if invalid.
|
|
549
|
+
* Expected format: thrd_abc123;base64signature
|
|
550
|
+
*/
|
|
551
|
+
export async function verifySignedThreadId(
|
|
552
|
+
signedValue: string,
|
|
553
|
+
secret: string
|
|
554
|
+
): Promise<string | undefined> {
|
|
555
|
+
const parts = signedValue.split(';');
|
|
556
|
+
if (parts.length !== 2) {
|
|
557
|
+
return undefined;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const [threadId, providedSignature] = parts;
|
|
561
|
+
|
|
562
|
+
// Validate both parts exist
|
|
563
|
+
if (!threadId || !providedSignature) {
|
|
564
|
+
return undefined;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// Validate thread ID format before verifying signature
|
|
568
|
+
if (!isValidThreadId(threadId)) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Re-sign the thread ID and compare signatures
|
|
573
|
+
const expectedSigned = await signThreadId(threadId, secret);
|
|
574
|
+
const expectedSignature = expectedSigned.split(';')[1];
|
|
575
|
+
|
|
576
|
+
// Validate signature exists
|
|
577
|
+
if (!expectedSignature) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Constant-time comparison to prevent timing attacks
|
|
582
|
+
// Check lengths match first (fail fast if different lengths)
|
|
583
|
+
if (providedSignature.length !== expectedSignature.length) {
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
try {
|
|
588
|
+
// Convert to Buffers for constant-time comparison
|
|
589
|
+
const providedBuffer = Buffer.from(providedSignature, 'base64');
|
|
590
|
+
const expectedBuffer = Buffer.from(expectedSignature, 'base64');
|
|
591
|
+
|
|
592
|
+
if (timingSafeEqual(providedBuffer, expectedBuffer)) {
|
|
593
|
+
return threadId;
|
|
594
|
+
}
|
|
595
|
+
} catch {
|
|
596
|
+
// Comparison failed or buffer conversion error
|
|
597
|
+
return undefined;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
return undefined;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* DefaultThreadIDProvider will look for an HTTP header `x-thread-id` first,
|
|
605
|
+
* then fall back to a signed cookie named `atid`, and use that as the thread id.
|
|
606
|
+
* If not found, generate a new one. Validates incoming thread IDs against
|
|
607
|
+
* runtime constraints. Uses AGENTUITY_SDK_KEY for signing, falls back to 'agentuity'.
|
|
459
608
|
*/
|
|
460
609
|
export class DefaultThreadIDProvider implements ThreadIDProvider {
|
|
461
|
-
|
|
462
|
-
|
|
610
|
+
private getSecret(): string {
|
|
611
|
+
return process.env.AGENTUITY_SDK_KEY || 'agentuity';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
async getThreadId(_appState: AppState, ctx: Context<Env>): Promise<string> {
|
|
463
615
|
let threadId: string | undefined;
|
|
616
|
+
const secret = this.getSecret();
|
|
617
|
+
|
|
618
|
+
// Check signed header first
|
|
619
|
+
const headerValue = ctx.req.header('x-thread-id');
|
|
620
|
+
if (headerValue) {
|
|
621
|
+
const verifiedThreadId = await verifySignedThreadId(headerValue, secret);
|
|
622
|
+
if (verifiedThreadId) {
|
|
623
|
+
threadId = verifiedThreadId;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
464
626
|
|
|
465
|
-
|
|
466
|
-
|
|
627
|
+
// Fall back to signed cookie
|
|
628
|
+
if (!threadId) {
|
|
629
|
+
const cookieValue = await getSignedCookie(ctx, secret, 'atid');
|
|
630
|
+
if (cookieValue && typeof cookieValue === 'string' && isValidThreadId(cookieValue)) {
|
|
631
|
+
threadId = cookieValue;
|
|
632
|
+
}
|
|
467
633
|
}
|
|
468
634
|
|
|
469
635
|
threadId = threadId || generateId('thrd');
|
|
470
636
|
|
|
471
|
-
|
|
637
|
+
await setSignedCookie(ctx, 'atid', threadId, secret, {
|
|
638
|
+
httpOnly: true,
|
|
639
|
+
secure: isSecureConnection(ctx),
|
|
640
|
+
sameSite: 'Lax',
|
|
641
|
+
path: '/',
|
|
642
|
+
maxAge: 604800, // 1 week in seconds
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
// Set signed header in response
|
|
646
|
+
const signedHeader = await signThreadId(threadId, secret);
|
|
647
|
+
ctx.header('x-thread-id', signedHeader);
|
|
472
648
|
return threadId;
|
|
473
649
|
}
|
|
474
650
|
}
|
|
@@ -623,6 +799,22 @@ export class DefaultSession implements Session {
|
|
|
623
799
|
* @internal
|
|
624
800
|
* @experimental
|
|
625
801
|
*/
|
|
802
|
+
/**
|
|
803
|
+
* Configuration options for ThreadWebSocketClient
|
|
804
|
+
*/
|
|
805
|
+
export interface ThreadWebSocketClientOptions {
|
|
806
|
+
/** Connection timeout in milliseconds (default: 10000) */
|
|
807
|
+
connectionTimeoutMs?: number;
|
|
808
|
+
/** Request timeout in milliseconds (default: 10000) */
|
|
809
|
+
requestTimeoutMs?: number;
|
|
810
|
+
/** Base delay for reconnection backoff in milliseconds (default: 1000) */
|
|
811
|
+
reconnectBaseDelayMs?: number;
|
|
812
|
+
/** Maximum delay for reconnection backoff in milliseconds (default: 30000) */
|
|
813
|
+
reconnectMaxDelayMs?: number;
|
|
814
|
+
/** Maximum number of reconnection attempts (default: 5) */
|
|
815
|
+
maxReconnectAttempts?: number;
|
|
816
|
+
}
|
|
817
|
+
|
|
626
818
|
export class ThreadWebSocketClient {
|
|
627
819
|
private ws: WebSocket | null = null;
|
|
628
820
|
private authenticated = false;
|
|
@@ -631,7 +823,7 @@ export class ThreadWebSocketClient {
|
|
|
631
823
|
{ resolve: (data?: string) => void; reject: (err: Error) => void }
|
|
632
824
|
>();
|
|
633
825
|
private reconnectAttempts = 0;
|
|
634
|
-
private maxReconnectAttempts
|
|
826
|
+
private maxReconnectAttempts: number;
|
|
635
827
|
private apiKey: string;
|
|
636
828
|
private wsUrl: string;
|
|
637
829
|
private wsConnecting: Promise<void> | null = null;
|
|
@@ -639,10 +831,19 @@ export class ThreadWebSocketClient {
|
|
|
639
831
|
private isDisposed = false;
|
|
640
832
|
private initialConnectResolve: (() => void) | null = null;
|
|
641
833
|
private initialConnectReject: ((err: Error) => void) | null = null;
|
|
834
|
+
private connectionTimeoutMs: number;
|
|
835
|
+
private requestTimeoutMs: number;
|
|
836
|
+
private reconnectBaseDelayMs: number;
|
|
837
|
+
private reconnectMaxDelayMs: number;
|
|
642
838
|
|
|
643
|
-
constructor(apiKey: string, wsUrl: string) {
|
|
839
|
+
constructor(apiKey: string, wsUrl: string, options: ThreadWebSocketClientOptions = {}) {
|
|
644
840
|
this.apiKey = apiKey;
|
|
645
841
|
this.wsUrl = wsUrl;
|
|
842
|
+
this.connectionTimeoutMs = options.connectionTimeoutMs ?? 10_000;
|
|
843
|
+
this.requestTimeoutMs = options.requestTimeoutMs ?? 10_000;
|
|
844
|
+
this.reconnectBaseDelayMs = options.reconnectBaseDelayMs ?? 1_000;
|
|
845
|
+
this.reconnectMaxDelayMs = options.reconnectMaxDelayMs ?? 30_000;
|
|
846
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 5;
|
|
646
847
|
}
|
|
647
848
|
|
|
648
849
|
async connect(): Promise<void> {
|
|
@@ -659,20 +860,20 @@ export class ThreadWebSocketClient {
|
|
|
659
860
|
const rejectFn = this.initialConnectReject || reject;
|
|
660
861
|
this.initialConnectResolve = null;
|
|
661
862
|
this.initialConnectReject = null;
|
|
662
|
-
rejectFn(new Error(
|
|
663
|
-
},
|
|
863
|
+
rejectFn(new Error(`WebSocket connection timeout (${this.connectionTimeoutMs}ms)`));
|
|
864
|
+
}, this.connectionTimeoutMs);
|
|
664
865
|
|
|
665
866
|
try {
|
|
666
867
|
this.ws = new WebSocket(this.wsUrl);
|
|
667
868
|
|
|
668
|
-
this.ws.
|
|
869
|
+
this.ws.addEventListener('open', () => {
|
|
669
870
|
// Send authentication (do NOT clear timeout yet - wait for auth response)
|
|
670
871
|
this.ws?.send(JSON.stringify({ authorization: this.apiKey }));
|
|
671
872
|
});
|
|
672
873
|
|
|
673
|
-
this.ws.
|
|
874
|
+
this.ws.addEventListener('message', (event: MessageEvent) => {
|
|
674
875
|
try {
|
|
675
|
-
const message = JSON.parse(data
|
|
876
|
+
const message = JSON.parse(event.data);
|
|
676
877
|
|
|
677
878
|
// Handle auth response
|
|
678
879
|
if ('success' in message && !this.authenticated) {
|
|
@@ -715,7 +916,7 @@ export class ThreadWebSocketClient {
|
|
|
715
916
|
}
|
|
716
917
|
});
|
|
717
918
|
|
|
718
|
-
this.ws.
|
|
919
|
+
this.ws.addEventListener('error', (_event: Event) => {
|
|
719
920
|
clearTimeout(connectionTimeout);
|
|
720
921
|
if (!this.authenticated) {
|
|
721
922
|
// Don't reject immediately if we'll attempt reconnection
|
|
@@ -723,12 +924,12 @@ export class ThreadWebSocketClient {
|
|
|
723
924
|
const rejectFn = this.initialConnectReject || reject;
|
|
724
925
|
this.initialConnectResolve = null;
|
|
725
926
|
this.initialConnectReject = null;
|
|
726
|
-
rejectFn(new Error(`WebSocket error
|
|
927
|
+
rejectFn(new Error(`WebSocket error`));
|
|
727
928
|
}
|
|
728
929
|
}
|
|
729
930
|
});
|
|
730
931
|
|
|
731
|
-
this.ws.
|
|
932
|
+
this.ws.addEventListener('close', () => {
|
|
732
933
|
clearTimeout(connectionTimeout);
|
|
733
934
|
const wasAuthenticated = this.authenticated;
|
|
734
935
|
this.authenticated = false;
|
|
@@ -754,7 +955,10 @@ export class ThreadWebSocketClient {
|
|
|
754
955
|
// This handles server rollouts where connection closes before auth finishes
|
|
755
956
|
if (this.reconnectAttempts < this.maxReconnectAttempts) {
|
|
756
957
|
this.reconnectAttempts++;
|
|
757
|
-
const delay = Math.min(
|
|
958
|
+
const delay = Math.min(
|
|
959
|
+
this.reconnectBaseDelayMs * Math.pow(2, this.reconnectAttempts),
|
|
960
|
+
this.reconnectMaxDelayMs
|
|
961
|
+
);
|
|
758
962
|
|
|
759
963
|
internal.info(
|
|
760
964
|
`WebSocket disconnected, attempting reconnection ${this.reconnectAttempts}/${this.maxReconnectAttempts} in ${delay}ms`
|
|
@@ -818,13 +1022,13 @@ export class ThreadWebSocketClient {
|
|
|
818
1022
|
|
|
819
1023
|
this.ws!.send(JSON.stringify(message));
|
|
820
1024
|
|
|
821
|
-
// Timeout after
|
|
1025
|
+
// Timeout after configured duration
|
|
822
1026
|
setTimeout(() => {
|
|
823
1027
|
if (this.pendingRequests.has(requestId)) {
|
|
824
1028
|
this.pendingRequests.delete(requestId);
|
|
825
1029
|
reject(new Error('Request timeout'));
|
|
826
1030
|
}
|
|
827
|
-
},
|
|
1031
|
+
}, this.requestTimeoutMs);
|
|
828
1032
|
});
|
|
829
1033
|
}
|
|
830
1034
|
|
|
@@ -862,13 +1066,13 @@ export class ThreadWebSocketClient {
|
|
|
862
1066
|
|
|
863
1067
|
this.ws!.send(JSON.stringify(message));
|
|
864
1068
|
|
|
865
|
-
// Timeout after
|
|
1069
|
+
// Timeout after configured duration
|
|
866
1070
|
setTimeout(() => {
|
|
867
1071
|
if (this.pendingRequests.has(requestId)) {
|
|
868
1072
|
this.pendingRequests.delete(requestId);
|
|
869
1073
|
reject(new Error('Request timeout'));
|
|
870
1074
|
}
|
|
871
|
-
},
|
|
1075
|
+
}, this.requestTimeoutMs);
|
|
872
1076
|
});
|
|
873
1077
|
}
|
|
874
1078
|
|
|
@@ -897,13 +1101,13 @@ export class ThreadWebSocketClient {
|
|
|
897
1101
|
|
|
898
1102
|
this.ws!.send(JSON.stringify(message));
|
|
899
1103
|
|
|
900
|
-
// Timeout after
|
|
1104
|
+
// Timeout after configured duration
|
|
901
1105
|
setTimeout(() => {
|
|
902
1106
|
if (this.pendingRequests.has(requestId)) {
|
|
903
1107
|
this.pendingRequests.delete(requestId);
|
|
904
1108
|
reject(new Error('Request timeout'));
|
|
905
1109
|
}
|
|
906
|
-
},
|
|
1110
|
+
}, this.requestTimeoutMs);
|
|
907
1111
|
});
|
|
908
1112
|
}
|
|
909
1113
|
|
|
@@ -930,8 +1134,6 @@ export class ThreadWebSocketClient {
|
|
|
930
1134
|
}
|
|
931
1135
|
}
|
|
932
1136
|
|
|
933
|
-
const validThreadIdCharacters = /^[a-zA-Z0-9-]+$/;
|
|
934
|
-
|
|
935
1137
|
export class DefaultThreadProvider implements ThreadProvider {
|
|
936
1138
|
private appState: AppState | null = null;
|
|
937
1139
|
private wsClient: ThreadWebSocketClient | null = null;
|
|
@@ -948,7 +1150,7 @@ export class DefaultThreadProvider implements ThreadProvider {
|
|
|
948
1150
|
const serviceUrls = getServiceUrls(process.env.AGENTUITY_REGION ?? 'usc');
|
|
949
1151
|
const catalystUrl = serviceUrls.catalyst;
|
|
950
1152
|
const wsUrl = new URL('/thread/ws', catalystUrl.replace(/^http/, 'ws'));
|
|
951
|
-
|
|
1153
|
+
internal.debug('connecting to %s', wsUrl);
|
|
952
1154
|
|
|
953
1155
|
this.wsClient = new ThreadWebSocketClient(apiKey, wsUrl.toString());
|
|
954
1156
|
// Connect in background, don't block initialization
|
|
@@ -958,7 +1160,7 @@ export class DefaultThreadProvider implements ThreadProvider {
|
|
|
958
1160
|
this.wsConnecting = null;
|
|
959
1161
|
})
|
|
960
1162
|
.catch((err) => {
|
|
961
|
-
|
|
1163
|
+
internal.error('Failed to connect to thread WebSocket:', err);
|
|
962
1164
|
this.wsClient = null;
|
|
963
1165
|
this.wsConnecting = null;
|
|
964
1166
|
});
|
|
@@ -970,31 +1172,8 @@ export class DefaultThreadProvider implements ThreadProvider {
|
|
|
970
1172
|
}
|
|
971
1173
|
|
|
972
1174
|
async restore(ctx: Context<Env>): Promise<Thread> {
|
|
973
|
-
const threadId = this.threadIDProvider!.getThreadId(this.appState!, ctx);
|
|
974
|
-
|
|
975
|
-
if (!threadId) {
|
|
976
|
-
throw new Error(`the ThreadIDProvider returned an empty thread id for getThreadId`);
|
|
977
|
-
}
|
|
978
|
-
if (!threadId.startsWith('thrd_')) {
|
|
979
|
-
throw new Error(
|
|
980
|
-
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must start with the prefix 'thrd_'.`
|
|
981
|
-
);
|
|
982
|
-
}
|
|
983
|
-
if (threadId.length > 64) {
|
|
984
|
-
throw new Error(
|
|
985
|
-
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be less than 64 characters long.`
|
|
986
|
-
);
|
|
987
|
-
}
|
|
988
|
-
if (threadId.length < 32) {
|
|
989
|
-
throw new Error(
|
|
990
|
-
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must be at least 32 characters long.`
|
|
991
|
-
);
|
|
992
|
-
}
|
|
993
|
-
if (!validThreadIdCharacters.test(threadId.substring(5))) {
|
|
994
|
-
throw new Error(
|
|
995
|
-
`the ThreadIDProvider returned an invalid thread id (${threadId}) for getThreadId. The thread id must contain only characters that match the regular expression [a-zA-Z0-9-].`
|
|
996
|
-
);
|
|
997
|
-
}
|
|
1175
|
+
const threadId = await this.threadIDProvider!.getThreadId(this.appState!, ctx);
|
|
1176
|
+
validateThreadIdOrThrow(threadId);
|
|
998
1177
|
|
|
999
1178
|
// Wait for WebSocket connection if still connecting
|
|
1000
1179
|
if (this.wsConnecting) {
|
package/src/web.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { serveStatic } from 'hono/bun';
|
|
3
|
+
import { join, relative } from 'node:path';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a router that serves the web application.
|
|
8
|
+
* In dev mode (DEV=true), serves HTML with Vite HMR scripts (Bun server proxies assets to Vite).
|
|
9
|
+
* In production, serves static files from .agentuity/client/.
|
|
10
|
+
*/
|
|
11
|
+
export async function createWebRouter(): Promise<Hono> {
|
|
12
|
+
const router = new Hono();
|
|
13
|
+
const isDev = process.env.DEV === 'true';
|
|
14
|
+
const rootDir = process.cwd();
|
|
15
|
+
|
|
16
|
+
if (isDev) {
|
|
17
|
+
// In dev mode, serve HTML with Vite client scripts for HMR
|
|
18
|
+
// Bun server proxies /src/*, /@vite/*, etc. to Vite asset server
|
|
19
|
+
router.get('/', (c) => {
|
|
20
|
+
return c.html(
|
|
21
|
+
`<!DOCTYPE html>
|
|
22
|
+
<html lang="en">
|
|
23
|
+
<head>
|
|
24
|
+
<meta charset="UTF-8" />
|
|
25
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
26
|
+
<title>Agentuity App</title>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<div id="root"></div>
|
|
30
|
+
|
|
31
|
+
<script type="module" src="/@vite/client"></script>
|
|
32
|
+
<script type="module">
|
|
33
|
+
import RefreshRuntime from '/@react-refresh';
|
|
34
|
+
RefreshRuntime.injectIntoGlobalHook(window);
|
|
35
|
+
window.$RefreshReg$ = () => {};
|
|
36
|
+
window.$RefreshSig$ = () => (type) => type;
|
|
37
|
+
window.__vite_plugin_react_preamble_installed__ = true;
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
<script type="module" src="/src/web/frontend.tsx"></script>
|
|
41
|
+
</body>
|
|
42
|
+
</html>`
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
} else {
|
|
46
|
+
// Production: serve static files from .agentuity/client/
|
|
47
|
+
const clientDir = join(rootDir, '.agentuity', 'client');
|
|
48
|
+
|
|
49
|
+
// Verify client build exists
|
|
50
|
+
const indexHtmlPath = join(clientDir, 'index.html');
|
|
51
|
+
if (!existsSync(indexHtmlPath)) {
|
|
52
|
+
throw new Error(
|
|
53
|
+
`Client build not found. Missing ${indexHtmlPath}. Run build to generate client assets.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Compute relative paths for serveStatic (it expects relative paths from cwd)
|
|
58
|
+
let relClientDir = relative(process.cwd(), clientDir);
|
|
59
|
+
if (!relClientDir.startsWith('.')) {
|
|
60
|
+
relClientDir = './' + relClientDir;
|
|
61
|
+
}
|
|
62
|
+
let relIndexPath = relative(process.cwd(), indexHtmlPath);
|
|
63
|
+
if (!relIndexPath.startsWith('.')) {
|
|
64
|
+
relIndexPath = './' + relIndexPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Serve static files from .agentuity/client/
|
|
68
|
+
router.use('/*', serveStatic({ root: relClientDir }));
|
|
69
|
+
|
|
70
|
+
// Fallback to index.html for SPA routing
|
|
71
|
+
router.get('*', serveStatic({ path: relIndexPath }));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return router;
|
|
75
|
+
}
|