Agent Client Protocol seats any client
Link Harness is an ACP agent: a client of the Agent Client
Protocol opens a session on one of
its conversations, prompts it, and is streamed the answer as it's
written, its tool calls beside it. It is the only way in: Link's
connections, lnk agent chat send and its way in, the messages from its
schedules and subagents, all reach it so.
Connect
A connection is a WebSocket on the agent's endpoint port, at /acp,
one JSON-RPC message to a text frame. It is opened as any request to
the endpoint is: with the agent's endpoint key as a bearer token, and
Link-Agent or Link-Agent-Id where the harness answers for more than
one agent. Without the key, the WebSocket is refused before it opens.
initialize
initialize answers with ACP's first version, whatever version was
asked for, and says what Link Harness does: it loads, lists and resumes
sessions, and takes images and embedded resources in a prompt.
Sessions are conversations
A session is one of the agent's conversations, and its id is the
conversation's thread: telegram:42, default. A connection prompts
and cancels only the sessions it opened.
session/new
Opens the thread _meta.link.thread names, or a new one, acp: and a
number, which is then the session's id.
session/load
Replays a conversation as the updates that streamed it, then opens it. A replay holds your messages, the agent's answers, its calls and their answers, and each command as you typed it. Summaries and thinking are the model's, and never replayed.
session/resume and session/list
session/resume opens a conversation without replaying it, and
session/list lists the agent's conversations, kept any way.
session/prompt and session/cancel
session/prompt sends a message, answered as a turn, and
session/cancel ends the session's running turn: its prompt answers
cancelled.
Its commands
Once a session is open, the harness lists the commands that are on, in
an available_commands_update. A command is still the text of a prompt
(/chat:create), as it is on every connection: Commands.
Prompt it
A prompt's text blocks are its message. An image block, or an embedded
resource with its contents in base64 (blob) or as text, is an image
or a document of the message, taken as the agent's spec says
([connections] images and documents). A link to a resource, or audio,
is refused, never dropped.
While its turn runs
The session is sent agent_message_chunk, each piece of the answer as
it's written; tool_call, each call the model makes, its tool's name
as its title and its arguments as rawInput; and tool_call_update,
the call's end, completed or failed, with the tool's answer.
Its answer
The prompt answers end_turn once the turn ends, or cancelled when
it was cancelled. A prompt sent while another turn of the session runs
joins it, as the agent's spec says ([turns] busy): it then answers at
once, end_turn with _meta.link.steered true, and the running turn's
answer answers it too.
Steer, queue or interrupt
What a prompt sent while another turn of its session runs does is its
own to say, in _meta.asp.busy, as the Agent State
Protocol
(ASP) has it: steer joins the answer under way, queue waits for its
own turn, and interrupt ends the running turn and starts its own.
Left out, the agent's spec says ([turns] busy).
{"sessionId": "telegram:42", "prompt": [{"type": "text", "text": "Skip the tests."}], "_meta": {"asp": {"busy": "interrupt"}}}Which it takes
initialize says which it takes, in _meta.asp.steering: queue and
interrupt always, and steer where its spec steers, since only then
does a running turn read what joins it. One it doesn't take is refused.
Link's words beside a prompt
What Link says of a message is in its _meta, under link. A
_meta.link holding anything else, or a sender that isn't one, is
refused.
{"sessionId": "telegram:42", "prompt": [{"type": "text", "text": "Find the note."}], "_meta": {"link": {"from": "person:telegram:42", "fromName": "Ada", "hops": 1, "tools": ["memory__search"]}}}thread
On session/new, the conversation the session is, which it opens in
place of a new one.
from
Who the message is from, when it isn't the owner: a schedule, a subagent, another agent or a person. A message from anyone but the owner never runs a command.
fromName
A person's name on their connection, beside a person only, words for the model and never who they are.
hops
How many messages led to it, each sent by an agent on its own, which the harness passes on to its tools.
tools
The tools the conversation may use from this message on, never more than it had: a later list narrows the conversation's tools.
Its conversations, over ASP
It answers the agent state of ASP
for its conversations, and advertises them under initialize's
_meta.asp.agentState. _asp/state/export gives each conversation as
session/load replays it; _asp/state/import writes each one that
came in under its thread's name, so session/list lists it and it
goes on. Who the agent is, its skills and its memory are its spec's
and Link's memory's, which Link carries itself: an import says so of
each.
A conversation its thread already holds
A conversation that went on in another harness comes back to a thread
that holds its start. The import adds only the turns after what the
thread holds, when the owner's turns that came in open with the
thread's, each holding its words. Otherwise the conversation parted,
and goes on a thread beside it named for the harness it came from,
such as link:main~hermes, which lnk agent chat list shows. One that
names no harness goes on a new thread of its own.
Imported conversations
Session import keeps the exact exported updates beside native records for text, completed tool calls, images and supported resources. Historical tool calls never execute. Thinking without native signatures, audio and unsupported metadata remain archival: export preserves them, while the continuing conversation uses the content it can represent. The import response names this limit.
What Link adds
Two methods of Link's own sit beside ACP's: _link/taken, which it
sends as soon as it has a message, and _link/recover, which asks for
the answers a crash left.
_link/taken
A notification it sends a session as soon as it has a prompt's message, before the answer: a webhook's sender is told then.
_link/recover
A request for the answers of turns finished after a crash, or whose
caller went before the answer: {"two_phase": true} gives each with
its id, kept until {"done": ["<id>"]} says it was sent. Link's
bridge asks as it starts and every half minute.
Ask it from a shell
lnk-link-harness ask <thread> asks the harness on this machine's port
one message, as a bridge does, and prints its answer: the endpoint key
on stdin's first line, the message after it. --from <sender> sends it
from someone who isn't the owner. Why it works this way:
Decisions.
printf '%s\nWhat did I say?' "$KEY" | lnk-link-harness ask telegram:42Docs
- Decisions: Why Link Harness speaks ACP the way it does