feat: allow canvas agent rollout settings
This commit is contained in:
parent
020a74a6a8
commit
fae9d32ffb
@ -58,9 +58,9 @@ import {
|
||||
} from "@/lib/canvas/agent-context";
|
||||
import {
|
||||
canApplyCanvasAgentMutations,
|
||||
getCanvasAgentFeatureFlags,
|
||||
shouldStreamCanvasAgentShadowOperations,
|
||||
} from "@/lib/canvas/agent-feature-flags";
|
||||
import { getCanvasAgentFeatureFlagsFromSystemSettings } from "@/lib/canvas/agent-feature-settings";
|
||||
import {
|
||||
buildPendingCanvasOperation,
|
||||
extractPendingCanvasOperationFromWorkingState,
|
||||
@ -906,7 +906,7 @@ export async function POST(request: Request) {
|
||||
const initialRequestCanvasAgentContext = normalizeCanvasAgentContext(
|
||||
contextPayload || undefined
|
||||
);
|
||||
const canvasAgentFeatures = getCanvasAgentFeatureFlags();
|
||||
const canvasAgentFeatures = await getCanvasAgentFeatureFlagsFromSystemSettings();
|
||||
const canApplyCanvasAgentMutation =
|
||||
canApplyCanvasAgentMutations(canvasAgentFeatures);
|
||||
const currentMessageAttachmentCount = message.parts.filter(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { getCanvasAgentFeatureFlags } from "@/lib/canvas/agent-feature-flags";
|
||||
import { getCanvasAgentFeatureFlagsFromSystemSettings } from "@/lib/canvas/agent-feature-settings";
|
||||
import { applyCanvasOperationPlan } from "@/lib/canvas/agent-mutations";
|
||||
import { handleCanvasAgentOperationRouteRequest } from "@/lib/canvas/agent-operation-route";
|
||||
|
||||
@ -14,7 +14,7 @@ export async function POST(
|
||||
userId: session?.user?.id,
|
||||
projectId: id,
|
||||
readBody: () => request.json(),
|
||||
features: getCanvasAgentFeatureFlags(),
|
||||
features: await getCanvasAgentFeatureFlagsFromSystemSettings(),
|
||||
applyOperation: applyCanvasOperationPlan,
|
||||
logger: console,
|
||||
});
|
||||
|
||||
@ -11,7 +11,29 @@ export type CanvasAgentFeatureFlags = {
|
||||
annotationEditEnabled: boolean;
|
||||
};
|
||||
|
||||
export const CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY =
|
||||
"canvas_agent_operation_protocol_enabled";
|
||||
export const CANVAS_AGENT_WRITEBACK_MODE_KEY = "canvas_agent_writeback_mode";
|
||||
export const CANVAS_AGENT_SLOT_ENABLED_KEY = "canvas_agent_slot_enabled";
|
||||
export const CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY =
|
||||
"canvas_agent_view_operation_enabled";
|
||||
export const CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY =
|
||||
"canvas_agent_annotation_edit_enabled";
|
||||
|
||||
export const CANVAS_AGENT_FEATURE_SETTING_KEYS = [
|
||||
CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY,
|
||||
CANVAS_AGENT_WRITEBACK_MODE_KEY,
|
||||
CANVAS_AGENT_SLOT_ENABLED_KEY,
|
||||
CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY,
|
||||
CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY,
|
||||
] as const;
|
||||
|
||||
type EnvLike = Record<string, string | undefined>;
|
||||
export type CanvasAgentFeatureSettingKey =
|
||||
(typeof CANVAS_AGENT_FEATURE_SETTING_KEYS)[number];
|
||||
export type CanvasAgentFeatureSettingsLike = Partial<
|
||||
Record<CanvasAgentFeatureSettingKey, unknown>
|
||||
>;
|
||||
|
||||
const WRITEBACK_MODES = new Set<CanvasAgentWritebackMode>([
|
||||
"legacy_canvas_media",
|
||||
@ -23,6 +45,8 @@ export function normalizeCanvasAgentBoolean(
|
||||
value: unknown,
|
||||
fallback: boolean
|
||||
) {
|
||||
if (typeof value === "boolean") return value;
|
||||
if (typeof value === "number" && Number.isFinite(value)) return value !== 0;
|
||||
if (typeof value !== "string") return fallback;
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (!normalized) return fallback;
|
||||
@ -41,10 +65,41 @@ export function normalizeCanvasAgentWritebackMode(
|
||||
: fallback;
|
||||
}
|
||||
|
||||
export function getCanvasAgentFeatureFlags(
|
||||
env: EnvLike = process.env
|
||||
export function mergeCanvasAgentFeatureFlagsWithSettings(
|
||||
flags: CanvasAgentFeatureFlags,
|
||||
settings?: CanvasAgentFeatureSettingsLike | null
|
||||
): CanvasAgentFeatureFlags {
|
||||
if (!settings) return flags;
|
||||
|
||||
return {
|
||||
operationProtocolEnabled: normalizeCanvasAgentBoolean(
|
||||
settings[CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY],
|
||||
flags.operationProtocolEnabled
|
||||
),
|
||||
writebackMode: normalizeCanvasAgentWritebackMode(
|
||||
settings[CANVAS_AGENT_WRITEBACK_MODE_KEY],
|
||||
flags.writebackMode
|
||||
),
|
||||
slotEnabled: normalizeCanvasAgentBoolean(
|
||||
settings[CANVAS_AGENT_SLOT_ENABLED_KEY],
|
||||
flags.slotEnabled
|
||||
),
|
||||
viewOperationEnabled: normalizeCanvasAgentBoolean(
|
||||
settings[CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY],
|
||||
flags.viewOperationEnabled
|
||||
),
|
||||
annotationEditEnabled: normalizeCanvasAgentBoolean(
|
||||
settings[CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY],
|
||||
flags.annotationEditEnabled
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
export function getCanvasAgentFeatureFlags(
|
||||
env: EnvLike = process.env,
|
||||
settings?: CanvasAgentFeatureSettingsLike | null
|
||||
): CanvasAgentFeatureFlags {
|
||||
const envFlags: CanvasAgentFeatureFlags = {
|
||||
operationProtocolEnabled: normalizeCanvasAgentBoolean(
|
||||
env.CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED,
|
||||
true
|
||||
@ -66,6 +121,7 @@ export function getCanvasAgentFeatureFlags(
|
||||
true
|
||||
),
|
||||
};
|
||||
return mergeCanvasAgentFeatureFlagsWithSettings(envFlags, settings);
|
||||
}
|
||||
|
||||
export function canApplyCanvasAgentMutations(flags: CanvasAgentFeatureFlags) {
|
||||
|
||||
44
lib/canvas/agent-feature-settings.ts
Normal file
44
lib/canvas/agent-feature-settings.ts
Normal file
@ -0,0 +1,44 @@
|
||||
import { inArray } from "drizzle-orm";
|
||||
import { db } from "@/lib/db";
|
||||
import { systemSettings } from "@/lib/db/schema";
|
||||
import {
|
||||
CANVAS_AGENT_FEATURE_SETTING_KEYS,
|
||||
type CanvasAgentFeatureFlags,
|
||||
type CanvasAgentFeatureSettingsLike,
|
||||
getCanvasAgentFeatureFlags,
|
||||
} from "@/lib/canvas/agent-feature-flags";
|
||||
|
||||
type EnvLike = Record<string, string | undefined>;
|
||||
|
||||
export async function readCanvasAgentFeatureSettings(): Promise<CanvasAgentFeatureSettingsLike> {
|
||||
const rows = await db
|
||||
.select({ key: systemSettings.key, value: systemSettings.value })
|
||||
.from(systemSettings)
|
||||
.where(inArray(systemSettings.key, [...CANVAS_AGENT_FEATURE_SETTING_KEYS]));
|
||||
|
||||
const settings: CanvasAgentFeatureSettingsLike = {};
|
||||
rows.forEach((row) => {
|
||||
if (
|
||||
CANVAS_AGENT_FEATURE_SETTING_KEYS.includes(
|
||||
row.key as (typeof CANVAS_AGENT_FEATURE_SETTING_KEYS)[number]
|
||||
)
|
||||
) {
|
||||
settings[row.key as keyof CanvasAgentFeatureSettingsLike] = row.value;
|
||||
}
|
||||
});
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function getCanvasAgentFeatureFlagsFromSystemSettings(
|
||||
env: EnvLike = process.env
|
||||
): Promise<CanvasAgentFeatureFlags> {
|
||||
try {
|
||||
return getCanvasAgentFeatureFlags(env, await readCanvasAgentFeatureSettings());
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"[CanvasAgentFeatureFlags] Failed to read SystemSettings, using env fallback:",
|
||||
error
|
||||
);
|
||||
return getCanvasAgentFeatureFlags(env);
|
||||
}
|
||||
}
|
||||
@ -129,6 +129,11 @@ import {
|
||||
findCanvasAgentOperationAudit,
|
||||
} from "@/lib/canvas/agent-operation-audit";
|
||||
import {
|
||||
CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY,
|
||||
CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY,
|
||||
CANVAS_AGENT_SLOT_ENABLED_KEY,
|
||||
CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY,
|
||||
CANVAS_AGENT_WRITEBACK_MODE_KEY,
|
||||
canApplyCanvasAgentMutations,
|
||||
getCanvasAgentFeatureFlags,
|
||||
normalizeCanvasAgentBoolean,
|
||||
@ -1805,6 +1810,37 @@ test.describe("canvas chat pure helpers", () => {
|
||||
});
|
||||
expect(canApplyCanvasAgentMutations(disabledProtocol)).toBe(false);
|
||||
expect(shouldStreamCanvasAgentShadowOperations(disabledProtocol)).toBe(false);
|
||||
|
||||
const settingsOverride = getCanvasAgentFeatureFlags(
|
||||
{
|
||||
CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED: "disabled",
|
||||
CANVAS_AGENT_WRITEBACK_MODE: "legacy_canvas_media",
|
||||
CANVAS_AGENT_SLOT_ENABLED: "0",
|
||||
},
|
||||
{
|
||||
[CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED_KEY]: true,
|
||||
[CANVAS_AGENT_WRITEBACK_MODE_KEY]: "operation_authoritative",
|
||||
[CANVAS_AGENT_SLOT_ENABLED_KEY]: true,
|
||||
[CANVAS_AGENT_VIEW_OPERATION_ENABLED_KEY]: false,
|
||||
[CANVAS_AGENT_ANNOTATION_EDIT_ENABLED_KEY]: "off",
|
||||
}
|
||||
);
|
||||
expect(settingsOverride).toMatchObject({
|
||||
operationProtocolEnabled: true,
|
||||
writebackMode: "operation_authoritative",
|
||||
slotEnabled: true,
|
||||
viewOperationEnabled: false,
|
||||
annotationEditEnabled: false,
|
||||
});
|
||||
expect(canApplyCanvasAgentMutations(settingsOverride)).toBe(true);
|
||||
|
||||
const invalidSettingsFallback = getCanvasAgentFeatureFlags(
|
||||
{ CANVAS_AGENT_WRITEBACK_MODE: "operation_shadow" },
|
||||
{
|
||||
[CANVAS_AGENT_WRITEBACK_MODE_KEY]: "not-a-mode",
|
||||
}
|
||||
);
|
||||
expect(invalidSettingsFallback.writebackMode).toBe("operation_shadow");
|
||||
});
|
||||
|
||||
test("builds idempotent canvas operation audit results", () => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user