@granular-software/sdk 0.4.44 → 0.4.46

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/dist/index.js CHANGED
@@ -4022,6 +4022,9 @@ var MAX_TIMER_DELAY_MS = 2147483647;
4022
4022
  var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
4023
4023
  var DEFAULT_RPC_TIMEOUT_MS = 3e4;
4024
4024
  var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
4025
+ var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
4026
+ var DEFAULT_RECONNECT_DELAY_MS = 3e3;
4027
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
4025
4028
  function debugWs(...args) {
4026
4029
  if (DEBUG_WS) {
4027
4030
  console.log(...args);
@@ -4032,6 +4035,10 @@ function rpcTimeoutMsForMethod(method) {
4032
4035
  case "domain.fetchPackagePart":
4033
4036
  case "domain.getSummary":
4034
4037
  return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
4038
+ case "client.heartbeat":
4039
+ case "effects.publishCatalog":
4040
+ case "effects.refresh":
4041
+ return EFFECT_CONTROL_RPC_TIMEOUT_MS;
4035
4042
  default:
4036
4043
  return DEFAULT_RPC_TIMEOUT_MS;
4037
4044
  }
@@ -4051,6 +4058,7 @@ var WSClient = class {
4051
4058
  reconnectTimer = null;
4052
4059
  tokenRefreshTimer = null;
4053
4060
  isExplicitlyDisconnected = false;
4061
+ reconnectAttempts = 0;
4054
4062
  options;
4055
4063
  constructor(options) {
4056
4064
  this.options = options;
@@ -4205,6 +4213,7 @@ var WSClient = class {
4205
4213
  clearTimeout(this.reconnectTimer);
4206
4214
  this.reconnectTimer = null;
4207
4215
  }
4216
+ this.reconnectAttempts = 0;
4208
4217
  this.emit("open", {});
4209
4218
  resolve();
4210
4219
  });
@@ -4236,6 +4245,7 @@ var WSClient = class {
4236
4245
  clearTimeout(this.reconnectTimer);
4237
4246
  this.reconnectTimer = null;
4238
4247
  }
4248
+ this.reconnectAttempts = 0;
4239
4249
  this.emit("open", {});
4240
4250
  resolve();
4241
4251
  };
@@ -4295,7 +4305,8 @@ var WSClient = class {
4295
4305
  return new Error(`WebSocket disconnected${suffix}`);
4296
4306
  }
4297
4307
  handleDisconnect(close = {}) {
4298
- const reconnectDelayMs = 3e3;
4308
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4309
+ const maxReconnectAttempts = typeof this.options.maxReconnectAttempts === "number" && Number.isFinite(this.options.maxReconnectAttempts) && this.options.maxReconnectAttempts >= 0 ? Math.floor(this.options.maxReconnectAttempts) : DEFAULT_MAX_RECONNECT_ATTEMPTS;
4299
4310
  const unexpected = !this.isExplicitlyDisconnected;
4300
4311
  const info = {
4301
4312
  code: close.code,
@@ -4315,6 +4326,30 @@ var WSClient = class {
4315
4326
  const disconnectError = this.buildDisconnectError(info);
4316
4327
  this.rejectPending(disconnectError);
4317
4328
  this.emit("disconnect", info);
4329
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4330
+ const reconnectInfo = {
4331
+ error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4332
+ sessionId: this.sessionId,
4333
+ timestamp: Date.now()
4334
+ };
4335
+ this.emit("reconnect_error", reconnectInfo);
4336
+ if (this.options.onReconnectError) {
4337
+ try {
4338
+ this.options.onReconnectError(reconnectInfo);
4339
+ } catch (callbackError) {
4340
+ console.error(
4341
+ "[Granular] onReconnectError callback failed:",
4342
+ callbackError
4343
+ );
4344
+ }
4345
+ }
4346
+ return;
4347
+ }
4348
+ this.reconnectAttempts += 1;
4349
+ const reconnectDelayMs = Math.min(
4350
+ 3e4,
4351
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4352
+ );
4318
4353
  info.reconnectScheduled = true;
4319
4354
  info.reconnectDelayMs = reconnectDelayMs;
4320
4355
  if (this.options.onUnexpectedClose) {
@@ -4769,6 +4804,9 @@ var Session = class {
4769
4804
  promptCache = /* @__PURE__ */ new Map();
4770
4805
  /** Prompt ids locally answered before the document sync catches up. */
4771
4806
  hiddenPromptIds = /* @__PURE__ */ new Set();
4807
+ domainPackagePartCache = /* @__PURE__ */ new Map();
4808
+ domainPackagePartPromises = /* @__PURE__ */ new Map();
4809
+ domainPackageFetchQueue = Promise.resolve();
4772
4810
  constructor(client, clientId, options = {}) {
4773
4811
  this.client = client;
4774
4812
  this.clientId = clientId || `client_${Date.now()}`;
@@ -4976,12 +5014,18 @@ var Session = class {
4976
5014
  const resolvedAnswer = resolvePromptAnswer(prompt, answer);
4977
5015
  this.promptCache.delete(promptId);
4978
5016
  this.hiddenPromptIds.add(promptId);
5017
+ this.emit("prompt", { id: promptId, status: "answered" });
4979
5018
  try {
4980
- await this.client.call("prompt.answer", {
5019
+ const response = await this.client.call("prompt.answer", {
4981
5020
  promptId,
4982
5021
  answer: resolvedAnswer,
4983
5022
  value: resolvedAnswer
4984
5023
  });
5024
+ if (response && typeof response === "object" && "ok" in response && response.ok === false) {
5025
+ const rejected = response;
5026
+ const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
5027
+ throw new Error(errorMessage);
5028
+ }
4985
5029
  } catch (error) {
4986
5030
  this.hiddenPromptIds.delete(promptId);
4987
5031
  if (prompt) {
@@ -5209,11 +5253,33 @@ var Session = class {
5209
5253
  * Fetch a domain package part from the backend (no fallback).
5210
5254
  */
5211
5255
  async fetchDomainPart(part) {
5212
- const result = await this.client.call("domain.fetchPackagePart", {
5213
- moduleSpecifier: "@sandbox/domain",
5214
- part
5256
+ const cached = this.domainPackagePartCache.get(part);
5257
+ if (cached !== void 0) {
5258
+ return cached;
5259
+ }
5260
+ const inFlight = this.domainPackagePartPromises.get(part);
5261
+ if (inFlight) {
5262
+ return inFlight;
5263
+ }
5264
+ const fetchPromise = this.domainPackageFetchQueue.then(async () => {
5265
+ const result = await this.client.call("domain.fetchPackagePart", {
5266
+ moduleSpecifier: "@sandbox/domain",
5267
+ part
5268
+ });
5269
+ const content = result?.content ?? "";
5270
+ this.domainPackagePartCache.set(part, content);
5271
+ return content;
5215
5272
  });
5216
- return result?.content ?? "";
5273
+ this.domainPackagePartPromises.set(part, fetchPromise);
5274
+ this.domainPackageFetchQueue = fetchPromise.then(
5275
+ () => void 0,
5276
+ () => void 0
5277
+ );
5278
+ try {
5279
+ return await fetchPromise;
5280
+ } finally {
5281
+ this.domainPackagePartPromises.delete(part);
5282
+ }
5217
5283
  }
5218
5284
  /**
5219
5285
  * Get TypeScript class declarations for the current domain (for LLM/code gen).
@@ -5463,7 +5529,10 @@ import { ${allImports} } from "./sandbox-tools";
5463
5529
  const emitPrompt = (payload) => {
5464
5530
  const prompt = normalizePrompt(payload);
5465
5531
  if (!prompt) return;
5466
- this.hiddenPromptIds.delete(prompt.id);
5532
+ if (this.hiddenPromptIds.has(prompt.id)) {
5533
+ this.emit("prompt", { ...prompt, status: "answered" });
5534
+ return;
5535
+ }
5467
5536
  this.promptCache.set(prompt.id, prompt);
5468
5537
  this.emit("prompt", prompt);
5469
5538
  };
@@ -13074,6 +13143,7 @@ var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
13074
13143
  var SESSION_DATA_REQUEST_RETRY_COUNT = 4;
13075
13144
  var SESSION_DATA_REQUEST_RETRY_DELAY_MS = 500;
13076
13145
  var EFFECT_HOST_CONNECT_TIMEOUT_MS = 15e3;
13146
+ var SESSION_CONNECT_TIMEOUT_MS = 15e3;
13077
13147
  function filenameFromUploadBody(body) {
13078
13148
  const maybe = body;
13079
13149
  return typeof maybe.name === "string" && maybe.name.trim() ? maybe.name.trim() : null;
@@ -13093,7 +13163,7 @@ function bodyInitFromSessionFileUpload(body) {
13093
13163
  }
13094
13164
  return body;
13095
13165
  }
13096
- var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 3e4;
13166
+ var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 12e4;
13097
13167
  var EFFECT_CATALOG_SYNC_RETRY_COUNT = 3;
13098
13168
  var EFFECT_CATALOG_SYNC_RETRY_DELAY_MS = 1e3;
13099
13169
  function planRecordObjectsChunks(records, batchSize) {
@@ -15049,7 +15119,14 @@ var Granular = class _Granular {
15049
15119
  return tag;
15050
15120
  }
15051
15121
  buildManagedEnvironmentName(tag, versionId) {
15052
- return `__sdk__${tag}__${versionId}`;
15122
+ return `__sdk__${tag}__${versionId}__pinned`;
15123
+ }
15124
+ isManagedEnvironmentName(environment, tagName) {
15125
+ const name = environment.environment || environment.envName || "";
15126
+ return name.startsWith(`__sdk__${tagName}__`);
15127
+ }
15128
+ isPinnedToVersion(environment, versionId) {
15129
+ return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
15053
15130
  }
15054
15131
  matchesTagTrackedEnvironment(environment, tagName, tagId) {
15055
15132
  const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
@@ -15110,7 +15187,7 @@ var Granular = class _Granular {
15110
15187
  );
15111
15188
  const currentMatches = this.sortEnvironmentsByRecency(
15112
15189
  userEnvironments.filter(
15113
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
15190
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
15114
15191
  )
15115
15192
  );
15116
15193
  if (currentMatches.length > 0) {
@@ -15139,6 +15216,7 @@ var Granular = class _Granular {
15139
15216
  subjectId: user.granularId,
15140
15217
  environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
15141
15218
  tagId: tag.tagId,
15219
+ versionId: targetVersionId,
15142
15220
  permissionProfileId: null
15143
15221
  }),
15144
15222
  requestedOntology: ontology,
@@ -15184,6 +15262,7 @@ var Granular = class _Granular {
15184
15262
  row.summaryUpdatedAt ?? row.summary_updated_at
15185
15263
  ) : null,
15186
15264
  subjectId: row.subjectId != null ? String(row.subjectId) : null,
15265
+ sessionScope: row.sessionScope != null || row.session_scope != null ? String(row.sessionScope ?? row.session_scope) : null,
15187
15266
  jobCount: typeof row.jobCount === "number" ? row.jobCount : void 0,
15188
15267
  toolCallCount: typeof row.toolCallCount === "number" ? row.toolCallCount : void 0
15189
15268
  };
@@ -15391,7 +15470,11 @@ var Granular = class _Granular {
15391
15470
  onUnexpectedClose: this.onUnexpectedClose,
15392
15471
  onReconnectError: this.onReconnectError
15393
15472
  });
15394
- await client.connect();
15473
+ await withTimeout(
15474
+ client.connect(),
15475
+ SESSION_CONNECT_TIMEOUT_MS,
15476
+ `session WebSocket connect for ${session.sessionId}`
15477
+ );
15395
15478
  const environmentSession = new EnvironmentSession(
15396
15479
  client,
15397
15480
  environment,
@@ -15562,12 +15645,24 @@ var Granular = class _Granular {
15562
15645
  host.heartbeatInFlight = false;
15563
15646
  }
15564
15647
  async synchronizeEffectHost(host) {
15565
- await host.wsClient.call("client.hello", {
15566
- clientId: host.clientId,
15567
- protocolVersion: "2.0"
15568
- });
15569
- this.startEffectHostHeartbeat(host);
15570
- await this.publishSandboxEffectCatalog(host);
15648
+ if (host.syncPromise) {
15649
+ return host.syncPromise;
15650
+ }
15651
+ host.syncPromise = (async () => {
15652
+ await host.wsClient.call("client.hello", {
15653
+ clientId: host.clientId,
15654
+ protocolVersion: "2.0"
15655
+ });
15656
+ await this.publishSandboxEffectCatalog(host);
15657
+ this.startEffectHostHeartbeat(host);
15658
+ })();
15659
+ try {
15660
+ await host.syncPromise;
15661
+ } finally {
15662
+ if (host.syncPromise) {
15663
+ host.syncPromise = null;
15664
+ }
15665
+ }
15571
15666
  }
15572
15667
  async ensureSandboxEffectHost(sandboxId) {
15573
15668
  const existing = this.sandboxEffectHosts.get(sandboxId);
@@ -15603,7 +15698,8 @@ var Granular = class _Granular {
15603
15698
  wsClient,
15604
15699
  heartbeatTimer: null,
15605
15700
  heartbeatInFlight: false,
15606
- recovering: false
15701
+ recovering: false,
15702
+ syncPromise: null
15607
15703
  };
15608
15704
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
15609
15705
  const request = params;
@@ -17710,7 +17806,8 @@ function buildGranularAgentSessionBlock(sessionContext) {
17710
17806
  runtimeId: sessionContext?.sandboxId || null,
17711
17807
  environmentId: sessionContext?.environmentId || null,
17712
17808
  userName: sessionContext?.userName || null,
17713
- domainRevision: sessionContext?.domainRevision || null
17809
+ domainRevision: sessionContext?.domainRevision || null,
17810
+ uiContext: sessionContext?.uiContext || null
17714
17811
  });
17715
17812
  }
17716
17813
  function buildGranularAgentHeapBlock(heapSummary) {
@@ -17957,7 +18054,7 @@ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
17957
18054
  const rightScope = `${right.className || "global"}:${right.static ? "static" : "instance"}`;
17958
18055
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
17959
18056
  });
17960
- const writeActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
18057
+ const availableActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
17961
18058
  const scope = tool.className ? `${tool.static ? "class" : "record"}:${tool.className}` : "global";
17962
18059
  return {
17963
18060
  name: tool.name,
@@ -17968,7 +18065,8 @@ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
17968
18065
  const capabilities = {
17969
18066
  executeCode: resolvedCapabilities.executeCode,
17970
18067
  readEntities: resolvedCapabilities.readEntities,
17971
- writeActions,
18068
+ availableActions,
18069
+ writeActions: availableActions,
17972
18070
  workflowHelpers: resolvedCapabilities.workflowHelpers,
17973
18071
  savedData: resolvedCapabilities.savedData,
17974
18072
  showRecords: resolvedCapabilities.showRecords
@@ -17982,7 +18080,7 @@ function buildGranularAgentActionIndex(tools) {
17982
18080
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
17983
18081
  });
17984
18082
  if (normalizedTools.length === 0) {
17985
- return "No domain write actions are available.";
18083
+ return "No executable actions are available.";
17986
18084
  }
17987
18085
  const globalTools = normalizedTools.filter((tool) => !tool.className);
17988
18086
  const staticTools = normalizedTools.filter(
@@ -18076,6 +18174,76 @@ function splitDomainDocumentation(domainDocumentation) {
18076
18174
  }
18077
18175
  return { types: normalized, docs: "" };
18078
18176
  }
18177
+ var DOMAIN_HELPER_FUNCTION_NAMES = /* @__PURE__ */ new Set([
18178
+ "agent_heap_objects",
18179
+ "agent_message",
18180
+ "agent_text_message"
18181
+ ]);
18182
+ function inferGlobalActionToolsFromDomainTypes(domainTypes) {
18183
+ const inferred = [];
18184
+ const seen = /* @__PURE__ */ new Set();
18185
+ const declarationPattern = /(?:export\s+)?declare\s+function\s+([A-Za-z_$][\w$]*)\s*\(/g;
18186
+ let match;
18187
+ while (match = declarationPattern.exec(domainTypes)) {
18188
+ const name = match[1];
18189
+ if (!name || DOMAIN_HELPER_FUNCTION_NAMES.has(name) || seen.has(name)) {
18190
+ continue;
18191
+ }
18192
+ seen.add(name);
18193
+ inferred.push({
18194
+ name,
18195
+ description: "Executable global action declared by the domain runtime."
18196
+ });
18197
+ }
18198
+ const actionLinePattern = /^-\s*([A-Za-z_$][\w$]*)\s+\(global\):\s*(.+)$/gm;
18199
+ while (match = actionLinePattern.exec(domainTypes)) {
18200
+ const name = match[1];
18201
+ if (!name || DOMAIN_HELPER_FUNCTION_NAMES.has(name) || seen.has(name)) {
18202
+ continue;
18203
+ }
18204
+ seen.add(name);
18205
+ inferred.push({
18206
+ name,
18207
+ description: match[2]?.trim() || "Executable global action declared by the domain runtime."
18208
+ });
18209
+ }
18210
+ const scopedActionLinePattern = /^-\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s+\((record|class)\):\s*(.+)$/gm;
18211
+ while (match = scopedActionLinePattern.exec(domainTypes)) {
18212
+ const className = match[1]?.toLowerCase();
18213
+ const name = match[2];
18214
+ const scope = match[3];
18215
+ if (!className || !name || DOMAIN_HELPER_FUNCTION_NAMES.has(name)) {
18216
+ continue;
18217
+ }
18218
+ const key = `${className}:${scope}:${name}`;
18219
+ if (seen.has(key)) {
18220
+ continue;
18221
+ }
18222
+ seen.add(key);
18223
+ inferred.push({
18224
+ name,
18225
+ className,
18226
+ static: scope === "class",
18227
+ description: match[4]?.trim() || "Executable action declared by the domain runtime."
18228
+ });
18229
+ }
18230
+ return inferred;
18231
+ }
18232
+ function resolvePromptTools(tools, domainTypes) {
18233
+ const byKey = /* @__PURE__ */ new Map();
18234
+ for (const tool of tools || []) {
18235
+ if (!tool?.name) continue;
18236
+ const key = `${tool.className || "global"}:${tool.static ? "static" : "instance"}:${tool.name}`;
18237
+ byKey.set(key, tool);
18238
+ }
18239
+ for (const tool of inferGlobalActionToolsFromDomainTypes(domainTypes)) {
18240
+ const key = `global:instance:${tool.name}`;
18241
+ if (!byKey.has(key)) {
18242
+ byKey.set(key, tool);
18243
+ }
18244
+ }
18245
+ return [...byKey.values()];
18246
+ }
18079
18247
  function buildGranularAgentCheckpointBlock(checkpoint) {
18080
18248
  if (!checkpoint) {
18081
18249
  return renderConstBlock("previousCodeResult", null);
@@ -18148,12 +18316,13 @@ function buildGranularAgentSystemPrompt(input) {
18148
18316
  const outputMode = input.outputMode || "agentMessages";
18149
18317
  const promptCapabilities = resolvePromptCapabilities(input.capabilities);
18150
18318
  const domainSections = splitDomainDocumentation(input.domainDocumentation);
18319
+ const promptTools = resolvePromptTools(input.tools, domainSections.types);
18151
18320
  const sessionBlock = buildGranularAgentSessionBlock(input.sessionContext);
18152
18321
  const toolBlock = buildGranularAgentToolBlock(
18153
- input.tools,
18322
+ promptTools,
18154
18323
  input.capabilities
18155
18324
  );
18156
- const actionIndex = buildGranularAgentActionIndex(input.tools);
18325
+ const actionIndex = buildGranularAgentActionIndex(promptTools);
18157
18326
  const domainBlock = buildGranularAgentDomainBlock(domainSections.types);
18158
18327
  const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
18159
18328
  const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
@@ -18181,7 +18350,7 @@ function buildGranularAgentSystemPrompt(input) {
18181
18350
  - When \`agent_text_message(...)\` mentions a grounded record that should remain clickable/referable, wrap only the visible record label in a self-closing inline reference tag: \`<granular-object class="class_name" id="stable_id_or_path" label="Visible label" />\`. Use the actual class name and stable id/path from the runtime record or effect result; do not invent ids, field names, or snake/camel-case aliases that are not present in the type declarations or returned object.
18182
18351
  - Treat \`agent_heap_objects(...)\` as the UI display call for user-visible records, not as a general storage helper. Do not wrap records under an \`items\` key.
18183
18352
  - When records should remain reusable for follow-ups, first save the runtime record or ordered record array with \`await heap.setVar("stable_selection_name", value)\`, then display that saved selection exactly once with \`await agent_heap_objects({ variableNames: ["stable_selection_name"] })\`.
18184
- - \`heap.setVar(...)\` only accepts scalar values, runtime records/sandbox instances, or arrays of runtime records/sandbox instances. Do not save plain action/effect result objects. If an action returns an id/path for a created record that should remain referable, fetch the created record first, then save/display that fetched record.
18353
+ - \`heap.setVar(...)\` only accepts scalar values, runtime records/sandbox instances, or arrays of runtime records/sandbox instances from one class. Do not save plain action/effect result objects or arrays of JSON summaries returned by actions. If an action returns ids/paths for records that should remain referable or displayed as records, fetch the matching runtime records first with the generated class \`.get(...)\`/query API, then save/display those fetched records. If the action returned only structured summaries, answer from those summaries with \`agent_text_message(...)\`.
18185
18354
  - Do not use \`agent_heap_objects({ entries: [...] })\` or \`agent_heap_objects({ saveAs, entries })\` as a shortcut for ordered pages, queues, search results, or ranked lists; those forms can create duplicate or poorly labelled displays. Save the selection with \`heap.setVar(...)\` and display it via \`variableNames\` instead.
18186
18355
  - Use \`entryPaths\` only for a few already-known individual records and \`listNames\` only for a host-created list that you intentionally want to show. Do not display both an entry/list selection and a heap variable for the same records.
18187
18356
  - When the user asks to show, list, display, open, or "show them" for records you found, call \`agent_heap_objects(...)\`; do not answer only with a count or text summary.
@@ -18231,10 +18400,11 @@ ${outputRules}` : `Code:
18231
18400
  - Use choice only for 2 to 5 short grounded options.
18232
18401
  - For record choices, set each option value to a stable scalar such as the record \`_graphPath\` or \`id\`, not a label-only value.
18233
18402
  - After \`await loop.ask_user(...)\` returns from a choice prompt, tolerate either the option value, the option object, or a human-readable label by matching against value, id/path, label, and description before failing. If a returned label is a prefix or substring of exactly one option label, treat it as that option.
18234
- - Use \`loop.confirm(...)\` for yes/no confirmation only when the user explicitly asks for confirmation, action or permission metadata requires it, policy requires it, or material uncertainty remains after grounding.
18403
+ - Use \`loop.confirm(...)\` for yes/no confirmation only when the user explicitly asks for a separate confirmation step, policy requires confirmation outside the action runtime, or material uncertainty remains after grounding.
18404
+ - If action, effect, tool, or permission metadata already marks the invoked action as confirmation-gated, do not call \`loop.confirm(...)\` before invoking it. Ground the target and input, then call the action once; the runtime action policy will surface the confirmation prompt and resume the same invocation after approval.
18235
18405
  - Do not add a generic yes/no confirmation after the user has already made a grounded choice, unless one of those confirmation conditions still applies.
18236
- - Do not add confirmation only because an allowed mutation is visible to other people, customer-facing, or consequential. If the user clearly requested the mutation and the grounded target, action, and condition are unique, perform the mutation unless confirmation is required by the user, policy, action metadata, or remaining material uncertainty.
18237
- - A conditional request such as "if this is true, do that" is authorization to perform the requested action after you verify the condition. Once the condition, target, and action are grounded uniquely, call the action directly; do not ask "should I perform/post/send this?" unless the user, policy, action metadata, or unresolved material uncertainty requires confirmation. The visibility or impact of an allowed action is not by itself unresolved uncertainty.
18406
+ - Do not add confirmation only because an allowed mutation is visible to other people, customer-facing, or consequential. If the user clearly requested the mutation and the grounded target, action, and condition are unique, perform the mutation unless confirmation is required outside the action runtime or remaining material uncertainty exists.
18407
+ - A conditional request such as "if this is true, do that" is authorization to perform the requested action after you verify the condition. Once the condition, target, and action are grounded uniquely, call the action directly; do not ask "should I perform/post/send this?" unless the user, policy outside the action runtime, or unresolved material uncertainty requires confirmation. The visibility or impact of an allowed action is not by itself unresolved uncertainty.
18238
18408
  - If the user explicitly asks you to stop for confirmation, natural-language text such as "please confirm" is not enough: call \`await loop.confirm(...)\` before the mutation, then perform the approved mutation in the same resumed job when it returns true.
18239
18409
  - Reuse existing task, decision, and closure ids from [State].
18240
18410
  - If a user request matches both a domain record/action and a workflow helper, prefer the domain capability.` : "";
@@ -18380,7 +18550,8 @@ Query policy:
18380
18550
  - For operational blocker, risk, status, or "what is happening" questions, inspect the relevant record's scalar fields such as status, priority, blocker, summary, latest update/message, due date, amount, and other domain-specific descriptive fields before answering.
18381
18551
  - For read-only readiness, risk, health, or status summaries, call any visible read-only assessment/status action on the grounded primary record before ad-hoc aggregation when such an action semantically matches the request. Use the returned fields in the reply and supplement with counts or record reads only when useful.
18382
18552
  - Do not hide required visible read-only assessment/status actions inside broad try/catch blocks. The runtime action surface should show that the assessment action ran.
18383
- - Treat action/effect results as structured values, not necessarily arrays. Before indexing, iterating, checking \`.length\`, or calling array methods, normalize the result first: use the result itself only when \`Array.isArray(result)\`; otherwise read the exact array field shown in the output schema, or a documented array field such as \`items\`, \`matches\`, \`results\`, \`records\`, \`entries\`, \`candidates\`, \`options\`, or a domain-specific array field. Never convert a non-array object result to \`[]\` before checking its documented fields.
18553
+ - Treat action/effect results as structured values, not necessarily arrays. Before indexing, iterating, checking \`.length\`, or calling array methods, normalize the result first: use the result itself only when \`Array.isArray(result)\`; otherwise read the exact array field shown in the output schema, or a documented array field such as \`items\`, \`matches\`, \`results\`, \`records\`, \`entries\`, \`candidates\`, \`options\`, \`requests\`, \`vendors\`, \`transactions\`, \`approvals\`, \`receipts\`, or another domain-specific array field. If a structured result has \`count > 0\`, never conclude there are no matches until you inspect every array-valued field on that result object, especially fields named by the output schema. Never convert a non-array object result to \`[]\` before checking its documented fields.
18554
+ - Plain JSON objects returned by actions are not sandbox record instances, even when they contain ids, titles, labels, or status fields. Use them for reasoning and text responses. Do not pass action-returned JSON objects or arrays directly to \`heap.setVar(...)\` or \`agent_heap_objects(...)\`; fetch corresponding runtime records first when the user needs record display or follow-up references.
18384
18555
  - When a visible search, lookup, availability, or assessment action returns candidates or matches, treat those returned records as already scoped by the action inputs unless the output schema gives reliable fields for further narrowing. When matching returned candidates to grounded records, use the output schema's actual identifier fields, including \`id\`, \`path\`, or fields ending in \`Id\`; do not assume candidates have \`_graphPath\`. Do not discard all returned candidates by re-filtering on guessed property names.
18385
18556
  - For scheduling actions, convert relative wording into concrete ISO timestamps before mutating records.
18386
18557
  - When a decision depends on fresh external state and a visible read-only status/lookup action exists on the grounded record, call it before deciding, mutating, or refusing based on stale stored fields.
@@ -18442,6 +18613,7 @@ ${domainSections.docs}
18442
18613
 
18443
18614
  Actions:
18444
18615
  ${actionIndex}
18616
+ - Global actions are executable functions exported by "./sandbox-tools"; import each global action you call, e.g. \`import { some_action } from "./sandbox-tools"; await some_action(...)\`. This includes frontend actions such as opening, focusing, or navigating the host UI.
18445
18617
  - Actions listed under "Record-level" are instance methods. First fetch or find the specific record, then call the action on that instance, e.g. \`const item = await Item.get({ path }); await item.action_name(...)\`.
18446
18618
  - Actions listed under "Class-level" are class/static methods. Call them on the imported class, e.g. \`await Item.action_name(...)\`.
18447
18619
  - The action index is the visibility contract. If an action is listed for a class, call it directly on fetched/listed instances of that class; do not use \`typeof record.action_name === "function"\` as a discovery gate. If an action is not listed, do not call it.