Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,31 @@ for await (const event of client.observe(intent.intent_id)) {

---

## Agent Mesh - Monitor and Govern

```typescript
// Start heartbeat - agent appears in dashboard with live health
client.mesh.startHeartbeat(); // background interval, every 30s

// Report metrics after each task
client.mesh.reportMetric({ success: true, latencyMs: 230, costUsd: 0.02 });

// List all agents with health status
const agents = await client.mesh.listAgents();

// Kill a misbehaving agent - blocks all intents instantly
await client.mesh.kill("addr_...");

// Resume it
await client.mesh.resume("addr_...");
```

Open the live dashboard at [mesh.axme.ai](https://mesh.axme.ai) or run `axme mesh dashboard` from the CLI.

Set action policies (allowlist/denylist intent types) and cost policies (intents/day, $/day limits) per agent via dashboard or API. [Agent Mesh overview](https://github.com/AxmeAI/axme#agent-mesh---see-and-control-your-agents).

---

## Examples

```bash
Expand Down
10 changes: 10 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AxmeServerError,
AxmeValidationError,
} from "./errors.js";
import { MeshClient } from "./mesh.js";

export type AxmeClientConfig = {
baseUrl?: string;
Expand Down Expand Up @@ -190,6 +191,15 @@ export class AxmeClient {
private readonly mcpObserver?: (event: McpObserverEvent) => void;
private readonly mcpToolSchemas: Record<string, Record<string, unknown>>;
private readonly fetchImpl: typeof fetch;
private _mesh: MeshClient | null = null;

/** Access Agent Mesh operations (heartbeat, health, kill switch). */
get mesh(): MeshClient {
if (this._mesh === null) {
this._mesh = new MeshClient(this);
}
return this._mesh;
}

constructor(config: AxmeClientConfig, fetchImpl: typeof fetch = fetch) {
if (config.actorToken && config.bearerToken && config.actorToken !== config.bearerToken) {
Expand Down
9 changes: 9 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,12 @@ export {
AxmeServerError,
AxmeValidationError,
} from "./errors.js";
export {
MeshClient,
type MeshMetric,
type MeshAgent,
type MeshAgentsResponse,
type MeshEvent,
type ListAgentsOptions,
type ListEventsOptions,
} from "./mesh.js";
190 changes: 190 additions & 0 deletions src/mesh.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/**
* Agent Mesh module - heartbeat, health monitoring, metrics reporting.
*/

import type { AxmeClient } from "./client.js";

export type MeshMetric = {
success?: boolean;
latencyMs?: number;
costUsd?: number;
};

export type MeshAgent = {
address_id: string;
address: string;
display_name: string;
health_status: "healthy" | "degraded" | "unreachable" | "killed" | "unknown";
last_heartbeat_at: string | null;
created_at: string;
intents_period: number;
cost_period: number;
metadata: Record<string, unknown> | null;
};

export type MeshAgentsResponse = {
ok: boolean;
agents: MeshAgent[];
summary: {
total: number;
healthy: number;
degraded: number;
unreachable: number;
killed: number;
};
};

export type MeshEvent = {
event_id: string;
address: string;
event_type: string;
details: Record<string, unknown> | null;
actor_id: string | null;
created_at: string;
};

export type ListAgentsOptions = {
limit?: number;
health?: string;
window?: "day" | "week" | "month";
traceId?: string;
};

export type ListEventsOptions = {
limit?: number;
eventType?: string;
traceId?: string;
};

export class MeshClient {
private client: AxmeClient;
private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
private metricsBuffer: {
intents_total: number;
intents_succeeded: number;
intents_failed: number;
avg_latency_ms: number | null;
cost_usd: number;
} = { intents_total: 0, intents_succeeded: 0, intents_failed: 0, avg_latency_ms: null, cost_usd: 0 };

constructor(client: AxmeClient) {
this.client = client;
}

// ── Heartbeat ────────────────────────────────────────────────────

async heartbeat(metrics?: Record<string, unknown>, options?: { traceId?: string }): Promise<Record<string, unknown>> {
const body: Record<string, unknown> = {};
if (metrics) body.metrics = metrics;
return this.client["requestJson"]("/v1/mesh/heartbeat", {
method: "POST",
body: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined,
retryable: true,
traceId: options?.traceId,
});
}

startHeartbeat(intervalMs: number = 30_000, includeMetrics: boolean = true): void {
if (this.heartbeatTimer !== null) return;

this.heartbeatTimer = setInterval(async () => {
try {
const metrics = includeMetrics ? this.flushMetrics() : undefined;
await this.heartbeat(metrics ?? undefined);
} catch {
// Heartbeat failures are non-fatal
}
}, intervalMs);

// Unref so timer doesn't prevent Node.js from exiting
if (typeof this.heartbeatTimer === "object" && "unref" in this.heartbeatTimer) {
this.heartbeatTimer.unref();
}
}

stopHeartbeat(): void {
if (this.heartbeatTimer !== null) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
}

// ── Metrics ──────────────────────────────────────────────────────

reportMetric(metric: MeshMetric): void {
const buf = this.metricsBuffer;
buf.intents_total += 1;
if (metric.success !== false) {
buf.intents_succeeded += 1;
} else {
buf.intents_failed += 1;
}
if (metric.latencyMs !== undefined) {
const count = buf.intents_total;
const prevAvg = buf.avg_latency_ms ?? 0;
buf.avg_latency_ms = prevAvg + (metric.latencyMs - prevAvg) / count;
}
if (metric.costUsd !== undefined) {
buf.cost_usd += metric.costUsd;
}
}

private flushMetrics(): Record<string, unknown> | null {
const buf = this.metricsBuffer;
if (buf.intents_total === 0) return null;
const metrics = { ...buf };
this.metricsBuffer = { intents_total: 0, intents_succeeded: 0, intents_failed: 0, avg_latency_ms: null, cost_usd: 0 };
return metrics;
}

// ── Agent management ─────────────────────────────────────────────

async listAgents(options: ListAgentsOptions = {}): Promise<MeshAgentsResponse> {
const params = new URLSearchParams();
if (options.limit) params.set("limit", String(options.limit));
if (options.health) params.set("health", options.health);
if (options.window) params.set("window", options.window);
const qs = params.toString();
return this.client["requestJson"](`/v1/mesh/agents${qs ? `?${qs}` : ""}`, {
method: "GET",
retryable: true,
traceId: options.traceId,
}) as Promise<MeshAgentsResponse>;
}

async getAgent(addressId: string, options?: { traceId?: string }): Promise<Record<string, unknown>> {
return this.client["requestJson"](`/v1/mesh/agents/${addressId}`, {
method: "GET",
retryable: true,
traceId: options?.traceId,
});
}

async kill(addressId: string, options?: { traceId?: string }): Promise<Record<string, unknown>> {
return this.client["requestJson"](`/v1/mesh/agents/${addressId}/kill`, {
method: "POST",
retryable: false,
traceId: options?.traceId,
});
}

async resume(addressId: string, options?: { traceId?: string }): Promise<Record<string, unknown>> {
return this.client["requestJson"](`/v1/mesh/agents/${addressId}/resume`, {
method: "POST",
retryable: false,
traceId: options?.traceId,
});
}

async listEvents(options: ListEventsOptions = {}): Promise<{ ok: boolean; events: MeshEvent[] }> {
const params = new URLSearchParams();
if (options.limit) params.set("limit", String(options.limit));
if (options.eventType) params.set("event_type", options.eventType);
const qs = params.toString();
return this.client["requestJson"](`/v1/mesh/events${qs ? `?${qs}` : ""}`, {
method: "GET",
retryable: true,
traceId: options.traceId,
}) as Promise<{ ok: boolean; events: MeshEvent[] }>;
}
}
Loading