@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 +20 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +175 -27
- package/package.json +3 -2
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(
|
|
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(
|
|
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
|
|
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
|
|
512
|
-
server
|
|
513
|
-
|
|
514
|
-
|
|
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.
|
|
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.
|
|
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",
|