npm package: @strait/ts
Description: TypeScript SDK for the Strait platform API with full feature parity across all five Strait SDKs.
Latest version: 0.1.2

# @strait/ts

TypeScript SDK for the Strait platform API with full feature parity across all five Strait SDKs.

## Install

```bash
npm install @strait/ts
```

Or with Bun:

```bash
bun add @strait/ts
```

## Quick Start

```ts
import { createClient } from "@strait/ts";

const client = createClient({
  baseUrl: "https://api.strait.dev",
  auth: { type: "bearer", token: "sk_live_..." },
});

// List jobs
const jobs = await client.jobs.list({});

// Trigger a job
const run = await client.triggerJob({
  pathParams: { jobID: "job_abc" },
  body: { payload: { sku: "ABC-123" } },
});

console.log("Run ID:", run.id);
```

## Configuration

### From `strait.json` (recommended)

Create a `strait.json` at your project root:

```json
{
  "$schema": "https://strait.dev/schema.json",
  "project": {
    "id": "proj_abc123",
    "name": "My Project"
  },
  "sdk": {
    "base_url": "https://api.strait.dev",
    "auth_type": "apiKey",
    "timeout_ms": 30000
  }
}
```

Then load the client from it:

```ts
import { createClientFromConfigFile } from "@strait/ts/node";

// Reads strait.json from working directory + STRAIT_API_KEY from env
const client = await createClientFromConfigFile();

// Or specify a custom directory
const client = await createClientFromConfigFile({ cwd: "/path/to/project" });

// Or an explicit file path
const client = await createClientFromConfigFile({ configPath: "/path/to/custom-config.json" });
```

The SDK reads the `sdk` section from the file. Auth tokens are **never** read from the file — they always come from the `STRAIT_API_KEY` environment variable.

### From environment variables

```ts
import { createClientFromEnv } from "@strait/ts/node";

// Reads STRAIT_BASE_URL, STRAIT_API_KEY, STRAIT_AUTH_TYPE, STRAIT_TIMEOUT_MS
const client = createClientFromEnv();
```

### Inline

```ts
import { createClient } from "@strait/ts";

const client = createClient({
  baseUrl: "https://api.strait.dev",
  auth: { type: "bearer", token: "sk_live_..." },
  timeoutMs: 5000,
  defaultHeaders: { "X-Custom": "value" },
});
```

### Environment variable override precedence

Environment variables always take precedence over `strait.json` values:

| `strait.json` field | Env var | Wins |
|---|---|---|
| `sdk.base_url` | `STRAIT_BASE_URL` | env var |
| `sdk.auth_type` | `STRAIT_AUTH_TYPE` | env var |
| `sdk.timeout_ms` | `STRAIT_TIMEOUT_MS` | env var |
| *(not in file)* | `STRAIT_API_KEY` | env var (only source) |

## Authoring DSL

```ts
import { defineJob, zodSchema } from "@strait/ts";
import { z } from "zod";

const job = defineJob({
  name: "Sync Inventory",
  slug: "sync-inventory",
  endpointUrl: "https://worker.dev/jobs/sync",
  projectId: "proj_1",
  schema: z.object({ sku: z.string() }),
  maxConcurrency: 5,
  maxAttempts: 3,
  retryStrategy: "exponential",
  timeoutSecs: 300,
  run: async (payload, ctx) => {
    ctx.logger.info("Starting sync", { sku: payload.sku });
    const result = await syncInventory(payload.sku);
    await ctx.reportProgress(100, "Done");
    return result;
  },
  onSuccess: async ({ payload, output, ctx }) => {
    ctx.logger.info("Sync complete", { sku: payload.sku });
  },
  onFailure: async ({ payload, error, ctx }) => {
    ctx.logger.error("Sync failed", { sku: payload.sku });
  },
});

// Register and trigger
await job.register(client, { projectId: "proj_1" });
await job.trigger(client, { payload: { sku: "ABC-123" } });

// Trigger and wait for completion
const run = await job.triggerAndWait(client, { payload: { sku: "ABC-123" } });

// Batch trigger
await job.batchTrigger(client, {
  items: [
    { payload: { sku: "ABC-123" } },
    { payload: { sku: "DEF-456" }, priority: 10 },
  ],
});
```

### Schema adapters

The SDK supports multiple schema libraries. You can pass a Standard Schema v1 compliant object directly (Zod 3.24+, Valibot 1.0+, ArkType 2.0+), or use an explicit adapter:

```ts
import { zodSchema, effectSchema, standardSchema, customSchema } from "@strait/ts";

// Zod (auto-detected or explicit)
defineJob({ schema: z.object({ sku: z.string() }), ... });
defineJob({ schema: zodSchema(myZodSchema), ... });

// Effect Schema
defineJob({ schema: effectSchema(Schema.Struct({ sku: Schema.String })), ... });

// Any Standard Schema v1
defineJob({ schema: standardSchema(myValibotSchema), ... });

// Custom validation function
defineJob({ schema: customSchema((input) => validate(input)), ... });
```

## Workflow DAG

```ts
import { defineWorkflow, step } from "@strait/ts";

const pipeline = defineWorkflow({
  name: "Order Pipeline",
  slug: "order-pipeline",
  projectId: "proj_1",
  schema: z.object({ orderId: z.string() }),
  maxConcurrentRuns: 10,
  maxParallelSteps: 3,
  steps: [
    step.job("validate", "job_validate"),
    step.job("charge", "job_charge", {
      dependsOn: ["validate"],
      onFailure: "fail_workflow",
      retryMaxAttempts: 3,
    }),
    step.approval("review", {
      dependsOn: ["charge"],
      approvalTimeoutSecs: 3600,
    }),
    step.waitForEvent("confirm", "shipping.confirmed", {
      dependsOn: ["review"],
      eventTimeoutSecs: 86400,
    }),
    step.sleep("cooldown", 60, { dependsOn: ["confirm"] }),
    step.subWorkflow("notify", "wf_notifications", { dependsOn: ["cooldown"] }),
  ],
});

// Register and trigger
await pipeline.register(client);
await pipeline.trigger(client, { payload: { orderId: "ord_123" } });
```

### AI step builder

Use `step.ai()` to add an AI-powered step to a workflow DAG. It applies sensible defaults for long-running AI work: 600s timeout, 5 retries with exponential backoff, and the `large` resource class.

```ts
const steps = [
  step.job("extract", "job_extract"),
  step.ai("summarize", "job_summarize", { dependsOn: ["extract"] }),
];
```

### DAG definitions

`defineDag()` is an alias for `defineWorkflow()` that brands the definition as `dag`:

```ts
import { defineDag, step } from "@strait/ts";

const dag = defineDag({
  name: "ETL Pipeline",
  slug: "etl-pipeline",
  projectId: "proj_1",
  schema: z.object({ source: z.string() }),
  steps: [
    step.job("extract", "job_extract"),
    step.job("transform", "job_transform", { dependsOn: ["extract"] }),
    step.job("load", "job_load", { dependsOn: ["transform"] }),
  ],
});
```

## Durable AI Agents

`defineAgent()` creates a durable, cost-aware agent that can run across multiple iterations and survive restarts.

```ts
import { defineAgent } from "@strait/ts";

const agent = defineAgent({
  name: "Research Agent",
  slug: "research-agent",
  endpointUrl: "https://worker.dev/agents/research",
  projectId: "proj_1",
  schema: z.object({ topic: z.string() }),
  maxCostMicrousd: 5_000_000,
  run: async (payload, ctx) => {
    while (!ctx.isBudgetExceeded()) {
      const result = await doResearch(payload.topic);
      await ctx.checkpoint({ lastResult: result });
      await ctx.reportUsage?.({
        provider: "openai",
        model: "gpt-4o",
        costMicrousd: result.cost,
      });
    }
    return { summary: "done" };
  },
});
```

`AgentRunContext` exposes:

| Property / Method | Description |
|---|---|
| `ctx.iteration` | Current iteration index (auto-incremented on checkpoint) |
| `ctx.accumulatedCostMicrousd()` | Total cost so far in micro-USD |
| `ctx.isBudgetExceeded()` | Whether the cost budget has been exceeded |

Defaults applied by `defineAgent()`: `strait.kind=agent` tag, 600s timeout, 5 max attempts, exponential retry strategy.

## AI SDK Integration

The SDK integrates with [Vercel AI SDK v6](https://ai.vercel.dev) for automatic usage reporting, tool call logging, and streaming.

```ts
import { createStraitAgent, createStraitProvider } from "@strait/ts/ai";
import { openai } from "@ai-sdk/openai";

// Middleware approach — wrap any model
const middleware = createStraitProvider(ctx, {
  providerName: "openai",
  reportUsage: true,
  logToolCalls: true,
  streamToStrait: true,
});

// Agent approach — full ToolLoopAgent with Strait tools
const agent = createStraitAgent(ctx, {
  model: openai("gpt-4o"),
  instructions: "You are a helpful assistant.",
  tools: myTools,
  straitTools: { checkpoint: true, spawn: true, saveOutput: true },
});
```

Built-in Strait tools (automatically available to the agent):

| Tool | Description |
|---|---|
| `strait_checkpoint` | Save agent state for durable execution |
| `strait_spawn` | Spawn a child job run |
| `strait_save_output` | Save a structured output |
| `strait_wait_for_event` | Pause and wait for an external event |
| `strait_state_get` | Read from durable KV state |
| `strait_state_set` | Write to durable KV state |
| `strait_complete` | Mark the run as completed |

## Event Definitions

`defineEvent()` declares a typed event with schema validation:

```ts
import { defineEvent } from "@strait/ts";

const approvalEvent = defineEvent(
  "approval.granted",
  z.object({ approvedBy: z.string(), orderId: z.string() })
);

const parsed = await approvalEvent.parse(rawData);
```

## Extended RunContext

`RunContext` is the execution context passed to every job and agent handler. It exposes async methods for interacting with the Strait runtime during a run.

```ts
import { createRunContextFromClient } from "@strait/ts";

const ctx = createRunContextFromClient(client, "run_123", { attempt: 1 });
await ctx.state?.set("key", value);
```

| Method | Description |
|---|---|
| `ctx.checkpoint(state)` | Persist intermediate state for resume |
| `ctx.reportProgress(percent, message)` | Report progress (0-100) |
| `ctx.heartbeat()` | Keep the run alive |
| `ctx.reportUsage(metrics)` | Report LLM token/cost usage |
| `ctx.logToolCall(name, input, output)` | Log an external tool invocation |
| `ctx.saveOutput(key, value, schema)` | Save a named output artifact |
| `ctx.streamChunk(chunk, options)` | Stream incremental output |
| `ctx.waitForEvent(eventKey, options)` | Pause until an external event fires |
| `ctx.spawn(options)` | Spawn a child run |
| `ctx.continue(payload)` | Continue to the next iteration |
| `ctx.annotate(annotations)` | Attach key-value annotations |
| `ctx.complete(result)` | Mark the run as completed |
| `ctx.fail(error)` | Mark the run as failed |
| `ctx.state.get(key)` | Read from the run's KV store |
| `ctx.state.set(key, value)` | Write to the run's KV store |
| `ctx.state.delete(key)` | Delete from the run's KV store |
| `ctx.state.list()` | List all keys in the run's KV store |
| `ctx.logger` | Structured logger (info, warn, error) forwarded to Strait |

## Test Harness

`createTestContext()` returns an in-memory `RunContext` and a `TestRunRecord` that captures every side-effect, so you can unit-test handlers without a running platform.

```ts
import { createTestContext } from "@strait/ts";

const { ctx, record } = createTestContext("test-run");
await myJobHandler(payload, ctx);

expect(record.checkpoints).toHaveLength(1);
expect(record.completed).toBe(true);
expect(record.stateStore.get("key")).toBe("expected");
```

`TestRunRecord` captures everything the handler did:

| Field | Type | Description |
|---|---|---|
| `record.checkpoints` | `Record[]` | States passed to `checkpoint()` |
| `record.logs` | `{ level, message, data }[]` | Log entries |
| `record.usageReports` | `{ provider, model, ... }[]` | Usage reports |
| `record.toolCalls` | `{ toolName, input, output, ... }[]` | Tool call logs |
| `record.outputs` | `{ key, value }[]` | Saved outputs |
| `record.progressUpdates` | `{ percent, message }[]` | Progress updates |
| `record.stateStore` | `Map<string, unknown>` | Final KV state |
| `record.streamChunks` | `{ chunk, streamId, done }[]` | Streamed chunks |
| `record.heartbeats` | `number` | Heartbeat count |
| `record.spawns` | `{ jobSlug, projectId, payload }[]` | Spawned child runs |
| `record.events` | `{ eventKey, timeoutSecs, ... }[]` | Events waited on |
| `record.annotations` | `Record<string, string>[]` | Annotations |
| `record.continuations` | `{ payload }[]` | Continuations |
| `record.completed` | `boolean` | Whether `complete()` was called |
| `record.failed` | `boolean` | Whether `fail()` was called |
| `record.failError` | `string \| undefined` | Error message from `fail()` |
| `record.result` | `Record \| undefined` | Result passed to `complete()` |

## Composition Helpers

```ts
import {
  withRetry,
  waitForRun,
  paginate,
  collectAll,
  triggerAndWait,
  withIdempotency,
} from "@strait/ts";

// Retry with exponential backoff
const result = await withRetry(() => client.triggerJob({ ... }), {
  attempts: 5,
  delayMs: 250,
  factor: 2,
  jitter: "full",
});

// Wait for a run to complete
const run = await waitForRun(
  (input) => client.getRun(input),
  "run_123",
  { timeoutMs: 60_000 },
);

// Paginate through results
for await (const job of paginate((q) => client.listJobs({ query: q }))) {
  console.log(job.id);
}

// Or collect all into an array
const allJobs = await collectAll(paginate((q) => client.listJobs({ query: q })));

// Trigger and wait combined
const finalRun = await triggerAndWait(
  (input) => client.triggerJob({ pathParams: { jobID: "job_1" }, body: input }),
  (input) => client.getRun(input),
  { payload: { sku: "ABC-123" } },
);

// Idempotency header
await client.triggerJob(withIdempotency(
  { pathParams: { jobID: "job_1" }, body: { payload: { sku: "ABC-123" } } },
  "unique-key-123",
));
```

### Cost budget

`createCostTracker()` tracks accumulated cost across retries and iterations. Use `withCostBudget()` to automatically abort when the budget is exceeded.

```ts
import { createCostTracker, withCostBudget } from "@strait/ts";

const tracker = createCostTracker({
  maxCostMicrousd: 5_000_000,
  warningThreshold: 0.8,
  onWarning: (current, max) => console.warn(`Cost warning: ${current}/${max}`),
});

const result = await withCostBudget(async (t) => {
  t.add(200_000);
  return await callExpensiveModel();
}, { maxCostMicrousd: 5_000_000 });
```

### Checkpoint resume

`withCheckpointResume()` wraps a function so it can resume from the last checkpoint after a crash or restart.

```ts
import { withCheckpointResume } from "@strait/ts";

const result = await withCheckpointResume(
  ctx,
  lastCheckpoint,
  async (state, update) => {
    for (let i = state.nextIndex; i < 100; i++) {
      await doStep(i);
      update({ nextIndex: i + 1 });
    }
    return "done";
  },
  { initialState: { nextIndex: 0 }, checkpointInterval: 5 },
);
```

### Deployment lifecycle

Compose deployment operations into atomic workflows:

```ts
import {
  createAndFinalizeDeployment,
  createFinalizePromoteDeployment,
} from "@strait/ts";

// Create + finalize
const { created, finalized } = await createAndFinalizeDeployment(client, {
  create: {
    body: {
      project_id: "proj_1",
      environment: "staging",
      runtime: "node",
      artifact_uri: "s3://bucket/build.tar.gz",
    },
  },
});

// Create + finalize + promote (full deploy)
const { promoted } = await createFinalizePromoteDeployment(client, {
  create: {
    body: {
      project_id: "proj_1",
      environment: "production",
      runtime: "node",
      artifact_uri: "s3://bucket/build.tar.gz",
    },
  },
});
```

## FSM State Machines

```ts
import { canTransitionRun, isTerminalRunStatus } from "@strait/ts/fsm";

canTransitionRun("executing", "COMPLETE"); // true
isTerminalRunStatus("completed");           // true
```

XState machines are available for programmatic use:

```ts
import { createActor } from "xstate";
import { runMachine, workflowRunMachine, stepRunMachine } from "@strait/ts/fsm";

const actor = createActor(runMachine);
actor.start();
actor.send({ type: "ENQUEUE" });
actor.send({ type: "DEQUEUE" });
actor.send({ type: "EXECUTE" });
actor.getSnapshot().value; // "executing"
```

## Middleware

```ts
const client = createClient(
  {
    baseUrl: "https://api.strait.dev",
    auth: { type: "bearer", token: "sk_live_..." },
  },
  {
    middleware: [
      {
        onRequest: ({ method, url }) => console.log(`-> ${method} ${url}`),
        onResponse: ({ status, durationMs }) => console.log(`<- ${status} (${durationMs}ms)`),
        onError: ({ error }) => console.error("Error:", error),
      },
    ],
  }
);
```

## Custom Fetch

Pass a custom `fetch` implementation for testing or custom HTTP behavior:

```ts
const client = createClient(
  {
    baseUrl: "https://api.strait.dev",
    auth: { type: "bearer", token: "sk_live_..." },
  },
  { fetch: myCustomFetch }
);
```

## Error Handling

All errors are typed using Effect's `Data.TaggedError`. Use the `_tag` field to match specific error kinds:

```ts
import { createClient } from "@strait/ts";

try {
  await client.getRun({ pathParams: { runID: "nonexistent" } });
} catch (error) {
  switch (error._tag) {
    case "NotFoundError":
      console.log("Not found:", error.message);
      break;
    case "UnauthorizedError":
      console.log("Auth error:", error.message);
      break;
    case "RateLimitedError":
      console.log("Rate limited:", error.message);
      break;
    default:
      console.log("Error:", error);
  }
}
```

Or use Result variants for non-throwing error handling:

```ts
const result = await client.triggerJobResult({
  pathParams: { jobID: "job_1" },
  body: { payload: { sku: "ABC-123" } },
});

if (result.ok) {
  console.log("Run:", result.output.id);
} else {
  console.log("Error:", result.error);
}
```

| Error type | HTTP status | Description |
|---|---|---|
| `TransportError` | -- | Network/transport failure |
| `DecodeError` | -- | JSON decode / schema validation failure |
| `ValidationError` | -- | Config or input validation |
| `UnauthorizedError` | 401, 403 | Authentication failure |
| `NotFoundError` | 404 | Resource not found |
| `ConflictError` | 409 | Conflict (duplicate, etc.) |
| `RateLimitedError` | 429 | Rate limit exceeded |
| `ApiError` | other | Generic HTTP error |
| `TimeoutError` | -- | Polling timeout |
| `DagValidationError` | -- | Workflow DAG is invalid |
| `CostBudgetExceededError` | -- | Cost budget exceeded |

## Exports

| Import path | Description |
|---|---|
| `@strait/ts` | Client, config, errors, authoring DSL, composition helpers, schema adapters |
| `@strait/ts/node` | Node.js config file discovery, env loading, `createClientFromConfigFile`, `createClientFromEnv` |
| `@strait/ts/fsm` | XState run, workflow, step state machines |
| `@strait/ts/ai` | AI SDK v6 provider middleware, agent, tools |

## Development

```bash
bun install
bun test
bun run typecheck
bun run lint
```