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
78import { readFileSync } from "node:fs";
import { extname } from "node:path";
import type { ProviderRouter, Image } from "../providers/index.ts";
import { IMAGE_MEDIA_TYPES } from "../providers/imageLimits.ts";
import type { Paths } from "../store/paths.ts";
import type { RunLog } from "../store/runlog.ts";
import type { IrisConfig } from "../config.ts";
import type { PdfLink } from "../util/pdf.ts";
export interface InputImage {
name: string; // filename, e.g. page-001.png
order: number; // 1-based processing order
path: string;
// The link annotations on this page, when it came from a PDF that had any โ
// targets the image itself cannot carry (see pipeline/links.ts). Optional because
// most pages have none, and because callers that synthesize an InputImage from
// something other than an upload (the regression gate's fixture images) have no
// links to give it.
links?: PdfLink[];
}
// Everything a pipeline phase needs. Created once per run.
export interface PipelineContext {
sessionId: string;
cfg: IrisConfig;
paths: Paths;
router: ProviderRouter;
log: RunLog;
images: InputImage[];
feedback?: string; // present on feedback re-runs
maxReviewIterations: number;
// Pages extracted in parallel in this run. Always a valid integer >= 1
// (normalized by loadConfig), so consumers need no fallback.
extractionConcurrency: number;
// Corrected pages this run re-verifies for measurement only (config
// `defaults.recheck_sample_size`; pipeline/correction.ts `recheckSampler`). Always a
// valid integer >= 0, where 0 means the measurement is off. Carried here rather than
// read off `cfg` at the claim site for the same reason as the line above: one place
// resolves it, and a phase cannot disagree with the number the run was started with.
recheckSampleSize: number;
// The logged-in user's GitHub token โ used to file agent-suggestion issues
// attributed to them (unless a service token override is configured).
githubToken?: string;
// True when the request that started this run resolved to the deployment's shared
// anonymous identity (`github.anonymous_token`), so `githubToken` above is a PAT out of
// config rather than a user's own credential. Carried because a filing failure is
// diagnosed by WHICH credential failed (`installHintFor`), and a config PAT's access has
// nothing to do with the GitHub App installation a user's token depends on.
anonymousSession?: boolean;
}
// The extension -> media type map lives with the rest of the model's input limits
// (providers/imageLimits.ts), so the formats the upload route accepts and the media
// types sent to the model cannot disagree โ this file used to claim image/tiff for a
// format the model does not read.
export function mediaTypeFor(filename: string): string {
return IMAGE_MEDIA_TYPES[extname(filename).toLowerCase()] ?? "image/png";
}
export function loadImage(img: InputImage): Image {
return { data: readFileSync(img.path), media_type: mediaTypeFor(img.name) };
}
// Feedback is injected as a top-level instruction available to every
// downstream agent in the run. It is phrased as a required change so that, on an
// iterative feedback re-run, the Reader surfaces it as an issue (sourced to the
// affected block) and the Copy Editor applies it โ rather than the round no-opping
// when the document is otherwise accessibility-clean.
export function feedbackPreamble(ctx: PipelineContext): string {
if (!ctx.feedback) return "";
return (
`\n\n## User feedback (top-level instruction โ applies to this whole run)\n` +
`${ctx.feedback}\n\n` +
`Treat this feedback as a REQUIRED change. Wherever it applies, flag the affected ` +
`block(s) by their @source reference and make the change in your output.\n`
);
}