Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/junior-evals/src/guardian-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ function resolveGuardianModelId(): string {
if (configured) {
return configured;
}
return "openai/gpt-5.6-luna";
return "typesafe-ai/jev";
}

/** Run one Guardian proposal through the production reviewer boundary. */
Expand Down
2 changes: 1 addition & 1 deletion packages/junior-evals/vitest.evals.guardian.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ process.env.JUNIOR_STATE_ADAPTER = "redis";
process.env.JUNIOR_STATE_KEY_PREFIX ??= `junior:eval-guardian:${randomUUID()}`;
process.env.REDIS_URL =
process.env.JUNIOR_EVAL_REDIS_URL?.trim() || "redis://127.0.0.1:6382";
process.env.AI_GUARDIAN_MODEL ??= "openai/gpt-5.6-luna";
process.env.AI_GUARDIAN_MODEL ??= "typesafe-ai/jev";

export default defineConfig({
resolve: {
Expand Down
2 changes: 2 additions & 0 deletions packages/junior/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"dependencies": {
"@agentclientprotocol/sdk": "1.3.0",
"@ai-sdk/gateway": "^3.0.119",
"@ai-sdk/gateway-v4": "npm:@ai-sdk/gateway@4.0.85",
"@chat-adapter/slack": "4.29.0",
"@chat-adapter/state-memory": "4.29.0",
"@chat-adapter/state-redis": "4.29.0",
Expand All @@ -91,6 +92,7 @@
"@vercel/queue": "^0.2.0",
"@vercel/sandbox": "2.8.0",
"ai": "^6.0.190",
"ai-v7": "npm:ai@7.0.105",
"chat": "4.29.0",
"commander": "^14.0.3",
"drizzle-orm": "catalog:",
Expand Down
5 changes: 1 addition & 4 deletions packages/junior/src/chat/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,10 +242,7 @@ const DEFAULT_FAST_MODEL_ID = getModel(
"vercel-ai-gateway",
"openai/gpt-5.6-luna",
).id;
const DEFAULT_GUARDIAN_MODEL_ID = getModel(
"vercel-ai-gateway",
"openai/gpt-5.6-luna",
).id;
const DEFAULT_GUARDIAN_MODEL_ID = "typesafe-ai/jev";
const DEFAULT_HANDOFF_MODEL_ID = getModel(
"vercel-ai-gateway",
"openai/gpt-5.6-sol",
Expand Down
27 changes: 27 additions & 0 deletions packages/junior/src/chat/pi/evaluate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { createGatewayProvider } from "@ai-sdk/gateway-v4";
import {
experimental_evaluate as evaluate,
type Experimental_EvaluationQuestion as EvaluationQuestion,
} from "ai-v7";
import { resolveGatewayCredential } from "@/chat/pi/gateway-auth";

/** Evaluate typed questions with an AI Gateway evaluation model. */
export async function evaluateQuestions<
const Questions extends Record<string, EvaluationQuestion>,
>(args: {
modelId: string;
state: string;
questions: Questions;
signal?: AbortSignal;
}) {
const credential = await resolveGatewayCredential();
const gateway = createGatewayProvider(
credential ? { apiKey: credential.token } : {},
);
return evaluate({
model: gateway.evaluationModel(args.modelId),
state: args.state,
questions: args.questions,
abortSignal: args.signal,
});
}
54 changes: 53 additions & 1 deletion packages/junior/src/chat/services/guardian-action-review.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { z } from "zod";
import { logWarn } from "@/chat/logging";
import type { completeObject } from "@/chat/pi/client";
import { evaluateQuestions } from "@/chat/pi/evaluate";
import { GUARDIAN_ACTION_POLICY } from "@/chat/services/guardian-action-policy";
import { ProviderError } from "@/chat/services/provider-error";
import type {
Expand All @@ -20,6 +21,7 @@ const guardianDecisionSchema = z
type CompleteObject = typeof completeObject;
const GUARDIAN_REVIEW_TIMEOUT_MS = 60_000;
const GUARDIAN_REVIEW_MAX_TOKENS = 2_000;
const JEV_MODEL_ID = "typesafe-ai/jev";
const MAX_PROPOSAL_CHARS = 192_000;

/** Serialize one bounded proposal while keeping its contents untrusted. */
Expand Down Expand Up @@ -51,6 +53,54 @@ export function createGuardianActionReviewer(options: {
const signal = reviewOptions?.signal
? AbortSignal.any([reviewOptions.signal, timeoutSignal])
: timeoutSignal;
if (options.modelId === JEV_MODEL_ID) {
const result = await evaluateQuestions({
modelId: options.modelId,
state: [GUARDIAN_ACTION_POLICY, guardianPrompt(proposal)].join(
"\n\n",
),
questions: {
decision: {
type: "choice",
instructions: "Choose the required Guardian decision.",
criteria: {
allow: "The policy allows the action without confirmation.",
ask: "The policy requires explicit user confirmation.",
deny: "The policy prohibits the action.",
},
},
riskLevel: {
type: "choice",
instructions: "Choose the risk level of the planned action.",
criteria: {
low: "Low impact and easy to reverse.",
medium: "Meaningful but limited impact.",
high: "Substantial impact or hard to reverse.",
critical: "Severe, broad, or irreversible impact.",
},
},
userAuthorization: {
type: "choice",
instructions:
"Choose how clearly the user authorized the planned action.",
criteria: {
high: "The user explicitly authorized this exact action.",
medium: "The user intent supports the action but is not exact.",
low: "The action is only weakly implied.",
unknown: "The proposal shows no user authorization.",
},
},
},
signal,
});
return guardianDecisionSchema.parse({
decision: result.answers.decision.choice,
reason: "jev_evaluation",
riskLevel: result.answers.riskLevel.choice,
userAuthorization: result.answers.userAuthorization.choice,
});
}

const completeReview = () =>
options.completeObject({
modelId: options.modelId,
Expand Down Expand Up @@ -87,7 +137,9 @@ export function createGuardianActionReviewer(options: {
}
return {
...guardianDecisionSchema.parse(result.object),
...(result.costUsd !== undefined ? { costUsd: result.costUsd } : undefined),
...(result.costUsd !== undefined
? { costUsd: result.costUsd }
: undefined),
};
},
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ vi.mock("@/chat/pi/client", () => ({
};
}),
embedTexts: vi.fn(),
resolveGatewayModel: vi.fn((modelId: string) => ({ id: modelId })),
}));

function piMessages(instruction: string, turnId: string): PiMessage[] {
Expand Down
4 changes: 2 additions & 2 deletions packages/junior/tests/component/config/chat-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,13 @@ describe("chat config", () => {
expect(botConfig.fastModelId).toBe("openai/gpt-5.6-luna");
});

it("uses Luna for Guardian when no override is configured", async () => {
it("uses Jev for Guardian when no override is configured", async () => {
process.env.AI_MODEL = "anthropic/claude-opus-4.6";
process.env.AI_FAST_MODEL = "anthropic/claude-haiku-4.5";
delete process.env.AI_GUARDIAN_MODEL;

const { botConfig } = await loadConfig();
expect(botConfig.guardianModelId).toBe("openai/gpt-5.6-luna");
expect(botConfig.guardianModelId).toBe("typesafe-ai/jev");
});

it("uses the configured Guardian model override", async () => {
Expand Down
3 changes: 3 additions & 0 deletions packages/junior/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ const packageRoot = process.cwd();
loadJuniorTestEnvFiles({ workspaceRoot, packageRoots: [packageRoot] });

process.env.AI_GATEWAY_API_KEY = "test-gateway-key";
// The shared fake LLM implements chat completions, not evaluation models.
// Guardian evals cover the Jev path through the real AI Gateway protocol.
process.env.AI_GUARDIAN_MODEL = "openai/gpt-5.6-luna";
process.env.JUNIOR_SECRET = "junior-test-secret";
process.env.JUNIOR_STATE_ADAPTER = "memory";
process.env.JUNIOR_STATE_KEY_PREFIX ??= `junior:test:${process.pid}`;
Expand Down
60 changes: 60 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ syncInjectedDepsAfterScripts:
- build
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
# Temporary exception for the Jev evaluation model spike.
- "ai@7.0.105"
- "@ai-sdk/gateway@4.0.85"
- "@ai-sdk/provider@4.0.17"
- "@ai-sdk/provider-utils@5.0.43"
- "@sentry/starlight-theme"
- "@vitest-evals/core@0.16.1"
- "@vitest-evals/report-ui@0.16.1"
Expand Down
Loading