Skip to content
LACE
  • v0.1 Current
  • Python
  • TypeScript Soon

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_ticket vs do_everything).
  • Described for the model, not for a humandescription_for_model says when to reach for this tool instead of a sibling, not what the UI label is.
  • Policy-scopedpolicy: {read_only, network, writes_app_data} determines per-call approval and audit.
pythonapp/agents.py
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 escalate and 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

SurfaceDeclared inHow it runs
AgentSkillAgentDefinition.capabilities or in_loop_skills (InLoopSkillSpec)Progressive disclosure — the loop injects a metadata index + use_skill tool; the model fetches the body on demand
LoopGuardConfigAgentExecutionConfig.loop_guardExact-duplicate threshold, Jaccard near-duplicate, per-tool cap, doom-loop threshold
HookRegistryHost-registered (code, not manifest)PreToolUseHook / PostToolUseHook / StopHookHookBlock / HookTransform / HookVeto
GuardrailRegistryHost-registeredinput / 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.

OperationRouteWhat it does
Start sessionPOST /v1/agents/{agent_id}/sessionsCreate a durable session (Postgres-backed turns)
Send turnPOST /v1/agents/.../sessions/{id}/turnsRun the agentic loop (model chooses tools)
StreamSSE on the turn responseToken and tool-call streaming
SteerPOST .../steerRedirect a running loop
CancelPOST .../cancelCancel the turn and roll back in-flight tool calls where possible
InspectGET .../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.

terminalbash
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
ChannelDriverInbound
Emailsrc/lace/connectors/app_email_connector.pyPOST /v1/channels/email/inbound
SMS / WhatsAppapp_sms_connector.pyprovider webhook → normalized inbound
Slack / Teamsapp_slack_connector.pyevent subscription → session
Webhookschannel_webhooks.pygeneric inbound → session
Realtime voicesrc/lace/agent/runtime voice drivercall → 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_providers in the manifest; runtime routing via AgentRuntimeRouting / InteractionShapeKind.

Walkthrough — Intake Triage on web + email

  1. Create a tool that reads your data: tools@tool(tool_id="lookup_ticket")AppDataService.list_records.
  2. Declare the agent in app/agents.py with three capabilities, one approval gate, and a citation skill (above).
  3. Register the provider: agent_providers=["app.agents:AgentProvider"] in the manifest. Run locally with lace-app dev and hit POST /v1/agents/intake-triage/sessions.
  4. Wire channels in Studio: enable web chat and email for intake-triage. Studio creates the inbound route and the outbound reply template.
  5. 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.