@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.
@@ -1,5 +1,6 @@
1
1
  import { writeFile, mkdir } from 'fs/promises';
2
2
  import path from 'path';
3
+ import OpenAI from 'openai';
3
4
  import * as Automerge from '@automerge/automerge';
4
5
 
5
6
  var __create = Object.create;
@@ -3931,11 +3932,22 @@ var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
3931
3932
  var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
3932
3933
  var MAX_TIMER_DELAY_MS = 2147483647;
3933
3934
  var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
3935
+ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
3936
+ var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
3934
3937
  function debugWs(...args) {
3935
3938
  if (DEBUG_WS) {
3936
3939
  console.log(...args);
3937
3940
  }
3938
3941
  }
3942
+ function rpcTimeoutMsForMethod(method) {
3943
+ switch (method) {
3944
+ case "domain.fetchPackagePart":
3945
+ case "domain.getSummary":
3946
+ return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
3947
+ default:
3948
+ return DEFAULT_RPC_TIMEOUT_MS;
3949
+ }
3950
+ }
3939
3951
  var WSClient = class {
3940
3952
  ws = null;
3941
3953
  url;
@@ -4360,13 +4372,14 @@ var WSClient = class {
4360
4372
  return new Promise((resolve, reject) => {
4361
4373
  this.messageQueue.push({ resolve, reject, id });
4362
4374
  this.ws.send(JSON.stringify(request));
4375
+ const timeoutMs = rpcTimeoutMsForMethod(method);
4363
4376
  setTimeout(() => {
4364
4377
  const pending = this.messageQueue.find((q) => q.id === id);
4365
4378
  if (pending) {
4366
4379
  this.messageQueue = this.messageQueue.filter((q) => q.id !== id);
4367
4380
  reject(new Error(`RPC timeout: ${method}`));
4368
4381
  }
4369
- }, 3e4);
4382
+ }, timeoutMs);
4370
4383
  });
4371
4384
  }
4372
4385
  async handleIncomingRpc(request) {
@@ -4480,10 +4493,48 @@ function normalizePromptText(value) {
4480
4493
  function extractPromptTokens(value) {
4481
4494
  return normalizePromptText(value).split(/\s+/).map((token) => token.trim()).filter((token) => token.length > 0);
4482
4495
  }
4496
+ function parseJsonPromptChoiceOption(option) {
4497
+ const trimmed = option.trim();
4498
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
4499
+ try {
4500
+ const parsed = JSON.parse(trimmed);
4501
+ return asRecord(parsed);
4502
+ } catch {
4503
+ return null;
4504
+ }
4505
+ }
4506
+ function normalizePromptChoiceOption(option) {
4507
+ if (typeof option === "string") {
4508
+ const record2 = parseJsonPromptChoiceOption(option);
4509
+ if (!record2) {
4510
+ return { value: option, label: option };
4511
+ }
4512
+ const value2 = typeof record2.value === "string" ? record2.value : typeof record2.id === "string" ? record2.id : typeof record2.label === "string" ? record2.label : JSON.stringify(record2);
4513
+ return {
4514
+ value: value2,
4515
+ label: typeof record2.label === "string" ? record2.label : value2,
4516
+ description: typeof record2.description === "string" ? record2.description : void 0
4517
+ };
4518
+ }
4519
+ const record = option;
4520
+ if (!record) {
4521
+ return { value: "", label: "" };
4522
+ }
4523
+ const nestedJson = (typeof record.value === "string" ? parseJsonPromptChoiceOption(record.value) : null) || (typeof record.label === "string" ? parseJsonPromptChoiceOption(record.label) : null);
4524
+ if (nestedJson) {
4525
+ return normalizePromptChoiceOption(nestedJson);
4526
+ }
4527
+ const value = typeof record.value === "string" ? record.value : typeof record.label === "string" ? record.label : JSON.stringify(record);
4528
+ return {
4529
+ value,
4530
+ label: typeof record.label === "string" ? record.label : value,
4531
+ description: typeof record.description === "string" ? record.description : void 0
4532
+ };
4533
+ }
4483
4534
  function scorePromptChoiceMatch(answer, answerTokens, option) {
4484
- const value = typeof option === "string" ? option : typeof option?.value === "string" ? option.value : "";
4485
- const label = typeof option === "string" ? option : typeof option?.label === "string" ? option.label : "";
4486
- const description = typeof option === "string" ? "" : typeof option?.description === "string" ? option.description : "";
4535
+ const choice = normalizePromptChoiceOption(option);
4536
+ const { value, label } = choice;
4537
+ const description = choice.description || "";
4487
4538
  const haystack = normalizePromptText([value, label, description].filter(Boolean).join(" "));
4488
4539
  if (!haystack) return { score: 0, resolvedValue: value || label || null };
4489
4540
  let score = 0;
@@ -4519,7 +4570,9 @@ function normalizePrompt(rawValue) {
4519
4570
  type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
4520
4571
  title: typeof source.title === "string" ? source.title : "Input required",
4521
4572
  message: typeof source.message === "string" ? source.message : "",
4522
- options: Array.isArray(source.options) ? source.options : void 0,
4573
+ options: Array.isArray(source.options) ? source.options.map(
4574
+ (option) => typeof option === "string" || asRecord(option) ? normalizePromptChoiceOption(option) : option
4575
+ ) : void 0,
4523
4576
  defaultValue: source.defaultValue,
4524
4577
  placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
4525
4578
  allowEmpty: typeof source.allowEmpty === "boolean" ? source.allowEmpty : void 0,
@@ -4547,9 +4600,26 @@ function resolvePromptAnswer(prompt, answer) {
4547
4600
  }
4548
4601
 
4549
4602
  // src/session.ts
4603
+ var PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS = 5e3;
4604
+ function withPromptTranscriptTimeout(promise) {
4605
+ let timeout = null;
4606
+ return Promise.race([
4607
+ promise,
4608
+ new Promise((_, reject) => {
4609
+ timeout = setTimeout(() => {
4610
+ reject(new Error("Timed out appending prompt answer transcript."));
4611
+ }, PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS);
4612
+ })
4613
+ ]).finally(() => {
4614
+ if (timeout) {
4615
+ clearTimeout(timeout);
4616
+ }
4617
+ });
4618
+ }
4550
4619
  var Session = class {
4551
4620
  client;
4552
4621
  clientId;
4622
+ initialQuota;
4553
4623
  jobsMap = /* @__PURE__ */ new Map();
4554
4624
  pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
4555
4625
  eventListeners = /* @__PURE__ */ new Map();
@@ -4563,9 +4633,12 @@ var Session = class {
4563
4633
  lastKnownTools = /* @__PURE__ */ new Map();
4564
4634
  /** Last seen live prompts, keyed by prompt id, for answer normalization */
4565
4635
  promptCache = /* @__PURE__ */ new Map();
4566
- constructor(client, clientId) {
4636
+ /** Prompt ids locally answered before the document sync catches up. */
4637
+ hiddenPromptIds = /* @__PURE__ */ new Set();
4638
+ constructor(client, clientId, options = {}) {
4567
4639
  this.client = client;
4568
4640
  this.clientId = clientId || `client_${Date.now()}`;
4641
+ this.initialQuota = options.initialQuota || null;
4569
4642
  this.setupEventHandlers();
4570
4643
  this.setupToolInvokeHandler();
4571
4644
  }
@@ -4614,6 +4687,16 @@ var Session = class {
4614
4687
  get document() {
4615
4688
  return this.client.doc;
4616
4689
  }
4690
+ get quota() {
4691
+ return this.getQuota();
4692
+ }
4693
+ getQuota() {
4694
+ const quota = this.client.doc.billing?.quota;
4695
+ if (quota && typeof quota === "object") {
4696
+ return quota;
4697
+ }
4698
+ return this.initialQuota;
4699
+ }
4617
4700
  get sessionId() {
4618
4701
  return this.client.currentSessionId;
4619
4702
  }
@@ -4698,8 +4781,9 @@ var Session = class {
4698
4781
  * `effect.invoke` RPC back to the sandbox effect host, where the registered handlers
4699
4782
  * execute locally and return the result to the sandbox.
4700
4783
  */
4701
- async submitJob(code, domainRevision) {
4702
- let revision = domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4784
+ async submitJob(code, domainRevisionOrOptions) {
4785
+ const options = typeof domainRevisionOrOptions === "string" ? { domainRevision: domainRevisionOrOptions } : domainRevisionOrOptions || {};
4786
+ let revision = options.domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4703
4787
  if (!revision) {
4704
4788
  try {
4705
4789
  const summary = await this.getDomain();
@@ -4714,7 +4798,9 @@ var Session = class {
4714
4798
  }
4715
4799
  const result = await this.client.call("job.submit", {
4716
4800
  domainRevision: revision,
4717
- code
4801
+ code,
4802
+ metadata: options.metadata,
4803
+ agent: options.agent
4718
4804
  });
4719
4805
  if (!result.jobId) {
4720
4806
  throw new Error("Failed to submit job: no jobId returned");
@@ -4755,25 +4841,39 @@ var Session = class {
4755
4841
  const prompt = this.promptCache.get(promptId);
4756
4842
  const resolvedAnswer = resolvePromptAnswer(prompt, answer);
4757
4843
  this.promptCache.delete(promptId);
4758
- await this.client.call("prompt.answer", {
4759
- promptId,
4760
- answer: resolvedAnswer,
4761
- value: resolvedAnswer
4762
- });
4844
+ this.hiddenPromptIds.add(promptId);
4845
+ try {
4846
+ await this.client.call("prompt.answer", {
4847
+ promptId,
4848
+ answer: resolvedAnswer,
4849
+ value: resolvedAnswer
4850
+ });
4851
+ } catch (error) {
4852
+ this.hiddenPromptIds.delete(promptId);
4853
+ if (prompt) {
4854
+ this.promptCache.set(promptId, prompt);
4855
+ }
4856
+ throw error;
4857
+ }
4763
4858
  try {
4764
4859
  const content = this.stringifyConversationValue(resolvedAnswer);
4765
4860
  if (content.trim()) {
4766
- await this.appendConversationMessage({
4767
- role: "user",
4768
- content,
4769
- promptId
4770
- });
4861
+ await withPromptTranscriptTimeout(
4862
+ this.appendConversationMessage({
4863
+ role: "user",
4864
+ content,
4865
+ promptId
4866
+ })
4867
+ );
4771
4868
  }
4772
4869
  } catch {
4773
4870
  }
4774
4871
  }
4775
4872
  async appendConversationMessage(input) {
4776
- return this.client.call("conversation.append", input);
4873
+ return this.client.call(
4874
+ "conversation.append",
4875
+ input
4876
+ );
4777
4877
  }
4778
4878
  /**
4779
4879
  * Get the current list of available effects.
@@ -4782,9 +4882,53 @@ var Session = class {
4782
4882
  getEffects() {
4783
4883
  const doc = this.client.doc;
4784
4884
  const toolMap = /* @__PURE__ */ new Map();
4785
- const domainPkg = doc.domain?.packages?.domain;
4786
- if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
4787
- for (const tool of domainPkg.tools) {
4885
+ const domainPackages = doc.domain?.packages;
4886
+ const packageCandidates = domainPackages && typeof domainPackages === "object" ? [
4887
+ domainPackages.domain,
4888
+ domainPackages["@sandbox/domain"],
4889
+ ...Object.values(domainPackages)
4890
+ ].filter(Boolean) : [];
4891
+ for (const domainPkg of packageCandidates) {
4892
+ if (domainPkg?.tools && Array.isArray(domainPkg.tools)) {
4893
+ for (const tool of domainPkg.tools) {
4894
+ if (!tool?.name || toolMap.has(tool.name)) continue;
4895
+ toolMap.set(tool.name, {
4896
+ name: tool.name,
4897
+ description: tool.description,
4898
+ inputSchema: tool.inputSchema,
4899
+ outputSchema: tool.outputSchema,
4900
+ className: tool.className || void 0,
4901
+ static: tool.static || false,
4902
+ ready: false,
4903
+ publishedAt: void 0
4904
+ });
4905
+ }
4906
+ }
4907
+ if (!domainPkg?.classes || typeof domainPkg.classes !== "object") {
4908
+ continue;
4909
+ }
4910
+ for (const [className, classDef] of Object.entries(
4911
+ domainPkg.classes
4912
+ )) {
4913
+ const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
4914
+ for (const method of methods) {
4915
+ if (!method?.name || toolMap.has(method.name)) continue;
4916
+ toolMap.set(method.name, {
4917
+ name: method.name,
4918
+ description: method.description,
4919
+ inputSchema: method.inputSchema,
4920
+ outputSchema: method.outputSchema,
4921
+ className: method.className || classDef?.name || className,
4922
+ static: method.static || false,
4923
+ ready: false,
4924
+ publishedAt: void 0
4925
+ });
4926
+ }
4927
+ }
4928
+ }
4929
+ const legacyDomainPkg = doc.domain?.packages?.domain;
4930
+ if (legacyDomainPkg?.tools && Array.isArray(legacyDomainPkg.tools)) {
4931
+ for (const tool of legacyDomainPkg.tools) {
4788
4932
  if (!tool?.name) continue;
4789
4933
  toolMap.set(tool.name, {
4790
4934
  name: tool.name,
@@ -4798,6 +4942,27 @@ var Session = class {
4798
4942
  });
4799
4943
  }
4800
4944
  }
4945
+ if (legacyDomainPkg?.classes && typeof legacyDomainPkg.classes === "object") {
4946
+ for (const [className, classDef] of Object.entries(
4947
+ legacyDomainPkg.classes
4948
+ )) {
4949
+ const methods = Array.isArray(classDef?.methods) ? classDef.methods : [];
4950
+ for (const method of methods) {
4951
+ if (!method?.name || toolMap.has(method.name)) continue;
4952
+ toolMap.set(method.name, {
4953
+ name: method.name,
4954
+ description: method.description,
4955
+ inputSchema: method.inputSchema,
4956
+ outputSchema: method.outputSchema,
4957
+ className: method.className || classDef?.name || className,
4958
+ static: method.static || false,
4959
+ ready: false,
4960
+ publishedAt: void 0
4961
+ });
4962
+ }
4963
+ }
4964
+ }
4965
+ const hasPolicyFilteredDomainTools = toolMap.size > 0;
4801
4966
  const catalogs = doc.catalog?.rawToolCatalogs || {};
4802
4967
  for (const [clientId, catalog] of Object.entries(catalogs)) {
4803
4968
  const cat = catalog;
@@ -4805,6 +4970,7 @@ var Session = class {
4805
4970
  for (const tool of cat.tools) {
4806
4971
  if (!tool?.name) continue;
4807
4972
  const existing = toolMap.get(tool.name);
4973
+ if (hasPolicyFilteredDomainTools && !existing) continue;
4808
4974
  if (existing?.publishedAt && cat.publishedAt && existing.publishedAt > cat.publishedAt)
4809
4975
  continue;
4810
4976
  const isLocal = clientId === this.clientId;
@@ -4824,6 +4990,24 @@ var Session = class {
4824
4990
  }
4825
4991
  return Array.from(toolMap.values());
4826
4992
  }
4993
+ /**
4994
+ * Return the currently open prompt payloads known to this session.
4995
+ *
4996
+ * These come from live `prompt` / `prompt.request` websocket events and
4997
+ * preserve the exact shape used by `answerPrompt(...)`.
4998
+ */
4999
+ getPrompts() {
5000
+ return Array.from(this.promptCache.values()).map((prompt) => ({
5001
+ ...prompt,
5002
+ options: Array.isArray(prompt.options) ? prompt.options.map(
5003
+ (option) => typeof option === "string" ? option : { ...option }
5004
+ ) : void 0,
5005
+ metadata: prompt.metadata ? { ...prompt.metadata } : void 0
5006
+ }));
5007
+ }
5008
+ getHiddenPromptIds() {
5009
+ return Array.from(this.hiddenPromptIds);
5010
+ }
4827
5011
  /**
4828
5012
  * Backwards-compatible alias for `getEffects()`.
4829
5013
  */
@@ -4923,11 +5107,7 @@ var Session = class {
4923
5107
  if (!normalizedDocs) {
4924
5108
  return normalizedTypes;
4925
5109
  }
4926
- return [
4927
- normalizedTypes,
4928
- "Generated usage notes from ./sandbox-tools docs:",
4929
- normalizedDocs
4930
- ].join("\n\n");
5110
+ return [normalizedTypes, "[Docs]", normalizedDocs].join("\n\n");
4931
5111
  }
4932
5112
  if (normalizedDocs) {
4933
5113
  return normalizedDocs;
@@ -5149,6 +5329,7 @@ import { ${allImports} } from "./sandbox-tools";
5149
5329
  const emitPrompt = (payload) => {
5150
5330
  const prompt = normalizePrompt(payload);
5151
5331
  if (!prompt) return;
5332
+ this.hiddenPromptIds.delete(prompt.id);
5152
5333
  this.promptCache.set(prompt.id, prompt);
5153
5334
  this.emit("prompt", prompt);
5154
5335
  };
@@ -5331,6 +5512,7 @@ var JobImplementation = class {
5331
5512
  eventListeners = /* @__PURE__ */ new Map();
5332
5513
  bufferedAgentMessages = [];
5333
5514
  bufferedAgentMessageIds = /* @__PURE__ */ new Set();
5515
+ resultSettled = false;
5334
5516
  metadata;
5335
5517
  constructor(id, client, initialState) {
5336
5518
  this.id = id;
@@ -5355,7 +5537,9 @@ var JobImplementation = class {
5355
5537
  if (execData.error) {
5356
5538
  this.finalize("failed", void 0, execData.error);
5357
5539
  } else {
5358
- this.finalize("succeeded", execData.result);
5540
+ this.finalize("succeeded", execData.result, void 0, {
5541
+ hasResult: Object.prototype.hasOwnProperty.call(execData, "result")
5542
+ });
5359
5543
  }
5360
5544
  this.emit("status", this.status);
5361
5545
  }
@@ -5391,9 +5575,6 @@ var JobImplementation = class {
5391
5575
  if (normalizedStatus === "failed" || normalizedStatus === "timeout" || normalizedStatus === "canceled") {
5392
5576
  this.finalize(normalizedStatus);
5393
5577
  }
5394
- if (normalizedStatus === "succeeded") {
5395
- this.finalize("succeeded");
5396
- }
5397
5578
  this.emit("status", normalizedStatus);
5398
5579
  });
5399
5580
  this.client.on(`job.${id}.stdout`, (line) => {
@@ -5413,7 +5594,7 @@ var JobImplementation = class {
5413
5594
  this.emit("stderr", line);
5414
5595
  });
5415
5596
  this.client.on(`job.${id}.result`, (result) => {
5416
- this.finalize("succeeded", result);
5597
+ this.finalize("succeeded", result, void 0, { hasResult: true });
5417
5598
  });
5418
5599
  this.client.on(`job.${id}.error`, (error) => {
5419
5600
  this.finalize("failed", void 0, error);
@@ -5434,7 +5615,9 @@ var JobImplementation = class {
5434
5615
  this.client.on("job.completed", (data) => {
5435
5616
  const jobData = data;
5436
5617
  if (jobData.jobId === id) {
5437
- this.finalize("succeeded", jobData.result);
5618
+ this.finalize("succeeded", jobData.result, void 0, {
5619
+ hasResult: true
5620
+ });
5438
5621
  this.emit("status", this.status);
5439
5622
  }
5440
5623
  });
@@ -5559,7 +5742,7 @@ var JobImplementation = class {
5559
5742
  this.metadata.status = "running";
5560
5743
  }
5561
5744
  }
5562
- finalize(status, result, error) {
5745
+ finalize(status, result, error, options = {}) {
5563
5746
  if (!this.metadata.startedAt) {
5564
5747
  this.metadata.startedAt = Date.now();
5565
5748
  }
@@ -5567,14 +5750,18 @@ var JobImplementation = class {
5567
5750
  this.metadata.status = status;
5568
5751
  this.metadata.completedAt = this.metadata.completedAt || Date.now();
5569
5752
  this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
5570
- if (result !== void 0) {
5753
+ if (!this.resultSettled && (options.hasResult || result !== void 0)) {
5571
5754
  this.metadata.result = sanitizeFeedbackValue(result);
5755
+ this.resultSettled = true;
5572
5756
  this._resolveResult(result);
5573
5757
  }
5574
- if (error !== void 0) {
5575
- const message = error instanceof Error ? error.message : String(error);
5758
+ if (!this.resultSettled && (error !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
5759
+ const fallbackError = new Error(`Job ${this.id} ${status}.`);
5760
+ const cause = error ?? fallbackError;
5761
+ const message = cause instanceof Error ? cause.message : String(cause);
5576
5762
  this.metadata.error = truncateFeedbackString(message);
5577
- this._rejectResult(error);
5763
+ this.resultSettled = true;
5764
+ this._rejectResult(cause);
5578
5765
  }
5579
5766
  }
5580
5767
  upsertToolCall(next) {
@@ -5657,6 +5844,17 @@ function humanTextFromStdout(stdout) {
5657
5844
  }
5658
5845
  return null;
5659
5846
  }
5847
+ function responseTextFromAgentMessages(agentMessages) {
5848
+ for (const message of [...agentMessages].reverse()) {
5849
+ const record = asRecord2(message);
5850
+ if (!record) continue;
5851
+ for (const key of RESPONSE_KEYS) {
5852
+ const normalized = normalizeText(record[key]);
5853
+ if (normalized) return normalized;
5854
+ }
5855
+ }
5856
+ return null;
5857
+ }
5660
5858
  function pushString(target, value) {
5661
5859
  if (typeof value === "string" && value.trim()) {
5662
5860
  target.add(value.trim());
@@ -5682,6 +5880,41 @@ function collectReferencesFromRecord(record, refs) {
5682
5880
  for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
5683
5881
  pushStringArray(refs.variableNames, record[key]);
5684
5882
  }
5883
+ function stringValue(record, keys) {
5884
+ for (const key of keys) {
5885
+ const value = record[key];
5886
+ if (typeof value === "string" && value.trim()) {
5887
+ return value.trim();
5888
+ }
5889
+ }
5890
+ return null;
5891
+ }
5892
+ function findEntryPathForRecord(record, heap) {
5893
+ const directPath = stringValue(record, ["entryPath", "path"]);
5894
+ if (directPath && heap.entriesByPath?.[directPath]) {
5895
+ return directPath;
5896
+ }
5897
+ const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
5898
+ if (!id) {
5899
+ return null;
5900
+ }
5901
+ const className = stringValue(record, [
5902
+ "className",
5903
+ "_className",
5904
+ "__className",
5905
+ "prototype",
5906
+ "type"
5907
+ ]);
5908
+ const entries = Object.values(heap.entriesByPath || {});
5909
+ const exact = entries.find(
5910
+ (entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
5911
+ );
5912
+ if (exact?.path) {
5913
+ return exact.path;
5914
+ }
5915
+ const idOnlyMatches = entries.filter((entry) => entry.id === id);
5916
+ return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
5917
+ }
5685
5918
  function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
5686
5919
  if (value === null || value === void 0 || depth > 4 || seen.has(value))
5687
5920
  return;
@@ -5702,6 +5935,8 @@ function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__
5702
5935
  const record = asRecord2(value);
5703
5936
  if (!record) return;
5704
5937
  seen.add(value);
5938
+ const entryPath = findEntryPathForRecord(record, heap);
5939
+ if (entryPath) refs.entryPaths.add(entryPath);
5705
5940
  collectReferencesFromRecord(record, refs);
5706
5941
  for (const key of UI_CONTAINER_KEYS) {
5707
5942
  const nested = asRecord2(record[key]);
@@ -5810,6 +6045,7 @@ function resolveJobPresentation({
5810
6045
  jobId,
5811
6046
  result,
5812
6047
  stdout = [],
6048
+ agentMessages = [],
5813
6049
  sessionHeap,
5814
6050
  allowExplicitArtifacts = true
5815
6051
  }) {
@@ -5842,7 +6078,7 @@ function resolveJobPresentation({
5842
6078
  const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
5843
6079
  const lists = hasExplicitArtifacts ? explicitLists : jobLists;
5844
6080
  const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
5845
- const responseText = extractResponseText(result, stdout) || fallbackResponseText(entries, lists);
6081
+ const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
5846
6082
  return {
5847
6083
  responseText,
5848
6084
  entries,
@@ -10318,6 +10554,67 @@ external_exports.object({
10318
10554
  transitions: external_exports.array(StateMachineTransitionSchema),
10319
10555
  finalStates: external_exports.array(external_exports.string()).optional()
10320
10556
  }).strict();
10557
+ var POLICY_OPERATORS = [
10558
+ "eq",
10559
+ "neq",
10560
+ "gt",
10561
+ "gte",
10562
+ "lt",
10563
+ "lte",
10564
+ "contains",
10565
+ "not_contains",
10566
+ "starts_with",
10567
+ "ends_with",
10568
+ "exists"
10569
+ ];
10570
+ var PolicyPredicateSchema = external_exports.object({
10571
+ path: external_exports.union([external_exports.string(), external_exports.array(external_exports.string())]).optional(),
10572
+ field: external_exports.string().optional(),
10573
+ input: external_exports.string().optional(),
10574
+ operator: external_exports.enum([...POLICY_OPERATORS]),
10575
+ stringValue: external_exports.string().optional(),
10576
+ numberValue: external_exports.number().optional(),
10577
+ booleanValue: external_exports.boolean().optional(),
10578
+ value: external_exports.union([external_exports.string(), external_exports.number(), external_exports.boolean(), external_exports.null()]).optional()
10579
+ }).strict();
10580
+ var PolicyStateMachinePredicateSchema = external_exports.object({
10581
+ machine: external_exports.string().min(1),
10582
+ operator: external_exports.enum([...POLICY_OPERATORS]),
10583
+ state: external_exports.string().optional(),
10584
+ stringValue: external_exports.string().optional()
10585
+ }).strict();
10586
+ var PolicyConditionSchema = external_exports.lazy(
10587
+ () => external_exports.object({
10588
+ all: external_exports.array(PolicyConditionSchema).optional(),
10589
+ any: external_exports.array(PolicyConditionSchema).optional(),
10590
+ not: PolicyConditionSchema.optional(),
10591
+ input: PolicyPredicateSchema.optional(),
10592
+ object: PolicyPredicateSchema.optional(),
10593
+ stateMachine: PolicyStateMachinePredicateSchema.optional()
10594
+ }).strict().refine(
10595
+ (data) => [
10596
+ data.all,
10597
+ data.any,
10598
+ data.not,
10599
+ data.input,
10600
+ data.object,
10601
+ data.stateMachine
10602
+ ].filter((value) => value !== void 0).length === 1,
10603
+ {
10604
+ message: "Policy condition must define exactly one of all, any, not, input, object, or stateMachine"
10605
+ }
10606
+ )
10607
+ );
10608
+ var PolicyRuleSchema = external_exports.object({
10609
+ id: external_exports.string().min(1).optional(),
10610
+ reason: external_exports.string().optional(),
10611
+ when: PolicyConditionSchema
10612
+ }).strict();
10613
+ var PoliciesSchema = external_exports.object({
10614
+ allowWhen: external_exports.array(PolicyRuleSchema).optional(),
10615
+ confirmWhen: external_exports.array(PolicyRuleSchema).optional(),
10616
+ denyWhen: external_exports.array(PolicyRuleSchema).optional()
10617
+ }).strict();
10321
10618
  external_exports.object({
10322
10619
  postCondition: external_exports.union([
10323
10620
  external_exports.string(),
@@ -10347,7 +10644,8 @@ external_exports.object({
10347
10644
  reason: external_exports.string().optional(),
10348
10645
  mode: external_exports.string().optional()
10349
10646
  }).strict()
10350
- ]).optional()
10647
+ ]).optional(),
10648
+ policies: PoliciesSchema.optional()
10351
10649
  }).strict();
10352
10650
 
10353
10651
  // ../metamodel-core/src/index.ts
@@ -11021,6 +11319,110 @@ async function invokeRegisteredEffect(effectMap, request) {
11021
11319
  return resolved.handler(request.input, context);
11022
11320
  }
11023
11321
 
11322
+ // src/spend.ts
11323
+ function toGranularHttpBase(apiUrl) {
11324
+ const url = new URL(apiUrl);
11325
+ if (url.protocol === "ws:") {
11326
+ url.protocol = "http:";
11327
+ } else if (url.protocol === "wss:") {
11328
+ url.protocol = "https:";
11329
+ }
11330
+ url.pathname = url.pathname.replace(/\/ws\/connect$/, "").replace(/\/ws$/, "");
11331
+ if (!url.pathname || url.pathname === "/") {
11332
+ url.pathname = "/granular";
11333
+ }
11334
+ url.search = "";
11335
+ url.hash = "";
11336
+ return url.toString().replace(/\/$/, "");
11337
+ }
11338
+ function cleanIdPart(value) {
11339
+ return value.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
11340
+ }
11341
+ function buildOpenAISpendEventId(usage, context = {}) {
11342
+ const requestId = usage.requestId?.trim();
11343
+ if (!requestId) return void 0;
11344
+ const scope = context.sessionId || context.environmentId || context.subjectId || context.sandboxId || "global";
11345
+ return ["spend", "openai", scope, requestId].map(cleanIdPart).join("_");
11346
+ }
11347
+ function pricingEffectiveAtSeconds(value) {
11348
+ if (!value) return null;
11349
+ const parsed = Date.parse(value);
11350
+ return Number.isFinite(parsed) ? Math.floor(parsed / 1e3) : null;
11351
+ }
11352
+ function compactContext(context) {
11353
+ return Object.fromEntries(
11354
+ Object.entries(context).filter(
11355
+ ([, value]) => value != null && value !== ""
11356
+ )
11357
+ );
11358
+ }
11359
+ function omitTenantId(context) {
11360
+ const scopedContext = { ...context };
11361
+ delete scopedContext.tenantId;
11362
+ return scopedContext;
11363
+ }
11364
+ async function recordOpenAIUsageSpend(options) {
11365
+ const usageContext = compactContext({
11366
+ ...options.usage.usageContext || {},
11367
+ ...options.context || {}
11368
+ });
11369
+ const context = omitTenantId(usageContext);
11370
+ const spendEventId = options.usage.spendEventId || buildOpenAISpendEventId(options.usage, context);
11371
+ const metadata = {
11372
+ ...options.metadata || {},
11373
+ ...options.usage.rawUsage !== void 0 ? { openaiUsage: options.usage.rawUsage } : {},
11374
+ usageContext: context
11375
+ };
11376
+ const response = await fetch(
11377
+ `${toGranularHttpBase(options.apiUrl)}/control/spend/events`,
11378
+ {
11379
+ method: "POST",
11380
+ cache: "no-store",
11381
+ headers: {
11382
+ Authorization: `Bearer ${options.token}`,
11383
+ "Content-Type": "application/json"
11384
+ },
11385
+ body: JSON.stringify({
11386
+ ...spendEventId ? { spendEventId } : {},
11387
+ sandboxId: context.sandboxId || null,
11388
+ environmentId: context.environmentId || null,
11389
+ sessionId: context.sessionId || null,
11390
+ subjectId: context.subjectId || null,
11391
+ permissionProfileId: context.permissionProfileId || null,
11392
+ source: "openai",
11393
+ lineItemType: "llm_tokens",
11394
+ provider: options.usage.provider,
11395
+ model: options.usage.model,
11396
+ operation: options.usage.operation || "chat.completions",
11397
+ requestId: options.usage.requestId || null,
11398
+ inputTokens: options.usage.inputTokens,
11399
+ outputTokens: options.usage.outputTokens,
11400
+ cachedInputTokens: options.usage.cachedInputTokens,
11401
+ reasoningTokens: options.usage.reasoningTokens,
11402
+ quantity: options.usage.totalTokens,
11403
+ quantityUnit: "tokens",
11404
+ inputPricePerMillionMicros: options.usage.inputPricePerMillionMicros,
11405
+ cachedInputPricePerMillionMicros: options.usage.cachedInputPricePerMillionMicros,
11406
+ outputPricePerMillionMicros: options.usage.outputPricePerMillionMicros,
11407
+ amountMicros: options.usage.amountMicros,
11408
+ currency: options.usage.currency,
11409
+ pricingSource: options.usage.pricingSource,
11410
+ pricingEffectiveAt: pricingEffectiveAtSeconds(
11411
+ options.usage.pricingEffectiveAt
11412
+ ),
11413
+ estimated: false,
11414
+ metadata
11415
+ })
11416
+ }
11417
+ );
11418
+ if (!response.ok) {
11419
+ throw new Error(
11420
+ `Granular spend event failed (${response.status}): ${await response.text()}`
11421
+ );
11422
+ }
11423
+ return response.json();
11424
+ }
11425
+
11024
11426
  // ../metamodel-enum/src/index.ts
11025
11427
  function renderInlineStringUnion(values) {
11026
11428
  return values.map((value) => JSON.stringify(value)).join(" | ");
@@ -11379,6 +11781,148 @@ var noteMetamodelPackage = defineMetamodelPackage({
11379
11781
  }
11380
11782
  });
11381
11783
 
11784
+ // ../policy-engine/src/index.ts
11785
+ function isRecord(value) {
11786
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
11787
+ }
11788
+ function normalizePath(value) {
11789
+ if (Array.isArray(value)) {
11790
+ return value.map((part) => String(part)).filter(Boolean);
11791
+ }
11792
+ if (typeof value === "string") {
11793
+ return value.includes(".") ? value.split(".").filter(Boolean) : [value];
11794
+ }
11795
+ return [];
11796
+ }
11797
+ function firstDefinedValue(spec) {
11798
+ if ("value" in spec) return spec.value;
11799
+ if ("stringValue" in spec) return spec.stringValue;
11800
+ if ("numberValue" in spec) return spec.numberValue;
11801
+ if ("booleanValue" in spec) return spec.booleanValue;
11802
+ if ("state" in spec) return spec.state;
11803
+ return void 0;
11804
+ }
11805
+ function normalizeCondition(input) {
11806
+ if (input === void 0 || input === null) return { kind: "always" };
11807
+ if (!isRecord(input)) {
11808
+ throw new Error("Policy condition must be an object");
11809
+ }
11810
+ if (Array.isArray(input.all)) {
11811
+ return {
11812
+ kind: "all",
11813
+ conditions: input.all.map((item) => normalizeCondition(item))
11814
+ };
11815
+ }
11816
+ if (Array.isArray(input.any)) {
11817
+ return {
11818
+ kind: "any",
11819
+ conditions: input.any.map((item) => normalizeCondition(item))
11820
+ };
11821
+ }
11822
+ if (input.not !== void 0) {
11823
+ return { kind: "not", condition: normalizeCondition(input.not) };
11824
+ }
11825
+ for (const source of ["input", "object", "stateMachine"]) {
11826
+ const raw = input[source];
11827
+ if (!isRecord(raw)) continue;
11828
+ const operator = raw.operator;
11829
+ 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") {
11830
+ throw new Error(`Unsupported policy operator: ${String(operator)}`);
11831
+ }
11832
+ if (source === "stateMachine") {
11833
+ const machine = typeof raw.machine === "string" ? raw.machine : "";
11834
+ if (!machine) throw new Error("stateMachine condition requires machine");
11835
+ return {
11836
+ kind: "predicate",
11837
+ source,
11838
+ path: [machine],
11839
+ machine,
11840
+ operator,
11841
+ value: firstDefinedValue(raw)
11842
+ };
11843
+ }
11844
+ const path2 = normalizePath(raw.path ?? raw.field ?? raw.input);
11845
+ if (path2.length === 0) {
11846
+ throw new Error(`${source} condition requires a path`);
11847
+ }
11848
+ return {
11849
+ kind: "predicate",
11850
+ source,
11851
+ path: path2,
11852
+ operator,
11853
+ value: firstDefinedValue(raw)
11854
+ };
11855
+ }
11856
+ throw new Error(
11857
+ "Policy condition must contain all, any, not, input, object, or stateMachine"
11858
+ );
11859
+ }
11860
+ function summarizeCondition(condition) {
11861
+ switch (condition.kind) {
11862
+ case "always":
11863
+ return "always";
11864
+ case "all":
11865
+ return condition.conditions.map(summarizeCondition).join(" and ");
11866
+ case "any":
11867
+ return condition.conditions.map(summarizeCondition).join(" or ");
11868
+ case "not":
11869
+ return `not (${summarizeCondition(condition.condition)})`;
11870
+ case "predicate": {
11871
+ const path2 = condition.source === "stateMachine" ? `stateMachine.${condition.machine || condition.path.join(".")}` : `${condition.source}.${condition.path.join(".")}`;
11872
+ if (condition.operator === "exists") return `${path2} exists`;
11873
+ return `${path2} ${condition.operator} ${String(condition.value)}`;
11874
+ }
11875
+ }
11876
+ }
11877
+
11878
+ // ../metamodel-policy/src/index.ts
11879
+ function escapeGraphqlString(value) {
11880
+ return JSON.stringify(value);
11881
+ }
11882
+ function buildPolicyMutations(effectKey, spec) {
11883
+ const policies = spec.policies;
11884
+ if (!policies) return [];
11885
+ const mutations = [];
11886
+ const addRules = (key, outcome) => {
11887
+ const rules = policies[key] || [];
11888
+ rules.forEach((rule, index) => {
11889
+ const condition = normalizeCondition(rule.when);
11890
+ const summary = rule.reason || summarizeCondition(condition);
11891
+ const id = rule.id || `${effectKey}:${outcome}:${index + 1}`;
11892
+ mutations.push({
11893
+ label: `set policy ${outcome} on ${effectKey}`,
11894
+ 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))}) }`
11895
+ });
11896
+ });
11897
+ };
11898
+ addRules("allowWhen", "allow");
11899
+ addRules("confirmWhen", "confirm");
11900
+ addRules("denyWhen", "deny");
11901
+ return mutations;
11902
+ }
11903
+ var policyMetamodelPackage = defineMetamodelPackage({
11904
+ id: "policy",
11905
+ manifest: {
11906
+ buildEffectMutations: buildPolicyMutations
11907
+ },
11908
+ summary: {
11909
+ selections: {
11910
+ methodFields: ["policies"]
11911
+ },
11912
+ readMethodSummary(rawMethod) {
11913
+ return rawMethod.policies ? { metamodels: { policies: rawMethod.policies } } : {};
11914
+ }
11915
+ },
11916
+ docs: {
11917
+ effectRows: [
11918
+ {
11919
+ key: "policies",
11920
+ description: "Universal effect policies with allowWhen, confirmWhen, and denyWhen structural conditions."
11921
+ }
11922
+ ]
11923
+ }
11924
+ });
11925
+
11382
11926
  // ../metamodel-required/src/index.ts
11383
11927
  function buildRequiredFieldMutations(fieldPath, required) {
11384
11928
  if (!required) return [];
@@ -12153,7 +12697,8 @@ var DEFAULT_METAMODEL_PACKAGES = [
12153
12697
  searchableMetamodelPackage,
12154
12698
  validationRuleMetamodelPackage,
12155
12699
  stateMachineMetamodelPackage,
12156
- effectBehaviorsMetamodelPackage
12700
+ effectBehaviorsMetamodelPackage,
12701
+ policyMetamodelPackage
12157
12702
  ];
12158
12703
  createMetamodelRegistry(
12159
12704
  DEFAULT_METAMODEL_PACKAGES
@@ -12220,6 +12765,12 @@ var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_COUNT = 3;
12220
12765
  var DEFAULT_DIRECT_RECORD_OBJECTS_RETRY_DELAY_MS = 1e3;
12221
12766
  var LOCAL_CONTROL_REQUEST_RETRY_COUNT = 4;
12222
12767
  var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
12768
+ var SESSION_DATA_REQUEST_RETRY_COUNT = 4;
12769
+ var SESSION_DATA_REQUEST_RETRY_DELAY_MS = 500;
12770
+ var EFFECT_HOST_CONNECT_TIMEOUT_MS = 15e3;
12771
+ var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 3e4;
12772
+ var EFFECT_CATALOG_SYNC_RETRY_COUNT = 3;
12773
+ var EFFECT_CATALOG_SYNC_RETRY_DELAY_MS = 1e3;
12223
12774
  function planRecordObjectsChunks(records, batchSize) {
12224
12775
  const total = records.length;
12225
12776
  const size = Math.max(1, Math.min(batchSize, total));
@@ -12234,6 +12785,19 @@ function planRecordObjectsChunks(records, batchSize) {
12234
12785
  function sleep(ms) {
12235
12786
  return new Promise((resolve) => setTimeout(resolve, ms));
12236
12787
  }
12788
+ function withTimeout(promise, timeoutMs, label) {
12789
+ let timer = null;
12790
+ const timeout = new Promise((_, reject) => {
12791
+ timer = setTimeout(() => {
12792
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
12793
+ }, timeoutMs);
12794
+ });
12795
+ return Promise.race([promise, timeout]).finally(() => {
12796
+ if (timer) {
12797
+ clearTimeout(timer);
12798
+ }
12799
+ });
12800
+ }
12237
12801
  function isLocalControlUrl(url) {
12238
12802
  try {
12239
12803
  const parsed = new URL(url);
@@ -12247,7 +12811,19 @@ function isRetryableLocalWorkerRestart(status, body, url) {
12247
12811
  }
12248
12812
  function isRetryableRecordObjectsError(error) {
12249
12813
  const message = error instanceof Error ? error.message : String(error);
12250
- return /worker restarted mid-request|econnreset|network|socket connection was closed unexpectedly|timed out/i.test(
12814
+ 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(
12815
+ message
12816
+ );
12817
+ }
12818
+ function isRetryableEffectRegistrationError(error) {
12819
+ const message = error instanceof Error ? error.message : String(error);
12820
+ 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(
12821
+ message
12822
+ );
12823
+ }
12824
+ function isRetryableSessionDataError(error) {
12825
+ const message = error instanceof Error ? error.message : String(error);
12826
+ 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(
12251
12827
  message
12252
12828
  );
12253
12829
  }
@@ -12269,16 +12845,28 @@ function computeEffectRegistrationKey(effect) {
12269
12845
  effect.versionSelector
12270
12846
  )}`;
12271
12847
  }
12272
- function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
12273
- const url = new URL(apiUrl);
12274
- if (url.pathname.endsWith("/granular/ws/connect")) {
12848
+ function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId, effectHostUrl) {
12849
+ const overrideUrl = effectHostUrl || process.env.GRANULAR_EFFECT_HOST_URL || process.env.EFFECT_HOST_URL;
12850
+ const api = new URL(apiUrl);
12851
+ const localRuntimeBase = process.env.RUNTIME_ORCHESTRATOR_URL || (isLocalControlUrl(apiUrl) ? `${api.protocol}//${api.hostname}:8791` : "");
12852
+ const url = new URL(overrideUrl || localRuntimeBase || apiUrl);
12853
+ if (url.protocol === "https:") {
12854
+ url.protocol = "wss:";
12855
+ } else if (url.protocol === "http:") {
12856
+ url.protocol = "ws:";
12857
+ }
12858
+ if (!overrideUrl && isLocalControlUrl(apiUrl) && api.pathname.endsWith("/granular")) {
12859
+ url.pathname = "/granular/orchestrator/effects/connect";
12860
+ } else if (url.pathname.endsWith("/granular/ws/connect")) {
12275
12861
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
12276
12862
  } else if (url.pathname.endsWith("/granular")) {
12277
- url.pathname = `${url.pathname.replace(/\/$/, "")}/effects/connect`;
12863
+ url.pathname = isLocalControlUrl(url.toString()) ? "/granular/orchestrator/effects/connect" : `${url.pathname.replace(/\/$/, "")}/effects/connect`;
12278
12864
  } else if (url.pathname.endsWith("/v2/ws/connect")) {
12279
12865
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
12280
12866
  } else if (url.pathname.endsWith("/v2/ws")) {
12281
12867
  url.pathname = url.pathname.replace(/\/ws$/, "/effects/connect");
12868
+ } else if (url.pathname === "/" && isLocalControlUrl(url.toString()) && (url.port === "8791" || !overrideUrl && Boolean(localRuntimeBase))) {
12869
+ url.pathname = "/granular/orchestrator/effects/connect";
12282
12870
  } else if (url.pathname.endsWith("/ws/connect")) {
12283
12871
  url.pathname = url.pathname.replace(/\/ws\/connect$/, "/effects/connect");
12284
12872
  } else if (url.pathname.endsWith("/ws")) {
@@ -12313,6 +12901,79 @@ function normalizeHeapSnapshot(raw) {
12313
12901
  updatedAt: typeof heap.updatedAt === "number" ? heap.updatedAt : Date.now()
12314
12902
  };
12315
12903
  }
12904
+ function normalizeGraphPathSegment(value) {
12905
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
12906
+ }
12907
+ function extractRecordIdFromGraphPath(path2, className) {
12908
+ const normalizedPrefix = `${normalizeGraphPathSegment(className)}_`;
12909
+ if (path2.startsWith(normalizedPrefix)) {
12910
+ return path2.slice(normalizedPrefix.length);
12911
+ }
12912
+ const legacyPrefix = `${className}_`;
12913
+ if (path2.startsWith(legacyPrefix)) {
12914
+ return path2.slice(legacyPrefix.length);
12915
+ }
12916
+ return path2;
12917
+ }
12918
+ function toRecordSearchResult(className, node) {
12919
+ const path2 = typeof node.path === "string" ? node.path : "";
12920
+ if (!path2) return null;
12921
+ const fields = Array.isArray(node.submodels) ? node.submodels.flatMap(
12922
+ (submodel) => {
12923
+ const name = typeof submodel?.label === "string" && submodel.label.trim() ? submodel.label : typeof submodel?.path === "string" ? submodel.path.split(":").pop() || submodel.path : "";
12924
+ if (!name) return [];
12925
+ if (typeof submodel.string_value === "string") {
12926
+ return [{ name, type: "string", value: submodel.string_value }];
12927
+ }
12928
+ if (typeof submodel.number_value === "number") {
12929
+ return [{ name, type: "number", value: submodel.number_value }];
12930
+ }
12931
+ if (typeof submodel.boolean_value === "boolean") {
12932
+ return [
12933
+ {
12934
+ name,
12935
+ type: "boolean",
12936
+ value: submodel.boolean_value
12937
+ }
12938
+ ];
12939
+ }
12940
+ return [];
12941
+ }
12942
+ ) : [];
12943
+ return {
12944
+ path: path2,
12945
+ className,
12946
+ id: extractRecordIdFromGraphPath(path2, className),
12947
+ label: typeof node.label === "string" && node.label.trim() ? node.label : extractRecordIdFromGraphPath(path2, className),
12948
+ description: typeof node.description === "string" && node.description.trim() ? node.description : null,
12949
+ fields
12950
+ };
12951
+ }
12952
+ function normalizeRecordSearchText(value) {
12953
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim();
12954
+ }
12955
+ function rankRecordSearchResult(result, query, index) {
12956
+ const normalizedQuery = normalizeRecordSearchText(query);
12957
+ if (!normalizedQuery) {
12958
+ return index;
12959
+ }
12960
+ const label = normalizeRecordSearchText(result.label || "");
12961
+ const id = normalizeRecordSearchText(result.id || "");
12962
+ const path2 = normalizeRecordSearchText(result.path || "");
12963
+ const className = normalizeRecordSearchText(result.className || "");
12964
+ const searchable = [label, id, path2, className].filter(Boolean);
12965
+ if (label === normalizedQuery) return index;
12966
+ if (id === normalizedQuery || path2 === normalizedQuery) return 100 + index;
12967
+ if (label.startsWith(normalizedQuery)) return 200 + index;
12968
+ if (searchable.some((value) => value.startsWith(normalizedQuery))) {
12969
+ return 300 + index;
12970
+ }
12971
+ if (label.includes(normalizedQuery)) return 400 + index;
12972
+ if (searchable.some((value) => value.includes(normalizedQuery))) {
12973
+ return 500 + index;
12974
+ }
12975
+ return 900 + index;
12976
+ }
12316
12977
  function deriveRuntimeBaseUrl(apiEndpoint) {
12317
12978
  try {
12318
12979
  const endpoint = new URL(apiEndpoint);
@@ -12401,7 +13062,7 @@ function normalizeEnvironmentData(environment) {
12401
13062
  setup: normalizeEnvironmentSetupSummary(environment.setup)
12402
13063
  };
12403
13064
  }
12404
- var Environment = class {
13065
+ var Environment = class _Environment {
12405
13066
  granular;
12406
13067
  envData;
12407
13068
  _apiKey;
@@ -12596,28 +13257,30 @@ var Environment = class {
12596
13257
  return response.json();
12597
13258
  }
12598
13259
  // ==================== ID ↔ GRAPH PATH MAPPING ====================
13260
+ static normalizeGraphPathSegment(value) {
13261
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^A-Za-z0-9]+/g, "_").replace(/_+/g, "_").replace(/^_+|_+$/g, "").toLowerCase();
13262
+ }
12599
13263
  /**
12600
- * Convert a class name + real-world ID into a unique graph path.
12601
- *
12602
- * Two objects of *different* classes may share the same real-world ID,
12603
- * so the graph path must incorporate the class to guarantee uniqueness.
12604
- *
12605
- * Format: `{className}_{id}` — deterministic, human-readable.
13264
+ * Convert a class name + application record ID into Granular's graph path.
12606
13265
  *
12607
- * **Convention**: class names should be simple identifiers without
12608
- * underscores (e.g. `author`, `book`). This ensures the prefix is
12609
- * unambiguously parseable by `extractIdFromGraphPath`.
13266
+ * This mirrors the record-write path normalization used by the control plane.
13267
+ * Keep the original customer/system ID in `real_id`; graph paths are stable
13268
+ * internal addresses, not the source of truth for business identity.
12610
13269
  */
12611
13270
  static toGraphPath(className, id) {
12612
- return `${className}_${id}`;
13271
+ return `${_Environment.normalizeGraphPathSegment(className)}_${_Environment.normalizeGraphPathSegment(id)}`;
12613
13272
  }
12614
13273
  /**
12615
- * Extract the real-world ID from a graph path, given the class name.
13274
+ * Best-effort extraction of an ID-like suffix from a graph path.
12616
13275
  *
12617
- * Strips the `{className}_` prefix. Returns the raw path if the
12618
- * expected prefix is not found.
13276
+ * Prefer the record's `real_id` field whenever exact customer/system IDs
13277
+ * matter, because graph path normalization is intentionally lossy.
12619
13278
  */
12620
13279
  static extractIdFromGraphPath(graphPath, className) {
13280
+ const normalizedPrefix = `${_Environment.normalizeGraphPathSegment(className)}_`;
13281
+ if (graphPath.startsWith(normalizedPrefix)) {
13282
+ return graphPath.substring(normalizedPrefix.length);
13283
+ }
12621
13284
  const prefix = `${className}_`;
12622
13285
  return graphPath.startsWith(prefix) ? graphPath.substring(prefix.length) : graphPath;
12623
13286
  }
@@ -12664,6 +13327,62 @@ var Environment = class {
12664
13327
  }
12665
13328
  return response.json();
12666
13329
  }
13330
+ async searchRecords(query, options = {}) {
13331
+ const normalizedQuery = query.replace(/\s+/g, " ").trim();
13332
+ const limit = Math.max(1, Math.min(50, Math.floor(options.limit ?? 12)));
13333
+ const offset = Math.max(0, Math.floor(options.offset ?? 0));
13334
+ const response = await this.graphql(
13335
+ `
13336
+ query RecordMentionSearch(
13337
+ $query: String
13338
+ $limit: Int
13339
+ $offset: Int
13340
+ $classNames: [String!]
13341
+ ) {
13342
+ record_search(
13343
+ query: $query
13344
+ limit: $limit
13345
+ offset: $offset
13346
+ class_names: $classNames
13347
+ ) {
13348
+ className
13349
+ model {
13350
+ path
13351
+ label
13352
+ description
13353
+ submodels {
13354
+ path
13355
+ label
13356
+ string_value
13357
+ number_value
13358
+ boolean_value
13359
+ }
13360
+ }
13361
+ }
13362
+ }
13363
+ `,
13364
+ {
13365
+ query: normalizedQuery,
13366
+ limit,
13367
+ offset,
13368
+ classNames: options.classNames?.length ? options.classNames : []
13369
+ }
13370
+ );
13371
+ const seen = /* @__PURE__ */ new Set();
13372
+ const results = (response.data?.record_search || []).flatMap((entry) => {
13373
+ const className = entry.className?.trim();
13374
+ const item = className && entry.model ? toRecordSearchResult(className, entry.model) : null;
13375
+ if (!item || seen.has(item.path)) {
13376
+ return [];
13377
+ }
13378
+ seen.add(item.path);
13379
+ return [item];
13380
+ });
13381
+ return results.map((result, index) => ({
13382
+ result,
13383
+ rank: rankRecordSearchResult(result, normalizedQuery, index)
13384
+ })).sort((left, right) => left.rank - right.rank).map((item) => item.result).slice(0, limit);
13385
+ }
12667
13386
  // ==================== RELATIONSHIP METHODS ====================
12668
13387
  /**
12669
13388
  * Define a relationship between two model types.
@@ -13429,7 +14148,8 @@ var Environment = class {
13429
14148
  body: JSON.stringify({
13430
14149
  records,
13431
14150
  batchSize: options.batchSize,
13432
- setupRunId: options.setupRunId
14151
+ setupRunId: options.setupRunId,
14152
+ writeMode: options.writeMode
13433
14153
  })
13434
14154
  }
13435
14155
  );
@@ -13481,11 +14201,13 @@ var Environment = class {
13481
14201
  };
13482
14202
  var EnvironmentSession = class extends Session {
13483
14203
  environment;
14204
+ sessionDataRoutePrefix;
13484
14205
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
13485
14206
  graphContainerStatus = null;
13486
- constructor(client, environment, clientId) {
13487
- super(client, clientId);
14207
+ constructor(client, environment, clientId, options = {}) {
14208
+ super(client, clientId, { initialQuota: options.initialQuota });
13488
14209
  this.environment = environment;
14210
+ this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
13489
14211
  }
13490
14212
  get environmentId() {
13491
14213
  return this.environment.environmentId;
@@ -13530,7 +14252,7 @@ var EnvironmentSession = class extends Session {
13530
14252
  const doc = this.document;
13531
14253
  return normalizeHeapSnapshot(doc?.heap);
13532
14254
  }
13533
- async sessionDataRequest(path2, query) {
14255
+ async sessionDataRequest(path2, query, init2 = {}) {
13534
14256
  const searchParams = new URLSearchParams();
13535
14257
  for (const [key, value] of Object.entries(query || {})) {
13536
14258
  if (value !== null && typeof value !== "undefined" && value !== "") {
@@ -13538,23 +14260,39 @@ var EnvironmentSession = class extends Session {
13538
14260
  }
13539
14261
  }
13540
14262
  const queryString = searchParams.toString();
13541
- const response = await fetch(
13542
- `${this.environment.runtimeBaseUrl}/orchestrator/ws/sessions/${encodeURIComponent(this.sessionId)}${path2}${queryString ? `?${queryString}` : ""}`,
13543
- {
13544
- method: "GET",
13545
- headers: {
13546
- Authorization: `Bearer ${this.environment.authToken}`,
13547
- "Content-Type": "application/json"
14263
+ const url = `${this.environment.runtimeBaseUrl}${this.sessionDataRoutePrefix}/${encodeURIComponent(this.sessionId)}${path2}${queryString ? `?${queryString}` : ""}`;
14264
+ const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
14265
+ for (let attempt = 1; attempt <= SESSION_DATA_REQUEST_RETRY_COUNT; attempt += 1) {
14266
+ try {
14267
+ const response = await fetch(url, {
14268
+ method: init2.method || "GET",
14269
+ headers: {
14270
+ Authorization: `Bearer ${this.environment.authToken}`,
14271
+ "Content-Type": "application/json"
14272
+ },
14273
+ ...typeof body === "undefined" ? {} : { body }
14274
+ });
14275
+ if (response.ok) {
14276
+ return response.json();
13548
14277
  }
14278
+ const errorText = await response.text();
14279
+ const error = new Error(
14280
+ `Session data API Error (${response.status}): ${errorText}`
14281
+ );
14282
+ if (isLocalControlUrl(url) && isRetryableSessionDataError(error) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
14283
+ await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
14284
+ continue;
14285
+ }
14286
+ throw error;
14287
+ } catch (error) {
14288
+ if (isLocalControlUrl(url) && isRetryableSessionDataError(error) && attempt < SESSION_DATA_REQUEST_RETRY_COUNT) {
14289
+ await sleep(SESSION_DATA_REQUEST_RETRY_DELAY_MS * attempt);
14290
+ continue;
14291
+ }
14292
+ throw error;
13549
14293
  }
13550
- );
13551
- if (!response.ok) {
13552
- const errorText = await response.text();
13553
- throw new Error(
13554
- `Session data API Error (${response.status}): ${errorText}`
13555
- );
13556
14294
  }
13557
- return response.json();
14295
+ throw new Error(`Session data API Error: exhausted retries for ${url}`);
13558
14296
  }
13559
14297
  async collectAllSessionItems(listPage) {
13560
14298
  const items = [];
@@ -13612,10 +14350,21 @@ var EnvironmentSession = class extends Session {
13612
14350
  get: (name) => this.sessionDataRequest(
13613
14351
  `/heap/lists/${encodeURIComponent(name)}`
13614
14352
  )
13615
- }
13616
- };
13617
- }
13618
- get transcript() {
14353
+ },
14354
+ variables: {
14355
+ list: (options = {}) => this.sessionDataRequest("/heap/variables", options),
14356
+ get: (name) => this.sessionDataRequest(
14357
+ `/heap/variables/${encodeURIComponent(name)}`
14358
+ ),
14359
+ delete: (name) => this.sessionDataRequest(
14360
+ `/heap/variables/${encodeURIComponent(name)}`,
14361
+ void 0,
14362
+ { method: "DELETE" }
14363
+ )
14364
+ }
14365
+ };
14366
+ }
14367
+ get transcript() {
13619
14368
  return {
13620
14369
  list: async (options = {}) => {
13621
14370
  const [messages, jobs, entries, lists] = await Promise.all([
@@ -13680,6 +14429,19 @@ var EnvironmentSession = class extends Session {
13680
14429
  async graphql(query, variables) {
13681
14430
  return this.environment.graphql(query, variables);
13682
14431
  }
14432
+ async searchRecords(query, options = {}) {
14433
+ return this.environment.searchRecords(query, options);
14434
+ }
14435
+ async mentionRecord(input) {
14436
+ return this.sessionDataRequest(
14437
+ "/records/mention",
14438
+ void 0,
14439
+ {
14440
+ method: "POST",
14441
+ body: input
14442
+ }
14443
+ );
14444
+ }
13683
14445
  async defineRelationship(options) {
13684
14446
  return this.environment.defineRelationship(options);
13685
14447
  }
@@ -13827,6 +14589,7 @@ var Granular = class _Granular {
13827
14589
  WebSocketCtor;
13828
14590
  onUnexpectedClose;
13829
14591
  onReconnectError;
14592
+ effectHostUrl;
13830
14593
  debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
13831
14594
  /** Sandbox-level effect registry: sandboxId → (effectKey@selector → ToolWithHandler) */
13832
14595
  sandboxEffects = /* @__PURE__ */ new Map();
@@ -13855,6 +14618,7 @@ var Granular = class _Granular {
13855
14618
  this.WebSocketCtor = options.WebSocketCtor;
13856
14619
  this.onUnexpectedClose = options.onUnexpectedClose;
13857
14620
  this.onReconnectError = options.onReconnectError;
14621
+ this.effectHostUrl = options.effectHostUrl;
13858
14622
  this.httpUrl = this.apiUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://").replace(/\/ws$/, "");
13859
14623
  }
13860
14624
  /**
@@ -14025,6 +14789,30 @@ var Granular = class _Granular {
14025
14789
  permissions: options.permissions || options.user?.permissions || []
14026
14790
  });
14027
14791
  }
14792
+ /**
14793
+ * Run a registered environment importer against an environment that was
14794
+ * opened outside this SDK instance, for example by a delegated browser flow.
14795
+ *
14796
+ * This uses the same setup-run and queued record-import plumbing as
14797
+ * `openEnvironment()`: importer stages, expected object counts, and queued
14798
+ * import counters remain visible through `environment.setup` and
14799
+ * `getRecordImportSummary()`.
14800
+ */
14801
+ async runEnvironmentImporterForEnvironment(environmentId, options = {}) {
14802
+ const environmentData = await this.environments.get(environmentId);
14803
+ const environment = this.bindEnvironmentHandle(environmentData);
14804
+ const requestedOntology = options.ontology || environmentData.ontologyId || environmentData.sandboxId;
14805
+ return this.runEnvironmentImporter(
14806
+ {
14807
+ environment: environmentData,
14808
+ requestedOntology,
14809
+ sandboxId: environmentData.sandboxId,
14810
+ subjectId: environmentData.subjectId,
14811
+ setupTriggerReason: options.reason || "new_environment"
14812
+ },
14813
+ environment
14814
+ );
14815
+ }
14028
14816
  resolveRequestedTag(options, methodName) {
14029
14817
  const tag = options.tag?.trim() || options.tagName?.trim() || options.environment?.trim();
14030
14818
  if (!tag) {
@@ -14216,6 +15004,15 @@ var Granular = class _Granular {
14216
15004
  const environment = this.bindEnvironmentHandle(envData);
14217
15005
  return this.bindWebSocketEnvironmentSession(environment, clientId, minted);
14218
15006
  }
15007
+ async recordOpenAIUsageSpend(usage, context, options) {
15008
+ return recordOpenAIUsageSpend({
15009
+ apiUrl: this.apiUrl,
15010
+ token: this.apiKey,
15011
+ usage,
15012
+ context,
15013
+ metadata: options?.metadata
15014
+ });
15015
+ }
14219
15016
  /**
14220
15017
  * Mark a session closed in the control plane. If `environment` is the connected handle for that
14221
15018
  * `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
@@ -14268,15 +15065,25 @@ var Granular = class _Granular {
14268
15065
  return ontologyImporter;
14269
15066
  }
14270
15067
  async maybeRunEnvironmentImporter(resolved, environment) {
14271
- if (!resolved.setupTriggerReason) {
14272
- return;
15068
+ const setupTriggerReason = resolved.setupTriggerReason;
15069
+ if (!setupTriggerReason) {
15070
+ return null;
14273
15071
  }
15072
+ return this.runEnvironmentImporter(
15073
+ {
15074
+ ...resolved,
15075
+ setupTriggerReason
15076
+ },
15077
+ environment
15078
+ );
15079
+ }
15080
+ async runEnvironmentImporter(resolved, environment) {
14274
15081
  const importer = this.resolveEnvironmentImporter(
14275
15082
  resolved.requestedOntology,
14276
15083
  resolved.sandboxId
14277
15084
  );
14278
15085
  if (!importer) {
14279
- return;
15086
+ return null;
14280
15087
  }
14281
15088
  const setupRun = await this.request(
14282
15089
  `/control/environments/${environment.environmentId}/setup-runs`,
@@ -14316,16 +15123,24 @@ var Granular = class _Granular {
14316
15123
  },
14317
15124
  importRecords: async (records, options) => environment.enqueueRecordImport(records, {
14318
15125
  batchSize: options?.batchSize,
15126
+ writeMode: options?.writeMode,
14319
15127
  setupRunId
14320
15128
  })
14321
15129
  };
14322
15130
  try {
14323
15131
  await importer(importerContext);
14324
- await updateSetupRun({ markHookCompleted: true });
15132
+ const completedSetupRun = await this.request(
15133
+ `/control/environment-setup-runs/${setupRunId}`,
15134
+ {
15135
+ method: "PATCH",
15136
+ body: JSON.stringify({ markHookCompleted: true })
15137
+ }
15138
+ );
14325
15139
  const refreshedEnvironment = await this.environments.get(
14326
15140
  environment.environmentId
14327
15141
  );
14328
15142
  environment.syncEnvironmentData(refreshedEnvironment);
15143
+ return completedSetupRun;
14329
15144
  } catch (error) {
14330
15145
  await updateSetupRun({
14331
15146
  status: "failed",
@@ -14352,7 +15167,8 @@ var Granular = class _Granular {
14352
15167
  const environmentSession = new EnvironmentSession(
14353
15168
  client,
14354
15169
  environment,
14355
- clientId
15170
+ clientId,
15171
+ { initialQuota: session.quota || null }
14356
15172
  );
14357
15173
  await environmentSession.hello();
14358
15174
  return environmentSession;
@@ -14373,27 +15189,45 @@ var Granular = class _Granular {
14373
15189
  return effects;
14374
15190
  }
14375
15191
  serializeEffect(effect) {
14376
- return {
15192
+ const serialized = {
14377
15193
  effectKey: computeEffectKey2(effect),
14378
15194
  name: effect.name,
14379
15195
  description: effect.description,
14380
15196
  inputSchema: effect.inputSchema,
14381
- outputSchema: effect.outputSchema,
14382
15197
  stability: effect.stability || "stable",
14383
- provenance: effect.provenance || { source: "custom" },
14384
- tags: effect.tags,
14385
- className: effect.className,
14386
- static: effect.static,
14387
- versionSelector: effect.versionSelector
15198
+ provenance: effect.provenance || { source: "custom" }
14388
15199
  };
15200
+ if (effect.outputSchema !== void 0) {
15201
+ serialized.outputSchema = effect.outputSchema;
15202
+ }
15203
+ if (effect.tags !== void 0) {
15204
+ serialized.tags = effect.tags;
15205
+ }
15206
+ if (effect.className !== void 0) {
15207
+ serialized.className = effect.className;
15208
+ }
15209
+ if (effect.static !== void 0) {
15210
+ serialized.static = effect.static;
15211
+ }
15212
+ if (effect.versionSelector !== void 0) {
15213
+ serialized.versionSelector = effect.versionSelector;
15214
+ }
15215
+ if (effect.metamodels !== void 0) {
15216
+ serialized.metamodels = effect.metamodels;
15217
+ }
15218
+ return serialized;
14389
15219
  }
14390
15220
  async publishSandboxEffectCatalog(host) {
14391
15221
  const effects = Array.from(
14392
15222
  this.getSandboxEffectMap(host.sandboxId).values()
14393
15223
  ).map((effect) => this.serializeEffect(effect));
14394
- const result = await host.wsClient.call("effects.publishCatalog", {
14395
- effects
14396
- });
15224
+ const result = await withTimeout(
15225
+ host.wsClient.call("effects.publishCatalog", {
15226
+ effects
15227
+ }),
15228
+ EFFECT_CATALOG_SYNC_TIMEOUT_MS,
15229
+ `effects.publishCatalog for sandbox ${host.sandboxId}`
15230
+ );
14397
15231
  const acceptedCount = typeof result?.acceptedCount === "number" ? result.acceptedCount : 0;
14398
15232
  const rejected = Array.isArray(result?.rejected) ? result.rejected : [];
14399
15233
  if (acceptedCount === 0 && rejected.length > 0) {
@@ -14412,8 +15246,26 @@ var Granular = class _Granular {
14412
15246
  }
14413
15247
  }
14414
15248
  async syncSandboxEffectCatalog(sandboxId) {
14415
- const host = await this.ensureSandboxEffectHost(sandboxId);
14416
- await this.publishSandboxEffectCatalog(host);
15249
+ let lastError;
15250
+ for (let attempt = 1; attempt <= EFFECT_CATALOG_SYNC_RETRY_COUNT; attempt += 1) {
15251
+ try {
15252
+ const host = await this.ensureSandboxEffectHost(sandboxId);
15253
+ await this.publishSandboxEffectCatalog(host);
15254
+ return;
15255
+ } catch (error) {
15256
+ lastError = error;
15257
+ this.disconnectSandboxEffectHost(sandboxId);
15258
+ if (attempt === EFFECT_CATALOG_SYNC_RETRY_COUNT || !isRetryableEffectRegistrationError(error)) {
15259
+ throw error;
15260
+ }
15261
+ console.warn(
15262
+ `[Granular] Retrying effect registration for sandbox ${sandboxId} after transient failure (${attempt}/${EFFECT_CATALOG_SYNC_RETRY_COUNT - 1} retries used):`,
15263
+ error
15264
+ );
15265
+ await sleep(EFFECT_CATALOG_SYNC_RETRY_DELAY_MS * attempt);
15266
+ }
15267
+ }
15268
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
14417
15269
  }
14418
15270
  recoverEffectHost(host, error) {
14419
15271
  if (host.recovering) {
@@ -14506,7 +15358,8 @@ var Granular = class _Granular {
14506
15358
  this.apiUrl,
14507
15359
  sandboxId,
14508
15360
  effectClientId,
14509
- clientId
15361
+ clientId,
15362
+ this.effectHostUrl
14510
15363
  ),
14511
15364
  sessionId: `effect-host:${effectClientId}`,
14512
15365
  token: this.apiKey,
@@ -14542,7 +15395,11 @@ var Granular = class _Granular {
14542
15395
  wsClient.on("disconnect", () => {
14543
15396
  this.stopEffectHostHeartbeat(host);
14544
15397
  });
14545
- await wsClient.connect();
15398
+ await withTimeout(
15399
+ wsClient.connect(),
15400
+ EFFECT_HOST_CONNECT_TIMEOUT_MS,
15401
+ `effect host WebSocket connect for sandbox ${sandboxId}`
15402
+ );
14546
15403
  await this.synchronizeEffectHost(host);
14547
15404
  this.sandboxEffectHosts.set(sandboxId, host);
14548
15405
  return host;
@@ -14665,7 +15522,7 @@ var Granular = class _Granular {
14665
15522
  /**
14666
15523
  * Ensure a permission profile exists for a sandbox, creating it if needed.
14667
15524
  * If profileName matches an existing profile name, returns its ID.
14668
- * Otherwise, creates a new profile with default allow-all rules.
15525
+ * Otherwise, creates a v1 source-profile file shape with an allow default.
14669
15526
  */
14670
15527
  async ensurePermissionProfile(sandboxId, profileName) {
14671
15528
  try {
@@ -14679,8 +15536,11 @@ var Granular = class _Granular {
14679
15536
  const created = await this.permissionProfiles.create(sandboxId, {
14680
15537
  name: profileName,
14681
15538
  rules: {
14682
- effects: { allow: ["*"] },
14683
- resources: { allow: ["*"] }
15539
+ schemaVersion: 1,
15540
+ name: profileName,
15541
+ description: profileName === "allow-all" ? "Every declared action is visible unless a manifest policy denies it." : `Generated permission profile ${profileName}`,
15542
+ defaults: { actionPolicy: "allow" },
15543
+ actions: []
14684
15544
  }
14685
15545
  });
14686
15546
  return created.permissionProfileId;
@@ -14753,33 +15613,63 @@ var Granular = class _Granular {
14753
15613
  * Permission Profile management for sandboxes
14754
15614
  */
14755
15615
  get permissionProfiles() {
15616
+ const profileSourceFromRecord = (record) => {
15617
+ const profile = record.profile || record.rules || {};
15618
+ return {
15619
+ ...profile,
15620
+ schemaVersion: profile.schemaVersion || 1,
15621
+ name: profile.name || record.name,
15622
+ description: profile.description || record.description
15623
+ };
15624
+ };
14756
15625
  return {
14757
15626
  list: async (sandboxId) => {
14758
15627
  const result = await this.request(
14759
- `/control/sandboxes/${sandboxId}/permission-profiles`
15628
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`
14760
15629
  );
14761
15630
  return result.items;
14762
15631
  },
14763
15632
  get: async (sandboxId, profileId) => {
14764
- return this.request(
14765
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`
15633
+ const result = await this.request(
15634
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`
14766
15635
  );
15636
+ const profile = result.items.find(
15637
+ (item) => item.permissionProfileId === profileId || item.name === profileId
15638
+ );
15639
+ if (!profile) {
15640
+ throw new Error(`Permission profile source not found: ${profileId}`);
15641
+ }
15642
+ return profile;
14767
15643
  },
14768
15644
  create: async (sandboxId, data) => {
14769
- return this.request(
14770
- `/control/sandboxes/${sandboxId}/permission-profiles`,
15645
+ const profile = {
15646
+ ...data.rules,
15647
+ schemaVersion: 1,
15648
+ name: data.name
15649
+ };
15650
+ const existingProfiles = await this.permissionProfiles.list(sandboxId);
15651
+ const profiles = [
15652
+ ...existingProfiles.filter((existing) => existing.name !== data.name).map((existing) => profileSourceFromRecord(existing)),
15653
+ profile
15654
+ ];
15655
+ const result = await this.request(
15656
+ `/control/sandboxes/${sandboxId}/permission-profile-sources`,
14771
15657
  {
14772
- method: "POST",
14773
- body: JSON.stringify(data)
15658
+ method: "PUT",
15659
+ body: JSON.stringify({ profiles })
14774
15660
  }
14775
15661
  );
15662
+ const synced = result.items.find((item) => item.name === data.name) || result.items[0];
15663
+ if (!synced) {
15664
+ throw new Error(
15665
+ `Permission profile source sync did not return ${data.name}`
15666
+ );
15667
+ }
15668
+ return synced;
14776
15669
  },
14777
- delete: async (sandboxId, profileId) => {
14778
- return this.request(
14779
- `/control/sandboxes/${sandboxId}/permission-profiles/${profileId}`,
14780
- {
14781
- method: "DELETE"
14782
- }
15670
+ delete: async (_sandboxId, _profileId) => {
15671
+ throw new Error(
15672
+ "Permission profile sources are updated by syncing the desired source set."
14783
15673
  );
14784
15674
  }
14785
15675
  };
@@ -15058,21 +15948,8 @@ function uniqueStrings(values, maxCount) {
15058
15948
  }
15059
15949
  return output;
15060
15950
  }
15061
- function formatScalar(value) {
15062
- if (typeof value === "string") return JSON.stringify(value);
15063
- if (typeof value === "number" || typeof value === "boolean")
15064
- return String(value);
15065
- if (value === null) return "null";
15066
- return "unknown";
15067
- }
15068
- function describeHeapEntry(entry, previewFieldLimit = 3) {
15069
- const headline = entry.label || entry.id || entry.path || "Unknown";
15070
- const pathLabel = entry.path && entry.path !== headline ? ` <${entry.path}>` : "";
15071
- const classLabel = entry.className || "unknown";
15072
- const preview = asArray2(entry.fields).filter(
15073
- (field) => field?.name && field.name !== "_realId" && field.name !== "real_id"
15074
- ).slice(0, previewFieldLimit).map((field) => `${field.name}=${formatScalar(field.value)}`).join(", ");
15075
- return preview ? `${headline}${pathLabel} [${classLabel}] ${preview}` : `${headline}${pathLabel} [${classLabel}]`;
15951
+ function renderConstBlock(name, value) {
15952
+ return `const ${name} = ${JSON.stringify(value, null, 2)} as const;`;
15076
15953
  }
15077
15954
  function hashString(value) {
15078
15955
  if (!value) return null;
@@ -15083,101 +15960,6 @@ function hashString(value) {
15083
15960
  }
15084
15961
  return (hash >>> 0).toString(16).padStart(8, "0");
15085
15962
  }
15086
- function hasSubstantiveAwaitAfterPrompt(code, marker) {
15087
- const startIndex = code.indexOf(marker);
15088
- if (startIndex === -1) return true;
15089
- const segment = code.slice(startIndex + marker.length);
15090
- const callMatches = segment.matchAll(
15091
- /await\s+([A-Za-z0-9_$.]+)\.([A-Za-z0-9_]+)\s*\(/g
15092
- );
15093
- for (const match of callMatches) {
15094
- const receiver = match[1] || "";
15095
- const method = match[2] || "";
15096
- if (receiver === "loop" || receiver === "heap") continue;
15097
- if (method.startsWith("get_") || method.startsWith("get")) continue;
15098
- return true;
15099
- }
15100
- return false;
15101
- }
15102
- function reviewGeneratedJobCode(code) {
15103
- const normalized = typeof code === "string" ? code : "";
15104
- if (!normalized.trim()) return [];
15105
- const issues = [];
15106
- if (/require\s*\(\s*['"]\.\/sandbox-tools['"]\s*\)/.test(normalized)) {
15107
- issues.push({
15108
- code: "commonjs_require",
15109
- severity: "error",
15110
- 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."
15111
- });
15112
- }
15113
- const placeholderPatterns = [
15114
- /ready to make the change next/i,
15115
- /ready to .* next/i,
15116
- /ready to .* now/i,
15117
- /i can make the change now/i,
15118
- /i can do that next/i,
15119
- /i'?m ready to continue/i,
15120
- /have your approval .* ready to make/i,
15121
- /approved\./i
15122
- ];
15123
- if (normalized.includes("await loop.confirm(")) {
15124
- const postConfirm = normalized.slice(
15125
- normalized.indexOf("await loop.confirm(")
15126
- );
15127
- const hasPlaceholder = placeholderPatterns.some(
15128
- (pattern) => pattern.test(postConfirm)
15129
- );
15130
- const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(
15131
- normalized,
15132
- "await loop.confirm("
15133
- );
15134
- if (!hasSubstantiveAwait || hasPlaceholder) {
15135
- issues.push({
15136
- code: "placeholder_after_confirm",
15137
- severity: "error",
15138
- 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.'"
15139
- });
15140
- }
15141
- }
15142
- if (normalized.includes("await loop.ask_user(")) {
15143
- const postPrompt = normalized.slice(
15144
- normalized.indexOf("await loop.ask_user(")
15145
- );
15146
- const hasPlaceholder = placeholderPatterns.some(
15147
- (pattern) => pattern.test(postPrompt)
15148
- );
15149
- const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(
15150
- normalized,
15151
- "await loop.ask_user("
15152
- );
15153
- if (hasPlaceholder && !hasSubstantiveAwait) {
15154
- issues.push({
15155
- code: "placeholder_after_ask_user",
15156
- severity: "error",
15157
- 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."
15158
- });
15159
- }
15160
- }
15161
- const hasConversationalReturn = /return\s+[`'"]/.test(normalized) || /\breply\s*:/.test(normalized) || /\bagent_message\s*\(/.test(normalized) || /\bagent_text_message\s*\(/.test(normalized);
15162
- const returnsObjectLiteral = /return\s+\{[\s\S]*?\}/.test(normalized);
15163
- const returnsShowPayload = /return\s+\{[\s\S]*?\bshow\s*:/.test(normalized);
15164
- const closesLoop = /loop\.close_loop\s*\(/.test(normalized);
15165
- if (!hasConversationalReturn && returnsObjectLiteral && !closesLoop) {
15166
- issues.push({
15167
- code: "missing_user_reply",
15168
- severity: "error",
15169
- 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."
15170
- });
15171
- }
15172
- if (returnsShowPayload) {
15173
- issues.push({
15174
- code: "return_show_not_for_ui",
15175
- severity: "error",
15176
- 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."
15177
- });
15178
- }
15179
- return issues;
15180
- }
15181
15963
  function extractFocusHintsFromActionSummary(actionSummaryLines) {
15182
15964
  const variableNames = [];
15183
15965
  const listNames = [];
@@ -15205,6 +15987,247 @@ function extractFocusHintsFromActionSummary(actionSummaryLines) {
15205
15987
  function normalizeActionSummaryForPrompt(line) {
15206
15988
  return line.replace(/\blimit=/g, "perPage=").replace(/\blimit:/g, "perPage:");
15207
15989
  }
15990
+ function collectConversationReferents(liveDoc) {
15991
+ const conversation = asRecord4(liveDoc?.conversation);
15992
+ const persistedReferents = asArray2(conversation?.referents).map((value) => asRecord4(value)).filter((value) => Boolean(value));
15993
+ if (persistedReferents.length > 0) {
15994
+ return persistedReferents.slice().sort((left, right) => (right.ts || 0) - (left.ts || 0));
15995
+ }
15996
+ const heap = asRecord4(liveDoc?.heap);
15997
+ const entriesByPath = asRecord4(heap?.entriesByPath) || {};
15998
+ const listsByName = asRecord4(heap?.listsByName) || {};
15999
+ const variablesByName = asRecord4(heap?.variablesByName) || {};
16000
+ const messages = asArray2(conversation?.messages).map((value) => asRecord4(value)).filter((value) => Boolean(value)).slice().sort((left, right) => (Number(right.ts) || 0) - (Number(left.ts) || 0));
16001
+ const referents = [];
16002
+ const seen = /* @__PURE__ */ new Set();
16003
+ const pushReferent = (referent) => {
16004
+ if (!referent?.kind || !referent.ref) return;
16005
+ const key = `${referent.kind}:${referent.ref}`;
16006
+ if (seen.has(key)) return;
16007
+ seen.add(key);
16008
+ referents.push(referent);
16009
+ };
16010
+ for (const message of messages) {
16011
+ if (message.role !== "assistant") continue;
16012
+ const show = asRecord4(message.show);
16013
+ if (!show) continue;
16014
+ const ts = Number(message.ts) || 0;
16015
+ const messageId = typeof message.id === "string" ? message.id : void 0;
16016
+ const jobId = typeof message.jobId === "string" ? message.jobId : void 0;
16017
+ const entryPaths = uniqueStrings(asArray2(show.entryPaths));
16018
+ const entryClassCounts = /* @__PURE__ */ new Map();
16019
+ const entryMetadata = entryPaths.map((entryPath) => {
16020
+ const entry = asRecord4(entriesByPath[entryPath]);
16021
+ const className = typeof entry?.className === "string" ? entry.className : void 0;
16022
+ if (className) {
16023
+ entryClassCounts.set(
16024
+ className,
16025
+ (entryClassCounts.get(className) || 0) + 1
16026
+ );
16027
+ }
16028
+ return { entryPath, entry, className };
16029
+ });
16030
+ const displayGroupId = entryMetadata.length > 1 ? `message:${messageId || jobId || ts}:entries` : void 0;
16031
+ for (const [
16032
+ index,
16033
+ { entryPath, entry, className }
16034
+ ] of entryMetadata.entries()) {
16035
+ pushReferent({
16036
+ id: `entry:${entryPath}`,
16037
+ kind: "entry",
16038
+ ref: entryPath,
16039
+ role: "assistant",
16040
+ source: "heap_objects",
16041
+ entryPath,
16042
+ recordId: typeof entry?.id === "string" ? entry.id : void 0,
16043
+ className,
16044
+ label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : entryPath,
16045
+ ...displayGroupId ? {
16046
+ displayGroupId,
16047
+ displayGroupIndex: index,
16048
+ displayGroupSize: entryMetadata.length,
16049
+ ...className && (entryClassCounts.get(className) || 0) > 1 ? { displayGroupSameTypeSize: entryClassCounts.get(className) } : {}
16050
+ } : {},
16051
+ messageId,
16052
+ jobId,
16053
+ ts
16054
+ });
16055
+ }
16056
+ for (const listName of uniqueStrings(asArray2(show.listNames))) {
16057
+ const list = asRecord4(listsByName[listName]);
16058
+ pushReferent({
16059
+ id: `list:${listName}`,
16060
+ kind: "list",
16061
+ ref: listName,
16062
+ role: "assistant",
16063
+ source: "heap_objects",
16064
+ listName,
16065
+ className: typeof list?.className === "string" ? list.className : void 0,
16066
+ count: Array.isArray(list?.paths) ? list.paths.length : null,
16067
+ messageId,
16068
+ jobId,
16069
+ ts
16070
+ });
16071
+ }
16072
+ for (const variableName of uniqueStrings(
16073
+ asArray2(show.variableNames)
16074
+ )) {
16075
+ const variable = asRecord4(variablesByName[variableName]);
16076
+ const entryPath = typeof variable?.entryPath === "string" ? variable.entryPath : void 0;
16077
+ const listName = typeof variable?.listName === "string" ? variable.listName : void 0;
16078
+ const entry = entryPath ? asRecord4(entriesByPath[entryPath]) : null;
16079
+ const list = listName ? asRecord4(listsByName[listName]) : null;
16080
+ pushReferent({
16081
+ id: `variable:${variableName}`,
16082
+ kind: "variable",
16083
+ ref: variableName,
16084
+ role: "assistant",
16085
+ source: "heap_objects",
16086
+ variableName,
16087
+ variableKind: typeof variable?.kind === "string" ? variable.kind : void 0,
16088
+ entryPath,
16089
+ recordId: typeof entry?.id === "string" ? entry.id : void 0,
16090
+ listName,
16091
+ className: typeof variable?.className === "string" ? variable.className : typeof entry?.className === "string" ? entry.className : typeof list?.className === "string" ? list.className : void 0,
16092
+ label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : null,
16093
+ count: variable?.kind === "list" && Array.isArray(list?.paths) ? list.paths.length : null,
16094
+ scalarValue: variable?.kind === "scalar" && (typeof variable.value === "string" || typeof variable.value === "number" || typeof variable.value === "boolean" || variable.value === null) ? variable.value : void 0,
16095
+ messageId,
16096
+ jobId,
16097
+ ts
16098
+ });
16099
+ }
16100
+ }
16101
+ return referents;
16102
+ }
16103
+ function projectConversationReferentFocus(liveDoc) {
16104
+ const heap = asRecord4(liveDoc?.heap);
16105
+ const listsByName = asRecord4(heap?.listsByName) || {};
16106
+ const referents = collectConversationReferents(liveDoc);
16107
+ const entryPaths = [];
16108
+ const listNames = [];
16109
+ const variableNames = [];
16110
+ let entryCount = 0;
16111
+ let listCount = 0;
16112
+ let variableCount = 0;
16113
+ for (const referent of referents) {
16114
+ if (referent.kind === "entry" && typeof referent.entryPath === "string" && entryCount < 8) {
16115
+ entryCount += 1;
16116
+ entryPaths.push(referent.entryPath);
16117
+ continue;
16118
+ }
16119
+ if (referent.kind === "list" && typeof referent.listName === "string" && listCount < 4) {
16120
+ listCount += 1;
16121
+ listNames.push(referent.listName);
16122
+ const list = asRecord4(listsByName[referent.listName]);
16123
+ entryPaths.push(...asArray2(list?.paths).slice(0, 4));
16124
+ continue;
16125
+ }
16126
+ if (referent.kind === "variable" && typeof referent.variableName === "string" && variableCount < 4) {
16127
+ variableCount += 1;
16128
+ variableNames.push(referent.variableName);
16129
+ if (typeof referent.entryPath === "string") {
16130
+ entryPaths.push(referent.entryPath);
16131
+ }
16132
+ if (typeof referent.listName === "string") {
16133
+ listNames.push(referent.listName);
16134
+ const list = asRecord4(listsByName[referent.listName]);
16135
+ entryPaths.push(...asArray2(list?.paths).slice(0, 4));
16136
+ }
16137
+ }
16138
+ }
16139
+ return {
16140
+ entryPaths: uniqueStrings(entryPaths, 8),
16141
+ listNames: uniqueStrings(listNames, 4),
16142
+ variableNames: uniqueStrings(variableNames, 4)
16143
+ };
16144
+ }
16145
+ function selectConversationReferentsForPrompt(referents) {
16146
+ const selected = [];
16147
+ const seen = /* @__PURE__ */ new Set();
16148
+ let entryCount = 0;
16149
+ let listCount = 0;
16150
+ let variableCount = 0;
16151
+ for (const referent of referents) {
16152
+ if (!referent.kind || !referent.ref) continue;
16153
+ const key = `${referent.kind}:${referent.ref}`;
16154
+ if (seen.has(key)) continue;
16155
+ if (referent.kind === "entry") {
16156
+ if (entryCount >= 8) continue;
16157
+ entryCount += 1;
16158
+ } else if (referent.kind === "list") {
16159
+ if (listCount >= 4) continue;
16160
+ listCount += 1;
16161
+ } else if (referent.kind === "variable") {
16162
+ if (variableCount >= 4) continue;
16163
+ variableCount += 1;
16164
+ }
16165
+ seen.add(key);
16166
+ selected.push(referent);
16167
+ }
16168
+ return selected;
16169
+ }
16170
+ function projectConversationReferentSummary(liveDoc) {
16171
+ const referents = selectConversationReferentsForPrompt(
16172
+ collectConversationReferents(liveDoc)
16173
+ );
16174
+ const compact = referents.map((referent) => {
16175
+ if (referent.kind === "entry" && referent.entryPath) {
16176
+ return {
16177
+ kind: "entry",
16178
+ role: referent.role || null,
16179
+ source: referent.source || null,
16180
+ path: referent.entryPath,
16181
+ id: referent.recordId || null,
16182
+ type: referent.className || "unknown",
16183
+ label: referent.label || referent.entryPath,
16184
+ group: referent.displayGroupId ? {
16185
+ id: referent.displayGroupId,
16186
+ index: typeof referent.displayGroupIndex === "number" ? referent.displayGroupIndex : null,
16187
+ size: typeof referent.displayGroupSize === "number" ? referent.displayGroupSize : null,
16188
+ sameTypeSize: typeof referent.displayGroupSameTypeSize === "number" ? referent.displayGroupSameTypeSize : null
16189
+ } : void 0
16190
+ };
16191
+ }
16192
+ if (referent.kind === "entry" && referent.recordId) {
16193
+ return {
16194
+ kind: "entry",
16195
+ role: referent.role || null,
16196
+ source: referent.source || null,
16197
+ id: referent.recordId,
16198
+ type: referent.className || "unknown",
16199
+ label: referent.label || referent.recordId
16200
+ };
16201
+ }
16202
+ if (referent.kind === "list" && referent.listName) {
16203
+ return {
16204
+ kind: "list",
16205
+ role: referent.role || null,
16206
+ source: referent.source || null,
16207
+ name: referent.listName,
16208
+ type: referent.className || "unknown",
16209
+ count: typeof referent.count === "number" ? referent.count : null
16210
+ };
16211
+ }
16212
+ if (referent.kind === "variable" && referent.variableName) {
16213
+ return {
16214
+ kind: "variable",
16215
+ role: referent.role || null,
16216
+ source: referent.source || null,
16217
+ name: referent.variableName,
16218
+ valueKind: referent.variableKind || null,
16219
+ type: referent.className || null,
16220
+ path: referent.entryPath || null,
16221
+ list: referent.listName || null,
16222
+ label: referent.label || null,
16223
+ count: typeof referent.count === "number" ? referent.count : null,
16224
+ value: referent.variableKind === "scalar" ? referent.scalarValue ?? null : void 0
16225
+ };
16226
+ }
16227
+ return null;
16228
+ }).filter(Boolean);
16229
+ return renderConstBlock("recentReferences", compact);
16230
+ }
15208
16231
  function getCurrentClosureId(liveDoc) {
15209
16232
  const loop = asRecord4(liveDoc?.loop);
15210
16233
  return typeof loop?.currentClosureId === "string" ? loop.currentClosureId : null;
@@ -15424,56 +16447,24 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
15424
16447
  }
15425
16448
  function projectWorkflowSummary(liveDoc, pendingPrompts = [], options) {
15426
16449
  const focus = projectWorkflowFocus(liveDoc, pendingPrompts, options);
15427
- const lines = [];
15428
- lines.push("Workflow Boundary:");
15429
- if (focus.boundaryReason === "request_start") {
15430
- lines.push(
15431
- "- Start from work recorded after the current user request began."
15432
- );
15433
- } else if (focus.boundaryReason === "last_closed_loop" && focus.latestClosureId) {
15434
- lines.push(`- Start from work recorded after ${focus.latestClosureId}.`);
15435
- } else {
15436
- lines.push(
15437
- "- No prior closed loop recorded; use the latest user request as the boundary."
15438
- );
15439
- }
15440
- lines.push("", "Recent Actions:");
15441
- if (focus.recentActionSummary.length === 0) {
15442
- lines.push("- none");
15443
- } else {
15444
- for (const line of focus.recentActionSummary) {
15445
- lines.push(line.startsWith("- ") ? line : `- ${line}`);
15446
- }
15447
- }
15448
- lines.push("", "Working Set Hints:");
15449
- if (focus.variableNames.length === 0 && focus.listNames.length === 0 && focus.entryPaths.length === 0) {
15450
- lines.push("- none");
15451
- } else {
15452
- if (focus.variableNames.length > 0) {
15453
- lines.push(`- variables: ${focus.variableNames.join(", ")}`);
15454
- }
15455
- if (focus.listNames.length > 0) {
15456
- lines.push(`- lists: ${focus.listNames.join(", ")}`);
15457
- }
15458
- if (focus.entryPaths.length > 0) {
15459
- lines.push(`- entries: ${focus.entryPaths.join(", ")}`);
15460
- }
15461
- }
15462
- lines.push("", "Open Workflow Handles:");
15463
- if (focus.activeTaskIds.length === 0 && focus.openDecisionIds.length === 0 && focus.openPromptIds.length === 0) {
15464
- lines.push("- none");
15465
- } else {
15466
- if (focus.activeTaskIds.length > 0) {
15467
- lines.push(`- tasks: ${focus.activeTaskIds.join(", ")}`);
15468
- }
15469
- if (focus.openDecisionIds.length > 0) {
15470
- lines.push(`- decisions: ${focus.openDecisionIds.join(", ")}`);
15471
- }
15472
- if (focus.openPromptIds.length > 0) {
15473
- lines.push(`- prompts: ${focus.openPromptIds.join(", ")}`);
16450
+ return renderConstBlock("workflowContext", {
16451
+ boundary: {
16452
+ timestamp: focus.boundaryTimestamp,
16453
+ reason: focus.boundaryReason,
16454
+ latestClosureId: focus.latestClosureId || null
16455
+ },
16456
+ recentActions: focus.recentActionSummary,
16457
+ workingSet: {
16458
+ variables: focus.variableNames,
16459
+ lists: focus.listNames,
16460
+ entries: focus.entryPaths
16461
+ },
16462
+ openHandles: {
16463
+ tasks: focus.activeTaskIds,
16464
+ decisions: focus.openDecisionIds,
16465
+ prompts: focus.openPromptIds
15474
16466
  }
15475
- }
15476
- return lines.join("\n");
16467
+ });
15477
16468
  }
15478
16469
  function hasOpenPrompt(liveDoc, pendingPrompts) {
15479
16470
  if (pendingPrompts.length > 0) return true;
@@ -15489,7 +16480,6 @@ function hasOpenPrompt(liveDoc, pendingPrompts) {
15489
16480
  return false;
15490
16481
  }
15491
16482
  function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15492
- const lines = [];
15493
16483
  const loop = asRecord4(liveDoc?.loop);
15494
16484
  const boundary = getWorkflowBoundary(liveDoc, options);
15495
16485
  const tasks = toSortedRecords(loop?.tasksById).filter((task) => {
@@ -15511,22 +16501,12 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15511
16501
  5
15512
16502
  );
15513
16503
  const hiddenTaskCount = Math.max(0, activeTasks.length - visibleTasks.length);
15514
- lines.push("Tasks:");
15515
- if (visibleTasks.length === 0) {
15516
- lines.push("- none");
15517
- } else {
15518
- lines.push("- Reuse existing taskId values exactly as written below.");
15519
- for (const task of visibleTasks) {
15520
- const title = typeof task.title === "string" ? task.title : "Untitled task";
15521
- const taskId = typeof task.taskId === "string" ? task.taskId : "unknown";
15522
- const status = typeof task.status === "string" ? task.status : "pending";
15523
- const summary = typeof task.summary === "string" && task.summary.trim() ? ` \u2014 ${task.summary.trim()}` : "";
15524
- lines.push(`- [${status}] ${title} (${taskId})${summary}`);
15525
- }
15526
- if (hiddenTaskCount > 0) {
15527
- lines.push(`- ${hiddenTaskCount} more active task(s) omitted`);
15528
- }
15529
- }
16504
+ const compactTasks = visibleTasks.map((task) => ({
16505
+ id: typeof task.taskId === "string" ? task.taskId : "unknown",
16506
+ title: typeof task.title === "string" ? task.title : "Untitled task",
16507
+ status: typeof task.status === "string" ? task.status : "pending",
16508
+ summary: typeof task.summary === "string" && task.summary.trim() ? task.summary.trim() : null
16509
+ }));
15530
16510
  const decisions = toSortedRecords(loop?.decisionsById).filter((decision) => {
15531
16511
  const updatedAt = Number(decision.updatedAt) || Number(decision.createdAt) || 0;
15532
16512
  if (boundary.reason === "request_start") {
@@ -15540,33 +16520,29 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15540
16520
  (decision) => decision.status === "open"
15541
16521
  );
15542
16522
  const visibleDecisions = (openDecisions.length > 0 ? openDecisions : decisions.slice(0, 1)).slice(0, 3);
15543
- lines.push("", "Recent Decisions:");
15544
- if (visibleDecisions.length === 0) {
15545
- lines.push("- none");
15546
- } else {
15547
- lines.push("- Reuse existing decisionId values exactly as written below.");
15548
- for (const decision of visibleDecisions) {
15549
- const status = typeof decision.status === "string" ? decision.status : "resolved";
15550
- const title = typeof decision.title === "string" && decision.title.trim() ? decision.title.trim() : "Decision";
15551
- const decisionId = typeof decision.decisionId === "string" ? decision.decisionId : "unknown";
15552
- if (status === "open") {
15553
- const candidatePreview = asArray2(decision.candidates).slice(0, 3).map((candidate) => {
15554
- const record = asRecord4(candidate);
15555
- if (!record) return null;
15556
- const candidateId = typeof record.id === "string" ? record.id : "unknown";
15557
- const candidateLabel = typeof record.label === "string" && record.label.trim() ? record.label.trim() : candidateId;
15558
- return candidateLabel === candidateId ? candidateId : `${candidateLabel} (${candidateId})`;
15559
- }).filter((value) => Boolean(value)).join(", ");
15560
- lines.push(
15561
- `- [open] ${title} (${decisionId})${candidatePreview ? ` \u2014 candidates: ${candidatePreview}` : ""}`
15562
- );
15563
- } else {
15564
- const selected = asRecord4(decision.selected);
15565
- const label = typeof selected?.label === "string" ? selected.label : typeof selected?.id === "string" ? selected.id : "unknown";
15566
- lines.push(`- [resolved] ${title} (${decisionId}) -> ${label}`);
16523
+ const compactDecisions = visibleDecisions.map((decision) => {
16524
+ const status = typeof decision.status === "string" ? decision.status : "resolved";
16525
+ const selected = asRecord4(decision.selected);
16526
+ return {
16527
+ id: typeof decision.decisionId === "string" ? decision.decisionId : "unknown",
16528
+ title: typeof decision.title === "string" && decision.title.trim() ? decision.title.trim() : "Decision",
16529
+ status,
16530
+ candidates: status === "open" ? asArray2(decision.candidates).slice(0, 5).map((candidate) => {
16531
+ const record = asRecord4(candidate);
16532
+ if (!record) return null;
16533
+ return {
16534
+ id: typeof record.id === "string" ? record.id : "unknown",
16535
+ label: typeof record.label === "string" && record.label.trim() ? record.label.trim() : null,
16536
+ description: typeof record.description === "string" && record.description.trim() ? record.description.trim() : null,
16537
+ metadata: asRecord4(record.metadata)
16538
+ };
16539
+ }).filter(Boolean) : [],
16540
+ selected: status === "open" ? null : {
16541
+ id: typeof selected?.id === "string" ? selected.id : null,
16542
+ label: typeof selected?.label === "string" ? selected.label : null
15567
16543
  }
15568
- }
15569
- }
16544
+ };
16545
+ });
15570
16546
  const openPrompts = [
15571
16547
  ...pendingPrompts.map((prompt) => ({
15572
16548
  id: prompt.id,
@@ -15586,29 +16562,29 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
15586
16562
  (pendingPrompt) => pendingPrompt.id === promptId
15587
16563
  ) : false);
15588
16564
  }) : openPrompts;
15589
- lines.push("", "Open Prompts:");
15590
- if (visiblePrompts.length === 0) {
15591
- lines.push("- none");
15592
- } else {
15593
- for (const prompt of visiblePrompts.slice(0, 3)) {
15594
- const title = typeof prompt.title === "string" && prompt.title.trim() ? prompt.title.trim() : "Input required";
15595
- const type = typeof prompt.type === "string" ? prompt.type : "input";
15596
- const message = typeof prompt.message === "string" && prompt.message.trim() ? ` \u2014 ${prompt.message.trim()}` : "";
15597
- lines.push(`- [${type}] ${title}${message}`);
15598
- }
15599
- }
16565
+ const compactPrompts = visiblePrompts.slice(0, 3).map((prompt) => {
16566
+ const promptRecord = asRecord4(prompt) || {};
16567
+ return {
16568
+ id: typeof promptRecord.id === "string" ? promptRecord.id : typeof promptRecord.promptId === "string" ? promptRecord.promptId : null,
16569
+ type: typeof promptRecord.type === "string" ? promptRecord.type : "input",
16570
+ title: typeof promptRecord.title === "string" && promptRecord.title.trim() ? promptRecord.title.trim() : "Input required",
16571
+ message: typeof promptRecord.message === "string" && promptRecord.message.trim() ? promptRecord.message.trim() : null
16572
+ };
16573
+ });
15600
16574
  const currentClosureId = getCurrentClosureId(liveDoc);
15601
16575
  const closureRecord = currentClosureId ? asRecord4(asRecord4(loop?.closuresById)?.[currentClosureId]) : null;
15602
16576
  const visibleClosure = closureRecord && (boundary.reason !== "request_start" || (Number(closureRecord.createdAt) || 0) >= boundary.timestamp) ? closureRecord : null;
15603
- lines.push("", "Loop Closure:");
15604
- if (visibleClosure) {
15605
- const status = typeof visibleClosure.status === "string" ? visibleClosure.status : "completed";
15606
- const summary = typeof visibleClosure.summary === "string" ? visibleClosure.summary : "No summary";
15607
- lines.push(`- current: [${status}] ${summary} (${currentClosureId})`);
15608
- } else {
15609
- lines.push("- none");
15610
- }
15611
- return lines.join("\n");
16577
+ return renderConstBlock("workflowState", {
16578
+ tasks: compactTasks,
16579
+ hiddenActiveTaskCount: hiddenTaskCount,
16580
+ decisions: compactDecisions,
16581
+ openPrompts: compactPrompts,
16582
+ closure: visibleClosure ? {
16583
+ id: currentClosureId,
16584
+ status: typeof visibleClosure.status === "string" ? visibleClosure.status : "completed",
16585
+ summary: typeof visibleClosure.summary === "string" ? visibleClosure.summary : null
16586
+ } : null
16587
+ });
15612
16588
  }
15613
16589
  function projectHeapSummary(heap, options) {
15614
16590
  const heapRecord = asRecord4(heap) || {};
@@ -15653,55 +16629,72 @@ function projectHeapSummary(heap, options) {
15653
16629
  referencedPaths.add(path2);
15654
16630
  }
15655
16631
  const visibleLists = Object.values(listsByName).map((value) => asRecord4(value)).filter((value) => Boolean(value)).filter(
15656
- (list) => variables.some((variable) => variable.listName === list.name) || Boolean(list.name && focusedListNames.has(list.name))
16632
+ (list) => variables.some(
16633
+ (variable) => Boolean(variable?.listName === list.name)
16634
+ ) || Boolean(list.name && focusedListNames.has(list.name))
15657
16635
  ).sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0)).slice(0, maxLists);
15658
16636
  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);
15659
- const lines = [];
15660
- lines.push("Variables:");
15661
- if (variables.length === 0) {
15662
- lines.push("- none");
15663
- } else {
15664
- for (const variable of variables) {
15665
- if (variable.kind === "scalar") {
15666
- lines.push(
15667
- `- ${variable.name}: scalar = ${formatScalar(variable.value)}`
15668
- );
15669
- continue;
15670
- }
15671
- if (variable.kind === "entry") {
15672
- const entry = variable.entryPath ? asRecord4(
15673
- entriesByPath[variable.entryPath]
15674
- ) : null;
15675
- lines.push(
15676
- `- ${variable.name}: entry<${variable.className || entry?.className || "unknown"}> -> ${entry ? describeHeapEntry(entry) : variable.entryPath || "missing"}`
15677
- );
15678
- continue;
15679
- }
15680
- const list = variable.listName ? asRecord4(listsByName[variable.listName]) : null;
15681
- lines.push(
15682
- `- ${variable.name}: list<${variable.className || list?.className || "unknown"}> -> ${(list?.paths || []).length} item(s)`
15683
- );
15684
- }
15685
- }
15686
- lines.push("", "Named Lists:");
15687
- if (visibleLists.length === 0) {
15688
- lines.push("- none");
15689
- } else {
15690
- for (const list of visibleLists) {
15691
- lines.push(
15692
- `- ${list.name}: ${list.className || "unknown"}[${(list.paths || []).length}]`
15693
- );
15694
- }
15695
- }
15696
- lines.push("", "Active Entries:");
15697
- if (visibleEntries.length === 0) {
15698
- lines.push("- none");
15699
- } else {
15700
- for (const entry of visibleEntries) {
15701
- lines.push(`- ${describeHeapEntry(entry)}`);
15702
- }
15703
- }
15704
- return lines.join("\n");
16637
+ return renderConstBlock("savedData", {
16638
+ variables: Object.fromEntries(
16639
+ variables.filter((variable) => typeof variable.name === "string").map((variable) => {
16640
+ if (variable.kind === "scalar") {
16641
+ return [
16642
+ variable.name,
16643
+ { kind: "scalar", value: variable.value ?? null }
16644
+ ];
16645
+ }
16646
+ if (variable.kind === "entry") {
16647
+ const entry = variable.entryPath ? asRecord4(
16648
+ entriesByPath[variable.entryPath]
16649
+ ) : null;
16650
+ return [
16651
+ variable.name,
16652
+ {
16653
+ kind: "entry",
16654
+ type: variable.className || entry?.className || "unknown",
16655
+ path: variable.entryPath || null,
16656
+ label: entry?.label || entry?.id || null
16657
+ }
16658
+ ];
16659
+ }
16660
+ const list = variable.listName ? asRecord4(listsByName[variable.listName]) : null;
16661
+ return [
16662
+ variable.name,
16663
+ {
16664
+ kind: "list",
16665
+ type: variable.className || list?.className || "unknown",
16666
+ list: variable.listName || null,
16667
+ count: (list?.paths || []).length
16668
+ }
16669
+ ];
16670
+ })
16671
+ ),
16672
+ lists: Object.fromEntries(
16673
+ visibleLists.filter((list) => typeof list.name === "string").map((list) => [
16674
+ list.name,
16675
+ {
16676
+ type: list.className || "unknown",
16677
+ count: (list.paths || []).length
16678
+ }
16679
+ ])
16680
+ ),
16681
+ entries: Object.fromEntries(
16682
+ visibleEntries.filter((entry) => typeof entry.path === "string").map((entry) => [
16683
+ entry.path,
16684
+ {
16685
+ type: entry.className || "unknown",
16686
+ id: entry.id || null,
16687
+ label: entry.label || entry.id || null,
16688
+ fields: asArray2(entry.fields).filter(
16689
+ (field) => field?.name && field.name !== "_realId" && field.name !== "real_id"
16690
+ ).slice(0, 3).map((field) => ({
16691
+ name: field.name,
16692
+ value: field.value ?? null
16693
+ }))
16694
+ }
16695
+ ])
16696
+ )
16697
+ });
15705
16698
  }
15706
16699
  function createHarnessVerifierSnapshot(input) {
15707
16700
  const workflowFocus = projectWorkflowFocus(
@@ -15798,8 +16791,8 @@ function buildContinuationInstruction(resultPreview) {
15798
16791
  "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.",
15799
16792
  "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.",
15800
16793
  "If this request clearly spans multiple steps and there are no active tasks yet, create 2-4 short user-visible tasks now.",
15801
- "Reuse any existing taskId and decisionId values exactly as they appear in AGENT LOOP STATE.",
15802
- "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.",
16794
+ "Reuse any existing taskId and decisionId values exactly as they appear in [State].",
16795
+ "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.",
15803
16796
  "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.'",
15804
16797
  "If you ask the user a new question in this job, do not also close the loop in the same job.",
15805
16798
  "Write the smallest straightforward code for the current step. Avoid defensive fallback branches for hypothetical states that are not currently true.",
@@ -15810,39 +16803,101 @@ ${resultPreview}` : null
15810
16803
  ].filter(Boolean).join("\n\n");
15811
16804
  }
15812
16805
  function buildGranularAgentDomainBlock(domainDocumentation) {
15813
- return domainDocumentation?.trim() || "No domain reference available. The graph may not be ready yet.";
16806
+ return domainDocumentation?.trim() || "No domain contract available. The graph may not be ready yet.";
15814
16807
  }
15815
16808
  function buildGranularAgentSessionBlock(sessionContext) {
15816
- if (!sessionContext) return "No session metadata available.";
15817
- const rows = [
15818
- ["sandboxId", sessionContext.sandboxId],
15819
- ["environmentId", sessionContext.environmentId],
15820
- ["userName", sessionContext.userName]
15821
- ];
15822
- const activeRows = rows.filter(([, value]) => Boolean(value));
15823
- if (activeRows.length === 0) return "No session metadata available.";
15824
- return activeRows.map(([key, value]) => `${key}: ${value}`).join("\n");
16809
+ return renderConstBlock("session", {
16810
+ runtimeId: sessionContext?.sandboxId || null,
16811
+ environmentId: sessionContext?.environmentId || null,
16812
+ userName: sessionContext?.userName || null,
16813
+ domainRevision: sessionContext?.domainRevision || null
16814
+ });
15825
16815
  }
15826
16816
  function buildGranularAgentHeapBlock(heapSummary) {
15827
- return heapSummary?.trim() || "Heap is empty for this session.";
16817
+ return heapSummary?.trim() || renderConstBlock("savedData", {
16818
+ variables: {},
16819
+ lists: {},
16820
+ entries: {}
16821
+ });
15828
16822
  }
15829
16823
  function buildGranularAgentReferentBlock(referentSummary) {
15830
- return referentSummary?.trim() || "No recent referents recorded from prior assistant replies.";
16824
+ return referentSummary?.trim() || renderConstBlock("recentReferences", []);
15831
16825
  }
15832
16826
  function buildGranularAgentLoopBlock(loopSummary) {
15833
- return loopSummary?.trim() || "No active loop state recorded for this session.";
16827
+ return loopSummary?.trim() || renderConstBlock("workflowState", {
16828
+ tasks: [],
16829
+ decisions: [],
16830
+ openPrompts: [],
16831
+ closure: null
16832
+ });
15834
16833
  }
15835
16834
  function buildGranularAgentWorkflowBlock(workflowSummary) {
15836
- return workflowSummary?.trim() || "No current workflow snapshot recorded for this request yet.";
15837
- }
15838
- function buildGranularAgentToolBlock(tools) {
16835
+ return workflowSummary?.trim() || renderConstBlock("workflowContext", {
16836
+ boundary: null,
16837
+ recentActions: [],
16838
+ workingSet: {
16839
+ variables: [],
16840
+ lists: [],
16841
+ entries: []
16842
+ },
16843
+ openHandles: {
16844
+ tasks: [],
16845
+ decisions: [],
16846
+ prompts: []
16847
+ }
16848
+ });
16849
+ }
16850
+ function resolvePromptCapabilities(capabilities) {
16851
+ return {
16852
+ executeCode: capabilities?.executeCode !== false,
16853
+ readEntities: capabilities?.readEntities !== false,
16854
+ workflowHelpers: Array.isArray(capabilities?.workflowHelpers) ? capabilities.workflowHelpers : [
16855
+ "ask_user",
16856
+ "confirm",
16857
+ "open_decision",
16858
+ "close_decision",
16859
+ "create_task",
16860
+ "update_task",
16861
+ "complete_task",
16862
+ "close_loop"
16863
+ ],
16864
+ savedData: capabilities?.savedData !== false,
16865
+ showRecords: capabilities?.showRecords !== false
16866
+ };
16867
+ }
16868
+ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
16869
+ const resolvedCapabilities = resolvePromptCapabilities(capabilityOverrides);
16870
+ const normalizedTools = (tools || []).filter((tool) => tool?.name).slice().sort((left, right) => {
16871
+ const leftScope = `${left.className || "global"}:${left.static ? "static" : "instance"}`;
16872
+ const rightScope = `${right.className || "global"}:${right.static ? "static" : "instance"}`;
16873
+ return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
16874
+ });
16875
+ const writeActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
16876
+ const scope = tool.className ? `${tool.static ? "class" : "record"}:${tool.className}` : "global";
16877
+ return {
16878
+ name: tool.name,
16879
+ scope,
16880
+ description: tool.description?.trim() || null
16881
+ };
16882
+ });
16883
+ const capabilities = {
16884
+ executeCode: resolvedCapabilities.executeCode,
16885
+ readEntities: resolvedCapabilities.readEntities,
16886
+ writeActions,
16887
+ workflowHelpers: resolvedCapabilities.workflowHelpers,
16888
+ savedData: resolvedCapabilities.savedData,
16889
+ showRecords: resolvedCapabilities.showRecords
16890
+ };
16891
+ return renderConstBlock("capabilities", capabilities);
16892
+ }
16893
+ function buildGranularAgentActionIndex(tools) {
15839
16894
  const normalizedTools = (tools || []).filter((tool) => tool?.name).slice().sort((left, right) => {
15840
16895
  const leftScope = `${left.className || "global"}:${left.static ? "static" : "instance"}`;
15841
16896
  const rightScope = `${right.className || "global"}:${right.static ? "static" : "instance"}`;
15842
16897
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
15843
16898
  });
15844
16899
  if (normalizedTools.length === 0) {
15845
- return "No live effects are available in this session yet.";
16900
+ return "No domain write actions are available.";
15846
16901
  }
15847
16902
  const globalTools = normalizedTools.filter((tool) => !tool.className);
15848
16903
  const staticTools = normalizedTools.filter(
@@ -15851,9 +16906,7 @@ function buildGranularAgentToolBlock(tools) {
15851
16906
  const instanceTools = normalizedTools.filter(
15852
16907
  (tool) => Boolean(tool.className && !tool.static)
15853
16908
  );
15854
- const lines = [
15855
- "Treat this block as the planning map. Use DOMAIN REFERENCE below for exact signatures and query examples."
15856
- ];
16909
+ const lines = ["Available actions by scope:"];
15857
16910
  const appendGroup = (title, group) => {
15858
16911
  lines.push(`- ${title}:`);
15859
16912
  if (group.length === 0) {
@@ -15862,187 +16915,561 @@ function buildGranularAgentToolBlock(tools) {
15862
16915
  }
15863
16916
  for (const tool of group.slice(0, 10)) {
15864
16917
  const availability = tool.ready === false ? " [not ready]" : "";
16918
+ const schema = formatActionSchemaSummary(tool);
15865
16919
  const description = tool.description?.trim() ? ` - ${tool.description.trim()}` : "";
15866
- lines.push(` ${tool.name}${availability}${description}`);
16920
+ lines.push(` ${tool.name}${availability}${schema}${description}`);
15867
16921
  }
15868
16922
  if (group.length > 10) {
15869
16923
  lines.push(` +${group.length - 10} more`);
15870
16924
  }
15871
16925
  };
15872
- appendGroup("Global effects", globalTools);
15873
- appendGroup("Class-level effects", staticTools);
15874
- appendGroup("Record-level effects", instanceTools);
16926
+ appendGroup("Global", globalTools);
16927
+ appendGroup("Class-level", staticTools);
16928
+ appendGroup("Record-level", instanceTools);
15875
16929
  return lines.join("\n");
15876
16930
  }
15877
- function buildGranularAgentCheckpointBlock(checkpoint) {
15878
- if (!checkpoint) {
15879
- return "No previous execution checkpoint recorded for this request yet.";
15880
- }
15881
- const lines = [];
15882
- if (typeof checkpoint.iteration === "number") {
15883
- lines.push(`iteration: ${checkpoint.iteration}`);
15884
- }
15885
- if (checkpoint.latestJobStatus) {
15886
- lines.push(`latestJobStatus: ${checkpoint.latestJobStatus}`);
15887
- }
15888
- if (checkpoint.controllerOutcome) {
15889
- lines.push(`controllerOutcome: ${checkpoint.controllerOutcome}`);
16931
+ function normalizeJsonSchema(value) {
16932
+ if (typeof value === "string") {
16933
+ try {
16934
+ return asRecord4(JSON.parse(value));
16935
+ } catch {
16936
+ return null;
16937
+ }
15890
16938
  }
15891
- if (checkpoint.controllerReason) {
15892
- lines.push(`controllerReason: ${checkpoint.controllerReason}`);
16939
+ return asRecord4(value);
16940
+ }
16941
+ function jsonSchemaTypeName(schema) {
16942
+ const record = normalizeJsonSchema(schema);
16943
+ if (!record) return "unknown";
16944
+ const type = record.type;
16945
+ if (typeof type === "string") {
16946
+ if (type === "array") return "array";
16947
+ if (type === "object") return "object";
16948
+ return type;
15893
16949
  }
15894
- if (typeof checkpoint.noProgressCount === "number") {
15895
- lines.push(`noProgressCount: ${checkpoint.noProgressCount}`);
16950
+ return "unknown";
16951
+ }
16952
+ function summarizeObjectSchema(schema) {
16953
+ const record = normalizeJsonSchema(schema);
16954
+ const properties = asRecord4(record?.properties);
16955
+ if (!properties || Object.keys(properties).length === 0) {
16956
+ return record ? "{}" : null;
16957
+ }
16958
+ const required = new Set(asArray2(record?.required));
16959
+ const fields = Object.entries(properties).slice(0, 8).map(([name, property]) => {
16960
+ const marker = required.has(name) ? "*" : "?";
16961
+ return `${name}${marker}: ${jsonSchemaTypeName(property)}`;
16962
+ });
16963
+ const remaining = Object.keys(properties).length - fields.length;
16964
+ return remaining > 0 ? `${fields.join(", ")}, +${remaining}` : fields.join(", ");
16965
+ }
16966
+ function formatActionSchemaSummary(tool) {
16967
+ const input = summarizeObjectSchema(tool.inputSchema);
16968
+ const output = summarizeObjectSchema(tool.outputSchema);
16969
+ const parts = [];
16970
+ if (input) parts.push(`input { ${input} }`);
16971
+ if (output) parts.push(`output { ${output} }`);
16972
+ return parts.length ? ` (${parts.join("; ")})` : "";
16973
+ }
16974
+ function splitDomainDocumentation(domainDocumentation) {
16975
+ const normalized = domainDocumentation?.trim() || "";
16976
+ if (!normalized) return { types: "", docs: "" };
16977
+ const docsSectionMatch = normalized.match(/\n\s*\[Docs\]\s*\n/i);
16978
+ if (docsSectionMatch?.index !== void 0) {
16979
+ return {
16980
+ types: normalized.slice(0, docsSectionMatch.index).trim(),
16981
+ docs: normalized.slice(docsSectionMatch.index + docsSectionMatch[0].length).trim()
16982
+ };
15896
16983
  }
15897
- if (checkpoint.latestJobError?.trim()) {
15898
- lines.push(`latestJobError: ${checkpoint.latestJobError.trim()}`);
16984
+ const legacyMarker = "Generated usage notes from ./sandbox-tools docs:";
16985
+ const legacyIndex = normalized.indexOf(legacyMarker);
16986
+ if (legacyIndex !== -1) {
16987
+ return {
16988
+ types: normalized.slice(0, legacyIndex).trim(),
16989
+ docs: normalized.slice(legacyIndex + legacyMarker.length).trim()
16990
+ };
15899
16991
  }
15900
- if (Array.isArray(checkpoint.latestActionSummary) && checkpoint.latestActionSummary.length > 0) {
15901
- lines.push("latestActionSummary:");
15902
- for (const line of checkpoint.latestActionSummary.slice(0, 8)) {
15903
- const normalizedLine = normalizeActionSummaryForPrompt(line);
15904
- lines.push(
15905
- normalizedLine.startsWith("- ") ? normalizedLine : `- ${normalizedLine}`
15906
- );
16992
+ return { types: normalized, docs: "" };
16993
+ }
16994
+ function buildGranularAgentCheckpointBlock(checkpoint) {
16995
+ if (!checkpoint) {
16996
+ return renderConstBlock("previousCodeResult", null);
16997
+ }
16998
+ return renderConstBlock("previousCodeResult", {
16999
+ iteration: typeof checkpoint.iteration === "number" ? checkpoint.iteration : null,
17000
+ latestJobStatus: checkpoint.latestJobStatus || null,
17001
+ controllerOutcome: checkpoint.controllerOutcome || null,
17002
+ controllerReason: checkpoint.controllerReason || null,
17003
+ noProgressCount: typeof checkpoint.noProgressCount === "number" ? checkpoint.noProgressCount : null,
17004
+ latestJobError: checkpoint.latestJobError?.trim() || null,
17005
+ latestActionSummary: Array.isArray(checkpoint.latestActionSummary) ? checkpoint.latestActionSummary.slice(0, 8).map(normalizeActionSummaryForPrompt) : [],
17006
+ latestJobResult: checkpoint.latestJobResult?.trim() || null
17007
+ });
17008
+ }
17009
+ function parseSummaryOutcome(summary) {
17010
+ const outcome = {};
17011
+ for (const part of summary.split(",")) {
17012
+ const trimmed = part.trim();
17013
+ const match = /^([A-Za-z0-9_]+)=(.+)$/.exec(trimmed);
17014
+ if (!match) continue;
17015
+ const [, key, rawValue] = match;
17016
+ const unquoted = rawValue.replace(/^"|"$/g, "");
17017
+ if (/^-?\d+(?:\.\d+)?$/.test(unquoted)) {
17018
+ outcome[key] = Number(unquoted);
17019
+ } else if (unquoted === "true" || unquoted === "false") {
17020
+ outcome[key] = unquoted === "true";
17021
+ } else {
17022
+ outcome[key] = unquoted;
15907
17023
  }
15908
17024
  }
15909
- if (checkpoint.latestJobResult?.trim()) {
15910
- lines.push(`latestJobResult:
15911
- ${checkpoint.latestJobResult.trim()}`);
17025
+ return outcome;
17026
+ }
17027
+ function buildKnownFactsFromCheckpoint(checkpoint) {
17028
+ const summaries = Array.isArray(checkpoint?.latestActionSummary) ? checkpoint.latestActionSummary.map(normalizeActionSummaryForPrompt) : [];
17029
+ const facts = [];
17030
+ for (const summary of summaries) {
17031
+ const countedMatch = /^-\s*Counted\s+([A-Za-z0-9_]+).*?->\s*value=(\d+)/.exec(summary);
17032
+ if (countedMatch) {
17033
+ facts.push({
17034
+ entity: countedMatch[1],
17035
+ query: {},
17036
+ totalCount: Number(countedMatch[2])
17037
+ });
17038
+ continue;
17039
+ }
17040
+ const listedMatch = /^-\s*Listed\s+([A-Za-z0-9_]+).*?->\s*(.+)$/.exec(
17041
+ summary
17042
+ );
17043
+ if (!listedMatch) continue;
17044
+ const outcome = parseSummaryOutcome(listedMatch[2]);
17045
+ const count = typeof outcome.totalCount === "number" ? outcome.totalCount : typeof outcome.count === "number" ? outcome.count : void 0;
17046
+ if (typeof count !== "number") continue;
17047
+ const fact = {
17048
+ entity: listedMatch[1],
17049
+ query: {},
17050
+ totalCount: count
17051
+ };
17052
+ if (typeof outcome.hasMore === "boolean") {
17053
+ fact.lastPageHasMore = outcome.hasMore;
17054
+ fact.loadedAllItems = !outcome.hasMore;
17055
+ } else if (typeof outcome.count === "number" && outcome.count === count) {
17056
+ fact.loadedAllItems = true;
17057
+ }
17058
+ facts.push(fact);
15912
17059
  }
15913
- return lines.length > 0 ? lines.join("\n") : "No previous execution checkpoint recorded for this request yet.";
17060
+ return facts.slice(0, 8);
15914
17061
  }
15915
17062
  function buildGranularAgentSystemPrompt(input) {
17063
+ const outputMode = input.outputMode || "agentMessages";
17064
+ const promptCapabilities = resolvePromptCapabilities(input.capabilities);
17065
+ const domainSections = splitDomainDocumentation(input.domainDocumentation);
15916
17066
  const sessionBlock = buildGranularAgentSessionBlock(input.sessionContext);
15917
- const toolBlock = buildGranularAgentToolBlock(input.tools);
15918
- const domainBlock = buildGranularAgentDomainBlock(input.domainDocumentation);
17067
+ const toolBlock = buildGranularAgentToolBlock(
17068
+ input.tools,
17069
+ input.capabilities
17070
+ );
17071
+ const actionIndex = buildGranularAgentActionIndex(input.tools);
17072
+ const domainBlock = buildGranularAgentDomainBlock(domainSections.types);
15919
17073
  const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
15920
17074
  const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
15921
17075
  const heapBlock = buildGranularAgentHeapBlock(input.heapSummary);
15922
17076
  const referentBlock = buildGranularAgentReferentBlock(input.referentSummary);
15923
17077
  const loopBlock = buildGranularAgentLoopBlock(input.loopSummary);
15924
- return `You are an AI assistant for a live Granular session.
15925
- You can help the user understand the domain, answer questions, or generate and execute code against the live session.
15926
- Your tone must be natural and human-like.
17078
+ const knownFactsBlock = renderConstBlock(
17079
+ "knownFacts",
17080
+ buildKnownFactsFromCheckpoint(input.checkpoint)
17081
+ );
17082
+ 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 }\`.
17083
+ - Use \`{ reply, show }\` when the host UI should render records, heap variables, or lists from session state.
17084
+ - For multi-record display, prefer a saved list/listName so the UI can render a table; use entryPaths for a few individual records.
17085
+ - 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.
17086
+ - 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.
17087
+ - 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(...)\`.
17088
+ - \`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.
17089
+ - 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.
17090
+ - 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.
17091
+ - 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.
17092
+ - 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.
17093
+ - 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"] })\`.
17094
+ - \`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.
17095
+ - 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.
17096
+ - 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.
17097
+ - 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.
17098
+ - 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.
17099
+ - 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.
17100
+ - 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.
17101
+ - 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(...)\`.
17102
+ - \`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.
17103
+ - 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.
17104
+ - 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.
17105
+ - 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.`;
17106
+ const codeRules = promptCapabilities.executeCode ? `Code:
17107
+ - Use when the request needs session data, saved data, workflow state, record display, or available actions.
17108
+ - When using code, assistant text must be empty or one brief summary.
17109
+ - Code must be plain runnable JavaScript with top-level await.
17110
+ - Import needed classes and helpers from "./sandbox-tools".
17111
+ - Use static top-level imports such as \`import { Foo, agent_text_message } from "./sandbox-tools";\`. Do not use dynamic \`await import("./sandbox-tools")\`.
17112
+ - 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.
17113
+ - 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")\`.
17114
+ - 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.
17115
+ - User-visible output must use the provided message or record-display helpers.
17116
+ - After calling an action or effect, inspect the returned object and base the user-facing answer on its actual fields.
17117
+ - When calling an action, use the exact input property names from the action schema. Do not invent synonym keys for required inputs.
17118
+ - 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".
17119
+ - Never call \`process.exit(...)\`; emit a message and use \`return;\` to stop early.
17120
+ - 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.
17121
+ - 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.
17122
+ - Write \`//\` planning comments for the user, not for engineers: make them friendly, plain-language, and easy to understand.
17123
+ - 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.
17124
+ - 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.
17125
+ - Avoid technical terms, implementation names, code concepts, hidden helper names, and complex domain jargon in \`//\` planning comments unless the user already used that wording.
17126
+ - Each \`//\` planning comment should provide valuable feedback about the plan or next visible step. Do not add filler such as "Starting", "Running", or "Processing".
17127
+ ${outputRules}` : `Code:
17128
+ - Code execution is unavailable. Use text only, or ask the user for missing information.`;
17129
+ const workflowRules = promptCapabilities.workflowHelpers.length > 0 ? `Workflow:
17130
+ - Use workflow helpers when missing input should pause and resume the workflow.
17131
+ - If code discovers missing required input after a read, use \`await loop.ask_user(...)\`; do not just tell the user to provide it.
17132
+ - 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.
17133
+ - 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.
17134
+ - 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.
17135
+ - 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.
17136
+ - Use choice only for 2 to 5 short grounded options.
17137
+ - For record choices, set each option value to a stable scalar such as the record \`_graphPath\` or \`id\`, not a label-only value.
17138
+ - 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.
17139
+ - 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.
17140
+ - 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.
17141
+ - 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.
17142
+ - 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.
17143
+ - 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.
17144
+ - Reuse existing task, decision, and closure ids from [State].
17145
+ - If a user request matches both a domain record/action and a workflow helper, prefer the domain capability.` : "";
17146
+ return `[Harness]
17147
+ You are an assistant for a live user session. Use plain, natural language.
15927
17148
 
15928
- 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.
15929
- When you call \`execute_code\`, additional assistant text must be either:
15930
- - empty, or
15931
- - a brief summary of the actions the generated code will perform.
15932
- Do not include any other kind of commentary when calling \`execute_code\`.
15933
- - 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(...)\`.
15934
- - 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.
15935
- - 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.
15936
- - 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.
17149
+ Mode selection:
17150
+ Text only:
17151
+ - Use for general explanations, unsupported requests, or requests that do not need session data.
17152
+ - 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.
17153
+ - 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.
17154
+ - Do not expose internal names, helper names, file paths, parameter names, or code.
17155
+ - 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.
15937
17156
 
15938
- \u2500\u2500\u2500 STREAMING COMMENT RULES \u2500\u2500\u2500
15939
- - While you are writing code, add short single-line comments with the prefix \`// \` before meaningful blocks.
15940
- - These comments should explain the intent in friendly product language, not in implementation jargon.
15941
- - Comments are shown live as a reasoning trace, so keep them brief, concrete, and useful.
15942
- - Do not mention method names, file paths, or internal identifiers in those comments.
15943
- - Use only single-line \`//\` comments for this purpose. Do not use block comments.
15944
- - If you are replying with text only, you may also include a few leading \`// \` comment lines before the final answer.
15945
- - End text-only replies with the plain user-facing answer on normal lines, without a comment prefix.
17157
+ ${codeRules}
15946
17158
 
15947
- \u2500\u2500\u2500 RESPONSE STYLE RULES \u2500\u2500\u2500
15948
- - Use plain, friendly product language.
15949
- - Never mention internal implementation details in user-facing text:
15950
- class names, effect names, method names, function names, file paths, parameter names, or code snippets.
15951
- - Never expose dotted identifiers such as \`Class.method\` in user-facing text.
15952
- - Do not say "sandbox" in user-facing text unless the user is explicitly asking about the runtime environment itself.
15953
- - If you need clarification, ask in everyday language.
15954
- - 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.
15955
- - 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.
15956
- - Keep replies concise and clear.
15957
- - This is a conversation UI, not an API console. Favor human answers over machine-shaped payloads.
17159
+ ${workflowRules}
15958
17160
 
15959
- \u2500\u2500\u2500 SESSION CONTEXT \u2500\u2500\u2500
15960
- ${sessionBlock}
17161
+ High-priority execution rules:
17162
+ - 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.
17163
+ - 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.
17164
+ - 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.
17165
+ - 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.
17166
+ - 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.
17167
+ - 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.
17168
+ - 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.
17169
+ - 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.
17170
+ - 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.
17171
+ - 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.
17172
+ - 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.
17173
+ - 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.
17174
+ - 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.
17175
+ - 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.
17176
+ - 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.
17177
+ - 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.
17178
+ - 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.
17179
+ - 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.
17180
+ - 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.
17181
+ - 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.
17182
+ - 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.
15961
17183
 
15962
- \u2500\u2500\u2500 CAPABILITY SNAPSHOT \u2500\u2500\u2500
15963
- ${toolBlock}
17184
+ Intent resolution:
17185
+ - If intent is explicit, act directly.
17186
+ - 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.
17187
+ - 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.
17188
+ - 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.
17189
+ - 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.
17190
+ - 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.
17191
+ - 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.
17192
+ - 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.
17193
+ - 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.
17194
+ - 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.
17195
+ - 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.
17196
+ - 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.
17197
+ - 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.
17198
+ - 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.
17199
+ - 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.
17200
+ - 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.
17201
+ - 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.
17202
+ - Never call \`.get({ path: "" })\`; an empty path is not a saved reference.
17203
+ - 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.
17204
+ - \`.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.
17205
+ - 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.
17206
+ - If the entity, field, target, scope, ranking, or action is ambiguous, create 2 to 5 plausible interpretations.
17207
+ - Probe plausible interpretations with cheap read-only queries before deciding.
17208
+ - 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.
17209
+ - 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.
17210
+ - One strong match means proceed.
17211
+ - Several plausible matches means call \`loop.ask_user({ type: "choice", ... })\` with grounded choices.
17212
+ - No grounded match means ask for missing information.
17213
+ - For consequential changes, resolve first, confirm when needed, then act.
17214
+ - 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.
17215
+ - 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\`.
17216
+ - 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.
17217
+ - 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.
15964
17218
 
15965
- \u2500\u2500\u2500 DOMAIN REFERENCE (from ./sandbox-tools) \u2500\u2500\u2500
15966
- Import classes and effect functions from \`./sandbox-tools\` in generated code.
15967
- Use the TypeScript declarations for exact signatures. When present, the generated usage notes below them show query patterns and examples.
17219
+ Use exploratory probing when:
17220
+ - the user gives a human reference instead of an exact id or path
17221
+ - a noun could refer to multiple entity types
17222
+ - a name, number, label, date, or amount is given without a clear field
17223
+ - ranking words are used without a clear metric
17224
+ - a requested change has an unclear target
17225
+ - the first reasonable lookup returns zero results
17226
+ - the first reasonable lookup returns several plausible results
17227
+
17228
+ Do not explore when:
17229
+ - the entity, field, filter, and action are explicit
17230
+ - the request is a general explanation
17231
+ - the request is unsupported by available capabilities
17232
+ - the next step is already a required workflow answer or confirmation
17233
+
17234
+ [Types]
17235
+ Import classes, helpers, and available actions from "./sandbox-tools".
17236
+ 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.
15968
17237
 
15969
17238
  ${domainBlock}
15970
17239
 
15971
- \u2500\u2500\u2500 EXECUTION CHECKPOINT \u2500\u2500\u2500
17240
+ [Docs]
17241
+ Query policy:
17242
+ - Use filter, search, sort, count, page, list, and iterate on entity classes.
17243
+ - Push filtering and sorting into entity queries. Do not fetch a page only to filter or sort locally.
17244
+ - Valid filter fields are defined by each entity filter type.
17245
+ - Valid sort fields are defined by each entity sort field type.
17246
+ - Search is class-wide text retrieval, not a field-scoped operator.
17247
+ - Entity classes do not have a \`.search(...)\` method. Use \`.find({ search })\`, \`.page({ search, ... })\`, or \`.list({ search, ... })\`.
17248
+ - 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\`.
17249
+ - 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.
17250
+ - 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.
17251
+ - Combine search and filter when both free-text matching and exact constraints are needed.
17252
+ - 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.
17253
+ - 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.
17254
+ - Boolean filters use \`equal_to: true\` or \`equal_to: false\`.
17255
+ - 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.
17256
+ - 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.
17257
+ - 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.
17258
+ - 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.
17259
+ - 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.
17260
+ - 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.
17261
+ - 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.
17262
+ - 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.
17263
+ - 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.
17264
+ - Prefer generated instance relationship getters from a grounded record over hand-written deep nested relationship filters.
17265
+ - 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.
17266
+ - 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.
17267
+ - 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.
17268
+ - 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.
17269
+ - 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.
17270
+ - 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.
17271
+ - 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.
17272
+ - 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.
17273
+ - 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.
17274
+ - 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.
17275
+ - 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.
17276
+ - 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 }\`.
17277
+ - 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.
17278
+ - 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.
17279
+ - 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.
17280
+ - 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.
17281
+ - 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.
17282
+ - 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.
17283
+ - 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.
17284
+ - 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.
17285
+ - 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.
17286
+ - For scheduling actions, convert relative wording into concrete ISO timestamps before mutating records.
17287
+ - 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.
17288
+ - 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.
17289
+ - 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.
17290
+ - 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.
17291
+ - For exploratory work, use count for totals and page with small perPage for samples; use iteration only after the interpretation is chosen.
17292
+
17293
+ Lookup ladder:
17294
+ 1. Check recent references and saved session data.
17295
+ 2. Try exact id or path when the user gave an id-like value.
17296
+ 3. If the request names a parent/container plus a target, ground the parent/container and traverse declared relationships to target candidates.
17297
+ 4. Try exact filters on fields whose names or aliases match the user words.
17298
+ 5. Try class-wide search with short target-local terms, not the whole user phrase.
17299
+ 6. Try relationship filters when the user mentions connected concepts and the filter shape is documented.
17300
+ 7. If the user names a parent/container and says the label may be approximate, inspect related target records before reporting no match.
17301
+ 8. If still empty, try one small set of normalized, prefix, or fuzzy variants when search supports it.
17302
+ 9. If still empty or ambiguous, ask the user for steering.
17303
+
17304
+ Exploration budget:
17305
+ - For a simple ambiguous reference, try up to 3 strategies.
17306
+ - For a broad ambiguous task, try up to 5 strategies.
17307
+ - Probe with small pages.
17308
+ - Do not run exhaustive scans during probing unless the user explicitly asks for all records or the selected task requires aggregation.
17309
+ - Stop early when a strong unique match is found.
17310
+
17311
+ Strong unique match:
17312
+ - exactly one record matches an exact id or path
17313
+ - exactly one record matches an exact filter on a likely identifier field
17314
+ - exactly one recent reference or saved value fits the request
17315
+ - one interpretation has results and all other reasonable interpretations have none
17316
+
17317
+ Ask the user when:
17318
+ - multiple exact matches exist
17319
+ - several entity types match the same phrase
17320
+ - the best match comes only from broad search and other plausible matches exist
17321
+ - the ranking or metric is unclear
17322
+ - the target is unique but the requested action is unclear
17323
+
17324
+ Relationship filters:
17325
+ - One-record relationships use \`is\`.
17326
+ - Multi-record relationships use \`some\`.
17327
+ - 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.
17328
+ - 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.
17329
+ - 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.
17330
+ - 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.
17331
+ - Use \`some\` only when the generated TypeScript type says \`ManyRelationFilter\`.
17332
+ - 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.
17333
+ - Use \`{ relationship: { id: "record_id" } }\` or \`{ relationship: { path: "class_record_id" } }\` when matching a known related record.
17334
+ - 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\`.
17335
+ - 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.
17336
+ - Use \`{ relationship: { is: { field: { equal_to: value } } } }\` only for nested field filters. Never put \`id\` or \`path\` inside \`is\`.
17337
+ - 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.
17338
+ - Do not pass a full record instance into a filter; if you already fetched a record, filter by its id or path instead.
17339
+ ${domainSections.docs ? `
17340
+ Domain notes:
17341
+ ${domainSections.docs}
17342
+ ` : ""}
17343
+
17344
+ Actions:
17345
+ ${actionIndex}
17346
+ - 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(...)\`.
17347
+ - Actions listed under "Class-level" are class/static methods. Call them on the imported class, e.g. \`await Item.action_name(...)\`.
17348
+ - 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.
17349
+ - Never call a record-level action as \`Class.action_name(...)\`; that method will not exist.
17350
+ - 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.
17351
+ - 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.
17352
+ - 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.
17353
+ - 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.
17354
+ - 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.
17355
+ - 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.
17356
+ - 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.
17357
+ - 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.
17358
+
17359
+ [State]
17360
+ ${toolBlock}
17361
+
17362
+ ${sessionBlock}
17363
+
15972
17364
  ${checkpointBlock}
15973
17365
 
15974
- \u2500\u2500\u2500 WORKFLOW SNAPSHOT \u2500\u2500\u2500
15975
17366
  ${workflowBlock}
15976
17367
 
15977
- \u2500\u2500\u2500 RECENT REFERENTS \u2500\u2500\u2500
15978
17368
  ${referentBlock}
15979
17369
 
15980
- \u2500\u2500\u2500 SESSION HEAP \u2500\u2500\u2500
15981
17370
  ${heapBlock}
15982
17371
 
15983
- \u2500\u2500\u2500 AGENT LOOP STATE \u2500\u2500\u2500
15984
17372
  ${loopBlock}
15985
17373
 
15986
- \u2500\u2500\u2500 LOOP PLAYBOOK \u2500\u2500\u2500
15987
- - 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.
15988
- - Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
15989
- - Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
15990
- - Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
15991
- - 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.
15992
- - 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.
15993
- - If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
15994
- - 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.
15995
- - 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.
15996
- - 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.
15997
- - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
15998
- - If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
15999
- - Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
16000
- - 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.
16001
- - When \`type: 'choice'\` fits, do not ask the same question as plain text with bullets such as "Common options:" or "Choose one of these:".
16002
- - Use \`loop.confirm(...)\` for consequential approval unless the user already clearly instructed you to perform that exact action now.
16003
- - Await \`loop.ask_user(...)\` and \`loop.confirm(...)\`. After the job resumes, continue in the same job whenever the answer is enough to act.
16004
- - 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.
16005
- - If you ask a new question in the current job, do not also close the loop in that same job.
17374
+ ${knownFactsBlock}
16006
17375
 
16007
- \u2500\u2500\u2500 LOOP HELPER REFERENCE \u2500\u2500\u2500
16008
- - \`loop.ask_user(...)\`: pause the current job for missing input; use \`type: 'choice'\` only for a short grounded shortlist.
16009
- - \`loop.confirm(...)\`: pause for yes/no approval before a consequential action, then branch on the returned boolean.
16010
- - \`loop.open_decision(...)\`: save explicit candidates that later jobs can revisit; each candidate needs an \`id\`.
16011
- - \`loop.close_decision(...)\`: resolve an open decision with a stored \`selectedId\` and optional rationale.
16012
- - \`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.
16013
- - \`loop.close_loop(...)\`: record the workflow outcome when it is completed, canceled, or blocked.
17376
+ [Request]
17377
+ ${input.request?.trim() || "Use the latest user message in the conversation."}`;
17378
+ }
16014
17379
 
16015
- \u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
16016
- - Import from \`./sandbox-tools\`.
16017
- - If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
16018
- - Write top-level executable code with \`await\` at top level.
16019
- - The generated job body must be plain runnable JavaScript. Do not use TypeScript-only syntax.
16020
- - Follow the exact classes, methods, and parameter shapes in DOMAIN REFERENCE. Do not invent helpers or unsupported arguments.
16021
- - Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
16022
- - 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.
16023
- - \`perPage\` defaults to \`100\` and is capped at \`100\`.
16024
- - 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.
16025
- - Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
16026
- - 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.
16027
- - 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.
16028
- - If ordering alone answers the request, use \`sort\` without inventing a \`filter\`.
16029
- - 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(...)\`.
16030
- - 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.
16031
- - Call instance methods on instances, static methods on classes, and global effects by name.
16032
- - Use \`heap.getEntry(path)\` for remembered heap entries, \`heap.getList(name)\` for remembered lists, and \`heap.getVar(name)\` only for named variables.
16033
- - Use \`heap.setVar(...)\` and \`heap.deleteVar(...)\` only when they help the next step.
16034
- - Prefer \`heap.setVar(...)\` for scalars or one selected instance. Prefer \`ClassName.list({ saveAs })\` for reusable typed lists. Empty arrays are allowed.
16035
- - 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.
16036
- - Use the \`loop\` helpers to manage workflow state: \`ask_user\`, \`confirm\`, \`open_decision\`, \`close_decision\`, \`create_task\`, \`update_task\`, \`complete_task\`, and \`close_loop\`.
16037
- - Use \`type: 'choice'\` only for short grounded options. Use \`type: 'input'\` when the answer should stay open-ended.
16038
- - \`loop.confirm(...)\` is for consequential approval. Do not ask for approval in plain text.
16039
- - After \`await loop.ask_user(...)\` or \`await loop.confirm(...)\`, continue in the same resumed job when the answer is enough to act.
16040
- - Every job that answers the user must emit \`agent_text_message(...)\` and/or \`agent_heap_objects(...)\`.
16041
- - Use \`agent_text_message(...)\` for user-visible text.
16042
- - 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.
16043
- - 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.
16044
- - Keep the code small and direct. Avoid speculative branches, broad casts, and raw JSON dumps unless the user asked for them.
16045
- - Use \`console.log()\` only for intermediate diagnostics, not for the final user-facing answer.`;
17380
+ // src/openai-usage.ts
17381
+ var OPENAI_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
17382
+ var OPENAI_PRICING_EFFECTIVE_DATE = "2026-05-19";
17383
+ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
17384
+ "gpt-5.4": {
17385
+ provider: "openai",
17386
+ model: "gpt-5.4",
17387
+ currency: "USD",
17388
+ inputUsdPerMillion: 2.5,
17389
+ cachedInputUsdPerMillion: 0.25,
17390
+ outputUsdPerMillion: 15,
17391
+ sourceUrl: OPENAI_PRICING_SOURCE_URL,
17392
+ effectiveDate: OPENAI_PRICING_EFFECTIVE_DATE
17393
+ }
17394
+ };
17395
+ function asRecord5(value) {
17396
+ return value && typeof value === "object" ? value : null;
17397
+ }
17398
+ function numberField(record, key) {
17399
+ const value = record?.[key];
17400
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
17401
+ }
17402
+ function microsPerMillion(usdPerMillion) {
17403
+ return Math.round(usdPerMillion * 1e6);
17404
+ }
17405
+ function getOpenAIModelPricing(model) {
17406
+ return OPENAI_MODEL_PRICING_USD_PER_MILLION[model] || null;
17407
+ }
17408
+ function normalizeOpenAIUsage(rawUsage) {
17409
+ const usage = asRecord5(rawUsage);
17410
+ if (!usage) {
17411
+ return {
17412
+ inputTokens: 0,
17413
+ cachedInputTokens: 0,
17414
+ uncachedInputTokens: 0,
17415
+ outputTokens: 0,
17416
+ reasoningTokens: 0,
17417
+ totalTokens: 0
17418
+ };
17419
+ }
17420
+ const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
17421
+ const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
17422
+ const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
17423
+ const inputDetails = asRecord5(usage.prompt_tokens_details) || asRecord5(usage.input_tokens_details);
17424
+ const outputDetails = asRecord5(usage.completion_tokens_details) || asRecord5(usage.output_tokens_details);
17425
+ const cachedInputTokens = Math.min(
17426
+ inputTokens,
17427
+ numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
17428
+ );
17429
+ const reasoningTokens = numberField(outputDetails, "reasoning_tokens") || numberField(outputDetails, "reasoning_output_tokens");
17430
+ return {
17431
+ inputTokens,
17432
+ cachedInputTokens,
17433
+ uncachedInputTokens: Math.max(inputTokens - cachedInputTokens, 0),
17434
+ outputTokens,
17435
+ reasoningTokens,
17436
+ totalTokens
17437
+ };
17438
+ }
17439
+ function calculateOpenAITokenSpend(model, rawUsage) {
17440
+ const pricing = getOpenAIModelPricing(model);
17441
+ if (!pricing) return null;
17442
+ const usage = normalizeOpenAIUsage(rawUsage);
17443
+ const inputPricePerMillionMicros = microsPerMillion(
17444
+ pricing.inputUsdPerMillion
17445
+ );
17446
+ const cachedInputPricePerMillionMicros = microsPerMillion(
17447
+ pricing.cachedInputUsdPerMillion
17448
+ );
17449
+ const outputPricePerMillionMicros = microsPerMillion(
17450
+ pricing.outputUsdPerMillion
17451
+ );
17452
+ const amountMicros = Math.round(
17453
+ (usage.uncachedInputTokens * inputPricePerMillionMicros + usage.cachedInputTokens * cachedInputPricePerMillionMicros + usage.outputTokens * outputPricePerMillionMicros) / 1e6
17454
+ );
17455
+ return {
17456
+ provider: "openai",
17457
+ model,
17458
+ inputTokens: usage.inputTokens,
17459
+ cachedInputTokens: usage.cachedInputTokens,
17460
+ uncachedInputTokens: usage.uncachedInputTokens,
17461
+ outputTokens: usage.outputTokens,
17462
+ reasoningTokens: usage.reasoningTokens,
17463
+ totalTokens: usage.totalTokens,
17464
+ amountMicros,
17465
+ currency: "USD",
17466
+ inputPricePerMillionMicros,
17467
+ cachedInputPricePerMillionMicros,
17468
+ outputPricePerMillionMicros,
17469
+ pricingSource: pricing.sourceUrl,
17470
+ pricingEffectiveAt: pricing.effectiveDate,
17471
+ usage
17472
+ };
16046
17473
  }
16047
17474
 
16048
17475
  // src/agent-evals.ts
@@ -16050,7 +17477,7 @@ var DEFAULT_CONTROLLER_BUDGETS = {
16050
17477
  maxIterations: 6,
16051
17478
  maxNoProgressIterations: 2
16052
17479
  };
16053
- function asRecord5(value) {
17480
+ function asRecord6(value) {
16054
17481
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
16055
17482
  return value;
16056
17483
  }
@@ -16102,6 +17529,155 @@ function asArray3(value) {
16102
17529
  if (!value) return [];
16103
17530
  return Array.isArray(value) ? value : [value];
16104
17531
  }
17532
+ var GPT_54_TOKEN_PRICING_USD_PER_MILLION = {
17533
+ input: 2.5,
17534
+ cachedInput: 0.25,
17535
+ output: 15
17536
+ };
17537
+ function emptyTokenUsage() {
17538
+ return {
17539
+ calls: 0,
17540
+ inputTokens: 0,
17541
+ cachedInputTokens: 0,
17542
+ uncachedInputTokens: 0,
17543
+ outputTokens: 0,
17544
+ totalTokens: 0,
17545
+ inputCostUsd: 0,
17546
+ cachedInputCostUsd: 0,
17547
+ outputCostUsd: 0,
17548
+ totalCostUsd: 0,
17549
+ missingUsageCalls: 0
17550
+ };
17551
+ }
17552
+ function numberField2(record, key) {
17553
+ const value = record?.[key];
17554
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
17555
+ }
17556
+ function calculateTokenCost(input) {
17557
+ const spend = input.model ? calculateOpenAITokenSpend(input.model, {
17558
+ input_tokens: input.uncachedInputTokens + input.cachedInputTokens,
17559
+ output_tokens: input.outputTokens,
17560
+ input_tokens_details: { cached_tokens: input.cachedInputTokens }
17561
+ }) : null;
17562
+ const pricing = spend ? {
17563
+ input: spend.inputPricePerMillionMicros / 1e6,
17564
+ cachedInput: spend.cachedInputPricePerMillionMicros / 1e6,
17565
+ output: spend.outputPricePerMillionMicros / 1e6
17566
+ } : GPT_54_TOKEN_PRICING_USD_PER_MILLION;
17567
+ const inputCostUsd = input.uncachedInputTokens * pricing.input / 1e6;
17568
+ const cachedInputCostUsd = input.cachedInputTokens * pricing.cachedInput / 1e6;
17569
+ const outputCostUsd = input.outputTokens * pricing.output / 1e6;
17570
+ return {
17571
+ inputCostUsd,
17572
+ cachedInputCostUsd,
17573
+ outputCostUsd,
17574
+ totalCostUsd: inputCostUsd + cachedInputCostUsd + outputCostUsd
17575
+ };
17576
+ }
17577
+ function extractTokenUsageFromRaw(raw) {
17578
+ const usage = asRecord6(asRecord6(raw)?.usage);
17579
+ if (!usage) return null;
17580
+ const model = typeof asRecord6(raw)?.model === "string" ? asRecord6(raw)?.model : void 0;
17581
+ const inputTokens = numberField2(usage, "prompt_tokens") || numberField2(usage, "input_tokens");
17582
+ const outputTokens = numberField2(usage, "completion_tokens") || numberField2(usage, "output_tokens");
17583
+ const details = asRecord6(usage.prompt_tokens_details) || asRecord6(usage.input_tokens_details);
17584
+ const cachedInputTokens = Math.min(
17585
+ inputTokens,
17586
+ numberField2(details, "cached_tokens") || numberField2(details, "cached_input_tokens")
17587
+ );
17588
+ const uncachedInputTokens = Math.max(inputTokens - cachedInputTokens, 0);
17589
+ const totalTokens = numberField2(usage, "total_tokens") || inputTokens + outputTokens;
17590
+ const costs = calculateTokenCost({
17591
+ model,
17592
+ uncachedInputTokens,
17593
+ cachedInputTokens,
17594
+ outputTokens
17595
+ });
17596
+ return {
17597
+ calls: 1,
17598
+ inputTokens,
17599
+ cachedInputTokens,
17600
+ uncachedInputTokens,
17601
+ outputTokens,
17602
+ totalTokens,
17603
+ ...costs,
17604
+ missingUsageCalls: 0
17605
+ };
17606
+ }
17607
+ function addTokenUsage(aggregate, usage) {
17608
+ if (!usage) {
17609
+ return {
17610
+ ...aggregate,
17611
+ missingUsageCalls: aggregate.missingUsageCalls + 1
17612
+ };
17613
+ }
17614
+ const inputTokens = aggregate.inputTokens + usage.inputTokens;
17615
+ const cachedInputTokens = aggregate.cachedInputTokens + usage.cachedInputTokens;
17616
+ const uncachedInputTokens = aggregate.uncachedInputTokens + usage.uncachedInputTokens;
17617
+ const outputTokens = aggregate.outputTokens + usage.outputTokens;
17618
+ const costs = calculateTokenCost({
17619
+ uncachedInputTokens,
17620
+ cachedInputTokens,
17621
+ outputTokens
17622
+ });
17623
+ return {
17624
+ calls: aggregate.calls + usage.calls,
17625
+ inputTokens,
17626
+ cachedInputTokens,
17627
+ uncachedInputTokens,
17628
+ outputTokens,
17629
+ totalTokens: aggregate.totalTokens + usage.totalTokens,
17630
+ ...costs,
17631
+ missingUsageCalls: aggregate.missingUsageCalls + usage.missingUsageCalls
17632
+ };
17633
+ }
17634
+ function aggregateTokenUsage(usages) {
17635
+ return usages.reduce(
17636
+ (aggregate, usage) => addTokenUsage(aggregate, usage),
17637
+ emptyTokenUsage()
17638
+ );
17639
+ }
17640
+ function aggregateConversationTokenUsage(conversation) {
17641
+ return aggregateTokenUsage(
17642
+ (conversation.logTurns || []).flatMap(
17643
+ (turn) => turn.iterations.map((iteration) => iteration.tokenUsage)
17644
+ )
17645
+ );
17646
+ }
17647
+ function tokenUsageForGenerationOutput(generation) {
17648
+ const attempts = generation.generationAttempts?.length ? generation.generationAttempts : [{ raw: generation.raw }];
17649
+ return aggregateTokenUsage(
17650
+ attempts.map((attempt) => extractTokenUsageFromRaw(attempt.raw))
17651
+ );
17652
+ }
17653
+ function formatUsd(value) {
17654
+ return `$${value.toFixed(6)}`;
17655
+ }
17656
+ function formatTokenUsage(usage) {
17657
+ if (!usage || usage.calls === 0 && usage.missingUsageCalls === 0) {
17658
+ return ["- LLM calls with usage data: 0", "- Total cost: $0.000000"];
17659
+ }
17660
+ return [
17661
+ `- LLM calls with usage data: ${usage.calls}`,
17662
+ `- LLM calls missing usage data: ${usage.missingUsageCalls}`,
17663
+ `- Input tokens: ${usage.inputTokens}`,
17664
+ `- Cached input tokens: ${usage.cachedInputTokens}`,
17665
+ `- Uncached input tokens: ${usage.uncachedInputTokens}`,
17666
+ `- Output tokens: ${usage.outputTokens}`,
17667
+ `- Total tokens: ${usage.totalTokens}`,
17668
+ `- Input cost: ${formatUsd(usage.inputCostUsd)}`,
17669
+ `- Cached input cost: ${formatUsd(usage.cachedInputCostUsd)}`,
17670
+ `- Output cost: ${formatUsd(usage.outputCostUsd)}`,
17671
+ `- Total cost: ${formatUsd(usage.totalCostUsd)}`,
17672
+ `- Pricing basis: GPT-5.4 at $${GPT_54_TOKEN_PRICING_USD_PER_MILLION.input}/M input, $${GPT_54_TOKEN_PRICING_USD_PER_MILLION.cachedInput}/M cached input, $${GPT_54_TOKEN_PRICING_USD_PER_MILLION.output}/M output.`
17673
+ ];
17674
+ }
17675
+ function getJobAgentMessages(liveDoc, jobId) {
17676
+ const jobsById = asRecord6(asRecord6(liveDoc.jobs)?.byId);
17677
+ const job = asRecord6(jobsById?.[jobId]);
17678
+ const agentMessages = job?.agentMessages;
17679
+ return Array.isArray(agentMessages) ? agentMessages : [];
17680
+ }
16105
17681
  function buildScenarioSteps(scenario) {
16106
17682
  if (scenario.steps?.length) {
16107
17683
  return scenario.steps;
@@ -16157,12 +17733,12 @@ function buildHistory(entries) {
16157
17733
  );
16158
17734
  }
16159
17735
  function getOpenPromptsFromDoc(liveDoc) {
16160
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17736
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
16161
17737
  const prompts = [];
16162
17738
  for (const job of Object.values(jobsById)) {
16163
- const promptRecords = asRecord5(asRecord5(job)?.prompts) || {};
17739
+ const promptRecords = asRecord6(asRecord6(job)?.prompts) || {};
16164
17740
  for (const raw of Object.values(promptRecords)) {
16165
- const record = asRecord5(raw);
17741
+ const record = asRecord6(raw);
16166
17742
  if (!record || record.status !== "open" || typeof record.promptId !== "string")
16167
17743
  continue;
16168
17744
  const prompt = normalizePrompt({
@@ -16183,11 +17759,11 @@ function getOpenPromptsFromDoc(liveDoc) {
16183
17759
  return prompts;
16184
17760
  }
16185
17761
  function filterPromptsByBoundary(liveDoc, prompts, boundaryTimestamp) {
16186
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
17762
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
16187
17763
  return prompts.filter((prompt) => {
16188
17764
  for (const jobRecord of Object.values(jobsById)) {
16189
- const promptsById = asRecord5(asRecord5(jobRecord)?.prompts) || {};
16190
- const promptRecord = asRecord5(promptsById[prompt.id]);
17765
+ const promptsById = asRecord6(asRecord6(jobRecord)?.prompts) || {};
17766
+ const promptRecord = asRecord6(promptsById[prompt.id]);
16191
17767
  const openedAt = Number(promptRecord?.openedAt) || 0;
16192
17768
  if (openedAt >= boundaryTimestamp) return true;
16193
17769
  }
@@ -16217,19 +17793,99 @@ function extractJsonObject(text) {
16217
17793
  const start = text.indexOf("{");
16218
17794
  const end = text.lastIndexOf("}");
16219
17795
  if (start === -1 || end === -1 || end < start) return null;
17796
+ const candidate = text.slice(start, end + 1);
16220
17797
  try {
16221
- return JSON.parse(text.slice(start, end + 1));
17798
+ return JSON.parse(candidate);
16222
17799
  } catch {
16223
- return null;
17800
+ const fallback = {};
17801
+ for (const fieldName of ["action", "reply", "code"]) {
17802
+ const field = extractJsonStringField(candidate, fieldName);
17803
+ if (field?.complete) {
17804
+ fallback[fieldName] = field.value;
17805
+ }
17806
+ }
17807
+ return Object.keys(fallback).length > 0 ? fallback : null;
17808
+ }
17809
+ }
17810
+ function extractJsonStringField(source, fieldName) {
17811
+ const keyIndex = source.indexOf(JSON.stringify(fieldName));
17812
+ if (keyIndex === -1) return null;
17813
+ const colonIndex = source.indexOf(":", keyIndex + fieldName.length + 2);
17814
+ if (colonIndex === -1) return null;
17815
+ let cursor = colonIndex + 1;
17816
+ while (cursor < source.length && /\s/.test(source[cursor] || "")) cursor += 1;
17817
+ if (source[cursor] !== '"') return null;
17818
+ cursor += 1;
17819
+ let value = "";
17820
+ while (cursor < source.length) {
17821
+ const char = source[cursor];
17822
+ if (char === '"') return { value, complete: true };
17823
+ if (char !== "\\") {
17824
+ value += char;
17825
+ cursor += 1;
17826
+ continue;
17827
+ }
17828
+ if (cursor + 1 >= source.length) return { value, complete: false };
17829
+ const escaped = source[cursor + 1];
17830
+ if (escaped === "n") value += "\n";
17831
+ else if (escaped === "r") value += "\r";
17832
+ else if (escaped === "t") value += " ";
17833
+ else if (escaped === "b") value += "\b";
17834
+ else if (escaped === "f") value += "\f";
17835
+ else if (escaped === '"' || escaped === "\\" || escaped === "/") {
17836
+ value += escaped;
17837
+ } else if (escaped === "u") {
17838
+ const hex = source.slice(cursor + 2, cursor + 6);
17839
+ if (hex.length < 4 || !/^[0-9a-fA-F]{4}$/.test(hex)) {
17840
+ return { value, complete: false };
17841
+ }
17842
+ value += String.fromCharCode(Number.parseInt(hex, 16));
17843
+ cursor += 6;
17844
+ continue;
17845
+ } else {
17846
+ value += escaped;
17847
+ }
17848
+ cursor += 2;
16224
17849
  }
17850
+ return { value, complete: false };
16225
17851
  }
16226
17852
  function modelOutputInstruction() {
16227
17853
  return [
16228
17854
  "Return only a JSON object with this shape:",
16229
17855
  '{ "action": "reply" | "job", "reply": string, "code": string }',
16230
17856
  'Use "action":"reply" only when a plain conversational answer is enough and no live session state should change.',
17857
+ 'Do not use "action":"reply" to promise future tool work; if the user asks to check, find, look up, inspect, update, post, send, approve, schedule, reschedule, calculate, or confirm around a domain action, use "action":"job".',
17858
+ 'Do not use "action":"reply" to say a record is not grounded yet; if the request names or describes a domain record, use "action":"job" and ground it from session state, relationships, searches, or visible read-only actions first.',
17859
+ 'Before claiming you lack access, inspect the visible action list. If a visible read-only search, lookup, list, guidance, note, policy, or knowledge action can satisfy a "check", "find", "look up", or "whether we have guidance" request, choose "action":"job" and call it.',
17860
+ "Generated code must not report no matches for the primary human-described anchor after a single zero-result list/find/page call. Before that primary no-match return, retry the primary anchor with fewer text constraints or a distinct fallback such as owner/container grounding, relationship traversal, exact-id/path lookup, or shorter target-local search.",
16231
17861
  'Use "action":"job" when the next step should run code or mutate workflow state.',
16232
17862
  'When action is "job", include runnable code in "code".',
17863
+ "Generated code must not reference prompt-only symbols such as savedData, recentReferences, workflowContext, workflowState, or capabilities. Copy concrete paths/ids from the prompt into strings, fetch records with imports from ./sandbox-tools, or use documented runtime helpers.",
17864
+ "Generated code must import every class and helper it uses from ./sandbox-tools; do not leave undeclared identifiers in the job.",
17865
+ "Generated action calls must use the exact input property names from the visible action schema. Do not invent synonym keys for required inputs.",
17866
+ "If multiple possible targets or a needed human decision blocks a requested operation, put the pause inside code with loop.ask_user(...) or loop.confirm(...); listing candidates or asking only in reply text and returning is incomplete, including when ambiguity is discovered after a query returns several records.",
17867
+ "When a lookup before a mutation returns multiple plausible target records, generated code must ask for a grounded choice; do not mutate results[0], the earliest sorted record, or any other default pick unless the user supplied a unique identifier, ordinal, or selector.",
17868
+ "A bare pronoun such as it, that, or that one is not a unique mutation target when recentReferences, savedData, or the prior visible answer contains multiple compatible records. Do not let one exact recentReference path override that multi-record ambiguity; generated code must ask for a grounded choice before mutating.",
17869
+ "If a follow-up names the same/previous record and also names a related target or evidence type in a condition, use the same/previous record only as the anchor; traverse to the named related type before deciding or mutating.",
17870
+ "For owner/container plus target requests, generated code must ground the owner/container first, then discover the target through relationships, relationship filters, or short target-local search; do not combine owner/container words with target words in one target-class query or require owner/container words to appear in target-local title/summary fields.",
17871
+ "For requested categorical states, generated code should use positive exact filters or explicit local checks; do not use substring negation of another state as a proxy for the requested state.",
17872
+ "For conditional mutations based on a related evidence record, generated code order must be: load the action target or anchor, traverse to the related evidence record, call any visible status/lookup action, then decide whether to mutate. Do not decide, mutate, return, or reject the condition from parent/action-target fields before that evidence step.",
17873
+ "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.",
17874
+ "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.",
17875
+ "Generated code must await declared relationship getters before checking arrays, iterating, or reading related-record fields.",
17876
+ "Generated code must call only relationship getters declared on the current record's class; if the target is not direct, walk the declared intermediate getter first instead of inventing a convenience getter.",
17877
+ "Generated code must use declared relationship getters before reading fields from related records; relationship filter fields are not guaranteed to be hydrated nested objects.",
17878
+ "For suitable/available candidate requests, generated code must call a visible availability, matching, or search action when one exists instead of relying only on current relationships or assignments.",
17879
+ "A follow-up to a refused bypass, export, external-send, or restricted-data request must keep the refusal boundary for the same referent; choose a reply refusal instead of running a mutation job.",
17880
+ "Visible answers for grounded named records should include the stored display name or identifier, not only the user's shorthand.",
17881
+ "When the user asks for specific fields, the reply must include every requested field or explicitly say which grounded field is unavailable after fetching the grounded record if saved state is partial.",
17882
+ "When matching action-returned candidates to grounded records, use the output schema's actual identifier fields, including id, path, or fields ending in Id; do not assume returned candidates have _graphPath.",
17883
+ "When resolving a choice answer, accept an unambiguous prefix or substring of an option label; do not fail just because the returned label is abbreviated.",
17884
+ "Do not discard availability/search results solely because a candidate is already assigned or related, unless the user asked for a different candidate.",
17885
+ "After verifying a user-authorized conditional mutation, call the action directly; do not add loop.confirm(...) solely because the mutation is visible to other people, customer-facing, or consequential. Confirm only when the user, policy, action metadata, or unresolved material uncertainty requires it.",
17886
+ "When a job identifies a specific record in its visible answer, display it with agent_heap_objects(...) when the user should see or open it; otherwise save it with await heap.setVar(...) only when it is needed for follow-up resolution.",
17887
+ "heap.setVar(...) accepts scalars, runtime records, sandbox instances, or arrays of those values; do not save plain action/effect result objects. Fetch a created record by its returned id/path before saving or displaying it.",
17888
+ "For requested record fields, read the documented properties from the fetched record before saying a value is unavailable.",
16233
17889
  'When action is "reply", include the user-facing answer in "reply".'
16234
17890
  ].join("\n");
16235
17891
  }
@@ -16238,7 +17894,30 @@ function createOpenAIChatTurnGenerator(options) {
16238
17894
  /\/$/,
16239
17895
  ""
16240
17896
  );
16241
- const model = options.model || "gpt-5-mini";
17897
+ const model = options.model || "gpt-5.4";
17898
+ const client = new OpenAI({
17899
+ apiKey: options.apiKey,
17900
+ baseURL: baseUrl,
17901
+ defaultHeaders: options.headers
17902
+ });
17903
+ const emitUsage = async (rawUsage, requestId, usageContext) => {
17904
+ if (!rawUsage || !options.onUsage) return;
17905
+ const spend = calculateOpenAITokenSpend(model, rawUsage);
17906
+ if (!spend) return;
17907
+ const mergedUsageContext = {
17908
+ ...options.usageContext || {},
17909
+ ...usageContext || {}
17910
+ };
17911
+ await options.onUsage({
17912
+ ...spend,
17913
+ source: "openai",
17914
+ lineItemType: "llm_tokens",
17915
+ operation: "chat.completions",
17916
+ requestId: requestId || null,
17917
+ usageContext: mergedUsageContext,
17918
+ rawUsage
17919
+ });
17920
+ };
16242
17921
  return async (input) => {
16243
17922
  const messages = [
16244
17923
  {
@@ -16252,7 +17931,8 @@ ${modelOutputInstruction()}`
16252
17931
  ];
16253
17932
  const payload = {
16254
17933
  model,
16255
- messages
17934
+ messages,
17935
+ response_format: { type: "json_object" }
16256
17936
  };
16257
17937
  if (typeof options.temperature === "number") {
16258
17938
  payload.temperature = options.temperature;
@@ -16260,38 +17940,51 @@ ${modelOutputInstruction()}`
16260
17940
  let lastError = null;
16261
17941
  for (let attempt = 1; attempt <= 3; attempt += 1) {
16262
17942
  try {
16263
- const response = await fetch(`${baseUrl}/chat/completions`, {
16264
- method: "POST",
16265
- headers: {
16266
- "content-type": "application/json",
16267
- authorization: `Bearer ${options.apiKey}`,
16268
- ...options.headers
16269
- },
16270
- body: JSON.stringify(payload)
16271
- });
16272
- if (!response.ok) {
16273
- const errorText = await response.text();
16274
- if (attempt < 3 && (response.status >= 500 || response.status === 429)) {
16275
- await sleep2(500 * attempt);
16276
- continue;
17943
+ let raw;
17944
+ let text = "";
17945
+ let usage = null;
17946
+ let requestId = null;
17947
+ if (input.onTextDelta) {
17948
+ const onTextDelta = input.onTextDelta;
17949
+ const stream = await client.chat.completions.create({
17950
+ ...payload,
17951
+ stream: true,
17952
+ stream_options: { include_usage: true }
17953
+ });
17954
+ for await (const event of stream) {
17955
+ requestId = requestId || event.id || event._request_id || null;
17956
+ usage = event.usage || usage;
17957
+ const delta = event.choices?.[0]?.delta?.content;
17958
+ const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord6(part)?.text || "").join("") : "";
17959
+ if (!deltaText) continue;
17960
+ text += deltaText;
17961
+ await onTextDelta(deltaText);
16277
17962
  }
16278
- throw new Error(
16279
- `OpenAI chat generation failed: ${response.status} ${errorText}`
17963
+ raw = { streamed: true, model, usage, request_id: requestId };
17964
+ } else {
17965
+ const completion = await client.chat.completions.create(
17966
+ payload
16280
17967
  );
17968
+ raw = completion;
17969
+ usage = completion.usage;
17970
+ requestId = (typeof completion.id === "string" ? completion.id : null) || (typeof completion._request_id === "string" ? completion._request_id : null);
17971
+ const content = asRecord6(
17972
+ asRecord6(completion.choices?.[0])?.message
17973
+ )?.content;
17974
+ text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord6(part)?.text || "").join("") : "";
16281
17975
  }
16282
- const raw = await response.json();
16283
- const content = asRecord5(
16284
- asRecord5(raw.choices?.[0])?.message
16285
- )?.content;
16286
- const text = typeof content === "string" ? content : Array.isArray(content) ? content.map((part) => asRecord5(part)?.text || "").join("") : "";
17976
+ await emitUsage(usage, requestId, input.usageContext);
16287
17977
  const parsed = extractJsonObject(text);
16288
17978
  if (!parsed) {
16289
17979
  if (attempt < 3) {
16290
17980
  await sleep2(300 * attempt);
16291
17981
  continue;
16292
17982
  }
16293
- throw new Error(`Model output was not valid JSON:
16294
- ${text}`);
17983
+ return {
17984
+ reply: text.trim(),
17985
+ code: void 0,
17986
+ raw
17987
+ };
16295
17988
  }
16296
17989
  return {
16297
17990
  reply: typeof parsed.reply === "string" ? parsed.reply : void 0,
@@ -16300,9 +17993,10 @@ ${text}`);
16300
17993
  };
16301
17994
  } catch (error) {
16302
17995
  lastError = error instanceof Error ? error : new Error(String(error));
16303
- if (attempt < 3 && /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
17996
+ const status = Number(error?.status);
17997
+ if (attempt < 3 && (Number.isFinite(status) && (status >= 500 || status === 429) || /socket connection was closed unexpectedly|ECONNRESET|network|timed out/i.test(
16304
17998
  lastError.message
16305
- )) {
17999
+ ))) {
16306
18000
  await sleep2(500 * attempt);
16307
18001
  continue;
16308
18002
  }
@@ -16318,7 +18012,7 @@ async function ensureDir(dir) {
16318
18012
  async function sleep2(ms) {
16319
18013
  await new Promise((resolve) => setTimeout(resolve, ms));
16320
18014
  }
16321
- async function withTimeout(promise, ms, label) {
18015
+ async function withTimeout2(promise, ms, label) {
16322
18016
  let timeoutId;
16323
18017
  try {
16324
18018
  return await Promise.race([
@@ -16335,17 +18029,35 @@ async function withTimeout(promise, ms, label) {
16335
18029
  }
16336
18030
  }
16337
18031
  function getActionSummary(liveDoc, jobId) {
16338
- const jobsById = asRecord5(asRecord5(liveDoc?.jobs)?.byId) || {};
16339
- const job = asRecord5(jobsById[jobId]);
16340
- return Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
18032
+ const jobsById = asRecord6(asRecord6(liveDoc?.jobs)?.byId) || {};
18033
+ const job = asRecord6(jobsById[jobId]);
18034
+ const summary = Array.isArray(job?.actionSummary) ? job.actionSummary.filter(
16341
18035
  (line) => typeof line === "string"
16342
18036
  ) : [];
18037
+ const trace = Array.isArray(job?.actionTrace) ? job.actionTrace.map((event) => asRecord6(event)).filter((event) => Boolean(event)) : [];
18038
+ const traceLines = trace.map((event) => {
18039
+ const kind = typeof event.kind === "string" ? event.kind : "";
18040
+ const action = typeof event.action === "string" ? event.action : "";
18041
+ const target = typeof event.target === "string" ? event.target : "";
18042
+ if (!action) return "";
18043
+ if (kind === "effect_call") {
18044
+ return `- Called ${target || action}`;
18045
+ }
18046
+ if (kind === "loop_op" && action === "ask_user") {
18047
+ return "- Asked the user for input";
18048
+ }
18049
+ if (kind === "loop_op" && action === "confirm") {
18050
+ return "- Requested confirmation";
18051
+ }
18052
+ return "";
18053
+ }).filter((line) => Boolean(line));
18054
+ return Array.from(/* @__PURE__ */ new Set([...summary, ...traceLines]));
16343
18055
  }
16344
18056
  function normalizeHeapSnapshot2(heap) {
16345
18057
  return {
16346
- entriesByPath: asRecord5(heap?.entriesByPath) || {},
16347
- listsByName: asRecord5(heap?.listsByName) || {},
16348
- variablesByName: asRecord5(heap?.variablesByName) || {},
18058
+ entriesByPath: asRecord6(heap?.entriesByPath) || {},
18059
+ listsByName: asRecord6(heap?.listsByName) || {},
18060
+ variablesByName: asRecord6(heap?.variablesByName) || {},
16349
18061
  updatedAt: typeof heap?.updatedAt === "number" ? heap.updatedAt : Date.now()
16350
18062
  };
16351
18063
  }
@@ -16364,21 +18076,30 @@ ${checkpoint.latestJobResult}` : null
16364
18076
  async function waitForJobOutcome(input) {
16365
18077
  const stdout = [];
16366
18078
  const stderr = [];
18079
+ let lastLiveDoc = null;
18080
+ let lastPromptCount = 0;
18081
+ let lastMessageCount = 0;
18082
+ let lastJobSummary = null;
16367
18083
  input.job.on("stdout", (line) => stdout.push(String(line)));
16368
18084
  input.job.on("stderr", (line) => stderr.push(String(line)));
16369
18085
  const startedAt = Date.now();
16370
18086
  while (Date.now() - startedAt < input.timeoutMs) {
16371
18087
  const liveDoc = cloneJson(input.environment.document);
18088
+ lastLiveDoc = liveDoc;
16372
18089
  const prompts = filterPromptsByBoundary(
16373
18090
  liveDoc,
16374
18091
  getOpenPromptsFromDoc(liveDoc),
16375
18092
  input.boundaryTimestamp
16376
18093
  );
18094
+ lastPromptCount = prompts.length;
18095
+ const messages = asArray3(asRecord6(liveDoc.conversation)?.messages);
18096
+ lastMessageCount = messages.length;
18097
+ lastJobSummary = asRecord6(asRecord6(liveDoc.jobs)?.byId)?.[input.job.id] || null;
16377
18098
  if (prompts.length > 0) {
16378
18099
  return { kind: "prompt", prompts, liveDoc, stdout, stderr };
16379
18100
  }
16380
18101
  try {
16381
- const result = await withTimeout(
18102
+ const result = await withTimeout2(
16382
18103
  input.job.result,
16383
18104
  input.pollIntervalMs,
16384
18105
  `job ${input.job.id} tick`
@@ -16389,37 +18110,70 @@ async function waitForJobOutcome(input) {
16389
18110
  if (!/timed out after/.test(message)) throw error;
16390
18111
  }
16391
18112
  }
16392
- throw new Error(`Job ${input.job.id} timed out after ${input.timeoutMs}ms`);
18113
+ const diagnostics = {
18114
+ elapsedMs: Date.now() - startedAt,
18115
+ promptCount: lastPromptCount,
18116
+ messageCount: lastMessageCount,
18117
+ stdoutTail: stdout.slice(-5),
18118
+ stderrTail: stderr.slice(-5),
18119
+ job: lastJobSummary,
18120
+ hasLiveDoc: Boolean(lastLiveDoc)
18121
+ };
18122
+ throw new Error(
18123
+ `Job ${input.job.id} timed out after ${input.timeoutMs}ms. Diagnostics: ${JSON.stringify(diagnostics)}`
18124
+ );
16393
18125
  }
16394
18126
  async function generateTurnWithRepair(generator, input) {
16395
- let output = await generator(input);
16396
- let request = input.request;
16397
- let attempt = input.attempt;
16398
- for (let repairRound = 0; repairRound < 3; repairRound += 1) {
16399
- if (!output.code) return output;
16400
- const issues = reviewGeneratedJobCode(output.code);
16401
- if (issues.length === 0) return output;
16402
- request = [
16403
- request,
16404
- "",
16405
- "Regenerate the job code and fix these issues:",
16406
- ...issues.map((issue) => `- ${issue.message}`),
16407
- "Return the full corrected job code."
16408
- ].join("\n");
16409
- attempt += 1;
16410
- output = await generator({
16411
- ...input,
16412
- attempt,
16413
- request,
16414
- repairIssues: issues
16415
- });
16416
- }
16417
- return output;
18127
+ const output = await generator(input);
18128
+ const generationAttempts = [
18129
+ {
18130
+ attempt: input.attempt,
18131
+ request: input.request,
18132
+ repairIssues: input.repairIssues,
18133
+ reply: output.reply,
18134
+ code: output.code,
18135
+ raw: output.raw
18136
+ }
18137
+ ];
18138
+ return { ...output, generationAttempts };
16418
18139
  }
16419
18140
  async function writeJson(filePath, value) {
16420
18141
  await writeFile(filePath, `${JSON.stringify(value, null, 2)}
16421
18142
  `);
16422
18143
  }
18144
+ function describeScenarioBehavior(result) {
18145
+ if (result.scenario.description?.trim()) {
18146
+ return result.scenario.description.trim();
18147
+ }
18148
+ const steps = result.steps?.length ? result.steps : buildScenarioSteps(result.scenario).map((step, index) => ({
18149
+ id: step.id || `step-${index + 1}`,
18150
+ request: step.request
18151
+ }));
18152
+ const stepSummary = steps.map((step, index) => {
18153
+ const request = step.request.replace(/\s+/g, " ").trim();
18154
+ return `${index + 1}. ${step.id}: ${request}`;
18155
+ }).join(" ");
18156
+ return [
18157
+ `This report tests scenario \`${result.scenario.id}\` across ${steps.length} user turn${steps.length === 1 ? "" : "s"}.`,
18158
+ "It verifies that the agent grounds the natural-language request in the current ontology/session context, generates the expected job or refusal, and that the runtime executes or blocks the resulting behavior correctly.",
18159
+ stepSummary
18160
+ ].filter(Boolean).join(" ");
18161
+ }
18162
+ function describeJobSource(result) {
18163
+ if (result.scenario.jobSource === "hardcoded") {
18164
+ return "Hardcoded deterministic job code supplied by the test.";
18165
+ }
18166
+ if (result.scenario.jobSource === "mixed") {
18167
+ return "Mixed: LLM-generated agent jobs plus hardcoded setup/inspection jobs supplied by the test.";
18168
+ }
18169
+ const hasGeneratedCode = Boolean(
18170
+ result.finalCode || result.steps?.some((step) => step.finalCode)
18171
+ );
18172
+ if (hasGeneratedCode) {
18173
+ return "LLM-generated agent job code. Setup, direct inspections, and assertions are hardcoded by the test harness.";
18174
+ }
18175
+ return "LLM-generated agent response. Setup, direct inspections, and assertions are hardcoded by the test harness.";
18176
+ }
16423
18177
  function buildResultReport(result) {
16424
18178
  const stepSection = result.steps?.length ? [
16425
18179
  "## Steps",
@@ -16436,6 +18190,15 @@ function buildResultReport(result) {
16436
18190
  const lines = [
16437
18191
  `# Scenario Report: ${result.scenario.id}`,
16438
18192
  "",
18193
+ "## Behavior Under Test",
18194
+ describeScenarioBehavior(result),
18195
+ "",
18196
+ "## Job Source",
18197
+ describeJobSource(result),
18198
+ "",
18199
+ "## Token Usage And Cost",
18200
+ ...formatTokenUsage(result.tokenUsage),
18201
+ "",
16439
18202
  "## Request",
16440
18203
  result.scenario.request || result.steps?.[0]?.request || "_No single request_",
16441
18204
  "",
@@ -16462,9 +18225,21 @@ function buildResultReport(result) {
16462
18225
  `;
16463
18226
  }
16464
18227
  function buildSuiteIndex(results) {
18228
+ const totalUsage = aggregateTokenUsage(
18229
+ results.map((result) => result.tokenUsage)
18230
+ );
16465
18231
  const lines = [
16466
18232
  "# Agent Eval Report Index",
16467
18233
  "",
18234
+ "## Token Usage And Cost",
18235
+ ...formatTokenUsage(totalUsage),
18236
+ "",
18237
+ "## Session Logs",
18238
+ "",
18239
+ "- [Readable chronological logs](./logs/README.md)",
18240
+ "",
18241
+ "## Scenario Reports",
18242
+ "",
16468
18243
  ...results.map(
16469
18244
  (result) => `- [${result.scenario.id}](./${result.scenario.id}/REPORT.md) - ${result.status}`
16470
18245
  )
@@ -16472,6 +18247,192 @@ function buildSuiteIndex(results) {
16472
18247
  return `${lines.join("\n")}
16473
18248
  `;
16474
18249
  }
18250
+ function buildLogsIndex(results) {
18251
+ const totalUsage = aggregateTokenUsage(
18252
+ results.map((result) => result.tokenUsage)
18253
+ );
18254
+ const lines = [
18255
+ "# Agent Eval Session Logs",
18256
+ "",
18257
+ "Each file is a chronological session report with user requests, agent responses, generated code, runtime actions/results, and system prompts at the end.",
18258
+ "",
18259
+ "## Token Usage And Cost",
18260
+ ...formatTokenUsage(totalUsage),
18261
+ "",
18262
+ ...results.map((result) => {
18263
+ const logName = `${slugify(result.scenario.id)}.md`;
18264
+ return `- [${result.scenario.id}](./${logName}) - ${result.status} - ${formatUsd(result.tokenUsage?.totalCostUsd || 0)}`;
18265
+ })
18266
+ ];
18267
+ return `${lines.join("\n")}
18268
+ `;
18269
+ }
18270
+ function fenced(value, language = "") {
18271
+ const fence = value.includes("```") ? "````" : "```";
18272
+ return `${fence}${language}
18273
+ ${value}
18274
+ ${fence}`;
18275
+ }
18276
+ function jsonBlock(value) {
18277
+ return fenced(JSON.stringify(value, null, 2), "json");
18278
+ }
18279
+ function buildSessionLogReport(input) {
18280
+ const { conversation, result, error } = input;
18281
+ const logTurns = conversation.logTurns || [];
18282
+ const systemPrompts = logTurns.flatMap(
18283
+ (turn) => turn.iterations.map((iteration) => ({
18284
+ turn,
18285
+ iteration
18286
+ }))
18287
+ );
18288
+ const lines = [
18289
+ `# Session Log: ${conversation.label}`,
18290
+ "",
18291
+ "## Behavior Under Test",
18292
+ result ? describeScenarioBehavior(result) : "This session log captures the chronological agent/runtime behavior for a scenario that did not complete a structured result.",
18293
+ "",
18294
+ "## Job Source",
18295
+ result ? describeJobSource(result) : "LLM-generated agent jobs when generation completed; setup and harness assertions are hardcoded by the test harness.",
18296
+ "",
18297
+ "## Token Usage And Cost",
18298
+ ...formatTokenUsage(
18299
+ result?.tokenUsage || aggregateConversationTokenUsage(conversation)
18300
+ ),
18301
+ "",
18302
+ "## Metadata",
18303
+ `- Session id: \`${conversation.environment.sessionId}\``,
18304
+ `- Environment id: \`${conversation.environment.environmentId}\``,
18305
+ `- Sandbox id: \`${conversation.environment.sandboxId}\``,
18306
+ `- Status: ${result?.status || (error ? "failed" : "unknown")}`,
18307
+ ...result?.error || error ? [`- Error: ${result?.error || error}`] : [],
18308
+ "",
18309
+ "## Conversation"
18310
+ ];
18311
+ for (const turn of logTurns) {
18312
+ lines.push("", `### Turn ${turn.turnNumber}: ${turn.turnId}`, "");
18313
+ lines.push("**User**", "");
18314
+ lines.push(turn.request, "");
18315
+ for (const iteration of turn.iterations) {
18316
+ lines.push(`#### Agent Generation ${iteration.iteration}`, "");
18317
+ lines.push(
18318
+ "**Token Usage And Cost**",
18319
+ "",
18320
+ ...formatTokenUsage(iteration.tokenUsage),
18321
+ ""
18322
+ );
18323
+ if ((iteration.generationAttempts?.length || 0) > 1) {
18324
+ lines.push("**Generation Attempts**", "");
18325
+ for (const attempt of iteration.generationAttempts || []) {
18326
+ lines.push(`Attempt ${attempt.attempt}`, "");
18327
+ if (attempt.repairIssues?.length) {
18328
+ lines.push("Repair issues:", "");
18329
+ for (const issue of attempt.repairIssues) {
18330
+ lines.push(`- ${issue.code}: ${issue.message}`);
18331
+ }
18332
+ lines.push("");
18333
+ }
18334
+ lines.push("Request", "", fenced(attempt.request, "text"), "");
18335
+ if (attempt.reply?.trim()) {
18336
+ lines.push("Draft reply", "", attempt.reply.trim(), "");
18337
+ }
18338
+ if (attempt.code?.trim()) {
18339
+ lines.push("Code", "", fenced(attempt.code.trim(), "ts"), "");
18340
+ }
18341
+ }
18342
+ }
18343
+ if (iteration.generationReply?.trim()) {
18344
+ lines.push("**Draft Reply**", "", iteration.generationReply.trim(), "");
18345
+ }
18346
+ if (iteration.generatedCode?.trim()) {
18347
+ lines.push(
18348
+ "**Generated Code**",
18349
+ "",
18350
+ fenced(iteration.generatedCode.trim(), "ts"),
18351
+ ""
18352
+ );
18353
+ } else {
18354
+ lines.push("**Generated Code**", "", "_No code generated._", "");
18355
+ }
18356
+ if (iteration.promptInteractions?.length) {
18357
+ lines.push("**Structured User Input**", "");
18358
+ for (const interaction of iteration.promptInteractions) {
18359
+ lines.push(
18360
+ `- ${interaction.type}: ${interaction.message || interaction.title} -> \`${JSON.stringify(interaction.answer)}\``
18361
+ );
18362
+ }
18363
+ lines.push("");
18364
+ }
18365
+ if (iteration.actionSummary?.length) {
18366
+ lines.push("**Runtime Actions**", "");
18367
+ for (const action of iteration.actionSummary) lines.push(`- ${action}`);
18368
+ lines.push("");
18369
+ }
18370
+ if (iteration.responseText?.trim()) {
18371
+ lines.push("**Agent Response**", "", iteration.responseText.trim(), "");
18372
+ }
18373
+ if (iteration.continuation) {
18374
+ lines.push(
18375
+ "**Harness Continuation**",
18376
+ "",
18377
+ jsonBlock(iteration.continuation),
18378
+ ""
18379
+ );
18380
+ }
18381
+ if (iteration.result !== void 0) {
18382
+ lines.push("**Runtime Result**", "", jsonBlock(iteration.result), "");
18383
+ }
18384
+ if (iteration.error) {
18385
+ lines.push("**Error**", "", iteration.error, "");
18386
+ }
18387
+ }
18388
+ if (turn.completed) {
18389
+ lines.push(
18390
+ "**Turn Final Response**",
18391
+ "",
18392
+ turn.completed.responseText || "_No reply_",
18393
+ ""
18394
+ );
18395
+ if (turn.completed.actionSummary.length) {
18396
+ lines.push("**Turn Final Actions**", "");
18397
+ for (const action of turn.completed.actionSummary)
18398
+ lines.push(`- ${action}`);
18399
+ lines.push("");
18400
+ }
18401
+ }
18402
+ if (turn.error) {
18403
+ lines.push("**Turn Error**", "", turn.error, "");
18404
+ }
18405
+ }
18406
+ lines.push("", "## System Prompts", "");
18407
+ if (!systemPrompts.length) {
18408
+ lines.push("_No system prompts captured._", "");
18409
+ } else {
18410
+ for (const { turn, iteration } of systemPrompts) {
18411
+ lines.push(
18412
+ `### Turn ${turn.turnNumber}, Generation ${iteration.iteration}`,
18413
+ "",
18414
+ fenced(iteration.systemPrompt, "text"),
18415
+ ""
18416
+ );
18417
+ }
18418
+ }
18419
+ return `${lines.join("\n")}
18420
+ `;
18421
+ }
18422
+ async function writeSessionLogReport(input) {
18423
+ const logsDir = path.join(input.artifactDir, "logs");
18424
+ await ensureDir(logsDir);
18425
+ await writeFile(
18426
+ path.join(logsDir, `${slugify(input.conversation.label)}.md`),
18427
+ buildSessionLogReport(input)
18428
+ );
18429
+ }
18430
+ function findTurnLog(conversation, turnDir) {
18431
+ return conversation.logTurns.find((turn) => turn.turnDir === turnDir);
18432
+ }
18433
+ function latestIterationLog(turn) {
18434
+ return turn?.iterations[turn.iterations.length - 1];
18435
+ }
16475
18436
  async function applySetup(setup, context) {
16476
18437
  if (!setup) return;
16477
18438
  if (setup.manifest) {
@@ -16545,7 +18506,7 @@ async function runAgentEvalSuite(options) {
16545
18506
  promptInteractions: completed.promptInteractions,
16546
18507
  result: completed.result,
16547
18508
  heap: normalizeHeapSnapshot2(
16548
- asRecord5(
18509
+ asRecord6(
16549
18510
  cloneJson(conversation.environment.document)?.heap
16550
18511
  )
16551
18512
  ),
@@ -16556,7 +18517,7 @@ async function runAgentEvalSuite(options) {
16556
18517
  inspect: async (code) => {
16557
18518
  const session = conversation.environment;
16558
18519
  const job = await session.submitJob(code);
16559
- return withTimeout(
18520
+ return withTimeout2(
16560
18521
  job.result,
16561
18522
  9e4,
16562
18523
  `inspection job ${job.id}`
@@ -16631,6 +18592,7 @@ async function runAgentEvalSuite(options) {
16631
18592
  actionSummary: lastStep.actionSummary,
16632
18593
  promptInteractions: lastStep.promptInteractions,
16633
18594
  verification: lastStep.inspectionResults.length <= 1 ? lastStep.inspectionResults[0] ?? null : lastStep.inspectionResults,
18595
+ tokenUsage: aggregateConversationTokenUsage(conversation),
16634
18596
  steps: stepResults,
16635
18597
  turnDir: conversation.artifactDir
16636
18598
  };
@@ -16646,9 +18608,22 @@ async function runAgentEvalSuite(options) {
16646
18608
  path.join(conversation.artifactDir, "REPORT.md"),
16647
18609
  buildResultReport(result)
16648
18610
  );
18611
+ await writeSessionLogReport({
18612
+ artifactDir: options.harness.artifactDir,
18613
+ conversation,
18614
+ result
18615
+ });
16649
18616
  finalResult = result;
16650
18617
  } catch (error) {
16651
18618
  const failureMessage = error instanceof Error ? error.message : String(error);
18619
+ const failedTurn = conversation.logTurns?.[conversation.logTurns.length - 1];
18620
+ if (failedTurn && !failedTurn.completed) {
18621
+ failedTurn.error = failureMessage;
18622
+ const failedIteration = latestIterationLog(failedTurn);
18623
+ if (failedIteration && !failedIteration.responseText) {
18624
+ failedIteration.error = failureMessage;
18625
+ }
18626
+ }
16652
18627
  if (attempt < 2 && isTransientEvalError(error)) {
16653
18628
  await options.harness.closeConversation(conversation);
16654
18629
  continue;
@@ -16661,6 +18636,7 @@ async function runAgentEvalSuite(options) {
16661
18636
  actionSummary: [],
16662
18637
  promptInteractions: [],
16663
18638
  verification: null,
18639
+ tokenUsage: aggregateConversationTokenUsage(conversation),
16664
18640
  turnDir: path.join(options.harness.artifactDir, scenario.id),
16665
18641
  error: failureMessage
16666
18642
  };
@@ -16671,6 +18647,12 @@ async function runAgentEvalSuite(options) {
16671
18647
  path.join(failed.turnDir, "REPORT.md"),
16672
18648
  buildResultReport(failed)
16673
18649
  );
18650
+ await writeSessionLogReport({
18651
+ artifactDir: options.harness.artifactDir,
18652
+ conversation,
18653
+ result: failed,
18654
+ error: failureMessage
18655
+ });
16674
18656
  finalResult = failed;
16675
18657
  } finally {
16676
18658
  await options.harness.closeConversation(conversation);
@@ -16685,6 +18667,7 @@ async function runAgentEvalSuite(options) {
16685
18667
  actionSummary: [],
16686
18668
  promptInteractions: [],
16687
18669
  verification: null,
18670
+ tokenUsage: emptyTokenUsage(),
16688
18671
  turnDir: path.join(options.harness.artifactDir, scenario.id),
16689
18672
  error: "Scenario ended without a result."
16690
18673
  };
@@ -16699,6 +18682,11 @@ async function runAgentEvalSuite(options) {
16699
18682
  path.join(options.harness.artifactDir, "REPORT_INDEX.md"),
16700
18683
  buildSuiteIndex(results)
16701
18684
  );
18685
+ await ensureDir(path.join(options.harness.artifactDir, "logs"));
18686
+ await writeFile(
18687
+ path.join(options.harness.artifactDir, "logs", "README.md"),
18688
+ buildLogsIndex(results)
18689
+ );
16702
18690
  return { artifactDir: options.harness.artifactDir, results };
16703
18691
  }
16704
18692
  function createAgentEvalHarness(options) {
@@ -16731,6 +18719,10 @@ function createAgentEvalHarness(options) {
16731
18719
  promptEvents.push({ prompt, receivedAt: Date.now() });
16732
18720
  };
16733
18721
  environment.on("prompt", promptHandler);
18722
+ for (let attempt = 0; attempt < 12; attempt += 1) {
18723
+ if (environment.getEffects().length > 0) break;
18724
+ await sleep2(250);
18725
+ }
16734
18726
  await ensureDir(path.join(artifactDir, slugify(label)));
16735
18727
  return {
16736
18728
  label,
@@ -16738,7 +18730,8 @@ function createAgentEvalHarness(options) {
16738
18730
  history: [],
16739
18731
  promptEvents,
16740
18732
  artifactDir: path.join(artifactDir, slugify(label)),
16741
- turnCount: 0
18733
+ turnCount: 0,
18734
+ logTurns: []
16742
18735
  };
16743
18736
  }
16744
18737
  async function closeConversation(conversation) {
@@ -16752,11 +18745,11 @@ function createAgentEvalHarness(options) {
16752
18745
  }
16753
18746
  async function runCheckJob(code, session) {
16754
18747
  const job = await session.submitJob(code);
16755
- return withTimeout(job.result, jobTimeoutMs, `check job ${job.id}`);
18748
+ return withTimeout2(job.result, jobTimeoutMs, `check job ${job.id}`);
16756
18749
  }
16757
18750
  function buildCheckContext(conversation, completed, turnDir) {
16758
18751
  const liveDoc = cloneJson(conversation.environment.document);
16759
- const heap = normalizeHeapSnapshot2(asRecord5(liveDoc?.heap));
18752
+ const heap = normalizeHeapSnapshot2(asRecord6(liveDoc?.heap));
16760
18753
  return {
16761
18754
  conversation,
16762
18755
  environment: conversation.environment,
@@ -16776,14 +18769,26 @@ function createAgentEvalHarness(options) {
16776
18769
  };
16777
18770
  }
16778
18771
  async function runInspection(conversation, inspection, completed, turnDir) {
16779
- const result = await runCheckJob(inspection.code, conversation.environment);
16780
- const text = JSON.stringify(result, null, 2);
16781
- assertMatches(
16782
- `Verification for ${conversation.label}`,
16783
- text,
16784
- inspection.includes,
16785
- inspection.excludes
16786
- );
18772
+ let result = null;
18773
+ let lastError = null;
18774
+ for (let attempt = 0; attempt < 10; attempt += 1) {
18775
+ result = await runCheckJob(inspection.code, conversation.environment);
18776
+ const text = JSON.stringify(result, null, 2);
18777
+ try {
18778
+ assertMatches(
18779
+ `Verification for ${conversation.label}`,
18780
+ text,
18781
+ inspection.includes,
18782
+ inspection.excludes
18783
+ );
18784
+ lastError = null;
18785
+ break;
18786
+ } catch (error) {
18787
+ lastError = error;
18788
+ await sleep2(250);
18789
+ }
18790
+ }
18791
+ if (lastError) throw lastError;
16787
18792
  if (inspection.check) {
16788
18793
  await inspection.check({
16789
18794
  ...buildCheckContext(conversation, completed, turnDir),
@@ -16809,6 +18814,11 @@ function createAgentEvalHarness(options) {
16809
18814
  message: prompt.message,
16810
18815
  answer
16811
18816
  });
18817
+ const turnLog = findTurnLog(pending.conversation, pending.turnDir);
18818
+ const iterationLog = latestIterationLog(turnLog);
18819
+ if (iterationLog) {
18820
+ iterationLog.promptInteractions = pending.promptInteractions;
18821
+ }
16812
18822
  const resumed = await waitForJobOutcome({
16813
18823
  environment: pending.conversation.environment,
16814
18824
  job: pending.job,
@@ -16832,7 +18842,8 @@ function createAgentEvalHarness(options) {
16832
18842
  jobId: pending.job.id,
16833
18843
  result: resumed.result,
16834
18844
  stdout: [...pending.stdout, ...resumed.stdout],
16835
- sessionHeap: normalizeHeapSnapshot2(asRecord5(liveDoc?.heap))
18845
+ agentMessages: getJobAgentMessages(liveDoc, pending.job.id),
18846
+ sessionHeap: normalizeHeapSnapshot2(asRecord6(liveDoc?.heap))
16836
18847
  });
16837
18848
  const responseText = presentation.responseText || pending.finalReply || "Done.";
16838
18849
  pending.conversation.history.push({
@@ -16849,6 +18860,23 @@ function createAgentEvalHarness(options) {
16849
18860
  promptInteractions: pending.promptInteractions,
16850
18861
  result: resumed.result
16851
18862
  });
18863
+ const actionSummary = getActionSummary(liveDoc, pending.job.id);
18864
+ if (iterationLog) {
18865
+ iterationLog.responseText = responseText;
18866
+ iterationLog.terminalKind = getCurrentClosureId(liveDoc) ? "closure" : "reply";
18867
+ iterationLog.actionSummary = actionSummary;
18868
+ iterationLog.promptInteractions = pending.promptInteractions;
18869
+ iterationLog.result = resumed.result;
18870
+ }
18871
+ if (turnLog) {
18872
+ turnLog.completed = {
18873
+ responseText,
18874
+ terminalKind: getCurrentClosureId(liveDoc) ? "closure" : "reply",
18875
+ actionSummary,
18876
+ promptInteractions: pending.promptInteractions,
18877
+ result: resumed.result
18878
+ };
18879
+ }
16852
18880
  return {
16853
18881
  conversation: pending.conversation,
16854
18882
  request: pending.request,
@@ -16856,7 +18884,7 @@ function createAgentEvalHarness(options) {
16856
18884
  responseText,
16857
18885
  terminalKind: getCurrentClosureId(liveDoc) ? "closure" : "reply",
16858
18886
  finalCode: pending.finalCode,
16859
- actionSummary: getActionSummary(liveDoc, pending.job.id),
18887
+ actionSummary,
16860
18888
  promptInteractions: pending.promptInteractions,
16861
18889
  verification: null,
16862
18890
  result: resumed.result
@@ -16868,6 +18896,14 @@ function createAgentEvalHarness(options) {
16868
18896
  const turnId = `turn-${String(turnNumber).padStart(2, "0")}-${slugify(input.request.slice(0, 48))}`;
16869
18897
  const turnDir = path.join(conversation.artifactDir, turnId);
16870
18898
  await ensureDir(turnDir);
18899
+ const turnLog = {
18900
+ turnNumber,
18901
+ turnId,
18902
+ request: input.request,
18903
+ turnDir,
18904
+ iterations: []
18905
+ };
18906
+ conversation.logTurns.push(turnLog);
16871
18907
  if (input.prepareRecords?.length) {
16872
18908
  await conversation.environment.recordObjects(input.prepareRecords);
16873
18909
  }
@@ -16906,6 +18942,24 @@ function createAgentEvalHarness(options) {
16906
18942
  const workflowFocus = projectWorkflowFocus(liveDoc, pendingPrompts, {
16907
18943
  boundaryTimestamp
16908
18944
  });
18945
+ const referentFocus = projectConversationReferentFocus(liveDoc);
18946
+ const heapFocus = {
18947
+ variableNames: [
18948
+ ...workflowFocus.variableNames,
18949
+ ...referentFocus.variableNames
18950
+ ],
18951
+ listNames: [...workflowFocus.listNames, ...referentFocus.listNames],
18952
+ entryPaths: [...workflowFocus.entryPaths, ...referentFocus.entryPaths]
18953
+ };
18954
+ const tools = conversation.environment.getEffects().map((tool) => ({
18955
+ name: tool.name,
18956
+ description: tool.description,
18957
+ className: tool.className,
18958
+ static: tool.static,
18959
+ ready: tool.ready,
18960
+ inputSchema: tool.inputSchema,
18961
+ outputSchema: tool.outputSchema
18962
+ }));
16909
18963
  const systemPrompt = buildGranularAgentSystemPrompt({
16910
18964
  domainDocumentation: await conversation.environment.getDomainDocumentation(),
16911
18965
  sessionContext: {
@@ -16913,37 +18967,51 @@ function createAgentEvalHarness(options) {
16913
18967
  environmentId: conversation.environment.environmentId,
16914
18968
  domainRevision: conversation.environment.domainRevision
16915
18969
  },
16916
- heapSummary: projectHeapSummary(liveDoc, {
16917
- focus: workflowFocus
18970
+ heapSummary: projectHeapSummary(asRecord6(liveDoc?.heap), {
18971
+ focus: heapFocus
16918
18972
  }),
18973
+ referentSummary: projectConversationReferentSummary(liveDoc),
16919
18974
  loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
16920
18975
  boundaryTimestamp
16921
18976
  }),
16922
18977
  workflowSummary: projectWorkflowSummary(liveDoc, pendingPrompts, {
16923
18978
  boundaryTimestamp
16924
18979
  }),
16925
- tools: conversation.environment.getEffects().map((tool) => ({
16926
- name: tool.name,
16927
- description: tool.description,
16928
- className: tool.className,
16929
- static: tool.static,
16930
- ready: tool.ready
16931
- })),
18980
+ tools,
16932
18981
  checkpoint: latestCheckpoint
16933
18982
  });
16934
18983
  const request = iteration === 0 ? input.request : buildContinuationInstruction(
16935
18984
  buildContinuationPreview(latestCheckpoint, noProgressCount)
16936
18985
  );
16937
- const generation = await withTimeout(
18986
+ const generation = await withTimeout2(
16938
18987
  generateTurnWithRepair(options.generator, {
16939
18988
  systemPrompt,
16940
18989
  history: buildHistory(conversation.history),
16941
18990
  request,
16942
- attempt: 1
18991
+ attempt: 1,
18992
+ tools,
18993
+ usageContext: {
18994
+ sandboxId: conversation.environment.sandboxId,
18995
+ environmentId: conversation.environment.environmentId,
18996
+ sessionId: conversation.environment.sessionId,
18997
+ subjectId: conversation.environment.subjectId,
18998
+ permissionProfileId: conversation.environment.permissionProfileId
18999
+ }
16943
19000
  }),
16944
19001
  chatTimeoutMs,
16945
19002
  `chat generation for ${conversation.label} iteration ${iteration + 1}`
16946
19003
  );
19004
+ const iterationLog = {
19005
+ iteration: iteration + 1,
19006
+ request,
19007
+ systemPrompt,
19008
+ generationReply: generation.reply,
19009
+ generatedCode: generation.code,
19010
+ rawGeneration: generation.raw,
19011
+ generationAttempts: generation.generationAttempts,
19012
+ tokenUsage: tokenUsageForGenerationOutput(generation)
19013
+ };
19014
+ turnLog.iterations.push(iterationLog);
16947
19015
  await writeJson(
16948
19016
  path.join(turnDir, `iteration-${iteration + 1}-generation.json`),
16949
19017
  generation
@@ -16970,11 +19038,39 @@ function createAgentEvalHarness(options) {
16970
19038
  turnDir
16971
19039
  );
16972
19040
  }
19041
+ iterationLog.responseText = responseText2;
19042
+ iterationLog.terminalKind = "reply";
19043
+ iterationLog.actionSummary = [];
19044
+ iterationLog.promptInteractions = [];
19045
+ iterationLog.result = completed.result;
19046
+ turnLog.completed = {
19047
+ responseText: responseText2,
19048
+ terminalKind: "reply",
19049
+ actionSummary: [],
19050
+ promptInteractions: [],
19051
+ result: completed.result
19052
+ };
16973
19053
  await writeJson(path.join(turnDir, "result.json"), completed);
16974
19054
  return completed;
16975
19055
  }
16976
19056
  const session = conversation.environment;
16977
- const job = await session.submitJob(generation.code);
19057
+ const job = await session.submitJob(generation.code, {
19058
+ agent: {
19059
+ userRequest: input.request,
19060
+ generationRequest: request,
19061
+ systemPrompt,
19062
+ history: buildHistory(conversation.history),
19063
+ scenarioLabel: conversation.label,
19064
+ turnId,
19065
+ iteration: iteration + 1,
19066
+ tools,
19067
+ generationReply: generation.reply,
19068
+ rawGeneration: generation.raw,
19069
+ repairIssues: generation.generationAttempts?.flatMap(
19070
+ (attempt) => attempt.repairIssues || []
19071
+ )
19072
+ }
19073
+ });
16978
19074
  const outcome = await waitForJobOutcome({
16979
19075
  environment: conversation.environment,
16980
19076
  job,
@@ -17030,6 +19126,13 @@ function createAgentEvalHarness(options) {
17030
19126
  turnDir
17031
19127
  );
17032
19128
  }
19129
+ turnLog.completed = {
19130
+ responseText: resumed.responseText,
19131
+ terminalKind: resumed.terminalKind,
19132
+ actionSummary: resumed.actionSummary,
19133
+ promptInteractions: resumed.promptInteractions,
19134
+ result: resumed.result
19135
+ };
17033
19136
  return resumed;
17034
19137
  }
17035
19138
  }
@@ -17042,11 +19145,12 @@ function createAgentEvalHarness(options) {
17042
19145
  const settledLiveDoc = cloneJson(
17043
19146
  conversation.environment.document
17044
19147
  );
17045
- const sessionHeap = normalizeHeapSnapshot2(asRecord5(settledLiveDoc?.heap));
19148
+ const sessionHeap = normalizeHeapSnapshot2(asRecord6(settledLiveDoc?.heap));
17046
19149
  const presentation = resolveJobPresentation({
17047
19150
  jobId: job.id,
17048
19151
  result: outcome.result,
17049
19152
  stdout: outcome.stdout,
19153
+ agentMessages: getJobAgentMessages(settledLiveDoc, job.id),
17050
19154
  sessionHeap
17051
19155
  });
17052
19156
  const responseText = presentation.responseText || generation.reply?.trim() || "Done.";
@@ -17100,6 +19204,12 @@ function createAgentEvalHarness(options) {
17100
19204
  result: outcome.result
17101
19205
  }
17102
19206
  );
19207
+ iterationLog.responseText = responseText;
19208
+ iterationLog.terminalKind = getCurrentClosureId(settledLiveDoc) ? "closure" : "reply";
19209
+ iterationLog.actionSummary = latestCheckpoint.latestActionSummary || [];
19210
+ iterationLog.promptInteractions = [];
19211
+ iterationLog.continuation = continuation;
19212
+ iterationLog.result = outcome.result;
17103
19213
  if (!continuation.shouldContinue) {
17104
19214
  const completed = {
17105
19215
  conversation,
@@ -17121,6 +19231,13 @@ function createAgentEvalHarness(options) {
17121
19231
  turnDir
17122
19232
  );
17123
19233
  }
19234
+ turnLog.completed = {
19235
+ responseText,
19236
+ terminalKind: completed.terminalKind,
19237
+ actionSummary: completed.actionSummary,
19238
+ promptInteractions: [],
19239
+ result: outcome.result
19240
+ };
17124
19241
  await writeJson(path.join(turnDir, "result.json"), completed);
17125
19242
  return completed;
17126
19243
  }
@@ -17152,7 +19269,10 @@ function createAgentTester(options) {
17152
19269
  model: options.openai?.model || options.model,
17153
19270
  baseUrl: options.openai?.baseUrl,
17154
19271
  temperature: options.openai?.temperature,
17155
- headers: options.openai?.headers
19272
+ headers: options.openai?.headers,
19273
+ onUsage: async (usage) => {
19274
+ await granular.recordOpenAIUsageSpend(usage, usage.usageContext);
19275
+ }
17156
19276
  });
17157
19277
  let resolvedEnvironmentId = "environmentId" in options.target ? options.target.environmentId : null;
17158
19278
  let connectSeeded = false;