@strait/ts
v0.1.2
Published
TypeScript SDK for the Strait platform API with full feature parity across all five Strait SDKs.
Downloads
44
Readme
@strait/ts
TypeScript SDK for the Strait platform API with full feature parity across all five Strait SDKs.
Install
npm install @strait/tsOr with Bun:
bun add @strait/tsQuick Start
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:
{
"$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:
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
import { createClientFromEnv } from "@strait/ts/node";
// Reads STRAIT_BASE_URL, STRAIT_API_KEY, STRAIT_AUTH_TYPE, STRAIT_TIMEOUT_MS
const client = createClientFromEnv();Inline
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
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:
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
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.
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:
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.
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 for automatic usage reporting, tool call logging, and streaming.
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:
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.
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.
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
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.
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.
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:
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
import { canTransitionRun, isTerminalRunStatus } from "@strait/ts/fsm";
canTransitionRun("executing", "COMPLETE"); // true
isTerminalRunStatus("completed"); // trueXState machines are available for programmatic use:
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
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:
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:
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:
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
bun install
bun test
bun run typecheck
bun run lint