State Transforms¶
S factories return dict transforms that compose with >> as zero-cost workflow nodes. They manipulate session state between agent steps without making LLM calls.
flowchart TB
INPUT["<b>Input state</b><br>{ web, scholar, _debug }"] --> DROP
DROP["S.drop('_debug')<br>→ { web, scholar }"] --> MERGE
MERGE["S.merge('web','scholar', into='research')<br>→ { web, scholar, research }"] --> DEFAULT
DEFAULT["S.default(confidence=0.0)<br>→ { web, scholar, research, confidence }"] --> RENAME
RENAME["S.rename(research='input')<br>→ { web, scholar, input, confidence }"] --> PICK
PICK["S.pick('input','confidence')<br>→ { input, confidence }"] --> OUTPUT
OUTPUT["<b>Output state</b><br>{ input, confidence }"]
style INPUT fill:#FFF3E0,stroke:#E65100,color:#1A1A1A
style OUTPUT fill:#ecfdf5,stroke:#10b981,color:#1A1A1A
style DROP fill:#fef2f2,stroke:#e94560,color:#1A1A1A
style MERGE fill:#e0f2fe,stroke:#0ea5e9,color:#1A1A1A
style DEFAULT fill:#FFF8E1,stroke:#f59e0b,color:#1A1A1A
style RENAME fill:#f3e5f5,stroke:#a78bfa,color:#1A1A1A
style PICK fill:#ecfdf5,stroke:#10b981,color:#1A1A1A
Composition operators:
S.drop() >> S.merge() # chain: run in sequence
S.default() + S.rename() # combine: apply to same state
S.drop().pipe(S.merge_(["a", "b"], "c")); // chain: run in sequence
S.default_({ k: 1 }).add(S.rename({ a: "b" })); // combine: apply to same state
Note
TypeScript naming
JavaScript reserves the default keyword, so S.default becomes S.default_ and S.merge becomes S.merge_ in TypeScript. The signature also flips: S.merge_(["a","b"], "into") takes the keys as an array and the destination as a positional argument.
Basic Usage¶
from adk_fluent import S
pipeline = (
(web_agent | scholar_agent)
>> S.merge("web", "scholar", into="research")
>> S.default(confidence=0.0)
>> S.rename(research="input")
>> writer_agent
)
import { S } from "adk-fluent-ts";
const pipeline = webAgent
.parallel(scholarAgent)
.then(S.merge_(["web", "scholar"], "research"))
.then(S.default_({ confidence: 0.0 }))
.then(S.rename({ research: "input" }))
.then(writerAgent);
Transform Reference¶
Factory |
Purpose |
|---|---|
|
Keep only specified keys |
|
Remove specified keys |
|
Rename keys |
|
Fill missing keys |
|
Combine keys |
|
Map a single value |
|
Derive new keys |
|
Assert invariant |
|
Debug-print |
S.pick(*keys)¶
Keep only the specified keys in state, dropping everything else:
# After this, state only contains "name" and "email"
pipeline = agent >> S.pick("name", "email") >> next_agent
const pipeline = agent.then(S.pick("name", "email")).then(nextAgent);
S.drop(*keys)¶
Remove the specified keys from state:
# Remove temporary/internal keys before the next step
pipeline = agent >> S.drop("_internal", "_debug") >> next_agent
const pipeline = agent.then(S.drop("_internal", "_debug")).then(nextAgent);
S.rename(...)¶
Rename keys in state, mapping old names to new names:
# Rename "research" to "input" for the next agent
pipeline = researcher >> S.rename(research="input") >> writer
const pipeline = researcher.then(S.rename({ research: "input" })).then(writer);
S.default(...)¶
Fill in missing keys with default values. Existing keys are not overwritten:
# Ensure "confidence" exists with a default of 0.0
pipeline = agent >> S.default(confidence=0.0) >> evaluator
const pipeline = agent.then(S.default_({ confidence: 0.0 })).then(evaluator);
S.merge(...)¶
Combine multiple keys into a single key:
# Merge "web" and "scholar" results into "research"
pipeline = (
(web_agent | scholar_agent)
>> S.merge("web", "scholar", into="research")
>> writer
)
const pipeline = webAgent
.parallel(scholarAgent)
.then(S.merge_(["web", "scholar"], "research"))
.then(writer);
S.transform(key, fn)¶
Apply a function to transform a single value in state:
# Uppercase the "title" value
pipeline = agent >> S.transform("title", str.upper) >> next_agent
const pipeline = agent
.then(S.transform("title", (v) => String(v).toUpperCase()))
.then(nextAgent);
S.compute(...)¶
Derive new keys by applying functions to the state:
# Compute a new "summary_length" key from the existing state
pipeline = agent >> S.compute(
summary_length=lambda s: len(s.get("summary", "")),
has_citations=lambda s: "cite" in s.get("text", "").lower()
) >> evaluator
const pipeline = agent
.then(
S.compute({
summary_length: (s) => String(s.summary ?? "").length,
has_citations: (s) => String(s.text ?? "").toLowerCase().includes("cite"),
}),
)
.then(evaluator);
S.guard(pred)¶
Assert an invariant on the state. Raises an error if the predicate fails:
# Ensure "data" key is present before proceeding
pipeline = agent >> S.guard(lambda s: "data" in s) >> processor
const pipeline = agent
.then(S.guard((s) => "data" in s, "missing data key"))
.then(processor);
S.log(*keys)¶
Debug-print specified keys from state. Useful during development:
# Print "web" and "scholar" values for debugging
pipeline = (
(web_agent | scholar_agent)
>> S.log("web", "scholar")
>> S.merge("web", "scholar", into="research")
>> writer
)
const pipeline = webAgent
.parallel(scholarAgent)
.then(S.log("web", "scholar"))
.then(S.merge_(["web", "scholar"], "research"))
.then(writer);
STransform Composition¶
All S.xxx() methods return STransform objects that support two composition operators:
Chain (sequential)¶
# Clean state then merge — runs in order
cleanup = S.drop("_internal") >> S.merge("web", "scholar", into="research")
pipeline = agent >> cleanup >> writer
const cleanup = S.drop("_internal").pipe(S.merge_(["web", "scholar"], "research"));
const pipeline = agent.then(cleanup).then(writer);
Combine (parallel)¶
# Apply multiple transforms to the same state at once
setup = S.default(confidence=0.0) + S.rename(research="input")
pipeline = agent >> setup >> writer
const setup = S.default_({ confidence: 0.0 }).add(S.rename({ research: "input" }));
const pipeline = agent.then(setup).then(writer);
Composing with agents¶
STransform objects work seamlessly in pipelines:
# STransform >> Agent works directly
pipeline = S.capture("user_message") >> classifier >> handler
const pipeline = S.capture("user_message").then(classifier).then(handler);
Key tracking¶
STransforms track which state keys they read and write:
t = S.rename(old="new")
print(t._reads_keys) # frozenset({'old'})
print(t._writes_keys) # frozenset({'new'})
This metadata powers the contract checker’s data-flow analysis.
Complete Example¶
The following pipeline demonstrates multiple transforms woven between agents:
Complete transform flow:
┬─ web ────┐
└─ scholar ┘ (parallel)
│
S.log ──► S.merge ──► S.default ──► S.rename
debug combine fill gaps rewire
│
writer @ Report
│
S.guard(confidence > 0) ── fail? ──► raise
from pydantic import BaseModel
from adk_fluent import Agent, S, until
class Report(BaseModel):
title: str
body: str
confidence: float
pipeline = (
( Agent("web").model("gemini-2.5-flash").instruct("Search web.")
| Agent("scholar").model("gemini-2.5-flash").instruct("Search papers.")
)
>> S.log("web", "scholar") # Debug
>> S.merge("web", "scholar", into="research") # Combine
>> S.default(confidence=0.0) # Default
>> S.rename(research="input") # Rename
>> Agent("writer").model("gemini-2.5-flash").instruct("Write.") @ Report
>> S.guard(lambda s: s.get("confidence", 0) > 0) # Assert
)
import { Agent, S } from "adk-fluent-ts";
const ReportSchema = {
type: "object",
properties: {
title: { type: "string" },
body: { type: "string" },
confidence: { type: "number" },
},
required: ["title", "body", "confidence"],
} as const;
const pipeline = new Agent("web", "gemini-2.5-flash")
.instruct("Search web.")
.parallel(new Agent("scholar", "gemini-2.5-flash").instruct("Search papers."))
.then(S.log("web", "scholar")) // Debug
.then(S.merge_(["web", "scholar"], "research")) // Combine
.then(S.default_({ confidence: 0.0 })) // Default
.then(S.rename({ research: "input" })) // Rename
.then(new Agent("writer", "gemini-2.5-flash").instruct("Write.").outputAs(ReportSchema))
.then(S.guard((s) => Number(s.confidence ?? 0) > 0, "confidence must be > 0")); // Assert