HOWTO JSON-e i JSON-e Flows¶
Ten HOWTO jest przewodnikiem operacyjnym dla power userów i developerów middleware. Pokazuje, jak wybrać najmniej potężny executor, napisać konfigurację, testować efekty bez ich wykonywania, spakować przepływ i umieścić go w warstwowej architekturze Orbipleksu. Krótsze odpowiedzi koncepcyjne znajdują się w FAQ JSON-e i JSON-e Flows.
Mapa odpowiedzialności¶
JSON-e jest transformatorem wartości. JSON-e Flow jest należącą do hosta sekwencją statycznych kroków. Żadne z nich nie jest organem domenowym ani źródłem władzy.
flowchart LR
A["Invocation or artifact"] --> B["Host admission"]
B --> C["Operator-owned context projection"]
C --> D["JSON-e evaluator"]
D --> E["Named JSON value"]
E --> F["JSON-e Flow step runtime"]
F -->|"validate"| G["Schema gate"]
F -->|"call"| H["Host capability admission"]
H --> I["Inquirium / Sensorium / Memarium / AD / Agent"]
F -->|"respond"| J["Caller contract"]
Host jest właścicielem projekcji, walidacji schem, wyboru providera, autoryzacji,
timeoutów, control-plane operacji odroczonych i śladów. Szablon odpowiada wyłącznie za
konstruowanie wartości JSON. Konkretna definicja pozostaje jednak pełnoprawnym
komponentem middleware z własnymi id, module_id, component_id, bindingami,
limitami i tożsamością śladu.
Wybierz executor przed napisaniem szablonu¶
| Potrzeba | Użyj | Dlaczego |
|---|---|---|
| Wybór, normalizacja, adnotacja lub konstruowanie JSON | json_e |
Czysta transformacja i najmniejsza powierzchnia władzy. |
| Krótka statyczna sekwencja z kilkoma efektami hosta | json_e_flow |
Efekty pozostają literalne i dopuszczane przez hosta. |
| Rozbudowane rozgałęzienia lub skrypt polityki | nse_rhai |
Ograniczony język jest czytelniejszy niż kod przebrany za dane. |
| Stan, streaming, adapter protokołu lub bogata logika domenowa | channel_json lub nadzorowane middleware |
Zachowanie zasługuje na proces i jawny cykl życia. |
| Pliki, procesy, sesje terminala lub urządzenia | akcja Sensorium lub operacja Workbench | Władza nad OS pozostaje w warstwie enaction. |
Wybierz pierwszy wiersz zdolny wyrazić całe zachowanie. Nie wybieraj JSON-e tylko po to, aby uniknąć kodu, jeżeli wynikowa konfiguracja staje się mniej poznawalna niż kod.
Trzeba przy tym uwzględnić bieżącą dostępność: middleware-runtime implementuje oba
executory, lecz rejestr providerów konfigurowanych przez operatora w daemonie udostępnia
dziś middleware_json_e_flow_services, a nie równoległe
middleware_json_e_services. Czystego executora używaj bezpośrednio w integracjach i
testach na poziomie crate. Dla wdrażalnego komponentu daemona, który tylko przekształca
dane, użyj JSON-e Flow zawierającego wyłącznie render, validate i respond; nie
wymyślaj nieobsługiwanego klucza konfiguracji.
Poznaj dwa kontrakty konfiguracji¶
Czysta definicja json_e wymaga:
- tożsamości operacyjnej:
id,module_id,component_idi opcjonalnychbindings; - tożsamości interpretera:
template_id,profile_version; - granicy wejścia:
context_contract,context_projection; - granicy wyjścia:
output_contract; - powierzchni języka:
template,helper_profile,helpers; - granicy zasobów:
limits; - opcjonalnego i osobno bramkowanego
raw_signal_access.
Definicja json_e_flow zastępuje output_contract i top-level template statycznymi
steps. Dodaje także allowed_calls, opcjonalne granty rodzin capabilities,
trace_policy oraz deferred_response_mode.
Schemy będące źródłem prawdy to:
node:middleware-runtime/schemas/json-e-middleware.schema.json;node:middleware-runtime/schemas/json-e-flow-middleware.schema.json;node:middleware-runtime/schemas/json-e-context-role-execute.schema.json;node:middleware-runtime/schemas/json-e-evaluation-trace.schema.json.
Napisz czysty adapter roli w JSON-e¶
Poniższy kompletny executor mapuje wywołanie roli do
service-dispatch-response.v1. Nie wykonuje efektu i udostępnia szablonowi tylko pięć
potrzebnych wartości. Jest to kontrakt executora w middleware-runtime; jak opisano
wyżej, obecny daemon nie ładuje go bezpośrednio z osobnego top-level rejestru czystego
JSON-e.
{
"id": "role-example-summary-json-e",
"module_id": "example.json-e.roles",
"component_id": "middleware.example.roles.summary",
"bindings": {
"role_capability_id": "role.example-summary.execute"
},
"template_id": "example.role-summary.response.v1",
"profile_version": "orbiplex.json_e.v1",
"context_contract": "json_e.context.role_execute.v1",
"context_projection": {
"capability_id": "$.capability_id",
"role_capability_id": "$.role.capability_id",
"dispatch_id": "$.dispatch.id",
"request_input": "$.request.input",
"now": {"host_value": "invocation.rfc3339_now"}
},
"output_contract": "service-dispatch-response.v1",
"template": {
"schema_version": "v1",
"capability_id": "service_dispatch_execute",
"status": "completed",
"dispatch/id": "${dispatch_id}",
"completed-at": "${now}",
"answer/content": {
"summary": "${request_input.text}"
},
"answer/format": "application/json",
"confidence/signal": null,
"human-linked-participation": false,
"provenance/origin-classes": ["json_e"],
"message": null
},
"helper_profile": "orbiplex.json_e.helpers.basic.v1",
"helpers": [],
"limits": {
"max_template_bytes": 32768,
"max_context_bytes": 16384,
"max_output_bytes": 32768,
"max_evaluation_depth": 32,
"max_collection_size": 128,
"max_string_bytes": 16384,
"timeout_ms": 250
}
}
Przeglądaj context_projection jak granicę władzy. Dodanie "request": "$" nie jest
niewinnym skrótem autorskim: daje szablonowi każde pole wywołania i sprawia, że
przyszłe rozszerzenia envelope staną się widoczne bez kolejnego przeglądu.
Dostępne helpery profilu podstawowego to sha256_json, sha256_text, default,
has, pick i idempotency_key. Udostępniaj tylko te, których używa szablon. Przypnij
orbiplex.json_e.helpers.basic.v1; nie zakładaj istnienia niewersjonowanej powierzchni
helperów.
Zbuduj mały JSON-e Flow¶
Zacznij od przejścia danych, nie od listy usług. Ten przykład renderuje ograniczone
żądanie Inquirium, prosi hosta o wykonanie literalnej capability i zwraca wynik
providera. Grant inferencji jest czymś innym niż allowed_calls.
{
"id": "example-inquiry-flow",
"module_id": "example.inquiry",
"component_id": "middleware.example.inquiry",
"bindings": {
"role_capability_id": "role.example-inquiry.execute"
},
"template_id": "example.inquiry.flow.v1",
"profile_version": "orbiplex.json_e_flow.v1",
"context_contract": "json_e.context.role_execute.v1",
"context_projection": {
"capability_id": "$.capability_id",
"role_capability_id": "$.role.capability_id",
"dispatch_id": "$.dispatch.id",
"request_input": "$.request.input",
"now": {"host_value": "invocation.rfc3339_now"}
},
"helper_profile": "orbiplex.json_e.helpers.basic.v1",
"helpers": [],
"allowed_calls": ["inquirium.generate"],
"inference_grants": [
{
"capability": "inquirium.generate",
"max_request_bytes": 4096,
"allowed_runtime_refs": [],
"allowed_profile_refs": []
}
],
"limits": {
"max_template_bytes": 32768,
"max_context_bytes": 16384,
"max_output_bytes": 32768,
"max_evaluation_depth": 32,
"max_collection_size": 128,
"max_string_bytes": 16384,
"timeout_ms": 1000,
"max_flow_steps": 8,
"max_loop_steps": 16
},
"trace_policy": {
"success_sample_rate": 1.0,
"failure_sample_rate": 1.0,
"retain_recent": 100
},
"steps": [
{
"kind": "render",
"id": "render_inquiry",
"as": "inquiry_request",
"template": {
"schema": "inquirium.generate.request.v1",
"operation": "generate",
"turns": [
{
"role": "user",
"content": [
{"type": "text", "text": "Summarize: ${request_input.text}"}
]
}
],
"parameters": {"max_tokens": 256, "temperature": 0.0},
"policy": {
"locality": "local_only",
"trust_mode": "strict_local",
"scope": "example-summary",
"on_context_denied": "fail_closed",
"plurality": "preserve"
},
"metadata": {"dispatch/id": "${dispatch_id}"}
}
},
{
"kind": "call",
"id": "call_inquiry",
"capability": "inquirium.generate",
"input": "$.inquiry_request",
"as": "inquiry_result"
},
{
"kind": "respond",
"id": "respond",
"input": "$.inquiry_result"
}
]
}
capability jest literalną konfiguracją. Renderuj ciała żądań, nigdy nazwy
capabilities. Jeżeli caller wymaga konkretnego kontraktu odpowiedzi, dodaj krok
render budujący ten kontrakt, krok validate, a dopiero potem respond.
Składaj kroki bez ukrytego stanu¶
Każdy krok render, call lub extract zapisuje jedną nazwaną wartość przez as.
Kolejne kroki adresują stan przepływu ścieżkami JSON, takimi jak
$.inquiry_request lub $.sensorium_result.answer_content.
Używaj świadomie sześciu obecnych rodzajów kroków:
[
{"kind": "render", "id": "render_request", "as": "request", "template": {}},
{"kind": "validate", "id": "validate_request", "input": "$.request", "contract": "example.request.v1"},
{"kind": "call", "id": "call_provider", "capability": "example.execute", "input": "$.request", "as": "result"},
{"kind": "extract", "id": "extract_value", "from": "$.result", "path": "$.value", "as": "value"},
{"kind": "respond", "id": "respond", "input": "$.value"}
]
Krok fail służy do jawnej, statycznej odmowy. Nie imituj rozgałęzień przez
renderowanie różnych identyfikatorów capability ani dynamicznych list kroków: schema
odrzuca takie kształty, a architektura celowo trzyma wybór efektu poza danymi
wywołania.
Rozdziel kształt call od władzy¶
Przed dodaniem call odpowiedz na cztery pytania:
- Która stabilna host capability reprezentuje efekt?
- Czy
allowed_callszawiera dokładnie ten identyfikator? - Który paszport, lokalna polityka, host grant lub role binding autoryzuje komponent?
- Jakie schemy request i response stanowią granicę?
Dla Inquirium dodaj ograniczony wpis inference_grants. Dla agent.* dodaj pasujący
agent_grants oraz, gdy trzeba, statyczne observation_bindings. Bindingi są
operatorskimi mapowaniami do nieprzezroczystych source refs; wynik przepływu może je
wybrać, ale nie może wymyślić źródła ani poszerzyć age_max_ms i bytes_max.
Dla zwykłych capabilities, takich jak sensorium.directive.invoke, memarium.write
lub artifact.delivery.send, allowed_calls nadal nie jest ostateczną władzą. Host
stosuje odpowiedni paszport domenowy i politykę przed efektem.
Waliduj i wykonuj dry-run przed aktywacją¶
Każdą wdrażalną definicję przepływu umieść pod mapą
middleware_json_e_flow_services daemona, bezpośrednio albo przez fragment
konfiguracji zaufanego pakietu. Czysty JsonEExecutorConfig testuj w
middleware-runtime; jeśli ta sama transformacja ma być dziś wdrażalna przez
operatora, wyraź ją jako bezefektowy flow. Następnie zwaliduj cały profil daemona:
cargo run -p orbiplex-node-daemon -- \
check-config --data-dir "$DATA_DIR"
Pracuj nad efektowym przepływem przy użyciu fail-closed mocków:
cargo run -p orbiplex-node-daemon -- \
json-e-flow-dry-run \
--data-dir "$DATA_DIR" \
--middleware-id example-inquiry-flow \
--input invocation.json \
--mock-responses mock-responses.json
Mockuj po step id, gdy dwa wywołania tej samej capability potrzebują różnych odpowiedzi; capability id służy jako fallback:
{
"calls": {
"call_inquiry": {
"schema": "inquirium.generate.response.v1",
"status": "completed",
"output": {"text": "A bounded mock response"}
}
},
"capabilities": {
"memarium.write": {"status": "written", "fact_id": "fact:mock"}
}
}
Dry-run nigdy nie wywołuje prawdziwej host capability bez jawnego mocka. Raport
porażki zachowuje częściowy ślad kroków oraz wskazuje failure_class, step_id,
step_path i dostępne ścieżki template/input/context. Napraw warstwę wskazaną przez
błąd zamiast spekulacyjnie poszerzać uprawnienia lub limity.
Obsługuj operacje odroczone jawnie¶
Użyj wartości domyślnej, jeżeli caller może przyjąć zawieszenie i wznowienie zarządzane przez hosta:
{
"deferred_response_mode": "surface-to-caller"
}
Oczekujące deferred-operation.v1 jest danymi control-plane, nie wynikiem domenowym.
Obecny daemon zapisuje pierwotne wywołanie wraz z identyfikatorami middleware,
capability, szablonu i odroczonego kroku. Po ukończeniu interpretuje statyczny
przepływ ponownie od początku i wstrzykuje ukończony status w pasującym kroku call.
Każdy wcześniejszy efektowy call musi więc używać stabilnego klucza idempotencji i
tolerować replay. Przepływ nadal nie jest właścicielem pollingu, częstotliwości retry
ani TTL.
Ustaw reject-as-failure tylko wtedy, gdy kontrakt callera jest ściśle synchroniczny.
Ten wybór powinien być widoczny w review, ponieważ zamienia poprawną odpowiedź
odroczoną providera w deferred-not-accepted.
Do czekania na obserwowalny stan preferuj interaction-broker.wait, nie ukrytą pętlę
pollingu. Referencyjny fixture to
node:middleware-runtime/fixtures/json-e-flow/sensorium-workbench/30-wait-condition.json.
Proś o raw signal access tylko wtedy, gdy jest potrzebny¶
Surowe wejście jest świadomym otworem w abstrakcji. Aby go użyć, muszą istnieć wszystkie trzy bramki:
- konkretna definicja deklaruje
raw_signal_accessz powodem i limitami; - lokalna polityka hosta dopuszcza żądany hook i klasę wiadomości;
context_projectionmapuje dopuszczoną wartość do kontekstu autorskiego.
{
"raw_signal_access": {
"requires_raw_signal": true,
"reason": "Preserve the original signed request for a bounded compatibility adapter",
"max_raw_signal_bytes": 16384,
"accepted_hooks": ["inbound-local"],
"accepted_message_kinds": ["example.request.v1"]
},
"context_projection": {
"raw_signal": "$.trace.raw_signal_access.raw_signal"
}
}
Nie dodawaj tej deklaracji tylko dlatego, że brakuje wygodnego pola. Najpierw dodaj stabilne pole do zwykłego kontekstu autorskiego, jeżeli należy ono do danej abstrakcji.
Spakuj rodzinę przepływów¶
Pakiet middleware może przenosić całą mapę middleware_json_e_flow_services.
Minimalny układ wygląda tak:
middleware-packages/example-json-e-flows/
middleware.package.json
config/
json-e-flow-services.json
ui/
index.html
.signatures/
middleware-package.sig.json
Manifest deklaruje fragment konfiguracji zamiast przemycać go przez dowolny plik:
{
"schema": "middleware.package.v1",
"package_id": "example.json-e-flows",
"version": "0.1.0",
"module_id": "example.json-e-flows",
"provides": {
"config_fragments": [
{
"kind": "daemon.middleware_json_e_flow_services",
"path": "config/json-e-flow-services.json"
}
]
}
}
Podpisanie pakietu i władza runtime są osobnymi decyzjami. Podpis mówi, którym bajtom
zaufał operator; nie nadaje memarium.write, inquirium.generate ani żadnej innej
capability. Po każdej aktualizacji pakietu ponownie przeglądaj projekcje kontekstu i
granty, ponieważ aktualny podpis nad szerszą projekcją nadal oznacza szerszą lokalną
decyzję o władzy.
Stosuj wspólne wzorce integracyjne¶
Adapter roli: Inquirium -> Sensorium -> Memarium¶
Story-009 jest kompletnym przykładem referencyjnym. Każdy przepływ roli:
- projektuje kontekst
json_e.context.role_execute.v1; - opcjonalnie pobiera materiał doradczy z
inquirium.generate; - renderuje i wywołuje jedno
sensorium-directive.v1; - renderuje i zapisuje ograniczony fakt Memarium;
- publikuje
workflow.step.completed; - waliduje i zwraca
service-dispatch-response.v1.
Przepływ nie wie, jak serwowany jest model, jak wykonywana jest akcja OS ani jak
przechowywany jest fakt. Fixtures znajdują się w
node:middleware-runtime/fixtures/json-e-flow/story-009/.
Workbench: propozycja nie jest władzą wykonawczą¶
Przykład Workbench utrzymuje widoczne rozdzielenie wyniku modelu od działania:
observe -> inquirium.generate -> structured proposal
-> operator/host-reviewed argv
-> sensorium.directive.invoke -> Workbench policy gate -> execution
JSON-e Flow może przenieść propozycję wraz z dyrektywą, lecz Workbench wykonuje
operatorsko sprawdzone argv w ramach własnego command profile. Zobacz
node:middleware-runtime/fixtures/json-e-flow/sensorium-workbench/.
Konfigurację konektora, pełną dyrektywę, zgodę i wykonanie odroczone opisuje dalej
HOWTO Sensorium.
Artifact Delivery: efekt wychodzący, czysty predykat wejściowy¶
Dla wysyłki wyrenderuj artifact-delivery-envelope.v1, zwaliduj je, a potem wywołaj
artifact.delivery.send. Dla przyjęcia skonfiguruj czysty acceptor JSON-e Flow,
zwracający InboundAdmissionResult i niedeklarujący host calls. Pełna konfiguracja
acceptora znajduje się w HOWTO Artifact
Delivery.
Agent: wcześniej zadeklarowane granty i observation bindings¶
Przepływ może wywołać agent.spawn, agent.binding.create i
agent.controller.run tylko wtedy, gdy każde wywołanie ma pasujący wpis
agent_grants. Profile runtime, prefiksy agent id, capabilities i limity źródeł
obserwacji trzymaj w konfiguracji operatora. Pełny kształt pokazuje fixture
node:middleware-runtime/fixtures/json-e-flow/agent/10-bounded-agent-consumer.json.
Obserwuj runtime bez wycieku payloadów¶
Użyj jednej z powierzchni operatorskich:
orbiplex-node-launcher json-e-flow-middleware --data-dir "$DATA_DIR"
- Node UI:
/operator/json-e-flow; - lista/read model daemona:
GET /v1/json-e-flow-middleware?limit=10; - pojedynczy zachowany ślad:
GET /v1/json-e-flow-traces/{record_id}.
Read model pokazuje template ids, role bindings, helper profile, statyczne calls,
limity, trace policy, ostatnie porażki i digesty kroków. Nie powinien pokazywać ciał
żądań, promptów modeli, materiału prywatnego ani odpowiedzi capabilities.
retain_recent ogranicza operatorski read model; trwałość append-only śladów i
fizyczna kompaktacja należą do warstwy storage.
Checklista review¶
Przed włączeniem definicji sprawdź:
context_projectionpokazuje minimalny zestaw stabilnych wartości i żadnych credentials.- Wyjście jest walidowane na granicy, na której polega na nim inny komponent.
- Każdy capability id jest literalny i obecny w
allowed_calls. - Wywołania Inquirium i Agent mają dodatkowe, ograniczone granty.
- Limity pasują do oczekiwanego requestu, nie hipotetycznego maksimum.
- Klucze idempotencji wynikają ze stabilnych wejść semantycznych, nie timestampów ani etykiet.
- Odpowiedzi odroczone są świadomie powierzchniowane albo świadomie odrzucane.
- Trace policy zachowuje porażki i nie utrwala surowych wartości zawierających sekrety.
- Dry-run pokrywa sukces, błąd schemy, brak mocka, niedozwolony call oraz zbyt duże lub uszkodzone wejście.
- Definicja pozostaje krótsza i łatwiejsza do zrozumienia niż równoważny kod.
Jeśli ostatni warunek nie jest spełniony, wypromuj zachowanie do middleware opartego na kodzie, zachowując te same host capabilities i wire contracts.