@datalayer/agent-runtimes 1.3.103 → 1.3.104

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 (139) hide show
  1. package/dist-embed/chunks/{AgentDetails-CO9iM59C.js → AgentDetails-eWkEnFI1.js} +1 -1
  2. package/dist-embed/chunks/{AgentDetailsDialog-DmD6xm6k.js → AgentDetailsDialog-BXJek5nf.js} +1 -1
  3. package/dist-embed/chunks/{AgentInspector-kDMuvh45.js → AgentInspector-B1wLMu4Z.js} +1 -1
  4. package/dist-embed/chunks/{AgentInspectorDialog-fT96Pb4Z.js → AgentInspectorDialog-DcLrhdWo.js} +1 -1
  5. package/dist-embed/chunks/Avatar-BrPfm89P.js +1 -0
  6. package/dist-embed/chunks/{CellSidebarExtension-B-vao30u.js → CellSidebarExtension-BWsHtuqs.js} +2 -2
  7. package/dist-embed/chunks/ChatView-ef0jot57.js +32 -0
  8. package/dist-embed/chunks/DatalayerAgentBridge-DvlGUOdE.js +1 -0
  9. package/dist-embed/chunks/{DeckComponent-Fp6Vw8vN.js → DeckComponent-BfOXdQhk.js} +1 -1
  10. package/dist-embed/chunks/{DocumentView-DKUbyyJg.js → DocumentView-DCgaQoOU.js} +1 -1
  11. package/dist-embed/chunks/{EphemeralDocument-CgYPEfGC.js → EphemeralDocument-DVAfnQWS.js} +1 -1
  12. package/dist-embed/chunks/{EphemeralNotebook-DvDbCKnr.js → EphemeralNotebook-C1ANS_1u.js} +1 -1
  13. package/dist-embed/chunks/{ExcalidrawComponent-BehqBpwC.js → ExcalidrawComponent-0e3KqmKJ.js} +1 -1
  14. package/dist-embed/chunks/{ExcalidrawModal-B2sXbVJB.js → ExcalidrawModal-C5Or1sZ5.js} +1 -1
  15. package/dist-embed/chunks/{Flash-DdFxPtBI.js → Flash-C3nF3s4G.js} +1 -1
  16. package/dist-embed/chunks/{JupyterLexicalExtension-CUsjvchq.js → JupyterLexicalExtension-CEjN8cLT.js} +7 -7
  17. package/dist-embed/chunks/{KernelVariables-BUmOt4yF.js → KernelVariables-DyYN9sSS.js} +1 -1
  18. package/dist-embed/chunks/{NotebookSkeleton-D9WNoiWD.js → NotebookSkeleton-z8GKB7Tb.js} +1 -1
  19. package/dist-embed/chunks/{NotebookToolbar-C2_ZC9Ej.js → NotebookToolbar-Dv7QkxKA.js} +1 -1
  20. package/dist-embed/chunks/{NotebookView-BActuABH.js → NotebookView-CNHzc28A.js} +1 -1
  21. package/dist-embed/chunks/{SandboxDetailsDialog-C0afmOA7.js → SandboxDetailsDialog-D4HA7PZA.js} +1 -1
  22. package/dist-embed/chunks/{SchemaForm-B351kd79.js → SchemaForm-bWlj1cVy.js} +1 -1
  23. package/dist-embed/chunks/{Toolbar-B6ROXJ-2.js → Toolbar-BxAi9T92.js} +1 -1
  24. package/dist-embed/chunks/{YouTubePlugin-CqA-IwCx.js → YouTubePlugin-ZawlUPsx.js} +1 -1
  25. package/dist-embed/chunks/{ar-SA-G6X2FPQ2-DhQlC4AZ.js → ar-SA-G6X2FPQ2-DqE6ftcU.js} +1 -1
  26. package/dist-embed/chunks/{az-AZ-76LH7QW2-CDvJeI6H.js → az-AZ-76LH7QW2-8y_pqJhn.js} +1 -1
  27. package/dist-embed/chunks/{bg-BG-XCXSNQG7-B_2t7dwS.js → bg-BG-XCXSNQG7-4NX5jouU.js} +1 -1
  28. package/dist-embed/chunks/{bn-BD-2XOGV67Q-B7nTDj-4.js → bn-BD-2XOGV67Q-BB87_pS6.js} +1 -1
  29. package/dist-embed/chunks/{ca-ES-6MX7JW3Y-llJD_i9l.js → ca-ES-6MX7JW3Y-B9gyglRE.js} +1 -1
  30. package/dist-embed/chunks/{cs-CZ-2BRQDIVT-Cm_uiYT0.js → cs-CZ-2BRQDIVT-CmZhtHam.js} +1 -1
  31. package/dist-embed/chunks/{da-DK-5WZEPLOC-B0lo6-wZ.js → da-DK-5WZEPLOC-CU6rP6vm.js} +1 -1
  32. package/dist-embed/chunks/{de-DE-XR44H4JA-DZrN-mJo.js → de-DE-XR44H4JA-wk6_Fx0e.js} +1 -1
  33. package/dist-embed/chunks/{el-GR-BZB4AONW-C4D5ZbOc.js → el-GR-BZB4AONW-Cy5yeukb.js} +1 -1
  34. package/dist-embed/chunks/{es-ES-U4NZUMDT-D1yiS-ZI.js → es-ES-U4NZUMDT-Ccr3g95I.js} +1 -1
  35. package/dist-embed/chunks/{eu-ES-A7QVB2H4-Zm3lJsL6.js → eu-ES-A7QVB2H4-u0DKk40F.js} +1 -1
  36. package/dist-embed/chunks/{fa-IR-HGAKTJCU-Dhv2lg_O.js → fa-IR-HGAKTJCU-BYSSU2Gx.js} +1 -1
  37. package/dist-embed/chunks/{fi-FI-Z5N7JZ37-QeFFEfyg.js → fi-FI-Z5N7JZ37-98vDI-0h.js} +1 -1
  38. package/dist-embed/chunks/{fr-FR-RHASNOE6-DjbRpOS6.js → fr-FR-RHASNOE6-D2BvErn0.js} +1 -1
  39. package/dist-embed/chunks/{gl-ES-HMX3MZ6V-C-ev9dqC.js → gl-ES-HMX3MZ6V-DP7FRXtT.js} +1 -1
  40. package/dist-embed/chunks/{he-IL-6SHJWFNN-DHoNpuA6.js → he-IL-6SHJWFNN-B92ZDKBK.js} +1 -1
  41. package/dist-embed/chunks/{hi-IN-IWLTKZ5I-JZJZPoWj.js → hi-IN-IWLTKZ5I-Dw2EhOq2.js} +1 -1
  42. package/dist-embed/chunks/{hu-HU-A5ZG7DT2-RzCikyBW.js → hu-HU-A5ZG7DT2-CVh5JW9n.js} +1 -1
  43. package/dist-embed/chunks/{id-ID-SAP4L64H-Bn08nS9B.js → id-ID-SAP4L64H-CaKn7Yw0.js} +1 -1
  44. package/dist-embed/chunks/{index-DIHzcHI1.js → index-5Mm_lDnH.js} +1 -1
  45. package/dist-embed/chunks/{index-B1StFYI5.js → index-B46RcdCt.js} +1 -1
  46. package/dist-embed/chunks/{index-qzUKNibz.js → index-B6RMWdb4.js} +1 -1
  47. package/dist-embed/chunks/{index-88CEdMmU.js → index-BI_ojvKX.js} +1 -1
  48. package/dist-embed/chunks/{index-uutAM7NJ.js → index-BWxQD9cP.js} +1 -1
  49. package/dist-embed/chunks/{index-DCX_IqLg.js → index-B_WAs-60.js} +1 -1
  50. package/dist-embed/chunks/{index-BYofH50G.js → index-BaEB7wmN.js} +1 -1
  51. package/dist-embed/chunks/{index-C0iUgNwj.js → index-BmSB4k4X.js} +1 -1
  52. package/dist-embed/chunks/{index-KWzHwvvN.js → index-BsGIdntV.js} +4 -4
  53. package/dist-embed/chunks/{index-DgnVcOqG.js → index-BsP7mWx7.js} +1 -1
  54. package/dist-embed/chunks/{index-aX9x7qni.js → index-BzZvBTsd.js} +1 -1
  55. package/dist-embed/chunks/{index-BA2STD4N.js → index-C0ac25cM.js} +2 -2
  56. package/dist-embed/chunks/{index-DLk6Znxv.js → index-C0fJQ9v3.js} +1 -1
  57. package/dist-embed/chunks/{index-Bs1pVq7E.js → index-C0p6TrSH.js} +1 -1
  58. package/dist-embed/chunks/{index-DZN9JYOE.js → index-C9mAUugR.js} +1 -1
  59. package/dist-embed/chunks/{index-B1Egld01.js → index-CNBMan5A.js} +1 -1
  60. package/dist-embed/chunks/{index-1Ekpl0uC.js → index-CawAyW3R.js} +1 -1
  61. package/dist-embed/chunks/{index-DeYQGrTz.js → index-Cb-91zWU.js} +1 -1
  62. package/dist-embed/chunks/{index-B2dtLxcg.js → index-CkMGJGPw.js} +1 -1
  63. package/dist-embed/chunks/{index-Cqur_rhw.js → index-CsbfZWj5.js} +1 -1
  64. package/dist-embed/chunks/{index-DsX9nIHt.js → index-D1HhRyAi.js} +1 -1
  65. package/dist-embed/chunks/{index-C5xKtdLU.js → index-D3ml5vHy.js} +1 -1
  66. package/dist-embed/chunks/{index-CqKV027P.js → index-D7SzNRox.js} +1 -1
  67. package/dist-embed/chunks/{index-BUbwVG__.js → index-DAwoO3Uu.js} +1 -1
  68. package/dist-embed/chunks/{index-DHVYKZ44.js → index-DQaxtq0S.js} +1 -1
  69. package/dist-embed/chunks/{index-BbsUSk_Q.js → index-DbPtsB38.js} +1 -1
  70. package/dist-embed/chunks/{index-DJomc4za.js → index-DoYPeIKg.js} +1 -1
  71. package/dist-embed/chunks/{index-hqa_ODOD.js → index-Dr_NppDz.js} +1 -1
  72. package/dist-embed/chunks/{index-D7rnWGLu.js → index-DuDsMDnx.js} +3 -3
  73. package/dist-embed/chunks/{index-B5MiaXUV.js → index-IIxS-G5t.js} +1 -1
  74. package/dist-embed/chunks/{index-90Ss7516.js → index-W7nq_bBb.js} +1 -1
  75. package/dist-embed/chunks/{index-Cjx6uk6z.js → index-r_7Z9Uyr.js} +1 -1
  76. package/dist-embed/chunks/{index.esm-DlPZAW-m.js → index.esm-D5bfliIL.js} +1 -1
  77. package/dist-embed/chunks/{it-IT-JPQ66NNP-C2vJMuKM.js → it-IT-JPQ66NNP-CJGzhajv.js} +1 -1
  78. package/dist-embed/chunks/{ja-JP-DBVTYXUO-C8KOKQt9.js → ja-JP-DBVTYXUO-Jm7LgxUu.js} +1 -1
  79. package/dist-embed/chunks/{kaa-6HZHGXH3-CvxJbmMB.js → kaa-6HZHGXH3-DH21E_2q.js} +1 -1
  80. package/dist-embed/chunks/{kab-KAB-ZGHBKWFO-CQJhf9Nn.js → kab-KAB-ZGHBKWFO-2Cx_3mmx.js} +1 -1
  81. package/dist-embed/chunks/{kk-KZ-P5N5QNE5-Dl6YC0GH.js → kk-KZ-P5N5QNE5-C2v01t5f.js} +1 -1
  82. package/dist-embed/chunks/{km-KH-HSX4SM5Z-QYB-tJ4i.js → km-KH-HSX4SM5Z-WL_JAugw.js} +1 -1
  83. package/dist-embed/chunks/{ko-KR-MTYHY66A-D6yzBqJ5.js → ko-KR-MTYHY66A-DWjrj8WD.js} +1 -1
  84. package/dist-embed/chunks/{ku-TR-6OUDTVRD-C43l2362.js → ku-TR-6OUDTVRD-CwTRLeeu.js} +1 -1
  85. package/dist-embed/chunks/{lt-LT-XHIRWOB4-BG1v5NwF.js → lt-LT-XHIRWOB4-GVGjjS1d.js} +1 -1
  86. package/dist-embed/chunks/{lv-LV-5QDEKY6T-CAwm1y_0.js → lv-LV-5QDEKY6T-C29ftTES.js} +1 -1
  87. package/dist-embed/chunks/{markdown-CH2i4TVR.js → markdown-Bf4-A1yl.js} +1 -1
  88. package/dist-embed/chunks/{mr-IN-CRQNXWMA-DSk6q1fp.js → mr-IN-CRQNXWMA-Cyym_1yB.js} +1 -1
  89. package/dist-embed/chunks/{my-MM-5M5IBNSE-BRiKXulk.js → my-MM-5M5IBNSE-BOkGbWeI.js} +1 -1
  90. package/dist-embed/chunks/{nb-NO-T6EIAALU-CCDnctVd.js → nb-NO-T6EIAALU-BjaVTNUf.js} +1 -1
  91. package/dist-embed/chunks/{nl-NL-IS3SIHDZ-DwStD_Ch.js → nl-NL-IS3SIHDZ-DSVCFPQj.js} +1 -1
  92. package/dist-embed/chunks/{nn-NO-6E72VCQL-DNcfjETk.js → nn-NO-6E72VCQL-D5-_rRv0.js} +1 -1
  93. package/dist-embed/chunks/{oc-FR-POXYY2M6-BOjjS12P.js → oc-FR-POXYY2M6-CxKcn1R4.js} +1 -1
  94. package/dist-embed/chunks/{pa-IN-N4M65BXN-B4y5cv2_.js → pa-IN-N4M65BXN-D9r6O5PP.js} +1 -1
  95. package/dist-embed/chunks/{percentages-BXMCSKIN-BpahO1D1.js → percentages-BXMCSKIN-917PkDRT.js} +3 -3
  96. package/dist-embed/chunks/{pl-PL-T2D74RX3-CfuLoYHB.js → pl-PL-T2D74RX3-CFXCTA5m.js} +1 -1
  97. package/dist-embed/chunks/{plugin-BMH63P5M.js → plugin-CA_0EWiO.js} +1 -1
  98. package/dist-embed/chunks/plugin-LDJjixuG.js +1 -0
  99. package/dist-embed/chunks/{pt-BR-5N22H2LF-Fk60PfdA.js → pt-BR-5N22H2LF-7A-3KOon.js} +1 -1
  100. package/dist-embed/chunks/{pt-PT-UZXXM6DQ-SNAgJ5Vr.js → pt-PT-UZXXM6DQ-BJLo5w79.js} +1 -1
  101. package/dist-embed/chunks/{ro-RO-JPDTUUEW-YkxpKkWn.js → ro-RO-JPDTUUEW-DtvOR1_K.js} +1 -1
  102. package/dist-embed/chunks/{ru-RU-B4JR7IUQ-CaI1rtvc.js → ru-RU-B4JR7IUQ-CGQ0z7l6.js} +1 -1
  103. package/dist-embed/chunks/{sessions-ZDYLkdi_.js → sessions-By2lggFS.js} +2 -2
  104. package/dist-embed/chunks/{si-LK-N5RQ5JYF-CQz29xmh.js → si-LK-N5RQ5JYF-D13wg56d.js} +1 -1
  105. package/dist-embed/chunks/{sk-SK-C5VTKIMK-CZg7PVzd.js → sk-SK-C5VTKIMK-BqAZ0R3f.js} +1 -1
  106. package/dist-embed/chunks/{sl-SI-NN7IZMDC-D-EjMWyZ.js → sl-SI-NN7IZMDC-BjPUGdws.js} +1 -1
  107. package/dist-embed/chunks/{standalone-oyxre9MX.js → standalone-DyCMuq_1.js} +1 -1
  108. package/dist-embed/chunks/{subset-shared.chunk-BIvvzzX2.js → subset-shared.chunk-D-GUkcCO.js} +1 -1
  109. package/dist-embed/chunks/{subset-worker.chunk-DGgI6iRY.js → subset-worker.chunk-Bz8aK3Nt.js} +1 -1
  110. package/dist-embed/chunks/{sv-SE-XGPEYMSR-D8CsNMON.js → sv-SE-XGPEYMSR-DrXTnEWd.js} +1 -1
  111. package/dist-embed/chunks/{ta-IN-2NMHFXQM-CvuxKO7t.js → ta-IN-2NMHFXQM-bK104VxM.js} +1 -1
  112. package/dist-embed/chunks/{th-TH-HPSO5L25-NgQJigwq.js → th-TH-HPSO5L25-BSo6r7L4.js} +1 -1
  113. package/dist-embed/chunks/{tr-TR-DEFEU3FU-eu9-qLmi.js → tr-TR-DEFEU3FU-pYKvN-ro.js} +1 -1
  114. package/dist-embed/chunks/{uk-UA-QMV73CPH-CbjmuLVL.js → uk-UA-QMV73CPH-DFR0z9gx.js} +1 -1
  115. package/dist-embed/chunks/{usePrimerThemeBridge-LgREOVgz.js → usePrimerThemeBridge-CuRRiOcg.js} +1 -1
  116. package/dist-embed/chunks/{vi-VN-M7AON7JQ-u84yOuJ-.js → vi-VN-M7AON7JQ-BwMeNi1s.js} +1 -1
  117. package/dist-embed/chunks/{zh-CN-LNUGB5OW-ojkSgl-t.js → zh-CN-LNUGB5OW-QC_JIHYo.js} +1 -1
  118. package/dist-embed/chunks/{zh-HK-E62DVLB3-C6M89-Da.js → zh-HK-E62DVLB3-BHs8J1Gl.js} +1 -1
  119. package/dist-embed/chunks/{zh-TW-RAJ6MFWO-BaTtFORh.js → zh-TW-RAJ6MFWO-DpqpYlIS.js} +1 -1
  120. package/dist-embed/datalayer-app-main.js +330 -59
  121. package/lib/apps/apps/AppBrowserSandbox.d.ts +7 -0
  122. package/lib/apps/apps/AppBrowserSandbox.js +52 -0
  123. package/lib/apps/apps/AppRenderer.js +11 -2
  124. package/lib/apps/apps/appspec.js +31 -0
  125. package/lib/apps/apps/browserSandbox.d.ts +69 -0
  126. package/lib/apps/apps/browserSandbox.js +159 -0
  127. package/lib/apps/apps/index.d.ts +2 -0
  128. package/lib/apps/apps/index.js +2 -0
  129. package/lib/apps/plugins/chat/ChatView.js +59 -7
  130. package/lib/components/inspector/a2aSpans.js +5 -1
  131. package/lib/specs/apps.js +345 -8
  132. package/lib/specs/appspecSchema.js +79 -0
  133. package/lib/types/agentspecs.d.ts +28 -0
  134. package/package.json +1 -1
  135. package/scripts/codegen/generate_apps.py +3 -2
  136. package/dist-embed/chunks/Avatar-Dqz3ZWjZ.js +0 -1
  137. package/dist-embed/chunks/ChatView-CubqJum2.js +0 -288
  138. package/dist-embed/chunks/DatalayerAgentBridge-BmJbZ2Lk.js +0 -1
  139. package/dist-embed/chunks/plugin-DZPXm2dP.js +0 -1
@@ -0,0 +1,7 @@
1
+ import type { AppSpec } from '../../types/agentspecs';
2
+ export declare const APP_BROWSER_SANDBOX_PLUGIN_NAME = "@datalayer/loop-plugin-app-browser-sandbox";
3
+ /**
4
+ * The plugin, for one application. Made once per application — a new plugin
5
+ * rebuilds the workspace.
6
+ */
7
+ export declare function defineAppBrowserSandboxPlugin(app: Pick<AppSpec, 'id' | 'name' | 'samples'>): import("@datalayer/reactor").ReactorPlugin<Record<string, never>, unknown, unknown>;
@@ -0,0 +1,52 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ /**
6
+ * An application's code in the page's sandbox (STUDIO E-11): the tool its
7
+ * agent runs its code with when it turns in the page — `execute_code`,
8
+ * Python in this browser — and its documents put there before its first run
9
+ * (`browserSandbox`).
10
+ *
11
+ * Handed to the agent only while the sandbox is the browser's: on a runtime
12
+ * the agent has a sandbox of its own, and a tool of the page would be a
13
+ * second one beside it. `AppRenderer` adds the plugin for an application
14
+ * whose agent computes in code (`runsCodeInSandbox`).
15
+ *
16
+ * @module apps/apps/AppBrowserSandbox
17
+ */
18
+ import { definePlugin } from '@datalayer/reactor';
19
+ import { LoopFrontendTool } from '../core';
20
+ import { AGENTS_PLUGIN_NAME, AgentsPlugin, } from '../plugins/agents';
21
+ import { browserCodeTool } from './browserSandbox';
22
+ export const APP_BROWSER_SANDBOX_PLUGIN_NAME = '@datalayer/loop-plugin-app-browser-sandbox';
23
+ /**
24
+ * The plugin, for one application. Made once per application — a new plugin
25
+ * rebuilds the workspace.
26
+ */
27
+ export function defineAppBrowserSandboxPlugin(app) {
28
+ return definePlugin({
29
+ name: `${APP_BROWSER_SANDBOX_PLUGIN_NAME}-${app.id}`,
30
+ displayName: `${app.name}: its code in the browser`,
31
+ description: `What ${app.name} computes, run in Python in this browser when it turns in the page.`,
32
+ octicon: 'code',
33
+ emoji: '\u{1F40D}',
34
+ // The sandbox is the Agents plugin's: built first, read here.
35
+ dependencies: [AgentsPlugin],
36
+ register({ contribute, reactor }) {
37
+ const sandbox = reactor.getOutput(AGENTS_PLUGIN_NAME)?.sandbox;
38
+ if (!sandbox) {
39
+ return;
40
+ }
41
+ // One tool for the life of the workspace: its documents are put in the
42
+ // kernel once, before its first run.
43
+ const tool = browserCodeTool(app, code => sandbox.execute(code));
44
+ return contribute(LoopFrontendTool, {
45
+ id: `app-browser-sandbox-${app.id}`,
46
+ tools: workspace => workspace.sandbox.target === 'browser' ? [tool] : [],
47
+ // It changes no editor: kept in the chat view and beside a page.
48
+ chatView: true,
49
+ }, { id: `app-browser-sandbox-${app.id}` });
50
+ },
51
+ });
52
+ }
@@ -74,6 +74,8 @@ import { defineAppRuntimePlugin } from './AppRuntimePlugins';
74
74
  import { hasFolderModules } from './customComponents';
75
75
  import { appPluginPair } from './pluginPair';
76
76
  import { visitorAgentOf } from './visitorToken';
77
+ import { defineAppBrowserSandboxPlugin } from './AppBrowserSandbox';
78
+ import { browserSandboxNote, runsCodeInSandbox } from './browserSandbox';
77
79
  /** The id of an agent or a Cog, without its version. */
78
80
  export const agentIdOf = (app) => {
79
81
  const reference = app.agent || app.team;
@@ -127,8 +129,12 @@ export function defineAppPlugin(app, shown = app) {
127
129
  suggestions: startersAsOpeners(shown.interface.starters),
128
130
  // In the page, the agent is told what a runtime tells it: its own
129
131
  // spec's prompt, then the application's instructions; on the
130
- // application's model when it names one (`apps/apps/agent`).
131
- instructions: app.instructions,
132
+ // application's model when it names one (`apps/apps/agent`). An agent
133
+ // that computes in code is told where its code runs there, and where its
134
+ // documents are (STUDIO E-11, `browserSandbox`).
135
+ instructions: runsCodeInSandbox(app)
136
+ ? [app.instructions, browserSandboxNote(app)].filter(Boolean).join('\n\n')
137
+ : app.instructions,
132
138
  model: app.model,
133
139
  // Its modes and its profiles (LOOP P-19, P-20), as the session API
134
140
  // applies them: the profile's instructions and those of the options
@@ -238,6 +244,9 @@ export function appPreset(app, options = {}) {
238
244
  // page side read from it when its components are files of its folder
239
245
  // (P-29).
240
246
  defineAppRuntimePlugin(app.id, { pageSide: hasFolderModules(app) }),
247
+ // Its code in the browser's sandbox, when it turns in the page and
248
+ // computes in code (STUDIO E-11): `execute_code`, its documents there.
249
+ ...(runsCodeInSandbox(app) ? [defineAppBrowserSandboxPlugin(app)] : []),
241
250
  ...(withPage ? [defineAppPagePlugin(shown), ...blocks] : []),
242
251
  // The components its developer wrote (LOOP P-17): blocks of its page
243
252
  // and renderers of its page, its elements and its answers — its own.
@@ -150,6 +150,7 @@ export function emptyAppspec(kind = 'chat') {
150
150
  triggers: [],
151
151
  memory: '',
152
152
  notifications: [],
153
+ samples: { documents: [], alternatives: [] },
153
154
  setup: [],
154
155
  enabled: true,
155
156
  unavailable_because: '',
@@ -191,6 +192,7 @@ const KNOWN_KEYS = [
191
192
  'memory',
192
193
  'notifications',
193
194
  'decision',
195
+ 'samples',
194
196
  'enabled',
195
197
  'unavailable_because',
196
198
  'tags',
@@ -542,6 +544,21 @@ function parseDecision(data) {
542
544
  decisionModel: text(data.decision_model),
543
545
  };
544
546
  }
547
+ /** What it is tried on before it is anybody's (STUDIO E-06, E-11): read only. */
548
+ function parseSamples(data) {
549
+ return {
550
+ documents: records(data.documents).map(document => ({
551
+ name: text(document.name),
552
+ file: text(document.file),
553
+ text: text(document.text),
554
+ })),
555
+ alternatives: records(data.alternatives).map(alternative => ({
556
+ name: text(alternative.name),
557
+ evidence: text(alternative.evidence),
558
+ metrics: Object.fromEntries(Object.entries(isData(alternative.metrics) ? alternative.metrics : {}).filter((entry) => typeof entry[1] === 'number')),
559
+ })),
560
+ };
561
+ }
545
562
  function parseDeployment(data) {
546
563
  const deployment = {};
547
564
  if (isData(data.hosted)) {
@@ -707,6 +724,7 @@ export function parseAppspec(document) {
707
724
  triggers: records(data.triggers).map(parseTrigger),
708
725
  memory: text(data.memory),
709
726
  notifications: texts(data.notifications),
727
+ samples: parseSamples(isData(data.samples) ? data.samples : {}),
710
728
  enabled: flag(data.enabled, true),
711
729
  unavailable_because: text(data.unavailable_because),
712
730
  tags: texts(data.tags),
@@ -915,6 +933,18 @@ function dumpDecision(decision) {
915
933
  .part('weights', sorted(scenario.weights)).data))
916
934
  .text('decision_model', decision.decisionModel).data;
917
935
  }
936
+ function dumpSamples(samples) {
937
+ return new Writer()
938
+ .list('documents', samples.documents.map(document => new Writer()
939
+ .text('name', document.name, '\u0000')
940
+ .text('file', document.file, '\u0000')
941
+ .text('text', document.text, '\u0000').data))
942
+ .list('alternatives', samples.alternatives.map(alternative => new Writer()
943
+ .text('name', alternative.name, '\u0000')
944
+ .text('evidence', alternative.evidence, '\u0000')
945
+ // By criterion name, as written: the spec keeps their order.
946
+ .part('metrics', { ...alternative.metrics }).data)).data;
947
+ }
918
948
  /**
919
949
  * An application as the document its file holds.
920
950
  *
@@ -1037,6 +1067,7 @@ export function dumpAppspec(app) {
1037
1067
  writer.data.decision = dumpDecision(app.decision);
1038
1068
  }
1039
1069
  writer
1070
+ .part('samples', dumpSamples(app.samples))
1040
1071
  .value('enabled', app.enabled, true)
1041
1072
  .text('unavailable_because', app.unavailable_because)
1042
1073
  .list('tags', app.tags)
@@ -0,0 +1,69 @@
1
+ /**
2
+ * An application's code, run in the page (STUDIO E-11).
3
+ *
4
+ * On a runtime, an application's agent runs its code in a sandbox of the
5
+ * runtime's, through `execute_code` — what *Quote Calculator* and *Report
6
+ * from a File* compute with. Turned in the page, the same agent has the
7
+ * page's sandbox instead: Python in this browser (Pyodide), the one the
8
+ * Browser target starts. Nothing of the code runs on Datalayer, and nothing
9
+ * it writes leaves the browser.
10
+ *
11
+ * What it computes from is put there as files, under {@link SANDBOX_INPUTS}:
12
+ * the documents Datalayer publishes with the application (`samples`, read
13
+ * only) before its first run, and the files given on its page with the
14
+ * message that runs on them — which the page used to refuse, having no
15
+ * session to hand them to.
16
+ *
17
+ * Pure: no React, no kernel. The tool that runs it is
18
+ * `AppBrowserSandboxPlugin`'s.
19
+ *
20
+ * @module apps/apps/browserSandbox
21
+ */
22
+ import type { AppSpec } from '../../types/agentspecs';
23
+ import type { FrontendToolDefinition } from '../../types/tools';
24
+ import type { SandboxExecution } from '../plugins/agents/service';
25
+ /** The tool the agent runs its code with: the name a runtime gives it too. */
26
+ export declare const BROWSER_CODE_TOOL = "execute_code";
27
+ /** Where its files are, in the page's sandbox: in memory, gone with the page. */
28
+ export declare const SANDBOX_INPUTS = "/tmp/inputs";
29
+ /** A file for the sandbox: its name there, and what it holds, in base64. */
30
+ export type SandboxFile = {
31
+ file: string;
32
+ base64: string;
33
+ };
34
+ /**
35
+ * Whether an application's agent runs code: its agent has a sandbox, as every
36
+ * agent a runtime gives `execute_code` has.
37
+ */
38
+ export declare function runsCodeInSandbox(app: Pick<AppSpec, 'agent'>): boolean;
39
+ /** Where a file is, in the page's sandbox. */
40
+ export declare const sandboxPath: (file: string) => string;
41
+ /**
42
+ * A file's name as the sandbox is given it: its own, without a folder, and
43
+ * only the characters a path says plainly.
44
+ */
45
+ export declare function sandboxFileName(name: string): string;
46
+ /** UTF-8 text as base64, as the sandbox is given it. */
47
+ export declare function textAsBase64(text: string): string;
48
+ /** The base64 of a `data:` URL, as a page's file is given — or why it has none. */
49
+ export declare function dataUrlBase64(dataUrl: string): string;
50
+ /** The documents Datalayer publishes with the application, as files. */
51
+ export declare function sampleFiles(app: Pick<AppSpec, 'samples'>): SandboxFile[];
52
+ /** The Python that writes files into the sandbox. */
53
+ export declare function writeFilesCode(files: SandboxFile[]): string;
54
+ /**
55
+ * What the agent is told in the page, after its own prompt and the
56
+ * application's instructions: where its code runs, and where its documents
57
+ * are.
58
+ */
59
+ export declare function browserSandboxNote(app: Pick<AppSpec, 'samples'>): string;
60
+ /** What the message that runs on given files says of them. */
61
+ export declare function givenFilesSentence(files: string[]): string;
62
+ /** What the model reads of an execution: what it printed, what it returned, what failed. */
63
+ export declare function executionForModel(execution: SandboxExecution): Record<string, unknown>;
64
+ /**
65
+ * The tool an application's agent runs its code with, in the page: Python
66
+ * in the browser's sandbox, its documents written there before the first
67
+ * call.
68
+ */
69
+ export declare function browserCodeTool(app: Pick<AppSpec, 'samples'>, execute: (code: string) => Promise<SandboxExecution>): FrontendToolDefinition;
@@ -0,0 +1,159 @@
1
+ /*
2
+ * Copyright (c) 2025-2026 Datalayer, Inc.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+ import { resolveAgentspec } from './agent';
6
+ /** The tool the agent runs its code with: the name a runtime gives it too. */
7
+ export const BROWSER_CODE_TOOL = 'execute_code';
8
+ /** Where its files are, in the page's sandbox: in memory, gone with the page. */
9
+ export const SANDBOX_INPUTS = '/tmp/inputs';
10
+ /**
11
+ * Whether an application's agent runs code: its agent has a sandbox, as every
12
+ * agent a runtime gives `execute_code` has.
13
+ */
14
+ export function runsCodeInSandbox(app) {
15
+ return Boolean(app.agent && resolveAgentspec(app.agent)?.sandboxVariant);
16
+ }
17
+ /** Where a file is, in the page's sandbox. */
18
+ export const sandboxPath = (file) => `${SANDBOX_INPUTS}/${file}`;
19
+ /**
20
+ * A file's name as the sandbox is given it: its own, without a folder, and
21
+ * only the characters a path says plainly.
22
+ */
23
+ export function sandboxFileName(name) {
24
+ const base = name.split(/[\\/]/).pop() ?? '';
25
+ const plain = base.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^[.-]+/, '');
26
+ return plain || 'file';
27
+ }
28
+ /** UTF-8 text as base64, as the sandbox is given it. */
29
+ export function textAsBase64(text) {
30
+ const bytes = new TextEncoder().encode(text);
31
+ let binary = '';
32
+ for (const byte of bytes) {
33
+ binary += String.fromCharCode(byte);
34
+ }
35
+ return btoa(binary);
36
+ }
37
+ /** The base64 of a `data:` URL, as a page's file is given — or why it has none. */
38
+ export function dataUrlBase64(dataUrl) {
39
+ const at = dataUrl.indexOf(',');
40
+ if (!dataUrl.startsWith('data:') ||
41
+ at < 0 ||
42
+ !/;base64$/.test(dataUrl.slice(0, at))) {
43
+ throw new Error('A file given on the page is not a base64 data URL.');
44
+ }
45
+ return dataUrl.slice(at + 1);
46
+ }
47
+ /** The documents Datalayer publishes with the application, as files. */
48
+ export function sampleFiles(app) {
49
+ return (app.samples?.documents ?? []).map(document => ({
50
+ file: document.file,
51
+ base64: textAsBase64(document.text),
52
+ }));
53
+ }
54
+ /** The Python that writes files into the sandbox. */
55
+ export function writeFilesCode(files) {
56
+ const entries = files
57
+ .map(({ file, base64 }) => ` (${JSON.stringify(file)}, ${JSON.stringify(base64)}),`)
58
+ .join('\n');
59
+ return [
60
+ 'import base64 as _b64, pathlib as _pl',
61
+ `_inputs = _pl.Path(${JSON.stringify(SANDBOX_INPUTS)})`,
62
+ '_inputs.mkdir(parents=True, exist_ok=True)',
63
+ 'for _name, _data in [',
64
+ entries,
65
+ ']:',
66
+ ' (_inputs / _name).write_bytes(_b64.b64decode(_data))',
67
+ 'del _b64, _pl, _inputs',
68
+ ].join('\n');
69
+ }
70
+ /**
71
+ * What the agent is told in the page, after its own prompt and the
72
+ * application's instructions: where its code runs, and where its documents
73
+ * are.
74
+ */
75
+ export function browserSandboxNote(app) {
76
+ const documents = app.samples?.documents ?? [];
77
+ return [
78
+ `Your code runs in Python (Pyodide) in this browser, through \`${BROWSER_CODE_TOOL}\`. ` +
79
+ 'Variables and imports persist between calls. Compute every figure there, ' +
80
+ 'print it, and answer from what it printed.',
81
+ ...(documents.length > 0
82
+ ? [
83
+ 'The documents you answer from are files there, read only: ' +
84
+ documents
85
+ .map(document => `${document.name} at \`${sandboxPath(document.file)}\``)
86
+ .join('; ') +
87
+ '. Read them in code before you answer.',
88
+ ]
89
+ : []),
90
+ `A file given on the page is put in \`${SANDBOX_INPUTS}\` too: the message that runs on it names it.`,
91
+ ].join('\n\n');
92
+ }
93
+ /** What the message that runs on given files says of them. */
94
+ export function givenFilesSentence(files) {
95
+ return `The files given are in the sandbox: ${files.map(file => `\`${sandboxPath(file)}\``).join(', ')}.`;
96
+ }
97
+ /** What the model reads of an execution: what it printed, what it returned, what failed. */
98
+ export function executionForModel(execution) {
99
+ const results = (execution.outputs ?? []).flatMap(output => {
100
+ const data = output ??
101
+ {};
102
+ if (data.output_type !== 'execute_result' &&
103
+ data.output_type !== 'display_data') {
104
+ return [];
105
+ }
106
+ const plain = data.data?.['text/plain'];
107
+ return typeof plain === 'string' ? [plain] : [];
108
+ });
109
+ return {
110
+ success: execution.success,
111
+ ...(execution.stdout ? { stdout: execution.stdout } : {}),
112
+ ...(results.length > 0 ? { results } : {}),
113
+ ...(execution.error ? { error: execution.error } : {}),
114
+ };
115
+ }
116
+ /**
117
+ * The tool an application's agent runs its code with, in the page: Python
118
+ * in the browser's sandbox, its documents written there before the first
119
+ * call.
120
+ */
121
+ export function browserCodeTool(app, execute) {
122
+ let seeded = null;
123
+ const seed = () => {
124
+ const files = sampleFiles(app);
125
+ seeded =
126
+ seeded ??
127
+ (files.length === 0
128
+ ? Promise.resolve()
129
+ : execute(writeFilesCode(files)).then(written => {
130
+ if (!written.success) {
131
+ seeded = null;
132
+ throw new Error(`The application's documents could not be put in the sandbox: ${written.error ?? 'it failed'}.`);
133
+ }
134
+ }));
135
+ return seeded;
136
+ };
137
+ return {
138
+ name: BROWSER_CODE_TOOL,
139
+ description: 'Run Python in the sandbox, in this browser (Pyodide). Variables and imports ' +
140
+ `persist between calls. The application's documents and the files given on ` +
141
+ `its page are under ${SANDBOX_INPUTS}. Returns what it printed, what it ` +
142
+ 'returned and any error.',
143
+ parameters: {
144
+ type: 'object',
145
+ properties: {
146
+ code: { type: 'string', description: 'The Python to run.' },
147
+ },
148
+ required: ['code'],
149
+ },
150
+ handler: async (args) => {
151
+ const code = typeof args?.code === 'string' ? args.code : '';
152
+ if (!code.trim()) {
153
+ return { success: false, error: 'No code was given to run.' };
154
+ }
155
+ await seed();
156
+ return executionForModel(await execute(code));
157
+ },
158
+ };
159
+ }
@@ -4,11 +4,13 @@
4
4
  * @module apps/apps
5
5
  */
6
6
  export * from './agent';
7
+ export * from './AppBrowserSandbox';
7
8
  export * from './AppComposer';
8
9
  export * from './AppFeedback';
9
10
  export * from './AppKept';
10
11
  export * from './AppRenderer';
11
12
  export * from './appspec';
13
+ export * from './browserSandbox';
12
14
  export * from './checks';
13
15
  export * from './composer';
14
16
  export * from './computer';
@@ -8,11 +8,13 @@
8
8
  * @module apps/apps
9
9
  */
10
10
  export * from './agent';
11
+ export * from './AppBrowserSandbox';
11
12
  export * from './AppComposer';
12
13
  export * from './AppFeedback';
13
14
  export * from './AppKept';
14
15
  export * from './AppRenderer';
15
16
  export * from './appspec';
17
+ export * from './browserSandbox';
16
18
  export * from './checks';
17
19
  export * from './composer';
18
20
  export * from './computer';
@@ -37,7 +37,8 @@ import { INPUT_PROMPT_PLUGIN_NAME, } from '../input-prompt';
37
37
  import { agentRuntimeStore, useAgentRuntimeContextSnapshot, } from '../../../stores';
38
38
  import { useIAMStore } from '../../../state';
39
39
  import { useVisitorToken } from '../../apps/visitorToken';
40
- import { AGENTS_PLUGIN_NAME } from '../agents/plugin';
40
+ import { AGENTS_PLUGIN_NAME, } from '../agents/plugin';
41
+ import { BROWSER_CODE_TOOL, dataUrlBase64, givenFilesSentence, sandboxFileName, writeFilesCode, } from '../../apps/browserSandbox';
41
42
  import { useConfig } from '../../../hooks/useConfig';
42
43
  import { useSkills, useSkillActions } from '../../../hooks/useSkills';
43
44
  import { AI_MODEL_CATALOGUE, isChatModel } from '../../../specs/models';
@@ -357,6 +358,18 @@ export default function ChatView({ workspace }) {
357
358
  const heldForAdapter = useRef([]);
358
359
  /** Whether the agent is turned in this page: read by `sendNow`, set below. */
359
360
  const inPageRef = useRef(false);
361
+ /**
362
+ * The page's sandbox, when the agent turned in the page runs its code
363
+ * there (`execute_code`, STUDIO E-11): what a file given on the page is
364
+ * put in. Read by `sendNow`, set below.
365
+ */
366
+ const pageSandboxRef = useRef(undefined);
367
+ /**
368
+ * Why its host closed the conversation (`ChatAvailabilityProvider`), or
369
+ * nothing: a page's own send is refused with it as the composer is — a
370
+ * stopped deployment, a visitor's turns all taken. Set below.
371
+ */
372
+ const hostClosedRef = useRef(undefined);
360
373
  /** What goes with every run (an application's modes, LOOP P-19), read when it is sent. */
361
374
  const runProps = useContributions(LoopRunProps);
362
375
  const runPropsRef = useRef(runProps);
@@ -369,18 +382,49 @@ export default function ChatView({ workspace }) {
369
382
  *
370
383
  * What the page did besides the message goes with it as AG-UI's
371
384
  * `forwardedProps` (`loop`, LOOP R-04): the application's session takes a
372
- * file there. An agent turned in this page has no session, and no file is
373
- * handed to it: said, rather than dropped. What the workspace's plugins
374
- * send with every run (`LoopRunProps`: the modes the person is in, LOOP
375
- * P-19) goes under it — and an agent turned in this page applies the modes
376
- * itself (`modeEffect`, `BrowserAgentAdapter`).
385
+ * file there. An agent turned in this page has no session: one that runs
386
+ * its code in the page's sandbox has the files put there, and the message
387
+ * names them (STUDIO E-11); to any other no file is handed — said, rather
388
+ * than dropped. What the workspace's plugins send with every run
389
+ * (`LoopRunProps`: the modes the person is in, LOOP P-19) goes under it —
390
+ * and an agent turned in this page applies the modes itself (`modeEffect`,
391
+ * `BrowserAgentAdapter`).
377
392
  */
378
393
  const sendNow = useCallback((message, given) => {
394
+ if (hostClosedRef.current) {
395
+ return hostClosedRef.current;
396
+ }
379
397
  const forwardedProps = runForwardedProps(runPropsRef.current.map(entry => entry.value), given);
380
398
  // The page's files, or those attached in the composer (LOOP P-21).
381
399
  const loop = forwardedProps?.loop;
382
400
  if (inPageRef.current && loop?.files && loop.files.length > 0) {
383
- return 'A file is given to an application running on a runtime: run it on Datalayer or on your machine to give it one.';
401
+ const sandbox = pageSandboxRef.current;
402
+ if (!sandbox) {
403
+ return 'A file is given to an application running on a runtime: run it on Datalayer or on your machine to give it one.';
404
+ }
405
+ let files;
406
+ try {
407
+ files = loop.files.map(file => ({
408
+ file: sandboxFileName(String(file.name ?? '')),
409
+ base64: dataUrlBase64(String(file.data_url ?? '')),
410
+ }));
411
+ }
412
+ catch (error) {
413
+ return error instanceof Error ? error.message : String(error);
414
+ }
415
+ // Put in the sandbox first, then sent: the message names where.
416
+ void sandbox.execute(writeFilesCode(files)).then(written => {
417
+ if (!written.success) {
418
+ setTransient(`The files could not be put in the sandbox: ${written.error ?? 'it failed'}.`);
419
+ return;
420
+ }
421
+ // Sent as any message is, its files now in the sandbox.
422
+ sendNow(`${message}\n\n${givenFilesSentence(files.map(file => file.file))}`, {
423
+ ...forwardedProps,
424
+ loop: Object.fromEntries(Object.entries(loop).filter(([key]) => key !== 'files')),
425
+ });
426
+ });
427
+ return;
384
428
  }
385
429
  // A new turn: whatever the panel showed is gone, this message is it.
386
430
  turnFeedRef.current?.begin(message, controlsRef.current?.thread());
@@ -938,6 +982,11 @@ export default function ChatView({ workspace }) {
938
982
  ...commandTools.filter(tool => !taken.has(tool.name)),
939
983
  ];
940
984
  }, [notebookTools, commandTools]);
985
+ // The page's sandbox, while the agent turned here runs its code in it.
986
+ pageSandboxRef.current =
987
+ inPage && agentTools.some(tool => tool.name === BROWSER_CODE_TOOL)
988
+ ? reactor.getOutput(AGENTS_PLUGIN_NAME)?.sandbox
989
+ : undefined;
941
990
  /*
942
991
  * Where this workspace's agent actually lives.
943
992
  *
@@ -1045,6 +1094,9 @@ export default function ChatView({ workspace }) {
1045
1094
  * Studio's Preview and the hosted page, rather than left unanswered.
1046
1095
  */
1047
1096
  const ambient = useChatAvailability();
1097
+ hostClosedRef.current = ambient.disabled
1098
+ ? ambient.disableReason || 'This conversation is closed here.'
1099
+ : undefined;
1048
1100
  /*
1049
1101
  * No agent to talk to: a Datalayer runtime not assigned (STUDIO P-24,
1050
1102
  * round 9). Said in a sentence — why, when its launch was refused — and
@@ -199,7 +199,11 @@ class ResponseRecorder {
199
199
  'a2a.artifact.media_type': typeof media === 'string' ? media : undefined,
200
200
  },
201
201
  };
202
- chunk.text += text;
202
+ // An `append` chunk adds to what came; one that is not says the artifact
203
+ // whole — the runtime's last chunk carries the whole answer again. Added
204
+ // to it, an answer said in one chunk (a visitor's refusal) was written
205
+ // twice in the transcript (STUDIO H-03, 2026-10-10).
206
+ chunk.text = update.append ? chunk.text + text : text;
203
207
  chunk.events.push(message);
204
208
  if (update.append && !update.lastChunk) {
205
209
  // A chunk of an artifact being streamed: one event once it is whole.