@hue-run/sdk 0.4.2 → 0.5.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.
@@ -0,0 +1,171 @@
1
+ import { copyFile, mkdir, readdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { basename, extname, join } from "node:path";
3
+ import { outputContentTypes } from "../evals/files.js";
4
+ import { withFiles } from "../evals/types.js";
5
+ /**
6
+ * Direct (file) cases for `hue eval`: a case is a task plus pinned input files, and the agent's
7
+ * answer is one or more generated documents. The agent command receives a private case directory
8
+ * instead of stdin/stdout JSON, the same layout Hue's document workers use:
9
+ *
10
+ * <case dir>/inputs.json the case inputs
11
+ * <case dir>/case.json id, external key, run config and the staged file list
12
+ * <case dir>/files/<role>/<name> verified copies of the agent-visible pinned files
13
+ * <case dir>/output/ where the command writes what it produced
14
+ *
15
+ * Everything under `output/` becomes the execution's generated files. Optional helpers there:
16
+ * `manifest.json` (`{ "primary": "<filename>", "output": <json> }`), `result.json` (the JSON
17
+ * output) and `summary.txt` / `summary.md` (recorded as `{ "summary": "…" }`).
18
+ */
19
+ export const directCaseEnvironment = [
20
+ "HUE_CASE_DIR",
21
+ "HUE_CASE_INPUTS",
22
+ "HUE_CASE_OUTPUT_DIR",
23
+ "HUE_CASE_ID",
24
+ "HUE_CASE_KEY",
25
+ "HUE_EXECUTION_ID",
26
+ ];
27
+ /** Generated-file extensions Hue accepts and the content type recorded for each. */
28
+ export const outputExtensions = {
29
+ ".pdf": "application/pdf",
30
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
31
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
32
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
33
+ ".json": "application/json",
34
+ ".txt": "text/plain",
35
+ ".csv": "text/csv",
36
+ ".png": "image/png",
37
+ ".jpg": "image/jpeg",
38
+ ".jpeg": "image/jpeg",
39
+ ".webp": "image/webp",
40
+ };
41
+ for (const type of Object.values(outputExtensions))
42
+ if (!outputContentTypes.includes(type))
43
+ throw new Error(`outputExtensions maps to a content type Hue does not accept: ${type}`);
44
+ const HELPER_FILES = new Set(["manifest.json", "result.json", "output.json"]);
45
+ const SUMMARY_FILES = ["summary.txt", "summary.md", "final.txt", "resumen.txt"];
46
+ /** Writes the case directory: inputs, descriptor, verified file copies and an empty output folder. */
47
+ export async function stageDirectCase(root, input) {
48
+ const caseDirectory = join(root, "case");
49
+ const outputDirectory = join(caseDirectory, "output");
50
+ await mkdir(outputDirectory, { recursive: true, mode: 0o700 });
51
+ const inputsPath = join(caseDirectory, "inputs.json");
52
+ await writeFile(inputsPath, JSON.stringify(input.inputs, null, 2), { mode: 0o600 });
53
+ const used = new Set();
54
+ const files = [];
55
+ for (const file of input.files) {
56
+ const folder = join(caseDirectory, "files", file.role);
57
+ await mkdir(folder, { recursive: true, mode: 0o700 });
58
+ // Two inputs may share a filename (two incident reports, say); keep both.
59
+ const name = basename(file.filename);
60
+ const extension = extname(name);
61
+ const stem = name.slice(0, name.length - extension.length);
62
+ let target = join(folder, name);
63
+ for (let copy = 2; used.has(target); copy++)
64
+ target = join(folder, `${stem} (${copy})${extension}`);
65
+ used.add(target);
66
+ await copyFile(file.path, target);
67
+ files.push({ role: file.role, filename: basename(target), path: target });
68
+ }
69
+ await writeFile(join(caseDirectory, "case.json"), JSON.stringify({
70
+ id: input.item.id,
71
+ externalKey: input.item.externalKey,
72
+ executionId: input.executionId,
73
+ config: input.config,
74
+ files,
75
+ outputDirectory,
76
+ }, null, 2), { mode: 0o600 });
77
+ return { caseDirectory, inputsPath, outputDirectory, files };
78
+ }
79
+ async function readJsonFile(path) {
80
+ try {
81
+ return JSON.parse(await readFile(path, "utf8"));
82
+ }
83
+ catch (error) {
84
+ if (error.code === "ENOENT")
85
+ return undefined;
86
+ throw new Error(`${basename(path)} in the output directory is not valid JSON`);
87
+ }
88
+ }
89
+ /** Regular files anywhere under the output directory, named by their `/`-joined relative path.
90
+ * Hidden and lock entries (dot names, `~$…`) and symlinks are skipped at every level. */
91
+ async function listOutputFiles(directory, prefix = "") {
92
+ const entries = await readdir(directory, { withFileTypes: true }).catch(() => []);
93
+ const found = [];
94
+ for (const entry of entries) {
95
+ if (entry.name.startsWith(".") || entry.name.startsWith("~$"))
96
+ continue;
97
+ const path = join(directory, entry.name);
98
+ const name = `${prefix}${entry.name}`;
99
+ if (entry.isDirectory())
100
+ found.push(...(await listOutputFiles(path, `${name}/`)));
101
+ else if (entry.isFile())
102
+ found.push({ name, path });
103
+ }
104
+ return found;
105
+ }
106
+ /** Artifact filenames cannot contain path separators or controls. Encode only those forbidden
107
+ * characters plus `%`, then represent `/` as `%2F`; distinct output paths stay distinct. */
108
+ function artifactFilename(relativePath) {
109
+ return relativePath
110
+ .split("/")
111
+ .map((segment) => segment
112
+ .replaceAll("%", "%25")
113
+ .replaceAll("\\", "%5C")
114
+ // eslint-disable-next-line no-control-regex -- artifact filenames forbid controls
115
+ .replace(/[\x00-\x1f\x7f]/gu, (character) => encodeURIComponent(character)))
116
+ .join("%2F");
117
+ }
118
+ /**
119
+ * Turns the output folder into the target's result. Every regular file, in subdirectories too,
120
+ * becomes a generated file; an unsupported extension is the agent's error rather than a silently
121
+ * dropped document. `fallbackOutput` (for example the command's stdout) is used when no JSON
122
+ * output or summary file was written.
123
+ */
124
+ export async function collectDirectOutputs(outputDirectory, fallbackOutput) {
125
+ const manifest = await readJsonFile(join(outputDirectory, "manifest.json"));
126
+ const declaredPrimary = manifest && typeof manifest === "object" && !Array.isArray(manifest)
127
+ ? manifest.primary
128
+ : undefined;
129
+ const declaredOutput = manifest && typeof manifest === "object" && !Array.isArray(manifest)
130
+ ? manifest.output
131
+ : undefined;
132
+ let output = declaredOutput ??
133
+ (await readJsonFile(join(outputDirectory, "result.json"))) ??
134
+ (await readJsonFile(join(outputDirectory, "output.json")));
135
+ if (output === undefined)
136
+ for (const name of SUMMARY_FILES) {
137
+ const text = await readFile(join(outputDirectory, name), "utf8").catch(() => undefined);
138
+ if (text !== undefined) {
139
+ output = { summary: text };
140
+ break;
141
+ }
142
+ }
143
+ if (output === undefined)
144
+ output = fallbackOutput;
145
+ // Helper and summary names are reserved at the top level only; a nested one is a document.
146
+ const entries = (await listOutputFiles(outputDirectory)).filter((entry) => !HELPER_FILES.has(entry.name) && !SUMMARY_FILES.includes(entry.name));
147
+ // Directory order differs between filesystems; keep uploads stable.
148
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
149
+ const unsupported = entries.filter((entry) => !outputExtensions[extname(entry.name).toLowerCase()]);
150
+ if (unsupported.length)
151
+ throw new Error(`The agent wrote files Hue does not accept as generated documents: ${unsupported.map((entry) => entry.name).join(", ")} (accepted: ${Object.keys(outputExtensions).join(" ")})`);
152
+ const files = [];
153
+ for (const entry of entries) {
154
+ if ((await stat(entry.path)).size === 0)
155
+ throw new Error(`The agent wrote an empty file: ${entry.name}`);
156
+ files.push({
157
+ path: entry.path,
158
+ filename: artifactFilename(entry.name),
159
+ contentType: outputExtensions[extname(entry.name).toLowerCase()],
160
+ });
161
+ }
162
+ if (typeof declaredPrimary === "string") {
163
+ const primaryIndex = entries.findIndex((entry) => entry.name === declaredPrimary);
164
+ if (primaryIndex < 0)
165
+ throw new Error(`manifest.json names a primary file that was not written: ${declaredPrimary}`);
166
+ files[primaryIndex].primary = true;
167
+ }
168
+ else if (files.length === 1)
169
+ files[0].primary = true;
170
+ return withFiles(output, files);
171
+ }
@@ -0,0 +1,28 @@
1
+ import type { ExperimentCase, LocalFile } from "../evals/types.js";
2
+ import { TargetResult } from "../evals/types.js";
3
+ import { type SimulationTargetContext } from "../evals/simulation.js";
4
+ import type { JsonValue } from "../types.js";
5
+ /** Adapter contract: the module's `default` or `runMyAgent` export. */
6
+ export type EvalAdapter = (inputs: JsonValue, context: SimulationTargetContext) => JsonValue | undefined | Promise<JsonValue | undefined>;
7
+ /**
8
+ * Context of a direct (file) case: pinned input files in, generated documents out. The same
9
+ * adapter module serves both modes; `mode` tells it which context it received.
10
+ */
11
+ export interface DirectTargetContext {
12
+ mode: "direct";
13
+ config: JsonValue;
14
+ item: Pick<ExperimentCase, "id" | "externalKey">;
15
+ executionId: string;
16
+ /** Verified copies of the agent-visible pinned files (`source`, templates, originals). */
17
+ files: LocalFile[];
18
+ /** Private scratch directory for this case; generated files may be written here. */
19
+ outputDirectory: string;
20
+ signal?: AbortSignal;
21
+ }
22
+ export type DirectEvalAdapter = (inputs: JsonValue, context: DirectTargetContext) => JsonValue | TargetResult | undefined | Promise<JsonValue | TargetResult | undefined>;
23
+ /**
24
+ * `hue eval`: runs a local adapter or command against a Scenario or eval set through
25
+ * `runSimulation`, or registers it as an outbound worker through `runLocalAgent`.
26
+ * Returns the process exit code.
27
+ */
28
+ export declare function runEvalCommand(argv: string[]): Promise<number>;