agentplane 0.39.0

Durable, replayable agent runtime — the journal is the plan of record
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
//! Why a plane could not be assembled.
//!
//! Every variant here is a wiring mistake with a fix and no recovery: the
//! embedder's code, or the declaration beside it, says two contradictory things
//! and the runtime cannot pick one. They are refused at
//! [`RuntimeBuilder::try_build`] rather than at dispatch, because the cost of
//! finding out at dispatch is a run that has already begun.
//!
//! # Why this is a `Result` and also a panic
//!
//! [`build`] keeps panicking, and that is deliberate. For the ordinary case —
//! a binary wiring its own skills against manifests it ships — every one of
//! these is a bug in code the author is looking at, and `?`-propagating it to a
//! `main` that prints it is ceremony around an abort.
//!
//! But a manifest is a **file**, and a plane that loads one at runtime is a
//! legitimate shape this crate encourages: `Registry` pins digests, the CLI
//! reads YAML from disk, and a multi-tenant host may assemble a plane per
//! tenant from declarations it did not write. There, a bad file is an input,
//! not a bug, and a panic takes down every other tenant in the process to
//! report it. [`try_build`] is for that caller.
//!
//! One implementation, two entry points: `build` is `try_build` with an
//! `expect`, so the two cannot diverge about what is refused.
//!
//! [`RuntimeBuilder::try_build`]: crate::runtime::RuntimeBuilder::try_build
//! [`build`]: crate::runtime::RuntimeBuilder::build
//! [`try_build`]: crate::runtime::RuntimeBuilder::try_build

/// A plane this crate will not assemble.
#[derive(Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum BuildError {
    /// The plane and its blob store are scoped to different tenants.
    #[error(
        "this plane runs as tenant '{plane}' but its blob store serves '{store}'. \
         Blobs are content-addressed, so a shared store means two tenants' \
         identical bytes are one object — and erasing it for one destroys it for \
         the other while reporting both requests discharged"
    )]
    BlobStoreTenant { plane: String, store: String },

    /// A witness quorum this plane could never reach.
    ///
    /// Both directions are refused, because both spell witnessing that is on
    /// and is not. A quorum above the number of witnesses configured is a bar
    /// every round misses — so every sweep reports a shortfall, an operator
    /// learns to ignore it, and the deployment has the alerting cost of
    /// witnessing with none of the evidence. A witness list with no declared
    /// quorum is the same failure from the other side: whatever cosignatures
    /// happened to arrive become the bar they were held to.
    #[error(
        "this plane declares {declared} witness cosignature(s) per checkpoint and \
         {configured} witness(es) to ask, so no round can ever meet the bar — \
         a quorum nothing can reach reports a shortfall on every sweep, which is \
         how an operator learns to ignore the one that means something"
    )]
    WitnessQuorumUnreachable { declared: usize, configured: usize },

    /// The plane and its journal store are scoped to different tenants.
    ///
    /// The dangerous one, because it *works*. Runs are written into another
    /// tenant's keyspace while every key-scoped erasure and every policy request
    /// names the right one, so nothing at runtime looks wrong.
    #[error(
        "this plane runs as tenant '{plane}' but its journal store is scoped to \
         '{store}'. The mismatch does not surface at runtime: runs are written \
         into the other tenant's keyspace while every erasure and every policy \
         request names this one"
    )]
    JournalStoreTenant { plane: String, store: String },

    /// The plane and one of its state stores are scoped to different tenants.
    ///
    /// One variant for the five stores whose consequence is the same, with the
    /// store named as data rather than as five messages that differ only in a
    /// noun. When a key ring is wired, the plane seals this state under **its**
    /// tenant while the store writes rows under the store's, and the two scopes
    /// are both real — so nothing fails, nothing leaks, and the state sits
    /// under a scope the tenant's erasure does not name.
    ///
    /// That is the failure a deletion guarantee may not have: `erase` destroys
    /// the key it was asked for, reports success, and the sealed rows remain
    /// readable under the other scope. It is invisible at runtime because
    /// nothing about it is wrong except which of two correct scopes was used.
    #[error(
        "this plane runs as tenant '{plane}' but its {store} store serves \
         '{tenant}'. With a key ring wired the plane seals that state under \
         '{plane}' while the store keeps it under '{tenant}' — both scopes are \
         real, so nothing fails at runtime and an erasure for either tenant \
         destroys a key that does not reach these rows"
    )]
    StateStoreTenant {
        /// Which store disagreed: `case`, `event`, `task`, `memory`, `quota`,
        /// `timer`, `batch`, `authority` or `push`.
        store: &'static str,
        plane: String,
        tenant: String,
    },

    /// A tool server took the name reserved for agents on this plane.
    #[error(
        "tool server 'agent' is reserved: `tool://agent/<capability>` names an \
         agent on this plane and dispatches through `commission`. A transport \
         under that name would let a deployment change whether a grant means \
         \"an agent here\" or \"somebody's server\" without changing any \
         reviewed document — rename the server"
    )]
    ReservedToolServer,

    /// One name is both a registered peer and a wired tool server.
    ///
    /// A grant `tool://<name>/<capability>` would then dispatch to whichever
    /// the runtime checked first — a peer hop that extends the chain and
    /// counts against the delegation ceiling, or a tool call that does neither
    /// — and nothing in the reviewed document would say which.
    #[error(
        "'{server}' is registered as a peer and wired as a tool server; a grant \
         `tool://{server}/…` cannot mean both a delegating hop and a tool call — \
         rename one of them"
    )]
    PeerIsAlsoAToolServer { server: String },

    /// A manifest grants a peer a capability the registry never gave it.
    ///
    /// The chain the peer receives permits exactly the registry's scope, so
    /// the call would be refused at the peer's admission on every run — a
    /// grant that reads as a capability and cannot fire.
    #[error(
        "agent '{agent}' grants `tool://{peer}/{capability}`, but the peer registry's \
         scope for '{peer}' does not permit '{capability}' — widen the registry grant \
         or drop the manifest grant"
    )]
    PeerGrantOutsideScope {
        agent: String,
        peer: String,
        capability: String,
    },

    /// A specialist grants a peer, which is a hop it may never take.
    ///
    /// Calling a peer is delegation: the chain grows by a link and the
    /// delegation ceiling sees it. A `specialist` has a ceiling of zero, so
    /// the grant would be refused at dispatch on every run.
    #[error(
        "agent '{agent}' grants `tool://{peer}/…` while declaring itself a specialist \
         (delegation depth 0) — consulting a peer is delegation; declare \
         `topology.role: orchestrator` or drop the grant"
    )]
    PeerGrantOnASpecialist { agent: String, peer: String },

    /// A declarative agent needs a tool catalogue and the plane has none.
    ///
    /// Refused at build because it is knowable at build: the manifest says the
    /// agent runs a tool loop, and the plane says nothing reaches a tool server.
    /// Deferring it to the run would report a wiring mistake once per request
    /// instead of once, on a plane that assembled cleanly. The one shape that is
    /// legitimately catalogue-free is an agent whose grants are *all*
    /// `tool://agent/…` or `tool://<peer>/…` for a registered peer: those
    /// dispatch through `commission` and the peer wiring, and their catalogue
    /// is derived from the declaration.
    #[error(
        "agent '{agent}' declares `execution.kind: {kind}` with {grants}, but this \
         plane has no tool catalogue, so every run would fail identically. Wire one \
         with `RuntimeBuilder::toolbox(..)` — which derives it from this very \
         declaration — or state it with `.tools(catalog, client)`. Grants of the \
         form `tool://agent/<capability>`, or naming a peer registered with \
         `RuntimeBuilder::peers(..)`, need neither, because they dispatch through \
         `commission` and the peer wiring rather than a tool transport"
    )]
    DeclarativeToolsUnreachable {
        agent: String,
        kind: &'static str,
        grants: String,
    },

    /// A process-local erasure lock beside a store two instances can write.
    #[error(
        "this plane's journal store is shared between instances, and its \
         governed memory is sealed with a **process-local** erasure lock — so \
         the window between an erasure's legal-hold check and its key \
         destruction is open to the other instance, which can write an item \
         that ends up sealed under a scope about to stop existing. The erasure \
         would report success. Wire a coordinator that spans instances: \
         `EncryptedMemoryStore::new(..).coordinated_by(Arc::new(store.erasure_coordinator()))`"
    )]
    ErasureCoordinatorNotShared,

    /// An agent declares oversight on a plane that cannot ask anybody.
    ///
    /// The same shape as [`DeclarativeToolsUnreachable`](Self::DeclarativeToolsUnreachable),
    /// and both facts are in hand at `build`: the manifest says a human must
    /// decide, and the plane says there is nowhere to put the decision. Left to
    /// run time it surfaces on the one code path a test suite is least likely
    /// to reach — the first real approval — with the person already waiting.
    #[error(
        "agent '{agent}' declares {declared}, so a run must be able to open a \
         task and suspend until somebody decides it — but this plane has no \
         {missing}. Wire it with `RuntimeBuilder::{remedy}(..)`, or drop the \
         oversight declaration. A run admitted through plain `run(..)` also has \
         no case, so give it correlation keys with `run_correlated(..)` or name \
         one with `run_in_case(..)`"
    )]
    OversightUnreachable {
        agent: String,
        declared: String,
        missing: &'static str,
        remedy: &'static str,
    },

    /// An agent reads or writes memories on a plane that has nowhere to keep
    /// them.
    ///
    /// Knowable at build, and expensive at run time in a way most wiring
    /// mistakes are not: formation happens **after** the answer, so the run has
    /// already paid for its model calls, opened its approval task and waited for
    /// a person before failing on a store nobody wired.
    #[error(
        "agent '{agent}' declares `{declared}`, so its runs reach durable memory — but \
         this plane has no memory store. Wire one with `RuntimeBuilder::memory(..)`, or \
         drop the declaration. Left to run time, formation fails only once the run has \
         already paid for its model calls"
    )]
    MemoryWithoutStore {
        agent: String,
        /// The declaration that needs a store: `spec.memory.recall` or
        /// `spec.memory.formation`.
        declared: &'static str,
    },

    /// The plane's embedder and its index speak different languages.
    ///
    /// The one wiring mistake in this list that would otherwise never fail —
    /// see [`IndexIdentity`](crate::memory::IndexIdentity). The two strings
    /// differing is not itself the mistake: an index built from
    /// `…/search_document` asks for `…/search_query` here.
    #[error(
        "this plane embeds with '{embedder}' but its index accepts query vectors from \
         '{index}'. Two embedding revisions produce vectors of the same width that \
         compare with the same cosine and mean nothing to each other, so this pairing \
         would not fail — it would rank unrelated memories confidently on every search. \
         Wire the embedder the index names, or re-index"
    )]
    EmbeddingSpaceMismatch { embedder: String, index: String },

    /// A semantic index on a plane with no authoritative memory.
    ///
    /// Every search would fail at its last step, having already paid for an
    /// embedding call and a retrieval.
    #[error(
        "this plane wires a semantic index but no memory store. An index holds only \
         `(id, version, digest)` commitments — the content is materialised from \
         authoritative memory and re-checked before anything is exposed — so wire one \
         with `RuntimeBuilder::memory(..)`"
    )]
    SemanticMemoryWithoutStore,

    /// A memory subject binds to a case on a plane with no cases.
    ///
    /// The failure this prevents is worse than an error, which is why it is one:
    /// a binding that cannot resolve leaves the operator's fallback options as
    /// *fail the run* or *file everybody's memories under one key*, and the
    /// second is the defect bindings exist to remove.
    #[error(
        "agent '{agent}' files memories under '{subject}', which resolves from the run's \
         case — and this plane has no case store, so nothing could ever resolve it. Wire \
         one with `RuntimeBuilder::cases(..)` and admit runs with `run_correlated(..)`, or \
         declare a literal subject and accept that every subject's facts share one key"
    )]
    MemorySubjectUnbindable { agent: String, subject: String },

    /// An agent grant names a capability no agent on this plane provides.
    #[error(
        "agent '{agent}' grants 'tool://agent/{capability}', and no agent on \
         this plane provides '{capability}' — the model would be offered a \
         consultation that fails when chosen"
    )]
    AgentToolUnknownCapability { agent: String, capability: String },

    /// An agent grant names the granting agent's own capability.
    #[error(
        "agent '{agent}' grants 'tool://agent/{capability}', which it provides \
         itself — an agent consulting itself is a loop wearing a grant, and \
         the delegation ceiling would only bound how long it spins"
    )]
    AgentToolSelfReference { agent: String, capability: String },

    /// The policy set cannot be evaluated against a request this plane makes.
    ///
    /// Every rule is evaluated against every request, so a rule reading an
    /// attribute a request does not carry does not merely fail to match — it
    /// **errors**, and an unevaluable rule may be the `forbid` that would have
    /// stopped the call, so the gate refuses. A rule guarded on nothing
    /// therefore denies every effect of every run, from a policy set that
    /// compiled cleanly and validated against its schema.
    ///
    /// Some context attributes are conditional by design: `delegation_depth`,
    /// `owner` and `scope` exist only where a delegation chain does, and
    /// `label` only where a value is being sinked. A rule that reads one
    /// unconditionally is correct exactly until the first request without it.
    /// The remedy is Cedar's `has`: `context has delegation_depth &&
    /// context.delegation_depth >= 1`.
    ///
    /// Found at build by evaluating the compiled set against a canonical
    /// request of each shape the runtime issues — cheap, because evaluation is
    /// total and side-effect free — rather than at the first effect of the
    /// first run, which is where a deployment discovered it as a plane that
    /// denied everything.
    #[error(
        "this plane's policy set cannot be evaluated: {problems} — every rule is \
         evaluated against every request, so a rule reading an attribute the \
         request does not carry errors rather than not matching, and the gate \
         refuses because the rule that failed may be the one that would have \
         forbidden the call. Attributes that are conditional by design: \
         `delegation_depth`, `owner` and `scope` (a delegation chain only), \
         `label` (sinks only). Guard them — `context has delegation_depth && \
         context.delegation_depth >= 1`"
    )]
    PolicyUnevaluable { problems: String },

    /// A ceiling set to zero, which permits nothing at all.
    ///
    /// Zero is not a small budget; it is a budget already spent. These
    /// ceilings are checked before the work and against every effect of every
    /// kind, so a plane carrying one refuses its first operation on every run
    /// it will ever make — including a read-only tool call by an agent that
    /// declares no model.
    ///
    /// The manifest refuses this at parse, and a plane wired in Rust reaches
    /// the same budget without passing a parser: one rule, both doors.
    #[error(
        "the budget's `{field}` is 0, which permits nothing at all — not merely \
         no model spend. This ceiling is checked before every step and every \
         effect, so at 0 it is already reached and the run is refused its first \
         operation of any kind: a read-only tool call, a local lookup, an agent \
         that declares no models. Such a plane does not run once and stop, it \
         fails identically on every run it will ever make. Leave the field \
         `None` to mean 'no limit'. To stop a tenant doing work, use the \
         operator's emergency stop (`QuotaStore::set_halt`), which refuses new \
         runs with a reason attached — a halt says somebody is dealing with an \
         incident, where a ceiling only says not right now"
    )]
    BudgetPermitsNothing { field: &'static str },

    /// A lease TTL shorter than the store's expiry granularity.
    ///
    /// Both stores keep lease expiry in whole seconds and treat
    /// `expires_at <= now` as lapsed, so anything under the minimum is expired
    /// for part of every second it exists — no renewal frequency saves it.
    /// A plane built with one would have every run takeable by another
    /// instance while still working, and only under load.
    #[error(
        "a lease of {ttl:?} cannot be renewed: the store keeps expiry in whole \
         seconds and treats `expires_at <= now` as lapsed, so anything under \
         {minimum:?} expires between renewals however often they run — and a \
         run that cannot hold its lease can be taken over while it is still \
         working"
    )]
    LeaseUnrenewable {
        ttl: std::time::Duration,
        minimum: std::time::Duration,
    },

    /// One tool server name was registered twice.
    #[error(
        "tool server '{server}' is registered twice — registration order would \
         decide which transport carries a call"
    )]
    DuplicateToolServer { server: String },

    /// Both `tools(..)` and `toolbox(..)` were wired.
    ///
    /// Not a merge, and it must not silently become one: the stated catalogue is
    /// the operator saying something deliberate, the derived one is the agent's
    /// declaration, and overwriting either runs a plane under grants nobody
    /// chose.
    #[error(
        "this plane wires tools twice — `tools(..)` states the catalogue \
         explicitly and `toolbox(..)` derives it from the agents, so one of them \
         would silently replace the other's grants"
    )]
    ToolsWiredTwice,

    /// The tools this binary implements and a reviewed manifest disagree.
    #[error(
        "the tools this binary implements and the manifest of agent '{agent}' \
         disagree — the declaration a reviewer approved no longer describes the \
         agent:\n  {}", problems.join("\n  ")
    )]
    ToolDrift {
        agent: String,
        problems: Vec<String>,
    },

    /// Two agents grant one tool and declare it differently.
    #[error(
        "agents '{first}' and '{second}' both grant '{tool}' and declare it \
         differently — a plane has one catalogue, so one of the two reviewed \
         declarations would silently not be the one enforced"
    )]
    ToolDeclaredTwoWays {
        tool: String,
        first: String,
        second: String,
    },

    /// Tools were wired to a plane whose agents declare none.
    #[error(
        "tools were wired to a plane with no declared agent — a grant is an \
         agent's declaration, so there is nothing here that admits them"
    )]
    ToolsWithoutDeclaration,

    /// A stated catalogue is laxer than a reviewed grant.
    ///
    /// The one direction nobody can be right about: a read-only entry exempts
    /// the tool from the whole-value taint gate *and* carries `Recovery::Retry`,
    /// so a timed-out money-moving call is sent again.
    #[error(
        "the stated tool catalogue is laxer than a reviewed manifest grant — a \
         read-only entry exempts the tool from the whole-value taint gate and \
         makes a timed-out call retryable:\n  {}", problems.join("\n  ")
    )]
    CatalogueLaxerThanGrant { problems: Vec<String> },

    /// Two distinct skills share one name.
    #[error(
        "two skills on this plane are both named '{name}'. A skill name is how a \
         capability resolves to an implementation and how a run names what it \
         dispatched, so two of them make both answers arbitrary — rename one"
    )]
    DuplicateSkillName { name: String },

    /// Two agents claim one capability.
    #[error(
        "capability '{capability}' is claimed by two agents on this plane: \
         '{first}' and '{second}'. Dispatch resolves a capability to one skill \
         and to the manifest governing it, so the second claim would silently \
         take the first's work out from under the first's budget and grants. \
         Give them distinct capabilities, or put them on separate planes"
    )]
    CapabilityClaimedTwice {
        capability: String,
        first: String,
        second: String,
    },

    /// A skill answers a capability its agent's declaration never names.
    ///
    /// The manifest is the artifact that gets reviewed, digested and pinned,
    /// and the A2A card is built from it — so a capability served but not
    /// advertised is a door in a reviewed surface that the review could not
    /// see. The skill is still governed by the manifest, which is what makes
    /// this quiet rather than broken: budgets and grants apply, the run
    /// journals correctly, and nothing anywhere says the agent answers more
    /// than its file claims.
    #[error(
        "agent '{agent}' registers skills answering {undeclared:?}, which \
         `spec.capabilities.provides` does not name — the declaration is what \
         gets reviewed, digested and advertised, so a capability added in code \
         is a surface no reviewer of that file can see. Add it to `provides`, \
         or register the skill on its own agent"
    )]
    ProvidesWhatItDoesNotAdvertise {
        agent: String,
        undeclared: Vec<String>,
    },

    /// A declarative agent has no model to call.
    #[error(
        "agent '{agent}' declares execution but no privileged model — a \
         declarative agent has nothing to call"
    )]
    DeclarativeWithoutModel { agent: String },

    /// A declarative agent names a provider no driver is registered for.
    ///
    /// Named rather than defaulted: falling back to some other registered driver
    /// would run the agent on a model its own declaration does not name.
    #[error(
        "agent '{agent}' names provider '{provider}', which no driver is \
         registered for. Call RuntimeBuilder::provider(\"{provider}\", ..)"
    )]
    UnknownProvider { agent: String, provider: String },

    /// A declarative agent provides no capability.
    #[error(
        "agent '{agent}' declares execution but provides no capability — a \
         declarative agent nothing can call is a file that does nothing"
    )]
    DeclarativeProvidesNothing { agent: String },

    /// A manifest advertises capabilities none of its own skills provide.
    #[error(
        "agent '{agent}' advertises capabilities none of its own skills provide: \
         {missing:?}. A skill wired with `RuntimeBuilder::skill` is not governed \
         by any agent — it runs under the plane's budget and no manifest gate. \
         Register it on the agent instead: \
         `.agent(Agent::new(&manifest).skill(MySkill))`"
    )]
    AdvertisesWhatItCannotProvide { agent: String, missing: Vec<String> },
}

crate::core::error::debug_is_display!(BuildError);