@projectpac/flow-skills
v0.6.0
Published
Part of PAC: @projectpac/flow-skills.
Readme
@projectpac/flow-skills
Each node holds instruction documents, advertises some of them, and a peer can ask for one to be applied to something.
A skill arrives as a markdown document with a little front matter, because a skill is something a principal writes and edits. The two halves then live apart, and that split is the design: the body becomes a source, so this flow names it and never holds it, and the front matter stays a declaration the flow can read without reading the skill. Describing a skill must not mean reading it, the same way describing a plugin must not mean running it.
Every call runs in a box both sides verify and neither owns. The caller
stakes its request and whichever of its own private sources the principal
authorized to; the owner stakes two things, the skill and the model access that
pays for running it. Each side gets back only what it is entitled to: the caller an answer, the
owner the fact that a run happened. Neither node's material passes through the
other, and this flow holds none of it -- every step that touches material is
an apply, and the box program is proposed byte for byte so both sides approve
the same bytes. The program is ordinary python, the same bytes for every call:
each side derives it from its own operation-llm rather than taking a peer's
copy, and where it may reach is the session's declared egress, signed into the
approval beside the program's hash.
There is no local mode. A skill run on the owner's own model would mean the caller's input leaving the caller's machine, and the body sitting in flow memory to be put in a prompt -- so removing it is what lets the body be a source at all.
What a skill declares
A skill is not this flow's to keep. @projectpac/source-skills holds the
documents and answers what this node offers; this flow calls one. The document
lives there:
---
description: Builds a music-taste profile from someone's listening history.
provider: openrouter.ai
needs: spotify_songs
---All three are public, and that is the whole declaration. A skill used to also
name the model it ran on and the file holding the key that paid for it; both
are @projectpac/source-llm's now,
because a credential pinned to a skill means only that skill's owner can ever
pay to run one. provider stays because it is the disclosure -- the host a
peer consents to, and the one word the box session's declared egress speaks.
needs names the caller's sources, not the owner's: it is what a
caller is being asked to disclose, published so it can be read before anything
is disclosed. A call may narrow that list and may never widen it, and naming a
source outside it is refused rather than quietly dropped.
provider is a hostname, and it is the same word three times over: what a
peer reads on the board and consents to, what this node's box adapter checks
its own egress allowlist against, and what the box session declares its
program may reach.
Starting a call takes three answers. A list is exactly those sources. An empty list is none of them, and the call runs on its request text alone. Omitted is what the skill declares and this node holds -- the router's case, since it picks the skill and names no sources.
Nothing here asks a principal, and no layer under it does either: there is no
policy service on a node, so what a call may apply is decided by what the
principal authorized when they made it. The one real refusal on a principal's
behalf is the box adapter's egress allowlist, which is why a call to a
provider this node has not allowed is refused by the side that holds the data.
What is refused where it starts is a skill that reads only sources this node
does not hold, because a call over none of the data comes back saying the data
was missing and costs a box run to learn it.
A skill whose body is gone is declared but not offered: a peer that called it would get a failure this node could have predicted. Whether anybody can pay for it is a separate question, asked when a call is made -- a node that holds a skill and no access to its provider still offers it and refuses the call by name, because the skill is genuinely this principal's and hiding it would say something false.
The flow speaks for itself, and the host says nothing on its behalf. It
declares its route to the network adapter, so an envelope addressed to it is
dispatched into its own context and one addressed to a flow nobody declared is
refused. It advertises on discovery that this node speaks it, under
kind: "flow", with the version, the package, and the payload types a peer
needs to join in or fetch it -- the rows the router's
GET /flows/router/available reports. It declares its one intent to the router
from an optional scope, so a node with no router still holds skills and answers
its board, and one that gains a router later hears from it the moment the
service comes up. It keeps its records through ctx.records, the record store
it waits for before it serves anything. And it runs its own timers, each an
effect of its scope, so disabling the flow stops them with it.
| | |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| routes | POST /calls, GET /calls, GET /board -- writing a skill down and listing what this node holds are source-skills' routes |
| payloads | call.request, call.failed, call.accepted, call.session (this flow's own: the caller's first message carries the skill and its party key together) |
| records | calls, kept through ctx.records -- the record store the flow waits for |
| background | advertise every 5s (what its sources offer), advance every 2s (the box state machine), sweep every 30s (terminate what stalled); timers of its own |
| needs | the sources named in sources (skills for what it offers, whatever holds the caller's own data), @projectpac/operation-llm to derive the program, and a box adapter whose egress allows the provider |
| intents | skill, declared to the router from an optional scope -- picked only for something the board is offering right now |
| results | one call per call, both sides, declared to ctx.results from an optional scope: GET /flows/results/results reads them beside every flow's |
Five files: flow.ts is what the flow declares about itself and what every
step has in hand, calls.ts a call from the ask to the answer -- the record,
the box spec, the wire -- board.ts what the network is offering, program.ts
the python both sides approve, and index.ts the wiring and the routes.
Driving it by hand:
devtools/README.md.
Running it
pnpm check # includes tests/skills.spec.ts: two whole nodes, box double included
just demo # headless: a directory, two seeded nodes, the board, down
just dev # the lived-in lane: panes, pages, and the real box -- then, in another terminal:
just skill "three days in Tokyo, and I like food"just dev owns the terminal it runs in, so the call goes in another one, once
the seed pane rests. The text is the call's input; a second argument names any
skill the board is offering (just skill "what do I listen to?"
music-taste-profile), and it defaults to the trip-planner agent2 offers.
just a1 api GET /flows/skills/calls shows the call and the answer on the same
row; devtools/README.md has the other direction.
The call itself runs on somebody's model inside the box, so it needs a real
key: copy .env.example to .env and put an OPENROUTER_API_KEY in it before
seeding. What that variable is for is declared in
pac-plugins/devtools/fixtures/<agent>/models/, which is committed; only the
value is not. Without one the nodes seed with a placeholder and say so -- the
skills advertise and the board fills, but the call fails in the box.
The sibling layout
Cross-repo dependencies are link: paths into checkouts beside this one --
nothing of PAC's is fetched from a registry. Clone the repos this one links
(the link: values in the package manifests) into the same parent directory,
run pnpm install, and everything resolves from source. The flow's whole-node
suite lives in tests/, standing up real nodes from the sibling pac-node.
The design repo
(pac) is the front door: what PAC is, and how the repos fit together.
