Agent versioning and promotion: code¶
Every file in the Agent versioning and promotion recipe. To run the recipe, download agent-versioning-and-promotion.zip, unzip it, and run the scripts from the agent-versioning-and-promotion/ folder as the recipe describes. Comments in the files that say “see README” refer to the recipe page; the zip includes it as README.md.
sql/¶
sql/01_setup.sql¶
-- =============================================================================
-- 01_setup.sql
-- Agent versioning and promotion: role, warehouse, database, and the agent.
--
-- The first block runs as ACCOUNTADMIN and only creates this recipe's own
-- COOKBOOK_ objects. Everything after USE ROLE IDENTIFIER($agentver_role) runs
-- as the recipe role, which owns the agent.
--
-- The agent has no tools on purpose. Each version's response instruction makes
-- it start every answer with a version tag ("[v1]", "[v2]", ...), so you can
-- see which version served a request without opening traces.
--
-- CREATE AGENT produces two versions with the same spec: a committed VERSION$1
-- and a mutable LIVE version. This file then pins DEFAULT to VERSION$1 and
-- points the PRODUCTION alias at it, so later commits do not change what
-- unversioned or production callers get.
--
-- Idempotent: CREATE ... IF NOT EXISTS never replaces the agent. On a rerun the
-- pin and alias statements re-point DEFAULT and PRODUCTION at VERSION$1, which
-- is the same as a rollback (see 05).
-- =============================================================================
SET agentver_db = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
SET agentver_agent = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT';
SET agentver_user = '"' || CURRENT_USER() || '"';
USE ROLE ACCOUNTADMIN;
CREATE ROLE IF NOT EXISTS IDENTIFIER($agentver_role)
COMMENT = 'Cookbook agent-versioning-and-promotion: owns the recipe objects.';
-- Granting the role to SYSADMIN lets ACCOUNTADMIN, which inherits SYSADMIN,
-- drop what the role owns in 99_teardown.sql without relying on secondary roles.
GRANT ROLE IDENTIFIER($agentver_role) TO ROLE SYSADMIN;
GRANT ROLE IDENTIFIER($agentver_role) TO USER IDENTIFIER($agentver_user);
CREATE WAREHOUSE IF NOT EXISTS IDENTIFIER($agentver_wh)
WAREHOUSE_SIZE = XSMALL AUTO_SUSPEND = 60 AUTO_RESUME = TRUE INITIALLY_SUSPENDED = TRUE
COMMENT = 'Cookbook agent-versioning-and-promotion';
GRANT USAGE ON WAREHOUSE IDENTIFIER($agentver_wh) TO ROLE IDENTIFIER($agentver_role);
CREATE DATABASE IF NOT EXISTS IDENTIFIER($agentver_db)
COMMENT = 'Cookbook agent-versioning-and-promotion';
GRANT OWNERSHIP ON DATABASE IDENTIFIER($agentver_db) TO ROLE IDENTIFIER($agentver_role) COPY CURRENT GRANTS;
-- Cortex Agents need SNOWFLAKE.CORTEX_USER or SNOWFLAKE.CORTEX_AGENT_USER.
GRANT DATABASE ROLE SNOWFLAKE.CORTEX_USER TO ROLE IDENTIFIER($agentver_role);
-- ---------------------------------------------------------------------------
-- From here on, run as the recipe role.
-- ---------------------------------------------------------------------------
USE ROLE IDENTIFIER($agentver_role);
USE WAREHOUSE IDENTIFIER($agentver_wh);
USE DATABASE IDENTIFIER($agentver_db);
CREATE SCHEMA IF NOT EXISTS AGENTS;
USE SCHEMA AGENTS;
CREATE AGENT IF NOT EXISTS RELEASE_AGENT
COMMENT = 'Cookbook: agent used to demonstrate versioning and promotion'
FROM SPECIFICATION
$$
models:
orchestration: auto
instructions:
response: |
Start every answer with the tag [v1] followed by a space.
Answer in one short sentence.
$$;
-- Pin DEFAULT to a named version. Without a pin, DEFAULT follows the newest
-- commit, so every COMMIT would silently change what unversioned callers get.
ALTER AGENT IDENTIFIER($agentver_agent) SET DEFAULT_VERSION = 'VERSION$1';
-- Applications call the PRODUCTION alias, never a version number.
-- Unquoted aliases are stored in uppercase: callers must use !PRODUCTION.
ALTER AGENT IDENTIFIER($agentver_agent) MODIFY VERSION VERSION$1 SET ALIAS = production;
SHOW VERSIONS IN AGENT IDENTIFIER($agentver_agent);
sql/02_commit_candidate.sql¶
-- =============================================================================
-- 02_commit_candidate.sql
-- Iterate on the LIVE version, commit it as a new named version, and point the
-- CANARY alias at that new version.
--
-- Behavior this file relies on:
-- * COMMIT consumes LIVE. A new LIVE version is not created automatically.
-- * ADD LIVE VERSION FROM LAST fails if a LIVE version already exists, so the
-- first block adds one only when it is missing.
-- * MODIFY LIVE VERSION SET SPECIFICATION replaces the whole spec.
-- * Because 01 pinned DEFAULT_VERSION, the commit does not change what
-- unversioned callers or PRODUCTION callers get.
-- * MODIFY VERSION does not accept shortcuts (MODIFY VERSION LAST is a syntax
-- error), so the new version name is read from DESCRIBE AGENT and checked
-- against ^VERSION\$[0-9]+$ before it is used in a statement.
--
-- Rerunnable: each run commits one more named version and moves CANARY to it.
-- =============================================================================
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_agent = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT';
USE ROLE IDENTIFIER($agentver_role);
USE WAREHOUSE IDENTIFIER($agentver_wh);
-- 1. Make sure there is a LIVE version to edit.
EXECUTE IMMEDIATE $$
BEGIN
SHOW VERSIONS IN AGENT IDENTIFIER($agentver_agent);
LET qid VARCHAR := SQLID;
LET live_count INTEGER := (SELECT COUNT(*) FROM TABLE(RESULT_SCAN(:qid))
WHERE "spec_file_path" ILIKE '%/versions/live/');
IF (live_count = 0) THEN
ALTER AGENT IDENTIFIER($agentver_agent) ADD LIVE VERSION FROM LAST
COMMENT = 'Next iteration';
RETURN 'Added a LIVE version from LAST';
END IF;
RETURN 'LIVE version already exists';
END;
$$;
-- 2. Edit LIVE. The whole spec is replaced.
ALTER AGENT IDENTIFIER($agentver_agent) MODIFY LIVE VERSION SET SPECIFICATION =
$$
models:
orchestration: auto
instructions:
response: |
Start every answer with the tag [v2] followed by a space.
Answer in one short sentence.
$$;
-- 3. Freeze LIVE into the next VERSION$N.
ALTER AGENT IDENTIFIER($agentver_agent) COMMIT COMMENT = 'v2 candidate: new response instructions';
-- 4. Point CANARY at the version just committed (LAST).
EXECUTE IMMEDIATE $$
BEGIN
DESCRIBE AGENT IDENTIFIER($agentver_agent);
LET qid VARCHAR := SQLID;
LET last_version VARCHAR := (SELECT TRY_PARSE_JSON("aliases"):LAST::STRING FROM TABLE(RESULT_SCAN(:qid)));
IF (NOT REGEXP_LIKE(last_version, '^VERSION\\$[0-9]+$')) THEN
RETURN 'Unexpected version name: ' || COALESCE(last_version, 'NULL');
END IF;
EXECUTE IMMEDIATE 'ALTER AGENT IDENTIFIER($agentver_agent) MODIFY VERSION '
|| last_version || ' SET ALIAS = canary';
RETURN 'CANARY now points at ' || last_version;
END;
$$;
-- 5. Resume development. The new LIVE starts from the version just committed.
ALTER AGENT IDENTIFIER($agentver_agent) ADD LIVE VERSION FROM LAST
COMMENT = 'Next iteration after the v2 candidate';
DESCRIBE AGENT IDENTIFIER($agentver_agent);
SELECT "default_version_name", "aliases" FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()));
sql/03_route_by_version.sql¶
-- =============================================================================
-- 03_route_by_version.sql
-- Send the same question to each version reference and read the version tag
-- in the answer. Shows how aliases and shortcuts resolve.
--
-- Reference forms for DATA_AGENT_RUN('<db>.<schema>.<agent>[!<version>]', ...):
-- !VERSION$N a named version (system identifier)
-- !<ALIAS> a user-defined alias; unquoted aliases are stored in uppercase,
-- so use !PRODUCTION, not !production (lowercase returns error
-- 399535 "Version not found")
-- !LIVE the mutable live version (shown as DRAFT in Snowsight)
-- !FIRST the first committed named version
-- !LAST the most recently created named version
-- !DEFAULT the default version; same as omitting the suffix
--
-- The agent argument must be a constant (a literal or a session variable).
-- A column expression fails to compile, so routing logic has to choose between
-- fixed references (see 04).
-- =============================================================================
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_q = '{"messages":[{"role":"user","content":[{"type":"text","text":"Say hello."}]}]}';
USE ROLE IDENTIFIER($agentver_role);
USE WAREHOUSE IDENTIFIER($agentver_wh);
WITH runs AS (
SELECT 'PRODUCTION' AS ref, TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!PRODUCTION', $agentver_q)) AS j
UNION ALL SELECT 'CANARY', TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!CANARY', $agentver_q))
UNION ALL SELECT 'DEFAULT', TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!DEFAULT', $agentver_q))
UNION ALL SELECT '(none)', TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT', $agentver_q))
UNION ALL SELECT 'FIRST', TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!FIRST', $agentver_q))
UNION ALL SELECT 'LAST', TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!LAST', $agentver_q))
UNION ALL SELECT 'LIVE', TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!LIVE', $agentver_q))
UNION ALL SELECT 'production (lowercase)', TRY_PARSE_JSON(SNOWFLAKE.CORTEX.DATA_AGENT_RUN('COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!production', $agentver_q))
)
SELECT r.ref,
REGEXP_SUBSTR(MAX(IFF(f.value:type::STRING = 'text', f.value:text::STRING, NULL)), '\\[v[0-9]+\\]') AS version_tag,
ANY_VALUE(r.j:message::STRING) AS error_message
FROM runs r, LATERAL FLATTEN(input => r.j:content, OUTER => TRUE) f
GROUP BY r.ref
ORDER BY r.ref;
sql/04_ab_routing.sql¶
-- =============================================================================
-- 04_ab_routing.sql
-- Application-layer A/B routing by alias.
--
-- Snowflake does not split traffic between agent versions. The caller decides
-- which alias serves a request. This procedure buckets a stable routing key
-- (for example a user id) into 0-99 and sends the request to CANARY when the
-- bucket is below CANARY_PERCENT, otherwise to PRODUCTION. The same key always
-- lands in the same bucket, so a user sees one version consistently.
--
-- Design notes:
-- * The agent argument of DATA_AGENT_RUN must be a constant, so the procedure
-- chooses between two literal references. That is also an allowlist: a
-- caller cannot route to any other agent or version.
-- * The request body is built with OBJECT_CONSTRUCT from the question, never
-- by concatenating text into JSON.
-- * EXECUTE AS CALLER: the agent runs with the caller's privileges.
-- * Every request is logged with the alias that served it, so feedback and
-- scores can be compared per version. The routing key is stored hashed.
-- =============================================================================
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_db = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION';
USE ROLE IDENTIFIER($agentver_role);
USE WAREHOUSE IDENTIFIER($agentver_wh);
USE DATABASE IDENTIFIER($agentver_db);
USE SCHEMA AGENTS;
CREATE TABLE IF NOT EXISTS AB_REQUEST_LOG (
LOGGED_AT TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP(),
ROUTING_KEY_HASH VARCHAR,
BUCKET NUMBER,
SERVED_ALIAS VARCHAR,
QUESTION VARCHAR,
ANSWER VARCHAR,
RESPONSE VARIANT
);
CREATE OR REPLACE PROCEDURE ASK_WITH_AB_ROUTING(
ROUTING_KEY VARCHAR, QUESTION VARCHAR, CANARY_PERCENT NUMBER)
RETURNS VARIANT
LANGUAGE SQL
EXECUTE AS CALLER
AS
$$
DECLARE
bucket NUMBER;
served_alias VARCHAR;
body VARCHAR;
raw VARCHAR;
resp VARIANT;
answer VARCHAR;
BEGIN
IF (CANARY_PERCENT IS NULL OR CANARY_PERCENT < 0 OR CANARY_PERCENT > 100) THEN
RETURN OBJECT_CONSTRUCT('error', 'CANARY_PERCENT must be between 0 and 100');
END IF;
bucket := MOD(ABS(HASH(:ROUTING_KEY)), 100);
body := TO_JSON(OBJECT_CONSTRUCT('messages', ARRAY_CONSTRUCT(OBJECT_CONSTRUCT(
'role', 'user',
'content', ARRAY_CONSTRUCT(OBJECT_CONSTRUCT('type', 'text', 'text', :QUESTION))))));
IF (bucket < CANARY_PERCENT) THEN
served_alias := 'CANARY';
raw := (SELECT SNOWFLAKE.CORTEX.DATA_AGENT_RUN(
'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!CANARY', :body));
ELSE
served_alias := 'PRODUCTION';
raw := (SELECT SNOWFLAKE.CORTEX.DATA_AGENT_RUN(
'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT!PRODUCTION', :body));
END IF;
resp := TRY_PARSE_JSON(:raw);
answer := (SELECT LISTAGG(f.value:text::STRING, '')
FROM TABLE(FLATTEN(input => :resp:content)) f
WHERE f.value:type::STRING = 'text');
INSERT INTO COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.AB_REQUEST_LOG
(ROUTING_KEY_HASH, BUCKET, SERVED_ALIAS, QUESTION, ANSWER, RESPONSE)
SELECT SHA2(:ROUTING_KEY, 256), :bucket, :served_alias, :QUESTION, :answer, :resp;
RETURN OBJECT_CONSTRUCT('served_alias', served_alias, 'bucket', bucket,
'answer', answer, 'error', resp:message::STRING);
END;
$$;
-- Send a few synthetic users through a 50/50 split.
CALL ASK_WITH_AB_ROUTING('user-001@example.com', 'Say hello.', 50);
CALL ASK_WITH_AB_ROUTING('user-002@example.com', 'Say hello.', 50);
CALL ASK_WITH_AB_ROUTING('user-003@example.com', 'Say hello.', 50);
CALL ASK_WITH_AB_ROUTING('user-001@example.com', 'Say hello again.', 50);
-- Which alias served which bucket, and which version tag came back.
SELECT BUCKET, SERVED_ALIAS,
REGEXP_SUBSTR(ANSWER, '\\[v[0-9]+\\]') AS VERSION_TAG,
LEFT(ROUTING_KEY_HASH, 12) AS KEY_HASH
FROM COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.AB_REQUEST_LOG
ORDER BY LOGGED_AT;
sql/05_promote.sql¶
-- =============================================================================
-- 05_promote.sql
-- Promote the CANARY version to PRODUCTION and make it the DEFAULT.
--
-- Promotion is metadata only: it moves the PRODUCTION alias and the DEFAULT pin.
-- Callers keep calling !PRODUCTION (or no suffix) and get the new version on
-- their next request.
--
-- Observed behavior this relies on:
-- * MODIFY VERSION VERSION$N SET ALIAS = x succeeds when another version
-- already holds alias x. The alias moves to VERSION$N and the previous
-- holder is left without it. No UNSET is needed first.
-- * A version holds at most one alias. Setting PRODUCTION on the version that
-- holds CANARY replaces CANARY, so after promotion there is no CANARY alias
-- until 02 commits the next candidate. Callers of !CANARY get error 399535
-- in the meantime; 04 routes by alias, so run it only while CANARY exists.
--
-- The previous PRODUCTION version is written to PROMOTION_LOG so 06 can roll
-- back to it without anyone having to remember a version number.
-- =============================================================================
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_db = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION';
SET agentver_agent = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT';
USE ROLE IDENTIFIER($agentver_role);
USE WAREHOUSE IDENTIFIER($agentver_wh);
USE DATABASE IDENTIFIER($agentver_db);
USE SCHEMA AGENTS;
CREATE TABLE IF NOT EXISTS PROMOTION_LOG (
PROMOTED_AT TIMESTAMP_LTZ DEFAULT CURRENT_TIMESTAMP(),
ACTION VARCHAR,
FROM_VERSION VARCHAR,
TO_VERSION VARCHAR,
PROMOTED_BY VARCHAR DEFAULT CURRENT_USER()
);
EXECUTE IMMEDIATE $$
BEGIN
DESCRIBE AGENT IDENTIFIER($agentver_agent);
LET qid VARCHAR := SQLID;
LET aliases VARIANT := (SELECT TRY_PARSE_JSON("aliases") FROM TABLE(RESULT_SCAN(:qid)));
LET candidate VARCHAR := aliases:CANARY::STRING;
LET current_prod VARCHAR := aliases:PRODUCTION::STRING;
IF (candidate IS NULL) THEN
RETURN 'No CANARY alias to promote. Run 02 first.';
END IF;
IF (NOT REGEXP_LIKE(candidate, '^VERSION\\$[0-9]+$')) THEN
RETURN 'Refusing to promote unexpected version name: ' || candidate;
END IF;
IF (candidate = current_prod) THEN
RETURN candidate || ' is already PRODUCTION';
END IF;
INSERT INTO PROMOTION_LOG (ACTION, FROM_VERSION, TO_VERSION)
VALUES ('promote', :current_prod, :candidate);
-- Move the alias, then the default pin. Both are metadata-only changes.
-- A version holds at most one alias, so this also removes CANARY from the
-- candidate, and the previous PRODUCTION version is left with no alias.
EXECUTE IMMEDIATE 'ALTER AGENT IDENTIFIER($agentver_agent) MODIFY VERSION '
|| candidate || ' SET ALIAS = production';
EXECUTE IMMEDIATE 'ALTER AGENT IDENTIFIER($agentver_agent) SET DEFAULT_VERSION = '''
|| candidate || '''';
RETURN 'Promoted ' || candidate || ' (previous PRODUCTION: ' || COALESCE(current_prod, 'none') || ')';
END;
$$;
DESCRIBE AGENT IDENTIFIER($agentver_agent);
SELECT "default_version_name", "aliases" FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()));
sql/06_rollback.sql¶
-- =============================================================================
-- 06_rollback.sql
-- Roll PRODUCTION and DEFAULT back to the version that was PRODUCTION before
-- the most recent promotion (read from PROMOTION_LOG, written by 05).
--
-- Rollback is the same metadata operation as promotion, pointed backwards.
-- Named versions are immutable, so the version you roll back to is exactly the
-- configuration that served production before.
-- =============================================================================
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_db = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION';
SET agentver_agent = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT';
USE ROLE IDENTIFIER($agentver_role);
USE WAREHOUSE IDENTIFIER($agentver_wh);
USE DATABASE IDENTIFIER($agentver_db);
USE SCHEMA AGENTS;
EXECUTE IMMEDIATE $$
BEGIN
LET target VARCHAR := (SELECT FROM_VERSION FROM PROMOTION_LOG
WHERE ACTION = 'promote'
ORDER BY PROMOTED_AT DESC LIMIT 1);
IF (target IS NULL) THEN
RETURN 'Nothing to roll back: no promotion recorded in PROMOTION_LOG.';
END IF;
IF (NOT REGEXP_LIKE(target, '^VERSION\\$[0-9]+$')) THEN
RETURN 'Refusing to roll back to unexpected version name: ' || target;
END IF;
DESCRIBE AGENT IDENTIFIER($agentver_agent);
LET qid VARCHAR := SQLID;
LET current_prod VARCHAR := (SELECT TRY_PARSE_JSON("aliases"):PRODUCTION::STRING FROM TABLE(RESULT_SCAN(:qid)));
IF (current_prod = target) THEN
RETURN target || ' is already PRODUCTION';
END IF;
EXECUTE IMMEDIATE 'ALTER AGENT IDENTIFIER($agentver_agent) MODIFY VERSION '
|| target || ' SET ALIAS = production';
EXECUTE IMMEDIATE 'ALTER AGENT IDENTIFIER($agentver_agent) SET DEFAULT_VERSION = '''
|| target || '''';
INSERT INTO PROMOTION_LOG (ACTION, FROM_VERSION, TO_VERSION)
VALUES ('rollback', :current_prod, :target);
RETURN 'Rolled PRODUCTION back from ' || COALESCE(current_prod, 'none') || ' to ' || target;
END;
$$;
DESCRIBE AGENT IDENTIFIER($agentver_agent);
SELECT "default_version_name", "aliases" FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()));
SELECT * FROM PROMOTION_LOG ORDER BY PROMOTED_AT;
sql/07_import_version_from_stage.sql¶
-- =============================================================================
-- 07_import_version_from_stage.sql
-- Create a named version from a spec file on a stage, bypassing LIVE.
--
-- This is the CI/CD path: the agent spec lives in Git, a pipeline puts the
-- merged file on a stage (or reads it from a Git repository stage), and
-- ADD VERSION FROM creates the next VERSION$N without editing LIVE. The
-- DEFAULT pin is not touched; you promote the imported version with 05 like
-- any other. ADD VERSION FROM requires that no LIVE version exists (see below).
--
-- The stage folder must contain agent_spec.yaml, the same layout Snowflake uses
-- for each version (LIST 'snow://agent/<agent>/versions/VERSION$1/').
--
-- To keep the recipe self-contained this file writes the spec with COPY INTO.
-- In a pipeline you would PUT the file from your repository instead, for
-- example: PUT file://agent/agent_spec.yaml @AGENT_SPECS/release_v3/
-- AUTO_COMPRESS = FALSE OVERWRITE = TRUE;
--
-- Rerunnable: each run imports one more named version and moves CANARY to it.
-- =============================================================================
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_db = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION';
SET agentver_agent = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION.AGENTS.RELEASE_AGENT';
USE ROLE IDENTIFIER($agentver_role);
USE WAREHOUSE IDENTIFIER($agentver_wh);
USE DATABASE IDENTIFIER($agentver_db);
USE SCHEMA AGENTS;
-- Agent specs are copied onto this stage by CI. Server-side encryption keeps the
-- files readable by Snowflake services.
CREATE STAGE IF NOT EXISTS AGENT_SPECS
ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE')
COMMENT = 'Agent spec files imported as named versions';
COPY INTO @AGENT_SPECS/release_v3/agent_spec.yaml
FROM (SELECT $$models:
orchestration: auto
instructions:
response: |
Start every answer with the tag [v3] followed by a space.
Answer in one short sentence.
$$)
FILE_FORMAT = (TYPE = CSV COMPRESSION = NONE FIELD_DELIMITER = NONE
FIELD_OPTIONALLY_ENCLOSED_BY = NONE ESCAPE_UNENCLOSED_FIELD = NONE)
SINGLE = TRUE HEADER = FALSE OVERWRITE = TRUE;
LIST @AGENT_SPECS/release_v3/;
-- Observed: ADD VERSION FROM fails with "There is already a live version.
-- Please commit it first." while a LIVE version exists, and LIVE cannot be
-- dropped. Agents that are deployed from CI should not keep a LIVE version.
-- Here 02 left one behind, so commit it as a snapshot before importing.
EXECUTE IMMEDIATE $$
BEGIN
SHOW VERSIONS IN AGENT IDENTIFIER($agentver_agent);
LET qid VARCHAR := SQLID;
LET live_count INTEGER := (SELECT COUNT(*) FROM TABLE(RESULT_SCAN(:qid))
WHERE "spec_file_path" ILIKE '%/versions/live/');
IF (live_count > 0) THEN
ALTER AGENT IDENTIFIER($agentver_agent) COMMIT
COMMENT = 'Snapshot of LIVE before importing a version from stage';
RETURN 'Committed the existing LIVE version';
END IF;
RETURN 'No LIVE version';
END;
$$;
ALTER AGENT IDENTIFIER($agentver_agent) ADD VERSION FROM @AGENT_SPECS/release_v3
COMMENT = 'v3: imported from stage';
-- Make the imported version the next canary.
EXECUTE IMMEDIATE $$
BEGIN
DESCRIBE AGENT IDENTIFIER($agentver_agent);
LET qid VARCHAR := SQLID;
LET last_version VARCHAR := (SELECT TRY_PARSE_JSON("aliases"):LAST::STRING FROM TABLE(RESULT_SCAN(:qid)));
IF (last_version IS NULL OR NOT REGEXP_LIKE(last_version, '^VERSION\\$[0-9]+$')) THEN
RETURN 'Unexpected version name: ' || COALESCE(last_version, 'NULL');
END IF;
EXECUTE IMMEDIATE 'ALTER AGENT IDENTIFIER($agentver_agent) MODIFY VERSION '
|| last_version || ' SET ALIAS = canary';
RETURN 'Imported ' || last_version || ' and pointed CANARY at it';
END;
$$;
SHOW VERSIONS IN AGENT IDENTIFIER($agentver_agent);
SELECT "name", "alias", "is_default", "comment" FROM TABLE(RESULT_SCAN(LAST_QUERY_ID()));
sql/99_teardown.sql¶
-- =============================================================================
-- 99_teardown.sql
-- Removes everything this recipe created. There are no account-level
-- parameter changes to restore.
-- =============================================================================
SET agentver_db = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION';
SET agentver_wh = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_WH';
SET agentver_role = 'COOKBOOK_AGENT_VERSIONING_AND_PROMOTION_ADMIN';
USE ROLE ACCOUNTADMIN;
-- Dropping the database drops the agent, every named version, the stage, and
-- the log tables.
DROP DATABASE IF EXISTS IDENTIFIER($agentver_db);
DROP WAREHOUSE IF EXISTS IDENTIFIER($agentver_wh);
DROP ROLE IF EXISTS IDENTIFIER($agentver_role);
SHOW DATABASES LIKE 'COOKBOOK_AGENT_VERSIONING%';
SHOW WAREHOUSES LIKE 'COOKBOOK_AGENT_VERSIONING%';
SHOW ROLES LIKE 'COOKBOOK_AGENT_VERSIONING%';