Events
Events tell a mod what is happening in the session: that it started, that a turn began or ended, that the permission mode changed. A mod lists the events it wants in mod.json, and Syntic Code sends each one as a JSON line on the mod’s standard input.
Events are one-way notices. A mod cannot block, change or answer them. If you need to stop or rewrite what the agent does, use hooks instead.
The event message
Every event arrives in the same shape:
{"type":"event","event":"turn.end","data":{"turn":3,"seconds":12}}| Field | Type | Meaning |
|---|---|---|
type | string | Always "event". |
event | string | The event name. |
data | object | Details for that event (see below). |
All events
| Event | When it is sent | data fields |
|---|---|---|
session.start | Right after the session’s mods have started | session_id (string) |
session.end | When the interactive session is closing, just before mods are told bye | reason (string, currently always "exit") |
turn.start | When the agent starts answering a prompt | turn (number) |
turn.end | When the agent finishes answering | turn (number), seconds (number, whole seconds the turn took) |
mode.change | When the permission mode changes during the session | mode (string) |
These five names are the only ones Syntic Code knows. Listing any other name in mod.json makes the manifest invalid.
session.start
{"type":"event","event":"session.start","data":{"session_id":"8c1f…"}}Sent once. Use it to reset any per-session state. If you need more than the id (model, folder, mode), call session.state with the session.read permission.
session.end
{"type":"event","event":"session.end","data":{"reason":"exit"}}Sent once, when the session is shutting down. A bye message follows right away. After bye, the mod has half a second to exit before it is stopped. Do any final writes quickly.
turn.start and turn.end
{"type":"event","event":"turn.start","data":{"turn":4}}
{"type":"event","event":"turn.end","data":{"turn":4,"seconds":37}}turn is the session’s turn counter. The same number is used for the start and end of one turn. seconds is measured by the interface from the moment the answer started streaming to the moment it finished.
mode.change
{"type":"event","event":"mode.change","data":{"mode":"plan"}}Sent when the permission mode changes, for example after you cycle modes with the keyboard. It is not sent for the mode the session starts in. Possible values:
mode | Meaning |
|---|---|
default | Ask before risky actions |
acceptEdits | File edits are approved automatically |
auto | Routine actions run without asking |
plan | Read-only planning |
dontAsk | Anything that would prompt is refused instead |
bypass | Permission checks are skipped |
See Permission modes for what each mode does.
Subscribe and narrow
The events list in mod.json is the most a mod can ever receive. The hello message repeats it:
{"type":"hello","protocol":1,"mod":"turn-clock","events":["turn.start","turn.end"],"ui":["status","pane"],"granted":["notes"]}While running, a mod can ask for fewer events with subscribe:
{"type":"subscribe","events":["turn.end"]}The list you send replaces the current subscription. Names not in the manifest are ignored, so subscribe can narrow but never widen. Send the full manifest list again to get everything back. An empty list stops all events, while bye is still delivered.
Delivery rules
- Order is kept. Events for one mod arrive in the order they happened.
- Nothing waits for a mod. Each mod has a queue of 64 messages. If the mod is not reading and the queue fills, new events for it are dropped. Other mods and the session are not affected.
- Missed events are not replayed. If you need an accurate picture after a gap, call
session.stateto read the current turn count and mode. - Only interactive sessions. No events are sent in
syntic -pruns, because mods are not started there.
Read standard input in a loop that never blocks on anything else for long. A mod that sleeps or waits on the network in the same loop will fall behind and lose events.
Example: react to plan mode
This Python mod shows a band while the session is in plan mode and clears it otherwise:
import json, sys
def send(msg):
sys.stdout.write(json.dumps(msg) + "\n")
sys.stdout.flush()
for raw in sys.stdin:
msg = json.loads(raw)
if msg["type"] == "event" and msg["event"] == "mode.change":
if msg["data"]["mode"] == "plan":
send({"type": "draw", "slot": "band", "text": "Planning only: no files will change"})
else:
send({"type": "draw", "slot": "band"})
elif msg["type"] == "bye":
breakwith this manifest:
{
"name": "plan-banner",
"command": ["python3", "main.py"],
"events": ["mode.change"],
"ui": ["band"]
}Next steps
- Mod API: read the session summary and post notes.
- Screen areas: where your reaction can appear.
- Test a mod: feed fake events to your program.