@anvia/studio 1.0.1 → 1.0.2

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
@@ -35,7 +35,7 @@ const agent = new Agent({
35
35
  instructions: "Answer support questions clearly.",
36
36
  });
37
37
 
38
- new Studio([agent]).start({
38
+ await new Studio([agent]).serve({
39
39
  port: 4021,
40
40
  });
41
41
  ```
@@ -46,6 +46,25 @@ Then open:
46
46
  http://localhost:4021/ui/playground
47
47
  ```
48
48
 
49
+ ## Graceful shutdown
50
+
51
+ `serve()` handles both `SIGINT` and `SIGTERM`. Studio stops accepting work, aborts active Agent and
52
+ Pipeline runs, waits for their cancellation observers, and then runs `onShutdown`. Use that callback
53
+ to close observability clients or other caller-owned resources:
54
+
55
+ ```ts
56
+ await new Studio([agent]).serve({
57
+ port: 4021,
58
+ shutdownTimeoutMs: 30_000,
59
+ onShutdown: async () => {
60
+ await Promise.all([lens.close(), langfuse.close(), otelSdk.shutdown()]);
61
+ },
62
+ });
63
+ ```
64
+
65
+ `shutdown()` provides the same draining behavior for application-managed lifecycles. `close()`
66
+ remains synchronous for compatibility: it aborts active work but does not wait for cleanup.
67
+
49
68
  ## Multi-Provider Models
50
69
 
51
70
  Studio can expose a shared model catalog and let each agent choose from registered providers:
package/dist/index.d.ts CHANGED
@@ -581,7 +581,7 @@ type StudioSessionStore = StudioMemoryStore & {
581
581
  listSessionLogs?(options: StudioSessionLogListOptions): StudioSessionLogEntry[] | Promise<StudioSessionLogEntry[]>;
582
582
  deleteSession?(id: string): boolean | Promise<boolean>;
583
583
  };
584
- type StudioTraceStatus = "running" | "success" | "suspended" | "error";
584
+ type StudioTraceStatus = "running" | "success" | "suspended" | "error" | "cancelled";
585
585
  type StudioTraceObservationKind = "agent" | "generation" | "tool";
586
586
  type StudioTraceObservation = {
587
587
  id: string;
@@ -848,6 +848,7 @@ type StudioServeOptions = {
848
848
  hostname?: string;
849
849
  log?: boolean;
850
850
  handleSignals?: boolean;
851
+ shutdownTimeoutMs?: number;
851
852
  };
852
853
  type StudioServeLifecycleOptions = Omit<StudioServeOptions, "handleSignals"> & {
853
854
  signal?: AbortSignal;
@@ -1011,7 +1012,7 @@ type AgentRunRequest = (AgentRunRequestBase & {
1011
1012
  });
1012
1013
  type AgentRunResponse = Exclude<AgentOutcome, AgentInteractionOutcome> | Omit<AgentInteractionOutcome, "continuation" | "messages">;
1013
1014
  type AgentRunStreamEvent = AgentStreamEvent | StudioSessionLogEvent | StudioPipelineLogEvent | StudioPipelineFinalEvent;
1014
- type StudioErrorCode = "bad_request" | "conflict" | "forbidden" | "not_found" | "payload_too_large" | "unsupported_capability" | "internal_error";
1015
+ type StudioErrorCode = "bad_request" | "conflict" | "forbidden" | "not_found" | "payload_too_large" | "service_unavailable" | "unsupported_capability" | "internal_error";
1015
1016
  type StudioErrorResponse = {
1016
1017
  error: {
1017
1018
  code: StudioErrorCode;
@@ -1025,6 +1026,9 @@ type AnviaStudio = {
1025
1026
  fetch(request: Request): Response | Promise<Response>;
1026
1027
  config(): StudioConfig;
1027
1028
  close(): void;
1029
+ shutdown(options?: {
1030
+ timeoutMs?: number;
1031
+ }): Promise<void>;
1028
1032
  };
1029
1033
 
1030
1034
  type StudioTraceObserverOptions = {
@@ -1042,7 +1046,8 @@ declare class Studio implements AnviaStudio {
1042
1046
  private studio;
1043
1047
  private server;
1044
1048
  private websocketServer;
1045
- private sigintHandler;
1049
+ private signalHandlers;
1050
+ private shutdownPromise;
1046
1051
  constructor(targets?: StudioTarget[], options?: StudioOptions);
1047
1052
  get app(): Hono;
1048
1053
  fetch(request: Request): Response | Promise<Response>;
@@ -1051,6 +1056,12 @@ declare class Studio implements AnviaStudio {
1051
1056
  start(serveOptions?: StudioServeOptions): this;
1052
1057
  serve(serveOptions?: StudioServeLifecycleOptions): Promise<void>;
1053
1058
  close(): void;
1059
+ shutdown(options?: {
1060
+ timeoutMs?: number;
1061
+ }): Promise<void>;
1062
+ private shutdownResources;
1063
+ private removeSignalHandlers;
1064
+ private closeNetwork;
1054
1065
  private logAddress;
1055
1066
  }
1056
1067
 
package/dist/index.js CHANGED
@@ -5,6 +5,9 @@ import { serve } from "@hono/node-server";
5
5
  import { Hono as HonoApp } from "hono";
6
6
  import { WebSocketServer } from "ws";
7
7
 
8
+ // src/traces/trace-observer.ts
9
+ import { AgentRunCancelledError } from "@anvia/core/agent";
10
+
8
11
  // src/runtime/json.ts
9
12
  function toJsonValue(value) {
10
13
  return toJsonValueInternal(value, /* @__PURE__ */ new WeakSet());
@@ -213,7 +216,7 @@ var StudioRunTraceObserver = class {
213
216
  });
214
217
  }
215
218
  async error(args) {
216
- await this.save("error", {
219
+ await this.save(args.status === "cancelled" ? "cancelled" : "error", {
217
220
  endedAt: /* @__PURE__ */ new Date(),
218
221
  error: serializeUnknown(args.error),
219
222
  usage: args.usage,
@@ -355,11 +358,12 @@ var ChildAgentToolTraceAccumulator = class {
355
358
  if (child.type === "error") {
356
359
  const metadata = this.childMetadata(agentId, agentName, childTurn);
357
360
  if (isRecord(child.usage)) metadata.usage = toJsonValue(child.usage);
361
+ const status = child.error instanceof AgentRunCancelledError ? "cancelled" : "error";
358
362
  this.completedObservations.push(
359
363
  traceObservation({
360
364
  kind: "tool",
361
365
  name: `${agentLabel(agentId, agentName)}.error`,
362
- status: "error",
366
+ status,
363
367
  turn: this.parent.turn,
364
368
  startedAt: /* @__PURE__ */ new Date(),
365
369
  error: serializeUnknown(child.error),
@@ -385,7 +389,7 @@ var ChildAgentToolTraceAccumulator = class {
385
389
  parentObservationId,
386
390
  kind: "agent",
387
391
  name: `${agentLabel(agentStart.agentId, agentStart.agentName)}.run`,
388
- status: agentChildren.some((observation) => observation.status === "error") ? "error" : "success",
392
+ status: agentChildren.some((observation) => observation.status === "error") ? "error" : agentChildren.some((observation) => observation.status === "cancelled") ? "cancelled" : "success",
389
393
  turn: this.parent.turn,
390
394
  startedAt,
391
395
  endedAt,
@@ -3089,16 +3093,30 @@ function registerAgentRunRoute(app, props) {
3089
3093
  app.post("/agents/:agentId/runs", (c) => handleAgentRun(c, props));
3090
3094
  }
3091
3095
  async function handleAgentRun(c, props) {
3092
- const prepared = await prepareAgentRun(c, props);
3093
- if (prepared instanceof Response) {
3094
- return prepared;
3096
+ const lease = props.runLifecycle.start(c.req.raw.signal);
3097
+ if (lease === void 0) {
3098
+ return errorResponse(c, 503, "service_unavailable", "Anvia Studio is shutting down");
3095
3099
  }
3096
- if (prepared.body.stream === true) {
3097
- return handleStreamingAgentRun(c, prepared, props);
3100
+ try {
3101
+ const prepared = await prepareAgentRun(c, props, lease.abortSignal);
3102
+ if (prepared instanceof Response) {
3103
+ lease.finish();
3104
+ return prepared;
3105
+ }
3106
+ if (prepared.body.stream === true) {
3107
+ return handleStreamingAgentRun(c, prepared, props, lease);
3108
+ }
3109
+ try {
3110
+ return await handleBufferedAgentRun(c, prepared, props);
3111
+ } finally {
3112
+ lease.finish();
3113
+ }
3114
+ } catch (error) {
3115
+ lease.finish();
3116
+ throw error;
3098
3117
  }
3099
- return handleBufferedAgentRun(c, prepared, props);
3100
3118
  }
3101
- async function prepareAgentRun(c, props) {
3119
+ async function prepareAgentRun(c, props, abortSignal) {
3102
3120
  const agentId = c.req.param("agentId");
3103
3121
  const agent = props.agentMap.get(agentId);
3104
3122
  if (agent === void 0) {
@@ -3132,7 +3150,7 @@ async function prepareAgentRun(c, props) {
3132
3150
  if (body.stream !== void 0) resumedBody = { ...resumedBody, stream: body.stream };
3133
3151
  if (body.metadata !== void 0) resumedBody = { ...resumedBody, metadata: body.metadata };
3134
3152
  if (body.trace !== void 0) resumedBody = { ...resumedBody, trace: body.trace };
3135
- const runOptions = createRunOptions(resumedBody, agentId, source.session, c.req.raw.signal);
3153
+ const runOptions = createRunOptions(resumedBody, agentId, source.session, abortSignal);
3136
3154
  const execution2 = {
3137
3155
  generate: (options) => source.runAgent.generate({
3138
3156
  continuation: claimed.continuation,
@@ -3211,7 +3229,7 @@ async function prepareAgentRun(c, props) {
3211
3229
  execution,
3212
3230
  failureMessages: void 0,
3213
3231
  memoryCompactionLogged: false,
3214
- options: createRunOptions(body, agentId, session, c.req.raw.signal),
3232
+ options: createRunOptions(body, agentId, session, abortSignal),
3215
3233
  runAgent,
3216
3234
  runId,
3217
3235
  runStartedAt,
@@ -3339,7 +3357,7 @@ function createRunOptions(body, agentId, session, abortSignal) {
3339
3357
  }
3340
3358
  return options;
3341
3359
  }
3342
- function handleStreamingAgentRun(c, run, props) {
3360
+ function handleStreamingAgentRun(c, run, props, lease) {
3343
3361
  const streamOptions = withInternalAgentRunOptions({ ...run.options }, { runId: run.runId });
3344
3362
  const runStream = registerStreamingContinuation(
3345
3363
  run.execution.stream(streamOptions),
@@ -3367,7 +3385,7 @@ function handleStreamingAgentRun(c, run, props) {
3367
3385
  });
3368
3386
  stream = persistedRun.events;
3369
3387
  }
3370
- return streamAgentRunEvents(c, stream, {
3388
+ return streamAgentRunEvents(c, finishRunLease(stream, lease), {
3371
3389
  runId: run.runId,
3372
3390
  onCancel: async () => {
3373
3391
  const persistence = [];
@@ -3386,6 +3404,13 @@ function handleStreamingAgentRun(c, run, props) {
3386
3404
  }
3387
3405
  });
3388
3406
  }
3407
+ async function* finishRunLease(events, lease) {
3408
+ try {
3409
+ yield* events;
3410
+ } finally {
3411
+ lease.finish();
3412
+ }
3413
+ }
3389
3414
  async function handleBufferedAgentRun(c, run, props) {
3390
3415
  try {
3391
3416
  const runtimeOptions = await startBufferedSessionRun(run);
@@ -3680,7 +3705,7 @@ function parseTraceStatus(value) {
3680
3705
  if (status === void 0) {
3681
3706
  return void 0;
3682
3707
  }
3683
- return status === "running" || status === "success" || status === "suspended" || status === "error" ? status : false;
3708
+ return status === "running" || status === "success" || status === "suspended" || status === "error" || status === "cancelled" ? status : false;
3684
3709
  }
3685
3710
  function parseAfter(value) {
3686
3711
  if (value === void 0 || value.trim().length === 0) {
@@ -4997,7 +5022,7 @@ function registerPipelineRoutes(app, props) {
4997
5022
  if ("error" in body) {
4998
5023
  return body.error;
4999
5024
  }
5000
- return executePipelineRun(c, props, pipeline, body);
5025
+ return executeTrackedPipelineRun(c, props, pipeline, body);
5001
5026
  });
5002
5027
  app.post("/pipelines/:pipelineId/runs/:runId/replay", async (c) => {
5003
5028
  const pipeline = props.pipelineMap.get(c.req.param("pipelineId"));
@@ -5033,10 +5058,22 @@ function registerPipelineRoutes(app, props) {
5033
5058
  metadata: replayMetadata(sourceRun.metadata, body.metadata, sourceRun.runId)
5034
5059
  };
5035
5060
  if (body.stream !== void 0) replayRequest.stream = body.stream;
5036
- return executePipelineRun(c, props, pipeline, replayRequest);
5061
+ return executeTrackedPipelineRun(c, props, pipeline, replayRequest);
5037
5062
  });
5038
5063
  }
5039
- async function executePipelineRun(c, props, pipeline, body) {
5064
+ async function executeTrackedPipelineRun(c, props, pipeline, body) {
5065
+ const lease = props.runLifecycle.start(c.req.raw.signal);
5066
+ if (lease === void 0) {
5067
+ return errorResponse(c, 503, "service_unavailable", "Anvia Studio is shutting down");
5068
+ }
5069
+ try {
5070
+ return await executePipelineRun(c, props, pipeline, body, lease);
5071
+ } catch (error) {
5072
+ lease.finish();
5073
+ throw error;
5074
+ }
5075
+ }
5076
+ async function executePipelineRun(c, props, pipeline, body, lease) {
5040
5077
  const runId = globalThis.crypto.randomUUID();
5041
5078
  const startedAt = Date.now();
5042
5079
  const startedAtIso = new Date(startedAt).toISOString();
@@ -5063,12 +5100,13 @@ async function executePipelineRun(c, props, pipeline, body) {
5063
5100
  runId,
5064
5101
  input: body.input,
5065
5102
  startedAt,
5066
- startedAtIso
5103
+ startedAtIso,
5104
+ onFinish: lease.finish
5067
5105
  };
5068
5106
  if (body.metadata !== void 0) streamOptions.metadata = body.metadata;
5069
5107
  if (props.logStore !== void 0) streamOptions.logStore = props.logStore;
5070
5108
  if (props.runStore !== void 0) streamOptions.runStore = props.runStore;
5071
- streamOptions.abortSignal = c.req.raw.signal;
5109
+ streamOptions.abortSignal = lease.abortSignal;
5072
5110
  return streamPipelineRun(c, streamOptions);
5073
5111
  }
5074
5112
  try {
@@ -5077,7 +5115,7 @@ async function executePipelineRun(c, props, pipeline, body) {
5077
5115
  input: body.input,
5078
5116
  runId,
5079
5117
  metadata: body.metadata,
5080
- abortSignal: c.req.raw.signal,
5118
+ abortSignal: lease.abortSignal,
5081
5119
  observer: {
5082
5120
  async onEvent(event) {
5083
5121
  await appendPipelineLog(props.logStore, pipelineStageLog(event, pipeline.id));
@@ -5134,6 +5172,8 @@ async function executePipelineRun(c, props, pipeline, body) {
5134
5172
  pipelineRunFailedLog(pipeline.id, runId, error, startedAt)
5135
5173
  );
5136
5174
  return errorResponse(c, 500, "internal_error", "Pipeline run failed", serializeError(error));
5175
+ } finally {
5176
+ lease.finish();
5137
5177
  }
5138
5178
  }
5139
5179
  function pipelineDetail(pipeline) {
@@ -5232,7 +5272,11 @@ async function* pipelineRunEvents(props) {
5232
5272
  yield next.value;
5233
5273
  }
5234
5274
  } finally {
5235
- await run;
5275
+ try {
5276
+ await run;
5277
+ } finally {
5278
+ props.onFinish();
5279
+ }
5236
5280
  }
5237
5281
  }
5238
5282
  async function savePipelineRun(store, input) {
@@ -5320,6 +5364,61 @@ function parsePipelineLogAfter(value) {
5320
5364
  return after;
5321
5365
  }
5322
5366
 
5367
+ // src/runtime/run-lifecycle.ts
5368
+ var StudioRunLifecycle = class {
5369
+ shutdownController = new AbortController();
5370
+ active = /* @__PURE__ */ new Set();
5371
+ drainPromise;
5372
+ resolveDrain;
5373
+ closed = false;
5374
+ start(requestSignal) {
5375
+ if (this.closed) return void 0;
5376
+ const token = /* @__PURE__ */ Symbol("studio-run");
5377
+ this.active.add(token);
5378
+ let finished = false;
5379
+ return {
5380
+ abortSignal: AbortSignal.any([requestSignal, this.shutdownController.signal]),
5381
+ finish: () => {
5382
+ if (finished) return;
5383
+ finished = true;
5384
+ this.active.delete(token);
5385
+ if (this.active.size === 0) {
5386
+ this.resolveDrain?.();
5387
+ this.resolveDrain = void 0;
5388
+ this.drainPromise = void 0;
5389
+ }
5390
+ }
5391
+ };
5392
+ }
5393
+ close() {
5394
+ if (this.closed) return;
5395
+ this.closed = true;
5396
+ this.shutdownController.abort(new Error("Anvia Studio is shutting down."));
5397
+ }
5398
+ async drain(timeoutMs) {
5399
+ this.close();
5400
+ if (this.active.size === 0) return;
5401
+ this.drainPromise ??= new Promise((resolve2) => {
5402
+ this.resolveDrain = resolve2;
5403
+ });
5404
+ await withTimeout(this.drainPromise, timeoutMs);
5405
+ }
5406
+ };
5407
+ async function withTimeout(promise, timeoutMs) {
5408
+ let timer;
5409
+ const timeout = new Promise((_resolve, reject) => {
5410
+ timer = setTimeout(
5411
+ () => reject(new Error(`Anvia Studio shutdown timed out after ${timeoutMs}ms.`)),
5412
+ timeoutMs
5413
+ );
5414
+ });
5415
+ try {
5416
+ await Promise.race([promise, timeout]);
5417
+ } finally {
5418
+ if (timer !== void 0) clearTimeout(timer);
5419
+ }
5420
+ }
5421
+
5323
5422
  // src/runtime/sandbox-views.ts
5324
5423
  import { upgradeWebSocket } from "@hono/node-server";
5325
5424
  import { getConnInfo } from "@hono/node-server/conninfo";
@@ -6810,7 +6909,8 @@ var Studio = class {
6810
6909
  studio;
6811
6910
  server;
6812
6911
  websocketServer;
6813
- sigintHandler;
6912
+ signalHandlers;
6913
+ shutdownPromise;
6814
6914
  constructor(targets = [], options = {}) {
6815
6915
  this.options = studioOptionsFromTargets(targets, options);
6816
6916
  this.studio = createStudioApp(this.options);
@@ -6832,6 +6932,7 @@ var Studio = class {
6832
6932
  start(serveOptions = {}) {
6833
6933
  this.close();
6834
6934
  this.studio = createStudioApp(this.options);
6935
+ this.shutdownPromise = void 0;
6835
6936
  const port = serveOptions.port ?? Number(process.env.RUNNER_PORT ?? 4021);
6836
6937
  const serverOptions = {
6837
6938
  fetch: (request, env) => this.studio.app.fetch(request, env),
@@ -6851,16 +6952,27 @@ var Studio = class {
6851
6952
  this.logAddress(host, port);
6852
6953
  }
6853
6954
  if (serveOptions.handleSignals ?? true) {
6854
- this.sigintHandler = () => {
6855
- this.close();
6856
- process.exit(0);
6955
+ const shutdownFor = (signal) => {
6956
+ process.exitCode = signal === "SIGINT" ? 130 : 143;
6957
+ void this.shutdown(shutdownOptions(serveOptions.shutdownTimeoutMs)).catch(
6958
+ (error) => {
6959
+ console.error(error);
6960
+ process.exitCode = 1;
6961
+ }
6962
+ );
6963
+ };
6964
+ this.signalHandlers = {
6965
+ sigint: () => shutdownFor("SIGINT"),
6966
+ sigterm: () => shutdownFor("SIGTERM")
6857
6967
  };
6858
- process.once("SIGINT", this.sigintHandler);
6968
+ process.once("SIGINT", this.signalHandlers.sigint);
6969
+ process.once("SIGTERM", this.signalHandlers.sigterm);
6859
6970
  }
6860
6971
  return this;
6861
6972
  }
6862
6973
  async serve(serveOptions = {}) {
6863
- const { onShutdown, signal, ...startOptions } = serveOptions;
6974
+ const { onShutdown, shutdownTimeoutMs, signal, ...startOptions } = serveOptions;
6975
+ const failures = [];
6864
6976
  try {
6865
6977
  this.start({ ...startOptions, log: false, handleSignals: false });
6866
6978
  const server = this.server;
@@ -6875,21 +6987,55 @@ var Studio = class {
6875
6987
  this.logAddress(startOptions.hostname ?? "localhost", port);
6876
6988
  }
6877
6989
  await waitForShutdown(signal);
6878
- } finally {
6879
- this.close();
6990
+ } catch (error) {
6991
+ failures.push(error);
6992
+ }
6993
+ try {
6994
+ await this.shutdown(shutdownOptions(shutdownTimeoutMs));
6995
+ } catch (error) {
6996
+ failures.push(error);
6997
+ }
6998
+ try {
6880
6999
  await onShutdown?.();
7000
+ } catch (error) {
7001
+ failures.push(error);
7002
+ }
7003
+ if (failures.length === 1) throw failures[0];
7004
+ if (failures.length > 1) {
7005
+ throw new AggregateError(failures, "Failed to run and shut down Anvia Studio.");
6881
7006
  }
6882
7007
  }
6883
7008
  close() {
6884
- if (this.sigintHandler !== void 0) {
6885
- process.off("SIGINT", this.sigintHandler);
6886
- this.sigintHandler = void 0;
7009
+ this.removeSignalHandlers();
7010
+ this.closeNetwork();
7011
+ this.studio.close();
7012
+ }
7013
+ shutdown(options = {}) {
7014
+ const timeoutMs = options.timeoutMs ?? 3e4;
7015
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
7016
+ return Promise.reject(
7017
+ new TypeError("Anvia Studio shutdown timeoutMs must be a positive number.")
7018
+ );
6887
7019
  }
7020
+ this.shutdownPromise ??= this.shutdownResources(timeoutMs);
7021
+ return this.shutdownPromise;
7022
+ }
7023
+ async shutdownResources(timeoutMs) {
7024
+ this.removeSignalHandlers();
7025
+ this.closeNetwork();
7026
+ await this.studio.shutdown({ timeoutMs });
7027
+ }
7028
+ removeSignalHandlers() {
7029
+ if (this.signalHandlers === void 0) return;
7030
+ process.off("SIGINT", this.signalHandlers.sigint);
7031
+ process.off("SIGTERM", this.signalHandlers.sigterm);
7032
+ this.signalHandlers = void 0;
7033
+ }
7034
+ closeNetwork() {
6888
7035
  this.server?.close();
6889
7036
  this.server = void 0;
6890
7037
  this.websocketServer?.close();
6891
7038
  this.websocketServer = void 0;
6892
- this.studio.close();
6893
7039
  }
6894
7040
  logAddress(host, port) {
6895
7041
  if (isStudioUiEnabled(this.options.ui)) {
@@ -6900,6 +7046,9 @@ var Studio = class {
6900
7046
  }
6901
7047
  }
6902
7048
  };
7049
+ function shutdownOptions(timeoutMs) {
7050
+ return timeoutMs === void 0 ? {} : { timeoutMs };
7051
+ }
6903
7052
  function waitForServerListening(server) {
6904
7053
  if (server.listening) return Promise.resolve();
6905
7054
  return new Promise((resolve2, reject) => {
@@ -7045,6 +7194,7 @@ function createStudioApp(options) {
7045
7194
  const pipelineMap = new Map(pipelines.map((pipeline) => [pipeline.id, pipeline]));
7046
7195
  const evalMap = new Map(options.evals.map((suite) => [suite.id ?? suite.name, suite]));
7047
7196
  const continuationRegistry = createStudioContinuationRegistry();
7197
+ const runLifecycle = new StudioRunLifecycle();
7048
7198
  const sandboxRegistry = createStudioSandboxRegistry(agents, options.sandboxes ?? []);
7049
7199
  const memorySources = createStudioMemorySourceRegistry(agents, stores.sessions);
7050
7200
  const app = new HonoApp();
@@ -7107,7 +7257,8 @@ function createStudioApp(options) {
7107
7257
  registerKnowledgeRoutes(app, knowledgeOptions);
7108
7258
  const pipelineOptions = {
7109
7259
  pipelines,
7110
- pipelineMap
7260
+ pipelineMap,
7261
+ runLifecycle
7111
7262
  };
7112
7263
  if (stores.pipelineLogs !== void 0) pipelineOptions.logStore = stores.pipelineLogs;
7113
7264
  if (stores.pipelineRuns !== void 0) pipelineOptions.runStore = stores.pipelineRuns;
@@ -7116,7 +7267,8 @@ function createStudioApp(options) {
7116
7267
  agentMap,
7117
7268
  stores,
7118
7269
  modelRegistry,
7119
- continuationRegistry
7270
+ continuationRegistry,
7271
+ runLifecycle
7120
7272
  });
7121
7273
  if (memorySources.size > 0 || stores.sessions !== void 0) {
7122
7274
  registerMemoryRoutes(app, {
@@ -7139,6 +7291,14 @@ function createStudioApp(options) {
7139
7291
  app.all(`/${capability}`, (c) => unsupportedCapability(c, capability));
7140
7292
  app.all(`/${capability}/*`, (c) => unsupportedCapability(c, capability));
7141
7293
  }
7294
+ let closing = false;
7295
+ const beginClose = () => {
7296
+ if (closing) return;
7297
+ closing = true;
7298
+ continuationRegistry.clear();
7299
+ closeSandboxViews();
7300
+ runLifecycle.close();
7301
+ };
7142
7302
  const studio = {
7143
7303
  app,
7144
7304
  fetch(request) {
@@ -7148,8 +7308,11 @@ function createStudioApp(options) {
7148
7308
  return buildConfig(options, agents, pipelines, stores, sandboxRegistry.size);
7149
7309
  },
7150
7310
  close() {
7151
- continuationRegistry.clear();
7152
- closeSandboxViews();
7311
+ beginClose();
7312
+ },
7313
+ async shutdown(shutdownOptions2 = {}) {
7314
+ beginClose();
7315
+ await runLifecycle.drain(shutdownOptions2.timeoutMs ?? 3e4);
7153
7316
  }
7154
7317
  };
7155
7318
  if (stores.sessions !== void 0) Object.assign(studio, { sessionStore: stores.sessions });