@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.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);
@@ -4525,6 +4528,9 @@ var Session = class {
4525
4528
  get document() {
4526
4529
  return this.client.doc;
4527
4530
  }
4531
+ get sessionId() {
4532
+ return this.client.currentSessionId;
4533
+ }
4528
4534
  get domainRevision() {
4529
4535
  return this.currentDomainRevision;
4530
4536
  }
@@ -4620,7 +4626,11 @@ var Session = class {
4620
4626
  if (!result.jobId) {
4621
4627
  throw new Error("Failed to submit job: no jobId returned");
4622
4628
  }
4623
- 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
+ });
4624
4634
  this.jobsMap.set(result.jobId, job);
4625
4635
  return job;
4626
4636
  }
@@ -5014,6 +5024,78 @@ import { ${allImports} } from "./sandbox-tools";
5014
5024
  }
5015
5025
  }
5016
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
+ }
5017
5099
  var JobImplementation = class {
5018
5100
  id;
5019
5101
  client;
@@ -5022,9 +5104,20 @@ var JobImplementation = class {
5022
5104
  _resolveResult;
5023
5105
  _rejectResult;
5024
5106
  eventListeners = /* @__PURE__ */ new Map();
5025
- constructor(id, client) {
5107
+ metadata;
5108
+ constructor(id, client, initialState) {
5026
5109
  this.id = id;
5027
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
+ };
5028
5121
  this._resultPromise = new Promise((resolve, reject) => {
5029
5122
  this._resolveResult = resolve;
5030
5123
  this._rejectResult = reject;
@@ -5032,69 +5125,98 @@ var JobImplementation = class {
5032
5125
  this.client.on("exec.completed", (data) => {
5033
5126
  const execData = data;
5034
5127
  if (execData.execId === id || execData.jobId === id) {
5035
- this.status = "succeeded";
5036
- this.emit("status", this.status);
5037
5128
  if (execData.error) {
5038
- this._rejectResult(execData.error);
5129
+ this.finalize("failed", void 0, execData.error);
5039
5130
  } else {
5040
- this._resolveResult(execData.result);
5131
+ this.finalize("succeeded", execData.result);
5041
5132
  }
5133
+ this.emit("status", this.status);
5042
5134
  }
5043
5135
  });
5044
5136
  this.client.on("exec.progress", (data) => {
5045
5137
  const progressData = data;
5046
5138
  if (progressData.execId === id || progressData.jobId === id) {
5047
5139
  if (progressData.stdout) {
5140
+ this.markStarted();
5141
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(progressData.stdout)].slice(-100);
5048
5142
  this.emit("stdout", progressData.stdout);
5049
5143
  }
5050
5144
  if (progressData.stderr) {
5145
+ this.markStarted();
5146
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(progressData.stderr)].slice(-100);
5051
5147
  this.emit("stderr", progressData.stderr);
5052
5148
  }
5053
5149
  }
5054
5150
  });
5055
5151
  this.client.on(`job.${id}.status`, (status) => {
5056
- this.status = status;
5057
- 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);
5058
5165
  });
5059
5166
  this.client.on(`job.${id}.stdout`, (line) => {
5167
+ this.markStarted();
5168
+ this.metadata.stdout = [...this.metadata.stdout, truncateFeedbackString(String(line))].slice(-100);
5060
5169
  this.emit("stdout", line);
5061
5170
  });
5062
5171
  this.client.on(`job.${id}.stderr`, (line) => {
5172
+ this.markStarted();
5173
+ this.metadata.stderr = [...this.metadata.stderr, truncateFeedbackString(String(line))].slice(-100);
5063
5174
  this.emit("stderr", line);
5064
5175
  });
5065
5176
  this.client.on(`job.${id}.result`, (result) => {
5066
- this.status = "succeeded";
5067
- this._resolveResult(result);
5177
+ this.finalize("succeeded", result);
5068
5178
  });
5069
5179
  this.client.on(`job.${id}.error`, (error) => {
5070
- this.status = "failed";
5071
- this._rejectResult(error);
5180
+ this.finalize("failed", void 0, error);
5072
5181
  });
5073
5182
  this.client.on("job.completed", (data) => {
5074
5183
  const jobData = data;
5075
5184
  if (jobData.jobId === id) {
5076
- this.status = "succeeded";
5185
+ this.finalize("succeeded", jobData.result);
5077
5186
  this.emit("status", this.status);
5078
- this._resolveResult(jobData.result);
5079
5187
  }
5080
5188
  });
5081
5189
  this.client.on("job.failed", (data) => {
5082
5190
  const jobData = data;
5083
5191
  if (jobData.jobId === id) {
5084
- this.status = "failed";
5192
+ this.finalize("failed", void 0, jobData.error || new Error("Job failed"));
5085
5193
  this.emit("status", this.status);
5086
- this._rejectResult(jobData.error || new Error("Job failed"));
5087
5194
  }
5088
5195
  });
5089
5196
  this.client.on("tool.call.start", (data) => {
5090
5197
  const d = data;
5091
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
+ });
5092
5205
  this.emit("toolCallStart", { callId: d.callId, toolName: d.toolName, input: d.input, timestamp: d.timestamp });
5093
5206
  }
5094
5207
  });
5095
5208
  this.client.on("tool.call.end", (data) => {
5096
5209
  const d = data;
5097
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
+ });
5098
5220
  this.emit("toolCallEnd", { callId: d.callId, toolName: d.toolName, result: d.result, error: d.error, durationMs: d.durationMs, timestamp: d.timestamp });
5099
5221
  }
5100
5222
  });
@@ -5102,12 +5224,87 @@ var JobImplementation = class {
5102
5224
  get result() {
5103
5225
  return this._resultPromise;
5104
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
+ }
5105
5242
  on(event, handler) {
5106
5243
  if (!this.eventListeners.has(event)) {
5107
5244
  this.eventListeners.set(event, []);
5108
5245
  }
5109
5246
  this.eventListeners.get(event).push(handler);
5110
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
+ }
5111
5308
  emit(event, data) {
5112
5309
  const handlers = this.eventListeners.get(event);
5113
5310
  if (handlers) {
@@ -5248,7 +5445,7 @@ function normalizeUser(user) {
5248
5445
  permissions: Array.isArray(user.permissions) ? user.permissions : []
5249
5446
  };
5250
5447
  }
5251
- var Environment = class _Environment extends Session {
5448
+ var Environment = class extends Session {
5252
5449
  envData;
5253
5450
  _apiKey;
5254
5451
  _apiEndpoint;
@@ -5308,6 +5505,22 @@ var Environment = class _Environment extends Session {
5308
5505
  return this._apiEndpoint.replace(/\/orchestrator\/graphql$/, "").replace(/\/$/, "");
5309
5506
  }
5310
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
+ }
5311
5524
  /**
5312
5525
  * Close the session and disconnect from the sandbox.
5313
5526
  *
@@ -5904,105 +6117,84 @@ var Environment = class _Environment extends Session {
5904
6117
  * ```
5905
6118
  */
5906
6119
  async recordObject(options) {
5907
- const { className, id, label, fields, relationships } = options;
5908
- const graphPath = _Environment.toGraphPath(className, id);
5909
- const existsResult = await this.graphql(
5910
- `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
+ }
5911
6139
  );
5912
- const alreadyExists = !!existsResult.data?.model;
5913
- const instResult = await this.graphql(
5914
- `mutation {
5915
- at(path: "${className}") {
5916
- instantiate(path: "${graphPath}"${label ? `, label: "${label}"` : ""}) {
5917
- model { path }
5918
- }
5919
- }
5920
- }`
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
+ }
5921
6155
  );
5922
- if (instResult.errors?.length) {
5923
- throw new Error(`recordObject instantiate failed: ${instResult.errors[0].message}`);
5924
- }
5925
- const instancePath = instResult.data?.at?.instantiate?.model?.path ?? graphPath;
5926
- await this.graphql(
5927
- `mutation {
5928
- at(path: "${instancePath}") {
5929
- create_submodel(subpath: "_realId", label: "_realId") {
5930
- model { path }
5931
- }
5932
- }
5933
- }`
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}`
5934
6164
  );
5935
- await this.graphql(
5936
- `mutation {
5937
- at(path: "${instancePath}") {
5938
- at(submodel: "_realId") {
5939
- set_string_value(value: "${id.replace(/"/g, '\\"')}") { done }
5940
- }
5941
- }
5942
- }`
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`
5943
6173
  );
5944
- if (fields) {
5945
- for (const [fieldName, value] of Object.entries(fields)) {
5946
- if (value === null) continue;
5947
- await this.graphql(
5948
- `mutation {
5949
- at(path: "${instancePath}") {
5950
- create_submodel(subpath: "${fieldName}", label: "${fieldName}") {
5951
- model { path }
5952
- }
5953
- }
5954
- }`
5955
- );
5956
- if (typeof value === "string") {
5957
- await this.graphql(
5958
- `mutation {
5959
- at(path: "${instancePath}") {
5960
- at(submodel: "${fieldName}") {
5961
- set_string_value(value: "${value.replace(/"/g, '\\"')}") { done }
5962
- }
5963
- }
5964
- }`
5965
- );
5966
- } else if (typeof value === "number") {
5967
- await this.graphql(
5968
- `mutation {
5969
- at(path: "${instancePath}") {
5970
- at(submodel: "${fieldName}") {
5971
- set_number_value(value: ${value}) { done }
5972
- }
5973
- }
5974
- }`
5975
- );
5976
- } else if (typeof value === "boolean") {
5977
- await this.graphql(
5978
- `mutation {
5979
- at(path: "${instancePath}") {
5980
- at(submodel: "${fieldName}") {
5981
- set_boolean_value(value: ${value}) { done }
5982
- }
5983
- }
5984
- }`
5985
- );
5986
- }
5987
- }
5988
- }
5989
- if (relationships) {
5990
- const rels = await this.getRelationships(className);
5991
- const relMap = {};
5992
- for (const rel of rels) {
5993
- const submodelLeaf = rel.local_submodel.path.includes(":") ? rel.local_submodel.path.split(":").pop() : rel.local_submodel.path;
5994
- relMap[submodelLeaf] = rel.foreign_model.path;
5995
- }
5996
- for (const [submodelName, targets] of Object.entries(relationships)) {
5997
- const foreignClass = relMap[submodelName];
5998
- const targetList = Array.isArray(targets) ? targets : [targets];
5999
- for (const target of targetList) {
6000
- const targetGraphPath = foreignClass ? _Environment.toGraphPath(foreignClass, target) : target;
6001
- await this.attach(instancePath, submodelName, targetGraphPath);
6002
- }
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"
6003
6196
  }
6004
- }
6005
- return { path: instancePath, id, created: !alreadyExists };
6197
+ );
6006
6198
  }
6007
6199
  // ==================== PUBLISH TOOLS ====================
6008
6200
  /**