A bundle that is not JavaScript is launched and spoken to over MCP on stdio (hq ADR 0187, to-be 38 WP1b)
The runtime imported a bundle into its own process, which only JavaScript can be. Now an entrypoint that is not a plain JavaScript file — or is one marked executable — is started as a child with the runtime's environment and asked `tools/list` once and `tools/call` per call; what it lists is registered exactly as an imported bundle's registrations are, a `<seat>.<verb>` name as the seat's implementation. So a tools bundle may be in any language, and the mesh's part — the subjects, the seats, the `tools` answer, a failed bundle named — stays in the runtime and is shared by all of them. A child that exits mid-call tells the caller so and is started again on its next call. Proven against a real bus beside the three bundles already there: a Python bundle with no SDK at all answers its tool and its seat verb; a TypeScript bundle written against the protocol and marked executable is served through the launcher, shortcut off; a bundle told to exit is relaunched.
This commit is contained in:
+21
-6
@@ -15,6 +15,7 @@ import { useBroker } from "@novox/mesh-sdk/messaging";
|
||||
import { collectTools, toolKey, type ToolDefinition } from "@novox/mesh-sdk/tools";
|
||||
import type { Broker } from "@novox/mesh-sdk/messaging";
|
||||
import { atWork, seatToolSubject, type Credential, type RuntimeBroker } from "./broker-nats.js";
|
||||
import { launch, launches } from "./launch.js";
|
||||
|
||||
/**
|
||||
* The one verb every module's runtime answers for it (novox/hq ADR 0152, design 34 §3): the
|
||||
@@ -106,20 +107,31 @@ export async function runTools(opts: RuntimeOptions): Promise<() => void> {
|
||||
// Importing the entrypoint runs its registerModuleTools(...) — that is the whole handshake — and
|
||||
// the registrations it adds are the ones that appear after it, which is how each is attributed
|
||||
// to the module whose bundle made it.
|
||||
// A bundle that is not plain JavaScript — or is marked executable — is launched as a process
|
||||
// and spoken to over MCP on stdio instead (ADR 0187); what it lists is registered the same way.
|
||||
const failed = new Map<string, string>();
|
||||
const owner: string[] = []; // registration index → the module whose bundle registered it
|
||||
const launched: { module: string; owner: string; tools: ToolDefinition[] }[] = [];
|
||||
const children: Array<() => void> = [];
|
||||
for (const [module, entrypoints] of served) {
|
||||
for (const entry of entrypoints) {
|
||||
const before = collectTools().length;
|
||||
const path = resolve(entry);
|
||||
try {
|
||||
await import(pathToFileURL(resolve(entry)).href);
|
||||
if (launches(path)) {
|
||||
const child = await launch(module, path);
|
||||
children.push(child.stop);
|
||||
for (const r of child.registrations) launched.push({ ...r, owner: module });
|
||||
continue;
|
||||
}
|
||||
const before = collectTools().length;
|
||||
await import(pathToFileURL(path).href);
|
||||
const after = collectTools().length;
|
||||
for (let i = before; i < after; i++) owner[i] = module;
|
||||
} catch (err) {
|
||||
const why = err instanceof Error ? err.message : String(err);
|
||||
failed.set(module, why);
|
||||
console.log(`[mesh-tools] ${module}'s bundle ${entry} failed to load: ${why}; its tools are not served here`);
|
||||
}
|
||||
const after = collectTools().length;
|
||||
for (let i = before; i < after; i++) owner[i] = module;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,14 +143,17 @@ export async function runTools(opts: RuntimeOptions): Promise<() => void> {
|
||||
// out rather than fatal — on 2026-10-01 the credential of a module that had just learned to
|
||||
// implement a seat did not yet name the claim, and the whole runtime restarted for it.
|
||||
const claimed = seatsClaimed(served.keys(), self, opts.credential, runtime);
|
||||
const registrations = collectTools().map((r, i) => ({ ...r, owner: owner[i] ?? self ?? r.module }));
|
||||
const registrations = [
|
||||
...collectTools().map((r, i) => ({ ...r, owner: owner[i] ?? self ?? r.module })),
|
||||
...launched,
|
||||
];
|
||||
const ownRegistrations = registrations.filter(({ module, owner: by }) => {
|
||||
if (served.has(module)) return true;
|
||||
if (claimed.has(module)) return false;
|
||||
console.log(`[mesh-tools] ${by} registers tools under "${module}", which is neither a module served here nor a seat one of them claims; not served until the mesh issues the claim`);
|
||||
return false;
|
||||
});
|
||||
const stops: Array<() => void> = [];
|
||||
const stops: Array<() => void> = [...children];
|
||||
const stop = (): void => stops.splice(0).forEach((s) => s());
|
||||
|
||||
// Refused before anything is bound if a module named a tool of its own `tools`: one name
|
||||
|
||||
Reference in New Issue
Block a user