HumanLayer provides built-in state preservation to help maintain context across agent interactions. This is particularly useful when building agents that need to maintain conversation history or workflow state across multiple human interactions.
For a runnable example, see the FastAPI Email Example.
Overview
State can be preserved in both function calls and human contacts through the state
field:
# Store state when creating a function call
function_call = await hl.create_function_call(
spec=FunctionCallSpec(
fn="send_email",
kwargs={"to": "user@example.com"},
state={
"conversation_history": previous_messages,
"workflow_context": current_context
}
)
)
# Store state when creating a human contact
contact = await hl.create_human_contact(
spec=HumanContactSpec(
msg="Do you approve this draft?",
state={
"draft_version": 1,
"previous_feedback": feedback_history
}
)
)
The state object will be preserved and returned in webhooks, allowing your application to restore context when handling responses.
Example: Email Thread Management
Here’s a practical example of using state to manage an email conversation thread:
class Thread:
initial_email: EmailPayload
events: list[Event] # Track all events in the conversation
def to_state(self) -> dict:
"""Convert thread to a state dict"""
return self.model_dump(mode="json")
@classmethod
def from_state(cls, state: dict) -> "Thread":
"""Restore thread from preserved state"""
return cls.model_validate(state)
async def handle_response(human_response: HumanContact):
# Restore thread state from the response
if human_response.spec.state is not None:
thread = Thread.model_validate(human_response.spec.state)
else:
raise ValueError("state is required")
# Update thread with new response
thread.events.append(
Event(type=EventType.HUMAN_RESPONSE,
data={"response": human_response.status.response})
)
# Continue the conversation with preserved context
await handle_continued_thread(thread)
Here’s how to handle webhook responses with FastAPI background tasks:
@app.post("/webhook/human-response")
async def handle_webhook(
response: HumanContact,
background_tasks: BackgroundTasks
) -> Dict[str, str]:
"""Handle webhook responses from HumanLayer"""
if response.spec.state is None:
return {"status": "error", "message": "missing state"}
# Process response in background to avoid webhook timeout
background_tasks.add_task(handle_response, response)
return {"status": "ok"}
Important: Version your state objects carefully! If you change your Pydantic models,
responses from old webhooks may fail to deserialize. Consider:
- Adding version fields to state objects
- Supporting multiple versions of model schemas
- Using migration functions for old state formats
- Keeping old model versions around until all pending webhooks are processed
Example of versioned state:
class ThreadStateV1(BaseModel):
version: Literal[1] = 1
initial_email: EmailPayload
events: list[Event]
class ThreadStateV2(BaseModel):
version: Literal[2] = 2
initial_email: EmailPayload
events: list[Event]
metadata: dict # New in V2
def migrate_v1_to_v2(v1: ThreadStateV1) -> ThreadStateV2:
"""Migrate old state format to new version"""
return ThreadStateV2(
initial_email=v1.initial_email,
events=v1.events,
metadata={}, # Set defaults for new fields
)
def load_thread_state(state: dict) -> Thread:
"""Load thread state with version handling"""
version = state.get("version", 1)
if version == 1:
v1 = ThreadStateV1.model_validate(state)
return migrate_v1_to_v2(v1)
elif version == 2:
return ThreadStateV2.model_validate(state)
else:
raise ValueError(f"Unknown state version: {version}")
Best Practices
- Serializable State: Ensure your state object can be serialized to JSON
- Minimal State: Store only what’s necessary to restore context
- Type Safety: Use Pydantic models to ensure type safety of state objects
- Validation: Always validate state when restoring from webhooks
- Error Handling: Have fallbacks for missing or invalid state
State vs Run IDs
While run IDs group related operations together, state preservation allows for richer context:
- Run IDs: Group related operations, useful for audit trails
- State: Preserve detailed context needed for conversation continuity
Use both in combination for robust agent workflows:
hl = HumanLayer(
run_id="email-campaign-assistant", # Group operations
)
await hl.create_human_contact(
spec=HumanContactSpec(
msg="Review this campaign draft?",
state=thread.to_state(), # Preserve conversation context
)
)