Reactive loop
Your home agent reacts to Sprout-pushed events: approve a screen-time request by rule, defer the ambiguous ones to the parent through a skill. Push twin of Autonomous loop.
What you'll build
Something happens in the family , a kid asks for screen time, hits a gem milestone, finishes a task , and your home agent reacts: approve a request that fits the kid's plan, reject one that doesn't, defer to the parent through a skill when the call isn't clear-cut.
This is the reactive twin of Autonomous loop. Same read-decide-act shape; the trigger is an event, not a clock. There are three transports that can carry that event to your agent , the same decision logic behind each, different plumbing.
Three transports, best to simplest
| Transport | Latency | What it costs you |
|---|---|---|
| 1 , SSE push | Real-time | An agent session holding an open stream. |
| 2 , activity-triggered heartbeat | Event → next fire | Nothing to host , Sprout fires it. But sandbox limits apply. |
| 3 , poll on a runner wake | ~10 min | Free if you already run a loop runner , just check on each wake. |
Best is the one you can afford to host. Holding a live session? Push. Don't want to host anything? An activity heartbeat is push-without-a-daemon. Already running a loop runner? Folding a check into its wake costs nothing extra.
Pieces you'll combine
- Subscriptions over SSE: the push channel from Sprout.
resources/subscribeon a specific resource URI. - Your home agent's rules engine: lives on your side. The "approve a screentime request under N minutes if today's screen budget isn't blown" logic.
- The matching action tool:
screentime.review_request,task.review,gems.adjust, etc. Whichever one closes the loop on the event you received. - A deferral skill: when the rule says "ask the parent", invoke a
home_agentskill that posts the question to the family inbox viaskill.post_result. The parent decides in-app.
Transport 1 , SSE push
Real-time, and the richest: your agent holds an open stream and acts the moment the event lands. Four movements:
1. Subscribe to a channel. Pick the resource URI that matches the event you want to react to.
resources/subscribe sprout://child/{kidId}/screentime/requests
# Open SSE stream; events arrive whenever the kid requests screen time.
# See /docs/reference/subscriptions for the full channel list.2. On each event, evaluate. Your agent's rules: kid's earned gems, today's plan, time of day, what's still on the schedule. Make the decision local; don't round-trip a model call for every event if you don't have to.
3a. Act directly if the call is clear.
// Auto-approve a 15-min request when the kid still has budget.
screentime.review_request({
requestId: "<event.requestId>",
approve: true,
note: "auto-approved: within today's budget"
})3b. Defer to the parent if it's ambiguous. Wrap the question in a skill so the parent sees it in the family inbox and decides in-app.
skill.invoke({
skillId: "<parent-decision-skill>",
input: {
question: "Alex is asking for 30 more minutes after dinner. Budget is at 80%. Approve?",
options: ["Approve", "Approve 15min only", "Reject"],
callbackTool: "screentime.review_request",
callbackArgs: { requestId: "<event.requestId>" }
}
})
# The skill posts a card via skill.post_result; the parent taps in-app.
# Sprout's in-app agent calls the callback tool with the parent's choice.4. Keep the loop open. SSE streams stay open; your agent stays subscribed. Reconnect on drop. Idempotency keys on action calls protect against duplicate events.
Transport 2 , activity-triggered heartbeat
Push without a daemon. Instead of a cron time, a heartbeat fires on a kid activity. Sprout hosts it; you host nothing. Create it with triggerType: 'activity' and the activity types to listen for, and omit cron.
heartbeat.create({
name: "Screen-time auto-review",
triggerType: "activity",
triggerActivityTypes: ["screen_time.requested"], // omit cron
runContext: {
runSkillId: "<review-skill>",
notifyRung: "THREAD"
}
})
# Fires when a kid requests screen time; runs your review skill.
# V1 activity heartbeats support screen_time.requested only.Honest limit: screen_time.requested is the only shipped activity trigger today. Agent- and system-generated activity isn't triggerable yet. If the event you want to react to isn't a screen-time request, this transport can't carry it , use Transport 1 or 3.
canvas.update, no network, no file access. It shares the heartbeat budget , 4 fires per 24h , so a burst of requests past the cap won't all fire. And latency is event → next fire, not instant. If you need to rewrite a canvas or reach outside on the event, detect here and act on a runner (below).Transport 3 , poll on your runner's wake
If you already run a loop runner, it already wakes every ~10 minutes. Reacting to an event can be as cheap as reading the relevant state on each wake and acting when it changed , no new infrastructure, no open stream. Reaction latency is your wake interval (~10 min), and it's free because the wake already happens.
# Inside your runner's existing wake, after loop.listDue / claims:
screentime.list_requests({ status: "pending" })
# → react to anything new since last wake
screentime.review_request({ requestId: "<id>", approve: true, note: "..." })
# Reaction latency = your wake interval. Free , the wake already runs.Use this when real-time isn't required and you'd rather not hold a session or depend on the one shipped activity trigger.
Compose them , detect on a heartbeat, act on a runner loop
The transports aren't exclusive. The most robust reactive setup splits detection from action: an activity heartbeat notices the event cheaply (no daemon), and a runner loop does the heavy part the sandbox forbids , the canvas rewrite, the external call, the file write. The heartbeat's job is just to flag that something's due; the runner picks it up on its next wake and does the work with full hands.
That's the same detect-vs-act split the Loop protocol formalizes: the heartbeat is a trigger, the loop is the durable worker with a lease and a ledger.
When to use it
- Sprout is the source of truth for state changes you want to react to (screentime requests, gem milestones, task completions, parent-review pending).
- The decision can usually be made by rule, with a fallback to the parent for the edge cases.
- Anti-pattern: polling on a heartbeat for things that have a push channel. Use the subscription.
- Anti-pattern: deciding everything by hand-typed parent input. Encode your common rules; reserve deferral for the ambiguous N%.
Tools touched
- Transport 1:
resources/subscribe: the SSE channels.child/screentime/requestsandchild/gemsshipped;child/todayandfamily/activitySoon. - Transport 2:
heartbeat.createwithtriggerType:'activity'andtriggerActivityTypes:['screen_time.requested'](the only shipped activity trigger). - Transport 3: your loop runner's wake ,
loop.listDueplus a state read likescreentime.list_requests. - Whatever action tool closes the loop:
screentime.review_request,task.review,gems.adjust, etc. skill.invoke+skill.post_resultfor the parent-deferral path.
Recommended skills
No skills in the catalog pair with this pattern yet. Browse the catalog.
Roadmap
- Soon More channels.
child/today(full day state, push on change) andfamily/activity(the family feed as a stream) ship next. - Soon Deferral pattern as a first-class tool. A platform-level "ask the parent" tool so you don't have to build the skill yourself.
Seen in walkthroughs
Not yet. A "screentime auto-approver" walkthrough is on the list.
Related patterns
- Autonomous loop: the scheduled twin. Your agent on its own clock.
- Loop protocol: the runner + loop substrate behind Transport 3 and the composed pattern.
- Context bridge: also home-agent-side, but flowing the other direction (outside → Sprout).
- Reports and briefs: pair with this pattern to post the post-decision summary to the family inbox.