@granular-software/sdk 0.4.45 → 0.4.47

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.
@@ -4029,6 +4029,9 @@ var MAX_TIMER_DELAY_MS = 2147483647;
4029
4029
  var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
4030
4030
  var DEFAULT_RPC_TIMEOUT_MS = 3e4;
4031
4031
  var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
4032
+ var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
4033
+ var DEFAULT_RECONNECT_DELAY_MS = 3e3;
4034
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
4032
4035
  function debugWs(...args) {
4033
4036
  if (DEBUG_WS) {
4034
4037
  console.log(...args);
@@ -4039,6 +4042,10 @@ function rpcTimeoutMsForMethod(method) {
4039
4042
  case "domain.fetchPackagePart":
4040
4043
  case "domain.getSummary":
4041
4044
  return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
4045
+ case "client.heartbeat":
4046
+ case "effects.publishCatalog":
4047
+ case "effects.refresh":
4048
+ return EFFECT_CONTROL_RPC_TIMEOUT_MS;
4042
4049
  default:
4043
4050
  return DEFAULT_RPC_TIMEOUT_MS;
4044
4051
  }
@@ -4058,6 +4065,7 @@ var WSClient = class {
4058
4065
  reconnectTimer = null;
4059
4066
  tokenRefreshTimer = null;
4060
4067
  isExplicitlyDisconnected = false;
4068
+ reconnectAttempts = 0;
4061
4069
  options;
4062
4070
  constructor(options) {
4063
4071
  this.options = options;
@@ -4212,6 +4220,7 @@ var WSClient = class {
4212
4220
  clearTimeout(this.reconnectTimer);
4213
4221
  this.reconnectTimer = null;
4214
4222
  }
4223
+ this.reconnectAttempts = 0;
4215
4224
  this.emit("open", {});
4216
4225
  resolve();
4217
4226
  });
@@ -4243,6 +4252,7 @@ var WSClient = class {
4243
4252
  clearTimeout(this.reconnectTimer);
4244
4253
  this.reconnectTimer = null;
4245
4254
  }
4255
+ this.reconnectAttempts = 0;
4246
4256
  this.emit("open", {});
4247
4257
  resolve();
4248
4258
  };
@@ -4302,7 +4312,8 @@ var WSClient = class {
4302
4312
  return new Error(`WebSocket disconnected${suffix}`);
4303
4313
  }
4304
4314
  handleDisconnect(close = {}) {
4305
- const reconnectDelayMs = 3e3;
4315
+ const baseReconnectDelayMs = typeof this.options.reconnectDelayMs === "number" && Number.isFinite(this.options.reconnectDelayMs) && this.options.reconnectDelayMs > 0 ? this.options.reconnectDelayMs : DEFAULT_RECONNECT_DELAY_MS;
4316
+ const maxReconnectAttempts = typeof this.options.maxReconnectAttempts === "number" && Number.isFinite(this.options.maxReconnectAttempts) && this.options.maxReconnectAttempts >= 0 ? Math.floor(this.options.maxReconnectAttempts) : DEFAULT_MAX_RECONNECT_ATTEMPTS;
4306
4317
  const unexpected = !this.isExplicitlyDisconnected;
4307
4318
  const info = {
4308
4319
  code: close.code,
@@ -4322,6 +4333,30 @@ var WSClient = class {
4322
4333
  const disconnectError = this.buildDisconnectError(info);
4323
4334
  this.rejectPending(disconnectError);
4324
4335
  this.emit("disconnect", info);
4336
+ if (this.reconnectAttempts >= maxReconnectAttempts) {
4337
+ const reconnectInfo = {
4338
+ error: `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
4339
+ sessionId: this.sessionId,
4340
+ timestamp: Date.now()
4341
+ };
4342
+ this.emit("reconnect_error", reconnectInfo);
4343
+ if (this.options.onReconnectError) {
4344
+ try {
4345
+ this.options.onReconnectError(reconnectInfo);
4346
+ } catch (callbackError) {
4347
+ console.error(
4348
+ "[Granular] onReconnectError callback failed:",
4349
+ callbackError
4350
+ );
4351
+ }
4352
+ }
4353
+ return;
4354
+ }
4355
+ this.reconnectAttempts += 1;
4356
+ const reconnectDelayMs = Math.min(
4357
+ 3e4,
4358
+ baseReconnectDelayMs * 2 ** Math.max(0, this.reconnectAttempts - 1)
4359
+ );
4325
4360
  info.reconnectScheduled = true;
4326
4361
  info.reconnectDelayMs = reconnectDelayMs;
4327
4362
  if (this.options.onUnexpectedClose) {
@@ -4776,6 +4811,9 @@ var Session = class {
4776
4811
  promptCache = /* @__PURE__ */ new Map();
4777
4812
  /** Prompt ids locally answered before the document sync catches up. */
4778
4813
  hiddenPromptIds = /* @__PURE__ */ new Set();
4814
+ domainPackagePartCache = /* @__PURE__ */ new Map();
4815
+ domainPackagePartPromises = /* @__PURE__ */ new Map();
4816
+ domainPackageFetchQueue = Promise.resolve();
4779
4817
  constructor(client, clientId, options = {}) {
4780
4818
  this.client = client;
4781
4819
  this.clientId = clientId || `client_${Date.now()}`;
@@ -4983,12 +5021,18 @@ var Session = class {
4983
5021
  const resolvedAnswer = resolvePromptAnswer(prompt, answer);
4984
5022
  this.promptCache.delete(promptId);
4985
5023
  this.hiddenPromptIds.add(promptId);
5024
+ this.emit("prompt", { id: promptId, status: "answered" });
4986
5025
  try {
4987
- await this.client.call("prompt.answer", {
5026
+ const response = await this.client.call("prompt.answer", {
4988
5027
  promptId,
4989
5028
  answer: resolvedAnswer,
4990
5029
  value: resolvedAnswer
4991
5030
  });
5031
+ if (response && typeof response === "object" && "ok" in response && response.ok === false) {
5032
+ const rejected = response;
5033
+ const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
5034
+ throw new Error(errorMessage);
5035
+ }
4992
5036
  } catch (error) {
4993
5037
  this.hiddenPromptIds.delete(promptId);
4994
5038
  if (prompt) {
@@ -5216,11 +5260,33 @@ var Session = class {
5216
5260
  * Fetch a domain package part from the backend (no fallback).
5217
5261
  */
5218
5262
  async fetchDomainPart(part) {
5219
- const result = await this.client.call("domain.fetchPackagePart", {
5220
- moduleSpecifier: "@sandbox/domain",
5221
- part
5263
+ const cached = this.domainPackagePartCache.get(part);
5264
+ if (cached !== void 0) {
5265
+ return cached;
5266
+ }
5267
+ const inFlight = this.domainPackagePartPromises.get(part);
5268
+ if (inFlight) {
5269
+ return inFlight;
5270
+ }
5271
+ const fetchPromise = this.domainPackageFetchQueue.then(async () => {
5272
+ const result = await this.client.call("domain.fetchPackagePart", {
5273
+ moduleSpecifier: "@sandbox/domain",
5274
+ part
5275
+ });
5276
+ const content = result?.content ?? "";
5277
+ this.domainPackagePartCache.set(part, content);
5278
+ return content;
5222
5279
  });
5223
- return result?.content ?? "";
5280
+ this.domainPackagePartPromises.set(part, fetchPromise);
5281
+ this.domainPackageFetchQueue = fetchPromise.then(
5282
+ () => void 0,
5283
+ () => void 0
5284
+ );
5285
+ try {
5286
+ return await fetchPromise;
5287
+ } finally {
5288
+ this.domainPackagePartPromises.delete(part);
5289
+ }
5224
5290
  }
5225
5291
  /**
5226
5292
  * Get TypeScript class declarations for the current domain (for LLM/code gen).
@@ -5470,7 +5536,10 @@ import { ${allImports} } from "./sandbox-tools";
5470
5536
  const emitPrompt = (payload) => {
5471
5537
  const prompt = normalizePrompt(payload);
5472
5538
  if (!prompt) return;
5473
- this.hiddenPromptIds.delete(prompt.id);
5539
+ if (this.hiddenPromptIds.has(prompt.id)) {
5540
+ this.emit("prompt", { ...prompt, status: "answered" });
5541
+ return;
5542
+ }
5474
5543
  this.promptCache.set(prompt.id, prompt);
5475
5544
  this.emit("prompt", prompt);
5476
5545
  };
@@ -13081,6 +13150,7 @@ var LOCAL_CONTROL_REQUEST_RETRY_DELAY_MS = 500;
13081
13150
  var SESSION_DATA_REQUEST_RETRY_COUNT = 4;
13082
13151
  var SESSION_DATA_REQUEST_RETRY_DELAY_MS = 500;
13083
13152
  var EFFECT_HOST_CONNECT_TIMEOUT_MS = 15e3;
13153
+ var SESSION_CONNECT_TIMEOUT_MS = 15e3;
13084
13154
  function filenameFromUploadBody(body) {
13085
13155
  const maybe = body;
13086
13156
  return typeof maybe.name === "string" && maybe.name.trim() ? maybe.name.trim() : null;
@@ -13100,7 +13170,7 @@ function bodyInitFromSessionFileUpload(body) {
13100
13170
  }
13101
13171
  return body;
13102
13172
  }
13103
- var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 3e4;
13173
+ var EFFECT_CATALOG_SYNC_TIMEOUT_MS = 12e4;
13104
13174
  var EFFECT_CATALOG_SYNC_RETRY_COUNT = 3;
13105
13175
  var EFFECT_CATALOG_SYNC_RETRY_DELAY_MS = 1e3;
13106
13176
  function planRecordObjectsChunks(records, batchSize) {
@@ -15056,7 +15126,14 @@ var Granular = class _Granular {
15056
15126
  return tag;
15057
15127
  }
15058
15128
  buildManagedEnvironmentName(tag, versionId) {
15059
- return `__sdk__${tag}__${versionId}`;
15129
+ return `__sdk__${tag}__${versionId}__pinned`;
15130
+ }
15131
+ isManagedEnvironmentName(environment, tagName) {
15132
+ const name = environment.environment || environment.envName || "";
15133
+ return name.startsWith(`__sdk__${tagName}__`);
15134
+ }
15135
+ isPinnedToVersion(environment, versionId) {
15136
+ return environment.buildPolicy.mode === "pinned" && (environment.versionId === versionId || environment.buildPolicy.versionId === versionId || environment.buildPolicy.buildId === versionId);
15060
15137
  }
15061
15138
  matchesTagTrackedEnvironment(environment, tagName, tagId) {
15062
15139
  const environmentTagName = environment.tag?.name || environment.buildPolicy.tagName || null;
@@ -15117,7 +15194,7 @@ var Granular = class _Granular {
15117
15194
  );
15118
15195
  const currentMatches = this.sortEnvironmentsByRecency(
15119
15196
  userEnvironments.filter(
15120
- (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId
15197
+ (environment) => this.matchesTagTrackedEnvironment(environment, tagName, tag.tagId) && environment.versionId === targetVersionId && (!this.isManagedEnvironmentName(environment, tagName) || this.isPinnedToVersion(environment, targetVersionId))
15121
15198
  )
15122
15199
  );
15123
15200
  if (currentMatches.length > 0) {
@@ -15146,6 +15223,7 @@ var Granular = class _Granular {
15146
15223
  subjectId: user.granularId,
15147
15224
  environment: this.buildManagedEnvironmentName(tagName, targetVersionId),
15148
15225
  tagId: tag.tagId,
15226
+ versionId: targetVersionId,
15149
15227
  permissionProfileId: null
15150
15228
  }),
15151
15229
  requestedOntology: ontology,
@@ -15191,6 +15269,7 @@ var Granular = class _Granular {
15191
15269
  row.summaryUpdatedAt ?? row.summary_updated_at
15192
15270
  ) : null,
15193
15271
  subjectId: row.subjectId != null ? String(row.subjectId) : null,
15272
+ sessionScope: row.sessionScope != null || row.session_scope != null ? String(row.sessionScope ?? row.session_scope) : null,
15194
15273
  jobCount: typeof row.jobCount === "number" ? row.jobCount : void 0,
15195
15274
  toolCallCount: typeof row.toolCallCount === "number" ? row.toolCallCount : void 0
15196
15275
  };
@@ -15398,7 +15477,11 @@ var Granular = class _Granular {
15398
15477
  onUnexpectedClose: this.onUnexpectedClose,
15399
15478
  onReconnectError: this.onReconnectError
15400
15479
  });
15401
- await client.connect();
15480
+ await withTimeout(
15481
+ client.connect(),
15482
+ SESSION_CONNECT_TIMEOUT_MS,
15483
+ `session WebSocket connect for ${session.sessionId}`
15484
+ );
15402
15485
  const environmentSession = new EnvironmentSession(
15403
15486
  client,
15404
15487
  environment,
@@ -15569,12 +15652,24 @@ var Granular = class _Granular {
15569
15652
  host.heartbeatInFlight = false;
15570
15653
  }
15571
15654
  async synchronizeEffectHost(host) {
15572
- await host.wsClient.call("client.hello", {
15573
- clientId: host.clientId,
15574
- protocolVersion: "2.0"
15575
- });
15576
- this.startEffectHostHeartbeat(host);
15577
- await this.publishSandboxEffectCatalog(host);
15655
+ if (host.syncPromise) {
15656
+ return host.syncPromise;
15657
+ }
15658
+ host.syncPromise = (async () => {
15659
+ await host.wsClient.call("client.hello", {
15660
+ clientId: host.clientId,
15661
+ protocolVersion: "2.0"
15662
+ });
15663
+ await this.publishSandboxEffectCatalog(host);
15664
+ this.startEffectHostHeartbeat(host);
15665
+ })();
15666
+ try {
15667
+ await host.syncPromise;
15668
+ } finally {
15669
+ if (host.syncPromise) {
15670
+ host.syncPromise = null;
15671
+ }
15672
+ }
15578
15673
  }
15579
15674
  async ensureSandboxEffectHost(sandboxId) {
15580
15675
  const existing = this.sandboxEffectHosts.get(sandboxId);
@@ -15610,7 +15705,8 @@ var Granular = class _Granular {
15610
15705
  wsClient,
15611
15706
  heartbeatTimer: null,
15612
15707
  heartbeatInFlight: false,
15613
- recovering: false
15708
+ recovering: false,
15709
+ syncPromise: null
15614
15710
  };
15615
15711
  wsClient.registerRpcHandler("effect.invoke", async (params) => {
15616
15712
  const request = params;
@@ -16428,6 +16524,79 @@ function validateHarnessTemplateManifest(value, context = "HarnessTemplateManife
16428
16524
  function defineHarnessTemplateManifest(value, context) {
16429
16525
  return validateHarnessTemplateManifest(value, context);
16430
16526
  }
16527
+ var DEFAULT_IGNORED_REASONING_COMMENT_DIRECTIVES = [
16528
+ /^@ts-ignore\b/i,
16529
+ /^@ts-expect-error\b/i,
16530
+ /^eslint-[\w-]+\b/i,
16531
+ /^biome-ignore\b/i,
16532
+ /^prettier-ignore\b/i,
16533
+ /^istanbul ignore\b/i
16534
+ ];
16535
+ var DEFAULT_LOW_SIGNAL_REASONING_LINES = [
16536
+ /^running\.?$/i,
16537
+ /^working\.?$/i,
16538
+ /^thinking\.?$/i,
16539
+ /^generating(?: code)?\.?$/i,
16540
+ /^starting(?: execution)?\.?$/i
16541
+ ];
16542
+ function parseReasoningCommentLine(line, options = {}) {
16543
+ const trimmed = line.trimStart();
16544
+ if (!trimmed.startsWith("//")) return null;
16545
+ const text = trimmed.replace(/^\/\/\s?/, "").trim();
16546
+ if (!text) return { kind: "ignored" };
16547
+ const ignoredDirectives = options.ignoredCommentDirectives || DEFAULT_IGNORED_REASONING_COMMENT_DIRECTIVES;
16548
+ if (ignoredDirectives.some((pattern) => pattern.test(text))) {
16549
+ return { kind: "ignored" };
16550
+ }
16551
+ const lowSignalLines = options.lowSignalReasoningLines || DEFAULT_LOW_SIGNAL_REASONING_LINES;
16552
+ if (lowSignalLines.some((pattern) => pattern.test(text))) {
16553
+ return { kind: "ignored" };
16554
+ }
16555
+ return { kind: "reasoning", text };
16556
+ }
16557
+ function consumeGranularReasoningTraceChunk(buffer, chunk, options = {}) {
16558
+ let text = buffer + chunk;
16559
+ let visibleText = "";
16560
+ const reasoningLines = [];
16561
+ while (true) {
16562
+ const newlineIndex = text.indexOf("\n");
16563
+ if (newlineIndex === -1) break;
16564
+ const rawLine = text.slice(0, newlineIndex);
16565
+ text = text.slice(newlineIndex + 1);
16566
+ const comment = parseReasoningCommentLine(
16567
+ rawLine.replace(/\r$/, ""),
16568
+ options
16569
+ );
16570
+ if (comment?.kind === "reasoning") {
16571
+ reasoningLines.push(comment.text);
16572
+ } else if (comment?.kind === "ignored") {
16573
+ continue;
16574
+ } else {
16575
+ visibleText += `${rawLine}
16576
+ `;
16577
+ }
16578
+ }
16579
+ if (options.final && text.length > 0) {
16580
+ const comment = parseReasoningCommentLine(text.replace(/\r$/, ""), options);
16581
+ if (comment?.kind === "reasoning") {
16582
+ reasoningLines.push(comment.text);
16583
+ text = "";
16584
+ } else if (comment?.kind === "ignored") {
16585
+ text = "";
16586
+ } else {
16587
+ visibleText += text;
16588
+ text = "";
16589
+ }
16590
+ }
16591
+ return { buffer: text, visibleText, reasoningLines };
16592
+ }
16593
+ function consumeGranularReasoningOnlyChunk(buffer, chunk, options = {}) {
16594
+ const result = consumeGranularReasoningTraceChunk(buffer, chunk, options);
16595
+ return {
16596
+ buffer: result.buffer,
16597
+ reasoningLines: result.reasoningLines
16598
+ };
16599
+ }
16431
16600
  function asRecord4(value) {
16432
16601
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
16433
16602
  return value;
@@ -17633,7 +17802,8 @@ function buildGranularAgentSessionBlock(sessionContext) {
17633
17802
  runtimeId: sessionContext?.sandboxId || null,
17634
17803
  environmentId: sessionContext?.environmentId || null,
17635
17804
  userName: sessionContext?.userName || null,
17636
- domainRevision: sessionContext?.domainRevision || null
17805
+ domainRevision: sessionContext?.domainRevision || null,
17806
+ uiContext: sessionContext?.uiContext || null
17637
17807
  });
17638
17808
  }
17639
17809
  function buildGranularAgentHeapBlock(heapSummary) {
@@ -17880,7 +18050,7 @@ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
17880
18050
  const rightScope = `${right.className || "global"}:${right.static ? "static" : "instance"}`;
17881
18051
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
17882
18052
  });
17883
- const writeActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
18053
+ const availableActions = normalizedTools.filter((tool) => tool.ready !== false).map((tool) => {
17884
18054
  const scope = tool.className ? `${tool.static ? "class" : "record"}:${tool.className}` : "global";
17885
18055
  return {
17886
18056
  name: tool.name,
@@ -17891,7 +18061,8 @@ function buildGranularAgentToolBlock(tools, capabilityOverrides) {
17891
18061
  const capabilities = {
17892
18062
  executeCode: resolvedCapabilities.executeCode,
17893
18063
  readEntities: resolvedCapabilities.readEntities,
17894
- writeActions,
18064
+ availableActions,
18065
+ writeActions: availableActions,
17895
18066
  workflowHelpers: resolvedCapabilities.workflowHelpers,
17896
18067
  savedData: resolvedCapabilities.savedData,
17897
18068
  showRecords: resolvedCapabilities.showRecords
@@ -17905,7 +18076,7 @@ function buildGranularAgentActionIndex(tools) {
17905
18076
  return leftScope.localeCompare(rightScope) || left.name.localeCompare(right.name);
17906
18077
  });
17907
18078
  if (normalizedTools.length === 0) {
17908
- return "No domain write actions are available.";
18079
+ return "No executable actions are available.";
17909
18080
  }
17910
18081
  const globalTools = normalizedTools.filter((tool) => !tool.className);
17911
18082
  const staticTools = normalizedTools.filter(
@@ -17999,6 +18170,76 @@ function splitDomainDocumentation(domainDocumentation) {
17999
18170
  }
18000
18171
  return { types: normalized, docs: "" };
18001
18172
  }
18173
+ var DOMAIN_HELPER_FUNCTION_NAMES = /* @__PURE__ */ new Set([
18174
+ "agent_heap_objects",
18175
+ "agent_message",
18176
+ "agent_text_message"
18177
+ ]);
18178
+ function inferGlobalActionToolsFromDomainTypes(domainTypes) {
18179
+ const inferred = [];
18180
+ const seen = /* @__PURE__ */ new Set();
18181
+ const declarationPattern = /(?:export\s+)?declare\s+function\s+([A-Za-z_$][\w$]*)\s*\(/g;
18182
+ let match;
18183
+ while (match = declarationPattern.exec(domainTypes)) {
18184
+ const name = match[1];
18185
+ if (!name || DOMAIN_HELPER_FUNCTION_NAMES.has(name) || seen.has(name)) {
18186
+ continue;
18187
+ }
18188
+ seen.add(name);
18189
+ inferred.push({
18190
+ name,
18191
+ description: "Executable global action declared by the domain runtime."
18192
+ });
18193
+ }
18194
+ const actionLinePattern = /^-\s*([A-Za-z_$][\w$]*)\s+\(global\):\s*(.+)$/gm;
18195
+ while (match = actionLinePattern.exec(domainTypes)) {
18196
+ const name = match[1];
18197
+ if (!name || DOMAIN_HELPER_FUNCTION_NAMES.has(name) || seen.has(name)) {
18198
+ continue;
18199
+ }
18200
+ seen.add(name);
18201
+ inferred.push({
18202
+ name,
18203
+ description: match[2]?.trim() || "Executable global action declared by the domain runtime."
18204
+ });
18205
+ }
18206
+ const scopedActionLinePattern = /^-\s*([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)\s+\((record|class)\):\s*(.+)$/gm;
18207
+ while (match = scopedActionLinePattern.exec(domainTypes)) {
18208
+ const className = match[1]?.toLowerCase();
18209
+ const name = match[2];
18210
+ const scope = match[3];
18211
+ if (!className || !name || DOMAIN_HELPER_FUNCTION_NAMES.has(name)) {
18212
+ continue;
18213
+ }
18214
+ const key = `${className}:${scope}:${name}`;
18215
+ if (seen.has(key)) {
18216
+ continue;
18217
+ }
18218
+ seen.add(key);
18219
+ inferred.push({
18220
+ name,
18221
+ className,
18222
+ static: scope === "class",
18223
+ description: match[4]?.trim() || "Executable action declared by the domain runtime."
18224
+ });
18225
+ }
18226
+ return inferred;
18227
+ }
18228
+ function resolvePromptTools(tools, domainTypes) {
18229
+ const byKey = /* @__PURE__ */ new Map();
18230
+ for (const tool of tools || []) {
18231
+ if (!tool?.name) continue;
18232
+ const key = `${tool.className || "global"}:${tool.static ? "static" : "instance"}:${tool.name}`;
18233
+ byKey.set(key, tool);
18234
+ }
18235
+ for (const tool of inferGlobalActionToolsFromDomainTypes(domainTypes)) {
18236
+ const key = `global:instance:${tool.name}`;
18237
+ if (!byKey.has(key)) {
18238
+ byKey.set(key, tool);
18239
+ }
18240
+ }
18241
+ return [...byKey.values()];
18242
+ }
18002
18243
  function buildGranularAgentCheckpointBlock(checkpoint) {
18003
18244
  if (!checkpoint) {
18004
18245
  return renderConstBlock("previousCodeResult", null);
@@ -18071,12 +18312,13 @@ function buildGranularAgentSystemPrompt(input) {
18071
18312
  const outputMode = input.outputMode || "agentMessages";
18072
18313
  const promptCapabilities = resolvePromptCapabilities(input.capabilities);
18073
18314
  const domainSections = splitDomainDocumentation(input.domainDocumentation);
18315
+ const promptTools = resolvePromptTools(input.tools, domainSections.types);
18074
18316
  const sessionBlock = buildGranularAgentSessionBlock(input.sessionContext);
18075
18317
  const toolBlock = buildGranularAgentToolBlock(
18076
- input.tools,
18318
+ promptTools,
18077
18319
  input.capabilities
18078
18320
  );
18079
- const actionIndex = buildGranularAgentActionIndex(input.tools);
18321
+ const actionIndex = buildGranularAgentActionIndex(promptTools);
18080
18322
  const domainBlock = buildGranularAgentDomainBlock(domainSections.types);
18081
18323
  const workflowBlock = buildGranularAgentWorkflowBlock(input.workflowSummary);
18082
18324
  const checkpointBlock = buildGranularAgentCheckpointBlock(input.checkpoint);
@@ -18104,7 +18346,7 @@ function buildGranularAgentSystemPrompt(input) {
18104
18346
  - When \`agent_text_message(...)\` mentions a grounded record that should remain clickable/referable, wrap only the visible record label in a self-closing inline reference tag: \`<granular-object class="class_name" id="stable_id_or_path" label="Visible label" />\`. Use the actual class name and stable id/path from the runtime record or effect result; do not invent ids, field names, or snake/camel-case aliases that are not present in the type declarations or returned object.
18105
18347
  - Treat \`agent_heap_objects(...)\` as the UI display call for user-visible records, not as a general storage helper. Do not wrap records under an \`items\` key.
18106
18348
  - When records should remain reusable for follow-ups, first save the runtime record or ordered record array with \`await heap.setVar("stable_selection_name", value)\`, then display that saved selection exactly once with \`await agent_heap_objects({ variableNames: ["stable_selection_name"] })\`.
18107
- - \`heap.setVar(...)\` only accepts scalar values, runtime records/sandbox instances, or arrays of runtime records/sandbox instances. Do not save plain action/effect result objects. If an action returns an id/path for a created record that should remain referable, fetch the created record first, then save/display that fetched record.
18349
+ - \`heap.setVar(...)\` only accepts scalar values, runtime records/sandbox instances, or arrays of runtime records/sandbox instances from one class. Do not save plain action/effect result objects or arrays of JSON summaries returned by actions. If an action returns ids/paths for records that should remain referable or displayed as records, fetch the matching runtime records first with the generated class \`.get(...)\`/query API, then save/display those fetched records. If the action returned only structured summaries, answer from those summaries with \`agent_text_message(...)\`.
18108
18350
  - Do not use \`agent_heap_objects({ entries: [...] })\` or \`agent_heap_objects({ saveAs, entries })\` as a shortcut for ordered pages, queues, search results, or ranked lists; those forms can create duplicate or poorly labelled displays. Save the selection with \`heap.setVar(...)\` and display it via \`variableNames\` instead.
18109
18351
  - Use \`entryPaths\` only for a few already-known individual records and \`listNames\` only for a host-created list that you intentionally want to show. Do not display both an entry/list selection and a heap variable for the same records.
18110
18352
  - When the user asks to show, list, display, open, or "show them" for records you found, call \`agent_heap_objects(...)\`; do not answer only with a count or text summary.
@@ -18154,10 +18396,11 @@ ${outputRules}` : `Code:
18154
18396
  - Use choice only for 2 to 5 short grounded options.
18155
18397
  - For record choices, set each option value to a stable scalar such as the record \`_graphPath\` or \`id\`, not a label-only value.
18156
18398
  - After \`await loop.ask_user(...)\` returns from a choice prompt, tolerate either the option value, the option object, or a human-readable label by matching against value, id/path, label, and description before failing. If a returned label is a prefix or substring of exactly one option label, treat it as that option.
18157
- - Use \`loop.confirm(...)\` for yes/no confirmation only when the user explicitly asks for confirmation, action or permission metadata requires it, policy requires it, or material uncertainty remains after grounding.
18399
+ - Use \`loop.confirm(...)\` for yes/no confirmation only when the user explicitly asks for a separate confirmation step, policy requires confirmation outside the action runtime, or material uncertainty remains after grounding.
18400
+ - If action, effect, tool, or permission metadata already marks the invoked action as confirmation-gated, do not call \`loop.confirm(...)\` before invoking it. Ground the target and input, then call the action once; the runtime action policy will surface the confirmation prompt and resume the same invocation after approval.
18158
18401
  - Do not add a generic yes/no confirmation after the user has already made a grounded choice, unless one of those confirmation conditions still applies.
18159
- - Do not add confirmation only because an allowed mutation is visible to other people, customer-facing, or consequential. If the user clearly requested the mutation and the grounded target, action, and condition are unique, perform the mutation unless confirmation is required by the user, policy, action metadata, or remaining material uncertainty.
18160
- - A conditional request such as "if this is true, do that" is authorization to perform the requested action after you verify the condition. Once the condition, target, and action are grounded uniquely, call the action directly; do not ask "should I perform/post/send this?" unless the user, policy, action metadata, or unresolved material uncertainty requires confirmation. The visibility or impact of an allowed action is not by itself unresolved uncertainty.
18402
+ - Do not add confirmation only because an allowed mutation is visible to other people, customer-facing, or consequential. If the user clearly requested the mutation and the grounded target, action, and condition are unique, perform the mutation unless confirmation is required outside the action runtime or remaining material uncertainty exists.
18403
+ - A conditional request such as "if this is true, do that" is authorization to perform the requested action after you verify the condition. Once the condition, target, and action are grounded uniquely, call the action directly; do not ask "should I perform/post/send this?" unless the user, policy outside the action runtime, or unresolved material uncertainty requires confirmation. The visibility or impact of an allowed action is not by itself unresolved uncertainty.
18161
18404
  - If the user explicitly asks you to stop for confirmation, natural-language text such as "please confirm" is not enough: call \`await loop.confirm(...)\` before the mutation, then perform the approved mutation in the same resumed job when it returns true.
18162
18405
  - Reuse existing task, decision, and closure ids from [State].
18163
18406
  - If a user request matches both a domain record/action and a workflow helper, prefer the domain capability.` : "";
@@ -18303,7 +18546,8 @@ Query policy:
18303
18546
  - For operational blocker, risk, status, or "what is happening" questions, inspect the relevant record's scalar fields such as status, priority, blocker, summary, latest update/message, due date, amount, and other domain-specific descriptive fields before answering.
18304
18547
  - For read-only readiness, risk, health, or status summaries, call any visible read-only assessment/status action on the grounded primary record before ad-hoc aggregation when such an action semantically matches the request. Use the returned fields in the reply and supplement with counts or record reads only when useful.
18305
18548
  - Do not hide required visible read-only assessment/status actions inside broad try/catch blocks. The runtime action surface should show that the assessment action ran.
18306
- - Treat action/effect results as structured values, not necessarily arrays. Before indexing, iterating, checking \`.length\`, or calling array methods, normalize the result first: use the result itself only when \`Array.isArray(result)\`; otherwise read the exact array field shown in the output schema, or a documented array field such as \`items\`, \`matches\`, \`results\`, \`records\`, \`entries\`, \`candidates\`, \`options\`, or a domain-specific array field. Never convert a non-array object result to \`[]\` before checking its documented fields.
18549
+ - Treat action/effect results as structured values, not necessarily arrays. Before indexing, iterating, checking \`.length\`, or calling array methods, normalize the result first: use the result itself only when \`Array.isArray(result)\`; otherwise read the exact array field shown in the output schema, or a documented array field such as \`items\`, \`matches\`, \`results\`, \`records\`, \`entries\`, \`candidates\`, \`options\`, \`requests\`, \`vendors\`, \`transactions\`, \`approvals\`, \`receipts\`, or another domain-specific array field. If a structured result has \`count > 0\`, never conclude there are no matches until you inspect every array-valued field on that result object, especially fields named by the output schema. Never convert a non-array object result to \`[]\` before checking its documented fields.
18550
+ - Plain JSON objects returned by actions are not sandbox record instances, even when they contain ids, titles, labels, or status fields. Use them for reasoning and text responses. Do not pass action-returned JSON objects or arrays directly to \`heap.setVar(...)\` or \`agent_heap_objects(...)\`; fetch corresponding runtime records first when the user needs record display or follow-up references.
18307
18551
  - When a visible search, lookup, availability, or assessment action returns candidates or matches, treat those returned records as already scoped by the action inputs unless the output schema gives reliable fields for further narrowing. When matching returned candidates to grounded records, use the output schema's actual identifier fields, including \`id\`, \`path\`, or fields ending in \`Id\`; do not assume candidates have \`_graphPath\`. Do not discard all returned candidates by re-filtering on guessed property names.
18308
18552
  - For scheduling actions, convert relative wording into concrete ISO timestamps before mutating records.
18309
18553
  - When a decision depends on fresh external state and a visible read-only status/lookup action exists on the grounded record, call it before deciding, mutating, or refusing based on stale stored fields.
@@ -18365,6 +18609,7 @@ ${domainSections.docs}
18365
18609
 
18366
18610
  Actions:
18367
18611
  ${actionIndex}
18612
+ - Global actions are executable functions exported by "./sandbox-tools"; import each global action you call, e.g. \`import { some_action } from "./sandbox-tools"; await some_action(...)\`. This includes frontend actions such as opening, focusing, or navigating the host UI.
18368
18613
  - Actions listed under "Record-level" are instance methods. First fetch or find the specific record, then call the action on that instance, e.g. \`const item = await Item.get({ path }); await item.action_name(...)\`.
18369
18614
  - Actions listed under "Class-level" are class/static methods. Call them on the imported class, e.g. \`await Item.action_name(...)\`.
18370
18615
  - The action index is the visibility contract. If an action is listed for a class, call it directly on fetched/listed instances of that class; do not use \`typeof record.action_name === "function"\` as a discovery gate. If an action is not listed, do not call it.
@@ -19140,7 +19385,7 @@ function extractJsonStringField(source, fieldName) {
19140
19385
  function modelOutputInstruction() {
19141
19386
  return [
19142
19387
  "Return only a JSON object with this shape:",
19143
- '{ "action": "reply" | "job", "reply": string, "code": string }',
19388
+ '{ "action": "reply" | "job", "code": string, "reply": string }',
19144
19389
  'Use "action":"reply" only when a plain conversational answer is enough and no live session state should change.',
19145
19390
  'Do not use "action":"reply" to promise future tool work; if the user asks to check, find, look up, inspect, read, reopen, summarize, transform, update, post, send, approve, schedule, reschedule, calculate, or confirm around session state, session files, generated files, tools, or a domain action, use "action":"job".',
19146
19391
  'If the user asks to use an attached file, uploaded file, generated file, previous output file, or "the summary/workbook/file you just created", choose "action":"job" and read it through [Runtime Imports] instead of answering from memory.',
@@ -19148,7 +19393,8 @@ function modelOutputInstruction() {
19148
19393
  'Before claiming you lack access, inspect the visible action list. If a visible read-only search, lookup, list, guidance, note, policy, or knowledge action can satisfy a "check", "find", "look up", or "whether we have guidance" request, choose "action":"job" and call it.',
19149
19394
  "Generated code must not report no matches for the primary human-described anchor after a single zero-result list/find/page call. Before that primary no-match return, retry the primary anchor with fewer text constraints or a distinct fallback such as owner/container grounding, relationship traversal, exact-id/path lookup, or shorter target-local search.",
19150
19395
  'Use "action":"job" when the next step should run code or mutate workflow state.',
19151
- 'When action is "job", include runnable code in "code".',
19396
+ 'When action is "job", include runnable code in "code" and emit the "code" field before any non-empty "reply" field so generated code comments can stream as progress.',
19397
+ "Generated job code must use plain ASCII punctuation in string literals and comments. Do not use curly quotes, smart apostrophes, en dashes, em dashes, or other typographic punctuation in code.",
19152
19398
  "Generated code must not reference prompt-only symbols such as runtimeImports, savedData, sessionFileManifest, recentReferences, workflowContext, workflowState, or capabilities. Copy concrete paths/ids from the prompt into strings, fetch records with documented imports, or use documented runtime globals.",
19153
19399
  "Generated code must follow [Runtime Imports]: import module exports from their listed module, use listed globals directly without importing them, and do not leave undeclared identifiers in the job.",
19154
19400
  "Generated action calls must use the exact input property names from the visible action schema. Do not invent synonym keys for required inputs.",
@@ -19233,13 +19479,36 @@ ${modelOutputInstruction()}`
19233
19479
  let text = "";
19234
19480
  let usage = null;
19235
19481
  let requestId = null;
19236
- if (input.onTextDelta) {
19237
- const onTextDelta = input.onTextDelta;
19482
+ if (input.onTextDelta || input.onReplyDelta || input.onCodeDelta) {
19238
19483
  const stream = await client.chat.completions.create({
19239
19484
  ...payload,
19240
19485
  stream: true,
19241
19486
  stream_options: { include_usage: true }
19242
19487
  });
19488
+ let streamedReply = "";
19489
+ let streamedCode = "";
19490
+ const emitReplyDelta = async () => {
19491
+ if (!input.onReplyDelta) return;
19492
+ const replyField = extractJsonStringField(text, "reply");
19493
+ if (!replyField) return;
19494
+ const nextReply = replyField.value;
19495
+ if (!nextReply.startsWith(streamedReply)) return;
19496
+ const delta = nextReply.slice(streamedReply.length);
19497
+ if (!delta) return;
19498
+ streamedReply = nextReply;
19499
+ await input.onReplyDelta(delta);
19500
+ };
19501
+ const emitCodeDelta = async () => {
19502
+ if (!input.onCodeDelta) return;
19503
+ const codeField = extractJsonStringField(text, "code");
19504
+ if (!codeField) return;
19505
+ const nextCode = codeField.value;
19506
+ if (!nextCode.startsWith(streamedCode)) return;
19507
+ const delta = nextCode.slice(streamedCode.length);
19508
+ if (!delta) return;
19509
+ streamedCode = nextCode;
19510
+ await input.onCodeDelta(delta);
19511
+ };
19243
19512
  for await (const event of stream) {
19244
19513
  requestId = requestId || event.id || event._request_id || null;
19245
19514
  usage = event.usage || usage;
@@ -19247,8 +19516,12 @@ ${modelOutputInstruction()}`
19247
19516
  const deltaText = typeof delta === "string" ? delta : Array.isArray(delta) ? delta.map((part) => asRecord6(part)?.text || "").join("") : "";
19248
19517
  if (!deltaText) continue;
19249
19518
  text += deltaText;
19250
- await onTextDelta(deltaText);
19519
+ await input.onTextDelta?.(deltaText);
19520
+ await emitReplyDelta();
19521
+ await emitCodeDelta();
19251
19522
  }
19523
+ await emitReplyDelta();
19524
+ await emitCodeDelta();
19252
19525
  raw = { streamed: true, model, usage, request_id: requestId };
19253
19526
  } else {
19254
19527
  const completion = await client.chat.completions.create(
@@ -19350,6 +19623,64 @@ function normalizeHeapSnapshot2(heap) {
19350
19623
  updatedAt: typeof heap?.updatedAt === "number" ? heap.updatedAt : Date.now()
19351
19624
  };
19352
19625
  }
19626
+ function targetFromUiContext(context) {
19627
+ const target = asRecord6(context?.target) || asRecord6(context?.currentPageObject) || asRecord6(context?.commentaryTarget);
19628
+ const className = typeof target?.className === "string" ? target.className : "";
19629
+ const id = typeof target?.id === "string" ? target.id : "";
19630
+ if (!className || !id) return null;
19631
+ return {
19632
+ className,
19633
+ id,
19634
+ label: typeof target?.label === "string" ? target.label : void 0
19635
+ };
19636
+ }
19637
+ function heapEntryMatchesTarget(entry, target) {
19638
+ if (entry.className !== target.className) return false;
19639
+ if (entry.id === target.id) return true;
19640
+ const fields = asRecord6(entry.fields);
19641
+ return fields?.real_id === target.id || fields?._realId === target.id;
19642
+ }
19643
+ function focusedHeapEntryPathsFromUiContext(heap, context) {
19644
+ const target = targetFromUiContext(context);
19645
+ if (!target) return [];
19646
+ return Object.entries(heap.entriesByPath).filter(([, entry]) => heapEntryMatchesTarget(entry, target)).map(([path2]) => path2);
19647
+ }
19648
+ function hasSessionDocumentContext(document) {
19649
+ const doc = asRecord6(document);
19650
+ if (!doc) return false;
19651
+ const heap = normalizeHeapSnapshot2(asRecord6(doc.heap));
19652
+ if (Object.keys(heap.entriesByPath).length > 0 || Object.keys(heap.listsByName).length > 0 || Object.keys(heap.variablesByName).length > 0) {
19653
+ return true;
19654
+ }
19655
+ const domain = asRecord6(doc.domain);
19656
+ const packages = asRecord6(domain?.packages);
19657
+ if (packages && Object.keys(packages).length > 0) return true;
19658
+ const prompts = asRecord6(doc.prompts);
19659
+ if (prompts && Object.keys(prompts).length > 0) return true;
19660
+ const workflows = asRecord6(doc.workflows);
19661
+ if (workflows && Object.keys(workflows).length > 0) return true;
19662
+ return false;
19663
+ }
19664
+ async function waitForSessionDocumentContext(environment, timeoutMs = 3e3) {
19665
+ if (hasSessionDocumentContext(environment.document)) return;
19666
+ await new Promise((resolve) => {
19667
+ let settled = false;
19668
+ let unsubscribe = null;
19669
+ const settle = () => {
19670
+ if (settled) return;
19671
+ settled = true;
19672
+ if (unsubscribe) unsubscribe();
19673
+ clearTimeout(timer);
19674
+ resolve();
19675
+ };
19676
+ const timer = setTimeout(settle, timeoutMs);
19677
+ unsubscribe = environment.on("sync", (document) => {
19678
+ if (hasSessionDocumentContext(document)) {
19679
+ settle();
19680
+ }
19681
+ });
19682
+ });
19683
+ }
19353
19684
  function buildContinuationPreview(checkpoint, noProgressCount) {
19354
19685
  const lines = [
19355
19686
  `Controller no-progress count: ${noProgressCount}`,
@@ -19371,7 +19702,7 @@ function readableAgentMessage(message) {
19371
19702
  const show = asRecord6(record.show);
19372
19703
  const variableNames = asArray3(show?.variableNames).map((value) => String(value)).filter(Boolean);
19373
19704
  if (variableNames.length) {
19374
- return `Displayed ${variableNames.join(", ")}`;
19705
+ return variableNames.length === 1 ? "Displayed the selected record" : "Displayed the selected records";
19375
19706
  }
19376
19707
  if (typeof record.kind === "string") {
19377
19708
  return `Agent ${record.kind} message`;
@@ -19557,6 +19888,202 @@ async function writeJson(filePath, value) {
19557
19888
  await promises.writeFile(filePath, `${JSON.stringify(value, null, 2)}
19558
19889
  `);
19559
19890
  }
19891
+ function safePathSegment(value, fallback) {
19892
+ const normalized = (value || "").trim().toLowerCase();
19893
+ const sanitized = normalized.replace(/[^a-z0-9._-]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 128);
19894
+ return sanitized || fallback;
19895
+ }
19896
+ function extractToolCalls(rawGeneration) {
19897
+ const raw = asRecord6(rawGeneration);
19898
+ if (!raw) return null;
19899
+ const choices = asArray3(raw.choices);
19900
+ const firstChoiceMessage = asRecord6(choices[0]);
19901
+ const message = asRecord6(firstChoiceMessage?.message);
19902
+ const toolCalls = asArray3(message?.tool_calls);
19903
+ if (toolCalls.length > 0) return toolCalls;
19904
+ return asArray3(raw.tool_calls).length > 0 ? asArray3(raw.tool_calls) : null;
19905
+ }
19906
+ function buildTurnMdxReport(input) {
19907
+ const {
19908
+ status,
19909
+ conversation,
19910
+ request,
19911
+ requestTimestamp,
19912
+ turnLog,
19913
+ responseText,
19914
+ terminalKind,
19915
+ actionSummary = [],
19916
+ promptInteractions = [],
19917
+ result,
19918
+ prompts = [],
19919
+ verification,
19920
+ error
19921
+ } = input;
19922
+ const iterationLines = [];
19923
+ for (const iteration of turnLog.iterations) {
19924
+ iterationLines.push(`### Iteration ${iteration.iteration}`);
19925
+ iterationLines.push(
19926
+ `- Request: ${iteration.request}`,
19927
+ `- Generation duration: ${iteration.generationDurationMs !== void 0 ? `${iteration.generationDurationMs}ms` : "unknown"}`
19928
+ );
19929
+ if (iteration.templateId) {
19930
+ iterationLines.push(
19931
+ `- Template: ${iteration.templateId}@${iteration.templateVersion || "unknown"}`
19932
+ );
19933
+ }
19934
+ if (iteration.templateHash) {
19935
+ iterationLines.push(`- Template hash: ${iteration.templateHash}`);
19936
+ }
19937
+ if (iteration.promptInstanceHash) {
19938
+ iterationLines.push(`- Prompt hash: ${iteration.promptInstanceHash}`);
19939
+ }
19940
+ iterationLines.push("", "#### Full prompt sent to LLM", "");
19941
+ iterationLines.push(fenced(iteration.systemPrompt, "text"));
19942
+ if (iteration.generationReply?.trim()) {
19943
+ iterationLines.push(
19944
+ "",
19945
+ "#### LLM reply text",
19946
+ iteration.generationReply.trim(),
19947
+ ""
19948
+ );
19949
+ }
19950
+ if (iteration.generatedCode?.trim()) {
19951
+ iterationLines.push("#### Generated code", "", fenced(iteration.generatedCode.trim(), "ts"));
19952
+ }
19953
+ const toolCalls = extractToolCalls(iteration.rawGeneration);
19954
+ iterationLines.push("#### Tool calls / raw generation", "");
19955
+ if (toolCalls) {
19956
+ iterationLines.push(fenced(JSON.stringify(toolCalls, null, 2), "json"));
19957
+ } else if (iteration.rawGeneration) {
19958
+ iterationLines.push(fenced(JSON.stringify(iteration.rawGeneration, null, 2), "json"));
19959
+ } else {
19960
+ iterationLines.push("_No tool call information._");
19961
+ }
19962
+ if (iteration.tokenUsage) {
19963
+ iterationLines.push("", "#### Token usage", ...formatTokenUsage(iteration.tokenUsage));
19964
+ }
19965
+ if (iteration.responseText?.trim()) {
19966
+ iterationLines.push("", "#### Runtime/prompt outcome", iteration.responseText);
19967
+ }
19968
+ if (iteration.actionSummary?.length) {
19969
+ iterationLines.push("", "#### Action summary", "");
19970
+ iterationLines.push(...iteration.actionSummary.map((line) => `- ${line}`));
19971
+ }
19972
+ if (iteration.continuation) {
19973
+ iterationLines.push("", "#### Continuation", "", jsonBlock(iteration.continuation));
19974
+ }
19975
+ if (iteration.result !== void 0) {
19976
+ iterationLines.push("", "#### Result", "", jsonBlock(iteration.result));
19977
+ }
19978
+ if (iteration.error) {
19979
+ iterationLines.push("", `#### Error`, "", iteration.error);
19980
+ }
19981
+ iterationLines.push("", "---", "");
19982
+ }
19983
+ const promptEventLines = conversation.promptEvents.filter((event) => event.receivedAt >= requestTimestamp).map((event, index) => {
19984
+ const prompt = event.prompt;
19985
+ return `${index + 1}. ${prompt.type} ${prompt.title || ""} ${prompt.message ? `\u2014 ${prompt.message}` : ""}`;
19986
+ });
19987
+ const historyLines = conversation.history.map((entry, index) => {
19988
+ const label = `${index + 1}. ${entry.role}`;
19989
+ const detail = entry.content || (entry.code ? "(code)" : "");
19990
+ return `${label}: ${detail || "(no text)"}`;
19991
+ });
19992
+ const lines = [
19993
+ "# Local Agent Query Report",
19994
+ "",
19995
+ `- timestamp: ${new Date(requestTimestamp).toISOString()}`,
19996
+ `- turn: ${turnLog.turnNumber} (${turnLog.turnId})`,
19997
+ `- status: ${status}`,
19998
+ "",
19999
+ "## Table of contents",
20000
+ "- [Metadata](#metadata)",
20001
+ "- [User query and context](#user-query-and-context)",
20002
+ "- [Conversation history](#conversation-history)",
20003
+ "- [Harness loop iterations](#harness-loop-iterations)",
20004
+ "- [Result and interactions](#result-and-interactions)",
20005
+ "",
20006
+ "## Metadata",
20007
+ `- Ontology: ${conversation.environment.ontologyId}`,
20008
+ `- Subject: ${conversation.environment.subjectId}`,
20009
+ `- Session: ${conversation.environment.sessionId}`,
20010
+ `- Environment: ${conversation.environment.environmentId}`,
20011
+ `- Sandbox: ${conversation.environment.sandboxId}`,
20012
+ `- Permission profile: ${conversation.environment.permissionProfileId}`,
20013
+ `- Conversation artifacts: ${turnLog.turnDir}`,
20014
+ "",
20015
+ "## User query and context",
20016
+ "",
20017
+ "### Request",
20018
+ fenced(request, "text"),
20019
+ "",
20020
+ "### Open prompts at/after request time"
20021
+ ];
20022
+ if (promptEventLines.length) {
20023
+ lines.push(...promptEventLines.map((line) => `- ${line}`));
20024
+ } else {
20025
+ lines.push("- _None");
20026
+ }
20027
+ lines.push(
20028
+ "",
20029
+ "## Conversation history",
20030
+ ...historyLines.map((line) => `- ${line}`),
20031
+ "",
20032
+ "## Harness loop iterations",
20033
+ "",
20034
+ ...iterationLines,
20035
+ "## Result and interactions",
20036
+ ""
20037
+ );
20038
+ if (responseText) {
20039
+ lines.push("### Final response", responseText, "");
20040
+ }
20041
+ if (terminalKind) {
20042
+ lines.push(`### Terminal kind`, terminalKind, "");
20043
+ }
20044
+ lines.push("### Action summary");
20045
+ if (actionSummary.length) {
20046
+ lines.push(...actionSummary.map((line) => `- ${line}`));
20047
+ } else {
20048
+ lines.push("- None");
20049
+ }
20050
+ lines.push("", "### User interactions");
20051
+ if (promptInteractions.length) {
20052
+ for (const interaction of promptInteractions) {
20053
+ lines.push(
20054
+ `- ${interaction.type} ${interaction.message || interaction.title} -> ${JSON.stringify(interaction.answer)}`
20055
+ );
20056
+ }
20057
+ } else {
20058
+ lines.push("- None");
20059
+ }
20060
+ lines.push("", "### Pending prompts");
20061
+ if (prompts.length) {
20062
+ for (const prompt of prompts) {
20063
+ lines.push(`- ${prompt.type} ${prompt.title || ""} ${prompt.message || ""}`);
20064
+ }
20065
+ } else {
20066
+ lines.push("- None");
20067
+ }
20068
+ if (result !== void 0) {
20069
+ lines.push("", "### Result payload", "", jsonBlock(result));
20070
+ }
20071
+ if (verification !== void 0) {
20072
+ lines.push("", "### Verification", "", jsonBlock(verification));
20073
+ }
20074
+ if (error) {
20075
+ lines.push("", `### Error`, "", error);
20076
+ }
20077
+ if (turnLog.error) {
20078
+ lines.push("", "### Turn log error", "", turnLog.error);
20079
+ }
20080
+ lines.push("");
20081
+ if (iterationLines.length === 0) {
20082
+ lines.splice(lines.indexOf("## Harness loop iterations") + 1, 0, "- _No iterations recorded._");
20083
+ }
20084
+ return `${lines.join("\n")}
20085
+ `;
20086
+ }
19560
20087
  function describeScenarioBehavior(result) {
19561
20088
  if (result.scenario.description?.trim()) {
19562
20089
  return result.scenario.description.trim();
@@ -20337,6 +20864,16 @@ function createAgentEvalHarness(options) {
20337
20864
  const chatTimeoutMs = options.chatTimeoutMs ?? 12e4;
20338
20865
  const jobTimeoutMs = options.jobTimeoutMs ?? 9e4;
20339
20866
  const pollIntervalMs = options.pollIntervalMs ?? 250;
20867
+ const isTruthyEnv = (value) => {
20868
+ return value?.trim().toLowerCase() === "1" || value?.trim().toLowerCase() === "true" || value?.trim().toLowerCase() === "yes" || value?.trim().toLowerCase() === "on";
20869
+ };
20870
+ const localTurnReportsEnabled = (() => {
20871
+ if (typeof options.local === "boolean") return options.local;
20872
+ return process.env.NODE_ENV === "development" || isTruthyEnv(process.env.GRANULAR_LOCAL) || isTruthyEnv(process.env.GRANULAR_USE_LOCAL_ENDPOINTS) || isTruthyEnv(process.env.GRANULAR_USE_LOCAL) || process.env.GRANULAR_ENDPOINT_MODE?.toLowerCase() === "local";
20873
+ })();
20874
+ const localTurnReportBaseDir = path__default.default.resolve(
20875
+ options.localTurnReportBaseDir || process.cwd()
20876
+ );
20340
20877
  const resolvedTemplate = resolveHarnessTemplate(
20341
20878
  options.harnessTemplateId || process.env.GRANULAR_AGENT_HARNESS_TEMPLATE || "stable");
20342
20879
  const promptRenderer = options.promptRenderer || resolvedTemplate.renderPrompt;
@@ -20364,6 +20901,7 @@ function createAgentEvalHarness(options) {
20364
20901
  if (environment.getEffects().length > 0) break;
20365
20902
  await sleep2(250);
20366
20903
  }
20904
+ await waitForSessionDocumentContext(environment);
20367
20905
  await ensureDir(path__default.default.join(artifactDir, slugify(label)));
20368
20906
  return {
20369
20907
  label,
@@ -20384,6 +20922,41 @@ function createAgentEvalHarness(options) {
20384
20922
  } catch {
20385
20923
  }
20386
20924
  }
20925
+ async function writeLocalTurnReport(input) {
20926
+ if (!localTurnReportsEnabled) return;
20927
+ const { status, conversation, turnLog, request, requestTimestamp, error } = input;
20928
+ const requestId = safePathSegment(
20929
+ new Date(requestTimestamp).toISOString().replace(/[:.]/g, "-"),
20930
+ "turn"
20931
+ );
20932
+ const reportDir = path__default.default.join(
20933
+ localTurnReportBaseDir,
20934
+ safePathSegment(conversation.environment.ontologyId, "ontology"),
20935
+ safePathSegment(conversation.environment.subjectId, "subject"),
20936
+ safePathSegment(conversation.environment.sessionId, "session")
20937
+ );
20938
+ await ensureDir(reportDir);
20939
+ const completed = input.completed;
20940
+ const pending = input.pending;
20941
+ const report = buildTurnMdxReport({
20942
+ status,
20943
+ conversation,
20944
+ request,
20945
+ requestTimestamp,
20946
+ turnLog,
20947
+ responseText: completed?.responseText,
20948
+ terminalKind: completed?.terminalKind,
20949
+ actionSummary: completed?.actionSummary,
20950
+ promptInteractions: completed?.promptInteractions || pending?.promptInteractions,
20951
+ result: completed?.result,
20952
+ prompts: pending?.prompts,
20953
+ verification: completed?.verification,
20954
+ error
20955
+ });
20956
+ const reportPath = path__default.default.join(reportDir, `${requestId}.mdx`);
20957
+ await promises.writeFile(reportPath, report);
20958
+ console.log(`[agent][local] saved query report: ${reportPath}`);
20959
+ }
20387
20960
  async function runCheckJob(code, session) {
20388
20961
  const job = await session.submitJob(code);
20389
20962
  return withTimeout2(job.result, jobTimeoutMs, `check job ${job.id}`);
@@ -20643,237 +21216,329 @@ function createAgentEvalHarness(options) {
20643
21216
  const baselineClosureId = getCurrentClosureId(
20644
21217
  cloneJson(conversation.environment.document)
20645
21218
  );
20646
- while (iteration < maxIterations) {
20647
- const liveDoc = cloneJson(conversation.environment.document);
20648
- const pendingPrompts = filterPromptsByBoundary(
20649
- liveDoc,
20650
- getOpenPromptsFromDoc(liveDoc),
20651
- boundaryTimestamp
20652
- );
20653
- const workflowFocus = projectWorkflowFocus(liveDoc, pendingPrompts, {
20654
- boundaryTimestamp
21219
+ const writeTurnReport = async (status) => {
21220
+ await writeLocalTurnReport({
21221
+ status: status.type,
21222
+ conversation,
21223
+ turnLog,
21224
+ request: input.request,
21225
+ requestTimestamp: boundaryTimestamp,
21226
+ completed: status.completed,
21227
+ pending: status.pending,
21228
+ error: status.error
20655
21229
  });
20656
- const referentFocus = projectConversationReferentFocus(liveDoc);
20657
- const heapFocus = {
20658
- variableNames: [
20659
- ...workflowFocus.variableNames,
20660
- ...referentFocus.variableNames
20661
- ],
20662
- listNames: [...workflowFocus.listNames, ...referentFocus.listNames],
20663
- entryPaths: [...workflowFocus.entryPaths, ...referentFocus.entryPaths]
20664
- };
20665
- const tools = conversation.environment.getEffects().map((tool) => ({
20666
- name: tool.name,
20667
- description: tool.description,
20668
- className: tool.className,
20669
- static: tool.static,
20670
- ready: tool.ready,
20671
- inputSchema: tool.inputSchema,
20672
- outputSchema: tool.outputSchema
20673
- }));
20674
- const renderedPrompt = promptRenderer({
20675
- domainDocumentation: await conversation.environment.getDomainDocumentation(),
20676
- sessionContext: {
20677
- sandboxId: conversation.environment.sandboxId,
20678
- environmentId: conversation.environment.environmentId,
20679
- domainRevision: conversation.environment.domainRevision
20680
- },
20681
- heapSummary: projectHeapSummary(asRecord6(liveDoc?.heap), {
20682
- focus: heapFocus
20683
- }),
20684
- fileSummary: projectSessionFileSummary(liveDoc),
20685
- referentSummary: projectConversationReferentSummary(liveDoc),
20686
- loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
21230
+ };
21231
+ try {
21232
+ while (iteration < maxIterations) {
21233
+ const liveDoc = cloneJson(conversation.environment.document);
21234
+ const promptHeap = normalizeHeapSnapshot2(asRecord6(liveDoc?.heap));
21235
+ const pendingPrompts = filterPromptsByBoundary(
21236
+ liveDoc,
21237
+ getOpenPromptsFromDoc(liveDoc),
20687
21238
  boundaryTimestamp
20688
- }),
20689
- workflowSummary: projectWorkflowSummary(liveDoc, pendingPrompts, {
21239
+ );
21240
+ const workflowFocus = projectWorkflowFocus(liveDoc, pendingPrompts, {
20690
21241
  boundaryTimestamp
20691
- }),
20692
- tools,
20693
- checkpoint: latestCheckpoint
20694
- });
20695
- const systemPrompt = renderedPrompt.prompt;
20696
- await emitProgress(onProgress, {
20697
- phase: "prompt",
20698
- status: "passed",
20699
- scenarioId: conversation.label,
20700
- stepId: turnId,
20701
- iteration: iteration + 1,
20702
- templateId: renderedPrompt.templateId,
20703
- templateVersion: renderedPrompt.templateVersion,
20704
- title: "Rendered harness prompt",
20705
- message: `${systemPrompt.split("\n").length} lines`,
20706
- data: {
20707
- templateId: renderedPrompt.templateId,
20708
- templateVersion: renderedPrompt.templateVersion,
20709
- templateHash: renderedPrompt.templateHash,
20710
- promptInstanceHash: renderedPrompt.promptInstanceHash,
20711
- prompt: systemPrompt
20712
- }
20713
- });
20714
- const request = iteration === 0 ? input.request : continuationRenderer(
20715
- buildContinuationPreview(latestCheckpoint, noProgressCount)
20716
- ).instruction;
20717
- await emitProgress(onProgress, {
20718
- phase: "generation",
20719
- status: "running",
20720
- scenarioId: conversation.label,
20721
- stepId: turnId,
20722
- iteration: iteration + 1,
20723
- templateId: renderedPrompt.templateId,
20724
- templateVersion: renderedPrompt.templateVersion,
20725
- title: iteration === 0 ? "Generating agent response" : "Generating continuation",
20726
- message: request
20727
- });
20728
- const generation = await withTimeout2(
20729
- generateTurnWithRepair(options.generator, {
20730
- systemPrompt,
20731
- history: buildHistory(conversation.history),
20732
- request,
20733
- attempt: 1,
20734
- tools,
20735
- usageContext: {
21242
+ });
21243
+ const referentFocus = projectConversationReferentFocus(liveDoc);
21244
+ const heapFocus = {
21245
+ variableNames: [
21246
+ ...workflowFocus.variableNames,
21247
+ ...referentFocus.variableNames
21248
+ ],
21249
+ listNames: [...workflowFocus.listNames, ...referentFocus.listNames],
21250
+ entryPaths: [
21251
+ ...workflowFocus.entryPaths,
21252
+ ...referentFocus.entryPaths,
21253
+ ...focusedHeapEntryPathsFromUiContext(promptHeap, input.uiContext)
21254
+ ]
21255
+ };
21256
+ const tools = conversation.environment.getEffects().map((tool) => ({
21257
+ name: tool.name,
21258
+ description: tool.description,
21259
+ className: tool.className,
21260
+ static: tool.static,
21261
+ ready: tool.ready,
21262
+ inputSchema: tool.inputSchema,
21263
+ outputSchema: tool.outputSchema
21264
+ }));
21265
+ const renderedPrompt = promptRenderer({
21266
+ domainDocumentation: await conversation.environment.getDomainDocumentation(),
21267
+ sessionContext: {
20736
21268
  sandboxId: conversation.environment.sandboxId,
20737
21269
  environmentId: conversation.environment.environmentId,
20738
- sessionId: conversation.environment.sessionId,
20739
- subjectId: conversation.environment.subjectId,
20740
- permissionProfileId: conversation.environment.permissionProfileId
21270
+ domainRevision: conversation.environment.domainRevision,
21271
+ uiContext: input.uiContext || null
21272
+ },
21273
+ heapSummary: projectHeapSummary(asRecord6(liveDoc?.heap), {
21274
+ focus: heapFocus
21275
+ }),
21276
+ fileSummary: projectSessionFileSummary(liveDoc),
21277
+ referentSummary: projectConversationReferentSummary(liveDoc),
21278
+ loopSummary: projectLoopSummary(liveDoc, pendingPrompts, {
21279
+ boundaryTimestamp
21280
+ }),
21281
+ workflowSummary: projectWorkflowSummary(liveDoc, pendingPrompts, {
21282
+ boundaryTimestamp
21283
+ }),
21284
+ tools,
21285
+ checkpoint: latestCheckpoint
21286
+ });
21287
+ const systemPrompt = renderedPrompt.prompt;
21288
+ await emitProgress(onProgress, {
21289
+ phase: "prompt",
21290
+ status: "passed",
21291
+ scenarioId: conversation.label,
21292
+ stepId: turnId,
21293
+ iteration: iteration + 1,
21294
+ templateId: renderedPrompt.templateId,
21295
+ templateVersion: renderedPrompt.templateVersion,
21296
+ title: "Rendered harness prompt",
21297
+ message: `${systemPrompt.split("\n").length} lines`,
21298
+ data: {
21299
+ templateId: renderedPrompt.templateId,
21300
+ templateVersion: renderedPrompt.templateVersion,
21301
+ templateHash: renderedPrompt.templateHash,
21302
+ promptInstanceHash: renderedPrompt.promptInstanceHash,
21303
+ prompt: systemPrompt
20741
21304
  }
20742
- }),
20743
- chatTimeoutMs,
20744
- `chat generation for ${conversation.label} iteration ${iteration + 1}`
20745
- );
20746
- await emitProgress(onProgress, {
20747
- phase: "generation",
20748
- status: "passed",
20749
- scenarioId: conversation.label,
20750
- stepId: turnId,
20751
- iteration: iteration + 1,
20752
- templateId: renderedPrompt.templateId,
20753
- templateVersion: renderedPrompt.templateVersion,
20754
- title: generation.code ? "Generated job code" : "Generated text reply",
20755
- message: generation.code || generation.reply || "",
20756
- data: {
20757
- reply: generation.reply,
20758
- code: generation.code,
20759
- attempts: generation.generationAttempts,
20760
- usage: tokenUsageForGenerationOutput(generation)
20761
- }
20762
- });
20763
- const iterationLog = {
20764
- iteration: iteration + 1,
20765
- request,
20766
- systemPrompt,
20767
- templateId: renderedPrompt.templateId,
20768
- templateVersion: renderedPrompt.templateVersion,
20769
- templateHash: renderedPrompt.templateHash,
20770
- promptInstanceHash: renderedPrompt.promptInstanceHash,
20771
- generationReply: generation.reply,
20772
- generatedCode: generation.code,
20773
- rawGeneration: generation.raw,
20774
- generationAttempts: generation.generationAttempts,
20775
- tokenUsage: tokenUsageForGenerationOutput(generation)
20776
- };
20777
- turnLog.iterations.push(iterationLog);
20778
- await writeJson(
20779
- path__default.default.join(turnDir, `iteration-${iteration + 1}-generation.json`),
20780
- generation
20781
- );
20782
- if (!generation.code) {
20783
- const responseText2 = generation.reply?.trim() || "Done.";
20784
- conversation.history.push({ role: "assistant", content: responseText2 });
20785
- const completed = {
20786
- conversation,
20787
- request: input.request,
20788
- turnDir,
20789
- responseText: responseText2,
20790
- terminalKind: "reply",
20791
- actionSummary: [],
20792
- promptInteractions: [],
20793
- verification: null,
20794
- result: generation.reply?.trim() || responseText2
20795
- };
20796
- if (input.verification) {
20797
- completed.verification = await runInspection(
20798
- conversation,
20799
- input.verification,
20800
- completed,
20801
- turnDir
20802
- );
20803
- }
20804
- iterationLog.responseText = responseText2;
20805
- iterationLog.terminalKind = "reply";
20806
- iterationLog.actionSummary = [];
20807
- iterationLog.promptInteractions = [];
20808
- iterationLog.result = completed.result;
20809
- turnLog.completed = {
20810
- responseText: responseText2,
20811
- terminalKind: "reply",
20812
- actionSummary: [],
20813
- promptInteractions: [],
20814
- result: completed.result
21305
+ });
21306
+ const request = iteration === 0 ? input.request : continuationRenderer(
21307
+ buildContinuationPreview(latestCheckpoint, noProgressCount)
21308
+ ).instruction;
21309
+ await emitProgress(onProgress, {
21310
+ phase: "generation",
21311
+ status: "running",
21312
+ scenarioId: conversation.label,
21313
+ stepId: turnId,
21314
+ iteration: iteration + 1,
21315
+ templateId: renderedPrompt.templateId,
21316
+ templateVersion: renderedPrompt.templateVersion,
21317
+ title: iteration === 0 ? "Generating agent response" : "Generating continuation",
21318
+ message: request
21319
+ });
21320
+ const emittedGeneratedReasoningLines = /* @__PURE__ */ new Set();
21321
+ let generatedReasoningBuffer = "";
21322
+ const emitGeneratedReasoningLine = async (line) => {
21323
+ if (emittedGeneratedReasoningLines.has(line)) return;
21324
+ emittedGeneratedReasoningLines.add(line);
21325
+ await emitProgress(onProgress, {
21326
+ phase: "generation",
21327
+ status: "running",
21328
+ scenarioId: conversation.label,
21329
+ stepId: turnId,
21330
+ iteration: iteration + 1,
21331
+ templateId: renderedPrompt.templateId,
21332
+ templateVersion: renderedPrompt.templateVersion,
21333
+ title: "Generated reasoning comment",
21334
+ message: line,
21335
+ data: { source: "generated_code_comment" }
21336
+ });
20815
21337
  };
20816
- await writeJson(path__default.default.join(turnDir, "result.json"), completed);
21338
+ const generationStartedAt = Date.now();
21339
+ const generation = await withTimeout2(
21340
+ generateTurnWithRepair(options.generator, {
21341
+ systemPrompt,
21342
+ history: buildHistory(conversation.history),
21343
+ request,
21344
+ attempt: 1,
21345
+ tools,
21346
+ onReplyDelta: async (delta) => {
21347
+ await emitProgress(onProgress, {
21348
+ phase: "generation",
21349
+ status: "running",
21350
+ scenarioId: conversation.label,
21351
+ stepId: turnId,
21352
+ iteration: iteration + 1,
21353
+ templateId: renderedPrompt.templateId,
21354
+ templateVersion: renderedPrompt.templateVersion,
21355
+ title: "Generated text reply delta",
21356
+ message: delta,
21357
+ data: { delta }
21358
+ });
21359
+ },
21360
+ onCodeDelta: async (delta) => {
21361
+ const parsed = consumeGranularReasoningOnlyChunk(
21362
+ generatedReasoningBuffer,
21363
+ delta
21364
+ );
21365
+ generatedReasoningBuffer = parsed.buffer;
21366
+ for (const line of parsed.reasoningLines) {
21367
+ await emitGeneratedReasoningLine(line);
21368
+ }
21369
+ },
21370
+ usageContext: {
21371
+ sandboxId: conversation.environment.sandboxId,
21372
+ environmentId: conversation.environment.environmentId,
21373
+ sessionId: conversation.environment.sessionId,
21374
+ subjectId: conversation.environment.subjectId,
21375
+ permissionProfileId: conversation.environment.permissionProfileId
21376
+ }
21377
+ }),
21378
+ chatTimeoutMs,
21379
+ `chat generation for ${conversation.label} iteration ${iteration + 1}`
21380
+ );
21381
+ const generationDurationMs = Date.now() - generationStartedAt;
20817
21382
  await emitProgress(onProgress, {
20818
- phase: "step",
21383
+ phase: "generation",
20819
21384
  status: "passed",
20820
21385
  scenarioId: conversation.label,
20821
21386
  stepId: turnId,
20822
- title: "Step completed with text reply",
20823
- message: responseText2,
20824
- data: completed
21387
+ iteration: iteration + 1,
21388
+ templateId: renderedPrompt.templateId,
21389
+ templateVersion: renderedPrompt.templateVersion,
21390
+ title: generation.code ? "Generated job code" : "Generated text reply",
21391
+ message: generation.code || generation.reply || "",
21392
+ data: {
21393
+ reply: generation.reply,
21394
+ code: generation.code,
21395
+ attempts: generation.generationAttempts,
21396
+ usage: tokenUsageForGenerationOutput(generation)
21397
+ }
20825
21398
  });
20826
- return completed;
20827
- }
20828
- const session = conversation.environment;
20829
- const job = await session.submitJob(generation.code, {
20830
- agent: {
20831
- userRequest: input.request,
20832
- generationRequest: request,
20833
- systemPrompt,
20834
- history: buildHistory(conversation.history),
20835
- scenarioLabel: conversation.label,
20836
- turnId,
21399
+ const generatedReasoningLines = generation.code ? consumeGranularReasoningOnlyChunk("", generation.code, {
21400
+ final: true
21401
+ }).reasoningLines : [];
21402
+ for (const line of generatedReasoningLines) {
21403
+ await emitGeneratedReasoningLine(line);
21404
+ }
21405
+ const iterationLog = {
20837
21406
  iteration: iteration + 1,
20838
- tools,
21407
+ request,
21408
+ generationDurationMs,
21409
+ systemPrompt,
21410
+ templateId: renderedPrompt.templateId,
21411
+ templateVersion: renderedPrompt.templateVersion,
21412
+ templateHash: renderedPrompt.templateHash,
21413
+ promptInstanceHash: renderedPrompt.promptInstanceHash,
20839
21414
  generationReply: generation.reply,
21415
+ generatedCode: generation.code,
20840
21416
  rawGeneration: generation.raw,
20841
- repairIssues: generation.generationAttempts?.flatMap(
20842
- (attempt) => attempt.repairIssues || []
20843
- )
20844
- }
20845
- });
20846
- await emitProgress(onProgress, {
20847
- phase: "job",
20848
- status: "running",
20849
- scenarioId: conversation.label,
20850
- stepId: turnId,
20851
- iteration: iteration + 1,
20852
- jobId: job.id,
20853
- templateId: renderedPrompt.templateId,
20854
- templateVersion: renderedPrompt.templateVersion,
20855
- title: "Submitted Granular job",
20856
- message: job.id,
20857
- data: { code: generation.code }
20858
- });
20859
- const outcome = await waitForJobOutcome({
20860
- environment: conversation.environment,
20861
- job,
20862
- boundaryTimestamp,
20863
- timeoutMs: jobTimeoutMs,
20864
- pollIntervalMs,
20865
- onProgress: (event) => void onProgress?.(event),
20866
- progressContext: {
21417
+ generationAttempts: generation.generationAttempts,
21418
+ tokenUsage: tokenUsageForGenerationOutput(generation)
21419
+ };
21420
+ turnLog.iterations.push(iterationLog);
21421
+ await writeJson(
21422
+ path__default.default.join(turnDir, `iteration-${iteration + 1}-generation.json`),
21423
+ generation
21424
+ );
21425
+ if (!generation.code) {
21426
+ const responseText2 = generation.reply?.trim() || "Done.";
21427
+ conversation.history.push({ role: "assistant", content: responseText2 });
21428
+ const completed = {
21429
+ conversation,
21430
+ request: input.request,
21431
+ turnDir,
21432
+ responseText: responseText2,
21433
+ terminalKind: "reply",
21434
+ actionSummary: [],
21435
+ promptInteractions: [],
21436
+ verification: null,
21437
+ result: generation.reply?.trim() || responseText2
21438
+ };
21439
+ if (input.verification) {
21440
+ completed.verification = await runInspection(
21441
+ conversation,
21442
+ input.verification,
21443
+ completed,
21444
+ turnDir
21445
+ );
21446
+ }
21447
+ iterationLog.responseText = responseText2;
21448
+ iterationLog.terminalKind = "reply";
21449
+ iterationLog.actionSummary = [];
21450
+ iterationLog.promptInteractions = [];
21451
+ iterationLog.result = completed.result;
21452
+ turnLog.completed = {
21453
+ responseText: responseText2,
21454
+ terminalKind: "reply",
21455
+ actionSummary: [],
21456
+ promptInteractions: [],
21457
+ result: completed.result
21458
+ };
21459
+ await writeJson(path__default.default.join(turnDir, "result.json"), completed);
21460
+ await writeTurnReport({ type: "completed", completed });
21461
+ await emitProgress(onProgress, {
21462
+ phase: "step",
21463
+ status: "passed",
21464
+ scenarioId: conversation.label,
21465
+ stepId: turnId,
21466
+ title: "Step completed with text reply",
21467
+ message: responseText2,
21468
+ data: completed
21469
+ });
21470
+ return completed;
21471
+ }
21472
+ const session = conversation.environment;
21473
+ const job = await session.submitJob(generation.code, {
21474
+ agent: {
21475
+ userRequest: input.request,
21476
+ generationRequest: request,
21477
+ systemPrompt,
21478
+ history: buildHistory(conversation.history),
21479
+ scenarioLabel: conversation.label,
21480
+ turnId,
21481
+ iteration: iteration + 1,
21482
+ tools,
21483
+ generationReply: generation.reply,
21484
+ rawGeneration: generation.raw,
21485
+ repairIssues: generation.generationAttempts?.flatMap(
21486
+ (attempt) => attempt.repairIssues || []
21487
+ )
21488
+ }
21489
+ });
21490
+ await emitProgress(onProgress, {
21491
+ phase: "job",
21492
+ status: "running",
20867
21493
  scenarioId: conversation.label,
20868
21494
  stepId: turnId,
20869
21495
  iteration: iteration + 1,
21496
+ jobId: job.id,
20870
21497
  templateId: renderedPrompt.templateId,
20871
- templateVersion: renderedPrompt.templateVersion
20872
- }
20873
- });
20874
- if (outcome.kind === "prompt") {
20875
- if (!autoAnswerPrompts) {
20876
- return {
21498
+ templateVersion: renderedPrompt.templateVersion,
21499
+ title: "Submitted Granular job",
21500
+ message: job.id,
21501
+ data: { code: generation.code }
21502
+ });
21503
+ const outcome = await waitForJobOutcome({
21504
+ environment: conversation.environment,
21505
+ job,
21506
+ boundaryTimestamp,
21507
+ timeoutMs: jobTimeoutMs,
21508
+ pollIntervalMs,
21509
+ onProgress: (event) => void onProgress?.(event),
21510
+ progressContext: {
21511
+ scenarioId: conversation.label,
21512
+ stepId: turnId,
21513
+ iteration: iteration + 1,
21514
+ templateId: renderedPrompt.templateId,
21515
+ templateVersion: renderedPrompt.templateVersion
21516
+ }
21517
+ });
21518
+ if (outcome.kind === "prompt") {
21519
+ if (!autoAnswerPrompts) {
21520
+ const pending2 = {
21521
+ conversation,
21522
+ request: input.request,
21523
+ turnDir,
21524
+ boundaryTimestamp,
21525
+ finalCode: generation.code,
21526
+ finalReply: generation.reply?.trim() || "",
21527
+ stdout: outcome.stdout,
21528
+ stderr: outcome.stderr,
21529
+ prompts: outcome.prompts,
21530
+ promptInteractions: [],
21531
+ job
21532
+ };
21533
+ await writeTurnReport({ type: "pending", pending: pending2 });
21534
+ return pending2;
21535
+ }
21536
+ if (!input.human) {
21537
+ throw new Error(
21538
+ "This turn reached a human prompt but no responder was provided"
21539
+ );
21540
+ }
21541
+ let pending = {
20877
21542
  conversation,
20878
21543
  request: input.request,
20879
21544
  turnDir,
@@ -20886,187 +21551,180 @@ function createAgentEvalHarness(options) {
20886
21551
  promptInteractions: [],
20887
21552
  job
20888
21553
  };
21554
+ while ("prompts" in pending) {
21555
+ const resumed = await resumePendingTurn(pending, input.human);
21556
+ if ("prompts" in resumed) {
21557
+ pending = resumed;
21558
+ continue;
21559
+ }
21560
+ if (input.verification) {
21561
+ resumed.verification = await runInspection(
21562
+ conversation,
21563
+ input.verification,
21564
+ resumed,
21565
+ turnDir
21566
+ );
21567
+ }
21568
+ turnLog.completed = {
21569
+ responseText: resumed.responseText,
21570
+ terminalKind: resumed.terminalKind,
21571
+ actionSummary: resumed.actionSummary,
21572
+ promptInteractions: resumed.promptInteractions,
21573
+ result: resumed.result
21574
+ };
21575
+ await writeTurnReport({ type: "completed", completed: resumed });
21576
+ return resumed;
21577
+ }
20889
21578
  }
20890
- if (!input.human) {
21579
+ if (outcome.kind !== "completed") {
20891
21580
  throw new Error(
20892
- "This turn reached a human prompt but no responder was provided"
21581
+ "Unexpected non-completed outcome after prompt handling"
20893
21582
  );
20894
21583
  }
20895
- let pending = {
20896
- conversation,
20897
- request: input.request,
20898
- turnDir,
20899
- boundaryTimestamp,
20900
- finalCode: generation.code,
20901
- finalReply: generation.reply?.trim() || "",
21584
+ await sleep2(350);
21585
+ const settledLiveDoc = cloneJson(
21586
+ conversation.environment.document
21587
+ );
21588
+ const sessionHeap = normalizeHeapSnapshot2(asRecord6(settledLiveDoc?.heap));
21589
+ const presentation = resolveJobPresentation({
21590
+ jobId: job.id,
21591
+ result: outcome.result,
20902
21592
  stdout: outcome.stdout,
20903
- stderr: outcome.stderr,
20904
- prompts: outcome.prompts,
20905
- promptInteractions: [],
20906
- job
21593
+ agentMessages: getJobAgentMessages(settledLiveDoc, job.id),
21594
+ sessionHeap
21595
+ });
21596
+ const responseText = presentation.responseText || generation.reply?.trim() || "Done.";
21597
+ const verifierSnapshot = createHarnessVerifierSnapshot({
21598
+ finalCode: generation.code,
21599
+ resultPreview: JSON.stringify(outcome.result, null, 2),
21600
+ liveDoc: settledLiveDoc,
21601
+ projectionOptions: { boundaryTimestamp }
21602
+ });
21603
+ const continuation = evaluateContinuation({
21604
+ iteration,
21605
+ budgets: controllerBudgets,
21606
+ baselineClosureId,
21607
+ currentClosureId: getCurrentClosureId(settledLiveDoc),
21608
+ liveDoc: settledLiveDoc,
21609
+ pendingPrompts: filterPromptsByBoundary(
21610
+ settledLiveDoc,
21611
+ getOpenPromptsFromDoc(settledLiveDoc),
21612
+ boundaryTimestamp
21613
+ ),
21614
+ projectionOptions: { boundaryTimestamp },
21615
+ latestResponseText: responseText,
21616
+ previousSnapshot,
21617
+ currentSnapshot: verifierSnapshot,
21618
+ previousNoProgressCount: noProgressCount
21619
+ });
21620
+ latestCheckpoint = {
21621
+ iteration: iteration + 1,
21622
+ latestJobStatus: "succeeded",
21623
+ latestJobResult: JSON.stringify(outcome.result, null, 2),
21624
+ latestActionSummary: getActionSummary(settledLiveDoc, job.id),
21625
+ controllerOutcome: continuation.outcome,
21626
+ controllerReason: continuation.reason,
21627
+ noProgressCount: continuation.nextNoProgressCount
20907
21628
  };
20908
- while ("prompts" in pending) {
20909
- const resumed = await resumePendingTurn(pending, input.human);
20910
- if ("prompts" in resumed) {
20911
- pending = resumed;
20912
- continue;
21629
+ await emitProgress(onProgress, {
21630
+ phase: "continuation",
21631
+ status: continuation.shouldContinue ? "running" : "passed",
21632
+ scenarioId: conversation.label,
21633
+ stepId: turnId,
21634
+ iteration: iteration + 1,
21635
+ jobId: job.id,
21636
+ templateId: renderedPrompt.templateId,
21637
+ templateVersion: renderedPrompt.templateVersion,
21638
+ title: continuation.shouldContinue ? "Harness requested another loop" : "Harness accepted completion",
21639
+ message: `${continuation.reason}; ${continuation.outcome}`,
21640
+ data: {
21641
+ continuation,
21642
+ checkpoint: latestCheckpoint,
21643
+ verifierSnapshot
20913
21644
  }
21645
+ });
21646
+ previousSnapshot = verifierSnapshot;
21647
+ noProgressCount = continuation.nextNoProgressCount;
21648
+ conversation.history.push({
21649
+ role: "assistant",
21650
+ content: responseText,
21651
+ code: generation.code,
21652
+ jobStatus: "succeeded",
21653
+ jobResultPreview: JSON.stringify(outcome.result, null, 2)
21654
+ });
21655
+ await writeJson(
21656
+ path__default.default.join(turnDir, `iteration-${iteration + 1}-result.json`),
21657
+ {
21658
+ responseText,
21659
+ continuation,
21660
+ actionSummary: latestCheckpoint.latestActionSummary,
21661
+ result: outcome.result
21662
+ }
21663
+ );
21664
+ iterationLog.responseText = responseText;
21665
+ iterationLog.terminalKind = getCurrentClosureId(settledLiveDoc) ? "closure" : "reply";
21666
+ iterationLog.actionSummary = latestCheckpoint.latestActionSummary || [];
21667
+ iterationLog.promptInteractions = [];
21668
+ iterationLog.continuation = continuation;
21669
+ iterationLog.result = outcome.result;
21670
+ if (!continuation.shouldContinue) {
21671
+ const completed = {
21672
+ conversation,
21673
+ request: input.request,
21674
+ turnDir,
21675
+ responseText,
21676
+ terminalKind: getCurrentClosureId(settledLiveDoc) ? "closure" : "reply",
21677
+ finalCode: generation.code,
21678
+ actionSummary: latestCheckpoint.latestActionSummary || [],
21679
+ promptInteractions: [],
21680
+ verification: null,
21681
+ result: outcome.result
21682
+ };
20914
21683
  if (input.verification) {
20915
- resumed.verification = await runInspection(
21684
+ completed.verification = await runInspection(
20916
21685
  conversation,
20917
21686
  input.verification,
20918
- resumed,
21687
+ completed,
20919
21688
  turnDir
20920
21689
  );
20921
21690
  }
20922
21691
  turnLog.completed = {
20923
- responseText: resumed.responseText,
20924
- terminalKind: resumed.terminalKind,
20925
- actionSummary: resumed.actionSummary,
20926
- promptInteractions: resumed.promptInteractions,
20927
- result: resumed.result
21692
+ responseText,
21693
+ terminalKind: completed.terminalKind,
21694
+ actionSummary: completed.actionSummary,
21695
+ promptInteractions: [],
21696
+ result: outcome.result
20928
21697
  };
20929
- return resumed;
21698
+ await writeJson(path__default.default.join(turnDir, "result.json"), completed);
21699
+ await writeTurnReport({ type: "completed", completed });
21700
+ await emitProgress(onProgress, {
21701
+ phase: "step",
21702
+ status: "passed",
21703
+ scenarioId: conversation.label,
21704
+ stepId: turnId,
21705
+ iteration: iteration + 1,
21706
+ jobId: job.id,
21707
+ title: "Step completed",
21708
+ message: responseText,
21709
+ data: completed
21710
+ });
21711
+ return completed;
20930
21712
  }
21713
+ iteration += 1;
20931
21714
  }
20932
- if (outcome.kind !== "completed") {
20933
- throw new Error(
20934
- "Unexpected non-completed outcome after prompt handling"
20935
- );
20936
- }
20937
- await sleep2(350);
20938
- const settledLiveDoc = cloneJson(
20939
- conversation.environment.document
20940
- );
20941
- const sessionHeap = normalizeHeapSnapshot2(asRecord6(settledLiveDoc?.heap));
20942
- const presentation = resolveJobPresentation({
20943
- jobId: job.id,
20944
- result: outcome.result,
20945
- stdout: outcome.stdout,
20946
- agentMessages: getJobAgentMessages(settledLiveDoc, job.id),
20947
- sessionHeap
20948
- });
20949
- const responseText = presentation.responseText || generation.reply?.trim() || "Done.";
20950
- const verifierSnapshot = createHarnessVerifierSnapshot({
20951
- finalCode: generation.code,
20952
- resultPreview: JSON.stringify(outcome.result, null, 2),
20953
- liveDoc: settledLiveDoc,
20954
- projectionOptions: { boundaryTimestamp }
20955
- });
20956
- const continuation = evaluateContinuation({
20957
- iteration,
20958
- budgets: controllerBudgets,
20959
- baselineClosureId,
20960
- currentClosureId: getCurrentClosureId(settledLiveDoc),
20961
- liveDoc: settledLiveDoc,
20962
- pendingPrompts: filterPromptsByBoundary(
20963
- settledLiveDoc,
20964
- getOpenPromptsFromDoc(settledLiveDoc),
20965
- boundaryTimestamp
20966
- ),
20967
- projectionOptions: { boundaryTimestamp },
20968
- latestResponseText: responseText,
20969
- previousSnapshot,
20970
- currentSnapshot: verifierSnapshot,
20971
- previousNoProgressCount: noProgressCount
20972
- });
20973
- latestCheckpoint = {
20974
- iteration: iteration + 1,
20975
- latestJobStatus: "succeeded",
20976
- latestJobResult: JSON.stringify(outcome.result, null, 2),
20977
- latestActionSummary: getActionSummary(settledLiveDoc, job.id),
20978
- controllerOutcome: continuation.outcome,
20979
- controllerReason: continuation.reason,
20980
- noProgressCount: continuation.nextNoProgressCount
20981
- };
20982
- await emitProgress(onProgress, {
20983
- phase: "continuation",
20984
- status: continuation.shouldContinue ? "running" : "passed",
20985
- scenarioId: conversation.label,
20986
- stepId: turnId,
20987
- iteration: iteration + 1,
20988
- jobId: job.id,
20989
- templateId: renderedPrompt.templateId,
20990
- templateVersion: renderedPrompt.templateVersion,
20991
- title: continuation.shouldContinue ? "Harness requested another loop" : "Harness accepted completion",
20992
- message: `${continuation.reason}; ${continuation.outcome}`,
20993
- data: {
20994
- continuation,
20995
- checkpoint: latestCheckpoint,
20996
- verifierSnapshot
20997
- }
20998
- });
20999
- previousSnapshot = verifierSnapshot;
21000
- noProgressCount = continuation.nextNoProgressCount;
21001
- conversation.history.push({
21002
- role: "assistant",
21003
- content: responseText,
21004
- code: generation.code,
21005
- jobStatus: "succeeded",
21006
- jobResultPreview: JSON.stringify(outcome.result, null, 2)
21007
- });
21008
- await writeJson(
21009
- path__default.default.join(turnDir, `iteration-${iteration + 1}-result.json`),
21010
- {
21011
- responseText,
21012
- continuation,
21013
- actionSummary: latestCheckpoint.latestActionSummary,
21014
- result: outcome.result
21015
- }
21715
+ throw new Error(
21716
+ `Turn exceeded iteration budget (${maxIterations}) for ${conversation.label}`
21016
21717
  );
21017
- iterationLog.responseText = responseText;
21018
- iterationLog.terminalKind = getCurrentClosureId(settledLiveDoc) ? "closure" : "reply";
21019
- iterationLog.actionSummary = latestCheckpoint.latestActionSummary || [];
21020
- iterationLog.promptInteractions = [];
21021
- iterationLog.continuation = continuation;
21022
- iterationLog.result = outcome.result;
21023
- if (!continuation.shouldContinue) {
21024
- const completed = {
21025
- conversation,
21026
- request: input.request,
21027
- turnDir,
21028
- responseText,
21029
- terminalKind: getCurrentClosureId(settledLiveDoc) ? "closure" : "reply",
21030
- finalCode: generation.code,
21031
- actionSummary: latestCheckpoint.latestActionSummary || [],
21032
- promptInteractions: [],
21033
- verification: null,
21034
- result: outcome.result
21035
- };
21036
- if (input.verification) {
21037
- completed.verification = await runInspection(
21038
- conversation,
21039
- input.verification,
21040
- completed,
21041
- turnDir
21042
- );
21043
- }
21044
- turnLog.completed = {
21045
- responseText,
21046
- terminalKind: completed.terminalKind,
21047
- actionSummary: completed.actionSummary,
21048
- promptInteractions: [],
21049
- result: outcome.result
21050
- };
21051
- await writeJson(path__default.default.join(turnDir, "result.json"), completed);
21052
- await emitProgress(onProgress, {
21053
- phase: "step",
21054
- status: "passed",
21055
- scenarioId: conversation.label,
21056
- stepId: turnId,
21057
- iteration: iteration + 1,
21058
- jobId: job.id,
21059
- title: "Step completed",
21060
- message: responseText,
21061
- data: completed
21062
- });
21063
- return completed;
21718
+ } catch (error) {
21719
+ const message = error instanceof Error ? error.message : String(error);
21720
+ const latestIteration = latestIterationLog(turnLog);
21721
+ if (latestIteration) {
21722
+ latestIteration.error = message;
21064
21723
  }
21065
- iteration += 1;
21724
+ turnLog.error = message;
21725
+ await writeTurnReport({ type: "failed", error: message });
21726
+ throw error;
21066
21727
  }
21067
- throw new Error(
21068
- `Turn exceeded iteration budget (${maxIterations}) for ${conversation.label}`
21069
- );
21070
21728
  }
21071
21729
  return {
21072
21730
  artifactDir,
@@ -21101,6 +21759,7 @@ function createAgentTester(options) {
21101
21759
  const harness = createAgentEvalHarness({
21102
21760
  granular,
21103
21761
  environmentId: resolvedEnvironmentId || void 0,
21762
+ local: options.local,
21104
21763
  openEnvironment: async ({ clientId }) => {
21105
21764
  if (resolvedEnvironmentId) {
21106
21765
  return granular.createSession({