@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.
package/dist/index.js CHANGED
@@ -10886,6 +10886,50 @@ function computeEffectKey(effect) {
10886
10886
  }
10887
10887
  return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
10888
10888
  }
10889
+ function computeEffectVersionSelectorSpecificity(selector) {
10890
+ if (!selector || selector.mode === "all") {
10891
+ return 0;
10892
+ }
10893
+ if (selector.mode === "exact") {
10894
+ return 2;
10895
+ }
10896
+ return 1;
10897
+ }
10898
+ function matchesEffectVersionSelector(selector, buildVersionNumber) {
10899
+ if (!selector || selector.mode === "all") {
10900
+ return true;
10901
+ }
10902
+ if (typeof buildVersionNumber !== "number" || !Number.isFinite(buildVersionNumber)) {
10903
+ return false;
10904
+ }
10905
+ if (selector.mode === "exact") {
10906
+ return buildVersionNumber === selector.versionNumber;
10907
+ }
10908
+ if (selector.mode === "before") {
10909
+ return buildVersionNumber < selector.versionNumber;
10910
+ }
10911
+ return buildVersionNumber > selector.versionNumber;
10912
+ }
10913
+ function selectRegisteredEffect(effectMap, effectKey, buildVersionNumber) {
10914
+ let bestEffect;
10915
+ let bestSpecificity = Number.NEGATIVE_INFINITY;
10916
+ for (const effect of effectMap.values()) {
10917
+ if (computeEffectKey(effect) !== effectKey) {
10918
+ continue;
10919
+ }
10920
+ if (!matchesEffectVersionSelector(effect.versionSelector, buildVersionNumber)) {
10921
+ continue;
10922
+ }
10923
+ const specificity = computeEffectVersionSelectorSpecificity(
10924
+ effect.versionSelector
10925
+ );
10926
+ if (!bestEffect || specificity > bestSpecificity) {
10927
+ bestEffect = effect;
10928
+ bestSpecificity = specificity;
10929
+ }
10930
+ }
10931
+ return bestEffect;
10932
+ }
10889
10933
  function normalizeEffectBehaviors(value) {
10890
10934
  return normalizeEffectBehaviorSummary(
10891
10935
  value
@@ -10906,9 +10950,17 @@ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
10906
10950
  return void 0;
10907
10951
  }
10908
10952
  if (reverseHandler.includes(":")) {
10909
- return effectMap.get(reverseHandler);
10953
+ return selectRegisteredEffect(
10954
+ effectMap,
10955
+ reverseHandler,
10956
+ request.context?.buildVersionNumber
10957
+ );
10910
10958
  }
10911
- const directMatch = effectMap.get(reverseHandler);
10959
+ const directMatch = selectRegisteredEffect(
10960
+ effectMap,
10961
+ reverseHandler,
10962
+ request.context?.buildVersionNumber
10963
+ );
10912
10964
  if (directMatch) {
10913
10965
  return directMatch;
10914
10966
  }
@@ -10923,7 +10975,11 @@ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
10923
10975
  })
10924
10976
  ];
10925
10977
  for (const candidateKey of candidateKeys) {
10926
- const candidate = effectMap.get(candidateKey);
10978
+ const candidate = selectRegisteredEffect(
10979
+ effectMap,
10980
+ candidateKey,
10981
+ request.context?.buildVersionNumber
10982
+ );
10927
10983
  if (candidate) {
10928
10984
  return candidate;
10929
10985
  }
@@ -10959,7 +11015,11 @@ function resolveHandlerForMode(effectMap, effect, request) {
10959
11015
  return { effect, mode, handler: effect.handler };
10960
11016
  }
10961
11017
  async function invokeRegisteredEffect(effectMap, request) {
10962
- const effect = effectMap.get(request.effectKey);
11018
+ const effect = selectRegisteredEffect(
11019
+ effectMap,
11020
+ request.effectKey,
11021
+ request.context?.buildVersionNumber
11022
+ );
10963
11023
  if (!effect) {
10964
11024
  throw new Error(`Effect handler not found: ${request.effectKey}`);
10965
11025
  }
@@ -12218,6 +12278,17 @@ function computeEffectKey2(effect) {
12218
12278
  }
12219
12279
  return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
12220
12280
  }
12281
+ function computeEffectVersionSelectorKey(selector) {
12282
+ if (!selector || selector.mode === "all") {
12283
+ return "all";
12284
+ }
12285
+ return `${selector.mode}:${selector.versionNumber}`;
12286
+ }
12287
+ function computeEffectRegistrationKey(effect) {
12288
+ return `${computeEffectKey2(effect)}@${computeEffectVersionSelectorKey(
12289
+ effect.versionSelector
12290
+ )}`;
12291
+ }
12221
12292
  function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
12222
12293
  const url = new URL(apiUrl);
12223
12294
  if (url.pathname.endsWith("/granular/ws/connect")) {
@@ -12301,6 +12372,38 @@ function normalizeUser(user) {
12301
12372
  permissions: Array.isArray(user.permissions) ? user.permissions : []
12302
12373
  };
12303
12374
  }
12375
+ function normalizeEnvironmentSetupSummary(setup) {
12376
+ if (!setup) {
12377
+ return null;
12378
+ }
12379
+ const queuedRecords = Number(setup.queuedRecords || 0);
12380
+ const processingRecords = Number(setup.processingRecords || 0);
12381
+ return {
12382
+ ...setup,
12383
+ setupRunId: String(setup.setupRunId || ""),
12384
+ environmentId: String(setup.environmentId || ""),
12385
+ sandboxId: String(setup.sandboxId || ""),
12386
+ subjectId: String(setup.subjectId || ""),
12387
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
12388
+ lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
12389
+ stage: typeof setup.stage === "string" ? setup.stage : null,
12390
+ totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
12391
+ totalImports: Number(setup.totalImports || 0),
12392
+ activeImports: Number(setup.activeImports || 0),
12393
+ totalRecords: Number(setup.totalRecords || 0),
12394
+ queuedRecords,
12395
+ processingRecords,
12396
+ completedRecords: Number(setup.completedRecords || 0),
12397
+ failedRecords: Number(setup.failedRecords || 0),
12398
+ canceledRecords: Number(setup.canceledRecords || 0),
12399
+ awaitingRecords: Number(setup.awaitingRecords || 0) || queuedRecords + processingRecords,
12400
+ errorMessage: typeof setup.errorMessage === "string" ? setup.errorMessage : null,
12401
+ startedAt: Number(setup.startedAt || Date.now()),
12402
+ hookCompletedAt: setup.hookCompletedAt == null ? null : Number(setup.hookCompletedAt),
12403
+ finishedAt: setup.finishedAt == null ? null : Number(setup.finishedAt),
12404
+ updatedAt: Number(setup.updatedAt || Date.now())
12405
+ };
12406
+ }
12304
12407
  function normalizeEnvironmentData(environment) {
12305
12408
  const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
12306
12409
  mode: "pinned",
@@ -12314,7 +12417,8 @@ function normalizeEnvironmentData(environment) {
12314
12417
  envName: environmentName,
12315
12418
  environment: environmentName,
12316
12419
  buildPolicy,
12317
- tracking: environment.tracking || buildPolicy
12420
+ tracking: environment.tracking || buildPolicy,
12421
+ setup: normalizeEnvironmentSetupSummary(environment.setup)
12318
12422
  };
12319
12423
  }
12320
12424
  var Environment = class {
@@ -12372,6 +12476,10 @@ var Environment = class {
12372
12476
  get updateState() {
12373
12477
  return this.envData.updateState;
12374
12478
  }
12479
+ /** The latest setup/import run summary for this environment, when available. */
12480
+ get setup() {
12481
+ return this.envData.setup || null;
12482
+ }
12375
12483
  /** Convenience flag for whether this environment trails the current tag target */
12376
12484
  get isOutdated() {
12377
12485
  return this.envData.updateState === "update_available";
@@ -12392,6 +12500,9 @@ var Environment = class {
12392
12500
  get runtimeBaseUrl() {
12393
12501
  return this.getRuntimeBaseUrl();
12394
12502
  }
12503
+ syncEnvironmentData(envData) {
12504
+ this.envData = normalizeEnvironmentData(envData);
12505
+ }
12395
12506
  get sessions() {
12396
12507
  return {
12397
12508
  list: async (options) => this.listSessions(options?.status || "active"),
@@ -13337,7 +13448,8 @@ var Environment = class {
13337
13448
  method: "POST",
13338
13449
  body: JSON.stringify({
13339
13450
  records,
13340
- batchSize: options.batchSize
13451
+ batchSize: options.batchSize,
13452
+ setupRunId: options.setupRunId
13341
13453
  })
13342
13454
  }
13343
13455
  );
@@ -13485,18 +13597,12 @@ var EnvironmentSession = class extends Session {
13485
13597
  }
13486
13598
  get messages() {
13487
13599
  return {
13488
- list: (options = {}) => this.sessionDataRequest(
13489
- "/messages",
13490
- options
13491
- )
13600
+ list: (options = {}) => this.sessionDataRequest("/messages", options)
13492
13601
  };
13493
13602
  }
13494
13603
  get timeline() {
13495
13604
  return {
13496
- list: (options = {}) => this.sessionDataRequest(
13497
- "/timeline",
13498
- options
13499
- )
13605
+ list: (options = {}) => this.sessionDataRequest("/timeline", options)
13500
13606
  };
13501
13607
  }
13502
13608
  get jobs() {
@@ -13513,10 +13619,7 @@ var EnvironmentSession = class extends Session {
13513
13619
  get heap() {
13514
13620
  return {
13515
13621
  entries: {
13516
- list: (options = {}) => this.sessionDataRequest(
13517
- "/heap/entries",
13518
- options
13519
- ),
13622
+ list: (options = {}) => this.sessionDataRequest("/heap/entries", options),
13520
13623
  get: (path) => this.sessionDataRequest(
13521
13624
  `/heap/entries/${encodeURIComponent(path)}`
13522
13625
  )
@@ -13560,10 +13663,7 @@ var EnvironmentSession = class extends Session {
13560
13663
  const heap = normalizeHeapSnapshot({
13561
13664
  entriesByPath: Object.fromEntries(
13562
13665
  entries.map((entry) => {
13563
- return entry?.path ? [
13564
- entry.path,
13565
- entry
13566
- ] : null;
13666
+ return entry?.path ? [entry.path, entry] : null;
13567
13667
  }).filter(
13568
13668
  (entry) => Boolean(entry)
13569
13669
  )
@@ -13729,6 +13829,15 @@ var OntologyHandle = class {
13729
13829
  disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
13730
13830
  };
13731
13831
  }
13832
+ get importer() {
13833
+ return {
13834
+ onEnvironmentCreate: (handler) => this.granular.registerEnvironmentImporter(
13835
+ this.ontologyNameOrId,
13836
+ handler
13837
+ ),
13838
+ clear: () => this.granular.clearEnvironmentImporter(this.ontologyNameOrId)
13839
+ };
13840
+ }
13732
13841
  };
13733
13842
  var Granular = class _Granular {
13734
13843
  apiKey;
@@ -13739,12 +13848,16 @@ var Granular = class _Granular {
13739
13848
  onUnexpectedClose;
13740
13849
  onReconnectError;
13741
13850
  debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
13742
- /** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
13851
+ /** Sandbox-level effect registry: sandboxId → (effectKey@selector → ToolWithHandler) */
13743
13852
  sandboxEffects = /* @__PURE__ */ new Map();
13744
13853
  /** Live sandbox-scoped effect hosts keyed by sandboxId */
13745
13854
  sandboxEffectHosts = /* @__PURE__ */ new Map();
13746
13855
  /** In-flight host connection promises to avoid duplicate concurrent connects */
13747
13856
  sandboxEffectHostPromises = /* @__PURE__ */ new Map();
13857
+ /** Ontology-bound environment importer hooks keyed by the caller's ontology identifier. */
13858
+ ontologyImporters = /* @__PURE__ */ new Map();
13859
+ /** Resolved importer hooks keyed by sandboxId for fast lookups during openEnvironment(). */
13860
+ sandboxImporters = /* @__PURE__ */ new Map();
13748
13861
  /**
13749
13862
  * Create a new Granular client
13750
13863
  * @param options - Client configuration
@@ -13770,6 +13883,18 @@ var Granular = class _Granular {
13770
13883
  ontology(ontologyNameOrId) {
13771
13884
  return new OntologyHandle(this, ontologyNameOrId);
13772
13885
  }
13886
+ registerEnvironmentImporter(ontologyNameOrId, handler) {
13887
+ this.ontologyImporters.set(ontologyNameOrId, handler);
13888
+ if (ontologyNameOrId.startsWith("sbx_")) {
13889
+ this.sandboxImporters.set(ontologyNameOrId, handler);
13890
+ }
13891
+ }
13892
+ clearEnvironmentImporter(ontologyNameOrId) {
13893
+ this.ontologyImporters.delete(ontologyNameOrId);
13894
+ if (ontologyNameOrId.startsWith("sbx_")) {
13895
+ this.sandboxImporters.delete(ontologyNameOrId);
13896
+ }
13897
+ }
13773
13898
  /**
13774
13899
  * Records/upserts a user and prepares them for sandbox connections
13775
13900
  *
@@ -13886,11 +14011,13 @@ var Granular = class _Granular {
13886
14011
  * ```
13887
14012
  */
13888
14013
  async openEnvironment(options) {
13889
- const envData = await this.resolveOpenEnvironmentData(
14014
+ const resolved = await this.resolveOpenEnvironmentData(
13890
14015
  options,
13891
14016
  "openEnvironment"
13892
14017
  );
13893
- return this.bindEnvironmentHandle(envData);
14018
+ const environment = this.bindEnvironmentHandle(resolved.environment);
14019
+ await this.maybeRunEnvironmentImporter(resolved, environment);
14020
+ return environment;
13894
14021
  }
13895
14022
  /**
13896
14023
  * Deprecated compatibility alias for `openEnvironment()`.
@@ -13977,7 +14104,12 @@ var Granular = class _Granular {
13977
14104
  )
13978
14105
  );
13979
14106
  if (currentMatches.length > 0) {
13980
- return currentMatches[0];
14107
+ return {
14108
+ environment: currentMatches[0],
14109
+ requestedOntology: ontology,
14110
+ sandboxId: sandbox.sandboxId,
14111
+ subjectId: user.granularId
14112
+ };
13981
14113
  }
13982
14114
  const outdatedMatches = this.sortEnvironmentsByRecency(
13983
14115
  userEnvironments.filter(
@@ -13985,14 +14117,25 @@ var Granular = class _Granular {
13985
14117
  )
13986
14118
  );
13987
14119
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13988
- return outdatedMatches[0];
14120
+ return {
14121
+ environment: outdatedMatches[0],
14122
+ requestedOntology: ontology,
14123
+ sandboxId: sandbox.sandboxId,
14124
+ subjectId: user.granularId
14125
+ };
13989
14126
  }
13990
- return this.environments.create(sandbox.sandboxId, {
14127
+ return {
14128
+ environment: await this.environments.create(sandbox.sandboxId, {
14129
+ subjectId: user.granularId,
14130
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
14131
+ tagId: tag.tagId,
14132
+ permissionProfileId: null
14133
+ }),
14134
+ requestedOntology: ontology,
14135
+ sandboxId: sandbox.sandboxId,
13991
14136
  subjectId: user.granularId,
13992
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13993
- tagId: tag.tagId,
13994
- permissionProfileId: null
13995
- });
14137
+ setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
14138
+ };
13996
14139
  }
13997
14140
  /**
13998
14141
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -14109,6 +14252,80 @@ var Granular = class _Granular {
14109
14252
  });
14110
14253
  return this.connectSession({ sessionId, clientId: options?.clientId });
14111
14254
  }
14255
+ resolveEnvironmentImporter(requestedOntology, sandboxId) {
14256
+ const resolved = this.sandboxImporters.get(sandboxId) || this.ontologyImporters.get(requestedOntology);
14257
+ if (resolved && !this.sandboxImporters.has(sandboxId) && this.ontologyImporters.get(requestedOntology) === resolved) {
14258
+ this.sandboxImporters.set(sandboxId, resolved);
14259
+ }
14260
+ return resolved;
14261
+ }
14262
+ async maybeRunEnvironmentImporter(resolved, environment) {
14263
+ if (!resolved.setupTriggerReason) {
14264
+ return;
14265
+ }
14266
+ const importer = this.resolveEnvironmentImporter(
14267
+ resolved.requestedOntology,
14268
+ resolved.sandboxId
14269
+ );
14270
+ if (!importer) {
14271
+ return;
14272
+ }
14273
+ const setupRun = await this.request(
14274
+ `/control/environments/${environment.environmentId}/setup-runs`,
14275
+ {
14276
+ method: "POST",
14277
+ body: JSON.stringify({
14278
+ triggerReason: resolved.setupTriggerReason
14279
+ })
14280
+ }
14281
+ );
14282
+ const setupRunId = setupRun.setupRunId;
14283
+ const updateSetupRun = async (patch) => {
14284
+ await this.request(
14285
+ `/control/environment-setup-runs/${setupRunId}`,
14286
+ {
14287
+ method: "PATCH",
14288
+ body: JSON.stringify(patch)
14289
+ }
14290
+ );
14291
+ };
14292
+ const importerContext = {
14293
+ environmentId: environment.environmentId,
14294
+ sandboxId: environment.sandboxId,
14295
+ subjectId: environment.subjectId,
14296
+ reason: resolved.setupTriggerReason,
14297
+ incrementTotalObjectsToImportCount: async (n) => {
14298
+ const safeIncrement = Math.max(0, Math.trunc(n));
14299
+ if (safeIncrement <= 0) {
14300
+ return;
14301
+ }
14302
+ await updateSetupRun({
14303
+ incrementTotalObjectsToImportCount: safeIncrement
14304
+ });
14305
+ },
14306
+ setStage: async (stage) => {
14307
+ await updateSetupRun({ stage });
14308
+ },
14309
+ importRecords: async (records, options) => environment.enqueueRecordImport(records, {
14310
+ batchSize: options?.batchSize,
14311
+ setupRunId
14312
+ })
14313
+ };
14314
+ try {
14315
+ await importer(importerContext);
14316
+ await updateSetupRun({ markHookCompleted: true });
14317
+ const refreshedEnvironment = await this.environments.get(
14318
+ environment.environmentId
14319
+ );
14320
+ environment.syncEnvironmentData(refreshedEnvironment);
14321
+ } catch (error) {
14322
+ await updateSetupRun({
14323
+ status: "failed",
14324
+ errorMessage: error instanceof Error ? error.message : String(error)
14325
+ }).catch(() => void 0);
14326
+ throw error;
14327
+ }
14328
+ }
14112
14329
  bindEnvironmentHandle(envData) {
14113
14330
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
14114
14331
  return new Environment(this, envData, this.apiKey, graphqlEndpoint);
@@ -14158,7 +14375,8 @@ var Granular = class _Granular {
14158
14375
  provenance: effect.provenance || { source: "custom" },
14159
14376
  tags: effect.tags,
14160
14377
  className: effect.className,
14161
- static: effect.static
14378
+ static: effect.static,
14379
+ versionSelector: effect.versionSelector
14162
14380
  };
14163
14381
  }
14164
14382
  async publishSandboxEffectCatalog(host) {
@@ -14346,7 +14564,10 @@ var Granular = class _Granular {
14346
14564
  async registerEffect(sandboxNameOrId, effect) {
14347
14565
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
14348
14566
  const sandboxId = sandbox.sandboxId;
14349
- this.getSandboxEffectMap(sandboxId).set(computeEffectKey2(effect), effect);
14567
+ this.getSandboxEffectMap(sandboxId).set(
14568
+ computeEffectRegistrationKey(effect),
14569
+ effect
14570
+ );
14350
14571
  await this.syncSandboxEffectCatalog(sandboxId);
14351
14572
  }
14352
14573
  /**
@@ -14359,7 +14580,7 @@ var Granular = class _Granular {
14359
14580
  const sandboxId = sandbox.sandboxId;
14360
14581
  const map = this.getSandboxEffectMap(sandboxId);
14361
14582
  for (const effect of effects) {
14362
- map.set(computeEffectKey2(effect), effect);
14583
+ map.set(computeEffectRegistrationKey(effect), effect);
14363
14584
  }
14364
14585
  await this.syncSandboxEffectCatalog(sandboxId);
14365
14586
  }
@@ -14377,7 +14598,7 @@ var Granular = class _Granular {
14377
14598
  return;
14378
14599
  }
14379
14600
  const nextEntries = Array.from(currentMap.entries()).filter(
14380
- ([effectKey, effect]) => effectKey !== name && effect.name !== name
14601
+ ([, effect]) => computeEffectKey2(effect) !== name && effect.name !== name
14381
14602
  );
14382
14603
  if (nextEntries.length === currentMap.size) {
14383
14604
  return;
@@ -15746,12 +15967,11 @@ function buildContinuationInstruction(resultPreview) {
15746
15967
  "Continue the same user request using the latest structured session state.",
15747
15968
  "Take only the minimum next step that directly helps the user.",
15748
15969
  "Use the active tasks, decisions, prompts, and heap references as the source of truth instead of replaying old work.",
15749
- "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.",
15970
+ "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.",
15971
+ "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.",
15750
15972
  "If this request clearly spans multiple steps and there are no active tasks yet, create 2-4 short user-visible tasks now.",
15751
15973
  "Reuse any existing taskId and decisionId values exactly as they appear in AGENT LOOP STATE.",
15752
- "Use ask_user with type input for open-ended preferences or missing text. Use type choice only for a short explicit shortlist.",
15753
- "Do not ask for confirmation in plain text. Use loop.confirm(...) when approval is needed.",
15754
- "Await loop.ask_user(...) and loop.confirm(...). Those helpers pause the current job and resume it after the user answers.",
15974
+ "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.",
15755
15975
  "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.'",
15756
15976
  "If you ask the user a new question in this job, do not also close the loop in the same job.",
15757
15977
  "Write the smallest straightforward code for the current step. Avoid defensive fallback branches for hypothetical states that are not currently true.",
@@ -15939,12 +16159,13 @@ ${loopBlock}
15939
16159
  - 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.
15940
16160
  - Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
15941
16161
  - Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
15942
- - 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.
15943
- - 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.
15944
16162
  - Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
16163
+ - 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.
16164
+ - 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.
15945
16165
  - If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
15946
16166
  - 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.
15947
16167
  - 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.
16168
+ - 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.
15948
16169
  - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
15949
16170
  - If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
15950
16171
  - Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
@@ -15955,6 +16176,14 @@ ${loopBlock}
15955
16176
  - 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.
15956
16177
  - If you ask a new question in the current job, do not also close the loop in that same job.
15957
16178
 
16179
+ \u2500\u2500\u2500 LOOP HELPER REFERENCE \u2500\u2500\u2500
16180
+ - \`loop.ask_user(...)\`: pause the current job for missing input; use \`type: 'choice'\` only for a short grounded shortlist.
16181
+ - \`loop.confirm(...)\`: pause for yes/no approval before a consequential action, then branch on the returned boolean.
16182
+ - \`loop.open_decision(...)\`: save explicit candidates that later jobs can revisit; each candidate needs an \`id\`.
16183
+ - \`loop.close_decision(...)\`: resolve an open decision with a stored \`selectedId\` and optional rationale.
16184
+ - \`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.
16185
+ - \`loop.close_loop(...)\`: record the workflow outcome when it is completed, canceled, or blocked.
16186
+
15958
16187
  \u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
15959
16188
  - Import from \`./sandbox-tools\`.
15960
16189
  - If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
@@ -15964,6 +16193,7 @@ ${loopBlock}
15964
16193
  - Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
15965
16194
  - 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.
15966
16195
  - \`perPage\` defaults to \`100\` and is capped at \`100\`.
16196
+ - 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.
15967
16197
  - Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
15968
16198
  - 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.
15969
16199
  - 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.