@trelent/agents
v0.2.10
Published
TypeScript SDK for the Trelent Agent Orchestration API.
Readme
@trelent/agents
TypeScript SDK for the Trelent Agent Orchestration API.
Installation
npm install @trelent/agents
# or
bun add @trelent/agentsQuick Start
Without authentication
import { Client } from "@trelent/agents";
const client = new Client("http://localhost:8000");
const sandboxes = await client.sandboxes.list();
console.log(sandboxes);
const run = await client.runs.create({
sandbox: "example-agent:latest",
prompt: "Create hello.txt with a greeting",
});
console.log(run.id, run.status, run.harness.kind);With authentication
When the API has authentication enabled, provide your OAuth2 client credentials:
import { Client } from "@trelent/agents";
const client = new Client("https://agents.trelent.com", {
clientId: "your-client-id",
clientSecret: "your-client-secret",
});
const sandboxes = await client.sandboxes.list();
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Do something useful",
});The SDK handles token acquisition automatically — it exchanges your client credentials for a JWT via the API's /token endpoint before making authenticated requests.
If you need to call the token proxy directly, use client.tokens.create(...).
Client Options
const client = new Client(apiUrl, {
clientId: "...", // OAuth2 client ID (optional)
clientSecret: "...", // OAuth2 client secret (optional)
scope: "...", // Override default OAuth2 scope (optional)
timeoutMs: 10_000, // Request timeout in ms (default: 10000)
});Both clientId and clientSecret must be provided together, or both omitted.
Resources
client.sandboxes
const sandboxes = await client.sandboxes.list();
// => RegistrySandbox[] — [{ name: "my-sandbox", tags: ["latest", "v1"] }]When auth is enabled, sandboxes are automatically filtered to your namespace. Sandbox names are returned without the namespace prefix.
client.tokens
const token = await client.tokens.create({
client_id: "your-client-id",
client_secret: "your-client-secret",
scope: "AgentOrchestrator:runs:list",
});client.runs
import { HarnessKind, LocalImporter, ObjectStorageDumpExporter, SharedDrive } from "@trelent/agents";
// Create a run
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Build a web server",
harness: { kind: HarnessKind.ClaudeCode }, // optional, defaults to Claude Code
workdir: "/workspace", // optional, defaults to image WORKDIR or /workspace
timeout_seconds: 3600, // optional, default: 3600
imports: [new LocalImporter("./data")], // optional
exports: [new ObjectStorageDumpExporter()], // optional
shared_drives: [new SharedDrive("my-drive")], // optional
secrets: [ // optional
{ name: "openai", env_var: "OPENAI_API_KEY" },
],
});
// List runs (optionally filter by sandbox)
const runs = await client.runs.list();
const filtered = await client.runs.list("my-sandbox:latest");
// Get a specific run
const run = await client.runs.get("run-id");
// Check status
const status = await client.runs.getStatus("run-id");
// Cancel a run
const cancel = await client.runs.cancel("run-id");
// Get checkpoints
const chain = await client.runs.getCheckpointChain("run-id");
const checkpoint = await client.runs.getCheckpoint("run-id", "checkpoint-id");Run operations
// Fork (resume from checkpoint)
const forked = await run.fork("Continue from where you left off", {
workdir: "/workspace/project",
timeout_seconds: 1800,
});
// Refresh run state
await run.refresh();
console.log(run.status); // updated statusStructured outputs
Pass outputSchema as a Zod schema to get a typed result back. The SDK
auto-converts your schema to JSON Schema before sending and re-validates
the harness's structured output through it on the way back via
run.outputParsed. The type flows through Run<T> so the result is
statically typed:
import { z } from "zod";
import { HarnessKind } from "@trelent/agents";
const CalendarEvent = z.object({
name: z.string(),
date: z.string(),
participants: z.array(z.string()),
});
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Alice and Bob are going to a science fair on Friday. Extract the event.",
harness: { kind: HarnessKind.Codex },
outputSchema: CalendarEvent,
});
// poll until done, then:
await run.refresh();
const event = run.outputParsed; // typed as { name; date; participants: string[] } | null
if (event) {
console.log(event.name, event.date, event.participants);
}Zod is an optional peer dependency. Install it only if you use Zod
schemas: npm install zod@>=3.24 (or bun add zod).
Fetching a typed run later (or from another process). When a different
process or a later poll fetches the run, pass the same schema to get() and
outputParsed hydrates the same way:
const run = await client.runs.get<z.infer<typeof CalendarEvent>>("run-id", {
outputSchema: CalendarEvent,
});
const event = run.outputParsed; // typedYou can also pass a raw JSON Schema object if you don't want to use Zod:
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "...",
outputSchema: {
type: "object",
properties: { name: { type: "string" } },
required: ["name"],
additionalProperties: false,
},
});
// Read the raw object — `outputParsed` is `null` when no Zod schema was provided.
await run.refresh();
console.log(run.result?.structured_output);Codex (OpenAI) is stricter than Claude. Codex requires every property
in required, additionalProperties: false on every object, and rejects
patternProperties, format, optional-field keywords, etc. If your
schema violates these the API returns a 422 at request time with a
JSON-pointer path to the bad node. Claude accepts most schemas as-is.
Streaming Events
Subscribe to real-time events from a run using Server-Sent Events (SSE). Events are streamed as the agent executes, providing live visibility into reasoning, messages, and tool usage.
Basic streaming
import { Client, EventType } from "@trelent/agents";
const client = new Client(apiUrl, { clientId, clientSecret });
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Build a simple web server",
});
// Subscribe to the event stream
const stream = run.subscribe();
for await (const event of stream) {
switch (event.type) {
case EventType.MessageDelta:
process.stdout.write(event.data.delta.text);
break;
case EventType.ToolCallStarted:
console.log(`\nTool: ${event.data.name}`);
break;
case EventType.SessionCompleted:
console.log(`\nCompleted with exit code: ${event.data.exit_code}`);
break;
}
}Subscribing via client
You can also subscribe using the run ID directly:
const stream = client.runs.subscribe("run-abc123");
for await (const event of stream) {
console.log(event.type, event.seq);
}Closing the stream
The stream automatically closes when the run completes. To close early:
const stream = run.subscribe();
for await (const event of stream) {
if (someCondition) {
stream.close(); // Unsubscribe and close connection
break;
}
}Event types
| Event Type | Description |
|------------|-------------|
| session.started | Agent session initialized |
| session.completed | Session finished (includes exit_code and usage) |
| turn.started | New conversation turn began |
| turn.completed | Turn finished |
| turn.failed | Turn failed with error |
| message.started | Assistant message started |
| message.delta | Incremental text content |
| message.completed | Full message content available |
| reasoning.started | Extended thinking started |
| reasoning.delta | Incremental reasoning text |
| reasoning.completed | Reasoning block finished |
| tool_call.started | Tool invocation started (includes name and kind) |
| tool_call.input_delta | Streaming tool input JSON |
| tool_call.input_complete | Full tool input available |
| tool_call.output_delta | Streaming tool output |
| tool_call.completed | Tool execution finished (includes exit_code) |
| structured_output | Validated structured output (only when run was created with outputSchema; emitted just before session.completed) |
| error | Error occurred |
Event structure
Every event has a common envelope:
interface CommonEvent {
seq: number; // Sequence number (monotonically increasing)
timestamp: string; // ISO 8601 timestamp
type: EventType; // Event type discriminator
data: EventData; // Type-specific payload
}client.health()
const health = await client.health();
// => { status: "ok" }Connectors
Importing local files
import { LocalImporter } from "@trelent/agents";
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Process the data",
imports: [new LocalImporter("./my-data-dir")],
});Exporting to S3
import { ObjectStorageDumpExporter } from "@trelent/agents";
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Generate a report",
exports: [new ObjectStorageDumpExporter("my-bucket", "reports/")],
});Shared drives
Shared drives mount a user-scoped shared directory into the sandbox at
/mnt/shared/<name>. They are specified via a dedicated shared_drives field
on the run, separate from import/export connectors.
import { SharedDrive } from "@trelent/agents";
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Read and write files in /mnt/shared/my-drive",
shared_drives: [new SharedDrive("my-drive")],
});To mount a subdirectory of the shared drive:
shared_drives: [new SharedDrive("my-drive", "project-a")]To export a shared drive (or a subdirectory of it) to S3 after the run:
import { SharedDrive, SharedDriveExporter } from "@trelent/agents";
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Generate reports in /mnt/shared/my-drive",
shared_drives: [new SharedDrive("my-drive")],
exports: [new SharedDriveExporter("my-drive", "reports")],
});Secrets
Create a user-scoped secret once. The API stores the value in the deployment's configured secret backend and only stores metadata in the database.
await client.secrets.create("openai", "sk-...");
const secrets = await client.secrets.list();
const secret = await client.secrets.get("openai");
await client.secrets.update("openai", "sk-new");
await client.secrets.delete("openai");Use secrets in runs by logical name. A string shorthand injects the secret as an environment variable with the same name:
const run = await client.runs.create({
sandbox: "my-sandbox:latest",
prompt: "Use the injected secret",
secrets: ["openai"],
});For a custom environment variable:
secrets: [{ name: "openai", env_var: "OPENAI_API_KEY" }]To mount a secret as a file instead of an env var:
secrets: [{ name: "openai", mount: true }]The runner writes mounted secrets to /mnt/secrets/<secret_name> inside the
sandbox. You can also set both env_var and mount: true to expose both forms.
Docker Registry Setup
When auth is enabled, push sandbox images to your user namespace on the registry:
# Login with your OAuth2 credentials
docker login registry.example.com -u <client_id> -p <client_secret>
# Push to your namespace
docker tag my-sandbox:latest registry.example.com/<client_id>/my-sandbox:latest
docker push registry.example.com/<client_id>/my-sandbox:latestWhen creating runs via the SDK, just use the sandbox name without the namespace prefix:
const run = await client.runs.create({
sandbox: "my-sandbox:latest", // not "<client_id>/my-sandbox:latest"
prompt: "...",
});The API resolves the full registry path automatically based on your authenticated identity.
Types
The SDK exports the following types:
import { EventType, HarnessKind, RunStatus, ToolKind } from "@trelent/agents";
import type {
CancelRunResponse,
ClaudeCodeHarnessSpec,
CodexHarnessSpec,
ClientOptions,
CommonEvent,
CreateRunOptions,
RegistrySandbox,
RunResult,
RunStatusResponse,
CheckpointResponse,
ChatHistoryEntry,
FileOutput,
OutputFile,
HealthResponse,
HarnessInfo,
HarnessSpec,
ResumeRunOptions,
RunSecretBinding,
RunSecretSpec,
SecretResponse,
TokenRequest,
TokenResponse,
Usage,
WorkflowIds,
} from "@trelent/agents";