Skip to content
Merged
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
48 changes: 46 additions & 2 deletions src/microbots/auto_memory/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,57 @@
"""Iterative agent loop with memory feedback (auto_memory package)."""

from microbots.auto_memory.callbacks import CallbackResult, CallbackRunner
from microbots.auto_memory.cli import run_from_yaml
from microbots.auto_memory.config import TaskConfig
from microbots.auto_memory.orchestrator import TrainingLoopOrchestrator
from microbots.auto_memory.config import DEFAULT_PROMPT_TEMPLATE, TaskConfig
from microbots.auto_memory.data_models import (
CallbackSpec,
Feedback,
FinalStatus,
IterationStatus,
ReferenceInput,
)
from microbots.auto_memory.errors import (
AgentError,
AutoMemoryError,
AutoMemoryTimeoutError,
CallbackError,
ConfigError,
MemoryStoreError,
)
from microbots.auto_memory.memory import MemoryStore
from microbots.auto_memory.orchestrator import (
IterationRecord,
RunSummary,
TrainingLoopOrchestrator,
)
from microbots.auto_memory.runners import AgentResult, AgentRunner, IterationContext
from microbots.auto_memory.runners.writing_bot_runner import WritingBotRunner
from microbots.auto_memory.workspace import WorkspaceManager

__all__ = [
"CallbackResult",
"CallbackRunner",
"CallbackSpec",
"DEFAULT_PROMPT_TEMPLATE",
"AgentResult",
"AgentRunner",
"IterationContext",
"IterationRecord",
"IterationStatus",
"FinalStatus",
"Feedback",
"ReferenceInput",
"RunSummary",
"MemoryStore",
"WorkspaceManager",
"TrainingLoopOrchestrator",
"TaskConfig",
"WritingBotRunner",
"run_from_yaml",
"AutoMemoryError",
"ConfigError",
"AgentError",
"CallbackError",
"AutoMemoryTimeoutError",
"MemoryStoreError",
]
11 changes: 11 additions & 0 deletions src/microbots/auto_memory/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Enable ``python -m microbots.auto_memory``."""

from __future__ import annotations

import sys

from microbots.auto_memory.cli import main


if __name__ == "__main__": # pragma: no cover
sys.exit(main())
89 changes: 83 additions & 6 deletions src/microbots/auto_memory/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,18 @@ def run_all(
list[CallbackResult]
One result per spec, in the same order as *specs*.
"""
return [self._run_one(spec, logs_dir, candidate_path) for spec in specs]
logger.info(
"Running %d callback(s) against candidate at %s", len(specs), candidate_path
)
results = [self._run_one(spec, logs_dir, candidate_path) for spec in specs]
passed = sum(1 for r in results if r.passed)
logger.info(
"Callback batch finished: %d/%d passed (%.1fs total)",
passed,
len(results),
sum(r.duration_s for r in results),
)
return results

# ------------------------------------------------------------------
# Internal
Expand Down Expand Up @@ -140,16 +151,19 @@ def _run_one(
timed_out: bool = False
start = time.monotonic()

logger.info(
"Callback %r starting: cmd=%s (timeout=%ds, expected_rc=%d)",
spec.name,
_preview(spec.command),
spec.timeout_s,
spec.expected_return_code,
)
try:
with (
stdout_path.open("w", encoding="utf-8") as out_fh,
stderr_path.open("w", encoding="utf-8") as err_fh,
):
# Security note: spec.command is intentionally run with
# shell=True for developer convenience (supports pipes,
# redirects, etc.). This runner assumes configs are loaded
# from trusted local files only. Do NOT use with configs
# sourced from untrusted input.
# spec.command runs with shell=True; assumes configs come from trusted local files.
proc = subprocess.run(
spec.command,
shell=True,
Expand All @@ -173,6 +187,25 @@ def _run_one(
duration_s = time.monotonic() - start
passed = (not timed_out) and (return_code == spec.expected_return_code)

logger.info(
"Callback %r finished: rc=%d elapsed=%.1fs passed=%s stdout=%s stderr=%s",
spec.name,
return_code,
duration_s,
passed,
stdout_path,
stderr_path,
)
if not passed and not timed_out:
tail = _tail_lines(stderr_path, 15) or _tail_lines(stdout_path, 15)
if tail:
logger.info(
"Callback %r output tail (%d lines):\n%s",
spec.name,
len(tail),
"\n".join(tail),
)

return CallbackResult(
spec=spec,
return_code=return_code,
Expand All @@ -182,3 +215,47 @@ def _run_one(
timed_out=timed_out,
duration_s=duration_s,
)


def _preview(cmd: str, limit: int = 160) -> str:
"""Truncate long callback commands so a single log line stays readable.

Parameters
----------
cmd : str
Callback command to normalize and preview.
limit : int, optional
Maximum preview length before truncation.

Returns
-------
str
Single-line command preview.
"""
single = " ".join(cmd.split())
if len(single) <= limit:
return single
return single[:limit] + f"... [{len(single) - limit} more chars]"


def _tail_lines(path: Path, n: int) -> list[str]:
"""Read the last non-empty lines from a text file.

Parameters
----------
path : Path
Text file to read.
n : int
Maximum number of trailing non-empty lines to return.

Returns
-------
list[str]
Trailing non-empty lines, or an empty list on an I/O error.
"""
try:
text = path.read_text(encoding="utf-8", errors="replace")
except OSError:
return []
lines = [ln for ln in text.splitlines() if ln.strip()]
return lines[-n:]
Loading
Loading