@cuylabs/agent-foundry-agentserver-invocations 4.1.0 → 4.2.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
@@ -59,6 +59,26 @@ await runInvocationsServer({
59
59
  | `GET` | `/readiness` | Foundry readiness probe |
60
60
  | `GET` | `/readyz` | Readiness alias |
61
61
 
62
+ `/readiness` and `/readyz` return the Python-compatible readiness body:
63
+
64
+ ```json
65
+ { "status": "healthy" }
66
+ ```
67
+
68
+ ## Operations
69
+
70
+ The host mirrors the Python request-span lifecycle for the Invocations
71
+ protocol. It creates OpenTelemetry server spans named `invoke_agent`,
72
+ `get_invocation`, and `cancel_invocation`, propagates invocation/session IDs as
73
+ baggage, records handler errors on the active span, and ends streaming spans
74
+ only after the HTTP response finishes or the client disconnects.
75
+
76
+ By default, `runInvocationsServer()` also logs startup configuration and drains
77
+ in-flight requests for 30 seconds during `close()` before forcing sockets
78
+ closed. Pass `gracefulShutdownTimeoutSeconds` to tune that behavior, or
79
+ `configureObservability: false` if the containing application owns exporter
80
+ initialization itself.
81
+
62
82
  ## Python Mapping
63
83
 
64
84
  This package maps to:
package/dist/index.d.ts CHANGED
@@ -268,6 +268,17 @@ interface InvocationsServerOptions {
268
268
  trustProxy?: boolean | number | string;
269
269
  /** Optional logger. Falls back to `console`. */
270
270
  logger?: InvocationsServerLogger;
271
+ /**
272
+ * Seconds to wait for `close()` to drain in-flight requests before forcing
273
+ * sockets closed. Defaults to `30`, matching the Python host.
274
+ */
275
+ gracefulShutdownTimeoutSeconds?: number;
276
+ /**
277
+ * Initialize Azure Monitor / OTLP exporter plumbing from
278
+ * `APPLICATIONINSIGHTS_CONNECTION_STRING` and `OTEL_EXPORTER_OTLP_ENDPOINT`.
279
+ * Defaults to `true`; span creation itself always uses OpenTelemetry API.
280
+ */
281
+ configureObservability?: boolean;
271
282
  }
272
283
  interface InvocationsServerLogger {
273
284
  info(message: string, meta?: Record<string, unknown>): void;
package/dist/index.js CHANGED
@@ -323,11 +323,13 @@ import {
323
323
  USER_ISOLATION_HEADER,
324
324
  AGENTSERVER_CORE_PACKAGE_VERSION,
325
325
  buildPlatformServerHeader,
326
+ configureAgentServerObservability,
326
327
  createErrorBody,
327
328
  resolveAgentServerConfig,
328
329
  resolveInvocationIdentity as resolveInvocationIdentity2,
329
330
  resolveRequestId,
330
- sanitizeProtocolId
331
+ sanitizeProtocolId,
332
+ startAgentServerRequestSpan
331
333
  } from "@cuylabs/agent-foundry-agentserver-core";
332
334
 
333
335
  // src/package-version.ts
@@ -344,6 +346,10 @@ var DEFAULT_HOST = "0.0.0.0";
344
346
  var DEFAULT_BODY_LIMIT = "1mb";
345
347
  function createInvocationsApp(options) {
346
348
  const logger = options.logger ?? defaultLogger();
349
+ const config = resolveAgentServerConfig({
350
+ port: options.port,
351
+ gracefulShutdownTimeoutSeconds: options.gracefulShutdownTimeoutSeconds
352
+ });
347
353
  const trustProxy = options.trustProxy ?? 1;
348
354
  const bodyLimit = options.bodyLimit ?? DEFAULT_BODY_LIMIT;
349
355
  const appName = options.appName ?? "foundry-invocations-host";
@@ -358,10 +364,22 @@ function createInvocationsApp(options) {
358
364
  version: AGENTSERVER_INVOCATIONS_PACKAGE_VERSION
359
365
  }
360
366
  ]);
367
+ const observabilityReady = options.configureObservability === false ? Promise.resolve(void 0) : configureAgentServerObservability({ config, logger });
361
368
  const app = express();
362
369
  app.disable("x-powered-by");
363
370
  app.set("trust proxy", trustProxy);
364
371
  app.use(express.raw({ type: "*/*", limit: bodyLimit }));
372
+ app.use(async (_req, _res, next) => {
373
+ try {
374
+ await observabilityReady;
375
+ next();
376
+ } catch (error) {
377
+ logger.warn("Foundry Agent Server observability initialization failed", {
378
+ error: formatError(error)
379
+ });
380
+ next();
381
+ }
382
+ });
365
383
  app.use((req, res, next) => {
366
384
  const requestId = resolveRequestId((name) => req.header(name));
367
385
  req.agentServerRequestId = requestId;
@@ -390,9 +408,6 @@ function createInvocationsApp(options) {
390
408
  app.get("/healthz", (_req, res) => {
391
409
  res.json({ ok: true });
392
410
  });
393
- const readinessHandler = (_req, res) => {
394
- res.json({ ok: true, app: appName });
395
- };
396
411
  app.get("/readiness", readinessHandler);
397
412
  app.get("/readyz", readinessHandler);
398
413
  app.get("/invocations/docs/openapi.json", (_req, res) => {
@@ -419,10 +434,20 @@ function createInvocationsApp(options) {
419
434
  return;
420
435
  }
421
436
  const ctx = buildContextFromRequest(req, res);
437
+ const requestSpan = startInvocationRequestSpan({
438
+ req,
439
+ res,
440
+ config,
441
+ operation: "invoke_agent",
442
+ invocationId: ctx.invocationId,
443
+ sessionId: ctx.sessionId,
444
+ correlationRequestId: ctx.requestId
445
+ });
422
446
  try {
423
- await handler.handle(req, res, ctx);
447
+ await requestSpan.run(() => handler.handle(req, res, ctx));
424
448
  } catch (error) {
425
449
  const message = error instanceof Error ? error.message : String(error);
450
+ requestSpan.recordError(error, "handler_failed");
426
451
  logger.error("Invocation handler threw", {
427
452
  error: formatError(error),
428
453
  invocationId: ctx.invocationId,
@@ -443,35 +468,57 @@ function createInvocationsApp(options) {
443
468
  });
444
469
  app.get("/invocations/:invocationId", async (req, res) => {
445
470
  const handler = handlerProvider();
471
+ const invocationId = req.params.invocationId ?? "";
472
+ const sessionId = typeof req.query.agent_session_id === "string" ? req.query.agent_session_id : void 0;
473
+ const requestSpan = startInvocationRequestSpan({
474
+ req,
475
+ res,
476
+ config,
477
+ operation: "get_invocation",
478
+ invocationId,
479
+ sessionId,
480
+ correlationRequestId: req.agentServerRequestId
481
+ });
482
+ addProtectedHeaders(res, {
483
+ [INVOCATION_ID_HEADER]: invocationId
484
+ });
446
485
  if (!handler.getStatus) {
447
- res.status(404).json(
448
- createErrorBody(
449
- "not_implemented",
450
- "This handler does not expose long-running invocation status."
451
- )
452
- );
486
+ res.status(404).json(createErrorBody("not_found", "get_invocation not implemented"));
453
487
  return;
454
488
  }
455
489
  try {
456
- await handler.getStatus(req.params.invocationId ?? "", req, res);
490
+ await requestSpan.run(() => handler.getStatus?.(invocationId, req, res));
457
491
  } catch (error) {
492
+ requestSpan.recordError(error, "internal_error");
458
493
  handleHandlerError(res, error, logger, "getStatus");
459
494
  }
460
495
  });
461
496
  const cancelHandler = async (req, res) => {
462
497
  const handler = handlerProvider();
498
+ const invocationId = req.params.invocationId;
499
+ const sessionId = typeof req.query.agent_session_id === "string" ? req.query.agent_session_id : void 0;
500
+ const requestSpan = startInvocationRequestSpan({
501
+ req,
502
+ res,
503
+ config,
504
+ operation: "cancel_invocation",
505
+ invocationId,
506
+ sessionId,
507
+ correlationRequestId: req.agentServerRequestId
508
+ });
509
+ addProtectedHeaders(res, {
510
+ [INVOCATION_ID_HEADER]: invocationId
511
+ });
463
512
  if (!handler.cancel) {
464
513
  res.status(404).json(
465
- createErrorBody(
466
- "not_implemented",
467
- "This handler does not expose long-running invocation cancellation."
468
- )
514
+ createErrorBody("not_found", "cancel_invocation not implemented")
469
515
  );
470
516
  return;
471
517
  }
472
518
  try {
473
- await handler.cancel(req.params.invocationId, req, res);
519
+ await requestSpan.run(() => handler.cancel?.(invocationId, req, res));
474
520
  } catch (error) {
521
+ requestSpan.recordError(error, "internal_error");
475
522
  handleHandlerError(res, error, logger, "cancel");
476
523
  }
477
524
  };
@@ -490,12 +537,27 @@ function createInvocationsApp(options) {
490
537
  }
491
538
  async function runInvocationsServer(options) {
492
539
  const app = createInvocationsApp(options);
493
- const port = resolveAgentServerConfig({ port: options.port }).port;
540
+ const config = resolveAgentServerConfig({
541
+ port: options.port,
542
+ gracefulShutdownTimeoutSeconds: options.gracefulShutdownTimeoutSeconds
543
+ });
544
+ const port = config.port;
494
545
  const host = options.host ?? DEFAULT_HOST;
495
546
  const logger = options.logger ?? defaultLogger();
496
547
  const appName = options.appName ?? "foundry-invocations-host";
548
+ const platformServer = buildPlatformServerHeader([
549
+ {
550
+ name: "azure-ai-agentserver-core",
551
+ version: AGENTSERVER_CORE_PACKAGE_VERSION
552
+ },
553
+ {
554
+ name: "azure-ai-agentserver-invocations",
555
+ version: AGENTSERVER_INVOCATIONS_PACKAGE_VERSION
556
+ }
557
+ ]);
497
558
  const server = await new Promise((resolve, reject) => {
498
559
  const httpServer = app.listen(port, host, () => {
560
+ logStartupConfiguration(logger, config, platformServer);
499
561
  logger.info(
500
562
  `${appName} listening on http://${host}:${port}/invocations`
501
563
  );
@@ -508,21 +570,107 @@ async function runInvocationsServer(options) {
508
570
  port,
509
571
  host,
510
572
  async close() {
511
- await new Promise((resolve, reject) => {
512
- server.close((error) => {
513
- if (!error || error.code === "ERR_SERVER_NOT_RUNNING") {
514
- resolve();
515
- return;
516
- }
517
- reject(error);
518
- });
519
- });
573
+ await closeServerWithTimeout(
574
+ server,
575
+ config.gracefulShutdownTimeoutSeconds
576
+ );
520
577
  if (options.handler instanceof InvocationHandler) {
521
578
  await options.handler.close?.();
522
579
  }
523
580
  }
524
581
  };
525
582
  }
583
+ function startInvocationRequestSpan(options) {
584
+ const span = startAgentServerRequestSpan({
585
+ config: options.config,
586
+ headers: (name) => options.req.header(name),
587
+ requestId: options.invocationId,
588
+ operation: options.operation,
589
+ operationName: options.operation,
590
+ instrumentationScope: "Azure.AI.AgentServer.Invocations",
591
+ invocationId: options.invocationId,
592
+ sessionId: options.sessionId,
593
+ correlationRequestId: options.correlationRequestId
594
+ });
595
+ let finished = false;
596
+ options.res.once("finish", () => {
597
+ finished = true;
598
+ span.end();
599
+ });
600
+ options.res.once("close", () => {
601
+ if (finished) {
602
+ span.end();
603
+ return;
604
+ }
605
+ span.end(new Error("HTTP response closed before finish"));
606
+ });
607
+ return span;
608
+ }
609
+ async function closeServerWithTimeout(server, timeoutSeconds) {
610
+ const closePromise = new Promise((resolve, reject) => {
611
+ server.close((error) => {
612
+ if (!error || error.code === "ERR_SERVER_NOT_RUNNING") {
613
+ resolve();
614
+ return;
615
+ }
616
+ reject(error);
617
+ });
618
+ });
619
+ if (timeoutSeconds <= 0) {
620
+ await closePromise;
621
+ return;
622
+ }
623
+ let timeout;
624
+ const timeoutPromise = new Promise((resolve) => {
625
+ timeout = setTimeout(() => {
626
+ server.closeAllConnections?.();
627
+ resolve();
628
+ }, timeoutSeconds * 1e3);
629
+ });
630
+ try {
631
+ await Promise.race([closePromise, timeoutPromise]);
632
+ } finally {
633
+ if (timeout) {
634
+ clearTimeout(timeout);
635
+ }
636
+ }
637
+ }
638
+ function logStartupConfiguration(logger, config, platformServer) {
639
+ logger.info("Foundry Agent Server platform environment", {
640
+ isHosted: config.isHosted,
641
+ agentName: config.agentName || "(not set)",
642
+ agentVersion: config.agentVersion || "(not set)",
643
+ port: config.port,
644
+ sessionId: config.sessionId || "(not set)",
645
+ sseKeepAliveIntervalSeconds: config.sseKeepAliveIntervalSeconds > 0 ? config.sseKeepAliveIntervalSeconds : "disabled"
646
+ });
647
+ logger.info("Foundry Agent Server connectivity", {
648
+ projectEndpoint: maskUri(config.projectEndpoint),
649
+ otlpEndpoint: maskUri(config.otlpEndpoint),
650
+ applicationInsightsConfigured: Boolean(
651
+ config.applicationInsightsConnectionString.trim()
652
+ )
653
+ });
654
+ logger.info("Foundry Agent Server host options", {
655
+ gracefulShutdownTimeoutSeconds: config.gracefulShutdownTimeoutSeconds,
656
+ platformServer
657
+ });
658
+ }
659
+ function readinessHandler(_req, res) {
660
+ res.json({ status: "healthy" });
661
+ }
662
+ function maskUri(uri) {
663
+ const normalized = uri.trim();
664
+ if (!normalized) {
665
+ return "(not set)";
666
+ }
667
+ try {
668
+ const parsed = new URL(normalized);
669
+ return `${parsed.protocol}//${parsed.host}`;
670
+ } catch {
671
+ return "(redacted)";
672
+ }
673
+ }
526
674
  function makeHandlerProvider(input) {
527
675
  if (typeof input === "function") {
528
676
  return input;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cuylabs/agent-foundry-agentserver-invocations",
3
- "version": "4.1.0",
3
+ "version": "4.2.0",
4
4
  "description": "TypeScript Foundry Agent Server Invocations-protocol host (mirrors azure-ai-agentserver-invocations)",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -18,9 +18,10 @@
18
18
  ],
19
19
  "dependencies": {
20
20
  "express": "^5.0.0",
21
- "@cuylabs/agent-foundry-agentserver-core": "^4.1.0"
21
+ "@cuylabs/agent-foundry-agentserver-core": "^4.2.0"
22
22
  },
23
23
  "devDependencies": {
24
+ "@opentelemetry/api": "^1.9.0",
24
25
  "@types/express": "^5.0.0",
25
26
  "@types/node": "^22.0.0",
26
27
  "tsup": "^8.0.0",