Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f94a558
docs: add prompt queueing feature docs to README, website, and plans
Jul 24, 2026
6996949
feat: Implement queue data structure in Commands class
Jul 30, 2026
52cd12b
feat: Implement /queue command
Jul 30, 2026
9f3dd88
feat: Implement prompt queue feature and update plan
Aug 2, 2026
4e0b519
feat: Implement prompt queue functionality
Aug 2, 2026
92a09c5
style: fix formatting in queue.py (CLI-33)
Aug 2, 2026
939186b
fix: Improve management command handling and queue processing
Aug 2, 2026
ca3b489
feat: Implement prompt queue feature (CLI-33)
Aug 2, 2026
876b1a1
fix: deep-merge CLI agent-config with config file values
tomjuggler Aug 10, 2026
8c9ac2f
Draft 1 of LLM Reduction/LiteLLM Reduction, Support for:
Aug 11, 2026
240cbec
Fix models switching parameter dropping
Aug 11, 2026
5a1517f
Add reasoning effort configuration to new LLM sub system
Aug 11, 2026
7a2fc1b
Add tests for setting api base
Aug 11, 2026
da16bb5
Add images support for all providers
Aug 11, 2026
f2703ee
Make sure providers.json is actually laoded
Aug 11, 2026
9bee4da
Fix for models providing both reasoning and reasoning_details
Aug 11, 2026
2493fa6
Add optimistic refreshing for copilot tokens
Aug 11, 2026
1876ec7
Add all OpenAI compatible providers from model-metadata.json
Aug 11, 2026
9a9f837
Update model metadata json file
Aug 11, 2026
5e83283
Add remaining oepnai compatible providers
Aug 11, 2026
afb0a1b
Add Azure and Bedrock providers based off of LiteLLM's implementation
Aug 11, 2026
bcdbc9f
Make the system httpx version agnostic to match mcp library to have x…
Aug 11, 2026
040092f
Bump Version
Aug 11, 2026
8f70dfe
Fix sig v4 tests
Aug 11, 2026
b4c0aba
Actuaaly import default httpx as fallback
Aug 12, 2026
a2512b1
Add dotenv as dependency explicitly
Aug 12, 2026
0ec401b
Add guard for empty grep parameters
Aug 12, 2026
043583b
Prevent CI test hang
Aug 12, 2026
baed949
Merge PR #636: deep-merge CLI agent-config with config file values
Aug 12, 2026
2d05843
fix: deep-merge CLI yaml-to-json args with config-file values
Aug 12, 2026
82a7236
Merge pr-636-agent-config-merge: deep-merge CLI yaml-to-json args wit…
Aug 12, 2026
780954c
Merge remote-tracking branch 'origin/pr-629-head' into prompt-queueing
Aug 12, 2026
c5b7f12
Move prompt queue advancement into run_one()
Aug 12, 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
459 changes: 456 additions & 3 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cecli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from packaging import version

__version__ = "1.0.5.dev"
__version__ = "1.2.0.dev"
safe_version = __version__

try:
Expand Down
76 changes: 68 additions & 8 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
from cecli.commands import Commands, SwitchCoderSignal
from cecli.decoding import safe_open
from cecli.exceptions import LiteLLMExceptions
from cecli.helpers import command_parser, coroutines, nested, responses
from cecli.helpers import command_parser, command_queue, coroutines, nested, responses
from cecli.helpers.conversation import ConversationService, MessageTag
from cecli.helpers.file_system import FileSystemService
from cecli.helpers.io_proxy import IOProxy
Expand Down Expand Up @@ -585,6 +585,15 @@ def __init__(
self.commands = commands or Commands(self.io, self, args=args)
self.commands.coder = self

# Prompt queue for CLI-33: in-memory FIFO queue for deferred prompt
# processing. The queue lives on the coder so primary agents and
# sub-agents each have their own independent queue, managed by
# cecli.helpers.command_queue.
self.prompt_queue = []
self._queue_counter = 0
self._queue_lock = threading.Lock()
self._processing_queue = False

self.data_cache = {
"repo": {"last_key": "", "read_only_count": None},
}
Expand Down Expand Up @@ -1951,6 +1960,20 @@ async def run_one(self, user_message, preproc):

await self.auto_save_session(force=True)

# Move to the next queued prompt (CLI-33) only after the current message
# has fully completed, so the queue drains within run_one() instead of
# being watched by the generation loops.
if self.prompt_queue and not self._processing_queue:
self._processing_queue = True
try:
item = command_queue.dequeue_prompt(self)
finally:
self._processing_queue = False

if item is not None:
self.io.tool_output(f"Processing queued prompt (id: {item['id']})...")
await self.run_one(item["text"], preproc)

if not await HookIntegration.call_end_hooks(self):
self.io.tool_warning("Execution stopped by end hook")
return
Expand Down Expand Up @@ -3060,7 +3083,7 @@ async def _execute_local_tools(self, tool_calls):

async def _execute_mcp_tools(self, server, tool_calls):
"""Execute MCP tools via LiteLLM."""
import httpx
from cecli.http import httpx

tool_responses = []
try:
Expand Down Expand Up @@ -3491,9 +3514,9 @@ async def add_assistant_reply_to_cur_messages(self):
msg = response_dict["choices"][0]["message"]

if self.partial_response_tool_calls:
msg["tool_calls"] = self.partial_response_tool_calls
msg["tool_calls"] = [_tool_call_to_dict(tc) for tc in self.partial_response_tool_calls]
elif self.partial_response_function_call:
msg["function_call"] = self.partial_response_function_call
msg["function_call"] = _function_call_to_dict(self.partial_response_function_call)

if "reasoning_content" not in msg:
msg["reasoning_content"] = self.partial_response_reasoning_content
Expand Down Expand Up @@ -3611,7 +3634,7 @@ async def check_for_file_mentions(self, content):
return prompts.added_files.format(fnames=", ".join(added_fnames))

async def send(self, messages, model=None, functions=None, tools=None):
from litellm.types.utils import ModelResponse
ModelResponse = litellm.types.utils.ModelResponse

self.interrupt_event.clear()
self.got_reasoning_content = False
Expand Down Expand Up @@ -3745,7 +3768,7 @@ async def send(self, messages, model=None, functions=None, tools=None):
self.io.ai_output(json.dumps(args, indent=4))

async def show_send_output(self, completion):
from litellm.types.utils import ModelResponse
ModelResponse = litellm.types.utils.ModelResponse

if self.verbose:
print(completion)
Expand Down Expand Up @@ -4008,6 +4031,15 @@ def consolidate_chunks(self):
for key, value in psf.items():
if isinstance(value, list):
message_provider_specific_fields.setdefault(key, []).extend(value)
elif (
key in message_provider_specific_fields
and isinstance(message_provider_specific_fields[key], dict)
and isinstance(value, dict)
):
# Merge dict-valued metadata (e.g. gemini per-call
# function_call_signatures) so parallel tool calls
# each keep their own signature across chunks.
message_provider_specific_fields[key].update(value)
elif value is not None:
message_provider_specific_fields[key] = value
except (AttributeError, IndexError):
Expand Down Expand Up @@ -4101,7 +4133,8 @@ def _build_tool_calls_from_chunks(self):
parallel call is preserved, correctly ordered, and keeps its
provider-specific fields (e.g. thought signatures) attached.
"""
from litellm.types.utils import ChatCompletionMessageToolCall, Function
ChatCompletionMessageToolCall = litellm.types.utils.ChatCompletionMessageToolCall
Function = litellm.types.utils.Function

tool_calls_dict = {}

Expand Down Expand Up @@ -4632,7 +4665,12 @@ async def apply_updates(self):
def parse_partial_args(self):
# dump(self.partial_response_function_call)

data = self.partial_response_function_call.get("arguments")
function_call = self.partial_response_function_call
if isinstance(function_call, dict):
data = function_call.get("arguments")
else:
data = getattr(function_call, "arguments", None)

if not data:
return

Expand Down Expand Up @@ -4910,3 +4948,25 @@ def format_command_with_prefix(self, command):
else:
# Append the command to the prefix with a space
return f"{command_prefix} {command}"


def _tool_call_to_dict(tc):
"""Normalize a tool call (dict or litellm-shaped object) to a wire-format dict."""
if isinstance(tc, dict):
return tc

if hasattr(tc, "to_dict"):
return tc.to_dict()

return tc


def _function_call_to_dict(function_call):
"""Normalize a function call (dict or litellm-shaped Function) to a dict."""
if isinstance(function_call, dict):
return function_call

if hasattr(function_call, "to_dict"):
return function_call.to_dict()

return function_call
10 changes: 5 additions & 5 deletions cecli/coders/copypaste_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,11 +259,11 @@ def _safe_token_count(text):
],
created=int(time.time()),
model=model.name,
usage={
"prompt_tokens": prompt_tokens,
"completion_tokens": completion_tokens,
"total_tokens": total_tokens,
},
usage=litellm.Usage(
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
total_tokens=total_tokens,
),
)

kwargs = dict(model=model.name, messages=messages, stream=False)
Expand Down
6 changes: 5 additions & 1 deletion cecli/coders/editblock_func_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,11 @@ def render_incremental_response(self, final=False):
return res

async def _update_files(self):
name = self.partial_response_function_call.get("name")
function_call = self.partial_response_function_call
if isinstance(function_call, dict):
name = function_call.get("name")
else:
name = getattr(function_call, "name", None)

if name and name != "replace_lines":
raise ValueError(f'Unknown function_call name="{name}", use name="replace_lines"')
Expand Down
6 changes: 5 additions & 1 deletion cecli/coders/wholefile_func_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,11 @@ def live_diffs(self, fname, content, final):
return "\n".join(show_diff)

async def _update_files(self):
name = self.partial_response_function_call.get("name")
function_call = self.partial_response_function_call
if isinstance(function_call, dict):
name = function_call.get("name")
else:
name = getattr(function_call, "name", None)
if name and name != "write_file":
raise ValueError(f'Unknown function_call name="{name}", use name="write_file"')

Expand Down
8 changes: 8 additions & 0 deletions cecli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
from .include_skill import IncludeSkillCommand
from .lint import LintCommand
from .list_mcp import ListMcpCommand
from .list_queue import ListQueueCommand
from .list_sessions import ListSessionsCommand
from .list_skills import ListSkillsCommand
from .load import LoadCommand
Expand All @@ -55,6 +56,7 @@
from .models import ModelsCommand
from .multiline_mode import MultilineModeCommand
from .paste import PasteCommand
from .queue import QueueCommand
from .quit import QuitCommand
from .read_only import ReadOnlyCommand
from .read_only_stub import ReadOnlyStubCommand
Expand All @@ -63,6 +65,7 @@
from .remove_hook import RemoveHookCommand
from .remove_mcp import RemoveMcpCommand
from .remove_memory import RemoveMemoryCommand
from .remove_queue import RemoveQueueCommand
from .remove_skill import RemoveSkillCommand
from .report import ReportCommand
from .reset import ResetCommand
Expand Down Expand Up @@ -133,6 +136,7 @@
CommandRegistry.register(IncludeSkillCommand)
CommandRegistry.register(LintCommand)
CommandRegistry.register(ListMcpCommand)
CommandRegistry.register(ListQueueCommand)
CommandRegistry.register(ListSessionsCommand)
CommandRegistry.register(ListSkillsCommand)
CommandRegistry.register(LoadCommand)
Expand All @@ -148,13 +152,15 @@
CommandRegistry.register(ModelsCommand)
CommandRegistry.register(MultilineModeCommand)
CommandRegistry.register(PasteCommand)
CommandRegistry.register(QueueCommand)
CommandRegistry.register(QuitCommand)
CommandRegistry.register(ReadOnlyCommand)
CommandRegistry.register(ReadOnlyStubCommand)
CommandRegistry.register(ReasoningEffortCommand)
CommandRegistry.register(RemoveHookCommand)
CommandRegistry.register(RemoveMcpCommand)
CommandRegistry.register(RemoveMemoryCommand)
CommandRegistry.register(RemoveQueueCommand)
CommandRegistry.register(RemoveSkillCommand)
CommandRegistry.register(ReportCommand)
CommandRegistry.register(ResetCommand)
Expand Down Expand Up @@ -237,6 +243,7 @@
"parse_quoted_filenames",
"PasteCommand",
"quote_filename",
"QueueCommand",
"QuitCommand",
"ReadOnlyCommand",
"ReadOnlyStubCommand",
Expand All @@ -245,6 +252,7 @@
"RemoveHookCommand",
"RemoveMcpCommand",
"RemoveMemoryCommand",
"RemoveQueueCommand",
"RemoveSkillCommand",
"ReportCommand",
"ResetCommand",
Expand Down
57 changes: 56 additions & 1 deletion cecli/commands/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,54 @@ def __init__(
self.cmd_running_event.set()
self.last_command_show_notification = True

# Commands that should NOT trigger auto-processing of the queue
self._MANAGEMENT_COMMANDS = {"queue", "list-queue", "remove-queue"}

# ── Queue Management Methods (CLI-33) ────────────────────────────── #
#
# The prompt queue itself lives on the coder (Coder.prompt_queue) and
# is managed by cecli.helpers.command_queue. These thin wrappers keep
# the /queue, /list-queue and /remove-queue command implementations
# stable while operating on the coder that owns this Commands
# instance, so each sub-agent's commands manage that sub-agent's own
# queue.

@property
def prompt_queue(self):
"""Proxy to the owning coder's prompt queue."""
coder = self.coder
return coder.prompt_queue if coder is not None else []

def _enqueue_prompt(self, text: str) -> dict:
"""Add a prompt to the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.enqueue_prompt(self.coder, text)

def _dequeue_prompt(self) -> dict | None:
"""Remove and return the first item from the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.dequeue_prompt(self.coder)

def _get_queue_length(self) -> int:
"""Return the current number of items in the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.get_queue_length(self.coder)

def _remove_from_queue(self, index: int) -> dict | None:
"""Remove and return the item at the given index from the owning coder's queue."""
from cecli.helpers import command_queue

return command_queue.remove_from_queue(self.coder, index)

def _clear_queue(self) -> list:
"""Remove all items from the owning coder's queue and return them."""
from cecli.helpers import command_queue

return command_queue.clear_queue(self.coder)

def _load_custom_commands(self, custom_commands):
"""
Load custom commands from plugin paths.
Expand Down Expand Up @@ -183,7 +231,8 @@ async def execute(self, cmd_name, args, coder=None, **kwargs):
return

self.last_command_show_notification = command_class.show_completion_notification
self.cmd_running_event.clear()
if cmd_name not in self._MANAGEMENT_COMMANDS:
self.cmd_running_event.clear()

try:
kwargs.update(
Expand Down Expand Up @@ -226,6 +275,12 @@ def matching_commands(self, inp):
return matching_commands, first_word, rest_inp

async def run(self, inp, coder=None, **kwargs):
if inp.startswith("/"):
words = inp.strip().split()
cmd_name = words[0][1:]
rest_inp = inp[len(words[0]) :].strip()
return await self.execute(cmd_name, rest_inp, coder=coder, **kwargs)

if inp.startswith("!!!"):
return await self.execute(
"run", inp[3:], coder=coder, background=True, suppress_add=True
Expand Down
Loading
Loading