📦 EqualifyEverything / equalify-iris

📄 reconciliation.ts · 93 lines
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93import { extractJson } from "../util/json.ts";
import { feedbackPreamble, loadImage, type PipelineContext } from "./context.ts";
import type { Fragment } from "./fragment.ts";

const RECON_AGENT = "reconciliation";

const SYSTEM_PROMPT = `You are the Reconciliation Agent (PRD §7.6). You decide whether a content block cut off at
the BOTTOM edge of one image continues at the TOP edge of the next image, and if so produce a
single joined HTML fragment.

Be conservative. Only "join" when the content type matches AND the textual/structural
similarity at the edges is high. A false join is silently wrong; a missed join is visibly two
blocks the Reader can flag. When in doubt, prefer "suspected" or "separate".

Respond with ONLY this JSON:
{ "decision": "join" | "suspected" | "separate",
  "joined_html": "<merged accessible HTML, present only when decision is join>" }`;

// PRD §7.6: for each adjacent image pair, attempt to stitch bottom-edge
// fragments of image N onto top-edge fragments of image N+1.
export async function runReconciliation(
  ctx: PipelineContext,
  fragments: Fragment[],
): Promise<Fragment[]> {
  const consumed = new Set<Fragment>();
  const added: Fragment[] = [];

  const orders = [...new Set(fragments.map((f) => f.order))].sort((a, b) => a - b);
  for (let i = 0; i < orders.length - 1; i++) {
    const top = orders[i];
    const bottom = orders[i + 1];
    const aCandidates = fragments.filter(
      (f) => f.order === top && !consumed.has(f) && f.edges.some((e) => e.includes("bottom")),
    );
    const bCandidates = fragments.filter(
      (f) => f.order === bottom && !consumed.has(f) && f.edges.some((e) => e.includes("top")),
    );

    for (const a of aCandidates) {
      const b = bCandidates.find((x) => x.agent === a.agent && !consumed.has(x));
      if (!b) continue;

      const imgA = ctx.images.find((im) => im.name === a.image);
      const imgB = ctx.images.find((im) => im.name === b.image);
      const images = [imgA, imgB].filter((x) => x != null).map((x) => loadImage(x!));

      const user =
        `Agent type: ${a.agent}\n\n` +
        `BOTTOM-edge fragment of ${a.image}:\n${a.innerHtml}\n(log: ${a.log})\n\n` +
        `TOP-edge fragment of ${b.image}:\n${b.innerHtml}\n(log: ${b.log})\n\n` +
        `The two source images are attached in order.` +
        feedbackPreamble(ctx);

      const res = await ctx.router.complete(
        RECON_AGENT,
        "vision",
        [
          { role: "system", content: SYSTEM_PROMPT },
          { role: "user", content: user },
        ],
        { images },
      );
      ctx.log.agentCall({
        agent: { name: RECON_AGENT, file: `${RECON_AGENT}.md`, content: SYSTEM_PROMPT, capabilities: ["vision"], sha: null, sessionBuilt: false },
        phase: "reconciliation",
        output: res.text,
      });

      const parsed = extractJson<{ decision?: string; joined_html?: string }>(res.text);
      const decision = parsed?.decision ?? "separate";
      if (decision === "join" && parsed?.joined_html) {
        consumed.add(a);
        consumed.add(b);
        added.push({
          image: `${a.image}+${b.image}`,
          order: a.order,
          agent: a.agent,
          region: `${a.region}+${b.region}`,
          innerHtml: parsed.joined_html,
          edges: [],
          log: `reconciled across ${a.image} and ${b.image}`,
          reconciled: true,
        });
      } else if (decision === "suspected") {
        b.suspectedContinuation = true;
      }
    }
  }

  const kept = fragments.filter((f) => !consumed.has(f));
  return [...kept, ...added].sort((x, y) => x.order - y.order);
}