-
Notifications
You must be signed in to change notification settings - Fork 6
[SILO-788] feat: add docs for Agents in Plane #176
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: preview
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,368 @@ | ||
| --- | ||
| title: Best Practices | ||
| sidebarTitle: Best Practices | ||
| description: Guidelines for building responsive, user-friendly Plane agents that provide a seamless experience. | ||
| --- | ||
|
|
||
| <Note>Plane Agents are currently in **Beta**. Please send any feedback to support@plane.so.</Note> | ||
|
|
||
| ## Overview | ||
|
|
||
| Building a great agent experience requires thoughtful design around responsiveness, error handling, and user communication. This guide covers best practices to ensure your agent feels native to Plane and provides a seamless experience for users. | ||
|
|
||
| ## Sending Immediate Thought Activity | ||
|
|
||
| When your agent receives a webhook, users are waiting for a response. The most important best practice is to **acknowledge the request immediately**. | ||
|
|
||
| ### Why Immediate Acknowledgment Matters | ||
|
|
||
| - Users see that your agent is active and processing their request | ||
| - Prevents the Agent Run from being marked as `stale` (5-minute timeout) | ||
| - Builds trust that the agent received and understood the request | ||
| - Provides visual feedback during potentially long processing times | ||
|
|
||
| ### Implementation | ||
|
|
||
| Send a `thought` activity within the first few seconds of receiving a webhook: | ||
|
|
||
| <Tabs> | ||
| <Tab title="TypeScript"> | ||
|
|
||
| ```typescript | ||
| async function handleWebhook(webhook: AgentRunActivityWebhook) { | ||
| const agentRunId = webhook.agent_run.id; | ||
|
|
||
| // IMMEDIATELY acknowledge receipt | ||
| await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { | ||
| type: "thought", | ||
| content: { | ||
| type: "thought", | ||
| body: "Received your request. Analyzing...", | ||
| }, | ||
| }); | ||
|
|
||
| // Now proceed with actual processing | ||
| // This can take longer since user knows agent is working | ||
| const result = await processRequest(webhook); | ||
|
|
||
| // ... rest of the logic | ||
| } | ||
| ``` | ||
|
|
||
| </Tab> | ||
| <Tab title="Python"> | ||
|
|
||
| ```python | ||
| async def handle_webhook(webhook: dict): | ||
| agent_run_id = webhook["agent_run"]["id"] | ||
|
|
||
| # IMMEDIATELY acknowledge receipt | ||
| plane_client.agent_runs.activities.create( | ||
| workspace_slug=workspace_slug, | ||
| agent_run_id=agent_run_id, | ||
| type="thought", | ||
| content={ | ||
| "type": "thought", | ||
| "body": "Received your request. Analyzing...", | ||
| }, | ||
| ) | ||
|
|
||
| # Now proceed with actual processing | ||
| result = await process_request(webhook) | ||
|
|
||
| # ... rest of the logic | ||
| ``` | ||
|
|
||
| </Tab> | ||
| </Tabs> | ||
|
|
||
| ### Thought Activity Best Practices | ||
|
|
||
| - Keep thoughts concise but informative | ||
| - Update thoughts as you progress through different stages | ||
| - Use thoughts to explain what the agent is doing, not technical details | ||
|
|
||
| **Good examples:** | ||
| - "Analyzing your question about project timelines..." | ||
| - "Searching for relevant work items..." | ||
| - "Preparing response with the requested data..." | ||
|
|
||
| **Avoid:** | ||
| - "Initializing LLM context with temperature 0.7..." | ||
| - "Executing database query SELECT * FROM..." | ||
| - Generic messages like "Working..." repeated multiple times | ||
|
|
||
| ## Acknowledging Important Signals | ||
|
|
||
| Signals communicate user intent beyond the message content. Your agent **must** handle the `stop` signal appropriately. | ||
|
|
||
| ### The Stop Signal | ||
|
|
||
| When a user wants to stop an agent run, Plane sends a `stop` signal with the activity. Your agent should: | ||
|
|
||
| 1. **Recognize the signal immediately** | ||
| 2. **Stop any ongoing processing** | ||
| 3. **Send a confirmation response** | ||
|
|
||
| <Tabs> | ||
| <Tab title="TypeScript"> | ||
|
|
||
| ```typescript | ||
| async function handleWebhook(webhook: AgentRunActivityWebhook) { | ||
| const signal = webhook.agent_run_activity.signal; | ||
| const agentRunId = webhook.agent_run.id; | ||
|
|
||
| // ALWAYS check for stop signal first | ||
| if (signal === "stop") { | ||
| // Cancel any ongoing work | ||
| cancelOngoingTasks(agentRunId); | ||
|
|
||
| // Acknowledge the stop | ||
| await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { | ||
| type: "response", | ||
| content: { | ||
| type: "response", | ||
| body: "Understood. I've stopped processing your previous request.", | ||
| }, | ||
| }); | ||
|
|
||
| return; // Exit early | ||
| } | ||
|
|
||
| // Continue with normal processing... | ||
| } | ||
| ``` | ||
|
|
||
| </Tab> | ||
| <Tab title="Python"> | ||
|
|
||
| ```python | ||
| async def handle_webhook(webhook: dict): | ||
| signal = webhook["agent_run_activity"]["signal"] | ||
| agent_run_id = webhook["agent_run"]["id"] | ||
|
|
||
| # ALWAYS check for stop signal first | ||
| if signal == "stop": | ||
| # Cancel any ongoing work | ||
| cancel_ongoing_tasks(agent_run_id) | ||
|
|
||
| # Acknowledge the stop | ||
| plane_client.agent_runs.activities.create( | ||
| workspace_slug=workspace_slug, | ||
| agent_run_id=agent_run_id, | ||
| type="response", | ||
| content={ | ||
| "type": "response", | ||
| "body": "Understood. I've stopped processing your previous request.", | ||
| }, | ||
| ) | ||
|
|
||
| return # Exit early | ||
|
|
||
| # Continue with normal processing... | ||
| ``` | ||
|
Comment on lines
+139
to
+163
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same async/await inconsistency as previous example. The Python example at lines 140-163 has the same issue: 🤖 Prompt for AI Agents |
||
|
|
||
| </Tab> | ||
| </Tabs> | ||
|
|
||
| ### Signal Considerations | ||
|
|
||
| | Signal | How to Handle | | ||
| |--------|---------------| | ||
| | `continue` | Default behavior, proceed with processing | | ||
| | `stop` | Immediately halt and confirm | | ||
|
|
||
| ## Progress Communication | ||
|
|
||
| For long-running tasks, keep users informed with progress updates. | ||
|
|
||
| ### Multi-Step Operations | ||
|
|
||
| When your agent performs multiple steps, send thought activities for each: | ||
|
|
||
| ```typescript | ||
| // Step 1: Acknowledge | ||
| await createThought("Understanding your request..."); | ||
|
|
||
| // Step 2: First action | ||
| await createAction("searchDocuments", { query: userQuery }); | ||
| const searchResults = await searchDocuments(userQuery); | ||
|
|
||
| // Step 3: Processing | ||
| await createThought("Found relevant information. Analyzing..."); | ||
|
|
||
| // Step 4: Additional work | ||
| await createAction("generateSummary", { data: searchResults }); | ||
| const summary = await generateSummary(searchResults); | ||
|
|
||
| // Step 5: Final response | ||
| await createResponse(`Here's what I found: ${summary}`); | ||
| ``` | ||
|
|
||
| ### Avoiding Information Overload | ||
|
|
||
| While progress updates are important, too many can be overwhelming: | ||
|
|
||
| - **Don't** send a thought for every internal function call | ||
| - **Do** send thoughts for user-meaningful milestones | ||
| - **Don't** expose technical implementation details | ||
| - **Do** explain what value is being created for the user | ||
|
|
||
| ## Error Handling | ||
|
|
||
| Graceful error handling is crucial for a good user experience. | ||
|
|
||
| ### Always Catch and Report Errors | ||
|
|
||
| ```typescript | ||
| async function handleWebhook(webhook: AgentRunActivityWebhook) { | ||
| const agentRunId = webhook.agent_run.id; | ||
|
|
||
| try { | ||
| await createThought("Processing your request..."); | ||
|
|
||
| // Your logic here... | ||
| const result = await processRequest(webhook); | ||
|
|
||
| await createResponse(result); | ||
|
|
||
| } catch (error) { | ||
| // ALWAYS inform the user about errors | ||
| await planeClient.agentRuns.activities.create(workspaceSlug, agentRunId, { | ||
| type: "error", | ||
| content: { | ||
| type: "error", | ||
| body: getUserFriendlyErrorMessage(error), | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| function getUserFriendlyErrorMessage(error: Error): string { | ||
| // Map technical errors to user-friendly messages | ||
| if (error.message.includes("rate limit")) { | ||
| return "I'm receiving too many requests right now. Please try again in a few minutes."; | ||
| } | ||
| if (error.message.includes("timeout")) { | ||
| return "The operation took too long. Please try a simpler request or try again later."; | ||
| } | ||
| // Generic fallback | ||
| return "I encountered an unexpected error. Please try again or contact support if the issue persists."; | ||
| } | ||
| ``` | ||
|
|
||
| ### Error Message Guidelines | ||
|
|
||
| **Do:** | ||
| - Use clear, non-technical language | ||
| - Suggest next steps when possible | ||
| - Be honest about what went wrong (at a high level) | ||
|
|
||
| **Don't:** | ||
| - Expose stack traces or technical details | ||
| - Blame the user for errors | ||
| - Leave users without any feedback | ||
|
|
||
| ## Handling Conversation Context | ||
|
|
||
| For multi-turn conversations, maintain context from previous activities. | ||
|
|
||
| ### Fetching Previous Activities | ||
|
|
||
| ```typescript | ||
| // Get all activities for context | ||
| const activities = await planeClient.agentRuns.activities.list( | ||
| workspaceSlug, | ||
| agentRunId | ||
| ); | ||
|
|
||
| // Build conversation history | ||
| const history = activities.results | ||
| .filter(a => a.type === "prompt" || a.type === "response") | ||
| .map(a => ({ | ||
| role: a.type === "prompt" ? "user" : "assistant", | ||
| content: a.content.body, | ||
| })); | ||
|
|
||
| // Use history in your LLM call or logic | ||
| const response = await processWithContext(newPrompt, history); | ||
| ``` | ||
|
|
||
| ### Context Best Practices | ||
|
|
||
| - Retrieve relevant history, not every single activity | ||
| - Filter to meaningful exchanges (prompts and responses) | ||
| - Consider summarizing long histories to save tokens/processing | ||
| - Don't assume infinite context availability | ||
|
|
||
| ## Rate Limiting and Timeouts | ||
|
|
||
| Be mindful of Plane's API limits and your own processing time. | ||
|
|
||
| ### Stale Run Prevention | ||
|
|
||
| Agent Runs are marked as `stale` after 5 minutes of inactivity. For long operations: | ||
|
|
||
| ```typescript | ||
| async function longRunningTask(agentRunId: string) { | ||
| const HEARTBEAT_INTERVAL = 60000; // 1 minute | ||
|
|
||
| const heartbeat = setInterval(async () => { | ||
| await createThought("Still working on your request..."); | ||
| }, HEARTBEAT_INTERVAL); | ||
|
|
||
| try { | ||
| const result = await performLongOperation(); | ||
| return result; | ||
| } finally { | ||
| clearInterval(heartbeat); | ||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ### Webhook Response Time | ||
|
|
||
| - Return HTTP 200 from your webhook handler quickly (within seconds) | ||
| - Process the actual agent logic asynchronously | ||
| - Don't block the webhook response waiting for LLM calls | ||
|
|
||
| ```typescript | ||
| // Good: Respond immediately, process async | ||
| app.post("/webhook", async (req, res) => { | ||
| res.status(200).json({ received: true }); | ||
|
|
||
| // Process in background | ||
| processWebhookAsync(req.body).catch(console.error); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Summary Checklist | ||
|
|
||
| <CardGroup cols={2}> | ||
| <Card title="Responsiveness" icon="bolt"> | ||
| - Send thought within seconds of webhook | ||
| - Return webhook response quickly | ||
| - Send heartbeats for long operations | ||
| </Card> | ||
| <Card title="Signal Handling" icon="signal"> | ||
| - Always check for `stop` signal first | ||
| - Handle all signal types appropriately | ||
| - Confirm when stopping | ||
| </Card> | ||
| <Card title="Error Handling" icon="triangle-exclamation"> | ||
| - Wrap processing in try/catch | ||
| - Always send error activity on failure | ||
| - Use friendly error messages | ||
| </Card> | ||
| <Card title="User Experience" icon="user"> | ||
| - Progress updates for long tasks | ||
| - Clear, non-technical communication | ||
| - Maintain conversation context | ||
| </Card> | ||
| </CardGroup> | ||
|
|
||
| ## Next Steps | ||
|
|
||
| - Learn about [Signals & Content Payload](/dev-tools/agents/signals-content-payload) for advanced activity handling | ||
| - Review the [Building an Agent](/dev-tools/agents/building-an-agent) guide for implementation details | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Inconsistent async/await usage in Python example.
The function
handle_webhookis declared asasync def, but the SDK callplane_client.agent_runs.activities.create(...)on line 60 is called synchronously (withoutawait). Either make the SDK call awaited if the SDK supports async, or remove theasynckeyword from the function definition for consistency.async def handle_webhook(webhook: dict): agent_run_id = webhook["agent_run"]["id"] # IMMEDIATELY acknowledge receipt - plane_client.agent_runs.activities.create( + await plane_client.agent_runs.activities.create( workspace_slug=workspace_slug, agent_run_id=agent_run_id, type="thought", content={ "type": "thought", "body": "Received your request. Analyzing...", }, ) # Now proceed with actual processing result = await process_request(webhook) # ... rest of the logic