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 only

A 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] default replaces Link's [model.anthropic] default too, your [channels] working Link's [channel.slack] working, and [tools] answer a [tool.<name>] answer set 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. With extends = "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, owner and teams are 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, calls and 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, and lnk agent start says so.

Link's spec is link_plugin's agent_spec/link.toml.

Sections

SectionKeyWhat it takesLink's
topnameLowercase letters, digits and dashes; the agent's own name, no other
topextends"link" (as when left out) or "none": Link's set is empty
topowner, teamsPrinted: whose it is, and the teams that list it
harnessnameLowercase letters, digits and dashes
modeldefaultThe model when it names nonePer provider, below
modelallowThe only models it may run; left out, any
modelanswerThe longest answer, in tokens, as you set it, at least 256window.answer's share
modelthinkingoff, low, medium or high, where the model thinkslow
modelthinking_budgetTokens, at least 1024, for a model that thinks within a budget2048
model.<provider>the keys aboveFor anthropic, openai, openrouter, ollama or local, replacing only the keys it has; a provider's table holds no tables of its ownEach hosted provider's default
windowmaxThe 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 window128000
windowunknownThe window of a model Link doesn't know, in tokens, at least 102432768
windowmarginThe share kept back, 0 to under 0.5: tokens are counted roughly before a turn, exactly by the provider after0.125
windowanswerThe 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 say0.25
turnsroundsThe most rounds of calls in one turn, from 18
turnscallsThe most calls one answer makes, from 116
turnsbusyWhat a message sent while it answers does: queue (answered after), steer (joins the answer) or interrupt (ends it)steer
turnsrenewHow often a running turn renews its lease, at least "1s""10s"
turnslapseHow long a lease not renewed lasts before its turn is taken for crashed and finished; more than twice renew"60s"
instructionsbaseThe 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"
instructionsenvironmentIts 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"
instructionsstandingIts sixth: your standing instructions, a file in its files, read at each message, or none"instructions/instruction.md"
instructionsnowThe 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.maxbase, environment, standing, nowEach section's most, 64 B to 64 KB: what's past it is cut16 KB, 4 KB, 32 KB, 512 B
toolsanswerWhat's kept of a tool's answer, 1 KB to 16 MB"32 KB"
tool.<name>answerThe same, for one tool
toolstimeoutHow 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>timeoutThe same, for one tool
tool.memorykeepHow long its session stays once no turn uses it"10m"
tool.memorymax_ageHow long its session lives at most"1h"
tool.memorytokensThe most a search or a read gives, at least 1006000
tool.memorymodelThe embedding model it ranks by; none, by words only, no model startedqwen3-embedding
tool.schedulerper_dayMessages its schedules and subagents send it a day, the day in its zone, from 1. It bounds how many messages agents send each other100
tool.schedulersubagentsSubagents at work at once, from 15
tool.schedulerhopsHow 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 again8
tool.schedulerlateA firing missed asleep: run once on waking, or skip (a recurring one fires at its next time)run
tool.scheduleraskWhether task, schedule and cancel ask you firstfalse
tool.schedulerzoneIts times' time zone: local (this machine's) or a zone's name (Europe/Paris)local
tool.schedulerfiringHow long it waits on a firing, a subagent's whole work, before giving up on it, 1s to 1d"30m"
tool.schedulerenabledWhether the agent has a scheduler at all; false: no subagents or schedules, and nothing runs themtrue
channelsstreamThe answer shown as it's written; false, once donetrue
channelspaceHow 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>groupsIn a group, which of your messages it answers: mention (those that mention it or answer it), or nevermention
channels, channel.<name>working, steeredA reaction while it answers, and on a message that joined an answer, where the channel has them (Slack). Empty ("") is noneNone
channel.<name>the keys aboveFor one channel. Of Link's channels only Slack marks a message, so working and steered are refused for Telegram, Discord, WhatsApp and SignalSlack: 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, MB or GB, powers of 1024.
  • Durations: a number and ms, s, m, h or d.
  • Thinking: on Anthropic's newer models, the effort the model thinks at, and off the least it can, as some always think. On a model that takes a budget, thinking_budget tokens, 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, and tool.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: its id and name, its members by ID ([member.<name>], with an id and a role), a [files] shared folder, and any of the opinion sections above, which its members share. Its file holds no name of an agent's, owner, teams, [harness] or extends.
id = "t4m9q2x7k1ab"
name = "research"

[member.scout]
id = "a1b2c3d4e5f6"
role = "finds sources"

[member.writer]
id = "g7h8j9k0m1n2"
role = "drafts the report"

[turns]
rounds = 12

Why it works this way: decisions.md. Every spec Link reads, side by side: Specs.