Skip to content

Commit 8e8905f

Browse files
committed
demo(workflow): ADK Web wrapper for RFC google#93 authored workflows
A discoverable `root_agent` (a Workflow) that exposes the google#93 flow in ADK Web: the model authors a typed WorkflowSpec, ADK validates it against the capability registry, freezes spec+hash to session state, and executes it on the real engine via the google#92 supervisor — each step surfaced as a chat message so the authored plan, validation, capabilities, frozen hash, and final output are all visible in the ADK Web chat / State / Events surfaces. adk web contributing/samples/workflows/authored_workflow_demo Load-or-author: if a frozen spec already exists in the session it is REUSED (planner not re-invoked) and replayed — so the resume/reproducibility claim is real, verified by a CI-safe no-LLM test (test_demo_agent.py: import + name + registry + spec validation + reuse-path-with-stub-registry). Reuses the authored_workflow_spike/ stack; model from env (default gemini-2.5-flash; gemini-3.5-flash needs location=global); no hardcoded project. README is the ~7-min recording script. pyink/isort/mdformat clean; 4 demo tests pass.
1 parent da4f7aa commit 8e8905f

4 files changed

Lines changed: 531 additions & 0 deletions

File tree

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# ADK Web demo — model-authored typed Workflows (RFC #93)
2+
3+
A ~7-minute demo: a model authors a typed `WorkflowSpec`, ADK validates it
4+
against a capability registry, freezes it to session state, and executes it on
5+
the real ADK engine via the #92 supervisor — all visible in ADK Web's chat,
6+
state, and event surfaces. **ADK Web sells the product fit; pytest/CI sells the
7+
correctness.**
8+
9+
## 0. Configure a model (no hardcoded project)
10+
11+
```bash
12+
export GOOGLE_GENAI_USE_VERTEXAI=1
13+
export GOOGLE_CLOUD_PROJECT=<your-project>
14+
export GOOGLE_CLOUD_LOCATION=global # gemini-3.5-flash serves from `global`
15+
export SPIKE_GEMINI_MODEL=gemini-3.5-flash # or any flash model you can access
16+
```
17+
18+
## 1. Thesis (20s)
19+
20+
- **#92** is the supervised concurrent executor (`DynamicNodeSupervisor` + `ctx.pipeline`).
21+
- **#93** is the model-authored typed `WorkflowSpec` layer.
22+
- The demo: a model authors a *validated* plan, then ADK executes that *frozen* plan reproducibly.
23+
24+
## 2. ADK Web walkthrough (3–5 min)
25+
26+
```bash
27+
adk web contributing/samples/workflows/authored_workflow_demo \
28+
--port 8000 --session_service_uri "sqlite:///demo_sessions.db"
29+
```
30+
31+
Open the UI, pick `security_audit_planner`, and send:
32+
33+
```text
34+
Plan and run a codebase security review.
35+
```
36+
37+
Point at the ADK-native evidence as it streams:
38+
39+
1. **Authored `WorkflowSpec`** — the chat shows the JSON plan (`fan_out → step → step`).
40+
1. **Validation** — "Validation passed" + the capability list (all registered).
41+
1. **Frozen spec + hash** — open the **State** tab: `authored_workflow:frozen_spec` and `…_hash`.
42+
1. **Execution** — the **Events / trace** view shows the `reviewer` fan-out, then `triager`, then `formatter` node runs.
43+
1. **Final output** — the triaged audit (e.g. 3 HIGH + 1 MEDIUM across `auth.py`/`db.py`/`net.py`/`math.py`).
44+
45+
(Re-send the same prompt to show resume reuses the frozen spec — same hash, not re-authored.)
46+
47+
## 3. Shape sweep — not a one-off (1–2 min)
48+
49+
```bash
50+
SPIKE_LIVE=1 pytest \
51+
contributing/samples/workflows/authored_workflow_spike/test_live_planner_sweep.py -q -s
52+
```
53+
54+
Proof points: multi-stage `fan_out → step → step`; branch `step → branch`; loop `loop_until`.
55+
56+
## 4. Correctness proof (60s)
57+
58+
```bash
59+
pytest contributing/samples/workflows/dynamic_supervisor_spike/test_dynamic_supervisor_spike.py -q # 11
60+
pytest contributing/samples/workflows/authored_workflow_spike/test_authoring.py -q # 10
61+
pytest contributing/samples/workflows/authored_workflow_demo/test_demo_agent.py -q # 4
62+
```
63+
64+
- Deterministic suites: #92 **11** + #93 **10** + demo **4** = **25** (incl. a no-LLM reuse-path test).
65+
- PR #3 CI green except the documented fork-only `agent-triage` token job.
66+
67+
## Recording notes
68+
69+
- macOS `Cmd+Shift+5` or Loom; browser at 110–125% zoom, terminal font 16+.
70+
- Hide project IDs / env vars. Keep it under ~7 minutes.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent # noqa: F401
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""ADK Web demo agent for RFC #93 — model-authored typed Workflows.
16+
17+
`root_agent` is a `Workflow` whose single node:
18+
1. asks a planner `LlmAgent(output_schema=WorkflowSpec)` to author a plan,
19+
2. validates it (`WorkflowSpecValidator`) against a capability registry,
20+
3. persists the frozen spec + hash to session state,
21+
4. executes it on the real ADK engine via the #92 supervisor,
22+
surfacing each step as a chat message so the ADK Web UI shows the authored
23+
plan, validation, capabilities, frozen hash, and final output. Run with:
24+
25+
adk web contributing/samples/workflows/authored_workflow_demo
26+
27+
Configure a model first (no hardcoded project):
28+
export GOOGLE_GENAI_USE_VERTEXAI=1 GOOGLE_CLOUD_PROJECT=<project>
29+
export GOOGLE_CLOUD_LOCATION=global SPIKE_GEMINI_MODEL=gemini-3.5-flash
30+
"""
31+
32+
from __future__ import annotations
33+
34+
import hashlib
35+
import json
36+
import os
37+
import sys
38+
from typing import Literal
39+
40+
from google.adk import Agent
41+
from google.adk import Context
42+
from google.adk import Event
43+
from google.adk import Workflow
44+
from google.adk.workflow import node
45+
from google.genai import types
46+
from pydantic import BaseModel
47+
48+
# Reuse the committed #93 authoring stack (sibling sample dir).
49+
sys.path.insert(
50+
0,
51+
os.path.join(
52+
os.path.dirname(os.path.abspath(__file__)),
53+
"..",
54+
"..",
55+
"authored_workflow_spike",
56+
),
57+
)
58+
from authoring import Capability # noqa: E402
59+
from authoring import CapabilityRegistry # noqa: E402
60+
from authoring import SpecInterpreter # noqa: E402
61+
from authoring import WorkflowSpec # noqa: E402
62+
from authoring import WorkflowSpecValidator # noqa: E402
63+
64+
MODEL = os.environ.get("SPIKE_GEMINI_MODEL", "gemini-2.5-flash")
65+
DET = types.GenerateContentConfig(temperature=0)
66+
67+
# A small, deliberately-mixed codebase to audit (3 vulnerable, 1 safe).
68+
FILES = [
69+
{
70+
"path": "auth.py",
71+
"code": "def login(pw): return pw == 'admin123' # hardcoded",
72+
},
73+
{
74+
"path": "db.py",
75+
"code": "q = 'SELECT * FROM users WHERE id=' + request.args['id']",
76+
},
77+
{"path": "net.py", "code": "os.system('ping ' + user_supplied_host)"},
78+
{"path": "math.py", "code": "def mean(xs):\n return sum(xs) / len(xs)"},
79+
]
80+
81+
82+
class Finding(BaseModel):
83+
path: str
84+
severity: Literal["CRITICAL", "HIGH", "MEDIUM", "LOW", "NONE"]
85+
issue: str
86+
87+
88+
class ReportFixed(BaseModel):
89+
total: int
90+
critical: int
91+
high: int
92+
medium: int
93+
low: int
94+
none: int
95+
summary: str
96+
97+
98+
class Note(BaseModel):
99+
note: str
100+
101+
102+
def _registry() -> CapabilityRegistry:
103+
return CapabilityRegistry([
104+
Capability(
105+
name="reviewer",
106+
input_kind="item",
107+
output_model=Finding,
108+
serialize_input=True,
109+
max_fan_out=50,
110+
build=lambda: Agent(
111+
name="reviewer",
112+
model=MODEL,
113+
output_schema=Finding,
114+
generate_content_config=DET,
115+
instruction=(
116+
"Input JSON with keys path and code. Output a Finding"
117+
" (echo the path)."
118+
),
119+
),
120+
),
121+
Capability(
122+
name="triager",
123+
input_kind="list",
124+
output_model=ReportFixed,
125+
serialize_input=True,
126+
build=lambda: Agent(
127+
name="triager",
128+
model=MODEL,
129+
output_schema=ReportFixed,
130+
generate_content_config=DET,
131+
instruction=(
132+
"Input: a JSON list of Findings. Output ReportFixed:"
133+
" total, per-severity counts (must sum to total), and"
134+
" a one-line summary."
135+
),
136+
),
137+
),
138+
Capability(
139+
name="formatter",
140+
input_kind="item",
141+
output_model=Note,
142+
serialize_input=True,
143+
build=lambda: Agent(
144+
name="formatter",
145+
model=MODEL,
146+
output_schema=Note,
147+
generate_content_config=DET,
148+
instruction=(
149+
"Input: a ReportFixed JSON. Output a Note: a one-line"
150+
" markdown bullet summarizing the audit."
151+
),
152+
),
153+
),
154+
])
155+
156+
157+
_REGISTRY_DESC = (
158+
"reviewer (item: a file with path and code -> Finding), triager (LIST of"
159+
" Findings -> ReportFixed), formatter (item: a ReportFixed -> Note)."
160+
)
161+
_PLANNER_INSTR = (
162+
"Author a WorkflowSpec using ONLY these capabilities: "
163+
+ _REGISTRY_DESC
164+
+ " The task input has a 'files' list of objects with path and code."
165+
" Author:"
166+
" a fan_out of reviewer over task.files, then a step running triager on the"
167+
" findings, then a step running formatter on the report. Use"
168+
" Binding(source='task', path='files') and Binding(source='step',"
169+
" step=<id>) to chain. Set output to the formatter step."
170+
)
171+
172+
173+
def _msg(text: str) -> Event:
174+
return Event(
175+
content=types.Content(role="model", parts=[types.Part(text=text)])
176+
)
177+
178+
179+
def _hash(spec: WorkflowSpec) -> str:
180+
return hashlib.sha256(
181+
json.dumps(spec.model_dump(), sort_keys=True).encode()
182+
).hexdigest()[:12]
183+
184+
185+
@node(rerun_on_resume=True)
186+
async def author_validate_execute(ctx: Context, node_input):
187+
reg = _registry()
188+
189+
# 1. LOAD-OR-AUTHOR. If a frozen spec exists in this session, REUSE it (do not
190+
# re-author) — this is the resume/reproducibility claim. Otherwise the model
191+
# authors a fresh typed WorkflowSpec (data, not code).
192+
existing = ctx.state.get("authored_workflow:frozen_spec")
193+
if existing:
194+
spec = WorkflowSpec.model_validate(existing)
195+
spec_hash = ctx.state.get("authored_workflow:frozen_spec_hash") or _hash(
196+
spec
197+
)
198+
reused = True
199+
yield _msg(
200+
f"♻️ **Reusing frozen plan** from session state — hash `{spec_hash}`. "
201+
"The model is NOT re-invoked; the exact prior plan is replayed."
202+
)
203+
else:
204+
reused = False
205+
yield _msg(
206+
"🧭 **Model-authored Workflow** — planning a security audit over "
207+
f"{len(FILES)} files using only registered capabilities "
208+
"(`reviewer`, `triager`, `formatter`)."
209+
)
210+
planner = Agent(
211+
name="planner",
212+
model=MODEL,
213+
output_schema=WorkflowSpec,
214+
generate_content_config=DET,
215+
instruction=_PLANNER_INSTR,
216+
)
217+
raw = await ctx.run_node(
218+
planner,
219+
node_input=f"Audit these files: {[f['path'] for f in FILES]}.",
220+
run_id="plan",
221+
)
222+
spec = WorkflowSpec.model_validate(raw)
223+
spec_hash = _hash(spec)
224+
steps = " → ".join(s.kind for s in spec.steps)
225+
yield _msg(
226+
f"📋 **Authored plan** (`{steps}`):\n```json\n"
227+
f"{json.dumps(spec.model_dump(), indent=1)}\n```"
228+
)
229+
230+
# 2. VALIDATE — semantic validation against the registry (always).
231+
warnings = WorkflowSpecValidator(reg).validate(spec) # raises on hard error
232+
caps = sorted({getattr(s, "capability", None) for s in spec.steps} - {None})
233+
yield _msg(
234+
"✅ **Validation passed.** Capabilities referenced (all registered): "
235+
f"`{caps}`."
236+
+ (f"\n⚠️ warnings: {warnings}" if warnings else "")
237+
)
238+
239+
# 3. FREEZE — persist spec + hash to session state on first author only
240+
# (visible in the State tab; reused runs already have it).
241+
if not reused:
242+
ctx.state["authored_workflow:frozen_spec"] = spec.model_dump()
243+
ctx.state["authored_workflow:frozen_spec_hash"] = spec_hash
244+
yield _msg(
245+
f"🔒 **Frozen spec** persisted to session state — hash `{spec_hash}`. "
246+
"Re-send the prompt: it replays this exact plan, not a new one."
247+
)
248+
249+
# 4. EXECUTE — run the validated plan on the real ADK engine (#92 supervisor).
250+
result = await SpecInterpreter(reg, ctx).execute(spec, {"files": FILES})
251+
yield _msg(
252+
"📄 **Audit result:**"
253+
f" {result.get('note') if isinstance(result, dict) else result}"
254+
)
255+
yield Event(
256+
output={
257+
"hash": spec_hash,
258+
"result": result,
259+
"capabilities": caps,
260+
"reused": reused,
261+
}
262+
)
263+
264+
265+
root_agent = Workflow(
266+
name="security_audit_planner",
267+
edges=[("START", author_validate_execute)],
268+
)

0 commit comments

Comments
 (0)