@kontextmind/kxm
Version:
KXM local-first multi-agent orchestration and operator dashboard
570 lines (565 loc) • 18.6 kB
JavaScript
// plugins/kxm/src/client.ts
import { createHash } from "node:crypto";
import { hostname } from "node:os";
// plugins/kxm/src/protocol.ts
var DEFAULT_MESSAGE_TTL_MS = 24 * 60 * 6e4;
var MAX_MESSAGE_TTL_MS = 7 * 24 * 60 * 6e4;
var DEFAULT_MESSAGE_RETENTION_MS = 7 * 24 * 60 * 6e4;
var MAX_BODY_BYTES = 256 * 1024;
var MAX_AGENT_HOST_CHARS = 64;
var MAX_LEASE_TTL_MS = 10 * 6e4;
var DEFAULT_LEASE_TTL_MS = 5 * 6e4;
// plugins/kxm/src/relevance.ts
var RELEVANCE_STOPWORDS = Object.freeze(/* @__PURE__ */ new Set([
"a",
"an",
"and",
"are",
"as",
"at",
"be",
"been",
"but",
"by",
"can",
"could",
"did",
"do",
"does",
"for",
"from",
"had",
"has",
"have",
"how",
"if",
"in",
"into",
"is",
"it",
"its",
"of",
"on",
"or",
"our",
"should",
"so",
"than",
"that",
"the",
"their",
"them",
"then",
"there",
"these",
"they",
"this",
"those",
"to",
"was",
"we",
"were",
"what",
"when",
"where",
"which",
"while",
"who",
"why",
"will",
"with",
"would",
"you",
"your"
]));
// plugins/kxm/src/workflow.ts
function canonicalWorkflowEvidenceKey(value) {
return value.trim().replace(/\s+/gu, " ").toLowerCase();
}
// plugins/kxm/src/client.ts
var MeshWaitError = class extends Error {
waitStatus;
constructor(waitStatus, messageId) {
super(waitStatus === "aborted" ? "await cancelled" : `timed out waiting for ${messageId}`);
this.name = "MeshWaitError";
this.waitStatus = waitStatus;
}
};
function defaultHostLabel() {
try {
return hostname().trim().slice(0, MAX_AGENT_HOST_CHARS) || void 0;
} catch {
return void 0;
}
}
function completedFanoutResult(target, message) {
if (message.status === "queued" || message.status === "delivered") {
throw new Error(`message ${message.id} is not complete`);
}
return {
target,
messageId: message.id,
status: message.status,
...message.reply ? { reply: message.reply.content } : {},
...message.error ? { error: message.error } : {}
};
}
function fanoutIdempotencyKey(prefix, target, correlationId, workflowContext) {
const scope = JSON.stringify(workflowContext ? {
prefix,
correlationId: correlationId ?? null,
target: target.toLowerCase(),
workflowContext: {
runId: workflowContext.runId,
stageId: workflowContext.stageId,
requirementKey: canonicalWorkflowEvidenceKey(workflowContext.requirementKey),
attempt: workflowContext.attempt
}
} : {
prefix,
correlationId: correlationId ?? null,
target: target.toLowerCase()
});
return `fanout:${createHash("sha256").update(scope).digest("hex")}`;
}
var HubHttpError = class extends Error {
statusCode;
code;
requestId;
extras;
constructor(statusCode, message, code, requestId, extras) {
super(message);
this.name = "HubHttpError";
this.statusCode = statusCode;
if (code) this.code = code;
if (requestId) this.requestId = requestId;
if (extras) this.extras = extras;
}
};
var HubClient = class {
options;
agent;
agentKey;
heartbeatTimer;
eventsAbort;
stopped = true;
onEvent;
registration;
eventLoop;
constructor(options) {
this.options = { heartbeatMs: 1e4, reconnectMs: 1e3, requestTimeoutMs: 15e3, ...options };
}
async start(onEvent) {
if (!this.stopped) throw new Error("hub client is already started");
this.stopped = false;
this.onEvent = onEvent;
try {
await this.register();
} catch (error) {
this.stopped = true;
throw error;
}
this.heartbeatTimer = setInterval(() => void this.heartbeat(), this.options.heartbeatMs);
this.heartbeatTimer.unref();
this.eventLoop = this.runEventLoop();
return this.agent;
}
async stop() {
if (this.stopped) return;
this.stopped = true;
this.eventsAbort?.abort();
if (this.heartbeatTimer) clearInterval(this.heartbeatTimer);
await this.eventLoop;
if (this.agent) {
try {
await this.request(`/v1/agents/${encodeURIComponent(this.agent.id)}`, { method: "DELETE" });
} catch {
}
}
this.agent = void 0;
this.agentKey = void 0;
this.onEvent = void 0;
this.eventLoop = void 0;
}
/** Online peers of this client's project. `includeOffline` also returns
* registered members whose lease the hub has already retired. */
async listAgents(options = {}) {
const path = options.includeOffline ? "/v1/agents?includeOffline=true" : "/v1/agents";
const result = await this.request(path);
return result.agents;
}
async send(options) {
const result = await this.request("/v1/messages", {
method: "POST",
body: JSON.stringify(options)
});
return result.message;
}
async fanout(options) {
const targets = [...new Set(options.targets.map((target) => target.trim().toLowerCase()).filter(Boolean))];
if (targets.length < 1 || targets.length > 3) throw new Error("fanout requires between one and three unique targets");
return await Promise.all(targets.map(async (target) => {
let message;
try {
message = await this.send({
target,
content: options.content,
delivery: "followUp",
...options.correlationId ? { correlationId: options.correlationId } : {},
...options.workflowContext ? { workflowContext: options.workflowContext } : {},
...options.idempotencyKeyPrefix ? {
idempotencyKey: fanoutIdempotencyKey(
options.idempotencyKeyPrefix,
target,
options.correlationId,
options.workflowContext
)
} : {},
...options.ttlMs ? { ttlMs: options.ttlMs } : {},
...options.hops !== void 0 ? { hops: options.hops } : {},
...options.maxHops !== void 0 ? { maxHops: options.maxHops } : {}
});
const completed = await this.awaitResponse(
message.id,
options.timeoutMs ?? 30 * 6e4,
options.signal
);
return completedFanoutResult(target, completed);
} catch (error) {
if (message && error instanceof MeshWaitError) {
try {
const current = await this.getMessage(message.id);
if (current.status === "queued" || current.status === "delivered") {
return {
target,
messageId: current.id,
status: "pending",
messageStatus: current.status,
expiresAt: current.expiresAt,
waitStatus: error.waitStatus
};
}
return completedFanoutResult(target, current);
} catch (finalError) {
return {
target,
messageId: message.id,
status: "error",
error: finalError instanceof Error ? finalError.message : String(finalError)
};
}
}
return {
target,
...message ? { messageId: message.id } : {},
status: "error",
error: error instanceof Error ? error.message : String(error)
};
}
}));
}
/** This agent's open inbound requests (queued or delivered), oldest first. A read: nothing
* is acknowledged. */
async listInbox() {
if (!this.agent) throw new Error("hub client is not registered");
const result = await this.request(
`/v1/agents/${encodeURIComponent(this.agent.id)}/inbox`
);
return result.messages;
}
async getMessage(messageId) {
const result = await this.request(`/v1/messages/${encodeURIComponent(messageId)}`);
return result.message;
}
async acknowledge(messageId) {
const result = await this.request(
`/v1/messages/${encodeURIComponent(messageId)}/ack`,
{ method: "POST", body: "{}" }
);
return result.message;
}
async reply(messageId, content) {
const result = await this.request(
`/v1/messages/${encodeURIComponent(messageId)}/reply`,
{ method: "POST", body: JSON.stringify({ content }) }
);
return result.message;
}
async cancel(messageId) {
const result = await this.request(
`/v1/messages/${encodeURIComponent(messageId)}`,
{ method: "DELETE" }
);
return result.message;
}
// ----- Fenced leases over shared resources (P3) -----
/**
* Take or extend the lease over `resource` inside this client's project.
*
* The returned `fencingToken` is the whole point: hold it, present it on every
* renewal, and present it again before committing anything shared. A hub that
* has moved past it refuses, and the caller must stop rather than retry —
* another holder owns the resource now. Rejects `HubHttpError` with code
* `lease_held` when a live holder has it.
*/
async acquireLease(resource, ttlMs) {
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/acquire`, {
method: "POST",
body: JSON.stringify(ttlMs === void 0 ? {} : { ttlMs })
});
}
/** Extend a lease this client holds. The token never changes on renewal; a
* `lease_superseded` or `lease_expired` refusal means it is gone. */
async renewLease(resource, fencingToken, ttlMs) {
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/renew`, {
method: "POST",
body: JSON.stringify(ttlMs === void 0 ? { fencingToken } : { fencingToken, ttlMs })
});
}
/** Give the resource back. */
async releaseLease(resource, fencingToken) {
return await this.request(`/v1/leases/${encodeURIComponent(resource)}/release`, {
method: "POST",
body: JSON.stringify({ fencingToken })
});
}
async listWorkflows() {
const result = await this.request("/v1/workflows");
return result.runs;
}
async getWorkflow(runId) {
return await this.request(`/v1/workflows/${encodeURIComponent(runId)}`);
}
async checkpointWorkflow(runId, input) {
return await this.request(`/v1/workflows/${encodeURIComponent(runId)}/checkpoints`, {
method: "POST",
body: JSON.stringify(input)
});
}
async waitForWorkflowSignal(runId, input) {
return await this.request(`/v1/workflows/${encodeURIComponent(runId)}/waits`, {
method: "POST",
body: JSON.stringify(input)
});
}
async recordWorkflowEntry(runId, input) {
const result = await this.request(
`/v1/workflows/${encodeURIComponent(runId)}/journal`,
{ method: "POST", body: JSON.stringify(input) }
);
return result.entry;
}
async improvementReport() {
return await this.request("/v1/improvements");
}
// ----- Context operating-system API (v0.5, issue #34) -----
async contextGet(input) {
return await this.request("/v1/context/get", { method: "POST", body: JSON.stringify(input) });
}
async contextRecall(input) {
return await this.request("/v1/context/recall", { method: "POST", body: JSON.stringify(input) });
}
async contextState(input) {
return await this.request("/v1/context/state", { method: "POST", body: JSON.stringify(input) });
}
async contextStatePropose(input) {
return await this.request("/v1/context/state/propose", { method: "POST", body: JSON.stringify(input) });
}
async contextStatePromote(input) {
return await this.request("/v1/context/state/promote", { method: "POST", body: JSON.stringify(input) });
}
async contextEpisode(input) {
return await this.request("/v1/context/episode", { method: "POST", body: JSON.stringify(input) });
}
async contextExplain(input) {
return await this.request("/v1/context/explain", { method: "POST", body: JSON.stringify(input) });
}
async contextWikiCompile(input) {
return await this.request("/v1/context/wiki/compile", { method: "POST", body: JSON.stringify(input) });
}
async awaitResponse(messageId, timeoutMs = 30 * 6e4, signal) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
if (signal?.aborted) throw new MeshWaitError("aborted", messageId);
const message = await this.getMessage(messageId);
if (["replied", "cancelled", "expired", "error"].includes(message.status)) return message;
await new Promise((resolve, reject) => {
const onAbort = () => {
signal?.removeEventListener("abort", onAbort);
clearTimeout(timer);
reject(new MeshWaitError("aborted", messageId));
};
const timer = setTimeout(() => {
signal?.removeEventListener("abort", onAbort);
resolve();
}, Math.min(500, Math.max(1, deadline - Date.now())));
signal?.addEventListener("abort", onAbort, { once: true });
if (signal?.aborted) onAbort();
timer.unref();
});
}
throw new MeshWaitError("timed_out", messageId);
}
async heartbeat() {
if (this.stopped || !this.agent) return;
try {
await this.request(`/v1/agents/${encodeURIComponent(this.agent.id)}/heartbeat`, {
method: "POST",
body: "{}"
});
} catch (error) {
if (error instanceof HubHttpError && error.statusCode === 401) void this.recoverRegistration();
}
}
async runEventLoop() {
while (!this.stopped && this.agent) {
this.eventsAbort = new AbortController();
try {
const response = await (this.options.fetchImpl ?? fetch)(
`${this.options.serverUrl.replace(/\/$/, "")}/v1/events?agentId=${encodeURIComponent(this.agent.id)}`,
{
headers: this.headers(),
signal: this.eventsAbort.signal
}
);
if (response.status === 401) {
await response.body?.cancel();
await this.recoverRegistration();
continue;
}
if (!response.ok || !response.body) throw new Error(`event stream failed with HTTP ${response.status}`);
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of response.body) {
if (this.stopped) break;
buffer += decoder.decode(chunk, { stream: true }).replaceAll("\r\n", "\n");
let boundary;
while ((boundary = buffer.indexOf("\n\n")) >= 0) {
const frame = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
const data = frame.split("\n").filter((line) => line.startsWith("data:")).map((line) => line.slice(5).trimStart()).join("\n");
if (!data) continue;
const parsed = JSON.parse(data);
if ("type" in parsed) await this.onEvent?.(parsed);
}
}
} catch (error) {
if (this.stopped || error instanceof Error && error.name === "AbortError") return;
}
if (!this.stopped) await new Promise((resolve) => setTimeout(resolve, this.options.reconnectMs));
}
}
headers(includeIdentity = true) {
const headers = { "content-type": "application/json" };
if (this.options.authToken) headers.authorization = `Bearer ${this.options.authToken}`;
if (includeIdentity && this.agent && this.agentKey) {
headers["x-kxm-agent-id"] = this.agent.id;
headers["x-kxm-agent-key"] = this.agentKey;
}
return headers;
}
async register() {
if (this.registration) return this.registration;
this.registration = (async () => {
const registration = await this.request("/v1/agents/register", {
method: "POST",
body: JSON.stringify({
name: this.options.name,
purpose: this.options.purpose,
project: this.options.project,
model: this.options.model,
host: this.options.host ?? defaultHostLabel()
})
}, false);
this.agent = registration.agent;
this.agentKey = registration.agentKey;
return registration.agent;
})();
try {
return await this.registration;
} finally {
this.registration = void 0;
}
}
async recoverRegistration() {
if (this.stopped) return;
this.agent = void 0;
this.agentKey = void 0;
await this.register();
}
async request(path, init = {}, includeIdentity = true) {
return await hubJsonRequest(this.options, path, init, this.headers(includeIdentity));
}
};
async function hubJsonRequest(options, path, init, headers) {
const requestTimeoutMs = options.requestTimeoutMs ?? 15e3;
const timeoutSignal = AbortSignal.timeout(requestTimeoutMs);
const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
let response;
try {
response = await (options.fetchImpl ?? fetch)(`${options.serverUrl.replace(/\/$/, "")}${path}`, {
...init,
signal,
headers: { ...headers, ...init.headers ?? {} }
});
} catch (error) {
if (timeoutSignal.aborted) throw new Error(`request timed out after ${requestTimeoutMs}ms`);
throw error;
}
const text = await response.text();
let body = {};
if (text) {
try {
body = JSON.parse(text);
} catch {
throw new Error(`hub returned invalid JSON with HTTP ${response.status}`);
}
}
if (!response.ok) {
const extras = {};
for (const key of ["operation", "nextAction", "assignedCoordinatorName"]) {
if (typeof body[key] === "string") extras[key] = body[key];
}
if (body.lease && typeof body.lease === "object") extras.lease = body.lease;
throw new HubHttpError(
response.status,
String(body.error ?? `HTTP ${response.status}`),
typeof body.code === "string" ? body.code : void 0,
response.headers.get("x-request-id") ?? void 0,
Object.keys(extras).length > 0 ? extras : void 0
);
}
return body;
}
var RuntimeHubClient = class {
options;
constructor(options) {
this.options = options;
}
async heartbeat() {
const result = await this.request("/v1/runtime/presence", {
project: this.options.project,
runtimeId: this.options.runtimeId,
host: this.options.host ?? defaultHostLabel()
});
return result.presence;
}
/** Push already-derived `kxm.sync-event.v1` objects, in outbox order. */
async pushSyncEvents(events) {
return await this.request("/v1/sync/events", {
project: this.options.project,
runtimeId: this.options.runtimeId,
events
});
}
async request(path, body) {
const headers = { "content-type": "application/json" };
if (this.options.authToken) headers.authorization = `Bearer ${this.options.authToken}`;
return await hubJsonRequest(this.options, path, { method: "POST", body: JSON.stringify(body) }, headers);
}
};
export {
HubClient,
HubHttpError,
RuntimeHubClient
};