Interface
The commands a store driver, lnk-<name>, answers for an agent's
collections.
Store drivers (lnk-<name>)
The contract is
store.rs.
A driver keeps an agent's things at a place: Link's are
lnk-file-store (File Store),
lnk-sqlite (SQLite Store),
lnk-bucket-store (Bucket Store)
and lnk-gcs, off the machine
(Google Cloud Storage Store).
Where Link Harness uses one
Link Harness keeps an agent's conversations with a driver when its spec
says the driver's name ([storage] conversations = "sqlite"). A
conversation a driver keeps has an empty store.<name> in its folder,
which says so to Link Harness, memory and lnk agent chat show.
The store bridge
Link Harness calls a driver through the agent's store bridge: a thread
of the agent's bridge (lnk agent bridge), outside the sandbox,
listening at .link-store.sock in the harness's own folder (600),
which runs the driver there.
Kept running or run once
A driver whose contract says serve is kept running, at most 4 of it,
each answering call after call on what it holds (a connection to its
store), and stopped with the bridge; any other is run once a call. A
call is stopped past 30 seconds.
Each call
Each call is a bridge's call whose action is the Request below,
proved as the agent the request names, answered with one Reply line,
a driver's {"reply": "error"} answered as the error envelope. A call
naming another agent's conversations, or proved as any agent but the
one whose bridge it is, is refused
(link_store_contract::store::serve_bridge).
Its limits
It holds at most 16 proved calls at once, 8 of one agent's (one more is
answered busy), waits at most 5 seconds for a proved call's request,
and runs at most 4 drivers at once.
Links and its socket
It refuses a call while its driver's folder or the agent's own folder is a link. It never changes its socket's mode, which is in a folder the harness writes: the proof decides who's answered.
Which adapters get it
Legacy adapters require the agent's folder layout, writing
chats/ but not the folder around it.
Outside the sandbox
The driver's program, and whatever it needs to reach its store, stay outside the sandbox.
Without a bridge
Without a bridge (memory, the step of lnk agent chat show, a
conversation kept by another driver than the spec's now), a driver runs
where its caller runs, in its caller's sandbox, which reads its
program.
LNK_STORE_DIR
The folder a driver on this machine keeps what it keeps in, the agent's
chats/ in its own folder (Agent/chats), so it moves
with the agent. Whoever runs it may set more in its environment, such
as where its own settings are, as its docs name them
(link_store_contract::store::Driver::with_env).
lnk-<name> contract
Prints link_store_contract::store::Contract as JSON: {"version", "serve"?, "jobs"?, "files"?}. Each left out, it doesn't. The store
bridge asks it once a program.
version and serve
version is the version of this contract it speaks, 1, and serve is
true when it answers lnk-<name> serve.
jobs and files
jobs is true when it keeps agents' jobs, the job_* ops, and files
when it keeps agents' files by path, the file_* ops. The bridge never
sends a job_* op to a driver that doesn't say jobs, nor a file_*
op to one that doesn't say files: the call is refused, saying so.
lnk-<name> call
Reads one link_store_contract::store::Request as JSON on stdin,
{"op", "agent": {"id", "owner"}, ...}, and prints one Reply,
{"reply", ...}, exiting 0; {"reply": "error", "error"} when it
fails. The agent is named as Link names it (LNK_AGENT_ID,
LNK_OWNER; for an agent without them, its name and no owner). Each
op of a conversation follows, its arguments in brackets, and its reply.
append
(thread, event): writes the event, a JSON object whose seq is its
number from 1, only if its number is free and the next: done, else
taken with the conversation's last number (last). Of two writers of
one number, on any machines, one writes it. An event written is never
changed, and nothing of it but seq is the driver's to read.
read
(thread, first): the events numbered first and on, in order:
events (events), none for a conversation it doesn't keep.
last and threads
last (thread) answers the last event's number, 0 for none: last.
threads answers each conversation it keeps of the agent, with its
last number: threads ([{"thread", "last"}]).
remove
(thread): forgets a conversation, its events and its lease: done.
The tools a list narrowed it to stay (tools_read answers them still),
so a conversation removed is never made again with more.
take
(thread, holder, lapse_ms): takes the conversation's lease, which
says which holder runs its turn, for
holder: took with took, which is took (no turn ran), lapsed
or held (another's, renewed lately). lapsed is taken from a holder
not renewed for lapse_ms, judged to the millisecond, never by a whole
second: its turn crashed, and there's one taker only.
renew and end
renew (thread, holder) answers done, or lost when the lease
isn't holder's any more. end (thread, holder) lets holder's
lease go, nothing when it isn't its: done.
leases
(lapse_ms): every lease of the agent's conversations, leases
([{"thread", "holder", "started", "renewed", "lapsed"}]), in seconds
since 1970 by the driver's clock, lapsed judged by lapse_ms.
Jobs
A driver whose contract says jobs keeps the agent's scheduled jobs
too, for a scheduler whose agent runs only while it answers (Link
Scheduler,
--store-bridge): they outlive it on the machine, and any machine it's
bound to next reads them.
A job
A job is {"id", "spec", "due"}: its ID (1 to 256 bytes, no control
character), its spec (JSON, at most 1 MiB, none of it the driver's to
read) and when it's due (seconds since 1970). Each is in a place,
waiting, firing or ending, and one ID may be in more than one.
job_put, job_get and job_remove
job_put (place, job) keeps the job in place, in place of any
there with its ID: done. job_get (place, id) answers job
(job, the job or null). job_remove (place, id) answers
found (found), whether it was there.
job_list and job_count
job_list (place) answers every job in place, in no order: jobs
(jobs). job_count (place) answers how many, none read: count
(count).
job_shift
(id, from, to): moves the job from from to to, in place of
any there with its ID, whole: found (found), whether it was in
from. Of two shifts of one job from one place, on any machines, one
is made, as of two appends of one number one writes it.
job_next
When the soonest job waiting is due, never one being fired: next
(next, null with none waiting). A store off the machine answers
what's due for every agent from the same.
job_day and job_set_day
(day, sent): the day's count of the agent's messages, the day as
days since 1970 in the agent's time zone: day (day, sent; 0 and
0 for none kept), and done.
job_mark
A number that differs once a job is put in, shifted to or from, or
removed from waiting, which a runner watches between its looks:
mark (mark).
Files
A driver whose contract says files keeps the agent's files by path. A
driver that doesn't keep a collection's files refuses any file_* op
of it, {"reply": "error"}.
A file's path
A path of its instructions or its skills, its first part naming which
(instructions/instruction.md, skills/greet/SKILL.md): relative, no
part empty, . or .., at most 1,024 bytes
(link_store_contract::store::check_file_path).
What the bridge checks
The store bridge checks every file_* op's path, and the size and
base64 of what it writes, before the driver runs, refusing any other.
file_read
(path): file (file: {"data", "version"}, its bytes as base64,
at most 4 MiB, link_store_contract::store::FILE_MAX, and its version;
null for none).
file_list
(folder): the paths of the agent's files under folder (skills/),
in order, at most 4,096 (link_store_contract::store::FILES_MAX):
files (files). A folder of more is refused, never answered cut
short; a caller refuses a longer answer.
file_write
(path, data, version): writes the file at path with data
(base64, at most 4 MiB) if its version is still version (0: there's
none): wrote (version, its new version), else stale (version,
its version now, 0 for none). Of two writers of one version, one
writes; the other is told stale and reads again.
Versions never come back
A file is never given a version it had before, even made again after
its removal, so a writer still holding one from before is told stale.
file_remove
(path, version): removes the file if its version is still
version: found (found), whether it did.
file_append
(path, data): adds data (base64) at the file's end, made if
there's none, in one step: of two appends at once both are kept, one
after the other, never one within the other. wrote (version). One
that would grow it past 4 MiB is refused, the file left as it was.
A conversation's tools
The tools a conversation was narrowed to: a subagent's
(Link-Tools).
A driver that doesn't keep them refuses these ops, {"reply": "error"}.
tools_read
(thread): tools (tools, a list, or null when nothing narrowed
it and it has every tool).
tools_list
Every conversation a list narrowed, with what it may use, those removed
(their narrowing kept) and those narrowed before their first event
included, which threads lists neither of: narrowed (narrowed,
{"<thread>": [<tool>, ...]}). A move carries each from here, so none
arrives with every tool.
tools_narrow
(thread, tools): narrows the conversation to tools, at most 256
tools' names (link_store_contract::store::check_tools, which the
store bridge checks), of those it may use, in one step: a tool it may
not use stays out, so of two at once the second narrows what the first
left, and a list never widens it. tools, what it may use now.
Kept off the machine
A caller takes an answer naming a tool it didn't send, or null, as an
error. A harness whose conversations are kept off the machine keeps
their tools only here, and fails a turn whose tools it can't read
rather than offer every tool.
lnk-<name> serve
A driver whose contract says serve only. Reads requests as call
does, one a line on stdin, and prints each one's Reply as a line, in
order, until stdin ends, then exits 0; an error is that request's
{"reply": "error"}, and the next is read.
One call at a time
A call still runs as one: the bridge sends the next only once it has this one's answer, and stops the driver when an answer takes past 30 seconds.
Its main
link_store_contract::store::drive is its main: it answers --version,
--help, contract, call and serve, and takes the driver's name, its
help, its contract (built on Contract::serving, with jobs or files
for a driver that keeps them) and its answer. Any other command is a usage
error, and a driver writes nothing else to its stdout.
Test a driver
Every driver's tests run the contract's checks, run here and behind the store bridge.
check
link_store_contract::store::check runs a driver through all of it,
eight writers of one number at once and four takers of one lapsed lease
included.
check_jobs
Runs a driver that keeps jobs through theirs, eight shifters of one job at once included, and another agent's jobs its own.
check_files and check_narrowing
check_files runs a driver that keeps files through theirs, eight
writers of one version and eight appends at once included, and another
agent's files its own. check_narrowing runs one through a
conversation's narrowed tools, eight lists at once included.