Use durable streams when output is produced over time and should remain readable after the request or worker finishes: CSV exports, logs, generated reports, transcripts, and long model outputs.
npm install @agentuity/streamimport { StreamClient } from '@agentuity/stream';
const streams = new StreamClient();
const stream = await streams.create('monthly-reports', {
contentType: 'text/csv',
metadata: { accountId: 'acct_123', month: '2026-04' },
ttl: 60 * 60 * 24 * 90,
});
try {
await stream.write('account_id,month,total\n');
await stream.write('acct_123,2026-04,42817\n');
} finally {
await stream.close();
}
const url = stream.url;StreamClient reads AGENTUITY_SDK_KEY, then AGENTUITY_CLI_KEY, from the environment. Pass constructor options only when a script needs an explicit apiKey, url, orgId, or logger.
When to use durable streams
| Need | Use |
|---|---|
| generated output readable by URL | Durable Streams |
| files or binary data | Object Storage |
| exact key lookup | Key-Value |
| semantic search or RAG retrieval | Vector |
| relational joins or transactions | Database |
Use streams when you need to write chunks as they are produced, hand a URL to another service, or keep a finished artifact around without first assembling the full file in memory.
Create and Write
create() returns a writable stream with an id, public url, bytesWritten, and compressed flag.
import { StreamClient } from '@agentuity/stream';
const streams = new StreamClient();
async function writeAuditLog(userId: string, events: readonly string[]): Promise<string> {
const stream = await streams.create('audit-logs', {
contentType: 'text/plain',
metadata: { userId },
});
try {
for (const event of events) {
await stream.write(`${new Date().toISOString()} ${event}\n`);
}
} finally {
await stream.close();
}
return stream.url;
}write() accepts strings, Uint8Array, ArrayBuffer, and objects. Objects are serialized as JSON. Each write can send up to 5 MB, so split larger payloads across multiple writes.
Streams do not auto-close. Use finally around writes so the stream is finalized even when generation fails partway through.
TTL
Streams expire after 30 days unless you pass ttl when creating the stream.
| Value | Behavior |
|---|---|
| omitted | expire after 30 days |
null or 0 | never expire |
>= 60 | expire after that many seconds |
Managed stream storage clamps TTL values below 60 seconds to 60 seconds and values above 90 days to 90 days.
Compression
Set compress: true for text-heavy output such as CSV, JSON, and logs.
const stream = await streams.create('exports', {
contentType: 'application/json',
compress: true,
});
await stream.write({ status: 'ok' });
await stream.close();Compression is finalized when the stream closes. Clients that fetch the stream URL should support gzip, which browsers and most HTTP clients handle automatically.
Read, List, and Delete
Use get() for metadata, download() for content, list() for filtered discovery, and delete() for cleanup.
import { StreamClient } from '@agentuity/stream';
const streams = new StreamClient();
const info = await streams.get('stream_abc123');
const content = await streams.download(info.id);
const text = await new Response(content).text();
const recentExports = await streams.list({
namespace: 'monthly-reports',
metadata: { accountId: 'acct_123' },
limit: 25,
});
const totalExports = recentExports.total;
void totalExports;
await streams.delete(info.id);list() returns { success, message, streams, total }. Each listed stream includes id, namespace, metadata, url, sizeBytes, and expiresAt. Stream metadata values must be strings; encode richer payloads with JSON.stringify before storing.
Use the stream URL as the artifact pointer you store in KV, a queue message, an email, or a task. Keep larger structured context in metadata or a database row instead of encoding it into the URL.
Background Work
Create streams inside the worker, job, or route that owns the generated output. If the work should outlive the request, enqueue it first and store status separately.
import { StreamClient } from '@agentuity/stream';
interface ReportJob {
readonly accountId: string;
readonly month: string;
}
const streams = new StreamClient();
export async function writeReport(job: ReportJob): Promise<string> {
const stream = await streams.create('monthly-reports', {
contentType: 'text/csv',
metadata: { accountId: job.accountId, month: job.month },
ttl: 60 * 60 * 24 * 90,
});
try {
await stream.write('account_id,month,total\n');
await stream.write(`${job.accountId},${job.month},42817\n`);
} finally {
await stream.close();
}
return stream.url;
}See Background Work: it pairs queues, KV status records, and durable streams for jobs that should continue after a request returns.
Hono
In Hono apps, @agentuity/hono initializes StreamClient once and exposes it on c.var.stream.
npm install @agentuity/hono honoimport { agentuity } from '@agentuity/hono';
import type { Services } from '@agentuity/hono';
import { Hono } from 'hono';
type Variables = Pick<Services, 'stream'>;
const app = new Hono<{ Variables: Variables }>();
app.use('*', agentuity());
app.post('/exports', async (c) => {
const stream = await c.var.stream.create('exports', {
contentType: 'text/plain',
});
try {
await stream.write('hello\n');
} finally {
await stream.close();
}
return c.json({ id: stream.id, url: stream.url });
});
export default app;Next Steps
- Background Work: run long jobs with queues, status records, and streams
- Object Storage: store files and binary blobs with Bun S3
- Key-Value Storage: store job status and exact-key state
- Streams API Reference: inspect REST fields and lower-level method details