Skip to content
Merged
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__ = "0.100.14.dev"
__version__ = "1.0.1.dev"
safe_version = __version__

try:
Expand Down
1 change: 1 addition & 0 deletions cecli/coders/agent_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -1454,6 +1454,7 @@ async def preproc_user_input(self, inp):
inp = self.wrap_user_input(inp)

BaseTool.clear_invocation_cache()
ConversationService.get_chunks(self).reset_message_tracker()
self.agent_finished = False
self.turn_count = 0
return inp
Expand Down
2 changes: 1 addition & 1 deletion cecli/coders/architect_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ async def reply_completed(self):
self.total_cost = editor_coder.total_cost
self.coder_commit_hashes = editor_coder.coder_commit_hashes
except Exception as e:
self.io.tool_error(e)
self.io.tool_error(str(e))
# Restore original state on error
ConversationService.get_manager(original_coder or self).initialize(
reset=True,
Expand Down
1 change: 1 addition & 0 deletions cecli/coders/base_coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ def total_cached_tokens(self, value):
max_reflections = 3
num_tool_calls = 0
max_tool_calls = 25
turn_count = 0
edit_format = None
file_diffs = True
hashlines = False
Expand Down
1 change: 1 addition & 0 deletions cecli/helpers/agents/defaults/memorizer.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
name: memorizer
description: A sub-agent charged with maintaining and interacting with a fact database about the project. Can be used for looking up known facts to aid with tasks.
model: <weak_model>
auto_reap: true
agent-config:
allow_orchestration: false
tools_includelist:
Expand Down
24 changes: 24 additions & 0 deletions cecli/helpers/conversation/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ def __init__(self, coder):
self.uuid = coder.uuid
self.last_clear_count = 0
self._deferred_removals = set()
self.message_tracker = dict()

@classmethod
def get_instance(cls, coder) -> "ConversationChunks":
Expand Down Expand Up @@ -976,11 +977,32 @@ def flush_removals(self):
def reset_clear_count(self):
self.last_clear_count = 0

def reset_message_tracker(self):
self.message_tracker = dict()

def update_message_tracker(self, coder, message_type="default"):
if coder.edit_format not in ("agent", "subagent"):
self.message_tracker[message_type] = coder.turn_count

def debounce_message_injection(self, coder, message_type="default", frequency=10):
if coder.edit_format not in ("agent", "subagent"):
return False

if not self.message_tracker.get(message_type):
return False

should_send = coder.turn_count - self.message_tracker[message_type] > frequency

return not should_send

def _cancel_post_message_injections(self, modulus=10):
coder = self.get_coder()
if not coder:
return False

if self.debounce_message_injection(coder):
return True

# Add system reminder as a pre-prompt context block
if coder.edit_format in ("agent", "subagent"):
if coder.turn_count < modulus:
Expand All @@ -992,10 +1014,12 @@ def _cancel_post_message_injections(self, modulus=10):
if coder.turn_count % modulus != 0:
# and coder.turn_count % modulus != 0
if not coder.edit_allowed:
self.update_message_tracker(coder)
return False
else:
return True

self.update_message_tracker(coder)
return False

def add_copy_paste_tool_instructions(self) -> None:
Expand Down
1 change: 0 additions & 1 deletion cecli/helpers/memory/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,6 @@ async def invoke_memorizer(
name="memorizer",
prompt=prompt,
independent=True,
auto_reap=True,
)
except Exception:
logger.debug("Failed to spawn memorizer sub-agent", exc_info=True)
30 changes: 11 additions & 19 deletions cecli/helpers/orchestration/environment.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,7 +427,7 @@ def _state_snapshot(self) -> list:

return entries

async def execute(self, code_str: str) -> dict:
async def execute(self, code_str: str, values: dict | None = None) -> dict:

code_str = code_str.strip()
code_str = _escape_newlines_in_strings(code_str)
Expand All @@ -437,6 +437,14 @@ async def execute(self, code_str: str) -> dict:
if not code_str:
return {"results": "", "state_variables": self._state_snapshot()}

injected_keys = []

if values:
for key, value in values.items():
var_name = f"_o_{key}"
self.globals[var_name] = value
injected_keys.append(var_name)

captured_output: list[str] = []

# Clear mutation tracking for this execution
Expand Down Expand Up @@ -515,6 +523,8 @@ def _build_result(msg: str = "") -> dict:
finally:
self.locals.pop("__agent_async_runner", None)
self.globals["__builtins__"]["print"] = print
for key in injected_keys:
self.globals.pop(key, None)
# Mutation tracking is handled automatically by TrackedDict

return _build_result()
Expand Down Expand Up @@ -609,24 +619,6 @@ def build_orchestration_context_block(agent_config: dict[str, Any]) -> str | Non
Result list items are plain dicts — use item['content'] / item.get('content')
and item['_'] / item.get('_') to access data and metadata respectively.

### Creating New Files

Use `ResourceManager` to create an empty file, then `EditFile` to write
initial content (use `@000` for start/end on empty files)::

rm = Agent.get_tool("ResourceManager")
edit = Agent.get_tool("EditFile")

await rm.call(create=["path/to/new_file.py"])
await edit.call(edits=[{
"file_path": "path/to/new_file.py",
"operation": "replace",
"start_line": "@000",
"end_line": "@000",
"text": "def greet(name):\n return f\"Hello, {name}!\"\n",
}])

After creation, use `resolve_regions` and `edit_region` for targeted edits.

### Editing with Regions

Expand Down
12 changes: 12 additions & 0 deletions cecli/llm.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ async def clear_queue(self) -> None:

logging_worker.GLOBAL_LOGGING_WORKER = NoOpLoggingWorker()

# Patch Delta.__init__ to support 'reasoning_text' -> 'reasoning_content' mapping
_original_delta_init = self._lazy_module.types.utils.Delta.__init__

def _patched_delta_init(self_delta, *args, **kwargs):
# Intercept and map 'reasoning_text' to 'reasoning_content'
if kwargs.get("reasoning_content") is None and "reasoning_text" in kwargs:
kwargs["reasoning_content"] = kwargs.pop("reasoning_text", None)
# Pass the modified kwargs to the original __init__
_original_delta_init(self_delta, *args, **kwargs)

self._lazy_module.types.utils.Delta.__init__ = _patched_delta_init


litellm = LazyLiteLLM()

Expand Down
10 changes: 9 additions & 1 deletion cecli/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1315,7 +1315,6 @@ async def send_completion(

if self.debug:
self._log_messages(messages)
kwargs["logger_fn"] = self._log_request

kwargs["messages"] = messages

Expand Down Expand Up @@ -1381,6 +1380,9 @@ async def send_completion(

kwargs = deep_merge(kwargs, {"allowed_openai_params": ["tools", "tool_choice"]})

if self.debug:
kwargs["logger_fn"] = self._log_request

completion_coro = litellm.acompletion(**kwargs)
res, interrupted = await coroutines.interruptible(completion_coro, interrupt_event)
if interrupted:
Expand Down Expand Up @@ -1428,6 +1430,12 @@ async def send_completion(
else:
await asyncio.sleep(retry_delay)
continue
except Exception as err:
if self.debug:
import traceback

traceback.print_exc()
raise err

async def simple_send_with_retries(
self,
Expand Down
4 changes: 3 additions & 1 deletion cecli/tools/_yield.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ async def execute(cls, coder, **kwargs):
response = ToolResponse(cls.NORM_NAME)

if coder:
waited_for_sub_agents = False
# Check for active child sub-agents and await their tasks before finishing
try:
agent_service = AgentService.get_instance(coder)
Expand All @@ -67,6 +68,7 @@ async def execute(cls, coder, **kwargs):
coder.io.tool_warning(
f"Waiting for {len(active_tasks)} sub-agent(s) to complete before yielding..."
)
waited_for_sub_agents = True

# Single asyncio.wait that includes both the sub-agent tasks and
# the interrupt event, avoiding nested asyncio.wait() calls.
Expand Down Expand Up @@ -180,7 +182,7 @@ async def execute(cls, coder, **kwargs):
summary = kwargs.get("summary", None)

# Fire memorizer with yield summary (skip if already a memorizer)
if getattr(coder, "auto_memory", False) and summary:
if not waited_for_sub_agents and getattr(coder, "auto_memory", False) and summary:
agent_service = AgentService.get_instance(coder)
if agent_service.get_agent_name(coder) != "memorizer":
from cecli.helpers.memory.utils import invoke_memorizer
Expand Down
45 changes: 42 additions & 3 deletions cecli/tools/orchestrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ class Tool(BaseTool):
"other tools programmatically. Use this instead of making many "
"individual tool calls for batch operations. The environment provides "
"`Agent.get_tool(name)` to get tool proxies, `gather(*tasks)` for "
"parallel execution, and `state` for persistent storage across calls."
"parallel execution, and `state` for persistent storage across calls. "
"Use the `values` argument instead of writing string variables inline "
"to prevent string escaping issues."
),
"parameters": {
"type": "object",
Expand All @@ -35,21 +37,55 @@ class Tool(BaseTool):
"context block for available primitives and calling conventions."
),
},
"values": {
"type": "object",
"description": (
"Optional key-value dictionary of variables to inject "
"into the execution environment. Values must be strings "
"or numbers. Each key is exposed as a global variable "
"with the prefix '_o_' (e.g., '{\"file_content\": \"...\"}' "
"becomes '_o_file_content' in the code). Non-string/number "
"values are omitted with an error message."
"This is useful especially for longer strings like file contents."
),
},
},
"required": ["code"],
},
},
}

@classmethod
async def execute(cls, coder, code, **kwargs):
async def execute(cls, coder, code, values=None, **kwargs):
BaseTool.clear_invocation_cache()
env = OrchestrationService.get_instance(coder)
result = await env.execute(code)

errors = []
sanitized = {}

if values and isinstance(values, dict):
for key, value in values.items():
if not isinstance(key, str):
errors.append(f"Values key '{key}' is not a string, skipping.")
continue

if isinstance(value, (int, float, str, bool)):
sanitized[key] = value
else:
errors.append(
f"Value for '{key}' is not a string or number "
f"(got {type(value).__name__}), skipping."
)

result = await env.execute(code, values=sanitized)
response = ToolResponse(cls.NORM_NAME, result_type="list")
response.append_result(
content=result["results"], metadata={"state_variables": result["state_variables"]}
)

for error in errors:
response.append_error(error)

return response

@classmethod
Expand All @@ -67,6 +103,9 @@ def format_output(cls, coder, mcp_server, tool_response):
coder.io.tool_error("Invalid Tool JSON")
return

if params.get("values"):
coder.io.tool_output(f"{color_start}values:{color_end} True")

code = params.get("code", "")
if code:
coder.io.tool_output("")
Expand Down
Loading