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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ remain historical snapshots. Beta does not mean error-free operation.

### Fixed

- Stop repeated `delegate_status` polling after three identical pending results. The
completion watcher still delivers the final worker result separately, while the
bounded turn now names the paused job instead of spending its remaining step budget.
- Include a sanitized per-tool count when a turn reaches the hard step limit, so an
unfinished run no longer ends with an unexplained generic sentence.
- Disable Q35's implicit PS/2 controller in the planned Mac QMP launch command.
The virtio keyboard and single absolute tablet remain; the existing preflight
still refuses extra pointers before sending any input. This is not a production
Expand Down
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ preferences live in `USER.md`. All three are operator-owned prompt state and rel
|---|---|
| Gate path | `policy.py`, **985 lines** — has to stay readable in one sitting |
| Tools | **33**, every one gated |
| Suites | **3219** collected tests · **263** adversarial · 44 end-to-end |
| Suites | **3222** collected tests · **263** adversarial · 44 end-to-end |
| Home | <https://talos-agent.ch> · docs at `/docs/` |
| Repository | `talos-kernel/talos` is the public source tree |

Expand Down Expand Up @@ -261,7 +261,7 @@ properties covered by `test_fallback_control.py` and `test_runtime_failure_repai
```bash
python3 -m venv .venv && . .venv/bin/activate && pip install --require-hashes -r requirements.lock -r requirements-dev.lock

python -m pytest tests/ -q # 3219 collected tests; platform-specific skips are explicit
python -m pytest tests/ -q # 3222 collected tests; platform-specific skips are explicit
python redteam.py # 263 adversarial cases — mandatory for any kernel change
python e2e.py # 44 cases against a real model (costs tokens and time)
python -m talos --once # single cycle, for diagnosis
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,10 @@
</p>

<p align="center">
<!-- ⚠️ Bewusst „tests“, nicht „passing“: die Zahl kommt aus dem Einsammeln (3219).
<!-- ⚠️ Bewusst „tests“, nicht „passing“: die Zahl kommt aus dem Einsammeln (3222).
Plattformabhaengige Sandbox- und Repository-Pruefungen koennen uebersprungen werden;
`test_site_claims` prueft deshalb die gesammelte Zahl statt ein Umgebungsresultat. -->
<img src="https://img.shields.io/badge/tests-3219-2e7d32.svg" alt="Tests">
<img src="https://img.shields.io/badge/tests-3222-2e7d32.svg" alt="Tests">
<img src="https://img.shields.io/badge/red%20team-263%2F263-2e7d32.svg" alt="Red team">
<img src="https://img.shields.io/badge/gate%20path-985%20lines-8a4318.svg" alt="Gate path">
<img src="https://img.shields.io/badge/tools-33%20gated-8a4318.svg" alt="Tools">
Expand Down
4 changes: 2 additions & 2 deletions site/console.html
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ <h1 id="main-content" tabindex="-1">Witness one deed from door to ledger.</h1>
<a class="btn ghost" href="#measured">This candidate, measured</a>
</div>
<div class="meter-row">
<span><b>3219</b>automated tests</span>
<span><b>3222</b>automated tests</span>
<span><b>263/263</b>adversarial</span>
<span><b>985</b>lines · gate</span>
<span><b>33</b>gated tools</span>
Expand Down Expand Up @@ -554,7 +554,7 @@ <h2 class="rv">Numbers this candidate must ship.</h2>

<div class="stats" id="releaseStats">
<div class="stat rv"><div class="n" data-f="version">0.20.0-beta.4</div><div class="l">beta candidate</div><div class="sub-l" data-f="installer_version">public installer v0.20.0-beta.3</div></div>
<div class="stat rv"><div class="n" data-f="tests">3219</div><div class="l">automated tests, collected by pytest</div></div>
<div class="stat rv"><div class="n" data-f="tests">3222</div><div class="l">automated tests, collected by pytest</div></div>
<div class="stat rv"><div class="n" data-f="redteam_faelle">263</div><div class="l">adversarial cases, all behaving</div></div>
<div class="stat rv"><div class="n" data-f="kernel_zeilen">985</div><div class="l">lines in policy.py, readable in one sitting</div></div>
<div class="stat rv"><div class="n" data-f="werkzeuge">33</div><div class="l">tools, every one through the gate</div></div>
Expand Down
2 changes: 1 addition & 1 deletion site/docs/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@ <h1 id="main-content" tabindex="-1">Talos AI agent: the field manual</h1>
<p class="lede">That one decision shapes the rest of this manual. It is why the
command list is short, and why a few obvious conveniences are missing on purpose.</p>
<div class="spec-row">
<div class="spec"><b>3219</b>automated tests</div>
<div class="spec"><b>3222</b>automated tests</div>
<div class="spec"><b>263/263</b>adversarial</div>
<div class="spec"><b>985</b>gate lines</div>
<div class="spec"><b>33</b>gated tools</div>
Expand Down
2 changes: 1 addition & 1 deletion site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -638,7 +638,7 @@ <h2 class="rv">Measured, with the path stated</h2>
<p class="sub rv">None of this asks for belief. The installer runs the unit and
adversarial suites in front of you; the real-model cases are a separate opt-in E2E run.</p>
<p class="source-caption">Release snapshot: 0.20.0-beta.4. Counts describe its test suite, not speed or real-world success rates.</p><div class="stats">
<div class="stat rv"><div class="n cnt" data-n="3219">0</div><div class="l">automated tests collected from the source tree</div></div>
<div class="stat rv"><div class="n cnt" data-n="3222">0</div><div class="l">automated tests collected from the source tree</div></div>
<div class="stat rv"><div class="n cnt" data-n="263">0</div><div class="l">adversarial cases trying to pass the kernel</div></div>
<div class="stat rv"><div class="n cnt" data-n="44">0</div><div class="l">opt-in end-to-end cases against a real model</div></div>
<div class="stat rv"><div class="n cnt" data-n="985">0</div><div class="l">lines in the file that decides every verdict</div></div>
Expand Down
2 changes: 1 addition & 1 deletion site/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

- Permission kernel: 985 lines of Python, the only code that decides
- 33 tools, all gated; unattended runs cannot escalate
- 3219 tests collected in the source tree; 263/263 red-team cases passed in source verification. Release contents follow their own signed tag.
- 3222 tests collected in the source tree; 263/263 red-team cases passed in source verification. Release contents follow their own signed tag.
- MCP servers run only inside a UID-separated worker, from an operator-owned registry — no marketplace, no third-party code in the agent process
- Channels: Telegram, mail (IMAP pull), CLI, WhatsApp via an operator-controlled broker — every channel fetches; no inbound port, no webhook
- Updates: signed tarballs, verified before anything is unpacked; dependencies pinned and hashed, installed with --require-hashes
Expand Down
2 changes: 1 addition & 1 deletion site/redteam/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ <h1 id="main-content" tabindex="-1">263 ways to break an agent.</h1>
<div class="spec"><b class="red">25</b>attack surfaces</div>
<div class="spec"><b>985</b>kernel lines</div>
<div class="spec"><b>33</b>gated tools</div>
<div class="spec"><b>3219</b>automated tests</div>
<div class="spec"><b>3222</b>automated tests</div>
<div class="spec"><b class="red">0</b>passes sold as safety</div>
</div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion site/registry/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ <h1 id="main-content" tabindex="-1">The MCP registry</h1>
guide with the full protocol is <a href="https://github.com/talos-kernel/talos/blob/master/docs/claude-worker.md">docs/claude-worker.md</a>.
A curated starting point ships as <code>examples/mcp-servers.json</code>.</p>
<div class="spec-row">
<div class="spec"><b>3219</b>automated tests</div>
<div class="spec"><b>3222</b>automated tests</div>
<div class="spec"><b>263/263</b>adversarial</div>
<div class="spec"><b>985</b>gate lines</div>
<div class="spec"><b>33</b>gated tools</div>
Expand Down
2 changes: 1 addition & 1 deletion site/setup/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ <h1 id="main-content" tabindex="-1" class="rv">This address moved.</h1>
<a class="btn ghost" href="/">Return to the keep</a>
</div>
<div class="spec-row rv">
<div class="spec"><b>3219</b>automated tests</div>
<div class="spec"><b>3222</b>automated tests</div>
<div class="spec"><b>263/263</b>adversarial</div>
<div class="spec"><b>985</b>gate lines</div>
<div class="spec"><b>33</b>gated tools</div>
Expand Down
2 changes: 1 addition & 1 deletion site/status.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"version": "0.20.0-beta.4",
"release_state": "released",
"installer_version": "0.20.0-beta.4",
"tests": 3219,
"tests": 3222,
"redteam_faelle": 263,
"kernel_zeilen": 985,
"werkzeuge": 33,
Expand Down
2 changes: 1 addition & 1 deletion site/vergleich/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ <h1 id="main-content" tabindex="-1">The others can do more.</h1>
<div class="spec-row">
<div class="spec"><b>33</b>tools here, every one gated</div>
<div class="spec"><b>100+</b>skills there · counted honestly</div>
<div class="spec"><b>3219</b>automated tests</div>
<div class="spec"><b>3222</b>automated tests</div>
<div class="spec"><b>263/263</b>adversarial cases</div>
<div class="spec"><b>985</b>gate lines</div>
<div class="spec"><b>0</b>inbound ports</div>
Expand Down
50 changes: 49 additions & 1 deletion talos/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@
_TOOL_LINE = re.compile(r"^[ \t]*TOOL_CALL:[ \t]*(?P<payload>.*?)[ \t]*$")
_TOOL_PREFIX = re.compile(r"^[ \t]*TOOL_CALL:")

# `delegate_status` ist READ und passiert den Kernel frei. Ein Modell, das auf einen
# laufenden Job wartet, fragt sonst bis zur Notbremse nach - im echten Lauf 98-mal, und
# der Betreiber bekam nur "Step limit reached.". Bleibt die Antwort fuer denselben offenen
# Job dreimal gleich, endet der Lauf; der Abschluss kommt als eigener Push
# (`notify.watching`).
MAX_IDENTICAL_POLLS = 3
POLL_TOOLS = frozenset({"delegate_status"})
_POLL_PENDING = re.compile(r"\bstate=(?!done\b|failed\b|timeout\b)\w+")
_SAFE_JOB_ID = re.compile(r"^[A-Za-z0-9_.-]{1,64}$")
POLL_STOP_NOTE = (
"Task paused: delegated job {job} is still running (status unchanged after {n} checks). "
"Polling stopped here; the result arrives as a separate notification when the job "
"finishes. No action was repeated."
)

# --- Fremdsyntax: der Ausfall, der wie eine Antwort aussieht ------------------------
# Der Reasoner laeuft als Subprozess einer CLI, die ihre EIGENE Werkzeug-Schreibweise
# kennt. Rutscht das Modell in die (`Read(/tmp/x)`, `<invoke name="Read">`), passiert
Expand Down Expand Up @@ -467,6 +482,8 @@ def run_agent(
empty_repairs = 0
announcement_repairs = 0
recovery_attempts = 0
polls: dict[str, tuple[str, int]] = {}
tool_counts: dict[str, int] = {}
from .computer.observation import ObservationProgress, image_followup
observations = ObservationProgress()
followup = None
Expand Down Expand Up @@ -737,6 +754,25 @@ def run_agent(
)

history.append(tool_history_entry(tool, outcome.status.value, outcome.detail, outcome.result, args=args))
tool_counts[tool] = tool_counts.get(tool, 0) + 1
if tool in POLL_TOOLS and outcome.status is Status.DONE:
job = str(args.get("job_id", ""))
seen = f"{outcome.detail} {'' if outcome.result is None else outcome.result}"
if _POLL_PENDING.search(seen):
last, count = polls.get(job, ("", 0))
count = count + 1 if seen == last else 1
polls[job] = (seen, count)
if count >= MAX_IDENTICAL_POLLS:
executor.log.append(Event(run_id, "agent", "protocol.repair", {
"tool": tool, "reason": "identical status polls",
"attempt": count, "exhausted": True,
}))
return AgentResult(
AgentStatus.ANSWERED,
POLL_STOP_NOTE.format(
job=job if _SAFE_JOB_ID.match(job) else "(unnamed)", n=count),
steps=step, history=tuple(history), plan=active,
)
if outcome.status is Status.DONE:
# A new receipt is real progress. Permit one correction at the next
# step, while two announcements without intervening progress still stop.
Expand Down Expand Up @@ -776,13 +812,25 @@ def run_agent(

return AgentResult(
AgentStatus.STEP_LIMIT,
"Step limit reached.",
_step_limit_note(max_steps, tool_counts),
steps=max_steps,
history=tuple(history),
plan=active,
)


def _step_limit_note(max_steps: int, tool_counts: dict[str, int]) -> str:
"""Notbremse mit Bericht: welche Werkzeuge liefen, statt eines nackten Satzes."""
if not tool_counts:
return "Step limit reached."
used = ", ".join(
f"{name if _SAFE_SCHEMA_NAME.match(name) else '?'} x{n}"
for name, n in sorted(tool_counts.items(), key=lambda kv: -kv[1])
)
return (f"Step limit reached after {max_steps} steps. Tools used: {used}. "
"Earlier tool results remain valid; no completed action was replayed.")


def _with_verdict(answer: str, plan: PlanRun | None) -> str:
"""Haengt das deterministische Urteil an — als Notiz, nie in die Prosa hinein.

Expand Down
68 changes: 68 additions & 0 deletions tests/test_agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,3 +477,71 @@ def propose(_h: list[str]) -> str:
assert "TOOL_CALL" not in result.text and "powershell" not in result.text
assert zuege[0] == MAX_CONTROL_REPAIRS + 1
assert not any(row["type"] == "exec.intent" for row in executor.log.recent(20))


# --- Polling ohne Fortschritt ---------------------------------------------------------
# Aus einem echten Lauf: ein delegate_codex, danach 98x delegate_status, bis die Notbremse
# griff. Der Betreiber bekam nur "Step limit reached." und keinen Hinweis auf den Job.


class _PollExecutor:
def __init__(self, tmp_path: Path, result) -> None:
self.log = EventLog(tmp_path / "ev.db")
self.result = result
self.calls = 0

def run(self, req, run_id):
from types import SimpleNamespace

self.calls += 1
return SimpleNamespace(
status=agent_loop.Status.DONE,
detail="ok",
result=self.result(self.calls),
)


def _poll(job: str = "job-abc") -> str:
return _tool_call("delegate_status", {"job_id": job}, [])


def test_identical_polls_end_the_run_with_a_push_note(tmp_path: Path) -> None:
executor = _PollExecutor(
tmp_path,
lambda _n: "delegate_status job_id=job-abc state=running",
)
result = run_agent(lambda _h: _poll(), executor, OWNER, "polls")

assert result.status is AgentStatus.ANSWERED
assert executor.calls == agent_loop.MAX_IDENTICAL_POLLS
assert "job-abc" in result.text and "notification" in result.text


def test_polls_with_changing_status_keep_going(tmp_path: Path) -> None:
states = {1: "running seq=1", 2: "running seq=2", 3: "done"}
executor = _PollExecutor(
tmp_path,
lambda n: f"delegate_status job_id=job-abc state={states[n]}",
)
result = run_agent(
lambda _h: _poll() if executor.calls < 3 else "Fertig.",
executor,
OWNER,
"polls-ok",
)

assert result.status is AgentStatus.ANSWERED
assert executor.calls == 3
assert result.text.startswith("Fertig.")


def test_step_limit_note_names_the_tools_used(tmp_path: Path) -> None:
executor = _PollExecutor(
tmp_path,
lambda n: f"delegate_status job_id=job-abc state=running seq={n}",
)
result = run_agent(lambda _h: _poll(), executor, OWNER, "polls-limit", max_steps=4)

assert result.status is AgentStatus.STEP_LIMIT
assert result.text.startswith("Step limit reached")
assert "delegate_status x4" in result.text
Loading