# @anycast/agent

TypeScript SDK for [Anycast Agents](https://agents.anycast.com) P2P connectivity.

Connect agents to the Anycast network for peer-to-peer communication with relay fallback, powered by the global Anycast edge infrastructure.

## Installation

```bash
npm install @anycast/agent
# or
pnpm add @anycast/agent
```

## Quick Start

```typescript
import { AnycastAgent } from '@anycast/agent';

const agent = new AnycastAgent({
  rendezvousUrl: 'wss://agents.anycast.com/rendezvous',
  token: 'agt_your_token_here',
});

agent.on('connect', ({ agentId }) => {
  console.log(`Connected as ${agentId}`);
});

agent.on('peer', (peerId, status) => {
  console.log(`Peer ${peerId} is ${status}`);
});

agent.on('error', (err) => {
  console.error(`Error: ${err.message} (${err.code})`);
});

await agent.connect();

// Request a connection to another agent
const sessionId = agent.requestConnection('target-agent-id');

// Handle incoming WebRTC signals
agent.on('signal', (sessionId, fromAgentId, signal) => {
  // Forward to your WebRTC implementation
});

// Send WebRTC signals
agent.sendSignal(sessionId, 'target-agent-id', {
  type: 'offer',
  data: JSON.stringify(sdpOffer),
});

// Disconnect when done
await agent.disconnect();
```

## API Reference

### `new AnycastAgent(options)`

Create a new agent instance.

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `rendezvousUrl` | `string` | required | WebSocket URL of the rendezvous server |
| `token` | `string` | required | Agent auth token from the Anycast portal |
| `machineId` | `string` | auto-generated | Unique machine identifier |
| `publicKey` | `string` | auto-generated | Public key for E2E encryption |
| `version` | `string` | — | Agent version string |
| `reconnect` | `boolean` | `true` | Auto-reconnect on disconnect |
| `reconnectInterval` | `number` | `3000` | Base reconnect interval (ms) |
| `timeout` | `number` | `30000` | Connection timeout (ms) |
| `heartbeatInterval` | `number` | `25000` | Heartbeat interval (ms) |
| `discoveryMode` | `string` | `'rendezvous'` | Discovery mode: `rendezvous`, `auto`, or `dht` |

### Properties

| Property | Type | Description |
|----------|------|-------------|
| `agentId` | `string \| null` | Assigned agent ID (set after connect) |
| `tenantId` | `string \| null` | Tenant ID (set after connect) |
| `connected` | `boolean` | Whether the agent is connected |
| `connectionState` | `ConnectionState` | Current connection state |

### Methods

#### `connect(): Promise<void>`
Connect to the rendezvous server. Resolves when registered.

#### `disconnect(): Promise<void>`
Disconnect from the server.

#### `requestConnection(targetAgentId: string): string`
Request a P2P connection to another agent. Returns a session ID.

#### `sendSignal(sessionId, targetAgentId, signal)`
Send a WebRTC signal (offer/answer/ICE candidate) via the rendezvous server.

#### `disconnectSession(sessionId: string)`
Close a specific session.

### Events

| Event | Payload | Description |
|-------|---------|-------------|
| `connect` | `{ agentId, tenantId, name }` | Connected and registered |
| `disconnect` | `reason: string` | Disconnected |
| `reconnecting` | `attempt: number` | Attempting reconnection |
| `peer` | `agentId, status` | Peer came online/offline |
| `connect_incoming` | `sessionId, fromAgentId, fromAgentName` | Incoming connection request |
| `connect_accepted` | `sessionId, agentId, publicKey` | Connection accepted by peer |
| `connect_rejected` | `sessionId, reason` | Connection rejected |
| `signal` | `sessionId, fromAgentId, signal` | WebRTC signal from peer |
| `relay_assigned` | `sessionId, host, port, token` | Relay server assigned |
| `error` | `AnycastError` | Error occurred |
| `stateChange` | `ConnectionState` | Connection state changed |
| `discovery` | `{ mode, fallback }` | Discovery mode resolved (emitted before connect) |

### ConnectionState

```typescript
enum ConnectionState {
  DISCONNECTED = 'DISCONNECTED',
  CONNECTING = 'CONNECTING',
  CONNECTED = 'CONNECTED',
  RECONNECTING = 'RECONNECTING',
}
```

### AnycastError

```typescript
class AnycastError extends Error {
  code: string;    // Error code (e.g., 'AUTH_FAILED', 'TIMEOUT')
  fatal: boolean;  // If true, reconnection won't help
}
```

## Usage Tracking & Auto-Instrumentation

Track LLM token usage through the Anycast portal for cost visibility and analytics.

### Manual tracking

```typescript
await agent.trackUsage({
  model: 'claude-sonnet-4-6',
  promptTokens: 1200,
  completionTokens: 340,
  triggeredBy: 'user-123',
  context: 'slack_thread',
});
```

### Anthropic SDK (automatic)

```typescript
import Anthropic from '@anthropic-ai/sdk';

const anthropic = agent.wrapAnthropic(new Anthropic(), {
  triggeredBy: 'user-123',
  context: 'slack_thread',
});
// All calls now auto-report usage — no code changes needed
const msg = await anthropic.messages.create({
  model: 'claude-sonnet-4-6',
  max_tokens: 1024,
  messages: [{ role: 'user', content: 'Hello' }],
});
```

### OpenAI SDK (automatic)

```typescript
import OpenAI from 'openai';

const openai = agent.wrapOpenAI(new OpenAI(), { context: 'api' });
const chat = await openai.chat.completions.create({
  model: 'gpt-4o',
  messages: [{ role: 'user', content: 'Hello' }],
});
```

### Generic wrapper

```typescript
const result = await agent.tracked(
  () => myLlmCall(prompt),
  {
    model: 'custom-model',
    extractUsage: (r) => ({
      promptTokens: r.usage.input,
      completionTokens: r.usage.output,
    }),
  }
);
```

All tracking is opt-in, non-blocking, and never affects the return value or throws.

## Discovery Modes

The SDK supports three discovery modes, controlled via the `discoveryMode` option:

| Mode | Description |
|------|-------------|
| `rendezvous` | **(default)** Connects through the managed Anycast rendezvous server. Most reliable, works everywhere, no extra dependencies. |
| `auto` | Tries DHT (Holepunch) first for direct peer-to-peer discovery. Falls back to rendezvous if `hyperswarm` is not installed or DHT fails. |
| `dht` | DHT only via Holepunch. Requires `hyperswarm` (`npm install hyperswarm`). Throws if the dependency is missing. Experimental. |

```typescript
const agent = new AnycastAgent({
  rendezvousUrl: 'wss://agents.anycast.com/rendezvous',
  token: 'agt_xxx',
  discoveryMode: 'auto', // try DHT, fall back to rendezvous
});

agent.on('discovery', ({ mode, fallback }) => {
  console.log(`Using ${mode} discovery${fallback ? ' (fallback)' : ''}`);
});

await agent.connect();
```

For the Go binary (`any`), use `--discovery-mode`:

```bash
any up --token agt_xxx --expose localhost:8080 --discovery-mode auto
# or via environment variable:
ANY_DISCOVERY_MODE=dht any up --token agt_xxx --expose localhost:8080
```

## Auto-Reconnection

By default, the agent automatically reconnects with exponential backoff (3s, 6s, 12s, ..., max 30s). Disable with `reconnect: false`.

## Receiving Messages from the Platform

Flows and integrations can send messages directly to your agent and await a reply. Handle incoming messages with the `message` event:

```typescript
agent.on('message', async ({ text, context, reply }) => {
  // Process the message — you have 30 seconds to reply
  const response = await handleQuery(text);
  reply(response);
});
```

The `reply()` function must be called within the timeout window (default 30 seconds). After that, the platform marks the delivery as timed out and the response is discarded.

You can also reply manually using the messageId:

```typescript
agent.on('message', ({ messageId, text }) => {
  processAsync(text).then((result) => {
    agent.replyToMessage(messageId, result);
  });
});
```

The `context` field contains metadata from the triggering flow (e.g. Slack channel, thread timestamp, flow ID).

## License

MIT
