tx

Agent SDK

TypeScript SDK for building custom agents with tx task management

API Server Required

The Agent SDK connects to tx via the REST API server. You must start the API server before using the SDK:

tx serve
# Server running at http://localhost:3456

Alternatively, use direct mode with dbPath to skip the API server entirely (requires @jamesaphoenix/tx-core and effect as dependencies).

Installation

npm install @jamesaphoenix/tx-agent-sdk

Or with your preferred package manager:

# pnpm
pnpm add @jamesaphoenix/tx-agent-sdk

# yarn
yarn add @jamesaphoenix/tx-agent-sdk

# bun
bun add @jamesaphoenix/tx-agent-sdk

Quick Start

import { TxClient } from "@jamesaphoenix/tx-agent-sdk"

// HTTP mode (connects to tx API server)
const tx = new TxClient({ apiUrl: "http://localhost:3456" })

// Get the next ready task
const ready = await tx.tasks.ready({ limit: 1 })
if (ready.length === 0) {
  console.log("No tasks ready")
  process.exit(0)
}

const task = ready[0]
console.log(`Working on: ${task.title}`)

// Get context (relevant learnings for this task)
const ctx = await tx.context.forTask(task.id)
for (const l of ctx.learnings) {
  console.log(`- ${l.content}`)
}

// ... do the work ...

// Mark complete and see what was unblocked
const { nowReady } = await tx.tasks.done(task.id)
console.log(`Unblocked ${nowReady.length} tasks`)

// Record what you learned
await tx.learnings.add({
  content: "Use retry logic for flaky network calls",
  category: "best-practices",
})

Connection Modes

The SDK supports two transport modes:

ModeConfigRequiresBest For
HTTPapiUrlRunning tx serveRemote agents, distributed systems
DirectdbPath@jamesaphoenix/tx-core + effectLocal agents, single-machine setups
// HTTP mode
const tx = new TxClient({ apiUrl: "http://localhost:3456" })

// Direct SQLite mode (no server needed)
const tx = new TxClient({ dbPath: ".tx/tasks.db" })

// With authentication
const tx = new TxClient({
  apiUrl: "http://localhost:3456",
  apiKey: process.env.TX_API_KEY,
  timeout: 60000, // 60 second timeout
})

When both apiUrl and dbPath are provided, direct mode takes precedence. Direct mode requires @jamesaphoenix/tx-core and effect as installed dependencies.

API Reference

Tasks

tx.tasks.ready(options?)

Get tasks that are ready to be worked on (all blockers completed). Returns tasks sorted by priority score (descending).

const ready = await tx.tasks.ready({ limit: 5 })

if (ready.length > 0) {
  console.log(`Next task: ${ready[0].title}`)
}

Options:

OptionTypeDefaultDescription
limitnumber100Maximum number of tasks to return
tx ready --limit 5 --json
{
  "tool": "tx_ready",
  "arguments": {
    "limit": 5
  }
}
curl http://localhost:3456/api/tasks/ready?limit=5

tx.tasks.done(id)

Mark a task as done. Returns the completed task and an array of tasks that became ready as a result.

const { task, nowReady } = await tx.tasks.done("tx-abc123")
console.log(`Completed: ${task.title}`)
console.log(`Unblocked ${nowReady.length} tasks`)
tx done tx-abc123
{
  "tool": "tx_done",
  "arguments": {
    "taskId": "tx-abc123"
  }
}
curl -X POST http://localhost:3456/api/tasks/tx-abc123/done

tx.tasks.create(data)

Create a new task.

const task = await tx.tasks.create({
  title: "Implement auth",
  description: "Add JWT-based authentication",
  score: 100,
  metadata: { component: "auth" },
})

Parameters:

FieldTypeRequiredDescription
titlestringYesTask title
descriptionstringNoDetailed description
parentIdstringNoParent task ID for hierarchy
scorenumberNoPriority score (higher = more urgent)
metadataRecord<string, unknown>NoArbitrary key-value metadata
tx add "Implement auth" --score 100
{
  "tool": "tx_add",
  "arguments": {
    "title": "Implement auth",
    "description": "Add JWT-based authentication",
    "score": 100
  }
}
curl -X POST http://localhost:3456/api/tasks \
  -H "Content-Type: application/json" \
  -d '{"title": "Implement auth", "description": "Add JWT-based authentication", "score": 100}'

tx.tasks.get(id)

Get a task by ID, including full dependency information.

const task = await tx.tasks.get("tx-abc123")
console.log(task.title, task.isReady)
console.log(`Blocked by: ${task.blockedBy.join(", ")}`)
console.log(`Blocks: ${task.blocks.join(", ")}`)

tx.tasks.update(id, data)

Update a task's fields. Only provided fields are changed.

await tx.tasks.update("tx-abc123", {
  status: "active",
  score: 200,
})

tx.tasks.delete(id, options?)

Delete a task. Fails if the task has children unless cascade: true is set.

// Delete a single task
await tx.tasks.delete("tx-abc123")

// Delete task and all descendants
await tx.tasks.delete("tx-abc123", { cascade: true })

tx.tasks.list(options?)

List tasks with pagination and filtering.

// List all tasks
const all = await tx.tasks.list()

// Filter by status
const active = await tx.tasks.list({ status: "active" })

// Multiple statuses
const working = await tx.tasks.list({ status: ["active", "planning"] })

// Full-text search
const results = await tx.tasks.list({ search: "auth" })

// Paginate
const page1 = await tx.tasks.list({ limit: 10 })
if (page1.hasMore) {
  const page2 = await tx.tasks.list({ limit: 10, cursor: page1.nextCursor! })
}

Options:

OptionTypeDefaultDescription
cursorstring-Pagination cursor from nextCursor
limitnumber20Maximum tasks to return
statusTaskStatus | TaskStatus[]-Filter by status
searchstring-Full-text search across title and description

tx.tasks.block(id, blockerId)

Add a blocker dependency. Circular dependencies are rejected.

// "deploy" can't start until "build" is done
await tx.tasks.block("tx-deploy", "tx-build")

tx.tasks.unblock(id, blockerId)

Remove a blocker dependency.

await tx.tasks.unblock("tx-deploy", "tx-build")

tx.tasks.tree(id)

Get a task and all its descendants as a flat array.

const tree = await tx.tasks.tree("tx-root")
console.log(`${tree.length} tasks in tree`)

Learnings

tx.learnings.search(options?)

Search learnings using BM25 text search. Returns results with relevance scores.

// Search by keyword
const results = await tx.learnings.search({ query: "authentication" })

// Get recent learnings (no query)
const recent = await tx.learnings.search({ limit: 5 })

// Filter by category
const practices = await tx.learnings.search({
  query: "error handling",
  category: "best-practices",
})

Options:

OptionTypeDefaultDescription
querystring-Search query (omit for recent)
limitnumber10Maximum results
minScorenumber-Minimum relevance score (0-1)
categorystring-Filter by category
tx learning:search "authentication"
{
  "tool": "tx_learning_search",
  "arguments": {
    "query": "authentication",
    "limit": 10
  }
}
curl "http://localhost:3456/api/learnings?query=authentication&limit=10"

tx.learnings.add(data)

Create a new learning to persist knowledge for future agents.

await tx.learnings.add({
  content: "Use bcrypt for password hashing, not SHA256",
  sourceType: "manual",
  sourceRef: "tx-abc123",
  category: "security",
  keywords: ["passwords", "hashing", "bcrypt"],
})

Parameters:

FieldTypeRequiredDescription
contentstringYesThe learning content
sourceTypestringNo'manual', 'run', 'compaction', or 'claude_md'
sourceRefstringNoReference to the source (e.g. task ID)
categorystringNoCategory for filtering
keywordsstring[]NoKeywords for search indexing
tx learning:add "Use bcrypt for password hashing, not SHA256"
{
  "tool": "tx_learning_add",
  "arguments": {
    "content": "Use bcrypt for password hashing, not SHA256",
    "category": "security"
  }
}
curl -X POST http://localhost:3456/api/learnings \
  -H "Content-Type: application/json" \
  -d '{"content": "Use bcrypt for password hashing, not SHA256", "category": "security"}'

tx.learnings.get(id)

Get a learning by its numeric ID.

const learning = await tx.learnings.get(42)
console.log(learning.content)

tx.learnings.helpful(id, score?)

Record that a learning was helpful, boosting its outcome score. Higher outcome scores cause learnings to rank higher in future searches.

await tx.learnings.helpful(42)
await tx.learnings.helpful(42, 0.8) // partial helpfulness

Context

tx.context.forTask(taskId)

Get contextual learnings for a task. Uses the task's title and description to find relevant learnings. This is the primary mechanism for injecting memory into agent prompts.

const ctx = await tx.context.forTask("tx-abc123")
console.log(`Found ${ctx.learnings.length} relevant learnings`)

for (const l of ctx.learnings) {
  console.log(`- [${(l.relevanceScore * 100).toFixed(0)}%] ${l.content}`)
}

Returns SerializedContextResult:

FieldTypeDescription
taskIdstringThe task ID queried
taskTitlestringThe task's title
learningsSerializedLearningWithScore[]Relevant learnings with scores
searchQuerystringThe generated search query
searchDurationnumberSearch time in milliseconds
tx context tx-abc123
{
  "tool": "tx_context",
  "arguments": {
    "taskId": "tx-abc123"
  }
}
curl http://localhost:3456/api/context/tx-abc123

File Learnings

tx.fileLearnings.list(path?)

List all file learnings, optionally filtering by file path.

// List all
const all = await tx.fileLearnings.list()

// Filter by path
const forFile = await tx.fileLearnings.list("src/auth.ts")

tx.fileLearnings.recall(path)

Recall file learnings matching a specific file path. Use this before working on a file to retrieve attached notes.

const notes = await tx.fileLearnings.recall("src/auth.ts")
for (const note of notes) {
  console.log(`${note.filePattern}: ${note.note}`)
}

tx.fileLearnings.add(data)

Associate a note with a file pattern.

await tx.fileLearnings.add({
  filePattern: "src/auth.ts",
  note: "JWT tokens expire after 1 hour, refresh logic is in middleware",
  taskId: "tx-abc123",
})

Messages

Inter-agent communication via channel-based messaging.

tx.messages.send(data)

Send a message to a channel.

await tx.messages.send({
  channel: "worker-1",
  content: "Task tx-abc123 is ready for review",
  sender: "orchestrator",
  ttlSeconds: 3600,
  correlationId: "req-001",
})

Parameters:

FieldTypeRequiredDescription
channelstringYesChannel name
contentstringYesMessage content
senderstringNoSender name (default: 'sdk')
taskIdstringNoAssociated task ID
ttlSecondsnumberNoTime-to-live in seconds
correlationIdstringNoFor request/reply patterns
metadataRecord<string, unknown>NoArbitrary metadata

tx.messages.inbox(channel, options?)

Read messages from a channel inbox.

const msgs = await tx.messages.inbox("agent-1", { limit: 10 })
const fromOrch = await tx.messages.inbox("agent-1", { sender: "orchestrator" })

Options:

OptionTypeDefaultDescription
afterIdnumber-Cursor: only messages with ID > this value
limitnumber-Maximum messages to return
senderstring-Filter by sender
correlationIdstring-Filter by correlation ID
includeAckedbooleanfalseInclude acknowledged messages

tx.messages.ack(id)

Acknowledge a single message.

await tx.messages.ack(42)

tx.messages.ackAll(channel)

Acknowledge all pending messages on a channel.

const { ackedCount } = await tx.messages.ackAll("agent-1")

tx.messages.pending(channel)

Get count of pending (unacknowledged) messages.

const count = await tx.messages.pending("agent-1")

tx.messages.gc(options?)

Garbage collect expired and old acknowledged messages.

const { expired, acked } = await tx.messages.gc({ ackedOlderThanHours: 24 })

Claims

Lease-based task claiming for worker coordination.

tx.claims.claim(taskId, workerId, leaseDurationMinutes?)

Claim a task with an exclusive lease.

const claim = await tx.claims.claim("tx-abc123", "worker-1", 30)
console.log(`Lease expires at ${claim.leaseExpiresAt}`)

tx.claims.release(taskId, workerId)

Release a claim on a task.

await tx.claims.release("tx-abc123", "worker-1")

tx.claims.renew(taskId, workerId)

Renew an existing claim's lease.

const renewed = await tx.claims.renew("tx-abc123", "worker-1")
console.log(`New expiry: ${renewed.leaseExpiresAt}`)

tx.claims.getActive(taskId)

Get the active claim for a task, or null if unclaimed.

const claim = await tx.claims.getActive("tx-abc123")
if (claim) {
  console.log(`Claimed by ${claim.workerId}`)
}

Error Handling

The SDK uses TxError for all errors, with typed error codes:

import { TxError } from "@jamesaphoenix/tx-agent-sdk"

try {
  await tx.tasks.get("tx-nonexistent")
} catch (err) {
  if (err instanceof TxError) {
    if (err.isNotFound()) {
      console.log("Task not found")
    } else if (err.isValidation()) {
      console.log("Validation error:", err.message)
    } else if (err.isCircularDependency()) {
      console.log("Circular dependency detected")
    }
    console.log("Error code:", err.code)
    console.log("HTTP status:", err.statusCode)
  }
}

Retry Logic

Built-in retry helper for resilient API calls:

import { withRetry } from "@jamesaphoenix/tx-agent-sdk"

const task = await withRetry(() => tx.tasks.get("tx-abc123"), {
  maxAttempts: 3,
  initialDelayMs: 100,
  maxDelayMs: 5000,
  backoffMultiplier: 2,
})

By default, withRetry retries on network errors and 5xx server responses.

Utility Functions

The SDK exports helper functions for common operations:

import {
  filterByStatus,
  filterReady,
  sortByScore,
  getNextTask,
  isValidTaskId,
  isValidTaskStatus,
  parseDate,
  wasCompletedRecently,
} from "@jamesaphoenix/tx-agent-sdk"

// Get the highest-priority ready task from a list
const tasks = (await tx.tasks.list({ limit: 100 })).items
const next = getNextTask(tasks)

// Validate IDs
isValidTaskId("tx-abc123") // true
isValidTaskStatus("active") // true

// Filter tasks client-side
const active = filterByStatus(tasks, "active")
const ready = filterReady(tasks)
const sorted = sortByScore(tasks)

// Date helpers
const recentlyDone = tasks.filter((t) => wasCompletedRecently(t, 24))

Cleanup

When using direct mode, dispose of resources when done:

const tx = new TxClient({ dbPath: ".tx/tasks.db" })
try {
  await tx.tasks.ready()
} finally {
  await tx.dispose()
}

HTTP mode has no resources to dispose. dispose() is safe to call on either mode.

Example: Full Agent Loop

import { TxClient } from "@jamesaphoenix/tx-agent-sdk"

const tx = new TxClient({ apiUrl: "http://localhost:3456" })

async function agentLoop() {
  while (true) {
    // Get next task
    const ready = await tx.tasks.ready({ limit: 1 })
    if (ready.length === 0) break

    const task = ready[0]
    console.log(`Starting: ${task.title}`)

    // Get relevant context
    const ctx = await tx.context.forTask(task.id)
    const contextStr = ctx.learnings
      .map((l) => `- ${l.content}`)
      .join("\n")

    // ... pass task + context to your LLM ...

    // Record what was learned
    await tx.learnings.add({
      content: "Discovered pattern X works for problem Y",
      sourceRef: task.id,
    })

    // Complete the task
    const { nowReady } = await tx.tasks.done(task.id)
    console.log(`Done. ${nowReady.length} tasks unblocked.`)
  }

  console.log("All tasks complete!")
}

agentLoop()

On this page