@granular-software/sdk 0.4.33 → 0.4.35

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.
@@ -10866,6 +10866,50 @@ function computeEffectKey(effect) {
10866
10866
  }
10867
10867
  return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
10868
10868
  }
10869
+ function computeEffectVersionSelectorSpecificity(selector) {
10870
+ if (!selector || selector.mode === "all") {
10871
+ return 0;
10872
+ }
10873
+ if (selector.mode === "exact") {
10874
+ return 2;
10875
+ }
10876
+ return 1;
10877
+ }
10878
+ function matchesEffectVersionSelector(selector, buildVersionNumber) {
10879
+ if (!selector || selector.mode === "all") {
10880
+ return true;
10881
+ }
10882
+ if (typeof buildVersionNumber !== "number" || !Number.isFinite(buildVersionNumber)) {
10883
+ return false;
10884
+ }
10885
+ if (selector.mode === "exact") {
10886
+ return buildVersionNumber === selector.versionNumber;
10887
+ }
10888
+ if (selector.mode === "before") {
10889
+ return buildVersionNumber < selector.versionNumber;
10890
+ }
10891
+ return buildVersionNumber > selector.versionNumber;
10892
+ }
10893
+ function selectRegisteredEffect(effectMap, effectKey, buildVersionNumber) {
10894
+ let bestEffect;
10895
+ let bestSpecificity = Number.NEGATIVE_INFINITY;
10896
+ for (const effect of effectMap.values()) {
10897
+ if (computeEffectKey(effect) !== effectKey) {
10898
+ continue;
10899
+ }
10900
+ if (!matchesEffectVersionSelector(effect.versionSelector, buildVersionNumber)) {
10901
+ continue;
10902
+ }
10903
+ const specificity = computeEffectVersionSelectorSpecificity(
10904
+ effect.versionSelector
10905
+ );
10906
+ if (!bestEffect || specificity > bestSpecificity) {
10907
+ bestEffect = effect;
10908
+ bestSpecificity = specificity;
10909
+ }
10910
+ }
10911
+ return bestEffect;
10912
+ }
10869
10913
  function normalizeEffectBehaviors(value) {
10870
10914
  return normalizeEffectBehaviorSummary(
10871
10915
  value
@@ -10886,9 +10930,17 @@ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
10886
10930
  return void 0;
10887
10931
  }
10888
10932
  if (reverseHandler.includes(":")) {
10889
- return effectMap.get(reverseHandler);
10933
+ return selectRegisteredEffect(
10934
+ effectMap,
10935
+ reverseHandler,
10936
+ request.context?.buildVersionNumber
10937
+ );
10890
10938
  }
10891
- const directMatch = effectMap.get(reverseHandler);
10939
+ const directMatch = selectRegisteredEffect(
10940
+ effectMap,
10941
+ reverseHandler,
10942
+ request.context?.buildVersionNumber
10943
+ );
10892
10944
  if (directMatch) {
10893
10945
  return directMatch;
10894
10946
  }
@@ -10903,7 +10955,11 @@ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
10903
10955
  })
10904
10956
  ];
10905
10957
  for (const candidateKey of candidateKeys) {
10906
- const candidate = effectMap.get(candidateKey);
10958
+ const candidate = selectRegisteredEffect(
10959
+ effectMap,
10960
+ candidateKey,
10961
+ request.context?.buildVersionNumber
10962
+ );
10907
10963
  if (candidate) {
10908
10964
  return candidate;
10909
10965
  }
@@ -10939,7 +10995,11 @@ function resolveHandlerForMode(effectMap, effect, request) {
10939
10995
  return { effect, mode, handler: effect.handler };
10940
10996
  }
10941
10997
  async function invokeRegisteredEffect(effectMap, request) {
10942
- const effect = effectMap.get(request.effectKey);
10998
+ const effect = selectRegisteredEffect(
10999
+ effectMap,
11000
+ request.effectKey,
11001
+ request.context?.buildVersionNumber
11002
+ );
10943
11003
  if (!effect) {
10944
11004
  throw new Error(`Effect handler not found: ${request.effectKey}`);
10945
11005
  }
@@ -12198,6 +12258,17 @@ function computeEffectKey2(effect) {
12198
12258
  }
12199
12259
  return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
12200
12260
  }
12261
+ function computeEffectVersionSelectorKey(selector) {
12262
+ if (!selector || selector.mode === "all") {
12263
+ return "all";
12264
+ }
12265
+ return `${selector.mode}:${selector.versionNumber}`;
12266
+ }
12267
+ function computeEffectRegistrationKey(effect) {
12268
+ return `${computeEffectKey2(effect)}@${computeEffectVersionSelectorKey(
12269
+ effect.versionSelector
12270
+ )}`;
12271
+ }
12201
12272
  function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
12202
12273
  const url = new URL(apiUrl);
12203
12274
  if (url.pathname.endsWith("/granular/ws/connect")) {
@@ -12281,6 +12352,38 @@ function normalizeUser(user) {
12281
12352
  permissions: Array.isArray(user.permissions) ? user.permissions : []
12282
12353
  };
12283
12354
  }
12355
+ function normalizeEnvironmentSetupSummary(setup) {
12356
+ if (!setup) {
12357
+ return null;
12358
+ }
12359
+ const queuedRecords = Number(setup.queuedRecords || 0);
12360
+ const processingRecords = Number(setup.processingRecords || 0);
12361
+ return {
12362
+ ...setup,
12363
+ setupRunId: String(setup.setupRunId || ""),
12364
+ environmentId: String(setup.environmentId || ""),
12365
+ sandboxId: String(setup.sandboxId || ""),
12366
+ subjectId: String(setup.subjectId || ""),
12367
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
12368
+ lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
12369
+ stage: typeof setup.stage === "string" ? setup.stage : null,
12370
+ totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
12371
+ totalImports: Number(setup.totalImports || 0),
12372
+ activeImports: Number(setup.activeImports || 0),
12373
+ totalRecords: Number(setup.totalRecords || 0),
12374
+ queuedRecords,
12375
+ processingRecords,
12376
+ completedRecords: Number(setup.completedRecords || 0),
12377
+ failedRecords: Number(setup.failedRecords || 0),
12378
+ canceledRecords: Number(setup.canceledRecords || 0),
12379
+ awaitingRecords: Number(setup.awaitingRecords || 0) || queuedRecords + processingRecords,
12380
+ errorMessage: typeof setup.errorMessage === "string" ? setup.errorMessage : null,
12381
+ startedAt: Number(setup.startedAt || Date.now()),
12382
+ hookCompletedAt: setup.hookCompletedAt == null ? null : Number(setup.hookCompletedAt),
12383
+ finishedAt: setup.finishedAt == null ? null : Number(setup.finishedAt),
12384
+ updatedAt: Number(setup.updatedAt || Date.now())
12385
+ };
12386
+ }
12284
12387
  function normalizeEnvironmentData(environment) {
12285
12388
  const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
12286
12389
  mode: "pinned",
@@ -12294,7 +12397,8 @@ function normalizeEnvironmentData(environment) {
12294
12397
  envName: environmentName,
12295
12398
  environment: environmentName,
12296
12399
  buildPolicy,
12297
- tracking: environment.tracking || buildPolicy
12400
+ tracking: environment.tracking || buildPolicy,
12401
+ setup: normalizeEnvironmentSetupSummary(environment.setup)
12298
12402
  };
12299
12403
  }
12300
12404
  var Environment = class {
@@ -12352,6 +12456,10 @@ var Environment = class {
12352
12456
  get updateState() {
12353
12457
  return this.envData.updateState;
12354
12458
  }
12459
+ /** The latest setup/import run summary for this environment, when available. */
12460
+ get setup() {
12461
+ return this.envData.setup || null;
12462
+ }
12355
12463
  /** Convenience flag for whether this environment trails the current tag target */
12356
12464
  get isOutdated() {
12357
12465
  return this.envData.updateState === "update_available";
@@ -12372,6 +12480,9 @@ var Environment = class {
12372
12480
  get runtimeBaseUrl() {
12373
12481
  return this.getRuntimeBaseUrl();
12374
12482
  }
12483
+ syncEnvironmentData(envData) {
12484
+ this.envData = normalizeEnvironmentData(envData);
12485
+ }
12375
12486
  get sessions() {
12376
12487
  return {
12377
12488
  list: async (options) => this.listSessions(options?.status || "active"),
@@ -13317,7 +13428,8 @@ var Environment = class {
13317
13428
  method: "POST",
13318
13429
  body: JSON.stringify({
13319
13430
  records,
13320
- batchSize: options.batchSize
13431
+ batchSize: options.batchSize,
13432
+ setupRunId: options.setupRunId
13321
13433
  })
13322
13434
  }
13323
13435
  );
@@ -13465,18 +13577,12 @@ var EnvironmentSession = class extends Session {
13465
13577
  }
13466
13578
  get messages() {
13467
13579
  return {
13468
- list: (options = {}) => this.sessionDataRequest(
13469
- "/messages",
13470
- options
13471
- )
13580
+ list: (options = {}) => this.sessionDataRequest("/messages", options)
13472
13581
  };
13473
13582
  }
13474
13583
  get timeline() {
13475
13584
  return {
13476
- list: (options = {}) => this.sessionDataRequest(
13477
- "/timeline",
13478
- options
13479
- )
13585
+ list: (options = {}) => this.sessionDataRequest("/timeline", options)
13480
13586
  };
13481
13587
  }
13482
13588
  get jobs() {
@@ -13493,10 +13599,7 @@ var EnvironmentSession = class extends Session {
13493
13599
  get heap() {
13494
13600
  return {
13495
13601
  entries: {
13496
- list: (options = {}) => this.sessionDataRequest(
13497
- "/heap/entries",
13498
- options
13499
- ),
13602
+ list: (options = {}) => this.sessionDataRequest("/heap/entries", options),
13500
13603
  get: (path2) => this.sessionDataRequest(
13501
13604
  `/heap/entries/${encodeURIComponent(path2)}`
13502
13605
  )
@@ -13540,10 +13643,7 @@ var EnvironmentSession = class extends Session {
13540
13643
  const heap = normalizeHeapSnapshot({
13541
13644
  entriesByPath: Object.fromEntries(
13542
13645
  entries.map((entry) => {
13543
- return entry?.path ? [
13544
- entry.path,
13545
- entry
13546
- ] : null;
13646
+ return entry?.path ? [entry.path, entry] : null;
13547
13647
  }).filter(
13548
13648
  (entry) => Boolean(entry)
13549
13649
  )
@@ -13709,6 +13809,15 @@ var OntologyHandle = class {
13709
13809
  disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
13710
13810
  };
13711
13811
  }
13812
+ get importer() {
13813
+ return {
13814
+ onEnvironmentCreate: (handler) => this.granular.registerEnvironmentImporter(
13815
+ this.ontologyNameOrId,
13816
+ handler
13817
+ ),
13818
+ clear: () => this.granular.clearEnvironmentImporter(this.ontologyNameOrId)
13819
+ };
13820
+ }
13712
13821
  };
13713
13822
  var Granular = class _Granular {
13714
13823
  apiKey;
@@ -13719,12 +13828,16 @@ var Granular = class _Granular {
13719
13828
  onUnexpectedClose;
13720
13829
  onReconnectError;
13721
13830
  debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
13722
- /** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
13831
+ /** Sandbox-level effect registry: sandboxId → (effectKey@selector → ToolWithHandler) */
13723
13832
  sandboxEffects = /* @__PURE__ */ new Map();
13724
13833
  /** Live sandbox-scoped effect hosts keyed by sandboxId */
13725
13834
  sandboxEffectHosts = /* @__PURE__ */ new Map();
13726
13835
  /** In-flight host connection promises to avoid duplicate concurrent connects */
13727
13836
  sandboxEffectHostPromises = /* @__PURE__ */ new Map();
13837
+ /** Ontology-bound environment importer hooks keyed by the caller's ontology identifier. */
13838
+ ontologyImporters = /* @__PURE__ */ new Map();
13839
+ /** Resolved importer hooks keyed by sandboxId for fast lookups during openEnvironment(). */
13840
+ sandboxImporters = /* @__PURE__ */ new Map();
13728
13841
  /**
13729
13842
  * Create a new Granular client
13730
13843
  * @param options - Client configuration
@@ -13750,6 +13863,18 @@ var Granular = class _Granular {
13750
13863
  ontology(ontologyNameOrId) {
13751
13864
  return new OntologyHandle(this, ontologyNameOrId);
13752
13865
  }
13866
+ registerEnvironmentImporter(ontologyNameOrId, handler) {
13867
+ this.ontologyImporters.set(ontologyNameOrId, handler);
13868
+ if (ontologyNameOrId.startsWith("sbx_")) {
13869
+ this.sandboxImporters.set(ontologyNameOrId, handler);
13870
+ }
13871
+ }
13872
+ clearEnvironmentImporter(ontologyNameOrId) {
13873
+ this.ontologyImporters.delete(ontologyNameOrId);
13874
+ if (ontologyNameOrId.startsWith("sbx_")) {
13875
+ this.sandboxImporters.delete(ontologyNameOrId);
13876
+ }
13877
+ }
13753
13878
  /**
13754
13879
  * Records/upserts a user and prepares them for sandbox connections
13755
13880
  *
@@ -13866,11 +13991,13 @@ var Granular = class _Granular {
13866
13991
  * ```
13867
13992
  */
13868
13993
  async openEnvironment(options) {
13869
- const envData = await this.resolveOpenEnvironmentData(
13994
+ const resolved = await this.resolveOpenEnvironmentData(
13870
13995
  options,
13871
13996
  "openEnvironment"
13872
13997
  );
13873
- return this.bindEnvironmentHandle(envData);
13998
+ const environment = this.bindEnvironmentHandle(resolved.environment);
13999
+ await this.maybeRunEnvironmentImporter(resolved, environment);
14000
+ return environment;
13874
14001
  }
13875
14002
  /**
13876
14003
  * Deprecated compatibility alias for `openEnvironment()`.
@@ -13957,7 +14084,12 @@ var Granular = class _Granular {
13957
14084
  )
13958
14085
  );
13959
14086
  if (currentMatches.length > 0) {
13960
- return currentMatches[0];
14087
+ return {
14088
+ environment: currentMatches[0],
14089
+ requestedOntology: ontology,
14090
+ sandboxId: sandbox.sandboxId,
14091
+ subjectId: user.granularId
14092
+ };
13961
14093
  }
13962
14094
  const outdatedMatches = this.sortEnvironmentsByRecency(
13963
14095
  userEnvironments.filter(
@@ -13965,14 +14097,25 @@ var Granular = class _Granular {
13965
14097
  )
13966
14098
  );
13967
14099
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13968
- return outdatedMatches[0];
14100
+ return {
14101
+ environment: outdatedMatches[0],
14102
+ requestedOntology: ontology,
14103
+ sandboxId: sandbox.sandboxId,
14104
+ subjectId: user.granularId
14105
+ };
13969
14106
  }
13970
- return this.environments.create(sandbox.sandboxId, {
14107
+ return {
14108
+ environment: await this.environments.create(sandbox.sandboxId, {
14109
+ subjectId: user.granularId,
14110
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
14111
+ tagId: tag.tagId,
14112
+ permissionProfileId: null
14113
+ }),
14114
+ requestedOntology: ontology,
14115
+ sandboxId: sandbox.sandboxId,
13971
14116
  subjectId: user.granularId,
13972
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13973
- tagId: tag.tagId,
13974
- permissionProfileId: null
13975
- });
14117
+ setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
14118
+ };
13976
14119
  }
13977
14120
  /**
13978
14121
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -14089,6 +14232,80 @@ var Granular = class _Granular {
14089
14232
  });
14090
14233
  return this.connectSession({ sessionId, clientId: options?.clientId });
14091
14234
  }
14235
+ resolveEnvironmentImporter(requestedOntology, sandboxId) {
14236
+ const resolved = this.sandboxImporters.get(sandboxId) || this.ontologyImporters.get(requestedOntology);
14237
+ if (resolved && !this.sandboxImporters.has(sandboxId) && this.ontologyImporters.get(requestedOntology) === resolved) {
14238
+ this.sandboxImporters.set(sandboxId, resolved);
14239
+ }
14240
+ return resolved;
14241
+ }
14242
+ async maybeRunEnvironmentImporter(resolved, environment) {
14243
+ if (!resolved.setupTriggerReason) {
14244
+ return;
14245
+ }
14246
+ const importer = this.resolveEnvironmentImporter(
14247
+ resolved.requestedOntology,
14248
+ resolved.sandboxId
14249
+ );
14250
+ if (!importer) {
14251
+ return;
14252
+ }
14253
+ const setupRun = await this.request(
14254
+ `/control/environments/${environment.environmentId}/setup-runs`,
14255
+ {
14256
+ method: "POST",
14257
+ body: JSON.stringify({
14258
+ triggerReason: resolved.setupTriggerReason
14259
+ })
14260
+ }
14261
+ );
14262
+ const setupRunId = setupRun.setupRunId;
14263
+ const updateSetupRun = async (patch) => {
14264
+ await this.request(
14265
+ `/control/environment-setup-runs/${setupRunId}`,
14266
+ {
14267
+ method: "PATCH",
14268
+ body: JSON.stringify(patch)
14269
+ }
14270
+ );
14271
+ };
14272
+ const importerContext = {
14273
+ environmentId: environment.environmentId,
14274
+ sandboxId: environment.sandboxId,
14275
+ subjectId: environment.subjectId,
14276
+ reason: resolved.setupTriggerReason,
14277
+ incrementTotalObjectsToImportCount: async (n) => {
14278
+ const safeIncrement = Math.max(0, Math.trunc(n));
14279
+ if (safeIncrement <= 0) {
14280
+ return;
14281
+ }
14282
+ await updateSetupRun({
14283
+ incrementTotalObjectsToImportCount: safeIncrement
14284
+ });
14285
+ },
14286
+ setStage: async (stage) => {
14287
+ await updateSetupRun({ stage });
14288
+ },
14289
+ importRecords: async (records, options) => environment.enqueueRecordImport(records, {
14290
+ batchSize: options?.batchSize,
14291
+ setupRunId
14292
+ })
14293
+ };
14294
+ try {
14295
+ await importer(importerContext);
14296
+ await updateSetupRun({ markHookCompleted: true });
14297
+ const refreshedEnvironment = await this.environments.get(
14298
+ environment.environmentId
14299
+ );
14300
+ environment.syncEnvironmentData(refreshedEnvironment);
14301
+ } catch (error) {
14302
+ await updateSetupRun({
14303
+ status: "failed",
14304
+ errorMessage: error instanceof Error ? error.message : String(error)
14305
+ }).catch(() => void 0);
14306
+ throw error;
14307
+ }
14308
+ }
14092
14309
  bindEnvironmentHandle(envData) {
14093
14310
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
14094
14311
  return new Environment(this, envData, this.apiKey, graphqlEndpoint);
@@ -14138,7 +14355,8 @@ var Granular = class _Granular {
14138
14355
  provenance: effect.provenance || { source: "custom" },
14139
14356
  tags: effect.tags,
14140
14357
  className: effect.className,
14141
- static: effect.static
14358
+ static: effect.static,
14359
+ versionSelector: effect.versionSelector
14142
14360
  };
14143
14361
  }
14144
14362
  async publishSandboxEffectCatalog(host) {
@@ -14326,7 +14544,10 @@ var Granular = class _Granular {
14326
14544
  async registerEffect(sandboxNameOrId, effect) {
14327
14545
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
14328
14546
  const sandboxId = sandbox.sandboxId;
14329
- this.getSandboxEffectMap(sandboxId).set(computeEffectKey2(effect), effect);
14547
+ this.getSandboxEffectMap(sandboxId).set(
14548
+ computeEffectRegistrationKey(effect),
14549
+ effect
14550
+ );
14330
14551
  await this.syncSandboxEffectCatalog(sandboxId);
14331
14552
  }
14332
14553
  /**
@@ -14339,7 +14560,7 @@ var Granular = class _Granular {
14339
14560
  const sandboxId = sandbox.sandboxId;
14340
14561
  const map = this.getSandboxEffectMap(sandboxId);
14341
14562
  for (const effect of effects) {
14342
- map.set(computeEffectKey2(effect), effect);
14563
+ map.set(computeEffectRegistrationKey(effect), effect);
14343
14564
  }
14344
14565
  await this.syncSandboxEffectCatalog(sandboxId);
14345
14566
  }
@@ -14357,7 +14578,7 @@ var Granular = class _Granular {
14357
14578
  return;
14358
14579
  }
14359
14580
  const nextEntries = Array.from(currentMap.entries()).filter(
14360
- ([effectKey, effect]) => effectKey !== name && effect.name !== name
14581
+ ([, effect]) => computeEffectKey2(effect) !== name && effect.name !== name
14361
14582
  );
14362
14583
  if (nextEntries.length === currentMap.size) {
14363
14584
  return;
@@ -15546,12 +15767,11 @@ function buildContinuationInstruction(resultPreview) {
15546
15767
  "Continue the same user request using the latest structured session state.",
15547
15768
  "Take only the minimum next step that directly helps the user.",
15548
15769
  "Use the active tasks, decisions, prompts, and heap references as the source of truth instead of replaying old work.",
15549
- "If the user names a concrete record that is not already in the heap, fetch it from the graph instead of replying that it is not in context.",
15770
+ "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.",
15771
+ "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.",
15550
15772
  "If this request clearly spans multiple steps and there are no active tasks yet, create 2-4 short user-visible tasks now.",
15551
15773
  "Reuse any existing taskId and decisionId values exactly as they appear in AGENT LOOP STATE.",
15552
- "Use ask_user with type input for open-ended preferences or missing text. Use type choice only for a short explicit shortlist.",
15553
- "Do not ask for confirmation in plain text. Use loop.confirm(...) when approval is needed.",
15554
- "Await loop.ask_user(...) and loop.confirm(...). Those helpers pause the current job and resume it after the user answers.",
15774
+ "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.",
15555
15775
  "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.'",
15556
15776
  "If you ask the user a new question in this job, do not also close the loop in the same job.",
15557
15777
  "Write the smallest straightforward code for the current step. Avoid defensive fallback branches for hypothetical states that are not currently true.",
@@ -15739,12 +15959,13 @@ ${loopBlock}
15739
15959
  - 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.
15740
15960
  - Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
15741
15961
  - Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
15742
- - If the user names a record that is not already in the heap, fetch it from the graph instead of saying it is not in context.
15743
- - Treat user-provided names as human references, not exact keys. If one strong partial match exists, use it. If several plausible matches exist, ask the user to choose.
15744
15962
  - Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
15963
+ - 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.
15964
+ - 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.
15745
15965
  - If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
15746
15966
  - 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.
15747
15967
  - 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.
15968
+ - 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.
15748
15969
  - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
15749
15970
  - If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
15750
15971
  - Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
@@ -15755,6 +15976,14 @@ ${loopBlock}
15755
15976
  - 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.
15756
15977
  - If you ask a new question in the current job, do not also close the loop in that same job.
15757
15978
 
15979
+ \u2500\u2500\u2500 LOOP HELPER REFERENCE \u2500\u2500\u2500
15980
+ - \`loop.ask_user(...)\`: pause the current job for missing input; use \`type: 'choice'\` only for a short grounded shortlist.
15981
+ - \`loop.confirm(...)\`: pause for yes/no approval before a consequential action, then branch on the returned boolean.
15982
+ - \`loop.open_decision(...)\`: save explicit candidates that later jobs can revisit; each candidate needs an \`id\`.
15983
+ - \`loop.close_decision(...)\`: resolve an open decision with a stored \`selectedId\` and optional rationale.
15984
+ - \`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.
15985
+ - \`loop.close_loop(...)\`: record the workflow outcome when it is completed, canceled, or blocked.
15986
+
15758
15987
  \u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
15759
15988
  - Import from \`./sandbox-tools\`.
15760
15989
  - If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
@@ -15764,6 +15993,7 @@ ${loopBlock}
15764
15993
  - Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
15765
15994
  - 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.
15766
15995
  - \`perPage\` defaults to \`100\` and is capped at \`100\`.
15996
+ - 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.
15767
15997
  - Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
15768
15998
  - 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.
15769
15999
  - 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.