Specialist Agents¶
The orchestrator-pattern notebook introduced the Specialist as the worker an orchestrator hands tasks to. This notebook dives into the Specialist itself, using RIGHTSIZER — a cloud cost-and-capacity specialist — as the running case: how to narrow a model's failure surface with a focused system prompt, a hand-picked tool set, optional playbooks, and a confidence threshold.
RIGHTSIZER fingerprints a compute workload for rightsizing: it maps observable
utilization telemetry (CPU p50, memory mean, IOPS, network throughput) to a
(family, size, action) verdict, then only ships the recommendation once the
evidence clears a grounding threshold — an under-observed instance abstains
rather than guessing.
This notebook covers:
Specialist— a TulipAgentwith role metadata (specialist_type,description), a tool list, and aconfidence_threshold.Playbook+PlaybookStep— encode a procedure: preconditions, ordered steps with required tools and expected outputs, plus failure handling.specialist.select_playbook(task)— picks one playbook from a pool by matching the task description.- Grounding the verdict with the domain-neutral GSAR core
(
Claim,EvidenceType,Partition,gsar_score,decide,Decision) — ship a recommendation only when the evidence partition scores above the proceed threshold; abstain otherwise. - Pre-built helpers (
create_log_analyst,create_metrics_analyst,create_trace_analyst,create_code_analyst) for common observability domains.
Prerequisites¶
- Agent basics.
- The orchestrator-pattern notebook — Specialists are the workers it routes to.
Run¶
The default provider is the bundled mock model. Set TULIP_MODEL_PROVIDER
(openai / anthropic) and credentials to use a live model. Set
TULIP_MODEL_PROVIDER=mock for offline runs.
Source¶
# Copyright 2026 Tulip Labs
# SPDX-License-Identifier: Apache-2.0
"""
Notebook 27: RIGHTSIZER — the cloud-rightsizing specialist.
Notebook 26 introduced the Specialist as the worker MARSHAL hands tasks
to. This notebook dives into the Specialist itself, using RIGHTSIZER —
the platform team's cloud cost-and-capacity specialist — as the running
case: how to narrow a model's failure surface with a focused system
prompt, a hand-picked tool set, optional playbooks (your runbooks,
encoded), and a confidence threshold.
RIGHTSIZER's job is workload fingerprinting for rightsizing: identifying
which instance family and size a compute workload should run on purely
from observable utilization telemetry (CPU p50, memory mean, IOPS,
network throughput) — no agent on the box, no guesswork. The payoff is
cutting waste on over-provisioned instances and catching capacity risk
on hot ones before it pages someone.
The capability is demonstrated with the domain-neutral GSAR grounding
core (``tulip.reasoning.gsar``): a deterministic classifier maps a
utilization feature vector to a ``WorkloadVerdict``, and the verdict
becomes a recommendation **only** when the evidence partition clears the
grounding threshold. Low feature coverage yields a weak evidence
partition, so an under-observed instance abstains rather than asserting a
rightsizing — a wrong instance-type call is worse than none.
- A ``Specialist`` is a Tulip ``Agent`` with role metadata
(``specialist_type``, ``description``), a tool list, and a
``confidence_threshold`` that gates "good enough" answers.
- ``Playbook`` and ``PlaybookStep`` encode standard procedures —
preconditions, ordered steps with required tools and expected
outputs, plus failure handling.
- A specialist can carry multiple playbooks; ``select_playbook(task)``
picks one based on task description matching.
- Pre-built helpers (``create_log_analyst``, ``create_metrics_analyst``,
``create_trace_analyst``, ``create_code_analyst``) ship with sensible
defaults for evidence domains every investigation touches.
Run it:
.venv/bin/python examples/notebook_27_specialist_agents.py
The default provider is the bundled mock model. Set TULIP_MODEL_PROVIDER=openai
(or anthropic) and the matching credentials to use a live model. Set
``TULIP_MODEL_PROVIDER=mock`` for offline runs.
Prerequisites:
- Notebook 06 (Agent basics).
- Notebook 26 (Orchestrator) — Specialists are the workers MARSHAL routes to.
"""
import asyncio
import time
from collections.abc import Mapping
from dataclasses import dataclass
from config import get_model, print_config
from tulip.agent import Agent
from tulip.multiagent.specialist import (
Playbook,
PlaybookStep,
Specialist,
create_code_analyst,
create_log_analyst,
create_metrics_analyst,
create_trace_analyst,
)
from tulip.reasoning.gsar import (
Claim,
Decision,
EvidenceType,
Partition,
decide,
gsar_score,
)
from tulip.tools.decorator import tool
def _llm_call(
prompt: str, *, system: str = "Reply in one short sentence.", max_tokens: int = 80
) -> str:
"""One model call with a timing/token banner — used for commentary."""
agent = Agent(model=get_model(max_tokens=max_tokens), system_prompt=system)
t0 = time.perf_counter()
res = agent.run_sync(prompt)
dt = time.perf_counter() - t0
print(
f" [model call: {dt:.2f}s · {res.metrics.prompt_tokens}→{res.metrics.completion_tokens} tokens]"
)
return res.message.strip()
# The expected utilization-feature schema for a rightsizing verdict.
# Coverage is the fraction of these the telemetry pull actually returned;
# below this floor the classifier returns low confidence and grounding abstains.
_WORKLOAD_FEATURES = ("cpu_p50", "mem_mean", "iops_mean", "net_mbps")
_COVERAGE_FLOOR = 0.60
@dataclass
class WorkloadVerdict:
"""A rightsizing call: which family/size a workload fits, and how sure."""
family: str
size: str
action: str
confidence: float
feature_coverage: float
def _sample_utilization(instance: str) -> dict[str, float]:
"""Deterministic offline utilization sample for an instance.
A real deployment swaps in a CloudWatch / Stackdriver query behind
this same signature; offline we return a fixed feature vector so the
notebook runs end-to-end with no cloud credentials.
"""
return {"cpu_p50": 18.4, "mem_mean": 22.1, "iops_mean": 140.0, "net_mbps": 31.5}
def classify_workload(features: Mapping[str, float]) -> WorkloadVerdict:
"""Deterministic mock rightsizing classifier.
Maps a utilization feature vector to a ``(family, size, action)``
verdict over a fixed lookup — no model file, no scikit, no network.
A real deployment swaps in a rightsizing service behind this same
signature. Coverage below :data:`_COVERAGE_FLOOR` returns a
low-confidence "unknown" verdict so the caller abstains.
Args:
features: Observed utilization features keyed by name.
Returns:
A :class:`WorkloadVerdict`.
"""
observed = [f for f in _WORKLOAD_FEATURES if f in features]
coverage = len(observed) / len(_WORKLOAD_FEATURES)
if coverage < _COVERAGE_FLOOR:
return WorkloadVerdict(
family="unknown",
size="unknown",
action="unknown",
confidence=0.30,
feature_coverage=coverage,
)
# Fixed lookup: sustained low CPU p50 with memory headroom reads as an
# over-provisioned workload that fits a smaller general-purpose size.
cpu = features.get("cpu_p50", 100.0)
if cpu <= 25.0:
return WorkloadVerdict(
family="general-purpose",
size="m6i.large",
action="downsize",
confidence=0.91,
feature_coverage=coverage,
)
return WorkloadVerdict(
family="compute-optimized",
size="c6i.2xlarge",
action="upsize",
confidence=0.78,
feature_coverage=coverage,
)
async def main():
print("=" * 60)
print("Notebook 27: RIGHTSIZER — the cloud-rightsizing specialist")
print("=" * 60)
print()
print_config()
model = get_model()
# =========================================================================
# Part 1: anatomy of a Specialist
# =========================================================================
print("\n=== Part 1: Specialist Anatomy ===\n")
# A specialist pairs a focused system prompt with domain tools,
# optional playbooks, and a confidence threshold. RIGHTSIZER is the
# platform team's cost-and-capacity specialist.
specialist = Specialist(
name="RIGHTSIZER",
specialist_type="cloud_rightsizing",
description="Recommends instance rightsizing from utilization telemetry",
system_prompt="""You are RIGHTSIZER, a cloud cost-and-capacity specialist. Your expertise:
1. Pulling utilization telemetry (CPU p50, memory mean, IOPS, network throughput)
2. Mapping a utilization feature vector to a (family, size, action) verdict
3. Reconciling a recommendation against the approved instance catalog
4. Flagging over-provisioned waste and capacity risk on hot fleets
When rightsizing:
- Treat a single data point as a hint, not evidence — collect a stable feature vector
- Report feature coverage; abstain when too little of the schema is observed
- Never assert a rightsizing you cannot ground — a wrong instance-type call is worse than none""",
max_iterations=10,
confidence_threshold=0.85,
model=model,
)
print(f"Specialist: {specialist.name}")
print(f" Type: {specialist.specialist_type}")
print(f" Max iterations: {specialist.max_iterations}")
print(f" Confidence threshold: {specialist.confidence_threshold}")
t0 = time.perf_counter()
p1 = await specialist.execute(task="In one sentence, what is your specialty?")
dt = time.perf_counter() - t0
print(f" [model call: {dt:.2f}s · specialist.execute()]")
if p1.output:
print(f" Smoke output: {p1.output[:160]}")
# =========================================================================
# Part 2: hand-pick tools for the domain
# =========================================================================
print("\n=== Part 2: Domain Tools ===\n")
@tool(name="pull_utilization", description="Pull utilization telemetry for an instance")
async def pull_utilization(instance: str, source: str = "cloudwatch") -> str:
# Query the metrics backend (CloudWatch / Stackdriver) where the
# measurement actually lives; falls back to a deterministic sample
# offline so the notebook runs with no cloud credentials.
feats = _sample_utilization(instance)
observed = sum(1 for f in _WORKLOAD_FEATURES if f in feats)
body = " ".join(f"{k}={v}" for k, v in feats.items())
return (
f"Telemetry {instance} via {source}: {body} "
f"({observed}/{len(_WORKLOAD_FEATURES)} features observed)"
)
@tool(
name="classify_instance", description="Map the utilization vector to a rightsizing verdict"
)
async def classify_instance() -> str:
return "WorkloadVerdict: downsize to m6i.large (general-purpose), confidence 0.91, coverage 1.00"
@tool(
name="check_catalog", description="Compare a verdict against the approved instance catalog"
)
async def check_catalog() -> str:
return "Catalog: instance i-0abc123def currently on c6i.2xlarge; m6i.large is an approved target"
specialist = Specialist(
name="RIGHTSIZER",
specialist_type="cloud_rightsizing",
description="Recommends instance rightsizing from utilization telemetry",
system_prompt="You recommend instance rightsizing from utilization telemetry.",
tools=[pull_utilization, classify_instance, check_catalog],
model=model,
)
print(f"Tools available: {[t.name for t in specialist.tools]}")
print(
f"AI commentary: {_llm_call('In one sentence, why does giving a Specialist domain-specific cloud tools dramatically narrow its failure surface?')}"
)
# =========================================================================
# Part 3: encode a runbook as a Playbook
# =========================================================================
print("\n=== Part 3: Specialist Playbooks ===\n")
rightsizing_playbook = Playbook(
name="Rightsizing Procedure",
description="Standard procedure for rightsizing a compute instance",
preconditions=[
"Instance is in scope for the cost-optimization review",
"Telemetry covers a representative window, not a quiet weekend",
],
steps=[
PlaybookStep(
instruction="Pull the utilization feature vector",
required_tools=["pull_utilization"],
expected_output="CPU p50, memory mean, IOPS, network throughput, and coverage",
),
PlaybookStep(
instruction="Classify the feature vector to a family/size/action verdict",
required_tools=["classify_instance"],
expected_output="WorkloadVerdict with confidence and feature coverage",
on_failure="Abstain if feature coverage is below the schema floor",
),
PlaybookStep(
instruction="Compare the verdict against the approved instance catalog",
required_tools=["check_catalog"],
expected_output="Match / mismatch against what the catalog allows",
),
],
success_criteria="Grounded verdict reconciled against the catalog, or a logged abstention",
)
specialist.playbooks.append(rightsizing_playbook)
print(f"Playbook: {rightsizing_playbook.name}")
print(f" Preconditions: {rightsizing_playbook.preconditions}")
print(f" Steps: {len(rightsizing_playbook.steps)}")
print(f" Success criteria: {rightsizing_playbook.success_criteria}")
# ``to_prompt()`` renders the playbook as the text block injected
# into the system prompt when the playbook is selected.
playbook_prompt = rightsizing_playbook.to_prompt()
print("\nPlaybook prompt:")
print("-" * 40)
print(playbook_prompt[:500] + "...")
print(
f"AI commentary: {_llm_call('In one sentence, when does attaching a fixed Playbook to a cloud Specialist matter most?')}"
)
# =========================================================================
# Part 4: pick the right playbook from a pool
# =========================================================================
print("\n=== Part 4: Playbook Selection ===\n")
cost_anomaly_playbook = Playbook(
name="Cost Anomaly Triage",
description="Procedure for investigating a sudden cloud-spend spike",
steps=[
PlaybookStep(instruction="Break the spend spike down by service and account"),
PlaybookStep(instruction="Check whether new resources were launched in the window"),
PlaybookStep(instruction="Correlate the spike against the deploy and scaling history"),
],
)
config_drift_playbook = Playbook(
name="Configuration Drift Investigation",
description="Procedure for investigating an instance running an unexpected type",
steps=[
PlaybookStep(instruction="Re-pull telemetry to confirm the running instance type"),
PlaybookStep(instruction="Diff the running type against the approved catalog record"),
PlaybookStep(instruction="Escalate a confirmed mismatch to the change-control owner"),
],
)
specialist.playbooks.extend([cost_anomaly_playbook, config_drift_playbook])
# select_playbook matches the task description against each playbook's
# name and description; it returns None if nothing fits.
tasks = [
"Recommend rightsizing for instance i-0abc123def for ticket FIN-2026-017",
"Investigate a sudden cloud-spend spike on the public-api fleet",
"Investigate an instance that appears to be running an unexpected type",
]
for task in tasks:
selected = specialist.select_playbook(task)
if selected:
print(f"Task: '{task[:40]}...'")
print(f" Selected playbook: {selected.name}")
print(
f"AI commentary: {_llm_call('In one sentence, why is automatic playbook selection by task description risky for cloud ops and how do you mitigate it?')}"
)
# =========================================================================
# Part 4b: ground the verdict — emit a recommendation only with evidence
# =========================================================================
# RIGHTSIZER's verdict is not a recommendation until it clears the GSAR
# grounding threshold. We partition the evidence, score it with
# ``gsar_score``, and ``decide`` whether to ship or abstain; a weak
# partition abstains. This is the domain-neutral grounding core that
# keeps a confident-sounding model from acting on thin evidence.
print("\n=== Part 4b: Grounding the Verdict ===\n")
# Full-coverage telemetry: 4/4 utilization features observed. The
# classifier returns a high-confidence verdict; the utilization feature
# vector is the recommendation's evidence, partitioned by GSAR type.
full_features = {"cpu_p50": 18.4, "mem_mean": 22.1, "iops_mean": 140.0, "net_mbps": 31.5}
verdict = classify_workload(full_features)
print(
f"WorkloadVerdict (coverage {verdict.feature_coverage:.0%}): "
f"{verdict.action} to {verdict.size} ({verdict.family}) "
f"@ confidence {verdict.confidence:.0%}"
)
grounded_partition = Partition(
grounded=[
Claim(
text="CPU p50 18.4% sits well below the 40% rightsizing band",
type=EvidenceType.TOOL_MATCH,
evidence_refs=["telemetry:cpu_p50=18.4"],
),
Claim(
text="memory mean 22.1% confirms ample headroom on the current size",
type=EvidenceType.SPECIFIC_DATA,
evidence_refs=["telemetry:mem_mean=22.1"],
),
Claim(
text="IOPS mean 140 fits comfortably inside the m6i.large envelope",
type=EvidenceType.SIGNAL_MATCH,
evidence_refs=["telemetry:iops_mean=140.0"],
),
],
)
score = gsar_score(grounded_partition)
decision = decide(score)
if decision == Decision.PROCEED:
print(f" SHIPPED recommendation: {verdict.action} i-0abc123def to {verdict.size}")
print(f" gsar_score={score:.2f} decision={decision.value}")
else:
print(
f" abstained ({decision.value}): grounding score {score:.2f} below proceed threshold"
)
# Low-coverage telemetry: 1/4 features. The classifier returns "unknown"
# at low confidence; the partition is a lone inference claim, so GSAR
# decides replan rather than asserting a rightsizing.
sparse_features = {"cpu_p50": 41.0}
weak_verdict = classify_workload(sparse_features)
print(
f"\nUnder-observed instance (coverage {weak_verdict.feature_coverage:.0%}): "
f"classifier confidence {weak_verdict.confidence:.0%}"
)
weak_partition = Partition(
ungrounded=[
Claim(
text="a single CPU sample loosely resembles a busy workload",
type=EvidenceType.INFERENCE,
evidence_refs=["telemetry:cpu_p50=41.0"],
),
],
)
weak_score = gsar_score(weak_partition)
weak_decision = decide(weak_score)
if weak_decision == Decision.PROCEED:
print(f" SHIPPED recommendation: {weak_verdict.action} to {weak_verdict.size}")
else:
print(f" abstained ({weak_decision.value}): grounding score {weak_score:.2f}")
# =========================================================================
# Part 5: drive the specialist end-to-end
# =========================================================================
print("\n=== Part 5: Executing Specialists ===\n")
result = await specialist.execute(
task="Recommend rightsizing for instance i-0abc123def and reconcile the verdict "
"against the approved instance catalog.",
context={
"ticket_id": "FIN-2026-017",
"instance": "i-0abc123def",
"submitted_by": "quarterly cost-optimization review",
},
)
print("Execution Result:")
print(f" Success: {result.success}")
print(f" Confidence: {result.confidence:.0%}")
print(f" Duration: {result.duration_ms:.0f}ms")
if result.output:
print(f" Output: {result.output[:300]}...")
if result.error:
print(f" Error: {result.error}")
# =========================================================================
# Part 6: pre-built specialists for common evidence domains
# =========================================================================
print("\n=== Part 6: Pre-built Specialists ===\n")
log_analyst = create_log_analyst(model=model)
metrics_analyst = create_metrics_analyst(model=model)
trace_analyst = create_trace_analyst(model=model)
code_analyst = create_code_analyst(model=model)
specialists = [log_analyst, metrics_analyst, trace_analyst, code_analyst]
print("Pre-built Specialists:")
for spec in specialists:
print(f"\n {spec.name}")
print(f" Type: {spec.specialist_type}")
prompt_preview = spec.system_prompt.split("\n")[0]
print(f" Focus: {prompt_preview[:60]}...")
t0 = time.perf_counter()
p6 = await metrics_analyst.execute(
task="In one sentence, what does a metrics analyst contribute to capacity planning?"
)
dt = time.perf_counter() - t0
print(f"\n [model call: {dt:.2f}s · metrics_analyst.execute()]")
if p6.output:
print(f" Output: {p6.output[:160]}")
# =========================================================================
# Part 7: extend a pre-built specialist with your own tools
# =========================================================================
print("\n=== Part 7: Custom Tools Integration ===\n")
@tool(name="search_app_logs", description="Search application logs for patterns")
async def search_app_logs(pattern: str, timerange: str = "1h") -> str:
return f"Found 42 app-log matches for '{pattern}' in last {timerange}"
@tool(name="get_scaling_history", description="Get recent autoscaling events for a service")
async def get_scaling_history(limit: int = 10) -> str:
return f"Retrieved {limit} most recent autoscaling events"
custom_log_analyst = create_log_analyst(
model=model,
tools=[search_app_logs, get_scaling_history],
)
print(f"Custom log analyst tools: {[t.name for t in custom_log_analyst.tools]}")
log_result = await custom_log_analyst.execute(
task="Search for OOMKilled events on the checkout service in the last hour",
)
print("Log analysis result:")
print(f" Confidence: {log_result.confidence:.0%}")
if log_result.output:
print(f" Output: {log_result.output[:200]}...")
# =========================================================================
# Part 8: how confidence is estimated
# =========================================================================
print("\n=== Part 8: Confidence Estimation ===\n")
# The built-in estimator scans the response for hedging vs.
# certainty markers — a rough proxy that you'd typically replace
# with a domain-specific scorer in production.
responses = [
(
"definitely over-provisioned — utilization profile is unambiguous",
"High confidence markers",
),
("might fit a smaller size, hard to say from one data point", "Low confidence markers"),
("confirmed by the full utilization feature vector", "Verification markers"),
("unclear which family this workload belongs on", "Uncertainty markers"),
]
print("Confidence markers in responses:")
for response, description in responses:
confidence = specialist._estimate_confidence(response)
print(f" '{response}' -> {confidence:.0%} ({description})")
print(
f"AI commentary: {_llm_call('In one sentence, why is keyword-based confidence estimation only a rough proxy for a rightsizing decision?')}"
)
# =========================================================================
# Part 9: common specialist shapes
# =========================================================================
print("\n=== Part 9: Specialist Patterns ===\n")
print("Pattern 1: Domain Expert")
print(" Focused prompt + domain tools + high confidence threshold.")
print()
print("Pattern 2: Procedure Follower")
print(" Runbook-driven; each step validated against expected output.")
print()
print("Pattern 3: Adaptive Analyst")
print(" Multiple playbooks; the right one selected per request.")
print()
print("Pattern 4: Pipeline Stage")
print(" Drops into a larger FinOps workflow; structured output, context in/out.")
print(
f"AI suggestion: {_llm_call('Suggest one extra cloud Specialist pattern not in the four listed above. One short sentence.')}"
)
# =========================================================================
# Part 10: assemble a capacity-review team
# =========================================================================
print("\n=== Part 10: Specialist Teams ===\n")
def create_capacity_review_team(model):
"""One intake specialist plus three pre-built analysts."""
return {
"intake": Specialist(
name="Intake Specialist",
specialist_type="intake",
description="Initial request assessment and priority classification",
system_prompt="Assess rightsizing requests and determine priority and routing.",
model=model,
),
"logs": create_log_analyst(model=model),
"metrics": create_metrics_analyst(model=model),
"code": create_code_analyst(model=model),
}
team = create_capacity_review_team(model)
print("Capacity Review Team:")
for role, spec in team.items():
print(f" {role}: {spec.name}")
t0 = time.perf_counter()
p10 = await team["intake"].execute(
task="In one sentence, classify this request: 'p95 CPU on the checkout fleet held above 85% for 6 hours.'",
)
dt = time.perf_counter() - t0
print(f" [model call: {dt:.2f}s · intake.execute()]")
if p10.output:
print(f" Intake verdict: {p10.output[:160]}")
# =========================================================================
print("\n" + "=" * 60)
print("Next: Notebook 28 — A2A Protocol")
print("=" * 60)
if __name__ == "__main__":
asyncio.run(main())