Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
1 change: 1 addition & 0 deletions python/packages/core/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,7 @@ The vector store API is experimental under the shared `VECTOR_STORES` feature ID
- **`todos_remaining(*, looping_modes=None)`** / **`todos_remaining_message`** - Helper factories for todo-driven loops (the Python counterpart of .NET's `TodoCompletionLoopEvaluator`), designed for `create_harness_agent` but usable with any agent that registers a `TodoProvider` via `context_providers`. They resolve the `TodoProvider`/`AgentModeProvider` from the *running agent* (`agent.context_providers`, via `_resolve_context_provider`) rather than taking the provider as an argument, so they can be wired directly into `loop_should_continue`/`loop_next_message`. `todos_remaining` returns a `should_continue` predicate that loops while any todo is open; pass `looping_modes=[...]` to gate looping to specific operating modes (case-insensitive; honors the `AgentModeProvider`'s `source_id`/`available_modes`), `looping_modes=None` (default) applies in every mode, and an empty sequence raises `ValueError`. `todos_remaining_message` is a `next_message` callable that lists the still-open todo titles and tells the agent to finish them, returning `None` when the session/agent/provider is unavailable or nothing is open (in which case the middleware's default `None` handling applies: reuse the previous iteration's messages verbatim under the default `fresh_context=False`, or `DEFAULT_NEXT_MESSAGE` only when `fresh_context=True`).
- **`background_tasks_running()`** / **`background_tasks_running_message`** - Helper factories for background-agent-driven loops, mirroring the `todos_remaining` pair. They resolve the `BackgroundAgentsProvider` from the *running agent* (`agent.context_providers`, via `_resolve_context_provider`) rather than taking the provider as an argument, so they can be wired directly into `create_harness_agent`'s `loop_should_continue`/`loop_next_message`. `background_tasks_running` returns a `should_continue` predicate that loops while the provider's persisted state shows any task with `status == RUNNING` (pair it with `max_iterations` so the loop is bounded even if a task's persisted status is never refreshed). `background_tasks_running_message` is a `next_message` callable that lists the still-running tasks (`#<id> (<agent_name>): <description>`) and tells the agent to wait for them to finish and retrieve their results, returning `None` when the session/agent/provider is unavailable or no task is running.
- **Approval escape hatch** - `_has_pending_approval_request(result)` checks whether an iteration's response carries a pending tool-approval request (any content with `type == "function_approval_request"`). Both the streaming and non-streaming loops stop and return that response to the caller *before* evaluating `should_continue`/`max_iterations` or injecting `next_message`, so the loop is HITL-safe even when wrapped outermost around a `ToolApprovalMiddleware` (mirrors the C# `LoopAgent`'s `HasPendingApprovalRequests`).
- **Signal protocol** (`_harness/_loop.py`, alongside `AgentLoopMiddleware`) - `SignalParser` and `signal_should_continue()` provide a drop-in `should_continue` predicate that stops the loop when the agent emits `TASK_COMPLETE: <summary>` (sets exit reason `"completed"`) or `NEED_INPUT: <question>` (sets exit reason `"need_input"`). A token counts only at the start of a line (leading whitespace allowed); mid-sentence or backtick-quoted mentions are ignored. `SignalParser` and `get_loop_exit_reason` share one detection helper (`_find_signal`). Only assistant-role messages are scanned; user-role nudges injected between iterations are ignored. `LoopExitReason` holds the typed string constants; the tokens are exposed as `SignalParser.TASK_COMPLETE_TOKEN` / `SignalParser.NEED_INPUT_TOKEN`. All four public symbols are `@experimental(feature_id=ExperimentalFeature.HARNESS)`. `get_loop_exit_reason(response)` scans assistant text first, then falls back to `response.additional_properties["loop_exit_reason"]` (unconditionally overwritten by the middleware when the iteration cap fires; non-string values are ignored). Signal tokens always win over the cap. The middleware stamps the cap reason using the shared `LOOP_EXIT_REASON_KEY` / `LoopExitReason.iteration_cap_reached` constants defined in the same module.
- **Harness integration** - `create_harness_agent` enables the loop when a `loop_should_continue` callable is passed; it prepends `AgentLoopMiddleware(loop_should_continue, max_iterations=loop_max_iterations, next_message=loop_next_message)` ahead of `ToolApprovalMiddleware` so the loop is the outermost middleware (each iteration is a full agent run including tool approval, and the escape hatch hands pending approvals back to the caller). `loop_next_message` and `loop_max_iterations` only take effect together with `loop_should_continue` (with no `loop_should_continue` there is no loop, so they are ignored); `loop_max_iterations` defaults to the loop's default cap (`None` → unbounded).

### Workflows (`_workflows/`)
Expand Down
8 changes: 8 additions & 0 deletions python/packages/core/agent_framework/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,12 @@
"._harness._loop": (
"AgentLoopMiddleware",
"JudgeVerdict",
"LoopExitReason",
"SignalParser",
"background_tasks_running",
"background_tasks_running_message",
"get_loop_exit_reason",
"signal_should_continue",
"todos_remaining",
"todos_remaining_message",
),
Expand Down Expand Up @@ -554,6 +558,7 @@
"InlineSkillScript",
"JudgeVerdict",
"LocalEvaluator",
"LoopExitReason",
"MCPSkill",
"MCPSkillResource",
"MCPSkillsSource",
Expand Down Expand Up @@ -596,6 +601,7 @@
"ServiceSessionId",
"SessionContext",
"SessionStore",
"SignalParser",
"SingleEdgeGroup",
"Skill",
"SkillFrontmatter",
Expand Down Expand Up @@ -696,6 +702,7 @@
"executor",
"function_middleware",
"get_agent_mode",
"get_loop_exit_reason",
"get_run_context",
"handler",
"included_messages",
Expand All @@ -715,6 +722,7 @@
"resolve_agent_id",
"response_handler",
"set_agent_mode",
"signal_should_continue",
"step",
"todos_remaining",
"todos_remaining_message",
Expand Down
8 changes: 8 additions & 0 deletions python/packages/core/agent_framework/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,12 @@ from ._harness._file_memory import DEFAULT_FILE_MEMORY_INSTRUCTIONS, DEFAULT_FIL
from ._harness._loop import (
AgentLoopMiddleware,
JudgeVerdict,
LoopExitReason,
SignalParser,
background_tasks_running,
background_tasks_running_message,
get_loop_exit_reason,
signal_should_continue,
todos_remaining,
todos_remaining_message,
)
Expand Down Expand Up @@ -509,6 +513,7 @@ __all__ = [
"InlineSkillScript",
"JudgeVerdict",
"LocalEvaluator",
"LoopExitReason",
"MCPSkill",
"MCPSkillResource",
"MCPSkillsSource",
Expand Down Expand Up @@ -551,6 +556,7 @@ __all__ = [
"ServiceSessionId",
"SessionContext",
"SessionStore",
"SignalParser",
"SingleEdgeGroup",
"Skill",
"SkillFrontmatter",
Expand Down Expand Up @@ -651,6 +657,7 @@ __all__ = [
"executor",
"function_middleware",
"get_agent_mode",
"get_loop_exit_reason",
"get_run_context",
"handler",
"included_messages",
Expand All @@ -670,6 +677,7 @@ __all__ = [
"resolve_agent_id",
"response_handler",
"set_agent_mode",
"signal_should_continue",
"step",
"todos_remaining",
"todos_remaining_message",
Expand Down
Loading
Loading