@granular-software/sdk 0.4.8 → 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);
@@ -4503,6 +4506,9 @@ var Session = class {
4503
4506
  get document() {
4504
4507
  return this.client.doc;
4505
4508
  }
4509
+ get sessionId() {
4510
+ return this.client.currentSessionId;
4511
+ }
4506
4512
  get domainRevision() {
4507
4513
  return this.currentDomainRevision;
4508
4514
  }
@@ -4598,7 +4604,11 @@ var Session = class {
4598
4604
  if (!result.jobId) {
4599
4605
  throw new Error("Failed to submit job: no jobId returned");
4600
4606
  }
4601
- 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
+ });
4602
4612
  this.jobsMap.set(result.jobId, job);
4603
4613
  return job;
4604
4614
  }
@@ -4992,6 +5002,78 @@ import { ${allImports} } from "./sandbox-tools";
4992
5002
  }
4993
5003
  }
4994
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
+ }
4995
5077
  var JobImplementation = class {
4996
5078
  id;
4997
5079
  client;
@@ -5000,9 +5082,20 @@ var JobImplementation = class {
5000
5082
  _resolveResult;
5001
5083
  _rejectResult;
5002
5084
  eventListeners = /* @__PURE__ */ new Map();
5003
- constructor(id, client) {
5085
+ metadata;
5086
+ constructor(id, client, initialState) {
5004
5087
  this.id = id;
5005
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
+ };
5006
5099
  this._resultPromise = new Promise((resolve, reject) => {
5007
5100
  this._resolveResult = resolve;
5008
5101
  this._rejectResult = reject;
@@ -5010,69 +5103,98 @@ var JobImplementation = class {
5010
5103
  this.client.on("exec.completed", (data) => {
5011
5104
  const execData = data;
5012
5105
  if (execData.execId === id || execData.jobId === id) {
5013
- this.status = "succeeded";
5014
- this.emit("status", this.status);
5015
5106
  if (execData.error) {
5016
- this._rejectResult(execData.error);
5107
+ this.finalize("failed", void 0, execData.error);
5017
5108
  } else {
5018
- this._resolveResult(execData.result);
5109
+ this.finalize("succeeded", execData.result);
5019
5110
  }
5111
+ this.emit("status", this.status);
5020
5112
  }
5021
5113
  });
5022
5114
  this.client.on("exec.progress", (data) => {
5023
5115
  const progressData = data;
5024
5116
  if (progressData.execId === id || progressData.jobId === id) {
5025
5117
  if (progressData.stdout) {
5118
+ this.markStarted();
5119
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(progressData.stdout)].slice(-100);
5026
5120
  this.emit("stdout", progressData.stdout);
5027
5121
  }
5028
5122
  if (progressData.stderr) {
5123
+ this.markStarted();
5124
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(progressData.stderr)].slice(-100);
5029
5125
  this.emit("stderr", progressData.stderr);
5030
5126
  }
5031
5127
  }
5032
5128
  });
5033
5129
  this.client.on(`job.${id}.status`, (status) => {
5034
- this.status = status;
5035
- 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);
5036
5143
  });
5037
5144
  this.client.on(`job.${id}.stdout`, (line) => {
5145
+ this.markStarted();
5146
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(String(line))].slice(-100);
5038
5147
  this.emit("stdout", line);
5039
5148
  });
5040
5149
  this.client.on(`job.${id}.stderr`, (line) => {
5150
+ this.markStarted();
5151
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(String(line))].slice(-100);
5041
5152
  this.emit("stderr", line);
5042
5153
  });
5043
5154
  this.client.on(`job.${id}.result`, (result) => {
5044
- this.status = "succeeded";
5045
- this._resolveResult(result);
5155
+ this.finalize("succeeded", result);
5046
5156
  });
5047
5157
  this.client.on(`job.${id}.error`, (error) => {
5048
- this.status = "failed";
5049
- this._rejectResult(error);
5158
+ this.finalize("failed", void 0, error);
5050
5159
  });
5051
5160
  this.client.on("job.completed", (data) => {
5052
5161
  const jobData = data;
5053
5162
  if (jobData.jobId === id) {
5054
- this.status = "succeeded";
5163
+ this.finalize("succeeded", jobData.result);
5055
5164
  this.emit("status", this.status);
5056
- this._resolveResult(jobData.result);
5057
5165
  }
5058
5166
  });
5059
5167
  this.client.on("job.failed", (data) => {
5060
5168
  const jobData = data;
5061
5169
  if (jobData.jobId === id) {
5062
- this.status = "failed";
5170
+ this.finalize("failed", void 0, jobData.error || new Error("Job failed"));
5063
5171
  this.emit("status", this.status);
5064
- this._rejectResult(jobData.error || new Error("Job failed"));
5065
5172
  }
5066
5173
  });
5067
5174
  this.client.on("tool.call.start", (data) => {
5068
5175
  const d = data;
5069
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
+ });
5070
5183
  this.emit("toolCallStart", { callId: d.callId, toolName: d.toolName, input: d.input, timestamp: d.timestamp });
5071
5184
  }
5072
5185
  });
5073
5186
  this.client.on("tool.call.end", (data) => {
5074
5187
  const d = data;
5075
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
+ });
5076
5198
  this.emit("toolCallEnd", { callId: d.callId, toolName: d.toolName, result: d.result, error: d.error, durationMs: d.durationMs, timestamp: d.timestamp });
5077
5199
  }
5078
5200
  });
@@ -5080,12 +5202,87 @@ var JobImplementation = class {
5080
5202
  get result() {
5081
5203
  return this._resultPromise;
5082
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
+ }
5083
5220
  on(event, handler) {
5084
5221
  if (!this.eventListeners.has(event)) {
5085
5222
  this.eventListeners.set(event, []);
5086
5223
  }
5087
5224
  this.eventListeners.get(event).push(handler);
5088
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
+ }
5089
5286
  emit(event, data) {
5090
5287
  const handlers = this.eventListeners.get(event);
5091
5288
  if (handlers) {
@@ -5226,7 +5423,7 @@ function normalizeUser(user) {
5226
5423
  permissions: Array.isArray(user.permissions) ? user.permissions : []
5227
5424
  };
5228
5425
  }
5229
- var Environment = class _Environment extends Session {
5426
+ var Environment = class extends Session {
5230
5427
  envData;
5231
5428
  _apiKey;
5232
5429
  _apiEndpoint;
@@ -5286,6 +5483,22 @@ var Environment = class _Environment extends Session {
5286
5483
  return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
5287
5484
  }
5288
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
+ }
5289
5502
  /**
5290
5503
  * Close the session and disconnect from the sandbox.
5291
5504
  *
@@ -5882,105 +6095,84 @@ var Environment = class _Environment extends Session {
5882
6095
  * ```
5883
6096
  */
5884
6097
  async recordObject(options) {
5885
- const { className, id, label, fields, relationships } = options;
5886
- const graphPath = _Environment.toGraphPath(className, id);
5887
- const existsResult = await this.graphql(
5888
- `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
+ }
5889
6117
  );
5890
- const alreadyExists = !!existsResult.data?.model;
5891
- const instResult = await this.graphql(
5892
- `mutation {
5893
- at(path: "${className}") {
5894
- instantiate(path: "${graphPath}"${label ? `, label: "${label}"` : ""}) {
5895
- model { path }
5896
- }
5897
- }
5898
- }`
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
+ }
5899
6133
  );
5900
- if (instResult.errors?.length) {
5901
- throw new Error(`recordObject instantiate failed: ${instResult.errors[0].message}`);
5902
- }
5903
- const instancePath = instResult.data?.at?.instantiate?.model?.path ?? graphPath;
5904
- await this.graphql(
5905
- `mutation {
5906
- at(path: "${instancePath}") {
5907
- create_submodel(subpath: "_realId", label: "_realId") {
5908
- model { path }
5909
- }
5910
- }
5911
- }`
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}`
5912
6142
  );
5913
- await this.graphql(
5914
- `mutation {
5915
- at(path: "${instancePath}") {
5916
- at(submodel: "_realId") {
5917
- set_string_value(value: "${id.replace(/"/g, '\\"')}") { done }
5918
- }
5919
- }
5920
- }`
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`
5921
6151
  );
5922
- if (fields) {
5923
- for (const [fieldName, value] of Object.entries(fields)) {
5924
- if (value === null) continue;
5925
- await this.graphql(
5926
- `mutation {
5927
- at(path: "${instancePath}") {
5928
- create_submodel(subpath: "${fieldName}", label: "${fieldName}") {
5929
- model { path }
5930
- }
5931
- }
5932
- }`
5933
- );
5934
- if (typeof value === "string") {
5935
- await this.graphql(
5936
- `mutation {
5937
- at(path: "${instancePath}") {
5938
- at(submodel: "${fieldName}") {
5939
- set_string_value(value: "${value.replace(/"/g, '\\"')}") { done }
5940
- }
5941
- }
5942
- }`
5943
- );
5944
- } else if (typeof value === "number") {
5945
- await this.graphql(
5946
- `mutation {
5947
- at(path: "${instancePath}") {
5948
- at(submodel: "${fieldName}") {
5949
- set_number_value(value: ${value}) { done }
5950
- }
5951
- }
5952
- }`
5953
- );
5954
- } else if (typeof value === "boolean") {
5955
- await this.graphql(
5956
- `mutation {
5957
- at(path: "${instancePath}") {
5958
- at(submodel: "${fieldName}") {
5959
- set_boolean_value(value: ${value}) { done }
5960
- }
5961
- }
5962
- }`
5963
- );
5964
- }
5965
- }
5966
- }
5967
- if (relationships) {
5968
- const rels = await this.getRelationships(className);
5969
- const relMap = {};
5970
- for (const rel of rels) {
5971
- const submodelLeaf = rel.local_submodel.path.includes(":") ? rel.local_submodel.path.split(":").pop() : rel.local_submodel.path;
5972
- relMap[submodelLeaf] = rel.foreign_model.path;
5973
- }
5974
- for (const [submodelName, targets] of Object.entries(relationships)) {
5975
- const foreignClass = relMap[submodelName];
5976
- const targetList = Array.isArray(targets) ? targets : [targets];
5977
- for (const target of targetList) {
5978
- const targetGraphPath = foreignClass ? _Environment.toGraphPath(foreignClass, target) : target;
5979
- await this.attach(instancePath, submodelName, targetGraphPath);
5980
- }
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"
5981
6174
  }
5982
- }
5983
- return { path: instancePath, id, created: !alreadyExists };
6175
+ );
5984
6176
  }
5985
6177
  // ==================== PUBLISH TOOLS ====================
5986
6178
  /**