@agentuity/runtime 0.1.31 → 0.1.33
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/dist/_standalone.d.ts.map +1 -1
- package/dist/_standalone.js +3 -0
- package/dist/_standalone.js.map +1 -1
- package/dist/_waituntil.d.ts +15 -1
- package/dist/_waituntil.d.ts.map +1 -1
- package/dist/_waituntil.js +78 -19
- package/dist/_waituntil.js.map +1 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +20 -5
- package/dist/agent.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +213 -29
- package/dist/middleware.js.map +1 -1
- package/dist/router.d.ts.map +1 -1
- package/dist/router.js +19 -1
- package/dist/router.js.map +1 -1
- package/dist/services/sandbox/http.d.ts +8 -1
- package/dist/services/sandbox/http.d.ts.map +1 -1
- package/dist/services/sandbox/http.js +53 -1
- package/dist/services/sandbox/http.js.map +1 -1
- package/dist/services/session/http.d.ts.map +1 -1
- package/dist/services/session/http.js +12 -7
- package/dist/services/session/http.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +1 -0
- package/dist/session.js.map +1 -1
- package/package.json +7 -7
- package/src/_standalone.ts +3 -0
- package/src/_waituntil.ts +88 -18
- package/src/agent.ts +19 -5
- package/src/middleware.ts +392 -31
- package/src/router.ts +28 -1
- package/src/services/sandbox/http.ts +88 -0
- package/src/services/session/http.ts +16 -8
- package/src/session.ts +1 -0
package/src/middleware.ts
CHANGED
|
@@ -29,6 +29,7 @@ import * as runtimeConfig from './_config';
|
|
|
29
29
|
import { getSessionEventProvider } from './_services';
|
|
30
30
|
import { internal } from './logger/internal';
|
|
31
31
|
import { STREAM_DONE_PROMISE_KEY, IS_STREAMING_RESPONSE_KEY } from './handlers/sse';
|
|
32
|
+
import { loadBuildMetadata } from './_metadata';
|
|
32
33
|
|
|
33
34
|
const SESSION_HEADER = 'x-session-id';
|
|
34
35
|
const THREAD_HEADER = 'x-thread-id';
|
|
@@ -302,6 +303,8 @@ export function createOtelMiddleware() {
|
|
|
302
303
|
},
|
|
303
304
|
},
|
|
304
305
|
async (span): Promise<void> => {
|
|
306
|
+
// Track request duration from the SDK's perspective
|
|
307
|
+
const requestStartTime = performance.now();
|
|
305
308
|
const sctx = span.spanContext();
|
|
306
309
|
const sessionId = sctx?.traceId ? `sess_${sctx.traceId}` : generateId('sess');
|
|
307
310
|
|
|
@@ -321,6 +324,7 @@ export function createOtelMiddleware() {
|
|
|
321
324
|
|
|
322
325
|
if (projectId) traceState = traceState.set('pid', projectId);
|
|
323
326
|
if (orgId) traceState = traceState.set('oid', orgId);
|
|
327
|
+
if (deploymentId) traceState = traceState.set('did', deploymentId);
|
|
324
328
|
if (isDevMode) traceState = traceState.set('d', '1');
|
|
325
329
|
|
|
326
330
|
// Update the active context with the new trace state
|
|
@@ -353,8 +357,46 @@ export function createOtelMiddleware() {
|
|
|
353
357
|
const sessionEventProvider = getSessionEventProvider();
|
|
354
358
|
if (sessionEventProvider) {
|
|
355
359
|
try {
|
|
356
|
-
//
|
|
357
|
-
|
|
360
|
+
// Look up routeId from build metadata by matching method and path
|
|
361
|
+
// We need to do this here because the router wrapper hasn't run yet
|
|
362
|
+
const metadata = loadBuildMetadata();
|
|
363
|
+
const methodUpper = c.req.method.toUpperCase();
|
|
364
|
+
|
|
365
|
+
// Normalize paths: trim trailing slashes for consistent matching
|
|
366
|
+
const normalizePath = (p: string) => {
|
|
367
|
+
const decoded = decodeURIComponent(p);
|
|
368
|
+
return decoded.endsWith('/') && decoded.length > 1
|
|
369
|
+
? decoded.slice(0, -1)
|
|
370
|
+
: decoded;
|
|
371
|
+
};
|
|
372
|
+
const requestPath = normalizePath(c.req.path);
|
|
373
|
+
|
|
374
|
+
// Helper to check if requestPath ends with routePath at a segment boundary
|
|
375
|
+
// e.g., "/api/translate" matches "/translate" but "/api/translate-v2" does not
|
|
376
|
+
const matchesAtSegmentBoundary = (reqPath: string, routePath: string) => {
|
|
377
|
+
if (reqPath === routePath) return true;
|
|
378
|
+
if (!reqPath.endsWith(routePath)) return false;
|
|
379
|
+
// Check that the character before the match is a path separator
|
|
380
|
+
const charBeforeMatch = reqPath[reqPath.length - routePath.length - 1];
|
|
381
|
+
return charBeforeMatch === '/';
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
// Try matching by exact normalized path first
|
|
385
|
+
let route = metadata?.routes?.find(
|
|
386
|
+
(r) =>
|
|
387
|
+
r.method.toUpperCase() === methodUpper &&
|
|
388
|
+
normalizePath(r.path) === requestPath
|
|
389
|
+
);
|
|
390
|
+
// Fall back to segment-boundary matching (handles /api/translate matching /translate)
|
|
391
|
+
if (!route) {
|
|
392
|
+
route = metadata?.routes?.find(
|
|
393
|
+
(r) =>
|
|
394
|
+
r.method.toUpperCase() === methodUpper &&
|
|
395
|
+
matchesAtSegmentBoundary(requestPath, normalizePath(r.path))
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
const routeId = route?.id || '';
|
|
399
|
+
|
|
358
400
|
await sessionEventProvider.start({
|
|
359
401
|
id: sessionId,
|
|
360
402
|
threadId: thread.id,
|
|
@@ -418,9 +460,35 @@ export function createOtelMiddleware() {
|
|
|
418
460
|
}
|
|
419
461
|
};
|
|
420
462
|
|
|
463
|
+
// Track state for finalization
|
|
464
|
+
let responseStatus = 200;
|
|
465
|
+
let errorMessage: string | undefined;
|
|
466
|
+
let handlerDurationMs = 0;
|
|
467
|
+
// Track whether span should be ended in finally block (false for streaming - ended in waitUntil)
|
|
468
|
+
let shouldEndSpanInFinally = true;
|
|
469
|
+
|
|
421
470
|
try {
|
|
471
|
+
internal.info(
|
|
472
|
+
'[request] %s %s - handler starting (session: %s)',
|
|
473
|
+
method,
|
|
474
|
+
url.pathname,
|
|
475
|
+
sessionId
|
|
476
|
+
);
|
|
477
|
+
|
|
422
478
|
await next();
|
|
423
479
|
|
|
480
|
+
// Capture timing immediately after next() returns - this is when the handler completed
|
|
481
|
+
// This is the HTTP response time we want to report (excludes waitUntil/finalization)
|
|
482
|
+
handlerDurationMs = performance.now() - requestStartTime;
|
|
483
|
+
|
|
484
|
+
internal.info(
|
|
485
|
+
'[request] %s %s - handler completed in %sms (session: %s)',
|
|
486
|
+
method,
|
|
487
|
+
url.pathname,
|
|
488
|
+
handlerDurationMs.toFixed(2),
|
|
489
|
+
sessionId
|
|
490
|
+
);
|
|
491
|
+
|
|
424
492
|
// Check if this is a streaming response that needs deferred finalization
|
|
425
493
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
426
494
|
const streamDone = (c as any).get(STREAM_DONE_PROMISE_KEY) as
|
|
@@ -433,67 +501,347 @@ export function createOtelMiddleware() {
|
|
|
433
501
|
// or if the response status indicates an error
|
|
434
502
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
435
503
|
const honoError = (c as any).error as Error | undefined;
|
|
436
|
-
|
|
504
|
+
responseStatus = c.res?.status ?? 200;
|
|
437
505
|
const isError = honoError || responseStatus >= 500;
|
|
438
506
|
|
|
507
|
+
internal.info(
|
|
508
|
+
'[request] %s %s - status: %d, streaming: %s, error: %s (session: %s)',
|
|
509
|
+
method,
|
|
510
|
+
url.pathname,
|
|
511
|
+
responseStatus,
|
|
512
|
+
isStreaming,
|
|
513
|
+
isError,
|
|
514
|
+
sessionId
|
|
515
|
+
);
|
|
516
|
+
|
|
517
|
+
if (isError) {
|
|
518
|
+
// Capture error message for finalization
|
|
519
|
+
errorMessage = honoError
|
|
520
|
+
? (honoError.stack ?? honoError.message)
|
|
521
|
+
: `HTTP ${responseStatus}`;
|
|
522
|
+
span.setStatus({
|
|
523
|
+
code: SpanStatusCode.ERROR,
|
|
524
|
+
message: honoError?.message ?? errorMessage,
|
|
525
|
+
});
|
|
526
|
+
if (honoError) {
|
|
527
|
+
span.recordException(honoError);
|
|
528
|
+
}
|
|
529
|
+
} else {
|
|
530
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// For streaming responses, defer everything until stream completes
|
|
439
534
|
if (isStreaming && streamDone) {
|
|
440
|
-
// Defer session/thread saving until stream completes
|
|
441
|
-
// This ensures thread state changes made during streaming are persisted
|
|
442
535
|
internal.info(
|
|
443
|
-
'[
|
|
536
|
+
'[request] %s %s - streaming response, deferring finalization (session: %s)',
|
|
537
|
+
method,
|
|
538
|
+
url.pathname,
|
|
444
539
|
sessionId
|
|
445
540
|
);
|
|
446
541
|
|
|
542
|
+
// For streaming, we end the span inside waitUntil after setting attributes
|
|
543
|
+
shouldEndSpanInFinally = false;
|
|
544
|
+
|
|
545
|
+
// Capture pending promises BEFORE adding finalization waitUntil to avoid deadlock
|
|
546
|
+
const pendingPromises = handler.getPendingSnapshot();
|
|
547
|
+
const hasPendingTasks = pendingPromises.length > 0;
|
|
548
|
+
|
|
549
|
+
if (hasPendingTasks) {
|
|
550
|
+
internal.info(
|
|
551
|
+
'[request] %s %s - %d pending waitUntil tasks to wait for after stream (session: %s)',
|
|
552
|
+
method,
|
|
553
|
+
url.pathname,
|
|
554
|
+
pendingPromises.length,
|
|
555
|
+
sessionId
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Capture values needed for span attributes (responseStatus already captured above)
|
|
560
|
+
const capturedResponseStatus = responseStatus;
|
|
561
|
+
const capturedErrorMessage = errorMessage;
|
|
562
|
+
|
|
563
|
+
// Use waitUntil to handle stream completion and finalization
|
|
564
|
+
// This runs AFTER the response is sent to the client
|
|
565
|
+
// Note: We intentionally do NOT use noSpan here - the waitUntil span helps
|
|
566
|
+
// track the streaming finalization work in telemetry
|
|
447
567
|
handler.waitUntil(async () => {
|
|
568
|
+
// Track if stream ended with error so we can update finalization status
|
|
569
|
+
let streamError: unknown = undefined;
|
|
570
|
+
|
|
448
571
|
try {
|
|
449
572
|
await streamDone;
|
|
450
573
|
internal.info(
|
|
451
|
-
'[
|
|
574
|
+
'[request] %s %s - stream completed (session: %s)',
|
|
575
|
+
method,
|
|
576
|
+
url.pathname,
|
|
452
577
|
sessionId
|
|
453
578
|
);
|
|
454
579
|
} catch (ex) {
|
|
455
|
-
|
|
580
|
+
streamError = ex;
|
|
456
581
|
internal.info(
|
|
457
|
-
'[
|
|
458
|
-
|
|
582
|
+
'[request] %s %s - stream ended with error: %s (session: %s)',
|
|
583
|
+
method,
|
|
584
|
+
url.pathname,
|
|
585
|
+
ex,
|
|
586
|
+
sessionId
|
|
459
587
|
);
|
|
460
588
|
}
|
|
461
|
-
await finalizeSession();
|
|
462
|
-
});
|
|
463
589
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
590
|
+
// Record duration now that stream is complete - set attributes BEFORE ending span
|
|
591
|
+
const streamDurationMs = performance.now() - requestStartTime;
|
|
592
|
+
const durationNs = Math.round(streamDurationMs * 1_000_000);
|
|
593
|
+
internal.info(
|
|
594
|
+
'[request] %s %s - recording stream duration: %sms (session: %s)',
|
|
595
|
+
method,
|
|
596
|
+
url.pathname,
|
|
597
|
+
streamDurationMs.toFixed(2),
|
|
598
|
+
sessionId
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
// Determine final status - use stream error if present
|
|
602
|
+
const finalStatus = streamError ? 500 : capturedResponseStatus;
|
|
603
|
+
const finalErrorMessage = streamError
|
|
604
|
+
? streamError instanceof Error
|
|
605
|
+
? (streamError.stack ?? streamError.message)
|
|
606
|
+
: String(streamError)
|
|
607
|
+
: capturedErrorMessage;
|
|
608
|
+
|
|
609
|
+
try {
|
|
610
|
+
// Wait for pending tasks (evals, etc.) captured BEFORE this waitUntil was added
|
|
611
|
+
if (hasPendingTasks) {
|
|
612
|
+
internal.info(
|
|
613
|
+
'[request] %s %s - waiting for %d pending waitUntil tasks (session: %s)',
|
|
614
|
+
method,
|
|
615
|
+
url.pathname,
|
|
616
|
+
pendingPromises.length,
|
|
617
|
+
sessionId
|
|
618
|
+
);
|
|
619
|
+
const logger = c.get('logger');
|
|
620
|
+
await handler.waitForPromises(pendingPromises, logger, sessionId);
|
|
621
|
+
internal.info(
|
|
622
|
+
'[request] %s %s - all waitUntil tasks complete (session: %s)',
|
|
623
|
+
method,
|
|
624
|
+
url.pathname,
|
|
625
|
+
sessionId
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Finalize session after stream completes and evals finish
|
|
630
|
+
await finalizeSession(
|
|
631
|
+
finalStatus >= 500 ? finalStatus : undefined,
|
|
632
|
+
finalErrorMessage
|
|
633
|
+
);
|
|
634
|
+
internal.info(
|
|
635
|
+
'[request] %s %s - stream session finalization complete (session: %s)',
|
|
636
|
+
method,
|
|
637
|
+
url.pathname,
|
|
638
|
+
sessionId
|
|
639
|
+
);
|
|
640
|
+
} finally {
|
|
641
|
+
// Set span attributes and end span AFTER all work is done
|
|
642
|
+
span.setAttribute('@agentuity/request.duration', durationNs);
|
|
643
|
+
span.setAttribute('http.status_code', finalStatus);
|
|
644
|
+
|
|
645
|
+
// Set span status based on whether there was an error
|
|
646
|
+
if (streamError) {
|
|
647
|
+
span.setStatus({
|
|
648
|
+
code: SpanStatusCode.ERROR,
|
|
649
|
+
message: finalErrorMessage ?? 'Stream ended with error',
|
|
650
|
+
});
|
|
651
|
+
if (streamError instanceof Error) {
|
|
652
|
+
span.recordException(streamError);
|
|
653
|
+
}
|
|
654
|
+
} else {
|
|
655
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
span.end();
|
|
659
|
+
internal.info(
|
|
660
|
+
'[request] %s %s - stream span ended (session: %s)',
|
|
661
|
+
method,
|
|
662
|
+
url.pathname,
|
|
663
|
+
sessionId
|
|
664
|
+
);
|
|
665
|
+
// Note: We don't call waitUntilAll() here because this waitUntil callback
|
|
666
|
+
// IS the final cleanup task. Calling waitUntilAll() would deadlock since
|
|
667
|
+
// it would wait for this very promise to complete.
|
|
668
|
+
}
|
|
473
669
|
});
|
|
474
|
-
if (honoError) {
|
|
475
|
-
span.recordException(honoError);
|
|
476
|
-
}
|
|
477
|
-
await finalizeSession(responseStatus, errorMessage);
|
|
478
670
|
} else {
|
|
479
|
-
// Non-streaming
|
|
480
|
-
|
|
481
|
-
|
|
671
|
+
// Non-streaming: record duration immediately
|
|
672
|
+
const durationNs = Math.round(handlerDurationMs * 1_000_000);
|
|
673
|
+
internal.info(
|
|
674
|
+
'[request] %s %s - recording duration: %sms (%dns) (session: %s)',
|
|
675
|
+
method,
|
|
676
|
+
url.pathname,
|
|
677
|
+
handlerDurationMs.toFixed(2),
|
|
678
|
+
durationNs,
|
|
679
|
+
sessionId
|
|
680
|
+
);
|
|
681
|
+
span.setAttribute('@agentuity/request.duration', durationNs);
|
|
682
|
+
span.setAttribute('http.status_code', responseStatus);
|
|
683
|
+
|
|
684
|
+
// Capture pending promises BEFORE adding finalization waitUntil to avoid deadlock.
|
|
685
|
+
// If we called waitUntilAll inside waitUntil, it would wait for itself.
|
|
686
|
+
const pendingPromises = handler.getPendingSnapshot();
|
|
687
|
+
const hasPendingTasks = pendingPromises.length > 0;
|
|
688
|
+
|
|
689
|
+
if (hasPendingTasks) {
|
|
690
|
+
internal.info(
|
|
691
|
+
'[request] %s %s - %d pending waitUntil tasks to wait for (session: %s)',
|
|
692
|
+
method,
|
|
693
|
+
url.pathname,
|
|
694
|
+
pendingPromises.length,
|
|
695
|
+
sessionId
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
// Capture values for use in waitUntil callback
|
|
700
|
+
const capturedResponseStatus = responseStatus;
|
|
701
|
+
const capturedErrorMessage = errorMessage;
|
|
702
|
+
|
|
703
|
+
// Defer session finalization to run AFTER response is sent
|
|
704
|
+
// Use noSpan: true since finalizeSession creates its own Session End span
|
|
705
|
+
handler.waitUntil(
|
|
706
|
+
async () => {
|
|
707
|
+
// Wait for the snapshot of pending tasks (evals, etc.) captured BEFORE this waitUntil was added
|
|
708
|
+
if (hasPendingTasks) {
|
|
709
|
+
internal.info(
|
|
710
|
+
'[request] %s %s - waiting for %d pending waitUntil tasks (session: %s)',
|
|
711
|
+
method,
|
|
712
|
+
url.pathname,
|
|
713
|
+
pendingPromises.length,
|
|
714
|
+
sessionId
|
|
715
|
+
);
|
|
716
|
+
const logger = c.get('logger');
|
|
717
|
+
await handler.waitForPromises(pendingPromises, logger, sessionId);
|
|
718
|
+
internal.info(
|
|
719
|
+
'[request] %s %s - all waitUntil tasks complete (session: %s)',
|
|
720
|
+
method,
|
|
721
|
+
url.pathname,
|
|
722
|
+
sessionId
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Finalize session - this is the actual work
|
|
727
|
+
internal.info(
|
|
728
|
+
'[request] %s %s - starting session finalization (session: %s)',
|
|
729
|
+
method,
|
|
730
|
+
url.pathname,
|
|
731
|
+
sessionId
|
|
732
|
+
);
|
|
733
|
+
try {
|
|
734
|
+
await finalizeSession(
|
|
735
|
+
capturedResponseStatus >= 500 ? capturedResponseStatus : undefined,
|
|
736
|
+
capturedErrorMessage
|
|
737
|
+
);
|
|
738
|
+
internal.info(
|
|
739
|
+
'[request] %s %s - session finalization complete (session: %s)',
|
|
740
|
+
method,
|
|
741
|
+
url.pathname,
|
|
742
|
+
sessionId
|
|
743
|
+
);
|
|
744
|
+
} catch (ex) {
|
|
745
|
+
internal.error(
|
|
746
|
+
'[request] %s %s - session finalization failed: %s (session: %s)',
|
|
747
|
+
method,
|
|
748
|
+
url.pathname,
|
|
749
|
+
ex,
|
|
750
|
+
sessionId
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
// Note: We don't call waitUntilAll() here because this waitUntil callback
|
|
754
|
+
// IS the final cleanup task. Calling waitUntilAll() would deadlock since
|
|
755
|
+
// it would wait for this very promise to complete.
|
|
756
|
+
},
|
|
757
|
+
{ noSpan: true }
|
|
758
|
+
);
|
|
482
759
|
}
|
|
483
760
|
} catch (ex) {
|
|
761
|
+
// Record request metrics even on exceptions (500 status)
|
|
762
|
+
const exceptionDurationMs = performance.now() - requestStartTime;
|
|
763
|
+
const durationNs = Math.round(exceptionDurationMs * 1_000_000);
|
|
764
|
+
internal.info(
|
|
765
|
+
'[request] %s %s - recording exception duration: %sms (session: %s)',
|
|
766
|
+
method,
|
|
767
|
+
url.pathname,
|
|
768
|
+
exceptionDurationMs.toFixed(2),
|
|
769
|
+
sessionId
|
|
770
|
+
);
|
|
771
|
+
span.setAttribute('@agentuity/request.duration', durationNs);
|
|
772
|
+
span.setAttribute('http.status_code', 500);
|
|
773
|
+
|
|
484
774
|
if (ex instanceof Error) {
|
|
485
775
|
span.recordException(ex);
|
|
486
776
|
}
|
|
487
|
-
|
|
777
|
+
errorMessage = ex instanceof Error ? (ex.stack ?? ex.message) : String(ex);
|
|
778
|
+
responseStatus = 500;
|
|
488
779
|
span.setStatus({
|
|
489
780
|
code: SpanStatusCode.ERROR,
|
|
490
781
|
message: ex instanceof Error ? ex.message : String(ex),
|
|
491
782
|
});
|
|
492
783
|
|
|
493
|
-
|
|
784
|
+
// Capture error message for use in waitUntil callback
|
|
785
|
+
const capturedErrorMessage = errorMessage;
|
|
786
|
+
|
|
787
|
+
// Capture pending promises BEFORE adding finalization waitUntil to avoid deadlock
|
|
788
|
+
const pendingPromises = handler.getPendingSnapshot();
|
|
789
|
+
const hasPendingTasks = pendingPromises.length > 0;
|
|
790
|
+
|
|
791
|
+
if (hasPendingTasks) {
|
|
792
|
+
internal.info(
|
|
793
|
+
'[request] %s %s - %d pending waitUntil tasks to wait for after error (session: %s)',
|
|
794
|
+
method,
|
|
795
|
+
url.pathname,
|
|
796
|
+
pendingPromises.length,
|
|
797
|
+
sessionId
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Still defer finalization even on error
|
|
802
|
+
// Use noSpan: true since finalizeSession creates its own Session End span
|
|
803
|
+
handler.waitUntil(
|
|
804
|
+
async () => {
|
|
805
|
+
// Wait for pending tasks (evals, etc.) captured BEFORE this waitUntil was added
|
|
806
|
+
if (hasPendingTasks) {
|
|
807
|
+
internal.info(
|
|
808
|
+
'[request] %s %s - waiting for %d pending waitUntil tasks (session: %s)',
|
|
809
|
+
method,
|
|
810
|
+
url.pathname,
|
|
811
|
+
pendingPromises.length,
|
|
812
|
+
sessionId
|
|
813
|
+
);
|
|
814
|
+
const logger = c.get('logger');
|
|
815
|
+
await handler.waitForPromises(pendingPromises, logger, sessionId);
|
|
816
|
+
internal.info(
|
|
817
|
+
'[request] %s %s - all waitUntil tasks complete (session: %s)',
|
|
818
|
+
method,
|
|
819
|
+
url.pathname,
|
|
820
|
+
sessionId
|
|
821
|
+
);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
try {
|
|
825
|
+
await finalizeSession(500, capturedErrorMessage);
|
|
826
|
+
} catch (finalizeEx) {
|
|
827
|
+
internal.error(
|
|
828
|
+
'[request] %s %s - error session finalization failed: %s (session: %s)',
|
|
829
|
+
method,
|
|
830
|
+
url.pathname,
|
|
831
|
+
finalizeEx,
|
|
832
|
+
sessionId
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
// Note: We don't call waitUntilAll() here because this waitUntil callback
|
|
836
|
+
// IS the final cleanup task. Calling waitUntilAll() would deadlock since
|
|
837
|
+
// it would wait for this very promise to complete.
|
|
838
|
+
},
|
|
839
|
+
{ noSpan: true }
|
|
840
|
+
);
|
|
494
841
|
|
|
495
842
|
throw ex;
|
|
496
843
|
} finally {
|
|
844
|
+
// Set response headers - this is the only thing that should block the response
|
|
497
845
|
const headers: Record<string, string> = {};
|
|
498
846
|
propagation.inject(context.active(), headers);
|
|
499
847
|
for (const key of Object.keys(headers)) {
|
|
@@ -501,7 +849,20 @@ export function createOtelMiddleware() {
|
|
|
501
849
|
}
|
|
502
850
|
const traceId = sctx?.traceId || sessionId.replace(/^sess_/, '');
|
|
503
851
|
c.header(SESSION_HEADER, `sess_${traceId}`);
|
|
504
|
-
|
|
852
|
+
|
|
853
|
+
internal.info(
|
|
854
|
+
'[request] %s %s - response ready, duration: %sms (session: %s)',
|
|
855
|
+
method,
|
|
856
|
+
url.pathname,
|
|
857
|
+
handlerDurationMs.toFixed(2),
|
|
858
|
+
sessionId
|
|
859
|
+
);
|
|
860
|
+
|
|
861
|
+
// Only end span here for non-streaming responses
|
|
862
|
+
// For streaming, span is ended in the waitUntil callback after setting duration attributes
|
|
863
|
+
if (shouldEndSpanInFinally) {
|
|
864
|
+
span.end();
|
|
865
|
+
}
|
|
505
866
|
}
|
|
506
867
|
}
|
|
507
868
|
);
|
package/src/router.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { type Context, Hono, type Schema, type Env as HonoEnv } from 'hono';
|
|
3
3
|
import { returnResponse } from './_util';
|
|
4
4
|
import type { Env } from './app';
|
|
5
|
+
import { loadBuildMetadata } from './_metadata';
|
|
5
6
|
|
|
6
7
|
// Re-export both Env types
|
|
7
8
|
export type { Env };
|
|
@@ -163,8 +164,34 @@ export const createRouter = <E extends Env = Env, S extends Schema = Schema>():
|
|
|
163
164
|
return _originalInvoker(path, ...args);
|
|
164
165
|
}
|
|
165
166
|
|
|
166
|
-
// Wrap the handler to add our response conversion
|
|
167
|
+
// Wrap the handler to add our response conversion and set routeId
|
|
167
168
|
const wrapper = async (c: Context): Promise<Response> => {
|
|
169
|
+
// Look up the route ID from build metadata by matching method and path
|
|
170
|
+
// Try both the registered path and the actual request path (which may include base path)
|
|
171
|
+
const metadata = loadBuildMetadata();
|
|
172
|
+
const methodUpper = method.toUpperCase();
|
|
173
|
+
const requestPath = c.req.routePath || c.req.path;
|
|
174
|
+
|
|
175
|
+
// Try matching by registered path first, then by request path, then by path ending
|
|
176
|
+
let route = metadata?.routes?.find(
|
|
177
|
+
(r) => r.method.toUpperCase() === methodUpper && r.path === path
|
|
178
|
+
);
|
|
179
|
+
if (!route) {
|
|
180
|
+
route = metadata?.routes?.find(
|
|
181
|
+
(r) => r.method.toUpperCase() === methodUpper && r.path === requestPath
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (!route) {
|
|
185
|
+
// Try matching by path ending (handles /api/translate matching /translate)
|
|
186
|
+
route = metadata?.routes?.find(
|
|
187
|
+
(r) => r.method.toUpperCase() === methodUpper && r.path.endsWith(path)
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (route?.id) {
|
|
192
|
+
(c as any).set('routeId', route.id);
|
|
193
|
+
}
|
|
194
|
+
|
|
168
195
|
let result = handler(c);
|
|
169
196
|
if (result instanceof Promise) result = await result;
|
|
170
197
|
// If handler returns a Response, return it unchanged
|
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
sandboxRun,
|
|
9
9
|
sandboxWriteFiles,
|
|
10
10
|
sandboxReadFile,
|
|
11
|
+
snapshotCreate,
|
|
12
|
+
snapshotGet,
|
|
13
|
+
snapshotList,
|
|
14
|
+
snapshotDelete,
|
|
15
|
+
snapshotTag,
|
|
11
16
|
} from '@agentuity/server';
|
|
12
17
|
import type {
|
|
13
18
|
SandboxService,
|
|
@@ -23,6 +28,11 @@ import type {
|
|
|
23
28
|
StreamReader,
|
|
24
29
|
SandboxStatus,
|
|
25
30
|
FileToWrite,
|
|
31
|
+
SnapshotService,
|
|
32
|
+
SnapshotCreateOptions,
|
|
33
|
+
SnapshotInfo,
|
|
34
|
+
SnapshotListParams,
|
|
35
|
+
SnapshotListResponse,
|
|
26
36
|
} from '@agentuity/core';
|
|
27
37
|
import { context, SpanKind, SpanStatusCode, trace } from '@opentelemetry/api';
|
|
28
38
|
|
|
@@ -149,13 +159,91 @@ function createSandboxInstance(
|
|
|
149
159
|
};
|
|
150
160
|
}
|
|
151
161
|
|
|
162
|
+
/**
|
|
163
|
+
* HTTP implementation of the SnapshotService interface
|
|
164
|
+
*/
|
|
165
|
+
class HTTPSnapshotService implements SnapshotService {
|
|
166
|
+
private client: APIClient;
|
|
167
|
+
|
|
168
|
+
constructor(client: APIClient) {
|
|
169
|
+
this.client = client;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async create(sandboxId: string, options?: SnapshotCreateOptions): Promise<SnapshotInfo> {
|
|
173
|
+
return withSpan(
|
|
174
|
+
'agentuity.sandbox.snapshot.create',
|
|
175
|
+
{
|
|
176
|
+
'sandbox.id': sandboxId,
|
|
177
|
+
'snapshot.name': options?.name ?? '',
|
|
178
|
+
'snapshot.tag': options?.tag ?? '',
|
|
179
|
+
},
|
|
180
|
+
() =>
|
|
181
|
+
snapshotCreate(this.client, {
|
|
182
|
+
sandboxId,
|
|
183
|
+
name: options?.name,
|
|
184
|
+
description: options?.description,
|
|
185
|
+
tag: options?.tag,
|
|
186
|
+
public: options?.public,
|
|
187
|
+
})
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async get(snapshotId: string): Promise<SnapshotInfo> {
|
|
192
|
+
return withSpan('agentuity.sandbox.snapshot.get', { 'snapshot.id': snapshotId }, () =>
|
|
193
|
+
snapshotGet(this.client, { snapshotId })
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async list(params?: SnapshotListParams): Promise<SnapshotListResponse> {
|
|
198
|
+
return withSpan(
|
|
199
|
+
'agentuity.sandbox.snapshot.list',
|
|
200
|
+
{
|
|
201
|
+
'snapshot.sandboxId': params?.sandboxId ?? '',
|
|
202
|
+
'snapshot.limit': params?.limit ?? 50,
|
|
203
|
+
},
|
|
204
|
+
() =>
|
|
205
|
+
snapshotList(this.client, {
|
|
206
|
+
sandboxId: params?.sandboxId,
|
|
207
|
+
limit: params?.limit,
|
|
208
|
+
offset: params?.offset,
|
|
209
|
+
})
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async delete(snapshotId: string): Promise<void> {
|
|
214
|
+
return withSpan('agentuity.sandbox.snapshot.delete', { 'snapshot.id': snapshotId }, () =>
|
|
215
|
+
snapshotDelete(this.client, { snapshotId })
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
async tag(snapshotId: string, tag: string | null): Promise<SnapshotInfo> {
|
|
220
|
+
return withSpan(
|
|
221
|
+
'agentuity.sandbox.snapshot.tag',
|
|
222
|
+
{
|
|
223
|
+
'snapshot.id': snapshotId,
|
|
224
|
+
'snapshot.tag': tag ?? '',
|
|
225
|
+
},
|
|
226
|
+
() => snapshotTag(this.client, { snapshotId, tag })
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* HTTP implementation of the SandboxService interface
|
|
233
|
+
*/
|
|
152
234
|
export class HTTPSandboxService implements SandboxService {
|
|
153
235
|
private client: APIClient;
|
|
154
236
|
private streamBaseUrl: string;
|
|
155
237
|
|
|
238
|
+
/**
|
|
239
|
+
* Snapshot management operations
|
|
240
|
+
*/
|
|
241
|
+
public readonly snapshot: SnapshotService;
|
|
242
|
+
|
|
156
243
|
constructor(client: APIClient, streamBaseUrl: string) {
|
|
157
244
|
this.client = client;
|
|
158
245
|
this.streamBaseUrl = streamBaseUrl;
|
|
246
|
+
this.snapshot = new HTTPSnapshotService(client);
|
|
159
247
|
}
|
|
160
248
|
|
|
161
249
|
async run(options: SandboxRunOptions): Promise<SandboxRunResult> {
|