mirror of
https://github.com/Skyvern-AI/skyvern.git
synced 2026-04-28 03:30:10 +00:00
refactor: replace adaptive_caching with code_version, remove code_v2 (#5227)
Co-authored-by: Shuchang Zheng <wintonzheng0325@gmail.com>
This commit is contained in:
parent
a5a8172b8a
commit
4d04d8eb55
28 changed files with 213 additions and 172 deletions
|
|
@ -0,0 +1,31 @@
|
|||
"""code_version db migration
|
||||
|
||||
Revision ID: 787109c06571
|
||||
Revises: 01dbfbf87496
|
||||
Create Date: 2026-03-25 01:06:29.590429+00:00
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "787109c06571"
|
||||
down_revision: Union[str, None] = "01dbfbf87496"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.add_column("workflows", sa.Column("code_version", sa.Integer(), server_default=sa.text("2"), nullable=True))
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_column("workflows", "code_version")
|
||||
# ### end Alembic commands ###
|
||||
|
|
@ -190,7 +190,7 @@ type RunWorkflowRequestBody = {
|
|||
max_screenshot_scrolls?: number | null;
|
||||
extra_http_headers?: Record<string, string> | null;
|
||||
browser_address?: string | null;
|
||||
run_with?: "agent" | "code" | "code_v2";
|
||||
run_with?: "agent" | "code";
|
||||
ai_fallback?: boolean;
|
||||
};
|
||||
|
||||
|
|
@ -265,18 +265,17 @@ function transformToWorkflowRunRequest(
|
|||
return transformed;
|
||||
}
|
||||
|
||||
const VALID_RUN_WITH = new Set(["agent", "code", "code_v2"]);
|
||||
const VALID_RUN_WITH = new Set(["agent", "code"]);
|
||||
|
||||
function deriveRunWith(
|
||||
workflow?: WorkflowApiResponse,
|
||||
override?: string | null,
|
||||
): "agent" | "code" | "code_v2" {
|
||||
): "agent" | "code" {
|
||||
if (override && VALID_RUN_WITH.has(override))
|
||||
return override as "agent" | "code" | "code_v2";
|
||||
if (workflow?.run_with === "code_v2") return "code_v2";
|
||||
if (workflow?.adaptive_caching && workflow?.run_with === "code")
|
||||
return "code_v2";
|
||||
return override as "agent" | "code";
|
||||
if (workflow?.run_with === "agent") return "agent";
|
||||
if (workflow?.run_with === "code") return "code";
|
||||
if ((workflow?.code_version ?? 0) >= 1) return "code";
|
||||
return "agent";
|
||||
}
|
||||
|
||||
|
|
@ -287,7 +286,7 @@ type RunWorkflowFormType = Record<string, unknown> & {
|
|||
cdpAddress: string | null;
|
||||
maxScreenshotScrolls: number | null;
|
||||
extraHttpHeaders: string | null;
|
||||
runWith: "agent" | "code" | "code_v2";
|
||||
runWith: "agent" | "code";
|
||||
aiFallback: boolean | null;
|
||||
};
|
||||
|
||||
|
|
@ -394,15 +393,6 @@ function RunWorkflowForm({
|
|||
status: "published",
|
||||
});
|
||||
|
||||
const { data: blockScriptsV2 } = useBlockScriptsQuery({
|
||||
cacheKey,
|
||||
cacheKeyValue: cacheKeyValue ? `${cacheKeyValue}:v2` : "v2",
|
||||
workflowPermanentId,
|
||||
status: "published",
|
||||
});
|
||||
|
||||
const hasCodeV2 = Object.keys(blockScriptsV2 ?? {}).length > 0;
|
||||
|
||||
const [hasCode, setHasCode] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -569,25 +559,6 @@ function RunWorkflowForm({
|
|||
} satisfies ApiCommandOptions;
|
||||
}}
|
||||
/>
|
||||
{hasCodeV2 && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={
|
||||
runWorkflowMutation.isPending || hasLoginBlockValidationError
|
||||
}
|
||||
onClick={() => {
|
||||
form.setValue("runWith", "code_v2");
|
||||
form.handleSubmit(onSubmit, handleInvalid)();
|
||||
}}
|
||||
>
|
||||
<PlayIcon className="mr-2 h-4 w-4" />
|
||||
Run with Code 2.0
|
||||
<span className="ml-2 rounded bg-amber-500/20 px-1.5 py-0.5 text-xs font-semibold text-amber-400">
|
||||
Beta
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={
|
||||
|
|
@ -910,12 +881,6 @@ function RunWorkflowForm({
|
|||
generated).
|
||||
</span>
|
||||
),
|
||||
code_v2: (
|
||||
<span>
|
||||
Run this workflow with Code 2.0 (adaptive caching with
|
||||
self-healing scripts).
|
||||
</span>
|
||||
),
|
||||
};
|
||||
return (
|
||||
<FormItem>
|
||||
|
|
@ -942,7 +907,6 @@ function RunWorkflowForm({
|
|||
<SelectContent>
|
||||
<SelectItem value="agent">Skyvern Agent</SelectItem>
|
||||
<SelectItem value="code">Code</SelectItem>
|
||||
<SelectItem value="code_v2">Code 2.0</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
|
|||
title: "New Workflow",
|
||||
description: "",
|
||||
ai_fallback: true,
|
||||
adaptive_caching: true,
|
||||
code_version: 2,
|
||||
run_with: "code",
|
||||
workflow_definition: {
|
||||
version: 2,
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const emptyWorkflowRequest: WorkflowCreateYAMLRequest = {
|
|||
title: "New Workflow",
|
||||
description: "",
|
||||
ai_fallback: true,
|
||||
adaptive_caching: true,
|
||||
code_version: 2,
|
||||
run_with: "code",
|
||||
workflow_definition: {
|
||||
version: 2,
|
||||
|
|
|
|||
|
|
@ -124,10 +124,8 @@ function getWorkflowElements(version: WorkflowVersion) {
|
|||
extraHttpHeaders: version.extra_http_headers
|
||||
? JSON.stringify(version.extra_http_headers)
|
||||
: null,
|
||||
runWith:
|
||||
version.adaptive_caching && version.run_with === "code"
|
||||
? "code_v2"
|
||||
: version.run_with,
|
||||
runWith: version.run_with,
|
||||
codeVersion: version.code_version ?? null,
|
||||
scriptCacheKey: version.cache_key,
|
||||
aiFallback: version.ai_fallback ?? true,
|
||||
runSequentially: version.run_sequentially ?? false,
|
||||
|
|
|
|||
|
|
@ -451,13 +451,13 @@ export function WorkflowCopilotChat({
|
|||
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
|
||||
totp_verification_url: saveData.workflow.totp_verification_url,
|
||||
extra_http_headers: extraHttpHeaders,
|
||||
run_with:
|
||||
saveData.settings.runWith === "code_v2"
|
||||
? "code"
|
||||
: saveData.settings.runWith,
|
||||
run_with: saveData.settings.runWith,
|
||||
cache_key: normalizedKey,
|
||||
ai_fallback: saveData.settings.aiFallback ?? true,
|
||||
adaptive_caching: saveData.settings.runWith === "code_v2",
|
||||
code_version:
|
||||
saveData.settings.runWith === "code"
|
||||
? saveData.settings.codeVersion ?? 2
|
||||
: undefined,
|
||||
workflow_definition: {
|
||||
version: saveData.workflowDefinitionVersion,
|
||||
parameters: saveData.parameters,
|
||||
|
|
|
|||
|
|
@ -82,10 +82,8 @@ function Debugger() {
|
|||
extraHttpHeaders: workflow.extra_http_headers
|
||||
? JSON.stringify(workflow.extra_http_headers)
|
||||
: null,
|
||||
runWith:
|
||||
workflow.adaptive_caching && workflow.run_with === "code"
|
||||
? "code_v2"
|
||||
: workflow.run_with,
|
||||
runWith: workflow.run_with,
|
||||
codeVersion: workflow.code_version ?? null,
|
||||
scriptCacheKey: workflow.cache_key,
|
||||
aiFallback: workflow.ai_fallback ?? true,
|
||||
runSequentially: workflow.run_sequentially ?? false,
|
||||
|
|
|
|||
|
|
@ -74,10 +74,8 @@ function WorkflowEditor() {
|
|||
extraHttpHeaders: workflow.extra_http_headers
|
||||
? JSON.stringify(workflow.extra_http_headers)
|
||||
: null,
|
||||
runWith:
|
||||
workflow.adaptive_caching && workflow.run_with === "code"
|
||||
? "code_v2"
|
||||
: workflow.run_with ?? "agent",
|
||||
runWith: workflow.run_with ?? "agent",
|
||||
codeVersion: workflow.code_version ?? null,
|
||||
scriptCacheKey: workflow.cache_key,
|
||||
aiFallback: workflow.ai_fallback ?? true,
|
||||
runSequentially: workflow.run_sequentially ?? false,
|
||||
|
|
|
|||
|
|
@ -1131,10 +1131,8 @@ function Workspace({
|
|||
extraHttpHeaders: workflowData.extra_http_headers
|
||||
? JSON.stringify(workflowData.extra_http_headers)
|
||||
: null,
|
||||
runWith:
|
||||
workflowData.adaptive_caching && workflowData.run_with === "code"
|
||||
? "code_v2"
|
||||
: workflowData.run_with ?? "agent",
|
||||
runWith: workflowData.run_with ?? "agent",
|
||||
codeVersion: workflowData.code_version ?? null,
|
||||
scriptCacheKey: workflowData.cache_key ?? null,
|
||||
aiFallback: workflowData.ai_fallback ?? true,
|
||||
runSequentially: workflowData.run_sequentially ?? false,
|
||||
|
|
@ -1181,10 +1179,8 @@ function Workspace({
|
|||
extraHttpHeaders: selectedVersion.extra_http_headers
|
||||
? JSON.stringify(selectedVersion.extra_http_headers)
|
||||
: null,
|
||||
runWith:
|
||||
selectedVersion.adaptive_caching && selectedVersion.run_with === "code"
|
||||
? "code_v2"
|
||||
: selectedVersion.run_with ?? "agent",
|
||||
runWith: selectedVersion.run_with ?? "agent",
|
||||
codeVersion: selectedVersion.code_version ?? null,
|
||||
scriptCacheKey: selectedVersion.cache_key,
|
||||
aiFallback: selectedVersion.ai_fallback ?? true,
|
||||
runSequentially: selectedVersion.run_sequentially ?? false,
|
||||
|
|
@ -1979,13 +1975,14 @@ function Workspace({
|
|||
created_at: new Date().toISOString(),
|
||||
modified_at: new Date().toISOString(),
|
||||
deleted_at: null,
|
||||
run_with:
|
||||
saveData.settings.runWith === "code_v2"
|
||||
? "code"
|
||||
: saveData.settings.runWith,
|
||||
run_with: saveData.settings.runWith,
|
||||
cache_key: saveData.settings.scriptCacheKey,
|
||||
ai_fallback: saveData.settings.aiFallback,
|
||||
adaptive_caching: saveData.settings.runWith === "code_v2",
|
||||
adaptive_caching: false,
|
||||
code_version:
|
||||
saveData.settings.runWith === "code"
|
||||
? saveData.settings.codeVersion ?? 2
|
||||
: null,
|
||||
run_sequentially: saveData.settings.runSequentially,
|
||||
sequential_key: saveData.settings.sequentialKey,
|
||||
folder_id: null,
|
||||
|
|
|
|||
|
|
@ -307,12 +307,6 @@ function StartNode({ id, data, parentId }: NodeProps<StartNode>) {
|
|||
Skyvern Agent
|
||||
</SelectItem>
|
||||
<SelectItem value="code">Code</SelectItem>
|
||||
<SelectItem value="code_v2">
|
||||
<span>Code 2.0</span>{" "}
|
||||
<span className="text-xs italic text-yellow-400">
|
||||
new
|
||||
</span>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export type WorkflowStartNodeData = {
|
|||
extraHttpHeaders: string | Record<string, unknown> | null;
|
||||
editable: boolean;
|
||||
runWith: string | null;
|
||||
codeVersion: number | null;
|
||||
scriptCacheKey: string | null;
|
||||
aiFallback: boolean;
|
||||
runSequentially: boolean;
|
||||
|
|
|
|||
|
|
@ -156,10 +156,8 @@ function getWorkflowElements(version: WorkflowVersion) {
|
|||
extraHttpHeaders: version.extra_http_headers
|
||||
? JSON.stringify(version.extra_http_headers)
|
||||
: null,
|
||||
runWith:
|
||||
version.adaptive_caching && version.run_with === "code"
|
||||
? "code_v2"
|
||||
: version.run_with,
|
||||
runWith: version.run_with,
|
||||
codeVersion: version.code_version ?? null,
|
||||
scriptCacheKey: version.cache_key,
|
||||
aiFallback: version.ai_fallback ?? true,
|
||||
runSequentially: version.run_sequentially ?? false,
|
||||
|
|
|
|||
|
|
@ -1534,6 +1534,7 @@ function getElements(
|
|||
extraHttpHeaders: settings.extraHttpHeaders,
|
||||
editable,
|
||||
runWith: settings.runWith,
|
||||
codeVersion: settings.codeVersion,
|
||||
scriptCacheKey: settings.scriptCacheKey,
|
||||
aiFallback: settings.aiFallback ?? true,
|
||||
label: "__start_block__",
|
||||
|
|
@ -2779,7 +2780,8 @@ function getWorkflowSettings(nodes: Array<AppNode>): WorkflowSettings {
|
|||
model: null,
|
||||
maxScreenshotScrolls: null,
|
||||
extraHttpHeaders: null,
|
||||
runWith: "code_v2",
|
||||
runWith: "code",
|
||||
codeVersion: 2,
|
||||
scriptCacheKey: null,
|
||||
aiFallback: true,
|
||||
runSequentially: false,
|
||||
|
|
@ -2806,6 +2808,7 @@ function getWorkflowSettings(nodes: Array<AppNode>): WorkflowSettings {
|
|||
? JSON.stringify(data.extraHttpHeaders)
|
||||
: data.extraHttpHeaders,
|
||||
runWith: data.runWith,
|
||||
codeVersion: data.codeVersion,
|
||||
scriptCacheKey: data.scriptCacheKey,
|
||||
aiFallback: data.aiFallback,
|
||||
runSequentially: data.runSequentially,
|
||||
|
|
@ -4034,6 +4037,7 @@ function convert(workflow: WorkflowApiResponse): WorkflowCreateYAMLRequest {
|
|||
status: workflow.status,
|
||||
run_with: workflow.run_with,
|
||||
adaptive_caching: workflow.adaptive_caching ?? undefined,
|
||||
code_version: workflow.code_version ?? undefined,
|
||||
cache_key: workflow.cache_key,
|
||||
ai_fallback: workflow.ai_fallback ?? undefined,
|
||||
run_sequentially: workflow.run_sequentially ?? undefined,
|
||||
|
|
|
|||
|
|
@ -600,10 +600,11 @@ export type WorkflowApiResponse = {
|
|||
created_at: string;
|
||||
modified_at: string;
|
||||
deleted_at: string | null;
|
||||
run_with: string | null; // 'agent', 'code', or 'code_v2'
|
||||
run_with: string | null; // 'agent' or 'code'
|
||||
cache_key: string | null;
|
||||
ai_fallback: boolean | null;
|
||||
adaptive_caching: boolean | null;
|
||||
code_version: number | null;
|
||||
run_sequentially: boolean | null;
|
||||
sequential_key: string | null;
|
||||
folder_id: string | null;
|
||||
|
|
@ -617,7 +618,8 @@ export type WorkflowSettings = {
|
|||
model: WorkflowModel | null;
|
||||
maxScreenshotScrolls: number | null;
|
||||
extraHttpHeaders: string | null;
|
||||
runWith: string | null; // 'agent', 'code', or 'code_v2'
|
||||
runWith: string | null; // 'agent' or 'code'
|
||||
codeVersion: number | null;
|
||||
scriptCacheKey: string | null;
|
||||
aiFallback: boolean | null;
|
||||
runSequentially: boolean;
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export type WorkflowCreateYAMLRequest = {
|
|||
cache_key?: string | null;
|
||||
ai_fallback?: boolean;
|
||||
adaptive_caching?: boolean;
|
||||
code_version?: number | null;
|
||||
run_sequentially?: boolean;
|
||||
sequential_key?: string | null;
|
||||
folder_id?: string | null;
|
||||
|
|
|
|||
|
|
@ -77,9 +77,6 @@ function WorkflowRunCode(props?: Props) {
|
|||
workflowRunId: workflowRun?.workflow_run_id,
|
||||
});
|
||||
|
||||
const isAdaptiveCaching =
|
||||
workflow?.adaptive_caching && workflow?.run_with === "code";
|
||||
|
||||
useEffect(() => {
|
||||
const keys = Object.keys(blockScriptsPublished?.blocks ?? {});
|
||||
setHasPublishedCode(
|
||||
|
|
@ -127,15 +124,15 @@ function WorkflowRunCode(props?: Props) {
|
|||
? selectedVersionCode
|
||||
: activeScripts;
|
||||
|
||||
// For non-adaptive-caching, use block labels from the displayed version
|
||||
// Use block labels from the displayed version when viewing an older version
|
||||
// (older versions may have different blocks than the current run)
|
||||
const displayBlockLabels = isViewingOtherVersion
|
||||
? Object.keys(displayScripts?.blocks ?? {})
|
||||
: orderedBlockLabels;
|
||||
|
||||
// For adaptive caching, prefer the full main.py script over stitched blocks
|
||||
// Prefer the full main_script when available, fall back to stitched blocks
|
||||
const code = (
|
||||
isAdaptiveCaching && displayScripts?.main_script
|
||||
displayScripts?.main_script
|
||||
? displayScripts.main_script
|
||||
: getCode(displayBlockLabels, displayScripts?.blocks).join("")
|
||||
).trim();
|
||||
|
|
|
|||
|
|
@ -152,13 +152,13 @@ const useWorkflowSave = (opts?: WorkflowSaveOpts) => {
|
|||
max_screenshot_scrolls: saveData.settings.maxScreenshotScrolls,
|
||||
totp_verification_url: saveData.workflow.totp_verification_url,
|
||||
extra_http_headers: extraHttpHeaders,
|
||||
run_with:
|
||||
saveData.settings.runWith === "code_v2"
|
||||
? "code"
|
||||
: saveData.settings.runWith,
|
||||
run_with: saveData.settings.runWith,
|
||||
cache_key: normalizedKey,
|
||||
ai_fallback: saveData.settings.aiFallback ?? true,
|
||||
adaptive_caching: saveData.settings.runWith === "code_v2",
|
||||
code_version:
|
||||
saveData.settings.runWith === "code"
|
||||
? saveData.settings.codeVersion ?? 2
|
||||
: undefined,
|
||||
workflow_definition: {
|
||||
version: saveData.workflowDefinitionVersion,
|
||||
parameters: saveData.parameters,
|
||||
|
|
|
|||
|
|
@ -475,7 +475,7 @@ def _validate_definition_structure(json_def: WorkflowCreateYamlRequest | None, a
|
|||
|
||||
|
||||
_CODE_V2_DEFAULTS: dict[str, Any] = {
|
||||
"adaptive_caching": True,
|
||||
"code_version": 2,
|
||||
"run_with": "code",
|
||||
}
|
||||
_DEFAULT_MCP_PROXY_LOCATION = ProxyLocation.RESIDENTIAL
|
||||
|
|
@ -603,7 +603,7 @@ def _inject_missing_top_level_defaults(definition: str, fmt: str, defaults: dict
|
|||
|
||||
|
||||
def _inject_code_v2_defaults(definition: str, fmt: str) -> str:
|
||||
"""Inject Code 2.0 defaults into a JSON definition string when not explicitly set.
|
||||
"""Inject Code 2.0 defaults (code_version=2, run_with=code) when not explicitly set.
|
||||
|
||||
Only modifies JSON definitions (or auto-detected JSON). YAML is returned unchanged.
|
||||
"""
|
||||
|
|
@ -827,8 +827,8 @@ async def skyvern_workflow_create(
|
|||
"""Create a new Skyvern workflow from a YAML or JSON definition. Use when you need to save
|
||||
a new automation workflow that can be run repeatedly with different parameters.
|
||||
|
||||
By default, workflows created via MCP use Code 2.0 (adaptive caching with run_with="code").
|
||||
To disable this, explicitly set "adaptive_caching": false and/or "run_with": null in your definition.
|
||||
By default, workflows created via MCP use Code 2.0 (code_version=2, run_with="code").
|
||||
To disable this, explicitly set "code_version": 1 and/or "run_with": null in your definition.
|
||||
|
||||
Best practice: use one block per logical step with a short focused prompt (2-3 sentences).
|
||||
Use "navigation" blocks for actions (filling forms, clicking) and "extraction" blocks for pulling data.
|
||||
|
|
|
|||
|
|
@ -1939,6 +1939,7 @@ class AgentDB(BaseAlchemyDB):
|
|||
ai_fallback: bool = True,
|
||||
cache_key: str | None = None,
|
||||
adaptive_caching: bool = False,
|
||||
code_version: int | None = None,
|
||||
generate_script_on_terminal: bool = False,
|
||||
run_sequentially: bool = False,
|
||||
sequential_key: str | None = None,
|
||||
|
|
@ -1964,6 +1965,7 @@ class AgentDB(BaseAlchemyDB):
|
|||
ai_fallback=ai_fallback,
|
||||
cache_key=cache_key or DEFAULT_SCRIPT_RUN_ID,
|
||||
adaptive_caching=adaptive_caching,
|
||||
code_version=code_version,
|
||||
generate_script_on_terminal=generate_script_on_terminal,
|
||||
run_sequentially=run_sequentially,
|
||||
sequential_key=sequential_key,
|
||||
|
|
@ -7464,7 +7466,7 @@ class AgentDB(BaseAlchemyDB):
|
|||
base_filters = [
|
||||
WorkflowRunModel.workflow_run_id.in_(run_ids_subquery),
|
||||
WorkflowRunModel.organization_id == organization_id,
|
||||
WorkflowRunModel.run_with.in_(["code", "code_v2"]),
|
||||
WorkflowRunModel.run_with.in_(["code", "code_v2"]), # include legacy "code_v2" rows
|
||||
]
|
||||
|
||||
# Count statuses via GROUP BY (also gives us total_count)
|
||||
|
|
@ -7550,7 +7552,7 @@ class AgentDB(BaseAlchemyDB):
|
|||
WorkflowScriptModel.deleted_at.is_(None),
|
||||
WorkflowScriptModel.workflow_run_id.isnot(None),
|
||||
WorkflowRunModel.organization_id == organization_id,
|
||||
WorkflowRunModel.run_with.in_(["code", "code_v2"]),
|
||||
WorkflowRunModel.run_with.in_(["code", "code_v2"]), # include legacy "code_v2" rows
|
||||
)
|
||||
.group_by(WorkflowScriptModel.script_id, WorkflowRunModel.status)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -307,6 +307,7 @@ class WorkflowModel(Base):
|
|||
ai_fallback = Column(Boolean, default=True, nullable=False, server_default=sqlalchemy.true())
|
||||
cache_key = Column(String, nullable=True)
|
||||
adaptive_caching = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.false())
|
||||
code_version = Column(Integer, nullable=True, server_default=sqlalchemy.text("2"))
|
||||
generate_script_on_terminal = Column(Boolean, default=False, nullable=False, server_default=sqlalchemy.false())
|
||||
run_sequentially = Column(Boolean, nullable=True)
|
||||
sequential_key = Column(String, nullable=True)
|
||||
|
|
|
|||
|
|
@ -401,6 +401,7 @@ def convert_to_workflow(
|
|||
ai_fallback=workflow_model.ai_fallback,
|
||||
cache_key=workflow_model.cache_key,
|
||||
adaptive_caching=workflow_model.adaptive_caching,
|
||||
code_version=workflow_model.code_version,
|
||||
generate_script_on_terminal=workflow_model.generate_script_on_terminal,
|
||||
run_sequentially=workflow_model.run_sequentially,
|
||||
sequential_key=workflow_model.sequential_key,
|
||||
|
|
|
|||
|
|
@ -713,7 +713,7 @@ async def get_workflow_script_blocks(
|
|||
if not workflow:
|
||||
raise HTTPException(status_code=404, detail="Workflow not found")
|
||||
|
||||
include_main_script = bool(workflow.adaptive_caching)
|
||||
include_main_script = True
|
||||
workflow_run_id = block_script_request.workflow_run_id
|
||||
if workflow_run_id:
|
||||
workflow_run = await app.DATABASE.get_workflow_run(
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ class Workflow(BaseModel):
|
|||
ai_fallback: bool = True
|
||||
cache_key: str | None = None
|
||||
adaptive_caching: bool = False
|
||||
code_version: int | None = None
|
||||
generate_script_on_terminal: bool = False
|
||||
run_sequentially: bool | None = None
|
||||
sequential_key: str | None = None
|
||||
|
|
@ -188,11 +189,25 @@ class WorkflowRun(BaseModel):
|
|||
|
||||
|
||||
def is_adaptive_caching(workflow: Workflow, workflow_run: WorkflowRun) -> bool:
|
||||
"""Compute effective adaptive caching mode from run-level override or workflow setting."""
|
||||
if workflow_run.run_with == "code_v2":
|
||||
return True
|
||||
if workflow_run.run_with in ("code", "agent"):
|
||||
"""Compute effective adaptive caching mode from run-level override or workflow setting.
|
||||
|
||||
Uses code_version >= 2 as the primary check. Falls back to the legacy
|
||||
adaptive_caching bool for rows that haven't been backfilled yet
|
||||
(code_version is None).
|
||||
"""
|
||||
run_with = workflow_run.run_with or workflow.run_with
|
||||
# Explicit agent mode → never adaptive
|
||||
if run_with == "agent":
|
||||
return False
|
||||
# When run_with is "code" (or legacy "code_v2"), check code_version
|
||||
if run_with in ("code", "code_v2"):
|
||||
if workflow.code_version is not None:
|
||||
return workflow.code_version >= 2
|
||||
return workflow.adaptive_caching
|
||||
# run_with is None — check code_version as the implicit fallback
|
||||
# (workflows with code_version >= 2 implicitly run as code)
|
||||
if workflow.code_version is not None:
|
||||
return workflow.code_version >= 2
|
||||
return workflow.adaptive_caching
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -615,16 +615,19 @@ class WorkflowService:
|
|||
if workflow_request.extra_http_headers is None and workflow.extra_http_headers is not None:
|
||||
workflow_request.extra_http_headers = workflow.extra_http_headers
|
||||
|
||||
# Force ai_fallback=True for adaptive caching (code_v2) runs.
|
||||
# Force ai_fallback=True for adaptive caching (code_version >= 2) runs.
|
||||
# Adaptive caching requires AI fallback to self-heal when cached scripts break.
|
||||
# Without this, a caller sending ai_fallback=false would silently disable recovery.
|
||||
if workflow_request.run_with == "code_v2" or (workflow_request.run_with is None and workflow.adaptive_caching):
|
||||
effective_code_version = (
|
||||
workflow.code_version if workflow.code_version is not None else (2 if workflow.adaptive_caching else None)
|
||||
)
|
||||
if (effective_code_version or 0) >= 2 and (workflow_request.run_with in ("code", None)):
|
||||
if workflow_request.ai_fallback is False:
|
||||
LOG.info(
|
||||
"Overriding ai_fallback to True for adaptive caching run",
|
||||
workflow_permanent_id=workflow_permanent_id,
|
||||
request_run_with=workflow_request.run_with,
|
||||
workflow_adaptive_caching=workflow.adaptive_caching,
|
||||
workflow_code_version=workflow.code_version,
|
||||
)
|
||||
workflow_request.ai_fallback = True
|
||||
|
||||
|
|
@ -1307,7 +1310,7 @@ class WorkflowService:
|
|||
LOG.error("Failed to load static script", exc_info=True)
|
||||
|
||||
# Mark workflow as running, preserving the user's original run_with intent.
|
||||
# The run_with field records what the user requested (e.g. "code_v2"),
|
||||
# The run_with field records what the user requested (e.g. "code"),
|
||||
# not whether a script was actually found. Execution mode is determined
|
||||
# separately by is_script_run and script_mode below.
|
||||
await self.mark_workflow_run_as_running(workflow_run_id=workflow_run_id, run_with=workflow_run.run_with)
|
||||
|
|
@ -2209,7 +2212,7 @@ class WorkflowService:
|
|||
and block.label not in script_blocks_by_label
|
||||
and workflow_run_block_result.status in cacheable_statuses
|
||||
and block.block_type in BLOCK_TYPES_THAT_SHOULD_BE_CACHED
|
||||
# For traditional caching (adaptive_caching=False), only track blocks
|
||||
# For traditional caching (code_version < 2), only track blocks
|
||||
# for regeneration when actually running with code. Agent-mode runs
|
||||
# should not trigger regeneration — doing so creates an infinite loop
|
||||
# where every run deletes and regenerates the script because blocks
|
||||
|
|
@ -2687,6 +2690,7 @@ class WorkflowService:
|
|||
sequential_key: str | None = None,
|
||||
folder_id: str | None = None,
|
||||
adaptive_caching: bool = False,
|
||||
code_version: int | None = None,
|
||||
generate_script_on_terminal: bool = False,
|
||||
) -> Workflow:
|
||||
try:
|
||||
|
|
@ -2714,6 +2718,7 @@ class WorkflowService:
|
|||
sequential_key=sequential_key,
|
||||
folder_id=folder_id,
|
||||
adaptive_caching=adaptive_caching,
|
||||
code_version=code_version,
|
||||
generate_script_on_terminal=generate_script_on_terminal,
|
||||
)
|
||||
except IntegrityError as e:
|
||||
|
|
@ -4403,6 +4408,7 @@ class WorkflowService:
|
|||
sequential_key=request.sequential_key,
|
||||
folder_id=existing_latest_workflow.folder_id,
|
||||
adaptive_caching=request.adaptive_caching,
|
||||
code_version=request.code_version,
|
||||
generate_script_on_terminal=request.generate_script_on_terminal,
|
||||
)
|
||||
else:
|
||||
|
|
@ -4429,6 +4435,7 @@ class WorkflowService:
|
|||
sequential_key=request.sequential_key,
|
||||
folder_id=request.folder_id,
|
||||
adaptive_caching=request.adaptive_caching,
|
||||
code_version=request.code_version,
|
||||
generate_script_on_terminal=request.generate_script_on_terminal,
|
||||
)
|
||||
# Keeping track of the new workflow id to delete it if an error occurs during the creation process
|
||||
|
|
@ -5200,20 +5207,22 @@ class WorkflowService:
|
|||
) -> bool:
|
||||
"""Determine whether this run should attempt to execute cached scripts.
|
||||
|
||||
Priority: run-level run_with > workflow-level run_with > adaptive_caching fallback > default (agent).
|
||||
When adaptive_caching is enabled at the workflow level and no explicit run_with
|
||||
is set, the run defaults to code mode so cached scripts are actually used.
|
||||
Priority: run-level run_with > workflow-level run_with > code_version fallback > default (agent).
|
||||
When code_version >= 1 at the workflow level and no explicit run_with is set,
|
||||
the run defaults to code mode so cached scripts are actually used.
|
||||
"""
|
||||
if workflow_run.run_with in ("code", "code_v2"):
|
||||
return True
|
||||
if workflow_run.run_with == "agent":
|
||||
return False
|
||||
if workflow.run_with in ("code", "code_v2"):
|
||||
if workflow.run_with in ("code", "code_v2"): # include legacy "code_v2" workflows
|
||||
return True
|
||||
if workflow.run_with == "agent":
|
||||
return False
|
||||
# No explicit run_with on either workflow or run — fall back to
|
||||
# adaptive_caching: if the workflow has caching enabled, run code.
|
||||
# code_version / adaptive_caching: if the workflow has caching enabled, run code.
|
||||
if workflow.code_version is not None:
|
||||
return workflow.code_version >= 1
|
||||
if workflow.adaptive_caching:
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -521,10 +521,18 @@ class WorkflowRunRequest(BaseModel):
|
|||
)
|
||||
run_with: str | None = Field(
|
||||
default=None,
|
||||
description="Whether to run the workflow with agent, code, or code_v2 (adaptive caching).",
|
||||
examples=["agent", "code", "code_v2"],
|
||||
description="Whether to run the workflow with agent or code.",
|
||||
examples=["agent", "code"],
|
||||
)
|
||||
|
||||
@field_validator("run_with", mode="before")
|
||||
@classmethod
|
||||
def normalize_run_with(cls, v: str | None) -> str | None:
|
||||
"""Normalize legacy 'code_v2' to 'code'."""
|
||||
if v == "code_v2":
|
||||
return "code"
|
||||
return v
|
||||
|
||||
@field_validator("webhook_url", "totp_url")
|
||||
@classmethod
|
||||
def validate_urls(cls, url: str | None) -> str | None:
|
||||
|
|
@ -634,9 +642,18 @@ class WorkflowRunResponse(BaseRunResponse):
|
|||
run_type: Literal[RunType.workflow_run] = Field(description="Type of run - always workflow_run for workflow runs")
|
||||
run_with: str | None = Field(
|
||||
default=None,
|
||||
description="Whether the workflow run was executed with agent, code, or code_v2 (adaptive caching)",
|
||||
examples=["agent", "code", "code_v2"],
|
||||
description="Whether the workflow run was executed with agent or code",
|
||||
examples=["agent", "code"],
|
||||
)
|
||||
|
||||
@field_validator("run_with", mode="before")
|
||||
@classmethod
|
||||
def normalize_run_with(cls, v: str | None) -> str | None:
|
||||
"""Normalize legacy 'code_v2' to 'code' in API responses."""
|
||||
if v == "code_v2":
|
||||
return "code"
|
||||
return v
|
||||
|
||||
ai_fallback: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to fallback to AI if code run fails.",
|
||||
|
|
|
|||
|
|
@ -1078,6 +1078,7 @@ class WorkflowCreateYAMLRequest(BaseModel):
|
|||
ai_fallback: bool = True
|
||||
cache_key: str | None = "default"
|
||||
adaptive_caching: bool = False
|
||||
code_version: int | None = Field(default=None, ge=1, le=2)
|
||||
generate_script_on_terminal: bool = False
|
||||
run_sequentially: bool = False
|
||||
sequential_key: str | None = None
|
||||
|
|
|
|||
|
|
@ -34,11 +34,11 @@ def _minimal_workflow_json(**overrides: object) -> str:
|
|||
|
||||
|
||||
def test_defaults_injected_when_not_specified() -> None:
|
||||
"""When adaptive_caching and run_with are omitted, _inject_code_v2_defaults adds them."""
|
||||
"""When code_version and run_with are omitted, _inject_code_v2_defaults adds them."""
|
||||
definition = _minimal_workflow_json()
|
||||
result = _inject_code_v2_defaults(definition, "json")
|
||||
parsed = json.loads(result)
|
||||
assert parsed["adaptive_caching"] is True
|
||||
assert parsed["code_version"] == 2
|
||||
assert parsed["run_with"] == "code"
|
||||
|
||||
|
||||
|
|
@ -47,16 +47,16 @@ def test_defaults_injected_in_auto_mode() -> None:
|
|||
definition = _minimal_workflow_json()
|
||||
result = _inject_code_v2_defaults(definition, "auto")
|
||||
parsed = json.loads(result)
|
||||
assert parsed["adaptive_caching"] is True
|
||||
assert parsed["code_version"] == 2
|
||||
assert parsed["run_with"] == "code"
|
||||
|
||||
|
||||
def test_explicit_values_preserved() -> None:
|
||||
"""When the user explicitly sets these fields, their values are preserved."""
|
||||
definition = _minimal_workflow_json(adaptive_caching=False, run_with="agent")
|
||||
definition = _minimal_workflow_json(code_version=1, run_with="agent")
|
||||
result = _inject_code_v2_defaults(definition, "json")
|
||||
parsed = json.loads(result)
|
||||
assert parsed["adaptive_caching"] is False
|
||||
assert parsed["code_version"] == 1
|
||||
assert parsed["run_with"] == "agent"
|
||||
|
||||
|
||||
|
|
@ -66,8 +66,8 @@ def test_explicit_null_run_with_preserved() -> None:
|
|||
result = _inject_code_v2_defaults(definition, "json")
|
||||
parsed = json.loads(result)
|
||||
assert parsed["run_with"] is None
|
||||
# adaptive_caching was not set, so it gets the default
|
||||
assert parsed["adaptive_caching"] is True
|
||||
# code_version was not set, so it gets the default
|
||||
assert parsed["code_version"] == 2
|
||||
|
||||
|
||||
def test_proxy_default_injected_when_not_specified_json() -> None:
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
"""Tests for WorkflowService.should_run_script() and script reviewer gating.
|
||||
|
||||
Verifies the priority chain:
|
||||
run-level run_with > workflow-level run_with > adaptive_caching fallback > default (agent).
|
||||
When adaptive_caching=True and no explicit run_with is set, the run defaults to code mode.
|
||||
run-level run_with > workflow-level run_with > code_version fallback > default (agent).
|
||||
When code_version >= 1 and no explicit run_with is set, the run defaults to code mode.
|
||||
|
||||
Also verifies that the script reviewer only fires when the script was actually
|
||||
executed (should_run_script=True), not merely when adaptive caching is enabled.
|
||||
|
|
@ -20,7 +20,11 @@ from skyvern.forge.sdk.workflow.models.workflow import (
|
|||
)
|
||||
|
||||
|
||||
def _make_workflow(run_with: str | None = None, adaptive_caching: bool = False) -> Workflow:
|
||||
def _make_workflow(
|
||||
run_with: str | None = None,
|
||||
adaptive_caching: bool = False,
|
||||
code_version: int | None = None,
|
||||
) -> Workflow:
|
||||
return Workflow(
|
||||
workflow_id="wf_test",
|
||||
organization_id="org_test",
|
||||
|
|
@ -31,6 +35,7 @@ def _make_workflow(run_with: str | None = None, adaptive_caching: bool = False)
|
|||
workflow_definition={"parameters": [], "blocks": []},
|
||||
run_with=run_with,
|
||||
adaptive_caching=adaptive_caching,
|
||||
code_version=code_version,
|
||||
created_at=datetime.now(timezone.utc),
|
||||
modified_at=datetime.now(timezone.utc),
|
||||
)
|
||||
|
|
@ -57,26 +62,21 @@ def service():
|
|||
|
||||
|
||||
class TestShouldRunScript:
|
||||
"""Run-level run_with takes priority, then workflow-level, then adaptive_caching fallback, then agent."""
|
||||
"""Run-level run_with takes priority, then workflow-level, then code_version fallback, then agent."""
|
||||
|
||||
def test_run_code_overrides_workflow_agent(self, service):
|
||||
wf = _make_workflow(run_with="agent")
|
||||
wr = _make_run(run_with="code")
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
def test_run_code_v2_overrides_workflow_agent(self, service):
|
||||
wf = _make_workflow(run_with="agent")
|
||||
wr = _make_run(run_with="code_v2")
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
def test_run_agent_overrides_workflow_code(self, service):
|
||||
wf = _make_workflow(run_with="code")
|
||||
wr = _make_run(run_with="agent")
|
||||
assert service.should_run_script(wf, wr) is False
|
||||
|
||||
def test_run_agent_overrides_adaptive_caching(self, service):
|
||||
"""Explicit run-level agent overrides adaptive_caching fallback."""
|
||||
wf = _make_workflow(run_with=None, adaptive_caching=True)
|
||||
def test_run_agent_overrides_code_version(self, service):
|
||||
"""Explicit run-level agent overrides code_version fallback."""
|
||||
wf = _make_workflow(run_with=None, code_version=2)
|
||||
wr = _make_run(run_with="agent")
|
||||
assert service.should_run_script(wf, wr) is False
|
||||
|
||||
|
|
@ -90,36 +90,42 @@ class TestShouldRunScript:
|
|||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is False
|
||||
|
||||
def test_workflow_agent_overrides_adaptive_caching(self, service):
|
||||
"""Explicit workflow-level agent takes priority over adaptive_caching."""
|
||||
wf = _make_workflow(run_with="agent", adaptive_caching=True)
|
||||
def test_workflow_agent_overrides_code_version(self, service):
|
||||
"""Explicit workflow-level agent takes priority over code_version."""
|
||||
wf = _make_workflow(run_with="agent", code_version=2)
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is False
|
||||
|
||||
def test_both_null_defaults_to_agent(self, service):
|
||||
"""When neither workflow nor run specifies run_with and no adaptive_caching, default to agent."""
|
||||
"""When neither workflow nor run specifies run_with and no code_version, default to agent."""
|
||||
wf = _make_workflow(run_with=None)
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is False
|
||||
|
||||
def test_adaptive_caching_defaults_to_code(self, service):
|
||||
"""adaptive_caching=True with run_with=null should default to code mode."""
|
||||
def test_code_version_defaults_to_code(self, service):
|
||||
"""code_version >= 1 with run_with=null should default to code mode."""
|
||||
wf = _make_workflow(run_with=None, code_version=2)
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
def test_code_version_1_defaults_to_code(self, service):
|
||||
"""code_version=1 with run_with=null should also run code."""
|
||||
wf = _make_workflow(run_with=None, code_version=1)
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
def test_code_version_with_workflow_code_runs_code(self, service):
|
||||
"""code_version=2 with run_with=code should run code."""
|
||||
wf = _make_workflow(run_with="code", code_version=2)
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
def test_legacy_adaptive_caching_fallback(self, service):
|
||||
"""Legacy adaptive_caching=True with no code_version should still default to code."""
|
||||
wf = _make_workflow(run_with=None, adaptive_caching=True)
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
def test_adaptive_caching_with_workflow_code_runs_code(self, service):
|
||||
"""adaptive_caching=True with run_with=code should run code (v1→v2 upgrade is separate)."""
|
||||
wf = _make_workflow(run_with="code", adaptive_caching=True)
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
def test_workflow_code_v2_with_null_run(self, service):
|
||||
"""workflow.run_with='code_v2' should be treated the same as 'code'."""
|
||||
wf = _make_workflow(run_with="code_v2")
|
||||
wr = _make_run(run_with=None)
|
||||
assert service.should_run_script(wf, wr) is True
|
||||
|
||||
|
||||
class TestScriptReviewerGate:
|
||||
"""The script reviewer should only fire when the script was actually executed.
|
||||
|
|
@ -129,20 +135,26 @@ class TestScriptReviewerGate:
|
|||
"""
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"run_with,adaptive_caching,expect_reviewer",
|
||||
"run_with,code_version,expect_reviewer",
|
||||
[
|
||||
("code_v2", True, True),
|
||||
("code_v2", False, True), # is_adaptive_caching=True for code_v2 regardless
|
||||
(None, True, True), # adaptive_caching=True defaults to code mode now
|
||||
(None, False, False),
|
||||
("agent", True, False),
|
||||
("agent", False, False),
|
||||
("code", True, False), # code without code_v2 → is_adaptive_caching=False
|
||||
("code", False, False),
|
||||
("code", 2, True), # code + code_version=2 → adaptive caching on
|
||||
("code", 1, False), # code + code_version=1 → adaptive caching off
|
||||
(None, 2, True), # code_version=2 defaults to code mode, adaptive
|
||||
(None, None, False), # no code_version, no run_with → agent
|
||||
("agent", 2, False), # agent overrides everything
|
||||
("agent", None, False),
|
||||
("code", None, False), # code without code_version → not adaptive
|
||||
],
|
||||
)
|
||||
def test_reviewer_gate(self, service, run_with, adaptive_caching, expect_reviewer):
|
||||
wf = _make_workflow(run_with=None, adaptive_caching=adaptive_caching)
|
||||
def test_reviewer_gate(self, service, run_with, code_version, expect_reviewer):
|
||||
wf = _make_workflow(run_with=None, code_version=code_version)
|
||||
wr = _make_run(run_with=run_with)
|
||||
should_review = is_adaptive_caching(wf, wr) and service.should_run_script(wf, wr)
|
||||
assert should_review is expect_reviewer
|
||||
|
||||
def test_legacy_adaptive_caching_backward_compat(self, service):
|
||||
"""Legacy adaptive_caching=True with code_version=None still enables reviewer."""
|
||||
wf = _make_workflow(run_with=None, adaptive_caching=True, code_version=None)
|
||||
wr = _make_run(run_with="code")
|
||||
should_review = is_adaptive_caching(wf, wr) and service.should_run_script(wf, wr)
|
||||
assert should_review is True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue