ares-server 0.10.1

ARES agent server with multi-provider LLM support, tool calling, RAG, and MCP integration
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
# Runtime Services

This chapter describes the runtime services that live beside the kernel
core: effect ownership, the fiber-scoped timer suite, the logger service,
the module graph, and the tenant file fence. Every signature here comes
from `crates/cordis/src` and `crates/ares-tools/src/fence.rs`.

## Effect Ownership

The kernel models cleanup as *effects*. An effect is anything that
implements one method:

```rust
pub trait Disposable: Send + 'static {
    fn dispose(self: Box<Self>);
}
```

Every closure with a compatible signature is a `Disposable`. A fiber
holds its effects as labeled undo entries. `Fiber::dispose` pops them in
last-in, first-out (LIFO) order and runs each undo once. A reactive pass
through `Unloading` runs the same stack. This gives one rule: teardown
order is the reverse of registration order, always.

### EffectHandle dispose semantics

The timer suite returns an `EffectHandle` per registration. Its rules:

- Dropping the handle does NOT cancel the effect. Callers must dispose
  it explicitly or let the owning fiber do it.
- Clones share one cancellation flag. Disposing any clone cancels the
  registration.
- Disposal is idempotent: the flag flips once and the teardown hook runs
  once.
- `handle.is_cancelled()` reports the state at any time.
- Each handle pushes a labeled undo (prefix `timer:`) onto the current
  fiber scope. Fiber disposal therefore cancels timers without caller
  action.

A registration made outside a fiber scope logs a warning and returns an
*orphan* handle. An orphan still works when you dispose it by hand; no
fiber cancels it automatically.

### Inert handles

Two APIs return handles that may be *inert*: disposing them flips
nothing.

- Event listener registration rides the `internal/listener` veto point.
  When that chain bails or errors, the registration is cancelled before
  it enters either registry. The caller receives an inert handle whose
  `dispose` does nothing. The failure is fail-closed: an erroring veto
  chain cancels too.
- `Context::register_accessor` returns an accessor `EffectHandle`.
  `handle.dispose()` removes the declaration and every alias bound to
  it, and returns `true` only when the declaration was still live. After
  removal, reads resolve `None`.

## Fiber-Scoped Timer Suite

`cordis::timer` provides six primitives. All of them run on one shared
timer thread named `cordis-timer`, never on the owning task. The thread
sleeps until the nearest deadline in a shared wheel, drains all due
entries under one short lock, then runs callbacks outside the lock.
Callbacks must be cheap and non-blocking. A panicking callback is caught
and logged; the thread survives.

### Wheel mechanics

The wheel is a min-heap of entries ordered by `(deadline, seq)`. The
sequence number breaks ties so equal deadlines fire in registration
order. One process-wide instance serves every fiber; it lives behind a
`LazyLock<Mutex<Wheel>>`.

The driver loop has two phases:

1. **Sleep.** Read the nearest deadline while holding the lock, release,
   then `park_timeout` for that long. A park with no timeout waits for
   the next insert. Any `schedule` call pushes its entry and unparks the
   thread, so an earlier deadline preempts a long sleep immediately.
2. **Drain.** Re-acquire the lock, pop every entry whose deadline is at
   or before now, release, then run each job. Callbacks run outside the
   lock on purpose: an interval callback re-arms itself by calling
   `schedule`, which needs the lock. Holding it across callbacks would
   deadlock every self-re-arming pattern, including debounce and
   throttle emits.

Cancellation is cooperative through `EffectHandle`. Disposal flips the
shared flag but does not remove the heap entry. Two paths make that
safe:

- The `timeout` job checks the flag inside the job body. A disposal
  racing the drain still prevents the callback from running.
- The future-based `sleep` resolves early and silently when disposed
  while pending; nothing observes a cancelled wake-up.

A panicking callback lands in `catch_unwind`. The driver logs a warning
and moves to the next job. One bad callback cannot starve the rest of
the wheel.

| Primitive | Shape |
|---|---|
| `timeout(delay, callback)` | One-shot delay, then callback. Returns `EffectHandle`. |
| `sleep(delay)` | One-shot delay as a future. Returns `(EffectHandle, impl Future)`. |
| `interval(delay, callback)` | Repeating callback. Returns `EffectHandle`. |
| `interval_stream(delay)` | Repeating ticks as a pollable stream. Returns `Interval`. |
| `debounce(delay)` | Trailing-edge burst collapse. Returns `Scheduled<T>`. |
| `throttle(delay, no_trailing)` | Leading edge plus optional trailing edge. Returns `Scheduled<T>`. |

Register inside `with_current_fiber(&fiber, || ..)` to attach the effect
to that fiber. Key behaviors:

- `timeout` checks the cancellation flag inside the job, so a disposal
  that races the drain still prevents the callback from running.
- `interval` re-arms the next tick from the moment each tick fires. The
  cadence never runs ahead of the callback.
- `sleep` resolves early and silently when its handle is disposed while
  the future is pending.
- `interval_stream` queues ticks in a channel while nobody polls. After
  disposal the stream yields exactly ONE final
  `Err(InactiveEffect)` item, then closes. Ticks queued before the
  disposal are discarded, so teardown is always the final observation.
- `debounce` keeps only the last value of a burst and delivers it after
  a quiet window. `throttle` delivers the first value immediately and,
  unless `no_trailing` is set, the last value of the window at close.

`Scheduled<T>` pairs a submit side (`call`) with a consumer side
(`receive`, `receive_timeout`). Cancellation drops pending values; later
receives return `None`.

## LoggerService

`LoggerService` is a bounded ring of recent messages plus exporter
fan-out. Provide it once on the root context:

```rust
ctx.provide(LoggerService::new());
```

The default capacity is 1000 messages. Use `LoggerService::with_capacity`
to change it. The oldest message leaves first at capacity. `snapshot`
returns detached clones, oldest first.

### Write path

Every write follows four steps:

1. Resolve the effective threshold for the logger name.
2. Bail BEFORE argument assembly when the kind fails the gate.
3. Append the record to the ring.
4. Fan out to every exporter that accepts `(name, kind)`.

Step 2 matters for cost. Prefer `log_with` with a closure so disabled
paths never build arguments:

```rust
logger.log_with(&ctx, "db", LogKind::Debug, || {
    vec!["rows".into(), count.into()]
});
```

Convenience methods `error`, `warn`, `info`, and `debug` take pre-built
arguments and still pass the gate. Facade methods on `Context`
(`ctx.info(..)`, and so on) no-op when no `LoggerService` is provided.

### Levels

Severity ranks are numeric: `Error=0`, `Warn=1`, `Info=2`, `Debug=3`.
Lower means more severe. A kind passes a threshold when its rank is less
than or equal to the threshold value.

- `set_default_level` pins the threshold for unlisted names. The default
  is `DEBUG`, which passes everything.
- `set_level(name, level)` pins one name. It wins over the default.
- `clear_level(name)` removes a pin.

### Exporters

An exporter implements `export(&self, message: &Message, text: &str)`.
It runs inline on the writer's thread and must not panic. Registration
takes an `ExporterConfig` with two fields:

- `levels`: per-name thresholds for this sink. Unlisted names pass.
- `max_length`: character cap on rendered text. Default 4096.

`register` returns a `Box<dyn Disposable>`. Disposing it removes the
sink. The buffer keeps recording after a sink leaves.

### Printf placeholders

When the leading argument is a string containing `%`, `Message::render`
treats it as a format string:

| Specifier | Meaning |
|---|---|
| `%s` | String |
| `%d`, `%i` | Integer |
| `%f` | Float |
| `%o` | Compact JSON object |
| `%O` | Pretty JSON object |
| `%c` | Colorized with the stable palette slot for this name |
| `%C` | Bold colorized variant of `%c` |
| `%%` | Literal percent |

Unknown specifiers and exhausted arguments stay literal. Unconsumed
arguments join at the end with spaces. Without a format head, arguments
join with single spaces.

### Stable color slots

`%c` and `%C` pick one of sixteen ANSI colors from the logger name. The
slot must be stable across processes and platforms, so it comes from a
hash, not a counter. `name_color_code` in `logger.rs` computes FNV-1a
over the name bytes:

$$h_0 = \texttt{0xcbf29ce484222325}, \qquad h_{i+1} = (h_i \oplus b_i) \cdot \texttt{0x00000100000001b3} \;\bmod\; 2^{64}$$

where \\(b_i\\) is the \\(i\\)-th name byte. The palette index is then

$$\text{color}(name) = \text{ANSI16}[\,h \bmod 16\,]$$

with `ANSI16 = [30..=37, 90..=97]`: eight normal foregrounds followed by
their bright variants. The multiplication wraps (`wrapping_mul`), so no
input can overflow-panic. The same name always renders in the same
color — in tests, in production logs, and across restarts. Test anchor:
`colorization_is_stable_per_name`.

### LoggerIntercept override

`LoggerIntercept` rides the normal intercept channel. Install it with
`ctx.intercept(..)`. Writes through that context handle resolve it at
write time:

```rust
let child = root.intercept(LoggerIntercept {
    name: Some("svc".into()),   // None matches every logger
    level: Some(LogLevel::ERROR),
});
```

`level: Some(l)` replaces the effective threshold for matching writes,
over both pins and defaults. Names that do not match keep the ambient
configuration.

### Derived names

`hyphenate` turns `CamelCase` into `kebab-case` and handles acronym
heads (`HTTPServer` becomes `http-server`). `derived_name::<T>()`
applies it to the short type name. Use it for logger naming:
`ctx.info(&derived_name::<Self>(), ..)`.

## Module Graph Transactional Reloads

The watcher fans file changes out to service-level dependents by
`TypeId`. That layer cannot answer "which plugin must reload because
this file changed?" because file edges carry no `TypeId`. `ModuleGraph`
is that missing layer.

Callers register every dynamic module under a key, usually the watched
file stem:

```rust
graph.register_module("foo", vec!["shared".into()], "FooPlugin");
```

Each entry carries its declared dependencies and the plugin that
implements it.

### Transaction shape

`ModuleGraph::change_many(ctx, keys)` runs one settled batch in two
phases:

1. **Compute** (read-only): walk the transitive dependent set across ALL
   input keys with a shared visited set. Cycles terminate. A plugin
   reachable from several inputs appears exactly once. If no input key
   matches a registered module, nothing is computed.
2. **Apply** (sequential): reload each affected plugin through the
   `ModuleReload` seam in breadth-first propagation order. The FIRST
   failure rolls that plugin back to its previous state and stops the
   batch. Earlier successes stay active.

The classified result is a `ChangeOutcome`:

- `Ignored`: no key matched a registered module. Nothing changed.
- `Reloaded(plugins)`: every affected plugin reloaded, deduped.
- `RolledBack { reloaded, failed_plugin, error }`: names what applied,
  what failed, and the error text. The text also reports a rollback
  failure when the restore itself failed.

The default seam, `NoopReload`, never fails. Deployments wire their own
`reload` / `rollback` pair, or swap one in later with `set_reloader`.

### One transaction, narrated

Watch two shared files change at once: `routes.toml` and `auth.toml`.
Three modules depend on them. `foo` depends on both; `bar` depends on
`shared`; `baz` is independent.

1. The watcher's debounce settles with both paths. Each path maps to its
   file stem, and the watcher hands `["routes", "auth"]` to
   `change_many`.
2. The compute phase walks the transitive dependent set across BOTH keys
   with one shared visited set. It reaches `foo` through either key but
   records it once — dedup happens during the walk, not after.
3. The apply phase reloads affected plugins in breadth-first order:
   dependencies before their dependents, so each plugin reloads into a
   kernel where what it injects already exists.
4. Suppose `bar`'s reload fails on its new code. The seam rolls `bar`
   back to its previous state and the batch stops there. `baz` never ran
   — it matched no input key.

The result is
`RolledBack { reloaded: ["foo"], failed_plugin: "bar", error: .. }`.

**Sibling survival.** Plugins that reloaded before the failure stay
active on their NEW code. The batch does not unwind earlier successes.
This guarantee shapes how you write reloaders:

- A reload must leave the kernel consistent on its own. Earlier siblings
  will not be reverted for you.
- Order failures so cheap ones fail first when possible; breadth-first
  order plus early failure minimizes divergence between old and new.
- The rollback text reports a restore failure separately. Rolling back
  `bar` can itself error; the error field says so. Surface that case as
  an operator alert: the running state then matches no recorded state.

Contrast with the loader's two-phase reload (see
[Lifecycle](lifecycle.md)): the loader rolls back everything newest-first;
the module graph deliberately keeps successful siblings. The loader owns
declarative trees. The module graph owns native-code hot swap, where a
reloaded `.so` cannot always be unloaded again safely.

### Watcher integration

When a debounced watcher batch settles, the watcher maps each changed
path to its file stem and hands those stems to `change_many` — but only
when a `ModuleGraph` is provided on the context. No graph registered
means zero cost; the `TypeId` path stays unchanged. The HMR dynamic
library fingerprint gate is untouched by this layer; neither consults
the other.

## File Fence Layers L0-L3

The tenant filesystem permission fence lives in
`crates/ares-tools/src/fence.rs`. One `Fence` instance serves one
session. Its policy value is pure and shareable; the observed-set ledger
and audit ring sit behind a mutex.

Layers run in fixed order. A path passes only when every active layer
passes:

- **L0 mode**: `FenceMode::ReadOnly` denies every write. Reads still
  pass L1 and L2.
- **L1 boundary**: the resolved path must stay inside `workspace_root`.
  `FenceMode::Full` waives this layer.
- **L2 blocklist**: a blocked name denies reads and writes in every
  mode.
- **L3 write guards**: session-level enforcement over the policy.

`check_read` and `check_write` on `FencePolicy` stay pure path checks
(L0-L2). Only the `Fence` methods touch file contents.

### Layer matrix

The matrix lists, for each layer, what it guarantees and which stable
code reports its failure. Layers run top to bottom; the first failure
decides the code.

| Layer | Guarantee | Fails with | Applies to |
| --- | --- | --- | --- |
| L0 mode | `ReadOnly` denies every write | `FS_FENCE_DENIED` | Writes only |
| L1 boundary | Resolved path stays inside `workspace_root` (`Full` waives) | `FS_FENCE_DENIED` | Reads and writes |
| L2 blocklist | Blocked names denied in every mode | `FS_FENCE_DENIED` | Reads and writes |
| L3 observation | Canonical path observed before any guarded write in non-blind modes; missing paths record version 0 | `FS_NOT_OBSERVED` | Writes only |
| L3 contract | Guard matches observed state: absent path for create, unchanged version for replace | `FS_EXISTS`, `FS_VERSION_CONFLICT` | Writes only |
| L3 I/O | Atomic sibling-temp-plus-rename write | `FS_IO` | Writes only |

Reading the table as an operator:

- Three different denials all report `FS_FENCE_DENIED`; the audit ring
  entry records the reason text that separates them.
- `FS_NOT_OBSERVED` is a protocol error, not a permission error. The
  agent forgot to read before writing. A read of a missing path counts,
  so creating a new file needs one prior failed-or-absent read.
- `FS_VERSION_CONFLICT` means someone changed the file after your read.
  Re-read and re-apply the edit.
- `FS_IO` covers everything underneath the policy: permission bits at
  the OS level, full disks, vanished parents. The reason text carries
  the OS message.

Determinism is the point. The same path, mode, guard, and observed state
always produce the same code. Agent-facing retry logic branches on codes,
not on parsed prose.

### L3 write guards

Every write names a guard contract (`WriteGuard`):

- `Unconditional`: overwrite whatever is there.
- `CreateIfAbsent`: fails with `FS_EXISTS` when the path already exists.
- `ReplaceIfVersion { version }`: fails with `FS_VERSION_CONFLICT` when
  the file is gone or changed since observation.

In modes without blind-write allowance, the canonical path must have
been observed through `Fence::fence_read` first. Otherwise the write
fails with `FS_NOT_OBSERVED`. This covers every contract, including
creating new files. A read records a version fingerprint; a missing path
records version `0`, so a later create can prove absence.

Writes land through a sibling temporary file and an atomic rename, so an
interrupted write leaves no torn file behind. A successful write becomes
the new observed version, so chained guarded writes work against your
own output.

Errors carry stable `FS_*` codes: `FS_NOT_OBSERVED`,
`FS_VERSION_CONFLICT`, `FS_EXISTS`, `FS_FENCE_DENIED`, and `FS_IO`. The
first failing layer determines the code, so agent-facing errors stay
deterministic. Every operation lands in a bounded audit ring
(`audit_log()`); the oldest entry leaves at capacity 200.