omkit 0.8.13 → 0.8.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -185,7 +185,7 @@ flakyBackgroundJob().result.catch((e) => console.warn("job failed, carrying on",
185
185
 
186
186
  Every action's output, events, asserts, snapshots, and lifecycle land on **one timeline** — streamed live to the terminal (curated milestones) and written to a per-run folder:
187
187
 
188
- - `result.json` — the run's tree, per-node status, verdict, and curated artifacts. Each node also carries what its definition declared about itself: the `.describe()` summary, the `.mcp()` exposure, and the `.args()` schema as JSON Schema. Each node also records `calledAt`, the line that launched it, so a built-in step (a `prompt`, a `command`) points back at your code rather than omkit's, and a `step(...)` is defined at the line where you wrote it.
188
+ - `result.json` — the run's tree, per-node status, verdict, and curated artifacts. Each node also carries what its definition declared about itself: the `.describe()` summary, the `.mcp()` exposure, and the `.args()` schema as JSON Schema. Each node also records what it returned (`value`, or a `valueNote` when it was too large or not JSON) and `calledAt`, the line that launched it, so a built-in step (a `prompt`, a `command`) points back at your code rather than omkit's, and a `step(...)` is defined at the line where you wrote it.
189
189
  - `raw.jsonl` — every entry, machine-readable.
190
190
  - `main.log` + one `.log` per action — the human-readable timelines.
191
191
  - `events.log` / `asserts.log` / `snapshots.log` / `artifacts.log` — cross-cutting rollups.
@@ -391,6 +391,7 @@ export class ExecutionTree {
391
391
  duration: node.duration,
392
392
  logFile: this.logFile(node.path),
393
393
  ...this.metaView(node),
394
+ ...valueView(node.value),
394
395
  children: node.children.map((child) => this.nodeView(child)),
395
396
  };
396
397
  }
@@ -506,6 +507,36 @@ const messageOf = (error) => error instanceof Error ? (error.stack ?? error.mess
506
507
  * body may hand back anything — a capability handle, a cyclic object — and the run must
507
508
  * still write its record; a value that does not survive JSON is dropped, not fatal.
508
509
  */
510
+ /** A node's return value is recorded up to this many characters of JSON. */
511
+ const VALUE_CAP = 512 * 1024;
512
+ /**
513
+ * What a node returned, for the record: the value itself when JSON can carry it and it fits,
514
+ * otherwise a one-line note saying what it was — so "returned a Page" is still on the record.
515
+ */
516
+ function valueView(value) {
517
+ if (value === undefined)
518
+ return {};
519
+ const kind = value === null
520
+ ? "null"
521
+ : typeof value === "object"
522
+ ? (value.constructor?.name ?? "object")
523
+ : typeof value;
524
+ let text;
525
+ try {
526
+ text = JSON.stringify(value);
527
+ }
528
+ catch {
529
+ text = undefined;
530
+ }
531
+ if (text === undefined)
532
+ return { valueNote: `${kind} (not JSON)` };
533
+ if (text.length > VALUE_CAP) {
534
+ return {
535
+ valueNote: `${kind}, ${Math.round(text.length / 1024)} KB of JSON — too large to record`,
536
+ };
537
+ }
538
+ return { value: JSON.parse(text) };
539
+ }
509
540
  function serialisable(value) {
510
541
  if (value === undefined)
511
542
  return undefined;
@@ -337,6 +337,7 @@ a.icon-btn { text-decoration: none; }
337
337
  .tk-k { color: var(--tk-k); }
338
338
  .tk-f { color: var(--tk-f); }
339
339
  .tk-t { color: var(--tk-t); }
340
+ .tk-p { color: var(--tk-f); }
340
341
 
341
342
  /* ── popover tabs, prompt panel, flow snapshot strip ───── */
342
343
  .code-pop .cp-tabs { display: flex; gap: 2px; padding: 0 8px; border-bottom: 1px solid var(--line); background: var(--panel-2); }
@@ -355,6 +356,18 @@ a.icon-btn { text-decoration: none; }
355
356
  .flow-shots .shot:hover rect { stroke: var(--accent); }
356
357
  .flow-shots .shot text { fill: var(--text-3); font: 600 9px var(--mono); pointer-events: none; }
357
358
 
359
+ /* ── returned values ───────────────────────────────────── */
360
+ .code-btn path { fill: none; stroke: var(--text-2); stroke-width: 2.6; stroke-linecap: round; stroke-linejoin: round; pointer-events: none; }
361
+ .code-btn:hover path { stroke: var(--accent); }
362
+ .cp-tools { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 6px 10px 6px 14px; border-bottom: 1px solid var(--line-soft); font-size: 12px; }
363
+ .btn.small { padding: 3px 9px; font-size: 12px; }
364
+ .btn.small svg { width: 13px; height: 13px; }
365
+ .code-box { border: 1px solid var(--line-soft); border-radius: 8px; background: var(--panel-2); overflow: auto; max-height: 60vh; }
366
+ .code-box .cp-code, .code-pop .cp-code { font: 11.5px/1.65 var(--mono); padding: 8px 0; min-width: max-content; }
367
+ .code-box .cl { display: grid; grid-template-columns: 40px auto; white-space: pre; }
368
+ .code-box .ln { color: var(--text-3); text-align: right; padding-right: 12px; user-select: none; }
369
+ .code-box .lc { padding-right: 14px; }
370
+
358
371
  @media (max-width: 1100px) {
359
372
  .bottom { grid-template-columns: minmax(0, 1fr); }
360
373
  .legend, .overview { display: none; }
@@ -43,6 +43,7 @@
43
43
  info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v5M12 8h.01",
44
44
  locate: "M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM12 2v3M12 19v3M2 12h3M19 12h3M12 10.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z",
45
45
  code: "M8 7l-5 5 5 5M16 7l5 5-5 5",
46
+ ret: "M9 10l-4 4 4 4M5 14h11a4 4 0 0 0 4-4V5",
46
47
  external: "M14 4h6v6M20 4l-9 9M18 14v6H4V6h6",
47
48
  };
48
49
  const svgIcon = (name, extra) =>
@@ -505,9 +506,12 @@
505
506
  r.append("rect").attr("class", "bar").attr("rx", 5).attr("y", BAR_Y).attr("height", BAR_H);
506
507
  r.append("text").attr("class", "bar-label").attr("y", BAR_Y + BAR_H / 2 + 4);
507
508
  r.append("g").attr("class", "marks");
508
- const cb = r.append("g").attr("class", "code-btn");
509
+ const cb = r.append("g").attr("class", "code-btn src");
509
510
  cb.append("circle").attr("r", 9);
510
511
  cb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
512
+ const rb = r.append("g").attr("class", "code-btn ret");
513
+ rb.append("circle").attr("r", 9);
514
+ rb.append("path").attr("d", ICON.ret).attr("transform", "translate(-6,-6) scale(0.5)");
511
515
  return r;
512
516
  })
513
517
  .attr("transform", (_n, i) => `translate(0,${i * ROW_H})`)
@@ -547,18 +551,33 @@
547
551
  // …and, for a block spanning the chart, inside it right after its label
548
552
  const cx =
549
553
  right + 8 <= W - 10 ? right + 8 : left - 8 >= 10 ? left - 8 : Math.min(W - 12, (flip ? x0 : lx + tw) + 14);
550
- d3.select(this.parentNode)
551
- .select("g.code-btn")
552
- .style("display", hasCode(n) ? null : "none")
554
+ // buttons stack away from the block: rightward when they sit after it, leftward before it
555
+ const dir = right + 8 <= W - 10 || left - 8 < 10 ? 1 : -1;
556
+ const src = hasCode(n);
557
+ const row = d3.select(this.parentNode);
558
+ row
559
+ .select("g.code-btn.src")
560
+ .style("display", src ? null : "none")
553
561
  .attr("transform", `translate(${cx},${BAR_Y + BAR_H / 2})`);
562
+ row
563
+ .select("g.code-btn.ret")
564
+ .style("display", hasValue(n) ? null : "none")
565
+ .attr("transform", `translate(${src ? cx + dir * 21 : cx},${BAR_Y + BAR_H / 2})`);
554
566
  });
555
- g.select("g.code-btn")
567
+ g.select("g.code-btn.src")
556
568
  .on("click", (ev, n) => {
557
569
  ev.stopPropagation();
558
570
  toggleCode(n, ev);
559
571
  })
560
572
  .on("mouseenter", (ev) => showTip(ev, "Show source"))
561
573
  .on("mouseleave", hideTip);
574
+ g.select("g.code-btn.ret")
575
+ .on("click", (ev, n) => {
576
+ ev.stopPropagation();
577
+ toggleCode(n, ev, "value", true);
578
+ })
579
+ .on("mouseenter", (ev, n) => showTip(ev, `Returned: <span class="mono">${esc(valuePreview(n, 80))}</span>`))
580
+ .on("mouseleave", hideTip);
562
581
  g.selectAll("rect.hit, text.bar-label").call(bindNode);
563
582
 
564
583
  g.select("g.marks").each(function (n) {
@@ -802,6 +821,21 @@
802
821
  });
803
822
  fcb.append("circle").attr("r", 9);
804
823
  fcb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
824
+ const frb = card
825
+ .filter((d) => hasValue(d.data))
826
+ .append("g")
827
+ .attr("class", "code-btn ret")
828
+ .attr("transform", (d) => `translate(${CW / 2 + (hasCode(d.data) ? 22 : 0)},0)`)
829
+ .on("click", (ev, d) => {
830
+ ev.stopPropagation();
831
+ toggleCode(d.data, ev, "value", true);
832
+ })
833
+ .on("mouseenter", (ev, d) => {
834
+ ev.stopPropagation();
835
+ showTip(ev, `Returned: <span class="mono">${esc(valuePreview(d.data, 80))}</span>`);
836
+ });
837
+ frb.append("circle").attr("r", 9);
838
+ frb.append("path").attr("d", ICON.ret).attr("transform", "translate(-6,-6) scale(0.5)");
805
839
  const TW = 46;
806
840
  const TH = 28;
807
841
  const strip = card
@@ -1004,6 +1038,7 @@
1004
1038
  const tabs = [
1005
1039
  ["overview", "Overview"],
1006
1040
  ["args", "Args"],
1041
+ ...(hasValue(n) ? [["value", "Return"]] : []),
1007
1042
  ["events", `Events`],
1008
1043
  ["logs", "Logs"],
1009
1044
  ["snapshots", "Snapshots"],
@@ -1026,6 +1061,7 @@
1026
1061
  ${userSite(n) ? `<div class="k">${siteLabel(n)}</div><div class="v defined">
1027
1062
  <button class="icon-btn" data-copy-source aria-label="Copy relative path">${svgIcon("copy")}</button>
1028
1063
  ${hasCode(n) ? `<button class="icon-btn" data-show-code aria-label="Show source">${svgIcon("code")}</button>` : ""}
1064
+ ${hasValue(n) ? `<button class="icon-btn" data-show-value aria-label="Show returned value">${svgIcon("ret")}</button>` : ""}
1029
1065
  <a class="icon-btn" href="${esc(editorUrl(userSite(n)))}" aria-label="Open in VS Code" data-tip-text="Open in VS Code">${svgIcon("external")}</a>
1030
1066
  <span class="defined-name" >${esc(basename(sourceOf(n)))}</span>
1031
1067
  </div>` : ""}
@@ -1044,6 +1080,13 @@
1044
1080
  copySrc.addEventListener("mousemove", (ev) => moveTip(ev));
1045
1081
  copySrc.addEventListener("mouseleave", hideTip);
1046
1082
  }
1083
+ const showValue = el.details.querySelector("[data-show-value]");
1084
+ if (showValue) {
1085
+ showValue.addEventListener("click", (ev) => toggleCode(n, ev, "value", true));
1086
+ showValue.addEventListener("mouseenter", (ev) => showTip(ev, `Returned: <span class="mono">${esc(valuePreview(n, 80))}</span>`));
1087
+ showValue.addEventListener("mouseleave", hideTip);
1088
+ }
1089
+ wireValue(el.details, n);
1047
1090
  const showCode = el.details.querySelector("[data-show-code]");
1048
1091
  if (showCode) {
1049
1092
  showCode.addEventListener("click", (ev) => toggleCode(n, ev));
@@ -1070,6 +1113,9 @@
1070
1113
 
1071
1114
  function detailTabHtml(n) {
1072
1115
  const tab = state.detailTab;
1116
+ if (tab === "value") {
1117
+ return hasValue(n) ? `<div class="code-box">${valueHtml(n)}</div>` : '<div class="muted">Returned nothing.</div>';
1118
+ }
1073
1119
  if (tab === "args") {
1074
1120
  const params = schemaFields(n.v.inputSchema);
1075
1121
  const schema = params.length ? `<div class="section-title">Schema</div><div class="params">${params.map(paramHtml).join("")}</div><div class="section-title">Values</div>` : "";
@@ -1343,6 +1389,7 @@
1343
1389
  ["When", `${fmtRel(n.start)} → ${fmtRel(n.end)}`],
1344
1390
  v.tags.length ? ["Tags", esc(v.tags.join(" · "))] : null,
1345
1391
  userSite(n) ? [siteLabel(n) === "Called at" ? "Called" : "Defined", `<span class="mono">${esc(sourceOf(n))}</span>`] : null,
1392
+ hasValue(n) ? ["Returns", `<span class="mono">${esc(valuePreview(n, 90))}</span>`] : null,
1346
1393
  n.children.length ? ["Children", String(n.children.length)] : null,
1347
1394
  n.marks.length ? ["Events", String(n.marks.length)] : null,
1348
1395
  ].filter(Boolean);
@@ -1391,6 +1438,7 @@
1391
1438
  codeEl.hidden = true;
1392
1439
  document.body.appendChild(codeEl);
1393
1440
  let codeFor = null;
1441
+ let codeTab = null;
1394
1442
  let codeAway = null;
1395
1443
  /** A site inside omkit or an installed package — library source, not the user's. */
1396
1444
  function isLib(site) {
@@ -1416,6 +1464,7 @@
1416
1464
  function codeTabs(n) {
1417
1465
  const tabs = [];
1418
1466
  if (promptOf(n)) tabs.push({ key: "prompt", label: "Prompt" });
1467
+ if (hasValue(n)) tabs.push({ key: "value", label: "Returned" });
1419
1468
  const mine = userSite(n);
1420
1469
  if (codeOf(mine)) tabs.push({ key: "user", label: siteLabel(n) === "Called at" ? "Call site" : "Source", site: mine });
1421
1470
  const def = n.v.definedAt;
@@ -1423,11 +1472,36 @@
1423
1472
  return tabs;
1424
1473
  }
1425
1474
  function hasCode(n) {
1426
- return codeTabs(n).length > 0;
1475
+ return codeTabs(n).some((t) => t.key !== "value");
1427
1476
  }
1428
1477
  function relOf(site) {
1429
1478
  return (model.data.sources && model.data.sources[site]) || String(site).replace(/:\d+(:\d+)?$/, "");
1430
1479
  }
1480
+ function hasValue(n) {
1481
+ return n.v.value !== undefined || !!n.v.valueNote;
1482
+ }
1483
+ function valuePreview(n, max) {
1484
+ if (n.v.value === undefined) return n.v.valueNote || "";
1485
+ return truncate(oneLine(n.v.value), max);
1486
+ }
1487
+ /** The returned value in the code view: pretty JSON, highlighted, numbered, copyable. */
1488
+ function valueHtml(n) {
1489
+ if (n.v.value === undefined) {
1490
+ return `<div class="pp"><div class="muted">${esc(n.v.valueNote || "Returned nothing.")}</div></div>`;
1491
+ }
1492
+ const text = json(n.v.value);
1493
+ const lines = highlight(text);
1494
+ const kind = Array.isArray(n.v.value) ? `array · ${n.v.value.length} items` : n.v.value === null ? "null" : typeof n.v.value === "object" ? `object · ${Object.keys(n.v.value).length} keys` : typeof n.v.value;
1495
+ return `<div class="cp-tools"><span class="muted">${esc(kind)} · ${lines.length} line${lines.length > 1 ? "s" : ""} · ${fmtSize(text.length)}</span>
1496
+ <button class="btn solo small" data-a="copy-json">${svgIcon("copy")}Copy JSON</button></div>
1497
+ <div class="cp-code">${lines
1498
+ .map((html, i) => `<div class="cl"><span class="ln">${i + 1}</span><span class="lc">${html || " "}</span></div>`)
1499
+ .join("")}</div>`;
1500
+ }
1501
+ function wireValue(root, n) {
1502
+ const b = root.querySelector('[data-a="copy-json"]');
1503
+ if (b) b.addEventListener("click", () => copy(json(n.v.value), "Copied the returned value"));
1504
+ }
1431
1505
  function codeHtml(site) {
1432
1506
  const c = codeOf(site);
1433
1507
  const line = Number((/:(\d+)(?::\d+)?$/.exec(site) || [])[1]);
@@ -1464,11 +1538,13 @@
1464
1538
  ${answer}
1465
1539
  </div>`;
1466
1540
  }
1467
- function toggleCode(n, ev, want) {
1468
- if (codeFor === n.id && !want) return closeCode();
1541
+ function toggleCode(n, ev, want, toggle) {
1542
+ // the <> closes whatever is open on its node; the return button closes only its own tab
1543
+ if (codeFor === n.id && (!want || (toggle && codeTab === want))) return closeCode();
1469
1544
  const tabs = codeTabs(n);
1470
1545
  if (!tabs.length) return;
1471
- const tab = tabs.find((t) => t.key === want) || tabs[0];
1546
+ const tab = tabs.find((t) => t.key === want) || tabs.find((t) => t.key !== "value") || tabs[0];
1547
+ codeTab = tab.key;
1472
1548
  hideTip();
1473
1549
  const reopen = codeFor !== n.id;
1474
1550
  codeFor = n.id;
@@ -1483,7 +1559,8 @@
1483
1559
  <button class="icon-btn" data-a="close" aria-label="Close">${svgIcon("close")}</button>
1484
1560
  </div>
1485
1561
  ${tabs.length > 1 ? `<div class="cp-tabs">${tabs.map((t) => `<button class="tab ${t === tab ? "active" : ""}" data-tab="${t.key}">${esc(t.label)}</button>`).join("")}</div>` : ""}
1486
- <div class="cp-body">${tab.key === "prompt" ? promptHtml(promptOf(n)) : codeHtml(site)}</div>`;
1562
+ <div class="cp-body">${tab.key === "prompt" ? promptHtml(promptOf(n)) : tab.key === "value" ? valueHtml(n) : codeHtml(site)}</div>`;
1563
+ wireValue(codeEl, n);
1487
1564
  const cp = codeEl.querySelector('[data-a="copy"]');
1488
1565
  if (cp) cp.addEventListener("click", () => copy(rel, `Copied ${rel}`));
1489
1566
  codeEl.querySelector('[data-a="close"]').addEventListener("click", closeCode);
@@ -1503,7 +1580,7 @@
1503
1580
  if (at) at.scrollIntoView({ block: "center" });
1504
1581
  if (codeAway) document.removeEventListener("mousedown", codeAway, true);
1505
1582
  codeAway = (e) => {
1506
- if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]")) closeCode();
1583
+ if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code], [data-show-value]")) closeCode();
1507
1584
  };
1508
1585
  setTimeout(() => codeAway && document.addEventListener("mousedown", codeAway, true), 0);
1509
1586
  }
@@ -1710,7 +1787,7 @@
1710
1787
  if (m.index > last) parts.push([null, text.slice(last, m.index)]);
1711
1788
  let cls = null;
1712
1789
  if (m[1]) cls = "tk-c";
1713
- else if (m[2]) cls = "tk-s";
1790
+ else if (m[2]) cls = /^\s*:/.test(text.slice(TOKEN.lastIndex, TOKEN.lastIndex + 8)) ? "tk-p" : "tk-s";
1714
1791
  else if (m[3]) cls = "tk-n";
1715
1792
  else if (m[4]) {
1716
1793
  if (KEYWORDS.has(m[4])) cls = "tk-k";
@@ -31,6 +31,10 @@ export interface NodeView {
31
31
  };
32
32
  /** The definition's `.args()` schema as JSON Schema; absent without one. */
33
33
  readonly inputSchema?: unknown;
34
+ /** What the node returned, when JSON can carry it; absent for `undefined` and for failures. */
35
+ readonly value?: unknown;
36
+ /** Why a returned value is not in {@link value}: what it was, and that it was too big or not JSON. */
37
+ readonly valueNote?: string;
34
38
  readonly children: readonly NodeView[];
35
39
  }
36
40
  /** A curated artifact, as projected into `result.json`. Data-only — no core import. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "omkit",
3
- "version": "0.8.13",
3
+ "version": "0.8.14",
4
4
  "description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",