axn 0.1.0.pre.alpha.5 → 0.1.0.pre.alpha.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/AGENTS-consuming.md +78 -17
- data/AGENTS-tool-adapters.md +57 -7
- data/CHANGELOG.md +131 -0
- data/lib/axn/async/adapters/active_job.rb +8 -2
- data/lib/axn/async/adapters/sidekiq.rb +14 -3
- data/lib/axn/async/batch_enqueue.rb +1 -1
- data/lib/axn/async/enqueue_all_orchestrator.rb +5 -1
- data/lib/axn/async/exception_reporting.rb +16 -3
- data/lib/axn/async.rb +3 -3
- data/lib/axn/configurable.rb +13 -5
- data/lib/axn/configuration.rb +99 -9
- data/lib/axn/core/ambient_context.rb +74 -22
- data/lib/axn/core/automatic_logging.rb +1 -0
- data/lib/axn/core/context/facade.rb +23 -8
- data/lib/axn/core/context/facade_inspector.rb +23 -8
- data/lib/axn/core/context/internal.rb +3 -3
- data/lib/axn/core/context.rb +8 -1
- data/lib/axn/core/contract/redaction.rb +351 -52
- data/lib/axn/core/contract/shape_declaration.rb +514 -68
- data/lib/axn/core/contract/subfield_contradictions.rb +112 -53
- data/lib/axn/core/contract/validator_class_cache.rb +72 -0
- data/lib/axn/core/contract.rb +4183 -292
- data/lib/axn/core/contract_for_subfields.rb +184 -63
- data/lib/axn/core/default_call.rb +1 -1
- data/lib/axn/core/executor.rb +293 -47
- data/lib/axn/core/flow/callbacks.rb +1 -1
- data/lib/axn/core/flow/fails_on.rb +149 -10
- data/lib/axn/core/flow/handlers/invoker.rb +16 -2
- data/lib/axn/core/flow/handlers/matcher.rb +70 -12
- data/lib/axn/core/flow/handlers/resolvers/callback_resolver.rb +8 -2
- data/lib/axn/core/flow/handlers/resolvers/message_resolver.rb +63 -14
- data/lib/axn/core/flow/messages.rb +1 -1
- data/lib/axn/core/hooks.rb +3 -3
- data/lib/axn/core/instance_deferral.rb +461 -0
- data/lib/axn/core/logging.rb +8 -1
- data/lib/axn/core/method_shadowing.rb +195 -9
- data/lib/axn/core/nesting_tracking.rb +53 -3
- data/lib/axn/core/tagging.rb +16 -4
- data/lib/axn/core/validation/base.rb +465 -65
- data/lib/axn/core/validation/clusivity_sets.rb +304 -0
- data/lib/axn/core/validation/container_contents.rb +32 -0
- data/lib/axn/core/validation/fields.rb +39 -23
- data/lib/axn/core/validation/validators/exclusion_validator.rb +16 -0
- data/lib/axn/core/validation/validators/inclusion_validator.rb +16 -0
- data/lib/axn/core/validation/validators/non_emptiness_validator.rb +4 -1
- data/lib/axn/core/validation/validators/of_validator.rb +300 -10
- data/lib/axn/core/validation/validators/shape_validator.rb +29 -12
- data/lib/axn/core/validation/validators/type_validator.rb +23 -3
- data/lib/axn/core/validation/validators/validate_validator.rb +24 -4
- data/lib/axn/core/validation/validators/whole_value_clusivity.rb +33 -0
- data/lib/axn/core.rb +77 -19
- data/lib/axn/exceptions.rb +199 -6
- data/lib/axn/extensions/config.rb +30 -1
- data/lib/axn/extensions/invoked_via.rb +64 -0
- data/lib/axn/extensions/serialization.rb +10 -1
- data/lib/axn/extensions/tracing.rb +59 -0
- data/lib/axn/extensions.rb +126 -9
- data/lib/axn/extras/strategies/client.rb +18 -10
- data/lib/axn/extras/strategies/vernier.rb +3 -3
- data/lib/axn/factory.rb +23 -1
- data/lib/axn/form_object.rb +11 -2
- data/lib/axn/internal/action_state.rb +114 -0
- data/lib/axn/internal/call_logger.rb +71 -9
- data/lib/axn/internal/coercion.rb +5 -1
- data/lib/axn/internal/current_entry_point.rb +35 -0
- data/lib/axn/internal/cycle_guard.rb +14 -0
- data/lib/axn/internal/exception_context.rb +1 -1
- data/lib/axn/internal/fails_on_verdicts.rb +49 -0
- data/lib/axn/internal/field_config.rb +34 -0
- data/lib/axn/internal/name_ownership.rb +169 -0
- data/lib/axn/internal/native_methods.rb +306 -26
- data/lib/axn/internal/reflection/pattern.rb +219 -0
- data/lib/axn/internal/reflection/property_names.rb +53 -9
- data/lib/axn/internal/reflection/schema.rb +1809 -153
- data/lib/axn/internal/reflection/values.rb +17 -19
- data/lib/axn/internal/reflection.rb +6 -2
- data/lib/axn/internal/rendering.rb +37 -2
- data/lib/axn/internal/resolved_subfields.rb +4 -0
- data/lib/axn/internal/shape_graph.rb +169 -34
- data/lib/axn/internal/subfield_tree.rb +156 -23
- data/lib/axn/internal/tracing.rb +61 -0
- data/lib/axn/internal/transparent_bubbling.rb +35 -0
- data/lib/axn/mountable/helpers/class_builder.rb +9 -2
- data/lib/axn/mountable/mounting_strategies/step.rb +31 -21
- data/lib/axn/mountable.rb +1 -1
- data/lib/axn/result.rb +96 -39
- data/lib/axn/strategies/form.rb +21 -3
- data/lib/axn/testing.rb +9 -1
- data/lib/axn/tools/adapter_roots.rb +161 -6
- data/lib/axn/tools/adapter_serialization.rb +159 -0
- data/lib/axn/tools/invoker.rb +31 -7
- data/lib/axn/tools.rb +13 -4
- data/lib/axn/version.rb +1 -1
- data/lib/axn.rb +33 -2
- metadata +18 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96629802248edd18b5c1d3607b7aa3ad6775c9185a8667808f4d81ae98cfb69e
|
|
4
|
+
data.tar.gz: 66185b1f3f784c7a4c57680ed7b1fdcda1ae958904b69d53dfa003fb50c88286
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bc8ffc522d6226bcb15f401d42f23560cec3fd0497d30f710456d2b68f12c1238c5ca6453f151aec00baddebfe707b619a649177c62779f6db0cf44eb43e84fb
|
|
7
|
+
data.tar.gz: 8bd82a4adf9fd10a612ae70ff4abfc7b37284970dbed31a6e615c5d842b8384e51ad9ff17b1a6914738257fc2bc2d86ce374fa8d18a21019f069e19e300653dd
|
data/AGENTS-consuming.md
CHANGED
|
@@ -60,13 +60,13 @@ Common options (same for `expects` and `exposes`):
|
|
|
60
60
|
|
|
61
61
|
| Option | Meaning |
|
|
62
62
|
| --- | --- |
|
|
63
|
-
| `type:` | `is_a?` check. `type: :boolean` (no Ruby Boolean class; also defines a `field?` predicate), `type: :uuid`, `type: :params` (a Hash or any `ActionController::Parameters`). Union: `type: [String, Symbol]`. |
|
|
63
|
+
| `type:` | `is_a?` check. `type: :boolean` (no Ruby Boolean class; also defines a `field?` predicate), `type: :uuid`, `type: :params` (a Hash or any `ActionController::Parameters`). Union: `type: [String, Symbol]`. A token outside that grammar (`type: false`, `type: [String, nil]`) raises `ArgumentError` at declaration. |
|
|
64
64
|
| `optional: true` | Don't fail when the field is missing or nil (≡ `allow_blank: true`); removes the auto presence check. **Preferred** spelling. Caveat: a *typed* field still type-checks a non-nil blank — `type: Hash, optional: true` still rejects `""` (a `type: String` field accepts it, since `"".is_a?(String)`). |
|
|
65
65
|
| `allow_empty: true` | Accept an empty collection or string but **not** `nil` — the field stays required. Needs a `type:` whose values can be empty — `Array`/`Hash`/`Set`/`String`/`:params`, or any class or module defining `empty?`; raises otherwise, as does any value other than `true`/`false`/`nil`. Pair with a tolerance flag inverted (`optional: true, allow_empty: false`) for "may be omitted, but not empty". Don't also declare `presence:` — the two answer the same question, and a disagreement raises. |
|
|
66
66
|
| `allow_nil:` / `allow_blank:` | Finer-grained than `optional:`. |
|
|
67
67
|
| `default:` | Used when the field is missing or explicitly `nil` (**not** for blank values). |
|
|
68
68
|
| `sensitive: true` | Filter the value in logs / error reports / `inspect`. Accepts a proc/symbol for runtime decisions. |
|
|
69
|
-
| `of:` |
|
|
69
|
+
| `of:` | Names what is INSIDE a container. `type: Array`: each element's class (`of: String`, `of: [String, Numeric]`), errors reporting the failing index. `type: Hash`: a map (`of: { keys: Symbol, values: Integer }`) — either axis may be omitted to leave it unconstrained, and a failing entry is reported by its ordinal, never its key. Refused on any other type, a union `type: [Array, Hash]` included. It **nests**: an element or an axis may take an inner-contract bag carrying `klass:`/`of:`/`shape:`/`message:` — `of: { klass: Array, of: Integer }` (an array of arrays), `of: { values: { klass: Hash, shape: … } }` (a map of shaped records; members at an axis must be objects answering `field`/`validations`, e.g. a `Struct.new(:field, :validations)`, since no block form reaches an axis). On a Hash, `of:` may sit beside a `shape:` or a block, which names specific keys: those keys are **exempt** from the map contract, exactly as JSON Schema's `additionalProperties` applies only to keys `properties` does not match. |
|
|
70
70
|
| `validate:` | Custom: `validate: ->(v) { "must be > 10" unless v > 10 }` — return a string (or raise) to fail. |
|
|
71
71
|
| any ActiveModel validation | e.g. `length:`, `format:`, `numericality:` — passed through as if to `validates`. |
|
|
72
72
|
|
|
@@ -92,14 +92,29 @@ See <https://teamshares.github.io/axn/reference/tool-invoker>.
|
|
|
92
92
|
| `done!("msg", **kw)` | Abort now as **success** (early return); skips remaining `call` + `after` hooks. |
|
|
93
93
|
| `log("msg", level: :info)` | Log via `Axn.config.logger`, prefixed with the class name. |
|
|
94
94
|
| field readers | Read any `expects` field by name; `result.<field>` reads exposures (rare inside `call`). |
|
|
95
|
+
| `Axn::Extensions::Tracing.annotate_span(**attrs)` | Write vendor-namespaced OTel attributes onto axn's own `axn.call` span — for a gem, not an app (apps use `tag`/`dimension`); never `OpenTelemetry::Trace.current_span`. |
|
|
95
96
|
|
|
96
97
|
If you declare `exposes :x` you must `expose x: …` on every success path — **unless** `x` is also an
|
|
97
98
|
`expects` field, in which case Axn auto-copies it (see Gotchas). Outbound validation still runs on
|
|
98
99
|
`done!`, so a required exposure that's unset makes the action fail with `OutboundValidationError`.
|
|
99
100
|
|
|
100
101
|
Hooks: `before`, `after`, `around` (block or symbol method). A `fail!`/raise in a hook fails the
|
|
101
|
-
action. `done!` skips `after` hooks
|
|
102
|
-
`
|
|
102
|
+
action. `done!` skips `after` hooks — and because it unwinds via an exception, statements *after*
|
|
103
|
+
`chain.call` in an `around` hook are skipped too (`fail!` and an unhandled raise unwind the same
|
|
104
|
+
way). Put such teardown in an `ensure` inside the `around`, or use `use :transaction`, which rescues
|
|
105
|
+
the signal so the transaction still commits. Note the `around` hook (and its `ensure`) covers only
|
|
106
|
+
halts raised **after the hook chain is entered** — an inbound `expects` failure, or an *inbound*
|
|
107
|
+
`preprocess:`/`default:` callable that raises, settles before the hooks run, so neither fires. The
|
|
108
|
+
`exposes` side is bounded too: outbound resolution (an `exposes` `default:`, outbound validation)
|
|
109
|
+
runs *after* the hook body returns, so the hooks complete **normally** and never observe a raise from
|
|
110
|
+
it — an `around` that rescues to record failures misses them. Use the callbacks for per-call
|
|
111
|
+
observability that must not miss either end. Callbacks
|
|
112
|
+
(`on_success`, `on_error`, `on_failure`, `on_exception`) fire once the action **settles** — which is
|
|
113
|
+
not the same as "after `call`": they fire even when `call` never ran, as on an inbound validation
|
|
114
|
+
failure. That is what makes them the seam that sees every call. `on_error` is a superset, co-firing
|
|
115
|
+
with whichever of `on_failure`/`on_exception` applies. A raise in a callback does **not** flip `ok?` —
|
|
116
|
+
it is swallowed, logged, and reported to `Axn.config.on_ignored_exception` (which defaults to your
|
|
117
|
+
`on_exception` handler) carrying `context[:axn_ignored]`.
|
|
103
118
|
<https://teamshares.github.io/axn/usage/writing>.
|
|
104
119
|
|
|
105
120
|
## Using a result
|
|
@@ -149,7 +164,9 @@ expects :user, model: true
|
|
|
149
164
|
|
|
150
165
|
`user_id` always means *the record's primary key*, on every path. Passing both a record and a
|
|
151
166
|
disagreeing `user_id` (default `:find` finder) raises `InboundValidationError` — contradictory
|
|
152
|
-
input is a developer error.
|
|
167
|
+
input is a developer error. `klass:` must be a single Class/Module (no union, no `type:`-style
|
|
168
|
+
pseudo-type); anything else raises `ArgumentError` at declaration. Source:
|
|
169
|
+
`lib/axn/core/field_resolvers/model.rb`.
|
|
153
170
|
|
|
154
171
|
**`on:` — subfields (the `:extract` resolver).** Declare expectations about nested data and get a
|
|
155
172
|
flat reader:
|
|
@@ -162,8 +179,12 @@ expects :zip, on: "address.billing" # dotted path; reader: zip
|
|
|
162
179
|
```
|
|
163
180
|
|
|
164
181
|
Subfields support all the normal options and `default:`; `readers: false` skips reader creation;
|
|
165
|
-
`as:`/`prefix:` rename. `default:`/`preprocess:`/`sensitive:`
|
|
166
|
-
|
|
182
|
+
`as:`/`prefix:` rename. `default:`/`preprocess:`/`sensitive:` work on a *nested parent* too (whether
|
|
183
|
+
reached by dotted path or by pointing `on:` at another subfield). `default:`/`preprocess:` resolve on
|
|
184
|
+
the **read path**, when the subfield is read; `sensitive:` is not part of that read — it resolves
|
|
185
|
+
only when something requests redaction (see Gotchas). Either way the parent is never mutated and
|
|
186
|
+
intermediates are never materialized; on an ambient parent (`on: :ambient_context`) only
|
|
187
|
+
`user_facing:` is unsupported. Subfield hashes accept string **or** symbol keys (indifferent). Source:
|
|
167
188
|
`lib/axn/core/field_resolvers/extract.rb`. Reference:
|
|
168
189
|
<https://teamshares.github.io/axn/reference/class>.
|
|
169
190
|
|
|
@@ -190,6 +211,9 @@ reported to `Axn.config.on_exception`. Key consequences:
|
|
|
190
211
|
the idiom for the "save an ActiveRecord model" case — a plain action plus
|
|
191
212
|
`fails_on ActiveRecord::RecordInvalid, &:message` surfaces the record's validation errors as the
|
|
192
213
|
failure message (and, e.g., `fails_on Stripe::CardError, &:message` for a card-declined message).
|
|
214
|
+
`if:`/`unless:` (evaluated against the action at settlement, same mechanism as `error`/callbacks)
|
|
215
|
+
gate the reclassification itself, not just the message — a condition living *inside* the message
|
|
216
|
+
block never did that, which is the footgun `if:`/`unless:` exist to close.
|
|
193
217
|
- **`expects` violations are dev-facing by default** → exception bucket, pages, generic
|
|
194
218
|
`"Something went wrong"`. A missing required input is your bug. Mark a genuinely caller-supplied
|
|
195
219
|
field `user_facing: true` (or a String/Symbol/Proc message) to move *its* violations to the
|
|
@@ -215,12 +239,28 @@ error "email already taken", if: ArgumentError # reason → "Couldn't sync use
|
|
|
215
239
|
fail! "missing field" # reason → "Couldn't sync user: missing field"
|
|
216
240
|
```
|
|
217
241
|
|
|
218
|
-
Composing actions: a base `error` on the parent auto-prefixes
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
242
|
+
Composing actions: a base `error` on the parent auto-prefixes the child's **resolved `result.error`**
|
|
243
|
+
surfaced via `call!`, and this is **bucket-independent** — it applies whether the child failed via
|
|
244
|
+
`fail!`, a `fails_on`-classified exception, or an unexpected exception. What differs by bucket is the
|
|
245
|
+
*exception object*, not the message: `fail!` re-raises as `Axn::Failure`, while a `fails_on`-matched
|
|
246
|
+
or unhandled exception bubbles as the **original** exception. Either way the child's message is woven
|
|
247
|
+
in (`"Onboarding failed: Charge failed: card declined"`) — **unless the parent itself declares a
|
|
248
|
+
matching conditional reason**, which *replaces* the child's presentation rather than prefixing it
|
|
249
|
+
(`error "Record not found", if: NotFoundErr` on the parent yields `"Onboarding failed: Record not
|
|
250
|
+
found"`, and `standalone: true` drops the parent's base too, leaving `"Record not found"`). So a
|
|
251
|
+
parent that authors its own reason for an exception class opts out of carrying the child's context.
|
|
252
|
+
An *unexpected* exception still picks up a
|
|
253
|
+
leaf if a conditional `error "…", if: SomeError` matches it — reason matching is independent of
|
|
254
|
+
`fails_on` — giving `"Onboarding failed: Charge failed: retry later"` while the outcome stays
|
|
255
|
+
`exception`. Only with **no matching reason** is there no leaf, and then just the declared base
|
|
256
|
+
headers chain (`"Onboarding failed: Charge failed"`). The raw exception message never enters
|
|
257
|
+
`result.error` **by default** — it stays the technical `#message` on `result.exception` — but you can
|
|
258
|
+
opt a class in explicitly with `error(if: SomeError, &:message)` or `fails_on SomeError, &:message`,
|
|
259
|
+
and once opted in it aggregates like any other reason (`"Onboarding failed: Charge failed: <raw
|
|
260
|
+
message>"`). Only do that where the message is genuinely user-facing. A level declaring no base
|
|
261
|
+
contributes nothing. Reach for non-bang `call` +
|
|
262
|
+
`fail!("context: #{child.error}")` when you want to author a *different* message than this automatic
|
|
263
|
+
aggregation, or to add per-call context — not to carry the child's message through.
|
|
224
264
|
|
|
225
265
|
⚠️ **Message bodies are NOT redacted** and propagate outward to every ancestor's `result.error`,
|
|
226
266
|
logs, and error trackers. Never interpolate secrets/PII into `error`/`success`/`fail!` text — put
|
|
@@ -246,8 +286,17 @@ sensitive values in `sensitive:` fields. Detail:
|
|
|
246
286
|
unchanged (not wrapped). `fails_on` reclassification is sticky across `call!` boundaries.
|
|
247
287
|
- **Hooks vs callbacks.** A raise/`fail!` in a `before`/`after`/`around` hook flips `ok?` to false; a
|
|
248
288
|
raise in a callback (`on_success` etc.) is reported but does **not** change `ok?`.
|
|
249
|
-
-
|
|
250
|
-
|
|
289
|
+
- **Callable-option timing.** A `sensitive:` Proc is `instance_exec`'d with **no arguments** (write
|
|
290
|
+
`sensitive: -> { !include_pii }`, reading other fields by name — a lambda declaring a parameter
|
|
291
|
+
raises), and it resolves lazily, only when something actually redacts. By then `default:`s are
|
|
292
|
+
applied, so it reads another field's *defaulted* value. `preprocess:` is the opposite: it runs
|
|
293
|
+
*before* defaults, seeing `nil` for an omitted top-level field. Only a **non-nil** result bypasses
|
|
294
|
+
the `default:` — return `nil` and the default still applies, so an intentional nil does not survive
|
|
295
|
+
(`false` does, matching `default:`'s missing-or-nil rule). On a **subfield** the preprocessor runs
|
|
296
|
+
only when the leaf's immediate parent is present: `parent: {}` still invokes it with `nil`, but an
|
|
297
|
+
absent or `nil` parent skips it entirely. The subfield's own `default:` still resolves in that case
|
|
298
|
+
— the reader returns the default, and is `nil` only when there is none. So don't rely on a nested
|
|
299
|
+
`preprocess:` to manufacture a value when the parent is missing; declare a `default:` for that.
|
|
251
300
|
|
|
252
301
|
## Strategies (DRYed configuration via `use`)
|
|
253
302
|
|
|
@@ -264,13 +313,25 @@ chains existing action classes. Data flows via the shared context; a step failur
|
|
|
264
313
|
with the step name prefixed (`"validate: Email is invalid"`).
|
|
265
314
|
<https://teamshares.github.io/axn/usage/steps>.
|
|
266
315
|
|
|
316
|
+
## Declaring your entry point
|
|
317
|
+
|
|
318
|
+
If you're building a gem that dispatches Axns on behalf of an external trigger (an inbound webhook, a
|
|
319
|
+
scheduled job runner, a queue consumer) rather than calling them straight from app code, wrap your
|
|
320
|
+
dispatch in `Axn::Extensions::InvokedVia.with(:your_gem) { handler.call!(**args) }`. The value becomes
|
|
321
|
+
the `invoked_via` dimension on the whole call tree — every nested sub-Axn, span, log line, metric, and
|
|
322
|
+
exception report — so an app can tell "traffic that came in through your gem" apart from ordinary
|
|
323
|
+
direct calls without touching the classes it dispatches. A tool-adapter gem gets this automatically
|
|
324
|
+
through `Axn::Tools::Invoker`'s `adapter:` kwarg; everyone else calls `InvokedVia.with` once, at the
|
|
325
|
+
outermost point they control. `dimension :invoked_via` / `tag :invoked_via` are reserved — declaring
|
|
326
|
+
either raises. See `docs/recipes/declaring-entry-points.md` for the full recipe.
|
|
327
|
+
|
|
267
328
|
## Pointers
|
|
268
329
|
|
|
269
330
|
Human docs — <https://teamshares.github.io/axn/>:
|
|
270
331
|
build (`/usage/writing`), use (`/usage/using`), class DSL (`/reference/class`), instance helpers
|
|
271
332
|
(`/reference/instance`), result (`/reference/axn-result`), strategies (`/strategies/`), steps
|
|
272
333
|
(`/usage/steps`), async (`/reference/async`), config (`/reference/configuration`), tool invoker
|
|
273
|
-
(`/reference/tool-invoker`).
|
|
334
|
+
(`/reference/tool-invoker`), entry points (`/recipes/declaring-entry-points`).
|
|
274
335
|
|
|
275
336
|
Source entry points (resolve with `bundle show axn`):
|
|
276
337
|
- `lib/axn.rb` — `include Axn` wiring.
|
|
@@ -279,4 +340,4 @@ Source entry points (resolve with `bundle show axn`):
|
|
|
279
340
|
- `lib/axn/core/validation/validators/` — `type`, `of`, `model`, `validate`, `shape` validators.
|
|
280
341
|
- `lib/axn/core/flow/` — `messages.rb`, `fails_on.rb`, `handlers/` (failure/message/callback resolution).
|
|
281
342
|
- `lib/axn/result.rb`, `lib/axn/core/context/facade.rb` — the `Result` surface.
|
|
282
|
-
- `lib/axn/strategies/` — `
|
|
343
|
+
- `lib/axn/strategies/` — `form.rb`, `transaction.rb`; `lib/axn/extras/strategies/client.rb` (`use :client`).
|
data/AGENTS-tool-adapters.md
CHANGED
|
@@ -48,6 +48,11 @@ GemName.wrap(axn_class, **opts) # one Axn -> the transport's native tool object
|
|
|
48
48
|
- **Directory membership is optional.** `extend Axn::Tools::AdapterRoots` → a validated `tool_roots`
|
|
49
49
|
setting the registry reads. Its `validate!` reuses core's broad-path guard (rejects `app`/`actions`/`.`/`..`).
|
|
50
50
|
The reference gems don't adopt it (they use explicit `tool`/`configure`); add it only if it fits.
|
|
51
|
+
- **Shipping a non-empty default root? Call `tool_roots_default %w[agent_tools]`** — don't re-declare
|
|
52
|
+
`setting :tool_roots` by hand, which means hand-copying `AdapterRoots.validate!`'s lambda too.
|
|
53
|
+
`tool_roots_default` validates EAGERLY at the call site (fails at gem load, not at the registry's first
|
|
54
|
+
read) and re-declares through the normal `setting` path, so an app's own assignment still wins and
|
|
55
|
+
`config.reset!(:tool_roots)` returns to the adapter's default, not core's `[]`.
|
|
51
56
|
|
|
52
57
|
Source: `lib/axn/tools.rb` (`.register_adapter`, `.for`, `.versions`), `lib/axn/tools/registry.rb` (membership,
|
|
53
58
|
eager-load), `lib/axn/tools/adapter_roots.rb`, `lib/axn/core/tool_declaration.rb` (`tool` DSL, `tool_name`).
|
|
@@ -103,8 +108,17 @@ Source: `lib/axn/core/schema_reflection.rb`, `lib/axn/internal/reflection/schema
|
|
|
103
108
|
pre-pass — it drifts from the renderer.
|
|
104
109
|
- Keep the two guarantees apart: encodability is unconditional, declared-shape is what the flag buys.
|
|
105
110
|
`reject_opaque: false` never means "might not be JSON" — that is why it isn't named `strict:`.
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
- This is call-time by construction, not a boot check: the verdict is about the exposed VALUE's own class
|
|
112
|
+
(does it define its own `to_h`/`as_json`), and a field's declared `type:` is only a lower bound on what
|
|
113
|
+
could show up there — a subclass or a singleton method can make an otherwise-opaque declared type render
|
|
114
|
+
cleanly. A check keyed on the declared class alone would refuse programs that run correctly.
|
|
115
|
+
- **If `reject_opaque:` is per-tool (usually is), call `YourAdapter.serialize_exposed(result)` instead of
|
|
116
|
+
`render` directly.** `Axn::Tools::AdapterSerialization` bundles the declaration and the
|
|
117
|
+
resolve-then-render chain — see Per-adapter configuration below. `render(result, reject_opaque:
|
|
118
|
+
config.reject_opaque)` reading the flag off the gem-wide `config` is the mistake this exists to
|
|
119
|
+
prevent: it ignores any per-tool `configure(:key)` / `tool key: { … }` override.
|
|
120
|
+
|
|
121
|
+
Source: `lib/axn/extensions/serialization.rb` (the renderer itself is `lib/axn/internal/reflection/values.rb`, core-internal), `lib/axn/tools/adapter_serialization.rb`.
|
|
108
122
|
|
|
109
123
|
## Per-adapter configuration
|
|
110
124
|
|
|
@@ -117,8 +131,15 @@ Source: `lib/axn/extensions/serialization.rb` (the renderer itself is `lib/axn/i
|
|
|
117
131
|
- A **render toggle** (structured serialized `exposes` vs. the Axn's message) is a common per-adapter
|
|
118
132
|
setting. `axn-mcp` and `axn-ruby_llm` both name it `present_as` (`:structured` / `:message`) — reuse the
|
|
119
133
|
name/values if you have the concept. It's adapter-specific, not core (an `axn-http_api` has no such toggle).
|
|
134
|
+
- **`reject_opaque_exposed_values` is shared — declare it with `extend Axn::Tools::AdapterSerialization` +
|
|
135
|
+
`declare_reject_opaque_exposed_values! default: <bool>`**, not a hand-written `setting`. `default:` is a
|
|
136
|
+
REQUIRED kwarg with no core-picked value: an HTTP adapter with a published `output_schema` wants `true`
|
|
137
|
+
(axn-openapi); an LLM-facing adapter usually wants `false` — ugly-but-honest beats a failed call
|
|
138
|
+
(axn-mcp, axn-ruby_llm). Never fix the default in a shared declaration; parameterize it.
|
|
139
|
+
- **The transport-mapping step needs its own never-raises guard — use `guard_tool_response`,** from the
|
|
140
|
+
same mixin. See Invocation & result → response below.
|
|
120
141
|
|
|
121
|
-
Source: `lib/axn/configurable.rb` (`config_namespace`, `resolve_override_for`, `overrides`).
|
|
142
|
+
Source: `lib/axn/configurable.rb` (`config_namespace`, `resolve_override_for`, `overrides`), `lib/axn/tools/adapter_serialization.rb`.
|
|
122
143
|
|
|
123
144
|
## Extension registry
|
|
124
145
|
|
|
@@ -135,6 +156,9 @@ Source: `lib/axn/extensions/config.rb`, `lib/axn/core/semantic_hints.rb`.
|
|
|
135
156
|
(always-on wire coercion, opt-in user-facing input-error surfacing, undeclared-key rejection, the
|
|
136
157
|
ambient guard) that a trusted in-process `.call` deliberately omits. See
|
|
137
158
|
<https://teamshares.github.io/axn/reference/tool-invoker>.
|
|
159
|
+
- Pass `adapter: :your_key` to `Invoker.new` and every call it makes is stamped with the `invoked_via`
|
|
160
|
+
dimension across its whole call tree — the seam that lets an app's dashboard separate tool-driven
|
|
161
|
+
traffic from ordinary direct calls. See <https://teamshares.github.io/axn/recipes/declaring-entry-points>.
|
|
138
162
|
- Map from: `result.ok?`; `result.error` (**user-facing** — show to the LLM/client); `result.success` /
|
|
139
163
|
`result.message` (success string); `result.exception` (**dev-facing** detail, e.g. the
|
|
140
164
|
`Axn::InboundValidationError` — do **NOT** surface it).
|
|
@@ -147,8 +171,17 @@ Source: `lib/axn/extensions/config.rb`, `lib/axn/core/semantic_hints.rb`.
|
|
|
147
171
|
`fail!("…", standalone: true)`.
|
|
148
172
|
- For per-field inbound detail: `Axn::Tools::Invoker.input_invalid?(result)` and
|
|
149
173
|
`result.exception.field_errors`.
|
|
150
|
-
|
|
151
|
-
|
|
174
|
+
- **Never let the mapping step escape.** `axn_class.call` (via the Invoker) never raises, but the
|
|
175
|
+
result→response mapping AFTER it — `serialize_exposed`/render, your own response-building code — runs
|
|
176
|
+
outside core's executor and CAN raise. Wrap just that step:
|
|
177
|
+
`YourAdapter.guard_tool_response(axn_class, on_error: ->(e) { your_error_response }) { map_result_to_response }`.
|
|
178
|
+
It reports through `Axn.config.on_exception`, re-raises when `Axn::Extensions.raises_in_dev?`, and
|
|
179
|
+
otherwise calls `on_error` with the exception so you build the transport-native error response (this
|
|
180
|
+
method never constructs one). **Scope it to the mapping only — never to `axn_class.call`** (already
|
|
181
|
+
reports its own exceptions; wrapping both double-reports).
|
|
182
|
+
|
|
183
|
+
Source: `lib/axn/extensions.rb` (`owned_failure?`), `lib/axn/tools/invoker.rb`, `lib/axn/result.rb`,
|
|
184
|
+
`lib/axn/tools/adapter_serialization.rb` (`guard_tool_response`).
|
|
152
185
|
|
|
153
186
|
## Error boundary
|
|
154
187
|
|
|
@@ -174,6 +207,20 @@ Source: `lib/axn/extensions.rb` (`owned_failure?`), `lib/axn/tools/invoker.rb`,
|
|
|
174
207
|
|
|
175
208
|
Source: `lib/axn/error.rb`.
|
|
176
209
|
|
|
210
|
+
## Span annotation
|
|
211
|
+
|
|
212
|
+
If your gem needs to write vendor-namespaced OTel attributes it can't know at declaration time
|
|
213
|
+
(`gen_ai.*`, `db.*`, …), use `Axn::Extensions::Tracing.annotate_span(**attrs)` — never
|
|
214
|
+
`OpenTelemetry::Trace.current_span`. That resolves through `OpenTelemetry::Context.current`, ambient
|
|
215
|
+
process-wide state anything else running mid-chain can move (verified against the Datadog OTel bridge in
|
|
216
|
+
production, PRO-3278); `annotate_span` is the span axn's own tracer opened for the action currently
|
|
217
|
+
running, direct, no guessing. It no-ops (no raise) when there is no span, skips a `nil` value, and
|
|
218
|
+
converts a Symbol key to a String. `axn-ruby_llm`'s `record_otel_attributes!` is the reference use.
|
|
219
|
+
`Axn::Extensions::Tracing.current_span` is the raw accessor for anything beyond attributes — valid only
|
|
220
|
+
for the duration of the action's own body; don't hold it past the call.
|
|
221
|
+
|
|
222
|
+
Source: `lib/axn/extensions/tracing.rb`, `docs/reference/configuration.md#annotating-the-span-from-your-own-code`.
|
|
223
|
+
|
|
177
224
|
## ambient_context
|
|
178
225
|
|
|
179
226
|
Server/session data (`current_user`, `company`) an author declares via `expects :user_id, on: :ambient_context`.
|
|
@@ -234,14 +281,17 @@ Source: `lib/axn/core/ambient_context.rb`, `lib/axn/tools/invoker.rb`.
|
|
|
234
281
|
|
|
235
282
|
Docs — <https://teamshares.github.io/axn/>: authoring a tool-adapter gem
|
|
236
283
|
(`/recipes/authoring-tool-adapters`), tool invoker (`/reference/tool-invoker`), gem configuration
|
|
237
|
-
(`/recipes/gem-configuration`),
|
|
238
|
-
(`/reference/
|
|
284
|
+
(`/recipes/gem-configuration`), declaring an entry point (`/recipes/declaring-entry-points`), factory
|
|
285
|
+
(`/reference/factory`), class DSL (`/reference/class`), result (`/reference/axn-result`).
|
|
286
|
+
Action-authoring: `AGENTS-consuming.md` (this gem).
|
|
239
287
|
|
|
240
288
|
Core source entry points (resolve with `bundle show axn`):
|
|
241
289
|
- `lib/axn/tools.rb` — `Axn::Tools.register_adapter`, `.adapters`, `.for`, `.versions`, `.validate_contracts!`.
|
|
242
290
|
- `lib/axn/extensions.rb` — `Axn::Extensions.best_effort`, `.config`, `.owned_failure?` (extension-author surface).
|
|
243
291
|
- `lib/axn/tools/registry.rb`, `lib/axn/tools/adapter_roots.rb`, `lib/axn/core/tool_declaration.rb` —
|
|
244
292
|
membership, `tool_name`.
|
|
293
|
+
- `lib/axn/tools/adapter_serialization.rb` — `declare_reject_opaque_exposed_values!`,
|
|
294
|
+
`serialize_exposed`, `guard_tool_response`.
|
|
245
295
|
- `lib/axn/core/schema_reflection.rb`, `lib/axn/internal/reflection/schema.rb`, `lib/axn/internal/reflection/values.rb` — reflection.
|
|
246
296
|
- `lib/axn/configurable.rb` — `config_namespace`, `resolve_override_for`, `overrides`.
|
|
247
297
|
- `lib/axn/tools/invoker.rb` — the tool call path.
|