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";
|
} from "@/lib/canvas/agent-context";
|
||||||
import {
|
import {
|
||||||
canApplyCanvasAgentMutations,
|
canApplyCanvasAgentMutations,
|
||||||
getCanvasAgentFeatureFlags,
|
|
||||||
shouldStreamCanvasAgentShadowOperations,
|
shouldStreamCanvasAgentShadowOperations,
|
||||||
} from "@/lib/canvas/agent-feature-flags";
|
} from "@/lib/canvas/agent-feature-flags";
|
||||||
|
import { getCanvasAgentFeatureFlagsFromSystemSettings } from "@/lib/canvas/agent-feature-settings";
|
||||||
import {
|
import {
|
||||||
buildPendingCanvasOperation,
|
buildPendingCanvasOperation,
|
||||||
extractPendingCanvasOperationFromWorkingState,
|
extractPendingCanvasOperationFromWorkingState,
|
||||||
@ -906,7 +906,7 @@ export async function POST(request: Request) {
|
|||||||
const initialRequestCanvasAgentContext = normalizeCanvasAgentContext(
|
const initialRequestCanvasAgentContext = normalizeCanvasAgentContext(
|
||||||
contextPayload || undefined
|
contextPayload || undefined
|
||||||
);
|
);
|
||||||
const canvasAgentFeatures = getCanvasAgentFeatureFlags();
|
const canvasAgentFeatures = await getCanvasAgentFeatureFlagsFromSystemSettings();
|
||||||
const canApplyCanvasAgentMutation =
|
const canApplyCanvasAgentMutation =
|
||||||
canApplyCanvasAgentMutations(canvasAgentFeatures);
|
canApplyCanvasAgentMutations(canvasAgentFeatures);
|
||||||
const currentMessageAttachmentCount = message.parts.filter(
|
const currentMessageAttachmentCount = message.parts.filter(
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
import { auth } from "@/app/(auth)/auth";
|
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 { applyCanvasOperationPlan } from "@/lib/canvas/agent-mutations";
|
||||||
import { handleCanvasAgentOperationRouteRequest } from "@/lib/canvas/agent-operation-route";
|
import { handleCanvasAgentOperationRouteRequest } from "@/lib/canvas/agent-operation-route";
|
||||||
|
|
||||||
@ -14,7 +14,7 @@ export async function POST(
|
|||||||
userId: session?.user?.id,
|
userId: session?.user?.id,
|
||||||
projectId: id,
|
projectId: id,
|
||||||
readBody: () => request.json(),
|
readBody: () => request.json(),
|
||||||
features: getCanvasAgentFeatureFlags(),
|
features: await getCanvasAgentFeatureFlagsFromSystemSettings(),
|
||||||
applyOperation: applyCanvasOperationPlan,
|
applyOperation: applyCanvasOperationPlan,
|
||||||
logger: console,
|
logger: console,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -11,7 +11,29 @@ export type CanvasAgentFeatureFlags = {
|
|||||||
annotationEditEnabled: boolean;
|
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>;
|
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>([
|
const WRITEBACK_MODES = new Set<CanvasAgentWritebackMode>([
|
||||||
"legacy_canvas_media",
|
"legacy_canvas_media",
|
||||||
@ -23,6 +45,8 @@ export function normalizeCanvasAgentBoolean(
|
|||||||
value: unknown,
|
value: unknown,
|
||||||
fallback: boolean
|
fallback: boolean
|
||||||
) {
|
) {
|
||||||
|
if (typeof value === "boolean") return value;
|
||||||
|
if (typeof value === "number" && Number.isFinite(value)) return value !== 0;
|
||||||
if (typeof value !== "string") return fallback;
|
if (typeof value !== "string") return fallback;
|
||||||
const normalized = value.trim().toLowerCase();
|
const normalized = value.trim().toLowerCase();
|
||||||
if (!normalized) return fallback;
|
if (!normalized) return fallback;
|
||||||
@ -41,10 +65,41 @@ export function normalizeCanvasAgentWritebackMode(
|
|||||||
: fallback;
|
: fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCanvasAgentFeatureFlags(
|
export function mergeCanvasAgentFeatureFlagsWithSettings(
|
||||||
env: EnvLike = process.env
|
flags: CanvasAgentFeatureFlags,
|
||||||
|
settings?: CanvasAgentFeatureSettingsLike | null
|
||||||
): CanvasAgentFeatureFlags {
|
): CanvasAgentFeatureFlags {
|
||||||
|
if (!settings) return flags;
|
||||||
|
|
||||||
return {
|
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(
|
operationProtocolEnabled: normalizeCanvasAgentBoolean(
|
||||||
env.CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED,
|
env.CANVAS_AGENT_OPERATION_PROTOCOL_ENABLED,
|
||||||
true
|
true
|
||||||
@ -66,6 +121,7 @@ export function getCanvasAgentFeatureFlags(
|
|||||||
true
|
true
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
return mergeCanvasAgentFeatureFlagsWithSettings(envFlags, settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function canApplyCanvasAgentMutations(flags: CanvasAgentFeatureFlags) {
|
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,
|
findCanvasAgentOperationAudit,
|
||||||
} from "@/lib/canvas/agent-operation-audit";
|
} from "@/lib/canvas/agent-operation-audit";
|
||||||
import {
|
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,
|
canApplyCanvasAgentMutations,
|
||||||
getCanvasAgentFeatureFlags,
|
getCanvasAgentFeatureFlags,
|
||||||
normalizeCanvasAgentBoolean,
|
normalizeCanvasAgentBoolean,
|
||||||
@ -1805,6 +1810,37 @@ test.describe("canvas chat pure helpers", () => {
|
|||||||
});
|
});
|
||||||
expect(canApplyCanvasAgentMutations(disabledProtocol)).toBe(false);
|
expect(canApplyCanvasAgentMutations(disabledProtocol)).toBe(false);
|
||||||
expect(shouldStreamCanvasAgentShadowOperations(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", () => {
|
test("builds idempotent canvas operation audit results", () => {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user