mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
bad05a2bdc
* Add initial harness console for python * Add textual to project * Add planning and approval flows with list selector * Address PR comments * Fix list selection bug * Fix PR #6312 round 2 review comments - Escape untrusted agent text with rich.markup.escape() in observers (text_output, planning_output, reasoning_display) to prevent markup injection - Remove non-functional 'Always approve' choices from tool_approval.py (framework lacks CreateAlwaysApproveToolResponse support) - Remove textual from root pyproject.toml dev deps (sample-specific) - Add PEP 723 inline script metadata to harness_research.py - Narrow except Exception to except NoMatches in list_selection.py Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Fix build error * Fix build errors --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
261 lines
7.5 KiB
Python
261 lines
7.5 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""Application state and core data types for the harness console.
|
|
|
|
This module defines enums, dataclasses, follow-up action types, and the
|
|
HarnessAppState dataclass which holds all UI state that may change during
|
|
application execution. The state driver mutates this state to coordinate
|
|
between the agent runner and the Textual UI components.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import TYPE_CHECKING
|
|
|
|
if TYPE_CHECKING:
|
|
from agent_framework import Message
|
|
|
|
from .state_driver import IUXStateDriver
|
|
|
|
|
|
# region Enums
|
|
|
|
|
|
class OutputEntryType(Enum):
|
|
"""Type of output entry in the console conversation."""
|
|
|
|
USER_INPUT = "user_input"
|
|
"""User input echo (e.g., 'You: hello')."""
|
|
|
|
STREAMING_TEXT = "streaming_text"
|
|
"""In-progress streaming text from the agent (accumulated chunk by chunk)."""
|
|
|
|
INFO_LINE = "info_line"
|
|
"""Informational line (tool calls, errors, usage, approval requests, etc.)."""
|
|
|
|
STREAM_FOOTER = "stream_footer"
|
|
"""Stream footer (e.g., '(no text response from agent)')."""
|
|
|
|
PENDING_MESSAGE = "pending_message"
|
|
"""Pending injected message notification."""
|
|
|
|
|
|
class BottomPanelMode(Enum):
|
|
"""Mode of the bottom panel UI."""
|
|
|
|
TEXT_INPUT = "text_input"
|
|
"""Show text input for user messages."""
|
|
|
|
LIST_SELECTION = "list_selection"
|
|
"""Show choice list for user selection."""
|
|
|
|
STREAMING = "streaming"
|
|
"""Show 'streaming...' indicator while agent is generating."""
|
|
|
|
|
|
# endregion
|
|
|
|
# region Output Entry
|
|
|
|
|
|
@dataclass
|
|
class OutputEntry:
|
|
"""A single output entry in the console conversation history.
|
|
|
|
Used internally by the state driver to track conversation output,
|
|
including streaming text, tool calls, errors, and user input echoes.
|
|
|
|
Args:
|
|
type: The type of output entry.
|
|
text: The text content of the entry.
|
|
color: Optional Rich color string (e.g., "cyan", "red", "dim").
|
|
"""
|
|
|
|
type: OutputEntryType
|
|
text: str
|
|
color: str | None = None
|
|
|
|
|
|
# endregion
|
|
|
|
# region Follow-Up Actions
|
|
|
|
|
|
class FollowUpAction:
|
|
"""Base class for follow-up actions returned by observers.
|
|
|
|
Follow-up actions describe either a question to ask the user
|
|
(via FollowUpQuestion subclasses) or a message to add directly
|
|
to the next agent input (FollowUpMessage).
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class FollowUpQuestion(FollowUpAction):
|
|
"""A question to ask the user with a continuation.
|
|
|
|
The continuation delegate is invoked with the user's answer and the
|
|
UX state driver, and returns an optional Message to add to the next
|
|
agent invocation.
|
|
|
|
Args:
|
|
prompt: The question text shown to the user.
|
|
continuation: Async function invoked with the user's answer and state driver.
|
|
Returns an optional Message to add to the next agent input.
|
|
"""
|
|
|
|
prompt: str
|
|
continuation: Callable[[str, IUXStateDriver], Awaitable[Message | None]]
|
|
|
|
|
|
@dataclass
|
|
class TextFollowUpQuestion(FollowUpQuestion):
|
|
"""A free-form text question.
|
|
|
|
The user may type any response. This is the base FollowUpQuestion type
|
|
with no additional constraints.
|
|
"""
|
|
|
|
pass
|
|
|
|
|
|
@dataclass
|
|
class ChoiceFollowUpQuestion(FollowUpQuestion):
|
|
"""A multiple choice question.
|
|
|
|
The user picks from the provided choices, with an optional ability to
|
|
enter custom text when allow_custom_text is True.
|
|
|
|
Args:
|
|
prompt: The question text shown to the user.
|
|
choices: List of pre-defined choices.
|
|
allow_custom_text: If True, the user may type a custom response in
|
|
addition to the listed choices.
|
|
continuation: Async function invoked with the user's choice/text and
|
|
state driver. Returns an optional Message to add to the next agent input.
|
|
"""
|
|
|
|
choices: list[str]
|
|
allow_custom_text: bool = False
|
|
|
|
|
|
@dataclass
|
|
class FollowUpMessage(FollowUpAction):
|
|
"""A message to add directly to the next agent invocation without prompting.
|
|
|
|
Used when an observer wants to inject a message into the conversation
|
|
without user interaction (e.g., automatic tool results, system messages).
|
|
|
|
Args:
|
|
message: The Message to add to the conversation.
|
|
"""
|
|
|
|
message: Message
|
|
|
|
|
|
# endregion
|
|
|
|
# region Application State
|
|
|
|
|
|
@dataclass
|
|
class HarnessAppState:
|
|
"""All UI state for the harness console application.
|
|
|
|
This state is mutated by the UX state driver and read by the Textual
|
|
app to update the UI.
|
|
"""
|
|
|
|
# --- Bottom panel mode ---
|
|
|
|
mode: BottomPanelMode = BottomPanelMode.TEXT_INPUT
|
|
"""Which component is shown in the bottom panel."""
|
|
|
|
# --- Follow-up question queue ---
|
|
|
|
pending_questions: list[FollowUpQuestion] = field(default_factory=list)
|
|
"""Queue of follow-up questions waiting for user answers.
|
|
|
|
The head ([0]) is the question currently being displayed; subsequent items
|
|
are dispatched in order as each is answered.
|
|
"""
|
|
|
|
accumulated_follow_up_responses: list[Message] = field(default_factory=list)
|
|
"""Accumulated follow-up response messages collected during the current agent turn.
|
|
|
|
Both direct FollowUpMessages emitted by observers and continuation results
|
|
from answered questions. Consumed by the runner via take_follow_up_responses().
|
|
"""
|
|
|
|
# --- Text input (active in TextInput / Streaming modes) ---
|
|
|
|
prompt: str = "> "
|
|
"""The prompt string for text input mode."""
|
|
|
|
placeholder: str = ""
|
|
"""Placeholder text shown when the input is empty."""
|
|
|
|
input_text: str = ""
|
|
"""The current input text being typed."""
|
|
|
|
input_enabled: bool = True
|
|
"""Whether input is enabled (disabled during streaming without injection)."""
|
|
|
|
streaming_prompt: str = "(agent is running...)"
|
|
"""The prompt to show during streaming when input is disabled."""
|
|
|
|
# --- List selection (active in ListSelection mode) ---
|
|
|
|
list_selection_title: str | None = None
|
|
"""Title text displayed above the list selection."""
|
|
|
|
list_selection_options: list[str] = field(default_factory=list)
|
|
"""The list selection options."""
|
|
|
|
list_selection_index: int = 0
|
|
"""The highlighted option index in list selection mode."""
|
|
|
|
list_selection_custom_text_placeholder: str | None = None
|
|
"""Placeholder text for the custom text input option in the list."""
|
|
|
|
list_selection_custom_input_text: str = ""
|
|
"""Current text being typed into the list's custom text option."""
|
|
|
|
# --- Scroll / output area ---
|
|
|
|
output_entries: list[OutputEntry] = field(default_factory=list)
|
|
"""Output entries in the scroll area conversation history."""
|
|
|
|
queued_items: list[str] = field(default_factory=list)
|
|
"""Queued input items to display (pending injected messages)."""
|
|
|
|
# --- Agent mode + status display ---
|
|
|
|
mode_color: str | None = None
|
|
"""Rich color string for the rule borders and mode label."""
|
|
|
|
mode_text: str | None = None
|
|
"""Current mode name displayed (e.g., 'plan', 'execute')."""
|
|
|
|
help_text: str | None = None
|
|
"""Help text displayed below the bottom rule (available commands)."""
|
|
|
|
show_spinner: bool = False
|
|
"""Whether the agent status spinner is visible."""
|
|
|
|
usage_text: str | None = None
|
|
"""Formatted token usage text to display in the status bar."""
|
|
|
|
# --- Command handler signals ---
|
|
|
|
shutdown_requested: bool = False
|
|
"""Set to True when /exit is invoked; the app should exit."""
|
|
|
|
replaced_session: object | None = None
|
|
"""When set, the app should swap its session to this AgentSession."""
|