DeepSeek Harness: Protocolo StreamChunk e Tratamento de Erros
Última atualização: 2026-08-31
Saída em streaming é a experiência central de interação com o Agent — usuários não querem esperar 30 segundos por uma resposta completa, querem ver o Agent pensar palavra por palavra. O protocolo StreamChunk é a abstração unificada do DSH para saída em streaming, e tratamento de erros garante degradação graciosa quando exceções ocorrem.
📋 Pré-requisitos: Ter completado 24-llm-adapter.md, entender adaptadores LLM
1. O Que Você Vai Aprender
- Protocolo de saída em streaming: StreamChunk
- Tipos de chunk: text/tool_call/tool_result
- Mecanismos de recuperação de erros
- Interrupção e cancelamento
- Estratégias de tentativa
- Logging e observabilidade
2. Detalhes do Protocolo StreamChunk
(1) Definição Completa
type StreamChunk =
| TextChunk
| ToolCallChunk
| ToolResultChunk
| ErrorChunk
| DoneChunk
interface TextChunk {
type: 'text'
content: string
}
interface ToolCallChunk {
type: 'tool_call'
id: string
name: string
arguments: string
index: number
}
interface ToolResultChunk {
type: 'tool_result'
id: string
toolCallId: string
result: any
isError: boolean
}
interface ErrorChunk {
type: 'error'
error: Error
recoverable: boolean
retryAfter?: number
}
interface DoneChunk {
type: 'done'
reason: 'stop' | 'tool_use' | 'length' | 'cancel' | 'error'
usage?: TokenUsage
}
interface TokenUsage {
promptTokens: number
completionTokens: number
totalTokens: number
}
(2) Fluxo Típico
graph LR
START[Stream inicia] --> TEXT[TextChunk × N]
TEXT --> TC[ToolCallChunk]
TC --> TR[ToolResultChunk]
TR --> TEXT2[TextChunk × N]
TEXT2 --> DONE[DoneChunk]
Um fluxo típico de conversa de Agent:
- LLM emite texto → TextChunk
- LLM decide chamar uma ferramenta → ToolCallChunk
- Execução da ferramenta completa → ToolResultChunk
- LLM continua saída → TextChunk
- Stream termina → DoneChunk
(3) Exemplo de Consumo Completo
▶ Exemplo 1: Consumo Completo de Stream com Switch
const stream = ctx.llm.stream({
messages: [{ role: 'user', content: 'list project files' }],
tools: availableTools
})
for await (const chunk of stream) {
switch (chunk.type) {
case 'text':
process.stdout.write(chunk.content)
break
case 'tool_call':
console.log(`\n🔧 Chamada de ferramenta: ${chunk.name}`)
console.log(` Argumentos: ${chunk.arguments}`)
break
case 'tool_result':
if (chunk.isError) {
console.log(` ❌ Erro da ferramenta: ${chunk.result}`)
} else {
console.log(` ✅ Resultado: ${JSON.stringify(chunk.result)}`)
}
break
case 'error':
console.error(`\n⚠️ Erro: ${chunk.error.message}`)
if (chunk.recoverable) {
console.log(` Tentará novamente em ${chunk.retryAfter}ms`)
}
break
case 'done':
console.log(`\n✅ Concluído (${chunk.reason})`)
if (chunk.usage) {
console.log(` Uso de tokens: ${chunk.usage.totalTokens}`)
}
break
}
}
3. Tipos de Chunk em Detalhe
(1) TextChunk
Fragmentos de conteúdo de texto, emitidos incrementalmente:
// LLM emite "Hello, world!"
// Pode produzir múltiplos TextChunks:
// chunk 1: { type: 'text', content: 'Hello' }
// chunk 2: { type: 'text', content: ', ' }
// chunk 3: { type: 'text', content: 'world' }
// chunk 4: { type: 'text', content: '!' }
Consumers devem concatenar todos os TextChunks em vez de exibi-los individualmente.
(2) ToolCallChunk
LLM requisita uma chamada de ferramenta:
{
type: 'tool_call',
id: 'call_abc123',
name: 'file_edit',
arguments: '{"action":"read","path":"src/index.ts"}',
index: 0
}
Nota: arguments é uma string JSON que precisa parsing.
(3) ToolResultChunk
Resultados após execução da ferramenta:
{
type: 'tool_result',
id: 'result_xyz789',
toolCallId: 'call_abc123',
result: { content: 'export const name = ...' },
isError: false
}
isError: true indica falha na execução da ferramenta; result contém informação de erro.
(4) Múltiplas Chamadas de Ferramenta
Uma única conversa pode chamar múltiplas ferramentas:
TextChunk: "Vou verificar dois arquivos"
ToolCallChunk: { name: "file_edit", id: "call_1", index: 0 }
ToolCallChunk: { name: "file_edit", id: "call_2", index: 1 }
ToolResultChunk: { toolCallId: "call_1", result: ... }
ToolResultChunk: { toolCallId: "call_2", result: ... }
TextChunk: "O conteúdo de ambos os arquivos acima..."
DoneChunk: { reason: "stop" }
4. Mecanismo de Recuperação de Erros
(1) Classificação de Erros
// Erro não-recuperável (ex.: chave de API inválida)
{
type: 'error',
error: new Error('Invalid API key'),
recoverable: false
}
// Erro recuperável (ex.: limite de taxa)
{
type: 'error',
error: new Error('Rate limit exceeded'),
recoverable: true,
retryAfter: 5000
}
// Erro recuperável (ex.: instabilidade de rede)
{
type: 'error',
error: new Error('Connection timeout'),
recoverable: true,
retryAfter: 2000
}
(2) Fluxo de Auto-Recovery
graph TD
ERR[ErrorChunk] --> CHECK{recuperável?}
CHECK -->|Não| FAIL[Encerrar stream + DoneChunk<br/>reason: error]
CHECK -->|Sim| WAIT[Aguardar retryAfter]
WAIT --> RETRY[Tentar novamente requisição]
RETRY --> SUCCESS{Sucesso?}
SUCCESS -->|Sim| CONTINUE[Continuar stream]
SUCCESS -->|Não| ERR2[Outro ErrorChunk]
ERR2 --> CHECK2{Tentativas esgotadas?}
CHECK2 -->|Não| WAIT
CHECK2 -->|Sim| FAIL
(3) Recuperação Manual
▶ Exemplo 2: Tratamento Manual de Erros de Stream
for await (const chunk of stream) {
if (chunk.type === 'error') {
if (chunk.recoverable) {
ctx.logger.warn(`Erro de stream, recuperável: ${chunk.error.message}`)
// Framework auto-tenta novamente
} else {
ctx.logger.error(`Erro de stream, não-recuperável: ${chunk.error.message}`)
break
}
}
}
(4) Tratamento de Erros de Ferramenta
Quando a execução de uma ferramenta falha, o framework retorna o erro como ToolResultChunk para o LLM:
{
type: 'tool_result',
id: 'result_1',
toolCallId: 'call_1',
result: { error: true, message: 'Permission denied: /etc/passwd' },
isError: true
}
O LLM pode então escolher:
- Tentar novamente com um caminho diferente
- Informar o usuário sobre permissões insuficientes
- Completar a tarefa de outra forma
5. Interrupção e Cancelamento
(1) Cancelamento pelo Usuário
Quando um usuário clica no botão "Parar" na Web UI, o stream é cancelado:
const controller = new AbortController()
const stream = ctx.llm.stream(request, { signal: controller.signal })
// Usuário cancela
controller.abort()
// Stream produz DoneChunk
// { type: 'done', reason: 'cancel' }
(2) Cancelamento por Timeout
const controller = new AbortController()
const timer = setTimeout(() => controller.abort(), 60000)
try {
for await (const chunk of ctx.llm.stream(request, { signal: controller.signal })) {
// Processar chunk
}
} finally {
clearTimeout(timer)
}
(3) Cancelamento Condicional
let tokenCount = 0
for await (const chunk of stream) {
if (chunk.type === 'text') {
tokenCount += chunk.content.length
if (tokenCount > 10000) {
controller.abort()
break
}
}
}
(4) Limpeza Após Cancelamento
Após cancelamento, o framework garante:
- Para de receber novos chunks
- Chunks já recebidos são processados normalmente
- Produz
DoneChunk { reason: 'cancel' } - Libera conexões de rede
6. Estratégias de Tentativa
(1) Estratégias de Tentativa Built-in
| Tipo de Erro | Tentativas | Estratégia de Backoff |
|---|---|---|
| Limite de taxa (429) | 3 | Backoff exponencial |
| Timeout de rede | 2 | Intervalo fixo |
| Erro de servidor (5xx) | 2 | Backoff exponencial |
| Erro de autenticação (401) | 0 | Sem tentativa |
| Erro de requisição (400) | 0 | Sem tentativa |
(2) Configurando Tentativas
export const Config = Schema.object({
maxRetries: Schema.number().default(3).description('Máximo de tentativas'),
retryDelay: Schema.number().default(1000).description('Atraso inicial de tentativa (ms)'),
retryMultiplier: Schema.number().default(2).description('Multiplicador de atraso para backoff exponencial')
})
(3) Lógica de Tentativa Customizada
▶ Exemplo 3: Tentativa com Backoff Exponencial
async function streamWithRetry(
ctx: Context,
request: LLMRequest,
maxRetries = 3
): Promise<void> {
let attempt = 0
while (attempt <= maxRetries) {
try {
for await (const chunk of ctx.llm.stream(request)) {
if (chunk.type === 'error' && chunk.recoverable) {
attempt++
const delay = Math.min(1000 * Math.pow(2, attempt), 30000)
ctx.logger.warn(`tentativa ${attempt}/${maxRetries} em ${delay}ms`)
await sleep(delay)
break // Re-entrar no loop while
}
// Processar chunks normais
}
return // Sucesso
} catch (error) {
attempt++
if (attempt > maxRetries) throw error
}
}
}
7. Logging e Observabilidade
(1) Logging de Stream
ctx.on('llm/stream/start', (request) => {
ctx.logger.info(`stream iniciado: model=${request.model}`)
})
ctx.on('llm/stream/chunk', (chunk) => {
ctx.logger.debug(`chunk: type=${chunk.type}`)
})
ctx.on('llm/stream/end', (done) => {
ctx.logger.info(`stream encerrado: reason=${done.reason}, tokens=${done.usage?.totalTokens}`)
})
(2) Métricas de Desempenho
interface StreamMetrics {
ttfb: number // Time to First Byte
totalDuration: number // Duração total
chunkCount: number // Contagem de chunks
toolCallCount: number // Contagem de chamadas de ferramenta
retryCount: number // Contagem de tentativas
tokenUsage: TokenUsage // Uso de tokens
}
(3) Logging Estruturado
ctx.logger.info('llm_request', {
model: request.model,
messageCount: request.messages.length,
toolCount: request.tools?.length || 0,
stream: true
})
ctx.logger.info('llm_response', {
model: response.model,
duration: Date.now() - startTime,
tokens: response.usage?.totalTokens
})
❓ Perguntas Frequentes
typescript const start = Date.now() for await (const chunk of stream) { if (chunk.type === 'text') { const ttfb = Date.now() - start ctx.logger.info(`TTFB: ${ttfb}ms`) break } } complete() e stream(). Chamadores escolhem conforme necessário.typescript async function* mockStream(): AsyncIterable<StreamChunk> { yield { type: 'text', content: 'Hello' } yield { type: 'text', content: ', world!' } yield { type: 'done', reason: 'stop' } } 📖 Resumo
- StreamChunk cinco tipos: text, tool_call, tool_result, error, done
- Ciclo de vida do stream: saída de texto → chamada de ferramenta → resultado da ferramenta → continuar saída → done
- Erros são classificados como recuperáveis e não-recuperáveis; erros recuperáveis são automaticamente tentados novamente
- Cancelamento via AbortController produz
DoneChunk { reason: 'cancel' } - Estratégias de tentativa variam por tipo de erro; backoff exponencial é o padrão
- Logging e observabilidade através de eventos de ciclo de vida e logging estruturado
📝 Exercícios
1. ⭐ Básico: Escreva um consumer que itera sobre um stream StreamChunk, contando chunks text/tool_call/tool_result/error/done separadamente. Teste com um stream mock.
2. ⭐⭐ Intermediário: Implemente um consumer de stream com timeout — se nenhum chunk chegar em 30 segundos, automaticamente cancela o stream. Use AbortController para cancelamento.
3. ⭐⭐⭐ Desafio: Implemente um wrapper de tentativa customizado em torno de ctx.llm.stream() que auto-tenta em erros recuperáveis (backoff exponencial, máx. 3 tentativas). Registre o atraso e resultado de cada tentativa. Teste com um adaptador LLM mock que alterna entre falha e sucesso.