11. Channels with SSSN
Goal: connect a tactic to a stream of events. You will keep support
messages in a channel, triage each one with a tactic through TacticProcessor,
serve the store over HTTP, continue the work from another process, and finally
mount the tactic, the channels, and the processor in one AAAX strategy.
You need: pip install "lllm-core[server,client]" and curl. Nothing here
calls a model, so it all runs offline. Steps 5 to 7 start servers; keep a
second terminal open in the same folder.
Work in an empty folder:
mkdir support-desk && cd support-desk
Step 1: create a store and two channels
make_store.py:
from lllm.network import ChannelExistsError, LocalStore
store = LocalStore(".lllm/network")
for name, description in [
("inbox", "Support messages as they arrive."),
("triaged", "One triage result per message."),
]:
try:
store.create_channel({"name": name, "form": "log", "description": description})
except ChannelExistsError:
pass
print([channel.name for channel in store.list_channels()])
python make_store.py
['inbox', 'triaged']
The whole store is one folder: SQLite for channels, events, and cursors, and a directory for artifact files.
.lllm/network/
network.sqlite
artifacts/
Catching ChannelExistsError makes the script safe to run twice.
Step 2: append events
Every message is an Event: a payload plus the fields that let other systems
route and trace it. send.py:
from lllm.network import Event, LocalStore
store = LocalStore(".lllm/network")
messages = {
"T-100": "The checkout page is down for every customer",
"T-101": "How do I change the email on my account?",
"T-102": "Invoices from March show the wrong tax rate",
}
for ticket, text in messages.items():
event = store.append_event(
Event(
channel="inbox",
source="web-form",
kind="message",
payload={"text": text},
correlation_id=ticket,
)
)
print(event.cursor, event.correlation_id, event.payload["text"])
python send.py
1 T-100 The checkout page is down for every customer
2 T-101 How do I change the email on my account?
3 T-102 Invoices from March show the wrong tax rate
The store assigns each event a cursor, its position in the store. The
correlation_id is the ticket number; it will follow the message into every
channel it reaches.
The CLI writes to the same store (.lllm/network is its default):
lllm network append inbox '{"text": "Password reset emails never arrive"}' \
--kind message --source cli --correlation-id T-103
{"id":"d5339382-115a-4318-a665-b94613f8d575","cursor":4,"channel":"inbox","timestamp":1793846679.3149977,"source":"cli","kind":"message","payload":{"text":"Password reset emails never arrive"},"schema":null,"metadata":{},"correlation_id":"T-103","parent_ids":[]}
Step 3: write a tactic
The tactic triages one message. It is plain Python so this tutorial runs
offline; a native tactic that calls a model plugs in the same way, because the
processor only calls run() (or arun()). tactics.py:
import re
from pydantic import BaseModel
from lllm import Tactic
URGENT = re.compile(r"\b(down|outage|failing|broken)\b", re.IGNORECASE)
class Ticket(BaseModel):
text: str
class Triage(BaseModel):
priority: str
summary: str
class TriageTicket(Tactic[Ticket, Triage]):
name = "triage"
description = "Assign a priority and a short summary to a support message."
input_type = Ticket
output_type = Triage
def _run(self, input_value, *, context=None):
text = input_value.text
priority = "urgent" if URGENT.search(text) else "normal"
return Triage(priority=priority, summary=" ".join(text.split()[:6]))
Try it directly:
python -c '
from tactics import TriageTicket
print(TriageTicket().run({"text": "The API is down"}))
print(TriageTicket().run({"text": "Where can I download my invoices?"}))
'
priority='urgent' summary='The API is down'
priority='normal' summary='Where can I download my invoices?'
Step 4: run the tactic over the channel
TacticProcessor reads inbox through a named subscription, runs the tactic
on each message, and appends the result to triaged. process.py:
from lllm.network import LocalStore, TacticProcessor
from tactics import TriageTicket
store = LocalStore(".lllm/network")
processor = TacticProcessor(
TriageTicket(),
store,
source="inbox",
sink="triaged",
kind="message",
)
for event in processor.process():
print(event.cursor, event.correlation_id, event.payload)
python process.py
5 T-100 {'priority': 'urgent', 'summary': 'The checkout page is down for'}
6 T-101 {'priority': 'normal', 'summary': 'How do I change the email'}
7 T-102 {'priority': 'normal', 'summary': 'Invoices from March show the wrong'}
8 T-103 {'priority': 'normal', 'summary': 'Password reset emails never arrive'}
Run it again and it prints nothing. The processor's subscription,
triage-inbox-message-processor (the tactic's name, the source channel, and
the kind filter, plus -processor), lives in the store and remembers the last
event it finished. The cursor moves only after a result is written, so a run
that stops halfway repeats the unfinished message next time:
python process.py
lllm network get-subscription triage-inbox-message-processor
{"id":"triage-inbox-message-processor","channel":"inbox","cursor":4,"consumer":"triage-inbox-message-processor","batch_size":100,"filters":{"kind":"message"},"metadata":{}}
Each result points back at the message it came from. trace.py:
from lllm.network import LocalStore
store = LocalStore(".lllm/network")
result = store.query_events("triaged", limit=1)[0]
origin = store.get_event(result.parent_ids[0])
print(result.source, result.kind, result.correlation_id)
print(origin.channel, origin.payload["text"])
python trace.py
triage-inbox-message-processor result T-100
inbox The checkout page is down for every customer
Step 5: serve the store
In the second terminal, from the support-desk folder:
lllm network serve --port 7700
The store API is now at http://127.0.0.1:7700, with interactive docs at
/docs:
curl -s -w '\n' http://127.0.0.1:7700/health
curl -s -w '\n' "http://127.0.0.1:7700/events?channel=triaged&limit=1"
{"ok":true}
[{"id":"ea41698d-0f45-4afa-b745-4604de002705","cursor":5,"channel":"triaged","timestamp":1793846681.0239878,"source":"triage-inbox-message-processor","kind":"result","payload":{"priority":"urgent","summary":"The checkout page is down for"},"schema":null,"metadata":{},"correlation_id":"T-100","parent_ids":["381bb4ae-10dc-4c09-846f-15d7f70b4651"]}]
The server opens the same folder, so local scripts and HTTP clients can share the store.
Step 6: read it from another process
RemoteStore has the same methods as LocalStore. A dashboard that shows new
triage results keeps its own subscription. dashboard.py:
from lllm.network import RemoteStore
store = RemoteStore("http://127.0.0.1:7700")
store.create_subscription("triaged", subscription_id="dashboard")
for event in store.pull_subscription("dashboard"):
print(event.correlation_id, event.payload["priority"], event.payload["summary"])
python dashboard.py
T-100 urgent The checkout page is down for
T-101 normal How do I change the email
T-102 normal Invoices from March show the wrong
T-103 normal Password reset emails never arrive
Run it again and it prints nothing new. create_subscription with an existing
id returns the existing subscription, so the script is safe to rerun.
Now move the worker off the local store. Instead of hard-coding the URL, bind
the channels to the server in .lllm/config.toml:
[refs."lllm://demo/support-desk/channels/inbox"]
url = "http://127.0.0.1:7700"
[refs."lllm://demo/support-desk/channels/triaged"]
url = "http://127.0.0.1:7700"
worker.py asks lllm.Resolver for the store behind the ref:
from lllm import Resolver
from lllm.network import TacticProcessor
from tactics import TriageTicket
resolver = Resolver.from_config(".")
store = resolver.store("lllm://demo/support-desk/channels/inbox")
processor = TacticProcessor(
TriageTicket(), store, source="inbox", sink="triaged", kind="message"
)
print(type(store).__name__)
for event in processor.process():
print(event.correlation_id, event.payload)
Send a message over plain HTTP, then run the worker and the dashboard:
curl -s -w '\n' -X POST http://127.0.0.1:7700/events \
-H 'content-type: application/json' \
-d '{"channel": "inbox", "kind": "message", "source": "curl", "correlation_id": "T-104", "payload": {"text": "Login is down on the mobile app"}}'
python worker.py
python dashboard.py
{"id":"b969625d-89c8-4916-802c-746f9dedd11d","cursor":9,"channel":"inbox","timestamp":1793846688.7259398,"source":"curl","kind":"message","payload":{"text":"Login is down on the mobile app"},"schema":null,"metadata":{},"correlation_id":"T-104","parent_ids":[]}
RemoteStore
T-104 {'priority': 'urgent', 'summary': 'Login is down on the mobile'}
T-104 urgent Login is down on the mobile
The worker processed only T-104. It used the same
triage-inbox-message-processor subscription as step 4, and that subscription
lives in the store, not in the process, so a worker on another machine picks
up exactly where the local one stopped. To point the worker back at the local
folder, change the binding to store = ".lllm/network"; the code stays the
same.
Step 7: mount it all in an AAAX strategy
Describe the folder as a package. lllm.toml:
[package]
org = "demo"
name = "support-desk"
version = "0.1.0"
kind = "app"
description = "Triage support messages."
[tactics.triage]
entry = "tactics:TriageTicket"
description = "Assign a priority and a short summary."
[channels.inbox]
form = "log"
description = "Support messages as they arrive."
[channels.triaged]
form = "log"
description = "One triage result per message."
The shell reads it without starting anything:
lllm shell inspect .
support-desk: Triage support messages.
package support-desk lllm://demo/support-desk
tactic triage lllm://demo/support-desk/tactics/triage
channel inbox lllm://demo/support-desk/channels/inbox
channel triaged lllm://demo/support-desk/channels/triaged
A strategy adds what the manifest cannot say: what /run should do. Here it
appends the request to inbox and drains the backlog through the processor.
strategy.py:
from pathlib import Path
from lllm.network import TacticProcessor
from lllm.shell import Strategy
def build_strategy() -> Strategy:
strategy = Strategy("support-desk", description="Triage support messages.")
strategy.use_package(Path(__file__).parent) # the tactic and channels in lllm.toml
processor = TacticProcessor(
strategy.tactics()["triage"],
strategy.store,
source="inbox",
sink="triaged",
kind="message",
)
@strategy.runner
async def run(input_value, *, context=None):
strategy.store.append_event(
{"channel": "inbox", "kind": "message", "source": "shell", "payload": input_value}
)
return [event.payload for event in await processor.aprocess()]
return strategy
use_package mounts the tactic and both channels and opens the package store,
.lllm/network in the package folder: the same store as every step before.
With strategy.py present, lllm shell serve . loads it instead of the bare
manifest. Stop the store server from step 5 (or leave it running; the two can
share the folder) and start the strategy:
lllm shell serve . --port 8400
From the other terminal, run the strategy, call the tactic on its own, and queue a message through the channel shortcut:
curl -s -w '\n' -X POST http://127.0.0.1:8400/run \
-H 'content-type: application/json' \
-d '{"input": {"text": "Refunds are failing for every customer"}}'
curl -s -w '\n' -X POST http://127.0.0.1:8400/tactics/triage/run \
-H 'content-type: application/json' \
-d '{"input": {"text": "Where can I download my invoices?"}}'
curl -s -w '\n' -X POST http://127.0.0.1:8400/channels/inbox/events \
-H 'content-type: application/json' \
-d '{"input": {"kind": "message", "correlation_id": "T-105", "payload": {"text": "Search results are broken"}}}'
{"output":[{"priority":"urgent","summary":"Refunds are failing for every customer"}],"strategy":"support-desk","metadata":{}}
{"output":{"priority":"normal","summary":"Where can I download my invoices?"},"request_id":"db41a404-25e8-446c-be59-aa7345fc76a4","tactic":"triage"}
{"output":{"id":"4f3b1f3f-a0c3-41ba-a9e9-06ba8289f381","cursor":13,"channel":"inbox","timestamp":1793846694.8979056,"source":"lllm.shell","kind":"message","payload":{"text":"Search results are broken"},"schema":null,"metadata":{},"correlation_id":"T-105","parent_ids":[]},"strategy":"support-desk","metadata":{"resource":"inbox","endpoint":"channel"}}
The tactic route ran the tactic without touching the channels. The queued
message waits in inbox until the next /run drains it along with the new
request:
curl -s -w '\n' -X POST http://127.0.0.1:8400/run \
-H 'content-type: application/json' \
-d '{"input": {"text": "Thanks, all good now"}}'
curl -s -w '\n' http://127.0.0.1:8400/network/subscriptions/triage-inbox-message-processor
{"output":[{"priority":"urgent","summary":"Search results are broken"},{"priority":"normal","summary":"Thanks, all good now"}],"strategy":"support-desk","metadata":{}}
{"id":"triage-inbox-message-processor","channel":"inbox","cursor":14,"consumer":"triage-inbox-message-processor","batch_size":100,"filters":{"kind":"message"},"metadata":{}}
One server now offers the strategy's /run, the tactic at /tactics/triage/,
channel shortcuts at /channels/<name>/events, and the full store API at
/network/. GET /strategy lists everything that is mounted. Stop the
servers with Ctrl+C when you are done.
Summary
| You want | Use |
|---|---|
| A store on disk | LocalStore(".lllm/network"), create_channel |
| To record something that happened | append_event(Event(...)) or lllm network append |
| To run a tactic over every new event | TacticProcessor(tactic, store, source=..., sink=...).process() |
| To trace a result to its input | parent_ids, correlation_id, get_event |
| To share a store between processes | lllm network serve, RemoteStore, AsyncRemoteStore |
| To keep URLs out of code | .lllm/config.toml refs and lllm.Resolver |
| One API for tactics, channels, and workflows | lllm.toml, a Strategy with a runner, lllm shell serve |
Read more in SSSN (channels, processing, serving) and AAAX.
Next: Tutorials index