@anvia/studio 0.7.32 → 0.7.34

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.
Files changed (45) hide show
  1. package/dist/index.d.ts +78 -6
  2. package/dist/index.js +860 -77
  3. package/dist/index.js.map +1 -1
  4. package/dist/ui/assets/evals-page-CHxft1PA.js +2 -0
  5. package/dist/ui/assets/{evals-page-DN6aZ9dT.js.map → evals-page-CHxft1PA.js.map} +1 -1
  6. package/dist/ui/assets/{format-C9HPGfkW.js → format-D43vTqrO.js} +2 -2
  7. package/dist/ui/assets/format-D43vTqrO.js.map +1 -0
  8. package/dist/ui/assets/index-DMB9ju7U.css +1 -0
  9. package/dist/ui/assets/index-DgQb2APn.js +55 -0
  10. package/dist/ui/assets/index-DgQb2APn.js.map +1 -0
  11. package/dist/ui/assets/{knowledge-page-DYwhR8Ly.js → knowledge-page-BIphUowG.js} +2 -2
  12. package/dist/ui/assets/{knowledge-page-DYwhR8Ly.js.map → knowledge-page-BIphUowG.js.map} +1 -1
  13. package/dist/ui/assets/{mcps-page-B3e3YF6D.js → mcps-page-CEyvZfpe.js} +2 -2
  14. package/dist/ui/assets/{mcps-page-B3e3YF6D.js.map → mcps-page-CEyvZfpe.js.map} +1 -1
  15. package/dist/ui/assets/{memory-page-BeR1NNHY.js → memory-page-C2Nph26O.js} +2 -2
  16. package/dist/ui/assets/{memory-page-BeR1NNHY.js.map → memory-page-C2Nph26O.js.map} +1 -1
  17. package/dist/ui/assets/{pipelines-page-DdYD59h4.js → pipelines-page-BllEnKpu.js} +3 -3
  18. package/dist/ui/assets/{pipelines-page-DdYD59h4.js.map → pipelines-page-BllEnKpu.js.map} +1 -1
  19. package/dist/ui/assets/{renderers-m-gqGocU.js → renderers-Dzjh-nGh.js} +2 -2
  20. package/dist/ui/assets/{renderers-m-gqGocU.js.map → renderers-Dzjh-nGh.js.map} +1 -1
  21. package/dist/ui/assets/sandboxes-page-B8GcUS5f.js +2 -0
  22. package/dist/ui/assets/sandboxes-page-B8GcUS5f.js.map +1 -0
  23. package/dist/ui/assets/{sessions-page-SAmGnFSU.js → sessions-page-BY2ysr2u.js} +2 -2
  24. package/dist/ui/assets/{sessions-page-SAmGnFSU.js.map → sessions-page-BY2ysr2u.js.map} +1 -1
  25. package/dist/ui/assets/{status-page-g__90tnX.js → status-page-CZ0HBJY5.js} +2 -2
  26. package/dist/ui/assets/{status-page-g__90tnX.js.map → status-page-CZ0HBJY5.js.map} +1 -1
  27. package/dist/ui/assets/tool-runner-CkQ3WBw4.js +2 -0
  28. package/dist/ui/assets/{tool-runner-CcqIP2r3.js.map → tool-runner-CkQ3WBw4.js.map} +1 -1
  29. package/dist/ui/assets/{tools-page-By0mx0ua.js → tools-page-CmVAorUe.js} +2 -2
  30. package/dist/ui/assets/{tools-page-By0mx0ua.js.map → tools-page-CmVAorUe.js.map} +1 -1
  31. package/dist/ui/assets/trace-browser-DekV4BNP.js +10 -0
  32. package/dist/ui/assets/{trace-browser-BDet5qes.js.map → trace-browser-DekV4BNP.js.map} +1 -1
  33. package/dist/ui/assets/transcript-item-CQCD2CY3.js +3 -0
  34. package/dist/ui/assets/transcript-item-CQCD2CY3.js.map +1 -0
  35. package/dist/ui/index.html +3 -3
  36. package/package.json +4 -4
  37. package/dist/ui/assets/evals-page-DN6aZ9dT.js +0 -2
  38. package/dist/ui/assets/format-C9HPGfkW.js.map +0 -1
  39. package/dist/ui/assets/index-D7w3LxHM.js +0 -55
  40. package/dist/ui/assets/index-D7w3LxHM.js.map +0 -1
  41. package/dist/ui/assets/index-Dl647xDC.css +0 -1
  42. package/dist/ui/assets/tool-runner-CcqIP2r3.js +0 -2
  43. package/dist/ui/assets/trace-browser-BDet5qes.js +0 -10
  44. package/dist/ui/assets/transcript-item-3F08pehk.js +0 -3
  45. package/dist/ui/assets/transcript-item-3F08pehk.js.map +0 -1
package/dist/index.js CHANGED
@@ -634,6 +634,11 @@ function registerStudioUi(app, options) {
634
634
  app.get(`${options.path}/tools`, (c) => redirectWithQuery(c, "/tools"));
635
635
  app.get(`${options.path}/mcps`, (c) => redirectWithQuery(c, "/mcps"));
636
636
  app.get(`${options.path}/pipelines`, (c) => redirectWithQuery(c, "/pipelines"));
637
+ app.get(`${options.path}/sandboxes`, (c) => redirectWithQuery(c, "/sandboxes"));
638
+ app.get(
639
+ `${options.path}/sandboxes/:sandboxRef`,
640
+ (c) => redirectWithQuery(c, `/sandboxes/${encodeURIComponent(c.req.param("sandboxRef"))}`)
641
+ );
637
642
  app.get(`${options.path}/evals`, (c) => redirectWithQuery(c, "/evals"));
638
643
  app.get(`${options.path}/memory`, (c) => redirectWithQuery(c, "/memory"));
639
644
  app.get(`${options.path}/status`, (c) => redirectWithQuery(c, "/status"));
@@ -654,6 +659,8 @@ function registerStudioUi(app, options) {
654
659
  app.get("/tools", async (c) => c.html(await renderShell()));
655
660
  app.get("/mcps", async (c) => c.html(await renderShell()));
656
661
  app.get("/pipelines", shellWhenHtml(renderShell));
662
+ app.get("/sandboxes", shellWhenHtml(renderShell));
663
+ app.get("/sandboxes/:sandboxRef", shellWhenHtml(renderShell));
657
664
  app.get("/evals", shellWhenHtml(renderShell));
658
665
  app.get("/memory", async (c) => c.html(await renderShell()));
659
666
  app.get("/status", shellWhenHtml(renderShell));
@@ -764,8 +771,8 @@ function renderLegacyStudioUiShell(props) {
764
771
  "</html>"
765
772
  ].join("");
766
773
  }
767
- function normalizeUiPath(path) {
768
- const trimmed = path.trim();
774
+ function normalizeUiPath(path2) {
775
+ const trimmed = path2.trim();
769
776
  const withSlash = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
770
777
  const withoutTrailingSlash = withSlash.length > 1 ? withSlash.replace(/\/+$/, "") : withSlash;
771
778
  if (withoutTrailingSlash.length === 0 || withoutTrailingSlash === "/") {
@@ -1808,8 +1815,69 @@ async function* mergeRunAndApprovalEvents(runEvents, approvalEvents) {
1808
1815
  approvalEvents.close();
1809
1816
  }
1810
1817
  }
1811
- function streamAgentRunEvents(_c, events) {
1812
- return streamStudioJsonl(events);
1818
+ function streamAgentRunEvents(_c, events, options = {}) {
1819
+ return streamStudioJsonl(withAgentRunCancellation(events, options.onCancel));
1820
+ }
1821
+ function withAgentRunCancellation(events, onCancel) {
1822
+ const iterator = events[Symbol.asyncIterator]();
1823
+ let done = false;
1824
+ let terminal = false;
1825
+ let cancellationHandled = false;
1826
+ const handleCancellation = async () => {
1827
+ if (cancellationHandled) {
1828
+ return;
1829
+ }
1830
+ cancellationHandled = true;
1831
+ await onCancel?.();
1832
+ };
1833
+ return {
1834
+ [Symbol.asyncIterator]() {
1835
+ return {
1836
+ async next() {
1837
+ if (done) {
1838
+ return { done: true, value: void 0 };
1839
+ }
1840
+ try {
1841
+ const next = await iterator.next();
1842
+ if (next.done === true) {
1843
+ done = true;
1844
+ if (!terminal) {
1845
+ await handleCancellation();
1846
+ }
1847
+ return next;
1848
+ }
1849
+ if (next.value.type === "final" || next.value.type === "error") {
1850
+ terminal = true;
1851
+ }
1852
+ return next;
1853
+ } catch (error) {
1854
+ done = true;
1855
+ terminal = true;
1856
+ throw error;
1857
+ }
1858
+ },
1859
+ async return() {
1860
+ if (done) {
1861
+ return { done: true, value: void 0 };
1862
+ }
1863
+ done = true;
1864
+ let closeError;
1865
+ const closePromise = iterator.return?.().catch((error) => {
1866
+ closeError = error;
1867
+ return { done: true, value: void 0 };
1868
+ });
1869
+ if (!terminal) {
1870
+ await handleCancellation();
1871
+ }
1872
+ await closePromise;
1873
+ if (closeError !== void 0) {
1874
+ throw closeError;
1875
+ }
1876
+ return { done: true, value: void 0 };
1877
+ }
1878
+ };
1879
+ }
1880
+ };
1813
1881
  }
1814
1882
  function traceForRun(trace, agentId, session) {
1815
1883
  const options = {};
@@ -1822,53 +1890,144 @@ function traceForRun(trace, agentId, session) {
1822
1890
  else if (session !== void 0) options.sessionId = session.id;
1823
1891
  return options;
1824
1892
  }
1825
- async function* persistStreamingSessionTranscript(props) {
1893
+ function createPersistedStreamingSessionTranscript(props) {
1826
1894
  const transcript = [messageToTranscriptEntry(props.message, 0)];
1827
1895
  const title = optionalTitle(props.message);
1828
- await props.store.saveSessionRunTranscript({
1829
- id: props.session.id,
1830
- runId: props.runId,
1831
- ...title,
1832
- transcript,
1833
- status: "running"
1834
- });
1835
- try {
1836
- for await (const event of props.stream) {
1837
- acceptTranscriptStreamEvent(transcript, event);
1838
- const saveInput = {
1839
- id: props.session.id,
1840
- runId: props.runId,
1841
- ...title,
1842
- transcript,
1843
- status: event.type === "final" ? "success" : event.type === "error" ? "error" : "running"
1844
- };
1845
- if (event.type === "error") saveInput.error = serializeError(event.error);
1846
- const nextSession = await props.store.saveSessionRunTranscript(saveInput);
1896
+ let status = "running";
1897
+ let saveTail = Promise.resolve();
1898
+ const isCancelled = () => status === "cancelled";
1899
+ const saveTranscript = (input) => {
1900
+ const operation = saveTail.then(async () => {
1901
+ const nextSession = await props.store.saveSessionRunTranscript(input);
1847
1902
  if (nextSession === void 0) {
1848
1903
  throw new Error("Session not found");
1849
1904
  }
1850
- const generatedMessages = event.type === "final" ? event.messages.slice(1) : [];
1851
- if (props.persistGeneratedMessages === true && generatedMessages.length > 0) {
1852
- await props.store.append({
1853
- context: { sessionId: props.session.id },
1854
- runId: props.runId,
1855
- turn: 1,
1856
- messages: generatedMessages
1857
- });
1858
- }
1859
- yield event;
1860
- }
1861
- } catch (error) {
1862
- appendTranscriptAssistantError(transcript, errorText(error));
1863
- await props.store.saveSessionRunTranscript({
1905
+ });
1906
+ saveTail = operation.catch(() => void 0);
1907
+ return operation;
1908
+ };
1909
+ const events = (async function* () {
1910
+ await saveTranscript({
1864
1911
  id: props.session.id,
1865
1912
  runId: props.runId,
1866
1913
  ...title,
1867
1914
  transcript,
1868
- status: "error",
1869
- error: serializeError(error)
1915
+ status: "running"
1870
1916
  });
1871
- throw error;
1917
+ if (isCancelled()) {
1918
+ return;
1919
+ }
1920
+ try {
1921
+ for await (const event of props.stream) {
1922
+ if (isCancelled()) {
1923
+ return;
1924
+ }
1925
+ acceptTranscriptStreamEvent(transcript, event);
1926
+ if (event.type === "final" || event.type === "error") {
1927
+ assignTranscriptRunDuration(transcript, Date.now() - props.startedAt);
1928
+ }
1929
+ const eventStatus = event.type === "final" ? "success" : event.type === "error" ? "error" : "running";
1930
+ const saveInput = {
1931
+ id: props.session.id,
1932
+ runId: props.runId,
1933
+ ...title,
1934
+ transcript,
1935
+ status: eventStatus
1936
+ };
1937
+ if (event.type === "error") saveInput.error = serializeError(event.error);
1938
+ await saveTranscript(saveInput);
1939
+ const generatedMessages = event.type === "final" ? event.messages.slice(1) : [];
1940
+ if (props.persistGeneratedMessages === true && generatedMessages.length > 0) {
1941
+ await props.store.append({
1942
+ context: { sessionId: props.session.id },
1943
+ runId: props.runId,
1944
+ turn: 1,
1945
+ messages: generatedMessages
1946
+ });
1947
+ }
1948
+ if (isCancelled()) {
1949
+ return;
1950
+ }
1951
+ status = eventStatus;
1952
+ yield event;
1953
+ }
1954
+ } catch (error) {
1955
+ if (status !== "running") {
1956
+ throw error;
1957
+ }
1958
+ status = "error";
1959
+ appendTranscriptAssistantError(transcript, errorText(error));
1960
+ assignTranscriptRunDuration(transcript, Date.now() - props.startedAt);
1961
+ await saveTranscript({
1962
+ id: props.session.id,
1963
+ runId: props.runId,
1964
+ ...title,
1965
+ transcript,
1966
+ status: "error",
1967
+ error: serializeError(error)
1968
+ });
1969
+ throw error;
1970
+ }
1971
+ })();
1972
+ return {
1973
+ events,
1974
+ async cancel() {
1975
+ if (status !== "running") {
1976
+ return;
1977
+ }
1978
+ status = "cancelled";
1979
+ cancelPendingTranscriptInputs(transcript, (/* @__PURE__ */ new Date()).toISOString());
1980
+ assignTranscriptRunDuration(transcript, Date.now() - props.startedAt);
1981
+ await saveTranscript({
1982
+ id: props.session.id,
1983
+ runId: props.runId,
1984
+ ...title,
1985
+ transcript,
1986
+ status: "cancelled"
1987
+ });
1988
+ }
1989
+ };
1990
+ }
1991
+ function assignTranscriptRunDuration(transcript, durationMs2) {
1992
+ const normalizedDurationMs = Math.max(0, durationMs2);
1993
+ for (let index = transcript.length - 1; index >= 0; index -= 1) {
1994
+ const entry = transcript[index];
1995
+ if (entry?.kind === "message" && entry.role === "assistant") {
1996
+ entry.durationMs = normalizedDurationMs;
1997
+ return;
1998
+ }
1999
+ if (entry?.kind === "message" && entry.role === "user") {
2000
+ break;
2001
+ }
2002
+ }
2003
+ transcript.push({
2004
+ entryId: transcript.length,
2005
+ kind: "message",
2006
+ role: "assistant",
2007
+ text: "",
2008
+ durationMs: normalizedDurationMs
2009
+ });
2010
+ }
2011
+ function cancelPendingTranscriptInputs(transcript, cancelledAt) {
2012
+ for (const entry of transcript) {
2013
+ if (entry.kind !== "tool") {
2014
+ continue;
2015
+ }
2016
+ if (entry.approval?.status === "pending") {
2017
+ entry.approval = {
2018
+ ...entry.approval,
2019
+ status: "cancelled",
2020
+ resolvedAt: cancelledAt,
2021
+ reason: "Run cancelled in Anvia Studio."
2022
+ };
2023
+ }
2024
+ if (entry.question?.status === "pending") {
2025
+ entry.question = {
2026
+ ...entry.question,
2027
+ status: "cancelled",
2028
+ cancelledAt
2029
+ };
2030
+ }
1872
2031
  }
1873
2032
  }
1874
2033
  function acceptTranscriptStreamEvent(transcript, event) {
@@ -1985,6 +2144,9 @@ function acceptTranscriptStreamEvent(transcript, event) {
1985
2144
  questions: event.question.questions
1986
2145
  };
1987
2146
  if (event.question.answeredAt !== void 0) question.answeredAt = event.question.answeredAt;
2147
+ if (event.question.cancelledAt !== void 0) {
2148
+ question.cancelledAt = event.question.cancelledAt;
2149
+ }
1988
2150
  if (event.question.answers !== void 0) question.answers = event.question.answers;
1989
2151
  matched.question = question;
1990
2152
  }
@@ -2476,6 +2638,19 @@ function runCompletedLog(props) {
2476
2638
  metadata
2477
2639
  };
2478
2640
  }
2641
+ function runCancelledLog(sessionId, runId, startedAt) {
2642
+ return {
2643
+ sessionId,
2644
+ runId,
2645
+ level: "info",
2646
+ category: "run",
2647
+ event: "run.cancelled",
2648
+ message: "Run cancelled",
2649
+ metadata: {
2650
+ durationMs: Date.now() - startedAt
2651
+ }
2652
+ };
2653
+ }
2479
2654
  function memorySavedLog(props) {
2480
2655
  const metadata = {};
2481
2656
  if (props.messageCount !== void 0) metadata.messageCount = props.messageCount;
@@ -2662,6 +2837,7 @@ function logsFromStreamEvent(props) {
2662
2837
  ];
2663
2838
  }
2664
2839
  if (event.type === "tool_question_result") {
2840
+ const cancelled = event.question.status === "cancelled";
2665
2841
  const metadata = {
2666
2842
  questionId: event.question.id,
2667
2843
  toolName: event.question.toolName,
@@ -2677,8 +2853,8 @@ function logsFromStreamEvent(props) {
2677
2853
  runId,
2678
2854
  level: "info",
2679
2855
  category: "question",
2680
- event: "question.answered",
2681
- message: `Question answered for ${event.question.toolName}`,
2856
+ event: cancelled ? "question.cancelled" : "question.answered",
2857
+ message: cancelled ? `Question cancelled for ${event.question.toolName}` : `Question answered for ${event.question.toolName}`,
2682
2858
  metadata
2683
2859
  }
2684
2860
  ];
@@ -3055,21 +3231,45 @@ function handleStreamingAgentRun(c, run, props) {
3055
3231
  run.request.requestHook(effectiveHook);
3056
3232
  }
3057
3233
  const runStream = mergeRunAndApprovalEvents(run.request.stream(), runtimeEvents);
3058
- const stream = run.session === void 0 || run.sessionStore === void 0 ? runStream : persistStreamingSessionTranscript({
3059
- stream: streamSessionRunLogs({
3060
- stream: runStream,
3234
+ let stream = runStream;
3235
+ let persistedRun;
3236
+ if (run.session !== void 0 && run.sessionStore !== void 0) {
3237
+ persistedRun = createPersistedStreamingSessionTranscript({
3238
+ stream: streamSessionRunLogs({
3239
+ stream: runStream,
3240
+ store: run.sessionStore,
3241
+ session: run.session,
3242
+ runId: run.runId,
3243
+ startedAt: run.runStartedAt
3244
+ }),
3061
3245
  store: run.sessionStore,
3062
3246
  session: run.session,
3247
+ message: run.body.message,
3063
3248
  runId: run.runId,
3064
- startedAt: run.runStartedAt
3065
- }),
3066
- store: run.sessionStore,
3067
- session: run.session,
3068
- message: run.body.message,
3069
- runId: run.runId,
3070
- persistGeneratedMessages: run.shouldPersistSessionMessages
3249
+ startedAt: run.runStartedAt,
3250
+ persistGeneratedMessages: run.shouldPersistSessionMessages
3251
+ });
3252
+ stream = persistedRun.events;
3253
+ }
3254
+ return streamAgentRunEvents(c, stream, {
3255
+ onCancel: async () => {
3256
+ props.approvalRuntime.cancelRun(run.runId);
3257
+ props.questionRuntime.cancelRun(run.runId);
3258
+ const persistence = [];
3259
+ if (persistedRun !== void 0) {
3260
+ persistence.push(persistedRun.cancel());
3261
+ }
3262
+ if (run.session !== void 0 && run.sessionStore !== void 0) {
3263
+ persistence.push(
3264
+ appendSessionLog(
3265
+ run.sessionStore,
3266
+ runCancelledLog(run.session.id, run.runId, run.runStartedAt)
3267
+ )
3268
+ );
3269
+ }
3270
+ await Promise.all(persistence);
3271
+ }
3071
3272
  });
3072
- return streamAgentRunEvents(c, stream);
3073
3273
  }
3074
3274
  async function handleBufferedAgentRun(c, run, props) {
3075
3275
  try {
@@ -3123,11 +3323,14 @@ async function completeBufferedSessionRun(run, response) {
3123
3323
  });
3124
3324
  }
3125
3325
  }
3326
+ const durationMs2 = Date.now() - run.runStartedAt;
3327
+ const transcript = transcriptFromMessages(response.messages);
3328
+ assignTranscriptRunDuration(transcript, durationMs2);
3126
3329
  await run.sessionStore.saveSessionRunTranscript({
3127
3330
  id: run.session.id,
3128
3331
  runId: run.runId,
3129
3332
  ...optionalTitle(run.body.message),
3130
- transcript: transcriptFromMessages(response.messages),
3333
+ transcript,
3131
3334
  status: "success"
3132
3335
  });
3133
3336
  await appendSessionLog(
@@ -3135,7 +3338,7 @@ async function completeBufferedSessionRun(run, response) {
3135
3338
  runCompletedLog({
3136
3339
  sessionId: run.session.id,
3137
3340
  runId: run.runId,
3138
- durationMs: Date.now() - run.runStartedAt,
3341
+ durationMs: durationMs2,
3139
3342
  usage: response.usage,
3140
3343
  output: response.output,
3141
3344
  messageCount: response.messages.length
@@ -3158,11 +3361,14 @@ async function failBufferedSessionRun(run, error) {
3158
3361
  sessionId: run.session.id,
3159
3362
  metadata: run.memoryMetadata
3160
3363
  });
3364
+ const durationMs2 = Date.now() - run.runStartedAt;
3365
+ const transcript = transcriptFromMessages(messages.slice(run.session.messageCount));
3366
+ assignTranscriptRunDuration(transcript, durationMs2);
3161
3367
  await run.sessionStore.saveSessionRunTranscript({
3162
3368
  id: run.session.id,
3163
3369
  runId: run.runId,
3164
3370
  ...optionalTitle(run.body.message),
3165
- transcript: transcriptFromMessages(messages.slice(run.session.messageCount)),
3371
+ transcript,
3166
3372
  status: "error",
3167
3373
  error: serializeError(error)
3168
3374
  });
@@ -3322,6 +3528,22 @@ function createApprovalRuntime() {
3322
3528
  return true;
3323
3529
  }).map(publicApproval);
3324
3530
  },
3531
+ cancelRun(runId) {
3532
+ const cancelled = [];
3533
+ for (const approval of approvals.values()) {
3534
+ if (!isPendingApproval(approval) || approval.runId !== runId) {
3535
+ continue;
3536
+ }
3537
+ const resolved = resolveApproval(approval, "cancelled", {
3538
+ reason: "Run cancelled in Anvia Studio."
3539
+ });
3540
+ approvals.set(approval.id, resolved);
3541
+ approval.emit?.({ type: "tool_approval_result", approval: resolved });
3542
+ approval.resolve(approvalDecision(false, resolved.reason));
3543
+ cancelled.push(resolved);
3544
+ }
3545
+ return cancelled;
3546
+ },
3325
3547
  decide(id, decision) {
3326
3548
  const approval = approvals.get(id);
3327
3549
  if (approval === void 0) {
@@ -3581,7 +3803,7 @@ function pipelineConfig(pipeline) {
3581
3803
  if (pipeline.metadata !== void 0) config.metadata = pipeline.metadata;
3582
3804
  return config;
3583
3805
  }
3584
- function buildConfig(options, agents, pipelines, stores) {
3806
+ function buildConfig(options, agents, pipelines, stores, sandboxCount = 0) {
3585
3807
  const models = options.models === void 0 ? void 0 : studioModelsConfig(createStudioModelRegistry(options.models), agents);
3586
3808
  const config = {
3587
3809
  id: runnerId(options),
@@ -3591,7 +3813,7 @@ function buildConfig(options, agents, pipelines, stores) {
3591
3813
  chat: {
3592
3814
  quickPrompts: Object.fromEntries(agents.map((agent) => [agent.id, agent.quickPrompts ?? []]))
3593
3815
  },
3594
- capabilities: capabilityConfig(options, agents, pipelines, stores),
3816
+ capabilities: capabilityConfig(options, agents, pipelines, stores, sandboxCount),
3595
3817
  unsupportedCapabilities: unsupportedCapabilities(stores)
3596
3818
  };
3597
3819
  if (options.name !== void 0) config.name = options.name;
@@ -3600,7 +3822,7 @@ function buildConfig(options, agents, pipelines, stores) {
3600
3822
  if (models !== void 0) config.models = models;
3601
3823
  return config;
3602
3824
  }
3603
- function capabilityConfig(_options, agents, pipelines, stores) {
3825
+ function capabilityConfig(_options, agents, pipelines, stores, sandboxCount = 0) {
3604
3826
  const capabilities = {
3605
3827
  agents: { enabled: true },
3606
3828
  observability: { enabled: true },
@@ -3627,6 +3849,9 @@ function capabilityConfig(_options, agents, pipelines, stores) {
3627
3849
  if (agents.some(agentHasMcpTools)) {
3628
3850
  capabilities.mcps = { enabled: true };
3629
3851
  }
3852
+ if (sandboxCount > 0) {
3853
+ capabilities.sandboxes = { enabled: true };
3854
+ }
3630
3855
  if (agents.some(
3631
3856
  (agent) => agent.agent.hook !== void 0 || agent.agent.toolSet.values().some((tool) => tool.approval)
3632
3857
  )) {
@@ -4910,6 +5135,20 @@ function createQuestionRuntime() {
4910
5135
  return true;
4911
5136
  }).map(publicQuestion);
4912
5137
  },
5138
+ cancelRun(runId) {
5139
+ const cancelled = [];
5140
+ for (const question of questions.values()) {
5141
+ if (!isPendingQuestion(question) || question.runId !== runId) {
5142
+ continue;
5143
+ }
5144
+ const resolved = cancelQuestion(question);
5145
+ questions.set(question.id, resolved);
5146
+ question.emit?.({ type: "tool_question_result", question: resolved });
5147
+ question.resolve([]);
5148
+ cancelled.push(resolved);
5149
+ }
5150
+ return cancelled;
5151
+ },
4913
5152
  answer(id, answers) {
4914
5153
  const question = questions.get(id);
4915
5154
  if (question === void 0) {
@@ -5010,6 +5249,13 @@ function resolveQuestion(question, answers) {
5010
5249
  answers
5011
5250
  });
5012
5251
  }
5252
+ function cancelQuestion(question) {
5253
+ return publicQuestion({
5254
+ ...question,
5255
+ status: "cancelled",
5256
+ cancelledAt: (/* @__PURE__ */ new Date()).toISOString()
5257
+ });
5258
+ }
5013
5259
  function publicQuestion(question) {
5014
5260
  const { emit, resolve: resolve2, ...rest } = question;
5015
5261
  void emit;
@@ -5017,6 +5263,446 @@ function publicQuestion(question) {
5017
5263
  return rest;
5018
5264
  }
5019
5265
 
5266
+ // src/runtime/sandboxes.ts
5267
+ import path from "path";
5268
+ var sandboxToolMetadataKey = /* @__PURE__ */ Symbol.for("anvia.sandbox.tool.metadata");
5269
+ var maxFileResponseBytes = 10 * 1024 * 1024;
5270
+ var defaultProcessLogBytes = 64 * 1024;
5271
+ var maxProcessLogBytes = 1024 * 1024;
5272
+ var StudioSandboxRegistry = class {
5273
+ entries;
5274
+ constructor(entries) {
5275
+ this.entries = new Map(entries.map((entry) => [entry.summary.ref, entry]));
5276
+ }
5277
+ get size() {
5278
+ return this.entries.size;
5279
+ }
5280
+ list() {
5281
+ return [...this.entries.values()].map((entry) => copySummary(entry.summary));
5282
+ }
5283
+ get(ref) {
5284
+ return this.entries.get(ref);
5285
+ }
5286
+ };
5287
+ function createStudioSandboxRegistry(agents) {
5288
+ const drafts = /* @__PURE__ */ new Map();
5289
+ for (const agent of agents) {
5290
+ for (const { tool } of agentToolItems(agent)) {
5291
+ const session = sandboxSessionFromTool(tool);
5292
+ if (session === void 0) {
5293
+ continue;
5294
+ }
5295
+ const draft = drafts.get(session) ?? {
5296
+ session,
5297
+ agentIds: /* @__PURE__ */ new Set(),
5298
+ toolNames: /* @__PURE__ */ new Set()
5299
+ };
5300
+ draft.agentIds.add(agent.id);
5301
+ draft.toolNames.add(tool.name);
5302
+ drafts.set(session, draft);
5303
+ }
5304
+ }
5305
+ const refCounts = /* @__PURE__ */ new Map();
5306
+ const entries = [...drafts.values()].sort(
5307
+ (left, right) => `${left.session.provider}\0${left.session.id}`.localeCompare(
5308
+ `${right.session.provider}\0${right.session.id}`
5309
+ )
5310
+ ).map((draft) => {
5311
+ const baseRef = sandboxRef(draft.session.provider, draft.session.id);
5312
+ const occurrence = (refCounts.get(baseRef) ?? 0) + 1;
5313
+ refCounts.set(baseRef, occurrence);
5314
+ const ref = occurrence === 1 ? baseRef : `${baseRef}.${occurrence}`;
5315
+ return {
5316
+ session: draft.session,
5317
+ summary: {
5318
+ ref,
5319
+ id: draft.session.id,
5320
+ provider: draft.session.provider,
5321
+ workdir: draft.session.workdir,
5322
+ agentIds: [...draft.agentIds].sort(),
5323
+ toolNames: [...draft.toolNames].sort(),
5324
+ capabilities: {
5325
+ files: true,
5326
+ ports: supportsPorts(draft.session),
5327
+ processes: supportsProcesses(draft.session)
5328
+ }
5329
+ }
5330
+ };
5331
+ });
5332
+ return new StudioSandboxRegistry(entries);
5333
+ }
5334
+ function registerSandboxRoutes(app, registry) {
5335
+ app.use("/sandboxes", sandboxNoStore);
5336
+ app.use("/sandboxes/*", sandboxNoStore);
5337
+ app.get("/sandboxes", (c) => c.json({ sandboxes: registry.list() }));
5338
+ app.get("/sandboxes/:sandboxRef", (c) => {
5339
+ const entry = registry.get(c.req.param("sandboxRef"));
5340
+ if (entry === void 0) {
5341
+ return errorResponse(c, 404, "not_found", "Sandbox not found");
5342
+ }
5343
+ return c.json(copySummary(entry.summary));
5344
+ });
5345
+ app.get("/sandboxes/:sandboxRef/files", async (c) => {
5346
+ const entry = registry.get(c.req.param("sandboxRef"));
5347
+ if (entry === void 0) {
5348
+ return errorResponse(c, 404, "not_found", "Sandbox not found");
5349
+ }
5350
+ try {
5351
+ const requestedPath = c.req.query("path") ?? ".";
5352
+ const normalizedPath = normalizeSandboxPath(requestedPath, true);
5353
+ const entries = await listSandboxFiles(entry.session, normalizedPath);
5354
+ return c.json({
5355
+ sandboxRef: entry.summary.ref,
5356
+ path: normalizedPath,
5357
+ entries
5358
+ });
5359
+ } catch (error) {
5360
+ return sandboxErrorResponse(c, error);
5361
+ }
5362
+ });
5363
+ app.get("/sandboxes/:sandboxRef/files/content", async (c) => {
5364
+ const entry = registry.get(c.req.param("sandboxRef"));
5365
+ if (entry === void 0) {
5366
+ return errorResponse(c, 404, "not_found", "Sandbox not found");
5367
+ }
5368
+ try {
5369
+ const requestedPath = c.req.query("path");
5370
+ if (requestedPath === void 0) {
5371
+ throw new SandboxRouteError(400, "bad_request", "path query parameter is required");
5372
+ }
5373
+ const download = c.req.query("download");
5374
+ if (download !== void 0 && download !== "1") {
5375
+ throw new SandboxRouteError(400, "bad_request", "download must be 1 when provided");
5376
+ }
5377
+ const normalizedPath = normalizeSandboxPath(requestedPath, false);
5378
+ const parentPath = path.posix.dirname(normalizedPath);
5379
+ const siblings = await listSandboxFiles(entry.session, parentPath);
5380
+ const file = siblings.find((candidate) => candidate.path === normalizedPath);
5381
+ if (file === void 0) {
5382
+ throw new SandboxRouteError(404, "not_found", "Sandbox file not found");
5383
+ }
5384
+ if (file.type !== "file") {
5385
+ throw new SandboxRouteError(
5386
+ 400,
5387
+ "bad_request",
5388
+ "Sandbox content path must refer to a regular file"
5389
+ );
5390
+ }
5391
+ if (file.size !== void 0 && file.size > maxFileResponseBytes) {
5392
+ throw fileTooLarge(file.size);
5393
+ }
5394
+ const rawBytes = await entry.session.readFile(normalizedPath);
5395
+ if (!(rawBytes instanceof Uint8Array)) {
5396
+ throw new TypeError("Sandbox readFile returned an invalid byte payload");
5397
+ }
5398
+ if (rawBytes.byteLength > maxFileResponseBytes) {
5399
+ throw fileTooLarge(rawBytes.byteLength);
5400
+ }
5401
+ const bytes = new Uint8Array(rawBytes.byteLength);
5402
+ bytes.set(rawBytes);
5403
+ const disposition = download === "1" ? "attachment" : "inline";
5404
+ const filename = encodeURIComponent(path.posix.basename(normalizedPath));
5405
+ return new Response(bytes, {
5406
+ headers: {
5407
+ "cache-control": "no-store",
5408
+ "content-disposition": `${disposition}; filename*=UTF-8''${filename}`,
5409
+ "content-length": String(bytes.byteLength),
5410
+ "content-type": "application/octet-stream",
5411
+ "x-content-type-options": "nosniff"
5412
+ }
5413
+ });
5414
+ } catch (error) {
5415
+ return sandboxErrorResponse(c, error);
5416
+ }
5417
+ });
5418
+ app.get("/sandboxes/:sandboxRef/ports", (c) => {
5419
+ const entry = registry.get(c.req.param("sandboxRef"));
5420
+ if (entry === void 0) {
5421
+ return errorResponse(c, 404, "not_found", "Sandbox not found");
5422
+ }
5423
+ if (!supportsPorts(entry.session)) {
5424
+ return unsupportedSandboxOperation(c, "ports");
5425
+ }
5426
+ try {
5427
+ return c.json({
5428
+ sandboxRef: entry.summary.ref,
5429
+ ports: normalizePorts(entry.session.publishedPorts)
5430
+ });
5431
+ } catch (error) {
5432
+ return sandboxErrorResponse(c, error);
5433
+ }
5434
+ });
5435
+ app.get("/sandboxes/:sandboxRef/processes", async (c) => {
5436
+ const entry = registry.get(c.req.param("sandboxRef"));
5437
+ if (entry === void 0) {
5438
+ return errorResponse(c, 404, "not_found", "Sandbox not found");
5439
+ }
5440
+ if (!supportsProcesses(entry.session)) {
5441
+ return unsupportedSandboxOperation(c, "processes");
5442
+ }
5443
+ try {
5444
+ return c.json({
5445
+ sandboxRef: entry.summary.ref,
5446
+ processes: normalizeProcesses(await entry.session.listProcesses())
5447
+ });
5448
+ } catch (error) {
5449
+ return sandboxErrorResponse(c, error);
5450
+ }
5451
+ });
5452
+ app.get("/sandboxes/:sandboxRef/processes/:processId/logs", async (c) => {
5453
+ const entry = registry.get(c.req.param("sandboxRef"));
5454
+ if (entry === void 0) {
5455
+ return errorResponse(c, 404, "not_found", "Sandbox not found");
5456
+ }
5457
+ if (!supportsProcesses(entry.session)) {
5458
+ return unsupportedSandboxOperation(c, "processes.logs");
5459
+ }
5460
+ try {
5461
+ const tailBytes = parseTailBytes(c.req.query("tailBytes"));
5462
+ const processId = c.req.param("processId");
5463
+ const processes = normalizeProcesses(await entry.session.listProcesses());
5464
+ if (!processes.some((process2) => process2.id === processId)) {
5465
+ throw new SandboxRouteError(404, "not_found", "Sandbox process not found");
5466
+ }
5467
+ const logs = normalizeProcessLogs(
5468
+ await entry.session.readProcessLogs(processId, { tailBytes }),
5469
+ tailBytes
5470
+ );
5471
+ return c.json({
5472
+ sandboxRef: entry.summary.ref,
5473
+ processId,
5474
+ ...logs
5475
+ });
5476
+ } catch (error) {
5477
+ return sandboxErrorResponse(c, error);
5478
+ }
5479
+ });
5480
+ }
5481
+ async function sandboxNoStore(c, next) {
5482
+ c.header("cache-control", "no-store");
5483
+ await next();
5484
+ }
5485
+ function sandboxSessionFromTool(tool) {
5486
+ const metadata = tool[sandboxToolMetadataKey];
5487
+ if (!isRecord4(metadata)) {
5488
+ return void 0;
5489
+ }
5490
+ return isSandboxSession(metadata.session) ? metadata.session : void 0;
5491
+ }
5492
+ function isSandboxSession(value) {
5493
+ return isRecord4(value) && typeof value.id === "string" && value.id.length > 0 && typeof value.provider === "string" && value.provider.length > 0 && typeof value.workdir === "string" && value.workdir.length > 0 && typeof value.listFiles === "function" && typeof value.readFile === "function";
5494
+ }
5495
+ function supportsPorts(session) {
5496
+ return Array.isArray(session.publishedPorts);
5497
+ }
5498
+ function supportsProcesses(session) {
5499
+ return typeof session.listProcesses === "function" && typeof session.readProcessLogs === "function";
5500
+ }
5501
+ function sandboxRef(provider, id) {
5502
+ return Buffer.from(JSON.stringify([provider, id]), "utf8").toString("base64url");
5503
+ }
5504
+ function copySummary(summary) {
5505
+ return {
5506
+ ...summary,
5507
+ agentIds: [...summary.agentIds],
5508
+ toolNames: [...summary.toolNames],
5509
+ capabilities: { ...summary.capabilities }
5510
+ };
5511
+ }
5512
+ async function listSandboxFiles(session, filePath) {
5513
+ return normalizeFileEntries(await session.listFiles(filePath)).sort((left, right) => {
5514
+ if (left.type === "directory" && right.type !== "directory") return -1;
5515
+ if (left.type !== "directory" && right.type === "directory") return 1;
5516
+ return left.path.localeCompare(right.path);
5517
+ });
5518
+ }
5519
+ function normalizeFileEntries(value) {
5520
+ if (!Array.isArray(value)) {
5521
+ throw new TypeError("Sandbox listFiles returned an invalid payload");
5522
+ }
5523
+ return value.map((item) => {
5524
+ if (!isRecord4(item) || typeof item.path !== "string" || !isFileType(item.type)) {
5525
+ throw new TypeError("Sandbox listFiles returned an invalid entry");
5526
+ }
5527
+ let entryPath;
5528
+ try {
5529
+ entryPath = normalizeSandboxPath(item.path, false);
5530
+ } catch {
5531
+ throw new TypeError("Sandbox listFiles returned an invalid entry path");
5532
+ }
5533
+ const entry = { path: entryPath, type: item.type };
5534
+ if (item.size !== void 0) {
5535
+ if (typeof item.size !== "number" || !Number.isFinite(item.size) || item.size < 0) {
5536
+ throw new TypeError("Sandbox listFiles returned an invalid file size");
5537
+ }
5538
+ entry.size = item.size;
5539
+ }
5540
+ return entry;
5541
+ });
5542
+ }
5543
+ function normalizePorts(value) {
5544
+ if (!Array.isArray(value)) {
5545
+ throw new TypeError("Sandbox publishedPorts returned an invalid payload");
5546
+ }
5547
+ return value.map((item) => {
5548
+ if (!isRecord4(item) || !isPort(item.containerPort) || typeof item.host !== "string" || !isPort(item.hostPort) || typeof item.protocol !== "string") {
5549
+ throw new TypeError("Sandbox publishedPorts returned an invalid entry");
5550
+ }
5551
+ return {
5552
+ containerPort: item.containerPort,
5553
+ host: item.host,
5554
+ hostPort: item.hostPort,
5555
+ protocol: item.protocol
5556
+ };
5557
+ });
5558
+ }
5559
+ function normalizeProcesses(value) {
5560
+ if (!Array.isArray(value)) {
5561
+ throw new TypeError("Sandbox listProcesses returned an invalid payload");
5562
+ }
5563
+ return value.map((item) => {
5564
+ if (!isRecord4(item) || typeof item.id !== "string" || typeof item.command !== "string" || !Array.isArray(item.args) || !item.args.every((arg) => typeof arg === "string") || !isProcessStatus(item.status) || typeof item.startedAt !== "string") {
5565
+ throw new TypeError("Sandbox listProcesses returned an invalid entry");
5566
+ }
5567
+ const process2 = {
5568
+ id: item.id,
5569
+ command: item.command,
5570
+ args: item.args,
5571
+ status: item.status,
5572
+ startedAt: item.startedAt
5573
+ };
5574
+ if (typeof item.cwd === "string") process2.cwd = item.cwd;
5575
+ if (typeof item.exitCode === "number") process2.exitCode = item.exitCode;
5576
+ if (typeof item.endedAt === "string") process2.endedAt = item.endedAt;
5577
+ return process2;
5578
+ });
5579
+ }
5580
+ function normalizeProcessLogs(value, maxBytes) {
5581
+ if (!isRecord4(value) || typeof value.stdout !== "string" || typeof value.stderr !== "string" || typeof value.stdoutTruncated !== "boolean" || typeof value.stderrTruncated !== "boolean") {
5582
+ throw new TypeError("Sandbox readProcessLogs returned an invalid payload");
5583
+ }
5584
+ const stdout = boundedLogText(value.stdout, maxBytes);
5585
+ const stderr = boundedLogText(value.stderr, maxBytes);
5586
+ return {
5587
+ stdout: stdout.text,
5588
+ stderr: stderr.text,
5589
+ stdoutTruncated: value.stdoutTruncated || stdout.truncated,
5590
+ stderrTruncated: value.stderrTruncated || stderr.truncated
5591
+ };
5592
+ }
5593
+ function boundedLogText(value, maxBytes) {
5594
+ const bytes = new TextEncoder().encode(value);
5595
+ if (bytes.byteLength <= maxBytes) {
5596
+ return { text: value, truncated: false };
5597
+ }
5598
+ if (maxBytes === 0) {
5599
+ return { text: "", truncated: true };
5600
+ }
5601
+ let start = bytes.byteLength - maxBytes;
5602
+ while (start < bytes.byteLength && (bytes[start] ?? 0) >> 6 === 2) {
5603
+ start += 1;
5604
+ }
5605
+ return {
5606
+ text: new TextDecoder().decode(bytes.subarray(start)),
5607
+ truncated: true
5608
+ };
5609
+ }
5610
+ function normalizeSandboxPath(input, allowRoot) {
5611
+ if (input.length === 0) {
5612
+ throw new SandboxRouteError(400, "bad_request", "Sandbox path cannot be empty");
5613
+ }
5614
+ if (input.includes("\0")) {
5615
+ throw new SandboxRouteError(400, "bad_request", "Sandbox path cannot contain null bytes");
5616
+ }
5617
+ const normalized = path.posix.normalize(input.replaceAll("\\", "/"));
5618
+ if (path.posix.isAbsolute(normalized)) {
5619
+ throw new SandboxRouteError(400, "bad_request", "Sandbox path must be relative");
5620
+ }
5621
+ if (normalized === ".." || normalized.startsWith("../")) {
5622
+ throw new SandboxRouteError(400, "bad_request", "Sandbox path cannot leave the workspace");
5623
+ }
5624
+ if (normalized === "." && !allowRoot) {
5625
+ throw new SandboxRouteError(400, "bad_request", "Sandbox path must refer to a file");
5626
+ }
5627
+ return normalized;
5628
+ }
5629
+ function parseTailBytes(value) {
5630
+ if (value === void 0) {
5631
+ return defaultProcessLogBytes;
5632
+ }
5633
+ const parsed = Number(value);
5634
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > maxProcessLogBytes) {
5635
+ throw new SandboxRouteError(
5636
+ 400,
5637
+ "bad_request",
5638
+ `tailBytes must be an integer from 0 through ${maxProcessLogBytes}`
5639
+ );
5640
+ }
5641
+ return parsed;
5642
+ }
5643
+ function unsupportedSandboxOperation(c, operation) {
5644
+ return errorResponse(c, 501, "unsupported_capability", `Sandbox does not support ${operation}`, {
5645
+ capability: "sandboxes",
5646
+ operation
5647
+ });
5648
+ }
5649
+ function sandboxErrorResponse(c, error) {
5650
+ if (error instanceof SandboxRouteError) {
5651
+ return errorResponse(c, error.status, error.code, error.message);
5652
+ }
5653
+ const name = errorName(error);
5654
+ if (name === "SandboxPathError") {
5655
+ return errorResponse(c, 400, "bad_request", errorMessage(error));
5656
+ }
5657
+ if (name === "SandboxFileSizeError") {
5658
+ return errorResponse(c, 413, "payload_too_large", errorMessage(error));
5659
+ }
5660
+ if (name === "SandboxSessionDestroyedError") {
5661
+ return errorResponse(c, 409, "conflict", "Sandbox session is no longer available");
5662
+ }
5663
+ return errorResponse(
5664
+ c,
5665
+ 500,
5666
+ "internal_error",
5667
+ "Sandbox inspection failed",
5668
+ serializeError(error)
5669
+ );
5670
+ }
5671
+ function fileTooLarge(size) {
5672
+ return new SandboxRouteError(
5673
+ 413,
5674
+ "payload_too_large",
5675
+ `Sandbox file exceeds the ${maxFileResponseBytes}-byte response limit (${size} bytes)`
5676
+ );
5677
+ }
5678
+ var SandboxRouteError = class extends Error {
5679
+ constructor(status, code, message) {
5680
+ super(message);
5681
+ this.status = status;
5682
+ this.code = code;
5683
+ }
5684
+ status;
5685
+ code;
5686
+ };
5687
+ function isFileType(value) {
5688
+ return value === "file" || value === "directory" || value === "symlink" || value === "other";
5689
+ }
5690
+ function isProcessStatus(value) {
5691
+ return value === "running" || value === "exited" || value === "stopped";
5692
+ }
5693
+ function isPort(value) {
5694
+ return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535;
5695
+ }
5696
+ function isRecord4(value) {
5697
+ return typeof value === "object" && value !== null;
5698
+ }
5699
+ function errorName(error) {
5700
+ return isRecord4(error) && typeof error.name === "string" ? error.name : "";
5701
+ }
5702
+ function errorMessage(error) {
5703
+ return isRecord4(error) && typeof error.message === "string" ? error.message : String(error);
5704
+ }
5705
+
5020
5706
  // src/runtime/sessions.ts
5021
5707
  function registerSessionRoutes(app, props) {
5022
5708
  app.get("/sessions", async (c) => {
@@ -5505,6 +6191,7 @@ function registerStatusRoutes(app, props) {
5505
6191
  agents: props.agents.length,
5506
6192
  pipelines: props.pipelines.length
5507
6193
  };
6194
+ if (props.sandboxCount > 0) counts.sandboxes = props.sandboxCount;
5508
6195
  if (props.stores.sessions !== void 0) {
5509
6196
  counts.sessions = (await props.stores.sessions.listSessions({ limit: 100 })).length;
5510
6197
  }
@@ -5525,7 +6212,13 @@ function registerStatusRoutes(app, props) {
5525
6212
  runner,
5526
6213
  storage,
5527
6214
  counts,
5528
- capabilities: capabilityConfig(props.options, props.agents, props.pipelines, props.stores),
6215
+ capabilities: capabilityConfig(
6216
+ props.options,
6217
+ props.agents,
6218
+ props.pipelines,
6219
+ props.stores,
6220
+ props.sandboxCount
6221
+ ),
5529
6222
  generatedAt: (/* @__PURE__ */ new Date()).toISOString()
5530
6223
  };
5531
6224
  return c.json(summary);
@@ -5734,20 +6427,38 @@ var Studio = class {
5734
6427
  const log = serveOptions.log ?? true;
5735
6428
  if (log) {
5736
6429
  const host = serveOptions.hostname ?? "localhost";
5737
- if (isStudioUiEnabled(this.options.ui)) {
5738
- const uiPath = studioUiEntryPath(resolveStudioUiOptions(this.options.ui));
5739
- console.log(`Studio UI: http://${host}:${port}${uiPath}`);
5740
- } else {
5741
- console.log(`Studio API: http://${host}:${port}`);
5742
- }
6430
+ this.logAddress(host, port);
6431
+ }
6432
+ if (serveOptions.handleSignals ?? true) {
6433
+ this.sigintHandler = () => {
6434
+ this.close();
6435
+ process.exit(0);
6436
+ };
6437
+ process.once("SIGINT", this.sigintHandler);
5743
6438
  }
5744
- this.sigintHandler = () => {
5745
- this.close();
5746
- process.exit(0);
5747
- };
5748
- process.once("SIGINT", this.sigintHandler);
5749
6439
  return this;
5750
6440
  }
6441
+ async serve(serveOptions = {}) {
6442
+ const { onShutdown, signal, ...startOptions } = serveOptions;
6443
+ try {
6444
+ this.start({ ...startOptions, log: false, handleSignals: false });
6445
+ const server = this.server;
6446
+ if (server === void 0) {
6447
+ throw new Error("Studio server did not start");
6448
+ }
6449
+ await waitForServerListening(server);
6450
+ if (startOptions.log ?? true) {
6451
+ const requestedPort = startOptions.port ?? Number(process.env.RUNNER_PORT ?? 4021);
6452
+ const address = server.address();
6453
+ const port = typeof address === "object" && address !== null ? address.port : requestedPort;
6454
+ this.logAddress(startOptions.hostname ?? "localhost", port);
6455
+ }
6456
+ await waitForShutdown(signal);
6457
+ } finally {
6458
+ this.close();
6459
+ await onShutdown?.();
6460
+ }
6461
+ }
5751
6462
  close() {
5752
6463
  if (this.sigintHandler !== void 0) {
5753
6464
  process.off("SIGINT", this.sigintHandler);
@@ -5757,7 +6468,68 @@ var Studio = class {
5757
6468
  this.server = void 0;
5758
6469
  this.studio.close();
5759
6470
  }
6471
+ logAddress(host, port) {
6472
+ if (isStudioUiEnabled(this.options.ui)) {
6473
+ const uiPath = studioUiEntryPath(resolveStudioUiOptions(this.options.ui));
6474
+ console.log(`Studio UI: http://${host}:${port}${uiPath}`);
6475
+ } else {
6476
+ console.log(`Studio API: http://${host}:${port}`);
6477
+ }
6478
+ }
5760
6479
  };
6480
+ function waitForServerListening(server) {
6481
+ if (server.listening) return Promise.resolve();
6482
+ return new Promise((resolve2, reject) => {
6483
+ const cleanup = () => {
6484
+ server.off("listening", onListening);
6485
+ server.off("error", onError);
6486
+ };
6487
+ const onListening = () => {
6488
+ cleanup();
6489
+ resolve2();
6490
+ };
6491
+ const onError = (error) => {
6492
+ cleanup();
6493
+ reject(error);
6494
+ };
6495
+ server.once("listening", onListening);
6496
+ server.once("error", onError);
6497
+ });
6498
+ }
6499
+ function waitForShutdown(signal) {
6500
+ if (signal?.aborted) return Promise.resolve();
6501
+ return new Promise((resolve2) => {
6502
+ const useRawInput = process.stdin.isTTY === true;
6503
+ const wasRaw = process.stdin.isRaw;
6504
+ const wasFlowing = process.stdin.readableFlowing;
6505
+ const cleanup = () => {
6506
+ process.off("SIGINT", finish);
6507
+ process.off("SIGTERM", finish);
6508
+ signal?.removeEventListener("abort", finish);
6509
+ if (useRawInput) {
6510
+ process.stdin.off("data", onInput);
6511
+ if (!wasRaw && process.stdin.isRaw) process.stdin.setRawMode(false);
6512
+ if (wasFlowing !== true) process.stdin.pause();
6513
+ }
6514
+ };
6515
+ const finish = () => {
6516
+ cleanup();
6517
+ resolve2();
6518
+ };
6519
+ const onInput = (chunk) => {
6520
+ const interrupted = typeof chunk === "string" ? chunk.includes("") : chunk.includes(3);
6521
+ if (interrupted) finish();
6522
+ };
6523
+ process.once("SIGINT", finish);
6524
+ process.once("SIGTERM", finish);
6525
+ signal?.addEventListener("abort", finish, { once: true });
6526
+ if (useRawInput) {
6527
+ if (!wasRaw) process.stdin.setRawMode(true);
6528
+ process.stdin.resume();
6529
+ process.stdin.on("data", onInput);
6530
+ }
6531
+ });
6532
+ }
5761
6533
  function studioOptionsFromTargets(targets, options) {
5762
6534
  const agents = targets.filter((target) => target instanceof Agent2);
5763
6535
  const pipelines = targets.filter(
@@ -5836,6 +6608,7 @@ function createStudioApp(options) {
5836
6608
  const evalMap = new Map(options.evals.map((suite) => [suite.id ?? suite.name, suite]));
5837
6609
  const approvalRuntime = createApprovalRuntime();
5838
6610
  const questionRuntime = createQuestionRuntime();
6611
+ const sandboxRegistry = createStudioSandboxRegistry(agents);
5839
6612
  const app = new HonoApp();
5840
6613
  const uiOptions = isStudioUiEnabled(options.ui) ? resolveStudioUiOptions(options.ui) : void 0;
5841
6614
  if (uiOptions !== void 0 && !uiOptions.protectShell) {
@@ -5855,8 +6628,17 @@ function createStudioApp(options) {
5855
6628
  }
5856
6629
  })
5857
6630
  );
5858
- app.get("/config", (c) => c.json(buildConfig(options, agents, pipelines, stores)));
5859
- registerStatusRoutes(app, { options, agents, pipelines, stores });
6631
+ app.get(
6632
+ "/config",
6633
+ (c) => c.json(buildConfig(options, agents, pipelines, stores, sandboxRegistry.size))
6634
+ );
6635
+ registerStatusRoutes(app, {
6636
+ options,
6637
+ agents,
6638
+ pipelines,
6639
+ stores,
6640
+ sandboxCount: sandboxRegistry.size
6641
+ });
5860
6642
  app.get("/agents", (c) => c.json({ agents: agents.map(agentConfig) }));
5861
6643
  app.get("/agents/:agentId", (c) => {
5862
6644
  const agent = agentMap.get(c.req.param("agentId"));
@@ -5875,6 +6657,7 @@ function createStudioApp(options) {
5875
6657
  registerModelRoutes(app, { registry: modelRegistry, agentMap });
5876
6658
  registerMcpRoutes(app, { agentMap });
5877
6659
  registerToolRoutes(app, { agentMap });
6660
+ registerSandboxRoutes(app, sandboxRegistry);
5878
6661
  registerApprovalRoutes(app, approvalRuntime);
5879
6662
  registerQuestionRoutes(app, questionRuntime);
5880
6663
  registerObservabilityRoutes(app, observabilityHub);
@@ -5923,7 +6706,7 @@ function createStudioApp(options) {
5923
6706
  return app.fetch(request);
5924
6707
  },
5925
6708
  config() {
5926
- return buildConfig(options, agents, pipelines, stores);
6709
+ return buildConfig(options, agents, pipelines, stores, sandboxRegistry.size);
5927
6710
  },
5928
6711
  close() {
5929
6712
  }
@@ -5962,8 +6745,8 @@ function createSqliteSessionStore(options = {}) {
5962
6745
  return new SqliteSessionStore(options.path ?? ":memory:");
5963
6746
  }
5964
6747
  var SqliteSessionStore = class {
5965
- constructor(path) {
5966
- this.path = path;
6748
+ constructor(path2) {
6749
+ this.path = path2;
5967
6750
  }
5968
6751
  path;
5969
6752
  kind = "sqlite";