@aliceshimada/mica 1.1.1 → 1.2.1

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,19 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1 - 2026-06-17
4
+
5
+ - Add watchdog on agent tick loop: force-reset `$HiddenAgentInProgress` if stuck > 120s.
6
+ - Add periodic backend sweep (every 10s) for liveness and timed-out requests.
7
+ - Fix `markTimedOut` to use `claimedAt` for running requests.
8
+ - Remove `runCell` timeout upper bound; default to no timeout.
9
+
10
+ ## 1.2.0 - 2026-06-16
11
+
12
+ - Add `mma_create_notebook` tool: create a new blank notebook in the Wolfram FrontEnd.
13
+ - Add `mma_open_notebook` tool: open an existing notebook file (.nb) from disk.
14
+ - Add `CreateNotebook` and `OpenNotebook` permissions (default false).
15
+ - Fix agent-level tools when no live notebook exists for routing.
16
+
3
17
  ## 1.1.1 - 2026-06-16
4
18
 
5
19
  - Fix `RestartKernelRequest`: kill kernel via `Quit[]` before restarting.
@@ -81,7 +81,10 @@ export class BackendQueue {
81
81
  for (const request of this.requests.values()) {
82
82
  if (request.status !== "queued" && request.status !== "running")
83
83
  continue;
84
- if (now - request.createdAt < request.timeoutMs)
84
+ const elapsed = request.status === "running" && request.claimedAt
85
+ ? now - request.claimedAt
86
+ : now - request.createdAt;
87
+ if (elapsed < request.timeoutMs)
85
88
  continue;
86
89
  const updated = this.cloneRequest({ ...request, status: "timed_out" });
87
90
  this.requests.set(request.requestId, updated);
@@ -6,7 +6,7 @@ export const DEFAULT_TIMEOUTS_MS = {
6
6
  readCell: 10_000,
7
7
  mutation: 10_000,
8
8
  insertCell: 60_000,
9
- runCell: 120_000,
9
+ runCell: 3_600_000,
10
10
  symbolLookup: 30_000,
11
11
  agentHeartbeatDegradedMs: 10_000,
12
12
  agentHeartbeatOfflineMs: 30_000,
@@ -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.1";
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.1";
12
12
  export async function startBunRuntime(deps = {}) {
13
13
  const config = deps.runtimeConfig ?? loadRuntimeConfig();
14
14
  const bridgeOnly = deps.bridgeOnly ?? config.bridgeOnly;
@@ -65,6 +65,11 @@ export async function startBunRuntime(deps = {}) {
65
65
  console.error("Bun MCP mode enabled; connecting stdio transport.");
66
66
  await server.connect(createTransport());
67
67
  }
68
+ const sweepInterval = setInterval(() => {
69
+ state.sweepLiveness();
70
+ state.queue.markTimedOut(Date.now());
71
+ }, 10_000);
72
+ sweepInterval.unref();
68
73
  }
69
74
  catch (error) {
70
75
  await stop();
@@ -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) {
@@ -128,7 +128,7 @@ const queuedNotebookTools = [
128
128
  schema: runCellSchema.shape,
129
129
  permission: "RunCell",
130
130
  timeoutMs: (args) => {
131
- const timeoutSec = typeof args.timeoutSec === "number" ? args.timeoutSec : 120;
131
+ const timeoutSec = typeof args.timeoutSec === "number" ? args.timeoutSec : 86_400;
132
132
  return timeoutSec * 1000;
133
133
  },
134
134
  requiresExplicitTarget: true,
@@ -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.1") {
39
41
  return new McpServer({ name, version }, { instructions: MICA_AGENT_INSTRUCTIONS });
40
42
  }
41
43
  export function registerMicaPrompts(server) {
@@ -33,7 +33,7 @@ export const deleteCellSchema = z.object({
33
33
  export const runCellSchema = z.object({
34
34
  ...notebookSelectorFields,
35
35
  cellId: z.string().min(1),
36
- timeoutSec: z.number().int().positive().max(3600).default(120)
36
+ timeoutSec: z.number().int().positive().optional()
37
37
  }).strict();
38
38
  export const abortEvaluationSchema = z.object({
39
39
  ...notebookSelectorFields
@@ -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.1",
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"}]
@@ -1370,7 +1376,7 @@ DeleteCellRequest[args_Association] := Module[{notebookId, record, notebook, cel
1370
1376
  <|"status" -> "deleted", "cellId" -> cellId, "deletedArtifactCount" -> Length[artifacts]|>
1371
1377
  ];
1372
1378
 
1373
- RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cell, timeoutSec = Lookup[args, "timeoutSec", 120], installedEpilog, evaluateResult},
1379
+ RunCellRequest[args_Association] := Module[{notebookId, record, notebook, cellId, cell, timeoutSec = Lookup[args, "timeoutSec", Infinity], installedEpilog, evaluateResult},
1374
1380
  notebookId = TargetNotebookId[args];
1375
1381
  If[Not @ ConfirmAction["RunCell", "AI requests running 1 cell. Allow?", notebookId], Return[$Canceled]];
1376
1382
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1384,20 +1390,19 @@ 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;
1398
1404
  $RunningStartedAt = AbsoluteTime[];
1399
- If[!NumericQ[timeoutSec], timeoutSec = 120];
1400
- $RunningTimeoutAt = AbsoluteTime[] + timeoutSec;
1405
+ $RunningTimeoutAt = If[NumericQ[timeoutSec] && timeoutSec < Infinity, AbsoluteTime[] + timeoutSec, Infinity];
1401
1406
  SelectionMove[cell, All, Cell];
1402
1407
  (* RunCell returns immediately; Palette-local cancellation can still abort the running evaluation. *)
1403
1408
  evaluateResult = Quiet @ Check[FrontEndTokenExecute[notebook, "EvaluateCells"], $Failed];
@@ -1424,15 +1429,15 @@ AbortEvaluationRequest[args_Association] := Module[{notebookId, record, notebook
1424
1429
  FinishRunningCell["finished"];
1425
1430
  Return[<|"status" -> "finished", "cellId" -> runningCellId, "requestId" -> runningRequestId|>]
1426
1431
  ];
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
- ]
1432
+ If[wasRunning,
1433
+ $AbortRequestedAt = AbsoluteTime[];
1434
+ $RunningStatus = "abort_requested"
1435
+ ];
1436
+ Quiet @ Check[FrontEndTokenExecute[notebook, "EvaluatorAbort"], Null];
1437
+ If[wasRunning,
1438
+ <|"status" -> "abort_requested", "cellId" -> runningCellId, "requestId" -> runningRequestId|>,
1439
+ <|"status" -> "idle"|>
1440
+ ]
1436
1441
  ];
1437
1442
 
1438
1443
  KillKernelRequest[args_Association] := Module[{notebookId, record, notebook, evaluatorName},
@@ -1467,9 +1472,42 @@ RestartKernelRequest[args_Association] := Module[{notebookId, record, notebook,
1467
1472
  <|"status" -> "restarted", "notebookId" -> notebookId|>
1468
1473
  ];
1469
1474
 
1475
+ CreateNotebookRequest[args_Association] := Module[{title, nb, notebookId, payload, response},
1476
+ title = Lookup[args, "title", "Untitled"];
1477
+ If[!StringQ[title] || StringLength[StringTrim[title]] == 0, title = "Untitled"];
1478
+ nb = Quiet @ Check[CreateDocument[{}, WindowTitle -> title, Visible -> True], $Failed];
1479
+ If[Head[nb] =!= NotebookObject, Return[Failure["CREATE_FAILED", <|"message" -> "The FrontEnd failed to create the notebook."|>]]];
1480
+ payload = NotebookHeartbeatPayload[nb];
1481
+ response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1482
+ notebookId = If[AssociationQ[response],
1483
+ Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]],
1484
+ None
1485
+ ];
1486
+ <|"status" -> "created", "notebookId" -> notebookId, "title" -> title|>
1487
+ ];
1488
+
1489
+ OpenNotebookRequest[args_Association] := Module[{path, nb, notebookId, payload, response},
1490
+ path = Lookup[args, "path", ""];
1491
+ If[!StringQ[path] || StringLength[StringTrim[path]] == 0,
1492
+ Return[Failure["BAD_REQUEST", <|"message" -> "path is required."|>]]
1493
+ ];
1494
+ If[!FileExistsQ[path],
1495
+ Return[Failure["NOT_FOUND", <|"message" -> "File not found: " <> path|>]]
1496
+ ];
1497
+ nb = Quiet @ Check[NotebookOpen[path, Visible -> True], $Failed];
1498
+ If[Head[nb] =!= NotebookObject, Return[Failure["OPEN_FAILED", <|"message" -> "The FrontEnd failed to open the notebook."|>]]];
1499
+ payload = NotebookHeartbeatPayload[nb];
1500
+ response = Quiet @ Check[BridgePost["/notebooks/heartbeat", payload], $Failed];
1501
+ notebookId = If[AssociationQ[response],
1502
+ Lookup[Lookup[response, "notebook", <||>], "notebookId", Lookup[response, "notebookId", None]],
1503
+ None
1504
+ ];
1505
+ <|"status" -> "opened", "notebookId" -> notebookId, "path" -> path|>
1506
+ ];
1507
+
1470
1508
  SaveNotebookRequest[args_Association] := Module[{notebookId, record, notebook},
1471
1509
  notebookId = TargetNotebookId[args];
1472
- If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?", notebookId], Return[$Canceled]];
1510
+ If[Not @ ConfirmAction["SaveNotebook", "AI requests saving the notebook. Allow?", notebookId], Return[$Canceled]];
1473
1511
  If[!StringQ[notebookId] || StringLength[notebookId] == 0, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
1474
1512
  record = NotebookRecord[notebookId];
1475
1513
  If[!AssociationQ[record] || record === <||>, Return[Failure["BAD_REQUEST", <|"message" -> "No notebook is selected."|>]]];
@@ -1661,7 +1699,15 @@ ExecuteAgentRequest[request_Association] := Module[{args = Lookup[request, "argu
1661
1699
  ];
1662
1700
 
1663
1701
  SafeHiddenAgentTick[] := Module[{payload, request},
1664
- If[TrueQ[$HiddenAgentInProgress], Return[$LastResultStatus]];
1702
+ If[TrueQ[$HiddenAgentInProgress],
1703
+ If[NumberQ[$HiddenAgentStartedAt] && AbsoluteTime[] - $HiddenAgentStartedAt > 120,
1704
+ $HiddenAgentInProgress = False;
1705
+ $HiddenAgentStartedAt = None;
1706
+ $HiddenAgentStartedAt = None,
1707
+ Return[$LastResultStatus]
1708
+ ]
1709
+ ];
1710
+ $HiddenAgentStartedAt = AbsoluteTime[];
1665
1711
  Internal`WithLocalSettings[
1666
1712
  $HiddenAgentInProgress = True,
1667
1713
  (
@@ -1686,7 +1732,9 @@ SafeHiddenAgentTick[] := Module[{payload, request},
1686
1732
  ];
1687
1733
  Null
1688
1734
  ),
1689
- $HiddenAgentInProgress = False
1735
+ $HiddenAgentInProgress = False;
1736
+ $HiddenAgentStartedAt = None;
1737
+ $HiddenAgentStartedAt = None
1690
1738
  ]
1691
1739
  ];
1692
1740
 
@@ -1839,13 +1887,15 @@ ExecuteRequest[request_Association] := Module[{requestId, tool, args, result},
1839
1887
  "mma_insert_cell", InsertCellRequest[args],
1840
1888
  "mma_modify_cell", ModifyCellRequest[args],
1841
1889
  "mma_delete_cell", DeleteCellRequest[args],
1842
- "mma_run_cell", RunCellRequest[args],
1890
+ "mma_run_cell", RunCellRequest[args],
1843
1891
  "mma_abort_evaluation", AbortEvaluationRequest[args],
1844
1892
  "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],
1893
+ "mma_restart_kernel", RestartKernelRequest[args],
1894
+ "mma_create_notebook", CreateNotebookRequest[args],
1895
+ "mma_open_notebook", OpenNotebookRequest[args],
1896
+ "mma_get_cell_output", GetCellOutputById[args],
1897
+ "mma_read_artifact", ReadArtifactById[args],
1898
+ "mma_save_notebook", SaveNotebookRequest[args],
1849
1899
  "mma_select_notebook", SelectNotebookRequest[args],
1850
1900
  "mma_symbol_lookup", SymbolLookup[Lookup[args, "query", ""]],
1851
1901
  _, Failure["BAD_REQUEST", <|"Message" -> StringTemplate["Unknown tool ``."][tool]|>]