@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.mjs CHANGED
@@ -3950,6 +3950,9 @@ var WSClient = class {
3950
3950
  this.sessionId = options.sessionId;
3951
3951
  this.token = options.token;
3952
3952
  }
3953
+ get currentSessionId() {
3954
+ return this.sessionId;
3955
+ }
3953
3956
  clearTokenRefreshTimer() {
3954
3957
  if (this.tokenRefreshTimer) {
3955
3958
  clearTimeout(this.tokenRefreshTimer);
@@ -4474,6 +4477,18 @@ var Session = class {
4474
4477
  this.setupEventHandlers();
4475
4478
  this.setupToolInvokeHandler();
4476
4479
  }
4480
+ extractDomainRevisionFromDoc(doc) {
4481
+ const domain = doc?.domain;
4482
+ const active = domain?.active;
4483
+ if (typeof active === "string" && active.trim()) {
4484
+ return active;
4485
+ }
4486
+ const nestedRevision = active?.domainRevision;
4487
+ if (typeof nestedRevision === "string" && nestedRevision.trim()) {
4488
+ return nestedRevision;
4489
+ }
4490
+ return null;
4491
+ }
4477
4492
  buildLegacyEffectContext() {
4478
4493
  return {
4479
4494
  effectClientId: this.clientId,
@@ -4491,6 +4506,9 @@ var Session = class {
4491
4506
  get document() {
4492
4507
  return this.client.doc;
4493
4508
  }
4509
+ get sessionId() {
4510
+ return this.client.currentSessionId;
4511
+ }
4494
4512
  get domainRevision() {
4495
4513
  return this.currentDomainRevision;
4496
4514
  }
@@ -4568,7 +4586,14 @@ var Session = class {
4568
4586
  * execute locally and return the result to the sandbox.
4569
4587
  */
4570
4588
  async submitJob(code, domainRevision) {
4571
- const revision = domainRevision || this.currentDomainRevision || this.client.doc?.domain?.active || void 0;
4589
+ let revision = domainRevision || this.currentDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4590
+ if (!revision) {
4591
+ try {
4592
+ const summary = await this.getDomain();
4593
+ revision = summary.activeDomainRevision || this.extractDomainRevisionFromDoc(this.client.doc) || void 0;
4594
+ } catch {
4595
+ }
4596
+ }
4572
4597
  if (!revision) {
4573
4598
  throw new Error("No domain revision available. Register live effects or ensure the build schema is activated.");
4574
4599
  }
@@ -4579,7 +4604,11 @@ var Session = class {
4579
4604
  if (!result.jobId) {
4580
4605
  throw new Error("Failed to submit job: no jobId returned");
4581
4606
  }
4582
- const job = new JobImplementation(result.jobId, this.client);
4607
+ const job = new JobImplementation(result.jobId, this.client, {
4608
+ code,
4609
+ domainRevision: revision,
4610
+ createdAt: Date.now()
4611
+ });
4583
4612
  this.jobsMap.set(result.jobId, job);
4584
4613
  return job;
4585
4614
  }
@@ -4693,7 +4722,13 @@ var Session = class {
4693
4722
  * Get the current domain state and available tools
4694
4723
  */
4695
4724
  async getDomain() {
4696
- return this.client.call("domain.getSummary", {});
4725
+ const summary = await this.client.call("domain.getSummary", {});
4726
+ if (summary.activeDomainRevision) {
4727
+ this.currentDomainRevision = summary.activeDomainRevision;
4728
+ } else {
4729
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(this.client.doc);
4730
+ }
4731
+ return summary;
4697
4732
  }
4698
4733
  /**
4699
4734
  * Fetch a domain package part from the backend (no fallback).
@@ -4911,6 +4946,7 @@ import { ${allImports} } from "./sandbox-tools";
4911
4946
  }
4912
4947
  setupEventHandlers() {
4913
4948
  this.client.on("sync", (doc) => {
4949
+ this.currentDomainRevision = this.extractDomainRevisionFromDoc(doc);
4914
4950
  this.emit("sync", doc);
4915
4951
  this.checkForToolChanges();
4916
4952
  });
@@ -4966,6 +5002,78 @@ import { ${allImports} } from "./sandbox-tools";
4966
5002
  }
4967
5003
  }
4968
5004
  };
5005
+ var MAX_FEEDBACK_STRING_LENGTH = 16e3;
5006
+ var MAX_FEEDBACK_TOOL_CALLS = 50;
5007
+ var MAX_FEEDBACK_ARRAY_ITEMS = 50;
5008
+ var MAX_FEEDBACK_OBJECT_KEYS = 50;
5009
+ var MAX_FEEDBACK_DEPTH = 6;
5010
+ function truncateFeedbackString(value, maxLength = MAX_FEEDBACK_STRING_LENGTH) {
5011
+ if (value.length <= maxLength) {
5012
+ return value;
5013
+ }
5014
+ return `${value.slice(0, maxLength)}...[truncated ${value.length - maxLength} chars]`;
5015
+ }
5016
+ function normalizeJobStatus(status) {
5017
+ if (typeof status !== "string") {
5018
+ return "queued";
5019
+ }
5020
+ switch (status) {
5021
+ case "completed":
5022
+ return "succeeded";
5023
+ case "failed":
5024
+ case "running":
5025
+ case "queued":
5026
+ case "succeeded":
5027
+ case "timeout":
5028
+ case "canceled":
5029
+ return status;
5030
+ default:
5031
+ return "queued";
5032
+ }
5033
+ }
5034
+ function sanitizeFeedbackValue(value, depth = 0, seen = /* @__PURE__ */ new WeakSet()) {
5035
+ if (depth >= MAX_FEEDBACK_DEPTH) {
5036
+ return "[Max depth reached]";
5037
+ }
5038
+ if (value === null || value === void 0) {
5039
+ return null;
5040
+ }
5041
+ if (typeof value === "string") {
5042
+ return truncateFeedbackString(value);
5043
+ }
5044
+ if (typeof value === "number" || typeof value === "boolean") {
5045
+ return value;
5046
+ }
5047
+ if (typeof value === "bigint") {
5048
+ return truncateFeedbackString(value.toString());
5049
+ }
5050
+ if (value instanceof Error) {
5051
+ return {
5052
+ name: truncateFeedbackString(value.name),
5053
+ message: truncateFeedbackString(value.message),
5054
+ stack: truncateFeedbackString(value.stack || "")
5055
+ };
5056
+ }
5057
+ if (value instanceof Date) {
5058
+ return value.toISOString();
5059
+ }
5060
+ if (Array.isArray(value)) {
5061
+ return value.slice(0, MAX_FEEDBACK_ARRAY_ITEMS).map((entry) => sanitizeFeedbackValue(entry, depth + 1, seen));
5062
+ }
5063
+ if (typeof value === "object") {
5064
+ if (seen.has(value)) {
5065
+ return "[Circular]";
5066
+ }
5067
+ seen.add(value);
5068
+ const entries = Object.entries(value).slice(0, MAX_FEEDBACK_OBJECT_KEYS);
5069
+ const sanitized = {};
5070
+ for (const [key, entryValue] of entries) {
5071
+ sanitized[key] = sanitizeFeedbackValue(entryValue, depth + 1, seen);
5072
+ }
5073
+ return sanitized;
5074
+ }
5075
+ return truncateFeedbackString(String(value));
5076
+ }
4969
5077
  var JobImplementation = class {
4970
5078
  id;
4971
5079
  client;
@@ -4974,9 +5082,20 @@ var JobImplementation = class {
4974
5082
  _resolveResult;
4975
5083
  _rejectResult;
4976
5084
  eventListeners = /* @__PURE__ */ new Map();
4977
- constructor(id, client) {
5085
+ metadata;
5086
+ constructor(id, client, initialState) {
4978
5087
  this.id = id;
4979
5088
  this.client = client;
5089
+ this.metadata = {
5090
+ source: "sdk",
5091
+ status: "queued",
5092
+ code: truncateFeedbackString(initialState.code),
5093
+ domainRevision: initialState.domainRevision,
5094
+ createdAt: initialState.createdAt,
5095
+ stdout: [],
5096
+ stderr: [],
5097
+ toolCalls: []
5098
+ };
4980
5099
  this._resultPromise = new Promise((resolve, reject) => {
4981
5100
  this._resolveResult = resolve;
4982
5101
  this._rejectResult = reject;
@@ -4984,69 +5103,98 @@ var JobImplementation = class {
4984
5103
  this.client.on("exec.completed", (data) => {
4985
5104
  const execData = data;
4986
5105
  if (execData.execId === id || execData.jobId === id) {
4987
- this.status = "succeeded";
4988
- this.emit("status", this.status);
4989
5106
  if (execData.error) {
4990
- this._rejectResult(execData.error);
5107
+ this.finalize("failed", void 0, execData.error);
4991
5108
  } else {
4992
- this._resolveResult(execData.result);
5109
+ this.finalize("succeeded", execData.result);
4993
5110
  }
5111
+ this.emit("status", this.status);
4994
5112
  }
4995
5113
  });
4996
5114
  this.client.on("exec.progress", (data) => {
4997
5115
  const progressData = data;
4998
5116
  if (progressData.execId === id || progressData.jobId === id) {
4999
5117
  if (progressData.stdout) {
5118
+ this.markStarted();
5119
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(progressData.stdout)].slice(-100);
5000
5120
  this.emit("stdout", progressData.stdout);
5001
5121
  }
5002
5122
  if (progressData.stderr) {
5123
+ this.markStarted();
5124
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(progressData.stderr)].slice(-100);
5003
5125
  this.emit("stderr", progressData.stderr);
5004
5126
  }
5005
5127
  }
5006
5128
  });
5007
5129
  this.client.on(`job.${id}.status`, (status) => {
5008
- this.status = status;
5009
- this.emit("status", status);
5130
+ const normalizedStatus = normalizeJobStatus(status);
5131
+ this.status = normalizedStatus;
5132
+ this.metadata.status = normalizedStatus;
5133
+ if (normalizedStatus === "running") {
5134
+ this.markStarted();
5135
+ }
5136
+ if (normalizedStatus === "failed" || normalizedStatus === "timeout" || normalizedStatus === "canceled") {
5137
+ this.finalize(normalizedStatus);
5138
+ }
5139
+ if (normalizedStatus === "succeeded") {
5140
+ this.finalize("succeeded");
5141
+ }
5142
+ this.emit("status", normalizedStatus);
5010
5143
  });
5011
5144
  this.client.on(`job.${id}.stdout`, (line) => {
5145
+ this.markStarted();
5146
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(String(line))].slice(-100);
5012
5147
  this.emit("stdout", line);
5013
5148
  });
5014
5149
  this.client.on(`job.${id}.stderr`, (line) => {
5150
+ this.markStarted();
5151
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(String(line))].slice(-100);
5015
5152
  this.emit("stderr", line);
5016
5153
  });
5017
5154
  this.client.on(`job.${id}.result`, (result) => {
5018
- this.status = "succeeded";
5019
- this._resolveResult(result);
5155
+ this.finalize("succeeded", result);
5020
5156
  });
5021
5157
  this.client.on(`job.${id}.error`, (error) => {
5022
- this.status = "failed";
5023
- this._rejectResult(error);
5158
+ this.finalize("failed", void 0, error);
5024
5159
  });
5025
5160
  this.client.on("job.completed", (data) => {
5026
5161
  const jobData = data;
5027
5162
  if (jobData.jobId === id) {
5028
- this.status = "succeeded";
5163
+ this.finalize("succeeded", jobData.result);
5029
5164
  this.emit("status", this.status);
5030
- this._resolveResult(jobData.result);
5031
5165
  }
5032
5166
  });
5033
5167
  this.client.on("job.failed", (data) => {
5034
5168
  const jobData = data;
5035
5169
  if (jobData.jobId === id) {
5036
- this.status = "failed";
5170
+ this.finalize("failed", void 0, jobData.error || new Error("Job failed"));
5037
5171
  this.emit("status", this.status);
5038
- this._rejectResult(jobData.error || new Error("Job failed"));
5039
5172
  }
5040
5173
  });
5041
5174
  this.client.on("tool.call.start", (data) => {
5042
5175
  const d = data;
5043
5176
  if (d.jobId === id) {
5177
+ this.metadata.toolCalls = this.upsertToolCall({
5178
+ callId: d.callId,
5179
+ toolName: d.toolName,
5180
+ input: sanitizeFeedbackValue(d.input),
5181
+ startedAt: d.timestamp || Date.now()
5182
+ });
5044
5183
  this.emit("toolCallStart", { callId: d.callId, toolName: d.toolName, input: d.input, timestamp: d.timestamp });
5045
5184
  }
5046
5185
  });
5047
5186
  this.client.on("tool.call.end", (data) => {
5048
5187
  const d = data;
5049
5188
  if (d.jobId === id) {
5189
+ const completedAt = d.timestamp || Date.now();
5190
+ this.metadata.toolCalls = this.upsertToolCall({
5191
+ callId: d.callId,
5192
+ toolName: d.toolName,
5193
+ output: sanitizeFeedbackValue(d.result),
5194
+ error: d.error ? truncateFeedbackString(d.error) : void 0,
5195
+ completedAt,
5196
+ durationMs: typeof d.durationMs === "number" ? d.durationMs : void 0
5197
+ });
5050
5198
  this.emit("toolCallEnd", { callId: d.callId, toolName: d.toolName, result: d.result, error: d.error, durationMs: d.durationMs, timestamp: d.timestamp });
5051
5199
  }
5052
5200
  });
@@ -5054,12 +5202,87 @@ var JobImplementation = class {
5054
5202
  get result() {
5055
5203
  return this._resultPromise;
5056
5204
  }
5205
+ async leaveFeedback(input) {
5206
+ const sentiment = input.sentiment;
5207
+ if (sentiment !== "good" && sentiment !== "bad") {
5208
+ throw new Error('Job feedback sentiment must be "good" or "bad".');
5209
+ }
5210
+ const comment = typeof input.comment === "string" ? input.comment.trim() : "";
5211
+ const response = await this.client.call("job.feedback", {
5212
+ jobId: this.id,
5213
+ sentiment,
5214
+ comment: comment || void 0,
5215
+ metadata: this.buildFeedbackMetadata()
5216
+ });
5217
+ this.emit("feedback", response);
5218
+ return response;
5219
+ }
5057
5220
  on(event, handler) {
5058
5221
  if (!this.eventListeners.has(event)) {
5059
5222
  this.eventListeners.set(event, []);
5060
5223
  }
5061
5224
  this.eventListeners.get(event).push(handler);
5062
5225
  }
5226
+ buildFeedbackMetadata() {
5227
+ const startedAt = this.metadata.startedAt;
5228
+ const completedAt = this.metadata.completedAt;
5229
+ return {
5230
+ ...this.metadata,
5231
+ durationMs: typeof startedAt === "number" && typeof completedAt === "number" ? completedAt - startedAt : this.metadata.durationMs ?? null,
5232
+ stdout: [...this.metadata.stdout],
5233
+ stderr: [...this.metadata.stderr],
5234
+ toolCalls: this.metadata.toolCalls.slice(0, MAX_FEEDBACK_TOOL_CALLS).map((call) => ({ ...call }))
5235
+ };
5236
+ }
5237
+ markStarted(timestamp = Date.now()) {
5238
+ if (!this.metadata.startedAt) {
5239
+ this.metadata.startedAt = timestamp;
5240
+ }
5241
+ if (this.status === "queued") {
5242
+ this.status = "running";
5243
+ }
5244
+ if (this.metadata.status === "queued") {
5245
+ this.metadata.status = "running";
5246
+ }
5247
+ }
5248
+ finalize(status, result, error) {
5249
+ if (!this.metadata.startedAt) {
5250
+ this.metadata.startedAt = Date.now();
5251
+ }
5252
+ this.status = status;
5253
+ this.metadata.status = status;
5254
+ this.metadata.completedAt = this.metadata.completedAt || Date.now();
5255
+ this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
5256
+ if (result !== void 0) {
5257
+ this.metadata.result = sanitizeFeedbackValue(result);
5258
+ this._resolveResult(result);
5259
+ }
5260
+ if (error !== void 0) {
5261
+ const message = error instanceof Error ? error.message : String(error);
5262
+ this.metadata.error = truncateFeedbackString(message);
5263
+ this._rejectResult(error);
5264
+ }
5265
+ }
5266
+ upsertToolCall(next) {
5267
+ const callId = next.callId || `tool-call-${Date.now()}`;
5268
+ const existingIndex = this.metadata.toolCalls.findIndex((entry) => entry.callId === callId);
5269
+ const existing = existingIndex >= 0 ? this.metadata.toolCalls[existingIndex] : void 0;
5270
+ const merged = {
5271
+ ...existing,
5272
+ ...next,
5273
+ callId,
5274
+ toolName: next.toolName || existing?.toolName
5275
+ };
5276
+ if (merged.durationMs === void 0 && typeof merged.startedAt === "number" && typeof merged.completedAt === "number") {
5277
+ merged.durationMs = merged.completedAt - merged.startedAt;
5278
+ }
5279
+ if (existingIndex >= 0) {
5280
+ const updated = [...this.metadata.toolCalls];
5281
+ updated[existingIndex] = merged;
5282
+ return updated.slice(-MAX_FEEDBACK_TOOL_CALLS);
5283
+ }
5284
+ return [...this.metadata.toolCalls, merged].slice(-MAX_FEEDBACK_TOOL_CALLS);
5285
+ }
5063
5286
  emit(event, data) {
5064
5287
  const handlers = this.eventListeners.get(event);
5065
5288
  if (handlers) {
@@ -5200,7 +5423,7 @@ function normalizeUser(user) {
5200
5423
  permissions: Array.isArray(user.permissions) ? user.permissions : []
5201
5424
  };
5202
5425
  }
5203
- var Environment = class _Environment extends Session {
5426
+ var Environment = class extends Session {
5204
5427
  envData;
5205
5428
  _apiKey;
5206
5429
  _apiEndpoint;
@@ -5260,6 +5483,22 @@ var Environment = class _Environment extends Session {
5260
5483
  return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
5261
5484
  }
5262
5485
  }
5486
+ async controlPlaneRequest(path, options = {}) {
5487
+ const runtimeBase = this.getRuntimeBaseUrl();
5488
+ const url = `${runtimeBase}${path}`;
5489
+ const response = await fetch(url, {
5490
+ ...options,
5491
+ headers: {
5492
+ "Authorization": `Bearer ${this._apiKey}`,
5493
+ "Content-Type": "application/json",
5494
+ ...options.headers
5495
+ }
5496
+ });
5497
+ if (!response.ok) {
5498
+ throw new Error(`Control Plane API Error (${response.status}): ${await response.text()}`);
5499
+ }
5500
+ return response.json();
5501
+ }
5263
5502
  /**
5264
5503
  * Close the session and disconnect from the sandbox.
5265
5504
  *
@@ -5856,105 +6095,84 @@ var Environment = class _Environment extends Session {
5856
6095
  * ```
5857
6096
  */
5858
6097
  async recordObject(options) {
5859
- const { className, id, label, fields, relationships } = options;
5860
- const graphPath = _Environment.toGraphPath(className, id);
5861
- const existsResult = await this.graphql(
5862
- `query { model(path: "${graphPath}") { path } }`
6098
+ const results = await this.recordObjects([options]);
6099
+ return results[0];
6100
+ }
6101
+ /**
6102
+ * Batch version of `recordObject()`.
6103
+ *
6104
+ * Sends several upserts through the control-plane batch endpoint so the
6105
+ * server can collapse the graph mutations into far fewer round trips.
6106
+ */
6107
+ async recordObjects(records) {
6108
+ if (!Array.isArray(records) || records.length === 0) {
6109
+ return [];
6110
+ }
6111
+ const response = await this.controlPlaneRequest(
6112
+ `/control/environments/${this.environmentId}/records/batch`,
6113
+ {
6114
+ method: "POST",
6115
+ body: JSON.stringify({ records })
6116
+ }
5863
6117
  );
5864
- const alreadyExists = !!existsResult.data?.model;
5865
- const instResult = await this.graphql(
5866
- `mutation {
5867
- at(path: "${className}") {
5868
- instantiate(path: "${graphPath}"${label ? `, label: "${label}"` : ""}) {
5869
- model { path }
5870
- }
5871
- }
5872
- }`
6118
+ return Array.isArray(response.items) ? response.items : [];
6119
+ }
6120
+ /**
6121
+ * Queue a background record import for this environment.
6122
+ */
6123
+ async enqueueRecordImport(records, options = {}) {
6124
+ return this.controlPlaneRequest(
6125
+ `/control/environments/${this.environmentId}/record-imports`,
6126
+ {
6127
+ method: "POST",
6128
+ body: JSON.stringify({
6129
+ records,
6130
+ batchSize: options.batchSize
6131
+ })
6132
+ }
5873
6133
  );
5874
- if (instResult.errors?.length) {
5875
- throw new Error(`recordObject instantiate failed: ${instResult.errors[0].message}`);
5876
- }
5877
- const instancePath = instResult.data?.at?.instantiate?.model?.path ?? graphPath;
5878
- await this.graphql(
5879
- `mutation {
5880
- at(path: "${instancePath}") {
5881
- create_submodel(subpath: "_realId", label: "_realId") {
5882
- model { path }
5883
- }
5884
- }
5885
- }`
6134
+ }
6135
+ /**
6136
+ * List queued or completed record imports for this environment.
6137
+ */
6138
+ async listRecordImports(status) {
6139
+ const suffix = status ? `?status=${encodeURIComponent(status)}` : "";
6140
+ const response = await this.controlPlaneRequest(
6141
+ `/control/environments/${this.environmentId}/record-imports${suffix}`
5886
6142
  );
5887
- await this.graphql(
5888
- `mutation {
5889
- at(path: "${instancePath}") {
5890
- at(submodel: "_realId") {
5891
- set_string_value(value: "${id.replace(/"/g, '\\"')}") { done }
5892
- }
5893
- }
5894
- }`
6143
+ return Array.isArray(response.items) ? response.items : [];
6144
+ }
6145
+ /**
6146
+ * Fetch the latest aggregate import counters for this environment.
6147
+ */
6148
+ async getRecordImportSummary() {
6149
+ return this.controlPlaneRequest(
6150
+ `/control/environments/${this.environmentId}/record-imports/summary`
5895
6151
  );
5896
- if (fields) {
5897
- for (const [fieldName, value] of Object.entries(fields)) {
5898
- if (value === null) continue;
5899
- await this.graphql(
5900
- `mutation {
5901
- at(path: "${instancePath}") {
5902
- create_submodel(subpath: "${fieldName}", label: "${fieldName}") {
5903
- model { path }
5904
- }
5905
- }
5906
- }`
5907
- );
5908
- if (typeof value === "string") {
5909
- await this.graphql(
5910
- `mutation {
5911
- at(path: "${instancePath}") {
5912
- at(submodel: "${fieldName}") {
5913
- set_string_value(value: "${value.replace(/"/g, '\\"')}") { done }
5914
- }
5915
- }
5916
- }`
5917
- );
5918
- } else if (typeof value === "number") {
5919
- await this.graphql(
5920
- `mutation {
5921
- at(path: "${instancePath}") {
5922
- at(submodel: "${fieldName}") {
5923
- set_number_value(value: ${value}) { done }
5924
- }
5925
- }
5926
- }`
5927
- );
5928
- } else if (typeof value === "boolean") {
5929
- await this.graphql(
5930
- `mutation {
5931
- at(path: "${instancePath}") {
5932
- at(submodel: "${fieldName}") {
5933
- set_boolean_value(value: ${value}) { done }
5934
- }
5935
- }
5936
- }`
5937
- );
5938
- }
5939
- }
5940
- }
5941
- if (relationships) {
5942
- const rels = await this.getRelationships(className);
5943
- const relMap = {};
5944
- for (const rel of rels) {
5945
- const submodelLeaf = rel.local_submodel.path.includes(":") ? rel.local_submodel.path.split(":").pop() : rel.local_submodel.path;
5946
- relMap[submodelLeaf] = rel.foreign_model.path;
5947
- }
5948
- for (const [submodelName, targets] of Object.entries(relationships)) {
5949
- const foreignClass = relMap[submodelName];
5950
- const targetList = Array.isArray(targets) ? targets : [targets];
5951
- for (const target of targetList) {
5952
- const targetGraphPath = foreignClass ? _Environment.toGraphPath(foreignClass, target) : target;
5953
- await this.attach(instancePath, submodelName, targetGraphPath);
5954
- }
6152
+ }
6153
+ /**
6154
+ * Convenience helper returning queued + processing records for this environment.
6155
+ */
6156
+ async getAwaitingRecordCount() {
6157
+ const summary = await this.getRecordImportSummary();
6158
+ return summary.awaitingRecords;
6159
+ }
6160
+ /**
6161
+ * Fetch a single record import by id.
6162
+ */
6163
+ async getRecordImport(importId) {
6164
+ return this.controlPlaneRequest(`/control/record-imports/${importId}`);
6165
+ }
6166
+ /**
6167
+ * Cancel a queued/background record import.
6168
+ */
6169
+ async cancelRecordImport(importId) {
6170
+ return this.controlPlaneRequest(
6171
+ `/control/record-imports/${importId}/cancel`,
6172
+ {
6173
+ method: "POST"
5955
6174
  }
5956
- }
5957
- return { path: instancePath, id, created: !alreadyExists };
6175
+ );
5958
6176
  }
5959
6177
  // ==================== PUBLISH TOOLS ====================
5960
6178
  /**