npm package: @frumu/tandem-client
Description: TypeScript client for the Tandem autonomous agent engine HTTP + SSE API
Latest version: 0.4.23

# @frumu/tandem-client

TypeScript / Node.js client for the [Tandem](https://tandem.ac/) autonomous agent engine HTTP + SSE API.

## Install

```bash
npm install @frumu/tandem-client
```

Requires **Node 18+** (uses built-in `fetch` and `ReadableStream`).

## Quick start

```typescript
import { TandemClient } from "@frumu/tandem-client";

const client = new TandemClient({
  baseUrl: "http://localhost:39731", // engine URL
  token: "your-engine-token", // from `tandem-engine token generate`
});

// 1. Create a session
const sessionId = await client.sessions.create({
  title: "My agent session",
  directory: "/path/to/my/project",
});

// 2. Start an async run
const { runId } = await client.sessions.promptAsync(
  sessionId,
  "Summarize the README and list the top 3 TODOs"
);

// 3. Stream the response
for await (const event of client.stream(sessionId, runId)) {
  if (event.type === "session.response") {
    process.stdout.write(String(event.properties.delta ?? ""));
  }
  if (
    event.type === "run.complete" ||
    event.type === "run.completed" ||
    event.type === "run.failed" ||
    event.type === "session.run.finished"
  )
    break;
}
```

## API

### `new TandemClient(options)`

| Option      | Type     | Description                                     |
| ----------- | -------- | ----------------------------------------------- |
| `baseUrl`   | `string` | Engine base URL (e.g. `http://localhost:39731`) |
| `token`     | `string` | Engine API token                                |
| `timeoutMs` | `number` | Request timeout in ms (default 20000)           |

### `client.setToken(token)` → `void`

Update the bearer token used for subsequent HTTP and SSE requests.

### `client.health()` → `SystemHealth`

Check engine readiness.

### `client.stream(sessionId, runId?, options?)` → `AsyncGenerator<EngineEvent>`

Stream events from a session run. Yields typed `EngineEvent` objects.

### `client.globalStream(options?)` → `AsyncGenerator<EngineEvent>`

Stream all engine events across all sessions.

---

### `client.sessions`

| Method                               | Description                             |
| ------------------------------------ | --------------------------------------- |
| `create(options?)`                   | Create a session, returns `sessionId`   |
| `list(options?)`                     | List sessions                           |
| `get(sessionId)`                     | Get session details                     |
| `delete(sessionId)`                  | Delete a session                        |
| `messages(sessionId)`                | Get message history                     |
| `activeRun(sessionId)`               | Get the currently active run            |
| `promptAsync(sessionId, prompt)`     | Start an async run, returns `{ runId }` |
| `promptAsyncParts(sessionId, parts)` | Start async run with text/file parts    |

**Prompt with file attachments:**

```typescript
const { runId } = await client.sessions.promptAsyncParts(sessionId, [
  {
    type: "file",
    mime: "image/jpeg",
    filename: "photo.jpg",
    url: "/srv/tandem/channel_uploads/telegram/123/photo.jpg",
  },
  { type: "text", text: "Describe this image." },
]);
```

### `client.routines`

| Method                          | Description                   |
| ------------------------------- | ----------------------------- |
| `list(family?)`                 | List routines or automations  |
| `create(options, family?)`      | Create a scheduled routine    |
| `delete(id, family?)`           | Delete a routine              |
| `runNow(id, family?)`           | Trigger a routine immediately |
| `listRuns(family?, limit?)`     | List recent run records       |
| `listArtifacts(runId, family?)` | List artifacts from a run     |

**Create a scheduled routine:**

```typescript
await client.routines.create({
  name: "Daily digest",
  schedule: "0 8 * * *", // cron expression
  prompt: "Summarize today's activity and write a report",
  allowed_tools: ["read", "websearch", "webfetch"],
});
```

### `client.workflowPlans`

```typescript
const preview = await client.workflowPlans.preview({
  prompt: "Create a release checklist automation",
  planSource: "planner_page",
});

const started = await client.workflowPlans.chatStart({
  prompt: "Create a release checklist automation",
  planSource: "planner_page",
});

const updated = await client.workflowPlans.chatMessage({
  planId: started.plan.plan_id!,
  message: "Add a smoke-test step before rollout.",
});

const applied = await client.workflowPlans.apply({
  planId: updated.plan.plan_id,
  creatorId: "operator-1",
});

const importPreview = await client.workflowPlans.importPreview({
  bundle: applied.plan_package_bundle!,
});

const imported = await client.workflowPlans.importPlan({
  bundle: importPreview.bundle ?? applied.plan_package_bundle!,
});
```

### Planner page workflow

The Planner page uses the same `workflowPlans` surface, but keeps the bundle as the portable artifact. A minimal end-to-end flow looks like this:

```typescript
const started = await client.workflowPlans.chatStart({
  prompt: "Plan a release workflow with approval and handoff",
  planSource: "intent_planner_page",
  workspaceRoot: "/workspace/repos/tandem",
});

const revised = await client.workflowPlans.chatMessage({
  planId: started.plan.plan_id!,
  message: "Split the work into review, validate, and publish phases.",
});

const applied = await client.workflowPlans.apply({
  planId: revised.plan.plan_id!,
  creatorId: "planner-operator",
});

const previewImport = await client.workflowPlans.importPreview({
  bundle: applied.plan_package_bundle!,
});

if (previewImport.import_validation?.compatible) {
  await client.workflowPlans.importPlan({
    bundle: previewImport.bundle ?? applied.plan_package_bundle!,
  });
}
```

Use this flow when you want the same governed bundle the control-panel Planner page hands to Automations, Coding, and Orchestrator.

### Additional namespaces

The TypeScript SDK already includes the newer engine surfaces that have landed across the repo:

- `client.browser` for `status()`, `install()`, and `smokeTest()`
- `client.workflows` for workflow registry, runs, hooks, simulation, and live events
- `client.resources` for key-value resources
- `client.skills` for list/get/import plus validation, routing, evals, compile, and generate flows
- `client.packs` and `client.capabilities` for pack lifecycle and capability resolution
- `client.automationsV2`, `client.bugMonitor`, `client.coder`, `client.agentTeams`, and `client.missions` for newer orchestration APIs

```typescript
const browser = await client.browser.status();
const workflows = await client.workflows.list();
const resources = await client.resources.list({ prefix: "agent-config/" });
const skillCatalog = await client.skills.catalog();
```

### `client.coder`

`client.coder` also includes project-scoped GitHub Project intake helpers:

```typescript
await client.coder.putProjectBinding("repo-123", {
  github_project_binding: {
    owner: "acme-inc",
    project_number: 7,
    repo_slug: "acme-inc/tandem",
  },
});

const inbox = await client.coder.getProjectGithubInbox("repo-123");
const intake = await client.coder.intakeProjectItem("repo-123", {
  project_item_id: inbox.items[0].project_item_id,
  source_client: "sdk_test",
});
```

### `client.mcp`

| Method                      | Description                 |
| --------------------------- | --------------------------- |
| `list()`                    | List registered MCP servers |
| `listTools()`               | List all discovered tools   |
| `add(options)`              | Register an MCP server      |
| `connect(name)`             | Connect and discover tools  |
| `disconnect(name)`          | Disconnect                  |
| `refresh(name)`             | Re-discover tools           |
| `setEnabled(name, enabled)` | Enable/disable              |

```typescript
await client.mcp.add({ name: "arcade", transport: "https://mcp.arcade.ai/mcp" });
await client.mcp.connect("arcade");
const tools = await client.mcp.listTools();
```

### `client.channels`

| Method                                 | Description                                           |
| -------------------------------------- | ----------------------------------------------------- |
| `config()`                             | Get channel configuration, including security profile |
| `status()`                             | Get live connection status                            |
| `put(channel, payload)`                | Configure a channel                                   |
| `delete(channel)`                      | Remove a channel configuration                        |
| `verify(channel, payload?)`            | Verify connectivity and channel prerequisites         |
| `toolPreferences(channel)`             | Read per-channel tool preferences                     |
| `setToolPreferences(channel, payload)` | Update per-channel tool preferences                   |

### `client.packs`

| Method                                | Description                                 |
| ------------------------------------- | ------------------------------------------- |
| `list()`                              | List installed packs                        |
| `inspect(selector)`                   | Inspect pack manifest/trust/risk            |
| `install({ path \| url, source? })`   | Install a pack zip                          |
| `installFromAttachment(options)`      | Install from downloaded attachment path     |
| `uninstall({ pack_id \| name })`      | Uninstall pack                              |
| `export(options)`                     | Export installed pack to zip                |
| `detect({ path, ... })`               | Detect root `tandempack.yaml` marker in zip |
| `updates(selector)`                   | Check updates (stub in v0.4.0)              |
| `update(selector, { target_version})` | Apply update (stub in v0.4.0)               |

### `client.capabilities`

| Method              | Description                                      |
| ------------------- | ------------------------------------------------ |
| `getBindings()`     | Load current capability bindings file            |
| `setBindings(file)` | Replace capability bindings file                 |
| `discovery()`       | Discover provider tools for capability resolver  |
| `resolve(input)`    | Resolve capability IDs to provider tool bindings |

### `client.permissions`

| Method                    | Description                       |
| ------------------------- | --------------------------------- |
| `list()`                  | List pending requests and rules   |
| `reply(requestId, reply)` | Approve/deny a permission request |

### `client.memory`

```typescript
// Put (global record; SDK accepts `text`, server persists `content`)
await client.memory.put({
  text: "Use WAL mode for sqlite in long-lived services.",
  run_id: "run-123",
});

// Search
const found = await client.memory.search({ query: "sqlite wal", limit: 5 });

// List by user scope
const listing = await client.memory.list({ userId: "user-123", q: "sqlite" });

// Promote / demote / delete
await client.memory.promote({ id: listing.items[0].id! });
await client.memory.demote({ id: listing.items[0].id!, runId: "run-123" });
await client.memory.delete(listing.items[0].id!);

// Audit
const audit = await client.memory.audit({ run_id: "run-123" });
```

### `client.providers`

| Method                             | Description                        |
| ---------------------------------- | ---------------------------------- |
| `catalog()`                        | List available providers           |
| `config()`                         | Get current provider configuration |
| `setDefaults(providerId, modelId)` | Set default provider and model     |
| `setApiKey(providerId, apiKey)`    | Store an API key                   |

---

## Engine events reference

Common `event.type` values:

| Type                      | Description                                        |
| ------------------------- | -------------------------------------------------- |
| `session.response`        | Streaming text delta in `event.properties.delta`   |
| `session.tool_call`       | Tool invocation in `event.properties`              |
| `session.tool_result`     | Tool result                                        |
| `run.complete`            | Run finished successfully (legacy event name)      |
| `run.completed`           | Run finished successfully                          |
| `run.failed`              | Run failed                                         |
| `session.run.finished`    | Session-scoped terminal run event                  |
| `permission.request`      | Approval needed — use `client.permissions.reply()` |
| `memory.write.succeeded`  | Memory write persisted                             |
| `memory.search.performed` | Memory retrieval telemetry                         |
| `memory.context.injected` | Prompt context injection telemetry                 |

## License

MIT