@episcloud/agent
v0.9.1
Published
EpisCloud coding agent SDK — typed tools, permissions, streaming, sub-agents, handoffs, guardrails, structured outputs.
Downloads
177
Maintainers
Readme
@episcloud/agent
A TypeScript SDK for building coding agents on top of the EpisCloud AI
platform. Built for the same patterns developers know from other agent
toolkits: a query() async generator, typed tool definitions backed by
Zod, a permission system, hooks, and a streaming result protocol.
npm install @episcloud/agentNode 20+ required.
Quick start
import { query } from '@episcloud/agent';
for await (const msg of query('Review src/auth.ts for OWASP issues', {
apiKey: process.env.EPISCLOUD_API_KEY!, // epis_sk_…
allowedTools: ['Read', 'Glob', 'Grep'], // read-only inspection
})) {
if (msg.type === 'stream_event') process.stdout.write(msg.delta);
if (msg.type === 'tool_use') console.log('\n→', msg.name, msg.input);
if (msg.type === 'result') console.log('\n✓', msg.usage);
}ECAI Voice — real-time speech-to-speech
Talk to ECAI like a phone call: sub-second replies, natural fillers and interruptions, Australian-English and Vietnamese native voices, live-web grounded answers, caller memory across calls (Pro).
Browser — full runtime in 4 lines (mic capture, VAD auto-endpointing, gapless playback, auto barge-in, reconnect — all built in):
import { EcaiVoiceCall } from '@episcloud/agent/voice';
const call = new EcaiVoiceCall({
apiKey: EPIS_API_KEY,
tier: 'pro', // 'standard' $0.03/min | 'pro' $0.08/min
lang: 'en', // 'vi' → Vietnamese native voice (auto-Pro)
onSay: (s) => captions.show(s.text, s.filler),
onStateChange: (st) => ui.setOrb(st), // listening | thinking | speaking
onLevel: (rms) => ui.meter(rms),
onUsage: (u) => console.log(`turn: $${u.cost_microusd / 1e6}`),
});
await call.start(); // mic permission → live conversation, hands-free
// call.sendText('…') for a typed lane; call.end() to hang up.Any environment — low-level protocol client (bring your own audio I/O):
import { EcaiVoiceSession } from '@episcloud/agent/voice';
const call = new EcaiVoiceSession({
apiKey: process.env.EPIS_API_KEY!, // epis_sk_… or a session JWT
tier: 'pro', // 'standard' $0.03/min | 'pro' $0.08/min
lang: 'en', // 'vi' for Vietnamese (auto-Pro)
onSay: (s) => console.log('ecai:', s.text),
onAudio: (wav, meta) => player.enqueue(wav, meta.epoch), // 24 kHz mono WAV clips
onUsage: (u) => console.log(`turn: $${u.cost_microusd / 1e6}`),
});
await call.connect();
call.sendAudio(pcm16kChunk); // raw PCM16LE @16 kHz mono, ~100 ms chunks — keep streaming during silence
call.utteranceEnd(); // caller stopped talking
call.bargeIn(); // caller interrupted playback
call.sendText("What's the weather in Sydney?"); // typed lane
call.close();Protocol reference: docs/ai/ecai-voice-api.md in the gateway repo.
How it works
The SDK calls the EpisCloud Chat API (/v1/chat/completions) over HTTPS
with your epis_sk_ Bearer token. Each tool the agent invokes is run
inside your Node process — the SDK never executes anything server-side.
your code ──► @episcloud/agent ──► paas-ai.episcloud.com
│ │
│ ▼
│ ┌──────────────────┐
│ │ EpisCloud Chat │
│ │ (auth + bill + │
│ │ model loop) │
│ └──────────────────┘
│
▼ runs locally
Read · Glob · Grep · Edit · Write · Bash · …Built-in tools
| Tool | Purpose | Default permission |
|--------------|------------------------------------------------------|--------------------|
| Read | Read file with line offset / limit | allow |
| Write | Create / overwrite a file | ask |
| Edit | Replace exact string in an existing file | ask |
| Glob | Match files by glob (src/**/*.ts) | allow |
| Grep | Search regex across the working directory | allow |
| Bash | Run a shell command | ask |
| TodoWrite | Agent's internal multi-step task tracker | allow |
By default query() enables only the read-only set (Read, Glob,
Grep, TodoWrite). Opt in to mutators explicitly:
query(prompt, {
apiKey: '…',
allowedTools: ['Read', 'Glob', 'Grep', 'Edit', 'Write', 'Bash'],
permissions: { Bash: 'ask', Edit: 'allow', Write: 'allow' },
});Custom tools
import { z } from 'zod';
import { tool, query } from '@episcloud/agent';
const dbQuery = tool({
name: 'db_query',
description: 'Run a read-only SQL query against the analytics DB',
parameters: z.object({
sql: z.string().describe('SELECT only — DDL/DML rejected'),
limit: z.number().int().min(1).max(1000).default(100),
}),
defaultPermission: 'allow',
async execute({ sql, limit }) {
if (!/^\s*select/i.test(sql)) return 'error: only SELECT allowed';
const rows = await runQuery(sql, limit);
return JSON.stringify(rows);
},
});
for await (const msg of query('How many sign-ups last week?', {
apiKey: process.env.EPISCLOUD_API_KEY!,
tools: [dbQuery],
})) {
// …
}Permission modes
Pass permissionMode to set a coarse policy that overrides per-tool
defaults:
| Mode | Effect |
|-----------------------|-------------------------------------------------------------------|
| default | Honor each tool's defaultPermission + your permissions map |
| acceptEdits | Auto-approve file-mutating tools (Write/Edit/MultiEdit) |
| plan | Refuse every tool — model must produce a plan in prose |
| bypassPermissions | Allow everything (CI / unattended only) |
When a tool is gated by ask, the SDK invokes your onToolApproval:
query(prompt, {
apiKey: '…',
onToolApproval: ({ name, input, description }) => {
if (name === 'Bash' && /rm\s+-rf/.test((input as any).command)) {
return 'deny';
}
return 'allow';
},
});Hooks
Hooks fire at lifecycle points and can observe, mutate, or block:
query(prompt, {
apiKey: '…',
hooks: {
PreToolUse: ({ name }) => {
if (name === 'Write') return { permission: 'deny', reason: 'frozen tree' };
return {};
},
PostToolUse: ({ name, result }) => {
metrics.record(name, result.length);
},
UserPromptSubmit: ({ prompt }) => ({
prompt: prompt.replace(/<SECRET>/g, '[REDACTED]'),
}),
},
});Available hooks:
PreToolUse— before a tool runs; can denyPostToolUse— after a tool returnsUserPromptSubmit— before the user prompt reaches the modelStop— when the agent decides to finalize; can force another turnNotification— surface info/warn events to your UI
Cancellation & budgets
const ctrl = new AbortController();
setTimeout(() => ctrl.abort(), 30_000); // hard 30s cap
const gen = query(prompt, {
apiKey: '…',
signal: ctrl.signal,
maxTurns: 12, // tool-loop iteration cap
maxCostMicroUSD: 100_000, // $0.10 budget
requestTimeoutMs: 60_000, // per model call
});
for await (const msg of gen) {
if (msg.type === 'result' && msg.subtype === 'cancelled') {
console.log('partial result:', msg.result);
}
}Messages
query() yields a discriminated union (SDKMessage). Each message
carries session_id. Branch on .type:
| Type | When |
|----------------|-------------------------------------------------|
| system | Once at start (resolved options, tool catalog) |
| user | Echo of the prompt |
| assistant | One per turn — text + tool calls |
| tool_use | The agent invoked a tool |
| tool_result | The tool returned (success or is_error=true) |
| stream_event | Incremental text/reasoning chunk |
| result | Final — has usage, duration_ms, result |
Error handling
import { InsufficientBalanceError, RateLimitError } from '@episcloud/agent';
try {
for await (const msg of query(prompt, { apiKey: '…' })) { /* … */ }
} catch (err) {
if (err instanceof InsufficientBalanceError) {
promptUserToTopUp();
} else if (err instanceof RateLimitError) {
await sleep((err.retryAfterSec ?? 5) * 1000);
} else {
throw err;
}
}Typed errors: AuthenticationError, InsufficientBalanceError,
RateLimitError, InvalidOptionsError, MaxTurnsExceededError,
BudgetExceededError, ToolExecutionError, ToolDeniedError,
TransportError.
CLI
npm install -g @episcloud/agent
episctl-agent run "refactor src/api/client.ts to use fetch"
episctl-agent chat # interactive REPLCLI looks for ~/.episcloud/agent-settings.json and the
EPISCLOUD_API_KEY env var.
License
MIT
