Skip to content

feat(platform): copilot followups UI - #13192

Merged
majdyz merged 2 commits into
feat/copilot-schedule-followupfrom
feat/copilot-schedule-followup-ui
May 22, 2026
Merged

feat(platform): copilot followups UI#13192
majdyz merged 2 commits into
feat/copilot-schedule-followupfrom
feat/copilot-schedule-followup-ui

Conversation

@majdyz

@majdyz majdyz commented May 22, 2026

Copy link
Copy Markdown
Contributor

Why

Copilot can already schedule follow-up turns for itself (#13190), but there is no surface for the user to see what's pending or cancel one — they only see the side-effect when the copilot speaks up later. This adds the minimal Library page that lists them and lets the user cancel.

Stacked on top of #13190 — this PR's diff will collapse to just the UI commit once that lands.

What

  • New page at /library/followups listing the current user's pending copilot follow-ups, with:
    • Empty state when the user has none.
    • One row per scheduled turn showing message preview, next-run label (relative), recurrence label (humanised cron) and a session-id tag. Clicking the row deep-links to the originating /copilot?sessionId=....
    • A Cancel action that opens a confirmation dialog and calls the existing DELETE /api/schedules/{id} route; success toasts and refetches, failure toasts destructively.
  • New backend route GET /api/schedules/followups returning only CopilotTurnJobInfo rows for the caller. Split out from the polymorphic GET /api/schedules so the OpenAPI-generated client gets a single concrete return type per route instead of a discriminated union.

How

  • Backend: thin wrapper around Scheduler.get_execution_schedules(kind="copilot_turn") plus an explicit operation_id="listCopilotFollowupSchedules" to keep the generated TS client tidy.
  • Frontend: page + sub-components (EmptyFollowups, FollowupListItem) following the page.tsx + usePageName.ts + helpers.ts pattern in frontend/AGENTS.md. Design-system atoms/molecules only (Text, Button, Dialog, LoadingSpinner, ErrorCard), Phosphor icons only.
  • Data fetching via the Orval-generated useListCopilotFollowupSchedules hook; delete reuses useDeleteV1DeleteExecutionSchedule and invalidates the followups query key on success.
  • Tests:
    • Backend route test mocks the scheduler client and asserts the kind="copilot_turn" filter is forwarded and non-copilot rows are dropped.
    • Frontend integration tests (Vitest + RTL + MSW with Orval-generated handlers) cover: empty list, one row per followup, deep link href, delete success toast, delete failure toast.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have added tests for my changes
  • My changes generate no new warnings
  • I have ensured that my changes are backwards compatible

@majdyz
majdyz requested a review from a team as a code owner May 22, 2026 08:30
@github-project-automation github-project-automation Bot moved this to 🆕 Needs initial review in AutoGPT development kanban May 22, 2026
@majdyz
majdyz requested review from Bentlybro and ntindle and removed request for a team May 22, 2026 08:30
@github-actions github-actions Bot added platform/frontend AutoGPT Platform - Front end platform/backend AutoGPT Platform - Back end platform/blocks labels May 22, 2026
@coderabbitai

coderabbitai Bot commented May 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review (1)
  • dev

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 90eaf8c1-fdad-4837-b8dd-d16f61f70404

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR implements copilot follow-up scheduling, enabling users to defer work into future chat turns via a new ScheduleFollowupTool. The backend scheduler gains polymorphic job dispatch for graph executions and copilot-turn resumptions with separate job-kwargs models, shared metadata helpers, and generalized APIs. A frontend follow-ups management page lets users view and cancel scheduled continuations. The SDK's final-answer contract is refined to exclude assistant narration containing tool calls.

Changes

Copilot Follow-up Scheduling

Layer / File(s) Summary
Scheduler core polymorphism
autogpt_platform/backend/backend/executor/scheduler.py, scheduler_test.py, scheduler_unit_test.py
Scheduler service persists and dispatches two job kinds via discriminated GraphExecutionJobArgs and CopilotTurnJobArgs models. Shared job-info helpers extract metadata (id, timezone, next_run_time) from APScheduler rows. _persist_schedule centralizes job persistence with JSON-mode kwargs serialization. New add_copilot_turn_schedule endpoint; delete_graph_execution_schedule generalized to both kinds. New polymorphic get_execution_schedules with kind/graph_id/session_id filters. Copilot-turn dispatch validates session existence, enqueues via schedule_turn, and reschedules one-shot jobs on concurrency-cap errors with bounded retry depth.
SDK tool adapter required-args fix
autogpt_platform/backend/backend/copilot/sdk/tool_adapter.py, tool_adapter_test.py
MCP truncation wrapper now uses explicit required_args list instead of schema-derived heuristic, fixing empty-args detection when schema intentionally omits required field.
Schedule followup tool
autogpt_platform/backend/backend/copilot/tools/schedule_followup.py, schedule_followup_test.py, autogpt_platform/backend/backend/copilot/tracking.py
New authenticated tool lets users schedule future turns with one-shot delay_seconds (≥60s) or recurring cron. Validates session ownership on session_id override, pre-validates cron syntax, and tracks scheduling via PostHog analytics. Returns structured ScheduleCreatedResponse with next run time and recurrence flag.
Tool permissions and registry
autogpt_platform/backend/backend/copilot/permissions.py, copilot/tools/__init__.py
Adds schedule_followup to permitted tool names; registers ScheduleFollowupTool in tool registry.
Schedule management tool updates
autogpt_platform/backend/backend/copilot/tools/manage_schedules.py, manage_schedules_test.py
ListSchedules and DeleteSchedule tools now handle polymorphic kinds via unified ScheduleSummary model with kind discriminator and conditional fields (graph-specific or copilot-turn-specific).
API graph-scoped schedule queries
autogpt_platform/backend/backend/api/features/library/db.py, v1.py, v1_test.py, autogpt_platform/backend/backend/data/diagnostics.py
Graph-specific schedule endpoints use get_graph_execution_schedules() (excluding copilot turns). New /schedules/followups endpoint filters polymorphic results to CopilotTurnJobInfo only. Admin diagnostics exclude copilot-turn schedules with comments.
Orchestrator final-answer selection
autogpt_platform/backend/backend/blocks/orchestrator.py, test_orchestrator_execution_mode.py
SDK execution path tracks final_response_parts (text-only messages without tool calls) separately from full transcript via _select_final_answer_parts, preventing intermediate narration from polluting the finished output.
Tool schema budget
autogpt_platform/backend/backend/copilot/tools/tool_schema_test.py
Character budget increased to 37,000 to accommodate schedule_followup schema.
Frontend follow-ups page
autogpt_platform/frontend/src/app/(platform)/library/followups/page.tsx, useFollowupsPage.ts, components/FollowupListItem/*, components/EmptyFollowups/*, __tests__/main.test.tsx, autogpt_platform/frontend/src/app/api/openapi.json
New page lists scheduled copilot follow-ups with empty state, item rows (showing session link, message preview, next run time), and cancel dialogs. useFollowupListItem hook manages delete mutations, toast notifications, and React Query cache invalidation. OpenAPI schema adds CopilotTurnJobInfo, kind discriminators to job-info types, and updates ScheduleSummary for polymorphism.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant Frontend as Frontend Page
  participant API
  participant Scheduler
  participant ChatSession
  participant APScheduler as Job Runner
  User->>Frontend: Navigate to follow-ups
  Frontend->>API: GET /schedules/followups
  API->>Scheduler: get_execution_schedules(user_id, kind=copilot_turn)
  Scheduler->>API: [CopilotTurnJobInfo]
  API->>Frontend: 200 JSON
  Frontend->>Frontend: Render list with cancel buttons
  User->>Frontend: Click cancel on follow-up
  Frontend->>Frontend: Show confirmation dialog
  User->>Frontend: Confirm deletion
  Frontend->>API: DELETE /schedules/{schedule_id}
  API->>Scheduler: delete_graph_execution_schedule(schedule_id, user_id)
  Scheduler->>APScheduler: Remove job
  API->>Frontend: 200
  Frontend->>Frontend: Invalidate query, refetch list
  Frontend->>Frontend: Show success toast
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

size/xl, platform/frontend, platform/backend, Review effort 4/5

Suggested reviewers

  • Bentlybro
  • Swiftyos
  • 0ubbe

🐰 A tale of times deferred, now crystalline and clear,
Two jobs dance in schedules: graphs and sessions dear,
With cron and delay, the follow-ups take flight,
While final answers glow, untainted by tool's might,
The frontend blooms with buttons to rescind,
A future of turns that users can rescind.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.01% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat(platform): copilot followups UI' accurately describes the primary change: adding a UI feature for viewing and managing copilot follow-up schedules.
Description check ✅ Passed The PR description is comprehensive and directly related to the changeset, explaining the motivation, features, implementation approach, and testing strategy for the copilot followups UI.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/copilot-schedule-followup-ui

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 PR Overlap Detection

This check compares your PR against all other open PRs targeting the same branch to detect potential merge conflicts early.

🔴 Merge Conflicts Detected

The following PRs have been tested and will have merge conflicts if merged after this PR. Consider coordinating with the authors.

🟡 Medium Risk — Some Line Overlap

These PRs have some overlapping changes:

  • feat(backend/copilot): native scheduling for copilot turn followups #13190 (majdyz · updated 1h ago)
    • autogpt_platform/backend/backend/api/features/v1.py: L2299-2305, L2314-2320
    • autogpt_platform/backend/backend/api/features/library/db.py: L73-79, L906-912
    • autogpt_platform/backend/backend/copilot/sdk/tool_adapter.py: L669-707, L794-799, L803-812, L816-832, L840-848, L854-874, L877-886, L894-903, L909-919
    • autogpt_platform/backend/backend/copilot/tools/models.py: L45-51
    • autogpt_platform/backend/backend/copilot/permissions.py: L109-115
    • autogpt_platform/backend/backend/copilot/tools/__init__.py: L45-51, L88-95
    • autogpt_platform/backend/backend/copilot/sdk/tool_adapter_test.py: L218-266
    • autogpt_platform/backend/backend/executor/scheduler_unit_test.py: L1-371
    • autogpt_platform/backend/backend/executor/scheduler.py: L4-12, L19-34, L185-355, L402-407, L412-417, L422-441, L550-558, L563-607, L612-685, L696-701, L720-741, L744-801, L840-844, L940-971, L990-998, L1001-1151, L1190-1200
    • autogpt_platform/backend/backend/data/diagnostics.py: L374-385, L629-641, L684-696, L735-749
    • autogpt_platform/backend/backend/copilot/tools/tool_schema_test.py: L41-55
    • autogpt_platform/backend/backend/copilot/tools/manage_schedules.py: L1-13, L17-40, L43-76, L79-90, L93-102, L115-132, L141-147, L152-161, L170-176, L181-189
    • autogpt_platform/backend/backend/copilot/tools/schedule_followup_test.py: L1-233
    • autogpt_platform/backend/backend/executor/scheduler_test.py: L1-6, L39-110
    • autogpt_platform/backend/backend/copilot/tracking.py: L210-249
    • autogpt_platform/backend/backend/copilot/tools/manage_schedules_test.py: L1-6, L11-60, L64-71, L77-108, L112-143
    • autogpt_platform/backend/backend/copilot/tools/schedule_followup.py: L1-247

🟢 Low Risk — File Overlap Only

These PRs touch the same files but different sections (click to expand)

Summary: 6 conflict(s), 1 medium risk, 8 low risk (out of 15 PRs with file overlap)


Auto-generated on push. Ignores: openapi.json, lock files.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
autogpt_platform/backend/backend/executor/scheduler.py (1)

580-583: ⚡ Quick win

Use discriminator-based branching instead of duck-typing/type checks.

These branches currently rely on hasattr/isinstance for dispatch. Since schedule info is already discriminated by kind, switch to kind-based flow to satisfy typed dispatch rules and keep filtering logic simpler.

♻️ Suggested change
 def _timezone_from_job(job_obj: JobObj) -> str:
-    if hasattr(job_obj.trigger, "timezone"):
-        return str(job_obj.trigger.timezone)
+    if job_obj.next_run_time and job_obj.next_run_time.tzinfo:
+        return str(job_obj.next_run_time.tzinfo)
     return "UTC"
@@
-            if graph_id is not None and (
-                not isinstance(info, GraphExecutionJobInfo) or info.graph_id != graph_id
-            ):
-                continue
-            if session_id is not None and (
-                not isinstance(info, CopilotTurnJobInfo)
-                or info.session_id != session_id
-            ):
-                continue
+            if graph_id is not None and (
+                info.kind != "graph" or info.graph_id != graph_id
+            ):
+                continue
+            if session_id is not None and (
+                info.kind != "copilot_turn" or info.session_id != session_id
+            ):
+                continue

As per coding guidelines: "Do not use duck typing — avoid hasattr/getattr/isinstance for type dispatch; use typed interfaces/unions/protocols instead".

Also applies to: 1138-1145

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autogpt_platform/backend/backend/executor/scheduler.py` around lines 580 -
583, The _timezone_from_job function uses duck-typing via hasattr on
job_obj.trigger; change it to discriminate on job_obj.kind instead and handle
the cases that carry timezone info (e.g., the scheduled/cron trigger kinds) by
reading job_obj.trigger.timezone only for those kinds, otherwise return "UTC";
apply the same kind-based branching refactor to the similar logic referenced
around the other function/section that inspects triggers (the block at lines
handling job kind -> trigger attributes) so all dispatch uses job_obj.kind
rather than hasattr/isinstance.
autogpt_platform/backend/backend/executor/scheduler_test.py (1)

101-110: ⚡ Quick win

Tighten the failure assertion for invalid trigger input.

Catching bare Exception here is too permissive and may hide unrelated failures. Assert on the expected error message so this test only passes for the intended rejection path.

♻️ Suggested change
-    with pytest.raises(Exception) as exc:
+    with pytest.raises(Exception, match="Exactly one of `cron` or `run_at` must be provided"):
         await scheduler.add_copilot_turn_schedule(
             user_id=test_user.id,
             session_id=session_id,
             message="x",
             user_timezone="UTC",
         )
-    # ValueError from _build_trigger propagates as a RemoteError
-    # through the AppService transport; just verify the call rejected.
-    assert exc.value is not None
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autogpt_platform/backend/backend/executor/scheduler_test.py` around lines 101
- 110, The test currently catches a broad Exception when calling
scheduler.add_copilot_turn_schedule; narrow this to assert the expected failure
by checking the raised error and its message from the _build_trigger path:
replace the pytest.raises(Exception) with pytest.raises(RemoteError) or the
concrete transport-wrapped error type used in your test harness, capture it as
exc, and add an assertion that exc.value (or exc.value.args[0]) contains the
expected invalid-trigger message (e.g. text from _build_trigger like "invalid
trigger" or the exact ValueError message you expect) so the test only passes for
the intended rejection path originating from _build_trigger called by
scheduler.add_copilot_turn_schedule with test_user.id and session_id.
autogpt_platform/backend/backend/api/features/v1_test.py (1)

1052-1052: ⚡ Quick win

Move scheduler model imports to module scope.

Line 1052 introduces a local import in a backend test; this should be a top-level import.

♻️ Proposed refactor
 from backend.data.credit import AutoTopUpConfig
 from backend.data.graph import GraphModel
+from backend.executor.scheduler import CopilotTurnJobInfo, GraphExecutionJobInfo
 from backend.util.exceptions import InsufficientBalanceError
@@
-    from backend.executor.scheduler import CopilotTurnJobInfo, GraphExecutionJobInfo
-
     copilot_info = CopilotTurnJobInfo(

As per coding guidelines: "Use top-level imports only — avoid local/inner imports except for lazy imports of heavy optional dependencies like openpyxl".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autogpt_platform/backend/backend/api/features/v1_test.py` at line 1052, The
test contains a local import of CopilotTurnJobInfo and GraphExecutionJobInfo
from backend.executor.scheduler; move that import statement out of the local
scope and add it to the module-level imports near the other top-level imports so
CopilotTurnJobInfo and GraphExecutionJobInfo are imported at file import time
rather than inside the test body.
autogpt_platform/backend/backend/api/features/v1.py (1)

2336-2339: ⚡ Quick win

Avoid isinstance filtering after already requesting kind="copilot_turn".

Line 2339 introduces class-based filtering. Prefer discriminator-driven handling here and return the scheduler result directly (or validate kind) instead of isinstance dispatch.

♻️ Proposed simplification
 async def list_copilot_turn_schedules(
     user_id: Annotated[str, Security(get_user_id)],
 ) -> list[scheduler.CopilotTurnJobInfo]:
@@
-    schedules = await get_scheduler_client().get_execution_schedules(
-        user_id=user_id, kind="copilot_turn"
-    )
-    return [s for s in schedules if isinstance(s, scheduler.CopilotTurnJobInfo)]
+    schedules = await get_scheduler_client().get_execution_schedules(
+        user_id=user_id, kind="copilot_turn"
+    )
+    return cast(list[scheduler.CopilotTurnJobInfo], schedules)

As per coding guidelines: "Do not use duck typing — avoid hasattr/getattr/isinstance for type dispatch; use typed interfaces/unions/protocols instead".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autogpt_platform/backend/backend/api/features/v1.py` around lines 2336 -
2339, The code filters schedules with isinstance(s,
scheduler.CopilotTurnJobInfo) after calling
get_scheduler_client().get_execution_schedules(user_id=user_id,
kind="copilot_turn"); instead, remove the class-based filtering and return the
scheduler result directly (or assert/validate that each schedule.kind ==
"copilot_turn") so dispatch is driven by the kind discriminator rather than
isinstance checks; update the function that currently references schedules and
scheduler.CopilotTurnJobInfo to either return schedules as-is or perform a
lightweight kind validation on each item before returning.
autogpt_platform/backend/backend/copilot/tools/manage_schedules.py (1)

46-71: ⚡ Quick win

Replace isinstance-based dispatch with discriminator-based branching.

Line 49 currently dispatches by concrete class with isinstance(...). In this backend, type dispatch should use typed discriminators (e.g., job.kind) instead.

♻️ Proposed refactor
-from typing import Any, Literal
+from typing import Any, Literal, cast
@@
 def _to_summary(
     job: GraphExecutionJobInfo | CopilotTurnJobInfo,
 ) -> ScheduleSummary:
-    if isinstance(job, GraphExecutionJobInfo):
+    if job.kind == "graph":
+        graph_job = cast(GraphExecutionJobInfo, job)
         return ScheduleSummary(
-            schedule_id=job.id,
+            schedule_id=graph_job.id,
             kind="graph",
-            name=job.name,
-            timezone=job.timezone,
-            next_run_time=job.next_run_time,
-            cron=job.cron,
-            graph_id=job.graph_id,
-            graph_version=job.graph_version,
+            name=graph_job.name,
+            timezone=graph_job.timezone,
+            next_run_time=graph_job.next_run_time,
+            cron=graph_job.cron,
+            graph_id=graph_job.graph_id,
+            graph_version=graph_job.graph_version,
         )
-    run_at_str = job.run_at.isoformat() if job.run_at else None
+    copilot_job = cast(CopilotTurnJobInfo, job)
+    run_at_str = copilot_job.run_at.isoformat() if copilot_job.run_at else None
     return ScheduleSummary(
-        schedule_id=job.id,
+        schedule_id=copilot_job.id,
         kind="copilot_turn",
-        name=job.name,
-        timezone=job.timezone,
-        next_run_time=job.next_run_time,
-        cron=job.cron,
+        name=copilot_job.name,
+        timezone=copilot_job.timezone,
+        next_run_time=copilot_job.next_run_time,
+        cron=copilot_job.cron,
         run_at=run_at_str,
-        session_id=job.session_id,
-        message=job.message,
+        session_id=copilot_job.session_id,
+        message=copilot_job.message,
     )

As per coding guidelines: "Do not use duck typing — avoid hasattr/getattr/isinstance for type dispatch; use typed interfaces/unions/protocols instead".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@autogpt_platform/backend/backend/copilot/tools/manage_schedules.py` around
lines 46 - 71, The _to_summary function currently uses isinstance() to branch on
GraphExecutionJobInfo vs CopilotTurnJobInfo; change it to inspect the job.kind
discriminator instead (e.g., if job.kind == "graph": ... elif job.kind ==
"copilot_turn": ...). Keep the same ScheduleSummary field mappings: for kind
"graph" populate graph_id and graph_version from GraphExecutionJobInfo fields;
for "copilot_turn" convert job.run_at to ISO string when present and populate
session_id and message. Preserve job.id/name/timezone/next_run_time/cron in both
branches and raise or handle an unexpected job.kind if neither matches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@autogpt_platform/backend/backend/copilot/tools/schedule_followup.py`:
- Around line 127-247: The _execute method is doing too much; split it into
focused helpers to reduce complexity: extract authentication/session check into
authenticate_and_get_current_session(user_id, session) which returns
current_session_id or ErrorResponse, extract message validation into
validate_message(kwargs) returning message or ErrorResponse, extract target
session resolution into resolve_target_session(override_session_id,
current_session_id, user_id) which uses get_chat_session and returns
target_session_id or ErrorResponse, extract trigger parsing into
parse_trigger(kwargs) that returns (run_at, cron, is_recurring) and enforces the
mutually exclusive rule and delay >= 60, extract cron validation into
validate_cron(cron, user_timezone) which calls CronTrigger.from_crontab, extract
user/timezone lookup via user_db().get_user_by_id and get_user_timezone_or_utc
into get_user_timezone(user_id), extract scheduler call into
schedule_followup(...) which calls
get_scheduler_client().add_copilot_turn_schedule and handles ValueError, and
extract response/analytics into finalize_schedule_response(info,
current_session_id, target_session_id, is_recurring) which calls
track_followup_scheduled and returns ScheduleCreatedResponse; then make _execute
a short orchestrator that calls these helpers in sequence and returns their
ErrorResponse or the ScheduleCreatedResponse.

---

Nitpick comments:
In `@autogpt_platform/backend/backend/api/features/v1_test.py`:
- Line 1052: The test contains a local import of CopilotTurnJobInfo and
GraphExecutionJobInfo from backend.executor.scheduler; move that import
statement out of the local scope and add it to the module-level imports near the
other top-level imports so CopilotTurnJobInfo and GraphExecutionJobInfo are
imported at file import time rather than inside the test body.

In `@autogpt_platform/backend/backend/api/features/v1.py`:
- Around line 2336-2339: The code filters schedules with isinstance(s,
scheduler.CopilotTurnJobInfo) after calling
get_scheduler_client().get_execution_schedules(user_id=user_id,
kind="copilot_turn"); instead, remove the class-based filtering and return the
scheduler result directly (or assert/validate that each schedule.kind ==
"copilot_turn") so dispatch is driven by the kind discriminator rather than
isinstance checks; update the function that currently references schedules and
scheduler.CopilotTurnJobInfo to either return schedules as-is or perform a
lightweight kind validation on each item before returning.

In `@autogpt_platform/backend/backend/copilot/tools/manage_schedules.py`:
- Around line 46-71: The _to_summary function currently uses isinstance() to
branch on GraphExecutionJobInfo vs CopilotTurnJobInfo; change it to inspect the
job.kind discriminator instead (e.g., if job.kind == "graph": ... elif job.kind
== "copilot_turn": ...). Keep the same ScheduleSummary field mappings: for kind
"graph" populate graph_id and graph_version from GraphExecutionJobInfo fields;
for "copilot_turn" convert job.run_at to ISO string when present and populate
session_id and message. Preserve job.id/name/timezone/next_run_time/cron in both
branches and raise or handle an unexpected job.kind if neither matches.

In `@autogpt_platform/backend/backend/executor/scheduler_test.py`:
- Around line 101-110: The test currently catches a broad Exception when calling
scheduler.add_copilot_turn_schedule; narrow this to assert the expected failure
by checking the raised error and its message from the _build_trigger path:
replace the pytest.raises(Exception) with pytest.raises(RemoteError) or the
concrete transport-wrapped error type used in your test harness, capture it as
exc, and add an assertion that exc.value (or exc.value.args[0]) contains the
expected invalid-trigger message (e.g. text from _build_trigger like "invalid
trigger" or the exact ValueError message you expect) so the test only passes for
the intended rejection path originating from _build_trigger called by
scheduler.add_copilot_turn_schedule with test_user.id and session_id.

In `@autogpt_platform/backend/backend/executor/scheduler.py`:
- Around line 580-583: The _timezone_from_job function uses duck-typing via
hasattr on job_obj.trigger; change it to discriminate on job_obj.kind instead
and handle the cases that carry timezone info (e.g., the scheduled/cron trigger
kinds) by reading job_obj.trigger.timezone only for those kinds, otherwise
return "UTC"; apply the same kind-based branching refactor to the similar logic
referenced around the other function/section that inspects triggers (the block
at lines handling job kind -> trigger attributes) so all dispatch uses
job_obj.kind rather than hasattr/isinstance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 842ea27a-a4cb-47d2-820c-7bd2bb429a4f

📥 Commits

Reviewing files that changed from the base of the PR and between b50bf6b and ca617c1.

📒 Files selected for processing (28)
  • autogpt_platform/backend/backend/api/features/library/db.py
  • autogpt_platform/backend/backend/api/features/v1.py
  • autogpt_platform/backend/backend/api/features/v1_test.py
  • autogpt_platform/backend/backend/blocks/orchestrator.py
  • autogpt_platform/backend/backend/blocks/test/test_orchestrator_execution_mode.py
  • autogpt_platform/backend/backend/copilot/permissions.py
  • autogpt_platform/backend/backend/copilot/sdk/tool_adapter.py
  • autogpt_platform/backend/backend/copilot/sdk/tool_adapter_test.py
  • autogpt_platform/backend/backend/copilot/tools/__init__.py
  • autogpt_platform/backend/backend/copilot/tools/manage_schedules.py
  • autogpt_platform/backend/backend/copilot/tools/manage_schedules_test.py
  • autogpt_platform/backend/backend/copilot/tools/models.py
  • autogpt_platform/backend/backend/copilot/tools/schedule_followup.py
  • autogpt_platform/backend/backend/copilot/tools/schedule_followup_test.py
  • autogpt_platform/backend/backend/copilot/tools/tool_schema_test.py
  • autogpt_platform/backend/backend/copilot/tracking.py
  • autogpt_platform/backend/backend/data/diagnostics.py
  • autogpt_platform/backend/backend/executor/scheduler.py
  • autogpt_platform/backend/backend/executor/scheduler_test.py
  • autogpt_platform/backend/backend/executor/scheduler_unit_test.py
  • autogpt_platform/frontend/src/app/(platform)/library/followups/__tests__/main.test.tsx
  • autogpt_platform/frontend/src/app/(platform)/library/followups/components/EmptyFollowups/EmptyFollowups.tsx
  • autogpt_platform/frontend/src/app/(platform)/library/followups/components/FollowupListItem/FollowupListItem.tsx
  • autogpt_platform/frontend/src/app/(platform)/library/followups/components/FollowupListItem/helpers.ts
  • autogpt_platform/frontend/src/app/(platform)/library/followups/components/FollowupListItem/useFollowupListItem.ts
  • autogpt_platform/frontend/src/app/(platform)/library/followups/page.tsx
  • autogpt_platform/frontend/src/app/(platform)/library/followups/useFollowupsPage.ts
  • autogpt_platform/frontend/src/app/api/openapi.json

Comment thread autogpt_platform/backend/backend/copilot/tools/schedule_followup.py
@codecov

codecov Bot commented May 22, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 74.00000% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 71.65%. Comparing base (5ff2f27) to head (1475614).

Additional details and impacted files
@@                        Coverage Diff                         @@
##           feat/copilot-schedule-followup   #13192      +/-   ##
==================================================================
+ Coverage                           71.64%   71.65%   +0.01%     
==================================================================
  Files                                2224     2230       +6     
  Lines                              168261   168311      +50     
  Branches                            17101    17108       +7     
==================================================================
+ Hits                               120544   120603      +59     
+ Misses                              44177    44167      -10     
- Partials                             3540     3541       +1     
Flag Coverage Δ
platform-frontend 36.96% <74.00%> (+0.05%) ⬆️
platform-frontend-e2e 31.10% <ø> (+0.17%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Components Coverage Δ
Platform Backend 79.98% <ø> (ø)
Platform Frontend 41.88% <74.00%> (+0.12%) ⬆️
AutoGPT Libs ∅ <ø> (∅)
Classic AutoGPT 28.43% <ø> (ø)
🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@majdyz
majdyz changed the base branch from dev to feat/copilot-schedule-followup May 22, 2026 08:49
Add a Library page that lists pending copilot follow-up schedules and lets
users cancel them. Backed by a new GET /api/schedules/followups route that
returns only CopilotTurnJobInfo rows for the current user, so the generated
frontend client gets a single concrete return type instead of a union.

- backend: list_copilot_turn_schedules route + filter test
- frontend: /library/followups page with empty state, list item with
  next-run + cron labels, delete confirmation dialog
- tests: integration tests (empty / list / link / delete OK / delete fail)
@majdyz
majdyz force-pushed the feat/copilot-schedule-followup-ui branch from ca617c1 to 1475614 Compare May 22, 2026 09:00
Comment thread autogpt_platform/backend/backend/api/features/v1.py Outdated
majdyz added a commit that referenced this pull request May 22, 2026
@majdyz

majdyz commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

E2E Test Report

E2E Test Report: PR #13192 — feat(platform): copilot followups UI

  • Date: 2026-05-22
  • Branch: feat/copilot-schedule-followup-ui
  • Worktree: /Users/majdyz/Code/AutoGPT24
  • Stack: native — poetry run app (backend :8006) + pnpm dev (frontend :3000), docker deps profile for supabase/redis/rabbitmq/clamav.

Environment notes

  • Switched stack from PR feat(backend/copilot): native scheduling for copilot turn followups #13190's AutoGPT21 worktree to AutoGPT24 so the new /api/schedules/followups route is live. Confirmed via GET /openapi.json.
  • pnpm install re-run in the worktree to pick up @number-flow/react (declared in package.json but missing in node_modules, plus a next minor bump).
  • Copilot uses CHAT_USE_CLAUDE_CODE_SUBSCRIPTION=true (no fallback needed).
  • Test user test@test.com / testtest123.

Test Results

# Scenario Result
1 Cold load of /library/followups empty state PASS
2 Schedule 2 followups via copilot chat (cron + delay_seconds) PASS
3 /library/followups shows 2 rows with msg / next-run / cron-or-once / session prefix PASS
4 Row click opens /copilot?sessionId=… with full session history PASS
5 Delete one-shot via Cancel button + dialog → row disappears, toast shown PASS
6 Copilot "list my schedules" matches UI state PASS
7 API failure → ErrorCard renders PASS
8 Page handles copilot_turn-only filter with zero rows PASS

Detail

1. Cold load — empty state

Page renders header "Copilot follow-ups" + EmptyFollowups card "No copilot follow-ups — Ask your copilot to schedule something for later …". No errors. (02-followups-empty-state.png)

2. Schedule via copilot chat

Two follow-ups created in chat session 96d58196-3e3c-47de-ab70-4ebf11d21e61:

  • one-shot PR 13192 one-shot test, delay_seconds=120
  • cron PR 13192 daily cron test, cron 0 9 * * * (interpreted in Asia/Jakarta, the user's tz)

API verified — GET /api/schedules/followups returns 2 CopilotTurnJobInfo items with the right session_id, cron/run_at fields. (03-copilot-chat-scheduled-2-followups.png)

3. List page rendering

Both rows visible with message preview, "Next in less than a minute" / "Next in about 17 hours", "Runs once" / "Every day at 09:00", "Session 96d58196". Cron expression humanized correctly. (04-followups-list-2-rows.png)

4. Deep link click

data-testid="followup-open-session" link uses ?sessionId=… (frontend convention; the task informally referred to session_id). Click navigated to /copilot?sessionId=96d58196-3e3c-47de-ab70-4ebf11d21e61 with full session history visible (previous scheduling messages + tool outputs). (05-deep-link-copilot-session.png)

5. Delete

Original one-shot fired before I could click delete (only 120s), so I scheduled a new one with delay_seconds=3600 and deleted that one. Clicking the Delete button opened the Dialog ("Delete follow-up — Delete this scheduled follow-up?") with Keep it / Yes, delete buttons. Confirming triggered DELETE /api/schedules/{id}, toast "Follow-up deleted" appeared, row was removed via getListCopilotFollowupSchedulesQueryKey invalidation. API confirms the schedule is gone. (06-…before-delete.png, 07-…dialog.png, 08-…after-delete.png)

Note: button copy is "Delete" (with data-testid="followup-cancel-button" and aria-label="Delete follow-up"). The testid name is a leftover from earlier "Cancel" wording — minor consistency nit, not a blocker.

6. Copilot cross-check

"List my current follow-up schedules" returned Found 1 schedule(s). matching the UI's state (only the cron PR 13192 daily cron test remained). Copilot also noted the recently-deleted one-shot was missing, consistent with what we did in the UI. (09-copilot-cross-check-list.png)

7. Network / error handling

Used agent-browser network route 'http://localhost:3000/api/proxy/api/schedules/followups' --abort + window.location.reload(). Page renders ErrorCard with "Something went wrong — We had the following error when retrieving copilot follow-ups:" + Report Error button. No blank page / crash. (10-error-card.png)

8. Empty state with zero rows

After deleting the cron schedule via direct API DELETE, the page re-mounts to the EmptyFollowups empty state without trying to render any graph-only fields. No console errors. (11-empty-state-after-cleanup.png)

Summary

  • Total: 8
  • Passed: 8
  • Failed: 0
  • Bugs found / fixed: none functional. Minor copy inconsistency: data-testid="followup-cancel-button" survives a rename from "Cancel" → "Delete" — non-blocking.

Loose ends

Screenshots

00-login-page.png
01-copilot-page-logged-in.png
02-followups-empty-state.png
03-copilot-chat-scheduled-2-followups.png
04-followups-list-2-rows.png
05-deep-link-copilot-session.png
06-followups-list-before-delete.png
07-delete-confirmation-dialog.png
08-followups-after-delete.png
09-copilot-cross-check-list.png
10-error-card.png
11-empty-state-after-cleanup.png

…tch, delete terminology

- backend route: replace defensive `[s for s in schedules if isinstance(s, CopilotTurnJobInfo)]`
  with `cast(...)`. The `kind="copilot_turn"` filter is the source of truth; the
  trailing isinstance pass was tautology that would silently drop rows if the
  discriminator ever drifted. Cast narrows the static type without runtime cost.
- useFollowupListItem: drop `onDeleted` callback. `invalidateQueries` already
  triggers a refetch, and the parent-supplied callback was double-work that
  raced the toast in the failure path. Single source of truth.
- FollowupListItem: rename action from "Cancel" to "Delete" everywhere
  (button label, aria-label, dialog title, confirm button text "Yes, delete",
  back button "Keep it", toast titles). The action IS a delete (calls
  `useDeleteV1DeleteExecutionSchedule`); "Cancel" overloaded with
  cancel-this-dialog. Matching tests updated.
- page.tsx: drop now-unused `refetchFollowups` from the destructure.
@majdyz

majdyz commented May 22, 2026

Copy link
Copy Markdown
Contributor Author

Roadmap — copilot followup management

Capturing the bigger plan so the scope of this PR (and what's still ahead) is explicit. This PR is the first slice — the UI page only. Everything below is what comes next.

What we're ultimately trying to achieve

When a user changes their mind about a scheduled followup, the happy path is conversational, not a separate UI ritual:

User: "actually never mind, cancel that"
AutoPilot: "Got it — cancelled the followup that was set for tomorrow 9am."

The UI page added by this PR (/library/followups) is the fallback — for users who want to see everything in one place, audit, or batch-delete. The primary cancel/modify path should live in the chat.

Why the chat-mediated path is P0 over UI affordances

  1. Same surface that created the schedule cancels it. Users who scheduled a followup via natural language ("check the CI in 20 min") will instinctively cancel via natural language. Forcing them to a list page is a context switch.
  2. Modification is multi-step in the UI (read → edit modal → save). In chat it's one sentence.
  3. The model already has the conversational context to disambiguate ("cancel the CI check" vs "cancel the morning summary").

So the UI is the "atlas view", chat is the "GPS".

What's required for AutoPilot to do this

For the model to honour "cancel that", it needs three pieces of information that today's prompt does not surface:

  1. Whether anything is scheduled on this session at all. Otherwise the model has no signal to bring it up proactively ("by the way, you have a followup at 9am").
  2. The current session id, so when the model calls list_schedules/delete_schedule it can filter to this conversation (passing session_id to list_schedules is supported on feat(backend/copilot): native scheduling for copilot turn followups #13190 — the model needs to know which id to pass).
  3. A reliable way to enumerate + act on its own schedules, which we already have via list_schedules(session_id=...) and delete_schedule(schedule_id=...).

The first two are the open work.

The prompt-caching constraint (this is the load-bearing concern)

Anthropic prompt caching keys off prefix bytes. If we naively inject session_id into the system prompt or any cacheable prefix, cache hit rate drops to 0% across sessions because every session has a unique UUID. That's a >50% regression on the latency/cost dial we just spent weeks tuning.

Concrete rule: session_id cannot live in the cached prefix. The placement options, ranked by safety:

Placement Cache-safe? Notes
Inside latest user message (not system) User-message content is past the last cache_control breakpoint. New per-turn anyway.
Injected via PostToolUse / tool-result text Tool results are also past the cacheable prefix. Could be returned by a new get_my_context() tool.
system_post (some Anthropic-format clients support a post-context block) ⚠️ Verify with our SDK's exact placement — must land AFTER the final cache_control: ephemeral marker.
In the system prompt Every session busts cache. Do not do this.
Per-tool description (e.g. "your session_id is X") Tools are part of the cacheable prefix in our setup. Same problem.

Proposed slices (post-#13192)

Slice A — Session self-awareness for the model. Lowest-risk, highest-leverage.

  • Add a single line to the latest user message at turn dispatch time: [context] current_session_id=<uuid>; pending_followups=<n> (or null when zero). This lands AFTER the last cache breakpoint, so it does NOT bust the prefix cache. Compute pending_followups cheaply via the existing list_schedules(session_id=current) call — already O(1) per turn.
  • Update the schedule-related tools' descriptions (NOT the values — descriptions are cached) to mention: "if you weren't given a session_id explicitly, use the session_id from the [context] line in this turn's user message."
  • Cache-impact test: run a A/B with one with-context-line / one without on identical messages, confirm cache_read_input_tokens is identical before the user-message block.

Slice B — Proactive followup reminders in chat. Optional.

  • When the user opens a session that has pending followups, the autopilot's first response includes a one-liner: "You have 2 followups pending: 'Check CI' (in 20m), 'Daily summary' (every weekday 9am). Say 'cancel X' to remove one."
  • Implementable as a system_post note or a synthetic tool-result injected before the model runs.

Slice C — Modification via chat. "Move that to 8am instead" — needs a replace_followup_schedule(old_id, new_when, new_message) tool. Could compose delete_schedule + schedule_followup but as a single tool call avoids the orphan-window between delete and re-create.

Slice D — UI nav link. Add Followups to the library sidebar (the thread above tracks this). Trivial change, but easier once we know where Library / Marketplace / Copilot sit relative to each other in the broader nav redesign.

Are we passing session_id today?

No — verified by grepping backend/copilot/sdk/ and backend/copilot/prompting.py. The model has zero awareness of its own session UUID. The closest signal is conversation history (which can hint via prior schedule_followup calls), but a fresh session inheriting an old transcript would have no way to disambiguate. Slice A is genuinely net-new visibility, not just "expose what's already there".

Why this is in this PR's comments and not a separate issue

This PR (#13192) is the UI page slice. The next slice (A) doesn't belong in #13192 because:

  • It modifies the copilot prompt / SDK adapter, which is a different review surface (backend/copilot, not platform/library).
  • Stacking it would make the diff cross two concerns and slow review of both.
  • The slices are independent: this PR is shippable without A, and A is shippable without the UI page (it just makes chat-mediated cancel work).

Once #13190 + #13192 land, I'll open a fresh PR for Slice A with the cache-impact A/B test attached as PR-test evidence.

Open questions worth flagging now

  1. Should [context] injection happen for every turn, or only the first turn of a session? Every-turn is safe (it lands past the cache boundary) but is ~50 extra tokens per turn at scale. First-turn-only is cheaper but adds state to the SDK adapter.
  2. What's the right pending_followups truncation? Showing 5 inline before "…+12 more" is probably fine. Beyond that the message bloats and the user should be sent to /library/followups.
  3. Race between "cancel that" and the schedule firing. If the model says "cancelled" but the fire-time is 30s away, do we accept the race or coalesce? Probably accept — the worst case is one redundant turn arrives.

Tagging this so we don't forget when #13190 + #13192 are in dev.

@majdyz
majdyz merged commit a59947a into feat/copilot-schedule-followup May 22, 2026
13 of 17 checks passed
@majdyz
majdyz deleted the feat/copilot-schedule-followup-ui branch May 22, 2026 11:52
@github-project-automation github-project-automation Bot moved this to Done in Frontend May 22, 2026
@github-project-automation github-project-automation Bot moved this from 🆕 Needs initial review to ✅ Done in AutoGPT development kanban May 22, 2026
majdyz added a commit that referenced this pull request May 22, 2026
The action was renamed end-to-end (button label, dialog title, toast,
aria-label) in #13192's polish round 1, but the data-testid attributes
kept their old 'cancel' names. Rename both to match: 'followup-cancel-button'
→ 'followup-delete-button' and 'followup-confirm-cancel' →
'followup-confirm-delete'. Tests updated to match.

Cosmetic only; no behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform/backend AutoGPT Platform - Back end platform/frontend AutoGPT Platform - Front end size/xl

Projects

Status: ✅ Done
Status: Done

Development

Successfully merging this pull request may close these issues.

1 participant