@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.mjs CHANGED
@@ -4000,6 +4000,9 @@ var MAX_TIMER_DELAY_MS = 2147483647;
4000
4000
  var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
4001
4001
  var DEFAULT_RPC_TIMEOUT_MS = 3e4;
4002
4002
  var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
4003
+ var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
4004
+ var DEFAULT_RECONNECT_DELAY_MS = 3e3;
4005
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
4003
4006
  function debugWs(...args) {
4004
4007
  if (DEBUG_WS) {
4005
4008
  console.log(...args);
@@ -4010,6 +4013,10 @@ function rpcTimeoutMsForMethod(method) {
4010
4013
  case "domain.fetchPackagePart":
4011
4014
  case "domain.getSummary":
4012
4015
  return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
4016
+ case "client.heartbeat":
4017
+ case "effects.publishCatalog":
4018
+ case "effects.refresh":
4019
+ return EFFECT_CONTROL_RPC_TIMEOUT_MS;
4013
4020
  default:
4014
4021
  return DEFAULT_RPC_TIMEOUT_MS;
4015
4022
  }
@@ -4029,6 +4036,7 @@ var WSClient = class {
4029
4036
  reconnectTimer = null;
4030
4037
  tokenRefreshTimer = null;
4031
4038
  isExplicitlyDisconnected = false;
4039
+ reconnectAttempts = 0;
4032
4040
  options;
4033
4041
  constructor(options) {
4034
4042
  this.options = options;
@@ -4183,6 +4191,7 @@ var WSClient = class {
4183
4191
  clearTimeout(this.reconnectTimer);
4184
4192
  this.reconnectTimer = null;
4185
4193
  }
4194
+ this.reconnectAttempts = 0;
4186
4195
  this.emit("open", {});
4187
4196
  resolve();
4188
4197
  });
@@ -4214,6 +4223,7 @@ var WSClient = class {
4214
4223
  clearTimeout(this.reconnectTimer);
4215
4224
  this.reconnectTimer = null;
4216
4225
  }
4226
+ this.reconnectAttempts = 0;
4217
4227
  this.emit("open", {});
4218
4228
  resolve();
4219
4229
  };
@@ -4273,7 +4283,8 @@ var WSClient = class {
4273
4283
  return new Error(`WebSocket disconnected${suffix}`);
4274
4284
  }
4275
4285
  handleDisconnect(close = {}) {
4276
- const reconnectDelayMs = 3e3;
4286
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4287
+ 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;
4277
4288
  const unexpected = !this.isExplicitlyDisconnected;
4278
4289
  const info = {
4279
4290
  code: close.code,
@@ -4293,6 +4304,30 @@ var WSClient = class {
4293
4304
  const disconnectError = this.buildDisconnectError(info);
4294
4305
  this.rejectPending(disconnectError);
4295
4306
  this.emit("disconnect", info);
4307
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4308
+ const reconnectInfo = {
4309
+ error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4310
+ sessionId: this.sessionId,
4311
+ timestamp: Date.now()
4312
+ };
4313
+ this.emit("reconnect_error", reconnectInfo);
4314
+ if (this.options.onReconnectError) {
4315
+ try {
4316
+ this.options.onReconnectError(reconnectInfo);
4317
+ } catch (callbackError) {
4318
+ console.error(
4319
+ "[Granular] onReconnectError callback failed:",
4320
+ callbackError
4321
+ );
4322
+ }
4323
+ }
4324
+ return;
4325
+ }
4326
+ this.reconnectAttempts += 1;
4327
+ const reconnectDelayMs = Math.min(
4328
+ 3e4,
4329
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4330
+ );
4296
4331
  info.reconnectScheduled = true;
4297
4332
  info.reconnectDelayMs = reconnectDelayMs;
4298
4333
  if (this.options.onUnexpectedClose) {
@@ -4747,6 +4782,9 @@ var Session = class {
4747
4782
  promptCache = /* @__PURE__ */ new Map();
4748
4783
  /** Prompt ids locally answered before the document sync catches up. */
4749
4784
  hiddenPromptIds = /* @__PURE__ */ new Set();
4785
+ domainPackagePartCache = /* @__PURE__ */ new Map();
4786
+ domainPackagePartPromises = /* @__PURE__ */ new Map();
4787
+ domainPackageFetchQueue = Promise.resolve();
4750
4788
  constructor(client, clientId, options = {}) {
4751
4789
  this.client = client;
4752
4790
  this.clientId = clientId || `client_${Date.now()}`;
@@ -4954,12 +4992,18 @@ var Session = class {
4954
4992
  const resolvedAnswer = resolvePromptAnswer(prompt, answer);
4955
4993
  this.promptCache.delete(promptId);
4956
4994
  this.hiddenPromptIds.add(promptId);
4995
+ this.emit("prompt", { id: promptId, status: "answered" });
4957
4996
  try {
4958
- await this.client.call("prompt.answer", {
4997
+ const response = await this.client.call("prompt.answer", {
4959
4998
  promptId,
4960
4999
  answer: resolvedAnswer,
4961
5000
  value: resolvedAnswer
4962
5001
  });
5002
+ if (response && typeof response === "object" && "ok" in response && response.ok === false) {
5003
+ const rejected = response;
5004
+ const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
5005
+ throw new Error(errorMessage);
5006
+ }
4963
5007
  } catch (error) {
4964
5008
  this.hiddenPromptIds.delete(promptId);
4965
5009
  if (prompt) {
@@ -5187,11 +5231,33 @@ var Session = class {
5187
5231
  * Fetch a domain package part from the backend (no fallback).
5188
5232
  */
5189
5233
  async fetchDomainPart(part) {
5190
- const result = await this.client.call("domain.fetchPackagePart", {
5191
- moduleSpecifier: "@sandbox/domain",
5192
- part
5234
+ const cached = this.domainPackagePartCache.get(part);
5235
+ if (cached !== void 0) {
5236
+ return cached;
5237
+ }
5238
+ const inFlight = this.domainPackagePartPromises.get(part);
5239
+ if (inFlight) {
5240
+ return inFlight;
5241
+ }
5242
+ const fetchPromise = this.domainPackageFetchQueue.then(async () => {
5243
+ const result = await this.client.call("domain.fetchPackagePart", {
5244
+ moduleSpecifier: "@sandbox/domain",
5245
+ part
5246
+ });
5247
+ const content = result?.content ?? "";
5248
+ this.domainPackagePartCache.set(part, content);
5249
+ return content;
5193
5250
  });
5194
- return result?.content ?? "";
5251
+ this.domainPackagePartPromises.set(part, fetchPromise);
5252
+ this.domainPackageFetchQueue = fetchPromise.then(
5253
+ () => void 0,
5254
+ () => void 0
5255
+ );
5256
+ try {
5257
+ return await fetchPromise;
5258
+ } finally {
5259
+ this.domainPackagePartPromises.delete(part);
5260
+ }
5195
5261
  }
5196
5262
  /**
5197
5263
  * Get TypeScript class declarations for the current domain (for LLM/code gen).
@@ -5441,7 +5507,10 @@ import { ${allImports} } from "./sandbox-tools";
5441
5507
  const emitPrompt = (payload) => {
5442
5508
  const prompt = normalizePrompt(payload);
5443
5509
  if (!prompt) return;
5444
- this.hiddenPromptIds.delete(prompt.id);
5510
+ if (this.hiddenPromptIds.has(prompt.id)) {
5511
+ this.emit("prompt", { ...prompt, status: "answered" });
5512
+ return;
5513
+ }
5445
5514
  this.promptCache.set(prompt.id, prompt);
5446
5515
  this.emit("prompt", prompt);
5447
5516
  };
@@ -13052,6 +13121,7 @@ var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
13052
13121
  var SESSION_DATA_REQUEST_RETRY_COUNT = 4;
13053
13122
  var SESSION_DATA_REQUEST_RETRY_DELAY_MS = 500;
13054
13123
  var EFFECT_HOST_CONNECT_TIMEOUT_MS = 15e3;
13124
+ var SESSION_CONNECT_TIMEOUT_MS = 15e3;
13055
13125
  function filenameFromUploadBody(body) {
13056
13126
  const maybe = body;
13057
13127
  return typeof maybe.name === "string" && maybe.name.trim() ? maybe.name.trim() : null;
@@ -13071,7 +13141,7 @@ function bodyInitFromSessionFileUpload(body) {
13071
13141
  }
13072
13142
  return body;
13073
13143
  }
13074
- var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 3e4;
13144
+ var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 12e4;
13075
13145
  var EFFECT_CATALOG_SYNC_RETRY_COUNT = 3;
13076
13146
  var EFFECT_CATALOG_SYNC_RETRY_DELAY_MS = 1e3;
13077
13147
  function planRecordObjectsChunks(records, batchSize) {
@@ -15027,7 +15097,14 @@ var Granular = class _Granular {
15027
15097
  return tag;
15028
15098
  }
15029
15099
  buildManagedEnvironmentName(tag, versionId) {
15030
- return `__sdk__${tag}__${versionId}`;
15100
+ return `__sdk__${tag}__${versionId}__pinned`;
15101
+ }
15102
+ isManagedEnvironmentName(environment, tagName) {
15103
+ const name = environment.environment || environment.envName || "";
15104
+ return name.startsWith(`__sdk__${tagName}__`);
15105
+ }
15106
+ isPinnedToVersion(environment, versionId) {
15107
+ return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
15031
15108
  }
15032
15109
  matchesTagTrackedEnvironment(environment, tagName, tagId) {
15033
15110
  const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
@@ -15088,7 +15165,7 @@ var Granular = class _Granular {
15088
15165
  );
15089
15166
  const currentMatches = this.sortEnvironmentsByRecency(
15090
15167
  userEnvironments.filter(
15091
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
15168
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
15092
15169
  )
15093
15170
  );
15094
15171
  if (currentMatches.length > 0) {
@@ -15117,6 +15194,7 @@ var Granular = class _Granular {
15117
15194
  subjectId: user.granularId,
15118
15195
  environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
15119
15196
  tagId: tag.tagId,
15197
+ versionId: targetVersionId,
15120
15198
  permissionProfileId: null
15121
15199
  }),
15122
15200
  requestedOntology: ontology,
@@ -15162,6 +15240,7 @@ var Granular = class _Granular {
15162
15240
  row.summaryUpdatedAt ?? row.summary_updated_at
15163
15241
  ) : null,
15164
15242
  subjectId: row.subjectId != null ? String(row.subjectId) : null,
15243
+ sessionScope: row.sessionScope != null || row.session_scope != null ? String(row.sessionScope ?? row.session_scope) : null,
15165
15244
  jobCount: typeof row.jobCount === "number" ? row.jobCount : void 0,
15166
15245
  toolCallCount: typeof row.toolCallCount === "number" ? row.toolCallCount : void 0
15167
15246
  };
@@ -15369,7 +15448,11 @@ var Granular = class _Granular {
15369
15448
  onUnexpectedClose: this.onUnexpectedClose,
15370
15449
  onReconnectError: this.onReconnectError
15371
15450
  });
15372
- await client.connect();
15451
+ await withTimeout(
15452
+ client.connect(),
15453
+ SESSION_CONNECT_TIMEOUT_MS,
15454
+ `session WebSocket connect for ${session.sessionId}`
15455
+ );
15373
15456
  const environmentSession = new EnvironmentSession(
15374
15457
  client,
15375
15458
  environment,
@@ -15540,12 +15623,24 @@ var Granular = class _Granular {
15540
15623
  host.heartbeatInFlight = false;
15541
15624
  }
15542
15625
  async synchronizeEffectHost(host) {
15543
- await host.wsClient.call("client.hello", {
15544
- clientId: host.clientId,
15545
- protocolVersion: "2.0"
15546
- });
15547
- this.startEffectHostHeartbeat(host);
15548
- await this.publishSandboxEffectCatalog(host);
15626
+ if (host.syncPromise) {
15627
+ return host.syncPromise;
15628
+ }
15629
+ host.syncPromise = (async () => {
15630
+ await host.wsClient.call("client.hello", {
15631
+ clientId: host.clientId,
15632
+ protocolVersion: "2.0"
15633
+ });
15634
+ await this.publishSandboxEffectCatalog(host);
15635
+ this.startEffectHostHeartbeat(host);
15636
+ })();
15637
+ try {
15638
+ await host.syncPromise;
15639
+ } finally {
15640
+ if (host.syncPromise) {
15641
+ host.syncPromise = null;
15642
+ }
15643
+ }
15549
15644
  }
15550
15645
  async ensureSandboxEffectHost(sandboxId) {
15551
15646
  const existing = this.sandboxEffectHosts.get(sandboxId);
@@ -15581,7 +15676,8 @@ var Granular = class _Granular {
15581
15676
  wsClient,
15582
15677
  heartbeatTimer: null,
15583
15678
  heartbeatInFlight: false,
15584
- recovering: false
15679
+ recovering: false,
15680
+ syncPromise: null
15585
15681
  };
15586
15682
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
15587
15683
  const request = params;
@@ -17688,7 +17784,8 @@ function buildGranularAgentSessionBlock(sessionContext) {
17688
17784
  runtimeId: sessionContext?.sandboxId || null,
17689
17785
  environmentId: sessionContext?.environmentId || null,
17690
17786
  userName: sessionContext?.userName || null,
17691
- domainRevision: sessionContext?.domainRevision || null
17787
+ domainRevision: sessionContext?.domainRevision || null,
17788
+ uiContext: sessionContext?.uiContext || null
17692
17789
  });
17693
17790
  }
17694
17791
  function buildGranularAgentHeapBlock(heapSummary) {
@@ -17935,7 +18032,7 @@ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
17935
18032
  const rightScope = `${right.className || "global"}:${right.static ? "static" : "instance"}`;
17936
18033
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
17937
18034
  });
17938
- const writeActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
18035
+ const availableActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
17939
18036
  const scope = tool.className ? `${tool.static ? "class" : "record"}:${tool.className}` : "global";
17940
18037
  return {
17941
18038
  name: tool.name,
@@ -17946,7 +18043,8 @@ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
17946
18043
  const capabilities = {
17947
18044
  executeCode: resolvedCapabilities.executeCode,
17948
18045
  readEntities: resolvedCapabilities.readEntities,
17949
- writeActions,
18046
+ availableActions,
18047
+ writeActions: availableActions,
17950
18048
  workflowHelpers: resolvedCapabilities.workflowHelpers,
17951
18049
  savedData: resolvedCapabilities.savedData,
17952
18050
  showRecords: resolvedCapabilities.showRecords
@@ -17960,7 +18058,7 @@ function buildGranularAgentActionIndex(tools) {
17960
18058
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
17961
18059
  });
17962
18060
  if (normalizedTools.length === 0) {
17963
- return "No domain write actions are available.";
18061
+ return "No executable actions are available.";
17964
18062
  }
17965
18063
  const globalTools = normalizedTools.filter((tool) => !tool.className);
17966
18064
  const staticTools = normalizedTools.filter(
@@ -18054,6 +18152,76 @@ function splitDomainDocumentation(domainDocumentation) {
18054
18152
  }
18055
18153
  return { types: normalized, docs: "" };
18056
18154
  }
18155
+ var DOMAIN_HELPER_FUNCTION_NAMES = /* @__PURE__ */ new Set([
18156
+ "agent_heap_objects",
18157
+ "agent_message",
18158
+ "agent_text_message"
18159
+ ]);
18160
+ function inferGlobalActionToolsFromDomainTypes(domainTypes) {
18161
+ const inferred = [];
18162
+ const seen = /* @__PURE__ */ new Set();
18163
+ const declarationPattern = /(?:export\s+)?declare\s+function\s+([A-Za-z_$][\w$]*)\s*\(/g;
18164
+ let match;
18165
+ while (match = declarationPattern.exec(domainTypes)) {
18166
+ const name = match[1];
18167
+ if (!name || DOMAIN_HELPER_FUNCTION_NAMES.has(name) || seen.has(name)) {
18168
+ continue;
18169
+ }
18170
+ seen.add(name);
18171
+ inferred.push({
18172
+ name,
18173
+ description: "Executable global action declared by the domain runtime."
18174
+ });
18175
+ }
18176
+ const actionLinePattern = /^-\s*([A-Za-z_$][\w$]*)\s+\(global\):\s*(.+)$/gm;
18177
+ while (match = actionLinePattern.exec(domainTypes)) {
18178
+ const name = match[1];
18179
+ if (!name || DOMAIN_HELPER_FUNCTION_NAMES.has(name) || seen.has(name)) {
18180
+ continue;
18181
+ }
18182
+ seen.add(name);
18183
+ inferred.push({
18184
+ name,
18185
+ description: match[2]?.trim() || "Executable global action declared by the domain runtime."
18186
+ });
18187
+ }
18188
+ const scopedActionLinePattern = /^-\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s+\((record|class)\):\s*(.+)$/gm;
18189
+ while (match = scopedActionLinePattern.exec(domainTypes)) {
18190
+ const className = match[1]?.toLowerCase();
18191
+ const name = match[2];
18192
+ const scope = match[3];
18193
+ if (!className || !name || DOMAIN_HELPER_FUNCTION_NAMES.has(name)) {
18194
+ continue;
18195
+ }
18196
+ const key = `${className}:${scope}:${name}`;
18197
+ if (seen.has(key)) {
18198
+ continue;
18199
+ }
18200
+ seen.add(key);
18201
+ inferred.push({
18202
+ name,
18203
+ className,
18204
+ static: scope === "class",
18205
+ description: match[4]?.trim() || "Executable action declared by the domain runtime."
18206
+ });
18207
+ }
18208
+ return inferred;
18209
+ }
18210
+ function resolvePromptTools(tools, domainTypes) {
18211
+ const byKey = /* @__PURE__ */ new Map();
18212
+ for (const tool of tools || []) {
18213
+ if (!tool?.name) continue;
18214
+ const key = `${tool.className || "global"}:${tool.static ? "static" : "instance"}:${tool.name}`;
18215
+ byKey.set(key, tool);
18216
+ }
18217
+ for (const tool of inferGlobalActionToolsFromDomainTypes(domainTypes)) {
18218
+ const key = `global:instance:${tool.name}`;
18219
+ if (!byKey.has(key)) {
18220
+ byKey.set(key, tool);
18221
+ }
18222
+ }
18223
+ return [...byKey.values()];
18224
+ }
18057
18225
  function buildGranularAgentCheckpointBlock(checkpoint) {
18058
18226
  if (!checkpoint) {
18059
18227
  return renderConstBlock("previousCodeResult", null);
@@ -18126,12 +18294,13 @@ function buildGranularAgentSystemPrompt(input) {
18126
18294
  const outputMode = input.outputMode || "agentMessages";
18127
18295
  const promptCapabilities = resolvePromptCapabilities(input.capabilities);
18128
18296
  const domainSections = splitDomainDocumentation(input.domainDocumentation);
18297
+ const promptTools = resolvePromptTools(input.tools, domainSections.types);
18129
18298
  const sessionBlock = buildGranularAgentSessionBlock(input.sessionContext);
18130
18299
  const toolBlock = buildGranularAgentToolBlock(
18131
- input.tools,
18300
+ promptTools,
18132
18301
  input.capabilities
18133
18302
  );
18134
- const actionIndex = buildGranularAgentActionIndex(input.tools);
18303
+ const actionIndex = buildGranularAgentActionIndex(promptTools);
18135
18304
  const domainBlock = buildGranularAgentDomainBlock(domainSections.types);
18136
18305
  const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
18137
18306
  const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
@@ -18159,7 +18328,7 @@ function buildGranularAgentSystemPrompt(input) {
18159
18328
  - 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.
18160
18329
  - 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.
18161
18330
  - 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"] })\`.
18162
- - \`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.
18331
+ - \`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(...)\`.
18163
18332
  - 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.
18164
18333
  - 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.
18165
18334
  - 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.
@@ -18209,10 +18378,11 @@ ${outputRules}` : `Code:
18209
18378
  - Use choice only for 2 to 5 short grounded options.
18210
18379
  - For record choices, set each option value to a stable scalar such as the record \`_graphPath\` or \`id\`, not a label-only value.
18211
18380
  - 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.
18212
- - 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.
18381
+ - 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.
18382
+ - 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.
18213
18383
  - 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.
18214
- - 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.
18215
- - 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.
18384
+ - 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.
18385
+ - 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.
18216
18386
  - 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.
18217
18387
  - Reuse existing task, decision, and closure ids from [State].
18218
18388
  - If a user request matches both a domain record/action and a workflow helper, prefer the domain capability.` : "";
@@ -18358,7 +18528,8 @@ Query policy:
18358
18528
  - 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.
18359
18529
  - 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.
18360
18530
  - 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.
18361
- - 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.
18531
+ - 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.
18532
+ - 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.
18362
18533
  - 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.
18363
18534
  - For scheduling actions, convert relative wording into concrete ISO timestamps before mutating records.
18364
18535
  - 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.
@@ -18420,6 +18591,7 @@ ${domainSections.docs}
18420
18591
 
18421
18592
  Actions:
18422
18593
  ${actionIndex}
18594
+ - 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.
18423
18595
  - 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(...)\`.
18424
18596
  - Actions listed under "Class-level" are class/static methods. Call them on the imported class, e.g. \`await Item.action_name(...)\`.
18425
18597
  - 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.