Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions apps/agent/src/lead_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ class _Segment(TypedDict, total=False):
leadIds: list[str]


class _Followup(TypedDict, total=False):
id: str
text: str
status: str # "pending" | "done"
leadId: str

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To maintain consistency with the frontend TypeScript definition and the system prompt, leadId should be marked as optional or allow None if it's not always present.

Suggested change
leadId: str
leadId: Optional[str]



def _replace(_left: Any, right: Any) -> Any:
"""LangGraph reducer that always takes the most recent value.

Expand All @@ -107,6 +114,7 @@ class LeadCanvasState(AgentState):
selectedLeadId: NotRequired[Annotated[Optional[str], _replace]]
header: NotRequired[Annotated[_Header, _replace]]
sync: NotRequired[Annotated[_SyncMeta, _replace]]
followups: NotRequired[Annotated[list[_Followup], _replace]]


class LeadStateMiddleware(AgentMiddleware[LeadCanvasState, Any]): # type: ignore[type-arg]
Expand Down
24 changes: 24 additions & 0 deletions apps/agent/src/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
"- selectedLeadId: string | null\n"
"- header: { title: string, subtitle: string }\n"
"- sync: { databaseId: string, databaseTitle: string, syncedAt: string | null }\n"
"- followups: Followup[] // shared follow-up tasks (A2UI demo)\n"
" - Followup = { id: string, text: string,\n"
" status: 'pending' | 'done', leadId?: string }\n"
)


Expand Down Expand Up @@ -82,6 +85,18 @@
" draft). DO NOT call post_lead_comment in the SAME turn as\n"
" renderEmailDraft — wait for the user's approval to come back through\n"
" the chat as a follow-up message, then post.\n"
"- manage_followups({followups: Followup[]}): A2UI shared-state tool.\n"
" Both you and the user write to state.followups; the user can also\n"
" toggle / add / remove items via the rendered list. Pass the FULL\n"
" list every call (each side's edit overwrites the array, so partial\n"
" patches would silently drop user-side adds). Each item must include\n"
" status ('pending' | 'done'); leadId is optional and links the item\n"
" to a specific lead. Use this when the user asks to plan / schedule\n"
" / track follow-ups.\n"
"- renderFollowups({}): mount the live shared follow-ups list inline\n"
" in chat. No args — it reads state.followups directly. Call this\n"
" AFTER manage_followups so the user sees the result, or any time\n"
" the user asks to review the current follow-up list.\n"
)


Expand All @@ -103,6 +118,15 @@
" arguments + result. This means you can call backend tools (like the\n"
" Notion MCP read tools) freely and the UI will reflect the activity\n"
" without us writing a per-tool renderer.\n\n"
"TWO LAYOUTS:\n"
"- Chat-only mode (the default landing at `/`): centered chat with no\n"
" canvas behind it. Use renderLeadMiniCard / renderWorkshopDemand /\n"
" renderFollowups to give the user a visual answer inline. The user\n"
" can click 'Open canvas' in the header to expand to app mode.\n"
"- App mode (at `/leads`): kanban board, donut, demand bars, lead\n"
" detail panel + sidebar chat. The user reaches it manually from the\n"
" chat header. Drag-drop on a card moves it to a different status\n"
" column and persists via commitLeadEdit.\n\n"
"INTERACTION POLICY:\n"
"- The default canvas layout is a kanban grouped by Status (Not started\n"
" / In progress / Done) with a workshop-demand chart above it. Drag-drop\n"
Expand Down
Loading