Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
4ba8bd4
refactor(agent): introduce runtime contracts and step engine
Aug 7, 2026
da0fc8e
refactor(agent): add host-neutral agent loop
Aug 7, 2026
79b9f1a
refactor(runtime): add external service ports
Aug 7, 2026
75ceb24
refactor(session): introduce lifecycle session host
Aug 7, 2026
19fad5b
refactor(session): consolidate runtime execution loops
Aug 7, 2026
5870662
refactor: remove legacy task and subtask systems
xiami762 Aug 7, 2026
942b6be
refactor: clarify runtime loop context
xiami762 Aug 7, 2026
eec04fa
fix(runtime): preserve memory and deduplicate errors
xiami762 Aug 7, 2026
2adda5f
merge: sync dev into runtime refactor
xiami762 Aug 7, 2026
614e0f3
refactor(runtime): centralize prompt context assembly
xiami762 Aug 7, 2026
936de90
refactor(runtime): simplify session execution contracts
xiami762 Aug 7, 2026
920f1c7
chore: split runtime-adjacent changes
xiami762 Aug 11, 2026
1651ce2
Merge remote-tracking branch 'origin/dev' into refactor/agent-runtime…
xiami762 Aug 11, 2026
b94e1fb
chore: isolate runtime refactor scope
xiami762 Aug 11, 2026
e10fb04
chore: remove split diff noise
xiami762 Aug 11, 2026
a3a8f2e
merge(dev): resolve session runtime conflicts
Aug 13, 2026
682fcf1
fix(session): preserve hook continuation semantics
Aug 13, 2026
4aeb5fc
Merge remote-tracking branch 'origin/dev' into refactor/agent-runtime…
Aug 13, 2026
c66ee6f
Merge remote-tracking branch 'origin/dev' into refactor/agent-runtime…
Aug 13, 2026
933f5bd
test(session): remove obsolete runner tests
Aug 13, 2026
c5c3ded
fix(runtime): preserve retry accounting and task compatibility
Aug 14, 2026
50991f2
Merge remote-tracking branch 'origin/dev' into refactor/agent-runtime…
Aug 14, 2026
25d5000
fix(tui): use primary agent for review command
Aug 17, 2026
d56dbc1
fix(runtime): harden session lifecycle boundaries
Aug 17, 2026
42b496f
refactor(runtime): remove redundant session loop paths
Aug 17, 2026
7dc7958
Merge remote-tracking branch 'origin/dev' into refactor/agent-runtime…
Aug 17, 2026
59ef881
Revert "refactor(delegation): migrate legacy flows to delegate_task"
Aug 17, 2026
28f10cf
refactor(runtime): remove prompt changes split to PR 720
Aug 17, 2026
d9184d6
refactor(runtime): finish split cleanup
Aug 17, 2026
35641e7
perf(session): bound active model request state
Aug 17, 2026
bfcb603
fix(session): make runtime handoffs completion-safe
Aug 18, 2026
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
15 changes: 7 additions & 8 deletions flocks/channel/inbound/dispatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,14 +237,14 @@ async def deliver_text(self, text: str) -> None:
session_id=self.session_id,
)

def to_loop_callbacks(self, runner_callbacks=None):
def to_loop_callbacks(self, *, on_text_delta=None):
"""Convert to a LoopCallbacks dataclass understood by SessionLoop."""
from flocks.session.session_loop import LoopCallbacks
return LoopCallbacks(
on_step_end=self.on_step_end,
on_text_delta=on_text_delta,
on_error=self.on_error,
event_publish_callback=self._publish_sse_event,
runner_callbacks=runner_callbacks,
)

@staticmethod
Expand Down Expand Up @@ -514,8 +514,8 @@ async def _dispatch(self, msg: InboundMessage) -> None:
# what _process_session_message does in the WebUI route. Storing
# the resolved model on the user message keeps two things aligned
# between WebUI and channel:
# - Title generation (``SessionLoop._run_loop`` reads
# ``last_user.model``).
# - Title generation and turn preparation read
# ``last_user.model``.
# - The provider-specific base prompt template
# (``SystemPrompt.provider``) selected on the next loop tick.
# Without this, channel sessions ended up with the hardcoded
Expand Down Expand Up @@ -1223,13 +1223,12 @@ async def _run_agent_with_streaming(
return

try:
from flocks.session.runner import RunnerCallbacks

async def _on_text_delta(delta: str) -> None:
await card.append(delta)

runner_cbs = RunnerCallbacks(on_text_delta=_on_text_delta)
loop_callbacks = callbacks.to_loop_callbacks(runner_callbacks=runner_cbs)
loop_callbacks = callbacks.to_loop_callbacks(
on_text_delta=_on_text_delta,
)

result = await InboundDispatcher._run_session_loop(
binding,
Expand Down
54 changes: 13 additions & 41 deletions flocks/cli/session_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
- Tool execution display
- Streaming text display

Core logic is in session/runner.py
Core logic is exposed through SessionLoop.
"""

import asyncio
Expand All @@ -26,33 +26,18 @@

from flocks.utils.log import Log
from flocks.session.session import Session, SessionInfo
from flocks.session.runner import SessionRunner, RunnerCallbacks, ToolResult
from flocks.session.session_loop import LoopCallbacks
from flocks.session.message import Message, MessageRole
from flocks.agent.registry import Agent
from flocks.provider.provider import Provider
from flocks.tool.registry import ToolRegistry
from flocks.tool.registry import ToolRegistry, ToolResult
from flocks.project.project import Project
from dotenv import load_dotenv


log = Log.create(service="cli.runner")


# Module-level storage for CLI callbacks (used by SessionRunner during loop execution)
_CLI_CALLBACKS: Optional['RunnerCallbacks'] = None


def _set_cli_callbacks(callbacks: Optional['RunnerCallbacks']) -> None:
"""Set CLI callbacks for current execution"""
global _CLI_CALLBACKS
_CLI_CALLBACKS = callbacks


def _get_cli_callbacks() -> Optional['RunnerCallbacks']:
"""Get CLI callbacks for current execution"""
return _CLI_CALLBACKS


# Tool display styles
TOOL_STYLES: Dict[str, tuple] = {
"todo": ("Todo", "yellow bold"),
Expand All @@ -71,7 +56,7 @@ def _get_cli_callbacks() -> Optional['RunnerCallbacks']:

class CLISessionRunner:
"""
CLI wrapper for SessionRunner.
CLI wrapper for the public SessionLoop entry point.

Handles all CLI-specific display logic.
"""
Expand All @@ -90,7 +75,6 @@ def __init__(
self.agent_name = agent
self.auto_confirm = auto_confirm
self._session: Optional[SessionInfo] = None
self._runner: Optional[SessionRunner] = None
self._live: Optional[Live] = None
self._content_buffer: list[str] = []

Expand Down Expand Up @@ -299,8 +283,10 @@ async def _interactive_loop(self) -> None:

except KeyboardInterrupt:
self.console.print("\n[dim]Interrupted[/dim]")
if self._runner:
self._runner.abort()
if self._session:
from flocks.session.session_loop import SessionLoop

SessionLoop.abort(self._session.id)
break
except EOFError:
break
Expand Down Expand Up @@ -348,7 +334,6 @@ async def _process_message(
from flocks.input.dispatcher import dispatch_user_input
from flocks.input.events import UserInputEvent
from flocks.input.output import CliOutputSink
from flocks.session.message import Message

event = UserInputEvent(
source_type="cli",
Expand Down Expand Up @@ -408,29 +393,21 @@ async def _clear_history() -> None:
model={"providerID": provider_id, "modelID": model_id},
)

# Import SessionLoop and LoopCallbacks
from flocks.session.session_loop import SessionLoop, LoopCallbacks
from flocks.session.runner import RunnerCallbacks
# Import the stable session execution entry point.
from flocks.session.session_loop import SessionLoop

# Create loop callbacks (wrapping runner callbacks)
# Pass one explicit callback set through the full runtime.
loop_callbacks = LoopCallbacks(
on_step_start=self._on_step_start,
on_step_end=self._on_step_end,
on_error=self._on_error,
on_compaction=self._on_compaction,
)

# Store runner callbacks for tool events
# We need to hook into SessionRunner to get tool callbacks
# This is done by temporarily storing callbacks in a module-level variable
_set_cli_callbacks(RunnerCallbacks(
on_text_delta=self._on_text_delta,
on_reasoning_delta=self._on_reasoning_delta,
on_tool_start=self._on_tool_start,
on_tool_end=self._on_tool_end,
on_permission_request=self._on_permission_request,
on_error=self._on_error,
))
on_compaction=self._on_compaction,
)

# Start streaming display
self._content_buffer = []
Expand Down Expand Up @@ -486,9 +463,6 @@ async def _clear_history() -> None:
live.update(Text(""))
self._live = None

# Clear callbacks
_set_cli_callbacks(None)

# Print any remaining content not yet printed
if self._content_buffer:
self._flush_content()
Expand Down Expand Up @@ -806,8 +780,6 @@ def _print_help(self) -> None:
__all__ = [
"CLISessionRunner",
"run_session",
"_get_cli_callbacks",
"_set_cli_callbacks",
]


Expand Down
2 changes: 1 addition & 1 deletion flocks/provider/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
Centralises the logic for assembling thinking / reasoning / token-limit
kwargs that get forwarded to each provider's ``chat_stream`` call.

Both ``SessionRunner`` (session/runner.py) and ``AgentExecutor``
Both ``StepEngine`` and ``AgentExecutor``
(agent/runtime/executor.py) delegate to :func:`build_provider_options`
so that provider rules are maintained in exactly one place.
"""
Expand Down
2 changes: 1 addition & 1 deletion flocks/provider/sdk/google.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ def _convert_messages(
Rewrites history as text to bypass binary thought_signature requirements.

``session_id`` is forwarded by the runner via kwargs (see
``SessionRunner._call_llm``). When provided, we attempt to reconstruct
``StepEngine._call_llm``). When provided, we attempt to reconstruct
the conversation directly from persisted session messages – including
reasoning parts – which gives Gemini perfect context. As a defensive
fallback we also honour ``messages[0].sessionID`` / ``session_id``
Expand Down
25 changes: 5 additions & 20 deletions flocks/server/routes/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -1740,30 +1740,24 @@ async def unshare_session_local(sessionID: str, http_request: Request) -> Sessio
async def _abort_session_processing(sessionID: str) -> bool:
"""Abort active processing for a session and notify subscribers.

Aborts both the SessionLoop (sets abort_event so the next step check
stops the loop) and the SessionRunner (stops the current LLM stream).
Aborts SessionLoop, which owns the active StepEngine abort signal.
Also auto-rejects any pending Question tool requests so the question
handler polling loop unblocks immediately instead of timing out.

Cascades abort to all child sub-agent sessions (synchronous subtasks
and background tasks) so they stop together with the parent.
"""
from flocks.session.runner import SessionRunner
from flocks.session.session_loop import SessionLoop
from flocks.server.routes.question import reject_session_questions

# Abort the loop-level context (propagates to runner via shared abort_event)
loop_aborted = SessionLoop.abort(sessionID)

# Also cancel through the runner's own path (sets status to idle)
SessionRunner.cancel(sessionID)

# Unblock any pending Question tool waiting for user input
questions_rejected = await reject_session_questions(sessionID)

# --- Cascade abort to child sub-agent sessions ---
children_loops_aborted = SessionLoop.abort_children(sessionID)
children_runners_cancelled = SessionRunner.cancel_children(sessionID)

# Cancel background sub-agent tasks spawned by this session
bg_cancelled = 0
Expand All @@ -1778,7 +1772,6 @@ async def _abort_session_processing(sessionID: str) -> bool:
"loop_aborted": loop_aborted,
"questions_rejected": questions_rejected,
"children_loops_aborted": children_loops_aborted,
"children_runners_cancelled": children_runners_cancelled,
"bg_tasks_cancelled": bg_cancelled,
})

Expand Down Expand Up @@ -1835,7 +1828,7 @@ class InitRequest(BaseModel):
)
async def initialize_session(sessionID: str, request: InitRequest, http_request: Request) -> bool:
"""Initialize session"""
from flocks.session.runner import SessionRunner
from flocks.session.actions import render_session_command

current_user = require_user(http_request)
session = await _get_session_by_id_unfiltered(sessionID)
Expand All @@ -1847,12 +1840,10 @@ async def initialize_session(sessionID: str, request: InitRequest, http_request:
_require_session_write_access(session, current_user)

# Execute INIT command
await SessionRunner.command(
await render_session_command(
session_id=sessionID,
command="init",
arguments="",
message_id=request.messageID,
model=f"{request.providerID}/{request.modelID}",
)

log.info("session.initialized", {"session_id": sessionID})
Expand Down Expand Up @@ -3471,7 +3462,6 @@ async def _process_session_message(
from flocks.agent.registry import Agent
from flocks.provider.provider import Provider
from flocks.session.session_loop import SessionLoop, LoopCallbacks
from flocks.session.runner import RunnerCallbacks
import time
import os

Expand Down Expand Up @@ -4945,7 +4935,7 @@ class ShellRequest(BaseModel):
async def run_shell_command(sessionID: str, request: ShellRequest, http_request: Request):
"""Run shell command"""
from flocks.hooks.execution import ExecutionStopped
from flocks.session.runner import SessionRunner
from flocks.session.actions import run_session_shell

current_user = require_user(http_request)
session = await _get_session_by_id_unfiltered(sessionID)
Expand All @@ -4956,17 +4946,12 @@ async def run_shell_command(sessionID: str, request: ShellRequest, http_request:
)
_require_session_write_access(session, current_user)

model = None
if request.model:
model = {"providerID": request.model.providerID, "modelID": request.model.modelID}

try:
async with Session.active_operation(sessionID):
result = await SessionRunner.shell(
result = await run_session_shell(
session_id=sessionID,
agent=request.agent,
command=request.command,
model=model,
)
except SessionNotFoundError as exc:
raise HTTPException(
Expand Down
13 changes: 0 additions & 13 deletions flocks/session/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,6 @@
from flocks.session.prompt import SessionPrompt, SystemPrompt, ContextInfo
from flocks.session.lifecycle.compaction import SessionCompaction, CompactionResult, CompactionPolicy, ContextTier
from flocks.session.lifecycle.summary import SessionSummary, FileDiff
from flocks.session.runner import (
SessionRunner,
RunnerCallbacks,
ToolCall,
StepResult,
run_session,
)
from flocks.session.session_loop import (
SessionLoop,
LoopContext,
Expand Down Expand Up @@ -104,12 +97,6 @@
# Summary
"SessionSummary",
"FileDiff",
# Runner
"SessionRunner",
"RunnerCallbacks",
"ToolCall",
"StepResult",
"run_session",
# Session Loop
"SessionLoop",
"LoopContext",
Expand Down
Loading