diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 7dfd1d0..49bf3ce 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Test +name: CI on: push: @@ -27,3 +27,24 @@ jobs: - name: Lint run: pip install ruff && ruff check molecule_agent/ molecule_plugin/ + + all-required: + name: all-required + needs: [test] + # required: all matrix variants must succeed + if: always() + runs-on: ubuntu-latest + steps: + - name: Verify all required jobs passed + run: | + # Collect results from all test matrix variants + results="${{ needs.test.result }}" + echo "Matrix results: $results" + # Any result that is not "success" is a failure condition + if [[ "$results" == *"failure"* ]] || \ + [[ "$results" == *"cancelled"* ]] || \ + [[ "$results" == *"skipped"* ]]; then + echo "One or more required jobs did not succeed: $results" + exit 1 + fi + echo "All required jobs passed." diff --git a/CLAUDE.md b/CLAUDE.md index 1ebea23..2f8044e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -219,6 +219,21 @@ python -m molecule_agent verify-sha256 ./my-plugin-dir shut the loop down cleanly. Cursor optionally persisted to `--cursor-file` so restarts resume from the last-seen activity row. +- **`run_heartbeat_loop(stop_event)` and `run_agent_loop(stop_event)`** (KI-009): + Both loops accept an optional `threading.Event` parameter. When the event is set + from another thread, the loop exits immediately with return value `"stopped"`. + The check runs before `max_iterations` so a signal always wins. Useful for + embedding the client in a long-running process that needs a clean shutdown path: + ```python + import threading, time + from molecule_agent import RemoteAgentClient + stop = threading.Event() + client = RemoteAgentClient(...) + # Signal-safe: call stop.set() from any thread to stop the loop + terminal = client.run_heartbeat_loop(stop_event=stop) + # terminal == "stopped" if stop.set() was called, else "paused"/"deleted" + ``` + --- ## Relevant platform docs diff --git a/molecule_agent/README.md b/molecule_agent/README.md index 535125b..0c2e711 100644 --- a/molecule_agent/README.md +++ b/molecule_agent/README.md @@ -54,8 +54,11 @@ secrets = client.pull_secrets() client.install_plugin("molecule-dev") client.install_plugin("my-plugin", source="github://acme/my-plugin") -# 4. Run the heartbeat + state-poll loop until the platform pauses/deletes us. -terminal = client.run_heartbeat_loop() +# 4. Run the heartbeat + state-poll loop until the platform pauses/deletes us +# or until stop_event.set() is called from another thread. +import threading +stop = threading.Event() +terminal = client.run_heartbeat_loop(stop_event=stop) print(f"loop exited: {terminal}") ``` @@ -75,8 +78,8 @@ A runnable demo with full setup walkthrough lives at | `call_peer(target, message)` | 30.6 | Direct A2A with proxy fallback; response may be wrapped in OFFSEC-003 boundary markers — use ``strip_a2a_boundary()`` to remove them | | `fetch_inbound(since_id=…)` | 30.8c | One-shot poll of `/workspaces/:id/activity` for inbound A2A | | `reply(msg, text)` | 30.8c | Smart-routes reply to `/notify` (canvas user) or `/a2a` (peer) | -| `run_heartbeat_loop()` | combo | Drives heartbeat + state-poll on a timer; exits on pause/delete | -| `run_agent_loop(handler)` | combo | Heartbeat + state + **inbound dispatch**; exits on pause/delete | +| `run_heartbeat_loop(stop_event=None)` | combo | Drives heartbeat + state-poll on a timer; exits on pause/delete or when `stop_event.set()` is called from another thread (KI-009) | +| `run_agent_loop(handler, stop_event=None)` | combo | Heartbeat + state + **inbound dispatch**; exits on pause/delete or when `stop_event.set()` is called from another thread (KI-009) | ## Inbound delivery — push vs poll diff --git a/molecule_agent/__init__.py b/molecule_agent/__init__.py index a9f4f83..2b50dc6 100644 --- a/molecule_agent/__init__.py +++ b/molecule_agent/__init__.py @@ -9,6 +9,7 @@ and detect pause/resume/delete — all via the Phase 30.1–30.5 HTTP contract. Intended usage:: + import threading from molecule_agent import RemoteAgentClient client = RemoteAgentClient( @@ -18,7 +19,8 @@ Intended usage:: ) client.register() # mints + persists the auth token env = client.pull_secrets() # decrypted secrets dict - client.run_heartbeat_loop() # background heartbeat + state-poll + stop = threading.Event() + client.run_heartbeat_loop(stop_event=stop) # background heartbeat; stop.set() to exit cleanly See ``sdk/python/examples/remote-agent/`` for a runnable demo.