Durable Streams

Write generated output incrementally and keep it available by URL

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/stream
import { 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

NeedUse
generated output readable by URLDurable Streams
files or binary dataObject Storage
exact key lookupKey-Value
semantic search or RAG retrievalVector
relational joins or transactionsDatabase

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.

TTL

Streams expire after 30 days unless you pass ttl when creating the stream.

ValueBehavior
omittedexpire after 30 days
null or 0never expire
>= 60expire 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.

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 hono
import { 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