Builtin Library
README.md
notices
Post an event record that surfaces to the callers it is addressed to, on the response of the tool call that posted it, or on each one's next tool call.
A notice reaches a caller without the caller knowing to look for it: each caller in the notice's audience is shown it once, attached to the response of the tool call that was running when it was posted, whether that call succeeded or failed. A notice posted while no call was running is attached to that caller's next tool call. Reading it marks it read for that caller alone; the other callers it is addressed to still receive it.
Content-posted notices are marked as coming from world content, and their values render as data rather than as instructions.
notices.post("wave complete", { wave = 3 })
notices.post("save slot corrupted, using defaults", { slot = id }, { severity = "warn" })
Notices with the same template collapse into one line with a count, so a
loop that posts the same template many times renders once with the varying
values summarized:
for _, path in ipairs(rebuilt) do
notices.post("texture atlas rebuilt", { path = path })
end
-- surfaces as: texture atlas rebuilt (x950) { path="a.png", "b.png", ... } #5e0c2a91
A "warn" or "error" notice posted again with the same template and the
same values after its reader was shown it is a repeat. A repeat is held for a
while after that delivery and collects every further repeat, then reaches the
reader as one line carrying this delivery's count and the total, and each
delivery of a repeat doubles the next hold. A notice with other values, or
another template, is delivered at once beside it, so an error raised over and
over never buries a new one:
-- surfaces as: enemy spawn failed (x14 again, 15 in all) { wave="3" } #0b7d41e6
Keep the template a fixed sentence: it is the identity used for collapsing.
Put every varying value in params. opts.severity is "info" (default),
"warn", or "error"; opts.includeLocation attaches the emitting call
site. Each line a reader is shown ends with the notice's handle (#5e0c2a91
above). An agent that has acknowledged a repeating notice mutes it by that
handle through the notices toolbox (tools.use("notices", "suppress", { key = "5e0c2a91" }), or zero notices suppress 5e0c2a91 in bash); the mute is its own, and every other caller
keeps receiving the key. On an engine bound to a live world the mute is kept
in the signed-in user's store of that world, so it holds on the next engine
that opens the world too. It lasts until the agent unmutes the key, or until
the poster withdraws the notice because the condition it stated is over. A
poster that refreshes a standing notice with new values withdraws the old one
with notices.withdraw(template, { replacing = true }) and posts again: the
callers that muted it stay muted.
opts.audience says who reads the notice. Left out, it is the caller whose
code is running — the agent whose call, task or callback posted it — or
every caller when no caller is running (a notice the engine raises on its
own). "*" addresses every caller; a list addresses those callers only, each
entry an agent's name (as the agent toolbox's roster lists it) or a
caller key <kind>:<id> such as agent:<name>. A notice reaches the agents
on this engine: an entry naming no agent on this world's roster, an entry
naming an agent the roster lists on another engine of the world, or an empty
list, raises an error.
notices.post("boss defeated", nil, { audience = "*" })
opts.actionable says whether the callers a notice is addressed to are
expected to act on it, as opposed to taking it in as background. Left out, a
notice addressed to particular callers — the default audience included, so
the probe, task or callback an agent set up reaches that agent — is
actionable, and a notice for every caller is background.
What a receiver does with that is its own decision. An agent working in the editor's agent panel is handed its notices as they arrive rather than on its next tool call: an actionable one starts a turn of its own when the agent is idle, and a background one joins the agent's context on its next prompt or tool call.
notices.post("build ready for review", nil, { audience = { "terrain" }, actionable = true })
notices.post("lighting rebaked", nil, { audience = "*" }) -- background
Ephemeral notices
opts.ephemeral = true posts a reminder rather than a stored notice. An agent
whose harness has an ephemeral channel is shown it for a short window and it
is never stored in the agent's history; an agent with no ephemeral channel
gets it as a normal notice on its tool results.
notices.post("the build is not published", { uncommitted = 4 }, { ephemeral = true, audience = "*" })
An ephemeral notice is identified by its template and the code that posted it:
posting the same template again from the same code replaces the entry. Keep
the template a fixed sentence and put every varying value in params.
sticky = truekeeps the entry visible on every read until it is replaced, withdrawn or expires. It expires 120 seconds after its last post unless it is posted again; identical text refreshes it.ttlraises or lowers that lifetime, at most 600 seconds.renew = trueshows an entry again that is posted with identical text.audienceis"*", or a list of agent names or caller keys; left out it is the caller whose code is running. An empty list raises, as for a stored notice, and a post the store refuses raises with the store's reason. The audience is not checked against the roster: a list entry naming no agent on it is kept for a key nobody reads. A bare agent name in a list is read asagent:<name>; a single string audience is used as given.message,severity,includeLocationandactionablehave no effect on an ephemeral post, andsticky,ttlandrenewnone on a stored one.notices.withdraw(template)removes the ephemeral notices the calling chunk posted under the template, along with its stored ones. A failure to reach the ephemeral store is logged, and the stored notices are withdrawn all the same.
The notices toolbox's list tool shows the current entries as ephemeral.