@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.mjs CHANGED
@@ -10864,6 +10864,50 @@ function computeEffectKey(effect) {
10864
10864
  }
10865
10865
  return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
10866
10866
  }
10867
+ function computeEffectVersionSelectorSpecificity(selector) {
10868
+ if (!selector || selector.mode === "all") {
10869
+ return 0;
10870
+ }
10871
+ if (selector.mode === "exact") {
10872
+ return 2;
10873
+ }
10874
+ return 1;
10875
+ }
10876
+ function matchesEffectVersionSelector(selector, buildVersionNumber) {
10877
+ if (!selector || selector.mode === "all") {
10878
+ return true;
10879
+ }
10880
+ if (typeof buildVersionNumber !== "number" || !Number.isFinite(buildVersionNumber)) {
10881
+ return false;
10882
+ }
10883
+ if (selector.mode === "exact") {
10884
+ return buildVersionNumber === selector.versionNumber;
10885
+ }
10886
+ if (selector.mode === "before") {
10887
+ return buildVersionNumber < selector.versionNumber;
10888
+ }
10889
+ return buildVersionNumber > selector.versionNumber;
10890
+ }
10891
+ function selectRegisteredEffect(effectMap, effectKey, buildVersionNumber) {
10892
+ let bestEffect;
10893
+ let bestSpecificity = Number.NEGATIVE_INFINITY;
10894
+ for (const effect of effectMap.values()) {
10895
+ if (computeEffectKey(effect) !== effectKey) {
10896
+ continue;
10897
+ }
10898
+ if (!matchesEffectVersionSelector(effect.versionSelector, buildVersionNumber)) {
10899
+ continue;
10900
+ }
10901
+ const specificity = computeEffectVersionSelectorSpecificity(
10902
+ effect.versionSelector
10903
+ );
10904
+ if (!bestEffect || specificity > bestSpecificity) {
10905
+ bestEffect = effect;
10906
+ bestSpecificity = specificity;
10907
+ }
10908
+ }
10909
+ return bestEffect;
10910
+ }
10867
10911
  function normalizeEffectBehaviors(value) {
10868
10912
  return normalizeEffectBehaviorSummary(
10869
10913
  value
@@ -10884,9 +10928,17 @@ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
10884
10928
  return void 0;
10885
10929
  }
10886
10930
  if (reverseHandler.includes(":")) {
10887
- return effectMap.get(reverseHandler);
10931
+ return selectRegisteredEffect(
10932
+ effectMap,
10933
+ reverseHandler,
10934
+ request.context?.buildVersionNumber
10935
+ );
10888
10936
  }
10889
- const directMatch = effectMap.get(reverseHandler);
10937
+ const directMatch = selectRegisteredEffect(
10938
+ effectMap,
10939
+ reverseHandler,
10940
+ request.context?.buildVersionNumber
10941
+ );
10890
10942
  if (directMatch) {
10891
10943
  return directMatch;
10892
10944
  }
@@ -10901,7 +10953,11 @@ function resolveReverseEffect(effectMap, currentEffect, request, behaviors) {
10901
10953
  })
10902
10954
  ];
10903
10955
  for (const candidateKey of candidateKeys) {
10904
- const candidate = effectMap.get(candidateKey);
10956
+ const candidate = selectRegisteredEffect(
10957
+ effectMap,
10958
+ candidateKey,
10959
+ request.context?.buildVersionNumber
10960
+ );
10905
10961
  if (candidate) {
10906
10962
  return candidate;
10907
10963
  }
@@ -10937,7 +10993,11 @@ function resolveHandlerForMode(effectMap, effect, request) {
10937
10993
  return { effect, mode, handler: effect.handler };
10938
10994
  }
10939
10995
  async function invokeRegisteredEffect(effectMap, request) {
10940
- const effect = effectMap.get(request.effectKey);
10996
+ const effect = selectRegisteredEffect(
10997
+ effectMap,
10998
+ request.effectKey,
10999
+ request.context?.buildVersionNumber
11000
+ );
10941
11001
  if (!effect) {
10942
11002
  throw new Error(`Effect handler not found: ${request.effectKey}`);
10943
11003
  }
@@ -12196,6 +12256,17 @@ function computeEffectKey2(effect) {
12196
12256
  }
12197
12257
  return effect.static ? `class:${attachedClass}:static:${effect.name}` : `class:${attachedClass}:instance:${effect.name}`;
12198
12258
  }
12259
+ function computeEffectVersionSelectorKey(selector) {
12260
+ if (!selector || selector.mode === "all") {
12261
+ return "all";
12262
+ }
12263
+ return `${selector.mode}:${selector.versionNumber}`;
12264
+ }
12265
+ function computeEffectRegistrationKey(effect) {
12266
+ return `${computeEffectKey2(effect)}@${computeEffectVersionSelectorKey(
12267
+ effect.versionSelector
12268
+ )}`;
12269
+ }
12199
12270
  function buildEffectHostUrl(apiUrl, sandboxId, effectClientId, clientId) {
12200
12271
  const url = new URL(apiUrl);
12201
12272
  if (url.pathname.endsWith("/granular/ws/connect")) {
@@ -12279,6 +12350,38 @@ function normalizeUser(user) {
12279
12350
  permissions: Array.isArray(user.permissions) ? user.permissions : []
12280
12351
  };
12281
12352
  }
12353
+ function normalizeEnvironmentSetupSummary(setup) {
12354
+ if (!setup) {
12355
+ return null;
12356
+ }
12357
+ const queuedRecords = Number(setup.queuedRecords || 0);
12358
+ const processingRecords = Number(setup.processingRecords || 0);
12359
+ return {
12360
+ ...setup,
12361
+ setupRunId: String(setup.setupRunId || ""),
12362
+ environmentId: String(setup.environmentId || ""),
12363
+ sandboxId: String(setup.sandboxId || ""),
12364
+ subjectId: String(setup.subjectId || ""),
12365
+ triggerReason: setup.triggerReason === "fresh_after_version_update" ? "fresh_after_version_update" : "new_environment",
12366
+ lifecycleStatus: setup.lifecycleStatus === "completed" || setup.lifecycleStatus === "failed" ? setup.lifecycleStatus : "running",
12367
+ stage: typeof setup.stage === "string" ? setup.stage : null,
12368
+ totalObjectsToImport: Number(setup.totalObjectsToImport || 0),
12369
+ totalImports: Number(setup.totalImports || 0),
12370
+ activeImports: Number(setup.activeImports || 0),
12371
+ totalRecords: Number(setup.totalRecords || 0),
12372
+ queuedRecords,
12373
+ processingRecords,
12374
+ completedRecords: Number(setup.completedRecords || 0),
12375
+ failedRecords: Number(setup.failedRecords || 0),
12376
+ canceledRecords: Number(setup.canceledRecords || 0),
12377
+ awaitingRecords: Number(setup.awaitingRecords || 0) || queuedRecords + processingRecords,
12378
+ errorMessage: typeof setup.errorMessage === "string" ? setup.errorMessage : null,
12379
+ startedAt: Number(setup.startedAt || Date.now()),
12380
+ hookCompletedAt: setup.hookCompletedAt == null ? null : Number(setup.hookCompletedAt),
12381
+ finishedAt: setup.finishedAt == null ? null : Number(setup.finishedAt),
12382
+ updatedAt: Number(setup.updatedAt || Date.now())
12383
+ };
12384
+ }
12282
12385
  function normalizeEnvironmentData(environment) {
12283
12386
  const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
12284
12387
  mode: "pinned",
@@ -12292,7 +12395,8 @@ function normalizeEnvironmentData(environment) {
12292
12395
  envName: environmentName,
12293
12396
  environment: environmentName,
12294
12397
  buildPolicy,
12295
- tracking: environment.tracking || buildPolicy
12398
+ tracking: environment.tracking || buildPolicy,
12399
+ setup: normalizeEnvironmentSetupSummary(environment.setup)
12296
12400
  };
12297
12401
  }
12298
12402
  var Environment = class {
@@ -12350,6 +12454,10 @@ var Environment = class {
12350
12454
  get updateState() {
12351
12455
  return this.envData.updateState;
12352
12456
  }
12457
+ /** The latest setup/import run summary for this environment, when available. */
12458
+ get setup() {
12459
+ return this.envData.setup || null;
12460
+ }
12353
12461
  /** Convenience flag for whether this environment trails the current tag target */
12354
12462
  get isOutdated() {
12355
12463
  return this.envData.updateState === "update_available";
@@ -12370,6 +12478,9 @@ var Environment = class {
12370
12478
  get runtimeBaseUrl() {
12371
12479
  return this.getRuntimeBaseUrl();
12372
12480
  }
12481
+ syncEnvironmentData(envData) {
12482
+ this.envData = normalizeEnvironmentData(envData);
12483
+ }
12373
12484
  get sessions() {
12374
12485
  return {
12375
12486
  list: async (options) => this.listSessions(options?.status || "active"),
@@ -13315,7 +13426,8 @@ var Environment = class {
13315
13426
  method: "POST",
13316
13427
  body: JSON.stringify({
13317
13428
  records,
13318
- batchSize: options.batchSize
13429
+ batchSize: options.batchSize,
13430
+ setupRunId: options.setupRunId
13319
13431
  })
13320
13432
  }
13321
13433
  );
@@ -13463,18 +13575,12 @@ var EnvironmentSession = class extends Session {
13463
13575
  }
13464
13576
  get messages() {
13465
13577
  return {
13466
- list: (options = {}) => this.sessionDataRequest(
13467
- "/messages",
13468
- options
13469
- )
13578
+ list: (options = {}) => this.sessionDataRequest("/messages", options)
13470
13579
  };
13471
13580
  }
13472
13581
  get timeline() {
13473
13582
  return {
13474
- list: (options = {}) => this.sessionDataRequest(
13475
- "/timeline",
13476
- options
13477
- )
13583
+ list: (options = {}) => this.sessionDataRequest("/timeline", options)
13478
13584
  };
13479
13585
  }
13480
13586
  get jobs() {
@@ -13491,10 +13597,7 @@ var EnvironmentSession = class extends Session {
13491
13597
  get heap() {
13492
13598
  return {
13493
13599
  entries: {
13494
- list: (options = {}) => this.sessionDataRequest(
13495
- "/heap/entries",
13496
- options
13497
- ),
13600
+ list: (options = {}) => this.sessionDataRequest("/heap/entries", options),
13498
13601
  get: (path) => this.sessionDataRequest(
13499
13602
  `/heap/entries/${encodeURIComponent(path)}`
13500
13603
  )
@@ -13538,10 +13641,7 @@ var EnvironmentSession = class extends Session {
13538
13641
  const heap = normalizeHeapSnapshot({
13539
13642
  entriesByPath: Object.fromEntries(
13540
13643
  entries.map((entry) => {
13541
- return entry?.path ? [
13542
- entry.path,
13543
- entry
13544
- ] : null;
13644
+ return entry?.path ? [entry.path, entry] : null;
13545
13645
  }).filter(
13546
13646
  (entry) => Boolean(entry)
13547
13647
  )
@@ -13707,6 +13807,15 @@ var OntologyHandle = class {
13707
13807
  disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
13708
13808
  };
13709
13809
  }
13810
+ get importer() {
13811
+ return {
13812
+ onEnvironmentCreate: (handler) => this.granular.registerEnvironmentImporter(
13813
+ this.ontologyNameOrId,
13814
+ handler
13815
+ ),
13816
+ clear: () => this.granular.clearEnvironmentImporter(this.ontologyNameOrId)
13817
+ };
13818
+ }
13710
13819
  };
13711
13820
  var Granular = class _Granular {
13712
13821
  apiKey;
@@ -13717,12 +13826,16 @@ var Granular = class _Granular {
13717
13826
  onUnexpectedClose;
13718
13827
  onReconnectError;
13719
13828
  debugHttp = process.env.GRANULAR_DEBUG_HTTP === "1";
13720
- /** Sandbox-level effect registry: sandboxId → (effectKey → ToolWithHandler) */
13829
+ /** Sandbox-level effect registry: sandboxId → (effectKey@selector → ToolWithHandler) */
13721
13830
  sandboxEffects = /* @__PURE__ */ new Map();
13722
13831
  /** Live sandbox-scoped effect hosts keyed by sandboxId */
13723
13832
  sandboxEffectHosts = /* @__PURE__ */ new Map();
13724
13833
  /** In-flight host connection promises to avoid duplicate concurrent connects */
13725
13834
  sandboxEffectHostPromises = /* @__PURE__ */ new Map();
13835
+ /** Ontology-bound environment importer hooks keyed by the caller's ontology identifier. */
13836
+ ontologyImporters = /* @__PURE__ */ new Map();
13837
+ /** Resolved importer hooks keyed by sandboxId for fast lookups during openEnvironment(). */
13838
+ sandboxImporters = /* @__PURE__ */ new Map();
13726
13839
  /**
13727
13840
  * Create a new Granular client
13728
13841
  * @param options - Client configuration
@@ -13748,6 +13861,18 @@ var Granular = class _Granular {
13748
13861
  ontology(ontologyNameOrId) {
13749
13862
  return new OntologyHandle(this, ontologyNameOrId);
13750
13863
  }
13864
+ registerEnvironmentImporter(ontologyNameOrId, handler) {
13865
+ this.ontologyImporters.set(ontologyNameOrId, handler);
13866
+ if (ontologyNameOrId.startsWith("sbx_")) {
13867
+ this.sandboxImporters.set(ontologyNameOrId, handler);
13868
+ }
13869
+ }
13870
+ clearEnvironmentImporter(ontologyNameOrId) {
13871
+ this.ontologyImporters.delete(ontologyNameOrId);
13872
+ if (ontologyNameOrId.startsWith("sbx_")) {
13873
+ this.sandboxImporters.delete(ontologyNameOrId);
13874
+ }
13875
+ }
13751
13876
  /**
13752
13877
  * Records/upserts a user and prepares them for sandbox connections
13753
13878
  *
@@ -13864,11 +13989,13 @@ var Granular = class _Granular {
13864
13989
  * ```
13865
13990
  */
13866
13991
  async openEnvironment(options) {
13867
- const envData = await this.resolveOpenEnvironmentData(
13992
+ const resolved = await this.resolveOpenEnvironmentData(
13868
13993
  options,
13869
13994
  "openEnvironment"
13870
13995
  );
13871
- return this.bindEnvironmentHandle(envData);
13996
+ const environment = this.bindEnvironmentHandle(resolved.environment);
13997
+ await this.maybeRunEnvironmentImporter(resolved, environment);
13998
+ return environment;
13872
13999
  }
13873
14000
  /**
13874
14001
  * Deprecated compatibility alias for `openEnvironment()`.
@@ -13955,7 +14082,12 @@ var Granular = class _Granular {
13955
14082
  )
13956
14083
  );
13957
14084
  if (currentMatches.length > 0) {
13958
- return currentMatches[0];
14085
+ return {
14086
+ environment: currentMatches[0],
14087
+ requestedOntology: ontology,
14088
+ sandboxId: sandbox.sandboxId,
14089
+ subjectId: user.granularId
14090
+ };
13959
14091
  }
13960
14092
  const outdatedMatches = this.sortEnvironmentsByRecency(
13961
14093
  userEnvironments.filter(
@@ -13963,14 +14095,25 @@ var Granular = class _Granular {
13963
14095
  )
13964
14096
  );
13965
14097
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
13966
- return outdatedMatches[0];
14098
+ return {
14099
+ environment: outdatedMatches[0],
14100
+ requestedOntology: ontology,
14101
+ sandboxId: sandbox.sandboxId,
14102
+ subjectId: user.granularId
14103
+ };
13967
14104
  }
13968
- return this.environments.create(sandbox.sandboxId, {
14105
+ return {
14106
+ environment: await this.environments.create(sandbox.sandboxId, {
14107
+ subjectId: user.granularId,
14108
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
14109
+ tagId: tag.tagId,
14110
+ permissionProfileId: null
14111
+ }),
14112
+ requestedOntology: ontology,
14113
+ sandboxId: sandbox.sandboxId,
13969
14114
  subjectId: user.granularId,
13970
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
13971
- tagId: tag.tagId,
13972
- permissionProfileId: null
13973
- });
14115
+ setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
14116
+ };
13974
14117
  }
13975
14118
  /**
13976
14119
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -14087,6 +14230,80 @@ var Granular = class _Granular {
14087
14230
  });
14088
14231
  return this.connectSession({ sessionId, clientId: options?.clientId });
14089
14232
  }
14233
+ resolveEnvironmentImporter(requestedOntology, sandboxId) {
14234
+ const resolved = this.sandboxImporters.get(sandboxId) || this.ontologyImporters.get(requestedOntology);
14235
+ if (resolved && !this.sandboxImporters.has(sandboxId) && this.ontologyImporters.get(requestedOntology) === resolved) {
14236
+ this.sandboxImporters.set(sandboxId, resolved);
14237
+ }
14238
+ return resolved;
14239
+ }
14240
+ async maybeRunEnvironmentImporter(resolved, environment) {
14241
+ if (!resolved.setupTriggerReason) {
14242
+ return;
14243
+ }
14244
+ const importer = this.resolveEnvironmentImporter(
14245
+ resolved.requestedOntology,
14246
+ resolved.sandboxId
14247
+ );
14248
+ if (!importer) {
14249
+ return;
14250
+ }
14251
+ const setupRun = await this.request(
14252
+ `/control/environments/${environment.environmentId}/setup-runs`,
14253
+ {
14254
+ method: "POST",
14255
+ body: JSON.stringify({
14256
+ triggerReason: resolved.setupTriggerReason
14257
+ })
14258
+ }
14259
+ );
14260
+ const setupRunId = setupRun.setupRunId;
14261
+ const updateSetupRun = async (patch) => {
14262
+ await this.request(
14263
+ `/control/environment-setup-runs/${setupRunId}`,
14264
+ {
14265
+ method: "PATCH",
14266
+ body: JSON.stringify(patch)
14267
+ }
14268
+ );
14269
+ };
14270
+ const importerContext = {
14271
+ environmentId: environment.environmentId,
14272
+ sandboxId: environment.sandboxId,
14273
+ subjectId: environment.subjectId,
14274
+ reason: resolved.setupTriggerReason,
14275
+ incrementTotalObjectsToImportCount: async (n) => {
14276
+ const safeIncrement = Math.max(0, Math.trunc(n));
14277
+ if (safeIncrement <= 0) {
14278
+ return;
14279
+ }
14280
+ await updateSetupRun({
14281
+ incrementTotalObjectsToImportCount: safeIncrement
14282
+ });
14283
+ },
14284
+ setStage: async (stage) => {
14285
+ await updateSetupRun({ stage });
14286
+ },
14287
+ importRecords: async (records, options) => environment.enqueueRecordImport(records, {
14288
+ batchSize: options?.batchSize,
14289
+ setupRunId
14290
+ })
14291
+ };
14292
+ try {
14293
+ await importer(importerContext);
14294
+ await updateSetupRun({ markHookCompleted: true });
14295
+ const refreshedEnvironment = await this.environments.get(
14296
+ environment.environmentId
14297
+ );
14298
+ environment.syncEnvironmentData(refreshedEnvironment);
14299
+ } catch (error) {
14300
+ await updateSetupRun({
14301
+ status: "failed",
14302
+ errorMessage: error instanceof Error ? error.message : String(error)
14303
+ }).catch(() => void 0);
14304
+ throw error;
14305
+ }
14306
+ }
14090
14307
  bindEnvironmentHandle(envData) {
14091
14308
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
14092
14309
  return new Environment(this, envData, this.apiKey, graphqlEndpoint);
@@ -14136,7 +14353,8 @@ var Granular = class _Granular {
14136
14353
  provenance: effect.provenance || { source: "custom" },
14137
14354
  tags: effect.tags,
14138
14355
  className: effect.className,
14139
- static: effect.static
14356
+ static: effect.static,
14357
+ versionSelector: effect.versionSelector
14140
14358
  };
14141
14359
  }
14142
14360
  async publishSandboxEffectCatalog(host) {
@@ -14324,7 +14542,10 @@ var Granular = class _Granular {
14324
14542
  async registerEffect(sandboxNameOrId, effect) {
14325
14543
  const sandbox = await this.findOrCreateSandbox(sandboxNameOrId);
14326
14544
  const sandboxId = sandbox.sandboxId;
14327
- this.getSandboxEffectMap(sandboxId).set(computeEffectKey2(effect), effect);
14545
+ this.getSandboxEffectMap(sandboxId).set(
14546
+ computeEffectRegistrationKey(effect),
14547
+ effect
14548
+ );
14328
14549
  await this.syncSandboxEffectCatalog(sandboxId);
14329
14550
  }
14330
14551
  /**
@@ -14337,7 +14558,7 @@ var Granular = class _Granular {
14337
14558
  const sandboxId = sandbox.sandboxId;
14338
14559
  const map = this.getSandboxEffectMap(sandboxId);
14339
14560
  for (const effect of effects) {
14340
- map.set(computeEffectKey2(effect), effect);
14561
+ map.set(computeEffectRegistrationKey(effect), effect);
14341
14562
  }
14342
14563
  await this.syncSandboxEffectCatalog(sandboxId);
14343
14564
  }
@@ -14355,7 +14576,7 @@ var Granular = class _Granular {
14355
14576
  return;
14356
14577
  }
14357
14578
  const nextEntries = Array.from(currentMap.entries()).filter(
14358
- ([effectKey, effect]) => effectKey !== name && effect.name !== name
14579
+ ([, effect]) => computeEffectKey2(effect) !== name && effect.name !== name
14359
14580
  );
14360
14581
  if (nextEntries.length === currentMap.size) {
14361
14582
  return;
@@ -15724,12 +15945,11 @@ function buildContinuationInstruction(resultPreview) {
15724
15945
  "Continue the same user request using the latest structured session state.",
15725
15946
  "Take only the minimum next step that directly helps the user.",
15726
15947
  "Use the active tasks, decisions, prompts, and heap references as the source of truth instead of replaying old work.",
15727
- "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.",
15948
+ "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.",
15949
+ "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.",
15728
15950
  "If this request clearly spans multiple steps and there are no active tasks yet, create 2-4 short user-visible tasks now.",
15729
15951
  "Reuse any existing taskId and decisionId values exactly as they appear in AGENT LOOP STATE.",
15730
- "Use ask_user with type input for open-ended preferences or missing text. Use type choice only for a short explicit shortlist.",
15731
- "Do not ask for confirmation in plain text. Use loop.confirm(...) when approval is needed.",
15732
- "Await loop.ask_user(...) and loop.confirm(...). Those helpers pause the current job and resume it after the user answers.",
15952
+ "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.",
15733
15953
  "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.'",
15734
15954
  "If you ask the user a new question in this job, do not also close the loop in the same job.",
15735
15955
  "Write the smallest straightforward code for the current step. Avoid defensive fallback branches for hypothetical states that are not currently true.",
@@ -15917,12 +16137,13 @@ ${loopBlock}
15917
16137
  - 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.
15918
16138
  - Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
15919
16139
  - Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
15920
- - 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.
15921
- - 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.
15922
16140
  - Use RECENT REFERENTS to resolve follow-up references across turns, such as "that invoice", "that customer", "those products", or "the other one".
16141
+ - 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.
16142
+ - 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.
15923
16143
  - If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
15924
16144
  - 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.
15925
16145
  - 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.
16146
+ - 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.
15926
16147
  - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
15927
16148
  - If the request is ambiguous or clearly multi-step, create 2-4 short user-visible tasks and keep them updated as the workflow advances.
15928
16149
  - Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
@@ -15933,6 +16154,14 @@ ${loopBlock}
15933
16154
  - 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.
15934
16155
  - If you ask a new question in the current job, do not also close the loop in that same job.
15935
16156
 
16157
+ \u2500\u2500\u2500 LOOP HELPER REFERENCE \u2500\u2500\u2500
16158
+ - \`loop.ask_user(...)\`: pause the current job for missing input; use \`type: 'choice'\` only for a short grounded shortlist.
16159
+ - \`loop.confirm(...)\`: pause for yes/no approval before a consequential action, then branch on the returned boolean.
16160
+ - \`loop.open_decision(...)\`: save explicit candidates that later jobs can revisit; each candidate needs an \`id\`.
16161
+ - \`loop.close_decision(...)\`: resolve an open decision with a stored \`selectedId\` and optional rationale.
16162
+ - \`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.
16163
+ - \`loop.close_loop(...)\`: record the workflow outcome when it is completed, canceled, or blocked.
16164
+
15936
16165
  \u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
15937
16166
  - Import from \`./sandbox-tools\`.
15938
16167
  - If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
@@ -15942,6 +16171,7 @@ ${loopBlock}
15942
16171
  - Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
15943
16172
  - 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.
15944
16173
  - \`perPage\` defaults to \`100\` and is capped at \`100\`.
16174
+ - 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.
15945
16175
  - Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
15946
16176
  - 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.
15947
16177
  - 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.