mirror of
https://github.com/logos-co/logos-cpp-sdk.git
synced 2026-08-31 09:41:06 +00:00
* Extract the protocol layer into logos-protocol; consume it as a flake input The transport/token/IPC layer (transports incl. QRO + plain TCP/TLS, consumer core LogosAPIClient/LogosAPIConsumer with the capability auto-requestModule flow, ModuleProxy, token manager, QVariant<->JSON conversion, the abstract LogosProviderObject interface) now lives in the logos-protocol repo behind the versioned lp_* C ABI. This SDK keeps the typed C++ developer layer (LogosAPI, provider base classes + Qt provider glue, module context, code generator) and still compiles the protocol sources INTO liblogos_sdk.a from the flake input, so the installed artifact (archive symbols, include/ + include/cpp layouts, cmake config) stays byte-compatible: existing consumers need no changes. Public headers are unchanged; logos_provider_object.h keeps its name and now re-exports the abstract interface from logos_provider_interface.h. Transport/protocol component tests moved to logos-protocol with the code; the remaining sdk/generator/experimental suites are unchanged (432/432 green against the local protocol checkout). * lock: add logos-protocol input * Make the base SDK Qt-free: move the Qt developer layer to logos-qt-sdk LogosAPI, LogosAPIProvider, LogosProviderBase/LOGOS_PROVIDER macros, the QObject provider glue (QtProviderObject) and the legacy PluginInterface (core/interface.h) move to the new logos-qt-sdk repo. The protocol sources are no longer compiled into a monolithic archive — consumers link logos-qt-sdk (which layers on logos-protocol) instead. What remains here is header-only std C++: logos_module_context.h, logos_result.h (StdLogosResult), logos_json.h — exported as the CMake INTERFACE target logos-cpp-sdk::logos_headers — plus the code generator (a build-time tool; its introspection mode now includes logos_provider_interface.h from logos-protocol, where LogosProviderPlugin moved). Mechanically verified Qt-free: the logos-cpp-lib / logos-cpp-include closures contain only nlohmann_json. Tests: 245/245 (module-context std suite + generator + experimental). * fix: accept the installed source-export layout in the protocol-root check The fail-fast only tested <root>/cpp/logos_protocol.h, but the LP_SRC selection right below (and the error message itself) support the installed export layout <root>/include/cpp as well. Pointing LOGOS_PROTOCOL_ROOT at an installed export tripped the FATAL_ERROR before that fallback could apply. Caught by Copilot review on #82. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * lock: pin logos-protocol to the qt-free-split branch head The Qt-free SDK (and the cdylib backend stacked on it) reference LogosProviderPlugin from protocol's logos_provider_interface.h, which lands on feat/qt-free-split — the P1-branch pin no longer compiles standalone. Temporary — drop when the chain PRs merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * doctest: pin the logoscore runtime via its {release} placeholder The spec built logoscore-cli at bare master with only the cpp-sdk inputs overridden — master's stack cannot compile against the qt-free SDK, so the suite failed on the chain branches. With the placeholder, CI's --release-for pins expand it to the workspace's logoscore commit (and local runs without a pin still fall back to master, unchanged). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * doctest: override the nested module builders to {release} too capability_module (via logoscore's lock) and the cloned accounts module resolve module-builder from their own locks — pre-split revs whose LogosModule.cmake still detects the SDK by logos_api.h, which the qt-free SDK no longer ships ('logos-cpp-sdk not found'). Overriding the builder itself to the workspace-pinned chain rev (keeping the nested cpp-sdk override) builds both modules with the split-aware builder. Verified end-to-end locally with the exact doctest command. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * doctest: apply the {release} + nested-builder overrides to all three specs The runtime spec got the treatment in 210eea1; the composition and worker-thread specs have the same logoscore/module build commands and failed identically (pre-split builders from the modules' own locks). All executed run: blocks now pin logoscore-cli{release} and override the nested module builders to logos-module-builder{release}; the displayed code_block: variants stay in their generic master form. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * codegen: typed wrappers throw on call failure; dispatch catches escapes Generated sync client wrappers call the new err-out invokeRemoteMethod overload and throw logos::LogosCallError when the call fails (e.g. the bound module is missing) — previously the empty QVariant silently degraded to the return type's default and a caller could not tell failure from a legitimate 0 / "". Both generators (legacy + LIDL), both API styles. Async paths unchanged. Generated provider dispatch (universal qt glue + LOGOS_PROVIDER) wraps the method body in a catch-all that logs and returns an invalid QVariant — an escaped exception becomes an ordinary METHOD_FAILED instead of unwinding through Qt event dispatch and killing the module process. * codegen: CallError out-param instead of throwing wrappers Per review, the generated sync wrappers expose the error channel as an optional trailing parameter — add(a, b, &err) — rather than throwing: explicit, stateless, works on temporaries, and existing call sites compile unchanged (they keep default-on-failure, now with a qWarning so failures are visible in the module log). The dispatch catch-all from the previous commit stays: it contains author exceptions, it doesn't introduce any. * glue: fire onContextReady AFTER modules()/event wiring The generated onInit set the context (which fires the impl's onContextReady hook) before constructing the LogosModules aggregate and wiring typed event emission — so an impl doing its documented one-time setup there (typed dependency calls, event subscriptions) dereferenced a null aggregate and crashed the module process (signal 11). Found by the first module to subscribe to a dependency's typed event from onContextReady. Context now goes last. * ci: run workflows on stacked PRs + workflow_dispatch Both workflows filtered pull_request to master-based PRs, so stacked PRs (feat/qt-free-sdk -> feat/extract-logos-protocol, feat/cdylib-authoring -> feat/qt-free-sdk) ran NO checks at all. Drop the base-branch filter for pull_request and add workflow_dispatch for manual runs. Same fix as logos-module-builder 232b8a2. * lock: protocol at the typed-requestModule port (3de5398) * ci: chain pins for the doc-tests (drop at merge) In repo CI only cpp-sdk's {release} is the commit under test — logoscore-cli and module-builder expanded to master, which doesn't link against the chain SDK the specs override in ('Build the CLI with the SDK override' failed on every run since the stacked-PR triggers were enabled). Pin both to the extraction-chain heads; the workspace pipeline is unaffected (it pins every repo itself). * generator: distribute the LIDL frontend for external generators First step of moving ALL Qt glue emission out of this repo into logos-qt-sdk's logos-qt-generator (cpp-sdk's generator keeps only the Qt-free outputs: std typed wrappers, logos_sdk umbrella, cdylib impl-exports, LIDL derivation). - Shared emit helpers (lidlToPascalCase, lidlTypeToQt, lidlTypeToStd, lidlIsStdConvertible) move to a new lidl_emit_common.{h,cpp} unit, used by both generators. - The frontend set (AST, lexer, parser, serializer, validator, impl-header parser, emit-common) is installed under share/lidl-frontend/ — the qt generator compiles these sources in directly, so the two tools share one frontend without a binary ABI. * lock: protocol#3 merged — pin advances to protocol master --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
686 lines
30 KiB
YAML
686 lines
30 KiB
YAML
name: "Composing Two Modules Built Against This C++ SDK"
|
|
output: cpp-sdk-module-composition.md
|
|
release: ""
|
|
|
|
intro: |
|
|
The whole point of `logos-cpp-sdk` is that a module can call **another**
|
|
module without ever touching the raw `LogosAPI`: you declare a dependency,
|
|
and the SDK's code generator emits a typed `modules().<dep>` wrapper with
|
|
synchronous callers, asynchronous callers, and event subscribers. This
|
|
doc-test proves that cross-module path works end-to-end on the SDK commit
|
|
under test — by building *both* sides from scratch against it.
|
|
|
|
It is fully self-contained — no pre-existing module, no `requires:` chain:
|
|
|
|
1. Create `greeter_module`, a small **callee** with a couple of methods
|
|
(`greet`, `addInts`, `greetCount`) and a `greeted` event.
|
|
2. Create `orchestrator_module`, a **caller** that declares `greeter_module`
|
|
as a dependency and composes it through the generated
|
|
`modules().greeter_module` wrapper — synchronously, asynchronously, and by
|
|
subscribing to its event.
|
|
3. Build **both** modules' `.lgx` packages **against the C++ SDK commit under
|
|
test**, so the generated wrappers, the plugin glue, and the IPC layer all
|
|
come from this SDK.
|
|
4. Build `logoscore` (also against this SDK), install both modules, load them
|
|
together, and call the orchestrator's methods — each of which calls into
|
|
the greeter across the process boundary.
|
|
|
|
Because the caller, the callee, the generated cross-module wrappers, and the
|
|
runtime are all built from the SDK commit under test, a green run is real
|
|
evidence that this change keeps inter-module composition — the SDK's core
|
|
promise — working.
|
|
|
|
what_you_build: "Two modules — a `greeter_module` callee and an `orchestrator_module` caller — built against this SDK commit and run together in `logoscore`, with the caller driving the callee over IPC."
|
|
|
|
what_you_learn:
|
|
- How one module declares another as a dependency (`metadata.json` + `flake.nix` input)
|
|
- How the SDK generates a typed `modules().<dep>` wrapper with sync, async, and event APIs
|
|
- How to build a module — and its module dependency — against a specific `logos-cpp-sdk` commit
|
|
- How to load two modules in `logoscore` and chain calls so the caller drives the callee
|
|
- How async replies and event subscriptions survive between `call` commands under the daemon
|
|
|
|
prerequisites:
|
|
- |
|
|
**Nix** with flakes enabled. Install from [nixos.org](https://nixos.org/download.html), then enable flakes:
|
|
|
|
```bash
|
|
mkdir -p ~/.config/nix
|
|
echo 'experimental-features = nix-command flakes' >> ~/.config/nix/nix.conf
|
|
```
|
|
|
|
Verify: `nix flake --help >/dev/null 2>&1 && echo "Flakes enabled"`
|
|
- "**git** — nix flakes only see files tracked by git."
|
|
- "A Linux or macOS machine."
|
|
|
|
sections:
|
|
- title: "Create the callee: greeter_module"
|
|
step: true
|
|
text: |
|
|
`greeter_module` is an ordinary `core` module written in the pure-C++
|
|
(`interface: universal`) style: you write one plain class, and the builder
|
|
generates the Qt plugin glue. Every `public` method becomes callable over
|
|
IPC, and the `logos_events:` block declares an event other modules can
|
|
subscribe to.
|
|
steps:
|
|
- title: "metadata.json"
|
|
text: |
|
|
`dependencies` is empty — the greeter calls no one. `interface:
|
|
universal` selects the pure-C++ pattern.
|
|
file:
|
|
path: greeter_module/metadata.json
|
|
language: json
|
|
content: |
|
|
{
|
|
"name": "greeter_module",
|
|
"version": "1.0.0",
|
|
"type": "core",
|
|
"category": "general",
|
|
"description": "A callee module: greets, counts greetings, and emits an event",
|
|
"main": "greeter_module_plugin",
|
|
"interface": "universal",
|
|
"dependencies": [],
|
|
|
|
"nix": {
|
|
"packages": {
|
|
"build": [],
|
|
"runtime": []
|
|
},
|
|
"external_libraries": [],
|
|
"cmake": {
|
|
"find_packages": [],
|
|
"extra_sources": []
|
|
}
|
|
}
|
|
}
|
|
|
|
- title: "CMakeLists.txt"
|
|
text: "For a universal module you list only your plain C++ sources; the generated glue is compiled automatically."
|
|
file:
|
|
path: greeter_module/CMakeLists.txt
|
|
language: cmake
|
|
content: |
|
|
cmake_minimum_required(VERSION 3.14)
|
|
project(GreeterModulePlugin LANGUAGES CXX)
|
|
|
|
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
|
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
|
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake")
|
|
include(cmake/LogosModule.cmake)
|
|
else()
|
|
message(FATAL_ERROR "LogosModule.cmake not found")
|
|
endif()
|
|
|
|
logos_module(
|
|
NAME greeter_module
|
|
SOURCES
|
|
src/greeter_module_impl.h
|
|
src/greeter_module_impl.cpp
|
|
)
|
|
|
|
- title: "flake.nix"
|
|
text: |
|
|
A minimal flake that hands every input to `mkLogosModule`. The
|
|
`{release}` on the builder URL is pinned by the doc-test runner; the
|
|
builder owns the module's `logos-cpp-sdk` pin, which we override to the
|
|
commit under test in the build step.
|
|
file:
|
|
path: greeter_module/flake.nix
|
|
language: nix
|
|
content: |
|
|
{
|
|
description = "Greeter core module - a callee for the composition doc-test";
|
|
|
|
inputs = {
|
|
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
|
|
};
|
|
|
|
outputs = inputs@{ logos-module-builder, ... }:
|
|
logos-module-builder.lib.mkLogosModule {
|
|
src = ./.;
|
|
configFile = ./metadata.json;
|
|
flakeInputs = inputs;
|
|
};
|
|
}
|
|
|
|
- title: "src/greeter_module_impl.h — the class"
|
|
text: |
|
|
Plain C++ inheriting `LogosModuleContext`. The `///` doc comments
|
|
become each method's description; the `logos_events:` block declares
|
|
the `greeted` event (the token expands to `public` under a normal
|
|
compile, and the generator emits the event body).
|
|
file:
|
|
path: greeter_module/src/greeter_module_impl.h
|
|
language: cpp
|
|
content: |
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
#include <logos_module_context.h> // LogosModuleContext base + logos_events
|
|
|
|
// A simple callee module. The orchestrator composes these methods over
|
|
// IPC. It also emits a `greeted` event so the caller can exercise a
|
|
// typed event subscription.
|
|
class GreeterModuleImpl : public LogosModuleContext {
|
|
public:
|
|
GreeterModuleImpl() = default;
|
|
~GreeterModuleImpl() = default;
|
|
|
|
/// Returns a greeting for the given name.
|
|
std::string greet(const std::string& name);
|
|
|
|
/// Adds two integers and returns the sum.
|
|
int64_t addInts(int64_t a, int64_t b);
|
|
|
|
/// Returns how many times greet() has been called on this instance.
|
|
int64_t greetCount() const;
|
|
|
|
/// Greets the name and also emits a `greeted` event carrying it.
|
|
void greetNotify(const std::string& name);
|
|
|
|
logos_events:
|
|
/// Emitted by greetNotify() with the produced greeting string.
|
|
void greeted(const std::string& greeting);
|
|
|
|
private:
|
|
int64_t m_greetCount = 0;
|
|
};
|
|
|
|
- title: "src/greeter_module_impl.cpp — the implementation"
|
|
text: "Plain C++ — no Qt, no IPC plumbing. `greetNotify` fires the generated event."
|
|
file:
|
|
path: greeter_module/src/greeter_module_impl.cpp
|
|
language: cpp
|
|
content: |
|
|
#include "greeter_module_impl.h"
|
|
|
|
std::string GreeterModuleImpl::greet(const std::string& name)
|
|
{
|
|
++m_greetCount;
|
|
return "Hello, " + name + "!";
|
|
}
|
|
|
|
int64_t GreeterModuleImpl::addInts(int64_t a, int64_t b)
|
|
{
|
|
return a + b;
|
|
}
|
|
|
|
int64_t GreeterModuleImpl::greetCount() const
|
|
{
|
|
return m_greetCount;
|
|
}
|
|
|
|
void GreeterModuleImpl::greetNotify(const std::string& name)
|
|
{
|
|
// Emit the event declared in logos_events:. When loaded by a host
|
|
// this reaches every subscriber; constructed outside a host it is a
|
|
// safe no-op.
|
|
greeted("Hello, " + name + "!");
|
|
}
|
|
|
|
- title: "Create the caller: orchestrator_module"
|
|
step: true
|
|
text: |
|
|
`orchestrator_module` declares `greeter_module` as a dependency. That one
|
|
line in `metadata.json` is what makes the builder run the SDK's code
|
|
generator over the greeter's exported interface and emit a typed
|
|
`modules().greeter_module` wrapper — with sync callers, async callers, and
|
|
event subscribers — that the orchestrator uses without any raw `LogosAPI`.
|
|
steps:
|
|
- title: "metadata.json — declare the dependency"
|
|
text: |
|
|
The `dependencies` entry must match `greeter_module`'s own
|
|
`metadata.json` `name`.
|
|
file:
|
|
path: orchestrator_module/metadata.json
|
|
language: json
|
|
content: |
|
|
{
|
|
"name": "orchestrator_module",
|
|
"version": "1.0.0",
|
|
"type": "core",
|
|
"category": "general",
|
|
"description": "A caller module: composes greeter_module via typed sync/async calls and an event subscription",
|
|
"main": "orchestrator_module_plugin",
|
|
"interface": "universal",
|
|
"dependencies": ["greeter_module"],
|
|
|
|
"nix": {
|
|
"packages": {
|
|
"build": [],
|
|
"runtime": []
|
|
},
|
|
"external_libraries": [],
|
|
"cmake": {
|
|
"find_packages": [],
|
|
"extra_sources": []
|
|
}
|
|
}
|
|
}
|
|
|
|
- title: "CMakeLists.txt"
|
|
file:
|
|
path: orchestrator_module/CMakeLists.txt
|
|
language: cmake
|
|
content: |
|
|
cmake_minimum_required(VERSION 3.14)
|
|
project(OrchestratorModulePlugin LANGUAGES CXX)
|
|
|
|
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
|
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
|
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake")
|
|
include(cmake/LogosModule.cmake)
|
|
else()
|
|
message(FATAL_ERROR "LogosModule.cmake not found")
|
|
endif()
|
|
|
|
logos_module(
|
|
NAME orchestrator_module
|
|
SOURCES
|
|
src/orchestrator_module_impl.h
|
|
src/orchestrator_module_impl.cpp
|
|
)
|
|
|
|
- title: "flake.nix — add the dependency input"
|
|
text: |
|
|
Declare `greeter_module` as a flake input; the input name **must
|
|
match** the dependency name in `metadata.json`. The `path:` value is a
|
|
placeholder — we lock it to the real greeter checkout in the build step
|
|
with `--override-input` (Nix won't accept a relative `../` path written
|
|
directly into `flake.nix`).
|
|
file:
|
|
path: orchestrator_module/flake.nix
|
|
language: nix
|
|
content: |
|
|
{
|
|
description = "Orchestrator core module - calls greeter_module";
|
|
|
|
inputs = {
|
|
logos-module-builder.url = "github:logos-co/logos-module-builder{release}";
|
|
|
|
# The module this one depends on. Placeholder path — locked to the
|
|
# real checkout in the build step via --override-input.
|
|
greeter_module.url = "path:/path/to/your/greeter_module";
|
|
};
|
|
|
|
outputs = inputs@{ logos-module-builder, greeter_module, ... }:
|
|
logos-module-builder.lib.mkLogosModule {
|
|
src = ./.;
|
|
configFile = ./metadata.json;
|
|
flakeInputs = inputs;
|
|
};
|
|
}
|
|
|
|
- title: "src/orchestrator_module_impl.h — the class"
|
|
text: |
|
|
Three composition paths, all through `modules().greeter_module`:
|
|
`greetThrough`/`greetReport` (sync), `startAsyncGreet`/`asyncGreeting`
|
|
(async), and `subscribeGreeted`/`lastGreetedEvent` (event).
|
|
file:
|
|
path: orchestrator_module/src/orchestrator_module_impl.h
|
|
language: cpp
|
|
content: |
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
|
|
#include <logos_json.h> // LogosMap
|
|
#include <logos_module_context.h> // LogosModuleContext base + modules()
|
|
|
|
// A caller module. It does nothing on its own — it composes
|
|
// greeter_module through the typed modules().greeter_module wrapper the
|
|
// builder generates from the dependency declared in metadata.json.
|
|
class OrchestratorModuleImpl : public LogosModuleContext {
|
|
public:
|
|
OrchestratorModuleImpl() = default;
|
|
~OrchestratorModuleImpl() = default;
|
|
|
|
/// Calls greeter_module.greet(name) and returns its result verbatim.
|
|
std::string greetThrough(const std::string& name);
|
|
|
|
/// Composes several greeter_module calls into one map: a greeting,
|
|
/// an integer sum, and the greeter's current greet count.
|
|
LogosMap greetReport(const std::string& name, int64_t a, int64_t b);
|
|
|
|
/// Fires greeter_module.greetAsync(name) asynchronously and returns
|
|
/// "queued" immediately; the reply lands in a callback. Read it back
|
|
/// with asyncGreeting().
|
|
std::string startAsyncGreet(const std::string& name);
|
|
|
|
/// The greeting delivered by startAsyncGreet()'s callback, or empty
|
|
/// until it arrives.
|
|
std::string asyncGreeting() const;
|
|
|
|
/// Subscribes to greeter_module's `greeted` event with a typed
|
|
/// callback. Returns "ok" once registered.
|
|
std::string subscribeGreeted();
|
|
|
|
/// The last greeting captured by the `greeted` subscription, or
|
|
/// empty until the event fires.
|
|
std::string lastGreetedEvent() const;
|
|
|
|
private:
|
|
std::string m_asyncGreeting;
|
|
std::string m_lastGreetedEvent;
|
|
bool m_subscribed = false;
|
|
};
|
|
|
|
- title: "src/orchestrator_module_impl.cpp — the implementation"
|
|
text: |
|
|
The `.cpp` includes the generated `logos_sdk.h` (which defines
|
|
`LogosModules`) — that's why the cross-module calls live here and not in
|
|
the header the generator parses. Each method drives `greeter_module`
|
|
through the generated wrapper: `.greet(...)` (sync), `.greetAsync(...,
|
|
cb)` (async), `.onGreeted(cb)` (event).
|
|
file:
|
|
path: orchestrator_module/src/orchestrator_module_impl.cpp
|
|
language: cpp
|
|
content: |
|
|
#include "orchestrator_module_impl.h"
|
|
|
|
// Generated at build time by logos-cpp-generator. Defines LogosModules
|
|
// with one std-typed accessor per metadata.json dependency — here
|
|
// greeter_module. Included only in the .cpp so the impl header the
|
|
// generator parses stays free of Qt and codegen types.
|
|
#include "logos_sdk.h"
|
|
|
|
std::string OrchestratorModuleImpl::greetThrough(const std::string& name)
|
|
{
|
|
// The simplest cross-module round-trip: one typed sync call.
|
|
return modules().greeter_module.greet(name);
|
|
}
|
|
|
|
LogosMap OrchestratorModuleImpl::greetReport(const std::string& name,
|
|
int64_t a, int64_t b)
|
|
{
|
|
// Three typed sync calls into greeter_module, composed into one map.
|
|
auto& greeter = modules().greeter_module;
|
|
LogosMap report;
|
|
report["greeting"] = greeter.greet(name);
|
|
report["sum"] = greeter.addInts(a, b);
|
|
report["greetCount"] = greeter.greetCount();
|
|
return report;
|
|
}
|
|
|
|
std::string OrchestratorModuleImpl::startAsyncGreet(const std::string& name)
|
|
{
|
|
// The generated async overload returns immediately; the reply is
|
|
// delivered to the callback on this module's event loop.
|
|
modules().greeter_module.greetAsync(name, [this](const std::string& g) {
|
|
m_asyncGreeting = g;
|
|
});
|
|
return "queued";
|
|
}
|
|
|
|
std::string OrchestratorModuleImpl::asyncGreeting() const
|
|
{
|
|
return m_asyncGreeting;
|
|
}
|
|
|
|
std::string OrchestratorModuleImpl::subscribeGreeted()
|
|
{
|
|
if (m_subscribed) return "ok";
|
|
// Typed subscriber generated from greeter_module's logos_events:
|
|
// greeted(const std::string&). The accessor is `on` + the
|
|
// capitalized event name.
|
|
m_subscribed = modules().greeter_module.onGreeted(
|
|
[this](const std::string& greeting) {
|
|
m_lastGreetedEvent = greeting;
|
|
});
|
|
return m_subscribed ? "ok" : "failed";
|
|
}
|
|
|
|
std::string OrchestratorModuleImpl::lastGreetedEvent() const
|
|
{
|
|
return m_lastGreetedEvent;
|
|
}
|
|
|
|
- title: "Build both modules against this SDK"
|
|
step: true
|
|
text: |
|
|
Nix flakes only see files tracked by git, so initialise a repo in each
|
|
module first. Then build each module's `.lgx`, overriding `logos-cpp-sdk`
|
|
to the commit under test so the generated wrappers, plugin glue, and IPC
|
|
come from this SDK.
|
|
|
|
> Each override URL carries a `{release}` placeholder the doc-test runner
|
|
> expands to a concrete ref: locally that is this `logos-cpp-sdk`
|
|
> checkout's `HEAD` (see `run.sh`); in CI it is the commit being tested.
|
|
> With no pin it falls back to latest `master`.
|
|
steps:
|
|
- title: "Initialise git repos"
|
|
text: "The greeter is a dependency of the orchestrator, so both must be tracked."
|
|
run: |
|
|
(cd greeter_module && git init -q && git add -A)
|
|
(cd orchestrator_module && git init -q && git add -A)
|
|
check_file: "greeter_module/.git/HEAD"
|
|
|
|
- title: "Build the greeter's .lgx against this SDK"
|
|
text: |
|
|
The greeter has no module dependency, so only its builder's
|
|
`logos-cpp-sdk` needs overriding.
|
|
run: |
|
|
nix build 'path:./greeter_module#lgx' \
|
|
--override-input logos-module-builder 'github:logos-co/logos-module-builder{release}' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
-o greeter-lgx
|
|
code_block: |
|
|
# From inside the greeter clone this is simply:
|
|
# nix build '.#lgx' --override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk'
|
|
nix build 'path:./greeter_module#lgx' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
-o greeter-lgx
|
|
post_text: "The greeter package is under `./greeter-lgx/`:"
|
|
extra_run:
|
|
run: "ls greeter-lgx/*.lgx"
|
|
|
|
- title: "Build the orchestrator's .lgx against this SDK"
|
|
text: |
|
|
The orchestrator pulls in `greeter_module` as a dependency, so we lock
|
|
that input to the local greeter checkout **and** override
|
|
`logos-cpp-sdk` in both the orchestrator's builder and the greeter's
|
|
builder — so the dependency wrapper the generator emits, and both
|
|
plugins, are built against one consistent SDK.
|
|
run: |
|
|
nix build 'path:./orchestrator_module#lgx' \
|
|
--override-input greeter_module 'path:./greeter_module' \
|
|
--override-input logos-module-builder 'github:logos-co/logos-module-builder{release}' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input greeter_module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
-o orchestrator-lgx
|
|
code_block: |
|
|
nix build 'path:./orchestrator_module#lgx' \
|
|
--override-input greeter_module 'path:./greeter_module' \
|
|
--override-input logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input greeter_module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
-o orchestrator-lgx
|
|
post_text: "The orchestrator package is under `./orchestrator-lgx/`:"
|
|
extra_run:
|
|
run: "ls orchestrator-lgx/*.lgx"
|
|
|
|
- title: "Build the runtime and install both modules"
|
|
step: true
|
|
text: |
|
|
Build `logoscore` (against this SDK, the same way as the runtime doc-test)
|
|
and `lgpm`, then install both modules into a `./modules` directory the
|
|
daemon can scan.
|
|
steps:
|
|
- title: "Build logoscore against this SDK"
|
|
run: |
|
|
nix build 'github:logos-co/logos-logoscore-cli{release}' \
|
|
--override-input logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input logos-liblogos/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input logos-module-client/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--override-input logos-capability-module/logos-module-builder 'github:logos-co/logos-module-builder{release}' \
|
|
--override-input logos-capability-module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk{release}' \
|
|
--out-link ./logos
|
|
code_block: |
|
|
nix build 'github:logos-co/logos-logoscore-cli' \
|
|
--override-input logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input logos-liblogos/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input logos-module-client/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--override-input logos-capability-module/logos-module-builder/logos-cpp-sdk 'github:logos-co/logos-cpp-sdk' \
|
|
--out-link ./logos
|
|
check_file: "logos/bin/logoscore"
|
|
|
|
- title: "Build lgpm"
|
|
run: "nix build 'github:logos-co/logos-package-manager#cli' -o lgpm"
|
|
check_file: "lgpm/bin/lgpm"
|
|
|
|
- title: "Seed the modules directory with the capability module"
|
|
text: |
|
|
Loading a module goes through the host's capability layer, so the
|
|
modules directory needs the `capability_module` that ships with
|
|
`logoscore` (rebuilt against this SDK). Copy it across first.
|
|
run: |
|
|
mkdir -p modules
|
|
cp -RL ./logos/modules/. ./modules/
|
|
check_file: "modules/capability_module/manifest.json"
|
|
|
|
- title: "Install the greeter"
|
|
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file greeter-lgx/*.lgx"
|
|
expect_contains:
|
|
- "Installed to:"
|
|
|
|
- title: "Install the orchestrator"
|
|
run: "./lgpm/bin/lgpm --modules-dir ./modules --allow-unsigned install --file orchestrator-lgx/*.lgx"
|
|
expect_contains:
|
|
- "Installed to:"
|
|
|
|
- title: "Confirm both modules are installed"
|
|
run: "./lgpm/bin/lgpm --modules-dir ./modules list"
|
|
expect_contains:
|
|
- "greeter_module"
|
|
- "orchestrator_module"
|
|
check_file: "modules/orchestrator_module/manifest.json"
|
|
|
|
- title: "Load both modules and drive the caller"
|
|
step: true
|
|
text: |
|
|
Start `logoscore` in daemon mode (`-D`) — it keeps each module's process
|
|
alive between `call` commands, so an event subscription registered by one
|
|
call is still active when a later call triggers it, and an async reply
|
|
lands before the call that reads it. Load **both** modules, then call the
|
|
orchestrator's methods — each one reaches across the process boundary into
|
|
the greeter.
|
|
steps:
|
|
- title: "Start the daemon"
|
|
run: "sh -c './logos/bin/logoscore -D -m ./modules > logs.txt 2>&1 &'"
|
|
code_block: "logoscore -D -m ./modules > logs.txt &"
|
|
|
|
- run: "sleep 3"
|
|
|
|
- title: "Load the greeter (the dependency first)"
|
|
run: "./logos/bin/logoscore load-module greeter_module"
|
|
code_block: "logoscore load-module greeter_module"
|
|
expect_contains:
|
|
- "greeter_module"
|
|
|
|
- title: "Load the orchestrator"
|
|
run: "./logos/bin/logoscore load-module orchestrator_module"
|
|
code_block: "logoscore load-module orchestrator_module"
|
|
expect_contains:
|
|
- "orchestrator_module"
|
|
|
|
- title: "Confirm both report loaded"
|
|
run: "./logos/bin/logoscore status"
|
|
code_block: "logoscore status"
|
|
expect_contains:
|
|
- "greeter_module"
|
|
- "orchestrator_module"
|
|
- '"status":"loaded"'
|
|
|
|
- title: "Synchronous cross-module call"
|
|
text: |
|
|
`greetThrough(name)` forwards straight to `greeter_module.greet(name)`
|
|
and returns the result — one typed sync call across the process
|
|
boundary:
|
|
run: "./logos/bin/logoscore call orchestrator_module greetThrough World"
|
|
code_block: "logoscore call orchestrator_module greetThrough World"
|
|
expect_contains:
|
|
- '"result":"Hello, World!"'
|
|
|
|
- title: "Compose several calls into one result"
|
|
text: |
|
|
`greetReport(name, a, b)` fans out to three greeter calls — `greet`,
|
|
`addInts`, and `greetCount` — and returns them as one map. The greeter
|
|
has now been greeted twice (once above, once here), so `greetCount` is
|
|
`2`:
|
|
run: "./logos/bin/logoscore call orchestrator_module greetReport Logos 3 5"
|
|
code_block: "logoscore call orchestrator_module greetReport Logos 3 5"
|
|
expect_contains:
|
|
- '"greeting":"Hello, Logos!"'
|
|
- '"sum":8'
|
|
- '"greetCount":2'
|
|
|
|
- title: "Asynchronous cross-module call"
|
|
text: |
|
|
`startAsyncGreet(name)` fires `greeter_module.greetAsync(name)` and
|
|
returns `"queued"` immediately. The reply arrives on the daemon's event
|
|
loop; the next call, `asyncGreeting()`, reads what the callback stashed:
|
|
run: "./logos/bin/logoscore call orchestrator_module startAsyncGreet Async"
|
|
code_block: "logoscore call orchestrator_module startAsyncGreet Async"
|
|
expect_contains:
|
|
- '"result":"queued"'
|
|
|
|
- run: "sleep 1"
|
|
|
|
- title: "Read the async reply"
|
|
run: "./logos/bin/logoscore call orchestrator_module asyncGreeting"
|
|
code_block: "logoscore call orchestrator_module asyncGreeting"
|
|
expect_contains:
|
|
- '"result":"Hello, Async!"'
|
|
|
|
- title: "Subscribe to the greeter's event"
|
|
text: |
|
|
`subscribeGreeted()` registers a typed callback on `greeter_module`'s
|
|
`greeted` event. Then `greeter_module.greetNotify(...)` makes the
|
|
greeter emit it, and `lastGreetedEvent()` reads what the subscription
|
|
captured. Because the daemon keeps both modules loaded, the event fires
|
|
between the calls:
|
|
run: "./logos/bin/logoscore call orchestrator_module subscribeGreeted"
|
|
code_block: "logoscore call orchestrator_module subscribeGreeted"
|
|
expect_contains:
|
|
- '"result":"ok"'
|
|
|
|
- title: "Trigger the event from the greeter"
|
|
run: "./logos/bin/logoscore call greeter_module greetNotify Events"
|
|
code_block: "logoscore call greeter_module greetNotify Events"
|
|
|
|
- run: "sleep 1"
|
|
|
|
- title: "Read the captured event payload"
|
|
run: "./logos/bin/logoscore call orchestrator_module lastGreetedEvent"
|
|
code_block: "logoscore call orchestrator_module lastGreetedEvent"
|
|
expect_contains:
|
|
- '"result":"Hello, Events!"'
|
|
|
|
- title: "Stop the daemon"
|
|
run: "./logos/bin/logoscore stop"
|
|
code_block: "logoscore stop"
|
|
|
|
- run: "sleep 2"
|
|
|
|
- title: "Confirm the daemon has stopped"
|
|
run: "./logos/bin/logoscore status || true"
|
|
code_block: "logoscore status"
|
|
expect_contains:
|
|
- '"status":"not_running"'
|
|
|
|
- title: "Recap"
|
|
text: |
|
|
| Composition path | In the orchestrator | Driven via `logoscore` |
|
|
| ---------------- | ------------------- | ---------------------- |
|
|
| Typed **sync** call | `greetThrough()` → `greeter.greet(...)` | `"Hello, World!"` |
|
|
| Composed sync calls | `greetReport()` → `greet` + `addInts` + `greetCount` | one map of three results |
|
|
| Typed **async** call | `startAsyncGreet()` → `greetAsync(..., cb)` | `queued`, then `"Hello, Async!"` |
|
|
| Typed **event** subscription | `subscribeGreeted()` → `onGreeted(cb)` | captured `"Hello, Events!"` |
|
|
|
|
Every path went through `modules().greeter_module`, the wrapper the SDK's
|
|
code generator emitted from the `greeter_module` dependency — and both
|
|
modules, the wrapper, and the runtime were built against the SDK commit
|
|
under test. A green run means inter-module composition still works on this
|
|
SDK, end to end.
|