Skip to content

Latest commit

 

History

History
263 lines (206 loc) · 6.54 KB

File metadata and controls

263 lines (206 loc) · 6.54 KB

@agentrail-core/sdk

TypeScript SDK for the AgentRail Task Lifecycle API.

The SDK is a client library. It does not start AgentRail, connect GitHub, CircleCI, or Linear, create local config, or wake local coding agents. Start an AgentRail API first, then point the SDK at it.

For the full guide, see AgentRail SDK Guide.

Install

npm install @agentrail-core/sdk

Requires Node.js 18 or newer because it uses native fetch.

For local development against this repository before publication:

cd /path/to/agentrail/sdk/typescript
npm install
npm run build
cd /path/to/your-harness
npm install /path/to/agentrail/sdk/typescript

Local Setup

Use the CLI to create the local runtime, agent credentials, and provider setup:

npm install -g @agentrail-core/cli
agentrail init
agentrail server start

In another terminal:

agentrail agent create
source ~/.agentrail/agent.env
agentrail doctor

The generated env file provides AGENTRAIL_BASE_URL and AGENTRAIL_API_KEY. Use the ar_live_... secret as the bearer token. Values that start with akey_ are key ids, not secrets.

When you have a hosted AgentRail API base URL, pass that as baseUrl.

Quickstart

import { AgentRailClient } from "@agentrail-core/sdk";

const apiKey = process.env.AGENTRAIL_API_KEY;

if (!apiKey) {
  throw new Error("Set AGENTRAIL_API_KEY before using the SDK.");
}

const client = new AgentRailClient({
  baseUrl: process.env.AGENTRAIL_BASE_URL ?? "http://127.0.0.1:3000",
  apiKey,
});

const tasks = await client.listMyTasks({ status: "todo", limit: 10 });

for (const task of tasks.data) {
  console.log(`${task.identifier}: ${task.title}`);
  console.log(`Next actions: ${task.availableActions.join(", ")}`);
}

baseUrl is required. Passing it explicitly avoids a hidden dependency on local defaults.

Task Lifecycle

Use adapter_managed mode for real provider automation. In this mode AgentRail creates or reuses the provider PR and returns PR metadata.

const tasks = await client.listMyTasks({ status: "in_progress", limit: 1 });
const task = tasks.data[0];

if (!task) {
  process.exit(0);
}

const detail = await client.getTask(task.id);

if (detail.data.availableActions.includes("submit")) {
  const submission = await client.submitTask(
    task.id,
    {
      summary: "Implemented the task and pushed commits to the task branch.",
      mode: "adapter_managed",
      pullRequest: {
        title: `Submit ${detail.data.identifier}`,
        draft: false,
      },
    },
    `submit-${task.id}-v1`,
  );

  console.log(`PR: ${submission.data.prUrl}`);
}

const ci = await client.getTaskCiStatus(task.id);
const review = await client.getTaskReviewFeedback(task.id);
const refreshed = await client.getTask(task.id);

const headSha =
  ci.data.headSha ??
  review.data.latestDecision.headSha ??
  refreshed.data.headSha;

if (
  refreshed.data.availableActions.includes("ship") &&
  ci.data.overallStatus === "passed" &&
  review.data.latestDecision.outcome === "approved" &&
  headSha
) {
  await client.shipTask(
    task.id,
    {
      mode: "merge_and_deploy",
      targetEnvironment: "production",
      expectedHeadSha: headSha,
    },
    `ship-${task.id}-${headSha}`,
  );
}

Always follow availableActions instead of guessing the next lifecycle step. Gate shipping on ship, green CI, an approved review decision, and the latest head SHA.

Event Stream

const controller = new AbortController();

for await (const event of client.streamEvents({
  eventTypes: ["task.updated", "task.reviewed", "task.shipped"],
  heartbeatSeconds: 30,
  signal: controller.signal,
})) {
  console.log(event.id, event.type);
}

After reconnecting, pass the last event id as cursor.

Event Subscriptions and Webhooks

import { parseWebhookEvent } from "@agentrail-core/sdk";

const sub = await client.createEventSubscription(
  {
    url: "https://my-app.example.com/webhooks/tasks",
    eventTypes: ["task.awaiting_user", "task.updated", "task.reviewed", "task.shipped"],
    secret: "whsec_my_secret_at_least_16",
  },
  "event-sub-v1",
);

console.log(sub.data.id);

const event = parseWebhookEvent(rawBody, "whsec_my_secret_at_least_16", {
  "x-agentrail-subscription-id": req.headers["x-agentrail-subscription-id"],
  "x-agentrail-event-id": req.headers["x-agentrail-event-id"],
  "x-agentrail-event-type": req.headers["x-agentrail-event-type"],
  "x-agentrail-delivery-id": req.headers["x-agentrail-delivery-id"],
  "x-agentrail-delivery-attempt": req.headers["x-agentrail-delivery-attempt"],
  "x-agentrail-signature": req.headers["x-agentrail-signature"],
});

console.log(event.type, event.data);

Verify signatures against the raw request body before parsing JSON.

Retry Logic

The client retries 429, 500, 502, 503, and 504 responses with exponential backoff. Rate-limited responses respect Retry-After.

const client = new AgentRailClient({
  baseUrl: process.env.AGENTRAIL_BASE_URL ?? "http://127.0.0.1:3000",
  apiKey: process.env.AGENTRAIL_API_KEY!,
  retry: {
    maxAttempts: 5,
    initialDelayMs: 500,
    maxDelayMs: 60_000,
    retryableStatusCodes: [429, 500, 502, 503, 504],
  },
});

Error Handling

import {
  AgentRailClient,
  ConflictError,
  NotFoundError,
  RateLimitError,
  ValidationError,
} from "@agentrail-core/sdk";

try {
  await client.shipTask(taskId, request, idempotencyKey);
} catch (err) {
  if (err instanceof ConflictError) {
    console.log("Task is not shippable:", err.details);
  } else if (err instanceof NotFoundError) {
    console.log("Task not found");
  } else if (err instanceof RateLimitError) {
    console.log(`Retry after ${err.retryAfterSeconds}s`);
  } else if (err instanceof ValidationError) {
    console.log("Bad request:", err.details);
  }
}

API Key Management

Most users should create keys through agentrail init and agentrail agent create. Use these methods from admin tooling only.

const key = await client.createApiKey(
  {
    agent: {
      id: "agt_my_agent",
      displayName: "My Agent",
      role: "developer",
      externalIdentities: [{ provider: "github", subject: "my-bot" }],
    },
    scopes: ["tasks:read", "tasks:write", "ci:read", "reviews:read", "ship:write"],
    rateLimit: { windowSeconds: 60, maxRequests: 600 },
  },
  "create-my-agent-key-v1",
);

console.log(key.data.apiKey);

const rotated = await client.rotateApiKey(
  key.data.id,
  { expiresAt: "2026-07-01T00:00:00Z" },
  "rotate-my-agent-key-v1",
);

const usage = await client.getApiKeyUsage(rotated.data.id);
console.log(usage.data.totals.accepted);