Agent Spec
An agent's opinions, written as a file: its model, window, turns, tools
and channels. lnk agent start --spec reads it and lnk agent spec
prints the one the agent runs.
Every option
# The agent itself. Both are yours, to tell your specs apart and to mark a change.
name = "assistant" # the agent's own name
version = 1 # a number you bump when you change the file
# Start from nothing instead of Link's spec, so this file must hold every key.
# extends = "none" # left out, it's "link": Link's spec beneath yours
# Whose it is and the teams it's on are Link's to say, so a file never claims them.
# owner = "github-1234567" # printed by `lnk agent spec`; another owner is refused
# teams = ["research"] # teams whose files list it; any other is refused
# Which harness runs the agent. Switching keeps every opinion below.
[harness]
name = "link" # a harness's name: lowercase letters, digits and dashes
# Which model it uses, and how hard it thinks.
[model]
default = "claude-sonnet-5-5" # the model when the agent names none
allow = ["claude-sonnet-5-5", "claude-opus-5-5"] # the only models it may run; left out, any
answer = 8000 # the longest answer, in tokens; left out, the window's share
thinking = "medium" # off, low, medium or high, where the model can think
thinking_budget = 4096 # most tokens it thinks, for a model that takes a budget
# A provider's own table takes the same keys as [model] and beats it for that provider.
# The providers are anthropic, openai, openrouter, ollama and local.
[model.anthropic]
default = "claude-opus-5-5" # the model on Anthropic
thinking = "high" # think harder on Anthropic only
[model.openai]
default = "gpt-6-luna" # the model on OpenAI
# How much of the model's window a turn fills.
[window]
max = 200000 # tokens a turn fills; left out, the model's whole window
unknown = 32768 # the window, in tokens, of a model Link doesn't know
margin = 0.125 # share kept back, since tokens are counted roughly before a turn
answer = 0.25 # share kept for the answer, where [model] answer doesn't say
# How long a turn may go on, and what a new message does meanwhile.
[turns]
rounds = 8 # most rounds of tool calls in one turn
calls = 16 # most calls one answer makes
busy = "steer" # a message sent mid-answer: queue (after), steer (joins) or interrupt (ends it)
renew = "10s" # how often a running turn renews its lease, at least 1s
lapse = "60s" # a lease not renewed this long: its turn crashed; over twice renew
# What the agent is told, in sections: Link's, a file in its files, or none.
[instructions]
base = "instructions/base.md" # in its files: "You are my research assistant. Cite your sources."
environment = "link" # where its files are, the folders it may change, its channels
standing = "instructions/instruction.md" # your standing instructions, read at each message
now = "link" # the date, time, channel and speaker, at the head of each message
# How large each section may be: what's past it is cut.
[instructions.max]
standing = "8 KB" # 64 B to 64 KB
# What holds for every tool, unless its own table says otherwise.
[tools]
answer = "32 KB" # most of a tool's answer that's kept, 1 KB to 16 MB
timeout = "5m" # most a call may take, waiting for you to allow it included
# One tool's own, by its name. Any tool takes answer and timeout.
# [tool.<name>]
# answer = "64 KB" # this tool's answer, over [tools]
# timeout = "10m" # this tool's call, over [tools]
# The memory tool: what the agent remembers and finds again.
[tool.memory]
keep = "10m" # how long its session stays once no turn uses it
max_age = "1h" # how long its session lives at most
tokens = 6000 # most a search or a read gives back, at least 100
model = "qwen3-embedding" # embedding model it ranks by; "none": by words only, no model started
# The scheduler tool: the agent's schedules and subagents.
[tool.scheduler]
enabled = true # false: no subagents or schedules, and nothing runs them
per_day = 100 # most messages its schedules and subagents send it a day
subagents = 5 # most subagents at work at once
hops = 8 # how deep a chain of messages sent by agents on their own may go
late = "run" # a firing missed asleep: run once on waking, or skip
ask = false # true: task, schedule and cancel ask you first
zone = "Europe/Paris" # its times' time zone; "local" is this machine's
firing = "30m" # how long it waits on a firing before giving up, 1s to 1d
# What holds for every channel, unless its own table says otherwise.
[channels]
stream = true # the answer shown as it's written; false, once it's done
pace = "1s" # how often it's shown again, never faster than the platform allows
groups = "mention" # in a group, which of your messages it answers: mention or never
# working = "eyes" # a reaction while it answers, on channels that have them
# steered = "fast_forward" # a reaction on a message that joined a running answer
# One channel's own, by its plugin's name. The same keys, over [channels].
[channel.slack]
working = "eyes" # Slack marks a message with a reaction while it answers
steered = "fast_forward" # and with this one when the message joined an answer
[channel.telegram]
groups = "never" # on Telegram it talks in direct messages onlyA decision is in the code: what anyone with the facts would choose,
such as Telegram's 4,096 characters a message, or how often its
documentation says a bot may write. A model's own figures are facts too,
so they are data. Its window, longest answer and price are in Link's file
and yours (lnk model facts).
An opinion is a key here: a trade reasonable people make differently,
such as how much of the window to pay for. Link's own opinions are a
spec of their own, printed whole by lnk agent spec, so someone with the
opposite ones changes only the file.
Specs meet as sets
An agent runs three specs as one: Link's, then each of its teams', then
its own. Each is a set of keys, and any of them may be partial. The agent
runs their union, with a key from a later set winning, key by key. A
table of yours replaces only the keys it has. lnk agent spec prints the
union, each key marked yours, a team's or Link's.
-
A general key beats the specific ones before it. Your
[model] defaultreplaces Link's[model.anthropic] defaulttoo, your[channels] workingLink's[channel.slack] working, and[tools] answera[tool.<name>] answerset before. A specific key in the same set as the general one still wins for its provider, channel or tool. An empty reaction (working = "") is none. -
The union must be whole. Every key Link's spec has must be in it, but for those with a fallback:
[window] max, the model names and the reactions. Withextends = "none", Link's set is empty, so yours must have them all. A key no set holds refuses the start and names it. -
Teams agree, or you settle it. Two teams setting one key differently refuse the start, unless the agent's own spec sets it.
-
What isn't an opinion isn't yours to claim.
name,ownerandteamsare what Link knows of the agent here: a spec naming another agent, another owner, or a team whose file doesn't list it is refused, and the owner is always Link's, never a file's. -
Counts are yours.
rounds,callsand the scheduler's caps take any number from 1 to 65,535. Past what Link is tested with (64 rounds, 128 calls, 100,000 a day, 64 subagents or hops) the start is allowed, andlnk agent startsays so.
Link's spec is link_plugin's agent_spec/link.toml.
Sections
| Section | Key | What it takes | Link's |
|---|---|---|---|
| top | name | Lowercase letters, digits and dashes; the agent's own name, no other | |
| top | extends | "link" (as when left out) or "none": Link's set is empty | |
| top | owner, teams | Printed: whose it is, and the teams that list it | |
harness | name | Lowercase letters, digits and dashes | |
model | default | The model when it names none | Per provider, below |
model | allow | The only models it may run; left out, any | |
model | answer | The longest answer, in tokens, as you set it, at least 256 | window.answer's share |
model | thinking | off, low, medium or high, where the model thinks | low |
model | thinking_budget | Tokens, at least 1024, for a model that thinks within a budget | 2048 |
model.<provider> | the keys above | For anthropic, openai, openrouter, ollama or local, replacing only the keys it has; a provider's table holds no tables of its own | Each hosted provider's default |
window | max | The window it's sent, in tokens, at least 1024. Yours or a team's is used as set, and Link's is held to the model's window. Left out, the model's whole window | 128000 |
window | unknown | The window of a model Link doesn't know, in tokens, at least 1024 | 32768 |
window | margin | The share kept back, 0 to under 0.5: tokens are counted roughly before a turn, exactly by the provider after | 0.125 |
window | answer | The share kept for the answer, over 0 and under 1, and with margin under 1, at most the model's longest, where [model] answer doesn't say | 0.25 |
turns | rounds | The most rounds of calls in one turn, from 1 | 8 |
turns | calls | The most calls one answer makes, from 1 | 16 |
turns | busy | What a message sent while it answers does: queue (answered after), steer (joins the answer) or interrupt (ends it) | steer |
turns | renew | How often a running turn renews its lease, at least "1s" | "10s" |
turns | lapse | How long a lease not renewed lasts before its turn is taken for crashed and finished; more than twice renew | "60s" |
instructions | base | The prompt's first section, what any agent of yours is: link (Link's, a personal agent who answers briefly), none, or a file in its files (instructions/base.md) | "link" |
instructions | environment | Its fourth: where its files are, which folders it may change, its channels, as Link resolved them at its start; link, none or a file | "link" |
instructions | standing | Its sixth: your standing instructions, a file in its files, read at each message, or none | "instructions/instruction.md" |
instructions | now | The date, the time here, the channel and who's speaking, at the head of each message, never in the system prompt: link or none | "link" |
instructions.max | base, environment, standing, now | Each section's most, 64 B to 64 KB: what's past it is cut | 16 KB, 4 KB, 32 KB, 512 B |
tools | answer | What's kept of a tool's answer, 1 KB to 16 MB | "32 KB" |
tool.<name> | answer | The same, for one tool | |
tools | timeout | How long a tool's call may take, waiting for you to allow it included, at least 1 second; past it the model is told it didn't answer | "5m" |
tool.<name> | timeout | The same, for one tool | |
tool.memory | keep | How long its session stays once no turn uses it | "10m" |
tool.memory | max_age | How long its session lives at most | "1h" |
tool.memory | tokens | The most a search or a read gives, at least 100 | 6000 |
tool.memory | model | The embedding model it ranks by; none, by words only, no model started | qwen3-embedding |
tool.scheduler | per_day | Messages its schedules and subagents send it a day, the day in its zone, from 1. It bounds how many messages agents send each other | 100 |
tool.scheduler | subagents | Subagents at work at once, from 1 | 5 |
tool.scheduler | hops | How deep a chain goes, from 1, each message sent by an agent on its own. It is a depth and not a count, as a recurring job's firings start again | 8 |
tool.scheduler | late | A firing missed asleep: run once on waking, or skip (a recurring one fires at its next time) | run |
tool.scheduler | ask | Whether task, schedule and cancel ask you first | false |
tool.scheduler | zone | Its times' time zone: local (this machine's) or a zone's name (Europe/Paris) | local |
tool.scheduler | firing | How long it waits on a firing, a subagent's whole work, before giving up on it, 1s to 1d | "30m" |
tool.scheduler | enabled | Whether the agent has a scheduler at all; false: no subagents or schedules, and nothing runs them | true |
channels | stream | The answer shown as it's written; false, once done | true |
channels | pace | How often it's shown again, over 0. Never more often than the platform documents a bot may write: Telegram once a second in a chat and every 3 seconds in a group, Slack every 1.2 seconds, and Discord its global 50 requests a second and its other rate limits as it answers them | "1s" |
channels, channel.<name> | groups | In a group, which of your messages it answers: mention (those that mention it or answer it), or never | mention |
channels, channel.<name> | working, steered | A reaction while it answers, and on a message that joined an answer, where the channel has them (Slack). Empty ("") is none | None |
channel.<name> | the keys above | For one channel. Of Link's channels only Slack marks a message, so working and steered are refused for Telegram, Discord, WhatsApp and Signal | Slack: working = "eyes" |
Link's default models: claude-sonnet-5-5 on Anthropic, gpt-6-luna
on OpenAI and openrouter/auto on OpenRouter, the same in every
harness.
[instructions] holds only for a harness whose contract says it
takes them, which Link Harness does. A start refuses it for any other,
rather than ignore it. Its sections come in the order of how often each
changes, least first, so the provider's cache holds; sections 2
(model), 3 (tools), 5 (skills) and 7 (notes) have their places and no
keys yet. lnk agent instructions prints the prompt as the model gets
it. The rest is handed to every harness
(Settings.spec), which takes what it can.
Values
- Sizes: a number and
B,KB,MBorGB, powers of 1024. - Durations: a number and
ms,s,m,hord. - Thinking: on Anthropic's newer models, the effort the model
thinks at, and
offthe least it can, as some always think. On a model that takes a budget,thinking_budgettokens, under the answer's. On OpenAI's reasoning models, the reasoning effort. A model Link doesn't know isn't asked to think. tool.memory's keys are memory's only, andtool.scheduler's the scheduler's. Another tool's table with one is refused.- An unknown key or table, an empty table, or a value out of range is refused, naming the line.
Where it's kept
- Yours:
lnk agent start --spec <file>reads it strictly, checks the union it makes, and keeps it as~/.config/lnk/agents/<agent>/agent.toml, never in the files folder, where its harness could write it. Every start reads it again; a move carries it. A spec setting[harness]switches the agent to it. - Never a grant. What the agent may reach is its sandbox file
beside it,
sandbox.toml(lnk agent start --sandbox,lnk agent allow): a spec holds opinions only, and repeats nothing of it (Choose what your agent can reach). - A team's:
~/.config/lnk/teams/<team>.toml, named after the team: itsidandname, its members by ID ([member.<name>], with anidand arole), a[files] sharedfolder, and any of the opinion sections above, which its members share. Its file holds nonameof an agent's,owner,teams,[harness]orextends.
id = "t4m9q2x7k1ab"
name = "research"
[member.scout]
id = "a1b2c3d4e5f6"
role = "finds sources"
[member.writer]
id = "g7h8j9k0m1n2"
role = "drafts the report"
[turns]
rounds = 12Why it works this way: decisions.md. Every spec Link reads, side by side: Specs.
