@anvia/studio 1.0.0-rc.2 → 1.0.0-rc.4

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 (92) hide show
  1. package/README.md +38 -0
  2. package/dist/index.d.ts +152 -67
  3. package/dist/index.js +921 -1001
  4. package/dist/index.js.map +1 -1
  5. package/dist/ui/assets/ArrowClockwise.es-CSEY3Qr6.js +2 -0
  6. package/dist/ui/assets/{ArrowClockwise.es-DMJj7Hv0.js.map → ArrowClockwise.es-CSEY3Qr6.js.map} +1 -1
  7. package/dist/ui/assets/CaretDown.es-BHAPbv1R.js +2 -0
  8. package/dist/ui/assets/CaretDown.es-BHAPbv1R.js.map +1 -0
  9. package/dist/ui/assets/Check.es-BLChmggz.js +2 -0
  10. package/dist/ui/assets/Check.es-BLChmggz.js.map +1 -0
  11. package/dist/ui/assets/Wrench.es-BL6BmNv7.js +4 -0
  12. package/dist/ui/assets/Wrench.es-BL6BmNv7.js.map +1 -0
  13. package/dist/ui/assets/agents-page-DQqt45If.js +2 -0
  14. package/dist/ui/assets/{agents-page-CbQpO3Tg.js.map → agents-page-DQqt45If.js.map} +1 -1
  15. package/dist/ui/assets/{badge-BIWy1K8W.js → badge-CcajoiAt.js} +1 -1
  16. package/dist/ui/assets/{badge-BIWy1K8W.js.map → badge-CcajoiAt.js.map} +1 -1
  17. package/dist/ui/assets/button-07gzSO47.js +2 -0
  18. package/dist/ui/assets/button-07gzSO47.js.map +1 -0
  19. package/dist/ui/assets/chunk-D2ECOWN5-fs3ih0yA.js +65 -0
  20. package/dist/ui/assets/chunk-D2ECOWN5-fs3ih0yA.js.map +1 -0
  21. package/dist/ui/assets/dialog-CNNQ3hoF.js +2 -0
  22. package/dist/ui/assets/dialog-CNNQ3hoF.js.map +1 -0
  23. package/dist/ui/assets/dist-CqMdRzwk.js +2 -0
  24. package/dist/ui/assets/dist-CqMdRzwk.js.map +1 -0
  25. package/dist/ui/assets/dist-DWqJwN1i.js +2 -0
  26. package/dist/ui/assets/dist-DWqJwN1i.js.map +1 -0
  27. package/dist/ui/assets/es2015-DsZq7PFO.js +42 -0
  28. package/dist/ui/assets/es2015-DsZq7PFO.js.map +1 -0
  29. package/dist/ui/assets/icon-CqkMSRbt.js +2 -0
  30. package/dist/ui/assets/icon-CqkMSRbt.js.map +1 -0
  31. package/dist/ui/assets/index-BGNC3dLD.css +1 -0
  32. package/dist/ui/assets/index-BLaXiUWO.js +12 -0
  33. package/dist/ui/assets/index-BLaXiUWO.js.map +1 -0
  34. package/dist/ui/assets/{input-BTHDDFpv.js → input-BiI6gsxA.js} +1 -1
  35. package/dist/ui/assets/{input-BTHDDFpv.js.map → input-BiI6gsxA.js.map} +1 -1
  36. package/dist/ui/assets/knowledge-model-S51n2rKh.js +2 -0
  37. package/dist/ui/assets/knowledge-model-S51n2rKh.js.map +1 -0
  38. package/dist/ui/assets/knowledge-page-DzRhA9qQ.js +2 -0
  39. package/dist/ui/assets/{knowledge-page-C8zg8uTL.js.map → knowledge-page-DzRhA9qQ.js.map} +1 -1
  40. package/dist/ui/assets/mcps-page-ZVngljXI.js +2 -0
  41. package/dist/ui/assets/{mcps-page-gNYeqSPB.js.map → mcps-page-ZVngljXI.js.map} +1 -1
  42. package/dist/ui/assets/{memory-page-BLrfOqyU.js → memory-page-DAJk0Zof.js} +3 -3
  43. package/dist/ui/assets/{memory-page-BLrfOqyU.js.map → memory-page-DAJk0Zof.js.map} +1 -1
  44. package/dist/ui/assets/pipelines-page-8osYA129.js +8 -0
  45. package/dist/ui/assets/{pipelines-page-hTEhFZGg.js.map → pipelines-page-8osYA129.js.map} +1 -1
  46. package/dist/ui/assets/{renderers-DC_-TD_X.js → renderers-BPGo4d2i.js} +2 -2
  47. package/dist/ui/assets/{renderers-DC_-TD_X.js.map → renderers-BPGo4d2i.js.map} +1 -1
  48. package/dist/ui/assets/request-7oUPS8zY.js +4 -0
  49. package/dist/ui/assets/request-7oUPS8zY.js.map +1 -0
  50. package/dist/ui/assets/rfb-DiW3PDuT.js +6 -0
  51. package/dist/ui/assets/rfb-DiW3PDuT.js.map +1 -0
  52. package/dist/ui/assets/sandboxes-page-C6kokYwc.js +2 -0
  53. package/dist/ui/assets/sandboxes-page-C6kokYwc.js.map +1 -0
  54. package/dist/ui/assets/select-BOYfrfck.js +2 -0
  55. package/dist/ui/assets/select-BOYfrfck.js.map +1 -0
  56. package/dist/ui/assets/sessions-page-ClXzRM9K.js +2 -0
  57. package/dist/ui/assets/{sessions-page-DBzoP7MV.js.map → sessions-page-ClXzRM9K.js.map} +1 -1
  58. package/dist/ui/assets/status-page-DfuQ0fpa.js +2 -0
  59. package/dist/ui/assets/{status-page-Bf1lq444.js.map → status-page-DfuQ0fpa.js.map} +1 -1
  60. package/dist/ui/assets/studio-CeTsQ4gf.js +2 -0
  61. package/dist/ui/assets/studio-CeTsQ4gf.js.map +1 -0
  62. package/dist/ui/assets/textarea-DESKg_ZK.js +2 -0
  63. package/dist/ui/assets/textarea-DESKg_ZK.js.map +1 -0
  64. package/dist/ui/assets/tool-runner-Cdrcs2v4.js +2 -0
  65. package/dist/ui/assets/{tool-runner-BbUlfPEO.js.map → tool-runner-Cdrcs2v4.js.map} +1 -1
  66. package/dist/ui/assets/tools-page-C865VQmM.js +2 -0
  67. package/dist/ui/assets/{tools-page-BgUz-79v.js.map → tools-page-C865VQmM.js.map} +1 -1
  68. package/dist/ui/assets/trace-browser-D93H7Xxs.js +10 -0
  69. package/dist/ui/assets/{trace-browser-qfYPcdR2.js.map → trace-browser-D93H7Xxs.js.map} +1 -1
  70. package/dist/ui/assets/transcript-item-BadzBIzE.js +3 -0
  71. package/dist/ui/assets/transcript-item-BadzBIzE.js.map +1 -0
  72. package/dist/ui/assets/with-selector-CRZWaZzC.js +2 -0
  73. package/dist/ui/assets/with-selector-CRZWaZzC.js.map +1 -0
  74. package/dist/ui/index.html +19 -2
  75. package/package.json +11 -7
  76. package/dist/ui/assets/ArrowClockwise.es-DMJj7Hv0.js +0 -2
  77. package/dist/ui/assets/agents-page-CbQpO3Tg.js +0 -2
  78. package/dist/ui/assets/index-D-CbFx6c.css +0 -1
  79. package/dist/ui/assets/index-DizotQot.js +0 -119
  80. package/dist/ui/assets/index-DizotQot.js.map +0 -1
  81. package/dist/ui/assets/knowledge-page-C8zg8uTL.js +0 -2
  82. package/dist/ui/assets/mcps-page-gNYeqSPB.js +0 -2
  83. package/dist/ui/assets/pipelines-page-hTEhFZGg.js +0 -8
  84. package/dist/ui/assets/sandboxes-page-DtPnuza7.js +0 -2
  85. package/dist/ui/assets/sandboxes-page-DtPnuza7.js.map +0 -1
  86. package/dist/ui/assets/sessions-page-DBzoP7MV.js +0 -2
  87. package/dist/ui/assets/status-page-Bf1lq444.js +0 -2
  88. package/dist/ui/assets/tool-runner-BbUlfPEO.js +0 -2
  89. package/dist/ui/assets/tools-page-BgUz-79v.js +0 -2
  90. package/dist/ui/assets/trace-browser-qfYPcdR2.js +0 -10
  91. package/dist/ui/assets/transcript-item-HFuuqmKM.js +0 -3
  92. package/dist/ui/assets/transcript-item-HFuuqmKM.js.map +0 -1
package/dist/index.js CHANGED
@@ -3,6 +3,7 @@ import { Agent, getAgentToolState as getAgentToolState3 } from "@anvia/core/inte
3
3
  import { Pipeline } from "@anvia/core/pipeline";
4
4
  import { serve } from "@hono/node-server";
5
5
  import { Hono as HonoApp } from "hono";
6
+ import { WebSocketServer } from "ws";
6
7
 
7
8
  // src/runtime/json.ts
8
9
  function toJsonValue(value) {
@@ -159,6 +160,20 @@ var StudioRunTraceObserver = class {
159
160
  this.observations.push(parentObservation);
160
161
  this.observations.push(...childTrace.observations(parentObservation.id));
161
162
  },
163
+ suspend: (suspendArgs) => {
164
+ this.observations.push(
165
+ traceObservation({
166
+ kind: "tool",
167
+ name: args.toolName,
168
+ status: "suspended",
169
+ turn: args.turn,
170
+ startedAt,
171
+ input: parseOrString(args.args),
172
+ output: toJsonValue({ interaction: suspendArgs.interaction }),
173
+ metadata: toolMetadata(args, false)
174
+ })
175
+ );
176
+ },
162
177
  error: (errorArgs) => {
163
178
  const parentObservation = traceObservation({
164
179
  kind: "tool",
@@ -176,14 +191,23 @@ var StudioRunTraceObserver = class {
176
191
  };
177
192
  }
178
193
  async end(args) {
179
- await this.save("success", {
194
+ const result = {
195
+ runId: this.props.args.runId,
196
+ status: args.status,
197
+ text: args.text
198
+ };
199
+ if (args.resumedFrom !== void 0) result.resumedFrom = args.resumedFrom;
200
+ if (args.status === "completed") {
201
+ result.output = toJsonValue(args.output);
202
+ } else if (args.status === "blocked") {
203
+ result.stage = args.stage;
204
+ } else {
205
+ result.interaction = toJsonValue(args.interaction);
206
+ }
207
+ await this.save(args.status === "suspended" ? "suspended" : "success", {
180
208
  endedAt: /* @__PURE__ */ new Date(),
181
209
  output: args.status === "completed" ? traceOutput(args.output) : args.text,
182
- result: {
183
- status: args.status,
184
- text: args.text,
185
- ...args.status === "completed" ? { output: toJsonValue(args.output) } : { stage: args.stage }
186
- },
210
+ result,
187
211
  usage: args.usage,
188
212
  messages: toJsonValue(args.messages)
189
213
  });
@@ -1319,7 +1343,7 @@ function requestModalities(request) {
1319
1343
  return modalities;
1320
1344
  }
1321
1345
  function requestMessages(request) {
1322
- return request.messages;
1346
+ return request.type === "messages" ? request.messages : [];
1323
1347
  }
1324
1348
  function normalizeProviderId(id) {
1325
1349
  const trimmed = id.trim();
@@ -1611,6 +1635,7 @@ function isEventType(value) {
1611
1635
  import {
1612
1636
  customAgentEventsToClientStream
1613
1637
  } from "@anvia/client";
1638
+ import { parseAgentInteractionResponse } from "@anvia/core/agent";
1614
1639
  import {
1615
1640
  isJsonValue as isJsonValue2,
1616
1641
  parseMessages
@@ -1644,26 +1669,31 @@ function transcriptFromMessages(messages) {
1644
1669
  }
1645
1670
  }
1646
1671
  if (!textEntryAdded && attachments.length > 0) {
1647
- transcript.push({
1672
+ const entry = {
1648
1673
  entryId: transcript.length,
1649
1674
  kind: "message",
1650
1675
  role: "user",
1651
1676
  text: "",
1652
1677
  attachments
1653
- });
1678
+ };
1679
+ transcript.push(entry);
1654
1680
  }
1655
1681
  continue;
1656
1682
  }
1657
1683
  if (message.role === "tool") {
1658
1684
  for (const content of message.content) {
1659
- transcript.push({
1685
+ if (content.type !== "tool-result") {
1686
+ continue;
1687
+ }
1688
+ const entry = {
1660
1689
  entryId: transcript.length,
1661
1690
  kind: "tool",
1662
1691
  toolName: content.toolName,
1663
1692
  callId: content.callId ?? content.toolCallId,
1664
- result: toolResultText(content.output),
1665
- ...content.output.type === "content" ? { structuredResult: content.output.value } : {}
1666
- });
1693
+ result: toolResultText(content.output)
1694
+ };
1695
+ if (content.output.type === "content") entry.structuredResult = content.output.value;
1696
+ transcript.push(entry);
1667
1697
  }
1668
1698
  continue;
1669
1699
  }
@@ -1774,46 +1804,6 @@ var AsyncEventQueue = class {
1774
1804
  return new Promise((resolve2) => this.resolvers.push(resolve2));
1775
1805
  }
1776
1806
  };
1777
- async function* mergeRunAndApprovalEvents(runEvents, approvalEvents) {
1778
- const runIterator = runEvents[Symbol.asyncIterator]();
1779
- let runDone = false;
1780
- let runNext = runIterator.next();
1781
- let approvalNext = approvalEvents.next();
1782
- try {
1783
- while (runNext !== void 0 || approvalNext !== void 0) {
1784
- const pending = [];
1785
- if (runNext !== void 0) {
1786
- pending.push(runNext.then((value) => ({ source: "run", value })));
1787
- }
1788
- if (approvalNext !== void 0) {
1789
- pending.push(approvalNext.then((value) => ({ source: "approval", value })));
1790
- }
1791
- const result = await Promise.race(pending);
1792
- if (result.source === "run") {
1793
- if (result.value.done === true) {
1794
- runDone = true;
1795
- runNext = void 0;
1796
- approvalEvents.close();
1797
- } else {
1798
- runNext = runIterator.next();
1799
- yield result.value.value;
1800
- }
1801
- continue;
1802
- }
1803
- if (result.value.done === true) {
1804
- approvalNext = void 0;
1805
- } else {
1806
- approvalNext = approvalEvents.next();
1807
- yield result.value.value;
1808
- }
1809
- }
1810
- } finally {
1811
- if (!runDone && runIterator.return !== void 0) {
1812
- await runIterator.return();
1813
- }
1814
- approvalEvents.close();
1815
- }
1816
- }
1817
1807
  function streamAgentRunEvents(_c, events, options) {
1818
1808
  return streamStudioClient({
1819
1809
  events: customAgentEventsToClientStream({
@@ -1824,52 +1814,6 @@ function streamAgentRunEvents(_c, events, options) {
1824
1814
  });
1825
1815
  }
1826
1816
  function studioEventToClientEvent(event, context) {
1827
- if (event.type === "tool_approval_request" || event.type === "tool_approval_result") {
1828
- return {
1829
- type: "tool_approval",
1830
- runId: context.runId,
1831
- ...context.turn === void 0 ? {} : { turn: context.turn },
1832
- ...context.scope === void 0 ? {} : { scope: context.scope },
1833
- approval: {
1834
- id: event.approval.id,
1835
- runId: event.approval.runId,
1836
- agentId: event.approval.agentId,
1837
- ...event.approval.sessionId === void 0 ? {} : { sessionId: event.approval.sessionId },
1838
- toolName: event.approval.toolName,
1839
- ...event.approval.callId === void 0 ? {} : { callId: event.approval.callId },
1840
- internalCallId: event.approval.internalCallId,
1841
- input: parseJsonOrString(event.approval.args),
1842
- status: event.approval.status,
1843
- requestedAt: event.approval.requestedAt,
1844
- ...event.approval.resolvedAt === void 0 ? {} : { resolvedAt: event.approval.resolvedAt },
1845
- ...event.approval.reason === void 0 ? {} : { reason: event.approval.reason }
1846
- }
1847
- };
1848
- }
1849
- if (event.type === "tool_question_request" || event.type === "tool_question_result") {
1850
- return {
1851
- type: "tool_question",
1852
- runId: context.runId,
1853
- ...context.turn === void 0 ? {} : { turn: context.turn },
1854
- ...context.scope === void 0 ? {} : { scope: context.scope },
1855
- question: {
1856
- id: event.question.id,
1857
- runId: event.question.runId,
1858
- agentId: event.question.agentId,
1859
- ...event.question.sessionId === void 0 ? {} : { sessionId: event.question.sessionId },
1860
- toolName: event.question.toolName,
1861
- ...event.question.callId === void 0 ? {} : { callId: event.question.callId },
1862
- internalCallId: event.question.internalCallId,
1863
- input: parseJsonOrString(event.question.args),
1864
- questions: event.question.questions,
1865
- status: event.question.status,
1866
- requestedAt: event.question.requestedAt,
1867
- ...event.question.answeredAt === void 0 ? {} : { answeredAt: event.question.answeredAt },
1868
- ...event.question.cancelledAt === void 0 ? {} : { cancelledAt: event.question.cancelledAt },
1869
- ...event.question.answers === void 0 ? {} : { answers: event.question.answers }
1870
- }
1871
- };
1872
- }
1873
1817
  if (event.type === "session_log" && isJsonValue2(event.log)) {
1874
1818
  return {
1875
1819
  type: "data",
@@ -1898,14 +1842,6 @@ function studioEventToClientEvent(event, context) {
1898
1842
  }
1899
1843
  return void 0;
1900
1844
  }
1901
- function parseJsonOrString(value) {
1902
- try {
1903
- const parsed = JSON.parse(value);
1904
- return isJsonValue2(parsed) ? parsed : value;
1905
- } catch {
1906
- return value;
1907
- }
1908
- }
1909
1845
  function withAgentRunCancellation(events, onCancel) {
1910
1846
  const iterator = events[Symbol.asyncIterator]();
1911
1847
  let done = false;
@@ -2014,7 +1950,7 @@ function createPersistedStreamingSessionTranscript(props) {
2014
1950
  if (event.type === "final" || event.type === "error") {
2015
1951
  assignTranscriptRunDuration(transcript, Date.now() - props.startedAt);
2016
1952
  }
2017
- const eventStatus = event.type === "final" ? "success" : event.type === "error" ? "error" : "running";
1953
+ const eventStatus = event.type === "final" ? event.result.status === "suspended" ? "suspended" : "success" : event.type === "error" ? "error" : "running";
2018
1954
  const saveInput = {
2019
1955
  id: props.session.id,
2020
1956
  runId: props.runId,
@@ -2024,7 +1960,7 @@ function createPersistedStreamingSessionTranscript(props) {
2024
1960
  };
2025
1961
  if (event.type === "error") saveInput.error = serializeError(event.error);
2026
1962
  await saveTranscript(saveInput);
2027
- const generatedMessages = event.type === "final" ? event.result.messages.slice(1) : [];
1963
+ const generatedMessages = event.type === "final" ? event.result.messages.slice(props.generatedMessagesStartIndex ?? 1) : [];
2028
1964
  if (props.persistGeneratedMessages === true && generatedMessages.length > 0) {
2029
1965
  await props.store.append({
2030
1966
  scope: { sessionId: props.session.id },
@@ -2172,71 +2108,63 @@ function acceptTranscriptStreamEvent(transcript, event) {
2172
2108
  }
2173
2109
  appendChildAgentTranscriptEvent(matched, event);
2174
2110
  }
2175
- if (event.type === "tool_approval_request") {
2176
- const matched = findTranscriptToolEntry(
2177
- transcript,
2178
- event.approval.toolName,
2179
- approvalCallId(event.approval)
2180
- );
2181
- if (matched !== void 0) {
2182
- matched.approval = {
2183
- id: event.approval.id,
2184
- status: event.approval.status,
2185
- requestedAt: event.approval.requestedAt
2186
- };
2187
- }
2188
- }
2189
- if (event.type === "tool_approval_result") {
2111
+ if (event.type === "final" && event.result.status === "suspended") {
2112
+ const interaction = event.result.interaction;
2190
2113
  const matched = findTranscriptToolEntry(
2191
2114
  transcript,
2192
- event.approval.toolName,
2193
- approvalCallId(event.approval)
2115
+ interaction.toolName,
2116
+ interaction.callId ?? interaction.toolCallId
2194
2117
  );
2195
- if (matched !== void 0) {
2118
+ if (matched !== void 0 && interaction.type === "tool-approval") {
2196
2119
  const approval = {
2197
- id: event.approval.id,
2198
- status: event.approval.status,
2199
- requestedAt: event.approval.requestedAt
2120
+ id: interaction.id,
2121
+ status: "pending",
2122
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
2200
2123
  };
2201
- if (event.approval.resolvedAt !== void 0) approval.resolvedAt = event.approval.resolvedAt;
2202
- if (event.approval.reason !== void 0) approval.reason = event.approval.reason;
2124
+ if (interaction.reason !== void 0) approval.reason = interaction.reason;
2203
2125
  matched.approval = approval;
2204
2126
  }
2205
- }
2206
- if (event.type === "tool_question_request") {
2207
- const matched = findTranscriptToolEntry(
2208
- transcript,
2209
- event.question.toolName,
2210
- questionCallId(event.question)
2211
- );
2212
- if (matched !== void 0) {
2127
+ if (matched !== void 0 && interaction.type === "tool-question") {
2213
2128
  matched.question = {
2214
- id: event.question.id,
2215
- status: event.question.status,
2216
- requestedAt: event.question.requestedAt,
2217
- questions: event.question.questions
2129
+ id: interaction.id,
2130
+ status: "pending",
2131
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
2132
+ questions: interaction.questions.map((question) => ({
2133
+ id: question.id,
2134
+ question: question.text,
2135
+ choices: [...question.choices ?? []],
2136
+ allowCustom: question.choices === void 0 || question.allowCustom === true
2137
+ }))
2218
2138
  };
2219
2139
  }
2220
2140
  }
2221
- if (event.type === "tool_question_result") {
2141
+ if (event.type === "interaction_response") {
2142
+ const response = event.response;
2222
2143
  const matched = findTranscriptToolEntry(
2223
2144
  transcript,
2224
- event.question.toolName,
2225
- questionCallId(event.question)
2145
+ response.toolName,
2146
+ response.callId ?? response.toolCallId
2226
2147
  );
2227
- if (matched !== void 0) {
2228
- const question = {
2229
- id: event.question.id,
2230
- status: event.question.status,
2231
- requestedAt: event.question.requestedAt,
2232
- questions: event.question.questions
2148
+ const respondedAt = (/* @__PURE__ */ new Date()).toISOString();
2149
+ if (matched?.approval !== void 0 && response.type === "tool-approval-response") {
2150
+ const approval = {
2151
+ ...matched.approval,
2152
+ status: response.approved ? "approved" : "rejected",
2153
+ resolvedAt: respondedAt
2154
+ };
2155
+ if (response.reason !== void 0) approval.reason = response.reason;
2156
+ matched.approval = approval;
2157
+ }
2158
+ if (matched?.question !== void 0 && response.type === "tool-question-response") {
2159
+ matched.question = {
2160
+ ...matched.question,
2161
+ status: "answered",
2162
+ answeredAt: respondedAt,
2163
+ answers: response.answers.map((answer) => ({
2164
+ questionId: answer.questionId,
2165
+ answer: answer.value
2166
+ }))
2233
2167
  };
2234
- if (event.question.answeredAt !== void 0) question.answeredAt = event.question.answeredAt;
2235
- if (event.question.cancelledAt !== void 0) {
2236
- question.cancelledAt = event.question.cancelledAt;
2237
- }
2238
- if (event.question.answers !== void 0) question.answers = event.question.answers;
2239
- matched.question = question;
2240
2168
  }
2241
2169
  }
2242
2170
  if (event.type === "final" && event.result.trace?.traceId !== void 0) {
@@ -2246,12 +2174,6 @@ function acceptTranscriptStreamEvent(transcript, event) {
2246
2174
  appendTranscriptAssistantError(transcript, errorText(event.error));
2247
2175
  }
2248
2176
  }
2249
- function approvalCallId(approval) {
2250
- return approval.callId ?? approval.toolCallId;
2251
- }
2252
- function questionCallId(question) {
2253
- return question.callId ?? question.toolCallId;
2254
- }
2255
2177
  function appendChildAgentTranscriptEvent(entry, event) {
2256
2178
  const childEvent = childAgentTranscriptEvent(event);
2257
2179
  if (childEvent === void 0) {
@@ -2524,6 +2446,42 @@ async function parseRunRequest(c) {
2524
2446
  return parseRunRequestOptions(c, body, request);
2525
2447
  }
2526
2448
  function parseRunRequestBody(c, body) {
2449
+ if (body.type === "interaction_response") {
2450
+ if (typeof body.interactionId !== "string" || body.interactionId.trim().length === 0) {
2451
+ return { error: errorResponse(c, 400, "bad_request", "interactionId must be a string") };
2452
+ }
2453
+ if (body.messages !== void 0 || body.sessionId !== void 0 || body.model !== void 0) {
2454
+ return {
2455
+ error: errorResponse(
2456
+ c,
2457
+ 400,
2458
+ "bad_request",
2459
+ "Interaction responses cannot include messages, sessionId, or model"
2460
+ )
2461
+ };
2462
+ }
2463
+ try {
2464
+ return {
2465
+ type: "interaction_response",
2466
+ interactionId: body.interactionId,
2467
+ response: parseAgentInteractionResponse(body.response)
2468
+ };
2469
+ } catch {
2470
+ return {
2471
+ error: errorResponse(c, 400, "bad_request", "response must be an interaction response")
2472
+ };
2473
+ }
2474
+ }
2475
+ if (body.type !== "messages") {
2476
+ return {
2477
+ error: errorResponse(
2478
+ c,
2479
+ 400,
2480
+ "bad_request",
2481
+ 'type must be "messages" or "interaction_response"'
2482
+ )
2483
+ };
2484
+ }
2527
2485
  let messages;
2528
2486
  try {
2529
2487
  messages = parseMessages(body.messages);
@@ -2548,10 +2506,10 @@ function parseRunRequestBody(c, body) {
2548
2506
  error: errorResponse(c, 400, "bad_request", "The final message must be user-authored")
2549
2507
  };
2550
2508
  }
2551
- return { messages };
2509
+ return { type: "messages", messages };
2552
2510
  }
2553
2511
  function parseRunRequestOptions(c, body, request) {
2554
- if ("sessionId" in body) {
2512
+ if (request.type === "messages" && "sessionId" in body) {
2555
2513
  if (typeof body.sessionId !== "string" || body.sessionId.trim().length === 0) {
2556
2514
  return { error: errorResponse(c, 400, "bad_request", "sessionId must be a string") };
2557
2515
  }
@@ -2568,7 +2526,7 @@ function parseRunRequestOptions(c, body, request) {
2568
2526
  }
2569
2527
  request.stream = body.stream;
2570
2528
  }
2571
- if ("maxTurns" in body) {
2529
+ if (request.type === "messages" && "maxTurns" in body) {
2572
2530
  if (!isNonNegativeInteger(body.maxTurns)) {
2573
2531
  return {
2574
2532
  error: errorResponse(c, 400, "bad_request", "maxTurns must be a non-negative integer")
@@ -2576,7 +2534,7 @@ function parseRunRequestOptions(c, body, request) {
2576
2534
  }
2577
2535
  request.maxTurns = body.maxTurns;
2578
2536
  }
2579
- if ("toolConcurrency" in body) {
2537
+ if (request.type === "messages" && "toolConcurrency" in body) {
2580
2538
  if (!isPositiveInteger(body.toolConcurrency)) {
2581
2539
  return {
2582
2540
  error: errorResponse(c, 400, "bad_request", "toolConcurrency must be a positive integer")
@@ -2584,7 +2542,7 @@ function parseRunRequestOptions(c, body, request) {
2584
2542
  }
2585
2543
  request.toolConcurrency = body.toolConcurrency;
2586
2544
  }
2587
- if ("model" in body) {
2545
+ if (request.type === "messages" && "model" in body) {
2588
2546
  if (isObject(body.model) && typeof body.model.providerId === "string" && typeof body.model.modelId === "string") {
2589
2547
  request.model = {
2590
2548
  providerId: body.model.providerId,
@@ -2877,102 +2835,78 @@ function logsFromStreamEvent(props) {
2877
2835
  }
2878
2836
  if (event.type === "final") {
2879
2837
  const result = event.result;
2880
- return [
2881
- runCompletedLog({
2882
- sessionId,
2883
- runId,
2884
- durationMs: Date.now() - props.startedAt,
2885
- usage: result.usage,
2886
- output: result.text,
2887
- messageCount: result.messages.length
2888
- }),
2889
- memorySavedLog({ sessionId, runId, messageCount: result.messages.length })
2890
- ];
2891
- }
2892
- if (event.type === "error") {
2893
- return [runFailedLog(sessionId, runId, event.error, props.startedAt)];
2894
- }
2895
- if (event.type === "tool_approval_request") {
2896
- const metadata = {
2897
- approvalId: event.approval.id,
2898
- toolName: event.approval.toolName,
2899
- status: event.approval.status,
2900
- hasReason: event.approval.reason !== void 0,
2901
- argumentBytes: byteLength2(event.approval.args)
2902
- };
2903
- if (event.approval.callId !== void 0) metadata.callId = event.approval.callId;
2904
- return [
2905
- {
2838
+ const logs = [];
2839
+ if (result.status === "suspended") {
2840
+ logs.push({
2906
2841
  sessionId,
2907
2842
  runId,
2908
2843
  level: "info",
2909
- category: "approval",
2910
- event: "approval.requested",
2911
- message: `Approval requested for ${event.approval.toolName}`,
2912
- metadata
2913
- }
2914
- ];
2915
- }
2916
- if (event.type === "tool_approval_result") {
2917
- const metadata = {
2918
- approvalId: event.approval.id,
2919
- toolName: event.approval.toolName,
2920
- status: event.approval.status,
2921
- hasReason: event.approval.reason !== void 0
2922
- };
2923
- if (event.approval.callId !== void 0) metadata.callId = event.approval.callId;
2924
- return [
2925
- {
2926
- sessionId,
2927
- runId,
2928
- level: event.approval.status === "approved" ? "info" : "warn",
2929
- category: "approval",
2930
- event: "approval.resolved",
2931
- message: `Approval ${event.approval.status} for ${event.approval.toolName}`,
2932
- metadata
2933
- }
2934
- ];
2935
- }
2936
- if (event.type === "tool_question_request") {
2937
- const metadata = {
2938
- questionId: event.question.id,
2939
- toolName: event.question.toolName,
2940
- status: event.question.status,
2941
- questionCount: event.question.questions.length,
2942
- argumentBytes: byteLength2(event.question.args)
2943
- };
2944
- if (event.question.callId !== void 0) metadata.callId = event.question.callId;
2945
- return [
2946
- {
2844
+ category: "run",
2845
+ event: "run.suspended",
2846
+ message: `Run suspended for ${result.interaction.type}`,
2847
+ metadata: {
2848
+ interactionId: result.interaction.id,
2849
+ interactionType: result.interaction.type,
2850
+ toolName: result.interaction.toolName,
2851
+ sourceRunId: result.continuation.sourceRunId
2852
+ }
2853
+ });
2854
+ } else {
2855
+ logs.push(
2856
+ runCompletedLog({
2857
+ sessionId,
2858
+ runId,
2859
+ durationMs: Date.now() - props.startedAt,
2860
+ usage: result.usage,
2861
+ output: result.text,
2862
+ messageCount: result.messages.length
2863
+ })
2864
+ );
2865
+ }
2866
+ if (result.resumedFrom !== void 0) {
2867
+ logs.push({
2947
2868
  sessionId,
2948
2869
  runId,
2949
2870
  level: "info",
2950
- category: "question",
2951
- event: "question.requested",
2952
- message: `Question requested by ${event.question.toolName}`,
2953
- metadata
2954
- }
2955
- ];
2871
+ category: "run",
2872
+ event: "interaction.resumed",
2873
+ message: "Interaction resumed in a linked run",
2874
+ metadata: {
2875
+ interactionId: result.resumedFrom.interactionId,
2876
+ sourceRunId: result.resumedFrom.runId,
2877
+ resumedRunId: result.runId
2878
+ }
2879
+ });
2880
+ }
2881
+ logs.push(memorySavedLog({ sessionId, runId, messageCount: result.messages.length }));
2882
+ return logs;
2883
+ }
2884
+ if (event.type === "error") {
2885
+ return [runFailedLog(sessionId, runId, event.error, props.startedAt)];
2956
2886
  }
2957
- if (event.type === "tool_question_result") {
2958
- const cancelled = event.question.status === "cancelled";
2887
+ if (event.type === "interaction_response") {
2888
+ const response = event.response;
2959
2889
  const metadata = {
2960
- questionId: event.question.id,
2961
- toolName: event.question.toolName,
2962
- status: event.question.status,
2963
- questionCount: event.question.questions.length,
2964
- argumentBytes: byteLength2(event.question.args),
2965
- answerCount: event.question.answers?.length ?? 0
2890
+ interactionId: response.interactionId,
2891
+ interactionType: response.type === "tool-approval-response" ? "tool-approval" : "tool-question",
2892
+ toolName: response.toolName,
2893
+ sourceRunId: event.sourceRunId
2966
2894
  };
2967
- if (event.question.callId !== void 0) metadata.callId = event.question.callId;
2895
+ if (response.callId !== void 0) metadata.callId = response.callId;
2896
+ if (response.type === "tool-approval-response") {
2897
+ metadata.approved = response.approved;
2898
+ metadata.hasReason = response.reason !== void 0;
2899
+ } else {
2900
+ metadata.answerCount = response.answers.length;
2901
+ }
2968
2902
  return [
2969
2903
  {
2970
2904
  sessionId,
2971
2905
  runId,
2972
2906
  level: "info",
2973
- category: "question",
2974
- event: cancelled ? "question.cancelled" : "question.answered",
2975
- message: cancelled ? `Question cancelled for ${event.question.toolName}` : `Question answered for ${event.question.toolName}`,
2907
+ category: "run",
2908
+ event: "interaction.responded",
2909
+ message: `Interaction response received for ${response.toolName}`,
2976
2910
  metadata
2977
2911
  }
2978
2912
  ];
@@ -3167,6 +3101,56 @@ async function prepareAgentRun(c, props) {
3167
3101
  if ("error" in body) {
3168
3102
  return body.error;
3169
3103
  }
3104
+ if (body.type === "interaction_response") {
3105
+ const registration = props.continuationRegistry.take(
3106
+ body.interactionId,
3107
+ agentId,
3108
+ body.response
3109
+ );
3110
+ if (registration.status === "missing") {
3111
+ return errorResponse(c, 404, "not_found", "Interaction continuation is unavailable");
3112
+ }
3113
+ if (registration.status === "claimed") {
3114
+ return errorResponse(c, 409, "conflict", "Interaction response was already claimed");
3115
+ }
3116
+ if (registration.status === "invalid") {
3117
+ return errorResponse(c, 400, "bad_request", registration.error.message);
3118
+ }
3119
+ const { registration: claimed } = registration;
3120
+ const source = claimed.context;
3121
+ const runId2 = globalThis.crypto.randomUUID();
3122
+ let resumedBody = {
3123
+ ...source.body
3124
+ };
3125
+ if (body.stream !== void 0) resumedBody = { ...resumedBody, stream: body.stream };
3126
+ if (body.metadata !== void 0) resumedBody = { ...resumedBody, metadata: body.metadata };
3127
+ if (body.trace !== void 0) resumedBody = { ...resumedBody, trace: body.trace };
3128
+ const runOptions = createRunOptions(resumedBody, agentId, source.session, c.req.raw.signal);
3129
+ const execution2 = {
3130
+ generate: (options) => source.runAgent.generate({
3131
+ continuation: claimed.continuation,
3132
+ response: body.response,
3133
+ ...options
3134
+ }),
3135
+ stream: (options) => source.runAgent.stream({
3136
+ continuation: claimed.continuation,
3137
+ response: body.response,
3138
+ ...options
3139
+ })
3140
+ };
3141
+ return {
3142
+ ...source,
3143
+ body: resumedBody,
3144
+ execution: execution2,
3145
+ failureMessages: void 0,
3146
+ memoryCompactionLogged: false,
3147
+ options: runOptions,
3148
+ runId: runId2,
3149
+ runStartedAt: Date.now(),
3150
+ generatedMessagesStartIndex: 0,
3151
+ shouldPersistSessionMessages: source.shouldPersistSessionMessages
3152
+ };
3153
+ }
3170
3154
  const session = await resolveRunSession(c, body, agentId, props.stores);
3171
3155
  if (session instanceof Response) {
3172
3156
  return session;
@@ -3224,6 +3208,7 @@ async function prepareAgentRun(c, props) {
3224
3208
  runAgent,
3225
3209
  runId,
3226
3210
  runStartedAt,
3211
+ generatedMessagesStartIndex: 1,
3227
3212
  selectedModel,
3228
3213
  session,
3229
3214
  sessionStore,
@@ -3336,8 +3321,10 @@ function createRunExecution(props) {
3336
3321
  }
3337
3322
  function createRunOptions(body, agentId, session, abortSignal) {
3338
3323
  const options = { abortSignal };
3339
- if (body.maxTurns !== void 0) options.maxTurns = body.maxTurns;
3340
- if (body.toolConcurrency !== void 0) options.toolConcurrency = body.toolConcurrency;
3324
+ if (body.type === "messages" && body.maxTurns !== void 0) options.maxTurns = body.maxTurns;
3325
+ if (body.type === "messages" && body.toolConcurrency !== void 0) {
3326
+ options.toolConcurrency = body.toolConcurrency;
3327
+ }
3341
3328
  if (body.trace !== void 0) {
3342
3329
  options.trace = traceForRun(body.trace, agentId, session);
3343
3330
  } else if (session !== void 0) {
@@ -3346,34 +3333,11 @@ function createRunOptions(body, agentId, session, abortSignal) {
3346
3333
  return options;
3347
3334
  }
3348
3335
  function handleStreamingAgentRun(c, run, props) {
3349
- const runtimeEvents = new AsyncEventQueue();
3350
- const approvalContext = {
3351
- runId: run.runId,
3352
- agentId: run.agentId,
3353
- emit: (event) => runtimeEvents.push(event)
3354
- };
3355
- if (run.session !== void 0) approvalContext.sessionId = run.session.id;
3356
- if (run.body.metadata !== void 0) approvalContext.metadata = run.body.metadata;
3357
- const questionContext = {
3358
- runId: run.runId,
3359
- agentId: run.agentId,
3360
- emit: (event) => runtimeEvents.push(event)
3361
- };
3362
- if (run.session !== void 0) questionContext.sessionId = run.session.id;
3363
- if (run.body.metadata !== void 0) questionContext.metadata = run.body.metadata;
3364
- const effectiveHook = props.questionRuntime.createHook(questionContext);
3365
- const streamOptions = withInternalAgentRunOptions(
3366
- { ...run.options },
3367
- { hook: effectiveHook, runId: run.runId }
3368
- );
3369
- const runStream = mergeRunAndApprovalEvents(
3370
- resumeStreamingApprovals(
3371
- run.runAgent,
3372
- run.execution.stream(streamOptions),
3373
- props.approvalRuntime,
3374
- approvalContext
3375
- ),
3376
- runtimeEvents
3336
+ const streamOptions = withInternalAgentRunOptions({ ...run.options }, { runId: run.runId });
3337
+ const runStream = registerStreamingContinuation(
3338
+ run.execution.stream(streamOptions),
3339
+ props.continuationRegistry,
3340
+ run
3377
3341
  );
3378
3342
  let stream = runStream;
3379
3343
  let persistedRun;
@@ -3391,15 +3355,14 @@ function handleStreamingAgentRun(c, run, props) {
3391
3355
  message: run.body.messages.at(-1),
3392
3356
  runId: run.runId,
3393
3357
  startedAt: run.runStartedAt,
3394
- persistGeneratedMessages: run.shouldPersistSessionMessages
3358
+ persistGeneratedMessages: run.shouldPersistSessionMessages,
3359
+ generatedMessagesStartIndex: run.generatedMessagesStartIndex
3395
3360
  });
3396
3361
  stream = persistedRun.events;
3397
3362
  }
3398
3363
  return streamAgentRunEvents(c, stream, {
3399
3364
  runId: run.runId,
3400
3365
  onCancel: async () => {
3401
- props.approvalRuntime.cancelRun(run.runId);
3402
- props.questionRuntime.cancelRun(run.runId);
3403
3366
  const persistence = [];
3404
3367
  if (persistedRun !== void 0) {
3405
3368
  persistence.push(persistedRun.cancel());
@@ -3418,35 +3381,29 @@ function handleStreamingAgentRun(c, run, props) {
3418
3381
  }
3419
3382
  async function handleBufferedAgentRun(c, run, props) {
3420
3383
  try {
3421
- const runtimeOptions = await startBufferedSessionRun(run, props);
3422
- let result = await run.execution.generate(runtimeOptions);
3423
- const approvalContext = createApprovalContext(run);
3424
- while (result.status === "approval_required") {
3425
- const decision = await props.approvalRuntime.request(approvalContext, result.approval);
3426
- result = await run.runAgent.resume(result, decision);
3427
- }
3428
- const response = result;
3429
- await completeBufferedSessionRun(run, response);
3430
- return c.json(response);
3384
+ const runtimeOptions = await startBufferedSessionRun(run);
3385
+ const result = await run.execution.generate(runtimeOptions);
3386
+ if (result.status === "suspended") {
3387
+ props.continuationRegistry.register({ continuation: result.continuation, context: run });
3388
+ }
3389
+ await completeBufferedSessionRun(run, result);
3390
+ return c.json(agentRunResponse(result));
3431
3391
  } catch (error) {
3432
3392
  await failBufferedSessionRun(run, error);
3433
3393
  return errorResponse(c, 500, "internal_error", "Agent run failed", serializeError(error));
3434
3394
  }
3435
3395
  }
3436
- async function startBufferedSessionRun(run, props) {
3396
+ function agentRunResponse(result) {
3397
+ if (result.status !== "suspended") return result;
3398
+ const { continuation: _continuation, messages: _messages, ...response } = result;
3399
+ return response;
3400
+ }
3401
+ async function startBufferedSessionRun(run) {
3437
3402
  if (run.session !== void 0) {
3438
3403
  await appendSessionLog(run.sessionStore, runStartedLog(run.session, run.runId));
3439
3404
  await appendSessionLog(run.sessionStore, memoryLoadedLog(run.session, run.runId));
3440
3405
  }
3441
- const questionContext = {
3442
- runId: run.runId,
3443
- agentId: run.agentId
3444
- };
3445
- if (run.session !== void 0) questionContext.sessionId = run.session.id;
3446
- if (run.body.metadata !== void 0) questionContext.metadata = run.body.metadata;
3447
- const effectiveHook = props.questionRuntime.createHook(questionContext);
3448
3406
  const internalOptions = {
3449
- hook: effectiveHook,
3450
3407
  onFailure: ({ messages }) => {
3451
3408
  run.failureMessages = structuredClone(messages);
3452
3409
  },
@@ -3469,30 +3426,12 @@ async function startBufferedSessionRun(run, props) {
3469
3426
  }
3470
3427
  return withInternalAgentRunOptions({ ...run.options }, internalOptions);
3471
3428
  }
3472
- function createApprovalContext(run, emit) {
3473
- const context = {
3474
- runId: run.runId,
3475
- agentId: run.agentId
3476
- };
3477
- if (run.session !== void 0) context.sessionId = run.session.id;
3478
- if (run.body.metadata !== void 0) context.metadata = run.body.metadata;
3479
- if (emit !== void 0) context.emit = emit;
3480
- return context;
3481
- }
3482
- async function* resumeStreamingApprovals(agent, initialStream, approvalRuntime, context) {
3483
- let stream = initialStream;
3484
- while (true) {
3485
- let pending;
3486
- for await (const event of stream) {
3487
- if (event.type === "approval_required") {
3488
- pending = event;
3489
- break;
3490
- }
3491
- yield event;
3429
+ async function* registerStreamingContinuation(stream, registry, run) {
3430
+ for await (const event of stream) {
3431
+ if (event.type === "final" && event.result.status === "suspended") {
3432
+ registry.register({ continuation: event.result.continuation, context: run });
3492
3433
  }
3493
- if (pending === void 0) return;
3494
- const decision = await approvalRuntime.request(context, pending.approval);
3495
- stream = agent.resume(pending, decision);
3434
+ yield event;
3496
3435
  }
3497
3436
  }
3498
3437
  async function completeBufferedSessionRun(run, response) {
@@ -3500,7 +3439,7 @@ async function completeBufferedSessionRun(run, response) {
3500
3439
  return;
3501
3440
  }
3502
3441
  if (run.shouldPersistSessionMessages) {
3503
- const generatedMessages = response.messages.slice(1);
3442
+ const generatedMessages = response.messages.slice(run.generatedMessagesStartIndex);
3504
3443
  if (generatedMessages.length > 0) {
3505
3444
  await run.sessionStore.append({
3506
3445
  scope: { sessionId: run.session.id, metadata: run.memoryMetadata },
@@ -3518,7 +3457,7 @@ async function completeBufferedSessionRun(run, response) {
3518
3457
  runId: run.runId,
3519
3458
  ...optionalTitle(run.body.messages.at(-1)),
3520
3459
  transcript,
3521
- status: "success"
3460
+ status: response.status === "suspended" ? "suspended" : "success"
3522
3461
  });
3523
3462
  if (response.memoryCompaction !== void 0 && !run.memoryCompactionLogged) {
3524
3463
  await appendSessionLog(
@@ -3533,7 +3472,20 @@ async function completeBufferedSessionRun(run, response) {
3533
3472
  }
3534
3473
  await appendSessionLog(
3535
3474
  run.sessionStore,
3536
- runCompletedLog({
3475
+ response.status === "suspended" ? {
3476
+ sessionId: run.session.id,
3477
+ runId: run.runId,
3478
+ level: "info",
3479
+ category: "run",
3480
+ event: "run.suspended",
3481
+ message: `Run suspended for ${response.interaction.type}`,
3482
+ metadata: {
3483
+ interactionId: response.interaction.id,
3484
+ interactionType: response.interaction.type,
3485
+ toolName: response.interaction.toolName,
3486
+ sourceRunId: response.continuation.sourceRunId
3487
+ }
3488
+ } : runCompletedLog({
3537
3489
  sessionId: run.session.id,
3538
3490
  runId: run.runId,
3539
3491
  durationMs: durationMs2,
@@ -3542,6 +3494,46 @@ async function completeBufferedSessionRun(run, response) {
3542
3494
  messageCount: response.messages.length
3543
3495
  })
3544
3496
  );
3497
+ if (response.resumedFrom !== void 0) {
3498
+ const responseMessage = response.messages[0];
3499
+ const responsePart = responseMessage?.role === "tool" ? responseMessage.content[0] : void 0;
3500
+ if (responsePart !== void 0 && responsePart.type !== "tool-result") {
3501
+ const metadata = {
3502
+ interactionId: responsePart.interactionId,
3503
+ interactionType: responsePart.type === "tool-approval-response" ? "tool-approval" : "tool-question",
3504
+ toolName: responsePart.toolName,
3505
+ sourceRunId: response.resumedFrom.runId
3506
+ };
3507
+ if (responsePart.type === "tool-approval-response") {
3508
+ metadata.approved = responsePart.approved;
3509
+ metadata.hasReason = responsePart.reason !== void 0;
3510
+ } else {
3511
+ metadata.answerCount = responsePart.answers.length;
3512
+ }
3513
+ await appendSessionLog(run.sessionStore, {
3514
+ sessionId: run.session.id,
3515
+ runId: run.runId,
3516
+ level: "info",
3517
+ category: "run",
3518
+ event: "interaction.responded",
3519
+ message: `Interaction response received for ${responsePart.toolName}`,
3520
+ metadata
3521
+ });
3522
+ }
3523
+ await appendSessionLog(run.sessionStore, {
3524
+ sessionId: run.session.id,
3525
+ runId: run.runId,
3526
+ level: "info",
3527
+ category: "run",
3528
+ event: "interaction.resumed",
3529
+ message: "Interaction resumed in a linked run",
3530
+ metadata: {
3531
+ interactionId: response.resumedFrom.interactionId,
3532
+ sourceRunId: response.resumedFrom.runId,
3533
+ resumedRunId: response.runId
3534
+ }
3535
+ });
3536
+ }
3545
3537
  await appendSessionLog(
3546
3538
  run.sessionStore,
3547
3539
  memorySavedLog({
@@ -3577,286 +3569,48 @@ function usesStoreAsAgentMemory(agent, store) {
3577
3569
  return memoryStore !== void 0 && rawObservedStore(memoryStore) === rawObservedStore(store);
3578
3570
  }
3579
3571
 
3580
- // src/runtime/approvals.ts
3581
- import { getAgentApprovalRequestDetails } from "@anvia/core/internal/agent";
3572
+ // src/runtime/config.ts
3573
+ import { isQuestionTool } from "@anvia/core/tool";
3582
3574
 
3583
- // src/runtime/query.ts
3584
- function optionalQueryString(value) {
3585
- const trimmed = value?.trim();
3586
- return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3575
+ // src/runtime/eval-config.ts
3576
+ var CASE_PREVIEW_LIMIT = 5;
3577
+ var PREVIEW_VALUE_MAX_LENGTH = 500;
3578
+ function evalConfig(suite) {
3579
+ const casePreviews = suite.cases.slice(0, CASE_PREVIEW_LIMIT).map(casePreview);
3580
+ const metricSummaries = suite.metrics.map(metricSummary);
3581
+ const config = {
3582
+ id: suite.id ?? suite.name,
3583
+ name: suite.name,
3584
+ caseCount: suite.cases.length,
3585
+ metricNames: suite.metrics.map((metric) => metric.name),
3586
+ casePreviewCount: casePreviews.length,
3587
+ casePreviews,
3588
+ metricSummaries
3589
+ };
3590
+ if (suite.description !== void 0) config.description = suite.description;
3591
+ if (suite.concurrency !== void 0) config.concurrency = suite.concurrency;
3592
+ if (suite.metadata !== void 0) config.metadata = suite.metadata;
3593
+ return config;
3587
3594
  }
3588
- function parseLimit(value, defaultMax = 50, max = 100) {
3589
- if (value === void 0 || value.trim().length === 0) {
3590
- return defaultMax;
3591
- }
3592
- const limit = Number(value);
3593
- if (!Number.isInteger(limit) || limit <= 0) {
3594
- return void 0;
3595
+ function casePreview(testCase, index) {
3596
+ if (!isRecord2(testCase)) {
3597
+ return {
3598
+ id: `case-${index + 1}`,
3599
+ input: previewValue(testCase)
3600
+ };
3595
3601
  }
3596
- return Math.min(limit, max);
3602
+ const preview = {
3603
+ id: typeof testCase.id === "string" && testCase.id.length > 0 ? testCase.id : `case-${index + 1}`
3604
+ };
3605
+ if ("input" in testCase) preview.input = previewValue(testCase.input);
3606
+ if ("expected" in testCase) preview.expected = previewValue(testCase.expected);
3607
+ const keys = metadataKeys(testCase.metadata);
3608
+ if (keys !== void 0) preview.metadataKeys = keys;
3609
+ return preview;
3597
3610
  }
3598
- function parseTraceStatus(value) {
3599
- const status = optionalQueryString(value);
3600
- if (status === void 0) {
3601
- return void 0;
3602
- }
3603
- return status === "running" || status === "success" || status === "error" ? status : false;
3604
- }
3605
- function parseAfter(value) {
3606
- if (value === void 0 || value.trim().length === 0) {
3607
- return void 0;
3608
- }
3609
- const after = Number(value);
3610
- if (!Number.isInteger(after) || after < 0) {
3611
- return false;
3612
- }
3613
- return after;
3614
- }
3615
-
3616
- // src/runtime/approvals.ts
3617
- function registerApprovalRoutes(app, approvals) {
3618
- app.get("/approvals", (c) => {
3619
- const status = parseApprovalStatus(c.req.query("status"));
3620
- if (status === false) {
3621
- return errorResponse(c, 400, "bad_request", "status must be pending or resolved");
3622
- }
3623
- const options = {};
3624
- const runId = optionalQueryString(c.req.query("runId"));
3625
- const agentId = optionalQueryString(c.req.query("agentId"));
3626
- const sessionId = optionalQueryString(c.req.query("sessionId"));
3627
- if (status !== void 0) {
3628
- options.status = status;
3629
- }
3630
- if (runId !== void 0) {
3631
- options.runId = runId;
3632
- }
3633
- if (agentId !== void 0) {
3634
- options.agentId = agentId;
3635
- }
3636
- if (sessionId !== void 0) {
3637
- options.sessionId = sessionId;
3638
- }
3639
- return c.json({
3640
- approvals: approvals.list(options)
3641
- });
3642
- });
3643
- app.post("/approvals/:approvalId/decision", async (c) => {
3644
- const body = await parseApprovalDecisionRequest(c);
3645
- if ("error" in body) {
3646
- return body.error;
3647
- }
3648
- const result = approvals.decide(c.req.param("approvalId"), body);
3649
- if (result === "missing") {
3650
- return errorResponse(c, 404, "not_found", "Approval not found");
3651
- }
3652
- if (result === "resolved") {
3653
- return errorResponse(c, 409, "conflict", "Approval is already resolved");
3654
- }
3655
- return c.json(result);
3656
- });
3657
- }
3658
- function parseApprovalStatus(value) {
3659
- const status = optionalQueryString(value);
3660
- if (status === void 0) {
3661
- return void 0;
3662
- }
3663
- return status === "pending" || status === "resolved" ? status : false;
3664
- }
3665
- async function parseApprovalDecisionRequest(c) {
3666
- let body;
3667
- try {
3668
- body = await c.req.json();
3669
- } catch {
3670
- return { error: errorResponse(c, 400, "bad_request", "Request body must be JSON") };
3671
- }
3672
- if (!isObject(body)) {
3673
- return { error: errorResponse(c, 400, "bad_request", "Request body must be an object") };
3674
- }
3675
- if (typeof body.approved !== "boolean") {
3676
- return { error: errorResponse(c, 400, "bad_request", "approved must be a boolean") };
3677
- }
3678
- if ("reason" in body && typeof body.reason !== "string") {
3679
- return { error: errorResponse(c, 400, "bad_request", "reason must be a string") };
3680
- }
3681
- const reason = typeof body.reason === "string" && body.reason.trim().length > 0 ? body.reason.trim() : void 0;
3682
- return approvalDecision(body.approved, reason);
3683
- }
3684
- function createApprovalRuntime() {
3685
- const approvals = /* @__PURE__ */ new Map();
3686
- return {
3687
- approvals,
3688
- async request(context, approval) {
3689
- const decision = await requestApproval(approvals, context, approval);
3690
- return decision.approved ? { approved: true, ...decision.reason === void 0 ? {} : { reason: decision.reason } } : {
3691
- approved: false,
3692
- ...decision.reason === void 0 ? {} : { reason: decision.reason }
3693
- };
3694
- },
3695
- list(options) {
3696
- return [...approvals.values()].filter((approval) => {
3697
- if (options.status === "pending" && approval.status !== "pending") {
3698
- return false;
3699
- }
3700
- if (options.status === "resolved" && approval.status === "pending") {
3701
- return false;
3702
- }
3703
- if (options.runId !== void 0 && approval.runId !== options.runId) {
3704
- return false;
3705
- }
3706
- if (options.agentId !== void 0 && approval.agentId !== options.agentId) {
3707
- return false;
3708
- }
3709
- if (options.sessionId !== void 0 && approval.sessionId !== options.sessionId) {
3710
- return false;
3711
- }
3712
- return true;
3713
- }).map(publicApproval);
3714
- },
3715
- cancelRun(runId) {
3716
- const cancelled = [];
3717
- for (const approval of approvals.values()) {
3718
- if (!isPendingApproval(approval) || approval.runId !== runId) {
3719
- continue;
3720
- }
3721
- const resolved = resolveApproval(approval, "cancelled", {
3722
- reason: "Run cancelled in Anvia Studio."
3723
- });
3724
- approvals.set(approval.id, resolved);
3725
- approval.emit?.({ type: "tool_approval_result", approval: resolved });
3726
- approval.resolve(approvalDecision(false, resolved.reason));
3727
- cancelled.push(resolved);
3728
- }
3729
- return cancelled;
3730
- },
3731
- decide(id, decision) {
3732
- const approval = approvals.get(id);
3733
- if (approval === void 0) {
3734
- return "missing";
3735
- }
3736
- if (!isPendingApproval(approval)) {
3737
- return "resolved";
3738
- }
3739
- const reason = decision.approved ? decision.reason : decision.reason ?? approval.rejectMessage ?? "Rejected in Anvia Studio.";
3740
- const resolved = resolveApproval(
3741
- approval,
3742
- decision.approved ? "approved" : "rejected",
3743
- reason === void 0 ? {} : { reason }
3744
- );
3745
- approvals.set(id, resolved);
3746
- approval.emit?.({ type: "tool_approval_result", approval: resolved });
3747
- approval.resolve(approvalDecision(decision.approved, reason));
3748
- return publicApproval(resolved);
3749
- }
3750
- };
3751
- }
3752
- async function requestApproval(approvals, context, request) {
3753
- const id = request.id;
3754
- const details = getAgentApprovalRequestDetails(request);
3755
- const approval = {
3756
- id,
3757
- runId: context.runId,
3758
- agentId: context.agentId,
3759
- toolName: request.toolName,
3760
- internalCallId: details?.internalCallId ?? request.id,
3761
- args: details?.rawArgs ?? JSON.stringify(request.input) ?? "null",
3762
- status: "pending",
3763
- requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
3764
- resolve: () => {
3765
- }
3766
- };
3767
- if (context.sessionId !== void 0) approval.sessionId = context.sessionId;
3768
- if (request.toolCallId !== void 0) approval.callId = request.toolCallId;
3769
- if (request.reason !== void 0) approval.reason = request.reason;
3770
- if (details?.rejectMessage !== void 0) approval.rejectMessage = details.rejectMessage;
3771
- if (context.emit !== void 0) approval.emit = context.emit;
3772
- const decision = new Promise((resolve2) => {
3773
- approval.resolve = (decision2) => {
3774
- const current = approvals.get(id);
3775
- if (!isPendingApproval(current)) {
3776
- if (current !== void 0) {
3777
- resolve2(approvalDecision(current.status === "approved", current.reason));
3778
- }
3779
- return;
3780
- }
3781
- const reason = decision2.approved ? decision2.reason : decision2.reason ?? details?.rejectMessage ?? "Rejected in Anvia Studio.";
3782
- const resolved = resolveApproval(
3783
- current,
3784
- decision2.approved ? "approved" : "rejected",
3785
- reason === void 0 ? {} : { reason }
3786
- );
3787
- approvals.set(id, resolved);
3788
- context.emit?.({ type: "tool_approval_result", approval: resolved });
3789
- resolve2(approvalDecision(decision2.approved, reason));
3790
- };
3791
- });
3792
- approvals.set(id, approval);
3793
- context.emit?.({ type: "tool_approval_request", approval: publicApproval(approval) });
3794
- return decision;
3795
- }
3796
- function isPendingApproval(approval) {
3797
- return approval !== void 0 && approval.status === "pending" && "resolve" in approval;
3798
- }
3799
- function resolveApproval(approval, status, options = {}) {
3800
- const resolved = {
3801
- ...approval,
3802
- status,
3803
- resolvedAt: (/* @__PURE__ */ new Date()).toISOString()
3804
- };
3805
- if (options.reason !== void 0) resolved.reason = options.reason;
3806
- return publicApproval(resolved);
3807
- }
3808
- function approvalDecision(approved, reason) {
3809
- const decision = { approved };
3810
- if (reason !== void 0) decision.reason = reason;
3811
- return decision;
3812
- }
3813
- function publicApproval(approval) {
3814
- const { emit, rejectMessage, resolve: resolve2, ...rest } = approval;
3815
- void emit;
3816
- void rejectMessage;
3817
- void resolve2;
3818
- return rest;
3819
- }
3820
-
3821
- // src/runtime/eval-config.ts
3822
- var CASE_PREVIEW_LIMIT = 5;
3823
- var PREVIEW_VALUE_MAX_LENGTH = 500;
3824
- function evalConfig(suite) {
3825
- const casePreviews = suite.cases.slice(0, CASE_PREVIEW_LIMIT).map(casePreview);
3826
- const metricSummaries = suite.metrics.map(metricSummary);
3827
- const config = {
3828
- id: suite.id ?? suite.name,
3829
- name: suite.name,
3830
- caseCount: suite.cases.length,
3831
- metricNames: suite.metrics.map((metric) => metric.name),
3832
- casePreviewCount: casePreviews.length,
3833
- casePreviews,
3834
- metricSummaries
3835
- };
3836
- if (suite.description !== void 0) config.description = suite.description;
3837
- if (suite.concurrency !== void 0) config.concurrency = suite.concurrency;
3838
- if (suite.metadata !== void 0) config.metadata = suite.metadata;
3839
- return config;
3840
- }
3841
- function casePreview(testCase, index) {
3842
- if (!isRecord2(testCase)) {
3843
- return {
3844
- id: `case-${index + 1}`,
3845
- input: previewValue(testCase)
3846
- };
3847
- }
3848
- const preview = {
3849
- id: typeof testCase.id === "string" && testCase.id.length > 0 ? testCase.id : `case-${index + 1}`
3850
- };
3851
- if ("input" in testCase) preview.input = previewValue(testCase.input);
3852
- if ("expected" in testCase) preview.expected = previewValue(testCase.expected);
3853
- const keys = metadataKeys(testCase.metadata);
3854
- if (keys !== void 0) preview.metadataKeys = keys;
3855
- return preview;
3856
- }
3857
- function metricSummary(metric, index) {
3858
- if (!isRecord2(metric)) {
3859
- return { name: `metric-${index + 1}` };
3611
+ function metricSummary(metric, index) {
3612
+ if (!isRecord2(metric)) {
3613
+ return { name: `metric-${index + 1}` };
3860
3614
  }
3861
3615
  const summary = {
3862
3616
  name: typeof metric.name === "string" && metric.name.length > 0 ? metric.name : `metric-${index + 1}`
@@ -3898,6 +3652,41 @@ function isRecord2(value) {
3898
3652
 
3899
3653
  // src/runtime/knowledge.ts
3900
3654
  import { getAgentToolState } from "@anvia/core/internal/agent";
3655
+
3656
+ // src/runtime/query.ts
3657
+ function optionalQueryString(value) {
3658
+ const trimmed = value?.trim();
3659
+ return trimmed === void 0 || trimmed.length === 0 ? void 0 : trimmed;
3660
+ }
3661
+ function parseLimit(value, defaultMax = 50, max = 100) {
3662
+ if (value === void 0 || value.trim().length === 0) {
3663
+ return defaultMax;
3664
+ }
3665
+ const limit = Number(value);
3666
+ if (!Number.isInteger(limit) || limit <= 0) {
3667
+ return void 0;
3668
+ }
3669
+ return Math.min(limit, max);
3670
+ }
3671
+ function parseTraceStatus(value) {
3672
+ const status = optionalQueryString(value);
3673
+ if (status === void 0) {
3674
+ return void 0;
3675
+ }
3676
+ return status === "running" || status === "success" || status === "suspended" || status === "error" ? status : false;
3677
+ }
3678
+ function parseAfter(value) {
3679
+ if (value === void 0 || value.trim().length === 0) {
3680
+ return void 0;
3681
+ }
3682
+ const after = Number(value);
3683
+ if (!Number.isInteger(after) || after < 0) {
3684
+ return false;
3685
+ }
3686
+ return after;
3687
+ }
3688
+
3689
+ // src/runtime/knowledge.ts
3901
3690
  function registerKnowledgeRoutes(app, props) {
3902
3691
  app.get("/knowledge", async (c) => {
3903
3692
  const limit = parseLimit(c.req.query("limit"));
@@ -4446,8 +4235,10 @@ function capabilityConfig(_options, agents, pipelines, stores, sandboxCount = 0)
4446
4235
  if (sandboxCount > 0) {
4447
4236
  capabilities.sandboxes = { enabled: true };
4448
4237
  }
4449
- if (agents.some((agent) => agent.agent.tools.some(toolRequiresApproval))) {
4450
- capabilities.approvals = { enabled: true };
4238
+ if (agents.some(
4239
+ (agent) => agent.agent.tools.some((tool) => toolRequiresApproval(tool) || isQuestionTool(tool))
4240
+ )) {
4241
+ capabilities.interactions = { enabled: true };
4451
4242
  }
4452
4243
  if (agents.some(agentHasKnowledge)) {
4453
4244
  capabilities.knowledge = { enabled: true };
@@ -4529,6 +4320,47 @@ async function parseEvalRunRequest(c) {
4529
4320
  return request;
4530
4321
  }
4531
4322
 
4323
+ // src/runtime/interactions.ts
4324
+ import {
4325
+ assertAgentInteractionResponse
4326
+ } from "@anvia/core/agent";
4327
+ function createStudioContinuationRegistry() {
4328
+ const pending = /* @__PURE__ */ new Map();
4329
+ const claimed = /* @__PURE__ */ new Set();
4330
+ return {
4331
+ register(registration) {
4332
+ const id = registration.continuation.interaction.id;
4333
+ if (pending.has(id) || claimed.has(id)) {
4334
+ throw new TypeError(`Duplicate Studio interaction ID "${id}".`);
4335
+ }
4336
+ pending.set(id, registration);
4337
+ },
4338
+ take(interactionId, agentId, response) {
4339
+ if (claimed.has(interactionId)) return { status: "claimed" };
4340
+ const registration = pending.get(interactionId);
4341
+ if (registration === void 0 || registration.continuation.agentId !== agentId) {
4342
+ return { status: "missing" };
4343
+ }
4344
+ const request = registration.continuation.interaction;
4345
+ try {
4346
+ assertAgentInteractionResponse(request, response);
4347
+ } catch (error) {
4348
+ return {
4349
+ status: "invalid",
4350
+ error: error instanceof TypeError ? error : new TypeError(String(error))
4351
+ };
4352
+ }
4353
+ pending.delete(interactionId);
4354
+ claimed.add(interactionId);
4355
+ return { status: "ready", registration };
4356
+ },
4357
+ clear() {
4358
+ pending.clear();
4359
+ claimed.clear();
4360
+ }
4361
+ };
4362
+ }
4363
+
4532
4364
  // src/runtime/mcps.ts
4533
4365
  function registerMcpRoutes(app, props) {
4534
4366
  app.get("/agents/:agentId/mcps", async (c) => {
@@ -5481,272 +5313,284 @@ function parsePipelineLogAfter(value) {
5481
5313
  return after;
5482
5314
  }
5483
5315
 
5484
- // src/runtime/questions.ts
5485
- import { createHook } from "@anvia/core/internal/agent";
5486
- import { parseToolArgs } from "@anvia/core/tool";
5487
- function registerQuestionRoutes(app, questions) {
5488
- app.get("/questions", (c) => {
5489
- const status = parseQuestionStatus(c.req.query("status"));
5490
- if (status === false) {
5491
- return errorResponse(c, 400, "bad_request", "status must be pending or resolved");
5492
- }
5493
- const options = {};
5494
- const runId = optionalQueryString(c.req.query("runId"));
5495
- const agentId = optionalQueryString(c.req.query("agentId"));
5496
- const sessionId = optionalQueryString(c.req.query("sessionId"));
5497
- if (status !== void 0) {
5498
- options.status = status;
5499
- }
5500
- if (runId !== void 0) {
5501
- options.runId = runId;
5502
- }
5503
- if (agentId !== void 0) {
5504
- options.agentId = agentId;
5505
- }
5506
- if (sessionId !== void 0) {
5507
- options.sessionId = sessionId;
5508
- }
5509
- return c.json({ questions: questions.list(options) });
5316
+ // src/runtime/sandbox-views.ts
5317
+ import { upgradeWebSocket } from "@hono/node-server";
5318
+ import { getConnInfo } from "@hono/node-server/conninfo";
5319
+ import { WebSocket } from "ws";
5320
+ var minLeaseTimeoutMs = 5e3;
5321
+ var maxLeaseTimeoutMs = 12e4;
5322
+ var maxPendingWebSocketBytes = 1024 * 1024;
5323
+ function registerSandboxViewRoutes(app, registry) {
5324
+ const leases = /* @__PURE__ */ new Map();
5325
+ const upstreamSockets = /* @__PURE__ */ new Set();
5326
+ app.get("/sandboxes/:sandboxRef/views/:viewId/control", async (c) => {
5327
+ const resolved = await authorizedView(c, registry);
5328
+ if (resolved instanceof Response) return resolved;
5329
+ pruneExpiredLease(leases, viewKey(resolved));
5330
+ return c.json(resolved.view.source.control.snapshot());
5510
5331
  });
5511
- app.post("/questions/:questionId/answer", async (c) => {
5512
- const body = await parseQuestionAnswerRequest(c);
5513
- if ("error" in body) {
5514
- return body.error;
5515
- }
5516
- const result = questions.answer(c.req.param("questionId"), body.answers);
5517
- if (result === "missing") {
5518
- return errorResponse(c, 404, "not_found", "Question not found");
5519
- }
5520
- if (result === "resolved") {
5521
- return errorResponse(c, 409, "conflict", "Question is already answered");
5522
- }
5523
- return c.json(result);
5332
+ app.get("/sandboxes/:sandboxRef/views/:viewId/connection", async (c) => {
5333
+ const resolved = await authorizedView(c, registry);
5334
+ if (resolved instanceof Response) return resolved;
5335
+ const response = {
5336
+ sandboxRef: resolved.entry.summary.ref,
5337
+ viewId: resolved.view.id,
5338
+ protocol: "novnc",
5339
+ websocketPath: `/sandboxes/${encodeURIComponent(resolved.entry.summary.ref)}/views/${encodeURIComponent(resolved.view.id)}/ws`,
5340
+ authentication: resolved.view.authentication.type === "password" ? { type: "password", password: resolved.view.authentication.password } : { type: "none" }
5341
+ };
5342
+ c.header("cache-control", "no-store");
5343
+ return c.json(response);
5524
5344
  });
5525
- }
5526
- function parseQuestionStatus(value) {
5527
- const status = optionalQueryString(value);
5528
- if (status === void 0) {
5529
- return void 0;
5530
- }
5531
- return status === "pending" || status === "resolved" ? status : false;
5532
- }
5533
- async function parseQuestionAnswerRequest(c) {
5534
- let body;
5535
- try {
5536
- body = await c.req.json();
5537
- } catch {
5538
- return { error: errorResponse(c, 400, "bad_request", "Request body must be JSON") };
5539
- }
5540
- if (!isObject(body)) {
5541
- return { error: errorResponse(c, 400, "bad_request", "Request body must be an object") };
5542
- }
5543
- if (!Array.isArray(body.answers)) {
5544
- return { error: errorResponse(c, 400, "bad_request", "answers must be an array") };
5545
- }
5546
- const answers = [];
5547
- for (const answer of body.answers) {
5548
- if (!isObject(answer)) {
5549
- return { error: errorResponse(c, 400, "bad_request", "answers must contain objects") };
5550
- }
5551
- if (typeof answer.questionId !== "string" || answer.questionId.trim().length === 0) {
5552
- return { error: errorResponse(c, 400, "bad_request", "questionId must be a string") };
5345
+ app.post("/sandboxes/:sandboxRef/views/:viewId/control/acquire", async (c) => {
5346
+ const resolved = await authorizedView(c, registry);
5347
+ if (resolved instanceof Response) return resolved;
5348
+ const body = await readControlBody(c);
5349
+ if (body instanceof Response) return body;
5350
+ const key = viewKey(resolved);
5351
+ pruneExpiredLease(leases, key);
5352
+ if (leases.has(key)) {
5353
+ return errorResponse(c, 409, "conflict", "Browser view is already controlled");
5553
5354
  }
5554
- if (typeof answer.answer !== "string" || answer.answer.trim().length === 0) {
5555
- return { error: errorResponse(c, 400, "bad_request", "answer must be a string") };
5355
+ try {
5356
+ const lease = await resolved.view.source.control.acquireHumanControl({
5357
+ ownerId: body.ownerId,
5358
+ leaseTimeoutMs: body.leaseTimeoutMs,
5359
+ abortSignal: c.req.raw.signal
5360
+ });
5361
+ leases.set(key, lease);
5362
+ return c.json(leaseResponse(lease), 201);
5363
+ } catch (error) {
5364
+ return controlErrorResponse(c, error);
5556
5365
  }
5557
- if ("choice" in answer && typeof answer.choice !== "string") {
5558
- return { error: errorResponse(c, 400, "bad_request", "choice must be a string") };
5366
+ });
5367
+ app.post("/sandboxes/:sandboxRef/views/:viewId/control/renew", async (c) => {
5368
+ const resolved = await authorizedView(c, registry);
5369
+ if (resolved instanceof Response) return resolved;
5370
+ const body = await readLeaseBody(c);
5371
+ if (body instanceof Response) return body;
5372
+ const key = viewKey(resolved);
5373
+ pruneExpiredLease(leases, key);
5374
+ const lease = leases.get(key);
5375
+ if (lease === void 0 || lease.id !== body.leaseId) {
5376
+ return errorResponse(c, 409, "conflict", "Browser control lease is not active");
5559
5377
  }
5560
- if ("custom" in answer && typeof answer.custom !== "boolean") {
5561
- return { error: errorResponse(c, 400, "bad_request", "custom must be a boolean") };
5378
+ try {
5379
+ lease.renew({ leaseTimeoutMs: body.leaseTimeoutMs });
5380
+ return c.json(leaseResponse(lease));
5381
+ } catch (error) {
5382
+ leases.delete(key);
5383
+ return controlErrorResponse(c, error);
5562
5384
  }
5563
- const normalized = {
5564
- questionId: answer.questionId.trim(),
5565
- answer: answer.answer.trim()
5566
- };
5567
- if (typeof answer.choice === "string") normalized.choice = answer.choice;
5568
- if (typeof answer.custom === "boolean") normalized.custom = answer.custom;
5569
- answers.push(normalized);
5570
- }
5571
- return { answers };
5385
+ });
5386
+ app.post("/sandboxes/:sandboxRef/views/:viewId/control/release", async (c) => {
5387
+ const resolved = await authorizedView(c, registry);
5388
+ if (resolved instanceof Response) return resolved;
5389
+ const body = await readReleaseBody(c);
5390
+ if (body instanceof Response) return body;
5391
+ const key = viewKey(resolved);
5392
+ const lease = leases.get(key);
5393
+ if (lease === void 0 || lease.id !== body.leaseId) {
5394
+ return errorResponse(c, 409, "conflict", "Browser control lease is not active");
5395
+ }
5396
+ lease.release();
5397
+ leases.delete(key);
5398
+ return c.json({ released: true });
5399
+ });
5400
+ app.get("/sandboxes/:sandboxRef/views/:viewId/ws", async (c) => {
5401
+ const resolved = await authorizedView(c, registry);
5402
+ if (resolved instanceof Response) return resolved;
5403
+ const upstreamUrl = `ws://${hostForUrl(resolved.host)}:${resolved.port}/websockify`;
5404
+ return upgradeWebSocket(c, createProxyEvents(upstreamUrl, upstreamSockets), {
5405
+ onError: () => void 0
5406
+ });
5407
+ });
5408
+ return () => {
5409
+ for (const lease of leases.values()) lease.release();
5410
+ leases.clear();
5411
+ for (const socket of upstreamSockets) socket.close(1001, "Studio closed");
5412
+ upstreamSockets.clear();
5413
+ };
5572
5414
  }
5573
- function createQuestionRuntime() {
5574
- const questions = /* @__PURE__ */ new Map();
5415
+ function createProxyEvents(upstreamUrl, sockets) {
5416
+ let upstream;
5417
+ let pendingBytes = 0;
5418
+ const pending = [];
5575
5419
  return {
5576
- questions,
5577
- createHook(context) {
5578
- return createHook({
5579
- async onToolCall({ toolName, toolCallId, internalCallId, args, tool: control }) {
5580
- if (toolName !== "ask_question") {
5581
- return control.run();
5582
- }
5583
- const prompts = normalizeQuestionPrompts(parseToolArgs(args));
5584
- if ("error" in prompts) {
5585
- return control.skip(prompts.error);
5586
- }
5587
- const request = {
5588
- toolName,
5589
- internalCallId,
5590
- args,
5591
- questions: prompts.questions
5592
- };
5593
- if (toolCallId !== void 0) request.toolCallId = toolCallId;
5594
- const answers = await requestQuestion(questions, context, request);
5595
- return control.skip(JSON.stringify({ answers }));
5596
- }
5420
+ onOpen(_event, downstream) {
5421
+ const protocols = downstream.protocol === null || downstream.protocol.length === 0 ? void 0 : [downstream.protocol];
5422
+ upstream = new WebSocket(upstreamUrl, protocols, {
5423
+ headers: { origin: "http://127.0.0.1" }
5424
+ });
5425
+ sockets.add(upstream);
5426
+ upstream.binaryType = "arraybuffer";
5427
+ upstream.once("open", () => {
5428
+ for (const message of pending) upstream?.send(message);
5429
+ pending.length = 0;
5430
+ pendingBytes = 0;
5597
5431
  });
5432
+ upstream.on("message", (data, isBinary) => {
5433
+ const value = isBinary ? Uint8Array.from(Buffer.from(data)) : data.toString();
5434
+ downstream.send(value);
5435
+ });
5436
+ upstream.once("close", (code, reason) => {
5437
+ sockets.delete(upstream);
5438
+ downstream.close(normalizeCloseCode(code), reason.toString("utf8").slice(0, 123));
5439
+ });
5440
+ upstream.once("error", () => downstream.close(1011, "noVNC upstream failed"));
5598
5441
  },
5599
- list(options) {
5600
- return [...questions.values()].filter((question) => {
5601
- if (options.status === "pending" && question.status !== "pending") {
5602
- return false;
5603
- }
5604
- if (options.status === "resolved" && question.status === "pending") {
5605
- return false;
5606
- }
5607
- if (options.runId !== void 0 && question.runId !== options.runId) {
5608
- return false;
5609
- }
5610
- if (options.agentId !== void 0 && question.agentId !== options.agentId) {
5611
- return false;
5442
+ onMessage(event, downstream) {
5443
+ void normalizeWebSocketMessage(event.data).then((message) => {
5444
+ if (upstream?.readyState === WebSocket.OPEN) {
5445
+ upstream.send(message);
5446
+ return;
5612
5447
  }
5613
- if (options.sessionId !== void 0 && question.sessionId !== options.sessionId) {
5614
- return false;
5448
+ pendingBytes += typeof message === "string" ? Buffer.byteLength(message) : message.byteLength;
5449
+ if (pendingBytes > maxPendingWebSocketBytes) {
5450
+ downstream.close(1009, "Pending noVNC messages exceeded the limit");
5451
+ return;
5615
5452
  }
5616
- return true;
5617
- }).map(publicQuestion);
5453
+ pending.push(message);
5454
+ }).catch(() => downstream.close(1003, "Unsupported noVNC message"));
5618
5455
  },
5619
- cancelRun(runId) {
5620
- const cancelled = [];
5621
- for (const question of questions.values()) {
5622
- if (!isPendingQuestion(question) || question.runId !== runId) {
5623
- continue;
5624
- }
5625
- const resolved = cancelQuestion(question);
5626
- questions.set(question.id, resolved);
5627
- question.emit?.({ type: "tool_question_result", question: resolved });
5628
- question.resolve([]);
5629
- cancelled.push(resolved);
5630
- }
5631
- return cancelled;
5456
+ onClose() {
5457
+ upstream?.close(1e3, "Viewer disconnected");
5458
+ if (upstream !== void 0) sockets.delete(upstream);
5632
5459
  },
5633
- answer(id, answers) {
5634
- const question = questions.get(id);
5635
- if (question === void 0) {
5636
- return "missing";
5637
- }
5638
- if (!isPendingQuestion(question)) {
5639
- return "resolved";
5640
- }
5641
- const resolved = resolveQuestion(question, answers);
5642
- questions.set(id, resolved);
5643
- question.emit?.({ type: "tool_question_result", question: resolved });
5644
- question.resolve(answers);
5645
- return publicQuestion(resolved);
5460
+ onError() {
5461
+ upstream?.close(1011, "Viewer connection failed");
5646
5462
  }
5647
5463
  };
5648
5464
  }
5649
- async function requestQuestion(questions, context, request) {
5650
- const id = globalThis.crypto.randomUUID();
5651
- const question = {
5652
- id,
5653
- runId: context.runId,
5654
- agentId: context.agentId,
5655
- toolName: request.toolName,
5656
- internalCallId: request.internalCallId,
5657
- args: request.args,
5658
- questions: request.questions,
5659
- status: "pending",
5660
- requestedAt: (/* @__PURE__ */ new Date()).toISOString(),
5661
- resolve: () => {
5662
- }
5663
- };
5664
- if (context.sessionId !== void 0) question.sessionId = context.sessionId;
5665
- if (request.toolCallId !== void 0) question.callId = request.toolCallId;
5666
- if (context.emit !== void 0) question.emit = context.emit;
5667
- const answer = new Promise((resolve2) => {
5668
- question.resolve = (answers) => {
5669
- resolve2(answers);
5670
- };
5671
- });
5672
- questions.set(id, question);
5673
- context.emit?.({ type: "tool_question_request", question: publicQuestion(question) });
5674
- return answer;
5675
- }
5676
- function normalizeQuestionPrompts(args) {
5677
- if (!isObject(args)) {
5678
- return { error: "ask_question requires a JSON object with questions." };
5679
- }
5680
- const rawQuestions = Array.isArray(args.questions) ? args.questions : [args];
5681
- if (rawQuestions.length === 0) {
5682
- return { error: "ask_question requires at least one question." };
5683
- }
5684
- const questions = [];
5685
- for (const [index, question] of rawQuestions.entries()) {
5686
- const normalized = normalizeQuestionPrompt(question, index);
5687
- if (normalized === void 0) {
5688
- return {
5689
- error: "ask_question requires every question to include text and at least one choice."
5690
- };
5465
+ async function authorizedView(c, registry) {
5466
+ const entry = registry.get(c.req.param("sandboxRef") ?? "");
5467
+ if (entry === void 0) return errorResponse(c, 404, "not_found", "Sandbox not found");
5468
+ const view = entry.views.get(c.req.param("viewId") ?? "");
5469
+ if (view === void 0) return errorResponse(c, 404, "not_found", "Sandbox view not found");
5470
+ if (!await authorize(c, entry, view)) {
5471
+ return errorResponse(c, 403, "forbidden", "Sandbox view access denied");
5472
+ }
5473
+ const published = entry.inspector.publishedPorts?.find(
5474
+ (port) => port.containerPort === view.source.containerPort && port.protocol === "tcp" && isLoopbackAddress(port.host)
5475
+ );
5476
+ if (published === void 0) {
5477
+ return errorResponse(c, 503, "internal_error", "Sandbox view port is unavailable");
5478
+ }
5479
+ return { entry, view, host: published.host, port: published.hostPort };
5480
+ }
5481
+ async function authorize(c, entry, view) {
5482
+ if (view.access.mode === "authorize") {
5483
+ try {
5484
+ return await view.access.authorize({
5485
+ request: c.req.raw,
5486
+ sandboxRef: entry.summary.ref,
5487
+ viewId: view.id
5488
+ }) === true;
5489
+ } catch {
5490
+ return false;
5691
5491
  }
5692
- questions.push(normalized);
5693
5492
  }
5694
- return { questions };
5493
+ let remoteAddress;
5494
+ try {
5495
+ const address = getConnInfo(c).remote.address;
5496
+ if (address === void 0) return false;
5497
+ remoteAddress = address;
5498
+ } catch {
5499
+ return false;
5500
+ }
5501
+ if (!isLoopbackAddress(remoteAddress)) return false;
5502
+ const expectedOrigin = new URL(c.req.url).origin;
5503
+ const origin = c.req.header("origin");
5504
+ return origin === expectedOrigin || origin === void 0 && c.req.header("sec-fetch-site") === "same-origin";
5695
5505
  }
5696
- function normalizeQuestionPrompt(value, index) {
5697
- if (!isObject(value) || typeof value.question !== "string" || value.question.trim().length === 0) {
5698
- return void 0;
5506
+ async function readControlBody(c) {
5507
+ const body = await readJsonObject(c);
5508
+ if (body instanceof Response) return body;
5509
+ if (typeof body.ownerId !== "string" || body.ownerId.length === 0 || body.ownerId.length > 200) {
5510
+ return errorResponse(c, 400, "bad_request", "ownerId must be a non-empty string");
5699
5511
  }
5700
- const choices = Array.isArray(value.choices) ? value.choices.map(normalizeQuestionChoice).filter((choice) => choice !== void 0) : [];
5701
- if (choices.length === 0) {
5702
- return void 0;
5512
+ if (!isLeaseTimeout(body.leaseTimeoutMs)) {
5513
+ return errorResponse(c, 400, "bad_request", "leaseTimeoutMs is outside the allowed range");
5703
5514
  }
5704
- return {
5705
- id: typeof value.id === "string" && value.id.trim().length > 0 ? value.id.trim() : `question_${index + 1}`,
5706
- question: value.question.trim(),
5707
- choices
5708
- };
5515
+ return { ownerId: body.ownerId, leaseTimeoutMs: body.leaseTimeoutMs };
5709
5516
  }
5710
- function normalizeQuestionChoice(value) {
5711
- if (typeof value === "string" && value.trim().length > 0) {
5712
- return { label: value.trim(), value: value.trim() };
5517
+ async function readLeaseBody(c) {
5518
+ const body = await readJsonObject(c);
5519
+ if (body instanceof Response) return body;
5520
+ if (typeof body.leaseId !== "string" || body.leaseId.length === 0) {
5521
+ return errorResponse(c, 400, "bad_request", "leaseId must be a non-empty string");
5713
5522
  }
5714
- if (!isObject(value) || typeof value.label !== "string" || value.label.trim().length === 0) {
5715
- return void 0;
5523
+ if (!isLeaseTimeout(body.leaseTimeoutMs)) {
5524
+ return errorResponse(c, 400, "bad_request", "leaseTimeoutMs is outside the allowed range");
5716
5525
  }
5717
- return {
5718
- label: value.label.trim(),
5719
- value: typeof value.value === "string" && value.value.trim().length > 0 ? value.value.trim() : value.label.trim()
5720
- };
5526
+ return { leaseId: body.leaseId, leaseTimeoutMs: body.leaseTimeoutMs };
5721
5527
  }
5722
- function isPendingQuestion(question) {
5723
- return question !== void 0 && question.status === "pending" && "resolve" in question;
5528
+ async function readReleaseBody(c) {
5529
+ const body = await readJsonObject(c);
5530
+ if (body instanceof Response) return body;
5531
+ if (typeof body.leaseId !== "string" || body.leaseId.length === 0) {
5532
+ return errorResponse(c, 400, "bad_request", "leaseId must be a non-empty string");
5533
+ }
5534
+ return { leaseId: body.leaseId };
5724
5535
  }
5725
- function resolveQuestion(question, answers) {
5726
- return publicQuestion({
5727
- ...question,
5728
- status: "answered",
5729
- answeredAt: (/* @__PURE__ */ new Date()).toISOString(),
5730
- answers
5731
- });
5536
+ async function readJsonObject(c) {
5537
+ let body;
5538
+ try {
5539
+ body = await c.req.json();
5540
+ } catch {
5541
+ return errorResponse(c, 400, "bad_request", "Request body must be valid JSON");
5542
+ }
5543
+ return isRecord4(body) ? body : errorResponse(c, 400, "bad_request", "Request body must be an object");
5732
5544
  }
5733
- function cancelQuestion(question) {
5734
- return publicQuestion({
5735
- ...question,
5736
- status: "cancelled",
5737
- cancelledAt: (/* @__PURE__ */ new Date()).toISOString()
5738
- });
5545
+ function controlErrorResponse(c, error) {
5546
+ if (isRecord4(error) && error.code === "human_controlled") {
5547
+ return errorResponse(c, 409, "conflict", "Browser view is already controlled");
5548
+ }
5549
+ if (isRecord4(error) && error.code === "invalid_state") {
5550
+ return errorResponse(c, 409, "conflict", "Browser control lease is no longer active");
5551
+ }
5552
+ return errorResponse(c, 500, "internal_error", "Browser control operation failed");
5553
+ }
5554
+ function leaseResponse(lease) {
5555
+ return { id: lease.id, ownerId: lease.ownerId, expiresAt: lease.expiresAt };
5739
5556
  }
5740
- function publicQuestion(question) {
5741
- const { emit, resolve: resolve2, ...rest } = question;
5742
- void emit;
5743
- void resolve2;
5744
- return rest;
5557
+ function pruneExpiredLease(leases, key) {
5558
+ const lease = leases.get(key);
5559
+ if (lease !== void 0 && Date.parse(lease.expiresAt) <= Date.now()) {
5560
+ lease.release();
5561
+ leases.delete(key);
5562
+ }
5563
+ }
5564
+ function viewKey(resolved) {
5565
+ return `${resolved.entry.summary.ref}\0${resolved.view.id}`;
5566
+ }
5567
+ function isLeaseTimeout(value) {
5568
+ return Number.isSafeInteger(value) && Number(value) >= minLeaseTimeoutMs && Number(value) <= maxLeaseTimeoutMs;
5569
+ }
5570
+ function isLoopbackAddress(value) {
5571
+ return value === "localhost" || value === "127.0.0.1" || value === "::1" || value.startsWith("127.") || value.startsWith("::ffff:127.");
5572
+ }
5573
+ function hostForUrl(host) {
5574
+ return host.includes(":") ? `[${host}]` : host;
5575
+ }
5576
+ async function normalizeWebSocketMessage(value) {
5577
+ if (typeof value === "string") return value;
5578
+ if (value instanceof ArrayBuffer) return Uint8Array.from(new Uint8Array(value));
5579
+ if (ArrayBuffer.isView(value)) {
5580
+ return Uint8Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
5581
+ }
5582
+ if (value instanceof Blob) return Uint8Array.from(new Uint8Array(await value.arrayBuffer()));
5583
+ throw new TypeError("Unsupported noVNC WebSocket message type.");
5584
+ }
5585
+ function normalizeCloseCode(code) {
5586
+ return code >= 1e3 && code <= 4999 && code !== 1004 && code !== 1005 && code !== 1006 ? code : 1001;
5587
+ }
5588
+ function isRecord4(value) {
5589
+ return typeof value === "object" && value !== null && !Array.isArray(value);
5745
5590
  }
5746
5591
 
5747
5592
  // src/runtime/sandboxes.ts
5748
5593
  import path from "path";
5749
- var sandboxToolMetadataKey = /* @__PURE__ */ Symbol.for("anvia.sandbox.tool.metadata");
5750
5594
  var maxFileResponseBytes = 10 * 1024 * 1024;
5751
5595
  var defaultProcessLogBytes = 64 * 1024;
5752
5596
  var maxProcessLogBytes = 1024 * 1024;
@@ -5765,51 +5609,41 @@ var StudioSandboxRegistry = class {
5765
5609
  return this.entries.get(ref);
5766
5610
  }
5767
5611
  };
5768
- function createStudioSandboxRegistry(agents) {
5769
- const drafts = /* @__PURE__ */ new Map();
5770
- for (const agent of agents) {
5771
- for (const { tool } of agentToolItems(agent)) {
5772
- const session = sandboxSessionFromTool(tool);
5773
- if (session === void 0) {
5774
- continue;
5775
- }
5776
- const draft = drafts.get(session) ?? {
5777
- session,
5778
- agentIds: /* @__PURE__ */ new Set(),
5779
- toolNames: /* @__PURE__ */ new Set()
5780
- };
5781
- draft.agentIds.add(agent.id);
5782
- draft.toolNames.add(tool.name);
5783
- drafts.set(session, draft);
5784
- }
5785
- }
5786
- const refCounts = /* @__PURE__ */ new Map();
5787
- const entries = [...drafts.values()].sort(
5788
- (left, right) => `${left.session.provider}\0${left.session.id}`.localeCompare(
5789
- `${right.session.provider}\0${right.session.id}`
5790
- )
5791
- ).map((draft) => {
5792
- const baseRef = sandboxRef(draft.session.provider, draft.session.id);
5793
- const occurrence = (refCounts.get(baseRef) ?? 0) + 1;
5794
- refCounts.set(baseRef, occurrence);
5795
- const ref = occurrence === 1 ? baseRef : `${baseRef}.${occurrence}`;
5612
+ function createStudioSandboxRegistry(agents, registrations) {
5613
+ const agentIds = new Set(agents.map((agent) => agent.id));
5614
+ const refs = /* @__PURE__ */ new Set();
5615
+ const entries = registrations.map((registration, index) => {
5616
+ validateRegistration(registration, index, agentIds);
5617
+ const inspector = registration.inspector;
5618
+ const ref = sandboxRef(inspector.provider, inspector.id);
5619
+ if (refs.has(ref)) {
5620
+ throw new TypeError(
5621
+ `sandboxes contains a duplicate provider/id registration: ${inspector.provider}/${inspector.id}`
5622
+ );
5623
+ }
5624
+ refs.add(ref);
5625
+ const views = new Map((registration.views ?? []).map((view) => [view.id, view]));
5796
5626
  return {
5797
- session: draft.session,
5627
+ inspector,
5628
+ views,
5798
5629
  summary: {
5799
5630
  ref,
5800
- id: draft.session.id,
5801
- provider: draft.session.provider,
5802
- workdir: draft.session.workdir,
5803
- agentIds: [...draft.agentIds].sort(),
5804
- toolNames: [...draft.toolNames].sort(),
5631
+ id: inspector.id,
5632
+ provider: inspector.provider,
5633
+ workdir: inspector.workdir,
5634
+ agentIds: sortedUnique(registration.agentIds ?? []),
5635
+ toolNames: sortedUnique(registration.toolNames ?? []),
5636
+ views: [...views.values()].map((view) => ({ id: view.id, label: view.label, protocol: view.source.protocol })).sort((left, right) => left.id.localeCompare(right.id)),
5805
5637
  capabilities: {
5806
- files: true,
5807
- ports: supportsPorts(draft.session),
5808
- processes: supportsProcesses(draft.session)
5638
+ files: supportsFiles(inspector),
5639
+ ports: supportsPorts(inspector),
5640
+ processes: supportsProcesses(inspector),
5641
+ views: views.size > 0
5809
5642
  }
5810
5643
  }
5811
5644
  };
5812
5645
  });
5646
+ entries.sort((left, right) => left.summary.ref.localeCompare(right.summary.ref));
5813
5647
  return new StudioSandboxRegistry(entries);
5814
5648
  }
5815
5649
  function registerSandboxRoutes(app, registry) {
@@ -5818,24 +5652,19 @@ function registerSandboxRoutes(app, registry) {
5818
5652
  app.get("/sandboxes", (c) => c.json({ sandboxes: registry.list() }));
5819
5653
  app.get("/sandboxes/:sandboxRef", (c) => {
5820
5654
  const entry = registry.get(c.req.param("sandboxRef"));
5821
- if (entry === void 0) {
5822
- return errorResponse(c, 404, "not_found", "Sandbox not found");
5823
- }
5824
- return c.json(copySummary(entry.summary));
5655
+ return entry === void 0 ? errorResponse(c, 404, "not_found", "Sandbox not found") : c.json(copySummary(entry.summary));
5825
5656
  });
5826
5657
  app.get("/sandboxes/:sandboxRef/files", async (c) => {
5827
5658
  const entry = registry.get(c.req.param("sandboxRef"));
5828
- if (entry === void 0) {
5829
- return errorResponse(c, 404, "not_found", "Sandbox not found");
5830
- }
5659
+ if (entry === void 0) return errorResponse(c, 404, "not_found", "Sandbox not found");
5660
+ if (!supportsFiles(entry.inspector)) return unsupportedSandboxOperation(c, "files");
5831
5661
  try {
5832
5662
  const requestedPath = c.req.query("path") ?? ".";
5833
5663
  const normalizedPath = normalizeSandboxPath(requestedPath, true);
5834
- const entries = await listSandboxFiles(entry.session, normalizedPath);
5835
5664
  return c.json({
5836
5665
  sandboxRef: entry.summary.ref,
5837
5666
  path: normalizedPath,
5838
- entries
5667
+ entries: await listSandboxFiles(entry.inspector, normalizedPath, c.req.raw.signal)
5839
5668
  });
5840
5669
  } catch (error) {
5841
5670
  return sandboxErrorResponse(c, error);
@@ -5843,9 +5672,8 @@ function registerSandboxRoutes(app, registry) {
5843
5672
  });
5844
5673
  app.get("/sandboxes/:sandboxRef/files/content", async (c) => {
5845
5674
  const entry = registry.get(c.req.param("sandboxRef"));
5846
- if (entry === void 0) {
5847
- return errorResponse(c, 404, "not_found", "Sandbox not found");
5848
- }
5675
+ if (entry === void 0) return errorResponse(c, 404, "not_found", "Sandbox not found");
5676
+ if (!supportsFiles(entry.inspector)) return unsupportedSandboxOperation(c, "files.content");
5849
5677
  try {
5850
5678
  const requestedPath = c.req.query("path");
5851
5679
  if (requestedPath === void 0) {
@@ -5856,8 +5684,11 @@ function registerSandboxRoutes(app, registry) {
5856
5684
  throw new SandboxRouteError(400, "bad_request", "download must be 1 when provided");
5857
5685
  }
5858
5686
  const normalizedPath = normalizeSandboxPath(requestedPath, false);
5859
- const parentPath = path.posix.dirname(normalizedPath);
5860
- const siblings = await listSandboxFiles(entry.session, parentPath);
5687
+ const siblings = await listSandboxFiles(
5688
+ entry.inspector,
5689
+ path.posix.dirname(normalizedPath),
5690
+ c.req.raw.signal
5691
+ );
5861
5692
  const file = siblings.find((candidate) => candidate.path === normalizedPath);
5862
5693
  if (file === void 0) {
5863
5694
  throw new SandboxRouteError(404, "not_found", "Sandbox file not found");
@@ -5869,25 +5700,24 @@ function registerSandboxRoutes(app, registry) {
5869
5700
  "Sandbox content path must refer to a regular file"
5870
5701
  );
5871
5702
  }
5872
- if (file.size !== void 0 && file.size > maxFileResponseBytes) {
5703
+ if (file.size !== void 0 && file.size > maxFileResponseBytes)
5873
5704
  throw fileTooLarge(file.size);
5874
- }
5875
- const rawBytes = await entry.session.readFile(normalizedPath);
5705
+ const rawBytes = await entry.inspector.readFile({
5706
+ path: normalizedPath,
5707
+ abortSignal: c.req.raw.signal
5708
+ });
5876
5709
  if (!(rawBytes instanceof Uint8Array)) {
5877
5710
  throw new TypeError("Sandbox readFile returned an invalid byte payload");
5878
5711
  }
5879
- if (rawBytes.byteLength > maxFileResponseBytes) {
5880
- throw fileTooLarge(rawBytes.byteLength);
5881
- }
5882
- const bytes = new Uint8Array(rawBytes.byteLength);
5883
- bytes.set(rawBytes);
5712
+ if (rawBytes.byteLength > maxFileResponseBytes) throw fileTooLarge(rawBytes.byteLength);
5713
+ const bytes = rawBytes.slice();
5884
5714
  const disposition = download === "1" ? "attachment" : "inline";
5885
5715
  const filename = encodeURIComponent(path.posix.basename(normalizedPath));
5886
5716
  return new Response(bytes, {
5887
5717
  headers: {
5888
5718
  "cache-control": "no-store",
5889
5719
  "content-disposition": `${disposition}; filename*=UTF-8''${filename}`,
5890
- "content-length": String(bytes.byteLength),
5720
+ "content-length": `${bytes.byteLength}`,
5891
5721
  "content-type": "application/octet-stream",
5892
5722
  "x-content-type-options": "nosniff"
5893
5723
  }
@@ -5898,16 +5728,12 @@ function registerSandboxRoutes(app, registry) {
5898
5728
  });
5899
5729
  app.get("/sandboxes/:sandboxRef/ports", (c) => {
5900
5730
  const entry = registry.get(c.req.param("sandboxRef"));
5901
- if (entry === void 0) {
5902
- return errorResponse(c, 404, "not_found", "Sandbox not found");
5903
- }
5904
- if (!supportsPorts(entry.session)) {
5905
- return unsupportedSandboxOperation(c, "ports");
5906
- }
5731
+ if (entry === void 0) return errorResponse(c, 404, "not_found", "Sandbox not found");
5732
+ if (!supportsPorts(entry.inspector)) return unsupportedSandboxOperation(c, "ports");
5907
5733
  try {
5908
5734
  return c.json({
5909
5735
  sandboxRef: entry.summary.ref,
5910
- ports: normalizePorts(entry.session.publishedPorts)
5736
+ ports: normalizePorts(entry.inspector.publishedPorts)
5911
5737
  });
5912
5738
  } catch (error) {
5913
5739
  return sandboxErrorResponse(c, error);
@@ -5915,16 +5741,16 @@ function registerSandboxRoutes(app, registry) {
5915
5741
  });
5916
5742
  app.get("/sandboxes/:sandboxRef/processes", async (c) => {
5917
5743
  const entry = registry.get(c.req.param("sandboxRef"));
5918
- if (entry === void 0) {
5919
- return errorResponse(c, 404, "not_found", "Sandbox not found");
5920
- }
5921
- if (!supportsProcesses(entry.session)) {
5744
+ if (entry === void 0) return errorResponse(c, 404, "not_found", "Sandbox not found");
5745
+ if (!supportsProcesses(entry.inspector)) {
5922
5746
  return unsupportedSandboxOperation(c, "processes");
5923
5747
  }
5924
5748
  try {
5925
5749
  return c.json({
5926
5750
  sandboxRef: entry.summary.ref,
5927
- processes: normalizeProcesses(await entry.session.listProcesses())
5751
+ processes: normalizeProcesses(
5752
+ await entry.inspector.listProcesses({ abortSignal: c.req.raw.signal })
5753
+ )
5928
5754
  });
5929
5755
  } catch (error) {
5930
5756
  return sandboxErrorResponse(c, error);
@@ -5932,21 +5758,25 @@ function registerSandboxRoutes(app, registry) {
5932
5758
  });
5933
5759
  app.get("/sandboxes/:sandboxRef/processes/:processId/logs", async (c) => {
5934
5760
  const entry = registry.get(c.req.param("sandboxRef"));
5935
- if (entry === void 0) {
5936
- return errorResponse(c, 404, "not_found", "Sandbox not found");
5937
- }
5938
- if (!supportsProcesses(entry.session)) {
5761
+ if (entry === void 0) return errorResponse(c, 404, "not_found", "Sandbox not found");
5762
+ if (!supportsProcesses(entry.inspector)) {
5939
5763
  return unsupportedSandboxOperation(c, "processes.logs");
5940
5764
  }
5941
5765
  try {
5942
5766
  const tailBytes = parseTailBytes(c.req.query("tailBytes"));
5943
5767
  const processId = c.req.param("processId");
5944
- const processes = normalizeProcesses(await entry.session.listProcesses());
5768
+ const processes = normalizeProcesses(
5769
+ await entry.inspector.listProcesses({ abortSignal: c.req.raw.signal })
5770
+ );
5945
5771
  if (!processes.some((process2) => process2.id === processId)) {
5946
5772
  throw new SandboxRouteError(404, "not_found", "Sandbox process not found");
5947
5773
  }
5948
5774
  const logs = normalizeProcessLogs(
5949
- await entry.session.readProcessLogs(processId, { tailBytes }),
5775
+ await entry.inspector.readProcessLogs({
5776
+ processId,
5777
+ tailBytes,
5778
+ abortSignal: c.req.raw.signal
5779
+ }),
5950
5780
  tailBytes
5951
5781
  );
5952
5782
  return c.json({
@@ -5963,21 +5793,86 @@ async function sandboxNoStore(c, next) {
5963
5793
  c.header("cache-control", "no-store");
5964
5794
  await next();
5965
5795
  }
5966
- function sandboxSessionFromTool(tool) {
5967
- const metadata = tool[sandboxToolMetadataKey];
5968
- if (!isRecord4(metadata)) {
5969
- return void 0;
5796
+ function validateRegistration(registration, index, knownAgentIds) {
5797
+ if (!isRecord5(registration) || !isInspector(registration.inspector)) {
5798
+ throw new TypeError(`sandboxes[${index}] must contain a valid inspector.`);
5799
+ }
5800
+ const inspector = registration.inspector;
5801
+ const files = supportsFiles(inspector);
5802
+ if (inspector.listFiles === void 0 !== (inspector.readFile === void 0)) {
5803
+ throw new TypeError(`sandboxes[${index}] must register both file inspector methods.`);
5970
5804
  }
5971
- return isSandboxSession(metadata.session) ? metadata.session : void 0;
5805
+ const processes = supportsProcesses(inspector);
5806
+ if (inspector.listProcesses === void 0 !== (inspector.readProcessLogs === void 0)) {
5807
+ throw new TypeError(`sandboxes[${index}] must register both process inspector methods.`);
5808
+ }
5809
+ if (!files && !supportsPorts(inspector) && !processes) {
5810
+ throw new TypeError(`sandboxes[${index}] inspector exposes no capabilities.`);
5811
+ }
5812
+ for (const agentId of sortedUnique(registration.agentIds ?? [])) {
5813
+ if (!knownAgentIds.has(agentId)) {
5814
+ throw new TypeError(`sandboxes[${index}] references an unknown agent: ${agentId}`);
5815
+ }
5816
+ }
5817
+ sortedUnique(registration.toolNames ?? []);
5818
+ validateViews(registration.views, inspector, index);
5972
5819
  }
5973
- function isSandboxSession(value) {
5974
- 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";
5820
+ function validateViews(views, inspector, registrationIndex) {
5821
+ if (views === void 0) return;
5822
+ if (!Array.isArray(views))
5823
+ throw new TypeError(`sandboxes[${registrationIndex}].views must be an array.`);
5824
+ if (!supportsPorts(inspector)) {
5825
+ throw new TypeError(`sandboxes[${registrationIndex}] views require published port inspection.`);
5826
+ }
5827
+ const ids = /* @__PURE__ */ new Set();
5828
+ for (const [viewIndex, view] of views.entries()) {
5829
+ const prefix = `sandboxes[${registrationIndex}].views[${viewIndex}]`;
5830
+ const candidate = view;
5831
+ if (!isRecord5(candidate) || typeof candidate.id !== "string" || !/^[a-z0-9](?:[a-z0-9_-]{0,62})$/.test(candidate.id)) {
5832
+ throw new TypeError(`${prefix}.id must be a stable lowercase identifier.`);
5833
+ }
5834
+ if (ids.has(candidate.id)) throw new TypeError(`${prefix}.id is duplicated: ${candidate.id}`);
5835
+ ids.add(candidate.id);
5836
+ if (typeof candidate.label !== "string" || candidate.label.length === 0) {
5837
+ throw new TypeError(`${prefix}.label must be a non-empty string.`);
5838
+ }
5839
+ const source = candidate.source;
5840
+ if (!isRecord5(source) || source.protocol !== "novnc" || !isPort(source.containerPort) || !isRecord5(source.control) || typeof source.control.snapshot !== "function" || typeof source.control.acquireHumanControl !== "function") {
5841
+ throw new TypeError(`${prefix}.source must be a valid noVNC view source.`);
5842
+ }
5843
+ const port = inspector.publishedPorts?.find(
5844
+ (candidate2) => candidate2.containerPort === source.containerPort && candidate2.protocol === "tcp"
5845
+ );
5846
+ if (port === void 0 || !isLoopbackHost(port.host)) {
5847
+ throw new TypeError(`${prefix}.source must resolve to a loopback-published TCP port.`);
5848
+ }
5849
+ const access = candidate.access;
5850
+ if (!isRecord5(access) || access.mode !== "local" && access.mode !== "authorize") {
5851
+ throw new TypeError(`${prefix}.access must explicitly select local or authorize mode.`);
5852
+ }
5853
+ if (access.mode === "authorize" && typeof access.authorize !== "function") {
5854
+ throw new TypeError(`${prefix}.access.authorize must be a function.`);
5855
+ }
5856
+ const authentication = candidate.authentication;
5857
+ if (!isRecord5(authentication) || authentication.type !== "none" && authentication.type !== "password") {
5858
+ throw new TypeError(`${prefix}.authentication must explicitly select none or password.`);
5859
+ }
5860
+ if (authentication.type === "password" && (typeof authentication.password !== "string" || authentication.password.length === 0)) {
5861
+ throw new TypeError(`${prefix}.authentication.password must be a non-empty string.`);
5862
+ }
5863
+ }
5975
5864
  }
5976
- function supportsPorts(session) {
5977
- return Array.isArray(session.publishedPorts);
5865
+ function isInspector(value) {
5866
+ return isRecord5(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;
5978
5867
  }
5979
- function supportsProcesses(session) {
5980
- return typeof session.listProcesses === "function" && typeof session.readProcessLogs === "function";
5868
+ function supportsFiles(inspector) {
5869
+ return typeof inspector.listFiles === "function" && typeof inspector.readFile === "function";
5870
+ }
5871
+ function supportsPorts(inspector) {
5872
+ return Array.isArray(inspector.publishedPorts);
5873
+ }
5874
+ function supportsProcesses(inspector) {
5875
+ return typeof inspector.listProcesses === "function" && typeof inspector.readProcessLogs === "function";
5981
5876
  }
5982
5877
  function sandboxRef(provider, id) {
5983
5878
  return Buffer.from(JSON.stringify([provider, id]), "utf8").toString("base64url");
@@ -5987,22 +5882,26 @@ function copySummary(summary) {
5987
5882
  ...summary,
5988
5883
  agentIds: [...summary.agentIds],
5989
5884
  toolNames: [...summary.toolNames],
5885
+ views: summary.views.map((view) => ({ ...view })),
5990
5886
  capabilities: { ...summary.capabilities }
5991
5887
  };
5992
5888
  }
5993
- async function listSandboxFiles(session, filePath) {
5994
- return normalizeFileEntries(await session.listFiles(filePath)).sort((left, right) => {
5995
- if (left.type === "directory" && right.type !== "directory") return -1;
5996
- if (left.type !== "directory" && right.type === "directory") return 1;
5997
- return left.path.localeCompare(right.path);
5998
- });
5889
+ function isLoopbackHost(host) {
5890
+ return host === "127.0.0.1" || host === "::1" || host === "localhost";
5891
+ }
5892
+ async function listSandboxFiles(inspector, filePath, abortSignal) {
5893
+ return normalizeFileEntries(await inspector.listFiles({ path: filePath, abortSignal })).sort(
5894
+ (left, right) => {
5895
+ if (left.type === "directory" && right.type !== "directory") return -1;
5896
+ if (left.type !== "directory" && right.type === "directory") return 1;
5897
+ return left.path.localeCompare(right.path);
5898
+ }
5899
+ );
5999
5900
  }
6000
5901
  function normalizeFileEntries(value) {
6001
- if (!Array.isArray(value)) {
6002
- throw new TypeError("Sandbox listFiles returned an invalid payload");
6003
- }
5902
+ if (!Array.isArray(value)) throw new TypeError("Sandbox listFiles returned an invalid payload");
6004
5903
  return value.map((item) => {
6005
- if (!isRecord4(item) || typeof item.path !== "string" || !isFileType(item.type)) {
5904
+ if (!isRecord5(item) || typeof item.path !== "string" || !isFileType(item.type)) {
6006
5905
  throw new TypeError("Sandbox listFiles returned an invalid entry");
6007
5906
  }
6008
5907
  let entryPath;
@@ -6013,7 +5912,7 @@ function normalizeFileEntries(value) {
6013
5912
  }
6014
5913
  const entry = { path: entryPath, type: item.type };
6015
5914
  if (item.size !== void 0) {
6016
- if (typeof item.size !== "number" || !Number.isFinite(item.size) || item.size < 0) {
5915
+ if (!Number.isSafeInteger(item.size) || Number(item.size) < 0) {
6017
5916
  throw new TypeError("Sandbox listFiles returned an invalid file size");
6018
5917
  }
6019
5918
  entry.size = item.size;
@@ -6022,11 +5921,10 @@ function normalizeFileEntries(value) {
6022
5921
  });
6023
5922
  }
6024
5923
  function normalizePorts(value) {
6025
- if (!Array.isArray(value)) {
5924
+ if (!Array.isArray(value))
6026
5925
  throw new TypeError("Sandbox publishedPorts returned an invalid payload");
6027
- }
6028
5926
  return value.map((item) => {
6029
- if (!isRecord4(item) || !isPort(item.containerPort) || typeof item.host !== "string" || !isPort(item.hostPort) || typeof item.protocol !== "string") {
5927
+ if (!isRecord5(item) || !isPort(item.containerPort) || typeof item.host !== "string" || !isPort(item.hostPort) || typeof item.protocol !== "string") {
6030
5928
  throw new TypeError("Sandbox publishedPorts returned an invalid entry");
6031
5929
  }
6032
5930
  return {
@@ -6038,17 +5936,16 @@ function normalizePorts(value) {
6038
5936
  });
6039
5937
  }
6040
5938
  function normalizeProcesses(value) {
6041
- if (!Array.isArray(value)) {
5939
+ if (!Array.isArray(value))
6042
5940
  throw new TypeError("Sandbox listProcesses returned an invalid payload");
6043
- }
6044
5941
  return value.map((item) => {
6045
- 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") {
5942
+ if (!isRecord5(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") {
6046
5943
  throw new TypeError("Sandbox listProcesses returned an invalid entry");
6047
5944
  }
6048
5945
  const process2 = {
6049
5946
  id: item.id,
6050
5947
  command: item.command,
6051
- args: item.args,
5948
+ args: [...item.args],
6052
5949
  status: item.status,
6053
5950
  startedAt: item.startedAt
6054
5951
  };
@@ -6059,11 +5956,11 @@ function normalizeProcesses(value) {
6059
5956
  });
6060
5957
  }
6061
5958
  function normalizeProcessLogs(value, maxBytes) {
6062
- if (!isRecord4(value) || typeof value.stdout !== "string" || typeof value.stderr !== "string" || typeof value.stdoutTruncated !== "boolean" || typeof value.stderrTruncated !== "boolean") {
5959
+ if (!isRecord5(value) || !(value.stdout instanceof Uint8Array) || !(value.stderr instanceof Uint8Array) || typeof value.stdoutTruncated !== "boolean" || typeof value.stderrTruncated !== "boolean") {
6063
5960
  throw new TypeError("Sandbox readProcessLogs returned an invalid payload");
6064
5961
  }
6065
- const stdout = boundedLogText(value.stdout, maxBytes);
6066
- const stderr = boundedLogText(value.stderr, maxBytes);
5962
+ const stdout = boundedLogBytes(value.stdout, maxBytes);
5963
+ const stderr = boundedLogBytes(value.stderr, maxBytes);
6067
5964
  return {
6068
5965
  stdout: stdout.text,
6069
5966
  stderr: stderr.text,
@@ -6071,21 +5968,13 @@ function normalizeProcessLogs(value, maxBytes) {
6071
5968
  stderrTruncated: value.stderrTruncated || stderr.truncated
6072
5969
  };
6073
5970
  }
6074
- function boundedLogText(value, maxBytes) {
6075
- const bytes = new TextEncoder().encode(value);
6076
- if (bytes.byteLength <= maxBytes) {
6077
- return { text: value, truncated: false };
6078
- }
6079
- if (maxBytes === 0) {
6080
- return { text: "", truncated: true };
6081
- }
6082
- let start = bytes.byteLength - maxBytes;
6083
- while (start < bytes.byteLength && (bytes[start] ?? 0) >> 6 === 2) {
6084
- start += 1;
6085
- }
5971
+ function boundedLogBytes(value, maxBytes) {
5972
+ if (maxBytes === 0) return { text: "", truncated: value.byteLength > 0 };
5973
+ let start = Math.max(0, value.byteLength - maxBytes);
5974
+ while (start < value.byteLength && ((value[start] ?? 0) & 192) === 128) start += 1;
6086
5975
  return {
6087
- text: new TextDecoder().decode(bytes.subarray(start)),
6088
- truncated: true
5976
+ text: new TextDecoder("utf-8", { fatal: true }).decode(value.subarray(start)),
5977
+ truncated: start > 0
6089
5978
  };
6090
5979
  }
6091
5980
  function normalizeSandboxPath(input, allowRoot) {
@@ -6108,9 +5997,7 @@ function normalizeSandboxPath(input, allowRoot) {
6108
5997
  return normalized;
6109
5998
  }
6110
5999
  function parseTailBytes(value) {
6111
- if (value === void 0) {
6112
- return defaultProcessLogBytes;
6113
- }
6000
+ if (value === void 0) return defaultProcessLogBytes;
6114
6001
  const parsed = Number(value);
6115
6002
  if (!Number.isInteger(parsed) || parsed < 0 || parsed > maxProcessLogBytes) {
6116
6003
  throw new SandboxRouteError(
@@ -6131,15 +6018,13 @@ function sandboxErrorResponse(c, error) {
6131
6018
  if (error instanceof SandboxRouteError) {
6132
6019
  return errorResponse(c, error.status, error.code, error.message);
6133
6020
  }
6134
- const name = errorName(error);
6135
- if (name === "SandboxPathError") {
6136
- return errorResponse(c, 400, "bad_request", errorMessage(error));
6137
- }
6138
- if (name === "SandboxFileSizeError") {
6021
+ const code = errorCode(error);
6022
+ if (code === "invalid_path") return errorResponse(c, 400, "bad_request", errorMessage(error));
6023
+ if (code === "file_too_large") {
6139
6024
  return errorResponse(c, 413, "payload_too_large", errorMessage(error));
6140
6025
  }
6141
- if (name === "SandboxSessionDestroyedError") {
6142
- return errorResponse(c, 409, "conflict", "Sandbox session is no longer available");
6026
+ if (code === "invalid_state" || code === "sandbox_not_found") {
6027
+ return errorResponse(c, 409, "conflict", "Sandbox is no longer available");
6143
6028
  }
6144
6029
  return errorResponse(
6145
6030
  c,
@@ -6165,6 +6050,18 @@ var SandboxRouteError = class extends Error {
6165
6050
  status;
6166
6051
  code;
6167
6052
  };
6053
+ function sortedUnique(values) {
6054
+ if (!Array.isArray(values)) throw new TypeError("Sandbox associations must be arrays.");
6055
+ const result = /* @__PURE__ */ new Set();
6056
+ for (const value of values) {
6057
+ if (typeof value !== "string" || value.length === 0) {
6058
+ throw new TypeError("Sandbox associations must contain non-empty strings.");
6059
+ }
6060
+ if (result.has(value)) throw new TypeError(`Sandbox association is duplicated: ${value}`);
6061
+ result.add(value);
6062
+ }
6063
+ return [...result].sort();
6064
+ }
6168
6065
  function isFileType(value) {
6169
6066
  return value === "file" || value === "directory" || value === "symlink" || value === "other";
6170
6067
  }
@@ -6174,14 +6071,14 @@ function isProcessStatus(value) {
6174
6071
  function isPort(value) {
6175
6072
  return Number.isInteger(value) && Number(value) >= 1 && Number(value) <= 65535;
6176
6073
  }
6177
- function isRecord4(value) {
6178
- return typeof value === "object" && value !== null;
6074
+ function isRecord5(value) {
6075
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6179
6076
  }
6180
- function errorName(error) {
6181
- return isRecord4(error) && typeof error.name === "string" ? error.name : "";
6077
+ function errorCode(error) {
6078
+ return isRecord5(error) && typeof error.code === "string" ? error.code : void 0;
6182
6079
  }
6183
6080
  function errorMessage(error) {
6184
- return isRecord4(error) && typeof error.message === "string" ? error.message : String(error);
6081
+ return isRecord5(error) && typeof error.message === "string" ? error.message : "Sandbox failed";
6185
6082
  }
6186
6083
 
6187
6084
  // src/runtime/sessions.ts
@@ -6905,6 +6802,7 @@ var Studio = class {
6905
6802
  options;
6906
6803
  studio;
6907
6804
  server;
6805
+ websocketServer;
6908
6806
  sigintHandler;
6909
6807
  constructor(targets = [], options = {}) {
6910
6808
  this.options = studioOptionsFromTargets(targets, options);
@@ -6929,9 +6827,15 @@ var Studio = class {
6929
6827
  this.studio = createStudioApp(this.options);
6930
6828
  const port = serveOptions.port ?? Number(process.env.RUNNER_PORT ?? 4021);
6931
6829
  const serverOptions = {
6932
- fetch: (request) => this.fetch(request),
6830
+ fetch: (request, env) => this.studio.app.fetch(request, env),
6933
6831
  port
6934
6832
  };
6833
+ this.websocketServer = new WebSocketServer({ noServer: true });
6834
+ serverOptions.websocket = {
6835
+ // @types/ws overloads are stricter than Hono's structural adapter, while Hono officially
6836
+ // supports ws.WebSocketServer. Keep the interop assertion at this single ownership boundary.
6837
+ server: this.websocketServer
6838
+ };
6935
6839
  if (serveOptions.hostname !== void 0) serverOptions.hostname = serveOptions.hostname;
6936
6840
  this.server = serve(serverOptions);
6937
6841
  const log = serveOptions.log ?? true;
@@ -6976,6 +6880,8 @@ var Studio = class {
6976
6880
  }
6977
6881
  this.server?.close();
6978
6882
  this.server = void 0;
6883
+ this.websocketServer?.close();
6884
+ this.websocketServer = void 0;
6979
6885
  this.studio.close();
6980
6886
  }
6981
6887
  logAddress(host, port) {
@@ -7054,6 +6960,21 @@ function studioOptionsFromTargets(targets, options) {
7054
6960
  if (options.models !== void 0) runtimeOptions.models = options.models;
7055
6961
  if (options.stores !== void 0) runtimeOptions.stores = options.stores;
7056
6962
  if (options.ui !== void 0) runtimeOptions.ui = options.ui;
6963
+ if (options.sandboxes !== void 0) {
6964
+ runtimeOptions.sandboxes = options.sandboxes.map((registration) => {
6965
+ let snapshot = { inspector: registration.inspector };
6966
+ if (registration.agentIds !== void 0) {
6967
+ snapshot = { ...snapshot, agentIds: [...registration.agentIds] };
6968
+ }
6969
+ if (registration.toolNames !== void 0) {
6970
+ snapshot = { ...snapshot, toolNames: [...registration.toolNames] };
6971
+ }
6972
+ if (registration.views !== void 0) {
6973
+ snapshot = { ...snapshot, views: [...registration.views] };
6974
+ }
6975
+ return snapshot;
6976
+ });
6977
+ }
7057
6978
  return runtimeOptions;
7058
6979
  }
7059
6980
  function inferStudioAgents(agents, quickPrompts) {
@@ -7116,9 +7037,8 @@ function createStudioApp(options) {
7116
7037
  const agentMap = new Map(agents.map((agent) => [agent.id, agent]));
7117
7038
  const pipelineMap = new Map(pipelines.map((pipeline) => [pipeline.id, pipeline]));
7118
7039
  const evalMap = new Map(options.evals.map((suite) => [suite.id ?? suite.name, suite]));
7119
- const approvalRuntime = createApprovalRuntime();
7120
- const questionRuntime = createQuestionRuntime();
7121
- const sandboxRegistry = createStudioSandboxRegistry(agents);
7040
+ const continuationRegistry = createStudioContinuationRegistry();
7041
+ const sandboxRegistry = createStudioSandboxRegistry(agents, options.sandboxes ?? []);
7122
7042
  const memorySources = createStudioMemorySourceRegistry(agents, stores.sessions);
7123
7043
  const app = new HonoApp();
7124
7044
  const uiOptions = isStudioUiEnabled(options.ui) ? resolveStudioUiOptions(options.ui) : void 0;
@@ -7169,8 +7089,7 @@ function createStudioApp(options) {
7169
7089
  registerMcpRoutes(app, { agentMap });
7170
7090
  registerToolRoutes(app, { agentMap });
7171
7091
  registerSandboxRoutes(app, sandboxRegistry);
7172
- registerApprovalRoutes(app, approvalRuntime);
7173
- registerQuestionRoutes(app, questionRuntime);
7092
+ const closeSandboxViews = registerSandboxViewRoutes(app, sandboxRegistry);
7174
7093
  registerObservabilityRoutes(app, observabilityHub);
7175
7094
  registerEvalRoutes(app, {
7176
7095
  evals: options.evals,
@@ -7190,8 +7109,7 @@ function createStudioApp(options) {
7190
7109
  agentMap,
7191
7110
  stores,
7192
7111
  modelRegistry,
7193
- approvalRuntime,
7194
- questionRuntime
7112
+ continuationRegistry
7195
7113
  });
7196
7114
  if (memorySources.size > 0 || stores.sessions !== void 0) {
7197
7115
  registerMemoryRoutes(app, {
@@ -7223,6 +7141,8 @@ function createStudioApp(options) {
7223
7141
  return buildConfig(options, agents, pipelines, stores, sandboxRegistry.size);
7224
7142
  },
7225
7143
  close() {
7144
+ continuationRegistry.clear();
7145
+ closeSandboxViews();
7226
7146
  }
7227
7147
  };
7228
7148
  if (stores.sessions !== void 0) Object.assign(studio, { sessionStore: stores.sessions });
@@ -8271,36 +8191,36 @@ function messageFromRows(row, partRows) {
8271
8191
  throw new TypeError("Stored Studio message metadata is not a strict JSON object.");
8272
8192
  }
8273
8193
  if (row.role === "system") {
8274
- const message = {
8194
+ let message = {
8275
8195
  role: "system",
8276
- content: systemContentFromParts(parts),
8277
- ...metadata === void 0 ? {} : { metadata }
8196
+ content: systemContentFromParts(parts)
8278
8197
  };
8198
+ if (metadata !== void 0) message = { ...message, metadata };
8279
8199
  return message;
8280
8200
  }
8281
8201
  if (row.role === "user") {
8282
- const message = {
8202
+ let message = {
8283
8203
  role: "user",
8284
- content: parts,
8285
- ...metadata === void 0 ? {} : { metadata }
8204
+ content: parts
8286
8205
  };
8206
+ if (metadata !== void 0) message = { ...message, metadata };
8287
8207
  return message;
8288
8208
  }
8289
8209
  if (row.role === "assistant") {
8290
- const message = {
8210
+ let message = {
8291
8211
  role: "assistant",
8292
- content: parts,
8293
- ...row.message_id === null ? {} : { id: row.message_id },
8294
- ...metadata === void 0 ? {} : { metadata }
8212
+ content: parts
8295
8213
  };
8214
+ if (row.message_id !== null) message = { ...message, id: row.message_id };
8215
+ if (metadata !== void 0) message = { ...message, metadata };
8296
8216
  return message;
8297
8217
  }
8298
8218
  if (row.role === "tool") {
8299
- const message = {
8219
+ let message = {
8300
8220
  role: "tool",
8301
- content: parts,
8302
- ...metadata === void 0 ? {} : { metadata }
8221
+ content: parts
8303
8222
  };
8223
+ if (metadata !== void 0) message = { ...message, metadata };
8304
8224
  return message;
8305
8225
  }
8306
8226
  throw new Error(`Unsupported stored message role: ${row.role}`);