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}}
FieldTypeMeaning
typestringAlways "event".
eventstringThe event name.
dataobjectDetails for that event (see below).

All events

EventWhen it is sentdata fields
session.startRight after the session’s mods have startedsession_id (string)
session.endWhen the interactive session is closing, just before mods are told byereason (string, currently always "exit")
turn.startWhen the agent starts answering a promptturn (number)
turn.endWhen the agent finishes answeringturn (number), seconds (number, whole seconds the turn took)
mode.changeWhen the permission mode changes during the sessionmode (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:

modeMeaning
defaultAsk before risky actions
acceptEditsFile edits are approved automatically
autoRoutine actions run without asking
planRead-only planning
dontAskAnything that would prompt is refused instead
bypassPermission 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.state to read the current turn count and mode.
  • Only interactive sessions. No events are sent in syntic -p runs, 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":
        break

with this manifest:

{
  "name": "plan-banner",
  "command": ["python3", "main.py"],
  "events": ["mode.change"],
  "ui": ["band"]
}

Next steps