papeete-actor-simple
A papeete-actor you can run, restricted to two verbs.
request ask an actor to do something. It yields an ACK, never a result.
query ask an actor something. It yields an ANSWER.
That is the whole vocabulary. No publications, no subscriptions, no event log, no outbox, no positions, no cadence — and no method on this package that emits an unaddressed fact.
pip install papeete-actor-simple
The restriction is the product
papeete-actor publishes the ecosystem's contracts and gates them. Nothing in the ecosystem
runs an actor: a card declares offers — "what I can be asked to do" — and a mailbox, but
no code turns those declarations into a box that receives a message, decides, and replies.
This is that box, deliberately crippled. papeete-actor-card/v1 describes an actor with four
directions; two of them are unaddressed facts, pulled on somebody else's cadence, and that
is where the hard problems live — positions, redelivery, backfill, supersession, dead letters.
Cutting them out leaves a synchronous, addressed, refusable conversation, small enough to
prove correct in CI and therefore small enough to be the first thing anyone actually runs.
A simple actor is a conformant papeete-actor card that declares no publications and no subscriptions.
Not "must not" — cannot. There is no payload kind for an unaddressed fact, and no method
that would take one. The guarantee is an absence, which is why the suite can assert it with
hasattr.
A request still has no return value
papeete-actor-card/v1 is explicit: a request completes "as a refusal, or LATER as a
publication the sender may have subscribed to" (ADR-PA-0009 §4). Publications are banned
here, so this deployment supplies the other half — the disposition is discovered by query:
Auditor -> Archivist request {offer: record-deviation, subject: …, means: …}
Archivist -> Auditor ack {disposition: accepted, because: …} ref: req-0001
…Archivist does the work in its own repo, under its own review…
Auditor -> Archivist query {offer: deviation-state, about: req-0001}
Archivist -> Auditor answer {state: resolved, says: …, records: […]}
The rule survives intact, and the two verbs become mutually load-bearing rather than merely
coexisting: without query, nothing a request starts could ever be found again
(ADR-PAS-0002).
One actor using another — the coupling
dependencies on a card is the actors whose contract I resolve, and it is a business
edge: the coupled actor holds knowledge this one does not hold, and computes what this one may
not compute. What an actor is built with never appears there — that is interior — and the
contract it conforms to is already stated by card:.
The two worked examples are two repos, and each declares one coupling:
tests/e2e/scenario1_two_actors_in_process/actors/ <- the context: a small information
system, two actors wide, living
inside the scenario that tests it
profile.yaml the deployment's own facts: its rails, and where its index is
registry.yaml WHERE each card lives. The sole authority; no card says.
archivist/ papeete-actor.yaml EXA.ARCHIVIST dependencies: [EXA.AUDITOR @ 0.1.0]
ledger.py its store of record, and the only thing it writes
ledger.yaml that store, rendered after the worked conversation
auditor/ papeete-actor.yaml EXA.AUDITOR dependencies: [EXA.ARCHIVIST @ 0.1.0]
triage.py the coupling, in code
The registry is written against ecosystem-registry/v0, the contract papeete-actor 0.5.0
added for exactly this file, so its own whole-ecosystem tools run over the examples unchanged —
all three in CI, on every push:
papeete-actor lint-registry tests/e2e/scenario1_two_actors_in_process/actors/registry.yaml
# ok conforms to ecosystem-registry/v0 (2 row(s): 2 actor, 0 dangling, 0 external)
papeete-actor lint-card --registry tests/e2e/scenario1_two_actors_in_process/actors/registry.yaml tests/e2e/scenario1_two_actors_in_process/actors/*/papeete-actor.yaml
papeete-actor check --workspace tests --registry tests/e2e/scenario1_two_actors_in_process/actors/registry.yaml
# ok joined 2 card(s): 0 publications, 2 at papeete-actor-card/v1
The second is the cross-card join — dangling subscriptions, unsubscribed publications,
unpinned scripted subscriptions — run over two example actors exactly as it runs over the seven
real ones. That line is the restriction's central claim put to the ecosystem's own tool instead
of asserted here: a context that publishes nothing and pulls nothing gives the join nothing to
report. And a coupling naming an id the registry does not place fails the first command with
resolves nowhere in registry.yaml — a check this package cannot perform for itself.
What an accepted record-deviation actually writes — the caller's grounds kept as a
quotation, never restated as the Archivist's own finding:
- ref: req-0001 # opaque by contract. Nothing downstream may parse it.
door: record-deviation
rail: contract-deviation
opened_by: Auditor
subject: publication/v2 requires a payload shape for a fact that was never emitted
reported: # THEIRS. Quoted, attributed, unedited.
by: Auditor
means: 'my card carries `shape: none` with a status explaining why, and the join …'
recorded_by: EXA.ARCHIVIST # MINE. The judgement and the write.
state: resolved
A refusal reaches no entry — there is nothing to record about work an actor declined to take.
And the entry never crosses the membrane: answer.records is a list of strings by contract,
so what a caller reads back is one quotable line per entry, not the store.
A debt report reaching the Auditor is sometimes not debt — it is a shape that cannot express something true. Finishing that triage needs two things the Auditor does not have:
Archivist -> Auditor request {offer: report-debt, subject, means}
Auditor -> Archivist query …deviation-state "does your ledger already carry this?" KNOWLEDGE
Auditor -> Archivist request …record-deviation "yours to judge, and yours to record" COMPUTATION
Auditor -> Archivist ack {disposition, because}
The Auditor never writes in that ledger and does not decide the outcome — the Archivist is
free to refuse it. What it keeps is a quotation and a ref, so whoever reads debt-state
later can go and check at the source. And the door is not a string typed anywhere: the
coupling resolves EXA.ARCHIVIST to an exact card, and the card names the door for the
nature and rail the case has. Delete the declaration and the run stops at resolve — an
undeclared coupling is refused, because an edge a reader of the card cannot see is one nobody
can audit (ADR-PAS-0005).
Determinism sits at existence, never at interpretation
| deterministic — never the model | judged — the engine |
|---|---|
| is the envelope present and well-formed | should this request be accepted, on the grounds given |
| does the answering card declare this door | what does my own state say in answer to this |
do I hold an exchange under this ref |
|
| does the payload validate against its kind |
The split is structural, not policed. The JSON Schema an engine is constrained by is built
from each payload's judged: fields alone, so answer.state is not a field a model is trusted
to get right — it is one the model is never shown and cannot emit. A query about a ref
nobody opened returns state: unknown without calling the engine at all.
The engine is a port, not a vendor
class Engine(Protocol):
def judge(self, *, system: str, prompt: str, schema: dict) -> dict: ...
Three adapters, and neither vendor is a dependency of this package:
| engine | used for | needs |
|---|---|---|
scripted |
the conformance suite — no network, no key, runs on fork PRs | nothing |
claude |
local development — never runs in CI (pytest -m claude) |
pip install 'papeete-actor-simple[claude]' |
openai |
CI asserts the protocol with this one | pip install 'papeete-actor-simple[openai]' |
Running one suite against two unrelated vendors is the evidence for a claim
papeete-actor-card/v1 already makes — "what a papeete-actor is built with is interior; the
card is membrane". A suite that only ever ran against the model the runtime was developed
against could not tell a contract from a habit
(ADR-PAS-0003).
The system prompt is generated from the card, so impersonating a different actor is a
matter of pointing at a different papeete-actor.yaml — never of editing a prompt.
Try it
papeete-actor-simple contracts # …and the deployment profile in effect
papeete-actor-simple lint-card tests/e2e/scenario1_two_actors_in_process/actors/*/papeete-actor.yaml
# Two things this gate CARRIES and never decides — where coupled actors live, and which
# rails this deployment routes on. Both default sensibly; neither is this package's to own.
papeete-actor-simple lint-card --registry path/to/registry.yaml --profile path/to/profile.yaml CARD
papeete-actor-simple converse --from tests/e2e/scenario1_two_actors_in_process/actors/auditor \
--to tests/e2e/scenario1_two_actors_in_process/actors/archivist --engine scripted
from papeete_actor_simple.actor import SimpleActor
from papeete_actor_simple.engine import resolve
from papeete_actor_simple.mailbox import InProcessMailbox
box = InProcessMailbox()
engine = resolve("claude") # or "openai", or "scripted"
auditor = SimpleActor.from_card("tests/e2e/scenario1_two_actors_in_process/actors/auditor", engine, mailbox=box)
archivist = SimpleActor.from_card("tests/e2e/scenario1_two_actors_in_process/actors/archivist", engine, mailbox=box)
peer = auditor.resolve("EXA.ARCHIVIST") # a declared coupling, located by the context
door = peer.doors("action", "contract-deviation")[0]
ack = auditor.request(to=peer.name, offer=door.id,
subject="publication/v2 has no slot for a fact that was never emitted",
means="my card carries `shape: none` and the gate cannot tell why")
answer = auditor.query(to=peer.name, about=ack.ref, asks="what became of it?",
offer=peer.doors("query", "contract-deviation")[0].id)
assert not hasattr(auditor, "publish") # the guarantee is the absent method
The contract is in this repo
src/papeete_actor_simple/schemas/ — ordinary committed
source. The package IS simple-actor-protocol/v0, not a runtime that goes looking for it,
which is ADR-PA-0001's
position applied here. uv build reaches nothing outside its own checkout, and
papeete-actor-simple contracts is the assertion that the schema arrived in the wheel.
papeete-actor-card/v1 is the other way round: consumed at a pin, never re-authored. Not
one rule about a card appears in this package — card.py calls papeete_actor.cards.lint and
merges its findings (ADR-PAS-0004).
Two deployment facts travel the same way. Where a coupled actor lives comes from a
registry, and which rails a card's offers may name comes from a deployment profile
(ADR-PA-0016) — neither is a contract, both are facts about one deployment, and this package
carries them to the gate that owns them without reading either. papeete-actor-simple contracts prints the profile in effect beside the contract versions, because a card that
lints on one machine and fails on another usually differs by exactly that.
The four payload kinds are candidates for inter-agent-message/v0, whose doctrine says new
kinds "slot in without disturbing the layers already there". They are proven here, under a
restriction, before anyone proposes moving a shape that seven cards declare. The route home is
papeete-actor's own contract-deviation door.
Impersonation — where this is heading
SimpleActor.from_card(path, engine) boots an actor from any card. Today
reliever-design's card would be refused: it declares a publication and two subscriptions.
That refusal is the honest statement of the direction of travel — a business actor becomes
impersonatable when, and only when, some part of its work can be expressed inside the
restriction. See doc/SIMPLE-ACTOR.md.
Licence
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file papeete_actor_simple-0.1.0.tar.gz.
File metadata
- Download URL: papeete_actor_simple-0.1.0.tar.gz
- Upload date:
- Size: 122.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a0dcf64558279d446c5396a9d5cded658107f8e32110fbc2914122a352dba88b
|
|
| MD5 |
40e23a1cb7a0b5ab9471e282e2e29cc8
|
|
| BLAKE2b-256 |
46052bd6a5e3ef6e2f84fb6565ff6571d8dd1bc4f028fc10cbf3802965e0b5f9
|
File details
Details for the file papeete_actor_simple-0.1.0-py3-none-any.whl.
File metadata
- Download URL: papeete_actor_simple-0.1.0-py3-none-any.whl
- Upload date:
- Size: 43.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
638f331c9a50f52119f8faf7bbb4c4b570dcd5e698d90a7ea8977af21ea571f6
|
|
| MD5 |
14e9ade73689fd7d42408f48cfba9bd8
|
|
| BLAKE2b-256 |
98f03394a793a617ca69dcbcfae75504c1615c841c450781d2451742747b292b
|