Platform Guide
Agent Studio — governed agents on every channel
Design the tool surface, write the system prompt people will actually audit, and ship the same agent to web chat, email, SMS, WhatsApp, Slack, Teams, and voice — with one control plane for sessions, tasks, streaming, steering, and cancellation.
What Agent Studio is
Agent Studio is the design surface for product agents: tool-using definitions with deployments and channels. An agent is an AgentDefinition (model, instructions, capabilities, guardrails) that runs a real agentic loop in your sidecar where the model chooses tools. The platform enforces the declaration, so an agent can use only the tools listed in its capabilities. The capability list defines the boundary.
Studio is the visual complement to the code surface. Whether you design in Studio or in app/agents.py, the artifact is the same AgentDefinition contract that the sidecar executes.
Tool surface — the design question that matters most
The most important decision in Studio is which tools you give the model and what each one promises. Good tools are:
- Narrow — one verb, one return shape, one failure mode the model can understand (
lookup_ticketvsdo_everything). - Described for the model, not for a human —
description_for_modelsays when to reach for this tool instead of a sibling, not what the UI label is. - Policy-scoped —
policy: {read_only, network, writes_app_data}determines per-call approval and audit.
from lace_app_sdk.agents import AgentDefinition, ToolCapability, ModelPolicy, InstructionProfile
from lace_app_sdk.skills import AgentSkill
TRIAGE = AgentDefinition(
agent_id="field_intake.triage",
slug="intake-triage",
name="Intake Triage",
description="Classifies tickets, drafts replies with citations, escalates when blocked.",
instructions=InstructionProfile(
system="You are Intake Triage. Prefer the KB over guessing. Cite every factual claim.",
),
model_policy=ModelPolicy(default_model_alias="anthropic/claude-sonnet-4", temperature=0.2),
capabilities=[
ToolCapability(tool_id="lookup_ticket"),
ToolCapability(tool_id="search_kb"),
ToolCapability(tool_id="escalate", approval_required=True),
],
)
# in-loop skill — progressive disclosure, injected as use_skill tool
CITATION_SKILL = AgentSkill(
name="cite-before-escalate",
description="Always cite the KB before escalating.",
body="1. search_kb → 2. draft with citations → 3. escalate only if no citation found",
)
Capabilities appear as ToolCapability(tool_id="lookup_ticket") and ToolCapability(tool_id="escalate", approval_required=True). When approval_required=True, the loop pauses with waiting_approval before that tool executes. The pause is durable, so a human can approve an hour later and the same run resumes.
The system prompt — what Studio makes auditable
Studio keeps the prompt in a versioned InstructionProfile with history and diff, not as a string buried in code. The guidance we ship with every agent:
- Name the role and its scope first ("You are Intake Triage. You classify field tickets...").
- Prefer the KB over guessing; cite every factual claim.
- State the escalation rule explicitly — when to use
escalateand what to do when you cannot cite an answer.
Studio previews the loop with a real session against your dev dataset so you can see which tool the model reached for and why before you publish.
Reusable skills & guardrails
| Surface | Declared in | How it runs |
|---|---|---|
AgentSkill | AgentDefinition.capabilities or in_loop_skills (InLoopSkillSpec) | Progressive disclosure — the loop injects a metadata index + use_skill tool; the model fetches the body on demand |
LoopGuardConfig | AgentExecutionConfig.loop_guard | Exact-duplicate threshold, Jaccard near-duplicate, per-tool cap, doom-loop threshold |
HookRegistry | Host-registered (code, not manifest) | PreToolUseHook / PostToolUseHook / StopHook — HookBlock / HookTransform / HookVeto |
GuardrailRegistry | Host-registered | input / output / tool placements, GuardrailResult |
The control plane
Sessions, tasks, streaming, steering, and cancellation are platform concerns, not per-agent code. An operator can watch a run, redirect it mid-flight, or cancel it, and the same controls work whether the agent is answering in web chat or on a phone call. The plane lives at src/lace/agent/runtime and src/lace/api/routes/agent_channels.py.
| Operation | Route | What it does |
|---|---|---|
| Start session | POST /v1/agents/{agent_id}/sessions | Create a durable session (Postgres-backed turns) |
| Send turn | POST /v1/agents/.../sessions/{id}/turns | Run the agentic loop (model chooses tools) |
| Stream | SSE on the turn response | Token and tool-call streaming |
| Steer | POST .../steer | Redirect a running loop |
| Cancel | POST .../cancel | Cancel the turn and roll back in-flight tool calls where possible |
| Inspect | GET .../sessions/{id} | Turns, tool calls, approval state, prompt trace |
Channels — one control plane, many doors
An agent is a session, task, streaming, steering, and cancellation primitive that happens to have a channel driver. The same agent answers in web chat, on email, over SMS, in Slack and Teams, via webhook, and on a live voice call.
POST /v1/agents/field_intake.triage/sessions
# → { session_id }
POST /v1/agents/field_intake.triage/sessions/{id}/turns
{ "message": "Customer says heater failed at site 12, ticket #4821" }
# same agent, different channel — email
POST /v1/channels/email/inbound
{ "to": "intake@acme.lace", "subject": "Heater failed site 12" }
# → creates a session, runs same loop, replies via same channel driver
| Channel | Driver | Inbound |
|---|---|---|
src/lace/connectors/app_email_connector.py | POST /v1/channels/email/inbound | |
| SMS / WhatsApp | app_sms_connector.py | provider webhook → normalized inbound |
| Slack / Teams | app_slack_connector.py | event subscription → session |
| Webhooks | channel_webhooks.py | generic inbound → session |
| Realtime voice | src/lace/agent/runtime voice driver | call → session + realtime turn |
- Sessions, tasks, streaming (SSE), steering mid-run, and cancellation are control-plane operations — an operator can watch a run, redirect it, or cancel it without the agent cooperating.
- Every channel driver speaks the same contract:
src/lace/connectors/agents+src/lace/api/routes/agent_channels.py+src/lace/api/routes/channel_*. - Declarations:
channel_providersin the manifest; runtime routing viaAgentRuntimeRouting/InteractionShapeKind.
Walkthrough — Intake Triage on web + email
- Create a tool that reads your data: tools →
@tool(tool_id="lookup_ticket")→AppDataService.list_records. - Declare the agent in
app/agents.pywith three capabilities, one approval gate, and a citation skill (above). - Register the provider:
agent_providers=["app.agents:AgentProvider"]in the manifest. Run locally withlace-app devand hitPOST /v1/agents/intake-triage/sessions. - Wire channels in Studio: enable web chat and email for
intake-triage. Studio creates the inbound route and the outbound reply template. - Publish (
lace-app push) — the agent sidecar boots, the same loop that ran locally now runs behind the channel drivers. Approve escalations from the LACE console; they resume the same durable turn.
Operate it
- Inspect a session:
GET /v1/agents/{agent_id}/sessions/{session_id}— turns, tool calls, approval state, prompt trace. - Trace every LLM call: model alias, tokens, latency, tenant — in OpenTelemetry and the cost dashboard (observability, governance).
- Budget caps and per-tool caps stop a runaway loop before it stops your budget.
Next: agents & skills reference or the product page.