@granular-software/sdk 0.4.21 → 0.4.23

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.
@@ -24,7 +24,8 @@ function uniqueStrings(values, maxCount) {
24
24
  }
25
25
  function formatScalar(value) {
26
26
  if (typeof value === "string") return JSON.stringify(value);
27
- if (typeof value === "number" || typeof value === "boolean") return String(value);
27
+ if (typeof value === "number" || typeof value === "boolean")
28
+ return String(value);
28
29
  if (value === null) return "null";
29
30
  return "unknown";
30
31
  }
@@ -32,7 +33,9 @@ function describeHeapEntry(entry, previewFieldLimit = 3) {
32
33
  const headline = entry.label || entry.id || entry.path || "Unknown";
33
34
  const pathLabel = entry.path && entry.path !== headline ? ` <${entry.path}>` : "";
34
35
  const classLabel = entry.className || "unknown";
35
- const preview = asArray(entry.fields).filter((field) => field?.name && field.name !== "_realId" && field.name !== "real_id").slice(0, previewFieldLimit).map((field) => `${field.name}=${formatScalar(field.value)}`).join(", ");
36
+ const preview = asArray(entry.fields).filter(
37
+ (field) => field?.name && field.name !== "_realId" && field.name !== "real_id"
38
+ ).slice(0, previewFieldLimit).map((field) => `${field.name}=${formatScalar(field.value)}`).join(", ");
36
39
  return preview ? `${headline}${pathLabel} [${classLabel}] ${preview}` : `${headline}${pathLabel} [${classLabel}]`;
37
40
  }
38
41
  function hashString(value) {
@@ -48,7 +51,9 @@ function hasSubstantiveAwaitAfterPrompt(code, marker) {
48
51
  const startIndex = code.indexOf(marker);
49
52
  if (startIndex === -1) return true;
50
53
  const segment = code.slice(startIndex + marker.length);
51
- const callMatches = segment.matchAll(/await\s+([A-Za-z0-9_$.]+)\.([A-Za-z0-9_]+)\s*\(/g);
54
+ const callMatches = segment.matchAll(
55
+ /await\s+([A-Za-z0-9_$.]+)\.([A-Za-z0-9_]+)\s*\(/g
56
+ );
52
57
  for (const match of callMatches) {
53
58
  const receiver = match[1] || "";
54
59
  const method = match[2] || "";
@@ -80,9 +85,16 @@ function reviewGeneratedJobCode(code) {
80
85
  /approved\./i
81
86
  ];
82
87
  if (normalized.includes("await loop.confirm(")) {
83
- const postConfirm = normalized.slice(normalized.indexOf("await loop.confirm("));
84
- const hasPlaceholder = placeholderPatterns.some((pattern) => pattern.test(postConfirm));
85
- const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(normalized, "await loop.confirm(");
88
+ const postConfirm = normalized.slice(
89
+ normalized.indexOf("await loop.confirm(")
90
+ );
91
+ const hasPlaceholder = placeholderPatterns.some(
92
+ (pattern) => pattern.test(postConfirm)
93
+ );
94
+ const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(
95
+ normalized,
96
+ "await loop.confirm("
97
+ );
86
98
  if (!hasSubstantiveAwait || hasPlaceholder) {
87
99
  issues.push({
88
100
  code: "placeholder_after_confirm",
@@ -92,9 +104,16 @@ function reviewGeneratedJobCode(code) {
92
104
  }
93
105
  }
94
106
  if (normalized.includes("await loop.ask_user(")) {
95
- const postPrompt = normalized.slice(normalized.indexOf("await loop.ask_user("));
96
- const hasPlaceholder = placeholderPatterns.some((pattern) => pattern.test(postPrompt));
97
- const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(normalized, "await loop.ask_user(");
107
+ const postPrompt = normalized.slice(
108
+ normalized.indexOf("await loop.ask_user(")
109
+ );
110
+ const hasPlaceholder = placeholderPatterns.some(
111
+ (pattern) => pattern.test(postPrompt)
112
+ );
113
+ const hasSubstantiveAwait = hasSubstantiveAwaitAfterPrompt(
114
+ normalized,
115
+ "await loop.ask_user("
116
+ );
98
117
  if (hasPlaceholder && !hasSubstantiveAwait) {
99
118
  issues.push({
100
119
  code: "placeholder_after_ask_user",
@@ -175,7 +194,9 @@ function getLatestClosure(liveDoc) {
175
194
  if (!record) continue;
176
195
  closures.push({ ...record, closureId });
177
196
  }
178
- closures.sort((left, right) => (Number(right.createdAt) || 0) - (Number(left.createdAt) || 0));
197
+ closures.sort(
198
+ (left, right) => (Number(right.createdAt) || 0) - (Number(left.createdAt) || 0)
199
+ );
179
200
  return closures[0] || null;
180
201
  }
181
202
  function getWorkflowBoundary(liveDoc, options) {
@@ -213,7 +234,9 @@ function getJobRecords(liveDoc) {
213
234
  if (!record) continue;
214
235
  jobs.push({ ...record, jobId });
215
236
  }
216
- jobs.sort((left, right) => getJobTimestamp(right) - getJobTimestamp(left));
237
+ jobs.sort(
238
+ (left, right) => getJobTimestamp(right) - getJobTimestamp(left)
239
+ );
217
240
  return jobs;
218
241
  }
219
242
  function getPromptRecordsFromJobs(liveDoc) {
@@ -221,7 +244,9 @@ function getPromptRecordsFromJobs(liveDoc) {
221
244
  }
222
245
  function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
223
246
  const boundary = getWorkflowBoundary(liveDoc, options);
224
- const jobs = getJobRecords(liveDoc).filter((job) => getJobTimestamp(job) >= boundary.timestamp).slice(0, 6).reverse();
247
+ const jobs = getJobRecords(liveDoc).filter(
248
+ (job) => getJobTimestamp(job) >= boundary.timestamp
249
+ ).slice(0, 6).reverse();
225
250
  const actionSummaryLines = [];
226
251
  const variableNames = [];
227
252
  const listNames = [];
@@ -273,7 +298,9 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
273
298
  return updatedAt >= boundary.timestamp;
274
299
  }
275
300
  return status !== "completed" && status !== "canceled" ? true : updatedAt >= boundary.timestamp;
276
- }).sort((left, right) => (Number(right.updatedAt) || Number(right.createdAt) || 0) - (Number(left.updatedAt) || Number(left.createdAt) || 0));
301
+ }).sort(
302
+ (left, right) => (Number(right.updatedAt) || Number(right.createdAt) || 0) - (Number(left.updatedAt) || Number(left.createdAt) || 0)
303
+ );
277
304
  for (const task of tasks.slice(0, 4)) {
278
305
  if (typeof task.taskId === "string") {
279
306
  activeTaskIds.push(task.taskId);
@@ -285,7 +312,9 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
285
312
  return updatedAt >= boundary.timestamp;
286
313
  }
287
314
  return decision.status === "open" || updatedAt >= boundary.timestamp;
288
- }).sort((left, right) => (Number(right.updatedAt) || Number(right.createdAt) || 0) - (Number(left.updatedAt) || Number(left.createdAt) || 0));
315
+ }).sort(
316
+ (left, right) => (Number(right.updatedAt) || Number(right.createdAt) || 0) - (Number(left.updatedAt) || Number(left.createdAt) || 0)
317
+ );
289
318
  for (const decision of decisions.slice(0, 3)) {
290
319
  if (typeof decision.decisionId === "string" && decision.status === "open") {
291
320
  openDecisionIds.push(decision.decisionId);
@@ -364,11 +393,15 @@ function projectWorkflowSummary(liveDoc, pendingPrompts = [], options) {
364
393
  const lines = [];
365
394
  lines.push("Workflow Boundary:");
366
395
  if (focus.boundaryReason === "request_start") {
367
- lines.push("- Start from work recorded after the current user request began.");
396
+ lines.push(
397
+ "- Start from work recorded after the current user request began."
398
+ );
368
399
  } else if (focus.boundaryReason === "last_closed_loop" && focus.latestClosureId) {
369
400
  lines.push(`- Start from work recorded after ${focus.latestClosureId}.`);
370
401
  } else {
371
- lines.push("- No prior closed loop recorded; use the latest user request as the boundary.");
402
+ lines.push(
403
+ "- No prior closed loop recorded; use the latest user request as the boundary."
404
+ );
372
405
  }
373
406
  lines.push("", "Recent Actions:");
374
407
  if (focus.recentActionSummary.length === 0) {
@@ -437,12 +470,17 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
437
470
  return updatedAt >= boundary.timestamp;
438
471
  }
439
472
  return status !== "completed" && status !== "canceled" ? true : updatedAt >= boundary.timestamp;
440
- }).sort((left, right) => (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0));
473
+ }).sort(
474
+ (left, right) => (Number(right.updatedAt) || 0) - (Number(left.updatedAt) || 0)
475
+ );
441
476
  const activeTasks = tasks.filter((task) => {
442
477
  const status = typeof task.status === "string" ? task.status : "pending";
443
478
  return status !== "completed" && status !== "canceled";
444
479
  });
445
- const visibleTasks = (activeTasks.length > 0 ? activeTasks : tasks).slice(0, 5);
480
+ const visibleTasks = (activeTasks.length > 0 ? activeTasks : tasks).slice(
481
+ 0,
482
+ 5
483
+ );
446
484
  const hiddenTaskCount = Math.max(0, activeTasks.length - visibleTasks.length);
447
485
  lines.push("Tasks:");
448
486
  if (visibleTasks.length === 0) {
@@ -466,8 +504,12 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
466
504
  return updatedAt >= boundary.timestamp;
467
505
  }
468
506
  return decision.status === "open" || updatedAt >= boundary.timestamp;
469
- }).sort((left, right) => (Number(right.updatedAt) || Number(right.createdAt) || 0) - (Number(left.updatedAt) || Number(left.createdAt) || 0));
470
- const openDecisions = decisions.filter((decision) => decision.status === "open");
507
+ }).sort(
508
+ (left, right) => (Number(right.updatedAt) || Number(right.createdAt) || 0) - (Number(left.updatedAt) || Number(left.createdAt) || 0)
509
+ );
510
+ const openDecisions = decisions.filter(
511
+ (decision) => decision.status === "open"
512
+ );
471
513
  const visibleDecisions = (openDecisions.length > 0 ? openDecisions : decisions.slice(0, 1)).slice(0, 3);
472
514
  lines.push("", "Recent Decisions:");
473
515
  if (visibleDecisions.length === 0) {
@@ -486,7 +528,9 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
486
528
  const candidateLabel = typeof record.label === "string" && record.label.trim() ? record.label.trim() : candidateId;
487
529
  return candidateLabel === candidateId ? candidateId : `${candidateLabel} (${candidateId})`;
488
530
  }).filter((value) => Boolean(value)).join(", ");
489
- lines.push(`- [open] ${title} (${decisionId})${candidatePreview ? ` \u2014 candidates: ${candidatePreview}` : ""}`);
531
+ lines.push(
532
+ `- [open] ${title} (${decisionId})${candidatePreview ? ` \u2014 candidates: ${candidatePreview}` : ""}`
533
+ );
490
534
  } else {
491
535
  const selected = asRecord(decision.selected);
492
536
  const label = typeof selected?.label === "string" ? selected.label : typeof selected?.id === "string" ? selected.id : "unknown";
@@ -501,13 +545,17 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
501
545
  title: prompt.title,
502
546
  message: prompt.message
503
547
  })),
504
- ...Object.values(asRecord(asRecord(liveDoc?.jobs)?.byId) || {}).flatMap((job) => Object.values(asRecord(asRecord(job)?.prompts) || {})).map((prompt) => asRecord(prompt)).filter((prompt) => Boolean(prompt && prompt.status === "open"))
548
+ ...Object.values(asRecord(asRecord(liveDoc?.jobs)?.byId) || {}).flatMap((job) => Object.values(asRecord(asRecord(job)?.prompts) || {})).map((prompt) => asRecord(prompt)).filter(
549
+ (prompt) => Boolean(prompt && prompt.status === "open")
550
+ )
505
551
  ];
506
552
  const visiblePrompts = boundary.reason === "request_start" ? openPrompts.filter((prompt) => {
507
553
  const promptRecord = asRecord(prompt);
508
554
  const openedAt = Number(promptRecord?.openedAt) || 0;
509
555
  const promptId = typeof promptRecord?.id === "string" ? promptRecord.id : typeof prompt.id === "string" ? prompt.id : null;
510
- return openedAt >= boundary.timestamp || (promptId ? pendingPrompts.some((pendingPrompt) => pendingPrompt.id === promptId) : false);
556
+ return openedAt >= boundary.timestamp || (promptId ? pendingPrompts.some(
557
+ (pendingPrompt) => pendingPrompt.id === promptId
558
+ ) : false);
511
559
  }) : openPrompts;
512
560
  lines.push("", "Open Prompts:");
513
561
  if (visiblePrompts.length === 0) {
@@ -538,9 +586,15 @@ function projectHeapSummary(heap, options) {
538
586
  const entriesByPath = asRecord(heapRecord.entriesByPath) || {};
539
587
  const listsByName = asRecord(heapRecord.listsByName) || {};
540
588
  const variablesByName = asRecord(heapRecord.variablesByName) || {};
541
- const focusedVariableNames = new Set(uniqueStrings(options?.focus?.variableNames || []));
542
- const focusedListNames = new Set(uniqueStrings(options?.focus?.listNames || []));
543
- const focusedEntryPaths = new Set(uniqueStrings(options?.focus?.entryPaths || []));
589
+ const focusedVariableNames = new Set(
590
+ uniqueStrings(options?.focus?.variableNames || [])
591
+ );
592
+ const focusedListNames = new Set(
593
+ uniqueStrings(options?.focus?.listNames || [])
594
+ );
595
+ const focusedEntryPaths = new Set(
596
+ uniqueStrings(options?.focus?.entryPaths || [])
597
+ );
544
598
  const hasFocus = focusedVariableNames.size > 0 || focusedListNames.size > 0 || focusedEntryPaths.size > 0;
545
599
  const suppressRecentFallback = Boolean(options?.focus) && !hasFocus;
546
600
  const maxVariables = options?.maxVariables ?? (hasFocus ? 4 : 6);
@@ -552,20 +606,26 @@ function projectHeapSummary(heap, options) {
552
606
  return rightFocused - leftFocused || (right.updatedAt || 0) - (left.updatedAt || 0);
553
607
  }).filter((variable, index) => {
554
608
  if (index < maxVariables) return true;
555
- return Boolean(variable.name && focusedVariableNames.has(variable.name));
609
+ return Boolean(
610
+ variable.name && focusedVariableNames.has(variable.name)
611
+ );
556
612
  }).slice(0, maxVariables);
557
613
  const referencedPaths = /* @__PURE__ */ new Set();
558
614
  for (const variable of variables) {
559
615
  if (variable.entryPath) referencedPaths.add(variable.entryPath);
560
616
  if (variable.listName) {
561
- const list = asRecord(listsByName[variable.listName]);
617
+ const list = asRecord(
618
+ listsByName[variable.listName]
619
+ );
562
620
  for (const path of list?.paths || []) referencedPaths.add(path);
563
621
  }
564
622
  }
565
623
  for (const path of focusedEntryPaths) {
566
624
  referencedPaths.add(path);
567
625
  }
568
- const visibleLists = Object.values(listsByName).map((value) => asRecord(value)).filter((value) => Boolean(value)).filter((list) => variables.some((variable) => variable.listName === list.name) || Boolean(list.name && focusedListNames.has(list.name))).sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0)).slice(0, maxLists);
626
+ const visibleLists = Object.values(listsByName).map((value) => asRecord(value)).filter((value) => Boolean(value)).filter(
627
+ (list) => variables.some((variable) => variable.listName === list.name) || Boolean(list.name && focusedListNames.has(list.name))
628
+ ).sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0)).slice(0, maxLists);
569
629
  const visibleEntries = Object.values(entriesByPath).map((value) => asRecord(value)).filter((value) => Boolean(value)).filter((entry) => entry.path && referencedPaths.has(entry.path)).sort((left, right) => (right.updatedAt || 0) - (left.updatedAt || 0)).slice(0, maxEntries);
570
630
  const lines = [];
571
631
  lines.push("Variables:");
@@ -574,16 +634,24 @@ function projectHeapSummary(heap, options) {
574
634
  } else {
575
635
  for (const variable of variables) {
576
636
  if (variable.kind === "scalar") {
577
- lines.push(`- ${variable.name}: scalar = ${formatScalar(variable.value)}`);
637
+ lines.push(
638
+ `- ${variable.name}: scalar = ${formatScalar(variable.value)}`
639
+ );
578
640
  continue;
579
641
  }
580
642
  if (variable.kind === "entry") {
581
- const entry = variable.entryPath ? asRecord(entriesByPath[variable.entryPath]) : null;
582
- lines.push(`- ${variable.name}: entry<${variable.className || entry?.className || "unknown"}> -> ${entry ? describeHeapEntry(entry) : variable.entryPath || "missing"}`);
643
+ const entry = variable.entryPath ? asRecord(
644
+ entriesByPath[variable.entryPath]
645
+ ) : null;
646
+ lines.push(
647
+ `- ${variable.name}: entry<${variable.className || entry?.className || "unknown"}> -> ${entry ? describeHeapEntry(entry) : variable.entryPath || "missing"}`
648
+ );
583
649
  continue;
584
650
  }
585
651
  const list = variable.listName ? asRecord(listsByName[variable.listName]) : null;
586
- lines.push(`- ${variable.name}: list<${variable.className || list?.className || "unknown"}> -> ${(list?.paths || []).length} item(s)`);
652
+ lines.push(
653
+ `- ${variable.name}: list<${variable.className || list?.className || "unknown"}> -> ${(list?.paths || []).length} item(s)`
654
+ );
587
655
  }
588
656
  }
589
657
  lines.push("", "Named Lists:");
@@ -591,7 +659,9 @@ function projectHeapSummary(heap, options) {
591
659
  lines.push("- none");
592
660
  } else {
593
661
  for (const list of visibleLists) {
594
- lines.push(`- ${list.name}: ${list.className || "unknown"}[${(list.paths || []).length}]`);
662
+ lines.push(
663
+ `- ${list.name}: ${list.className || "unknown"}[${(list.paths || []).length}]`
664
+ );
595
665
  }
596
666
  }
597
667
  lines.push("", "Active Entries:");
@@ -605,11 +675,19 @@ function projectHeapSummary(heap, options) {
605
675
  return lines.join("\n");
606
676
  }
607
677
  function createHarnessVerifierSnapshot(input) {
608
- const workflowFocus = projectWorkflowFocus(input.liveDoc, [], input.projectionOptions);
609
- const heapDigest = hashString(projectHeapSummary(asRecord(input.liveDoc?.heap), {
610
- focus: workflowFocus
611
- })) || "00000000";
612
- const loopDigest = hashString(projectWorkflowSummary(input.liveDoc, [], input.projectionOptions)) || "00000000";
678
+ const workflowFocus = projectWorkflowFocus(
679
+ input.liveDoc,
680
+ [],
681
+ input.projectionOptions
682
+ );
683
+ const heapDigest = hashString(
684
+ projectHeapSummary(asRecord(input.liveDoc?.heap), {
685
+ focus: workflowFocus
686
+ })
687
+ ) || "00000000";
688
+ const loopDigest = hashString(
689
+ projectWorkflowSummary(input.liveDoc, [], input.projectionOptions)
690
+ ) || "00000000";
613
691
  return {
614
692
  codeDigest: hashString(input.finalCode?.trim()),
615
693
  resultDigest: hashString(input.resultPreview?.trim()),
@@ -739,8 +817,12 @@ function buildGranularAgentToolBlock(tools) {
739
817
  return "No live effects are available in this session yet.";
740
818
  }
741
819
  const globalTools = normalizedTools.filter((tool) => !tool.className);
742
- const staticTools = normalizedTools.filter((tool) => Boolean(tool.className && tool.static));
743
- const instanceTools = normalizedTools.filter((tool) => Boolean(tool.className && !tool.static));
820
+ const staticTools = normalizedTools.filter(
821
+ (tool) => Boolean(tool.className && tool.static)
822
+ );
823
+ const instanceTools = normalizedTools.filter(
824
+ (tool) => Boolean(tool.className && !tool.static)
825
+ );
744
826
  const lines = [
745
827
  "Treat this block as the planning map. Use DOMAIN TYPES below for exact signatures."
746
828
  ];
@@ -955,7 +1037,10 @@ ${loopBlock}
955
1037
  - Avoid \`as any\` and other broad casts when the DOMAIN TYPES block already tells you the correct class or list type.
956
1038
  - Prefer manipulating heap-backed instances and typed lists instead of returning raw JSON blobs or object IDs unless the user explicitly asks for them.
957
1039
  - If the user expects an answer after the job runs, the final \`return\` value must be either a short natural-language string or an object with a top-level \`reply\` string.
1040
+ - You may call \`agent_message(...)\` multiple times in one job to post several assistant messages while the job is still running.
958
1041
  - Prefer \`agent_message({ reply, show })\` when you want to leave a user-facing answer and optionally show heap-backed records in the UI.
1042
+ - \`agent_message(...)\` also accepts \`content\`, \`message\`, or \`text\` instead of \`reply\`.
1043
+ - \`agent_message({ show })\` may receive explicit refs or sandbox instances and arrays of sandbox instances. The runtime will convert those into UI references.
959
1044
  - When it helps the UI show specific heap-backed results, you may instead return:
960
1045
  \`{ reply: string, show: { entryPaths?: string[], listNames?: string[], variableNames?: string[] } }\`
961
1046
  - If you create or load objects the user should see, save them in the heap and return references to them through \`show\` instead of serializing full objects.