Skip to content

feat: add support for plans and steps - #648

Open
Lily Du (lilyydu) wants to merge 9 commits into
mainfrom
lilyydu/plan-design
Open

Lily Du (lilyydu) wants to merge 9 commits into
mainfrom
lilyydu/plan-design

Conversation

@lilyydu

Copy link
Copy Markdown
Collaborator

Summary

  • Add Plan, Step, StepSource, and PlanInfoEntity, with ctx.plan() for prompt-bound progress.
  • Validate outgoing payloads and handle control acknowledgments and completion responses.
  • Support safe Plan completion through the existing HTTP stream.

Copilot AI balanced review requested due to automatic review settings October 26, 2026 23:17

Copilot AI 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.

🟡 Changes recommended

Plan completion can bypass targeted-message privacy and accept conflicting control metadata instead of completing the Plan.

2 open issues
What changed in this PR

Adds prompt-bound Plans and progress steps to the Teams SDK, with completion through normal messages or the existing HTTP stream.

Changes:

  • Introduces Plan models and the ctx.plan() helper.
  • Adds control acknowledgment handling, completion routing, and stream timeout protection.
  • Adds model, transport, and application tests plus a lifecycle example.
File Description
uv.lock Registers the Plan example workspace package.
README.md Links to the Plan example.
packages/​apps/​tests/​test_plan.py Tests helpers, prompt binding, and send routing.
packages/​apps/​tests/​test_plan_stream.py Tests streamed completion and timeout behavior.
packages/​apps/​src/​microsoft_teams/​apps/​routing/​activity_context.py Adds cached, prompt-bound ctx.plan() helpers.
packages/​apps/​src/​microsoft_teams/​apps/​plan.py Implements Plan controls and completion.
packages/​apps/​src/​microsoft_teams/​apps/​http_stream.py Prevents Plan completion through the update fallback.
packages/​apps/​src/​microsoft_teams/​apps/​activity_send.py Routes Plan activities through creation rather than updates.
packages/​apps/​src/​microsoft_teams/​apps/​__init__.py Exports Plan.
packages/​api/​tests/​unit/​test_plan_transport.py Tests acknowledgments, completion responses, and errors.
packages/​api/​tests/​unit/​test_plan_models.py Tests model parsing and serialization.
packages/​api/​src/​microsoft_teams/​api/​models/​entity/​plan_info_entity.py Defines Plan and step models.
packages/​api/​src/​microsoft_teams/​api/​models/​entity/​entity.py Registers typed Plan entity parsing.
packages/​api/​src/​microsoft_teams/​api/​models/​entity/​__init__.py Exports Plan models and types.
packages/​api/​src/​microsoft_teams/​api/​clients/​conversation/​activity.py Handles Plan control acknowledgments.
examples/​plan/​src/​main.py Demonstrates completion, streaming, controls, and proactive Plans.
examples/​plan/​README.md Documents example commands.
examples/​plan/​pyproject.toml Defines example dependencies.

Review effort: Balanced

Comment thread packages/apps/src/microsoft_teams/apps/plan.py Outdated
Comment thread packages/apps/src/microsoft_teams/apps/plan.py Outdated
# module, so holding it here would be circular.
self._oauth_connection_names: List[str] = list(oauth_connection_names or [])
self._stream: Optional[StreamerProtocol] = None
self._plans: dict[str, Plan] = {}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Why is this a dict? Can’t you only have 1 Plan per activity?



class PlanConflictStreamError(HTTPStatusError):
"""A Plan conflict that must reach the caller without retry."""

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

what does this error actually mean? can you expand the comment?

Comment thread examples/plan/src/main.py
create_lookup_step(),
Step(
id="answer",
version=1,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for “version” let’s include a comment to draw attention that it needs to be sequentially increasing

Comment thread examples/plan/src/main.py
),
Step(
id="answer",
version=2,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

wait DOES it need to be sequentially increasing? what does version actually mean?

class StepSource(CustomBaseModel):
title: str
url: str
icon: Optional[str] = None

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

What format does icon accept—an emoji, an image URL, or a predefined icon name? Could we document the expected format with an example? If the service accepts a fixed set of names, a Literal or enum would make that contract clearer.

if not isinstance(entity_type, str):
return "_unknown"

if entity_type == "planInfo":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we add "planInfo": "planInfo" to _KNOWN_ENTITY_TYPE_TAGS instead of adding a separate branch? It has a unique type string, so the existing lookup handles it without special-casing. The isinstance checks above are needed to distinguish models that share the same type string.

Preview API. Diagnostic: ExperimentalTeamsPlan
"""

def __init__(self, api: ApiClient, conversation_ref: ConversationReference, *, reply_to_id: str) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we add a constructor docstring documenting each parameter (api, conversation_ref, and reply_to_id)? In particular, clarify which activity ID reply_to_id should refer to and how it relates to conversation_ref.activity_id. It would also help to mention that the conversation reference is copied and the API client is scoped to its service URL.

await self._send_plan_update("error")

async def _send_plan_update(self, operation: PlanType, steps: Sequence[Step] | None = None) -> None:
entity = PlanInfoEntity(plan_type=operation, steps=list(steps) if steps is not None else None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we add a debug log here identifying the plan operation and reply-to activity ID? That would help trace lifecycle sends when debugging without logging the step contents.

self._reply_to_id = reply_to_id
self.events = EventEmitter[Literal["sent"]]()

async def start(self, steps: Sequence[Step]) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we add docstrings for every public member of Plan, including start(), update(), stop(), error(), and the events emitter? Please document parameters, behavior, and relevant return values or exceptions. For events, explain the sent payload and when it fires.

self._reply_to_id = reply_to_id
self.events = EventEmitter[Literal["sent"]]()

async def start(self, steps: Sequence[Step]) -> None:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Could we also add this requirement to .github/copilot-instructions.md so future agent-generated changes include docstrings for public APIs by default? The instruction should cover public classes, constructors, methods, and exposed events, documenting their behavior, parameters, and relevant return values or exceptions.

"""
activity = self._final_activity or MessageActivityInput()
if any(isinstance(entity, PlanInfoEntity) for entity in activity.entities or []):
raise StreamTimedOutError(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

How should the Plan and streaming error contracts relate here? I understand why a Plan completion can't use the PUT fallback, but should callers receive StreamTimedOutError for that limitation, or a Plan-specific error? Could we clarify how callers should distinguish this case and recover?

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants