@granular-software/sdk 0.4.7 → 0.4.9

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
@@ -3972,6 +3972,9 @@ var WSClient = class {
3972
3972
  this.sessionId = options.sessionId;
3973
3973
  this.token = options.token;
3974
3974
  }
3975
+ get currentSessionId() {
3976
+ return this.sessionId;
3977
+ }
3975
3978
  clearTokenRefreshTimer() {
3976
3979
  if (this.tokenRefreshTimer) {
3977
3980
  clearTimeout(this.tokenRefreshTimer);
@@ -4496,6 +4499,18 @@ var Session = class {
4496
4499
  this.setupEventHandlers();
4497
4500
  this.setupToolInvokeHandler();
4498
4501
  }
4502
+ extractDomainRevisionFromDoc(doc) {
4503
+ const domain = doc?.domain;
4504
+ const active = domain?.active;
4505
+ if (typeof active === "string" && active.trim()) {
4506
+ return active;
4507
+ }
4508
+ const nestedRevision = active?.domainRevision;
4509
+ if (typeof nestedRevision === "string" && nestedRevision.trim()) {
4510
+ return nestedRevision;
4511
+ }
4512
+ return null;
4513
+ }
4499
4514
  buildLegacyEffectContext() {
4500
4515
  return {
4501
4516
  effectClientId: this.clientId,
@@ -4513,6 +4528,9 @@ var Session = class {
4513
4528
  get document() {
4514
4529
  return this.client.doc;
4515
4530
  }
4531
+ get sessionId() {
4532
+ return this.client.currentSessionId;
4533
+ }
4516
4534
  get domainRevision() {
4517
4535
  return this.currentDomainRevision;
4518
4536
  }
@@ -4590,7 +4608,14 @@ var Session = class {
4590
4608
  * execute locally and return the result to the sandbox.
4591
4609
  */
4592
4610
  async submitJob(code, domainRevision) {
4593
- const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
4611
+ let revision = domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4612
+ if (!revision) {
4613
+ try {
4614
+ const summary = await this.getDomain();
4615
+ revision = summary.activeDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4616
+ } catch {
4617
+ }
4618
+ }
4594
4619
  if (!revision) {
4595
4620
  throw new Error("No domain revision available. Register live effects or ensure the build schema is activated.");
4596
4621
  }
@@ -4601,7 +4626,11 @@ var Session = class {
4601
4626
  if (!result.jobId) {
4602
4627
  throw new Error("Failed to submit job: no jobId returned");
4603
4628
  }
4604
- const job = new JobImplementation(result.jobId, this.client);
4629
+ const job = new JobImplementation(result.jobId, this.client, {
4630
+ code,
4631
+ domainRevision: revision,
4632
+ createdAt: Date.now()
4633
+ });
4605
4634
  this.jobsMap.set(result.jobId, job);
4606
4635
  return job;
4607
4636
  }
@@ -4715,7 +4744,13 @@ var Session = class {
4715
4744
  * Get the current domain state and available tools
4716
4745
  */
4717
4746
  async getDomain() {
4718
- return this.client.call("domain.getSummary", {});
4747
+ const summary = await this.client.call("domain.getSummary", {});
4748
+ if (summary.activeDomainRevision) {
4749
+ this.currentDomainRevision = summary.activeDomainRevision;
4750
+ } else {
4751
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(this.client.doc);
4752
+ }
4753
+ return summary;
4719
4754
  }
4720
4755
  /**
4721
4756
  * Fetch a domain package part from the backend (no fallback).
@@ -4933,6 +4968,7 @@ import { ${allImports} } from "./sandbox-tools";
4933
4968
  }
4934
4969
  setupEventHandlers() {
4935
4970
  this.client.on("sync", (doc) => {
4971
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(doc);
4936
4972
  this.emit("sync", doc);
4937
4973
  this.checkForToolChanges();
4938
4974
  });
@@ -4988,6 +5024,78 @@ import { ${allImports} } from "./sandbox-tools";
4988
5024
  }
4989
5025
  }
4990
5026
  };
5027
+ var MAX_FEEDBACK_STRING_LENGTH = 16e3;
5028
+ var MAX_FEEDBACK_TOOL_CALLS = 50;
5029
+ var MAX_FEEDBACK_ARRAY_ITEMS = 50;
5030
+ var MAX_FEEDBACK_OBJECT_KEYS = 50;
5031
+ var MAX_FEEDBACK_DEPTH = 6;
5032
+ function truncateFeedbackString(value, maxLength = MAX_FEEDBACK_STRING_LENGTH) {
5033
+ if (value.length <= maxLength) {
5034
+ return value;
5035
+ }
5036
+ return `${value.slice(0, maxLength)}...[truncated ${value.length - maxLength} chars]`;
5037
+ }
5038
+ function normalizeJobStatus(status) {
5039
+ if (typeof status !== "string") {
5040
+ return "queued";
5041
+ }
5042
+ switch (status) {
5043
+ case "completed":
5044
+ return "succeeded";
5045
+ case "failed":
5046
+ case "running":
5047
+ case "queued":
5048
+ case "succeeded":
5049
+ case "timeout":
5050
+ case "canceled":
5051
+ return status;
5052
+ default:
5053
+ return "queued";
5054
+ }
5055
+ }
5056
+ function sanitizeFeedbackValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
5057
+ if (depth >= MAX_FEEDBACK_DEPTH) {
5058
+ return "[Max depth reached]";
5059
+ }
5060
+ if (value === null || value === void 0) {
5061
+ return null;
5062
+ }
5063
+ if (typeof value === "string") {
5064
+ return truncateFeedbackString(value);
5065
+ }
5066
+ if (typeof value === "number" || typeof value === "boolean") {
5067
+ return value;
5068
+ }
5069
+ if (typeof value === "bigint") {
5070
+ return truncateFeedbackString(value.toString());
5071
+ }
5072
+ if (value instanceof Error) {
5073
+ return {
5074
+ name: truncateFeedbackString(value.name),
5075
+ message: truncateFeedbackString(value.message),
5076
+ stack: truncateFeedbackString(value.stack || "")
5077
+ };
5078
+ }
5079
+ if (value instanceof Date) {
5080
+ return value.toISOString();
5081
+ }
5082
+ if (Array.isArray(value)) {
5083
+ return value.slice(0, MAX_FEEDBACK_ARRAY_ITEMS).map((entry) => sanitizeFeedbackValue(entry, depth + 1, seen));
5084
+ }
5085
+ if (typeof value === "object") {
5086
+ if (seen.has(value)) {
5087
+ return "[Circular]";
5088
+ }
5089
+ seen.add(value);
5090
+ const entries = Object.entries(value).slice(0, MAX_FEEDBACK_OBJECT_KEYS);
5091
+ const sanitized = {};
5092
+ for (const [key, entryValue] of entries) {
5093
+ sanitized[key] = sanitizeFeedbackValue(entryValue, depth + 1, seen);
5094
+ }
5095
+ return sanitized;
5096
+ }
5097
+ return truncateFeedbackString(String(value));
5098
+ }
4991
5099
  var JobImplementation = class {
4992
5100
  id;
4993
5101
  client;
@@ -4996,9 +5104,20 @@ var JobImplementation = class {
4996
5104
  _resolveResult;
4997
5105
  _rejectResult;
4998
5106
  eventListeners = /* @__PURE__ */ new Map();
4999
- constructor(id, client) {
5107
+ metadata;
5108
+ constructor(id, client, initialState) {
5000
5109
  this.id = id;
5001
5110
  this.client = client;
5111
+ this.metadata = {
5112
+ source: "sdk",
5113
+ status: "queued",
5114
+ code: truncateFeedbackString(initialState.code),
5115
+ domainRevision: initialState.domainRevision,
5116
+ createdAt: initialState.createdAt,
5117
+ stdout: [],
5118
+ stderr: [],
5119
+ toolCalls: []
5120
+ };
5002
5121
  this._resultPromise = new Promise((resolve, reject) => {
5003
5122
  this._resolveResult = resolve;
5004
5123
  this._rejectResult = reject;
@@ -5006,69 +5125,98 @@ var JobImplementation = class {
5006
5125
  this.client.on("exec.completed", (data) => {
5007
5126
  const execData = data;
5008
5127
  if (execData.execId === id || execData.jobId === id) {
5009
- this.status = "succeeded";
5010
- this.emit("status", this.status);
5011
5128
  if (execData.error) {
5012
- this._rejectResult(execData.error);
5129
+ this.finalize("failed", void 0, execData.error);
5013
5130
  } else {
5014
- this._resolveResult(execData.result);
5131
+ this.finalize("succeeded", execData.result);
5015
5132
  }
5133
+ this.emit("status", this.status);
5016
5134
  }
5017
5135
  });
5018
5136
  this.client.on("exec.progress", (data) => {
5019
5137
  const progressData = data;
5020
5138
  if (progressData.execId === id || progressData.jobId === id) {
5021
5139
  if (progressData.stdout) {
5140
+ this.markStarted();
5141
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(progressData.stdout)].slice(-100);
5022
5142
  this.emit("stdout", progressData.stdout);
5023
5143
  }
5024
5144
  if (progressData.stderr) {
5145
+ this.markStarted();
5146
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(progressData.stderr)].slice(-100);
5025
5147
  this.emit("stderr", progressData.stderr);
5026
5148
  }
5027
5149
  }
5028
5150
  });
5029
5151
  this.client.on(`job.${id}.status`, (status) => {
5030
- this.status = status;
5031
- this.emit("status", status);
5152
+ const normalizedStatus = normalizeJobStatus(status);
5153
+ this.status = normalizedStatus;
5154
+ this.metadata.status = normalizedStatus;
5155
+ if (normalizedStatus === "running") {
5156
+ this.markStarted();
5157
+ }
5158
+ if (normalizedStatus === "failed" || normalizedStatus === "timeout" || normalizedStatus === "canceled") {
5159
+ this.finalize(normalizedStatus);
5160
+ }
5161
+ if (normalizedStatus === "succeeded") {
5162
+ this.finalize("succeeded");
5163
+ }
5164
+ this.emit("status", normalizedStatus);
5032
5165
  });
5033
5166
  this.client.on(`job.${id}.stdout`, (line) => {
5167
+ this.markStarted();
5168
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(String(line))].slice(-100);
5034
5169
  this.emit("stdout", line);
5035
5170
  });
5036
5171
  this.client.on(`job.${id}.stderr`, (line) => {
5172
+ this.markStarted();
5173
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(String(line))].slice(-100);
5037
5174
  this.emit("stderr", line);
5038
5175
  });
5039
5176
  this.client.on(`job.${id}.result`, (result) => {
5040
- this.status = "succeeded";
5041
- this._resolveResult(result);
5177
+ this.finalize("succeeded", result);
5042
5178
  });
5043
5179
  this.client.on(`job.${id}.error`, (error) => {
5044
- this.status = "failed";
5045
- this._rejectResult(error);
5180
+ this.finalize("failed", void 0, error);
5046
5181
  });
5047
5182
  this.client.on("job.completed", (data) => {
5048
5183
  const jobData = data;
5049
5184
  if (jobData.jobId === id) {
5050
- this.status = "succeeded";
5185
+ this.finalize("succeeded", jobData.result);
5051
5186
  this.emit("status", this.status);
5052
- this._resolveResult(jobData.result);
5053
5187
  }
5054
5188
  });
5055
5189
  this.client.on("job.failed", (data) => {
5056
5190
  const jobData = data;
5057
5191
  if (jobData.jobId === id) {
5058
- this.status = "failed";
5192
+ this.finalize("failed", void 0, jobData.error || new Error("Job failed"));
5059
5193
  this.emit("status", this.status);
5060
- this._rejectResult(jobData.error || new Error("Job failed"));
5061
5194
  }
5062
5195
  });
5063
5196
  this.client.on("tool.call.start", (data) => {
5064
5197
  const d = data;
5065
5198
  if (d.jobId === id) {
5199
+ this.metadata.toolCalls = this.upsertToolCall({
5200
+ callId: d.callId,
5201
+ toolName: d.toolName,
5202
+ input: sanitizeFeedbackValue(d.input),
5203
+ startedAt: d.timestamp || Date.now()
5204
+ });
5066
5205
  this.emit("toolCallStart", { callId: d.callId, toolName: d.toolName, input: d.input, timestamp: d.timestamp });
5067
5206
  }
5068
5207
  });
5069
5208
  this.client.on("tool.call.end", (data) => {
5070
5209
  const d = data;
5071
5210
  if (d.jobId === id) {
5211
+ const completedAt = d.timestamp || Date.now();
5212
+ this.metadata.toolCalls = this.upsertToolCall({
5213
+ callId: d.callId,
5214
+ toolName: d.toolName,
5215
+ output: sanitizeFeedbackValue(d.result),
5216
+ error: d.error ? truncateFeedbackString(d.error) : void 0,
5217
+ completedAt,
5218
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : void 0
5219
+ });
5072
5220
  this.emit("toolCallEnd", { callId: d.callId, toolName: d.toolName, result: d.result, error: d.error, durationMs: d.durationMs, timestamp: d.timestamp });
5073
5221
  }
5074
5222
  });
@@ -5076,12 +5224,87 @@ var JobImplementation = class {
5076
5224
  get result() {
5077
5225
  return this._resultPromise;
5078
5226
  }
5227
+ async leaveFeedback(input) {
5228
+ const sentiment = input.sentiment;
5229
+ if (sentiment !== "good" && sentiment !== "bad") {
5230
+ throw new Error('Job feedback sentiment must be "good" or "bad".');
5231
+ }
5232
+ const comment = typeof input.comment === "string" ? input.comment.trim() : "";
5233
+ const response = await this.client.call("job.feedback", {
5234
+ jobId: this.id,
5235
+ sentiment,
5236
+ comment: comment || void 0,
5237
+ metadata: this.buildFeedbackMetadata()
5238
+ });
5239
+ this.emit("feedback", response);
5240
+ return response;
5241
+ }
5079
5242
  on(event, handler) {
5080
5243
  if (!this.eventListeners.has(event)) {
5081
5244
  this.eventListeners.set(event, []);
5082
5245
  }
5083
5246
  this.eventListeners.get(event).push(handler);
5084
5247
  }
5248
+ buildFeedbackMetadata() {
5249
+ const startedAt = this.metadata.startedAt;
5250
+ const completedAt = this.metadata.completedAt;
5251
+ return {
5252
+ ...this.metadata,
5253
+ durationMs: typeof startedAt === "number" && typeof completedAt === "number" ? completedAt - startedAt : this.metadata.durationMs ?? null,
5254
+ stdout: [...this.metadata.stdout],
5255
+ stderr: [...this.metadata.stderr],
5256
+ toolCalls: this.metadata.toolCalls.slice(0, MAX_FEEDBACK_TOOL_CALLS).map((call) => ({ ...call }))
5257
+ };
5258
+ }
5259
+ markStarted(timestamp = Date.now()) {
5260
+ if (!this.metadata.startedAt) {
5261
+ this.metadata.startedAt = timestamp;
5262
+ }
5263
+ if (this.status === "queued") {
5264
+ this.status = "running";
5265
+ }
5266
+ if (this.metadata.status === "queued") {
5267
+ this.metadata.status = "running";
5268
+ }
5269
+ }
5270
+ finalize(status, result, error) {
5271
+ if (!this.metadata.startedAt) {
5272
+ this.metadata.startedAt = Date.now();
5273
+ }
5274
+ this.status = status;
5275
+ this.metadata.status = status;
5276
+ this.metadata.completedAt = this.metadata.completedAt || Date.now();
5277
+ this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
5278
+ if (result !== void 0) {
5279
+ this.metadata.result = sanitizeFeedbackValue(result);
5280
+ this._resolveResult(result);
5281
+ }
5282
+ if (error !== void 0) {
5283
+ const message = error instanceof Error ? error.message : String(error);
5284
+ this.metadata.error = truncateFeedbackString(message);
5285
+ this._rejectResult(error);
5286
+ }
5287
+ }
5288
+ upsertToolCall(next) {
5289
+ const callId = next.callId || `tool-call-${Date.now()}`;
5290
+ const existingIndex = this.metadata.toolCalls.findIndex((entry) => entry.callId === callId);
5291
+ const existing = existingIndex >= 0 ? this.metadata.toolCalls[existingIndex] : void 0;
5292
+ const merged = {
5293
+ ...existing,
5294
+ ...next,
5295
+ callId,
5296
+ toolName: next.toolName || existing?.toolName
5297
+ };
5298
+ if (merged.durationMs === void 0 && typeof merged.startedAt === "number" && typeof merged.completedAt === "number") {
5299
+ merged.durationMs = merged.completedAt - merged.startedAt;
5300
+ }
5301
+ if (existingIndex >= 0) {
5302
+ const updated = [...this.metadata.toolCalls];
5303
+ updated[existingIndex] = merged;
5304
+ return updated.slice(-MAX_FEEDBACK_TOOL_CALLS);
5305
+ }
5306
+ return [...this.metadata.toolCalls, merged].slice(-MAX_FEEDBACK_TOOL_CALLS);
5307
+ }
5085
5308
  emit(event, data) {
5086
5309
  const handlers = this.eventListeners.get(event);
5087
5310
  if (handlers) {
@@ -5222,7 +5445,7 @@ function normalizeUser(user) {
5222
5445
  permissions: Array.isArray(user.permissions) ? user.permissions : []
5223
5446
  };
5224
5447
  }
5225
- var Environment = class _Environment extends Session {
5448
+ var Environment = class extends Session {
5226
5449
  envData;
5227
5450
  _apiKey;
5228
5451
  _apiEndpoint;
@@ -5282,6 +5505,22 @@ var Environment = class _Environment extends Session {
5282
5505
  return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
5283
5506
  }
5284
5507
  }
5508
+ async controlPlaneRequest(path, options = {}) {
5509
+ const runtimeBase = this.getRuntimeBaseUrl();
5510
+ const url = `${runtimeBase}${path}`;
5511
+ const response = await fetch(url, {
5512
+ ...options,
5513
+ headers: {
5514
+ "Authorization": `Bearer ${this._apiKey}`,
5515
+ "Content-Type": "application/json",
5516
+ ...options.headers
5517
+ }
5518
+ });
5519
+ if (!response.ok) {
5520
+ throw new Error(`Control Plane API Error (${response.status}): ${await response.text()}`);
5521
+ }
5522
+ return response.json();
5523
+ }
5285
5524
  /**
5286
5525
  * Close the session and disconnect from the sandbox.
5287
5526
  *
@@ -5878,105 +6117,84 @@ var Environment = class _Environment extends Session {
5878
6117
  * ```
5879
6118
  */
5880
6119
  async recordObject(options) {
5881
- const { className, id, label, fields, relationships } = options;
5882
- const graphPath = _Environment.toGraphPath(className, id);
5883
- const existsResult = await this.graphql(
5884
- `query { model(path: "${graphPath}") { path } }`
6120
+ const results = await this.recordObjects([options]);
6121
+ return results[0];
6122
+ }
6123
+ /**
6124
+ * Batch version of `recordObject()`.
6125
+ *
6126
+ * Sends several upserts through the control-plane batch endpoint so the
6127
+ * server can collapse the graph mutations into far fewer round trips.
6128
+ */
6129
+ async recordObjects(records) {
6130
+ if (!Array.isArray(records) || records.length === 0) {
6131
+ return [];
6132
+ }
6133
+ const response = await this.controlPlaneRequest(
6134
+ `/control/environments/${this.environmentId}/records/batch`,
6135
+ {
6136
+ method: "POST",
6137
+ body: JSON.stringify({ records })
6138
+ }
5885
6139
  );
5886
- const alreadyExists = !!existsResult.data?.model;
5887
- const instResult = await this.graphql(
5888
- `mutation {
5889
- at(path: "${className}") {
5890
- instantiate(path: "${graphPath}"${label ? `, label: "${label}"` : ""}) {
5891
- model { path }
5892
- }
5893
- }
5894
- }`
6140
+ return Array.isArray(response.items) ? response.items : [];
6141
+ }
6142
+ /**
6143
+ * Queue a background record import for this environment.
6144
+ */
6145
+ async enqueueRecordImport(records, options = {}) {
6146
+ return this.controlPlaneRequest(
6147
+ `/control/environments/${this.environmentId}/record-imports`,
6148
+ {
6149
+ method: "POST",
6150
+ body: JSON.stringify({
6151
+ records,
6152
+ batchSize: options.batchSize
6153
+ })
6154
+ }
5895
6155
  );
5896
- if (instResult.errors?.length) {
5897
- throw new Error(`recordObject instantiate failed: ${instResult.errors[0].message}`);
5898
- }
5899
- const instancePath = instResult.data?.at?.instantiate?.model?.path ?? graphPath;
5900
- await this.graphql(
5901
- `mutation {
5902
- at(path: "${instancePath}") {
5903
- create_submodel(subpath: "_realId", label: "_realId") {
5904
- model { path }
5905
- }
5906
- }
5907
- }`
6156
+ }
6157
+ /**
6158
+ * List queued or completed record imports for this environment.
6159
+ */
6160
+ async listRecordImports(status) {
6161
+ const suffix = status ? `?status=${encodeURIComponent(status)}` : "";
6162
+ const response = await this.controlPlaneRequest(
6163
+ `/control/environments/${this.environmentId}/record-imports${suffix}`
5908
6164
  );
5909
- await this.graphql(
5910
- `mutation {
5911
- at(path: "${instancePath}") {
5912
- at(submodel: "_realId") {
5913
- set_string_value(value: "${id.replace(/"/g, '\\"')}") { done }
5914
- }
5915
- }
5916
- }`
6165
+ return Array.isArray(response.items) ? response.items : [];
6166
+ }
6167
+ /**
6168
+ * Fetch the latest aggregate import counters for this environment.
6169
+ */
6170
+ async getRecordImportSummary() {
6171
+ return this.controlPlaneRequest(
6172
+ `/control/environments/${this.environmentId}/record-imports/summary`
5917
6173
  );
5918
- if (fields) {
5919
- for (const [fieldName, value] of Object.entries(fields)) {
5920
- if (value === null) continue;
5921
- await this.graphql(
5922
- `mutation {
5923
- at(path: "${instancePath}") {
5924
- create_submodel(subpath: "${fieldName}", label: "${fieldName}") {
5925
- model { path }
5926
- }
5927
- }
5928
- }`
5929
- );
5930
- if (typeof value === "string") {
5931
- await this.graphql(
5932
- `mutation {
5933
- at(path: "${instancePath}") {
5934
- at(submodel: "${fieldName}") {
5935
- set_string_value(value: "${value.replace(/"/g, '\\"')}") { done }
5936
- }
5937
- }
5938
- }`
5939
- );
5940
- } else if (typeof value === "number") {
5941
- await this.graphql(
5942
- `mutation {
5943
- at(path: "${instancePath}") {
5944
- at(submodel: "${fieldName}") {
5945
- set_number_value(value: ${value}) { done }
5946
- }
5947
- }
5948
- }`
5949
- );
5950
- } else if (typeof value === "boolean") {
5951
- await this.graphql(
5952
- `mutation {
5953
- at(path: "${instancePath}") {
5954
- at(submodel: "${fieldName}") {
5955
- set_boolean_value(value: ${value}) { done }
5956
- }
5957
- }
5958
- }`
5959
- );
5960
- }
5961
- }
5962
- }
5963
- if (relationships) {
5964
- const rels = await this.getRelationships(className);
5965
- const relMap = {};
5966
- for (const rel of rels) {
5967
- const submodelLeaf = rel.local_submodel.path.includes(":") ? rel.local_submodel.path.split(":").pop() : rel.local_submodel.path;
5968
- relMap[submodelLeaf] = rel.foreign_model.path;
5969
- }
5970
- for (const [submodelName, targets] of Object.entries(relationships)) {
5971
- const foreignClass = relMap[submodelName];
5972
- const targetList = Array.isArray(targets) ? targets : [targets];
5973
- for (const target of targetList) {
5974
- const targetGraphPath = foreignClass ? _Environment.toGraphPath(foreignClass, target) : target;
5975
- await this.attach(instancePath, submodelName, targetGraphPath);
5976
- }
6174
+ }
6175
+ /**
6176
+ * Convenience helper returning queued + processing records for this environment.
6177
+ */
6178
+ async getAwaitingRecordCount() {
6179
+ const summary = await this.getRecordImportSummary();
6180
+ return summary.awaitingRecords;
6181
+ }
6182
+ /**
6183
+ * Fetch a single record import by id.
6184
+ */
6185
+ async getRecordImport(importId) {
6186
+ return this.controlPlaneRequest(`/control/record-imports/${importId}`);
6187
+ }
6188
+ /**
6189
+ * Cancel a queued/background record import.
6190
+ */
6191
+ async cancelRecordImport(importId) {
6192
+ return this.controlPlaneRequest(
6193
+ `/control/record-imports/${importId}/cancel`,
6194
+ {
6195
+ method: "POST"
5977
6196
  }
5978
- }
5979
- return { path: instancePath, id, created: !alreadyExists };
6197
+ );
5980
6198
  }
5981
6199
  // ==================== PUBLISH TOOLS ====================
5982
6200
  /**