feat(agent): add bootstrap_room_message for self-starting agents#295
Draft
darvell-thenvoi wants to merge 7 commits into
Draft
feat(agent): add bootstrap_room_message for self-starting agents#295darvell-thenvoi wants to merge 7 commits into
darvell-thenvoi wants to merge 7 commits into
Conversation
Collaborator
Author
|
Merged current Conflict resolution:
Validation after merge:
Still draft; I did not mark ready for review. |
0d0ae15 to
8550307
Compare
Lets an agent start work on its own with an initial message that did not come through the platform. `Agent.kickoff(content)` creates a fresh chat room (or uses a supplied room_id) and injects a synthetic message; `Agent.bootstrap_room_message(room_id, message)` is the low-level primitive for callers that need control over message.id (retry/replay idempotency). The injected message is delivered to the adapter exactly like a real chat message but lives only in memory: no DB write, no mark_processing/mark_processed lifecycle, no retry tracking, other participants never see it. Reuses the existing synthetic-sender mechanism with a new SYNTHETIC_KICKOFF_SENDER_ID and a closed SYNTHETIC_SENDER_IDS set so real platform "System" messages keep their full mark_* lifecycle. on_event also skips the _first_ws_msg_id sync marker for synthetic messages so a kickoff arriving before any real WS message can't poison the /next sync point.
- Fix stale log message in ExecutionContext synthetic-message branch (still said "contact event message" after the broadening change). Now logs the actual sender_id. - Claim presence.rooms slot before awaiting subscribe_room so concurrent bootstrap calls for the same fresh room don't double-subscribe; roll back the claim if subscribe fails. - Refactor TestAgentKickoff to drive through the public Agent constructor with a stubbed PlatformRuntime instead of __new__-ing and poking private attributes. - Add a public-entry test that proves caller-supplied PlatformMessage ids reach the adapter unchanged through the full Agent → PlatformRuntime → AgentRuntime → ExecutionContext chain. - Add a guard test that kickoff() raises before start(). - Note in the kickoff docstring that it is safe to call repeatedly.
- Drop dead sender_id/sender_name kwargs from Agent.kickoff. The synthetic identity is load-bearing: ExecutionContext.bootstrap_message forces sender_type=System and sender_id=kickoff so the persistence-skip in _process_event triggers. Exposing the kwargs let callers think they could override identity when the implementation ignored them. Comment explains why the sender fields in the PlatformMessage are fixed. - Declare bootstrap_message on the Execution Protocol with a versionchanged note, mirroring the request_resync precedent. Custom Execution implementations now get typed conformance instead of a silent RuntimeError at the bootstrap call site. - Skip duplicate work in RoomPresence._handle_room_added when the room is already tracked. Avoids a duplicate Phoenix join when kickoff has already claimed and subscribed to a fresh room before the platform's room_added WS event arrives. - Improve Agent.kickoff error message to include task_id when create_agent_chat returns no data. - Add fire-and-forget delivery note to Agent.kickoff and Agent.bootstrap_room_message docstrings. - Tests: assert the synthetic message reaches the adapter on the create-room path; cover the not-started guard on bootstrap_room_message; cover the create-chat-returns-no-data error; add a regression test that room_added for a kickoff-claimed room does not call subscribe_room twice.
Demonstrates Agent.kickoff() — the recommended public entry point for self-starting agents driven by webhooks, cron, or any source where the initial work item does not arrive as a platform message. Uses the existing anthropic_agent config stanza so no new credentials are needed.
The previous example asked the agent to answer alone, which doesn't demonstrate why kickoff matters on a collaboration platform. Rewrite the kickoff message to drop the agent into an empty room and tell it to discover peers, invite a collaborator, and delegate the weather lookup. Bump model to claude-sonnet-4-6.
Drop Agent.kickoff. Agent.bootstrap_room_message now accepts either:
- a plain string: the most common case. The method creates a fresh
chat room (optionally task-linked), wraps the string in a synthetic
PlatformMessage, and injects it.
- a fully-constructed PlatformMessage with an explicit room_id: the
escape hatch for callers that need stable message ids for
retry/replay idempotency (webhooks, cron, external bridges).
One public method, two ergonomic shapes. Returns the room id in both
cases so callers can refer to it later. Raises ValueError if a
PlatformMessage is passed without room_id.
The synthetic identity (sender_type=System, sender_id=kickoff) is still
forced inside ExecutionContext.bootstrap_message — caller cannot
override, because that's what triggers the persistence-skip in
_process_event.
Tests: replace TestAgentKickoff with TestAgentBootstrap covering both
shapes plus the not-started and missing-room_id guards.
Verified live against app.thenvoi.com — the string call creates the
room, enqueues the synthetic message, and the Anthropic adapter
receives it with first_msg=True.
8550307 to
4044a6e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a single new public Agent API for self-starting agents — work that begins without a real platform message arriving:
Agent.bootstrap_room_messageaccepts either shape:task_id=), wraps the string in a syntheticPlatformMessage, and injects it. Returns the new room id.PlatformMessage+room_id: the escape hatch for callers that need stable message ids for retry/replay idempotency (webhooks, cron jobs, external bridges that need to dedupe). Returns the same room id back.The injected message is delivered to the adapter exactly like a real chat message but is not persisted: no DB write, no
mark_processing/mark_processedlifecycle, no retry tracking, other participants never see it.How it works
Agent.bootstrap_room_message(string shape) calls RESTcreate_agent_chatto mint a room, builds a syntheticPlatformMessage, and forwards to the runtime.PlatformRuntime.bootstrap_room_messagerequires the runtime to be started and forwards toAgentRuntime.AgentRuntime.bootstrap_room_messageensures the room is subscribed (claim-the-slot-then-subscribe with rollback on failure) and anExecutionContextexists, then delegates toExecutionContext.bootstrap_message.ExecutionContext.bootstrap_messagewraps thePlatformMessagein a syntheticMessageEvent(sender_type="System",sender_id="kickoff") and enqueues it viaon_event. The existing synthetic-message branch in_process_eventskips DB persistence and retry tracking.The synthetic identity is forced inside
ExecutionContext.bootstrap_message— callers cannot override it, because that combination is what triggers the persistence-skip.Two safety guards:
on_eventskips the_first_ws_msg_idsync-point marker for synthetic messages so a bootstrap arriving before any real WS message can't poison the/nextresync._handle_room_addedearly-returns when the room is already tracked, avoiding a duplicate Phoenix join when bootstrap has already claimed and subscribed before the platform'sroom_addedevent arrives.Synthetic-sender check, broadened safely
The
_process_eventsynthetic shortcut used to match a singlesender_id == SYNTHETIC_CONTACT_EVENTS_SENDER_ID. It now matchessender_id in SYNTHETIC_SENDER_IDS(a closedfrozensetof known synthetic ids). The check still requiressender_type == "System"AND a recognized id, so real platform "System" messages keep their fullmark_*lifecycle. A regression test pins this.Files
src/thenvoi/agent.pybootstrap_room_message(str | PlatformMessage, *, room_id=None, ...)— single public methodsrc/thenvoi/runtime/types.pySYNTHETIC_KICKOFF_SENDER_ID,SYNTHETIC_KICKOFF_SENDER_NAME,SYNTHETIC_SENDER_IDSsrc/thenvoi/runtime/execution.pybootstrap_messageimpl + Protocol declaration; on_event sync-marker guard; broadened synthetic checksrc/thenvoi/runtime/runtime.pyAgentRuntime.bootstrap_room_message(subscribe + ensure execution + bootstrap, with rollback)src/thenvoi/runtime/platform_runtime.pysrc/thenvoi/runtime/presence.py_handle_room_addedfor already-tracked roomstests/runtime/test_kickoff.pyexamples/scenarios/self_start_kickoff.pyanthropic_agentconfig stanzaLive smoke test
Ran
python examples/scenarios/self_start_kickoff.pyagainstapp.thenvoi.com(real REST + WS). The example callsagent.bootstrap_room_message("Find out the current weather in San Francisco and Paris...")with noroom_idand no custom prompt.Observed log sequence:
What this proves end-to-end:
create_agent_chatcall and got a fresh room back. The room id propagated all the way to the adapter.bootstrap_messageenqueued a syntheticMessageEventwith the auto-generated idkickoff:82071a56-….Got 0 messages) — it was injected in-memory only, exactly as designed.first_msg=Trueand dispatched the model call.mark_processing/mark_processedcalls fired against the platform for the synthetic id (verified in unit tests; the live run hit the same code path).The agent then made an Anthropic API call against the bootstrap message. We did not wait for the model response in this run; the goal was to prove the new code path delivers the message to the adapter, which it does.
Test plan
uv run pytest tests/runtime/test_kickoff.py -v— 13/13 passinguv run pytest tests/runtime/ tests/adapters/ tests/converters/ tests/preprocessing/ --no-cov -q— 1461 passinguv run ruff check . && uv run ruff format .— cleanuv run pyrefly check— 0 errorspython examples/scenarios/self_start_kickoff.pyagainstapp.thenvoi.com— confirmed bootstrap → room creation → synthetic enqueue → adapter delivery → model call