// Jev sees what the judge sees: both are rendered from the one request built here.
import type { GitState, RemoteFact } from "./facts.ts";
import { type HistoryEntry, newestWithin } from "./history.ts";
const EARLIER_CHARS = 4_000;
const HISTORY_CHARS = 8_000;
export type JudgeAsk = {
readonly rule: string;
readonly summary: string;
readonly source: string;
};
/** Marks a request only `judgeRequest` made. It exists in types alone. */
declare const built: unique symbol;
/** The MCP tool a request is about. */
export type McpJudged = {
readonly tool: string;
readonly description?: string;
};
// Never tool output, file contents, the agent's own messages or AGENTS.md.
export type JudgeFields = {
readonly command: string;
readonly asks: readonly JudgeAsk[];
readonly cwd: string;
readonly git?: GitState;
readonly remotes?: readonly RemoteFact[];
readonly earlierUserMessages?: readonly string[];
readonly history?: readonly HistoryEntry[];
readonly userMessage?: string;
readonly environment?: readonly string[];
/** Set for an MCP call; `command` is then its JSON arguments. */
readonly mcp?: McpJudged;
};
// The type keeps a hand-made request out of the renderers.
export type JudgeRequest = JudgeFields & { readonly [built]: true };
// Applies no other cap: the caller's own caps come first.
export function judgeRequest(fields: JudgeFields): JudgeRequest {
const earlier = earlierWithinBudget(fields.earlierUserMessages ?? []);
const history = historyWithinBudget(fields.history ?? [], fields.cwd);
const request: JudgeFields = {
command: fields.command,
asks: fields.asks,
cwd: fields.cwd,
...(fields.git && { git: fields.git }),
...(fields.mcp && { mcp: fields.mcp }),
...(fields.remotes && { remotes: fields.remotes }),
...(fields.environment && { environment: fields.environment }),
...(earlier.length > 0 && { earlierUserMessages: earlier }),
...(history.length > 0 && { history }),
...(fields.userMessage && { userMessage: fields.userMessage }),
};
// The one place a request is made: the mark is not a runtime property.
return request as JudgeRequest;
}
function gitLines(git: GitState | undefined): string[] {
if (!git) return [];
if (git.kind === "not-a-repo") return ["Git: not a git repository"];
if (git.kind === "unknown") return ["Git branch: unknown"];
const state = git.dirty ? "with uncommitted changes" : "clean";
return [`Git branch: ${git.branch}, ${state}`];
}
function remoteLines(remotes: readonly RemoteFact[] | undefined): string[] {
if (!remotes || remotes.length === 0) return [];
const flag = " (added or changed this session)";
const lines = remotes.map(
({ name, url, changed }) => `- ${name} ${url}${changed ? flag : ""}`,
);
return ["Git remotes:", ...lines];
}
// Any block's closing tag, in any case: none may end a block early.
const CLOSING_TAG =
/<\/(command|user_message|earlier_user_messages|session_history|tool_description|arguments)\b/gi;
function escaped(text: string): string {
return text.replace(CLOSING_TAG, "<\\/$1");
}
function block(tag: string, lines: readonly string[]): string[] {
return lines.length === 0 ? [] : [`<${tag}>`, ...lines, `${tag}>`];
}
function messageBody(text: string): string {
return escaped(text).replaceAll("\n", "\n ");
}
function earlierWithinBudget(messages: readonly string[]): string[] {
const overhead = `[${messages.length}] `.length + 1;
const cost = (text: string): number => messageBody(text).length + overhead;
return newestWithin(messages, cost, EARLIER_CHARS + 1);
}
function earlierLines(messages: readonly string[] = []): string[] {
return messages.map((text, i) => `[${i + 1}] ${messageBody(text)}`);
}
// The cwd shows only when it differs from the call's. A command's later lines
// are indented so none can pass for an entry of its own.
function entryBody(entry: HistoryEntry, cwd: string): string {
const notes = [
...(entry.cwd === cwd ? [] : [`cwd ${entry.cwd}`]),
...(entry.failed ? ["failed"] : []),
...(entry.background ? ["started in background"] : []),
];
const label =
notes.length === 0 ? entry.tool : `${entry.tool} (${notes.join("; ")})`;
return `${label}: ${escaped(entry.text).replaceAll("\n", "\n ")}`;
}
function historyWithinBudget(
entries: readonly HistoryEntry[],
cwd: string,
): HistoryEntry[] {
const overhead = `${entries.length}. `.length + 1;
const cost = (entry: HistoryEntry): number =>
entryBody(entry, cwd).length + overhead;
return newestWithin(entries, cost, HISTORY_CHARS + 1);
}
function historyLines(
entries: readonly HistoryEntry[] = [],
cwd: string,
): string[] {
return entries.map((entry, i) => `${i + 1}. ${entryBody(entry, cwd)}`);
}
function mcpLines(mcp: McpJudged, args: string): string[] {
const description = mcp.description ? [escaped(mcp.description)] : [];
return [
`MCP tool: ${escaped(mcp.tool)}`,
...block("tool_description", description),
...block("arguments", [escaped(args)]),
];
}
export function judgeInput(request: JudgeRequest): string {
const flagged = request.asks.map(
({ rule, summary, source }) => `- ${rule} (${summary}): ${source}`,
);
const said = request.userMessage;
return [
"Flagged by the bouncer:",
...flagged,
`Working directory: ${request.cwd}`,
...gitLines(request.git),
...remoteLines(request.remotes),
...block(
"earlier_user_messages",
earlierLines(request.earlierUserMessages),
),
...block("session_history", historyLines(request.history, request.cwd)),
...block("user_message", said ? [escaped(said)] : []),
...(request.mcp
? mcpLines(request.mcp, request.command)
: ["", escaped(request.command), ""]),
].join("\n");
}
export function jevState(request: JudgeRequest): Record {
const earlier = request.earlierUserMessages ?? [];
const history = request.history ?? [];
const remotes = request.remotes ?? [];
return {
flagged: request.asks.map(({ rule, summary, source }) => ({
rule,
summary,
source,
})),
working_directory: request.cwd,
...(request.git && { git: request.git }),
...(remotes.length > 0 && {
remotes: remotes.map(({ name, url, changed }) => ({
name,
url,
changed,
})),
}),
...(earlier.length > 0 && { earlier_user_messages: earlier }),
...(history.length > 0 && {
session_history: history.map(
({ tool, text, cwd, failed, background }) => ({
tool,
text,
cwd,
...(failed && { failed }),
...(background && { background }),
}),
),
}),
...(request.userMessage && { user_message: request.userMessage }),
command: request.command,
};
}