I've open-sourced Three-Live: a sandboxed Three.js runtime that compiles natively to Android Filament and iOS RealityKit (and also works in WebXR)
I recently open-sourced Three-Live (three-live and xr-live on npm, three-live-compiler on crates.io) and wanted to share it with the Three.js community.
- GitHub: google/three-live
- Live playgrounds: google.github.io
Why I built it
Three.js is a great way to author 3D scenes, materials and animations in just a few lines of JavaScript. But what happens when you want to add that 3D to a native Android or iOS app?
- Mobile AR & WebViews: iOS Safara, WKWebView and Android WebView don't support WebXR... Exporting a static usdz or glb file to AR Quick Look and Scene Viewer drops onUpdate loops, interactivity and any custom plugins.
- Mobile and headset apps: To deliver Three.js usually means rewriting the scene in Kotlin (Filament / Jetpack XR SceneCore) and Swift (RealityKit), or embedding a WebView that can't render into the headset's shared space.
- Sandboxing: If a scene script comes from an LLM or user input, running it on the main thread isn't safe.
There-Live is my attempt at a solution to these challenges.
How it works
Three-Live lets you write standard Three.js code and pass it to native mobile SDKs, or run in a web app:
- On Android: Uses an in-process Rust/Boa compiler (three-live-compiler) over zero-copy JNI to convert Three.js scripts into binary GLB + KHR_interactivity graphs, driving Google Filament, ARCore and Jetpack XR SceneCore
- On iOS, macOS and visionOS: Compiles the same script into Apply RealityKit Entity hierarchies (RealityKitHelper), ARView / RealityView and Metal.
- On Web: Evaluates the same script inside a sandboxed Web Worker or opaque-origin iframe (connect-src 'none', with CPU time and geometry quotas), streams the validated scene graph to a lightweight WebGL2/WebXR renderer, and includes in-memory glb and usdz pacakgers for one-tap mobile AR handoff.
Quick examples
Android
import com.google.threelive.FilamentHelper
import com.google.threelive.XRLiveRuntimeSession
import com.google.threelive.XRLiveSceneCoreHelper
// Compile Three.js to a zero-copy GLB buffer and load in Filament
val holder = FilamentHelper.loadModelZeroCopy(script) { buffer ->
assetLoader.createAssetFromBinary(buffer)
}
// Drive KHR interactivity graphs, head-follow springs and spatial/sim plugins per frame
val runtime = XRLiveRuntimeSession.fromGlb(holder.byteBuffer)
runtime.step(dtSeconds = 1.0 / 60.0)
// Or attach interactive spatial traits (grab, scale, billboard, anchor) in XR SceneCore
XRLiveSceneCoreHelper.attachTraitsFromGlb(xrSession, entity, holder.byteBuffer)
Module plugin architecture
To keep the core bundles small and dependency-free, advanced features are split into 28 optional plugins that have matching implementations for Android, iOS and Web:
- Core 3D and animation: glTF KHR_interactivity behavior graphs, PBR Next materials (clearcoat, transmission, sheen, iridescence, anisotropy), HDRI and spherical harmonics lighting, contact shadows, 3D surface hotspots, 64-joint GPU skinning, morph targets and keyframe cross-fading.
- Spatial perception, splats and AI: lazy head/hand follow, 3D manipulation handles, plane/mesh world understanding and 2D-to-3D object detection, real-time Gemini Live vision/audio streaming, 3D Catmull-Rom spatial paint, depth-colliding particles and splat/spz Gaussian splats.
- CAD review, layout, locomotion and co-presence: live bounding box dimension callouts and AR tape measure, magnetic socket/grid snapping, eye/head gaze dwell, multi-part CAD exploded views and GPU cross-section plane slicing, persistent anchors (with shareable #xr-state URL hashes), teleport/snap-turn locomotion and WebSocket multi-user co-presence.
- Simulation, agents and scenarios: capsule character controller, autonomous steering agents (patrol, chase, flee, follow, wander), hit/integrity/trigger zones and template spawners, checklist objectives with timers/scoring, and zero-asset procedural audio synthesis.
Full disclosure: I built this as a solo project at Google with Antigravity! I'd love for anyone to try it out and let me know what works well or what's missing!
- Jonathan
17 points · 5 comments
Your Agent Changed Last Night and Nobody Deployed Anything: Skill Drift on Google Cloud Skill Registry
The incident that never shows up in your deploy log
Here is a scenario that's easy to create on Gemini Enterprise Agent Platform today.
You build a Managed Agent. You attach a skill from Skill Registry called invoice-triage. It works great. You ship it.
Two weeks later, someone on another team updates invoice-triage to handle a new vendor format. They test it with their own agent. It works for them.
That same afternoon, your production agent starts misrouting invoices.
Nobody touched your agent. No pull request, no deploy, no change ticket. Your CI pipeline is green. Your agent config in Git is identical to last week.
What changed is the skill underneath it. I call this skill drift, and in this post I will show you exactly why it happens, how to measure a second hidden problem that comes with it (the context tax of mounting many skills), and a small lockfile pattern that fixes both.
Everything here is based on the public Managed Agents API and Skill Registry docs. Both are Preview, so treat the code as a starting point and test it in a sandbox project first.
Quick refresher: how skills reach your agent
A skill is a folder with a SKILL.md file plus optional scripts/, references/, and assets/. You upload it to Skill Registry as a zipped payload, and the registry keeps a revision history for every update.
When you create or update a Managed Agent, you mount skills into the agent's sandbox through base_environment.sources. The docs give you three ways to point at the registry:
| Source path | What the agent gets |
|---|---|
| projects/P/locations/L/skills/SKILL_ID | The default version, whatever it is right now |
| projects/P/locations/L/skills/SKILL_ID/skill_versions/VERSION | One exact version, frozen |
| projects/P/locations/L/skills | Up to 100 skills from that project and location |
The first and third options are the ones people copy from quickstarts. Both are floating references. That is the root of the problem.
Problem 1: floating references are :latest tags for agent behavior
We learned this lesson years ago with container images. Deploying myapp:latest to production means your production state is defined by whoever pushed last, not by what you reviewed.
A skill path without a version is the same thing, except worse in one way: a skill is mostly natural language instructions. A tiny wording change in SKILL.md can change which tool the model calls, what it refuses, or how it formats output. There is no compiler error. There is no failing unit test unless you wrote evals for it.
A few details from the docs make this sharper:
- UpdateSkill can replace the display name, description, and the whole zipped filesystem in one call.
- The skill ID becomes the folder name inside the agent sandbox, so the agent keeps "seeing" the same skill name even after the content changed.
- Skill Registry inherits project level IAM. Anyone with write access in the project can update any skill in it.
So the blast radius of one skill update is every agent that mounts that skill without a version.
Problem 2: the context tax of "mount everything"
The third path above is tempting. One line, and your agent has every skill in the project.
But think about how skills work. In the Agent Skills pattern, the agent sees each skill's name and description up front so it can decide which one to open. The full SKILL.md body only loads when the skill is picked. That is called progressive disclosure, and it keeps things cheap when you have a handful of skills.
With up to 100 skills mounted, those names and descriptions add up. That text rides along on every turn, whether or not any of those skills are relevant.
There is also a quality cost. More skills with overlapping descriptions means more chances for the model to pick the wrong one. Two skills named invoice-triage and invoice-review with similar descriptions is a routing bug waiting to happen.
You cannot manage what you do not measure, so let's measure it.
Measuring the skill metadata budget
This script lists every skill in a project and location, joins the names and descriptions the way an agent would roughly see them, and counts tokens with the Count Tokens API.
# skill_context_tax.py
import os
import requests
import google.auth
from google.auth.transport.requests import Request
from google import genai
PROJECT = os.environ["GCP_PROJECT_ID"]
LOCATION = os.environ.get("SKILL_LOCATION", "us-central1")
MODEL = os.environ["MODEL_ID"] # the Gemini model your agent uses
BASE = f"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT}/locations/{LOCATION}"
def authed_session():
creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
creds.refresh(Request())
s = requests.Session()
s.headers["Authorization"] = f"Bearer {creds.token}"
return s
def list_skills(s):
skills, token = [], None
while True:
params = {"pageToken": token} if token else {}
r = s.get(f"{BASE}/skills", params=params)
r.raise_for_status()
body = r.json()
skills.extend(body.get("skills", []))
token = body.get("nextPageToken")
if not token:
return skills
def main():
s = authed_session()
skills = list_skills(s)
catalog = "\n".join(
f"- {sk.get('displayName', '')}: {sk.get('description', '')}" for sk in skills
)
client = genai.Client(vertexai=True, project=PROJECT, location="global")
total = client.models.count_tokens(model=MODEL, contents=catalog).total_tokens
print(f"Skills in {LOCATION}: {len(skills)}")
print(f"Metadata tokens per turn (floor estimate): {total}")
if skills:
print(f"Average per skill: {total / len(skills):.1f}")
if __name__ == "__main__":
main()
Two honest caveats about this number:
1. It is a floor. The base agent may wrap each skill in extra formatting, file paths, or instructions that you cannot see.
2. It uses the registry description. If the description in your SKILL.md frontmatter is longer, the real cost is higher. Keep the two in sync.
To turn it into a monthly number, multiply by your turns per month and your model's input price. Here is the math with made up example numbers, just to show the shape:
80 skills x 70 tokens = 5,600 tokens of skill metadata per turn
5,600 x 2,000,000 turns/month = 11.2 billion input tokens per month
Plug in your own skill count, turn volume, and the current price from the pricing page. If the result surprises you, that is the point of measuring.
The fix: a skill lockfile
The pattern is the same one we use for package managers. Floating references are fine for development. Production gets exact, reviewed versions recorded in a file that lives in Git.
The flow looks like this:
author updates skill -> registry creates new revision
-> staging agent (floating) picks it up
-> evals run against staging
-> pass? update skills.lock.json in a PR
-> merge -> CI pins prod agent to locked versions
Step 1: generate and check the lockfile
# skill_lock.py
import json
import os
import sys
import requests
import google.auth
from google.auth.transport.requests import Request
PROJECT = os.environ["GCP_PROJECT_ID"]
LOCATION = os.environ.get("SKILL_LOCATION", "us-central1")
BASE = f"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT}/locations/{LOCATION}"
LOCKFILE = "skills.lock.json"
def authed_session():
creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
creds.refresh(Request())
s = requests.Session()
s.headers["Authorization"] = f"Bearer {creds.token}"
return s
def latest_revision(s, skill_id):
revs, token = [], None
while True:
params = {"pageToken": token} if token else {}
r = s.get(f"{BASE}/skills/{skill_id}/revisions", params=params)
r.raise_for_status()
body = r.json()
revs.extend(body.get("skillRevisions", []))
token = body.get("nextPageToken")
if not token:
break
if not revs:
return None
revs.sort(key=lambda rev: rev["createTime"])
newest = revs[-1]
return {"revision": newest["name"].split("/")[-1], "created": newest["createTime"]}
def lock(skill_ids):
s = authed_session()
data = {"project": PROJECT, "location": LOCATION, "skills": {}}
for sid in skill_ids:
data["skills"][sid] = latest_revision(s, sid)
with open(LOCKFILE, "w") as f:
json.dump(data, f, indent=2)
print(f"Wrote {LOCKFILE} with {len(skill_ids)} skills")
def check():
s = authed_session()
with open(LOCKFILE) as f:
data = json.load(f)
drift = []
for sid, locked in data["skills"].items():
current = latest_revision(s, sid)
if current is None or current["revision"] != locked["revision"]:
drift.append((sid, locked["revision"], current and current["revision"]))
for sid, old, new in drift:
print(f"DRIFT {sid}: locked {old}, registry now has {new}")
sys.exit(1 if drift else 0)
if __name__ == "__main__":
if sys.argv[1] == "lock":
lock(sys.argv[2:])
elif sys.argv[1] == "check":
check()
Usage:
python skill_lock.py lock invoice-triage vendor-lookup refund-policy
python skill_lock.py check # exits 1 if any skill moved
The check command is your drift alarm. It does not block anyone from updating a skill. It just tells you, loudly, that the registry no longer matches what production was approved with.
Step 2: pin the production agent to the lockfile
The agent update uses PATCH with update_mask=base_environment. There is no update method in the Python SDK for agents yet, so this goes through REST.
One gotcha worth calling out: the docs examples resend the whole base_environment object, including the network allowlist. Treat this update as a full replace, not a merge. If you send only sources, plan for the possibility that other environment settings go back to defaults. Network access is off by default, so a lost allowlist can break an agent that calls external APIs.
# pin_agent.py
import json
import os
import requests
import google.auth
from google.auth.transport.requests import Request
PROJECT = os.environ["GCP_PROJECT_ID"]
AGENT_ID = os.environ["AGENT_ID"]
# The agent docs call this segment "skill_versions", while the registry
# returns revision names under "revisions". Confirm which one your project
# accepts, then set it here.
PIN_SEGMENT = os.environ.get("PIN_SEGMENT", "skill_versions")
ALLOWLIST = json.loads(os.environ.get("NETWORK_ALLOWLIST", '[{"domain": "api.example.com"}]'))
AGENT_URL = (
f"https://aiplatform.googleapis.com/v1beta1/projects/{PROJECT}"
f"/locations/global/agents/{AGENT_ID}?update_mask=base_environment"
)
def main():
with open("skills.lock.json") as f:
lock = json.load(f)
sources = []
for sid, info in lock["skills"].items():
path = (
f"projects/{lock['project']}/locations/{lock['location']}"
f"/skills/{sid}/{PIN_SEGMENT}/{info['revision']}"
)
sources.append({"type": "skill_registry", "source": path, "target": "/.agent/skills"})
body = {
"name": AGENT_ID,
"base_environment": {
"type": "remote",
"sources": sources,
"network": {"allowlist": ALLOWLIST},
},
}
creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"])
creds.refresh(Request())
r = requests.patch(
AGENT_URL,
headers={"Authorization": f"Bearer {creds.token}", "Content-Type": "application/json"},
json=body,
)
r.raise_for_status()
print("Update started:", r.json().get("name"))
if __name__ == "__main__":
main()
The update is a long running operation, so poll the returned operation name before you call the rollout done.
Step 3: run the drift check on a schedule
Any scheduler works. Here is a minimal GitHub Actions version that runs every morning and fails loudly on drift:
name: skill-drift-check
on:
schedule:
- cron: "0 12 * * *"
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: ${{ secrets.WIF_PROVIDER }}
service_account: ${{ secrets.DRIFT_SA }}
- run: pip install requests google-auth
- run: python skill_lock.py check
env:
GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}
SKILL_LOCATION: us-central1
The service account only needs read access, so roles/aiplatform.viewer is enough for this job.
A few more things I noticed while building this
Skill IDs are a namespace you cannot get back quickly. Skill IDs are immutable, and after a delete the ID stays reserved for a while (the docs mention 24 hours in one place and permanent reservation in another, so plan as if it is long). Pick names carefully, and do not use delete plus recreate as a "rollback" strategy. Pinning a previous version is the rollback.
Regions do not line up one to one. Managed Agents only support the global location, while Skill Registry lives in specific regions (us-central1, europe-west4, us-east5 at the time of writing). Your mount path includes the skill's region, so write that region into your lockfile instead of assuming it.
Cloud Storage mounts skip validation. You can also mount skills straight from a bucket. The docs say the backend does not validate folder content in that case. That makes the registry path the safer choice for production, because uploads go through payload checks and you get revision history.
Write descriptions for routing, not marketing. Since descriptions are both your token cost and the model's routing signal, short and specific beats long and friendly. "Classifies AP invoices by vendor and urgency. Not for refunds." does more work than a paragraph.
Limitations and caveats
- Skill Registry and Managed Agents are Pre-GA. APIs, paths, and field names can change, and the docs say not to use confidential data with the Managed Agents preview.
- At the time of writing, Skill Registry does not support VPC Service Controls, CMEK, or HIPAA. Check the compliance table before you design around it.
- The docs use two different names for a pinned skill path (skill_versions on the agent side, revisions on the registry side). My script makes this configurable because I could not confirm which is final. Verify in your own project.
- The token numbers from the context tax script are a lower bound, not an exact bill. The base agent may add text you cannot see.
- A lockfile tells you what changed. It does not tell you whether the change is good. Pair it with evals on a staging agent before promoting.
Wrapping up
Skills make agents easier to build and share across teams. That same sharing means a skill update is a production change for every agent that mounts it without a version.
Three habits fix most of it: pin versions in production, measure what "mount everything" costs you, and run a daily drift check against a lockfile in Git. None of this needs new infrastructure. It is the same discipline we already apply to container images and package managers, pointed at a new kind of artifact.
If you try this pattern, I would love to hear what your skill metadata token count came out to.
References
- Create and manage agents (attach skills, pinning, ListSkills mount): docs.cloud.google.com
- Create and manage skills in Skill Registry: docs.cloud.google.com
- Google Skills repository (SKILL.md examples): google/skills
- Agent Skills format conventions: agentskills.io
8 reactions
Mentions
Claude CodeからPremiere Proを290回動かして、53分のPodcastを全自動編集してみた 普段は Premiere Pro と After Effects で動画を編集しています。社内で始めたポッドキャスト「Locked in」の第1回(約53分)を、今回は Claude Code に編集させてみました。 素材は iPhone で撮った 4K 映像が3アングル分(合計約36GB)と、マイク2本の WAV です。そこから YouTube 用の本編1本(4K)と、縦型ショート20本を作りました。 役割はこう分けました。 Claude Code:素材の同期、文字起こし、カット、字幕、テロップ作り、配置、書き出し 僕:ショートを1本ずつ見て直しを頼む、タイトルを選ぶ、色(Lumetri)の数値を決める Premiere の画面で僕が直接いじったのは、色の数値だけです。直しの依頼は、ほぼ音声入力で出していました。 やり方は、届く所はスクリプトで動かし、届かない所だけ画面操作に任せる、という分け方です。今回はスクリプトで動かした回数が約340回、画面操作に頼ったのは6回ほどでした。動画編集に限らず、スクリプトで操作できる GUI アプリなら、同じ構成を試せると思います。 ! この記事のコードは macOS、Premiere Pro 2026、After Effects 2026 で動かしたものです。Windows では After Effects の呼び出し方などが変わります。 完成したショートの一部 処理の流れ ①〜③ は Mac の上で完結しています。④〜⑦ は Claude Code が Adobe のアプリにスクリプトを送って動かしました。スクリプトで操作できないところだけ、Codex の Computer Use に画面操作を頼んでいます。 スクリプトの内訳は、Premiere が約290回、After Effects が約50回です。 MCP サーバーを使わなかった理由 Premiere 用・After Effects 用の MCP サーバーは、GitHub にいくつかあります(hetpatel-11/Adobe_Premiere_Pro_MCP、Dakkshin/after-effects-mcp など)。どれも中では CEP 経由で ExtendScript を呼んでいます。 ただ、実装によっては使いたい操作がツールになっていませんでした。.mogrt の配置がその一例です。任意のスクリプトを実行できるサーバーもありますが、それなら結局スクリプトを書くことになります。そこで今回は MCP サーバーを使わず、自作の小さな CEP 拡張に JSX を直接送る構成にしました。 今回必要だった操作は、ほとんど ExtendScript で書けました。シーケンスの複製、クリップの配置・トリム・移動、.mogrt の文字の差し替え、Lumetri の値の読み書き、書き出しの依頼です。コマの書き出しなど一部は、非公開の QE DOM を使っています。 画面を毎回読み取る方式にしなかったのは、量が多かったからです。字幕は本編だけで約860枚あります。スクリプトなら200枚ずつまとめて置けて、結果は placed 200 NG 0 のような1行で返ってきます。画面の確認が必要なときだけ、コマを書き出して見せました。 ExtendScript は古い仕組みで、Premiere の拡張は UXP(2025年12月に正式版)へ移りつつあります。ただ、Premiere のスクリプト資料には、ExtendScript は今も動き、廃止の予定は出ていないとあります。 Premiere 側:CEP ブリッジ Premiere の中に見えない CEP 拡張を1つ置き、その中で HTTP サーバーを 127.0.0.1 だけで立てています。届いた JSX を evalScript に渡し、戻り値を返すだけのものです。以下は実行部分の抜粋です。 index.html <script> const http = require('http'), fs = require('fs'), os = require('os'), path = require('path'); // 認証用のトークン。先に適当な文字列をこのファイルに保存しておく const token = fs.readFileSync(path.join(os.homedir(), '.claude-premiere-bridge-token'), 'utf8').trim(); const evalJsx = code => new Promise(res => window.adobe_cep.evalScript(code, res)); http.createServer((req, res) => { if (req.headers['authorization'] !== 'Bearer ' + token) { res.writeHead(401); return res.end(); } if (req.method === 'POST' && req.url === '/eval') { let body = ''; req.setEncoding('utf8'); req.on('data', c => body += c); req.on('end', async () => res.end(String(await evalJsx(body)))); return; } res.writeHead(404); res.end(); }).listen(47820, '127.0.0.1'); </script> ほかに必要なのは次のとおりです。 拡張のフォルダ(~/Library/Application Support/Adobe/CEP/extensions/ の下)と CSXS/manifest.xml manifest では CEF に --enable-nodejs と --mixed-context を渡す(パネルの中で require を使うため) 起動のきっかけを applicationActivate にし、AutoVisible を false にして画面には出さない 実際に使った manifest.xml CSXS/manifest.xml <?xml version="1.0" encoding="UTF-8"?> <ExtensionManifest Version="7.0" ExtensionBundleId="com.claude.premierebridge" ExtensionBundleVersion="1.0.0" ExtensionBundleName="Claude Bridge" xmlns:xsi="w3.org"> <ExtensionList> <Extension Id="com.claude.premierebridge.bg" Version="1.0.0"/> </ExtensionList> <ExecutionEnvironment> <HostList><Host Name="PPRO" Version="[22.0,99.9]"/></HostList> <LocaleList><Locale Code="All"/></LocaleList> <RequiredRuntimeList><RequiredRuntime Name="CSXS" Version="9.0"/></RequiredRuntimeList> </ExecutionEnvironment> <DispatchInfoList> <Extension Id="com.claude.premierebridge.bg"> <DispatchInfo> <Resources> <MainPath>./index.html</MainPath> <CEFCommandLine> <Parameter>--enable-nodejs</Parameter> <Parameter>--mixed-context</Parameter> </CEFCommandLine> </Resources> <Lifecycle> <AutoVisible>false</AutoVisible> <StartOn> <Event>com.adobe.csxs.events.ApplicationActivate</Event> <Event>applicationActivate</Event> </StartOn> </Lifecycle> <UI> <Type>Custom</Type> <Geometry><Size><Height>1</Height><Width>1</Width></Size></Geometry> </UI> </DispatchInfo> </Extension> </DispatchInfoList> </ExtensionManifest> 署名していない拡張を読み込ませるため、一度だけデバッグモードを有効にします(数字は CSXS のバージョンに合わせる)。 defaults write com.adobe.CSXS.12 PlayerDebugMode 1 呼び出し側は zsh のラッパーです。送るスクリプトを try/catch で包み、out() に渡した値をまとめて返します。 run_pr.sh #!/bin/zsh 使い方: zsh run_pr.sh sample.jsx T=$(cat ~/.claude-premiere-bridge-token) { print -r -- '(function(){var o=[];function out(x){o.push(String(x));} try{' cat "$1" print -r -- '}catch(e){out("ERROR line "+e.line+": "+e);} return __o.join(" | ");})()'; } > "${1%.jsx}_w.jsx" curl -s -H "Authorization: Bearer $T" -X POST --data-binary @"${1%.jsx}_w.jsx" 127.0.0.1 最初に試すなら、out(app.project.activeSequence.name) だけを送って、シーケンス名が返ってくるかを見るのがいいと思います。 After Effects 側:テンプレートを作る After Effects は、Mac なら AppleScript からスクリプトファイルを実行できます。 osascript -e 'tell application "Adobe After Effects 2026" to DoScriptFile "/path/to/build.jsx"' コンポを組んだら、Premiere 側で変えたいプロパティを登録し、.mogrt として書き出します。 textProp.addToMotionGraphicsTemplateAs(comp, "字幕"); // Premiere で編集できる項目にする app.beginSuppressDialogs(); try { comp.openInEssentialGraphics(); comp.exportAsMotionGraphicsTemplate(true, "/path/to/MOGRT/"); } finally { app.endSuppressDialogs(false); } beginSuppressDialogs() で囲んだのは、書き出しのたびに「このフォントは使用できません」という確認が出て、AE が止まったからです。Mac に入れてあるフォントなのに出る理由までは追えていませんが、今回はこれで止まらなくなりました。どうしても閉じられないエラー画面が一度だけあり、そのときは Codex に OK を押してもらっています。 もうひとつ詰まったのが、作り直しです。同じテンプレート名で .mogrt を書き出し直しても、Premiere には古い見た目のまま入ってきました。名前の末尾に v2、v3 と付けて入れ直したら、新しいほうが使われました。 字幕、名前テロップ、用語の解説、章の切り替え、オープニング、エンディングは、どれもこの方法で作っています。 章の切り替え(12章)。番号や話題は Premiere 側で差し替えている 名前テロップ。2人の板の幅はスライダーでそろえた Premiere に並べる .mogrt は importMGT で置きます。 ! 今回の環境では、importMGT で置いた直後のクリップがテンプレートの長さ(10秒)になり、その範囲にある既存のクリップを上書きしました。試すときは、複製したシーケンスか空いたトラックでやってください。 置いたあとは、getMGTComponent() のプロパティに文字を入れます。今回のテンプレートでは、字幕の値が JSON の文字列で取れたので、その中の textEditValue を書き換えて戻しています。 var clip = seq.importMGT(mogrtPath, String(ticks), videoTrackIndex, 0); clip.end = endTime; // 10秒から字幕の長さに縮める var props = clip.getMGTComponent().properties; for (var i = 0; i < props.numItems; i++) { if (props[i].displayName != "字幕") continue; var o = eval("(" + props[i].getValue() + ")"); // ExtendScript には JSON がない o.textEditValue = text; o.fontTextRunLength = [text.length]; // 字幕全体を1つの書式で扱う前提 props[i].setValue(toJson(o), true); // toJson はオブジェクトを文字列に戻す自作関数 } 上書きでは実際に事故がありました。仕上げの段階で、字幕が2秒ほど抜けている所に1枚足したら、後ろの4枚が消えたのです。空いている区間に置いてから move() で動かす方法も試しましたが、動かしたクリップは描画されませんでした。最後は、消えた4枚をその場で置き直しています。Claude Code が書き出したコマを見て気づき、作業前に複製しておいたシーケンスから文と時刻を拾って直しました。 本編のタイムライン。V3 の細かい帯が字幕、V2 が色を調整する調整レイヤー 確認と書き出し 字幕やレイアウトを直すたびに僕が再生していると、手が止まります。そこで、まず Claude Code 自身にコマを書き出させて確認させました。 app.enableQE(); qe.project.getActiveSequence().exportFramePNG("00:20:29:12", "/tmp/check.png"); // 実際には check.png.png という名前で保存される ! app.enableQE() で使える QE DOM は、Adobe が公開していない仕組みです。バージョンが変わると動かなくなるかもしれません。 コマで見ていたのは、字幕が出ているか、文字が顔にかかっていないか、テロップの位置です。音のつながりやカットの間は、静止画では分かりません。そこは僕が書き出した動画を見て、気になった所を伝えました。 書き出しは Media Encoder にキューで渡します。 app.encoder.launchEncoder(); app.encoder.encodeSequence(seq, outPath, presetPath, app.encoder.ENCODE_ENTIRE, 1); app.encoder.startBatch(); スクリプトで触れない操作は Codex に 調整レイヤーを新しく作る方法は、今回調べた範囲の ExtendScript と QE には見つかりませんでした。シーケンス設定の「HDR グラフィックスホワイト」も、ダイアログの中でしか変えられません。 こういう操作は、Claude Code がターミナルから Codex を呼んで、画面を操作させています。 codex exec --skip-git-repo-check "Use computer use on this Mac. Premiere Pro のプロジェクトパネルで 新規項目 > 調整レイヤー を作り、OK を押す。ほかは何もしない。" 使ったのは codex-cli 0.162.1 です。Computer Use の設定と、macOS の画面収録・アクセシビリティの許可は先に済ませてあります(Computer Use の説明)。操作が終わったら、ブリッジでプロジェクトの中身を読み、本当にできたかを確かめてから次へ進みます。 最初、Claude Code は「調整レイヤーを手で作ってください」と僕に頼んできました。「そういう時は Codex の Computer Use で追加してって言ってるよね?」と返して、それからは聞かずに Codex を呼ぶようになっています。 素材の処理(Python) 4K の素材を Premiere で再生して探すと重いので、切る位置は音と文字から決めさせました。 同期 音の包絡線どうしの相互相関を FFT で計算し、カメラとマイクの時間差を出しました。録音機の時計がリセットされていて、ファイルの日付が2020年になっていました。どれが今回の録音かも、音を突き合わせて見つけています。 話者の判定 最初はカメラの音から声紋(SpeechBrain)で判定していました。マイクの録音が届いてからは、2本のマイクの音量差で判定しています。今回の録音では、話している本人のマイクのほうが10dB以上大きく入っていました。声紋で出した結果とも、ほぼ一致しています。 文字起こし mlx-whisper は、上で出した話し手の交代位置で音声を区切ってから回しました。通しで起こした単語の時刻に話者の判定を重ねると、時刻のずれで語の途中に交代が入ってしまったからです。Whisper は無音の区間で「ご視聴ありがとうございました」と出してくることがあります。そこで turbo と large-v3 の2つで起こし、違う箇所を Claude Code に突き合わせて清書させました。怪しい所は、その区間だけマイクごとに起こし直して確かめています。 改行 BudouX で改行の候補に分け、1行14〜15字を目安に、行の長さの偏りが小さくなる組み合わせを動的計画法で選びました。 同じ瞬間の3アングル。左から、ゲスト側の縦、ホスト側の縦、2人の横 色まわりで詰まったこと 分割すると白っぽくなる 最初は、9GB の iPhone の映像を扱いやすくしようと、ffmpeg のストリームコピーで5分ずつに分けていました。ところが、分けたファイルを使うと、書き出しで肌が白く飛びました。ffprobe で見ると、元のファイルにある Dolby Vision の情報が、分けたファイルにはありません。Premiere での HDR から SDR への変換が変わったのは、このためだと考えています。分割はやめて、元のファイルとプロキシで編集しました。ちなみに、分けたファイルは AAC の音も Premiere で無音になっていました。 HDR シーケンスで字幕がくすむ iPhone の素材に合わせて、シーケンスを Rec.2100 HLG で作っていました。この状態だと、字幕の白が映像に対して暗く見えました。HDR グラフィックスホワイトを 300 nit にしたら直っています。最終的な書き出しは SDR(Rec.709)です。 色の数値 色はショート1本で僕が調整し、Claude Code にその Lumetri の値を全項目読み取らせて、残りのショートにコピーさせました。本編も、僕が数値を決めたあとは「ここから動かさないで」と伝えています。アングルごとの細かい色合わせはしていません。調整レイヤー1枚で全体にかけています。 指示の出し方 指示はほぼ全部、音声入力です。誤変換が混ざっていても、Claude Code はだいたい意図を拾ってくれました。 途中で何度かルールを足しています。特に何度も言ったのは次の3つです。 修正の前にシーケンスを複製しておく 僕が色を決めたあとは、その値を変えない 頼んでいないのに書き出さない 書き出しの件は、Claude Code が気を利かせて勝手に書き出しを始めたので、そのとき言いました。Premiere を触っている最中に書き出しが走ると、作業が止まるからです。 品質の基準は、ショートの最初の依頼で具体的に伝えました。「字幕が見やすい」「文末でカットを切り替える」「話している人の口元が映る」の3つです。「文の途中で終わらせない」も何度も言いました。これは最終的に、区間を決める処理の中にルールとして入っています。 あとは、見て引っかかった所をそのまま言いました。「終わり方が急」と言えば、最後の区間を文末まで延ばしてきます。 「改行の位置、微妙にきもいとこある」と言ったときは、こう変わりました。 左が直す前、右が直した後。同じ瞬間のコマ 直す前は、1行の文字数をそろえることを優先していたので、「志を/持って」のように意味の途中で切れていました。Claude Code はこの回の字幕を全部見直し、意味の切れ目で区切り直しています。1行15字以内という条件は変えていません。 ほかに変わった字幕の例 直す前 直した後 人たちを見て 自分も/そう思ってるから そういう 自分もそう思ってるから/そういう人たちを見ると そういう人たちに/届いて おっしゃって そういう人たちに届いて いただいたように この/ポッドキャストを見て感じて おっしゃっていただいたように/このポッドキャストを見て それからすごい大衆に/受けるように それから すごい大衆に/受けるようにしなきゃいけない 本編の「20:27 カットが不自然」は、映像のカットではありませんでした。相手の返事の間だけ字幕が2秒消えていたのが原因で、Claude Code はそこを見つけて字幕を足しています。 日程 10/27 夜:素材が届く前に、ブリッジの動作を確認し、テロップのデザインを始める 10/28:ショート20本。夜に全20本を書き出す 10/29 昼:本編。字幕、テロップ、エンディングを入れて書き出す 10/29 夜:見直しで3か所を直して、書き出し直す ブリッジの確認から最後の書き出しまで、約2日半でした。僕が実際に手を動かしていた時間は数えていません。 残った手作業 色の数値とタイトル選びは、自分でやったほうが早かったです。タイトルは Claude Code に案をいくつも出させ、その中から選ぶか、言い回しを変えて決めました。 もうひとつは、動画を通して見て仕上がりを判断することです。字幕の抜けのように Claude Code がコマから気づくこともありましたが、音のつながりやカットの間の違和感は、僕が動画を見て伝えています。直す作業のほうは、ほとんど任せられました。 参考 Premiere Pro スクリプトの資料:ppro-scripting.docsforadobe.dev After Effects スクリプトの資料:ae-scripting.docsforadobe.dev Premiere の UXP 正式版の発表:blog.developer.adobe.com Claude Code:code.claude.com Codex CLI:openai/codex Codex の Claude Code 用プラグイン:openai/codex-plugin-cc モーショングラフィックステンプレート:helpx.adobe.com
I've open-sourced Three-Live: a sandboxed Three.js runtime that compiles natively to Android Filament and iOS RealityKit (and also works in WebXR) I recently open-sourced Three-Live (three-live and xr-live on npm, three-live-compiler on crates.io) and wanted to share it with the Three.js community. - GitHub: google/three-live - Live playgrounds: google.github.io Why I built it Three.js is a great way to author 3D scenes, materials and animations in just a few lines of JavaScript. But what happens when you want to add that 3D to a native Android or iOS app? - Mobile AR & WebViews: iOS Safara, WKWebView and Android WebView don't support WebXR... Exporting a static usdz or glb file to AR Quick Look and Scene Viewer drops onUpdate loops, interactivity and any custom plugins. - Mobile and headset apps: To deliver Three.js usually means rewriting the scene in Kotlin (Filament / Jetpack XR SceneCore) and Swift (RealityKit), or embedding a WebView that can't render into the headset's shared space. - Sandboxing: If a scene script comes from an LLM or user input, running it on the main thread isn't safe. There-Live is my attempt at a solution to these challenges. How it works Three-Live lets you write standard Three.js code and pass it to native mobile SDKs, or run in a web app: - On Android: Uses an in-process Rust/Boa compiler (three-live-compiler) over zero-copy JNI to convert Three.js scripts into binary GLB + KHR_interactivity graphs, driving Google Filament, ARCore and Jetpack XR SceneCore - On iOS, macOS and visionOS: Compiles the same script into Apply RealityKit Entity hierarchies (RealityKitHelper), ARView / RealityView and Metal. - On Web: Evaluates the same script inside a sandboxed Web Worker or opaque-origin iframe (connect-src 'none', with CPU time and geometry quotas), streams the validated scene graph to a lightweight WebGL2/WebXR renderer, and includes in-memory glb and usdz pacakgers for one-tap mobile AR handoff. Quick examples Android import com.google.threelive.FilamentHelper import com.google.threelive.XRLiveRuntimeSession import com.google.threelive.XRLiveSceneCoreHelper // Compile Three.js to a zero-copy GLB buffer and load in Filament val holder = FilamentHelper.loadModelZeroCopy(script) { buffer -> assetLoader.createAssetFromBinary(buffer) } // Drive KHR interactivity graphs, head-follow springs and spatial/sim plugins per frame val runtime = XRLiveRuntimeSession.fromGlb(holder.byteBuffer) runtime.step(dtSeconds = 1.0 / 60.0) // Or attach interactive spatial traits (grab, scale, billboard, anchor) in XR SceneCore XRLiveSceneCoreHelper.attachTraitsFromGlb(xrSession, entity, holder.byteBuffer) Module plugin architecture To keep the core bundles small and dependency-free, advanced features are split into 28 optional plugins that have matching implementations for Android, iOS and Web: - Core 3D and animation: glTF KHR_interactivity behavior graphs, PBR Next materials (clearcoat, transmission, sheen, iridescence, anisotropy), HDRI and spherical harmonics lighting, contact shadows, 3D surface hotspots, 64-joint GPU skinning, morph targets and keyframe cross-fading. - Spatial perception, splats and AI: lazy head/hand follow, 3D manipulation handles, plane/mesh world understanding and 2D-to-3D object detection, real-time Gemini Live vision/audio streaming, 3D Catmull-Rom spatial paint, depth-colliding particles and splat/spz Gaussian splats. - CAD review, layout, locomotion and co-presence: live bounding box dimension callouts and AR tape measure, magnetic socket/grid snapping, eye/head gaze dwell, multi-part CAD exploded views and GPU cross-section plane slicing, persistent anchors (with shareable #xr-state URL hashes), teleport/snap-turn locomotion and WebSocket multi-user co-presence. - Simulation, agents and scenarios: capsule character controller, autonomous steering agents (patrol, chase, flee, follow, wander), hit/integrity/trigger zones and template spawners, checklist objectives with timers/scoring, and zero-asset procedural audio synthesis. Full disclosure: I built this as a solo project at Google with Antigravity! I'd love for anyone to try it out and let me know what works well or what's missing! - Jonathan
Three-Live – Run Live Three.js on Android and iOS (and WebXR)
Google's common library for parsing, formatting, and validating phone numbers
The SpeciesNet model identifies wildlife from camera-trap images. It's an open-source project at google/cameratrapai that could help conservation work by sorting data faster than manual review. google/cameratrapai
Agent Mesh v0.2.0 Deep in Agent Mesh my agent waited, unheard. Now: one path. Hello. Your agents can now take calls from outside the mesh. Same identity, same policy, one URL each. github.com/google/agentmesh/releases/tag/v0.2.0
Your Agent Changed Last Night and Nobody Deployed Anything: Skill Drift on Google Cloud Skill Registry The incident that never shows up in your deploy log Here is a scenario that's easy to create on Gemini Enterprise Agent Platform today. You build a Managed Agent. You attach a skill from Skill Registry called invoice-triage. It works great. You ship it. Two weeks later, someone on another team updates invoice-triage to handle a new vendor format. They test it with their own agent. It works for them. That same afternoon, your production agent starts misrouting invoices. Nobody touched your agent. No pull request, no deploy, no change ticket. Your CI pipeline is green. Your agent config in Git is identical to last week. What changed is the skill underneath it. I call this skill drift, and in this post I will show you exactly why it happens, how to measure a second hidden problem that comes with it (the context tax of mounting many skills), and a small lockfile pattern that fixes both. Everything here is based on the public Managed Agents API and Skill Registry docs. Both are Preview, so treat the code as a starting point and test it in a sandbox project first. Quick refresher: how skills reach your agent A skill is a folder with a SKILL.md file plus optional scripts/, references/, and assets/. You upload it to Skill Registry as a zipped payload, and the registry keeps a revision history for every update. When you create or update a Managed Agent, you mount skills into the agent's sandbox through base_environment.sources. The docs give you three ways to point at the registry: | Source path | What the agent gets | |---|---| | projects/P/locations/L/skills/SKILL_ID | The default version, whatever it is right now | | projects/P/locations/L/skills/SKILL_ID/skill_versions/VERSION | One exact version, frozen | | projects/P/locations/L/skills | Up to 100 skills from that project and location | The first and third options are the ones people copy from quickstarts. Both are floating references. That is the root of the problem. Problem 1: floating references are :latest tags for agent behavior We learned this lesson years ago with container images. Deploying myapp:latest to production means your production state is defined by whoever pushed last, not by what you reviewed. A skill path without a version is the same thing, except worse in one way: a skill is mostly natural language instructions. A tiny wording change in SKILL.md can change which tool the model calls, what it refuses, or how it formats output. There is no compiler error. There is no failing unit test unless you wrote evals for it. A few details from the docs make this sharper: - UpdateSkill can replace the display name, description, and the whole zipped filesystem in one call. - The skill ID becomes the folder name inside the agent sandbox, so the agent keeps "seeing" the same skill name even after the content changed. - Skill Registry inherits project level IAM. Anyone with write access in the project can update any skill in it. So the blast radius of one skill update is every agent that mounts that skill without a version. Problem 2: the context tax of "mount everything" The third path above is tempting. One line, and your agent has every skill in the project. But think about how skills work. In the Agent Skills pattern, the agent sees each skill's name and description up front so it can decide which one to open. The full SKILL.md body only loads when the skill is picked. That is called progressive disclosure, and it keeps things cheap when you have a handful of skills. With up to 100 skills mounted, those names and descriptions add up. That text rides along on every turn, whether or not any of those skills are relevant. There is also a quality cost. More skills with overlapping descriptions means more chances for the model to pick the wrong one. Two skills named invoice-triage and invoice-review with similar descriptions is a routing bug waiting to happen. You cannot manage what you do not measure, so let's measure it. Measuring the skill metadata budget This script lists every skill in a project and location, joins the names and descriptions the way an agent would roughly see them, and counts tokens with the Count Tokens API. # skill_context_tax.py import os import requests import google.auth from google.auth.transport.requests import Request from google import genai PROJECT = os.environ["GCP_PROJECT_ID"] LOCATION = os.environ.get("SKILL_LOCATION", "us-central1") MODEL = os.environ["MODEL_ID"] # the Gemini model your agent uses BASE = f"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT}/locations/{LOCATION}" def authed_session(): creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) creds.refresh(Request()) s = requests.Session() s.headers["Authorization"] = f"Bearer {creds.token}" return s def list_skills(s): skills, token = [], None while True: params = {"pageToken": token} if token else {} r = s.get(f"{BASE}/skills", params=params) r.raise_for_status() body = r.json() skills.extend(body.get("skills", [])) token = body.get("nextPageToken") if not token: return skills def main(): s = authed_session() skills = list_skills(s) catalog = "\n".join( f"- {sk.get('displayName', '')}: {sk.get('description', '')}" for sk in skills ) client = genai.Client(vertexai=True, project=PROJECT, location="global") total = client.models.count_tokens(model=MODEL, contents=catalog).total_tokens print(f"Skills in {LOCATION}: {len(skills)}") print(f"Metadata tokens per turn (floor estimate): {total}") if skills: print(f"Average per skill: {total / len(skills):.1f}") if __name__ == "__main__": main() Two honest caveats about this number: 1. It is a floor. The base agent may wrap each skill in extra formatting, file paths, or instructions that you cannot see. 2. It uses the registry description. If the description in your SKILL.md frontmatter is longer, the real cost is higher. Keep the two in sync. To turn it into a monthly number, multiply by your turns per month and your model's input price. Here is the math with made up example numbers, just to show the shape: 80 skills x 70 tokens = 5,600 tokens of skill metadata per turn 5,600 x 2,000,000 turns/month = 11.2 billion input tokens per month Plug in your own skill count, turn volume, and the current price from the pricing page. If the result surprises you, that is the point of measuring. The fix: a skill lockfile The pattern is the same one we use for package managers. Floating references are fine for development. Production gets exact, reviewed versions recorded in a file that lives in Git. The flow looks like this: author updates skill -> registry creates new revision -> staging agent (floating) picks it up -> evals run against staging -> pass? update skills.lock.json in a PR -> merge -> CI pins prod agent to locked versions Step 1: generate and check the lockfile # skill_lock.py import json import os import sys import requests import google.auth from google.auth.transport.requests import Request PROJECT = os.environ["GCP_PROJECT_ID"] LOCATION = os.environ.get("SKILL_LOCATION", "us-central1") BASE = f"https://{LOCATION}-aiplatform.googleapis.com/v1beta1/projects/{PROJECT}/locations/{LOCATION}" LOCKFILE = "skills.lock.json" def authed_session(): creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) creds.refresh(Request()) s = requests.Session() s.headers["Authorization"] = f"Bearer {creds.token}" return s def latest_revision(s, skill_id): revs, token = [], None while True: params = {"pageToken": token} if token else {} r = s.get(f"{BASE}/skills/{skill_id}/revisions", params=params) r.raise_for_status() body = r.json() revs.extend(body.get("skillRevisions", [])) token = body.get("nextPageToken") if not token: break if not revs: return None revs.sort(key=lambda rev: rev["createTime"]) newest = revs[-1] return {"revision": newest["name"].split("/")[-1], "created": newest["createTime"]} def lock(skill_ids): s = authed_session() data = {"project": PROJECT, "location": LOCATION, "skills": {}} for sid in skill_ids: data["skills"][sid] = latest_revision(s, sid) with open(LOCKFILE, "w") as f: json.dump(data, f, indent=2) print(f"Wrote {LOCKFILE} with {len(skill_ids)} skills") def check(): s = authed_session() with open(LOCKFILE) as f: data = json.load(f) drift = [] for sid, locked in data["skills"].items(): current = latest_revision(s, sid) if current is None or current["revision"] != locked["revision"]: drift.append((sid, locked["revision"], current and current["revision"])) for sid, old, new in drift: print(f"DRIFT {sid}: locked {old}, registry now has {new}") sys.exit(1 if drift else 0) if __name__ == "__main__": if sys.argv[1] == "lock": lock(sys.argv[2:]) elif sys.argv[1] == "check": check() Usage: python skill_lock.py lock invoice-triage vendor-lookup refund-policy python skill_lock.py check # exits 1 if any skill moved The check command is your drift alarm. It does not block anyone from updating a skill. It just tells you, loudly, that the registry no longer matches what production was approved with. Step 2: pin the production agent to the lockfile The agent update uses PATCH with update_mask=base_environment. There is no update method in the Python SDK for agents yet, so this goes through REST. One gotcha worth calling out: the docs examples resend the whole base_environment object, including the network allowlist. Treat this update as a full replace, not a merge. If you send only sources, plan for the possibility that other environment settings go back to defaults. Network access is off by default, so a lost allowlist can break an agent that calls external APIs. # pin_agent.py import json import os import requests import google.auth from google.auth.transport.requests import Request PROJECT = os.environ["GCP_PROJECT_ID"] AGENT_ID = os.environ["AGENT_ID"] # The agent docs call this segment "skill_versions", while the registry # returns revision names under "revisions". Confirm which one your project # accepts, then set it here. PIN_SEGMENT = os.environ.get("PIN_SEGMENT", "skill_versions") ALLOWLIST = json.loads(os.environ.get("NETWORK_ALLOWLIST", '[{"domain": "api.example.com"}]')) AGENT_URL = ( f"https://aiplatform.googleapis.com/v1beta1/projects/{PROJECT}" f"/locations/global/agents/{AGENT_ID}?update_mask=base_environment" ) def main(): with open("skills.lock.json") as f: lock = json.load(f) sources = [] for sid, info in lock["skills"].items(): path = ( f"projects/{lock['project']}/locations/{lock['location']}" f"/skills/{sid}/{PIN_SEGMENT}/{info['revision']}" ) sources.append({"type": "skill_registry", "source": path, "target": "/.agent/skills"}) body = { "name": AGENT_ID, "base_environment": { "type": "remote", "sources": sources, "network": {"allowlist": ALLOWLIST}, }, } creds, _ = google.auth.default(scopes=["https://www.googleapis.com/auth/cloud-platform"]) creds.refresh(Request()) r = requests.patch( AGENT_URL, headers={"Authorization": f"Bearer {creds.token}", "Content-Type": "application/json"}, json=body, ) r.raise_for_status() print("Update started:", r.json().get("name")) if __name__ == "__main__": main() The update is a long running operation, so poll the returned operation name before you call the rollout done. Step 3: run the drift check on a schedule Any scheduler works. Here is a minimal GitHub Actions version that runs every morning and fails loudly on drift: name: skill-drift-check on: schedule: - cron: "0 12 * * *" workflow_dispatch: jobs: check: runs-on: ubuntu-latest permissions: id-token: write contents: read steps: - uses: actions/checkout@v4 - uses: google-github-actions/auth@v2 with: workload_identity_provider: ${{ secrets.WIF_PROVIDER }} service_account: ${{ secrets.DRIFT_SA }} - run: pip install requests google-auth - run: python skill_lock.py check env: GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }} SKILL_LOCATION: us-central1 The service account only needs read access, so roles/aiplatform.viewer is enough for this job. A few more things I noticed while building this Skill IDs are a namespace you cannot get back quickly. Skill IDs are immutable, and after a delete the ID stays reserved for a while (the docs mention 24 hours in one place and permanent reservation in another, so plan as if it is long). Pick names carefully, and do not use delete plus recreate as a "rollback" strategy. Pinning a previous version is the rollback. Regions do not line up one to one. Managed Agents only support the global location, while Skill Registry lives in specific regions (us-central1, europe-west4, us-east5 at the time of writing). Your mount path includes the skill's region, so write that region into your lockfile instead of assuming it. Cloud Storage mounts skip validation. You can also mount skills straight from a bucket. The docs say the backend does not validate folder content in that case. That makes the registry path the safer choice for production, because uploads go through payload checks and you get revision history. Write descriptions for routing, not marketing. Since descriptions are both your token cost and the model's routing signal, short and specific beats long and friendly. "Classifies AP invoices by vendor and urgency. Not for refunds." does more work than a paragraph. Limitations and caveats - Skill Registry and Managed Agents are Pre-GA. APIs, paths, and field names can change, and the docs say not to use confidential data with the Managed Agents preview. - At the time of writing, Skill Registry does not support VPC Service Controls, CMEK, or HIPAA. Check the compliance table before you design around it. - The docs use two different names for a pinned skill path (skill_versions on the agent side, revisions on the registry side). My script makes this configurable because I could not confirm which is final. Verify in your own project. - The token numbers from the context tax script are a lower bound, not an exact bill. The base agent may add text you cannot see. - A lockfile tells you what changed. It does not tell you whether the change is good. Pair it with evals on a staging agent before promoting. Wrapping up Skills make agents easier to build and share across teams. That same sharing means a skill update is a production change for every agent that mounts it without a version. Three habits fix most of it: pin versions in production, measure what "mount everything" costs you, and run a daily drift check against a lockfile in Git. None of this needs new infrastructure. It is the same discipline we already apply to container images and package managers, pointed at a new kind of artifact. If you try this pattern, I would love to hear what your skill metadata token count came out to. References - Create and manage agents (attach skills, pinning, ListSkills mount): docs.cloud.google.com - Create and manage skills in Skill Registry: docs.cloud.google.com - Google Skills repository (SKILL.md examples): google/skills - Agent Skills format conventions: agentskills.io
Mozc に 3.35.6354 のタグをつけました 辞書データの更新や変換精度の改善にくわえて、ユーザー辞書の優先順位の見直しや Linux でのダークモードへの対応、コードベースへの広範なリファクタリングなどを含んでいます PRやフィードバックなどご協力ありがとうございました github.com/google/mozc/releases/tag/3.35.6354
#Google published open-source skills for designing and reviewing software with AI agents google/elephant-goldfish-model t.co
# AIに任せるなんてとんでもない!と言われている人達へ — 「AIを信じなくていい仕組み」から始める 「最先端のAI企業のエンジニアは、もうコードを書いていないらしい」「プロンプトを書く時代は終わった」「AIエージェントがひと晩でアプリを作った」——X や YouTube には、こんな話が毎日のように流れてきます。一方、職場では「AIに全部やらせるなんて危ない」という空気がある。どちらを信じればいいのか分からないまま、結局チャット欄にコードを貼っては貼り返す毎日に戻っている。この記事は、そんな人に向けて書きました。 先に結論を書きます。大事なのは、AIをどこまで信じるかではありません。AIを信じなくても困らない仕組みを、先に用意することです。 流れてくる話に、映っていないもの 派手な事例の多くは、たぶん本当です。ただ、記事や動画に映りやすいのは「AIが何をしたか」で、映りにくいのは「それが正しいと、誰がどう確かめたか」です。 自動化がうまくいっているチームは、たいていこの地味な部分を作り込んでいます。 テストが通らなければ、マージできない AIが触れてはいけないファイルは、そもそもAIから見えない AIの「修正しました」という報告は、証拠として扱わない 実際、派手な成果の正体は、この地味な部分だったという報告がいくつも出ています。OpenAI のあるチームは、人間が直接コードを書かずに、約5か月で100万行規模のソフトウェアをAIに作らせたと報告しました。ただ、その報告の大半は「AIに何を書かせたか」ではなく、「AIを迷わせない仕組み」の話です。短く保った指示ファイルや、間違いを指摘するだけでなく直し方まで教えてくれる検査ツールといったものです。LangChain は、AIのモデルを一切変えずに、周りの仕組みだけを作り直して、ベンチマークの順位を上位30位の圏外から上位5位まで上げています(2026年2月の発表時点)。 この部分を飛ばして真似ると、うまくいきません。そして、うまくいかなかった経験が「やっぱりAIに任せるのは危ない」という空気を強めます。派手な話も、慎重な声も、半分しか映していないのです。 人間が毎回見ていても、16回やり直した 私自身の失敗談を書きます。縦書きの本を組むツールを、AIとチャットしながら作っていたときのことです。図がページの境目で切れる、章がページの途中から始まる——こうしたレイアウト崩れを直すのに、20回以上のやり取りが必要になりました。 手を抜いていたわけではありません。毎回スクリーンショットを撮ってAIに見せ、直ったら自分の目で確かめていました。1回の確認は5分ほどです。それでも、20回以上のやり取りのうち16回は、直ったはずのものが直っていなかったためのやり直しでした。「直ったと思ったら、別の場所が崩れていた」「A5では直っていたのに、ほかの用紙サイズでは崩れたままだった」。このツールは用紙4種類×縦書き・横書きの8通りで見た目が変わるので、5分の目視ではとても見切れなかったのです。 つまり、人間がループの中にいても、品質は守られていませんでした。足りなかったのは人間の注意力ではありません。「直ったかどうか」を測る物差しです。 信じるのではなく、信じなくていいようにする そこで、物差しをプログラムで作りました。「どの図もページの境目をまたいでいない」「どの章もページの頭から始まる」を、ブラウザで実際に組んだ結果から測り、だめなら失敗で終わるテストです。8通りすべてに掛けても、人間の手間は増えません。 話はここで終わりません。このテスト自体にも穴がありました。わざと壊したツールに掛けてみたところ、横書きの崩れを一つも検出できていなかったのです。それでもテストは「合格」を返し続けていました。壊した版で試していなければ、何も測っていない物差しを信じていたことになります。 ここから言えるのは、「AIを信じるか、人間が全部見るか」の二択ではない、ということです。 合否を決める物差しを用意する その物差しが本当に測れているかも、わざと壊して確かめる この2つがあれば、AIの出力を一つひとつ信じて確かめる必要はなくなります。物差しが通さなければ、マージされないだけです。ただし物差しも完全ではありません。何を測っていて、何をまだ測っていないかを把握しておくのは、人間の仕事として残ります。 会社で「AIに全部やらせるのは危ない」と言われたら 企業では「中身を理解していないコードを本番に出すのは、説明責任の放棄だ」という声がよく上がります。この心配はもっともです。ただ、少し考えると、私たちはこれまでも、依存しているライブラリのコードを1行ずつ読んできたわけではありません。採用実績、脆弱性の監視、テストや監査の記録といった「信頼してよい根拠」を示すことで、説明責任を果たしてきました。 AIが書いたコードも同じです。人間がすべての行を読むのをやめる代わりに、人間が読むものを変えます。 仕様 —— 何を満たせば完成なのか テスト —— 仕様を正しく写しているか 間違えると被害が大きい箇所の差分 —— 認証、決済、データの削除など 差分をどこまで読むかは、一律に決めなくてかまいません。間違えたときの被害の大きさで変えます。画面の文言の修正ならテストの結果で十分ですし、認証や決済に触れる変更なら、人間が行単位で読みます。 それ以外は、テストの結果と記録で示します。「全部読みました」と言うより、検証できる根拠を示せる分、説明としてはむしろ強くなります。「フル自動は悪」と「全部任せればいい」の間には、この現実的な道があります。 明日からできる、小さな4歩 いきなり全部を自動化する必要はありません。コツは、注意書きを増やすのではなく、修正を「お願い」から「仕組み」へ一段ずつ下ろしていくことです。プロンプトや指示ファイルに書いた注意は、会話が長くなったりモデルが替わったりすると、いつの間にか埋もれます。テストや設定にしたものは、消すまで効き続けます。 4つも多いと感じたら、最初は一つだけでかまいません。次にAIに不具合の修正を頼むとき、直す前に、その不具合を再現するテストを書かせ、それが落ちるところを見せてもらってください。直したあとに同じテストが通れば、「直しました」を言葉ではなく結果で確かめたことになります。 そこから先は、次の4つへ広げていきます。どれも数日で試せて、どれも「AIを信じる」必要がありません。 本物のパスワードや鍵を、AIの作業場所に置かない —— 「.env を読まないで」とお願いするのではなく、そもそもファイルが存在しない状態にします。お願いは破られることがありますが、無いものは読めません。 「直しました」を信じず、CI の結果で判断する —— テストが通らなければマージできないよう、リポジトリの設定(ルールセットやブランチ保護)で縛ります。管理者も含めて例外を許さず、AIの使うアカウントには設定を変える権限を渡さなければ、AIにも人間にも抜け道がなくなります。 AIが一度ハマった失敗は、テストを1本書いて残す —— 注意書きを増やすより確実に、同じ失敗を防げます。そしてそのテストは、わざと壊した版で落ちることを確かめてから使います。 レビューの重心を、コードからテストと仕様へ移す —— テストや仕様の変更には人間の承認が必要になるよう設定し、AIがテストを書き換えて「合格」させる抜け道を塞ぎます。 この4つが揃うと、AIに任せる範囲を少しずつ広げても怖くなくなります。任せる範囲を決めるのは、AIの賢さではなく、物差しの確かさだからです。 もっと知りたい人へ この記事の背景にある考え方は、論文「自律型AIエージェント時代におけるソフトウェア工学の再定義」にまとめました。上の4歩をそのまま始められる雛形(エージェントへの指示ファイル AGENTS.md、仕様のテンプレート、CI とレビューの設定など)と、記事に出てきた組版ツール、そして「物差しの物差し」まで含めたテストも、同じリポジトリで公開しています。 リポジトリ:hamatz/agent-harness-kit 論文:github.com/hamatz/agent-harness-kit/blob/main/doc… リポジトリを取ってきて npm install のあと npm run demo を実行すると、わざと壊した組版ツールが検査で落ち、壊していない版が通る様子を数分で見られます。まずはそこから触ってみてください。 設計の段階については、Google が公開している Elephant-Goldfish Model というプラグインが参考になります。設計書をAIと一緒に書き、それまでの会話を知らない別のAIに査読させる仕組みです。このキットは、その後ろの「実装して、正しいと確かめる」段階を受け持つように作っています。 AIをどこまで信じるかで悩むのは、もうやめていいと思います。信じなくていい仕組みを、一つずつ足していきましょう。