Skip to content

TypeScript SDK Guide

Active Development

The SDK is under active development. Pin to a specific release for production use.

The TypeScript SDK lets you build Aether agents using modern async/await with full TypeScript types. It communicates with the Hub over gRPC via @grpc/grpc-js.

Installation

From npm

bash
npm install @aether/agent-sdk

From Source

bash
git clone https://github.com/amansrivastava/hacky-automation.git
cd hacky-automation/sdks/typescript

npm install
npm run build

Requires: Node.js 16+

Configuration

Environment Variables

bash
AETHER_AGENT_ID=my-agent-001
AETHER_AGENT_SECRET=sk-secret
AETHER_AGENT_NAME="My Agent"
AETHER_HUB_ADDRESS=localhost:50051
AETHER_VERSION=1.0.0
AETHER_HEARTBEAT_INTERVAL=30
AETHER_RECONNECT_DELAY=5
AETHER_MAX_RECONNECT_ATTEMPTS=-1

AgentConfig Reference

OptionTypeDefaultDescription
agentIdstringRequiredUnique identifier (e.g. pm_sarah)
agentSecretstringRequiredAuthentication secret
agentNamestringRequiredHuman-readable name
versionstring"0.1.0"Agent version
hubAddressstring"localhost:50051"Hub gRPC endpoint
capabilitiesobject{}Key-value capability flags
metadataobject{}Arbitrary key-value metadata
heartbeatIntervalnumber30Seconds between heartbeats
reconnectDelaynumber5Seconds before reconnect attempt
maxReconnectAttemptsnumber-1Max attempts (-1 = infinite)

Quickstart

Create Your Agent

Extend AetherAgent and override handleTask:

typescript
import { AetherAgent, Task, TaskResult } from '@aether/agent-sdk';

class MyAgent extends AetherAgent {
  agentId = 'my-agent-001';
  capabilities = ['analysis', 'processing'];

  async handleTask(task: Task): Promise<TaskResult> {
    console.log(`Processing task ${task.taskId}: ${task.description}`);

    try {
      const result = `Processed: ${task.description}`;
      return {
        taskId: task.taskId,
        success: true,
        result: Buffer.from(result),
        metrics: { processing_time: '0.5s' },
      };
    } catch (err) {
      return {
        taskId: task.taskId,
        success: false,
        errorMessage: String(err),
      };
    }
  }
}

const agent = new MyAgent({
  agentId: 'my-agent-001',
  agentSecret: process.env.AGENT_SECRET ?? '',
  agentName: 'My Example Agent',
  hubAddress: process.env.HUB_ADDRESS ?? 'localhost:50051',
});

agent.run();

Loading Config from Environment

typescript
import { AetherAgent, loadConfigFromEnv, Task, TaskResult } from '@aether/agent-sdk';

class MyAgent extends AetherAgent {
  agentId = 'my-agent';

  async handleTask(task: Task): Promise<TaskResult> {
    // your logic
    return { taskId: task.taskId, success: true, result: Buffer.from('done') };
  }
}

const config = loadConfigFromEnv({
  agentId: 'my-agent',
  agentName: 'My Agent',
});

new MyAgent(config).run();

Start, Stop, Run

typescript
// Start: connect + register (non-blocking)
await agent.start();

// Stop: unregister + disconnect
await agent.stop();

// Run: start + block until stopped (handles SIGINT/SIGTERM)
await agent.run();

Hub Services

Access Hub-proxied services through this.hub inside handleTask.

LLM

typescript
const response = await this.hub.llm.chat({
  messages: [
    { role: 'system', content: 'You are a helpful assistant.' },
    { role: 'user', content: task.payload.toString() },
  ],
  model: 'gpt-4o-mini',   // Hub may override based on trust level
  temperature: 0.7,
  maxTokens: 1000,
});

console.log(response.content);
console.log(`Tokens used: ${response.usage.totalTokens}`);

Tools

typescript
// List available tools
const tools = await this.hub.tools.list();
console.log(tools.map(t => t.name));

// Execute a tool
const result = await this.hub.tools.execute('add_comment', {
  issue_id: '123',
  comment: 'Review complete',
});
const output = JSON.parse(result.resultJson);

Memory

typescript
// Store data
await this.hub.memory.store('my-config', { theme: 'dark' }, 'agent', 3600);

// Retrieve data
const response = await this.hub.memory.retrieve('my-config', 'agent');
if (response.success) {
  const config = JSON.parse(response.valueJson);
}

Messaging

typescript
// Publish to NATS subject
await this.hub.messaging.publish('agent.status', {
  agent_id: this.agentId,
  status: 'processing',
});

// Request-reply
const response = await this.hub.messaging.request(
  'workflow.status',
  { workflow_id: '123' },
  10, // timeout in seconds
);

Example: Code Reviewer Agent

typescript
import { AetherAgent, Task, TaskResult } from '@aether/agent-sdk';

class CodeReviewerAgent extends AetherAgent {
  agentId = 'dev_ts_reviewer';
  capabilities = ['code_review', 'typescript'];

  async handleTask(task: Task): Promise<TaskResult> {
    const diff = task.payload?.toString() ?? task.description;

    const response = await this.hub.llm.chat({
      messages: [
        {
          role: 'system',
          content: 'You are an expert code reviewer. Provide concise, constructive feedback.',
        },
        { role: 'user', content: `Review this diff:\n\n${diff}` },
      ],
      temperature: 0.2,
    });

    if (!response.success) {
      return {
        taskId: task.taskId,
        success: false,
        errorMessage: `LLM call failed: ${response.errorMessage}`,
      };
    }

    // Notify other agents
    await this.hub.messaging.publish('review.completed', {
      task_id: task.taskId,
      agent: this.agentId,
    });

    return {
      taskId: task.taskId,
      success: true,
      result: Buffer.from(response.content),
      metrics: {
        model: response.model,
        tokens: String(response.usage.totalTokens),
      },
    };
  }
}

new CodeReviewerAgent({
  agentId: 'dev_ts_reviewer',
  agentSecret: process.env.AGENT_SECRET ?? '',
  agentName: 'TypeScript Code Reviewer',
  hubAddress: process.env.HUB_ADDRESS ?? 'localhost:50051',
  capabilities: { code_review: 'true', typescript: 'true' },
}).run();

Example: Custom Data Processor

typescript
import { AetherAgent, Task, TaskResult } from '@aether/agent-sdk';

class DataProcessorAgent extends AetherAgent {
  agentId = 'data_processor_ts';
  capabilities = ['data_processing'];

  async handleTask(task: Task): Promise<TaskResult> {
    let data: unknown;
    try {
      data = JSON.parse(task.payload.toString());
    } catch {
      data = { raw: task.description };
    }

    const response = await this.hub.llm.chat({
      messages: [
        { role: 'system', content: 'Classify and summarize this data as JSON.' },
        { role: 'user', content: JSON.stringify(data) },
      ],
      temperature: 0.0,
    });

    // Persist analysis
    await this.hub.memory.store(
      `analysis-${task.taskId}`,
      { analysis: response.content },
      'task',
      3600,
    );

    // Publish completion event
    await this.hub.messaging.publish('task.completed', {
      task_id: task.taskId,
      status: 'success',
    });

    return {
      taskId: task.taskId,
      success: true,
      result: Buffer.from(response.content),
    };
  }
}

new DataProcessorAgent({
  agentId: 'data_processor_ts',
  agentSecret: process.env.AGENT_SECRET ?? '',
  agentName: 'Data Processor',
  hubAddress: process.env.HUB_ADDRESS ?? 'localhost:50051',
}).run();

Error Handling

handleTask should never throw — catch errors and return a failure TaskResult:

typescript
async handleTask(task: Task): Promise<TaskResult> {
  try {
    // ... your logic
    return { taskId: task.taskId, success: true, result: Buffer.from('done') };
  } catch (err) {
    return {
      taskId: task.taskId,
      success: false,
      errorMessage: err instanceof Error ? err.message : String(err),
    };
  }
}

The SDK handles reconnection automatically. Configure limits with maxReconnectAttempts (default -1 = infinite).

Deployment

Standalone Node.js

bash
cd examples/code-reviewer
npm install
npm start

Docker

dockerfile
FROM node:20-alpine

WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --production

COPY dist/ ./dist/
CMD ["node", "dist/agent.js"]
bash
docker build -t my-ts-agent .
docker run -e HUB_ADDRESS=hub:50051 -e AGENT_SECRET=sk-xxx my-ts-agent

Docker Compose (with Aether)

yaml
services:
  my-ts-agent:
    build: ./my-ts-agent
    environment:
      HUB_ADDRESS: goway:9090
      AGENT_SECRET: ${AGENT_SECRET}
    depends_on:
      - goway
    restart: unless-stopped

Kubernetes

yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-ts-agent
spec:
  replicas: 1
  selector:
    matchLabels:
      app: my-ts-agent
  template:
    metadata:
      labels:
        app: my-ts-agent
    spec:
      containers:
        - name: agent
          image: my-ts-agent:latest
          env:
            - name: HUB_ADDRESS
              value: "aether-hub:9090"
            - name: AETHER_AGENT_SECRET
              valueFrom:
                secretKeyRef:
                  name: aether-secrets
                  key: agent-secret

API Reference

AetherAgent

Base class for all agents. Extend and override handleTask.

Abstract properties:

  • agentId: string — must be overridden with a unique identifier

Optional properties:

  • capabilities?: string[] — array of capability tags

Methods:

MethodDescription
handleTask(task): Promise<TaskResult>Override this — your task logic
start(): Promise<void>Connect and register with Hub
stop(): Promise<void>Unregister and disconnect
run(): Promise<void>Start and block until stopped

Properties:

  • this.hub: AgentClient — access to Hub services (available after start())

AgentClient

Low-level gRPC client. Accessible via this.hub.

MethodDescription
connect()Establish gRPC connection
disconnect()Close connection
register()Register with Hub → AgentRegistration
unregister()Unregister from Hub
sendHeartbeat(status?)Send heartbeat
streamTasks()AsyncIterableIterator<Task>
sendTaskResult(result)Submit task result

Service proxies:

  • hub.llmLLMProxy
  • hub.toolsToolsProxy
  • hub.memoryMemoryProxy
  • hub.messagingMessagingProxy

Key Types

typescript
interface Task {
  taskId: string;
  description: string;
  payload: Buffer;
  metadata: Record<string, string>;
}

interface TaskResult {
  taskId: string;
  success: boolean;
  result?: Buffer;
  errorMessage?: string;
  metrics?: Record<string, string>;
}

interface AgentConfig {
  agentId: string;
  agentSecret: string;
  agentName: string;
  version?: string;
  hubAddress?: string;
  capabilities?: Record<string, string>;
  metadata?: Record<string, string>;
  heartbeatInterval?: number;
  reconnectDelay?: number;
  maxReconnectAttempts?: number;
}

Development

bash
cd sdks/typescript

npm install       # Install dependencies
npm run build     # Compile TypeScript → dist/
npm run watch     # Watch mode for development
npm run lint      # ESLint
npm run format    # Prettier
npm test          # Run tests (Jest)

Troubleshooting

Error: 14 UNAVAILABLE: Connection refused

The Hub gRPC port is not reachable. Check:

  • Hub is running (docker-compose ps goway)
  • hubAddress points to the correct host/port (default 9090 for gRPC)

Error: 16 UNAUTHENTICATED

The agentSecret does not match the secret on the agent record in the Hub. Verify via the Aether API (GET /api/agents/:id).

Proto-generated types are stale

The TypeScript SDK includes pre-generated gRPC stubs in src/proto/. If the proto definition changes in goway/proto/, regenerate them:

bash
cd sdks/typescript
npm run proto-gen

Cannot find module '@aether/agent-sdk'

When using from source, build first:

bash
cd sdks/typescript
npm run build

Then link or reference the dist/ output directly.

Released under the MIT License.