Refactor to plugin architecture with B3 CLI UX, C2 smoke tests, C3 CI scripts
Architecture overhaul (Wave 1-4 collaborative work): - Migrated dstalk-core from monolithic api.cpp to plugin-based design with host/service_registry/event_bus/plugin_loader and topological initialization. - Split public headers into dstalk_host.h / dstalk_services.h / dstalk_lsp.h / dstalk_types.h; deleted obsolete dstalk_api.h and inlined TLS/file/net code now provided by plugins. - Added 9 plugins: deepseek, anthropic, network, session, context, tools, config, file-io, lsp; AI plugins register as "ai.<provider>" services. B3 CLI interaction enhancement: - Prompt now shows current model name (A1). - /status command prints model/base_url/api_key (sanitized: shown only as set/unset)/services readiness (A2). - SIGINT/Ctrl+C handled on POSIX (signal) and Windows (SetConsoleCtrlHandler); /quit no longer std::exit(0) but sets a quit flag so dstalk_shutdown runs exactly once via natural control flow (B1+B2). - Cross-DLL free fixed: print_file uses dstalk_free instead of std::free (B4). - --batch mode plus isatty auto-detection for piped stdin (C1). - fgets truncation detection with friendly error and stdin draining (C3). - Distinct exit codes (init/AI/service-unavailable) (C4). - /model rejects empty model name (C5). C2 smoke test extension: - 4 new test blocks: null-safety (file_io/session/tools/config), escape-boundary round-trip, tools->execute call chain, session robustness (add(nullptr), clear -> token_count == 0). C3 CI build scripts: - scripts/ci-build.sh and scripts/ci-build.bat invoke cmake configure + parallel build + ctest, suitable for GitHub Actions. Build verified: dstalk-cli compiles, smoke test passes via ctest. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
151
examples/example_plugin/example_plugin.cpp
Normal file
151
examples/example_plugin/example_plugin.cpp
Normal file
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* example_plugin.cpp - Minimal dstalk plugin demonstrating the API contract.
|
||||
*
|
||||
* Build instructions (conceptual):
|
||||
*
|
||||
* Linux / macOS:
|
||||
* g++ -std=c++20 -shared -fPIC -fvisibility=hidden \
|
||||
* -I<dstalk-include-dir> \
|
||||
* -o example_plugin.so example_plugin.cpp
|
||||
*
|
||||
* Windows (MSVC):
|
||||
* cl /std:c++20 /LD /EHsc \
|
||||
* /I<dstalk-include-dir> \
|
||||
* /Fe:example_plugin.dll example_plugin.cpp
|
||||
*
|
||||
* The resulting `.so` / `.dylib` / `.dll` can be loaded with:
|
||||
*
|
||||
* int id = dstalk_plugin_load("./example_plugin.so");
|
||||
*/
|
||||
|
||||
#include "dstalk/dstalk_host.h"
|
||||
|
||||
#include <cstdio> /* fprintf */
|
||||
#include <cstdlib> /* malloc, free */
|
||||
#include <cstring> /* strlen, strcmp */
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Private state (one instance per plugin load)
|
||||
* ------------------------------------------------------------------
|
||||
*
|
||||
* In a more complex plugin this struct would hold open database
|
||||
* connections, configuration, etc.
|
||||
*/
|
||||
|
||||
struct ExampleState {
|
||||
int call_count;
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Stored host API table so callbacks can use host services.
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static const dstalk_host_api_t* g_host = nullptr;
|
||||
|
||||
static ExampleState g_state; /* not heap-allocated: stays valid
|
||||
while the library is mapped */
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* on_init (was on_load)
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static int my_on_init(const dstalk_host_api_t* host)
|
||||
{
|
||||
g_host = host;
|
||||
g_state.call_count = 0;
|
||||
|
||||
/* TODO: real plugins would initialise resources here:
|
||||
* - parse a plugin-specific config file via host->config_get
|
||||
* - open a log file
|
||||
* - connect to a local service
|
||||
* - register services via host->register_service
|
||||
*
|
||||
* Return non-zero to signal a fatal initialisation error to the
|
||||
* host, which will then unload the plugin immediately.
|
||||
*/
|
||||
|
||||
if (host) {
|
||||
host->log(DSTALK_LOG_INFO, "[example-plugin] loaded (v1.0.0)");
|
||||
} else {
|
||||
std::fprintf(stderr, "[example-plugin] loaded (v1.0.0)\n");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* on_shutdown (was on_unload)
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void my_on_shutdown(void)
|
||||
{
|
||||
/* TODO: release any resources allocated in on_init. After this
|
||||
* function returns the host will unmap the shared library. */
|
||||
|
||||
if (g_host) {
|
||||
g_host->log(DSTALK_LOG_INFO, "[example-plugin] unloaded (%d events processed)",
|
||||
g_state.call_count);
|
||||
} else {
|
||||
std::fprintf(stderr,
|
||||
"[example-plugin] unloaded (%d callbacks processed)\n",
|
||||
g_state.call_count);
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* on_event (was on_message)
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static void my_on_event(int event_type, const void* data)
|
||||
{
|
||||
if (event_type == DSTALK_EVENT_MESSAGE && data) {
|
||||
const auto* msg = static_cast<const dstalk_message_t*>(data);
|
||||
g_state.call_count++;
|
||||
|
||||
/* A real plugin might:
|
||||
* - log the conversation to a file
|
||||
* - apply content moderation
|
||||
* - translate messages on the fly
|
||||
* - enrich messages with external data
|
||||
*/
|
||||
|
||||
if (g_host) {
|
||||
g_host->log(DSTALK_LOG_DEBUG, "[example-plugin] message | role=%-9s len=%zu",
|
||||
msg->role, std::strlen(msg->content));
|
||||
} else {
|
||||
std::fprintf(stderr,
|
||||
"[example-plugin] message | role=%-9s len=%zu\n",
|
||||
msg->role, std::strlen(msg->content));
|
||||
}
|
||||
}
|
||||
/* Other event types (DSTALK_EVENT_SESSION_CLEAR, DSTALK_EVENT_CONFIG_CHANGED,
|
||||
DSTALK_EVENT_PLUGIN_LOADED, DSTALK_EVENT_PLUGIN_UNLOADED, DSTALK_EVENT_CUSTOM+)
|
||||
are silently ignored by this minimal plugin. */
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Plugin descriptor (static -- lives for the lifetime of the .so)
|
||||
* ------------------------------------------------------------------ */
|
||||
|
||||
static dstalk_plugin_info_t g_info = {
|
||||
/* .name = */ "example-plugin",
|
||||
/* .version = */ "1.0.0",
|
||||
/* .description = */ "An example plugin for dstalk",
|
||||
/* .api_version = */ DSTALK_API_VERSION,
|
||||
/* .dependencies = */ {nullptr},
|
||||
/* .on_init = */ my_on_init,
|
||||
/* .on_shutdown = */ my_on_shutdown,
|
||||
/* .on_event = */ my_on_event,
|
||||
};
|
||||
|
||||
/* ------------------------------------------------------------------
|
||||
* Mandatory entry point
|
||||
* ------------------------------------------------------------------
|
||||
*
|
||||
* The host looks for this symbol via dlsym / GetProcAddress.
|
||||
* It MUST be declared extern "C" so the name is not mangled.
|
||||
*/
|
||||
|
||||
extern "C" DSTALK_PLUGIN_EXPORT dstalk_plugin_info_t* dstalk_plugin_init(void)
|
||||
{
|
||||
return &g_info;
|
||||
}
|
||||
Reference in New Issue
Block a user