@granular-software/sdk 0.4.34 → 0.4.36

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.
@@ -12352,6 +12352,38 @@ function normalizeUser(user) {
12352
12352
  permissions: Array.isArray(user.permissions) ? user.permissions : []
12353
12353
  };
12354
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
+ }
12355
12387
  function normalizeEnvironmentData(environment) {
12356
12388
  const buildPolicy = environment.buildPolicy || environment.tracking || (environment.tagId ? { mode: "tag", tagId: environment.tagId } : {
12357
12389
  mode: "pinned",
@@ -12365,7 +12397,8 @@ function normalizeEnvironmentData(environment) {
12365
12397
  envName: environmentName,
12366
12398
  environment: environmentName,
12367
12399
  buildPolicy,
12368
- tracking: environment.tracking || buildPolicy
12400
+ tracking: environment.tracking || buildPolicy,
12401
+ setup: normalizeEnvironmentSetupSummary(environment.setup)
12369
12402
  };
12370
12403
  }
12371
12404
  var Environment = class {
@@ -12423,6 +12456,10 @@ var Environment = class {
12423
12456
  get updateState() {
12424
12457
  return this.envData.updateState;
12425
12458
  }
12459
+ /** The latest setup/import run summary for this environment, when available. */
12460
+ get setup() {
12461
+ return this.envData.setup || null;
12462
+ }
12426
12463
  /** Convenience flag for whether this environment trails the current tag target */
12427
12464
  get isOutdated() {
12428
12465
  return this.envData.updateState === "update_available";
@@ -12443,6 +12480,9 @@ var Environment = class {
12443
12480
  get runtimeBaseUrl() {
12444
12481
  return this.getRuntimeBaseUrl();
12445
12482
  }
12483
+ syncEnvironmentData(envData) {
12484
+ this.envData = normalizeEnvironmentData(envData);
12485
+ }
12446
12486
  get sessions() {
12447
12487
  return {
12448
12488
  list: async (options) => this.listSessions(options?.status || "active"),
@@ -13388,7 +13428,8 @@ var Environment = class {
13388
13428
  method: "POST",
13389
13429
  body: JSON.stringify({
13390
13430
  records,
13391
- batchSize: options.batchSize
13431
+ batchSize: options.batchSize,
13432
+ setupRunId: options.setupRunId
13392
13433
  })
13393
13434
  }
13394
13435
  );
@@ -13536,18 +13577,12 @@ var EnvironmentSession = class extends Session {
13536
13577
  }
13537
13578
  get messages() {
13538
13579
  return {
13539
- list: (options = {}) => this.sessionDataRequest(
13540
- "/messages",
13541
- options
13542
- )
13580
+ list: (options = {}) => this.sessionDataRequest("/messages", options)
13543
13581
  };
13544
13582
  }
13545
13583
  get timeline() {
13546
13584
  return {
13547
- list: (options = {}) => this.sessionDataRequest(
13548
- "/timeline",
13549
- options
13550
- )
13585
+ list: (options = {}) => this.sessionDataRequest("/timeline", options)
13551
13586
  };
13552
13587
  }
13553
13588
  get jobs() {
@@ -13564,10 +13599,7 @@ var EnvironmentSession = class extends Session {
13564
13599
  get heap() {
13565
13600
  return {
13566
13601
  entries: {
13567
- list: (options = {}) => this.sessionDataRequest(
13568
- "/heap/entries",
13569
- options
13570
- ),
13602
+ list: (options = {}) => this.sessionDataRequest("/heap/entries", options),
13571
13603
  get: (path2) => this.sessionDataRequest(
13572
13604
  `/heap/entries/${encodeURIComponent(path2)}`
13573
13605
  )
@@ -13611,10 +13643,7 @@ var EnvironmentSession = class extends Session {
13611
13643
  const heap = normalizeHeapSnapshot({
13612
13644
  entriesByPath: Object.fromEntries(
13613
13645
  entries.map((entry) => {
13614
- return entry?.path ? [
13615
- entry.path,
13616
- entry
13617
- ] : null;
13646
+ return entry?.path ? [entry.path, entry] : null;
13618
13647
  }).filter(
13619
13648
  (entry) => Boolean(entry)
13620
13649
  )
@@ -13780,6 +13809,15 @@ var OntologyHandle = class {
13780
13809
  disconnect: async () => this.granular.disconnectEffects(this.ontologyNameOrId)
13781
13810
  };
13782
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
+ }
13783
13821
  };
13784
13822
  var Granular = class _Granular {
13785
13823
  apiKey;
@@ -13796,6 +13834,10 @@ var Granular = class _Granular {
13796
13834
  sandboxEffectHosts = /* @__PURE__ */ new Map();
13797
13835
  /** In-flight host connection promises to avoid duplicate concurrent connects */
13798
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();
13799
13841
  /**
13800
13842
  * Create a new Granular client
13801
13843
  * @param options - Client configuration
@@ -13821,6 +13863,32 @@ var Granular = class _Granular {
13821
13863
  ontology(ontologyNameOrId) {
13822
13864
  return new OntologyHandle(this, ontologyNameOrId);
13823
13865
  }
13866
+ registerEnvironmentImporter(ontologyNameOrId, handler) {
13867
+ this.ontologyImporters.set(ontologyNameOrId, handler);
13868
+ if (ontologyNameOrId.startsWith("sbx_")) {
13869
+ this.sandboxImporters.set(ontologyNameOrId, {
13870
+ handler,
13871
+ sourceOntology: ontologyNameOrId
13872
+ });
13873
+ return;
13874
+ }
13875
+ for (const [sandboxId, importer] of this.sandboxImporters.entries()) {
13876
+ if (importer.sourceOntology === ontologyNameOrId) {
13877
+ this.sandboxImporters.set(sandboxId, {
13878
+ handler,
13879
+ sourceOntology: ontologyNameOrId
13880
+ });
13881
+ }
13882
+ }
13883
+ }
13884
+ clearEnvironmentImporter(ontologyNameOrId) {
13885
+ this.ontologyImporters.delete(ontologyNameOrId);
13886
+ for (const [sandboxId, importer] of this.sandboxImporters.entries()) {
13887
+ if (importer.sourceOntology === ontologyNameOrId) {
13888
+ this.sandboxImporters.delete(sandboxId);
13889
+ }
13890
+ }
13891
+ }
13824
13892
  /**
13825
13893
  * Records/upserts a user and prepares them for sandbox connections
13826
13894
  *
@@ -13937,11 +14005,13 @@ var Granular = class _Granular {
13937
14005
  * ```
13938
14006
  */
13939
14007
  async openEnvironment(options) {
13940
- const envData = await this.resolveOpenEnvironmentData(
14008
+ const resolved = await this.resolveOpenEnvironmentData(
13941
14009
  options,
13942
14010
  "openEnvironment"
13943
14011
  );
13944
- return this.bindEnvironmentHandle(envData);
14012
+ const environment = this.bindEnvironmentHandle(resolved.environment);
14013
+ await this.maybeRunEnvironmentImporter(resolved, environment);
14014
+ return environment;
13945
14015
  }
13946
14016
  /**
13947
14017
  * Deprecated compatibility alias for `openEnvironment()`.
@@ -14028,7 +14098,12 @@ var Granular = class _Granular {
14028
14098
  )
14029
14099
  );
14030
14100
  if (currentMatches.length > 0) {
14031
- return currentMatches[0];
14101
+ return {
14102
+ environment: currentMatches[0],
14103
+ requestedOntology: ontology,
14104
+ sandboxId: sandbox.sandboxId,
14105
+ subjectId: user.granularId
14106
+ };
14032
14107
  }
14033
14108
  const outdatedMatches = this.sortEnvironmentsByRecency(
14034
14109
  userEnvironments.filter(
@@ -14036,14 +14111,25 @@ var Granular = class _Granular {
14036
14111
  )
14037
14112
  );
14038
14113
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
14039
- return outdatedMatches[0];
14114
+ return {
14115
+ environment: outdatedMatches[0],
14116
+ requestedOntology: ontology,
14117
+ sandboxId: sandbox.sandboxId,
14118
+ subjectId: user.granularId
14119
+ };
14040
14120
  }
14041
- return this.environments.create(sandbox.sandboxId, {
14121
+ return {
14122
+ environment: await this.environments.create(sandbox.sandboxId, {
14123
+ subjectId: user.granularId,
14124
+ environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
14125
+ tagId: tag.tagId,
14126
+ permissionProfileId: null
14127
+ }),
14128
+ requestedOntology: ontology,
14129
+ sandboxId: sandbox.sandboxId,
14042
14130
  subjectId: user.granularId,
14043
- environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
14044
- tagId: tag.tagId,
14045
- permissionProfileId: null
14046
- });
14131
+ setupTriggerReason: outdatedMatches.length > 0 ? "fresh_after_version_update" : "new_environment"
14132
+ };
14047
14133
  }
14048
14134
  /**
14049
14135
  * List active (open) sessions for an environment — each session is one agent conversation thread.
@@ -14160,6 +14246,94 @@ var Granular = class _Granular {
14160
14246
  });
14161
14247
  return this.connectSession({ sessionId, clientId: options?.clientId });
14162
14248
  }
14249
+ resolveEnvironmentImporter(requestedOntology, sandboxId) {
14250
+ const sandboxImporter = this.sandboxImporters.get(sandboxId);
14251
+ if (sandboxImporter) {
14252
+ const sourceImporter = this.ontologyImporters.get(
14253
+ sandboxImporter.sourceOntology
14254
+ );
14255
+ if (sourceImporter === sandboxImporter.handler) {
14256
+ return sandboxImporter.handler;
14257
+ }
14258
+ this.sandboxImporters.delete(sandboxId);
14259
+ }
14260
+ const ontologyImporter = this.ontologyImporters.get(requestedOntology);
14261
+ if (!ontologyImporter) {
14262
+ return void 0;
14263
+ }
14264
+ this.sandboxImporters.set(sandboxId, {
14265
+ handler: ontologyImporter,
14266
+ sourceOntology: requestedOntology
14267
+ });
14268
+ return ontologyImporter;
14269
+ }
14270
+ async maybeRunEnvironmentImporter(resolved, environment) {
14271
+ if (!resolved.setupTriggerReason) {
14272
+ return;
14273
+ }
14274
+ const importer = this.resolveEnvironmentImporter(
14275
+ resolved.requestedOntology,
14276
+ resolved.sandboxId
14277
+ );
14278
+ if (!importer) {
14279
+ return;
14280
+ }
14281
+ const setupRun = await this.request(
14282
+ `/control/environments/${environment.environmentId}/setup-runs`,
14283
+ {
14284
+ method: "POST",
14285
+ body: JSON.stringify({
14286
+ triggerReason: resolved.setupTriggerReason
14287
+ })
14288
+ }
14289
+ );
14290
+ const setupRunId = setupRun.setupRunId;
14291
+ const updateSetupRun = async (patch) => {
14292
+ await this.request(
14293
+ `/control/environment-setup-runs/${setupRunId}`,
14294
+ {
14295
+ method: "PATCH",
14296
+ body: JSON.stringify(patch)
14297
+ }
14298
+ );
14299
+ };
14300
+ const importerContext = {
14301
+ environmentId: environment.environmentId,
14302
+ sandboxId: environment.sandboxId,
14303
+ subjectId: environment.subjectId,
14304
+ reason: resolved.setupTriggerReason,
14305
+ incrementTotalObjectsToImportCount: async (n) => {
14306
+ const safeIncrement = Math.max(0, Math.trunc(n));
14307
+ if (safeIncrement <= 0) {
14308
+ return;
14309
+ }
14310
+ await updateSetupRun({
14311
+ incrementTotalObjectsToImportCount: safeIncrement
14312
+ });
14313
+ },
14314
+ setStage: async (stage) => {
14315
+ await updateSetupRun({ stage });
14316
+ },
14317
+ importRecords: async (records, options) => environment.enqueueRecordImport(records, {
14318
+ batchSize: options?.batchSize,
14319
+ setupRunId
14320
+ })
14321
+ };
14322
+ try {
14323
+ await importer(importerContext);
14324
+ await updateSetupRun({ markHookCompleted: true });
14325
+ const refreshedEnvironment = await this.environments.get(
14326
+ environment.environmentId
14327
+ );
14328
+ environment.syncEnvironmentData(refreshedEnvironment);
14329
+ } catch (error) {
14330
+ await updateSetupRun({
14331
+ status: "failed",
14332
+ errorMessage: error instanceof Error ? error.message : String(error)
14333
+ }).catch(() => void 0);
14334
+ throw error;
14335
+ }
14336
+ }
14163
14337
  bindEnvironmentHandle(envData) {
14164
14338
  const graphqlEndpoint = `${this.httpUrl}/orchestrator/graphql`;
14165
14339
  return new Environment(this, envData, this.apiKey, graphqlEndpoint);
@@ -15621,12 +15795,11 @@ function buildContinuationInstruction(resultPreview) {
15621
15795
  "Continue the same user request using the latest structured session state.",
15622
15796
  "Take only the minimum next step that directly helps the user.",
15623
15797
  "Use the active tasks, decisions, prompts, and heap references as the source of truth instead of replaying old work.",
15624
- "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.",
15798
+ "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
+ "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.",
15625
15800
  "If this request clearly spans multiple steps and there are no active tasks yet, create 2-4 short user-visible tasks now.",
15626
15801
  "Reuse any existing taskId and decisionId values exactly as they appear in AGENT LOOP STATE.",
15627
- "Use ask_user with type input for open-ended preferences or missing text. Use type choice only for a short explicit shortlist.",
15628
- "Do not ask for confirmation in plain text. Use loop.confirm(...) when approval is needed.",
15629
- "Await loop.ask_user(...) and loop.confirm(...). Those helpers pause the current job and resume it after the user answers.",
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.",
15630
15803
  "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.'",
15631
15804
  "If you ask the user a new question in this job, do not also close the loop in the same job.",
15632
15805
  "Write the smallest straightforward code for the current step. Avoid defensive fallback branches for hypothetical states that are not currently true.",
@@ -15814,12 +15987,13 @@ ${loopBlock}
15814
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.
15815
15988
  - Use CAPABILITY SNAPSHOT to choose the next step, then use DOMAIN REFERENCE for exact signatures and query shapes.
15816
15989
  - Take the minimum next step that directly helps the user. Avoid duplicate work, speculative cleanup, or extra fetching that is not needed yet.
15817
- - 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.
15818
- - 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.
15819
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.
15820
15993
  - If the request has more than one reasonable interpretation, ask the user to clarify instead of guessing.
15821
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.
15822
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.
15823
15997
  - Reuse exact \`taskId\`, \`decisionId\`, and \`closureId\` values from AGENT LOOP STATE. Never invent or rewrite them.
15824
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.
15825
15999
  - Use \`loop.ask_user({ type: 'choice', options: [...] })\` when you have a short, grounded shortlist the user can choose from. Otherwise use \`type: 'input'\`.
@@ -15830,6 +16004,14 @@ ${loopBlock}
15830
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.
15831
16005
  - If you ask a new question in the current job, do not also close the loop in that same job.
15832
16006
 
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.
16014
+
15833
16015
  \u2500\u2500\u2500 CODE RULES \u2500\u2500\u2500
15834
16016
  - Import from \`./sandbox-tools\`.
15835
16017
  - If you use \`heap\`, \`loop\`, \`agent_text_message\`, or \`agent_heap_objects\`, import them explicitly from \`./sandbox-tools\`.
@@ -15839,6 +16021,7 @@ ${loopBlock}
15839
16021
  - Use \`ClassName.get({ path })\` only for known graph paths when you want a direct graph fetch.
15840
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.
15841
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.
15842
16025
  - Push \`filter\`, \`search\`, and \`sort\` into graph queries instead of fetching a page and processing it locally.
15843
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.
15844
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.