@aliceshimada/mica 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.0 - 2026-06-16
4
+
5
+ - Add `mma_create_notebook` tool: create a new blank notebook in the Wolfram FrontEnd.
6
+ - Add `mma_open_notebook` tool: open an existing notebook file (.nb) from disk.
7
+ - Add `CreateNotebook` and `OpenNotebook` permissions (default false).
8
+ - Fix agent-level tools when no live notebook exists for routing.
9
+
3
10
  ## 1.1.1 - 2026-06-16
4
11
 
5
12
  - Fix `RestartKernelRequest`: kill kernel via `Quit[]` before restarting.
@@ -3,7 +3,7 @@ import http from "node:http";
3
3
  import { executeBackendMcpTool } from "../mcp/backendTools.js";
4
4
  import { renderDashboard } from "./dashboard.js";
5
5
  const JSON_BODY_LIMIT_BYTES = 1024 * 1024;
6
- const DEFAULT_VERSION = "1.1.1";
6
+ const DEFAULT_VERSION = "1.2.0";
7
7
  export async function createBunHttpApp({ state, host = "127.0.0.1", port, authToken, version = DEFAULT_VERSION }) {
8
8
  const runtimeInfo = {
9
9
  host,
@@ -338,7 +338,7 @@ function readPermissions(value) {
338
338
  if (typeof value !== "object" || value === null || Array.isArray(value))
339
339
  throw new Error("BAD_REQUEST");
340
340
  const record = value;
341
- const keys = ["ReadNotebook", "InsertCell", "ModifyCell", "DeleteCell", "RunCell", "SaveNotebook"];
341
+ const keys = ["ReadNotebook", "InsertCell", "ModifyCell", "DeleteCell", "RunCell", "SaveNotebook", "CreateNotebook", "OpenNotebook"];
342
342
  const permissions = {};
343
343
  for (const key of keys) {
344
344
  if (typeof record[key] !== "boolean")
@@ -8,7 +8,7 @@ import { loadRuntimeConfig } from "../runtime/config.js";
8
8
  import { writeSessionFile } from "../runtime/session.js";
9
9
  import { createBunHttpApp } from "./httpServer.js";
10
10
  const MCP_SERVER_NAME = "mica-bun";
11
- const MICA_PACKAGE_VERSION = "1.1.1";
11
+ const MICA_PACKAGE_VERSION = "1.2.0";
12
12
  export async function startBunRuntime(deps = {}) {
13
13
  const config = deps.runtimeConfig ?? loadRuntimeConfig();
14
14
  const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
@@ -1,6 +1,6 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { DEFAULT_TIMEOUTS_MS } from "../backend/protocol.js";
3
- import { abortEvaluationSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, killKernelSchema, listCellsSchema, modifyCellSchema, noArgsSchema, readArtifactSchema, readCellSchema, restartKernelSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
3
+ import { abortEvaluationSchema, createNotebookSchema, deleteCellSchema, getCellOutputSchema, insertCellSchema, killKernelSchema, listCellsSchema, modifyCellSchema, noArgsSchema, openNotebookSchema, readArtifactSchema, readCellSchema, restartKernelSchema, runCellSchema, selectNotebookSchema, saveNotebookSchema, symbolLookupSchema, } from "./toolSchemas.js";
4
4
  import { INSERT_ANCHOR_GUIDANCE, notebookToolDescription } from "./descriptions.js";
5
5
  import { toolFailure, toolSuccess, withToolErrors } from "./toolResults.js";
6
6
  function assertLiveAgent(state) {
@@ -196,6 +196,16 @@ export const MICA_BACKEND_TOOL_DEFINITIONS = [
196
196
  description: notebookToolDescription("Select the active Mathematica notebook in the backend registry."),
197
197
  schema: selectNotebookSchema.shape,
198
198
  },
199
+ {
200
+ name: "mma_create_notebook",
201
+ description: notebookToolDescription("Create a new blank notebook in the Wolfram FrontEnd with the given window title."),
202
+ schema: createNotebookSchema.shape,
203
+ },
204
+ {
205
+ name: "mma_open_notebook",
206
+ description: notebookToolDescription("Open an existing notebook file (.nb) from disk in the Wolfram FrontEnd."),
207
+ schema: openNotebookSchema.shape,
208
+ },
199
209
  ...queuedNotebookTools.map((config) => ({
200
210
  name: config.name,
201
211
  description: notebookToolDescription(config.summary, config.extraGuidance),
@@ -231,6 +241,28 @@ export async function executeBackendMcpTool(state, tool, args = {}, extra) {
231
241
  return toolSuccess({ activeNotebookId: state.activeNotebookId, notebook: target.notebook });
232
242
  });
233
243
  }
244
+ // Agent-level tools: route to any live agent without requiring a notebook target
245
+ if (tool === "mma_create_notebook" || tool === "mma_open_notebook") {
246
+ return withToolErrors({ tool, args: recordArgs }, () => {
247
+ sweepStateLiveness(state);
248
+ const live = state.agents.list().find((a) => !a.offline && !a.retired);
249
+ if (!live)
250
+ throw new Error("NO_LIVE_AGENT");
251
+ const notebook = state.notebooks.listLive().find((n) => n.agentSessionId === live.agentSessionId);
252
+ const targetNotebookId = notebook?.notebookId ?? `__agent__${live.agentSessionId}`;
253
+ const requestId = randomUUID();
254
+ state.queue.enqueue({
255
+ requestId,
256
+ tool,
257
+ arguments: recordArgs,
258
+ targetNotebookId,
259
+ agentSessionId: live.agentSessionId,
260
+ timeoutMs: DEFAULT_TIMEOUTS_MS.mutation,
261
+ createdAt: Date.now(),
262
+ });
263
+ return toolSuccess({ status: "queued", requestId });
264
+ });
265
+ }
234
266
  const queuedConfig = queuedNotebookTools.find((config) => config.name === tool);
235
267
  if (!queuedConfig) {
236
268
  return toolFailure(new Error(`UNKNOWN_TOOL: ${tool}`), { tool, args: recordArgs });
@@ -14,6 +14,8 @@ const TOOL_GUIDE = [
14
14
  ["mma_abort_evaluation", "Abort a running notebook evaluation."],
15
15
  ["mma_kill_kernel", "Quit the Wolfram kernel for a notebook (control agent kernel is protected)."],
16
16
  ["mma_restart_kernel", "Restart the Wolfram kernel for a notebook so it can evaluate cells again."],
17
+ ["mma_create_notebook", "Create a new blank notebook in the Wolfram FrontEnd."],
18
+ ["mma_open_notebook", "Open an existing notebook file (.nb) from disk in the Wolfram FrontEnd."],
17
19
  ["mma_get_cell_output", "Read output and messages produced by one cell; this may refresh completed run status."],
18
20
  ["mma_read_artifact", "Read large output or message artifacts by byte page; ids may become stale after notebook edits or reruns."],
19
21
  ["mma_save_notebook", "Save the selected notebook when SaveNotebook permission is granted."],
@@ -35,7 +37,7 @@ export const MICA_AGENT_INSTRUCTIONS = [
35
37
  "Tools:",
36
38
  ...TOOL_GUIDE.map(([name, description]) => `- ${name}: ${description}`),
37
39
  ].join("\n");
38
- export function createMicaMcpServer(name, version = "1.1.1") {
40
+ export function createMicaMcpServer(name, version = "1.2.0") {
39
41
  return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
40
42
  }
41
43
  export function registerMicaPrompts(server) {
@@ -44,6 +44,12 @@ export const killKernelSchema = z.object({
44
44
  export const restartKernelSchema = z.object({
45
45
  ...notebookSelectorFields
46
46
  }).strict();
47
+ export const createNotebookSchema = z.object({
48
+ title: z.string().min(1).describe("Window title for the new notebook")
49
+ }).strict();
50
+ export const openNotebookSchema = z.object({
51
+ path: z.string().min(1).describe("Absolute path to an existing .nb file")
52
+ }).strict();
47
53
  export const getCellOutputSchema = z.object({
48
54
  ...notebookSelectorFields,
49
55
  cellId: z.string().min(1),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aliceshimada/mica",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Local MCP bridge for controlling live Wolfram Desktop / Mathematica notebooks.",
5
5
  "type": "module",
6
6
  "repository": {
@@ -1,3 +1,5 @@
1
+ (* ::Package:: *)
2
+
1
3
  BeginPackage["MMAAgentBridge`"];
2
4
 
3
5
  StartMMAAgentPalette::usage = "StartMMAAgentPalette[] opens the MMA Agent Bridge palette.";
@@ -15,10 +17,10 @@ PollCancellations::usage = "PollCancellations[] polls palette-originated cancell
15
17
 
16
18
  Begin["`Private`"];
17
19
 
18
- $DefaultBridgeBaseURL = "http://127.0.0.1:19791";
19
- $BridgeBaseURL = $DefaultBridgeBaseURL;
20
- $BridgeAuthToken = None;
21
- $BridgeSessionFile = Automatic;
20
+ $DefaultBridgeBaseURL = "http://127.0.0.1:19791";
21
+ $BridgeBaseURL = $DefaultBridgeBaseURL;
22
+ $BridgeAuthToken = None;
23
+ $BridgeSessionFile = Automatic;
22
24
  $BridgeNotebooks = <||>;
23
25
  $ActiveNotebookId = None;
24
26
  $PaletteId = CreateUUID["palette-"];
@@ -33,24 +35,24 @@ $RunningNotebookId = None;
33
35
  $RunningNotebookObject = None;
34
36
  $RunningCellObject = None;
35
37
  $RunningCellOriginalEpilogRule = HoldComplete[CellEpilog -> Inherited];
36
- $RunningCellRestoreEpilogRule = HoldComplete[CellEpilog -> Inherited];
37
- $RunningStartedAt = None;
38
- $RunningStatus = None;
39
- $RunningStatusGraceSeconds = 2.0;
40
- $RunningTimeoutAt = None;
41
- $AbortRequestedAt = None;
42
- (* Reserved for future late-result surfacing; backend queue already rejects late timeout/cancel results. *)
43
- $LastLateResult = None;
44
- $LastRunStatusCellId = None;
38
+ $RunningCellRestoreEpilogRule = HoldComplete[CellEpilog -> Inherited];
39
+ $RunningStartedAt = None;
40
+ $RunningStatus = None;
41
+ $RunningStatusGraceSeconds = 2.0;
42
+ $RunningTimeoutAt = None;
43
+ $AbortRequestedAt = None;
44
+ (* Reserved for future late-result surfacing; backend queue already rejects late timeout/cancel results. *)
45
+ $LastLateResult = None;
46
+ $LastRunStatusCellId = None;
45
47
  $LastRunStatusNotebookId = None;
46
48
  $LastRunStatus = None;
47
49
  $LastStatus = <||>;
48
- $LastError = None;
49
- $PollingInProgress = False;
50
- $MaxArtifactScanCells = 20;
51
- $DefaultMaxCellPayloadBytes = 262144;
52
- $MaxCellPayloadBytes = 1024 * 1024;
53
- $BridgeHTTPTimeoutSeconds = 30;
50
+ $LastError = None;
51
+ $PollingInProgress = False;
52
+ $MaxArtifactScanCells = 20;
53
+ $DefaultMaxCellPayloadBytes = 262144;
54
+ $MaxCellPayloadBytes = 1024 * 1024;
55
+ $BridgeHTTPTimeoutSeconds = 30;
54
56
  $BridgeHTTPRetryCount = 3;
55
57
  $BridgeHTTPRetryDelaySeconds = 0.25;
56
58
  $BridgeInbox = {};
@@ -68,43 +70,43 @@ $MMAAgentBridgeSourceFile = If[StringQ[$InputFileName] && StringLength[$InputFil
68
70
  $ControlAgentNotebook = None;
69
71
  $ControlAgentEvaluatorName = "MMAAgentControl";
70
72
 
71
- UnixTimeMilliseconds[] := Round[1000 UnixTime[]];
72
-
73
- NonEmptyStringQ[value_] := StringQ[value] && StringLength[value] > 0;
74
-
75
- EnvironmentValue[name_String] := Module[{value},
76
- value = Quiet @ Check[Environment[name], ""];
77
- If[NonEmptyStringQ[value], value, ""]
78
- ];
79
-
80
- DefaultBridgeSessionFile[] := Module[{override, home},
81
- override = EnvironmentValue["MICA_SESSION_FILE"];
82
- If[NonEmptyStringQ[override], Return[override]];
83
- home = EnvironmentValue["HOME"];
84
- If[!NonEmptyStringQ[home], home = EnvironmentValue["USERPROFILE"]];
85
- If[!NonEmptyStringQ[home], home = Directory[]];
86
- FileNameJoin[{home, ".mica", "session.json"}]
87
- ];
88
-
89
- If[$BridgeSessionFile === Automatic, $BridgeSessionFile = DefaultBridgeSessionFile[]];
90
-
91
- LoadBridgeSession[] := Module[{sessionFile = $BridgeSessionFile, payload},
92
- If[!StringQ[sessionFile] || !FileExistsQ[sessionFile], Return[<||>]];
93
- payload = Quiet @ Check[Import[sessionFile, "RawJSON"], $Failed];
94
- If[AssociationQ[payload], payload, <||>]
95
- ];
96
-
97
- ConfigureBridgeFromSession[] := Module[{session, baseUrl, token},
98
- session = LoadBridgeSession[];
99
- baseUrl = Lookup[session, "baseUrl", None];
100
- If[NonEmptyStringQ[baseUrl],
101
- $BridgeBaseURL = baseUrl,
102
- $BridgeBaseURL = $DefaultBridgeBaseURL
103
- ];
104
- token = Lookup[session, "authToken", None];
105
- $BridgeAuthToken = If[NonEmptyStringQ[token], token, None];
106
- $BridgeBaseURL
107
- ];
73
+ UnixTimeMilliseconds[] := Round[1000 UnixTime[]];
74
+
75
+ NonEmptyStringQ[value_] := StringQ[value] && StringLength[value] > 0;
76
+
77
+ EnvironmentValue[name_String] := Module[{value},
78
+ value = Quiet @ Check[Environment[name], ""];
79
+ If[NonEmptyStringQ[value], value, ""]
80
+ ];
81
+
82
+ DefaultBridgeSessionFile[] := Module[{override, home},
83
+ override = EnvironmentValue["MICA_SESSION_FILE"];
84
+ If[NonEmptyStringQ[override], Return[override]];
85
+ home = EnvironmentValue["HOME"];
86
+ If[!NonEmptyStringQ[home], home = EnvironmentValue["USERPROFILE"]];
87
+ If[!NonEmptyStringQ[home], home = Directory[]];
88
+ FileNameJoin[{home, ".mica", "session.json"}]
89
+ ];
90
+
91
+ If[$BridgeSessionFile === Automatic, $BridgeSessionFile = DefaultBridgeSessionFile[]];
92
+
93
+ LoadBridgeSession[] := Module[{sessionFile = $BridgeSessionFile, payload},
94
+ If[!StringQ[sessionFile] || !FileExistsQ[sessionFile], Return[<||>]];
95
+ payload = Quiet @ Check[Import[sessionFile, "RawJSON"], $Failed];
96
+ If[AssociationQ[payload], payload, <||>]
97
+ ];
98
+
99
+ ConfigureBridgeFromSession[] := Module[{session, baseUrl, token},
100
+ session = LoadBridgeSession[];
101
+ baseUrl = Lookup[session, "baseUrl", None];
102
+ If[NonEmptyStringQ[baseUrl],
103
+ $BridgeBaseURL = baseUrl,
104
+ $BridgeBaseURL = $DefaultBridgeBaseURL
105
+ ];
106
+ token = Lookup[session, "authToken", None];
107
+ $BridgeAuthToken = If[NonEmptyStringQ[token], token, None];
108
+ $BridgeBaseURL
109
+ ];
108
110
 
109
111
  (* $BridgePermissions replaces the plan's $Permissions name because *)
110
112
  (* $Permissions is a protected Wolfram built-in symbol (Set::wrsym). *)
@@ -115,8 +117,10 @@ $DefaultBridgePermissions = <|
115
117
  "ModifyCell" -> False,
116
118
  "DeleteCell" -> False,
117
119
  "RunCell" -> False,
118
- "SaveNotebook" -> False
119
- |>;
120
+ "SaveNotebook" -> False,
121
+ "CreateNotebook" -> False,
122
+ "OpenNotebook" -> False
123
+ |>;
120
124
 
121
125
  If[!AssociationQ[Quiet @ Check[$BridgePermissions, None]],
122
126
  $BridgePermissions = $DefaultBridgePermissions
@@ -201,7 +205,7 @@ NotebookDisplayName[record_Association] := Module[{info, title, path, notebookId
201
205
  path = StringTrim[ToString[Lookup[info, "notebookPath", ""]]];
202
206
  If[title === "" && path === "",
203
207
  If[StringQ[notebookId] && StringLength[notebookId] > 0, notebookId, "Untitled notebook"],
204
- If[path === "", title, title <> " " <> FileNameTake[path]]
208
+ If[path === "", title, title <> " \[LongDash] " <> FileNameTake[path]]
205
209
  ]
206
210
  ];
207
211
 
@@ -311,7 +315,7 @@ RuntimeStatusCard[] := Module[{paletteConnected, transportMode, executorState, p
311
315
  Style["Running request: ", Bold],
312
316
  ToString[Lookup[runningRequest, "tool", "request"]],
313
317
  If[StringQ[Lookup[runningRequest, "requestId", ""]], " (" <> Lookup[runningRequest, "requestId", ""] <> ")", Nothing],
314
- If[NumberQ[elapsedSeconds], " " <> ToString[elapsedSeconds] <> "s", Nothing]
318
+ If[NumberQ[elapsedSeconds], " \[Bullet] " <> ToString[elapsedSeconds] <> "s", Nothing]
315
319
  },
316
320
  Nothing
317
321
  ]],
@@ -359,7 +363,9 @@ PermissionsPanel[] := Panel[
359
363
  {PalettePermissionRow["Modify cell", "ModifyCell", $ActiveNotebookId]},
360
364
  {PalettePermissionRow["Delete cell", "DeleteCell", $ActiveNotebookId]},
361
365
  {PalettePermissionRow["Run cell", "RunCell", $ActiveNotebookId]},
362
- {PalettePermissionRow["Save notebook", "SaveNotebook", $ActiveNotebookId]}
366
+ {PalettePermissionRow["Save notebook", "SaveNotebook", $ActiveNotebookId]},
367
+ {PalettePermissionRow["Create notebook", "CreateNotebook", $ActiveNotebookId]},
368
+ {PalettePermissionRow["Open notebook", "OpenNotebook", $ActiveNotebookId]}
363
369
  },
364
370
  Alignment -> Left,
365
371
  Spacings -> {1, 0.35}
@@ -519,14 +525,14 @@ PaletteView[] := DynamicModule[{},
519
525
  ]
520
526
  ];
521
527
 
522
- (* Re-read the small session file before each request so Wolfram follows MCP restarts and dynamic ports. *)
523
- BridgeURL[path_String] := ConfigureBridgeFromSession[] <> path;
524
-
525
- BridgeHeaders[] := If[
526
- StringQ[$BridgeAuthToken] && StringLength[$BridgeAuthToken] > 0,
527
- {"Authorization" -> "Bearer " <> $BridgeAuthToken},
528
- {}
529
- ];
528
+ (* Re-read the small session file before each request so Wolfram follows MCP restarts and dynamic ports. *)
529
+ BridgeURL[path_String] := ConfigureBridgeFromSession[] <> path;
530
+
531
+ BridgeHeaders[] := If[
532
+ StringQ[$BridgeAuthToken] && StringLength[$BridgeAuthToken] > 0,
533
+ {"Authorization" -> "Bearer " <> $BridgeAuthToken},
534
+ {}
535
+ ];
530
536
 
531
537
  URLComponentEncodeString[None] := "";
532
538
  URLComponentEncodeString[value_] := StringReplace[
@@ -584,8 +590,8 @@ JsonByteArrayToPayload[body_ByteArray] := Module[{text},
584
590
  Quiet @ Check[ImportString[text, "RawJSON"], $Failed]
585
591
  ];
586
592
 
587
- BridgeGet[path_String] := Module[{response},
588
- response = BridgeRequestWithRetries[HTTPRequest[BridgeURL[path], <|"Method" -> "GET", "Headers" -> BridgeHeaders[]|>]];
593
+ BridgeGet[path_String] := Module[{response},
594
+ response = BridgeRequestWithRetries[HTTPRequest[BridgeURL[path], <|"Method" -> "GET", "Headers" -> BridgeHeaders[]|>]];
589
595
  If[response === $Failed, Return[$Failed]];
590
596
  JsonByteArrayToPayload[response["BodyByteArray"]]
591
597
  ];
@@ -595,9 +601,9 @@ BridgePost[path_String, payload_Association] := Module[{response},
595
601
  HTTPRequest[
596
602
  BridgeURL[path],
597
603
  <|
598
- "Method" -> "POST",
599
- "Headers" -> BridgeHeaders[],
600
- "ContentType" -> "application/json; charset=utf-8",
604
+ "Method" -> "POST",
605
+ "Headers" -> BridgeHeaders[],
606
+ "ContentType" -> "application/json; charset=utf-8",
601
607
  "Body" -> PayloadToJsonBytes[payload]
602
608
  |>
603
609
  ]
@@ -825,141 +831,141 @@ CellPayload[cell_CellObject, id_String, index_Integer] := <|
825
831
  "tags" -> CellTagsList[cell]
826
832
  |>;
827
833
 
828
- CellGeneratedBoundaryQ[style_String] := MemberQ[{"Input", "Code", "Text", "Section", "Subsection", "Subsubsection", "Title", "Chapter"}, style];
829
-
830
- CellArtifactStyleQ[style_String] := MemberQ[{"Output", "Print", "Message"}, style];
831
-
832
- CellPayloadMaxBytes[args_Association] := Module[{value = Lookup[args, "maxBytes", $DefaultMaxCellPayloadBytes]},
833
- If[IntegerQ[value] && value > 0 && value <= $MaxCellPayloadBytes, value, $DefaultMaxCellPayloadBytes]
834
- ];
835
-
836
- Utf8LeadByteLength[byte_Integer] := Which[
837
- byte < 128, 1,
838
- byte < 224, 2,
839
- byte < 240, 3,
840
- byte < 248, 4,
841
- True, 1
842
- ];
843
-
844
- Utf8PrefixByteLength[bytes_List, maxBytes_Integer] := Module[{safeLength = Min[maxBytes, Length[bytes]], start, lead, expected},
845
- If[safeLength <= 0, Return[0]];
846
- start = safeLength;
847
- While[start > 1 && bytes[[start]] >= 128 && bytes[[start]] <= 191, start--];
848
- lead = bytes[[start]];
849
- expected = Utf8LeadByteLength[lead];
850
- If[safeLength - start + 1 >= expected, safeLength, Max[0, start - 1]]
851
- ];
852
-
853
- TruncateStringToUtf8Bytes[text_String, maxBytes_Integer] := Module[{originalBytes, originalByteLength, safeLength, returnedText},
854
- originalBytes = ToCharacterCode[text, "UTF8"];
855
- originalByteLength = Length[originalBytes];
856
- If[maxBytes <= 0,
857
- Return[<|"value" -> "", "truncated" -> (originalByteLength > 0), "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0|>]
858
- ];
859
- If[originalByteLength <= maxBytes,
860
- Return[<|"value" -> text, "truncated" -> False, "originalByteLength" -> originalByteLength, "returnedByteLength" -> originalByteLength|>]
861
- ];
862
- safeLength = Utf8PrefixByteLength[originalBytes, maxBytes];
863
- returnedText = If[safeLength > 0, ByteArrayToString[ByteArray[Take[originalBytes, safeLength]], "UTF8"], ""];
864
- <|"value" -> returnedText, "truncated" -> True, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength|>
865
- ];
866
-
867
- TruncatePayloadFields[content_String, outputs_List, messages_List, maxBytes_Integer, includeContentQ_] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, truncatedContent = "", truncatedOutputs = {}, truncatedMessages = {}, processString, output, message},
868
- processString[text_String] := Module[{item = TruncateStringToUtf8Bytes[text, remainingByteLength]},
869
- totalOriginalByteLength += item["originalByteLength"];
870
- totalReturnedByteLength += item["returnedByteLength"];
871
- anyTruncated = anyTruncated || TrueQ[item["truncated"]];
872
- remainingByteLength = Max[0, remainingByteLength - item["returnedByteLength"]];
873
- item["value"]
874
- ];
875
- If[TrueQ[includeContentQ], truncatedContent = processString[content]];
876
- Do[
877
- AppendTo[truncatedOutputs, processString[If[StringQ[output], output, ToString[output, InputForm]]]],
878
- {output, outputs}
879
- ];
880
- Do[
881
- AppendTo[truncatedMessages, processString[If[StringQ[message], message, ToString[message, InputForm]]]],
882
- {message, messages}
883
- ];
884
- Join[
885
- If[TrueQ[includeContentQ], <|"content" -> truncatedContent|>, <||>],
886
- <|
887
- "outputs" -> truncatedOutputs,
888
- "messages" -> truncatedMessages,
889
- "truncated" -> anyTruncated,
890
- "originalByteLength" -> totalOriginalByteLength,
891
- "returnedByteLength" -> totalReturnedByteLength
892
- |>
893
- ]
894
- ];
895
-
896
- ArtifactId[cellId_String, kind_String, index_Integer] := cellId <> ":" <> kind <> ":" <> ToString[index];
897
-
898
- ArtifactDescriptor[cellId_String, kind_String, index_Integer, text_String, maxBytes_Integer] := Module[{byteLength, preview},
899
- byteLength = Length[ToCharacterCode[text, "UTF8"]];
900
- preview = TruncateStringToUtf8Bytes[text, maxBytes];
901
- <|
902
- "artifactId" -> ArtifactId[cellId, kind, index],
903
- "type" -> kind,
904
- "index" -> index,
905
- "byteLength" -> byteLength,
906
- "preview" -> preview["value"],
907
- "previewByteLength" -> preview["returnedByteLength"],
908
- "truncated" -> True
909
- |>
910
- ];
911
-
912
- ArtifactPayloadFields[cellId_String, outputs_List, messages_List, maxBytes_Integer] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, processedOutputs = {}, processedMessages = {}, processString, output, message},
913
- processString[text_, kind_String, index_Integer] := Module[{value = If[StringQ[text], text, ToString[text, InputForm]], byteLength, descriptor},
914
- byteLength = Length[ToCharacterCode[value, "UTF8"]];
915
- totalOriginalByteLength += byteLength;
916
- If[byteLength <= remainingByteLength,
917
- totalReturnedByteLength += byteLength;
918
- remainingByteLength = Max[0, remainingByteLength - byteLength];
919
- value,
920
- anyTruncated = True;
921
- descriptor = ArtifactDescriptor[cellId, kind, index, value, remainingByteLength];
922
- totalReturnedByteLength += descriptor["previewByteLength"];
923
- remainingByteLength = Max[0, remainingByteLength - descriptor["previewByteLength"]];
924
- descriptor
925
- ]
926
- ];
927
- Do[
928
- AppendTo[processedOutputs, processString[outputs[[i]], "output", i - 1]],
929
- {i, Length[outputs]}
930
- ];
931
- Do[
932
- AppendTo[processedMessages, processString[messages[[i]], "message", i - 1]],
933
- {i, Length[messages]}
934
- ];
935
- <|
936
- "outputs" -> processedOutputs,
937
- "messages" -> processedMessages,
938
- "truncated" -> anyTruncated,
939
- "originalByteLength" -> totalOriginalByteLength,
940
- "returnedByteLength" -> totalReturnedByteLength
941
- |>
942
- ];
943
-
944
- Utf8SliceStringToBytes[text_String, offset_Integer, limit_Integer] := Module[{bytes, originalByteLength, start, available, safeLength, sliceBytes, value},
945
- bytes = ToCharacterCode[text, "UTF8"];
946
- originalByteLength = Length[bytes];
947
- If[limit <= 0 || offset >= originalByteLength,
948
- Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
949
- ];
950
- start = Max[1, offset + 1];
951
- While[start <= originalByteLength && bytes[[start]] >= 128 && bytes[[start]] <= 191, start++];
952
- available = originalByteLength - start + 1;
953
- If[available <= 0,
954
- Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
955
- ];
956
- safeLength = Utf8PrefixByteLength[Take[bytes, {start, originalByteLength}], Min[limit, available]];
957
- sliceBytes = If[safeLength > 0, Take[bytes, {start, start + safeLength - 1}], {}];
958
- value = If[safeLength > 0, ByteArrayToString[ByteArray[sliceBytes], "UTF8"], ""];
959
- <|"value" -> value, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength, "nextOffset" -> (start - 1 + safeLength)|>
960
- ];
961
-
962
- CellEvaluationTaggingPath[cellId_String] := {TaggingRules, "MMAAgentBridge", "evaluations", cellId, "complete"};
834
+ CellGeneratedBoundaryQ[style_String] := MemberQ[{"Input", "Code", "Text", "Section", "Subsection", "Subsubsection", "Title", "Chapter"}, style];
835
+
836
+ CellArtifactStyleQ[style_String] := MemberQ[{"Output", "Print", "Message"}, style];
837
+
838
+ CellPayloadMaxBytes[args_Association] := Module[{value = Lookup[args, "maxBytes", $DefaultMaxCellPayloadBytes]},
839
+ If[IntegerQ[value] && value > 0 && value <= $MaxCellPayloadBytes, value, $DefaultMaxCellPayloadBytes]
840
+ ];
841
+
842
+ Utf8LeadByteLength[byte_Integer] := Which[
843
+ byte < 128, 1,
844
+ byte < 224, 2,
845
+ byte < 240, 3,
846
+ byte < 248, 4,
847
+ True, 1
848
+ ];
849
+
850
+ Utf8PrefixByteLength[bytes_List, maxBytes_Integer] := Module[{safeLength = Min[maxBytes, Length[bytes]], start, lead, expected},
851
+ If[safeLength <= 0, Return[0]];
852
+ start = safeLength;
853
+ While[start > 1 && bytes[[start]] >= 128 && bytes[[start]] <= 191, start--];
854
+ lead = bytes[[start]];
855
+ expected = Utf8LeadByteLength[lead];
856
+ If[safeLength - start + 1 >= expected, safeLength, Max[0, start - 1]]
857
+ ];
858
+
859
+ TruncateStringToUtf8Bytes[text_String, maxBytes_Integer] := Module[{originalBytes, originalByteLength, safeLength, returnedText},
860
+ originalBytes = ToCharacterCode[text, "UTF8"];
861
+ originalByteLength = Length[originalBytes];
862
+ If[maxBytes <= 0,
863
+ Return[<|"value" -> "", "truncated" -> (originalByteLength > 0), "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0|>]
864
+ ];
865
+ If[originalByteLength <= maxBytes,
866
+ Return[<|"value" -> text, "truncated" -> False, "originalByteLength" -> originalByteLength, "returnedByteLength" -> originalByteLength|>]
867
+ ];
868
+ safeLength = Utf8PrefixByteLength[originalBytes, maxBytes];
869
+ returnedText = If[safeLength > 0, ByteArrayToString[ByteArray[Take[originalBytes, safeLength]], "UTF8"], ""];
870
+ <|"value" -> returnedText, "truncated" -> True, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength|>
871
+ ];
872
+
873
+ TruncatePayloadFields[content_String, outputs_List, messages_List, maxBytes_Integer, includeContentQ_] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, truncatedContent = "", truncatedOutputs = {}, truncatedMessages = {}, processString, output, message},
874
+ processString[text_String] := Module[{item = TruncateStringToUtf8Bytes[text, remainingByteLength]},
875
+ totalOriginalByteLength += item["originalByteLength"];
876
+ totalReturnedByteLength += item["returnedByteLength"];
877
+ anyTruncated = anyTruncated || TrueQ[item["truncated"]];
878
+ remainingByteLength = Max[0, remainingByteLength - item["returnedByteLength"]];
879
+ item["value"]
880
+ ];
881
+ If[TrueQ[includeContentQ], truncatedContent = processString[content]];
882
+ Do[
883
+ AppendTo[truncatedOutputs, processString[If[StringQ[output], output, ToString[output, InputForm]]]],
884
+ {output, outputs}
885
+ ];
886
+ Do[
887
+ AppendTo[truncatedMessages, processString[If[StringQ[message], message, ToString[message, InputForm]]]],
888
+ {message, messages}
889
+ ];
890
+ Join[
891
+ If[TrueQ[includeContentQ], <|"content" -> truncatedContent|>, <||>],
892
+ <|
893
+ "outputs" -> truncatedOutputs,
894
+ "messages" -> truncatedMessages,
895
+ "truncated" -> anyTruncated,
896
+ "originalByteLength" -> totalOriginalByteLength,
897
+ "returnedByteLength" -> totalReturnedByteLength
898
+ |>
899
+ ]
900
+ ];
901
+
902
+ ArtifactId[cellId_String, kind_String, index_Integer] := cellId <> ":" <> kind <> ":" <> ToString[index];
903
+
904
+ ArtifactDescriptor[cellId_String, kind_String, index_Integer, text_String, maxBytes_Integer] := Module[{byteLength, preview},
905
+ byteLength = Length[ToCharacterCode[text, "UTF8"]];
906
+ preview = TruncateStringToUtf8Bytes[text, maxBytes];
907
+ <|
908
+ "artifactId" -> ArtifactId[cellId, kind, index],
909
+ "type" -> kind,
910
+ "index" -> index,
911
+ "byteLength" -> byteLength,
912
+ "preview" -> preview["value"],
913
+ "previewByteLength" -> preview["returnedByteLength"],
914
+ "truncated" -> True
915
+ |>
916
+ ];
917
+
918
+ ArtifactPayloadFields[cellId_String, outputs_List, messages_List, maxBytes_Integer] := Module[{remainingByteLength = maxBytes, totalOriginalByteLength = 0, totalReturnedByteLength = 0, anyTruncated = False, processedOutputs = {}, processedMessages = {}, processString, output, message},
919
+ processString[text_, kind_String, index_Integer] := Module[{value = If[StringQ[text], text, ToString[text, InputForm]], byteLength, descriptor},
920
+ byteLength = Length[ToCharacterCode[value, "UTF8"]];
921
+ totalOriginalByteLength += byteLength;
922
+ If[byteLength <= remainingByteLength,
923
+ totalReturnedByteLength += byteLength;
924
+ remainingByteLength = Max[0, remainingByteLength - byteLength];
925
+ value,
926
+ anyTruncated = True;
927
+ descriptor = ArtifactDescriptor[cellId, kind, index, value, remainingByteLength];
928
+ totalReturnedByteLength += descriptor["previewByteLength"];
929
+ remainingByteLength = Max[0, remainingByteLength - descriptor["previewByteLength"]];
930
+ descriptor
931
+ ]
932
+ ];
933
+ Do[
934
+ AppendTo[processedOutputs, processString[outputs[[i]], "output", i - 1]],
935
+ {i, Length[outputs]}
936
+ ];
937
+ Do[
938
+ AppendTo[processedMessages, processString[messages[[i]], "message", i - 1]],
939
+ {i, Length[messages]}
940
+ ];
941
+ <|
942
+ "outputs" -> processedOutputs,
943
+ "messages" -> processedMessages,
944
+ "truncated" -> anyTruncated,
945
+ "originalByteLength" -> totalOriginalByteLength,
946
+ "returnedByteLength" -> totalReturnedByteLength
947
+ |>
948
+ ];
949
+
950
+ Utf8SliceStringToBytes[text_String, offset_Integer, limit_Integer] := Module[{bytes, originalByteLength, start, available, safeLength, sliceBytes, value},
951
+ bytes = ToCharacterCode[text, "UTF8"];
952
+ originalByteLength = Length[bytes];
953
+ If[limit <= 0 || offset >= originalByteLength,
954
+ Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
955
+ ];
956
+ start = Max[1, offset + 1];
957
+ While[start <= originalByteLength && bytes[[start]] >= 128 && bytes[[start]] <= 191, start++];
958
+ available = originalByteLength - start + 1;
959
+ If[available <= 0,
960
+ Return[<|"value" -> "", "originalByteLength" -> originalByteLength, "returnedByteLength" -> 0, "nextOffset" -> originalByteLength|>]
961
+ ];
962
+ safeLength = Utf8PrefixByteLength[Take[bytes, {start, originalByteLength}], Min[limit, available]];
963
+ sliceBytes = If[safeLength > 0, Take[bytes, {start, start + safeLength - 1}], {}];
964
+ value = If[safeLength > 0, ByteArrayToString[ByteArray[sliceBytes], "UTF8"], ""];
965
+ <|"value" -> value, "originalByteLength" -> originalByteLength, "returnedByteLength" -> safeLength, "nextOffset" -> (start - 1 + safeLength)|>
966
+ ];
967
+
968
+ CellEvaluationTaggingPath[cellId_String] := {TaggingRules, "MMAAgentBridge", "evaluations", cellId, "complete"};
963
969
 
964
970
  MarkCellEvaluationComplete[cellId_String] := Quiet @ Check[
965
971
  CurrentValue[EvaluationNotebook[], CellEvaluationTaggingPath[cellId]] = True,
@@ -1037,23 +1043,23 @@ RestoreRunningCellEpilog[] := Module[{},
1037
1043
  ClearRunningEvaluationState[] := Module[{cellId = $RunningCellId, notebook = $RunningNotebookObject},
1038
1044
  RestoreRunningCellEpilog[];
1039
1045
  If[Head[notebook] === NotebookObject && StringQ[cellId], ClearCellEvaluationComplete[notebook, cellId]];
1040
- $RunningCellId = None;
1041
- $RunningRequestId = None;
1042
- $RunningNotebookId = None;
1043
- $RunningNotebookObject = None;
1044
- $RunningStartedAt = None;
1045
- $RunningStatus = None;
1046
- $AbortRequestedAt = None;
1047
- $RunningTimeoutAt = None
1048
- ];
1049
-
1050
- FinishRunningCell[status_String] := Module[{cellId = $RunningCellId, notebookId = $RunningNotebookId},
1051
- ClearRunningEvaluationState[];
1052
- $LastRunStatusCellId = cellId;
1053
- $LastRunStatusNotebookId = notebookId;
1054
- $RunningStatus = status;
1055
- $LastRunStatus = status
1056
- ];
1046
+ $RunningCellId = None;
1047
+ $RunningRequestId = None;
1048
+ $RunningNotebookId = None;
1049
+ $RunningNotebookObject = None;
1050
+ $RunningStartedAt = None;
1051
+ $RunningStatus = None;
1052
+ $AbortRequestedAt = None;
1053
+ $RunningTimeoutAt = None
1054
+ ];
1055
+
1056
+ FinishRunningCell[status_String] := Module[{cellId = $RunningCellId, notebookId = $RunningNotebookId},
1057
+ ClearRunningEvaluationState[];
1058
+ $LastRunStatusCellId = cellId;
1059
+ $LastRunStatusNotebookId = notebookId;
1060
+ $RunningStatus = status;
1061
+ $LastRunStatus = status
1062
+ ];
1057
1063
 
1058
1064
  CellGeneratedArtifactQ[cell_CellObject] := TrueQ[Quiet @ Check[CurrentValue[cell, GeneratedCell], False]] && CellArtifactStyleQ[CellStyleName[cell]];
1059
1065
 
@@ -1113,14 +1119,14 @@ CellArtifactScan[cell_CellObject, cellId_String:"", notebook_:Automatic] := Modu
1113
1119
  "timeout",
1114
1120
  StringQ[cellId] && StringQ[$LastRunStatusCellId] && cellId === $LastRunStatusCellId && StringQ[$LastRunStatusNotebookId] && NotebookIdForObject[nb] === $LastRunStatusNotebookId && $LastRunStatus === "finished",
1115
1121
  "finished",
1116
- StringQ[cellId] && StringQ[$LastRunStatusCellId] && cellId === $LastRunStatusCellId && StringQ[$LastRunStatusNotebookId] && NotebookIdForObject[nb] === $LastRunStatusNotebookId && $LastRunStatus === "aborted",
1117
- "aborted",
1118
- sameRunningCellQ && NumberQ[$AbortRequestedAt] && !evaluationCompleteQ,
1119
- "abort_requested",
1120
- sameRunningCellQ && NumberQ[$AbortRequestedAt] && evaluationCompleteQ,
1121
- (FinishRunningCell["aborted"]; "aborted"),
1122
- sameRunningCellQ && NumberQ[$RunningStartedAt] && (AbsoluteTime[] - $RunningStartedAt < $RunningStatusGraceSeconds),
1123
- "running",
1122
+ StringQ[cellId] && StringQ[$LastRunStatusCellId] && cellId === $LastRunStatusCellId && StringQ[$LastRunStatusNotebookId] && NotebookIdForObject[nb] === $LastRunStatusNotebookId && $LastRunStatus === "aborted",
1123
+ "aborted",
1124
+ sameRunningCellQ && NumberQ[$AbortRequestedAt] && !evaluationCompleteQ,
1125
+ "abort_requested",
1126
+ sameRunningCellQ && NumberQ[$AbortRequestedAt] && evaluationCompleteQ,
1127
+ (FinishRunningCell["aborted"]; "aborted"),
1128
+ sameRunningCellQ && NumberQ[$RunningStartedAt] && (AbsoluteTime[] - $RunningStartedAt < $RunningStatusGraceSeconds),
1129
+ "running",
1124
1130
  sameRunningCellQ && (evaluationCompleteQ || hasFinalOutputQ),
1125
1131
  (FinishRunningCell["finished"]; "finished"),
1126
1132
  sameRunningCellQ,
@@ -1165,84 +1171,84 @@ RefreshCellMap[notebookId_String] := Module[{record, nb, cells, idByCell, previo
1165
1171
 
1166
1172
  ReadCellById[args_Association] := Module[{notebookId, record, cellId, cell, maxBytes, payload},
1167
1173
  notebookId = TargetNotebookId[args];
1168
- If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1169
- If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1170
- record = NotebookRecord[notebookId];
1171
- If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1172
- cellId = Lookup[args, "cellId", ""];
1173
- If[StringLength[cellId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "cellId is required."|>]]];
1174
- cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1175
- If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1176
- maxBytes = CellPayloadMaxBytes[args];
1177
- With[{artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]]},
1178
- payload = TruncatePayloadFields[CellContentString[cell], artifacts["outputs"], artifacts["messages"], maxBytes, True];
1179
- Join[
1180
- <|"cellId" -> cellId, "style" -> CellStyleName[cell]|>,
1181
- payload,
1182
- <|"status" -> artifacts["status"]|>
1183
- ]
1184
- ]
1185
- ];
1186
-
1174
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1175
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1176
+ record = NotebookRecord[notebookId];
1177
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1178
+ cellId = Lookup[args, "cellId", ""];
1179
+ If[StringLength[cellId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "cellId is required."|>]]];
1180
+ cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1181
+ If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1182
+ maxBytes = CellPayloadMaxBytes[args];
1183
+ With[{artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]]},
1184
+ payload = TruncatePayloadFields[CellContentString[cell], artifacts["outputs"], artifacts["messages"], maxBytes, True];
1185
+ Join[
1186
+ <|"cellId" -> cellId, "style" -> CellStyleName[cell]|>,
1187
+ payload,
1188
+ <|"status" -> artifacts["status"]|>
1189
+ ]
1190
+ ]
1191
+ ];
1192
+
1187
1193
  GetCellOutputById[args_Association] := Module[{notebookId, record, cellId, cell, artifacts, maxBytes, payload},
1188
1194
  notebookId = TargetNotebookId[args];
1189
- If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1190
- If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1191
- record = NotebookRecord[notebookId];
1192
- If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1195
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1196
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1197
+ record = NotebookRecord[notebookId];
1198
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1193
1199
  cellId = Lookup[args, "cellId", ""];
1194
1200
  If[StringLength[cellId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "cellId is required."|>]]];
1195
- cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1196
- If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1197
- maxBytes = CellPayloadMaxBytes[args];
1198
- artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1199
- payload = ArtifactPayloadFields[cellId, artifacts["outputs"], artifacts["messages"], maxBytes];
1200
- Join[
1201
- <|"cellId" -> cellId|>,
1202
- payload,
1203
- <|"status" -> artifacts["status"]|>
1204
- ]
1205
- ];
1206
-
1201
+ cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1202
+ If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1203
+ maxBytes = CellPayloadMaxBytes[args];
1204
+ artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1205
+ payload = ArtifactPayloadFields[cellId, artifacts["outputs"], artifacts["messages"], maxBytes];
1206
+ Join[
1207
+ <|"cellId" -> cellId|>,
1208
+ payload,
1209
+ <|"status" -> artifacts["status"]|>
1210
+ ]
1211
+ ];
1212
+
1207
1213
  ReadArtifactById[args_Association] := Module[{notebookId, record, artifactId, parts, cellId, kind, indexText, index, cell, artifacts, artifactList, text, offset, limit, page, nextOffset, done},
1208
1214
  notebookId = TargetNotebookId[args];
1209
- If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1210
- If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1211
- record = NotebookRecord[notebookId];
1212
- If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1213
- artifactId = Lookup[args, "artifactId", ""];
1214
- If[!StringQ[artifactId] || StringLength[artifactId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId is required."|>]]];
1215
- parts = StringSplit[artifactId, ":"];
1216
- If[Length[parts] =!= 3, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId must have the form cellId:type:index."|>]]];
1217
- {cellId, kind, indexText} = parts;
1218
- If[!MemberQ[{"output", "message"}, kind], Return[Failure["BAD_REQUEST", <|"message" -> "artifact type must be output or message."|>]]];
1219
- If[!StringMatchQ[indexText, DigitCharacter..], Return[Failure["BAD_REQUEST", <|"message" -> "artifact index must be a non-negative integer."|>]]];
1220
- index = ToExpression[indexText];
1221
- offset = Lookup[args, "offset", 0];
1222
- limit = Lookup[args, "limit", 65536];
1223
- If[!IntegerQ[offset] || offset < 0, Return[Failure["BAD_REQUEST", <|"message" -> "offset must be a non-negative integer."|>]]];
1224
- If[!IntegerQ[limit] || limit <= 0 || limit > $MaxCellPayloadBytes, Return[Failure["BAD_REQUEST", <|"message" -> "limit must be a positive integer up to 1 MiB."|>]]];
1225
- cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1226
- If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1227
- artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1228
- artifactList = If[kind === "output", artifacts["outputs"], artifacts["messages"]];
1229
- If[index >= Length[artifactList], Return[Failure["BAD_REQUEST", <|"message" -> "Requested artifact was not found."|>]]];
1230
- text = artifactList[[index + 1]];
1231
- page = Utf8SliceStringToBytes[text, offset, limit];
1232
- nextOffset = page["nextOffset"];
1233
- done = nextOffset >= page["originalByteLength"];
1234
- <|
1235
- "artifactId" -> artifactId,
1236
- "offset" -> offset,
1237
- "limit" -> limit,
1238
- "data" -> page["value"],
1239
- "nextOffset" -> nextOffset,
1240
- "done" -> done,
1241
- "byteLength" -> page["originalByteLength"]
1242
- |>
1243
- ];
1244
-
1245
- MakeCellExpression[content_String, style_String] := Module[{cellStyle = If[StringQ[style] && StringLength[style] > 0, style, "Input"]},
1215
+ If[RequireReadPermission[notebookId] === $Canceled, Return[$Canceled]];
1216
+ If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1217
+ record = NotebookRecord[notebookId];
1218
+ If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1219
+ artifactId = Lookup[args, "artifactId", ""];
1220
+ If[!StringQ[artifactId] || StringLength[artifactId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId is required."|>]]];
1221
+ parts = StringSplit[artifactId, ":"];
1222
+ If[Length[parts] =!= 3, Return[Failure["BAD_REQUEST", <|"message" -> "artifactId must have the form cellId:type:index."|>]]];
1223
+ {cellId, kind, indexText} = parts;
1224
+ If[!MemberQ[{"output", "message"}, kind], Return[Failure["BAD_REQUEST", <|"message" -> "artifact type must be output or message."|>]]];
1225
+ If[!StringMatchQ[indexText, DigitCharacter..], Return[Failure["BAD_REQUEST", <|"message" -> "artifact index must be a non-negative integer."|>]]];
1226
+ index = ToExpression[indexText];
1227
+ offset = Lookup[args, "offset", 0];
1228
+ limit = Lookup[args, "limit", 65536];
1229
+ If[!IntegerQ[offset] || offset < 0, Return[Failure["BAD_REQUEST", <|"message" -> "offset must be a non-negative integer."|>]]];
1230
+ If[!IntegerQ[limit] || limit <= 0 || limit > $MaxCellPayloadBytes, Return[Failure["BAD_REQUEST", <|"message" -> "limit must be a positive integer up to 1 MiB."|>]]];
1231
+ cell = Lookup[Lookup[record, "cellMap", <||>], cellId, Missing["NotFound"]];
1232
+ If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1233
+ artifacts = CellArtifactScan[cell, cellId, Lookup[record, "notebook", None]];
1234
+ artifactList = If[kind === "output", artifacts["outputs"], artifacts["messages"]];
1235
+ If[index >= Length[artifactList], Return[Failure["BAD_REQUEST", <|"message" -> "Requested artifact was not found."|>]]];
1236
+ text = artifactList[[index + 1]];
1237
+ page = Utf8SliceStringToBytes[text, offset, limit];
1238
+ nextOffset = page["nextOffset"];
1239
+ done = nextOffset >= page["originalByteLength"];
1240
+ <|
1241
+ "artifactId" -> artifactId,
1242
+ "offset" -> offset,
1243
+ "limit" -> limit,
1244
+ "data" -> page["value"],
1245
+ "nextOffset" -> nextOffset,
1246
+ "done" -> done,
1247
+ "byteLength" -> page["originalByteLength"]
1248
+ |>
1249
+ ];
1250
+
1251
+ MakeCellExpression[content_String, style_String] := Module[{cellStyle = If[StringQ[style] && StringLength[style] > 0, style, "Input"]},
1246
1252
  If[cellStyle === "Input",
1247
1253
  Cell[BoxData[content], "Input", CellTags -> {"AI-Generated"}],
1248
1254
  Cell[content, cellStyle, CellTags -> {"AI-Generated"}]
@@ -1384,14 +1390,14 @@ RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId
1384
1390
  If[MissingQ[cell], Return[Failure["BAD_REQUEST", <|"message" -> "Requested cell was not found."|>]]];
1385
1391
  ClearCellEvaluationComplete[notebook, cellId];
1386
1392
  installedEpilog = InstallRunningCellEpilog[cell, cellId];
1387
- If[MatchQ[installedEpilog, _Failure], Return[installedEpilog]];
1388
- $LastRunStatusCellId = None;
1389
- $LastRunStatusNotebookId = None;
1390
- $LastRunStatus = None;
1391
- $RunningStatus = "running";
1392
- $AbortRequestedAt = None;
1393
- $LastLateResult = None;
1394
- $RunningRequestId = Lookup[args, "requestId", $CurrentRequestId];
1393
+ If[MatchQ[installedEpilog, _Failure], Return[installedEpilog]];
1394
+ $LastRunStatusCellId = None;
1395
+ $LastRunStatusNotebookId = None;
1396
+ $LastRunStatus = None;
1397
+ $RunningStatus = "running";
1398
+ $AbortRequestedAt = None;
1399
+ $LastLateResult = None;
1400
+ $RunningRequestId = Lookup[args, "requestId", $CurrentRequestId];
1395
1401
  $RunningCellId = cellId;
1396
1402
  $RunningNotebookId = notebookId;
1397
1403
  $RunningNotebookObject = notebook;
@@ -1424,15 +1430,15 @@ AbortEvaluationRequest[args_Association] := Module[{notebookId, record, notebook
1424
1430
  FinishRunningCell["finished"];
1425
1431
  Return[<|"status" -> "finished", "cellId" -> runningCellId, "requestId" -> runningRequestId|>]
1426
1432
  ];
1427
- If[wasRunning,
1428
- $AbortRequestedAt = AbsoluteTime[];
1429
- $RunningStatus = "abort_requested"
1430
- ];
1431
- Quiet @ Check[FrontEndTokenExecute[notebook, "EvaluatorAbort"], Null];
1432
- If[wasRunning,
1433
- <|"status" -> "abort_requested", "cellId" -> runningCellId, "requestId" -> runningRequestId|>,
1434
- <|"status" -> "idle"|>
1435
- ]
1433
+ If[wasRunning,
1434
+ $AbortRequestedAt = AbsoluteTime[];
1435
+ $RunningStatus = "abort_requested"
1436
+ ];
1437
+ Quiet @ Check[FrontEndTokenExecute[notebook, "EvaluatorAbort"], Null];
1438
+ If[wasRunning,
1439
+ <|"status" -> "abort_requested", "cellId" -> runningCellId, "requestId" -> runningRequestId|>,
1440
+ <|"status" -> "idle"|>
1441
+ ]
1436
1442
  ];
1437
1443
 
1438
1444
  KillKernelRequest[args_Association] := Module[{notebookId, record, notebook, evaluatorName},
@@ -1467,9 +1473,42 @@ RestartKernelRequest[args_Association] := Module[{notebookId, record, notebook,
1467
1473
  <|"status" -> "restarted", "notebookId" -> notebookId|>
1468
1474
  ];
1469
1475
 
1476
+ CreateNotebookRequest[args_Association] := Module[{title, nb, notebookId, payload, response},
1477
+ title = Lookup[args, "title", "Untitled"];
1478
+ If[!StringQ[title] || StringLength[StringTrim[title]] == 0, title = "Untitled"];
1479
+ nb = Quiet @ Check[CreateDocument[{}, WindowTitle -> title, Visible -> True], $Failed];
1480
+ If[Head[nb] =!= NotebookObject, Return[Failure["CREATE_FAILED", <|"message" -> "The FrontEnd failed to create the notebook."|>]]];
1481
+ payload = NotebookHeartbeatPayload[nb];
1482
+ response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1483
+ notebookId = If[AssociationQ[response],
1484
+ Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]],
1485
+ None
1486
+ ];
1487
+ <|"status" -> "created", "notebookId" -> notebookId, "title" -> title|>
1488
+ ];
1489
+
1490
+ OpenNotebookRequest[args_Association] := Module[{path, nb, notebookId, payload, response},
1491
+ path = Lookup[args, "path", ""];
1492
+ If[!StringQ[path] || StringLength[StringTrim[path]] == 0,
1493
+ Return[Failure["BAD_REQUEST", <|"message" -> "path is required."|>]]
1494
+ ];
1495
+ If[!FileExistsQ[path],
1496
+ Return[Failure["NOT_FOUND", <|"message" -> "File not found: " <> path|>]]
1497
+ ];
1498
+ nb = Quiet @ Check[NotebookOpen[path, Visible -> True], $Failed];
1499
+ If[Head[nb] =!= NotebookObject, Return[Failure["OPEN_FAILED", <|"message" -> "The FrontEnd failed to open the notebook."|>]]];
1500
+ payload = NotebookHeartbeatPayload[nb];
1501
+ response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1502
+ notebookId = If[AssociationQ[response],
1503
+ Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]],
1504
+ None
1505
+ ];
1506
+ <|"status" -> "opened", "notebookId" -> notebookId, "path" -> path|>
1507
+ ];
1508
+
1470
1509
  SaveNotebookRequest[args_Association] := Module[{notebookId, record, notebook},
1471
1510
  notebookId = TargetNotebookId[args];
1472
- If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?", notebookId], Return[$Canceled]];
1511
+ If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?", notebookId], Return[$Canceled]];
1473
1512
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1474
1513
  record = NotebookRecord[notebookId];
1475
1514
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1839,13 +1878,15 @@ ExecuteRequest[request_Association] := Module[{requestId, tool, args, result},
1839
1878
  "mma_insert_cell", InsertCellRequest[args],
1840
1879
  "mma_modify_cell", ModifyCellRequest[args],
1841
1880
  "mma_delete_cell", DeleteCellRequest[args],
1842
- "mma_run_cell", RunCellRequest[args],
1881
+ "mma_run_cell", RunCellRequest[args],
1843
1882
  "mma_abort_evaluation", AbortEvaluationRequest[args],
1844
1883
  "mma_kill_kernel", KillKernelRequest[args],
1845
- "mma_restart_kernel", RestartKernelRequest[args],
1846
- "mma_get_cell_output", GetCellOutputById[args],
1847
- "mma_read_artifact", ReadArtifactById[args],
1848
- "mma_save_notebook", SaveNotebookRequest[args],
1884
+ "mma_restart_kernel", RestartKernelRequest[args],
1885
+ "mma_create_notebook", CreateNotebookRequest[args],
1886
+ "mma_open_notebook", OpenNotebookRequest[args],
1887
+ "mma_get_cell_output", GetCellOutputById[args],
1888
+ "mma_read_artifact", ReadArtifactById[args],
1889
+ "mma_save_notebook", SaveNotebookRequest[args],
1849
1890
  "mma_select_notebook", SelectNotebookRequest[args],
1850
1891
  "mma_symbol_lookup", SymbolLookup[Lookup[args, "query", ""]],
1851
1892
  _, Failure["BAD_REQUEST", <|"Message" -> StringTemplate["Unknown tool ``."][tool]|>]