@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.84.1
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.
- package/CHANGELOG.md +169 -3
- package/README.md +7 -3
- package/SECURITY.md +4 -3
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +28 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +13 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +275 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +17 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +6 -5
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md
CHANGED
|
@@ -51,13 +51,15 @@ record, which, not `decisions.jsonl` (one line per message id decided), counts t
|
|
|
51
51
|
`on_handoff` again, so the repair step bounds its retries (one repair is the practices') and passes any further
|
|
52
52
|
hand-off to the person's channel. A copy of `support_agent.py` made before 0.55.0 takes no `on_handoff` in
|
|
53
53
|
`from_config` and hangs on such a call (`grep -c ReentryError support_agent.py` prints 0 for it; `fabric_kit.py
|
|
54
|
-
status` covers the kit modules only): copy it again. The recipe also lacks
|
|
54
|
+
status` covers the kit modules only): copy it again. The recipe also lacks two parts of such an agent: the Lookup
|
|
55
55
|
(shared words, best three) is a relevance rule of its own, where the agent reads the knowledge service's per-item
|
|
56
|
-
`answer` flag and puts small closed rule sets into the prompt whole;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
`answer` flag and puts small closed rule sets into the prompt whole; and the decision does not record which model
|
|
57
|
+
answered, so fallback recoveries are not counted. A fallback of the writer's own vendor is refused when the agent is
|
|
58
|
+
built (`RecipeError`), not per call. **A person's categories come first** (since 0.83.0): `policy` (a config key, or
|
|
59
|
+
`policy=` on `SupportAgent`) maps each category a person must handle to the words that name it
|
|
60
|
+
(`{"refund": ["refund", "money back"], "recovery": ["recover my account"]}`); a message holding one is a hand-off
|
|
61
|
+
`policy:<category>` before any lookup or model call, so a refund is never a false knowledge gap. Count `policy:*`
|
|
62
|
+
as `policy`, never `inability`. Count its hand-offs by cause: a failed check is a refusal for the agent to repair, research and
|
|
61
63
|
hold, and only a hold on the material counts as `inability` (a hold after an outage or at the research time cap is
|
|
62
64
|
`infrastructure`); `unsupported:amount` is a money reason, never researched, and so is `unsupported:number` unless the
|
|
63
65
|
refused `draft` shows the number is not a price; `no-knowledge` and `model-unsure` are `inability`;
|
|
@@ -99,7 +101,8 @@ python3 <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/s
|
|
|
99
101
|
install <service>/support --lang py --module fabric_openrouter --module fabric_usage --module fabric_paths \
|
|
100
102
|
--module fabric_connections
|
|
101
103
|
cp <adapter>/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py <service>/support/
|
|
102
|
-
python3 <adapter>/.../fabric_kit.py status <service>/support # current, older or modified
|
|
104
|
+
python3 <adapter>/.../fabric_kit.py status <service>/support # the kit modules: current, older or modified
|
|
105
|
+
# support_agent.py is not a kit module: compare it with the recipe's copy (diff) after each adapter release
|
|
103
106
|
```
|
|
104
107
|
|
|
105
108
|
## The knowledge file as a source
|
|
@@ -137,7 +140,8 @@ FAC-SEM-162 to 165 (rule `channel-agent.declaration`).
|
|
|
137
140
|
|
|
138
141
|
| Key | Meaning |
|
|
139
142
|
|---|---|
|
|
140
|
-
| `knowledge` | the knowledge file: `{"entries": [{"id", "question", "answer", "tags"}]}`, ids unique |
|
|
143
|
+
| `knowledge` | the knowledge file: `{"entries": [{"id", "question", "answer", "tags"}]}`, ids unique and citable (no colon, at most 64) |
|
|
144
|
+
| `policy` | optional: `{category: [words]}`, the categories a person handles, routed before the lookup and the model |
|
|
141
145
|
| `knowledgeMaxAgeSeconds` | optional: the knowledge file is then a source with an age and a last good version (above) |
|
|
142
146
|
| `stateDir` | the decisions (`decisions.jsonl`), the handoff outbox (`handoffs.jsonl`) and the usage ledger (`usage.jsonl`) |
|
|
143
147
|
| `model`, `fallback` | two models of different vendors; a lane that reads what customers write runs on an open-vendor model (DEC-0083) |
|
|
@@ -169,15 +173,26 @@ Exit 1 on a broken config or knowledge file. A missing key is not a crash: the m
|
|
|
169
173
|
From a service's capability handler, build the agent once with `support_agent.from_config(path)` and call
|
|
170
174
|
`agent.answer(message_id, text, task_run_id=...)`; pass `on_handoff=` to `from_config` or `SupportAgent` to route
|
|
171
175
|
handoffs to the operator's channel instead of the outbox file. A handoff carries the customer's text as `question` and
|
|
172
|
-
its time as `at`.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
176
|
+
its time as `at`. The handler catches `ReentryError` before `RecipeError` (it is a subclass) and answers it as the
|
|
177
|
+
service's own fault, never the caller's: a retry of the same message id would pay for its draft again. From inside
|
|
178
|
+
`on_handoff` on the same thread, `agent.prune(...)` completes (the agent's lock is reentrant, held by a test), while
|
|
179
|
+
`agent.answer(...)` is refused with `ReentryError` before any input check, so even a bad id or an empty text there is a
|
|
180
|
+
`ReentryError`.
|
|
181
|
+
|
|
182
|
+
A Node agent (the communication template's) runs the same checks with `support-agent.mjs`: `checkReply(draft, question,
|
|
183
|
+
given, {maxChars, sources})`, the twin of `check_reply`, answers every case in
|
|
184
|
+
`test/fixtures/support-check-reply-cases.json` as Python does. Knowledge ids are what a citation can name: letters,
|
|
185
|
+
digits, `.`, `_` and `-`, at most 64; an entry whose id has a colon or is longer is refused at load (map it).
|
|
186
|
+
|
|
187
|
+
## Retention: the outbox keeps what customers wrote, redacted
|
|
188
|
+
|
|
189
|
+
Every handoff row in `handoffs.jsonl` holds the customer's message as `question`, redacted since 0.83.0: `redact()`
|
|
190
|
+
replaces an email address, a card number, a phone number or other long digit run, and a long key-shaped run with a
|
|
191
|
+
marker (`[email]`, `[card]`, `[number]`, `[secret]`) before the row is written (the standard's A4 and A9). The rest
|
|
192
|
+
stays until it is pruned. Choose a retention the product's privacy policy states, at most 30 days for an agent that
|
|
193
|
+
answers people (the standard's A1). An agent that passes its own `on_handoff` replaces the outbox and keeps its own
|
|
194
|
+
store: it redacts what it stores (`support_agent.redact` is there to call) and prunes that store itself, since `prune`
|
|
195
|
+
covers only `handoffs.jsonl` and `decisions.jsonl`. A refused `draft`, which can echo what the customer wrote,
|
|
181
196
|
reaches the returned decision and an `on_handoff` the agent passes, never a file: neither `decisions.jsonl` nor the
|
|
182
197
|
default outbox stores it. A capability handler returns the decision without it: a kit job's result is written to
|
|
183
198
|
the job's file (`fabric_interop`'s job store), so pass `draft` only to the repair step. Prune on a schedule:
|
|
@@ -12,5 +12,9 @@
|
|
|
12
12
|
"prices": {
|
|
13
13
|
"deepseek/deepseek-v4.1-flash": {"prompt": 0.0000002, "completion": 0.0000008},
|
|
14
14
|
"qwen/qwen3.8-flash": {"prompt": 0.0000001, "completion": 0.0000004}
|
|
15
|
+
},
|
|
16
|
+
"policy": {
|
|
17
|
+
"refund": ["refund", "money back", "chargeback"],
|
|
18
|
+
"recovery": ["recover my account", "account was stolen"]
|
|
15
19
|
}
|
|
16
20
|
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// The code checks of the support-agent recipe for a Node agent (FAA-264 item 10): checkReply, the gate the
|
|
2
|
+
// communication template runs on every final text, and what it reads (unsupported, scriptOf). The twin of
|
|
3
|
+
// support_agent.py's check_reply; both answer the cases in test/fixtures/support-check-reply-cases.json the same way.
|
|
4
|
+
// Python's Unicode classes are written out (\w as [\p{L}\p{N}_], \d as \p{Nd}, \b as lookarounds), since a JavaScript
|
|
5
|
+
// \w, \d and \b are ASCII. Node 20+, no dependency.
|
|
6
|
+
const W = '\\p{L}\\p{N}_';
|
|
7
|
+
const LETTER = '\\p{L}\\p{Nl}\\p{No}'; // Python's [^\W\d_]: a word character that is no decimal digit and no underscore
|
|
8
|
+
const DASHES = new RegExp(`[\\u2012\\u2014\\u2015\\u2e3a\\u2e3b\\ufe58]|--|(?<=[${LETTER}])[ \\u00a0][-\\u2013]\\s(?=[${LETTER}])`, 'u');
|
|
9
|
+
const CITATION = /\[KB:([A-Za-z0-9][A-Za-z0-9._-]{0,63})\]/g;
|
|
10
|
+
const URL_RUN = /https?:\/\/\S+/gu;
|
|
11
|
+
const CCTLD = ('ac ad ae af ag ai al am ao aq ar as at au aw ax az ba bb bd be bf bg bh bi bj bm bn bo br bs bt bw by bz ca '
|
|
12
|
+
+ 'cc cd cf cg ch ci ck cl cm cn co cr cu cv cw cx cy cz de dj dk dm do dz ec ee eg er es et eu fi fj fk fm fo '
|
|
13
|
+
+ 'fr ga gd ge gf gg gh gi gl gm gn gp gq gr gs gt gu gw gy hk hm hn hr ht hu id ie il im in io iq ir is it je '
|
|
14
|
+
+ 'jm jo jp ke kg kh ki km kn kp kr kw ky kz la lb lc li lk lr ls lt lu lv ly ma mc md me mg mh mk ml mm mn mo '
|
|
15
|
+
+ 'mp mq mr ms mt mu mv mw mx my mz na nc ne nf ng ni nl no np nr nu nz om pa pe pf pg ph pk pl pm pn pr ps pt '
|
|
16
|
+
+ 'pw py qa re ro rs ru rw sa sb sc sd se sg sh si sk sl sm sn so sr ss st su sv sx sy sz tc td tf tg th tj tk '
|
|
17
|
+
+ 'tl tm tn to tr tt tv tw tz ua ug uk us uy uz va vc ve vg vi vn vu wf ws ye yt za zm zw');
|
|
18
|
+
const GTLD = ('com net org info biz edu gov mil int app dev xyz online site shop store tech cloud email link live news blog '
|
|
19
|
+
+ 'page help support pro club top vip one world today space website mobi name travel agency company services '
|
|
20
|
+
+ 'solutions digital network media studio design money finance bank pay refund claim click download win '
|
|
21
|
+
+ 'рф рус укр бел срб');
|
|
22
|
+
const TLDS = [...new Set(`${CCTLD} ${GTLD}`.split(' '))].sort((a, b) => b.length - a.length || (a < b ? -1 : a > b ? 1 : 0)).join('|');
|
|
23
|
+
const STOP = '[^\\s<>()"\'\\[\\]]';
|
|
24
|
+
const LINK = new RegExp(`(?:(?<![${W}])https?://|(?<![${W}@.-])www\\.)${STOP}+`
|
|
25
|
+
+ `|(?<![${W}@.\\-/])(?:[\\p{L}\\p{N}](?:[${W}-]{0,61}[\\p{L}\\p{N}])?\\.)+(?:${TLDS})(?![${W}-])(?::\\p{Nd}{1,5})?(?:[/?#]${STOP}*)?`, 'giu');
|
|
26
|
+
const EMAIL = new RegExp(`[${W}.+-]+@[${W}-]+(?:\\.[${W}-]+)+`, 'gu');
|
|
27
|
+
const PHONE = new RegExp(`(?<![${W}+])\\+?\\p{Nd}(?:[ ().-]{0,2}\\p{Nd}){6,}`, 'gu');
|
|
28
|
+
const CURRENCY = { $: 'usd', '€': 'eur', '£': 'gbp', '¥': 'jpy', '₽': 'rub', '₹': 'inr', dollar: 'usd', dollars: 'usd', euro: 'eur', euros: 'eur', pound: 'gbp', pounds: 'gbp' };
|
|
29
|
+
const CODES = 'usd|eur|gbp|jpy|rub|inr|chf|cad|aud|cny|uah|pln|try';
|
|
30
|
+
const UNIT = `[$\\u20ac\\u00a3\\u00a5\\u20bd\\u20b9]|(?<![${W}])(?:${CODES}|dollars?|euros?|pounds?)(?![${W}])|руб[${W}]*`;
|
|
31
|
+
const NUM = '\\p{Nd}+(?:[.,]\\p{Nd}+)*';
|
|
32
|
+
const AMOUNT = new RegExp(`(${UNIT})\\s?(${NUM})|(${NUM})\\s?(${UNIT})`, 'giu');
|
|
33
|
+
const NUMBER = new RegExp(NUM, 'gu');
|
|
34
|
+
const TRAILING = ".,;:!?'\")]";
|
|
35
|
+
export const SCRIPTS = Object.freeze([
|
|
36
|
+
['latin', [[0x41, 0x5A], [0x61, 0x7A], [0xC0, 0x24F], [0x1E00, 0x1EFF]]],
|
|
37
|
+
['cyrillic', [[0x400, 0x52F]]],
|
|
38
|
+
['greek', [[0x370, 0x3FF]]],
|
|
39
|
+
['hebrew', [[0x590, 0x5FF]]],
|
|
40
|
+
['arabic', [[0x600, 0x6FF], [0x750, 0x77F]]],
|
|
41
|
+
['devanagari', [[0x900, 0x97F]]],
|
|
42
|
+
['thai', [[0xE00, 0xE7F]]],
|
|
43
|
+
['cjk', [[0x3040, 0x30FF], [0x3400, 0x4DBF], [0x4E00, 0x9FFF], [0xAC00, 0xD7AF]]],
|
|
44
|
+
]);
|
|
45
|
+
|
|
46
|
+
/** The script most of a text's letters are written in (citations and URLs left out), or null without letters. */
|
|
47
|
+
export function scriptOf(text) {
|
|
48
|
+
const counts = new Map();
|
|
49
|
+
for (const ch of text.replace(CITATION, ' ').replace(URL_RUN, ' ')) {
|
|
50
|
+
const code = ch.codePointAt(0);
|
|
51
|
+
const hit = SCRIPTS.find(([, ranges]) => ranges.some(([lo, hi]) => lo <= code && code <= hi));
|
|
52
|
+
if (hit) counts.set(hit[0], (counts.get(hit[0]) ?? 0) + 1);
|
|
53
|
+
}
|
|
54
|
+
let best = null;
|
|
55
|
+
for (const [name, n] of counts) if (best === null || n > counts.get(best) || (n === counts.get(best) && name > best)) best = name;
|
|
56
|
+
return best;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const currency = (unit) => {
|
|
60
|
+
const lowered = unit.toLowerCase();
|
|
61
|
+
return lowered.startsWith('руб') ? 'rub' : (CURRENCY[lowered] ?? lowered);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
function linkKey(link) {
|
|
65
|
+
let key = link.replace(/^https?:\/\//i, '');
|
|
66
|
+
while (key && (TRAILING + '/').includes(key[key.length - 1])) key = key.slice(0, -1);
|
|
67
|
+
const cut = key.search(/[/?#]/);
|
|
68
|
+
return cut < 0 ? key.toLowerCase() : key.slice(0, cut).toLowerCase() + key.slice(cut);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rstrip = (text, chars) => { let end = text.length; while (end && chars.includes(text[end - 1])) end -= 1; return text.slice(0, end); };
|
|
72
|
+
|
|
73
|
+
function claims(text) {
|
|
74
|
+
const amounts = new Set();
|
|
75
|
+
for (const m of text.matchAll(AMOUNT)) amounts.add(JSON.stringify([currency(m[1] ?? m[4]), m[2] ?? m[3]]));
|
|
76
|
+
return {
|
|
77
|
+
url: new Set([...text.matchAll(LINK)].map((m) => linkKey(m[0]))),
|
|
78
|
+
email: new Set([...text.matchAll(EMAIL)].map((m) => rstrip(m[0], TRAILING).toLowerCase())),
|
|
79
|
+
phone: new Set([...text.matchAll(PHONE)].map((m) => m[0].replace(/\P{Nd}/gu, ''))),
|
|
80
|
+
amount: amounts,
|
|
81
|
+
number: new Set([...text.matchAll(NUMBER)].map((m) => m[0])),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** unsupported:<kind> for each kind of claim in the reply that the source text does not carry. */
|
|
86
|
+
export function unsupported(reply, source) {
|
|
87
|
+
const said = claims(reply);
|
|
88
|
+
const known = claims(source);
|
|
89
|
+
const problems = [];
|
|
90
|
+
for (const kind of ['url', 'email', 'phone', 'amount', 'number']) {
|
|
91
|
+
const missing = kind === 'phone'
|
|
92
|
+
? [...said[kind]].filter((c) => ![...known[kind]].some((k) => k.endsWith(c)))
|
|
93
|
+
: [...said[kind]].filter((c) => !known[kind].has(c));
|
|
94
|
+
if (missing.length) problems.push(`unsupported:${kind}`);
|
|
95
|
+
}
|
|
96
|
+
return problems;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What code can prove about a draft: {ok, problems, citations, reply}; the twin of support_agent.check_reply. */
|
|
100
|
+
export function checkReply(draft, question, given, { maxChars, sources = {} } = {}) {
|
|
101
|
+
const problems = [];
|
|
102
|
+
const cited = [...new Set([...draft.matchAll(CITATION)].map((m) => m[1]))];
|
|
103
|
+
if (!cited.length) problems.push('no-citation');
|
|
104
|
+
const unknown = cited.filter((c) => !given.includes(c));
|
|
105
|
+
if (unknown.length) problems.push(`unknown-citation:${unknown.join(',')}`);
|
|
106
|
+
if (DASHES.test(draft)) problems.push('dash');
|
|
107
|
+
const reply = draft.replace(CITATION, '').replace(/[ \t]+/g, ' ').replaceAll(' .', '.').replaceAll(' ,', ',').trim();
|
|
108
|
+
if (!reply) problems.push('empty');
|
|
109
|
+
else if ([...reply].length > maxChars) problems.push('too-long');
|
|
110
|
+
problems.push(...unsupported(reply, cited.filter((c) => given.includes(c)).map((c) => (Object.hasOwn(sources, c) ? sources[c] : '')).join('\n')));
|
|
111
|
+
const wanted = scriptOf(question);
|
|
112
|
+
const written = scriptOf(reply);
|
|
113
|
+
if (wanted && written && wanted !== written) problems.push(`script:${written}-not-${wanted}`);
|
|
114
|
+
return { ok: problems.length === 0, problems, citations: cited, reply };
|
|
115
|
+
}
|
|
@@ -58,7 +58,8 @@ _DASHES = re.compile(r"[\u2012\u2014\u2015\u2e3a\u2e3b\ufe58]|--|(?<=[^\W\d_])[
|
|
|
58
58
|
_CITATION = re.compile(r"\[KB:([A-Za-z0-9][A-Za-z0-9._-]{0,63})\]")
|
|
59
59
|
_WORD = re.compile(r"\w+", re.UNICODE)
|
|
60
60
|
_URL = re.compile(r"https?://\S+")
|
|
61
|
-
_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}")
|
|
61
|
+
_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._:-]{0,127}") # a message id
|
|
62
|
+
_ENTRY_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9._-]{0,63}") # a knowledge id: exactly what _CITATION can name
|
|
62
63
|
# What a reply may carry only when the entries it cites carry it (D6): a link, an address, a phone number, an amount,
|
|
63
64
|
# a number. The customer's message never counts: an instruction hidden in it is exactly what this refuses.
|
|
64
65
|
# A bare host is a link only when its last label is a top-level domain people write in prose: every two-letter
|
|
@@ -154,8 +155,10 @@ class Knowledge:
|
|
|
154
155
|
def __init__(self, entries: List[Mapping[str, Any]]):
|
|
155
156
|
self.entries: Dict[str, Dict[str, Any]] = {}
|
|
156
157
|
for entry in entries:
|
|
157
|
-
if not isinstance(entry, dict) or not isinstance(entry.get("id"), str) or not
|
|
158
|
-
|
|
158
|
+
if not isinstance(entry, dict) or not isinstance(entry.get("id"), str) or not _ENTRY_ID.fullmatch(entry["id"]):
|
|
159
|
+
# the citation's own grammar (FAA-264): an id with a colon or over 64 characters could never be cited
|
|
160
|
+
raise RecipeError("every knowledge entry has an id a citation can name: letters, digits, '.', '_' or "
|
|
161
|
+
"'-', up to 64 (map one that has a colon)")
|
|
159
162
|
if not isinstance(entry.get("answer"), str) or not entry["answer"].strip():
|
|
160
163
|
raise RecipeError("knowledge entry %s has no answer" % entry["id"])
|
|
161
164
|
if "question" in entry and not isinstance(entry["question"], str):
|
|
@@ -452,10 +455,52 @@ def prune(state_dir: Path, keep_days: int, *, now: Optional[datetime] = None) ->
|
|
|
452
455
|
return removed
|
|
453
456
|
|
|
454
457
|
|
|
458
|
+
def check_policy(policy: Mapping[str, Any]) -> Dict[str, List[str]]:
|
|
459
|
+
"""Categories only a person handles (refunds, account recovery, legal), each a list of words or phrases; a message
|
|
460
|
+
that holds one is routed to a person before the lookup and the model, so a refund is never a knowledge gap."""
|
|
461
|
+
checked: Dict[str, List[str]] = {}
|
|
462
|
+
for category, phrases in dict(policy).items():
|
|
463
|
+
if not isinstance(category, str) or not _ENTRY_ID.fullmatch(category):
|
|
464
|
+
raise RecipeError("a policy category is a short id (letters, digits, '.', '_' or '-')")
|
|
465
|
+
if not isinstance(phrases, list) or not phrases or not all(isinstance(p, str) and p.strip() for p in phrases):
|
|
466
|
+
raise RecipeError("policy %s lists the words that route a message to a person" % category)
|
|
467
|
+
checked[category] = [p.strip().lower() for p in phrases]
|
|
468
|
+
return checked
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def policy_route(policy: Mapping[str, List[str]], text: str) -> Optional[str]:
|
|
472
|
+
"""The first category whose phrase the message holds (whole words, any case), or None."""
|
|
473
|
+
lowered = " %s " % " ".join(_WORD.findall(text.lower()))
|
|
474
|
+
for category, phrases in policy.items():
|
|
475
|
+
if any(" %s " % " ".join(_WORD.findall(p)) in lowered for p in phrases):
|
|
476
|
+
return category
|
|
477
|
+
return None
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
_REDACT = (
|
|
481
|
+
(re.compile(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+"), "[email]"),
|
|
482
|
+
(re.compile(r"(?<!\w)(?:\d[ -]?){13,19}(?!\w)"), "[card]"),
|
|
483
|
+
(re.compile(r"(?<![\w+])\+?\d[\d ()-]{6,}\d(?!\w)"), "[number]"),
|
|
484
|
+
(re.compile(r"[A-Za-z0-9+/=_-]{32,}"), "[secret]"),
|
|
485
|
+
)
|
|
486
|
+
|
|
487
|
+
|
|
488
|
+
def redact(text: str) -> str:
|
|
489
|
+
"""The customer's text as the outbox keeps it: an address, a card or phone number, and a long key-shaped run
|
|
490
|
+
replaced by a marker, so a person reading the queue never reads a code or a key the customer pasted."""
|
|
491
|
+
for pattern, marker in _REDACT:
|
|
492
|
+
text = pattern.sub(marker, text)
|
|
493
|
+
return text
|
|
494
|
+
|
|
495
|
+
|
|
455
496
|
class SupportAgent:
|
|
456
497
|
def __init__(self, *, knowledge: Union[Knowledge, "KnowledgeSource"], client: Any, model: str, fallback: str, state_dir: Path,
|
|
457
498
|
max_tokens: int = 600, max_chars: int = 1200, lookup_limit: int = 3,
|
|
458
|
-
on_handoff: Optional[Callable[[Dict[str, Any]], None]] = None
|
|
499
|
+
on_handoff: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
500
|
+
policy: Optional[Mapping[str, Any]] = None):
|
|
501
|
+
if fabric_openrouter.vendor(model) and fabric_openrouter.vendor(model) == fabric_openrouter.vendor(fallback):
|
|
502
|
+
raise RecipeError("the fallback is a model of another vendor than %s (refused at load, FAA-116)" % model)
|
|
503
|
+
self.policy = check_policy(policy or {})
|
|
459
504
|
self.knowledge, self.client = knowledge, client
|
|
460
505
|
self.model, self.fallback = model, fallback
|
|
461
506
|
self.state_dir = Path(state_dir)
|
|
@@ -492,6 +537,8 @@ class SupportAgent:
|
|
|
492
537
|
# The outbox keeps the customer's own text until it is pruned: see `prune` and the README's retention note.
|
|
493
538
|
fabric_paths.ensure_private_dir(self.state_dir)
|
|
494
539
|
kept = {k: v for k, v in decision.items() if k != "draft"} # a person reads the question and the reasons
|
|
540
|
+
if isinstance(kept.get("question"), str):
|
|
541
|
+
kept["question"] = redact(kept["question"]) # what a person reads carries no code, key, card or address
|
|
495
542
|
line = json.dumps(kept, ensure_ascii=False, separators=(",", ":")) + "\n"
|
|
496
543
|
_append_line(self.state_dir / "handoffs.jsonl", line.encode("utf-8"))
|
|
497
544
|
|
|
@@ -509,6 +556,9 @@ class SupportAgent:
|
|
|
509
556
|
rewrites the draft once with the refusal's reasons: a pass is a reply with `repaired: true`, and a repair that
|
|
510
557
|
does not end in a reply (a second refusal, a model `HANDOFF`, no knowledge on the retry; not a `model:*`
|
|
511
558
|
outage) a hand-off with `fallback: true` (the channel-agent standard's 4.4 and 13.2, FAA-197)."""
|
|
559
|
+
if getattr(self._local, "in_handoff", False): # first, so a bad input from on_handoff is a ReentryError too
|
|
560
|
+
raise ReentryError("answer() was called from on_handoff: queue the hand-off and call answer() again after "
|
|
561
|
+
"this call returns")
|
|
512
562
|
if not isinstance(message_id, str) or not _ID.fullmatch(message_id):
|
|
513
563
|
raise RecipeError("message id: letters, digits, '.', '_', ':' or '-', up to 128")
|
|
514
564
|
if not isinstance(text, str) or not text.strip():
|
|
@@ -518,9 +568,6 @@ class SupportAgent:
|
|
|
518
568
|
"decision or the on_handoff copy (a repeat returns no draft, and a fallback had its repair)")
|
|
519
569
|
if repair is not None and repair.get("messageId") == message_id:
|
|
520
570
|
raise RecipeError("repair= runs under a new message id: the refused one returns its stored decision")
|
|
521
|
-
if getattr(self._local, "in_handoff", False):
|
|
522
|
-
raise ReentryError("answer() was called from on_handoff: queue the hand-off and call answer() again after "
|
|
523
|
-
"this call returns")
|
|
524
571
|
text = re.sub("[\ud800-\udfff]", "\ufffd", text) # a lone surrogate (bytes a terminal could not decode) is kept, not fatal
|
|
525
572
|
with self._lock: # one decision per message id, in this process; the service's instance lock covers others
|
|
526
573
|
done = self._log.get(message_id)
|
|
@@ -540,6 +587,9 @@ class SupportAgent:
|
|
|
540
587
|
def _decide(self, message_id: str, text: str, task_run_id: Optional[str],
|
|
541
588
|
repair: Optional[Mapping[str, Any]] = None) -> Dict[str, Any]:
|
|
542
589
|
base = {"messageId": message_id, "citations": [], "costUsd": None}
|
|
590
|
+
routed = policy_route(self.policy, text)
|
|
591
|
+
if routed is not None: # a person's category goes to a person before any lookup or model call (FAA-116)
|
|
592
|
+
return dict(base, action="handoff", reasons=["policy:" + routed])
|
|
543
593
|
knowledge = self.knowledge.current() if isinstance(self.knowledge, KnowledgeSource) else self.knowledge
|
|
544
594
|
entries = knowledge.lookup(text, self.lookup_limit)
|
|
545
595
|
if not entries:
|
|
@@ -571,9 +621,10 @@ class SupportAgent:
|
|
|
571
621
|
def from_config(config_path: Path, *, on_handoff: Optional[Callable[[Dict[str, Any]], None]] = None,
|
|
572
622
|
**overrides: Any) -> SupportAgent:
|
|
573
623
|
"""An agent from a config file (see config.example.json): paths are relative to the config's folder. `on_handoff`
|
|
574
|
-
takes each hand-off in place of the outbox. Other keywords go to the model client
|
|
575
|
-
|
|
576
|
-
`
|
|
624
|
+
takes each hand-off in place of the outbox. Other keywords go to the model client as they are: the eleven the
|
|
625
|
+
config does not set (`base_url`, `attribution`, `unmeasured_call_usd`, `deadline_s`, `retries`, `environ`,
|
|
626
|
+
`transport`, `sleep`, `clock`, `now`, `rand`); the six it always sets (`ceiling_usd`, `window`, `ledger`, `prices`,
|
|
627
|
+
`key_env`, `project_id`) are not keywords here. `policy` in the config routes a person's categories first."""
|
|
577
628
|
config = json.loads(Path(config_path).read_text(encoding="utf-8"))
|
|
578
629
|
home = Path(config_path).resolve().parent
|
|
579
630
|
state = (home / config.get("stateDir", "state")).resolve()
|
|
@@ -593,7 +644,8 @@ def from_config(config_path: Path, *, on_handoff: Optional[Callable[[Dict[str, A
|
|
|
593
644
|
knowledge = Knowledge.load(home / config["knowledge"])
|
|
594
645
|
return SupportAgent(knowledge=knowledge, client=client, model=config["model"],
|
|
595
646
|
fallback=config["fallback"], state_dir=state, max_tokens=int(config.get("maxTokens", 600)),
|
|
596
|
-
max_chars=int(config.get("maxChars", 1200)), on_handoff=on_handoff
|
|
647
|
+
max_chars=int(config.get("maxChars", 1200)), on_handoff=on_handoff,
|
|
648
|
+
policy=config.get("policy"))
|
|
597
649
|
|
|
598
650
|
|
|
599
651
|
def main(argv: Optional[List[str]] = None) -> int:
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# A2A: serving a card and the binding, calling a peer
|
|
2
|
+
|
|
3
|
+
A service that other agents reach over the open A2A protocol (version 1.0, the contract's A2A peer profile) serves a
|
|
4
|
+
public Agent Card, answers A2A's JSON-RPC binding, and may call other A2A agents. The kit's module is
|
|
5
|
+
`fabric_a2a_server.py` with its Node twin `fabric-a2a-server.mjs` (FAA-10); `fabric_a2a.py` reads the card and the
|
|
6
|
+
well-known member (DEC-0122), and `check_service_a2a.py` holds the probe's A2A rules.
|
|
7
|
+
|
|
8
|
+
## The card
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from fabric_a2a import a2a_surface
|
|
12
|
+
from fabric_a2a_server import agent_card
|
|
13
|
+
|
|
14
|
+
card = agent_card(name="Example Agent", description="Drafts notes.", version="0.2.0",
|
|
15
|
+
url="http://127.0.0.1:47000/a2a/v1", capabilities=[{"name": "notes.draft", "description": "Drafts a note."}],
|
|
16
|
+
security_schemes={"own": {"apiKeySecurityScheme": {"location": "header", "name": "X-Example-Key"}}})
|
|
17
|
+
# serve card at GET /.well-known/agent-card.json, without the service token; declare it:
|
|
18
|
+
well_known["surfaces"]["a2a"] = a2a_surface()
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- Each capability is one `AgentSkill`; its id is the capability's name, the id the provider manifest's A2A profile
|
|
22
|
+
lists in `skillIds`.
|
|
23
|
+
- One interface: the JSON-RPC binding at `url`, `protocolVersion` 1.0. On a local placement the URL is on the service's
|
|
24
|
+
loopback origin; on a remote one it is https.
|
|
25
|
+
- `agent_card` runs `agent_card_findings` before it returns, so a card that names a member about the running process,
|
|
26
|
+
holds a value shaped like a credential, names the descriptor's token header as its scheme, or marks a Fabric
|
|
27
|
+
extension required is never served.
|
|
28
|
+
- **The interface never takes the descriptor's token** (DEC-0122 item 3). It has its own scheme (an API key of its own,
|
|
29
|
+
OAuth, or none on a loopback interface trusted under HA-3), and the service's `authorize` turns the request's headers
|
|
30
|
+
into a caller.
|
|
31
|
+
|
|
32
|
+
## The binding
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from fabric_a2a_server import A2ABinding, agent_message, text_of
|
|
36
|
+
|
|
37
|
+
def handler(start): # a new message
|
|
38
|
+
if text_of(start.message) == "ping":
|
|
39
|
+
return agent_message("pong") # a reply with no task
|
|
40
|
+
start.start("notes.draft") # or a task: a job in the interop job store
|
|
41
|
+
run_in_background(start.task_id) # the service moves the job: working, request_input, complete, fail
|
|
42
|
+
|
|
43
|
+
binding = A2ABinding(jobs, handler, skills=["notes.draft"], authorize=lambda headers: keys.get(headers.get("x-example-key")))
|
|
44
|
+
status, body = binding.handle(request_body_bytes, request_headers) # POST /a2a/v1
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| A2A 1.0 | The kit |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `SendMessage` | the handler replies (`{message}`) or starts a job (`{task}`); a message with `taskId` answers an input-required task |
|
|
50
|
+
| `SendStreamingMessage` | `binding.stream(params, caller, rid=...)`: server-sent events, the task first, then a `statusUpdate` at each change, ending at a terminal or input-required state |
|
|
51
|
+
| `GetTask` | the job as a Task |
|
|
52
|
+
| `CancelTask` | `JobStore.cancel`; an ended task answers -32002 |
|
|
53
|
+
| task id | the job id; only the caller that started it reads, answers or cancels it (any other gets -32001) |
|
|
54
|
+
| `TASK_STATE_WORKING`, `_INPUT_REQUIRED`, `_COMPLETED`, `_FAILED`, `_CANCELED` | job `working`, `input_required`, `completed`, `failed`, `cancelled` |
|
|
55
|
+
| an input-required question | the job's elicitation requests in the status message's data part; the answer is a data part `{inputResponses}` as `fabric.job.get` takes it, or plain text for a question of one field |
|
|
56
|
+
| the result | the job's result envelope as the artifact `result`, one data part |
|
|
57
|
+
| a failure | the status message names the job's error message and its code in a data part |
|
|
58
|
+
|
|
59
|
+
JSON-RPC codes follow A2A 1.0 section 5.4: -32001 task not found, -32002 not cancelable, -32004 unsupported, -32009 an
|
|
60
|
+
`A2A-Version` header that is not 1.0; -32700, -32600, -32601 and -32602 are JSON-RPC's own. A caller `authorize`
|
|
61
|
+
refuses gets HTTP 401; a body over 1 MiB gets 413.
|
|
62
|
+
|
|
63
|
+
## Calling a peer
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from fabric_a2a_server import A2AClient
|
|
67
|
+
|
|
68
|
+
client = A2AClient(["https://peer.example"], credential_env="EXAMPLE_PEER_KEY")
|
|
69
|
+
peer = client.discover("https://peer.example") # {card, url}
|
|
70
|
+
answer = client.call(peer["url"], "Draft a note about Monday.")
|
|
71
|
+
client.get(peer["url"], answer["task"]["id"]); client.cancel(peer["url"], answer["task"]["id"])
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
- **Only the operator's origins.** The client reaches an origin in its list, and the interface the card names must be
|
|
75
|
+
on one too.
|
|
76
|
+
- **The key from the environment.** `credential_env` names a variable that `use_secret.py run --vault-only <project>
|
|
77
|
+
<NAME> -- <command>` fills; the client never reads a key file beside the agent, and an error never carries the key.
|
|
78
|
+
- **Bounded.** Each call has a deadline (30 s) and a size cap (1 MiB) through `fabric_http`, follows no redirect and
|
|
79
|
+
never retries a `SendMessage`. A plain `http://` URL is refused unless it is loopback (and then its listener must be
|
|
80
|
+
this account's) or the client is built with `insecure=True`.
|
|
81
|
+
|
|
82
|
+
## The probe
|
|
83
|
+
|
|
84
|
+
`check_service_a2a.py <origin> [--remote] [--key-env NAME] [--probe-text TEXT]` reads `surfaces.a2a`, the card,
|
|
85
|
+
the interface and the binding's refusals (`a2a.surface`, `a2a.card`, `a2a.interface`, `a2a.task-not-found`, `a2a.cancel-not-found`,
|
|
86
|
+
`a2a.method-not-found`); `a2a.task-states` runs only with a message the service names as safe to send, since any
|
|
87
|
+
message may start real work. An interface that answers 401 without `--key-env` makes the binding rows NOT_RUN.
|
|
88
|
+
|
|
89
|
+
It is a command of its own, not a part of `check_service.py`: that probe's `a2a.card` reads the public card with no
|
|
90
|
+
Authorization header and never calls the card's interfaces (D5), while this one calls the interface with the
|
|
91
|
+
interface's own key. Run both for a service that serves A2A.
|
|
92
|
+
|
|
93
|
+
## Not here yet
|
|
94
|
+
|
|
95
|
+
- The interop test against a live Hermes A2A endpoint (`a2a_discover`, `a2a_call`): it waits for a Hermes A2A endpoint
|
|
96
|
+
reachable on this machine (FAA-10).
|
|
97
|
+
- The sample service serving both MCP and A2A.
|
|
98
|
+
- The REST and gRPC bindings, push notifications and `ListTasks`.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md
CHANGED
|
@@ -48,6 +48,16 @@ repository-relative path at a commit, an eval report). A decision without a numb
|
|
|
48
48
|
| building-fabric-services | at each release | contribute the release's measured wins as success cases and its new technical decisions |
|
|
49
49
|
| working-with-fabric | the retro after work | a lesson with a measured effect is also a success case: contribute it beside the lesson it records |
|
|
50
50
|
|
|
51
|
+
## A template and the communication prompt
|
|
52
|
+
|
|
53
|
+
For an agent that talks with people on a channel, the database's template and creating-fabric-agents' communication
|
|
54
|
+
agent prompt (`references/communication-agent-template.md`) are read together, never as two templates (the database's
|
|
55
|
+
own rule). Where they disagree, the template wins, in every part and not only the first-release ones: it is the merged
|
|
56
|
+
practice of the agents of its kind. A part the prompt carries and the template lacks (a first-release part, a check, a
|
|
57
|
+
pipeline step) is built too, unless the template rules it out, and goes back to the database as feedback on that
|
|
58
|
+
template (`agents-db.contribute`); the completion report names each such part. Without a template for the kind, the
|
|
59
|
+
prompt is the recipe.
|
|
60
|
+
|
|
51
61
|
## Recording the template
|
|
52
62
|
|
|
53
63
|
An agent built from a template declares it: `provenance.template` with its id and version, so a later template
|
|
@@ -121,7 +121,7 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
121
121
|
any other id as written. The mandate is a block fenced ` ```json `, the opening and the closing fence each at the start
|
|
122
122
|
of its own line, with no indent (lowercase; an indented fence, as inside a list item, is not read either; ` ````json `, ` ```JSON ` and `~~~` are not read), whose value has `kind: "mandate/1"`; any other JSON
|
|
123
123
|
block, a decision block included, is not read, with no warning. A fence quoted inside another fenced block is still
|
|
124
|
-
read: quote an example in prose, not in a fence. Two `mandate/1` blocks, whoever their chains end in, or one whose text holds `mandate/1` and is
|
|
124
|
+
read: quote an example in prose, not in a fence. Two `mandate/1` blocks, whoever their chains end in, or one whose text holds `mandate/1` (its slash plain or JSON-escaped, `\/` or `\u002f`, FAA-196 (13)) and is
|
|
125
125
|
not JSON (`NaN` included), read as none (a broken block without `mandate/1` is skipped, and a mandate beside it
|
|
126
126
|
is still read), and the responder emits `board.request.mandate_unread` (a warning) with the
|
|
127
127
|
request id and its `reason`, `mandate-blocks` or `mandate-unreadable`: the block holds his words, which reach no event, log or
|
|
@@ -131,6 +131,10 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
131
131
|
than 4300 digits on every Python (the kit refuses it there, as `int_max_str_digits` does from 3.11 and
|
|
132
132
|
the 3.8.14, 3.9.14 and 3.10.7 security releases; a limit configured
|
|
133
133
|
lower, `PYTHONINTMAXSTRDIGITS` on a Python that has one, refuses an integer longer than it).
|
|
134
|
+
- A request transferred to a new session runs again from a fresh `com.get`. Its mandate is read only when the message
|
|
135
|
+
read back carries the digest the claim took and names this request, capability and Project, as the first claim
|
|
136
|
+
checks; otherwise it is worked without one, and `board.request.mandate_unread` names `digest-mismatch` or
|
|
137
|
+
`not-addressed` (FAA-196 (1)).
|
|
134
138
|
- The handler gets `ctx.mandate` (the one block without `source`, or None) and `ctx.mandate_covers(call)` (Node
|
|
135
139
|
`ctx.mandateCovers(call)`, `call` being `{capability, action?, effect, spendUsd?, project?}`: pass `project` when the work
|
|
136
140
|
acts in one, since a call naming none is not limited by the mandate's `projects`; both are read failing closed, as
|
|
@@ -145,7 +149,12 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
145
149
|
spend; all five are read up front, whatever the scope limits, so a member that raises raises to the handler (in
|
|
146
150
|
Python a `KeyError` reads as not named), and a Python mapping's default, a `defaultdict`'s
|
|
147
151
|
included, is read as named, the `defaultdict` gaining the key): `mandate_covers` with `tier: "T1"`, `self` the service's `agent_id`, no `callerAgent` (the
|
|
148
|
-
board names a Project's principal, not an agent id the chain can bind, so the chain's end alone binds it)
|
|
152
|
+
board names a Project's principal, not an agent id the chain can bind, so the chain's end alone binds it), unless the
|
|
153
|
+
service passes `identify_agent=` (Node `identifyAgent`, FAA-135): a function of the request's sender (`{project,
|
|
154
|
+
principal}`, as the board verified it) answering the agent id the service authenticates for it, or None; asked only
|
|
155
|
+
when a mandate travelled, its agent id must be the one before `agent_id` in the chain, and an id the registry
|
|
156
|
+
refuses, an answer that is not a string or a hook that raises fails closed (`mandate-not-for-this-call`), as
|
|
157
|
+
[interop](interop.md) reads `identify_agent`; and the
|
|
149
158
|
responder's wall clock (`wall=`: seconds, `time.time` by default; Node milliseconds, `Date.now`; one that returns a value
|
|
150
159
|
that is not a finite number answers `{"fault": "now-unreadable"}`; a clock that raises passes its error to the handler). It answers `{"ok": True, "recordAs": {by, via: "mandate", task,
|
|
151
160
|
chain, caller}}` (`caller` the sender Project, as MD-3 records it; a sender that names no Project is never read), a
|
|
@@ -180,7 +189,7 @@ body (contract project-comms.md, DEC-0125 MD-3). The kit cannot tell his persona
|
|
|
180
189
|
| `authority_changed`, `not_authorized` | stop claiming; one `board.access.needed` event (notify) saying what to ask for (except `not_authorized` on the move-back claim of a moved slot, read as another session's slot, [below](#when-the-operator-moves-a-slot-to-another-session-faa-155)) |
|
|
181
190
|
| `fenced` on enroll or claim | another live holder: wait for it to lapse; never replace it on its own initiative. When this responder recorded holding the slot, it first reads `com.status`: a newer generation means the slot [moved](#when-the-operator-moves-a-slot-to-another-session-faa-155), and it is not taken again even after that holder lapses |
|
|
182
191
|
| lost after an effect began | keep the entry as `unknown`; `observe(entry)` is asked to settle it with `com.reconcile` |
|
|
183
|
-
| Fabric down, hub missing | bounded backoff (2 s doubling to 5 minutes, jittered), one `board.down` event, no spinning |
|
|
192
|
+
| Fabric down, hub missing | bounded backoff (2 s doubling to 5 minutes, jittered), one `board.down` event, no spinning; a board that flaps is said down once until it stays up `DOWN_QUIET_SECONDS` (300 s, FAA-187 (22)) |
|
|
184
193
|
| one call without an answer while the lease holds | `board.down`, but the session and the running work stay; the call goes again, with the same key, before the lease lapses |
|
|
185
194
|
| `revision_conflict` on `com.renew` or `com.progress` | they store nothing: the revision is read again (`com.get`) and the call sent once more; a second one loses the attempt |
|
|
186
195
|
| `idempotency_conflict` | a key sent with other content is a defect, not an outage: `board.blocked` (notify) naming it, and the session is left |
|
|
@@ -311,10 +320,10 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
311
320
|
Fabric release through v0.3.19 (2026-11-11) offers the operator's move (the control is CT-174's in Fabric, its agent twin CT-181 in the contract, then CT-125),
|
|
312
321
|
so today every move is another session taking the slot and take-back is the route back; the check below is ready
|
|
313
322
|
for the move. While
|
|
314
|
-
a slot is moved the responder reads `com.status` once a renewal (20 s) and, when the slot's generation is past the
|
|
323
|
+
a slot is moved the responder reads `com.status` once a renewal (20 s; one read a tick for every moved slot, FAA-194 (5)) and, when the slot's generation is past the
|
|
315
324
|
recorded move, sends one `com.claim` under a fresh key, as PR-2's "Learning the slot is its own" says (FAA-165,
|
|
316
325
|
0.60.0); the move itself drops the claim key of that capability, whose receipt the move made dead. A claim that
|
|
317
|
-
lands is the slot held again: the record loses `moved`, the request it handed out is worked (with none, its own
|
|
326
|
+
lands is the slot held again (`board.slot.returned`, info, says so; FAA-194 (8)): the record loses `moved`, the request it handed out is worked (with none, its own
|
|
318
327
|
enroll renews the lease at once), accepted work left `lapsed` comes back with a transfer, and a later move is said
|
|
319
328
|
again. `lease_expired` (the move reached this session and its lease lapsed first) enrolls again. `held` asks again
|
|
320
329
|
at the next renewal. `idempotency_window_expired` or `capacity_exceeded` asks again at the next renewal under a
|
|
@@ -323,7 +332,7 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
323
332
|
recorded as `moved` at that generation, so it is not asked about twice. A claim whose answer was lost (`unreachable`) is asked again under the same key at the next renewal, so a claim that
|
|
324
333
|
landed is not stranded; until then it counts as at most a lease from that key's first send (a fresh key starts
|
|
325
334
|
its own, and it counts only while more than the 10 s margin of that lease is left, then no more, as a lapsed slot), so a second lost answer keeps
|
|
326
|
-
the session (
|
|
335
|
+
the session (as any call without an answer does while a lease holds: the failure table above; FAA-194 (34)). A replayed receipt from a generation older than `com.status` shows (a move away and back meanwhile) is
|
|
327
336
|
dead: its key is dropped and a fresh key asks at the next renewal. A take-back drops the capability's claim key too, so a request a move-back claim handed out while the take-back raced
|
|
328
337
|
it waits for its attempt's lease (60 s) before it is claimed again (a take-back that lands before the move-back check makes its claim key
|
|
329
338
|
leaves that key to the claim loop, which works the request at once). Nothing is asked while the responder stops. It is never an enroll or a replace on a slot another session holds, and it waits
|
|
@@ -332,7 +341,9 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
332
341
|
- Otherwise only the operator brings the slot back: `take_back(capability)` (`takeBack`) forgets the record, and wins over a move
|
|
333
342
|
being read at the same time: the move is written under the responder's lock (Node: on the fence queue, after any fenced
|
|
334
343
|
call in flight) and only while the record is unchanged (FAA-158). Never edit the
|
|
335
|
-
checkpoint's `slots` record by hand to undo a move.
|
|
344
|
+
checkpoint's `slots` record by hand to undo a move. Once the slot is held again after a take-back (an enroll or a
|
|
345
|
+
replace lands, in the same process), the kit says `board.slot.returned` (info, once per take-back; FAA-325), as it
|
|
346
|
+
does for a move back. In the same board
|
|
336
347
|
session the next tick enrolls once the other session lets the slot go (a live holder is waited for) and takes the work
|
|
337
348
|
left `lapsed` back with a transfer. A restart or reconnect before that, with accepted work waiting for the slot, takes it
|
|
338
349
|
back at once by `com.responder_replace`, as a restart did before 0.53.0: the operator asked for the slot back, so the
|
|
@@ -345,7 +356,8 @@ held and not transferred). The responder keeps that move instead of undoing it:
|
|
|
345
356
|
(`interop.well-known-capabilities`): an agent whose MCP client sets `_meta` calls it over `/mcp` with the operator's
|
|
346
357
|
decision in `_meta.decision` (a call without one is `operator-decision-required`); Claude Code and Codex send
|
|
347
358
|
arguments only, so until CT-125 they hand the operator the page as Fabric Dashboards' `open_link`. Either door records
|
|
348
|
-
the take-back as a `board.slot.
|
|
359
|
+
the take-back as a `board.slot.taken_back` event (info, the capability as its subject; the slot is still away, so never
|
|
360
|
+
`board.slot.returned`, which is the kit's and means held again, FAA-325) whose sentence says the route in
|
|
349
361
|
words and whose `data` holds `by`, `via` and the `caller` (none when the feed refuses the caller), never the decision's words; a repeat after a lost answer is answered `{capability, moved: false}`. A service built on the kit declares it as an operator action, a button with its tool ([Every button has a
|
|
350
362
|
tool](../SKILL.md), `fabric-actions/0.1`) with gate `relayed` (the operator's decision), never `none`, so a call
|
|
351
363
|
without the operator's relayed decision is refused; on a server that reads mandates (`agent_id`) it also names its effect class, for example
|
|
@@ -385,7 +397,9 @@ checked by FAC-SEM-101 (no credential in data): `board.request.completed`, `boar
|
|
|
385
397
|
`board.reconcile.needed`, `board.slot.contested`, `board.slot.moved`, `board.slot.refused`, `board.access.needed`, `board.blocked`,
|
|
386
398
|
`board.down`, each notifying episode said once, and `board.outcome.invalid` (warning) when a handler's outcome fails
|
|
387
399
|
its checks and is not sent, and `board.request.mandate_unread` (warning) when a request's mandate is not read (above). The operator's hold (below) adds `board.held`, `board.slot.held`,
|
|
388
|
-
`board.slot.released` and `board.request.waiting`.
|
|
400
|
+
`board.slot.released` and `board.request.waiting`. A slot held again after a move back or a take-back is
|
|
401
|
+
`board.slot.returned` (info). The reference service adds `board.slot.taken_back` at a take-back, when the operator asks
|
|
402
|
+
for the slot (FAA-325).
|
|
389
403
|
|
|
390
404
|
`check_service.py <id> [--board-kit <the service's fabric_board.py or fabric-board.mjs>]` rule **board.responder**:
|
|
391
405
|
`NOT_RUN` when the well-known `protocols` names no `fabric-project-comms` revision; `FAIL` for a revision this kit does
|
|
@@ -405,11 +419,13 @@ Python (`fabric_hub`): `fabric_root(platform, env)`, `locate_hub(root=None, aliv
|
|
|
405
419
|
`request_board_access(agent_id, project_id, capabilities, reason)`, `poll_access(request_id, poll_secret,
|
|
406
420
|
credential_file)`, `join_board(project_id, credential_file) -> BoardGrant`, `call_door(tool, args)`,
|
|
407
421
|
`call_binding(tool, args, credential_file)`; `pid_alive(pid)` answers True, False, or None for another OS account's
|
|
408
|
-
process (not verified); `
|
|
422
|
+
process (not verified); on Windows it asks OpenProcess for the access Node's `process.kill(pid, 0)` asks (libuv's
|
|
423
|
+
terminate, query and synchronize), so an elevated process of the same user reads None in both kits, as another
|
|
424
|
+
account's does, where limited query access alone would have read it as running (FAA-323); `Backoff(first=2, cap=300, jitter=0.2, floor=1)`; `HubError(code,
|
|
409
425
|
message, retryable, data)`.
|
|
410
426
|
|
|
411
427
|
Python (`fabric_board`): `Responder(*, hub, project_id, capabilities, credential_file, checkpoint_file, handler,
|
|
412
|
-
admit, observe=None, emit=None, service, now, rand, spawn, session_factory, agent_id=None, mandate_from=None, wall=time.time)`
|
|
428
|
+
admit, observe=None, emit=None, service, now, rand, spawn, session_factory, agent_id=None, mandate_from=None, wall=time.time, identify_agent=None, links=None)`
|
|
413
429
|
with `step()`, `run(stop, drain)`, `shutdown(drain)`, `take_back(capability)`; `WorkContext.progress(note)`,
|
|
414
430
|
`.begin_effect(summary)`, `.stopped`, `.mandate`, `.mandate_covers(call)`; `PERSONAL_BOARD`; `BoardSession(grant)`;
|
|
415
431
|
`Checkpoint(path)`; `comms_canonical(value)`, `comms_digest(operation, value)`, `receipt_problems(value, op)`,
|
|
@@ -420,4 +436,4 @@ effect_began)`, `self_check()`; `BoardError`, `Stopped`.
|
|
|
420
436
|
Node: the same names in camelCase (`locateHub(root, {alive, estate, now})`, `new HubClient(root, {alive, timeout,
|
|
421
437
|
estate, post})`, `new Responder({...})` with async `step()`, `run({signal, drain})`, `shutdown(drain)`,
|
|
422
438
|
`takeBack(capability)`; `await ctx.progress(note)`, `await ctx.beginEffect(summary)`, `ctx.mandate`, `ctx.mandateCovers(call)`;
|
|
423
|
-
the options `agentId`, `mandateFrom
|
|
439
|
+
the options `agentId`, `mandateFrom`, `wall` (milliseconds), `identifyAgent` and `links`; `selfCheck()`). Both kits read and write the same checkpoint file. `links` maps a notify kind to the page on the service that resolves it (`{"board.slot.moved": "/#board"}`, a path, as `make_event` takes one): a notice that notifies carries it as `link`, so the banner opens that page; give it only with an emit that takes `link` (`JsonlEventLog.append` does; FAA-187 (21)). A Node emit receives `{notify, subject, link?, data?}` as one object: spread it before its own `subject` or `link` if it sets its own, or the responder's wins (FAA-187 (27)).
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md
CHANGED
|
@@ -37,7 +37,7 @@ const answer = await request('GET', 'https://api.example/v1/items', { headers, d
|
|
|
37
37
|
| The body is read to one byte past `max_bytes`; over it is an error | 8 MiB | a runaway answer never fills memory |
|
|
38
38
|
| Redirects are answered, never followed | | a redirect must not carry the `Authorization` header to another host |
|
|
39
39
|
| Plain `http://` is refused unless the host is loopback or `insecure=True`; credentials in a URL are refused | | a token never crosses a network in clear, nor sits in a URL |
|
|
40
|
-
| Plain `http://` to a loopback host goes on a connection whose server end is this OS account's process, told from the socket table before a byte is sent and never through a proxy from the environment (`fabric_peer_owner`); another account's listener, root's, or one whose owner cannot be told is kind `foreign-user`, nothing sent, never retried (on a restricted macOS table its reason says `table-restricted
|
|
40
|
+
| Plain `http://` to a loopback host goes on a connection whose server end is this OS account's process, told from the socket table before a byte is sent and never through a proxy from the environment (`fabric_peer_owner`); another account's listener, root's, or one whose owner cannot be told is kind `foreign-user`, nothing sent, never retried (on a restricted macOS table its reason says `table-restricted`, which no retry clears, unless this account's own processes hold the accepted socket, DEC-0155 RT-2: protocol.md, FAA-272, FAA-292). The Node twin sends such a request through `node:http` on a checked connection, not `fetch` | | DEC-0154 HA-2: loopback is one per computer, not one per OS account |
|
|
41
41
|
| Header names must be tokens; a value with a line break or NUL is refused | | header injection |
|
|
42
42
|
| A JSON body is written as `JSON.stringify` writes it: a lone surrogate escaped, NaN and Infinity refused | | both twins send the same bytes |
|
|
43
43
|
| Whatever the transport throws is a connection error, in both languages; a timeout is a timeout | | |
|
|
@@ -85,7 +85,7 @@ const answer = await client.chat(messages, { model, fallback, maxTokens: 800 });
|
|
|
85
85
|
| The fallback | required, of another vendor (the part before `/` differs) |
|
|
86
86
|
| Content | text only: a string, null, or parts that are all `{"type": "text"}`; an image, audio or file part is refused (`config`), because what it is billed cannot be bounded from its bytes |
|
|
87
87
|
| What the ceiling does not cover | the `provider` routing param can send a call to a provider priced above the catalog; two `Client` objects sharing one ledger each hold only their own calls in flight, so give an agent one client per key |
|
|
88
|
-
| The base URL | `base_url` (`baseUrl`), by default `https://openrouter.ai/api/v1`: checked by `check_url` when the client is built; ASCII whitespace and trailing `/` at its ends are trimmed (Node also trims every C0 control at the ends
|
|
88
|
+
| The base URL | `base_url` (`baseUrl`), by default `https://openrouter.ai/api/v1`: checked by `check_url` when the client is built; ASCII whitespace and trailing `/` at its ends are trimmed (Node also trims every C0 control at the ends); a base that is not a string is refused as `check_url` refuses it (`HttpError` kind `refused`, both twins); a query or a fragment is refused (kind `config`, both twins, before `check_url`). One rule reads the base as both twins send it, before `check_url` (`base_url_problem`, `baseUrlProblem`, FAA-187 (18)): a backslash anywhere, a path holding anything but URL path characters and `%XX` escapes (a space, a control, a tab or line break inside, a character outside ASCII) or a dot segment, escaped or not, is kind `config` in both, where Python refused some and Node sent them percent-encoded or rewritten. A host outside ASCII is kept as urllib dials it, in IDNA, so attribution reads the host the call reaches in both twins (FAA-187 (19)) |
|
|
89
89
|
| Attribution | off unless an `attribution` setting is given (FAC-SEM-098, `fabric_usage.attribution_headers`) |
|
|
90
90
|
| Request body | `model`, `messages`, `max_tokens`, `usage: {include: true}` and your params (temperature, reasoning, response_format, tools); `stream`, `model`, `messages`, `max_tokens` and `usage` as params are refused |
|
|
91
91
|
|