@granular-software/sdk 0.4.36 → 0.4.38

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
@@ -3951,11 +3951,22 @@ var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
3951
3951
  var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
3952
3952
  var MAX_TIMER_DELAY_MS = 2147483647;
3953
3953
  var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
3954
+ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
3955
+ var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
3954
3956
  function debugWs(...args) {
3955
3957
  if (DEBUG_WS) {
3956
3958
  console.log(...args);
3957
3959
  }
3958
3960
  }
3961
+ function rpcTimeoutMsForMethod(method) {
3962
+ switch (method) {
3963
+ case "domain.fetchPackagePart":
3964
+ case "domain.getSummary":
3965
+ return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
3966
+ default:
3967
+ return DEFAULT_RPC_TIMEOUT_MS;
3968
+ }
3969
+ }
3959
3970
  var WSClient = class {
3960
3971
  ws = null;
3961
3972
  url;
@@ -4380,13 +4391,14 @@ var WSClient = class {
4380
4391
  return new Promise((resolve, reject) => {
4381
4392
  this.messageQueue.push({ resolve, reject, id });
4382
4393
  this.ws.send(JSON.stringify(request));
4394
+ const timeoutMs = rpcTimeoutMsForMethod(method);
4383
4395
  setTimeout(() => {
4384
4396
  const pending = this.messageQueue.find((q) => q.id === id);
4385
4397
  if (pending) {
4386
4398
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4387
4399
  reject(new Error(`RPC timeout: ${method}`));
4388
4400
  }
4389
- }, 3e4);
4401
+ }, timeoutMs);
4390
4402
  });
4391
4403
  }
4392
4404
  async handleIncomingRpc(request) {
@@ -4500,10 +4512,48 @@ function normalizePromptText(value) {
4500
4512
  function extractPromptTokens(value) {
4501
4513
  return normalizePromptText(value).split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
4502
4514
  }
4515
+ function parseJsonPromptChoiceOption(option) {
4516
+ const trimmed = option.trim();
4517
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
4518
+ try {
4519
+ const parsed = JSON.parse(trimmed);
4520
+ return asRecord(parsed);
4521
+ } catch {
4522
+ return null;
4523
+ }
4524
+ }
4525
+ function normalizePromptChoiceOption(option) {
4526
+ if (typeof option === "string") {
4527
+ const record2 = parseJsonPromptChoiceOption(option);
4528
+ if (!record2) {
4529
+ return { value: option, label: option };
4530
+ }
4531
+ const value2 = typeof record2.value === "string" ? record2.value : typeof record2.id === "string" ? record2.id : typeof record2.label === "string" ? record2.label : JSON.stringify(record2);
4532
+ return {
4533
+ value: value2,
4534
+ label: typeof record2.label === "string" ? record2.label : value2,
4535
+ description: typeof record2.description === "string" ? record2.description : void 0
4536
+ };
4537
+ }
4538
+ const record = option;
4539
+ if (!record) {
4540
+ return { value: "", label: "" };
4541
+ }
4542
+ const nestedJson = (typeof record.value === "string" ? parseJsonPromptChoiceOption(record.value) : null) || (typeof record.label === "string" ? parseJsonPromptChoiceOption(record.label) : null);
4543
+ if (nestedJson) {
4544
+ return normalizePromptChoiceOption(nestedJson);
4545
+ }
4546
+ const value = typeof record.value === "string" ? record.value : typeof record.label === "string" ? record.label : JSON.stringify(record);
4547
+ return {
4548
+ value,
4549
+ label: typeof record.label === "string" ? record.label : value,
4550
+ description: typeof record.description === "string" ? record.description : void 0
4551
+ };
4552
+ }
4503
4553
  function scorePromptChoiceMatch(answer, answerTokens, option) {
4504
- const value = typeof option === "string" ? option : typeof option?.value === "string" ? option.value : "";
4505
- const label = typeof option === "string" ? option : typeof option?.label === "string" ? option.label : "";
4506
- const description = typeof option === "string" ? "" : typeof option?.description === "string" ? option.description : "";
4554
+ const choice = normalizePromptChoiceOption(option);
4555
+ const { value, label } = choice;
4556
+ const description = choice.description || "";
4507
4557
  const haystack = normalizePromptText([value, label, description].filter(Boolean).join(" "));
4508
4558
  if (!haystack) return { score: 0, resolvedValue: value || label || null };
4509
4559
  let score = 0;
@@ -4539,7 +4589,9 @@ function normalizePrompt(rawValue) {
4539
4589
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
4540
4590
  title: typeof source.title === "string" ? source.title : "Input required",
4541
4591
  message: typeof source.message === "string" ? source.message : "",
4542
- options: Array.isArray(source.options) ? source.options : void 0,
4592
+ options: Array.isArray(source.options) ? source.options.map(
4593
+ (option) => typeof option === "string" || asRecord(option) ? normalizePromptChoiceOption(option) : option
4594
+ ) : void 0,
4543
4595
  defaultValue: source.defaultValue,
4544
4596
  placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
4545
4597
  allowEmpty: typeof source.allowEmpty === "boolean" ? source.allowEmpty : void 0,
@@ -4567,9 +4619,26 @@ function resolvePromptAnswer(prompt, answer) {
4567
4619
  }
4568
4620
 
4569
4621
  // src/session.ts
4622
+ var PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS = 5e3;
4623
+ function withPromptTranscriptTimeout(promise) {
4624
+ let timeout = null;
4625
+ return Promise.race([
4626
+ promise,
4627
+ new Promise((_, reject) => {
4628
+ timeout = setTimeout(() => {
4629
+ reject(new Error("Timed out appending prompt answer transcript."));
4630
+ }, PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS);
4631
+ })
4632
+ ]).finally(() => {
4633
+ if (timeout) {
4634
+ clearTimeout(timeout);
4635
+ }
4636
+ });
4637
+ }
4570
4638
  var Session = class {
4571
4639
  client;
4572
4640
  clientId;
4641
+ initialQuota;
4573
4642
  jobsMap = /* @__PURE__ */ new Map();
4574
4643
  pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
4575
4644
  eventListeners = /* @__PURE__ */ new Map();
@@ -4583,9 +4652,12 @@ var Session = class {
4583
4652
  lastKnownTools = /* @__PURE__ */ new Map();
4584
4653
  /** Last seen live prompts, keyed by prompt id, for answer normalization */
4585
4654
  promptCache = /* @__PURE__ */ new Map();
4586
- constructor(client, clientId) {
4655
+ /** Prompt ids locally answered before the document sync catches up. */
4656
+ hiddenPromptIds = /* @__PURE__ */ new Set();
4657
+ constructor(client, clientId, options = {}) {
4587
4658
  this.client = client;
4588
4659
  this.clientId = clientId || `client_${Date.now()}`;
4660
+ this.initialQuota = options.initialQuota || null;
4589
4661
  this.setupEventHandlers();
4590
4662
  this.setupToolInvokeHandler();
4591
4663
  }
@@ -4634,6 +4706,16 @@ var Session = class {
4634
4706
  get document() {
4635
4707
  return this.client.doc;
4636
4708
  }
4709
+ get quota() {
4710
+ return this.getQuota();
4711
+ }
4712
+ getQuota() {
4713
+ const quota = this.client.doc.billing?.quota;
4714
+ if (quota && typeof quota === "object") {
4715
+ return quota;
4716
+ }
4717
+ return this.initialQuota;
4718
+ }
4637
4719
  get sessionId() {
4638
4720
  return this.client.currentSessionId;
4639
4721
  }
@@ -4718,8 +4800,9 @@ var Session = class {
4718
4800
  * `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
4719
4801
  * execute locally and return the result to the sandbox.
4720
4802
  */
4721
- async submitJob(code, domainRevision) {
4722
- let revision = domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4803
+ async submitJob(code, domainRevisionOrOptions) {
4804
+ const options = typeof domainRevisionOrOptions === "string" ? { domainRevision: domainRevisionOrOptions } : domainRevisionOrOptions || {};
4805
+ let revision = options.domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4723
4806
  if (!revision) {
4724
4807
  try {
4725
4808
  const summary = await this.getDomain();
@@ -4734,7 +4817,9 @@ var Session = class {
4734
4817
  }
4735
4818
  const result = await this.client.call("job.submit", {
4736
4819
  domainRevision: revision,
4737
- code
4820
+ code,
4821
+ metadata: options.metadata,
4822
+ agent: options.agent
4738
4823
  });
4739
4824
  if (!result.jobId) {
4740
4825
  throw new Error("Failed to submit job: no jobId returned");
@@ -4775,25 +4860,39 @@ var Session = class {
4775
4860
  const prompt = this.promptCache.get(promptId);
4776
4861
  const resolvedAnswer = resolvePromptAnswer(prompt, answer);
4777
4862
  this.promptCache.delete(promptId);
4778
- await this.client.call("prompt.answer", {
4779
- promptId,
4780
- answer: resolvedAnswer,
4781
- value: resolvedAnswer
4782
- });
4863
+ this.hiddenPromptIds.add(promptId);
4864
+ try {
4865
+ await this.client.call("prompt.answer", {
4866
+ promptId,
4867
+ answer: resolvedAnswer,
4868
+ value: resolvedAnswer
4869
+ });
4870
+ } catch (error) {
4871
+ this.hiddenPromptIds.delete(promptId);
4872
+ if (prompt) {
4873
+ this.promptCache.set(promptId, prompt);
4874
+ }
4875
+ throw error;
4876
+ }
4783
4877
  try {
4784
4878
  const content = this.stringifyConversationValue(resolvedAnswer);
4785
4879
  if (content.trim()) {
4786
- await this.appendConversationMessage({
4787
- role: "user",
4788
- content,
4789
- promptId
4790
- });
4880
+ await withPromptTranscriptTimeout(
4881
+ this.appendConversationMessage({
4882
+ role: "user",
4883
+ content,
4884
+ promptId
4885
+ })
4886
+ );
4791
4887
  }
4792
4888
  } catch {
4793
4889
  }
4794
4890
  }
4795
4891
  async appendConversationMessage(input) {
4796
- return this.client.call("conversation.append", input);
4892
+ return this.client.call(
4893
+ "conversation.append",
4894
+ input
4895
+ );
4797
4896
  }
4798
4897
  /**
4799
4898
  * Get the current list of available effects.
@@ -4802,9 +4901,53 @@ var Session = class {
4802
4901
  getEffects() {
4803
4902
  const doc = this.client.doc;
4804
4903
  const toolMap = /* @__PURE__ */ new Map();
4805
- const domainPkg = doc.domain?.packages?.domain;
4806
- if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
4807
- for (const tool of domainPkg.tools) {
4904
+ const domainPackages = doc.domain?.packages;
4905
+ const packageCandidates = domainPackages && typeof domainPackages === "object" ? [
4906
+ domainPackages.domain,
4907
+ domainPackages["@sandbox/domain"],
4908
+ ...Object.values(domainPackages)
4909
+ ].filter(Boolean) : [];
4910
+ for (const domainPkg of packageCandidates) {
4911
+ if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
4912
+ for (const tool of domainPkg.tools) {
4913
+ if (!tool?.name || toolMap.has(tool.name)) continue;
4914
+ toolMap.set(tool.name, {
4915
+ name: tool.name,
4916
+ description: tool.description,
4917
+ inputSchema: tool.inputSchema,
4918
+ outputSchema: tool.outputSchema,
4919
+ className: tool.className || void 0,
4920
+ static: tool.static || false,
4921
+ ready: false,
4922
+ publishedAt: void 0
4923
+ });
4924
+ }
4925
+ }
4926
+ if (!domainPkg?.classes || typeof domainPkg.classes !== "object") {
4927
+ continue;
4928
+ }
4929
+ for (const [className, classDef] of Object.entries(
4930
+ domainPkg.classes
4931
+ )) {
4932
+ const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
4933
+ for (const method of methods) {
4934
+ if (!method?.name || toolMap.has(method.name)) continue;
4935
+ toolMap.set(method.name, {
4936
+ name: method.name,
4937
+ description: method.description,
4938
+ inputSchema: method.inputSchema,
4939
+ outputSchema: method.outputSchema,
4940
+ className: method.className || classDef?.name || className,
4941
+ static: method.static || false,
4942
+ ready: false,
4943
+ publishedAt: void 0
4944
+ });
4945
+ }
4946
+ }
4947
+ }
4948
+ const legacyDomainPkg = doc.domain?.packages?.domain;
4949
+ if (legacyDomainPkg?.tools && Array.isArray(legacyDomainPkg.tools)) {
4950
+ for (const tool of legacyDomainPkg.tools) {
4808
4951
  if (!tool?.name) continue;
4809
4952
  toolMap.set(tool.name, {
4810
4953
  name: tool.name,
@@ -4818,6 +4961,27 @@ var Session = class {
4818
4961
  });
4819
4962
  }
4820
4963
  }
4964
+ if (legacyDomainPkg?.classes && typeof legacyDomainPkg.classes === "object") {
4965
+ for (const [className, classDef] of Object.entries(
4966
+ legacyDomainPkg.classes
4967
+ )) {
4968
+ const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
4969
+ for (const method of methods) {
4970
+ if (!method?.name || toolMap.has(method.name)) continue;
4971
+ toolMap.set(method.name, {
4972
+ name: method.name,
4973
+ description: method.description,
4974
+ inputSchema: method.inputSchema,
4975
+ outputSchema: method.outputSchema,
4976
+ className: method.className || classDef?.name || className,
4977
+ static: method.static || false,
4978
+ ready: false,
4979
+ publishedAt: void 0
4980
+ });
4981
+ }
4982
+ }
4983
+ }
4984
+ const hasPolicyFilteredDomainTools = toolMap.size > 0;
4821
4985
  const catalogs = doc.catalog?.rawToolCatalogs || {};
4822
4986
  for (const [clientId, catalog] of Object.entries(catalogs)) {
4823
4987
  const cat = catalog;
@@ -4825,6 +4989,7 @@ var Session = class {
4825
4989
  for (const tool of cat.tools) {
4826
4990
  if (!tool?.name) continue;
4827
4991
  const existing = toolMap.get(tool.name);
4992
+ if (hasPolicyFilteredDomainTools && !existing) continue;
4828
4993
  if (existing?.publishedAt && cat.publishedAt && existing.publishedAt > cat.publishedAt)
4829
4994
  continue;
4830
4995
  const isLocal = clientId === this.clientId;
@@ -4844,6 +5009,24 @@ var Session = class {
4844
5009
  }
4845
5010
  return Array.from(toolMap.values());
4846
5011
  }
5012
+ /**
5013
+ * Return the currently open prompt payloads known to this session.
5014
+ *
5015
+ * These come from live `prompt` / `prompt.request` websocket events and
5016
+ * preserve the exact shape used by `answerPrompt(...)`.
5017
+ */
5018
+ getPrompts() {
5019
+ return Array.from(this.promptCache.values()).map((prompt) => ({
5020
+ ...prompt,
5021
+ options: Array.isArray(prompt.options) ? prompt.options.map(
5022
+ (option) => typeof option === "string" ? option : { ...option }
5023
+ ) : void 0,
5024
+ metadata: prompt.metadata ? { ...prompt.metadata } : void 0
5025
+ }));
5026
+ }
5027
+ getHiddenPromptIds() {
5028
+ return Array.from(this.hiddenPromptIds);
5029
+ }
4847
5030
  /**
4848
5031
  * Backwards-compatible alias for `getEffects()`.
4849
5032
  */
@@ -4943,11 +5126,7 @@ var Session = class {
4943
5126
  if (!normalizedDocs) {
4944
5127
  return normalizedTypes;
4945
5128
  }
4946
- return [
4947
- normalizedTypes,
4948
- "Generated usage notes from ./sandbox-tools docs:",
4949
- normalizedDocs
4950
- ].join("\n\n");
5129
+ return [normalizedTypes, "[Docs]", normalizedDocs].join("\n\n");
4951
5130
  }
4952
5131
  if (normalizedDocs) {
4953
5132
  return normalizedDocs;
@@ -5169,6 +5348,7 @@ import { ${allImports} } from "./sandbox-tools";
5169
5348
  const emitPrompt = (payload) => {
5170
5349
  const prompt = normalizePrompt(payload);
5171
5350
  if (!prompt) return;
5351
+ this.hiddenPromptIds.delete(prompt.id);
5172
5352
  this.promptCache.set(prompt.id, prompt);
5173
5353
  this.emit("prompt", prompt);
5174
5354
  };
@@ -5351,6 +5531,7 @@ var JobImplementation = class {
5351
5531
  eventListeners = /* @__PURE__ */ new Map();
5352
5532
  bufferedAgentMessages = [];
5353
5533
  bufferedAgentMessageIds = /* @__PURE__ */ new Set();
5534
+ resultSettled = false;
5354
5535
  metadata;
5355
5536
  constructor(id, client, initialState) {
5356
5537
  this.id = id;
@@ -5375,7 +5556,9 @@ var JobImplementation = class {
5375
5556
  if (execData.error) {
5376
5557
  this.finalize("failed", void 0, execData.error);
5377
5558
  } else {
5378
- this.finalize("succeeded", execData.result);
5559
+ this.finalize("succeeded", execData.result, void 0, {
5560
+ hasResult: Object.prototype.hasOwnProperty.call(execData, "result")
5561
+ });
5379
5562
  }
5380
5563
  this.emit("status", this.status);
5381
5564
  }
@@ -5411,9 +5594,6 @@ var JobImplementation = class {
5411
5594
  if (normalizedStatus === "failed" || normalizedStatus === "timeout" || normalizedStatus === "canceled") {
5412
5595
  this.finalize(normalizedStatus);
5413
5596
  }
5414
- if (normalizedStatus === "succeeded") {
5415
- this.finalize("succeeded");
5416
- }
5417
5597
  this.emit("status", normalizedStatus);
5418
5598
  });
5419
5599
  this.client.on(`job.${id}.stdout`, (line) => {
@@ -5433,7 +5613,7 @@ var JobImplementation = class {
5433
5613
  this.emit("stderr", line);
5434
5614
  });
5435
5615
  this.client.on(`job.${id}.result`, (result) => {
5436
- this.finalize("succeeded", result);
5616
+ this.finalize("succeeded", result, void 0, { hasResult: true });
5437
5617
  });
5438
5618
  this.client.on(`job.${id}.error`, (error) => {
5439
5619
  this.finalize("failed", void 0, error);
@@ -5454,7 +5634,9 @@ var JobImplementation = class {
5454
5634
  this.client.on("job.completed", (data) => {
5455
5635
  const jobData = data;
5456
5636
  if (jobData.jobId === id) {
5457
- this.finalize("succeeded", jobData.result);
5637
+ this.finalize("succeeded", jobData.result, void 0, {
5638
+ hasResult: true
5639
+ });
5458
5640
  this.emit("status", this.status);
5459
5641
  }
5460
5642
  });
@@ -5579,7 +5761,7 @@ var JobImplementation = class {
5579
5761
  this.metadata.status = "running";
5580
5762
  }
5581
5763
  }
5582
- finalize(status, result, error) {
5764
+ finalize(status, result, error, options = {}) {
5583
5765
  if (!this.metadata.startedAt) {
5584
5766
  this.metadata.startedAt = Date.now();
5585
5767
  }
@@ -5587,14 +5769,18 @@ var JobImplementation = class {
5587
5769
  this.metadata.status = status;
5588
5770
  this.metadata.completedAt = this.metadata.completedAt || Date.now();
5589
5771
  this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
5590
- if (result !== void 0) {
5772
+ if (!this.resultSettled && (options.hasResult || result !== void 0)) {
5591
5773
  this.metadata.result = sanitizeFeedbackValue(result);
5774
+ this.resultSettled = true;
5592
5775
  this._resolveResult(result);
5593
5776
  }
5594
- if (error !== void 0) {
5595
- const message = error instanceof Error ? error.message : String(error);
5777
+ if (!this.resultSettled && (error !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
5778
+ const fallbackError = new Error(`Job ${this.id} ${status}.`);
5779
+ const cause = error ?? fallbackError;
5780
+ const message = cause instanceof Error ? cause.message : String(cause);
5596
5781
  this.metadata.error = truncateFeedbackString(message);
5597
- this._rejectResult(error);
5782
+ this.resultSettled = true;
5783
+ this._rejectResult(cause);
5598
5784
  }
5599
5785
  }
5600
5786
  upsertToolCall(next) {
@@ -5677,6 +5863,17 @@ function humanTextFromStdout(stdout) {
5677
5863
  }
5678
5864
  return null;
5679
5865
  }
5866
+ function responseTextFromAgentMessages(agentMessages) {
5867
+ for (const message of [...agentMessages].reverse()) {
5868
+ const record = asRecord2(message);
5869
+ if (!record) continue;
5870
+ for (const key of RESPONSE_KEYS) {
5871
+ const normalized = normalizeText(record[key]);
5872
+ if (normalized) return normalized;
5873
+ }
5874
+ }
5875
+ return null;
5876
+ }
5680
5877
  function pushString(target, value) {
5681
5878
  if (typeof value === "string" && value.trim()) {
5682
5879
  target.add(value.trim());
@@ -5702,6 +5899,41 @@ function collectReferencesFromRecord(record, refs) {
5702
5899
  for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
5703
5900
  pushStringArray(refs.variableNames, record[key]);
5704
5901
  }
5902
+ function stringValue(record, keys) {
5903
+ for (const key of keys) {
5904
+ const value = record[key];
5905
+ if (typeof value === "string" && value.trim()) {
5906
+ return value.trim();
5907
+ }
5908
+ }
5909
+ return null;
5910
+ }
5911
+ function findEntryPathForRecord(record, heap) {
5912
+ const directPath = stringValue(record, ["entryPath", "path"]);
5913
+ if (directPath && heap.entriesByPath?.[directPath]) {
5914
+ return directPath;
5915
+ }
5916
+ const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
5917
+ if (!id) {
5918
+ return null;
5919
+ }
5920
+ const className = stringValue(record, [
5921
+ "className",
5922
+ "_className",
5923
+ "__className",
5924
+ "prototype",
5925
+ "type"
5926
+ ]);
5927
+ const entries = Object.values(heap.entriesByPath || {});
5928
+ const exact = entries.find(
5929
+ (entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
5930
+ );
5931
+ if (exact?.path) {
5932
+ return exact.path;
5933
+ }
5934
+ const idOnlyMatches = entries.filter((entry) => entry.id === id);
5935
+ return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
5936
+ }
5705
5937
  function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
5706
5938
  if (value === null || value === void 0 || depth > 4 || seen.has(value))
5707
5939
  return;
@@ -5722,6 +5954,8 @@ function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__
5722
5954
  const record = asRecord2(value);
5723
5955
  if (!record) return;
5724
5956
  seen.add(value);
5957
+ const entryPath = findEntryPathForRecord(record, heap);
5958
+ if (entryPath) refs.entryPaths.add(entryPath);
5725
5959
  collectReferencesFromRecord(record, refs);
5726
5960
  for (const key of UI_CONTAINER_KEYS) {
5727
5961
  const nested = asRecord2(record[key]);
@@ -5830,6 +6064,7 @@ function resolveJobPresentation({
5830
6064
  jobId,
5831
6065
  result,
5832
6066
  stdout = [],
6067
+ agentMessages = [],
5833
6068
  sessionHeap,
5834
6069
  allowExplicitArtifacts = true
5835
6070
  }) {
@@ -5862,7 +6097,7 @@ function resolveJobPresentation({
5862
6097
  const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
5863
6098
  const lists = hasExplicitArtifacts ? explicitLists : jobLists;
5864
6099
  const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
5865
- const responseText = extractResponseText(result, stdout) || fallbackResponseText(entries, lists);
6100
+ const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
5866
6101
  return {
5867
6102
  responseText,
5868
6103
  entries,
@@ -10338,6 +10573,67 @@ external_exports.object({
10338
10573
  transitions: external_exports.array(StateMachineTransitionSchema),
10339
10574
  finalStates: external_exports.array(external_exports.string()).optional()
10340
10575
  }).strict();
10576
+ var POLICY_OPERATORS = [
10577
+ "eq",
10578
+ "neq",
10579
+ "gt",
10580
+ "gte",
10581
+ "lt",
10582
+ "lte",
10583
+ "contains",
10584
+ "not_contains",
10585
+ "starts_with",
10586
+ "ends_with",
10587
+ "exists"
10588
+ ];
10589
+ var PolicyPredicateSchema = external_exports.object({
10590
+ path: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional(),
10591
+ field: external_exports.string().optional(),
10592
+ input: external_exports.string().optional(),
10593
+ operator: external_exports.enum([...POLICY_OPERATORS]),
10594
+ stringValue: external_exports.string().optional(),
10595
+ numberValue: external_exports.number().optional(),
10596
+ booleanValue: external_exports.boolean().optional(),
10597
+ value: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]).optional()
10598
+ }).strict();
10599
+ var PolicyStateMachinePredicateSchema = external_exports.object({
10600
+ machine: external_exports.string().min(1),
10601
+ operator: external_exports.enum([...POLICY_OPERATORS]),
10602
+ state: external_exports.string().optional(),
10603
+ stringValue: external_exports.string().optional()
10604
+ }).strict();
10605
+ var PolicyConditionSchema = external_exports.lazy(
10606
+ () => external_exports.object({
10607
+ all: external_exports.array(PolicyConditionSchema).optional(),
10608
+ any: external_exports.array(PolicyConditionSchema).optional(),
10609
+ not: PolicyConditionSchema.optional(),
10610
+ input: PolicyPredicateSchema.optional(),
10611
+ object: PolicyPredicateSchema.optional(),
10612
+ stateMachine: PolicyStateMachinePredicateSchema.optional()
10613
+ }).strict().refine(
10614
+ (data) => [
10615
+ data.all,
10616
+ data.any,
10617
+ data.not,
10618
+ data.input,
10619
+ data.object,
10620
+ data.stateMachine
10621
+ ].filter((value) => value !== void 0).length === 1,
10622
+ {
10623
+ message: "Policy condition must define exactly one of all, any, not, input, object, or stateMachine"
10624
+ }
10625
+ )
10626
+ );
10627
+ var PolicyRuleSchema = external_exports.object({
10628
+ id: external_exports.string().min(1).optional(),
10629
+ reason: external_exports.string().optional(),
10630
+ when: PolicyConditionSchema
10631
+ }).strict();
10632
+ var PoliciesSchema = external_exports.object({
10633
+ allowWhen: external_exports.array(PolicyRuleSchema).optional(),
10634
+ confirmWhen: external_exports.array(PolicyRuleSchema).optional(),
10635
+ denyWhen: external_exports.array(PolicyRuleSchema).optional()
10636
+ }).strict();
10341
10637
  external_exports.object({
10342
10638
  postCondition: external_exports.union([
10343
10639
  external_exports.string(),
@@ -10367,7 +10663,8 @@ external_exports.object({
10367
10663
  reason: external_exports.string().optional(),
10368
10664
  mode: external_exports.string().optional()
10369
10665
  }).strict()
10370
- ]).optional()
10666
+ ]).optional(),
10667
+ policies: PoliciesSchema.optional()
10371
10668
  }).strict();
10372
10669
 
10373
10670
  // ../metamodel-core/src/index.ts
@@ -11041,6 +11338,110 @@ async function invokeRegisteredEffect(effectMap, request) {
11041
11338
  return resolved.handler(request.input, context);
11042
11339
  }
11043
11340
 
11341
+ // src/spend.ts
11342
+ function toGranularHttpBase(apiUrl) {
11343
+ const url = new URL(apiUrl);
11344
+ if (url.protocol === "ws:") {
11345
+ url.protocol = "http:";
11346
+ } else if (url.protocol === "wss:") {
11347
+ url.protocol = "https:";
11348
+ }
11349
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
11350
+ if (!url.pathname || url.pathname === "/") {
11351
+ url.pathname = "/granular";
11352
+ }
11353
+ url.search = "";
11354
+ url.hash = "";
11355
+ return url.toString().replace(/\/$/, "");
11356
+ }
11357
+ function cleanIdPart(value) {
11358
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
11359
+ }
11360
+ function buildOpenAISpendEventId(usage, context = {}) {
11361
+ const requestId = usage.requestId?.trim();
11362
+ if (!requestId) return void 0;
11363
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
11364
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
11365
+ }
11366
+ function pricingEffectiveAtSeconds(value) {
11367
+ if (!value) return null;
11368
+ const parsed = Date.parse(value);
11369
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
11370
+ }
11371
+ function compactContext(context) {
11372
+ return Object.fromEntries(
11373
+ Object.entries(context).filter(
11374
+ ([, value]) => value != null && value !== ""
11375
+ )
11376
+ );
11377
+ }
11378
+ function omitTenantId(context) {
11379
+ const scopedContext = { ...context };
11380
+ delete scopedContext.tenantId;
11381
+ return scopedContext;
11382
+ }
11383
+ async function recordOpenAIUsageSpend(options) {
11384
+ const usageContext = compactContext({
11385
+ ...options.usage.usageContext || {},
11386
+ ...options.context || {}
11387
+ });
11388
+ const context = omitTenantId(usageContext);
11389
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
11390
+ const metadata = {
11391
+ ...options.metadata || {},
11392
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
11393
+ usageContext: context
11394
+ };
11395
+ const response = await fetch(
11396
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
11397
+ {
11398
+ method: "POST",
11399
+ cache: "no-store",
11400
+ headers: {
11401
+ Authorization: `Bearer ${options.token}`,
11402
+ "Content-Type": "application/json"
11403
+ },
11404
+ body: JSON.stringify({
11405
+ ...spendEventId ? { spendEventId } : {},
11406
+ sandboxId: context.sandboxId || null,
11407
+ environmentId: context.environmentId || null,
11408
+ sessionId: context.sessionId || null,
11409
+ subjectId: context.subjectId || null,
11410
+ permissionProfileId: context.permissionProfileId || null,
11411
+ source: "openai",
11412
+ lineItemType: "llm_tokens",
11413
+ provider: options.usage.provider,
11414
+ model: options.usage.model,
11415
+ operation: options.usage.operation || "chat.completions",
11416
+ requestId: options.usage.requestId || null,
11417
+ inputTokens: options.usage.inputTokens,
11418
+ outputTokens: options.usage.outputTokens,
11419
+ cachedInputTokens: options.usage.cachedInputTokens,
11420
+ reasoningTokens: options.usage.reasoningTokens,
11421
+ quantity: options.usage.totalTokens,
11422
+ quantityUnit: "tokens",
11423
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
11424
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
11425
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
11426
+ amountMicros: options.usage.amountMicros,
11427
+ currency: options.usage.currency,
11428
+ pricingSource: options.usage.pricingSource,
11429
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
11430
+ options.usage.pricingEffectiveAt
11431
+ ),
11432
+ estimated: false,
11433
+ metadata
11434
+ })
11435
+ }
11436
+ );
11437
+ if (!response.ok) {
11438
+ throw new Error(
11439
+ `Granular spend event failed (${response.status}): ${await response.text()}`
11440
+ );
11441
+ }
11442
+ return response.json();
11443
+ }
11444
+
11044
11445
  // ../metamodel-enum/src/index.ts
11045
11446
  function renderInlineStringUnion(values) {
11046
11447
  return values.map((value) => JSON.stringify(value)).join(" | ");
@@ -11399,6 +11800,148 @@ var noteMetamodelPackage = defineMetamodelPackage({
11399
11800
  }
11400
11801
  });
11401
11802
 
11803
+ // ../policy-engine/src/index.ts
11804
+ function isRecord(value) {
11805
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11806
+ }
11807
+ function normalizePath(value) {
11808
+ if (Array.isArray(value)) {
11809
+ return value.map((part) => String(part)).filter(Boolean);
11810
+ }
11811
+ if (typeof value === "string") {
11812
+ return value.includes(".") ? value.split(".").filter(Boolean) : [value];
11813
+ }
11814
+ return [];
11815
+ }
11816
+ function firstDefinedValue(spec) {
11817
+ if ("value" in spec) return spec.value;
11818
+ if ("stringValue" in spec) return spec.stringValue;
11819
+ if ("numberValue" in spec) return spec.numberValue;
11820
+ if ("booleanValue" in spec) return spec.booleanValue;
11821
+ if ("state" in spec) return spec.state;
11822
+ return void 0;
11823
+ }
11824
+ function normalizeCondition(input) {
11825
+ if (input === void 0 || input === null) return { kind: "always" };
11826
+ if (!isRecord(input)) {
11827
+ throw new Error("Policy condition must be an object");
11828
+ }
11829
+ if (Array.isArray(input.all)) {
11830
+ return {
11831
+ kind: "all",
11832
+ conditions: input.all.map((item) => normalizeCondition(item))
11833
+ };
11834
+ }
11835
+ if (Array.isArray(input.any)) {
11836
+ return {
11837
+ kind: "any",
11838
+ conditions: input.any.map((item) => normalizeCondition(item))
11839
+ };
11840
+ }
11841
+ if (input.not !== void 0) {
11842
+ return { kind: "not", condition: normalizeCondition(input.not) };
11843
+ }
11844
+ for (const source of ["input", "object", "stateMachine"]) {
11845
+ const raw = input[source];
11846
+ if (!isRecord(raw)) continue;
11847
+ const operator = raw.operator;
11848
+ if (operator !== "eq" && operator !== "neq" && operator !== "gt" && operator !== "gte" && operator !== "lt" && operator !== "lte" && operator !== "contains" && operator !== "not_contains" && operator !== "starts_with" && operator !== "ends_with" && operator !== "exists") {
11849
+ throw new Error(`Unsupported policy operator: ${String(operator)}`);
11850
+ }
11851
+ if (source === "stateMachine") {
11852
+ const machine = typeof raw.machine === "string" ? raw.machine : "";
11853
+ if (!machine) throw new Error("stateMachine condition requires machine");
11854
+ return {
11855
+ kind: "predicate",
11856
+ source,
11857
+ path: [machine],
11858
+ machine,
11859
+ operator,
11860
+ value: firstDefinedValue(raw)
11861
+ };
11862
+ }
11863
+ const path = normalizePath(raw.path ?? raw.field ?? raw.input);
11864
+ if (path.length === 0) {
11865
+ throw new Error(`${source} condition requires a path`);
11866
+ }
11867
+ return {
11868
+ kind: "predicate",
11869
+ source,
11870
+ path,
11871
+ operator,
11872
+ value: firstDefinedValue(raw)
11873
+ };
11874
+ }
11875
+ throw new Error(
11876
+ "Policy condition must contain all, any, not, input, object, or stateMachine"
11877
+ );
11878
+ }
11879
+ function summarizeCondition(condition) {
11880
+ switch (condition.kind) {
11881
+ case "always":
11882
+ return "always";
11883
+ case "all":
11884
+ return condition.conditions.map(summarizeCondition).join(" and ");
11885
+ case "any":
11886
+ return condition.conditions.map(summarizeCondition).join(" or ");
11887
+ case "not":
11888
+ return `not (${summarizeCondition(condition.condition)})`;
11889
+ case "predicate": {
11890
+ const path = condition.source === "stateMachine" ? `stateMachine.${condition.machine || condition.path.join(".")}` : `${condition.source}.${condition.path.join(".")}`;
11891
+ if (condition.operator === "exists") return `${path} exists`;
11892
+ return `${path} ${condition.operator} ${String(condition.value)}`;
11893
+ }
11894
+ }
11895
+ }
11896
+
11897
+ // ../metamodel-policy/src/index.ts
11898
+ function escapeGraphqlString(value) {
11899
+ return JSON.stringify(value);
11900
+ }
11901
+ function buildPolicyMutations(effectKey, spec) {
11902
+ const policies = spec.policies;
11903
+ if (!policies) return [];
11904
+ const mutations = [];
11905
+ const addRules = (key, outcome) => {
11906
+ const rules = policies[key] || [];
11907
+ rules.forEach((rule, index) => {
11908
+ const condition = normalizeCondition(rule.when);
11909
+ const summary = rule.reason || summarizeCondition(condition);
11910
+ const id = rule.id || `${effectKey}:${outcome}:${index + 1}`;
11911
+ mutations.push({
11912
+ label: `set policy ${outcome} on ${effectKey}`,
11913
+ query: `mutation { set_policy_rule(effect_key: ${escapeGraphqlString(effectKey)}, policy_id: ${escapeGraphqlString(id)}, outcome: ${escapeGraphqlString(outcome)}, reason: ${escapeGraphqlString(summary)}, condition_json: ${escapeGraphqlString(JSON.stringify(condition))}) }`
11914
+ });
11915
+ });
11916
+ };
11917
+ addRules("allowWhen", "allow");
11918
+ addRules("confirmWhen", "confirm");
11919
+ addRules("denyWhen", "deny");
11920
+ return mutations;
11921
+ }
11922
+ var policyMetamodelPackage = defineMetamodelPackage({
11923
+ id: "policy",
11924
+ manifest: {
11925
+ buildEffectMutations: buildPolicyMutations
11926
+ },
11927
+ summary: {
11928
+ selections: {
11929
+ methodFields: ["policies"]
11930
+ },
11931
+ readMethodSummary(rawMethod) {
11932
+ return rawMethod.policies ? { metamodels: { policies: rawMethod.policies } } : {};
11933
+ }
11934
+ },
11935
+ docs: {
11936
+ effectRows: [
11937
+ {
11938
+ key: "policies",
11939
+ description: "Universal effect policies with allowWhen, confirmWhen, and denyWhen structural conditions."
11940
+ }
11941
+ ]
11942
+ }
11943
+ });
11944
+
11402
11945
  // ../metamodel-required/src/index.ts
11403
11946
  function buildRequiredFieldMutations(fieldPath, required) {
11404
11947
  if (!required) return [];
@@ -12173,7 +12716,8 @@ var DEFAULT_METAMODEL_PACKAGES = [
12173
12716
  searchableMetamodelPackage,
12174
12717
  validationRuleMetamodelPackage,
12175
12718
  stateMachineMetamodelPackage,
12176
- effectBehaviorsMetamodelPackage
12719
+ effectBehaviorsMetamodelPackage,
12720
+ policyMetamodelPackage
12177
12721
  ];
12178
12722
  createMetamodelRegistry(
12179
12723
  DEFAULT_METAMODEL_PACKAGES
@@ -12240,6 +12784,12 @@ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
12240
12784
  var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
12241
12785
  var LOCAL_CONTROL_REQUEST_RETRY_COUNT = 4;
12242
12786
  var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
12787
+ var SESSION_DATA_REQUEST_RETRY_COUNT = 4;
12788
+ var SESSION_DATA_REQUEST_RETRY_DELAY_MS = 500;
12789
+ var EFFECT_HOST_CONNECT_TIMEOUT_MS = 15e3;
12790
+ var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 3e4;
12791
+ var EFFECT_CATALOG_SYNC_RETRY_COUNT = 3;
12792
+ var EFFECT_CATALOG_SYNC_RETRY_DELAY_MS = 1e3;
12243
12793
  function planRecordObjectsChunks(records, batchSize) {
12244
12794
  const total = records.length;
12245
12795
  const size = Math.max(1, Math.min(batchSize, total));
@@ -12254,6 +12804,19 @@ function planRecordObjectsChunks(records, batchSize) {
12254
12804
  function sleep(ms) {
12255
12805
  return new Promise((resolve) => setTimeout(resolve, ms));
12256
12806
  }
12807
+ function withTimeout(promise, timeoutMs, label) {
12808
+ let timer = null;
12809
+ const timeout = new Promise((_, reject) => {
12810
+ timer = setTimeout(() => {
12811
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
12812
+ }, timeoutMs);
12813
+ });
12814
+ return Promise.race([promise, timeout]).finally(() => {
12815
+ if (timer) {
12816
+ clearTimeout(timer);
12817
+ }
12818
+ });
12819
+ }
12257
12820
  function isLocalControlUrl(url) {
12258
12821
  try {
12259
12822
  const parsed = new URL(url);
@@ -12267,7 +12830,19 @@ function isRetryableLocalWorkerRestart(status, body, url) {
12267
12830
  }
12268
12831
  function isRetryableRecordObjectsError(error) {
12269
12832
  const message = error instanceof Error ? error.message : String(error);
12270
- return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
12833
+ return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out|bad gateway|too many requests|gateway timeout|control plane api error \((?:429|500|502|503|504)\)|graphql api error \((?:429|500|502|503|504)\)|failed to record batch/i.test(
12834
+ message
12835
+ );
12836
+ }
12837
+ function isRetryableEffectRegistrationError(error) {
12838
+ const message = error instanceof Error ? error.message : String(error);
12839
+ return /timed out|websocket disconnected|websocket not connected|rpc timeout|worker restarted mid-request|network connection lost|bad gateway|gateway timeout|too many requests|(?:control plane|granular|graphql) api error \((?:429|500|502|503|504)\)/i.test(
12840
+ message
12841
+ );
12842
+ }
12843
+ function isRetryableSessionDataError(error) {
12844
+ const message = error instanceof Error ? error.message : String(error);
12845
+ return /network connection lost|worker restarted mid-request|econnreset|socket connection was closed unexpectedly|bad gateway|gateway timeout|service unavailable|session data api error \((?:429|500|502|503|504)\)/i.test(
12271
12846
  message
12272
12847
  );
12273
12848
  }
@@ -12289,16 +12864,28 @@ function computeEffectRegistrationKey(effect) {
12289
12864
  effect.versionSelector
12290
12865
  )}`;
12291
12866
  }
12292
- function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
12293
- const url = new URL(apiUrl);
12294
- if (url.pathname.endsWith("/granular/ws/connect")) {
12867
+ function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId, effectHostUrl) {
12868
+ const overrideUrl = effectHostUrl || process.env.GRANULAR_EFFECT_HOST_URL || process.env.EFFECT_HOST_URL;
12869
+ const api = new URL(apiUrl);
12870
+ const localRuntimeBase = process.env.RUNTIME_ORCHESTRATOR_URL || (isLocalControlUrl(apiUrl) ? `${api.protocol}//${api.hostname}:8791` : "");
12871
+ const url = new URL(overrideUrl || localRuntimeBase || apiUrl);
12872
+ if (url.protocol === "https:") {
12873
+ url.protocol = "wss:";
12874
+ } else if (url.protocol === "http:") {
12875
+ url.protocol = "ws:";
12876
+ }
12877
+ if (!overrideUrl && isLocalControlUrl(apiUrl) && api.pathname.endsWith("/granular")) {
12878
+ url.pathname = "/granular/orchestrator/effects/connect";
12879
+ } else if (url.pathname.endsWith("/granular/ws/connect")) {
12295
12880
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
12296
12881
  } else if (url.pathname.endsWith("/granular")) {
12297
- url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
12882
+ url.pathname = isLocalControlUrl(url.toString()) ? "/granular/orchestrator/effects/connect" : `${url.pathname.replace(/\/$/, "")}/effects/connect`;
12298
12883
  } else if (url.pathname.endsWith("/v2/ws/connect")) {
12299
12884
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
12300
12885
  } else if (url.pathname.endsWith("/v2/ws")) {
12301
12886
  url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
12887
+ } else if (url.pathname === "/" && isLocalControlUrl(url.toString()) && (url.port === "8791" || !overrideUrl && Boolean(localRuntimeBase))) {
12888
+ url.pathname = "/granular/orchestrator/effects/connect";
12302
12889
  } else if (url.pathname.endsWith("/ws/connect")) {
12303
12890
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
12304
12891
  } else if (url.pathname.endsWith("/ws")) {
@@ -12333,6 +12920,79 @@ function normalizeHeapSnapshot(raw) {
12333
12920
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
12334
12921
  };
12335
12922
  }
12923
+ function normalizeGraphPathSegment(value) {
12924
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
12925
+ }
12926
+ function extractRecordIdFromGraphPath(path, className) {
12927
+ const normalizedPrefix = `${normalizeGraphPathSegment(className)}_`;
12928
+ if (path.startsWith(normalizedPrefix)) {
12929
+ return path.slice(normalizedPrefix.length);
12930
+ }
12931
+ const legacyPrefix = `${className}_`;
12932
+ if (path.startsWith(legacyPrefix)) {
12933
+ return path.slice(legacyPrefix.length);
12934
+ }
12935
+ return path;
12936
+ }
12937
+ function toRecordSearchResult(className, node) {
12938
+ const path = typeof node.path === "string" ? node.path : "";
12939
+ if (!path) return null;
12940
+ const fields = Array.isArray(node.submodels) ? node.submodels.flatMap(
12941
+ (submodel) => {
12942
+ const name = typeof submodel?.label === "string" && submodel.label.trim() ? submodel.label : typeof submodel?.path === "string" ? submodel.path.split(":").pop() || submodel.path : "";
12943
+ if (!name) return [];
12944
+ if (typeof submodel.string_value === "string") {
12945
+ return [{ name, type: "string", value: submodel.string_value }];
12946
+ }
12947
+ if (typeof submodel.number_value === "number") {
12948
+ return [{ name, type: "number", value: submodel.number_value }];
12949
+ }
12950
+ if (typeof submodel.boolean_value === "boolean") {
12951
+ return [
12952
+ {
12953
+ name,
12954
+ type: "boolean",
12955
+ value: submodel.boolean_value
12956
+ }
12957
+ ];
12958
+ }
12959
+ return [];
12960
+ }
12961
+ ) : [];
12962
+ return {
12963
+ path,
12964
+ className,
12965
+ id: extractRecordIdFromGraphPath(path, className),
12966
+ label: typeof node.label === "string" && node.label.trim() ? node.label : extractRecordIdFromGraphPath(path, className),
12967
+ description: typeof node.description === "string" && node.description.trim() ? node.description : null,
12968
+ fields
12969
+ };
12970
+ }
12971
+ function normalizeRecordSearchText(value) {
12972
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
12973
+ }
12974
+ function rankRecordSearchResult(result, query, index) {
12975
+ const normalizedQuery = normalizeRecordSearchText(query);
12976
+ if (!normalizedQuery) {
12977
+ return index;
12978
+ }
12979
+ const label = normalizeRecordSearchText(result.label || "");
12980
+ const id = normalizeRecordSearchText(result.id || "");
12981
+ const path = normalizeRecordSearchText(result.path || "");
12982
+ const className = normalizeRecordSearchText(result.className || "");
12983
+ const searchable = [label, id, path, className].filter(Boolean);
12984
+ if (label === normalizedQuery) return index;
12985
+ if (id === normalizedQuery || path === normalizedQuery) return 100 + index;
12986
+ if (label.startsWith(normalizedQuery)) return 200 + index;
12987
+ if (searchable.some((value) => value.startsWith(normalizedQuery))) {
12988
+ return 300 + index;
12989
+ }
12990
+ if (label.includes(normalizedQuery)) return 400 + index;
12991
+ if (searchable.some((value) => value.includes(normalizedQuery))) {
12992
+ return 500 + index;
12993
+ }
12994
+ return 900 + index;
12995
+ }
12336
12996
  function deriveRuntimeBaseUrl(apiEndpoint) {
12337
12997
  try {
12338
12998
  const endpoint = new URL(apiEndpoint);
@@ -12421,7 +13081,7 @@ function normalizeEnvironmentData(environment) {
12421
13081
  setup: normalizeEnvironmentSetupSummary(environment.setup)
12422
13082
  };
12423
13083
  }
12424
- var Environment = class {
13084
+ var Environment = class _Environment {
12425
13085
  granular;
12426
13086
  envData;
12427
13087
  _apiKey;
@@ -12616,28 +13276,30 @@ var Environment = class {
12616
13276
  return response.json();
12617
13277
  }
12618
13278
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
13279
+ static normalizeGraphPathSegment(value) {
13280
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
13281
+ }
12619
13282
  /**
12620
- * Convert a class name + real-world ID into a unique graph path.
13283
+ * Convert a class name + application record ID into Granular's graph path.
12621
13284
  *
12622
- * Two objects of *different* classes may share the same real-world ID,
12623
- * so the graph path must incorporate the class to guarantee uniqueness.
12624
- *
12625
- * Format: `{className}_{id}` — deterministic, human-readable.
12626
- *
12627
- * **Convention**: class names should be simple identifiers without
12628
- * underscores (e.g. `author`, `book`). This ensures the prefix is
12629
- * unambiguously parseable by `extractIdFromGraphPath`.
13285
+ * This mirrors the record-write path normalization used by the control plane.
13286
+ * Keep the original customer/system ID in `real_id`; graph paths are stable
13287
+ * internal addresses, not the source of truth for business identity.
12630
13288
  */
12631
13289
  static toGraphPath(className, id) {
12632
- return `${className}_${id}`;
13290
+ return `${_Environment.normalizeGraphPathSegment(className)}_${_Environment.normalizeGraphPathSegment(id)}`;
12633
13291
  }
12634
13292
  /**
12635
- * Extract the real-world ID from a graph path, given the class name.
13293
+ * Best-effort extraction of an ID-like suffix from a graph path.
12636
13294
  *
12637
- * Strips the `{className}_` prefix. Returns the raw path if the
12638
- * expected prefix is not found.
13295
+ * Prefer the record's `real_id` field whenever exact customer/system IDs
13296
+ * matter, because graph path normalization is intentionally lossy.
12639
13297
  */
12640
13298
  static extractIdFromGraphPath(graphPath, className) {
13299
+ const normalizedPrefix = `${_Environment.normalizeGraphPathSegment(className)}_`;
13300
+ if (graphPath.startsWith(normalizedPrefix)) {
13301
+ return graphPath.substring(normalizedPrefix.length);
13302
+ }
12641
13303
  const prefix = `${className}_`;
12642
13304
  return graphPath.startsWith(prefix) ? graphPath.substring(prefix.length) : graphPath;
12643
13305
  }
@@ -12684,6 +13346,62 @@ var Environment = class {
12684
13346
  }
12685
13347
  return response.json();
12686
13348
  }
13349
+ async searchRecords(query, options = {}) {
13350
+ const normalizedQuery = query.replace(/\s+/g, " ").trim();
13351
+ const limit = Math.max(1, Math.min(50, Math.floor(options.limit ?? 12)));
13352
+ const offset = Math.max(0, Math.floor(options.offset ?? 0));
13353
+ const response = await this.graphql(
13354
+ `
13355
+ query RecordMentionSearch(
13356
+ $query: String
13357
+ $limit: Int
13358
+ $offset: Int
13359
+ $classNames: [String!]
13360
+ ) {
13361
+ record_search(
13362
+ query: $query
13363
+ limit: $limit
13364
+ offset: $offset
13365
+ class_names: $classNames
13366
+ ) {
13367
+ className
13368
+ model {
13369
+ path
13370
+ label
13371
+ description
13372
+ submodels {
13373
+ path
13374
+ label
13375
+ string_value
13376
+ number_value
13377
+ boolean_value
13378
+ }
13379
+ }
13380
+ }
13381
+ }
13382
+ `,
13383
+ {
13384
+ query: normalizedQuery,
13385
+ limit,
13386
+ offset,
13387
+ classNames: options.classNames?.length ? options.classNames : []
13388
+ }
13389
+ );
13390
+ const seen = /* @__PURE__ */ new Set();
13391
+ const results = (response.data?.record_search || []).flatMap((entry) => {
13392
+ const className = entry.className?.trim();
13393
+ const item = className && entry.model ? toRecordSearchResult(className, entry.model) : null;
13394
+ if (!item || seen.has(item.path)) {
13395
+ return [];
13396
+ }
13397
+ seen.add(item.path);
13398
+ return [item];
13399
+ });
13400
+ return results.map((result, index) => ({
13401
+ result,
13402
+ rank: rankRecordSearchResult(result, normalizedQuery, index)
13403
+ })).sort((left, right) => left.rank - right.rank).map((item) => item.result).slice(0, limit);
13404
+ }
12687
13405
  // ==================== RELATIONSHIP METHODS ====================
12688
13406
  /**
12689
13407
  * Define a relationship between two model types.
@@ -13449,7 +14167,8 @@ var Environment = class {
13449
14167
  body: JSON.stringify({
13450
14168
  records,
13451
14169
  batchSize: options.batchSize,
13452
- setupRunId: options.setupRunId
14170
+ setupRunId: options.setupRunId,
14171
+ writeMode: options.writeMode
13453
14172
  })
13454
14173
  }
13455
14174
  );
@@ -13501,11 +14220,13 @@ var Environment = class {
13501
14220
  };
13502
14221
  var EnvironmentSession = class extends Session {
13503
14222
  environment;
14223
+ sessionDataRoutePrefix;
13504
14224
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
13505
14225
  graphContainerStatus = null;
13506
- constructor(client, environment, clientId) {
13507
- super(client, clientId);
14226
+ constructor(client, environment, clientId, options = {}) {
14227
+ super(client, clientId, { initialQuota: options.initialQuota });
13508
14228
  this.environment = environment;
14229
+ this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
13509
14230
  }
13510
14231
  get environmentId() {
13511
14232
  return this.environment.environmentId;
@@ -13550,7 +14271,7 @@ var EnvironmentSession = class extends Session {
13550
14271
  const doc = this.document;
13551
14272
  return normalizeHeapSnapshot(doc?.heap);
13552
14273
  }
13553
- async sessionDataRequest(path, query) {
14274
+ async sessionDataRequest(path, query, init2 = {}) {
13554
14275
  const searchParams = new URLSearchParams();
13555
14276
  for (const [key, value] of Object.entries(query || {})) {
13556
14277
  if (value !== null && typeof value !== "undefined" && value !== "") {
@@ -13558,23 +14279,39 @@ var EnvironmentSession = class extends Session {
13558
14279
  }
13559
14280
  }
13560
14281
  const queryString = searchParams.toString();
13561
- const response = await fetch(
13562
- `${this.environment.runtimeBaseUrl}/orchestrator/ws/sessions/${encodeURIComponent(this.sessionId)}${path}${queryString ? `?${queryString}` : ""}`,
13563
- {
13564
- method: "GET",
13565
- headers: {
13566
- Authorization: `Bearer ${this.environment.authToken}`,
13567
- "Content-Type": "application/json"
14282
+ const url = `${this.environment.runtimeBaseUrl}${this.sessionDataRoutePrefix}/${encodeURIComponent(this.sessionId)}${path}${queryString ? `?${queryString}` : ""}`;
14283
+ const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
14284
+ for (let attempt = 1; attempt <= SESSION_DATA_REQUEST_RETRY_COUNT; attempt += 1) {
14285
+ try {
14286
+ const response = await fetch(url, {
14287
+ method: init2.method || "GET",
14288
+ headers: {
14289
+ Authorization: `Bearer ${this.environment.authToken}`,
14290
+ "Content-Type": "application/json"
14291
+ },
14292
+ ...typeof body === "undefined" ? {} : { body }
14293
+ });
14294
+ if (response.ok) {
14295
+ return response.json();
14296
+ }
14297
+ const errorText = await response.text();
14298
+ const error = new Error(
14299
+ `Session data API Error (${response.status}): ${errorText}`
14300
+ );
14301
+ if (isLocalControlUrl(url) && isRetryableSessionDataError(error) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
14302
+ await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
14303
+ continue;
13568
14304
  }
14305
+ throw error;
14306
+ } catch (error) {
14307
+ if (isLocalControlUrl(url) && isRetryableSessionDataError(error) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
14308
+ await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
14309
+ continue;
14310
+ }
14311
+ throw error;
13569
14312
  }
13570
- );
13571
- if (!response.ok) {
13572
- const errorText = await response.text();
13573
- throw new Error(
13574
- `Session data API Error (${response.status}): ${errorText}`
13575
- );
13576
14313
  }
13577
- return response.json();
14314
+ throw new Error(`Session data API Error: exhausted retries for ${url}`);
13578
14315
  }
13579
14316
  async collectAllSessionItems(listPage) {
13580
14317
  const items = [];
@@ -13632,6 +14369,17 @@ var EnvironmentSession = class extends Session {
13632
14369
  get: (name) => this.sessionDataRequest(
13633
14370
  `/heap/lists/${encodeURIComponent(name)}`
13634
14371
  )
14372
+ },
14373
+ variables: {
14374
+ list: (options = {}) => this.sessionDataRequest("/heap/variables", options),
14375
+ get: (name) => this.sessionDataRequest(
14376
+ `/heap/variables/${encodeURIComponent(name)}`
14377
+ ),
14378
+ delete: (name) => this.sessionDataRequest(
14379
+ `/heap/variables/${encodeURIComponent(name)}`,
14380
+ void 0,
14381
+ { method: "DELETE" }
14382
+ )
13635
14383
  }
13636
14384
  };
13637
14385
  }
@@ -13700,8 +14448,21 @@ var EnvironmentSession = class extends Session {
13700
14448
  async graphql(query, variables) {
13701
14449
  return this.environment.graphql(query, variables);
13702
14450
  }
13703
- async defineRelationship(options) {
13704
- return this.environment.defineRelationship(options);
14451
+ async searchRecords(query, options = {}) {
14452
+ return this.environment.searchRecords(query, options);
14453
+ }
14454
+ async mentionRecord(input) {
14455
+ return this.sessionDataRequest(
14456
+ "/records/mention",
14457
+ void 0,
14458
+ {
14459
+ method: "POST",
14460
+ body: input
14461
+ }
14462
+ );
14463
+ }
14464
+ async defineRelationship(options) {
14465
+ return this.environment.defineRelationship(options);
13705
14466
  }
13706
14467
  async getRelationships(modelPath) {
13707
14468
  return this.environment.getRelationships(modelPath);
@@ -13847,6 +14608,7 @@ var Granular = class _Granular {
13847
14608
  WebSocketCtor;
13848
14609
  onUnexpectedClose;
13849
14610
  onReconnectError;
14611
+ effectHostUrl;
13850
14612
  debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
13851
14613
  /** Sandbox-level effect registry: sandboxId → (effectKey@selector → ToolWithHandler) */
13852
14614
  sandboxEffects = /* @__PURE__ */ new Map();
@@ -13875,6 +14637,7 @@ var Granular = class _Granular {
13875
14637
  this.WebSocketCtor = options.WebSocketCtor;
13876
14638
  this.onUnexpectedClose = options.onUnexpectedClose;
13877
14639
  this.onReconnectError = options.onReconnectError;
14640
+ this.effectHostUrl = options.effectHostUrl;
13878
14641
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
13879
14642
  }
13880
14643
  /**
@@ -14045,6 +14808,30 @@ var Granular = class _Granular {
14045
14808
  permissions: options.permissions || options.user?.permissions || []
14046
14809
  });
14047
14810
  }
14811
+ /**
14812
+ * Run a registered environment importer against an environment that was
14813
+ * opened outside this SDK instance, for example by a delegated browser flow.
14814
+ *
14815
+ * This uses the same setup-run and queued record-import plumbing as
14816
+ * `openEnvironment()`: importer stages, expected object counts, and queued
14817
+ * import counters remain visible through `environment.setup` and
14818
+ * `getRecordImportSummary()`.
14819
+ */
14820
+ async runEnvironmentImporterForEnvironment(environmentId, options = {}) {
14821
+ const environmentData = await this.environments.get(environmentId);
14822
+ const environment = this.bindEnvironmentHandle(environmentData);
14823
+ const requestedOntology = options.ontology || environmentData.ontologyId || environmentData.sandboxId;
14824
+ return this.runEnvironmentImporter(
14825
+ {
14826
+ environment: environmentData,
14827
+ requestedOntology,
14828
+ sandboxId: environmentData.sandboxId,
14829
+ subjectId: environmentData.subjectId,
14830
+ setupTriggerReason: options.reason || "new_environment"
14831
+ },
14832
+ environment
14833
+ );
14834
+ }
14048
14835
  resolveRequestedTag(options, methodName) {
14049
14836
  const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
14050
14837
  if (!tag) {
@@ -14236,6 +15023,15 @@ var Granular = class _Granular {
14236
15023
  const environment = this.bindEnvironmentHandle(envData);
14237
15024
  return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
14238
15025
  }
15026
+ async recordOpenAIUsageSpend(usage, context, options) {
15027
+ return recordOpenAIUsageSpend({
15028
+ apiUrl: this.apiUrl,
15029
+ token: this.apiKey,
15030
+ usage,
15031
+ context,
15032
+ metadata: options?.metadata
15033
+ });
15034
+ }
14239
15035
  /**
14240
15036
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
14241
15037
  * `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
@@ -14288,15 +15084,25 @@ var Granular = class _Granular {
14288
15084
  return ontologyImporter;
14289
15085
  }
14290
15086
  async maybeRunEnvironmentImporter(resolved, environment) {
14291
- if (!resolved.setupTriggerReason) {
14292
- return;
15087
+ const setupTriggerReason = resolved.setupTriggerReason;
15088
+ if (!setupTriggerReason) {
15089
+ return null;
14293
15090
  }
15091
+ return this.runEnvironmentImporter(
15092
+ {
15093
+ ...resolved,
15094
+ setupTriggerReason
15095
+ },
15096
+ environment
15097
+ );
15098
+ }
15099
+ async runEnvironmentImporter(resolved, environment) {
14294
15100
  const importer = this.resolveEnvironmentImporter(
14295
15101
  resolved.requestedOntology,
14296
15102
  resolved.sandboxId
14297
15103
  );
14298
15104
  if (!importer) {
14299
- return;
15105
+ return null;
14300
15106
  }
14301
15107
  const setupRun = await this.request(
14302
15108
  `/control/environments/${environment.environmentId}/setup-runs`,
@@ -14336,16 +15142,24 @@ var Granular = class _Granular {
14336
15142
  },
14337
15143
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
14338
15144
  batchSize: options?.batchSize,
15145
+ writeMode: options?.writeMode,
14339
15146
  setupRunId
14340
15147
  })
14341
15148
  };
14342
15149
  try {
14343
15150
  await importer(importerContext);
14344
- await updateSetupRun({ markHookCompleted: true });
15151
+ const completedSetupRun = await this.request(
15152
+ `/control/environment-setup-runs/${setupRunId}`,
15153
+ {
15154
+ method: "PATCH",
15155
+ body: JSON.stringify({ markHookCompleted: true })
15156
+ }
15157
+ );
14345
15158
  const refreshedEnvironment = await this.environments.get(
14346
15159
  environment.environmentId
14347
15160
  );
14348
15161
  environment.syncEnvironmentData(refreshedEnvironment);
15162
+ return completedSetupRun;
14349
15163
  } catch (error) {
14350
15164
  await updateSetupRun({
14351
15165
  status: "failed",
@@ -14372,7 +15186,8 @@ var Granular = class _Granular {
14372
15186
  const environmentSession = new EnvironmentSession(
14373
15187
  client,
14374
15188
  environment,
14375
- clientId
15189
+ clientId,
15190
+ { initialQuota: session.quota || null }
14376
15191
  );
14377
15192
  await environmentSession.hello();
14378
15193
  return environmentSession;
@@ -14393,27 +15208,45 @@ var Granular = class _Granular {
14393
15208
  return effects;
14394
15209
  }
14395
15210
  serializeEffect(effect) {
14396
- return {
15211
+ const serialized = {
14397
15212
  effectKey: computeEffectKey2(effect),
14398
15213
  name: effect.name,
14399
15214
  description: effect.description,
14400
15215
  inputSchema: effect.inputSchema,
14401
- outputSchema: effect.outputSchema,
14402
15216
  stability: effect.stability || "stable",
14403
- provenance: effect.provenance || { source: "custom" },
14404
- tags: effect.tags,
14405
- className: effect.className,
14406
- static: effect.static,
14407
- versionSelector: effect.versionSelector
15217
+ provenance: effect.provenance || { source: "custom" }
14408
15218
  };
15219
+ if (effect.outputSchema !== void 0) {
15220
+ serialized.outputSchema = effect.outputSchema;
15221
+ }
15222
+ if (effect.tags !== void 0) {
15223
+ serialized.tags = effect.tags;
15224
+ }
15225
+ if (effect.className !== void 0) {
15226
+ serialized.className = effect.className;
15227
+ }
15228
+ if (effect.static !== void 0) {
15229
+ serialized.static = effect.static;
15230
+ }
15231
+ if (effect.versionSelector !== void 0) {
15232
+ serialized.versionSelector = effect.versionSelector;
15233
+ }
15234
+ if (effect.metamodels !== void 0) {
15235
+ serialized.metamodels = effect.metamodels;
15236
+ }
15237
+ return serialized;
14409
15238
  }
14410
15239
  async publishSandboxEffectCatalog(host) {
14411
15240
  const effects = Array.from(
14412
15241
  this.getSandboxEffectMap(host.sandboxId).values()
14413
15242
  ).map((effect) => this.serializeEffect(effect));
14414
- const result = await host.wsClient.call("effects.publishCatalog", {
14415
- effects
14416
- });
15243
+ const result = await withTimeout(
15244
+ host.wsClient.call("effects.publishCatalog", {
15245
+ effects
15246
+ }),
15247
+ EFFECT_CATALOG_SYNC_TIMEOUT_MS,
15248
+ `effects.publishCatalog for sandbox ${host.sandboxId}`
15249
+ );
14417
15250
  const acceptedCount = typeof result?.acceptedCount === "number" ? result.acceptedCount : 0;
14418
15251
  const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
14419
15252
  if (acceptedCount === 0 && rejected.length > 0) {
@@ -14432,8 +15265,26 @@ var Granular = class _Granular {
14432
15265
  }
14433
15266
  }
14434
15267
  async syncSandboxEffectCatalog(sandboxId) {
14435
- const host = await this.ensureSandboxEffectHost(sandboxId);
14436
- await this.publishSandboxEffectCatalog(host);
15268
+ let lastError;
15269
+ for (let attempt = 1; attempt <= EFFECT_CATALOG_SYNC_RETRY_COUNT; attempt += 1) {
15270
+ try {
15271
+ const host = await this.ensureSandboxEffectHost(sandboxId);
15272
+ await this.publishSandboxEffectCatalog(host);
15273
+ return;
15274
+ } catch (error) {
15275
+ lastError = error;
15276
+ this.disconnectSandboxEffectHost(sandboxId);
15277
+ if (attempt === EFFECT_CATALOG_SYNC_RETRY_COUNT || !isRetryableEffectRegistrationError(error)) {
15278
+ throw error;
15279
+ }
15280
+ console.warn(
15281
+ `[Granular] Retrying effect registration for sandbox ${sandboxId} after transient failure (${attempt}/${EFFECT_CATALOG_SYNC_RETRY_COUNT - 1} retries used):`,
15282
+ error
15283
+ );
15284
+ await sleep(EFFECT_CATALOG_SYNC_RETRY_DELAY_MS * attempt);
15285
+ }
15286
+ }
15287
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
14437
15288
  }
14438
15289
  recoverEffectHost(host, error) {
14439
15290
  if (host.recovering) {
@@ -14526,7 +15377,8 @@ var Granular = class _Granular {
14526
15377
  this.apiUrl,
14527
15378
  sandboxId,
14528
15379
  effectClientId,
14529
- clientId
15380
+ clientId,
15381
+ this.effectHostUrl
14530
15382
  ),
14531
15383
  sessionId: `effect-host:${effectClientId}`,
14532
15384
  token: this.apiKey,
@@ -14562,7 +15414,11 @@ var Granular = class _Granular {
14562
15414
  wsClient.on("disconnect", () => {
14563
15415
  this.stopEffectHostHeartbeat(host);
14564
15416
  });
14565
- await wsClient.connect();
15417
+ await withTimeout(
15418
+ wsClient.connect(),
15419
+ EFFECT_HOST_CONNECT_TIMEOUT_MS,
15420
+ `effect host WebSocket connect for sandbox ${sandboxId}`
15421
+ );
14566
15422
  await this.synchronizeEffectHost(host);
14567
15423
  this.sandboxEffectHosts.set(sandboxId, host);
14568
15424
  return host;
@@ -14685,7 +15541,7 @@ var Granular = class _Granular {
14685
15541
  /**
14686
15542
  * Ensure a permission profile exists for a sandbox, creating it if needed.
14687
15543
  * If profileName matches an existing profile name, returns its ID.
14688
- * Otherwise, creates a new profile with default allow-all rules.
15544
+ * Otherwise, creates a v1 source-profile file shape with an allow default.
14689
15545
  */
14690
15546
  async ensurePermissionProfile(sandboxId, profileName) {
14691
15547
  try {
@@ -14699,8 +15555,11 @@ var Granular = class _Granular {
14699
15555
  const created = await this.permissionProfiles.create(sandboxId, {
14700
15556
  name: profileName,
14701
15557
  rules: {
14702
- effects: { allow: ["*"] },
14703
- resources: { allow: ["*"] }
15558
+ schemaVersion: 1,
15559
+ name: profileName,
15560
+ description: profileName === "allow-all" ? "Every declared action is visible unless a manifest policy denies it." : `Generated permission profile ${profileName}`,
15561
+ defaults: { actionPolicy: "allow" },
15562
+ actions: []
14704
15563
  }
14705
15564
  });
14706
15565
  return created.permissionProfileId;
@@ -14773,33 +15632,63 @@ var Granular = class _Granular {
14773
15632
  * Permission Profile management for sandboxes
14774
15633
  */
14775
15634
  get permissionProfiles() {
15635
+ const profileSourceFromRecord = (record) => {
15636
+ const profile = record.profile || record.rules || {};
15637
+ return {
15638
+ ...profile,
15639
+ schemaVersion: profile.schemaVersion || 1,
15640
+ name: profile.name || record.name,
15641
+ description: profile.description || record.description
15642
+ };
15643
+ };
14776
15644
  return {
14777
15645
  list: async (sandboxId) => {
14778
15646
  const result = await this.request(
14779
- `/control/sandboxes/${sandboxId}/permission-profiles`
15647
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`
14780
15648
  );
14781
15649
  return result.items;
14782
15650
  },
14783
15651
  get: async (sandboxId, profileId) => {
14784
- return this.request(
14785
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`
15652
+ const result = await this.request(
15653
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`
15654
+ );
15655
+ const profile = result.items.find(
15656
+ (item) => item.permissionProfileId === profileId || item.name === profileId
14786
15657
  );
15658
+ if (!profile) {
15659
+ throw new Error(`Permission profile source not found: ${profileId}`);
15660
+ }
15661
+ return profile;
14787
15662
  },
14788
15663
  create: async (sandboxId, data) => {
14789
- return this.request(
14790
- `/control/sandboxes/${sandboxId}/permission-profiles`,
15664
+ const profile = {
15665
+ ...data.rules,
15666
+ schemaVersion: 1,
15667
+ name: data.name
15668
+ };
15669
+ const existingProfiles = await this.permissionProfiles.list(sandboxId);
15670
+ const profiles = [
15671
+ ...existingProfiles.filter((existing) => existing.name !== data.name).map((existing) => profileSourceFromRecord(existing)),
15672
+ profile
15673
+ ];
15674
+ const result = await this.request(
15675
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`,
14791
15676
  {
14792
- method: "POST",
14793
- body: JSON.stringify(data)
15677
+ method: "PUT",
15678
+ body: JSON.stringify({ profiles })
14794
15679
  }
14795
15680
  );
15681
+ const synced = result.items.find((item) => item.name === data.name) || result.items[0];
15682
+ if (!synced) {
15683
+ throw new Error(
15684
+ `Permission profile source sync did not return ${data.name}`
15685
+ );
15686
+ }
15687
+ return synced;
14796
15688
  },
14797
- delete: async (sandboxId, profileId) => {
14798
- return this.request(
14799
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`,
14800
- {
14801
- method: "DELETE"
14802
- }
15689
+ delete: async (_sandboxId, _profileId) => {
15690
+ throw new Error(
15691
+ "Permission profile sources are updated by syncing the desired source set."
14803
15692
  );
14804
15693
  }
14805
15694
  };
@@ -15055,6 +15944,85 @@ var Granular = class _Granular {
15055
15944
  };
15056
15945
 
15057
15946
  // src/agent-harness.ts
15947
+ var DEFAULT_IGNORED_REASONING_COMMENT_DIRECTIVES = [
15948
+ /^@ts-ignore\b/i,
15949
+ /^@ts-expect-error\b/i,
15950
+ /^eslint-[\w-]+\b/i,
15951
+ /^biome-ignore\b/i,
15952
+ /^prettier-ignore\b/i,
15953
+ /^istanbul ignore\b/i
15954
+ ];
15955
+ var DEFAULT_LOW_SIGNAL_REASONING_LINES = [
15956
+ /^running\.?$/i,
15957
+ /^working\.?$/i,
15958
+ /^thinking\.?$/i,
15959
+ /^generating(?: code)?\.?$/i,
15960
+ /^starting(?: execution)?\.?$/i
15961
+ ];
15962
+ function parseReasoningCommentLine(line, options = {}) {
15963
+ const trimmed = line.trimStart();
15964
+ if (!trimmed.startsWith("//")) return null;
15965
+ const text = trimmed.replace(/^\/\/\s?/, "").trim();
15966
+ if (!text) return { kind: "ignored" };
15967
+ const ignoredDirectives = options.ignoredCommentDirectives || DEFAULT_IGNORED_REASONING_COMMENT_DIRECTIVES;
15968
+ if (ignoredDirectives.some((pattern) => pattern.test(text))) {
15969
+ return { kind: "ignored" };
15970
+ }
15971
+ const lowSignalLines = options.lowSignalReasoningLines || DEFAULT_LOW_SIGNAL_REASONING_LINES;
15972
+ if (lowSignalLines.some((pattern) => pattern.test(text))) {
15973
+ return { kind: "ignored" };
15974
+ }
15975
+ return { kind: "reasoning", text };
15976
+ }
15977
+ function consumeGranularReasoningTraceChunk(buffer, chunk, options = {}) {
15978
+ let text = buffer + chunk;
15979
+ let visibleText = "";
15980
+ const reasoningLines = [];
15981
+ while (true) {
15982
+ const newlineIndex = text.indexOf("\n");
15983
+ if (newlineIndex === -1) break;
15984
+ const rawLine = text.slice(0, newlineIndex);
15985
+ text = text.slice(newlineIndex + 1);
15986
+ const comment = parseReasoningCommentLine(
15987
+ rawLine.replace(/\r$/, ""),
15988
+ options
15989
+ );
15990
+ if (comment?.kind === "reasoning") {
15991
+ reasoningLines.push(comment.text);
15992
+ } else if (comment?.kind === "ignored") {
15993
+ continue;
15994
+ } else {
15995
+ visibleText += `${rawLine}
15996
+ `;
15997
+ }
15998
+ }
15999
+ if (options.final && text.length > 0) {
16000
+ const comment = parseReasoningCommentLine(text.replace(/\r$/, ""), options);
16001
+ if (comment?.kind === "reasoning") {
16002
+ reasoningLines.push(comment.text);
16003
+ text = "";
16004
+ } else if (comment?.kind === "ignored") {
16005
+ text = "";
16006
+ } else {
16007
+ visibleText += text;
16008
+ text = "";
16009
+ }
16010
+ }
16011
+ return { buffer: text, visibleText, reasoningLines };
16012
+ }
16013
+ function consumeGranularReasoningOnlyChunk(buffer, chunk, options = {}) {
16014
+ const result = consumeGranularReasoningTraceChunk(buffer, chunk, options);
16015
+ return {
16016
+ buffer: result.buffer,
16017
+ reasoningLines: result.reasoningLines
16018
+ };
16019
+ }
16020
+ function stripGranularReasoningTrace(text, options = {}) {
16021
+ return consumeGranularReasoningTraceChunk("", text, {
16022
+ ...options,
16023
+ final: true
16024
+ }).visibleText.trim();
16025
+ }
15058
16026
  function asRecord4(value) {
15059
16027
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
15060
16028
  return value;
@@ -15078,21 +16046,8 @@ function uniqueStrings(values, maxCount) {
15078
16046
  }
15079
16047
  return output;
15080
16048
  }
15081
- function formatScalar(value) {
15082
- if (typeof value === "string") return JSON.stringify(value);
15083
- if (typeof value === "number" || typeof value === "boolean")
15084
- return String(value);
15085
- if (value === null) return "null";
15086
- return "unknown";
15087
- }
15088
- function describeHeapEntry(entry, previewFieldLimit = 3) {
15089
- const headline = entry.label || entry.id || entry.path || "Unknown";
15090
- const pathLabel = entry.path && entry.path !== headline ? ` <${entry.path}>` : "";
15091
- const classLabel = entry.className || "unknown";
15092
- const preview = asArray2(entry.fields).filter(
15093
- (field) => field?.name && field.name !== "_realId" && field.name !== "real_id"
15094
- ).slice(0, previewFieldLimit).map((field) => `${field.name}=${formatScalar(field.value)}`).join(", ");
15095
- return preview ? `${headline}${pathLabel} [${classLabel}] ${preview}` : `${headline}${pathLabel} [${classLabel}]`;
16049
+ function renderConstBlock(name, value) {
16050
+ return `const ${name} = ${JSON.stringify(value, null, 2)} as const;`;
15096
16051
  }
15097
16052
  function hashString(value) {
15098
16053
  if (!value) return null;
@@ -15103,97 +16058,248 @@ function hashString(value) {
15103
16058
  }
15104
16059
  return (hash >>> 0).toString(16).padStart(8, "0");
15105
16060
  }
15106
- function hasSubstantiveAwaitAfterPrompt(code, marker) {
15107
- const startIndex = code.indexOf(marker);
15108
- if (startIndex === -1) return true;
15109
- const segment = code.slice(startIndex + marker.length);
15110
- const callMatches = segment.matchAll(
15111
- /await\s+([A-Za-z0-9_$.]+)\.([A-Za-z0-9_]+)\s*\(/g
16061
+ function findUndefinedSimpleTemplateIdentifier(source) {
16062
+ const declared = /* @__PURE__ */ new Set();
16063
+ const globals = /* @__PURE__ */ new Set([
16064
+ "Array",
16065
+ "Boolean",
16066
+ "Date",
16067
+ "JSON",
16068
+ "Math",
16069
+ "Number",
16070
+ "Object",
16071
+ "Promise",
16072
+ "String",
16073
+ "undefined",
16074
+ "null",
16075
+ "true",
16076
+ "false"
16077
+ ]);
16078
+ for (const match of source.matchAll(/import\s*\{([^}]+)\}\s*from/g)) {
16079
+ for (const part of match[1].split(",")) {
16080
+ const aliasMatch = part.trim().match(/\bas\s+([A-Za-z_$][\w$]*)$/);
16081
+ const nameMatch = part.trim().match(/^([A-Za-z_$][\w$]*)/);
16082
+ const name = aliasMatch?.[1] || nameMatch?.[1];
16083
+ if (name) declared.add(name);
16084
+ }
16085
+ }
16086
+ for (const match of source.matchAll(
16087
+ /\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\b/g
16088
+ )) {
16089
+ declared.add(match[1]);
16090
+ }
16091
+ for (const match of source.matchAll(
16092
+ /\bfor\s*(?:await\s*)?\(\s*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s+of\b/g
16093
+ )) {
16094
+ declared.add(match[1]);
16095
+ }
16096
+ for (const match of source.matchAll(
16097
+ /\bcatch\s*\(\s*([A-Za-z_$][\w$]*)\s*\)/g
16098
+ )) {
16099
+ declared.add(match[1]);
16100
+ }
16101
+ for (const match of source.matchAll(
16102
+ /\(\s*([A-Za-z_$][\w$]*)\s*(?:,\s*[A-Za-z_$][\w$]*)*\s*\)\s*=>/g
16103
+ )) {
16104
+ declared.add(match[1]);
16105
+ }
16106
+ for (const match of source.matchAll(/\b([A-Za-z_$][\w$]*)\s*=>/g)) {
16107
+ declared.add(match[1]);
16108
+ }
16109
+ for (const match of source.matchAll(/\$\{\s*([A-Za-z_$][\w$]*)\s*\}/g)) {
16110
+ const identifier = match[1];
16111
+ if (!declared.has(identifier) && !globals.has(identifier)) {
16112
+ return identifier;
16113
+ }
16114
+ }
16115
+ return null;
16116
+ }
16117
+ function getGeneratedJobSyntaxError(source) {
16118
+ const withoutImports = source.replace(
16119
+ /^\s*import\s+[\s\S]*?\s+from\s+["'][^"']+["']\s*;?\s*$/gm,
16120
+ ""
15112
16121
  );
15113
- for (const match of callMatches) {
15114
- const receiver = match[1] || "";
15115
- const method = match[2] || "";
15116
- if (receiver === "loop" || receiver === "heap") continue;
15117
- if (method.startsWith("get_") || method.startsWith("get")) continue;
15118
- return true;
16122
+ try {
16123
+ new Function(`return (async () => {
16124
+ ${withoutImports}
16125
+ });`);
16126
+ return null;
16127
+ } catch (error) {
16128
+ return error instanceof Error ? error.message : String(error);
16129
+ }
16130
+ }
16131
+ function hasNestedTemplateLiteralExpression(source) {
16132
+ let inString = null;
16133
+ let escaped = false;
16134
+ const templateStack = [];
16135
+ for (let index = 0; index < source.length; index += 1) {
16136
+ const char = source[index];
16137
+ const next = source[index + 1] || "";
16138
+ if (escaped) {
16139
+ escaped = false;
16140
+ continue;
16141
+ }
16142
+ if (char === "\\") {
16143
+ escaped = true;
16144
+ continue;
16145
+ }
16146
+ if (inString === "'" || inString === '"') {
16147
+ if (char === inString) inString = null;
16148
+ continue;
16149
+ }
16150
+ if (inString === "`") {
16151
+ const current = templateStack[templateStack.length - 1];
16152
+ if (char === "`") {
16153
+ if (current?.expressionDepth && current.expressionDepth > 0) {
16154
+ return true;
16155
+ }
16156
+ templateStack.pop();
16157
+ if (templateStack.length === 0) inString = null;
16158
+ continue;
16159
+ }
16160
+ if (char === "$" && next === "{") {
16161
+ if (current) current.expressionDepth += 1;
16162
+ index += 1;
16163
+ continue;
16164
+ }
16165
+ if (char === "}" && current?.expressionDepth) {
16166
+ current.expressionDepth -= 1;
16167
+ }
16168
+ continue;
16169
+ }
16170
+ if (char === "'" || char === '"') {
16171
+ inString = char;
16172
+ continue;
16173
+ }
16174
+ if (char === "`") {
16175
+ inString = "`";
16176
+ templateStack.push({ expressionDepth: 0 });
16177
+ }
15119
16178
  }
15120
16179
  return false;
15121
16180
  }
15122
- function reviewGeneratedJobCode(code) {
16181
+ function reviewGeneratedJobCode(code, _options = {}) {
15123
16182
  const normalized = typeof code === "string" ? code : "";
15124
- if (!normalized.trim()) return [];
15125
16183
  const issues = [];
16184
+ if (!normalized.trim()) {
16185
+ return issues;
16186
+ }
15126
16187
  if (/require\s*\(\s*['"]\.\/sandbox-tools['"]\s*\)/.test(normalized)) {
15127
16188
  issues.push({
15128
16189
  code: "commonjs_require",
15129
16190
  severity: "error",
15130
- message: "Use ESM imports like `import { Customer, loop } from './sandbox-tools';` instead of require('./sandbox-tools'). Generated jobs must be plain runnable JavaScript for the sandbox runtime."
15131
- });
15132
- }
15133
- const placeholderPatterns = [
15134
- /ready to make the change next/i,
15135
- /ready to .* next/i,
15136
- /ready to .* now/i,
15137
- /i can make the change now/i,
15138
- /i can do that next/i,
15139
- /i'?m ready to continue/i,
15140
- /have your approval .* ready to make/i,
15141
- /approved\./i
15142
- ];
15143
- if (normalized.includes("await loop.confirm(")) {
15144
- const postConfirm = normalized.slice(
15145
- normalized.indexOf("await loop.confirm(")
15146
- );
15147
- const hasPlaceholder = placeholderPatterns.some(
15148
- (pattern) => pattern.test(postConfirm)
15149
- );
15150
- const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(
15151
- normalized,
15152
- "await loop.confirm("
15153
- );
15154
- if (!hasSubstantiveAwait || hasPlaceholder) {
15155
- issues.push({
15156
- code: "placeholder_after_confirm",
15157
- severity: "error",
15158
- message: "After await loop.confirm(...) returns true, the job must perform the approved mutation in the same resumed run. Do not stop with placeholder text like 'Approved, I can make the change now.'"
15159
- });
15160
- }
16191
+ message: "Use ESM imports from './sandbox-tools' instead of require('./sandbox-tools')."
16192
+ });
15161
16193
  }
15162
- if (normalized.includes("await loop.ask_user(")) {
15163
- const postPrompt = normalized.slice(
15164
- normalized.indexOf("await loop.ask_user(")
15165
- );
15166
- const hasPlaceholder = placeholderPatterns.some(
15167
- (pattern) => pattern.test(postPrompt)
15168
- );
15169
- const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(
15170
- normalized,
15171
- "await loop.ask_user("
15172
- );
15173
- if (hasPlaceholder && !hasSubstantiveAwait) {
15174
- issues.push({
15175
- code: "placeholder_after_ask_user",
15176
- severity: "error",
15177
- message: "After await loop.ask_user(...) returns a usable answer, continue the workflow in the same resumed run instead of stopping with placeholder text about doing the work later."
15178
- });
15179
- }
16194
+ if (/\bprocess\.exit\s*\(/.test(normalized)) {
16195
+ issues.push({
16196
+ code: "process_exit",
16197
+ severity: "error",
16198
+ message: "Generated jobs must not call process.exit(...). Return from the job or emit a runtime message instead."
16199
+ });
16200
+ }
16201
+ if (/\bawait\s+import\s*\(\s*['"]\.\/sandbox-tools['"]\s*\)/.test(normalized)) {
16202
+ issues.push({
16203
+ code: "dynamic_import_in_job",
16204
+ severity: "error",
16205
+ message: "Import sandbox tools with a static top-level import from './sandbox-tools'; do not use dynamic import for runtime tools."
16206
+ });
16207
+ }
16208
+ if (hasNestedTemplateLiteralExpression(normalized)) {
16209
+ issues.push({
16210
+ code: "nested_template_literal_in_job",
16211
+ severity: "error",
16212
+ message: "Avoid nested template literals inside template expressions. Precompute conditional text in variables or use simpler string construction."
16213
+ });
15180
16214
  }
15181
- const hasConversationalReturn = /return\s+[`'"]/.test(normalized) || /\breply\s*:/.test(normalized) || /\bagent_message\s*\(/.test(normalized) || /\bagent_text_message\s*\(/.test(normalized);
15182
- const returnsObjectLiteral = /return\s+\{[\s\S]*?\}/.test(normalized);
15183
- const returnsShowPayload = /return\s+\{[\s\S]*?\bshow\s*:/.test(normalized);
15184
- const closesLoop = /loop\.close_loop\s*\(/.test(normalized);
15185
- if (!hasConversationalReturn && returnsObjectLiteral && !closesLoop) {
16215
+ const syntaxError = getGeneratedJobSyntaxError(normalized);
16216
+ if (syntaxError) {
15186
16217
  issues.push({
15187
- code: "missing_user_reply",
16218
+ code: "syntax_error_in_job",
15188
16219
  severity: "error",
15189
- message: "User-facing jobs must end with a natural-language answer. Return a short string, an object with a top-level `reply` string, or post text with agent_text_message(...). Do not end with bare structured JSON."
16220
+ message: `The generated job has a JavaScript syntax error before runtime execution: ${syntaxError}.`
15190
16221
  });
15191
16222
  }
15192
- if (returnsShowPayload) {
16223
+ if (/[\u2018-\u201F]/.test(normalized)) {
15193
16224
  issues.push({
15194
- code: "return_show_not_for_ui",
16225
+ code: "syntax_error_in_job",
15195
16226
  severity: "error",
15196
- message: "Do not use the final return value to send UI record refs through `show`. Use agent_heap_objects(...) for heap-backed UI, then return plain text if you still want a final textual answer."
16227
+ message: "Use plain ASCII quotes and apostrophes in generated job strings."
16228
+ });
16229
+ }
16230
+ const undefinedTemplateIdentifier = findUndefinedSimpleTemplateIdentifier(normalized);
16231
+ if (undefinedTemplateIdentifier) {
16232
+ issues.push({
16233
+ code: "undefined_template_identifier",
16234
+ severity: "error",
16235
+ message: `The template literal references \`${undefinedTemplateIdentifier}\`, but that identifier is not declared in the generated job.`
16236
+ });
16237
+ }
16238
+ if (/\{\s*\.\.\.[A-Za-z_$][\w$]*/.test(normalized)) {
16239
+ issues.push({
16240
+ code: "object_spread_in_job",
16241
+ severity: "error",
16242
+ message: "Avoid object spread in generated jobs until the backend runtime transform can validate it structurally."
16243
+ });
16244
+ }
16245
+ if (/\bloop\./.test(normalized) && !/import\s*\{[^}]*\bloop\b[^}]*\}\s*from\s*['"]\.\/sandbox-tools['"]/.test(
16246
+ normalized
16247
+ )) {
16248
+ issues.push({
16249
+ code: "missing_loop_import",
16250
+ severity: "error",
16251
+ message: "The job calls loop.* but does not import loop from './sandbox-tools'."
16252
+ });
16253
+ }
16254
+ const bareLoopHelperImport = normalized.match(
16255
+ /import\s*\{[^}]*\b(ask_user|confirm|open_decision|close_decision|create_task|update_task|complete_task|close_loop)\b[^}]*\}\s*from\s*['"]\.\/sandbox-tools['"]/
16256
+ );
16257
+ if (bareLoopHelperImport) {
16258
+ issues.push({
16259
+ code: "bare_loop_helper_import",
16260
+ severity: "error",
16261
+ message: "Workflow helpers are exposed on the imported `loop` object. Import `loop` from './sandbox-tools' and call helpers as `loop.create_task(...)`, `loop.open_decision(...)`, `loop.confirm(...)`, etc.; do not import them as bare functions."
16262
+ });
16263
+ }
16264
+ if (/\bloop\.open_decision\s*\(\s*\{[\s\S]*?\boptions\s*:/.test(normalized)) {
16265
+ issues.push({
16266
+ code: "loop_helper_contract",
16267
+ severity: "error",
16268
+ message: "loop.open_decision(...) must use `candidates: [...]`, not `options: [...]`. Every candidate must include a string `id`."
16269
+ });
16270
+ }
16271
+ if (/\bloop\.close_decision\s*\(\s*\{[\s\S]*?\bselected\s*:/.test(normalized)) {
16272
+ issues.push({
16273
+ code: "loop_helper_contract",
16274
+ severity: "error",
16275
+ message: "loop.close_decision(...) must use `selectedId`, not `selected`."
16276
+ });
16277
+ }
16278
+ if (/\bloop\.(?:create_task|update_task|complete_task)\s*\(\s*\{[\s\S]*?\bid\s*:/.test(
16279
+ normalized
16280
+ )) {
16281
+ issues.push({
16282
+ code: "loop_helper_contract",
16283
+ severity: "error",
16284
+ message: "Loop task helpers must use `taskId`, not `id`, for explicit task identifiers."
16285
+ });
16286
+ }
16287
+ if (/\bconsole\.log\s*\(\s*JSON\.stringify\s*\(\s*\{[\s\S]*?\b(?:action|reply|code)\s*:/.test(
16288
+ normalized
16289
+ )) {
16290
+ issues.push({
16291
+ code: "stdout_json_reply",
16292
+ severity: "error",
16293
+ message: "Do not print JSON chat envelopes from generated jobs; use runtime messaging or return a plain result."
16294
+ });
16295
+ }
16296
+ if (/\breturn\s+\{[\s\S]*?\baction\s*:\s*['"]reply['"][\s\S]*?\breply\s*:/.test(
16297
+ normalized
16298
+ )) {
16299
+ issues.push({
16300
+ code: "return_chat_payload",
16301
+ severity: "error",
16302
+ message: "Do not return chat envelopes like { action, reply, code } from generated jobs; return a plain value or use runtime messaging."
15197
16303
  });
15198
16304
  }
15199
16305
  return issues;
@@ -15252,15 +16358,40 @@ function collectConversationReferents(liveDoc) {
15252
16358
  const ts = Number(message.ts) || 0;
15253
16359
  const messageId = typeof message.id === "string" ? message.id : void 0;
15254
16360
  const jobId = typeof message.jobId === "string" ? message.jobId : void 0;
15255
- for (const entryPath of uniqueStrings(asArray2(show.entryPaths))) {
16361
+ const entryPaths = uniqueStrings(asArray2(show.entryPaths));
16362
+ const entryClassCounts = /* @__PURE__ */ new Map();
16363
+ const entryMetadata = entryPaths.map((entryPath) => {
15256
16364
  const entry = asRecord4(entriesByPath[entryPath]);
16365
+ const className = typeof entry?.className === "string" ? entry.className : void 0;
16366
+ if (className) {
16367
+ entryClassCounts.set(
16368
+ className,
16369
+ (entryClassCounts.get(className) || 0) + 1
16370
+ );
16371
+ }
16372
+ return { entryPath, entry, className };
16373
+ });
16374
+ const displayGroupId = entryMetadata.length > 1 ? `message:${messageId || jobId || ts}:entries` : void 0;
16375
+ for (const [
16376
+ index,
16377
+ { entryPath, entry, className }
16378
+ ] of entryMetadata.entries()) {
15257
16379
  pushReferent({
15258
16380
  id: `entry:${entryPath}`,
15259
16381
  kind: "entry",
15260
16382
  ref: entryPath,
16383
+ role: "assistant",
16384
+ source: "heap_objects",
15261
16385
  entryPath,
15262
- className: typeof entry?.className === "string" ? entry.className : void 0,
16386
+ recordId: typeof entry?.id === "string" ? entry.id : void 0,
16387
+ className,
15263
16388
  label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : entryPath,
16389
+ ...displayGroupId ? {
16390
+ displayGroupId,
16391
+ displayGroupIndex: index,
16392
+ displayGroupSize: entryMetadata.length,
16393
+ ...className && (entryClassCounts.get(className) || 0) > 1 ? { displayGroupSameTypeSize: entryClassCounts.get(className) } : {}
16394
+ } : {},
15264
16395
  messageId,
15265
16396
  jobId,
15266
16397
  ts
@@ -15272,6 +16403,8 @@ function collectConversationReferents(liveDoc) {
15272
16403
  id: `list:${listName}`,
15273
16404
  kind: "list",
15274
16405
  ref: listName,
16406
+ role: "assistant",
16407
+ source: "heap_objects",
15275
16408
  listName,
15276
16409
  className: typeof list?.className === "string" ? list.className : void 0,
15277
16410
  count: Array.isArray(list?.paths) ? list.paths.length : null,
@@ -15292,9 +16425,12 @@ function collectConversationReferents(liveDoc) {
15292
16425
  id: `variable:${variableName}`,
15293
16426
  kind: "variable",
15294
16427
  ref: variableName,
16428
+ role: "assistant",
16429
+ source: "heap_objects",
15295
16430
  variableName,
15296
16431
  variableKind: typeof variable?.kind === "string" ? variable.kind : void 0,
15297
16432
  entryPath,
16433
+ recordId: typeof entry?.id === "string" ? entry.id : void 0,
15298
16434
  listName,
15299
16435
  className: typeof variable?.className === "string" ? variable.className : typeof entry?.className === "string" ? entry.className : typeof list?.className === "string" ? list.className : void 0,
15300
16436
  label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : null,
@@ -15315,18 +16451,24 @@ function projectConversationReferentFocus(liveDoc) {
15315
16451
  const entryPaths = [];
15316
16452
  const listNames = [];
15317
16453
  const variableNames = [];
15318
- for (const referent of referents.slice(0, 8)) {
15319
- if (referent.kind === "entry" && typeof referent.entryPath === "string") {
16454
+ let entryCount = 0;
16455
+ let listCount = 0;
16456
+ let variableCount = 0;
16457
+ for (const referent of referents) {
16458
+ if (referent.kind === "entry" && typeof referent.entryPath === "string" && entryCount < 8) {
16459
+ entryCount += 1;
15320
16460
  entryPaths.push(referent.entryPath);
15321
16461
  continue;
15322
16462
  }
15323
- if (referent.kind === "list" && typeof referent.listName === "string") {
16463
+ if (referent.kind === "list" && typeof referent.listName === "string" && listCount < 4) {
16464
+ listCount += 1;
15324
16465
  listNames.push(referent.listName);
15325
16466
  const list = asRecord4(listsByName[referent.listName]);
15326
16467
  entryPaths.push(...asArray2(list?.paths).slice(0, 4));
15327
16468
  continue;
15328
16469
  }
15329
- if (referent.kind === "variable" && typeof referent.variableName === "string") {
16470
+ if (referent.kind === "variable" && typeof referent.variableName === "string" && variableCount < 4) {
16471
+ variableCount += 1;
15330
16472
  variableNames.push(referent.variableName);
15331
16473
  if (typeof referent.entryPath === "string") {
15332
16474
  entryPaths.push(referent.entryPath);
@@ -15344,61 +16486,91 @@ function projectConversationReferentFocus(liveDoc) {
15344
16486
  variableNames: uniqueStrings(variableNames, 4)
15345
16487
  };
15346
16488
  }
15347
- function projectConversationReferentSummary(liveDoc) {
15348
- const referents = collectConversationReferents(liveDoc).slice(0, 8);
15349
- if (referents.length === 0) {
15350
- return "No recent referents recorded from prior assistant replies.";
15351
- }
15352
- const entryLines = [];
15353
- const listLines = [];
15354
- const variableLines = [];
16489
+ function selectConversationReferentsForPrompt(referents) {
16490
+ const selected = [];
16491
+ const seen = /* @__PURE__ */ new Set();
16492
+ let entryCount = 0;
16493
+ let listCount = 0;
16494
+ let variableCount = 0;
15355
16495
  for (const referent of referents) {
16496
+ if (!referent.kind || !referent.ref) continue;
16497
+ const key = `${referent.kind}:${referent.ref}`;
16498
+ if (seen.has(key)) continue;
16499
+ if (referent.kind === "entry") {
16500
+ if (entryCount >= 8) continue;
16501
+ entryCount += 1;
16502
+ } else if (referent.kind === "list") {
16503
+ if (listCount >= 4) continue;
16504
+ listCount += 1;
16505
+ } else if (referent.kind === "variable") {
16506
+ if (variableCount >= 4) continue;
16507
+ variableCount += 1;
16508
+ }
16509
+ seen.add(key);
16510
+ selected.push(referent);
16511
+ }
16512
+ return selected;
16513
+ }
16514
+ function projectConversationReferentSummary(liveDoc) {
16515
+ const referents = selectConversationReferentsForPrompt(
16516
+ collectConversationReferents(liveDoc)
16517
+ );
16518
+ const compact = referents.map((referent) => {
15356
16519
  if (referent.kind === "entry" && referent.entryPath) {
15357
- const label = referent.label || referent.entryPath;
15358
- const classLabel = referent.className || "unknown";
15359
- entryLines.push(`- ${label} <${referent.entryPath}> [${classLabel}]`);
15360
- continue;
16520
+ return {
16521
+ kind: "entry",
16522
+ role: referent.role || null,
16523
+ source: referent.source || null,
16524
+ path: referent.entryPath,
16525
+ id: referent.recordId || null,
16526
+ type: referent.className || "unknown",
16527
+ label: referent.label || referent.entryPath,
16528
+ group: referent.displayGroupId ? {
16529
+ id: referent.displayGroupId,
16530
+ index: typeof referent.displayGroupIndex === "number" ? referent.displayGroupIndex : null,
16531
+ size: typeof referent.displayGroupSize === "number" ? referent.displayGroupSize : null,
16532
+ sameTypeSize: typeof referent.displayGroupSameTypeSize === "number" ? referent.displayGroupSameTypeSize : null
16533
+ } : void 0
16534
+ };
16535
+ }
16536
+ if (referent.kind === "entry" && referent.recordId) {
16537
+ return {
16538
+ kind: "entry",
16539
+ role: referent.role || null,
16540
+ source: referent.source || null,
16541
+ id: referent.recordId,
16542
+ type: referent.className || "unknown",
16543
+ label: referent.label || referent.recordId
16544
+ };
15361
16545
  }
15362
16546
  if (referent.kind === "list" && referent.listName) {
15363
- const classLabel = referent.className || "unknown";
15364
- const countLabel = typeof referent.count === "number" ? referent.count : "?";
15365
- listLines.push(
15366
- `- ${referent.listName}: list<${classLabel}> -> ${countLabel} item(s)`
15367
- );
15368
- continue;
16547
+ return {
16548
+ kind: "list",
16549
+ role: referent.role || null,
16550
+ source: referent.source || null,
16551
+ name: referent.listName,
16552
+ type: referent.className || "unknown",
16553
+ count: typeof referent.count === "number" ? referent.count : null
16554
+ };
15369
16555
  }
15370
16556
  if (referent.kind === "variable" && referent.variableName) {
15371
- if (referent.variableKind === "entry" && referent.entryPath && referent.className) {
15372
- const label = referent.label || referent.entryPath;
15373
- variableLines.push(
15374
- `- ${referent.variableName}: entry<${referent.className}> -> ${label} <${referent.entryPath}>`
15375
- );
15376
- continue;
15377
- }
15378
- if (referent.variableKind === "list" && referent.listName && referent.className) {
15379
- const countLabel = typeof referent.count === "number" ? referent.count : "?";
15380
- variableLines.push(
15381
- `- ${referent.variableName}: list<${referent.className}> -> ${countLabel} item(s) via ${referent.listName}`
15382
- );
15383
- continue;
15384
- }
15385
- if (referent.variableKind === "scalar") {
15386
- variableLines.push(
15387
- `- ${referent.variableName}: scalar = ${formatScalar(referent.scalarValue)}`
15388
- );
15389
- continue;
15390
- }
15391
- variableLines.push(`- ${referent.variableName}`);
16557
+ return {
16558
+ kind: "variable",
16559
+ role: referent.role || null,
16560
+ source: referent.source || null,
16561
+ name: referent.variableName,
16562
+ valueKind: referent.variableKind || null,
16563
+ type: referent.className || null,
16564
+ path: referent.entryPath || null,
16565
+ list: referent.listName || null,
16566
+ label: referent.label || null,
16567
+ count: typeof referent.count === "number" ? referent.count : null,
16568
+ value: referent.variableKind === "scalar" ? referent.scalarValue ?? null : void 0
16569
+ };
15392
16570
  }
15393
- }
15394
- const lines = [];
15395
- lines.push("Entries:");
15396
- lines.push(...entryLines.length > 0 ? entryLines : ["- none"]);
15397
- lines.push("", "Lists:");
15398
- lines.push(...listLines.length > 0 ? listLines : ["- none"]);
15399
- lines.push("", "Variables:");
15400
- lines.push(...variableLines.length > 0 ? variableLines : ["- none"]);
15401
- return lines.join("\n");
16571
+ return null;
16572
+ }).filter(Boolean);
16573
+ return renderConstBlock("recentReferences", compact);
15402
16574
  }
15403
16575
  function getCurrentClosureId(liveDoc) {
15404
16576
  const loop = asRecord4(liveDoc?.loop);
@@ -15619,56 +16791,24 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
15619
16791
  }
15620
16792
  function projectWorkflowSummary(liveDoc, pendingPrompts = [], options) {
15621
16793
  const focus = projectWorkflowFocus(liveDoc, pendingPrompts, options);
15622
- const lines = [];
15623
- lines.push("Workflow Boundary:");
15624
- if (focus.boundaryReason === "request_start") {
15625
- lines.push(
15626
- "- Start from work recorded after the current user request began."
15627
- );
15628
- } else if (focus.boundaryReason === "last_closed_loop" && focus.latestClosureId) {
15629
- lines.push(`- Start from work recorded after ${focus.latestClosureId}.`);
15630
- } else {
15631
- lines.push(
15632
- "- No prior closed loop recorded; use the latest user request as the boundary."
15633
- );
15634
- }
15635
- lines.push("", "Recent Actions:");
15636
- if (focus.recentActionSummary.length === 0) {
15637
- lines.push("- none");
15638
- } else {
15639
- for (const line of focus.recentActionSummary) {
15640
- lines.push(line.startsWith("- ") ? line : `- ${line}`);
15641
- }
15642
- }
15643
- lines.push("", "Working Set Hints:");
15644
- if (focus.variableNames.length === 0 && focus.listNames.length === 0 && focus.entryPaths.length === 0) {
15645
- lines.push("- none");
15646
- } else {
15647
- if (focus.variableNames.length > 0) {
15648
- lines.push(`- variables: ${focus.variableNames.join(", ")}`);
15649
- }
15650
- if (focus.listNames.length > 0) {
15651
- lines.push(`- lists: ${focus.listNames.join(", ")}`);
15652
- }
15653
- if (focus.entryPaths.length > 0) {
15654
- lines.push(`- entries: ${focus.entryPaths.join(", ")}`);
15655
- }
15656
- }
15657
- lines.push("", "Open Workflow Handles:");
15658
- if (focus.activeTaskIds.length === 0 && focus.openDecisionIds.length === 0 && focus.openPromptIds.length === 0) {
15659
- lines.push("- none");
15660
- } else {
15661
- if (focus.activeTaskIds.length > 0) {
15662
- lines.push(`- tasks: ${focus.activeTaskIds.join(", ")}`);
15663
- }
15664
- if (focus.openDecisionIds.length > 0) {
15665
- lines.push(`- decisions: ${focus.openDecisionIds.join(", ")}`);
15666
- }
15667
- if (focus.openPromptIds.length > 0) {
15668
- lines.push(`- prompts: ${focus.openPromptIds.join(", ")}`);
16794
+ return renderConstBlock("workflowContext", {
16795
+ boundary: {
16796
+ timestamp: focus.boundaryTimestamp,
16797
+ reason: focus.boundaryReason,
16798
+ latestClosureId: focus.latestClosureId || null
16799
+ },
16800
+ recentActions: focus.recentActionSummary,
16801
+ workingSet: {
16802
+ variables: focus.variableNames,
16803
+ lists: focus.listNames,
16804
+ entries: focus.entryPaths
16805
+ },
16806
+ openHandles: {
16807
+ tasks: focus.activeTaskIds,
16808
+ decisions: focus.openDecisionIds,
16809
+ prompts: focus.openPromptIds
15669
16810
  }
15670
- }
15671
- return lines.join("\n");
16811
+ });
15672
16812
  }
15673
16813
  function hasOpenPrompt(liveDoc, pendingPrompts) {
15674
16814
  if (pendingPrompts.length > 0) return true;
@@ -15689,7 +16829,6 @@ function getExclusivePromptTarget(pendingPrompts) {
15689
16829
  return prompt?.type === "input" ? prompt : null;
15690
16830
  }
15691
16831
  function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15692
- const lines = [];
15693
16832
  const loop = asRecord4(liveDoc?.loop);
15694
16833
  const boundary = getWorkflowBoundary(liveDoc, options);
15695
16834
  const tasks = toSortedRecords(loop?.tasksById).filter((task) => {
@@ -15711,22 +16850,12 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15711
16850
  5
15712
16851
  );
15713
16852
  const hiddenTaskCount = Math.max(0, activeTasks.length - visibleTasks.length);
15714
- lines.push("Tasks:");
15715
- if (visibleTasks.length === 0) {
15716
- lines.push("- none");
15717
- } else {
15718
- lines.push("- Reuse existing taskId values exactly as written below.");
15719
- for (const task of visibleTasks) {
15720
- const title = typeof task.title === "string" ? task.title : "Untitled task";
15721
- const taskId = typeof task.taskId === "string" ? task.taskId : "unknown";
15722
- const status = typeof task.status === "string" ? task.status : "pending";
15723
- const summary = typeof task.summary === "string" && task.summary.trim() ? ` \u2014 ${task.summary.trim()}` : "";
15724
- lines.push(`- [${status}] ${title} (${taskId})${summary}`);
15725
- }
15726
- if (hiddenTaskCount > 0) {
15727
- lines.push(`- ${hiddenTaskCount} more active task(s) omitted`);
15728
- }
15729
- }
16853
+ const compactTasks = visibleTasks.map((task) => ({
16854
+ id: typeof task.taskId === "string" ? task.taskId : "unknown",
16855
+ title: typeof task.title === "string" ? task.title : "Untitled task",
16856
+ status: typeof task.status === "string" ? task.status : "pending",
16857
+ summary: typeof task.summary === "string" && task.summary.trim() ? task.summary.trim() : null
16858
+ }));
15730
16859
  const decisions = toSortedRecords(loop?.decisionsById).filter((decision) => {
15731
16860
  const updatedAt = Number(decision.updatedAt) || Number(decision.createdAt) || 0;
15732
16861
  if (boundary.reason === "request_start") {
@@ -15740,33 +16869,29 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15740
16869
  (decision) => decision.status === "open"
15741
16870
  );
15742
16871
  const visibleDecisions = (openDecisions.length > 0 ? openDecisions : decisions.slice(0, 1)).slice(0, 3);
15743
- lines.push("", "Recent Decisions:");
15744
- if (visibleDecisions.length === 0) {
15745
- lines.push("- none");
15746
- } else {
15747
- lines.push("- Reuse existing decisionId values exactly as written below.");
15748
- for (const decision of visibleDecisions) {
15749
- const status = typeof decision.status === "string" ? decision.status : "resolved";
15750
- const title = typeof decision.title === "string" && decision.title.trim() ? decision.title.trim() : "Decision";
15751
- const decisionId = typeof decision.decisionId === "string" ? decision.decisionId : "unknown";
15752
- if (status === "open") {
15753
- const candidatePreview = asArray2(decision.candidates).slice(0, 3).map((candidate) => {
15754
- const record = asRecord4(candidate);
15755
- if (!record) return null;
15756
- const candidateId = typeof record.id === "string" ? record.id : "unknown";
15757
- const candidateLabel = typeof record.label === "string" && record.label.trim() ? record.label.trim() : candidateId;
15758
- return candidateLabel === candidateId ? candidateId : `${candidateLabel} (${candidateId})`;
15759
- }).filter((value) => Boolean(value)).join(", ");
15760
- lines.push(
15761
- `- [open] ${title} (${decisionId})${candidatePreview ? ` \u2014 candidates: ${candidatePreview}` : ""}`
15762
- );
15763
- } else {
15764
- const selected = asRecord4(decision.selected);
15765
- const label = typeof selected?.label === "string" ? selected.label : typeof selected?.id === "string" ? selected.id : "unknown";
15766
- lines.push(`- [resolved] ${title} (${decisionId}) -> ${label}`);
16872
+ const compactDecisions = visibleDecisions.map((decision) => {
16873
+ const status = typeof decision.status === "string" ? decision.status : "resolved";
16874
+ const selected = asRecord4(decision.selected);
16875
+ return {
16876
+ id: typeof decision.decisionId === "string" ? decision.decisionId : "unknown",
16877
+ title: typeof decision.title === "string" && decision.title.trim() ? decision.title.trim() : "Decision",
16878
+ status,
16879
+ candidates: status === "open" ? asArray2(decision.candidates).slice(0, 5).map((candidate) => {
16880
+ const record = asRecord4(candidate);
16881
+ if (!record) return null;
16882
+ return {
16883
+ id: typeof record.id === "string" ? record.id : "unknown",
16884
+ label: typeof record.label === "string" && record.label.trim() ? record.label.trim() : null,
16885
+ description: typeof record.description === "string" && record.description.trim() ? record.description.trim() : null,
16886
+ metadata: asRecord4(record.metadata)
16887
+ };
16888
+ }).filter(Boolean) : [],
16889
+ selected: status === "open" ? null : {
16890
+ id: typeof selected?.id === "string" ? selected.id : null,
16891
+ label: typeof selected?.label === "string" ? selected.label : null
15767
16892
  }
15768
- }
15769
- }
16893
+ };
16894
+ });
15770
16895
  const openPrompts = [
15771
16896
  ...pendingPrompts.map((prompt) => ({
15772
16897
  id: prompt.id,
@@ -15786,29 +16911,29 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15786
16911
  (pendingPrompt) => pendingPrompt.id === promptId
15787
16912
  ) : false);
15788
16913
  }) : openPrompts;
15789
- lines.push("", "Open Prompts:");
15790
- if (visiblePrompts.length === 0) {
15791
- lines.push("- none");
15792
- } else {
15793
- for (const prompt of visiblePrompts.slice(0, 3)) {
15794
- const title = typeof prompt.title === "string" && prompt.title.trim() ? prompt.title.trim() : "Input required";
15795
- const type = typeof prompt.type === "string" ? prompt.type : "input";
15796
- const message = typeof prompt.message === "string" && prompt.message.trim() ? ` \u2014 ${prompt.message.trim()}` : "";
15797
- lines.push(`- [${type}] ${title}${message}`);
15798
- }
15799
- }
16914
+ const compactPrompts = visiblePrompts.slice(0, 3).map((prompt) => {
16915
+ const promptRecord = asRecord4(prompt) || {};
16916
+ return {
16917
+ id: typeof promptRecord.id === "string" ? promptRecord.id : typeof promptRecord.promptId === "string" ? promptRecord.promptId : null,
16918
+ type: typeof promptRecord.type === "string" ? promptRecord.type : "input",
16919
+ title: typeof promptRecord.title === "string" && promptRecord.title.trim() ? promptRecord.title.trim() : "Input required",
16920
+ message: typeof promptRecord.message === "string" && promptRecord.message.trim() ? promptRecord.message.trim() : null
16921
+ };
16922
+ });
15800
16923
  const currentClosureId = getCurrentClosureId(liveDoc);
15801
16924
  const closureRecord = currentClosureId ? asRecord4(asRecord4(loop?.closuresById)?.[currentClosureId]) : null;
15802
16925
  const visibleClosure = closureRecord && (boundary.reason !== "request_start" || (Number(closureRecord.createdAt) || 0) >= boundary.timestamp) ? closureRecord : null;
15803
- lines.push("", "Loop Closure:");
15804
- if (visibleClosure) {
15805
- const status = typeof visibleClosure.status === "string" ? visibleClosure.status : "completed";
15806
- const summary = typeof visibleClosure.summary === "string" ? visibleClosure.summary : "No summary";
15807
- lines.push(`- current: [${status}] ${summary} (${currentClosureId})`);
15808
- } else {
15809
- lines.push("- none");
15810
- }
15811
- return lines.join("\n");
16926
+ return renderConstBlock("workflowState", {
16927
+ tasks: compactTasks,
16928
+ hiddenActiveTaskCount: hiddenTaskCount,
16929
+ decisions: compactDecisions,
16930
+ openPrompts: compactPrompts,
16931
+ closure: visibleClosure ? {
16932
+ id: currentClosureId,
16933
+ status: typeof visibleClosure.status === "string" ? visibleClosure.status : "completed",
16934
+ summary: typeof visibleClosure.summary === "string" ? visibleClosure.summary : null
16935
+ } : null
16936
+ });
15812
16937
  }
15813
16938
  function projectHeapSummary(heap, options) {
15814
16939
  const heapRecord = asRecord4(heap) || {};
@@ -15853,55 +16978,72 @@ function projectHeapSummary(heap, options) {
15853
16978
  referencedPaths.add(path);
15854
16979
  }
15855
16980
  const visibleLists = Object.values(listsByName).map((value) => asRecord4(value)).filter((value) => Boolean(value)).filter(
15856
- (list) => variables.some((variable) => variable.listName === list.name) || Boolean(list.name && focusedListNames.has(list.name))
16981
+ (list) => variables.some(
16982
+ (variable) => Boolean(variable?.listName === list.name)
16983
+ ) || Boolean(list.name && focusedListNames.has(list.name))
15857
16984
  ).sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0)).slice(0, maxLists);
15858
16985
  const visibleEntries = Object.values(entriesByPath).map((value) => asRecord4(value)).filter((value) => Boolean(value)).filter((entry) => entry.path && referencedPaths.has(entry.path)).sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0)).slice(0, maxEntries);
15859
- const lines = [];
15860
- lines.push("Variables:");
15861
- if (variables.length === 0) {
15862
- lines.push("- none");
15863
- } else {
15864
- for (const variable of variables) {
15865
- if (variable.kind === "scalar") {
15866
- lines.push(
15867
- `- ${variable.name}: scalar = ${formatScalar(variable.value)}`
15868
- );
15869
- continue;
15870
- }
15871
- if (variable.kind === "entry") {
15872
- const entry = variable.entryPath ? asRecord4(
15873
- entriesByPath[variable.entryPath]
15874
- ) : null;
15875
- lines.push(
15876
- `- ${variable.name}: entry<${variable.className || entry?.className || "unknown"}> -> ${entry ? describeHeapEntry(entry) : variable.entryPath || "missing"}`
15877
- );
15878
- continue;
15879
- }
15880
- const list = variable.listName ? asRecord4(listsByName[variable.listName]) : null;
15881
- lines.push(
15882
- `- ${variable.name}: list<${variable.className || list?.className || "unknown"}> -> ${(list?.paths || []).length} item(s)`
15883
- );
15884
- }
15885
- }
15886
- lines.push("", "Named Lists:");
15887
- if (visibleLists.length === 0) {
15888
- lines.push("- none");
15889
- } else {
15890
- for (const list of visibleLists) {
15891
- lines.push(
15892
- `- ${list.name}: ${list.className || "unknown"}[${(list.paths || []).length}]`
15893
- );
15894
- }
15895
- }
15896
- lines.push("", "Active Entries:");
15897
- if (visibleEntries.length === 0) {
15898
- lines.push("- none");
15899
- } else {
15900
- for (const entry of visibleEntries) {
15901
- lines.push(`- ${describeHeapEntry(entry)}`);
15902
- }
15903
- }
15904
- return lines.join("\n");
16986
+ return renderConstBlock("savedData", {
16987
+ variables: Object.fromEntries(
16988
+ variables.filter((variable) => typeof variable.name === "string").map((variable) => {
16989
+ if (variable.kind === "scalar") {
16990
+ return [
16991
+ variable.name,
16992
+ { kind: "scalar", value: variable.value ?? null }
16993
+ ];
16994
+ }
16995
+ if (variable.kind === "entry") {
16996
+ const entry = variable.entryPath ? asRecord4(
16997
+ entriesByPath[variable.entryPath]
16998
+ ) : null;
16999
+ return [
17000
+ variable.name,
17001
+ {
17002
+ kind: "entry",
17003
+ type: variable.className || entry?.className || "unknown",
17004
+ path: variable.entryPath || null,
17005
+ label: entry?.label || entry?.id || null
17006
+ }
17007
+ ];
17008
+ }
17009
+ const list = variable.listName ? asRecord4(listsByName[variable.listName]) : null;
17010
+ return [
17011
+ variable.name,
17012
+ {
17013
+ kind: "list",
17014
+ type: variable.className || list?.className || "unknown",
17015
+ list: variable.listName || null,
17016
+ count: (list?.paths || []).length
17017
+ }
17018
+ ];
17019
+ })
17020
+ ),
17021
+ lists: Object.fromEntries(
17022
+ visibleLists.filter((list) => typeof list.name === "string").map((list) => [
17023
+ list.name,
17024
+ {
17025
+ type: list.className || "unknown",
17026
+ count: (list.paths || []).length
17027
+ }
17028
+ ])
17029
+ ),
17030
+ entries: Object.fromEntries(
17031
+ visibleEntries.filter((entry) => typeof entry.path === "string").map((entry) => [
17032
+ entry.path,
17033
+ {
17034
+ type: entry.className || "unknown",
17035
+ id: entry.id || null,
17036
+ label: entry.label || entry.id || null,
17037
+ fields: asArray2(entry.fields).filter(
17038
+ (field) => field?.name && field.name !== "_realId" && field.name !== "real_id"
17039
+ ).slice(0, 3).map((field) => ({
17040
+ name: field.name,
17041
+ value: field.value ?? null
17042
+ }))
17043
+ }
17044
+ ])
17045
+ )
17046
+ });
15905
17047
  }
15906
17048
  function createHarnessVerifierSnapshot(input) {
15907
17049
  const workflowFocus = projectWorkflowFocus(
@@ -15998,8 +17140,8 @@ function buildContinuationInstruction(resultPreview) {
15998
17140
  "If the user names a concrete record that is not already in the heap, resolve it from the graph before saying it is missing: try a broad search, then a small set of normalized/fuzzy variants or a paged scan when the domain supports it.",
15999
17141
  "If the request needs all matching records, use iterate(...) or page until hasMore is false. A single list(...) or page(...) call is only one page.",
16000
17142
  "If this request clearly spans multiple steps and there are no active tasks yet, create 2-4 short user-visible tasks now.",
16001
- "Reuse any existing taskId and decisionId values exactly as they appear in AGENT LOOP STATE.",
16002
- "When progress depends on the user's choice, missing detail, or approval, use loop.ask_user(...) or loop.confirm(...) so the job pauses and resumes through the live workflow.",
17143
+ "Reuse any existing taskId and decisionId values exactly as they appear in [State].",
17144
+ "When progress depends on the user's choice, missing detail, or confirmation, use loop.ask_user(...) or loop.confirm(...) so the job pauses and resumes through the live workflow.",
16003
17145
  "After a resumed ask_user or confirm call, continue the same job and perform the newly authorized action when the answer is sufficient. Do not stop with placeholder text like 'I'm ready to do it next.'",
16004
17146
  "If you ask the user a new question in this job, do not also close the loop in the same job.",
16005
17147
  "Write the smallest straightforward code for the current step. Avoid defensive fallback branches for hypothetical states that are not currently true.",
@@ -16010,39 +17152,101 @@ ${resultPreview}` : null
16010
17152
  ].filter(Boolean).join("\n\n");
16011
17153
  }
16012
17154
  function buildGranularAgentDomainBlock(domainDocumentation) {
16013
- return domainDocumentation?.trim() || "No domain reference available. The graph may not be ready yet.";
17155
+ return domainDocumentation?.trim() || "No domain contract available. The graph may not be ready yet.";
16014
17156
  }
16015
17157
  function buildGranularAgentSessionBlock(sessionContext) {
16016
- if (!sessionContext) return "No session metadata available.";
16017
- const rows = [
16018
- ["sandboxId", sessionContext.sandboxId],
16019
- ["environmentId", sessionContext.environmentId],
16020
- ["userName", sessionContext.userName]
16021
- ];
16022
- const activeRows = rows.filter(([, value]) => Boolean(value));
16023
- if (activeRows.length === 0) return "No session metadata available.";
16024
- return activeRows.map(([key, value]) => `${key}: ${value}`).join("\n");
17158
+ return renderConstBlock("session", {
17159
+ runtimeId: sessionContext?.sandboxId || null,
17160
+ environmentId: sessionContext?.environmentId || null,
17161
+ userName: sessionContext?.userName || null,
17162
+ domainRevision: sessionContext?.domainRevision || null
17163
+ });
16025
17164
  }
16026
17165
  function buildGranularAgentHeapBlock(heapSummary) {
16027
- return heapSummary?.trim() || "Heap is empty for this session.";
17166
+ return heapSummary?.trim() || renderConstBlock("savedData", {
17167
+ variables: {},
17168
+ lists: {},
17169
+ entries: {}
17170
+ });
16028
17171
  }
16029
17172
  function buildGranularAgentReferentBlock(referentSummary) {
16030
- return referentSummary?.trim() || "No recent referents recorded from prior assistant replies.";
17173
+ return referentSummary?.trim() || renderConstBlock("recentReferences", []);
16031
17174
  }
16032
17175
  function buildGranularAgentLoopBlock(loopSummary) {
16033
- return loopSummary?.trim() || "No active loop state recorded for this session.";
17176
+ return loopSummary?.trim() || renderConstBlock("workflowState", {
17177
+ tasks: [],
17178
+ decisions: [],
17179
+ openPrompts: [],
17180
+ closure: null
17181
+ });
16034
17182
  }
16035
17183
  function buildGranularAgentWorkflowBlock(workflowSummary) {
16036
- return workflowSummary?.trim() || "No current workflow snapshot recorded for this request yet.";
17184
+ return workflowSummary?.trim() || renderConstBlock("workflowContext", {
17185
+ boundary: null,
17186
+ recentActions: [],
17187
+ workingSet: {
17188
+ variables: [],
17189
+ lists: [],
17190
+ entries: []
17191
+ },
17192
+ openHandles: {
17193
+ tasks: [],
17194
+ decisions: [],
17195
+ prompts: []
17196
+ }
17197
+ });
16037
17198
  }
16038
- function buildGranularAgentToolBlock(tools) {
17199
+ function resolvePromptCapabilities(capabilities) {
17200
+ return {
17201
+ executeCode: capabilities?.executeCode !== false,
17202
+ readEntities: capabilities?.readEntities !== false,
17203
+ workflowHelpers: Array.isArray(capabilities?.workflowHelpers) ? capabilities.workflowHelpers : [
17204
+ "ask_user",
17205
+ "confirm",
17206
+ "open_decision",
17207
+ "close_decision",
17208
+ "create_task",
17209
+ "update_task",
17210
+ "complete_task",
17211
+ "close_loop"
17212
+ ],
17213
+ savedData: capabilities?.savedData !== false,
17214
+ showRecords: capabilities?.showRecords !== false
17215
+ };
17216
+ }
17217
+ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
17218
+ const resolvedCapabilities = resolvePromptCapabilities(capabilityOverrides);
17219
+ const normalizedTools = (tools || []).filter((tool) => tool?.name).slice().sort((left, right) => {
17220
+ const leftScope = `${left.className || "global"}:${left.static ? "static" : "instance"}`;
17221
+ const rightScope = `${right.className || "global"}:${right.static ? "static" : "instance"}`;
17222
+ return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
17223
+ });
17224
+ const writeActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
17225
+ const scope = tool.className ? `${tool.static ? "class" : "record"}:${tool.className}` : "global";
17226
+ return {
17227
+ name: tool.name,
17228
+ scope,
17229
+ description: tool.description?.trim() || null
17230
+ };
17231
+ });
17232
+ const capabilities = {
17233
+ executeCode: resolvedCapabilities.executeCode,
17234
+ readEntities: resolvedCapabilities.readEntities,
17235
+ writeActions,
17236
+ workflowHelpers: resolvedCapabilities.workflowHelpers,
17237
+ savedData: resolvedCapabilities.savedData,
17238
+ showRecords: resolvedCapabilities.showRecords
17239
+ };
17240
+ return renderConstBlock("capabilities", capabilities);
17241
+ }
17242
+ function buildGranularAgentActionIndex(tools) {
16039
17243
  const normalizedTools = (tools || []).filter((tool) => tool?.name).slice().sort((left, right) => {
16040
17244
  const leftScope = `${left.className || "global"}:${left.static ? "static" : "instance"}`;
16041
17245
  const rightScope = `${right.className || "global"}:${right.static ? "static" : "instance"}`;
16042
17246
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
16043
17247
  });
16044
17248
  if (normalizedTools.length === 0) {
16045
- return "No live effects are available in this session yet.";
17249
+ return "No domain write actions are available.";
16046
17250
  }
16047
17251
  const globalTools = normalizedTools.filter((tool) => !tool.className);
16048
17252
  const staticTools = normalizedTools.filter(
@@ -16051,9 +17255,7 @@ function buildGranularAgentToolBlock(tools) {
16051
17255
  const instanceTools = normalizedTools.filter(
16052
17256
  (tool) => Boolean(tool.className && !tool.static)
16053
17257
  );
16054
- const lines = [
16055
- "Treat this block as the planning map. Use DOMAIN REFERENCE below for exact signatures and query examples."
16056
- ];
17258
+ const lines = ["Available actions by scope:"];
16057
17259
  const appendGroup = (title, group) => {
16058
17260
  lines.push(`- ${title}:`);
16059
17261
  if (group.length === 0) {
@@ -16062,192 +17264,567 @@ function buildGranularAgentToolBlock(tools) {
16062
17264
  }
16063
17265
  for (const tool of group.slice(0, 10)) {
16064
17266
  const availability = tool.ready === false ? " [not ready]" : "";
17267
+ const schema = formatActionSchemaSummary(tool);
16065
17268
  const description = tool.description?.trim() ? ` - ${tool.description.trim()}` : "";
16066
- lines.push(` ${tool.name}${availability}${description}`);
17269
+ lines.push(` ${tool.name}${availability}${schema}${description}`);
16067
17270
  }
16068
17271
  if (group.length > 10) {
16069
17272
  lines.push(` +${group.length - 10} more`);
16070
17273
  }
16071
17274
  };
16072
- appendGroup("Global effects", globalTools);
16073
- appendGroup("Class-level effects", staticTools);
16074
- appendGroup("Record-level effects", instanceTools);
17275
+ appendGroup("Global", globalTools);
17276
+ appendGroup("Class-level", staticTools);
17277
+ appendGroup("Record-level", instanceTools);
16075
17278
  return lines.join("\n");
16076
17279
  }
16077
- function buildGranularAgentCheckpointBlock(checkpoint) {
16078
- if (!checkpoint) {
16079
- return "No previous execution checkpoint recorded for this request yet.";
16080
- }
16081
- const lines = [];
16082
- if (typeof checkpoint.iteration === "number") {
16083
- lines.push(`iteration: ${checkpoint.iteration}`);
16084
- }
16085
- if (checkpoint.latestJobStatus) {
16086
- lines.push(`latestJobStatus: ${checkpoint.latestJobStatus}`);
16087
- }
16088
- if (checkpoint.controllerOutcome) {
16089
- lines.push(`controllerOutcome: ${checkpoint.controllerOutcome}`);
17280
+ function normalizeJsonSchema(value) {
17281
+ if (typeof value === "string") {
17282
+ try {
17283
+ return asRecord4(JSON.parse(value));
17284
+ } catch {
17285
+ return null;
17286
+ }
16090
17287
  }
16091
- if (checkpoint.controllerReason) {
16092
- lines.push(`controllerReason: ${checkpoint.controllerReason}`);
17288
+ return asRecord4(value);
17289
+ }
17290
+ function jsonSchemaTypeName(schema) {
17291
+ const record = normalizeJsonSchema(schema);
17292
+ if (!record) return "unknown";
17293
+ const type = record.type;
17294
+ if (typeof type === "string") {
17295
+ if (type === "array") return "array";
17296
+ if (type === "object") return "object";
17297
+ return type;
16093
17298
  }
16094
- if (typeof checkpoint.noProgressCount === "number") {
16095
- lines.push(`noProgressCount: ${checkpoint.noProgressCount}`);
17299
+ return "unknown";
17300
+ }
17301
+ function summarizeObjectSchema(schema) {
17302
+ const record = normalizeJsonSchema(schema);
17303
+ const properties = asRecord4(record?.properties);
17304
+ if (!properties || Object.keys(properties).length === 0) {
17305
+ return record ? "{}" : null;
17306
+ }
17307
+ const required = new Set(asArray2(record?.required));
17308
+ const fields = Object.entries(properties).slice(0, 8).map(([name, property]) => {
17309
+ const marker = required.has(name) ? "*" : "?";
17310
+ return `${name}${marker}: ${jsonSchemaTypeName(property)}`;
17311
+ });
17312
+ const remaining = Object.keys(properties).length - fields.length;
17313
+ return remaining > 0 ? `${fields.join(", ")}, +${remaining}` : fields.join(", ");
17314
+ }
17315
+ function formatActionSchemaSummary(tool) {
17316
+ const input = summarizeObjectSchema(tool.inputSchema);
17317
+ const output = summarizeObjectSchema(tool.outputSchema);
17318
+ const parts = [];
17319
+ if (input) parts.push(`input { ${input} }`);
17320
+ if (output) parts.push(`output { ${output} }`);
17321
+ return parts.length ? ` (${parts.join("; ")})` : "";
17322
+ }
17323
+ function splitDomainDocumentation(domainDocumentation) {
17324
+ const normalized = domainDocumentation?.trim() || "";
17325
+ if (!normalized) return { types: "", docs: "" };
17326
+ const docsSectionMatch = normalized.match(/\n\s*\[Docs\]\s*\n/i);
17327
+ if (docsSectionMatch?.index !== void 0) {
17328
+ return {
17329
+ types: normalized.slice(0, docsSectionMatch.index).trim(),
17330
+ docs: normalized.slice(docsSectionMatch.index + docsSectionMatch[0].length).trim()
17331
+ };
16096
17332
  }
16097
- if (checkpoint.latestJobError?.trim()) {
16098
- lines.push(`latestJobError: ${checkpoint.latestJobError.trim()}`);
17333
+ const legacyMarker = "Generated usage notes from ./sandbox-tools docs:";
17334
+ const legacyIndex = normalized.indexOf(legacyMarker);
17335
+ if (legacyIndex !== -1) {
17336
+ return {
17337
+ types: normalized.slice(0, legacyIndex).trim(),
17338
+ docs: normalized.slice(legacyIndex + legacyMarker.length).trim()
17339
+ };
16099
17340
  }
16100
- if (Array.isArray(checkpoint.latestActionSummary) && checkpoint.latestActionSummary.length > 0) {
16101
- lines.push("latestActionSummary:");
16102
- for (const line of checkpoint.latestActionSummary.slice(0, 8)) {
16103
- const normalizedLine = normalizeActionSummaryForPrompt(line);
16104
- lines.push(
16105
- normalizedLine.startsWith("- ") ? normalizedLine : `- ${normalizedLine}`
16106
- );
17341
+ return { types: normalized, docs: "" };
17342
+ }
17343
+ function buildGranularAgentCheckpointBlock(checkpoint) {
17344
+ if (!checkpoint) {
17345
+ return renderConstBlock("previousCodeResult", null);
17346
+ }
17347
+ return renderConstBlock("previousCodeResult", {
17348
+ iteration: typeof checkpoint.iteration === "number" ? checkpoint.iteration : null,
17349
+ latestJobStatus: checkpoint.latestJobStatus || null,
17350
+ controllerOutcome: checkpoint.controllerOutcome || null,
17351
+ controllerReason: checkpoint.controllerReason || null,
17352
+ noProgressCount: typeof checkpoint.noProgressCount === "number" ? checkpoint.noProgressCount : null,
17353
+ latestJobError: checkpoint.latestJobError?.trim() || null,
17354
+ latestActionSummary: Array.isArray(checkpoint.latestActionSummary) ? checkpoint.latestActionSummary.slice(0, 8).map(normalizeActionSummaryForPrompt) : [],
17355
+ latestJobResult: checkpoint.latestJobResult?.trim() || null
17356
+ });
17357
+ }
17358
+ function parseSummaryOutcome(summary) {
17359
+ const outcome = {};
17360
+ for (const part of summary.split(",")) {
17361
+ const trimmed = part.trim();
17362
+ const match = /^([A-Za-z0-9_]+)=(.+)$/.exec(trimmed);
17363
+ if (!match) continue;
17364
+ const [, key, rawValue] = match;
17365
+ const unquoted = rawValue.replace(/^"|"$/g, "");
17366
+ if (/^-?\d+(?:\.\d+)?$/.test(unquoted)) {
17367
+ outcome[key] = Number(unquoted);
17368
+ } else if (unquoted === "true" || unquoted === "false") {
17369
+ outcome[key] = unquoted === "true";
17370
+ } else {
17371
+ outcome[key] = unquoted;
16107
17372
  }
16108
17373
  }
16109
- if (checkpoint.latestJobResult?.trim()) {
16110
- lines.push(`latestJobResult:
16111
- ${checkpoint.latestJobResult.trim()}`);
17374
+ return outcome;
17375
+ }
17376
+ function buildKnownFactsFromCheckpoint(checkpoint) {
17377
+ const summaries = Array.isArray(checkpoint?.latestActionSummary) ? checkpoint.latestActionSummary.map(normalizeActionSummaryForPrompt) : [];
17378
+ const facts = [];
17379
+ for (const summary of summaries) {
17380
+ const countedMatch = /^-\s*Counted\s+([A-Za-z0-9_]+).*?->\s*value=(\d+)/.exec(summary);
17381
+ if (countedMatch) {
17382
+ facts.push({
17383
+ entity: countedMatch[1],
17384
+ query: {},
17385
+ totalCount: Number(countedMatch[2])
17386
+ });
17387
+ continue;
17388
+ }
17389
+ const listedMatch = /^-\s*Listed\s+([A-Za-z0-9_]+).*?->\s*(.+)$/.exec(
17390
+ summary
17391
+ );
17392
+ if (!listedMatch) continue;
17393
+ const outcome = parseSummaryOutcome(listedMatch[2]);
17394
+ const count = typeof outcome.totalCount === "number" ? outcome.totalCount : typeof outcome.count === "number" ? outcome.count : void 0;
17395
+ if (typeof count !== "number") continue;
17396
+ const fact = {
17397
+ entity: listedMatch[1],
17398
+ query: {},
17399
+ totalCount: count
17400
+ };
17401
+ if (typeof outcome.hasMore === "boolean") {
17402
+ fact.lastPageHasMore = outcome.hasMore;
17403
+ fact.loadedAllItems = !outcome.hasMore;
17404
+ } else if (typeof outcome.count === "number" && outcome.count === count) {
17405
+ fact.loadedAllItems = true;
17406
+ }
17407
+ facts.push(fact);
16112
17408
  }
16113
- return lines.length > 0 ? lines.join("\n") : "No previous execution checkpoint recorded for this request yet.";
17409
+ return facts.slice(0, 8);
16114
17410
  }
16115
17411
  function buildGranularAgentSystemPrompt(input) {
17412
+ const outputMode = input.outputMode || "agentMessages";
17413
+ const promptCapabilities = resolvePromptCapabilities(input.capabilities);
17414
+ const domainSections = splitDomainDocumentation(input.domainDocumentation);
16116
17415
  const sessionBlock = buildGranularAgentSessionBlock(input.sessionContext);
16117
- const toolBlock = buildGranularAgentToolBlock(input.tools);
16118
- const domainBlock = buildGranularAgentDomainBlock(input.domainDocumentation);
17416
+ const toolBlock = buildGranularAgentToolBlock(
17417
+ input.tools,
17418
+ input.capabilities
17419
+ );
17420
+ const actionIndex = buildGranularAgentActionIndex(input.tools);
17421
+ const domainBlock = buildGranularAgentDomainBlock(domainSections.types);
16119
17422
  const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
16120
17423
  const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
16121
17424
  const heapBlock = buildGranularAgentHeapBlock(input.heapSummary);
16122
17425
  const referentBlock = buildGranularAgentReferentBlock(input.referentSummary);
16123
17426
  const loopBlock = buildGranularAgentLoopBlock(input.loopSummary);
16124
- return `You are an AI assistant for a live Granular session.
16125
- You can help the user understand the domain, answer questions, or generate and execute code against the live session.
16126
- Your tone must be natural and human-like.
17427
+ const knownFactsBlock = renderConstBlock(
17428
+ "knownFacts",
17429
+ buildKnownFactsFromCheckpoint(input.checkpoint)
17430
+ );
17431
+ const outputRules = outputMode === "returnValue" ? promptCapabilities.showRecords ? `- End every user-facing job by returning either a short natural-language string or an object like \`{ reply, show }\`.
17432
+ - Use \`{ reply, show }\` when the host UI should render records, heap variables, or lists from session state.
17433
+ - For multi-record display, prefer a saved list/listName so the UI can render a table; use entryPaths for a few individual records.
17434
+ - When the user asks to show, list, display, open, or "show them" for records you found, include those heap-backed records in \`show\`; do not answer only with a count or text summary.
17435
+ - For count-only questions such as "how many", "how many X do I have", or "what is the total number of X", call the entity \`.count(...)\` or use page \`totalCount\` only when a page is already needed for other reasons. Answer with text only. Do not fetch, save, or display sample records just to ground a numeric count.
17436
+ - Do not call \`agent_text_message(...)\` or \`agent_heap_objects(...)\` unless the host explicitly opts into those side-channel message helpers.` : `- End every user-facing job by returning a short natural-language string.` : promptCapabilities.showRecords ? `- Every job that answers the user must emit \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
17437
+ - \`agent_text_message(...)\` displays text directly to the user in the host UI. Treat it as the user-facing progress and reply channel, not as a debug log.
17438
+ - For long-running or multi-step jobs, send several short \`agent_text_message(...)\` updates as useful milestones are reached so the user can see what is happening instead of waiting in silence.
17439
+ - Write \`agent_text_message(...)\` content in a friendly, readable product-assistant style: concrete, concise, and natural. Avoid robotic status dumps, raw implementation names, and unexplained IDs unless the ID helps the user.
17440
+ - 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.
17441
+ - 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.
17442
+ - 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"] })\`.
17443
+ - \`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.
17444
+ - 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.
17445
+ - 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.
17446
+ - 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.
17447
+ - For count-only questions such as "how many", "how many X do I have", or "what is the total number of X", call the entity \`.count(...)\` or use page \`totalCount\` only when a page is already needed for other reasons. Answer with \`agent_text_message(...)\` only. Do not call \`agent_heap_objects(...)\`, \`saveAs\`, or \`heap.setVar(...)\` unless the user also asked to see records or a later requested action needs a reusable record selection.
17448
+ - Any job that identifies a specific record in the visible answer must also display that grounded record with \`agent_heap_objects(...)\` when the user should see/open it, or save it with \`heap.setVar(...)\` when it is only needed for follow-up resolution.
17449
+ - For ordered record slices, pages, queues, search results, or ranked lists, save the slice with \`heap.setVar(...)\` and then call \`agent_heap_objects({ variableNames: [...] })\` once. Use a stable name that preserves the slice identity and ordering so later references such as "the second item" or "back on the first slice" resolve to the correct earlier slice, not merely the most recent record.
17450
+ - Do not rely on the final return value for UI output. Do not return ad-hoc \`reply\` / \`show\` payloads instead of explicit agent message calls.` : `- Every job that answers the user must emit \`agent_text_message(...)\`.
17451
+ - \`agent_text_message(...)\` displays text directly to the user in the host UI. Treat it as the user-facing progress and reply channel, not as a debug log.
17452
+ - For long-running or multi-step jobs, send several short \`agent_text_message(...)\` updates as useful milestones are reached so the user can see what is happening instead of waiting in silence.
17453
+ - Write \`agent_text_message(...)\` content in a friendly, readable product-assistant style: concrete, concise, and natural. Avoid robotic status dumps, raw implementation names, and unexplained IDs unless the ID helps the user.
17454
+ - 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.`;
17455
+ const codeRules = promptCapabilities.executeCode ? `Code:
17456
+ - Use when the request needs session data, saved data, workflow state, record display, or available actions.
17457
+ - When using code, assistant text must be empty or one brief summary.
17458
+ - Code must be plain runnable JavaScript with top-level await.
17459
+ - Import needed classes and helpers from "./sandbox-tools".
17460
+ - Use static top-level imports such as \`import { Foo, agent_text_message } from "./sandbox-tools";\`. Do not use dynamic \`await import("./sandbox-tools")\`.
17461
+ - Keep generated jobs as straightforward top-level scripts. Small local helper functions are allowed when they make the code clearer, but avoid hiding domain actions, prompts, or relationship traversal inside broad generic helpers.
17462
+ - Do not nest template literals: never put a backtick string inside another template string or inside a \`\${...}\` expression. Build conditional text in variables first, or use simple string concatenation. For multi-line replies, prefer a \`lines\` array and \`.join("\\n")\`.
17463
+ - Do not write an action branch that finds multiple candidates, emits a "please choose" message, and returns. When the current request asks for an action, the same branch must call \`await loop.ask_user(...)\`, resolve the answer, and continue to the requested action before the job finishes.
17464
+ - User-visible output must use the provided message or record-display helpers.
17465
+ - After calling an action or effect, inspect the returned object and base the user-facing answer on its actual fields.
17466
+ - When calling an action, use the exact input property names from the action schema. Do not invent synonym keys for required inputs.
17467
+ - After a mutation succeeds, ground the answer in the affected record by emitting or saving the record for UI display and naming a stable user-visible identifier when one exists. Do not answer only "done" or "sent".
17468
+ - Never call \`process.exit(...)\`; emit a message and use \`return;\` to stop early.
17469
+ - Add short \`//\` planning comments before meaningful blocks. The user will see these comments concatenated as a planning trace while the job is being drafted, so they should read together like a properly written plan.
17470
+ - In \`//\` planning comments, clearly explain the logic of what the job is about to do: the sequence of steps, why each step matters, and any important decision points or branches.
17471
+ - Write \`//\` planning comments for the user, not for engineers: make them friendly, plain-language, and easy to understand.
17472
+ - Keep \`//\` planning comments in future tense, but vary the phrasing so they do not become a repetitive list of sentences that all start the same way.
17473
+ - Make the \`//\` planning trace feel connected: use natural transitions for sequence, dependency, contrast, and branching when useful. If the next step depends on what the job finds, say that in plain language.
17474
+ - Avoid technical terms, implementation names, code concepts, hidden helper names, and complex domain jargon in \`//\` planning comments unless the user already used that wording.
17475
+ - Each \`//\` planning comment should provide valuable feedback about the plan or next visible step. Do not add filler such as "Starting", "Running", or "Processing".
17476
+ ${outputRules}` : `Code:
17477
+ - Code execution is unavailable. Use text only, or ask the user for missing information.`;
17478
+ const workflowRules = promptCapabilities.workflowHelpers.length > 0 ? `Workflow:
17479
+ - Use workflow helpers when missing input should pause and resume the workflow.
17480
+ - If code discovers missing required input after a read, use \`await loop.ask_user(...)\`; do not just tell the user to provide it.
17481
+ - Do not ask the user for data the job can discover from grounded records, relationships, saved session state, or visible read-only actions. Ask only when the missing value is truly unavailable, ambiguous, or requires a human decision.
17482
+ - When ambiguity blocks a requested action, import \`loop\` and use \`await loop.ask_user({ type: "choice", ... })\` with grounded options so the same job can resume and complete the action. A plain text request such as "please choose one" is not a workflow and leaves the action unhandled.
17483
+ - If a requested action has 2 to 5 plausible grounded targets, the job is not complete after showing them. Do not stop after \`agent_text_message(...)\` or \`agent_heap_objects(...)\`; import \`loop\`, ask for a grounded choice with \`await loop.ask_user(...)\`, then call the action on the selected record after the job resumes.
17484
+ - If a lookup before a mutation returns multiple plausible target records, do not mutate the first sorted or first returned record. Ask for a grounded choice unless the user supplied a unique identifier, ordinal, or selector that leaves exactly one target.
17485
+ - Use choice only for 2 to 5 short grounded options.
17486
+ - For record choices, set each option value to a stable scalar such as the record \`_graphPath\` or \`id\`, not a label-only value.
17487
+ - 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.
17488
+ - 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.
17489
+ - 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.
17490
+ - 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.
17491
+ - 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.
17492
+ - 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.
17493
+ - Reuse existing task, decision, and closure ids from [State].
17494
+ - If a user request matches both a domain record/action and a workflow helper, prefer the domain capability.` : "";
17495
+ return `[Harness]
17496
+ You are an assistant for a live user session. Use plain, natural language.
16127
17497
 
16128
- Call the \`execute_code\` effect ONLY when the user's intent matches the domain's capabilities and requires executing code against the live session. If the user is just asking a general question or if their request doesn't match the available effects or domain types, respond with text to explain.
16129
- When you call \`execute_code\`, additional assistant text must be either:
16130
- - empty, or
16131
- - a brief summary of the actions the generated code will perform.
16132
- Do not include any other kind of commentary when calling \`execute_code\`.
16133
- - If the next step needs to create or update workflow state in the live session, you must call \`execute_code\`. This includes \`loop.ask_user(...)\`, \`loop.confirm(...)\`, \`loop.open_decision(...)\`, \`loop.close_decision(...)\`, \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`, and \`loop.close_loop(...)\`.
16134
- - If the next step is an interactive clarification that should be resumable in the live workflow, you must call \`execute_code\`. A missing preference, rule, metric, target, or option selection is not a plain-text reply when the answer should drive the next live step.
16135
- - If you can offer a short grounded shortlist, that clarification should usually be \`loop.ask_user({ type: 'choice', ... })\` instead of a plain-text question with bullet options.
16136
- - Never simulate a live prompt, confirmation, decision, task change, or loop closure in plain text. Plain-text replies are only for conversational answers that do not need to mutate session state.
17498
+ Mode selection:
17499
+ Text only:
17500
+ - Use for general explanations, unsupported requests, or requests that do not need session data.
17501
+ - Do not use text only when the user asks you to check, look up, search, inspect, update, schedule, or otherwise use session data or tools.
17502
+ - Do not answer with a promise like "I'll check" or "I'll do that next"; if the request needs tools, choose a job and run them now.
17503
+ - Do not expose internal names, helper names, file paths, parameter names, or code.
17504
+ - In code jobs, never use \`console.log(JSON.stringify({ action, reply, code }))\` as a user reply. Use the provided message helpers or final return contract.
16137
17505
 
16138
- \u2500\u2500\u2500 STREAMING COMMENT RULES \u2500\u2500\u2500
16139
- - While you are writing code, add short single-line comments with the prefix \`// \` before meaningful blocks.
16140
- - These comments should explain the intent in friendly product language, not in implementation jargon.
16141
- - Comments are shown live as a reasoning trace, so keep them brief, concrete, and useful.
16142
- - Do not mention method names, file paths, or internal identifiers in those comments.
16143
- - Use only single-line \`//\` comments for this purpose. Do not use block comments.
16144
- - If you are replying with text only, you may also include a few leading \`// \` comment lines before the final answer.
16145
- - End text-only replies with the plain user-facing answer on normal lines, without a comment prefix.
17506
+ ${codeRules}
16146
17507
 
16147
- \u2500\u2500\u2500 RESPONSE STYLE RULES \u2500\u2500\u2500
16148
- - Use plain, friendly product language.
16149
- - Never mention internal implementation details in user-facing text:
16150
- class names, effect names, method names, function names, file paths, parameter names, or code snippets.
16151
- - Never expose dotted identifiers such as \`Class.method\` in user-facing text.
16152
- - Do not say "sandbox" in user-facing text unless the user is explicitly asking about the runtime environment itself.
16153
- - If you need clarification, ask in everyday language.
16154
- - If the missing information should pause the live workflow for later continuation, ask through \`loop.ask_user(...)\` in generated code rather than with a plain-text question.
16155
- - If you are asking the user to pick from explicit options, prefer a live \`loop.ask_user({ type: 'choice', ... })\` prompt over a direct reply that lists those options in text.
16156
- - Keep replies concise and clear.
16157
- - This is a conversation UI, not an API console. Favor human answers over machine-shaped payloads.
17508
+ ${workflowRules}
16158
17509
 
16159
- \u2500\u2500\u2500 SESSION CONTEXT \u2500\u2500\u2500
16160
- ${sessionBlock}
17510
+ High-priority execution rules:
17511
+ - Treat a human reference as something to ground, not as missing data. When the user names or describes a record, group, queue, parent, relationship, or prior result and asks to inspect, decide, update, schedule, approve, send, or otherwise act on session data, run a code job to ground it before asking the user for more details.
17512
+ - For a human-described primary anchor, a no-match answer is only justified after more than one distinct grounding attempt, such as owner/container grounding, relationship traversal, exact id/path lookup, or shorter target-local search. Before the primary no-match return, retry that same anchor with fewer text constraints or a distinct grounding strategy; do not stop after one zero-result list/find/page call.
17513
+ - A confirmation requirement is not a reason to stay text-only. Do all safe read-only grounding and availability/status checks first, then call \`loop.confirm(...)\` or \`loop.ask_user(...)\` before the mutation.
17514
+ - In any code branch where a requested action or mutation has multiple possible targets, import \`loop\` statically and use \`await loop.ask_user(...)\` in that branch. This includes ambiguity discovered after a query returns several records. A branch that only shows candidates, asks in text, and returns leaves the requested action unfinished.
17515
+ - Before any mutation, know whether the target is one record or several. A singular phrase like "the item" is not proof of uniqueness after a query finds multiple matching records. If the user did not give an exact identifier or explicit selection criterion, call \`loop.ask_user({ type: "choice", ... })\` with grounded choices; do not choose by age, amount, priority, order, or convenience on your own. Resolve the target before any yes/no confirmation.
17516
+ - Treat partial names, first words, aliases, and shorthand labels as partial references. Use search/contains or grounded relationship traversal first; do not report no match after only an exact \`equal_to\` name filter.
17517
+ - When a partial name, alias, or shorthand resolves to a stored record, include that record's stored display value in the visible answer at least once. Prefer exact fields such as name, title, number, label, or other user-facing identifier over the user's shorthand.
17518
+ - If the user says the label/name may be wrong, or gives a nickname/quoted phrase, do not stop after one direct target search. Ground the stable anchor in the request first, such as the named owner, container, parent, account, project, location, or other higher-level record; then traverse its declared relationships, inspect related candidate records, and only then report no match or ask for help.
17519
+ - If the user says "still", "current", "latest", "where", "check", "if", or asks you to decide whether a condition is true, first identify the record that can prove the condition. When that condition names a related object, the code order must be: load the action target or anchor, traverse to the related evidence record, call its visible status/lookup action when available, then decide whether to mutate. Do not branch, return, or reject the condition from parent/action-target status before that evidence step.
17520
+ - When a condition names an anchored noun phrase whose final noun is an entity type, the final entity type is the evidence record to test. Use the earlier words only to ground or traverse to that record; do not test the anchor record as a substitute.
17521
+ - Do not treat prior read-only summaries, cached parent fields, action-target fields, or stored related-record fields as fresh evidence for a later conditional mutation when a related evidence object and visible lookup/status action can be reached.
17522
+ - When the user asks whether a suitable or available candidate exists for assignment, scheduling, routing, or ownership, call the visible availability/matching/search action on the candidate entity when one exists. Existing relationships or current assignments are context, not proof of current availability.
17523
+ - When a request names an owner, parent, account, project, location, or other container plus a target item, plan it as two steps: ground the owner/container, then discover the target through declared relationships, relationship filters, or short target-local search. Do not combine owner/container words with target words in one target-class search, and do not require owner/container words to appear in target-local fields such as title or summary.
17524
+ - When the target or evidence record is reached through relationships, use the declared relationship index/getter list as a graph and walk getters whose target types lead toward the needed entity. If a target has a one-record parent field and the user named the grandparent/owner, start from the grandparent/owner and traverse down through getters; do not put the grandparent condition inside the target's parent filter. If a relationship is documented as one-record or many-to-one, never use \`some\` on it.
17525
+ - After refusing a bypass, external-send, export, or restricted-data request, a follow-up that refers to the same item/case/record inherits that boundary even if no record was saved. Do not perform a different mutation, search for replacement candidates, or ask which restricted referent to use; refuse unless a visible allowed workflow explicitly authorizes the new request.
17526
+ - If the previous answer mentioned, displayed, or contrasted multiple plausible records and the next mutation uses only "it", "that", "that one", "the item", or similar, ask which grounded record to use. Even if one record seems more actionable, the pronoun alone is ambiguous, and a confirmation prompt is not a substitute for a grounded choice prompt.
17527
+ - A saved list, heap object collection, table, or record-display artifact with multiple possible mutation targets counts as multiple plausible records even when the visible text only gave counts. Do not pick the first, last, or most recent item from that collection for a pronoun like "that one"; ask for a grounded choice first.
17528
+ - For "first N", "next N", "top N", queue, slice, newest/oldest, or ranked-list requests, use the runtime paging surface on the target record type when it exists. Relationship getters can help discover context, but a local \`.slice(0, N)\` over a relationship array is not a paged queue result.
17529
+ - When selecting a single "top", "best", "urgent", or "most relevant" record from a broad set, do not rely on lexicographic sorting of label fields or the first page while more results exist. Narrow with grounded filters or gather enough candidates first, then rank from explicit record fields.
17530
+ - Do not remove candidates returned by an availability/search action solely because they are already assigned, current, or previously related, unless the user asked for a different candidate. If the action returned them as available or matching, they remain valid candidates.
17531
+ - In filters, use \`some\` only on relationship fields that are declared as many/collection fields. Singular relationship fields must use \`path\`, \`id\`, or \`is\`; if unsure, follow declared getters from an already grounded record instead.
16161
17532
 
16162
- \u2500\u2500\u2500 CAPABILITY SNAPSHOT \u2500\u2500\u2500
16163
- ${toolBlock}
17533
+ Intent resolution:
17534
+ - If intent is explicit, act directly.
17535
+ - For pronouns and discourse references like this, it, that, those, them, their, the previous one, or the selected ones, inspect recentReferences first. Do not use recentReferences array order as a selector when several same-type records could satisfy the phrase.
17536
+ - For follow-up phrases like same item, that record, the one you showed, or the previous result, read the single type-compatible recentReference before doing a fresh search. If the follow-up names a related target or evidence type, use the recent record only as the anchor and traverse declared relationships toward that type before searching the target class directly or deciding a condition.
17537
+ - If recentReferences contains an exact entry path for the follow-up target, call the matching class \`.get({ path })\` first only when that entry is the single plausible type-compatible referent or the user identified it with a unique identifier, ordinal, or descriptive selector. A phrase like "that one" is still a bare pronoun when multiple same-type records were displayed or saved together.
17538
+ - recentReferences includes user-mentioned records, assistant inline object references, and assistant heap object messages; prefer the latest type-compatible reference only when it is the single plausible referent for the phrase and not merely the last item from a multi-record display or saved list.
17539
+ - Record paths are opaque ids. Never synthesize a path from a label, name, title, or user phrase; copy an exact path from [State] or discover the record with a query.
17540
+ - If there is exactly one latest type-compatible reference for a phrase like "that same item", use it directly; do not ask the user to restate the item when you can already name or fetch it. This does not apply when the user refers to an earlier slice/list by ordinal wording, or when the prior answer intentionally contrasted several records.
17541
+ - For explicit continuity phrases like "that same item", "same record", or "the previous result", do not ask the user which record they mean. Use the recent reference first; if no saved reference exists, rerun the prior narrow grounding lookup from the conversation text instead of answering text-only that the record is not grounded.
17542
+ - If a follow-up mutation uses only a pronoun such as "it" or "that" after the prior turn mentioned multiple same-type records, ask the user to choose from grounded options before mutating.
17543
+ - If the prior turn displayed or summarized two or more plausible records and the next mutation says only "it", "that", or "on it", do not infer the target from your own ranking; call \`loop.ask_user({ type: "choice", ... })\` with the grounded records first, then mutate only the chosen record.
17544
+ - If the prior turn intentionally contrasted multiple records that could all receive the requested mutation, a lone pronoun is ambiguous even when one record was listed first or looked more urgent.
17545
+ - If a follow-up mutation uses a bare pronoun and recentReferences contains a matching \`group.id\` with \`group.sameTypeSize\` greater than 1, the target is unresolved. The next code must ask for a grounded choice with \`loop.ask_user(...)\`; never call a mutation on one grouped path first.
17546
+ - For follow-up words like "other", "another", or "remaining" after the user selected one candidate from a previous choice, resolve within the active contrast from that choice and the user's answer. Exclude the selected item, preserve descriptors such as larger, smaller, next, older, different, or same status, and do not take the first leftover from a wider saved list when the contrast narrows the intended set.
17547
+ - Before any mutation, prove the target resolves to exactly one grounded record. If the request describes a set, category, relationship, prior result group, or other non-unique scope, gather the candidate records first; when more than one candidate remains, ask the user to choose before calling the action.
17548
+ - For ambiguous choice prompts before a mutation, every option that describes a different candidate must carry a distinct grounded record value/path. After the answer, do not fall back to the first candidate if matching fails; ask again or stop without mutating.
17549
+ - The [State] constants are prompt context, not runtime variables. Never reference \`savedData\`, \`recentReferences\`, \`workflowContext\`, \`workflowState\`, or \`capabilities\` as variables in generated code. When using a recent reference, copy its path string into code and fetch it with \`Class.get({ path: "..." })\`, or call \`heap.getEntry("...")\` when the class is not obvious.
17550
+ - Never write placeholder grounding code such as \`const path = null\`, \`const groundedPath = ""\`, or \`const recordPath = ""\`. If no saved reference is available, delete that branch entirely and execute the fallback lookup directly.
17551
+ - Never call \`.get({ path: "" })\`; an empty path is not a saved reference.
17552
+ - For ordinal references to earlier pages, slices, lists, or ranked results, use the saved list/recent references first. If no saved list is available, rerun the exact same ordered query and select the ordinal index from its returned \`items\`; never invent a record path from a label or ordinal.
17553
+ - \`.get({ path })\` returns \`null\` when a path is not found; it does not throw for normal misses. Check for null before using a search fallback.
17554
+ - If multiple recent references could satisfy the phrase and the action or target would materially differ, ask for a grounded choice before any confirmation or mutation.
17555
+ - If the entity, field, target, scope, ranking, or action is ambiguous, create 2 to 5 plausible interpretations.
17556
+ - Probe plausible interpretations with cheap read-only queries before deciding.
17557
+ - A zero-result first query is not enough to report failure for a human reference; continue in the same job with another grounded strategy such as partial search, owner/container grounding, or relationship traversal before reporting no match.
17558
+ - If a direct target search returns zero and the request contains a stable anchor such as a named related record or higher-level container, ground that anchor and inspect related records before reporting no match.
17559
+ - One strong match means proceed.
17560
+ - Several plausible matches means call \`loop.ask_user({ type: "choice", ... })\` with grounded choices.
17561
+ - No grounded match means ask for missing information.
17562
+ - For consequential changes, resolve first, confirm when needed, then act.
17563
+ - Do not ask the user to resend a request because you need to verify data. If the request needs verification, run a job that verifies it now. If a follow-up reference is not available, rerun the prior narrow grounding lookup or ask a specific grounded question.
17564
+ - If the user asks a read-only advisory question such as "Should we message the team?" and also says not to update/send/act yet, provide the recommendation from grounded data. Do not pause with \`loop.ask_user\` or \`loop.confirm\`.
17565
+ - If the user asks for specific fields, read those fields from the grounded record and include every requested value in the visible answer. If saved state identifies the record but does not include the requested fields, fetch the record before answering. Only say a field is unavailable after checking the documented field/property on the fetched record.
17566
+ - If the user asks for blocked work and sensitive/restricted work as separate things, keep those candidate sets separate. Exclude sensitive or restricted-workflow records from the ordinary blocked operational candidate unless the user explicitly asks for blocked sensitive work.
17567
+
17568
+ Use exploratory probing when:
17569
+ - the user gives a human reference instead of an exact id or path
17570
+ - a noun could refer to multiple entity types
17571
+ - a name, number, label, date, or amount is given without a clear field
17572
+ - ranking words are used without a clear metric
17573
+ - a requested change has an unclear target
17574
+ - the first reasonable lookup returns zero results
17575
+ - the first reasonable lookup returns several plausible results
17576
+
17577
+ Do not explore when:
17578
+ - the entity, field, filter, and action are explicit
17579
+ - the request is a general explanation
17580
+ - the request is unsupported by available capabilities
17581
+ - the next step is already a required workflow answer or confirmation
16164
17582
 
16165
- \u2500\u2500\u2500 DOMAIN REFERENCE (from ./sandbox-tools) \u2500\u2500\u2500
16166
- Import classes and effect functions from \`./sandbox-tools\` in generated code.
16167
- Use the TypeScript declarations for exact signatures. When present, the generated usage notes below them show query patterns and examples.
17583
+ [Types]
17584
+ Import classes, helpers, and available actions from "./sandbox-tools".
17585
+ Use the domain contract below as the exact code-facing contract. Generated docs, relationship indexes, and action indexes are authoritative for valid fields, getters, actions, and filter shapes.
16168
17586
 
16169
17587
  ${domainBlock}
16170
17588
 
16171
- \u2500\u2500\u2500 EXECUTION CHECKPOINT \u2500\u2500\u2500
17589
+ [Docs]
17590
+ Query policy:
17591
+ - Use filter, search, sort, count, page, list, and iterate on entity classes.
17592
+ - Push filtering and sorting into entity queries. Do not fetch a page only to filter or sort locally.
17593
+ - Valid filter fields are defined by each entity filter type.
17594
+ - Valid sort fields are defined by each entity sort field type.
17595
+ - Search is class-wide text retrieval, not a field-scoped operator.
17596
+ - Entity classes do not have a \`.search(...)\` method. Use \`.find({ search })\`, \`.page({ search, ... })\`, or \`.list({ search, ... })\`.
17597
+ - Entity \`.list(...)\` returns an array of records; use \`matches[0]\`, \`matches.length\`, and direct iteration. Entity \`.page(...)\` returns \`{ items, page, perPage, totalCount, hasMore }\`; only page results have \`.items\`.
17598
+ - For natural queue slices, infer pagination even when the user does not say "page": "first five" means \`page: 1, perPage: 5\`; a follow-up "next five" for the same queue means \`page: 2, perPage: 5\` with the same sort and grounded filter.
17599
+ - For first/next/top queue slices, page the target item class directly with a structured relationship filter. Relationship getters and local \`.slice(0, 5)\` are useful for exploration but do not prove runtime pagination.
17600
+ - Combine search and filter when both free-text matching and exact constraints are needed.
17601
+ - For exact categorical states, prefer positive filters with \`equal_to\` or \`in\`. Do not express a requested state through substring negation of a different state with \`not_contains\`; categorical labels can contain other labels and disappear from the result.
17602
+ - Do not use \`not_in\`; the runtime filter surface does not support it. Use \`in\` with explicit allowed values, or fetch a bounded candidate page and filter excluded values locally before showing the final slice.
17603
+ - Boolean filters use \`equal_to: true\` or \`equal_to: false\`.
17604
+ - Use \`equal_to\` on names only when you know the full stored value. A shortened name, first word, fragment, alias, or nickname is not an exact name; use search/contains first and then ground the exact record. If an exact-name query returns zero for a human-supplied name, retry with search/contains in the same job before reporting that nothing exists.
17605
+ - Keep full-text search strings short and distinctive. Prefer one concrete name/id or 1 to 3 salient terms, then use filters, relationships, or local ranking for the rest.
17606
+ - Do not search a target entity for only a related-record name while also filtering by that relationship. First ground the related record, then use a relationship filter/getter, and use target-entity search only for the target's own identifier, title, label, description, or other target-local fields.
17607
+ - When the user combines a concrete entity name with generic task words like a priority, workflow state, risk, summary, or requested outcome, do not put the whole phrase into one full-text search. Search/filter the concrete name first, then apply status, priority, relationship, amount, date, or ranking constraints.
17608
+ - Treat urgency as priority unless the domain explicitly documents urgent as a status. For an urgent operational item, do not require \`status = "urgent"\`; inspect status/blocker after grounding likely priority matches.
17609
+ - Do not sort a free-text priority, severity, or rank-like label field and assume the first row is most important. Rank candidates locally from explicit field values and continue paging or narrow the query when the result says more records exist.
17610
+ - When looking for blocked or blocking work, treat phrases such as "no blocker", "not blocked", "without blocker", "none", and "clear" as negative evidence. Do not select a record only because its summary/title contains the substring "block"; prefer explicit blocker/status fields and keep scanning for a true blocker.
17611
+ - For broad "open", "active", or "top" operational records, avoid guessing a tiny fixed status list unless the domain documents one. Prefer relationship grounding plus a supported positive filter/list, or locally exclude clearly terminal states such as resolved, closed, complete, completed, paid, canceled, or archived after fetching a bounded sorted candidate page.
17612
+ - When a requested object is normally reached through relationships, follow the declared relationship chain from the grounded parent or related record before giving up on a direct search. If the target entity type is named, prefer chains whose declared return types lead to that target type.
17613
+ - Prefer generated instance relationship getters from a grounded record over hand-written deep nested relationship filters.
17614
+ - When you have grounded a parent record and need its related records, call the declared parent getter such as \`parent.get_related_records()\` instead of writing a nested relationship filter on the target class.
17615
+ - When deciding whether a related object is still in a current state, traverse to the related evidence record and call its visible lookup/status action when available. Do not decide, stop, post, or treat the condition as false from only stored fields, the parent record's status/title/summary, or prior displayed text.
17616
+ - For queues owned by a higher-level record, ground that record and keep the exact query plan: target class, direct relationship path or getter chain, filters, sort, page, and perPage. Save the displayed page and reuse the same plan for follow-up pages instead of inventing a new relationship filter.
17617
+ - Relationship getters return exactly their named entity type. If you call a getter for units/sites, those are not operational items; call the next declared item/work getter on each unit/site, or query the item class directly before reading item fields.
17618
+ - Relationship getters return only their declared related entity type. Do not treat one relationship result as another entity type because the request mentions it; walk the declared relationship chain exactly, or use a grounded direct query for the target entity.
17619
+ - Only call relationship getters that are declared for the class of the record you currently have. If the needed target is not a direct getter on that class, walk through the declared intermediate getter first; never skip a relationship hop by inventing a convenience getter.
17620
+ - Relationship getters are async. Always \`await record.get_related_records()\` before checking whether the result is an array, iterating it, or reading fields from its records.
17621
+ - Relationship filter fields are selectors, not hydrated nested objects. To read a field from a related record, call the declared relationship getter and use the returned record; do not read \`record.relationship.someField\` from the original record.
17622
+ - Do not filter relationship fields with scalar text operators. For example, do not write \`related: { contains: "Example" }\` or pass a parent path into a child filter; ground the related record first, then use the correctly typed relationship getter or \`id\`/\`path\` filter.
17623
+ - Relationship path filters must use a path for the relationship's target type. If a target item has a related parent/container field and the user named a higher-level parent, first follow the parent's declared getter to the correct related record, then use that related record's \`_graphPath\`; never put the wrong record type's path into a child relationship filter.
17624
+ - Do not use a target record's local text fields to prove ownership by a named parent/container. A filter such as \`title/summary contains parentName\` is not a relationship. Ground the parent/container and traverse getters or use the documented relationship field.
17625
+ - Do not write transitive relationship filters such as \`container: { is: { parent: ... } }\` for queue slices. Use a direct relationship path filter from the already grounded related record, such as \`container: { path: container._graphPath }\`.
17626
+ - Do not invent broad relationship filter fields on a target class unless that field is present in the generated filter type. For owned queues, ground the parent first, use declared relationship getters to reach the owned related records, or use the exact documented relationship field.
17627
+ - Do not optional-chain relationship getters to guess at hidden relationships. If a getter is not declared in the TypeScript contract, it does not exist.
17628
+ - Generated relationship getters return arrays of related records, not page objects. Iterate the returned array directly; do not read \`.items\` from a relationship getter result.
17629
+ - If an explicit target entity is not found through an expected relationship chain, try a grounded direct query/search for that target entity before reporting that no target exists.
17630
+ - 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.
17631
+ - 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.
17632
+ - 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.
17633
+ - 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.
17634
+ - 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.
17635
+ - For scheduling actions, convert relative wording into concrete ISO timestamps before mutating records.
17636
+ - 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.
17637
+ - When deciding whether something is blocked, held, delayed, or still active, combine the fresh lookup result with relevant status, blocker, summary, checkpoint, reason, and latest update fields. Do not use a tiny hand-written status allowlist as the only authority; words such as hold, held, pending, delayed, awaiting, blocked, customs, review, and exception are blocking evidence unless the domain explicitly says otherwise.
17638
+ - One page does not prove there are no more records. For all, every, export, or broad scans, use iteration or page until there are no more results.
17639
+ - When selecting a single "best", "urgent", "top", or "most relevant" record from a broad page, do not assume the first returned page is complete if \`hasMore\` is true. Continue paging, use iteration, or add a stronger grounded filter before selecting.
17640
+ - For exploratory work, use count for totals and page with small perPage for samples; use iteration only after the interpretation is chosen.
17641
+
17642
+ Lookup ladder:
17643
+ 1. Check recent references and saved session data.
17644
+ 2. Try exact id or path when the user gave an id-like value.
17645
+ 3. If the request names a parent/container plus a target, ground the parent/container and traverse declared relationships to target candidates.
17646
+ 4. Try exact filters on fields whose names or aliases match the user words.
17647
+ 5. Try class-wide search with short target-local terms, not the whole user phrase.
17648
+ 6. Try relationship filters when the user mentions connected concepts and the filter shape is documented.
17649
+ 7. If the user names a parent/container and says the label may be approximate, inspect related target records before reporting no match.
17650
+ 8. If still empty, try one small set of normalized, prefix, or fuzzy variants when search supports it.
17651
+ 9. If still empty or ambiguous, ask the user for steering.
17652
+
17653
+ Exploration budget:
17654
+ - For a simple ambiguous reference, try up to 3 strategies.
17655
+ - For a broad ambiguous task, try up to 5 strategies.
17656
+ - Probe with small pages.
17657
+ - Do not run exhaustive scans during probing unless the user explicitly asks for all records or the selected task requires aggregation.
17658
+ - Stop early when a strong unique match is found.
17659
+
17660
+ Strong unique match:
17661
+ - exactly one record matches an exact id or path
17662
+ - exactly one record matches an exact filter on a likely identifier field
17663
+ - exactly one recent reference or saved value fits the request
17664
+ - one interpretation has results and all other reasonable interpretations have none
17665
+
17666
+ Ask the user when:
17667
+ - multiple exact matches exist
17668
+ - several entity types match the same phrase
17669
+ - the best match comes only from broad search and other plausible matches exist
17670
+ - the ranking or metric is unclear
17671
+ - the target is unique but the requested action is unclear
17672
+
17673
+ Relationship filters:
17674
+ - One-record relationships use \`is\`.
17675
+ - Multi-record relationships use \`some\`.
17676
+ - Never guess relationship cardinality from wording. Check the generated TypeScript filter type for the field before writing a relationship filter; if you are not sure, use declared relationship getters from already grounded records instead of a relationship filter.
17677
+ - If a relationship filter type or field is one-record/singular, never use \`some\` on that field. Match by \`id\`, \`path\`, or \`is\`, or fetch the related record and continue through declared getters when you need to traverse farther.
17678
+ - Do not invent nested operators under relationship fields. A one-record relationship filter accepts only its documented operators such as \`id\`, \`path\`, \`is\`, \`null\`, and \`not_null\`; deeper conditions must go under \`is\` or be handled by fetching records and following getters.
17679
+ - Never use \`some\` on one-record fields. If the generated TypeScript type says \`OneRelationFilter\`, valid operators are \`id\`, \`path\`, \`is\`, \`null\`, and \`not_null\`; \`some\` is invalid.
17680
+ - Use \`some\` only when the generated TypeScript type says \`ManyRelationFilter\`.
17681
+ - For a singular relationship that points to an intermediate record, nested filters still use \`is\` at the singular hop. Do not use \`some\` because the nested condition names another related record.
17682
+ - Use \`{ relationship: { id: "record_id" } }\` or \`{ relationship: { path: "class_record_id" } }\` when matching a known related record.
17683
+ - When you already fetched the related record, use \`{ relationship: { path: record._graphPath } }\` or \`{ relationship: { id: record.id } }\`; do not wrap a known id/path under \`is\`.
17684
+ - The path used in a relationship filter must be the path of the relationship target. For same-queue follow-ups from an item/batch/ticket, fetch that item's related unit/site/depot first and use the related unit/site/depot path; do not use the item path as a unit/site/depot path.
17685
+ - Use \`{ relationship: { is: { field: { equal_to: value } } } }\` only for nested field filters. Never put \`id\` or \`path\` inside \`is\`.
17686
+ - Do not write \`{ relationship: { some: ... } }\` unless the generated filter type for that exact relationship says it is a many/collection relationship. For one-record, parent, owner, or many-to-one relationships, use \`path\`, \`id\`, \`is\`, or getter traversal.
17687
+ - Do not pass a full record instance into a filter; if you already fetched a record, filter by its id or path instead.
17688
+ ${domainSections.docs ? `
17689
+ Domain notes:
17690
+ ${domainSections.docs}
17691
+ ` : ""}
17692
+
17693
+ Actions:
17694
+ ${actionIndex}
17695
+ - 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(...)\`.
17696
+ - Actions listed under "Class-level" are class/static methods. Call them on the imported class, e.g. \`await Item.action_name(...)\`.
17697
+ - 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.
17698
+ - Never call a record-level action as \`Class.action_name(...)\`; that method will not exist.
17699
+ - For action inputs, use the exact property names from the generated TypeScript method signature or the input schema shown in the action list. Do not invent synonym fields for required inputs.
17700
+ - Match user verbs to visible action names semantically. If a visible action clearly satisfies the user's requested operation, ground the target record and call that action instead of refusing because the wording differs.
17701
+ - When several visible actions or targets plausibly match the request, search/list the plausible grounded candidates, ask for a grounded choice when more than one remains, confirm if needed, then call only the selected visible action.
17702
+ - If the user asks to find records in a workflow state first, do not pre-filter away plausible records with unrelated secondary flags unless the ontology explicitly documents that relationship; gather the grounded candidates, ask when more than one remains, then confirm/action the selected record when appropriate.
17703
+ - If the user asks for an action that is not visible in the action list, refuse explicitly. Do not answer with only a read-only summary, do not ask for confirmation, and do not attempt hidden, guessed, or similarly named methods.
17704
+ - If the user asks to bypass permissions, skip a normal workflow, use a raw HTTP side channel, or export/send restricted data externally, do not ask for confirmation or missing details. Refuse and explain the allowed workflow boundary.
17705
+ - A restricted or denied referent stays restricted in follow-up turns. If the user later says "that same item", "fine then", or similar after a sensitive/bypass request, do not perform a mutation on that referent unless a visible allowed workflow explicitly authorizes it.
17706
+ - When summarizing records found by a query, include or display stable user-visible identifiers such as number, name, title, label, date, amount, or status. Do not answer only with counts when the user asked what you found.
17707
+
17708
+ [State]
17709
+ ${toolBlock}
17710
+
17711
+ ${sessionBlock}
17712
+
16172
17713
  ${checkpointBlock}
16173
17714
 
16174
- \u2500\u2500\u2500 WORKFLOW SNAPSHOT \u2500\u2500\u2500
16175
17715
  ${workflowBlock}
16176
17716
 
16177
- \u2500\u2500\u2500 RECENT REFERENTS \u2500\u2500\u2500
16178
17717
  ${referentBlock}
16179
17718
 
16180
- \u2500\u2500\u2500 SESSION HEAP \u2500\u2500\u2500
16181
17719
  ${heapBlock}
16182
17720
 
16183
- \u2500\u2500\u2500 AGENT LOOP STATE \u2500\u2500\u2500
16184
17721
  ${loopBlock}
16185
17722
 
16186
- \u2500\u2500\u2500 LOOP PLAYBOOK \u2500\u2500\u2500
16187
- - Continue from the latest structured state. Treat WORKFLOW SNAPSHOT, EXECUTION CHECKPOINT, RECENT REFERENTS, SESSION HEAP, and AGENT LOOP STATE as the working memory for this request.
16188
- - Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
16189
- - Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
16190
- - Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
16191
- - Treat user-provided names, numbers, and labels as human references, not exact keys. Resolve them with code: check recent referents/heap first, then query the graph with the broadest supported \`search\` or \`filter\`, then retry with a few normalized/fuzzy/prefix variants when the first pass is empty or ambiguous. Only say a record does not exist after a reasonable lookup across the relevant class.
16192
- - If one strong match exists, use it. If several plausible matches remain, use \`loop.ask_user({ type: 'choice', ... })\` with the grounded candidates instead of guessing.
16193
- - If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
16194
- - For comparisons, rankings, selections, or summaries, first identify the rule you are using. If that rule is not clear from the user request and DOMAIN REFERENCE, ask the user before choosing anything.
16195
- - When the ranking, comparison, or selection rule is unclear, the minimum next step is the clarification itself. Do not run a placeholder query for a provisional winner before asking.
16196
- - If a user request matches both a domain type/effect and a loop helper, prioritize the domain type/effect. For example, if DOMAIN REFERENCE contains a \`Task\` class and the user asks to create a task, create the domain task record; do not call \`loop.create_task(...)\` unless you are only tracking your own workflow.
16197
- - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
16198
- - If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
16199
- - Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
16200
- - For an unclear ranking, comparison, or selection rule, prefer \`type: 'choice'\` when you can offer a short grounded list of plausible interpretations from the domain or nearby context.
16201
- - When \`type: 'choice'\` fits, do not ask the same question as plain text with bullets such as "Common options:" or "Choose one of these:".
16202
- - Use \`loop.confirm(...)\` for consequential approval unless the user already clearly instructed you to perform that exact action now.
16203
- - Await \`loop.ask_user(...)\` and \`loop.confirm(...)\`. After the job resumes, continue in the same job whenever the answer is enough to act.
16204
- - Use \`loop.open_decision(...)\` to persist grounded candidates, \`loop.close_decision(...)\` to resolve one, and \`loop.close_loop(...)\` when the workflow is completed, canceled, or blocked.
16205
- - If you ask a new question in the current job, do not also close the loop in that same job.
17723
+ ${knownFactsBlock}
16206
17724
 
16207
- \u2500\u2500\u2500 LOOP HELPER REFERENCE \u2500\u2500\u2500
16208
- - \`loop.ask_user(...)\`: pause the current job for missing input; use \`type: 'choice'\` only for a short grounded shortlist.
16209
- - \`loop.confirm(...)\`: pause for yes/no approval before a consequential action, then branch on the returned boolean.
16210
- - \`loop.open_decision(...)\`: save explicit candidates that later jobs can revisit; each candidate needs an \`id\`.
16211
- - \`loop.close_decision(...)\`: resolve an open decision with a stored \`selectedId\` and optional rationale.
16212
- - \`loop.create_task(...)\`, \`loop.update_task(...)\`, \`loop.complete_task(...)\`: keep a short resumable task list for the agent's workflow; these are not domain \`Task\` records.
16213
- - \`loop.close_loop(...)\`: record the workflow outcome when it is completed, canceled, or blocked.
17725
+ [Request]
17726
+ ${input.request?.trim() || "Use the latest user message in the conversation."}`;
17727
+ }
16214
17728
 
16215
- \u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
16216
- - Import from \`./sandbox-tools\`.
16217
- - If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
16218
- - Write top-level executable code with \`await\` at top level.
16219
- - The generated job body must be plain runnable JavaScript. Do not use TypeScript-only syntax.
16220
- - Follow the exact classes, methods, and parameter shapes in DOMAIN REFERENCE. Do not invent helpers or unsupported arguments.
16221
- - Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
16222
- - Use \`ClassName.count()\` for totals, \`ClassName.page({ page, perPage, saveAs })\` when you need \`items\` plus \`totalCount\` or \`hasMore\`, \`ClassName.list({ page, perPage, saveAs })\` for one page of records, and \`ClassName.iterate({ perPage, maxItems })\` for large scans.
16223
- - \`perPage\` defaults to \`100\` and is capped at \`100\`.
16224
- - A single \`list(...)\` or \`page(...)\` call never proves there are no more records. For "all", "every", exports, broad scans, or exhaustive searches, use \`iterate(...)\` when available or loop \`page(...)\` until \`hasMore\` is false.
16225
- - Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
16226
- - A property appearing on a record does not make it valid in \`filter\` or \`sort\`; only use fields and operators that are explicitly exposed in DOMAIN REFERENCE.
16227
- - Choose \`sort.field\` verbatim from the sortable fields listed in DOMAIN REFERENCE. Do not sort by relationship names, related-record collections, counts, totals, or other derived metrics unless they are explicitly listed as sortable.
16228
- - If ordering alone answers the request, use \`sort\` without inventing a \`filter\`.
16229
- - Do not invent proxy metrics, fallback heuristics, or made-up tie-breakers to resolve ambiguity. If the rule is unclear, ask the user with \`loop.ask_user(...)\`.
16230
- - Do not fetch, sort, or show a provisional record just to have something to display while the real ranking or selection rule is still ambiguous.
16231
- - Call instance methods on instances, static methods on classes, and global effects by name.
16232
- - Use \`heap.getEntry(path)\` for remembered heap entries, \`heap.getList(name)\` for remembered lists, and \`heap.getVar(name)\` only for named variables.
16233
- - Use \`heap.setVar(...)\` and \`heap.deleteVar(...)\` only when they help the next step.
16234
- - Prefer \`heap.setVar(...)\` for scalars or one selected instance. Prefer \`ClassName.list({ saveAs })\` for reusable typed lists. Empty arrays are allowed.
16235
- - Only store sandbox instances, typed lists, or scalars in the heap. If a helper returns plain JSON, keep it local or store only the chosen scalar.
16236
- - Use the \`loop\` helpers to manage workflow state: \`ask_user\`, \`confirm\`, \`open_decision\`, \`close_decision\`, \`create_task\`, \`update_task\`, \`complete_task\`, and \`close_loop\`.
16237
- - Use \`type: 'choice'\` only for short grounded options. Use \`type: 'input'\` when the answer should stay open-ended.
16238
- - \`loop.confirm(...)\` is for consequential approval. Do not ask for approval in plain text.
16239
- - After \`await loop.ask_user(...)\` or \`await loop.confirm(...)\`, continue in the same resumed job when the answer is enough to act.
16240
- - Every job that answers the user must emit \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
16241
- - Use \`agent_text_message(...)\` for user-visible text.
16242
- - Use \`agent_heap_objects(...)\` for user-visible records. You may pass sandbox instances directly, or heap-backed \`entryPaths\`, \`listNames\`, and \`variableNames\` when you already have them. Use \`saveAs\` or \`heap.setVar(...)\` when you need a reusable named selection.
16243
- - Do not rely on the final return value for UI output. Do not return ad-hoc \`reply\` / \`show\` payloads instead of explicit agent message calls.
16244
- - Keep the code small and direct. Avoid speculative branches, broad casts, and raw JSON dumps unless the user asked for them.
16245
- - Use \`console.log()\` only for intermediate diagnostics, not for the final user-facing answer.`;
17729
+ // src/openai-usage.ts
17730
+ var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
17731
+ var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
17732
+ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
17733
+ "gpt-5.4": {
17734
+ provider: "openai",
17735
+ model: "gpt-5.4",
17736
+ currency: "USD",
17737
+ inputUsdPerMillion: 2.5,
17738
+ cachedInputUsdPerMillion: 0.25,
17739
+ outputUsdPerMillion: 15,
17740
+ sourceUrl: OPENAI_PRICING_SOURCE_URL,
17741
+ effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
17742
+ }
17743
+ };
17744
+ function asRecord5(value) {
17745
+ return value && typeof value === "object" ? value : null;
17746
+ }
17747
+ function numberField(record, key) {
17748
+ const value = record?.[key];
17749
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
17750
+ }
17751
+ function microsPerMillion(usdPerMillion) {
17752
+ return Math.round(usdPerMillion * 1e6);
17753
+ }
17754
+ function getOpenAIModelPricing(model) {
17755
+ return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
17756
+ }
17757
+ function normalizeOpenAIUsage(rawUsage) {
17758
+ const usage = asRecord5(rawUsage);
17759
+ if (!usage) {
17760
+ return {
17761
+ inputTokens: 0,
17762
+ cachedInputTokens: 0,
17763
+ uncachedInputTokens: 0,
17764
+ outputTokens: 0,
17765
+ reasoningTokens: 0,
17766
+ totalTokens: 0
17767
+ };
17768
+ }
17769
+ const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
17770
+ const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
17771
+ const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
17772
+ const inputDetails = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
17773
+ const outputDetails = asRecord5(usage.completion_tokens_details) || asRecord5(usage.output_tokens_details);
17774
+ const cachedInputTokens = Math.min(
17775
+ inputTokens,
17776
+ numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
17777
+ );
17778
+ const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
17779
+ return {
17780
+ inputTokens,
17781
+ cachedInputTokens,
17782
+ uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
17783
+ outputTokens,
17784
+ reasoningTokens,
17785
+ totalTokens
17786
+ };
17787
+ }
17788
+ function calculateOpenAITokenSpend(model, rawUsage) {
17789
+ const pricing = getOpenAIModelPricing(model);
17790
+ if (!pricing) return null;
17791
+ const usage = normalizeOpenAIUsage(rawUsage);
17792
+ const inputPricePerMillionMicros = microsPerMillion(
17793
+ pricing.inputUsdPerMillion
17794
+ );
17795
+ const cachedInputPricePerMillionMicros = microsPerMillion(
17796
+ pricing.cachedInputUsdPerMillion
17797
+ );
17798
+ const outputPricePerMillionMicros = microsPerMillion(
17799
+ pricing.outputUsdPerMillion
17800
+ );
17801
+ const amountMicros = Math.round(
17802
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
17803
+ );
17804
+ return {
17805
+ provider: "openai",
17806
+ model,
17807
+ inputTokens: usage.inputTokens,
17808
+ cachedInputTokens: usage.cachedInputTokens,
17809
+ uncachedInputTokens: usage.uncachedInputTokens,
17810
+ outputTokens: usage.outputTokens,
17811
+ reasoningTokens: usage.reasoningTokens,
17812
+ totalTokens: usage.totalTokens,
17813
+ amountMicros,
17814
+ currency: "USD",
17815
+ inputPricePerMillionMicros,
17816
+ cachedInputPricePerMillionMicros,
17817
+ outputPricePerMillionMicros,
17818
+ pricingSource: pricing.sourceUrl,
17819
+ pricingEffectiveAt: pricing.effectiveDate,
17820
+ usage
17821
+ };
16246
17822
  }
16247
17823
 
16248
17824
  exports.Environment = Environment;
16249
17825
  exports.EnvironmentSession = EnvironmentSession;
16250
17826
  exports.Granular = Granular;
17827
+ exports.OPENAI_MODEL_PRICING_USD_PER_MILLION = OPENAI_MODEL_PRICING_USD_PER_MILLION;
16251
17828
  exports.OntologyHandle = OntologyHandle;
16252
17829
  exports.Session = Session;
16253
17830
  exports.WSClient = WSClient;
@@ -16261,17 +17838,24 @@ exports.buildGranularAgentSessionBlock = buildGranularAgentSessionBlock;
16261
17838
  exports.buildGranularAgentSystemPrompt = buildGranularAgentSystemPrompt;
16262
17839
  exports.buildGranularAgentToolBlock = buildGranularAgentToolBlock;
16263
17840
  exports.buildGranularAgentWorkflowBlock = buildGranularAgentWorkflowBlock;
17841
+ exports.buildOpenAISpendEventId = buildOpenAISpendEventId;
16264
17842
  exports.buildSessionTranscript = buildSessionTranscript;
17843
+ exports.calculateOpenAITokenSpend = calculateOpenAITokenSpend;
17844
+ exports.consumeGranularReasoningOnlyChunk = consumeGranularReasoningOnlyChunk;
17845
+ exports.consumeGranularReasoningTraceChunk = consumeGranularReasoningTraceChunk;
16265
17846
  exports.createHarnessVerifierSnapshot = createHarnessVerifierSnapshot;
16266
17847
  exports.evaluateContinuation = evaluateContinuation;
16267
17848
  exports.extractPromptTokens = extractPromptTokens;
16268
17849
  exports.getCurrentClosureId = getCurrentClosureId;
16269
17850
  exports.getExclusivePromptTarget = getExclusivePromptTarget;
17851
+ exports.getOpenAIModelPricing = getOpenAIModelPricing;
16270
17852
  exports.hasOpenPrompt = hasOpenPrompt;
16271
17853
  exports.invokeRegisteredEffect = invokeRegisteredEffect;
16272
17854
  exports.isLocalApiUrl = isLocalApiUrl;
16273
17855
  exports.normalizeEffectBehaviors = normalizeEffectBehaviors;
17856
+ exports.normalizeOpenAIUsage = normalizeOpenAIUsage;
16274
17857
  exports.normalizePrompt = normalizePrompt;
17858
+ exports.normalizePromptChoiceOption = normalizePromptChoiceOption;
16275
17859
  exports.normalizePromptText = normalizePromptText;
16276
17860
  exports.normalizePromptType = normalizePromptType;
16277
17861
  exports.projectConversationReferentFocus = projectConversationReferentFocus;
@@ -16280,11 +17864,14 @@ exports.projectHeapSummary = projectHeapSummary;
16280
17864
  exports.projectLoopSummary = projectLoopSummary;
16281
17865
  exports.projectWorkflowFocus = projectWorkflowFocus;
16282
17866
  exports.projectWorkflowSummary = projectWorkflowSummary;
17867
+ exports.recordOpenAIUsageSpend = recordOpenAIUsageSpend;
16283
17868
  exports.resolveApiUrl = resolveApiUrl;
16284
17869
  exports.resolveAuthTokenForApiUrl = resolveAuthTokenForApiUrl;
16285
17870
  exports.resolveJobPresentation = resolveJobPresentation;
16286
17871
  exports.resolvePromptAnswer = resolvePromptAnswer;
16287
17872
  exports.reviewGeneratedJobCode = reviewGeneratedJobCode;
16288
17873
  exports.scorePromptChoiceMatch = scorePromptChoiceMatch;
17874
+ exports.stripGranularReasoningTrace = stripGranularReasoningTrace;
17875
+ exports.toGranularHttpBase = toGranularHttpBase;
16289
17876
  //# sourceMappingURL=index.js.map
16290
17877
  //# sourceMappingURL=index.js.map