@plurnk/plurnk-digest 2.0.0 → 3.1.0

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.
@@ -1,7 +1,7 @@
1
1
  // Digest renderers ({§digest-programmatic-surface}): the markdown, JSON, reasoning, and packet
2
2
  // artifacts of one DigestModel, reading heavy evidence on demand through DigestEvidence.
3
3
  import { mkdirSync, writeFileSync } from "node:fs";
4
- import { dirname, join } from "node:path";
4
+ import { dirname, join, posix } from "node:path";
5
5
  import { aggregateProviderAccounting, } from "@plurnk/plurnk-providers";
6
6
  import { Validator } from "@plurnk/plurnk-contracts";
7
7
  import { EDIT_FORMS } from "./digest-rows.js";
@@ -11,6 +11,26 @@ function* projectRows(rows, project) {
11
11
  yield project(row);
12
12
  }
13
13
  export default class DigestRender {
14
+ static #literal(body, language = "text") {
15
+ let length = 3;
16
+ for (const match of body.matchAll(/`+/gu))
17
+ length = Math.max(length, match[0].length + 1);
18
+ const fence = "`".repeat(length);
19
+ return `${fence}${language}\n${body}\n${fence}`;
20
+ }
21
+ static #link(from, target, label) {
22
+ return `[${label}](${posix.relative(posix.dirname(from), target)})`;
23
+ }
24
+ static #turnLinks(m, turn, from) {
25
+ const stem = DigestRender.packetStems(m).get(turn.id);
26
+ if (stem === undefined)
27
+ return [];
28
+ return [
29
+ ...(turn.has_packet === 1 ? [DigestRender.#link(from, `${stem}.request.md`, "Input")] : []),
30
+ ...(turn.program !== null ? [DigestRender.#link(from, `${stem}.assistant.md`, "Output")] : []),
31
+ ...(turn.has_reasoning === 1 ? [DigestRender.#link(from, `${stem}.reasoning.md`, "Reasoning")] : []),
32
+ ];
33
+ }
14
34
  static #summarize(text, n = 80) {
15
35
  if (text === null || text === undefined)
16
36
  return "";
@@ -217,18 +237,13 @@ export default class DigestRender {
217
237
  const { packet } = m.evidence.packet(turn);
218
238
  if (packet === null)
219
239
  return null;
220
- return packet.messages(DigestRender.#emissionsOf(turn, m))
240
+ const messages = packet.messages();
241
+ if (messages.length === 0)
242
+ return null;
243
+ return messages
221
244
  .map(({ role, content }) => `${role}\n${content}`)
222
245
  .join("\n");
223
246
  }
224
- // {§emission-row} — the emissions the turn's worker announced; a stored request places those its log
225
- // section carried.
226
- static #emissionsOf(turn, m) {
227
- const loop = m.loopsById.get(turn.loop_id);
228
- if (loop === undefined)
229
- throw new Error(`digest: turn ${turn.id} names loop ${turn.loop_id}, which the digest did not read`);
230
- return m.emissionsByWorker.get(loop.worker_id) ?? new Map();
231
- }
232
247
  static #commonPrefixLength(left, right) {
233
248
  const bound = Math.min(left.length, right.length);
234
249
  let n = 0;
@@ -479,8 +494,11 @@ export default class DigestRender {
479
494
  : null;
480
495
  // {§provider-wire-emission} — an empty emission is read from what the wire carried, never guessed at.
481
496
  const wireLine = content.length === 0 && packet?.assistant != null ? DigestRender.wireLine(packet.assistantRaw) : null;
497
+ const roles = packet?.messages().map(({ role }) => role).join(" → ");
498
+ const requestLine = packet === null ? null
499
+ : ` ↳ request: ${roles || "input sections not retained"} (${stem}.request.md)`;
482
500
  const opLines = DigestRender.#renderOpLines(m.logEntriesByTurn.get(turn.id) ?? [], m);
483
- return [head, ...(summary ? [summary] : []), ...(reasoningLine ? [reasoningLine] : []), ...(wireLine ? [wireLine] : []), ...opLines].join("\n");
501
+ return [head, ...(requestLine ? [requestLine] : []), ...(summary ? [summary] : []), ...(reasoningLine ? [reasoningLine] : []), ...(wireLine ? [wireLine] : []), ...opLines].join("\n");
484
502
  }
485
503
  // {§provider-wire-emission} — public so the line can be witnessed on its own.
486
504
  static wireLine(assistantRaw) {
@@ -541,12 +559,55 @@ export default class DigestRender {
541
559
  `Tokens: ${usageStr}`,
542
560
  `Cost: ${costStr}${kindStr}${usagelessStr}`,
543
561
  `Wire: ${wireStr}`,
562
+ `Room: ${DigestRender.#renderRoom(worker, m)}`,
544
563
  `Op mix: ${opMix.length > 0 ? opMix : "(no ops)"}`,
545
564
  `EDITs: ${DigestRender.#renderEditCensus(DigestRender.#editCensus(m).byWorker.get(worker.id))}`,
546
565
  `Emissions: ${DigestRender.#renderEmissions(worker, m)}`,
547
566
  `Reasonings: ${DigestRender.#renderReasonings(worker, m)}`,
548
567
  ].join("\n");
549
568
  }
569
+ // {§digest-room-line} — the room in provider tokens: the largest budget the model was shown, at its
570
+ // request's own ratio, against the capacity; the wall's estimate against the provider's count; and the
571
+ // requests the estimate put under the wall while the count was over it.
572
+ static #renderRoom(worker, m) {
573
+ const measured = DigestRender.#roomMeasures(worker, m);
574
+ if (measured.length === 0)
575
+ return "(no measured requests)";
576
+ const room = Math.max(...measured.map(({ weight, budget, count, capacity }) => budget * count / weight / capacity));
577
+ const errors = measured.map(({ estimate, count }) => (estimate - count) / count);
578
+ const missed = measured.filter(({ estimate, count, wall }) => estimate <= wall && count > wall).length;
579
+ const percent = (ratio) => `${ratio < 0 ? "-" : "+"}${Math.abs(100 * ratio).toFixed(1)}%`;
580
+ return `budget up to ${Math.round(100 * room)}% of capacity · wall estimate ${percent(Math.min(...errors))} to ${percent(Math.max(...errors))} of the count`
581
+ + (missed === 0 ? "" : ` ⚠ ${missed} over the wall the estimate admitted`);
582
+ }
583
+ // One measure per packet-bearing inference turn whose request records a known capacity and wall and a
584
+ // provider count: the exact preflight measurement, else the reported input.
585
+ static #roomMeasures(worker, m) {
586
+ const measures = [];
587
+ for (const loop of m.loopsByWorker.get(worker.id) ?? []) {
588
+ for (const turn of m.turnsByLoop.get(loop.id) ?? []) {
589
+ if (turn.kind !== "inference" || turn.has_packet !== 1)
590
+ continue;
591
+ const packet = m.evidence.packet(turn).packet;
592
+ if (packet === null || packet.budget === null || packet.budget === 0 || packet.weight === 0)
593
+ continue;
594
+ const call = m.modelCalls.findLast((row) => row.turn_id === turn.id && row.kind === "emission" && row.capacity !== null);
595
+ const capacity = DigestRender.parseJson(call?.capacity);
596
+ if (capacity?.inputCapacity == null || capacity.inputWall == null)
597
+ continue;
598
+ const reported = (m.requestsByTurn.get(turn.id) ?? [])
599
+ .findLast((row) => row.kind === "emission" && row.outcome === "response" && row.usage_input !== null)?.usage_input ?? null;
600
+ const count = capacity.prompt?.kind === "exact" ? capacity.prompt.tokens ?? null : reported;
601
+ if (count === null || count === 0)
602
+ continue;
603
+ measures.push({
604
+ weight: packet.weight, budget: packet.budget, capacity: capacity.inputCapacity, wall: capacity.inputWall, count,
605
+ estimate: Math.ceil(packet.weight * capacity.inputCapacity / packet.budget),
606
+ });
607
+ }
608
+ }
609
+ return measures;
610
+ }
550
611
  // {§emission-row} — announced, retired by the model, and echoed back as headings in its own text.
551
612
  static #renderEmissions(worker, m) {
552
613
  const rows = m.emissionRows.filter((row) => row.worker_id === worker.id);
@@ -603,6 +664,7 @@ export default class DigestRender {
603
664
  for (const worker of workspaceWorkers) {
604
665
  lines.push("");
605
666
  lines.push(`### Worker #${worker.id} — ${worker.name}`);
667
+ lines.push(`Owner: ${worker.owner}`);
606
668
  lines.push("");
607
669
  lines.push("```");
608
670
  lines.push(DigestRender.#renderWorkerShape(worker, m));
@@ -629,24 +691,15 @@ export default class DigestRender {
629
691
  if (loop.status !== 200 && terminal?.problem?.detail !== undefined) {
630
692
  lines.push(`Terminal${loop.terminated_by !== null ? ` (${loop.terminated_by})` : ""}: ${terminal.problem.detail.trim()}`);
631
693
  }
632
- const policy = DigestRender.parseJson(loop.policy, {});
633
- const policySummary = Object.entries(policy)
634
- .map(([key, value]) => `${key}=${typeof value === "string" ? value : JSON.stringify(value)}`)
635
- .join(" ");
636
- if (policySummary.length > 0)
637
- lines.push(`Policy: ${policySummary}`);
694
+ lines.push("");
695
+ lines.push("| Turn | Packet artifacts | Physical requests |", "| --- | --- | --- |");
696
+ for (const turn of m.turnsByLoop.get(loop.id) ?? []) {
697
+ const requests = (m.requestsByTurn.get(turn.id) ?? []).map((request) => DigestRender.#link("digest.md", `requests/${request.id}.md`, `${request.id}: ${request.kind} ${request.outcome ?? request.state}`));
698
+ lines.push(`| ${turn.sequence} (${turn.kind}) | ${DigestRender.#turnLinks(m, turn, "digest.md").join(" · ") || "—"} | ${requests.join(" · ") || "—"} |`);
699
+ }
638
700
  lines.push("");
639
701
  const turnLines = (m.turnsByLoop.get(loop.id) ?? []).map((t) => DigestRender.#renderTurnLine(t, m));
640
- const findMaxTicks = (s) => {
641
- const matches = s.match(/`+/g);
642
- return matches ? Math.max(...matches.map((match) => match.length)) : 0;
643
- };
644
- const maxBackticks = turnLines.reduce((max, line) => Math.max(max, findMaxTicks(line)), 0);
645
- const fence = "`".repeat(Math.max(3, maxBackticks + 1));
646
- lines.push(fence);
647
- for (const line of turnLines)
648
- lines.push(line);
649
- lines.push(fence);
702
+ lines.push(DigestRender.#literal(turnLines.join("\n"), ""));
650
703
  }
651
704
  }
652
705
  }
@@ -667,7 +720,7 @@ export default class DigestRender {
667
720
  lines.push("(operation turn; no provider inference)");
668
721
  const reasoning = m.evidence.reasoning(t);
669
722
  if (reasoning !== null)
670
- lines.push("", reasoning);
723
+ lines.push("", DigestRender.#literal(reasoning));
671
724
  continue;
672
725
  }
673
726
  const attempts = m.attemptsByTurn.get(t.id) ?? [];
@@ -680,13 +733,14 @@ export default class DigestRender {
680
733
  if (packetFailure !== null)
681
734
  lines.push("(stored provider packet is invalid; see its packet artifacts)");
682
735
  else if (typeof reasoning === "string" && reasoning.length > 0)
683
- lines.push(reasoning);
736
+ lines.push(DigestRender.#literal(reasoning));
684
737
  else
685
738
  lines.push("(no admitted provider reasoning)");
686
739
  continue;
687
740
  }
688
741
  for (const attempt of attempts) {
689
- const response = DigestRender.parseJson(m.evidence.response(attempt.model_call_id), {});
742
+ const retainedResponse = m.evidence.response(attempt.model_call_id);
743
+ const response = DigestRender.parseJson(retainedResponse, {});
690
744
  const parseErrors = DigestRender.parseJson(attempt.parse_errors, []);
691
745
  lines.push("");
692
746
  const disposition = attempt.state === "error"
@@ -703,7 +757,7 @@ export default class DigestRender {
703
757
  lines.push(`Failure: ${JSON.stringify(DigestRender.parseJson(attempt.failure, attempt.failure))}`);
704
758
  }
705
759
  for (const request of m.requestsByAttempt.get(attempt.id) ?? []) {
706
- lines.push(`Physical request ${request.sequence}: [${request.state === "settled" ? request.outcome : request.state} evidence](requests/${request.id}.json)`);
760
+ lines.push(`Physical request ${request.sequence}: [${request.state === "settled" ? request.outcome : request.state} evidence](requests/${request.id}.md)`);
707
761
  }
708
762
  if (attempt.accepted !== 1) {
709
763
  for (const error of parseErrors) {
@@ -714,7 +768,9 @@ export default class DigestRender {
714
768
  }
715
769
  const reasoning = response.assistant?.reasoning ?? null;
716
770
  if (typeof reasoning === "string" && reasoning.length > 0)
717
- lines.push(reasoning);
771
+ lines.push(DigestRender.#literal(reasoning));
772
+ else if (retainedResponse === null && attempt.state === "response")
773
+ lines.push("(response evidence is not retained; readable reasoning availability is unknown)");
718
774
  else {
719
775
  const reasoningTokens = DigestRender.#accounting(m.requestsByAttempt.get(attempt.id) ?? [])?.usage?.outputTokenDetails?.reasoningTokens;
720
776
  lines.push(reasoningTokens !== undefined && reasoningTokens > 0
@@ -726,6 +782,108 @@ export default class DigestRender {
726
782
  }
727
783
  return lines.join("\n");
728
784
  }
785
+ // {§share-packet-names}: render the owned envelope, never infer roles from filenames or body text.
786
+ static request(messages, preamble = []) {
787
+ const blocks = messages.map(({ role, content }, index) => {
788
+ return `### ${index + 1}. ${role}\n\n${DigestRender.#literal(content)}`;
789
+ });
790
+ return ["# Request", ...preamble, "## Input messages", messages.map(({ role }) => role).join(" → "),
791
+ messages.length === 0 ? "No input message sections are retained; the original input cannot be reconstructed."
792
+ : "Stored text-message envelope, not a transport capture. Native payloads and provider transformations are not shown.",
793
+ ...blocks, ""].join("\n\n");
794
+ }
795
+ // {§digest-navigation}: navigation surrounds evidence; channel files stay verbatim.
796
+ static #requestPreamble(m, turn) {
797
+ const stems = DigestRender.packetStems(m);
798
+ const stem = stems.get(turn.id);
799
+ const from = `${stem}.request.md`;
800
+ const loop = m.loopsById.get(turn.loop_id);
801
+ const siblings = m.turns.filter((row) => row.has_packet === 1
802
+ && m.loopsById.get(row.loop_id)?.worker_id === loop.worker_id).toSorted((a, b) => a.id - b.id);
803
+ const index = siblings.findIndex(({ id }) => id === turn.id);
804
+ const previous = siblings[index - 1];
805
+ const next = siblings[index + 1];
806
+ const navigation = [
807
+ DigestRender.#link(from, "digest.md", "Digest"),
808
+ ...(previous === undefined ? [] : [DigestRender.#link(from, `${stems.get(previous.id)}.request.md`, "Previous")]),
809
+ ...(next === undefined ? [] : [DigestRender.#link(from, `${stems.get(next.id)}.request.md`, "Next")]),
810
+ ...DigestRender.#turnLinks(m, turn, from).filter((link) => !link.startsWith("[Input]")),
811
+ ];
812
+ const lines = [posix.basename(stem), navigation.join(" · ")];
813
+ const requests = m.requestsByTurn.get(turn.id) ?? [];
814
+ if (requests.length > 0) {
815
+ const table = ["| Request | Call | Admission | Outcome |", "| --- | --- | --- | --- |"];
816
+ for (const request of requests) {
817
+ table.push(`| ${DigestRender.#link(from, `requests/${request.id}.md`, String(request.id))} | ${request.kind} ${request.inference_call_id} | ${DigestRender.#admission(m, request)} | ${request.outcome ?? request.state} |`);
818
+ }
819
+ lines.push("## Attempts", table.join("\n"));
820
+ }
821
+ for (const attempt of m.attemptsByTurn.get(turn.id) ?? []) {
822
+ if (attempt.accepted === 1)
823
+ continue;
824
+ const ordinal = String(attempt.sequence).padStart(3, "0");
825
+ const prefix = `${stem}.attempt${ordinal}`;
826
+ if (attempt.state !== "response") {
827
+ lines.push(`### Emission attempt ${attempt.sequence}: ${attempt.state}`, DigestRender.#link(from, `${prefix}.${attempt.state}.json`, "Call evidence"));
828
+ if (attempt.failure !== null)
829
+ lines.push(DigestRender.#literal(attempt.failure, "json"));
830
+ continue;
831
+ }
832
+ const admission = attempt.accepted === 0 ? "rejected" : "unadmitted";
833
+ lines.push(`### Emission attempt ${attempt.sequence}: ${admission}`, m.evidence.response(attempt.model_call_id) === null ? "Response evidence is not retained."
834
+ : DigestRender.#link(from, `${prefix}.${admission}.assistant.md`, "Exact output"), DigestRender.#literal(JSON.stringify(DigestRender.parseJson(attempt.parse_errors, []), null, 2), "json"));
835
+ }
836
+ return lines;
837
+ }
838
+ static #admission(m, request) {
839
+ if (request.kind === "bare")
840
+ return "not an emission";
841
+ if (request.outcome !== "response")
842
+ return "not admitted";
843
+ const call = m.modelCalls.find(({ id }) => id === request.inference_call_id);
844
+ return call?.accepted === 1 ? "admitted" : call?.accepted === 0 ? "rejected" : "not admitted";
845
+ }
846
+ static #physicalRequest(m, request, evidence) {
847
+ const from = `requests/${request.id}.md`;
848
+ const turn = m.turns.find(({ id }) => id === request.turn_id);
849
+ const fields = typeof evidence === "object" && evidence !== null ? evidence : {};
850
+ const capture = fields.request;
851
+ const lines = [
852
+ `# Physical request ${request.id}`,
853
+ [DigestRender.#link(from, "digest.md", "Digest"),
854
+ DigestRender.#link(from, `requests/${request.id}.json`, "Complete evidence")].join(" · "),
855
+ ...(turn === undefined ? [] : [`Originating turn: ${DigestRender.#turnLinks(m, turn, from).join(" · ") || "no packet artifacts retained"}`]),
856
+ DigestRender.#literal(JSON.stringify({
857
+ provider: request.provider, model: request.model, kind: request.kind,
858
+ inferenceCallId: request.inference_call_id, sequence: request.sequence,
859
+ state: request.state, outcome: request.outcome, admission: DigestRender.#admission(m, request),
860
+ startedAt: request.started_at, completedAt: request.completed_at,
861
+ }, null, 2), "json"),
862
+ "## Accounting",
863
+ request.state === "pending" ? "Request is unsettled; accounting is not yet available."
864
+ : DigestRender.#literal(JSON.stringify(DigestRender.#requestAccounting(request), null, 2), "json"),
865
+ "## Dispatched request",
866
+ typeof capture?.body !== "string" ? "Dispatched request body was not retained."
867
+ : `${capture.method} ${capture.origin}\n\nExact serialized body; URL details and request headers are not captured.\n\n${DigestRender.#literal(capture.body, "json")}`,
868
+ ];
869
+ const call = m.modelCalls.find(({ id }) => id === request.inference_call_id);
870
+ if (request.outcome === "response" && call?.accepted === 0) {
871
+ lines.push("## Admission", "The provider returned a response, but Plurnk rejected its emission.", DigestRender.#literal(JSON.stringify(DigestRender.parseJson(call.parse_errors, []), null, 2), "json"));
872
+ }
873
+ if (fields.error !== undefined)
874
+ lines.push("## Failure", DigestRender.#literal(JSON.stringify(fields.error, null, 2), "json"));
875
+ if (evidence === null)
876
+ lines.push("## Response", "No settled response evidence is retained.");
877
+ else {
878
+ if (request.outcome === "error")
879
+ lines.push("Partial output; this physical request failed. It is not an admitted response.");
880
+ for (const [field, heading] of [["reasoning", "Reasoning"], ["content", "Content"]]) {
881
+ lines.push(`## ${heading}`, typeof fields[field] === "string"
882
+ ? DigestRender.#literal(fields[field]) : `No readable ${field} is retained in this request record.`);
883
+ }
884
+ }
885
+ return `${lines.join("\n\n")}\n`;
886
+ }
729
887
  // Per-turn forensic files. turnOps is the source authority; PacketWire
730
888
  // reproduces provider request slots, and assistantRaw preserves provider bytes.
731
889
  // {§share-packet-names}: the stem each turn's packet files carry is its log coordinate, worker, loop and
@@ -793,21 +951,32 @@ export default class DigestRender {
793
951
  const padded = stems.get(turn.id);
794
952
  const files = [];
795
953
  const { packet, packetFailure } = m.evidence.packet(turn);
954
+ const preamble = turn.has_packet === 1 ? DigestRender.#requestPreamble(m, turn) : [];
796
955
  if (packetFailure !== null) {
797
- files.push([`${padded}.packet.raw.txt`, packetFailure.raw], [`${padded}.packet.invalid.json`, JSON.stringify({
956
+ files.push([`${padded}.request.md`, ["# Request", ...preamble, "Stored packet is invalid; no input messages were reconstructed.",
957
+ DigestRender.#literal(JSON.stringify(packetFailure.error, null, 2), "json"),
958
+ DigestRender.#link(`${padded}.request.md`, `${padded}.packet.raw.txt`, "Stored packet")].join("\n\n")], [`${padded}.packet.raw.txt`, packetFailure.raw], [`${padded}.packet.invalid.json`, JSON.stringify({
798
959
  turnId: turn.id,
799
960
  error: packetFailure.error,
800
961
  }, null, 2)]);
801
962
  }
802
963
  if (packet !== null) {
803
- files.push([`${padded}.system.md`, packet.slot("system")], [`${padded}.user.md`, packet.slot("user")]);
804
964
  // {§packet-wire-envelope} — the exact text messages the request carried; a stored packet
805
965
  // whose log cannot be projected is evidence of its own, never a reason to stop the digest.
806
966
  try {
807
- files.push([`${padded}.wire.json`, JSON.stringify(packet.messages(DigestRender.#emissionsOf(turn, m)), null, 2)]);
967
+ const messages = packet.messages();
968
+ if (messages.length > 0)
969
+ files.push([`${padded}.system.md`, packet.slot("system")], [`${padded}.user.md`, packet.slot("user")], [`${padded}.wire.json`, JSON.stringify(messages, null, 2)]);
970
+ files.push([`${padded}.request.md`, DigestRender.request(messages, [
971
+ ...preamble,
972
+ ...(packet.attachments.length === 0 ? [] : ["## Native attachments", "Stored descriptors; selection alone does not prove transport or provider acceptance.", DigestRender.#literal(JSON.stringify(packet.attachments, null, 2), "json")]),
973
+ ])]);
808
974
  }
809
975
  catch (cause) {
810
976
  files.push([`${padded}.wire.invalid.json`, JSON.stringify({ turnId: turn.id, error: cause instanceof Error ? cause.message : String(cause) }, null, 2)]);
977
+ files.push([`${padded}.request.md`, ["# Request", ...preamble,
978
+ "Stored input could not be projected; no message order was reconstructed.",
979
+ DigestRender.#link(`${padded}.request.md`, `${padded}.wire.invalid.json`, "Projection failure")].join("\n\n")]);
811
980
  }
812
981
  }
813
982
  if (source !== null) {
@@ -844,22 +1013,30 @@ export default class DigestRender {
844
1013
  }, null, 2));
845
1014
  continue;
846
1015
  }
847
- const response = DigestRender.parseJson(m.evidence.response(attempt.model_call_id), {});
848
- const prefix = `${padded}.attempt${attemptPadded}.rejected`;
1016
+ const retainedResponse = m.evidence.response(attempt.model_call_id);
1017
+ const admission = attempt.accepted === 0 ? "rejected" : "unadmitted";
1018
+ const prefix = `${padded}.attempt${attemptPadded}.${admission}`;
1019
+ write(`${prefix}.parse-errors.json`, JSON.stringify(DigestRender.parseJson(attempt.parse_errors, []), null, 2));
1020
+ write(`${prefix}.attributions.json`, JSON.stringify(DigestRender.parseJson(attempt.attributions, []), null, 2));
1021
+ if (retainedResponse === null) {
1022
+ write(`${prefix}.response.md`, `# Emission attempt ${attempt.sequence}: ${admission}\n\nResponse evidence is not retained.\n`);
1023
+ continue;
1024
+ }
1025
+ const response = DigestRender.parseJson(retainedResponse, {});
849
1026
  const attemptFiles = [
850
1027
  [
851
1028
  `${prefix}.assistant.md`,
852
1029
  typeof response.assistant?.content === "string" ? response.assistant.content : "",
853
1030
  ],
854
1031
  [`${prefix}.response.json`, JSON.stringify(response, null, 2)],
855
- [`${prefix}.parse-errors.json`, JSON.stringify(DigestRender.parseJson(attempt.parse_errors, []), null, 2)],
856
- [`${prefix}.attributions.json`, JSON.stringify(DigestRender.parseJson(attempt.attributions, []), null, 2)],
857
1032
  ];
858
1033
  for (const [file, body] of attemptFiles)
859
1034
  write(file, body);
860
1035
  }
861
1036
  });
862
1037
  for (const request of m.providerRequests) {
1038
+ const evidence = DigestRender.parseJson(m.evidence.request(request.id));
1039
+ write(`requests/${request.id}.md`, DigestRender.#physicalRequest(m, request, evidence));
863
1040
  write(`requests/${request.id}.json`, JSON.stringify({
864
1041
  id: request.id,
865
1042
  inferenceCallId: request.inference_call_id,
@@ -870,7 +1047,7 @@ export default class DigestRender {
870
1047
  startedAt: request.started_at,
871
1048
  completedAt: request.completed_at,
872
1049
  accounting: request.state === "settled" ? DigestRender.#requestAccounting(request) : null,
873
- evidence: DigestRender.parseJson(m.evidence.request(request.id)),
1050
+ evidence,
874
1051
  }, null, 2));
875
1052
  }
876
1053
  return written;
@@ -889,12 +1066,13 @@ export default class DigestRender {
889
1066
  id: r.id,
890
1067
  workspace_id: r.workspace_id,
891
1068
  name: r.name,
1069
+ owner: r.owner,
892
1070
  accounting: DigestRender.#accounting(m.requestsByWorker.get(r.id) ?? []),
893
1071
  edit_census: DigestRender.#editCensus(m).byWorker.get(r.id) ?? null,
894
1072
  })),
895
1073
  loops: m.loops.map((l) => ({
896
1074
  id: l.id, worker_id: l.worker_id, sequence: l.sequence, status: l.status,
897
- prompt: l.prompt, policy: DigestRender.parseJson(l.policy, {}),
1075
+ prompt: l.prompt,
898
1076
  terminated_by: l.terminated_by,
899
1077
  claimed_at: l.claimed_at,
900
1078
  terminated_at: l.terminated_at,
@@ -989,6 +1167,8 @@ export default class DigestRender {
989
1167
  id: le.id, worker_id: le.worker_id, loop_id: le.loop_id,
990
1168
  turn_id: le.turn_id, sequence: le.sequence, origin: le.origin,
991
1169
  source: le.source, model_call_id: le.model_call_id,
1170
+ ...(le.inherited_history === undefined ? {} : { inherited_history: le.inherited_history === 1 }),
1171
+ ...(le.ambient_event_id === undefined ? {} : { ambient_event_id: le.ambient_event_id }),
992
1172
  attrs: DigestRender.parseJson(le.attrs, {}),
993
1173
  op: le.op, target: DigestRender.#renderTarget(le),
994
1174
  status_rx: le.status_rx, state: le.state, outcome: le.outcome,