Limits¶
This page lists the limits Agent Kourier places that an operator, a Binding author or an agent backend can run into. Each row gives the value, where it applies, what happens past it, and whether a setting changes it. The reasons and the how-tos are on the pages the rows link to.
Where a value is a constant of the code, or a default the code fills in, a test compares it with this page in every test run. Some values are rules of Kubernetes or of a chat platform that the code does not hold as a constant, and a few are shapes of the code, such as the message fields a trigger reads; the tests read what the code does for those. The figures that were measured, and not read from the code, say so.
The page lists the limits you can run into. It leaves out numbers that change nothing an operator, a Binding author or an agent can see: the intervals and batch sizes of the background loops, the retry pacing of the press worker and the outbox sweep, and Slack layout numbers that a lower cut earlier in the broker hides.
What "past it" means.
- Refused at load: the broker's own validation rejects the object when it reads it. On the files route one such object fails the whole config: at start the broker exits with code 2, and on a reload it keeps the last good config. On the custom resource route a new object is not served, and an object that was good before keeps being served as its last good generation. See What a bad object does.
- Refused by the API server: the CRD schema rejects the create or update, so the object never reaches the broker. This is the custom resource route only. A schema bound that the code also holds is refused at load too, because the broker checks every object it reads. The schema counts characters and the broker counts bytes, so a value of multibyte characters can pass the API server and still be refused at load.
- Cut: shortened. Where the text is shown to people, the end is marked
… (truncated). - Ends the turn, dropped, queued: the turn stops, the item is thrown away, or it waits. The row says which, and what the thread is told.
Run the broker¶
| Limit | Value | Past it | Setting |
|---|---|---|---|
Kubernetes version, custom resource route (config.source: crds) |
1.33 or later |
helm install and helm upgrade fail while the chart renders, with config.source crds needs Kubernetes 1.33 or later. The ConfigMap route has no minimum. |
None. With helm template, pass the cluster's version with --kube-version. |
| Replicas that do the work | One | With SQLite, replicaCount above 1 fails while the chart renders. With Postgres the replicas elect a leader through a Lease: one works and the others stand by. See Run two replicas. |
store.type, replicaCount |
| ChatConnections one broker serves | One | With files, a directory that holds none or several stops the start (exit code 2). With custom resources the broker serves only the one AGENTKOURIER_CHAT_CONNECTION names and ignores the others. |
config.chatConnection, custom resources only |
| Bindings on one channel of a ChatConnection | One | With files, refused at load. With custom resources the older Binding holds the channel, and the other is not served and says ChannelConflict. See Who holds a channel. |
None |
| One config file, files route | 1 MiB for each *.yaml or *.yml file of AGENTKOURIER_CONFIG_DIR |
Refused at load, so the whole config is. | None |
| How often a change is read | Every 10s, files and custom resources |
The change takes effect at the next read. | None |
| Delay of a deleted custom resource | About 10 to 20 seconds after the delete: it takes effect at the first read that finds nothing newly deleted, and no sooner than 8s after the last read that did. Other deletions can defer it, but a deletion first seen 1m ago takes effect at the next read whatever else is deleted. The exception is the broker's own ChatConnection: when it is deleted, it is held at its last good generation until it is back or the broker restarts, and a Binding deleted while it is held stays held too. |
Until then the object is served as it was. When a deletion takes effect, the object is kept, not served, for 1m more, so that a ChatConnection deleted just after it takes the object into the hold. See What a bad object does. |
None |
| Leader election, Postgres only | Lease duration 15s, renew deadline 10s, retry period 2s |
A standby takes the Lease a lease duration after the last renewal it saw. A leader that cannot renew within the renew deadline exits at once with code 5. Timings that do not satisfy lease duration > renew deadline > 1.2 × retry period stop the start (exit code 2). See High availability. |
leaderElection.leaseDuration, renewDeadline and retryPeriod (AGENTKOURIER_LEASE_*) |
| Wait for the Secret caches at start | 30s |
The start fails (exit code 4). | syncTimeout (AGENTKOURIER_SYNC_TIMEOUT) |
| Time to stop | The broker's ordered shutdown takes up to 25s. The chart gives the pod 40 seconds. |
The broker stops waiting at its total. Kubernetes ends the pod when the grace period does. | terminationGracePeriodSeconds; the shutdown total has none |
| Memory of the container | The chart's default limit is 256Mi, and it sets GOMEMLIMIT to four fifths of the limit. |
A container that passes the limit is ended by the kernel, and Kubernetes restarts it. Nothing in the broker bounds the total: see what has no limit. The events the Slack adapter holds (the row on events, in Start investigations) are bounded in number and not in bytes: the broker sets no limit on the size of an event it reads from the Socket Mode connection (the Slack client library has an option for one, off by default, which the adapter does not set), and Slack's largest event was not verified here. | resources.limits.memory; see the release that sets GOMEMLIMIT |
| SQLite volume, SQLite only | 1Gi by default. The database holds sessions, the outbox, the audit log and the votes. The audit log is kept for ever unless auditRetention is set, and nothing deletes a vote, so a default install grows toward the size. |
The volume fills, and a write to the database fails with the SQLite error database or disk is full. In a test at the store, with SQLite's max_page_count standing in for a full volume, appends to the audit log failed and kept failing, reads kept working, and a plain delete of audit rows was refused (the log is append-only), while the retention purge still ran, freed room, and appends worked again. A volume that is truly full, where the write-ahead log cannot grow either, was not tested, and what the broker does with each failed write was not traced. |
persistence.size, persistence.existingClaim (a claim you size), auditRetention |
| All the files of the files route, in one ConfigMap | The chart writes every resource of config.* as a file of one ConfigMap, and Kubernetes limits a ConfigMap to 1 MiB in all (a Kubernetes rule, not checked here against Kubernetes). |
The API server refuses the ConfigMap, so the install or upgrade fails (inferred from that rule, not run). | The custom resource route (config.source: crds), which has no such total. config.existingConfigMap names the one ConfigMap that is mounted, so the same total applies to it. |
| Liveness probe | The kubelet asks /healthz every 10 seconds and restarts the container after 3 misses in a row: about 30 seconds |
The container is restarted. | livenessProbe.periodSeconds, livenessProbe.failureThreshold |
| client-go log verbosity | Held to 7 |
The chart refuses a higher klogVerbosity, and the binary holds a higher AGENTKOURIER_KLOG_V to this value. client-go logs response bodies, Secret lists included, from 8 up. |
klogVerbosity (AGENTKOURIER_KLOG_V), up to this value |
| The broker's requests to the API server | Secret informers 50 a second with a burst of 100, Binding status writes 20 with 40, Lease renewals 5 with 10 |
A request waits for its client's limiter before it is sent. Each client has a limiter of its own, so a burst of status writes does not hold up a Lease renewal. | None |
| A Secret the API server answers forbidden or unauthorized (custom resources and files) | The broker asks for it again only at a recheck of the config, which comes every 1m (looked for at each read of 10s, so up to about 1 m 10 s after the last), and no sooner than 1m after it last asked. It asks for 10 denied Secrets at once and then for one every 100ms, 10 a second. A try is two requests. |
The informer of the Secret ends at the first answer, and the cache it held, if it had synced, is served as it was. The denial is logged once, and again only when what the answer says changes. One answer of forbidden or unauthorized that every Secret gets at once, such as an expired or rotated token of the broker or an outage of the authorizer, ends every Secret informer, the broker's own included. Cached values are served, and turns and readiness are unaffected, but a rotation is not seen until a try reaches that Secret: about 1 m 10 s, plus 0.1 s for each denied Secret beyond the first 10. With custom resources the Binding reports CredentialsUnreadable until a try succeeds, and with files a reload that names the Secret fails: a Role applied later is read at the first recheck after it, plus one tenth of a second for each denied Secret ahead of it in the queue beyond the first 10. A reload that comes sooner, because a config changed, asks for none. A Secret whose keys the config reads change is asked for at once. |
None |
| How often a Binding's status is written, custom resource route only | The leader writes a Binding's status at most once each 10s (counted from the end of its last write) and looks for work each 1s. A failed write is tried again after one gap, then after twice the one before, up to 5m. |
A write waits for its time, and a failed one is tried again for as long as it fails, with the backoff cap as the longest wait; it is not dropped, and one WARN says it failed until a write succeeds. A status that something else keeps changing is put back at its next write, and when it is put back 3 times in a row, each within 5 gaps of the one before, one WARN says so, until the run is broken; the writes go on. |
None. See Binding status. |
| Postgres connections, Postgres only | 10 for each replica. A statement may run 30s, and a transaction may sit idle 10s. |
A caller waits for a free connection. The server cancels a statement that runs longer, and, for the idle timeout, ends the session (Postgres's documented behaviour of idle_in_transaction_session_timeout, not run here). |
A DSN that sets statement_timeout or idle_in_transaction_session_timeout wins over the broker's value. store.postgres.timeouts: role leaves both to the database role. The connection count has no setting: a pool_max_conns in the DSN does not change it. |
Write a Binding, an AgentBackend or a ChatConnection¶
These caps hold the objects to a size, so that a stored object, and the memory the broker keeps for each of them, stay bounded. The upgrade page has the errors each prints. Counts are of entries. A length in bytes is of bytes.
| Limit | Value | Past it | Setting |
|---|---|---|---|
Triggers in a Binding (spec.chat.triggers) |
50 |
Refused at load, and by the API server. | None |
extract entries in a botMessage trigger. The same number bounds the entries of thread.closeWhen, whose keys must be extract names. |
32 |
Refused at load, and by the API server. | None |
match or exclude entries in a trigger: one for each message field |
6 |
A key that is not a message field is refused at load, and by the API server. | None |
A regular expression of match, exclude or extract |
512 bytes |
Refused at load, and by the API server. | None |
The program of one regular expression of match, exclude or extract |
2000 instructions: the length of the program Go compiles the expression to (regexp/syntax, after Simplify, counting the failure and match instructions). Measured with Go 1.27: the regular expressions of this repository's example Bindings are 10 to 98, .{0,900} is 1802 and .{0,1000} is 2002. |
Refused at load, with the field, the count and the limit. The API server cannot count instructions, so on the custom resource route it creates the object and the Binding shows Accepted as False with the reason InvalidSpec. The check ends at the first regular expression it refuses, so the message names one. The upgrade page has the errors. |
None |
| The programs of all the regular expressions of a Binding, over all its triggers | 5000 instructions |
Refused at load, as the row above says, at the regular expression that takes the sum past it. The trigger cap is 50, but the budget is what a Binding with many triggers meets first: the trigger of Investigate Alertmanager alerts compiles to 346 instructions (370 with the exclude of the heartbeat, as in the chart's CI values and the test fixture), so 14 of it fit and the 15th is refused (13 and 14 with the exclude). About half of that is the bounded repeat {1,32} of the optional bracket tag, 67 instructions each of the three times it appears, where [^\]]* is 6. |
None |
promptTemplate of a botMessage trigger |
8192 bytes |
Refused at load without being parsed, and by the API server. | None |
spec.agent.name of a Binding |
253 bytes |
Refused at load, and by the API server. | None |
spec.identity.userId of a Binding |
256 bytes |
Refused at load, and by the API server. | None |
Headers of an AgentBackend (spec.headers) |
32 |
Refused at load, and by the API server. | None |
| The name of a header | 256 bytes |
Refused at load, and by the API server. | None |
chat.mentions of a Binding |
50 |
Refused at load, and by the API server. | None |
chat.maxMentionsPerTurn |
From 1 to 10, default 2 |
A value out of range, 0 included, is refused at load, and by the API server. The mentions past the cap in one turn are not mentioned. | The field itself |
interactions.approverGroups |
10 groups |
Refused at load, and by the API server. | None |
allowedNamespaces of an AgentBackend or a ChatConnection |
256 namespaces |
Refused at load, and by the API server. | None |
The url of an AgentBackend |
2048 bytes. A ? or a # anywhere in it is refused, so a URL has no query and no fragment, even an empty one. |
Refused at load, and by the API server. | None |
The value of a header |
4096 bytes |
Refused at load, and by the API server. | None |
chat.channel, a trigger's from.id, and each entry of interactions.approverGroups |
32 bytes each |
Refused at load, and by the API server. | None |
The user of a chat.mentions entry |
21 characters |
Refused at load, and by the API server. | None |
chat.acknowledgment.emoji |
100 bytes |
Refused at load, and by the API server. | None |
The name of an extract entry. The keys of thread.closeWhen are extract names, so the same number bounds them. |
63 bytes |
Refused at load, and by the API server. | None |
A value of thread.closeWhen |
256 bytes |
Refused at load, and by the API server. | None |
The text of a duration (interactions.timeout, session.threadTTL, thread.cooldown, thread.maxIdle, rateLimit.per) |
64 characters, whatever the value that it writes |
Refused at load, and by the API server. The broker reads the text before it becomes a duration, so the file route refuses it too. | None |
thread.cooldown of a trigger |
From 1m to 168h |
Refused at load, and by the API server. | The field itself |
thread.maxIdle of a trigger |
At least twice the cooldown and at most 720h. Unset, it is the larger of 24h and twice the cooldown. |
Refused at load, and by the API server. | The field itself |
rateLimit.per of a trigger |
From 10s to 24h |
Refused at load, and by the API server. | The field itself |
interactions.timeout of a Binding |
At most 7d (168 hours) |
Refused at load, and by the API server. | None |
session.threadTTL of a Binding |
At most 365d (8760 hours) |
Refused at load, and by the API server. | None |
rateLimit.maxRuns, rateLimit.maxConcurrent and maxRunsPerDay |
At least 1 for the first two, and 0 or more for the last (0 is no cap). No upper bound. | Refused at load, and by the API server. | The fields themselves |
| Names of Bindings, AgentBackends, ChatConnections and Secrets (Kubernetes names) | 253 characters |
Refused at load, and by the API server. | None |
Namespaces, and the names of chat.mentions |
63 characters for a namespace, 63 for a mention name |
Refused at load, and by the API server. | None |
Some checks are not in the CRD schema. A prompt template's rules and a regular expression's syntax are not something the schema can express, so on the custom resource route the API server accepts them and the broker refuses the object at load. The size of a config file is a limit of the files route only. Tune triggers with a dry run loads files the way the broker does, so it finds what is refused at load before you apply a config.
Write a prompt template¶
A botMessage trigger's promptTemplate is text that runs over values taken out of a message. These limits keep what it can do small.
The release that limits prompt templates has the errors.
| Limit | Value | Past it | Setting |
|---|---|---|---|
| What a template may use | No range, define, template or block, and no variable. It calls only and, eq, ge, gt, index, le, len, lt, ne, not, or and slice. It reads values only as .Channel, .State, .Key, .Link, .Source, .Fields.name or index .Fields "name". |
Refused at load, and only there: the API server cannot read a template. | None |
| What a template writes | 256 KiB |
At load the Binding is refused. When a trigger renders it, no turn starts, a note is posted in the alert's thread, the start is audited, and agentkourier_trigger_refused_total counts it with the reason render_error. With the other bounds in force a template cannot reach it: one of the longest allowed length that does nothing but read .Link writes at most 910 × 256 bytes, about 227 KiB (computed from the bounds on this page, not run). It is a backstop. |
None |
| The values a load check runs the template over | 64 bytes each |
A template that slices a value past that length fails the load. One that slices past the length of a real value, but within that length, passes the load and fails when the first message renders: a render error, so no turn starts. | None |
A value the template reads: .Fields entries, .State, .Channel and .Source |
128 bytes, of ASCII letters, digits and _ . - : |
The render is refused: no turn, a note in the alert's thread, an audit entry, and agentkourier_trigger_refused_total with the reason too_long or characters. |
None |
.Key |
128 bytes, with / also allowed |
The same. | None |
.Link |
256 bytes of an http or https URL |
The same, with the reason too_long, characters or not_link. |
None |
| The instruction part of the first prompt (the template's output) | 4000 UTF-16 units |
Cut. | None |
| One field of the message in the untrusted block | 3000 UTF-16 units |
Cut. | None |
| The untrusted block as a whole | 8000 UTF-16 units |
Fields that do not fit are left out, and a last line says so. The whole prompt is at most 12002 units. |
None |
Build or connect an agent backend¶
What the broker reads and holds¶
The backend of a Binding may be a tenant's, so the broker bounds each thing it reads. A bound is on one response or one event, never on a stream's length. The values are constants of the binary: no chart value, file or custom resource field reaches them. The release that lowers the bounds has what each means for an agent.
| Limit | Value | Past it | Setting |
|---|---|---|---|
| The agent card | 1 MiB |
The card is not read, and no connection is made. A probe sets the Binding's BackendReachable condition to False with the reason InvalidAgentCard. The card is not an answer to a call, so what it does differs from the rows below: a send or a lookup that meets it is retried like any call that cannot reach the agent, and a send is given up after 15m as an agent that cannot be reached; an answer to a paused task reopens the pause and tells the thread that the answer could not be delivered; a follow of a task, or the read of the task that comes before it, tells the thread, in a fixed sentence, that Agent Kourier could not connect because the agent's card is too large, and that once the card is fixed Agent Kourier must be restarted; it cancels nothing, and leaves the task recorded and running at the agent, followed by no one (the turn ends with the outcome refused); a cancel is not counted as done, and the record stays. Nothing follows that task again by itself: what releases it is a restart of Agent Kourier, which then reads the recorded task and follows it, and a restart interrupts every turn of every tenant on that broker while it runs. Read from the code and its tests. |
None |
| One event of a stream, the lines of it up to the blank line | 2 MiB |
The read fails at the bound, never as a shortened event. The turn ends with the outcome response_too_large: the thread keeps what it was shown and is told, in a fixed sentence, that the agent sent more than Agent Kourier can read or hold, and Agent Kourier cancels the task at the agent, once, within 30s. A reply that names no task has none to cancel: a send whose first event is too large, or a reply that is one message and no task. An answer to a paused task whose reply is too large before the agent names the task again ends the turn at once, with the same sentence, and Agent Kourier cancels the task that was answered, so that it does not stay paused for a person to answer into the same reply; the answer is not sent again. On kagent 1.0 the final event of a task repeats the task's tool calls and results, so all of them together, from the start of the task, count against this bound, not only the last one. See the notice and what it leaves. |
None |
| What one turn holds of the agent's reply: its artifacts' text, tool results that wait for their call, and the bookkeeping of each tool part | 341 KiB |
Ends the turn as an event does, however many events the reply came in. What an answer reads first of the task it answers, the tool calls and results of the turn that paused, is not counted: that task is bounded by the GetTask that reads it (the GetTask row for a pressed answer, below), so the answer's own stream can be read for a task that paused after many tool steps. That is the answer's own stream only. What the answer notes of that task is held in memory for the whole answer, and was measured in a test process (a task of the smallest distinct tool parts, 78 bytes each on the wire, 53,847 of them in 4 MiB): 4.2 MiB for parts in an artifact and 5.2 MiB for parts in the history, for each answer in flight, besides the memory to decode the task. A follow builds no such note: it is the read after a restart, after a lost stream past the reconnect limit, and after an answer that the agent refused as busy or whose outcome is unknown. Its first snapshot holds every tool call and result again, and in a test (one artifact of call and result pairs with 40-byte call IDs) it passed the budget at about 350 pairs, where a live stream of the same turn passed it at about 400. A task that long, paused and then followed after a restart, is stopped as too large and cancelled. |
None |
GetTask that reads a task back: after a lost stream, a restart or a follow; to read a person's typed reply to a paused task, on the a2a dialect; and after an answer that the agent refused as busy or not ready, or whose outcome is unknown (the answer carries the task with its whole history) |
4 MiB |
The answer is not read, and the turn ends with the outcome response_too_large, as for an event. For a typed reply the turn that paused ends that way and its task is cancelled, and the reply stays queued as a turn of its own. |
None |
GetTask before a person's answer to a paused task that is pressed (a button, not a typed reply), which reads the task to tell what in the reply is new |
4 MiB |
The read is dropped, and the answer is sent without it. | None |
CancelTask. A server of a2a-go answers a cancel, and a repeat of one that took, with the whole task. |
4 MiB |
The answer is counted as a cancel that took, without being verified, and its size does not end the turn: the task is not left recorded. The audit row says cancel: failed response_too_large, which means the answer was not read, and a WARN names the unverified cancel. That a real server answers this way is shown in a test against a2a-go's handler and is unconfirmed on kagent, and an error with a large body reads the same, so nothing is known of what became of the task. The same holds when the agent says a task cannot be cancelled because it ended, and the task is then too large to read. The task is no longer recorded, so a restart does not help: nothing of this thread follows it afterwards, and restarting would interrupt every turn of every tenant on that broker for nothing. A task that still runs at the agent can only be ended at the agent. |
None |
A page of ListTasks, in the lookup that runs before a turn is sent again (after a restart, a busy agent or an unknown outcome). Each task of a page carries its history. |
4 MiB. A page too large is asked for again at half the size. The first ask names no size, so the first retry is for half of 50 tasks (25), and each further retry halves again, down to 3. |
If a page does not fit at the least size, and an earlier page held the turn, the holders found so far are the answer. Otherwise the lookup is incomplete: Agent Kourier cannot tell whether the agent has the turn. It sends nothing, because a message sent again into a context is not deduplicated, with one exception read from runner.go: a turn that this process queued, and whose every attempt was refused as busy or not ready (-32004) or never left the broker, is sent, since the agent cannot have it. Any other failure before the agent named a task, a JSON-RPC error such as -32603 among them, counts as an unknown outcome, because the agent may have started the task and failed on the way back, so such a turn fails with the outcome lookup_incomplete and is not sent. Otherwise the thread is told that Agent Kourier could not tell whether the agent already started the message, so it did not send it, and that the agent may be running it, and the person posts it again to run it; the turn ends with the outcome lookup_incomplete; the audit log has find_task: failed lookup_incomplete, followed by the cause, such as response_too_large or deadline_exceeded; and the log has WARN session turn failed. Nothing is cancelled, because no task is named. A turn behind a task that is still running waits instead, and is looked up again when that task ends. See the notice and what it leaves. |
None |
| The whole walk of that lookup | At most 5m and 200 pages. A page token that comes again also ends it. |
The same as a page that does not fit at the least size: an earlier holder is the answer, and otherwise the lookup is incomplete, with the outcome lookup_incomplete. A call that is refused on the first page asked for at a size the walk lowered (a 4xx other than 408 and 429, or an error the agent answered with) is taken as incomplete too, since whether a server continues a page token at another size is not known (unconfirmed). A 5xx, a timeout or a lost connection there is not taken so, and is retried like any failed lookup. The caller's own cancellation is none of these. |
None: no chart value or field reaches the deadline |
These bounds give each response a size and the broker no total, and what a response costs in memory depends on the JSON it holds: a response of many small objects at the bound costs much more than the same number of bytes of text. The figures are from a test process, not from a pod: the Go test binary on a laptop, with the fake agent and the client in one process, and a pod's own baseline was not measured. With small objects at the bound, two or three large responses read at the same time passed the chart's default limit of 256Mi. So the number of streams or answers that fit is not a limit to plan by. An AgentBackend must be one you trust, or get a broker of its own, because the backend decides how many large responses are in flight. The measurement and its counts are in the release that lowers the bounds.
How long the broker waits on an agent¶
| Limit | Value | Past it | Setting |
|---|---|---|---|
| A stream with no event | 5m |
The stream is cut and reconciled with GetTask and then SubscribeToTask, and no text is delivered twice. A tool call that is silent for longer is cut the same way. |
None |
| Reconnects in a row that bring nothing new | 5 |
The send is treated as a lost stream, and the turn follows the task again after a backoff. | None |
| The backoff before a resend or a follow | Starts at 1s, doubles up to 60s |
It waits. A turn that is blocked is retried after the backoff and is never dropped, except as the next rows say. | None |
| A call to start answering (response headers) | 30s |
The call fails. This bounds a call's start, not a stream's length. | None |
GetTask and CancelTask |
30s each |
The call fails. | None |
| Fetching the agent card to connect | 30s |
The connect fails. | None |
| An agent that cannot be reached | Retried for 15m |
The turn ends with the outcome unreachable, and the thread is told that the agent could not be reached. The second failure in a row also tells the thread that the broker is retrying. |
None |
An agent that answers busy or not ready (-32004) |
Retried with no limit | The turn waits behind it. The thread is told after 2m. |
None |
| A task still working after its stream was lost, with no sign of life | 1h |
The task is presumed wedged and cancelled, and the turn ends with the outcome wedged. A live stream is never limited by this. |
None |
| The cancel of a task whose reply was too large | 30s |
A cancel that fails is logged and leaves the task recorded, for a restart to reconcile: nothing in the thread can cancel it, and a restart interrupts every turn of every tenant on that broker while it runs. | None |
What the broker sends¶
| Limit | Value | Past it | Setting |
|---|---|---|---|
| The name and the email of the person who wrote a turn, as sent to the agent | 64 characters each |
Both are cleaned (control and format characters and brackets are dropped) and then cut, in the text prefix and in name and email of metadata["agentkourier.dev/actor"]. The ID in the metadata is as the chat platform gave it. See the agent contract. |
None |
What a pause may ask¶
A pause that the dialect cannot show, or that does not fit one message, is refused as unusable: the thread is told in a fixed sentence, the task is cancelled, and the refusal is audited. The agent contract lists the rest of the rules.
| Limit | Value | Past it | Setting |
|---|---|---|---|
| Questions in one pause | 20 |
Unusable. | None |
| Choices of one question | 100 |
Unusable. | None |
| Tools in one approval | 20 |
Unusable. | None |
| The name of one tool in an approval | 300 bytes, as the audit row writes the name: 300 ASCII characters, and fewer of any others |
Unusable. The audit row of the decision writes every tool's name whole, so a longer name is refused. | None |
| The arguments of one tool in an approval prompt | 3000 UTF-16 units |
Unusable, because the approver would not see what runs. | None |
| All of an approval prompt's argument text | 3600 UTF-16 units |
Unusable. | None |
| The Block Kit JSON of an approval prompt | 16000 UTF-16 units |
Unusable. | None |
| How long a pause waits for an answer | 30m by default |
A question's task is cancelled and the thread told. An approval is denied for every tool, with the reason that no one decided in time, and the prompt shows a timed-out ending. | interactions.timeout of the Binding |
Start investigations: triggers, rates and probes¶
| Limit | Value | Past it | Setting |
|---|---|---|---|
Starts of a botMessage trigger |
The rateLimit (maxRuns in any per, and maxConcurrent at once) and maxRunsPerDay, all required |
The start is not made: no turn, nothing in the alert's thread. It is counted in agentkourier_trigger_limited_total and in one digest line for the window. Only a start is limited: a repeat, a close and a note never are. See Limits and the digest line. |
limits of the trigger |
Starts of a mention trigger (a person's mention) |
No limit | Nothing stops a person's mention from starting a turn. | None: a mention trigger takes no limits field |
| A bot's mention of Agent Kourier, across every bot on a Binding | 3 in any 30m, 1 at a time, and 20 in any 24h |
The start is not made: no turn and nothing in the thread. It is counted, audited and in the window's digest line. | chat.botMentions: none turns bot mentions off. The numbers have no setting. |
| How much of a message field a trigger's regular expressions read | The first 16 KiB of each of the fields text, title, link, fallback, footer and body, cut at a character boundary (a character that the cut would split is left out whole). match, exclude and extract all read the same head. |
The rest of the field is not read: a regular expression finds nothing that is past it, and one that ends in $ ends at the end of the head. The message is otherwise unchanged. |
None |
| Slack events the adapter holds while the broker works through them | 16 for one channel (the one being handled and those waiting behind it), and 256 for all channels, past which a channel that already has an event held is refused another. A channel that holds none is never refused its first event, but once the total is full a channel takes one event at a time, so a press during its message, or the second alert of a burst, can be shed. The total is exceeded by one event, and one goroutine, for each other conversation (a channel or a direct message) Agent Kourier is in that has an event waiting: the lane of an event is the conversation Slack names in it, which a poster cannot choose, and events that decode to nothing share one lane (read from the code; what Slack guarantees about those identifiers was not verified here). The events of one channel are handled one at a time and in order, and different channels at once. When the broker is stopping, the events still waiting are not handled. |
The event is not queued: it is left unacknowledged, as when the broker fails to take an event, so Slack may send it again. Slack's retries are finite (how many, and how late, was not verified here), so a shed event can be lost. One that comes back after a later event of the same channel was taken is handled after it, so the order of events within a thread can invert (a close, then a fire): order within a thread holds only while the bounds do. It is counted in agentkourier_slack_events_shed_total by reason, and logged at WARN: the first at once, and then, when another is shed 10s or more after the last line, one line with the count shed since, over all channels (its channel is that of the event that made the line); a count still pending when the connection ends is logged then. A button press that is left this way may not be sent again (unverified), and the person sees it fail. |
None |
| Presses of buttons waiting to be processed | 1000 in all, 10 for one person |
A press past either is dropped. It is logged at WARN and acknowledged to the chat platform, and the person is told nothing. | None |
| A press that cannot be processed yet, which includes a vote on a reply whose message has no place recorded yet | Tried again until 1h after it was recorded |
The worker logs it at ERROR, gives up and deletes the press, and the person is told to press again. | None |
| The check that a person who pressed is a member of the channel and not a bot, and, for an approval with approver groups, that they are in one | 10s |
The check fails, and the press is kept for another try, until it is given up as the row above says. | None |
| Probes of a Binding | Every 5m, each pass shifted by up to 10% so that brokers do not probe together, and 4 at a time. Each Binding has two: the agent card and the channel. |
Each adapter bounds its own call a little earlier. A probe that takes more than 10s is Unknown with the reason ProbeTimeout. A change to the config calls for a pass no sooner than 30s after the last pass began. |
None |
| People one turn mentions | At most chat.maxMentionsPerTurn |
The people past the cap are not mentioned. | chat.maxMentionsPerTurn |
What a thread shows¶
The chat side cuts text to what a message holds. Units are UTF-16 units, as the chat platform counts them.
| Limit | Value | Past it | Setting |
|---|---|---|---|
| The agent's reply in one message | 12000 units (Slack's markdown block) |
Cut. The cut is marked. | None |
| Steps a live stream shows | 50 |
The calls after these are made and not shown. | None. chat.progress and chat.output choose what is shown, not how many steps. |
| Steps a message shows when it is updated | The latest 20, under a line that counts the earlier ones |
The earlier ones are not listed. | None. chat.progress and chat.output choose what is shown, not how many steps. |
| A tool's name and its arguments in a status line | 64 units and 160 units |
Cut. | None |
| A step card's title or details (Slack) | 256 units |
Cut. | None |
| Blocks in one Slack message | 50 |
A prompt past it is refused as unusable. A reply's step cards take the room that the other blocks leave, and a line says how many steps are not shown. | None |
| Choices as radio buttons or checkboxes (Slack) | 10 |
A question with more gets a select menu, up to 100 choices. |
None |
| One choice's text, and a button's (Slack) | 75 units |
A question with a longer choice, or more choices than a select holds, is shown as a numbered list that the person answers by number. Each line is cut at 200 units. A button's label is cut. |
None |
| A question's text (Slack) | 2000 units |
Cut. | None |
| A prompt's heading (Slack) | 150 units |
Cut. | None |
Replies carried as thread context to a turn on a Binding with chat.threadReplies: mention, and to a bot's mention |
The newest 50 replies, each cut at 1000 units, an author's name and ID at 100 each, and the whole block at 6000 |
Replies that do not fit are left out, the oldest first, and the block's last line counts them. | None |
| The window of a thread the broker reads for that context | 1000 messages |
A longer window cannot be read: the turn carries the fixed line that Agent Kourier could not read the earlier replies (a bot's mention keeps the message that opened the thread). | None |
| The detail of a notice in the thread, which says why a turn failed. It is put on one line first. | 200 units |
Cut: a detail longer than this shows only its start. | None |
| A condition's message in a Binding's status | 1024 bytes |
Cut, and marked, after secrets are taken out. | None |
What the broker keeps, and logs¶
The durations here are the ones people notice. Stored data has what each table holds and how a sweep runs.
| Limit | Value | Past it | Setting |
|---|---|---|---|
| How long a thread keeps its session after its last activity | 90d by default |
The session is expired. A reply that the Binding takes gets the notice that the conversation has expired. An unmentioned reply with chat.threadReplies: mention gets none, and neither does a bot's mention. |
session.threadTTL of the Binding |
| How long an expired session is kept | 90d after its threadTTL has run out |
The session is deleted with every row that belongs to it, except its votes, which are kept for good. | None |
| A reply that was never delivered | 30d |
Dropped, with its text. | None |
| A post that was never posted | 7d |
Abandoned with the reason expired, and its text dropped. A failed post is retried from 5s doubling up to 5m, and each call to the chat platform is bounded at 30s. |
None |
| The request and answer of a settled interaction | 25h after it was decided, and 7d for an answer that was never marked delivered |
The text is emptied, and the row stays. | None |
| The record of a trigger's starts, which its limits count | 24h |
The record is purged. | None |
| Audit entries | Kept for ever by default, so on SQLite a default install grows toward the volume. A retention must be at least 24h. |
An entry past the retention is purged. A retention under that stops the start (exit code 2). | auditRetention (AGENTKOURIER_AUDIT_RETENTION) |
Votes on investigations, in agentkourier_investigation_feedback_total |
100 alert names for each Binding |
The votes on any other name are counted under the name other. The votes themselves are kept for good. |
None |
| An error's text in a log line | 300 bytes |
Cut, and marked. The text that is cut is not logged anywhere. | None |
| The reason of an unusable pause in the audit log | 120 bytes |
Cut. | None |
What has no limit¶
The broker does not bound these, and the code confirms it. Where the number can hurt, the row says what to do.
| What | What to do |
|---|---|
Bindings, AgentBackends and ChatConnections in a cluster or a namespace, and the entries of allowedNamespaces |
A cap of the schema or of Validate bounds one object, never how many there are. Put a ResourceQuota in each tenant's namespace with count/bindings.agentkourier.dev and count/agentbackends.agentkourier.dev. The API server then refuses a create past it with exceeded quota (measured on kind 1.37.0, as Security says; not run again for this page). Add count/secrets for the Secrets a Binding's token or an AgentBackend's header can name (Kubernetes's own object-count quota, not run here): it bounds how many Secrets exist, and not how many names the objects give the broker to follow, since it follows each name with an informer of its own whether or not the Secret exists. A quota limits the count and not the size. What the broker holds of the largest object that Validate accepts was measured in a test process, not a pod. The informer's cache holds about 0.17 MiB for an AgentBackend, 0.03 MiB for a ChatConnection and 1.75 MiB for a Binding, with the labels, annotations, owner references, finalizers and managed fields of an object not held at all. On top of that the broker keeps the object it decoded: 1.44 MiB for a Binding, 0.16 MiB for an AgentBackend, and the Snapshot serves the Binding's, so a Binding costs about 3.2 MiB, and 3.8 MiB when it is served (the trigger table is 0.63 MiB). Every broker owns, decodes and keeps the Bindings that name a ChatConnection that does not exist, and every AgentBackend of the cluster, whether or not a Binding names it, so these cost every broker whoever created them. A full status (16 conditions of 32768 bytes) adds 0.50 MiB to the informer's copy of a Binding, and the figures assume an empty one. The conditions and the rest of the figures are in Security. |
| Streams and calls open to agents at once | Each has a size bound, but nothing bounds the number, so the bounds give the broker no total. Run only AgentBackends you trust, or give one its own broker, and size resources.limits.memory for the responses that can be in flight at once: see what the broker reads and holds. |
| The length of a stream, and the text a stream carries in all | A stream runs as long as its turn. The broker holds only up to the reply bound, and a stream that sends text that is thrown away can run as long as its context. |
| Threads, sessions and turns at once | Each thread runs one turn at a time and queues the rest. Nothing bounds how many threads run turns at once, or how many replies queue in a thread. |
| A person's mentions | A mention trigger has no limit. A rate limit applies to bots only. |
| Secrets that the objects of a namespace name, denied or not | Each name that a served object refers to has an informer of its own, and the first request of each (two, for a name that is denied) is not paced: the client's limiter of 50 a second spreads them, so a thousand denied names take about 40 s to be asked once, and a read of another tenant's Secret that makes its own request waits behind them (inferred from how the limiter queues, not measured). After that a denied name costs at most the paced tries above, about 20 requests a second in all however many there are, and a name that does not exist or is allowed costs one open watch. Put a count/secrets quota and the quotas on Bindings and AgentBackends in each tenant's namespace, which bound the names (see the security model). |
| The audit log and the votes | The audit log is kept for ever unless you set auditRetention. Nothing deletes a vote, and auditRetention does not reach them. |
The upper bound of rateLimit.maxRuns, maxConcurrent and maxRunsPerDay |
Only a lower bound is checked. |