Documentação
Crie um cliente assistente de streaming
Abra um turno, renderize seus eventos reais, responda solicitações pendentes e recupere a conversa após uma queda de conexão.
Traduzido automaticamente do inglês. Ver o original em inglês
Conecte-se ao sidecar em execução
Um cliente de assistente de streaming se conecta ao mesmo sidecar local que o desktop. Obtenha o URL de loopback atribuído e o token de portador do aplicativo atual da conexão com a qual você está integrando. Uma porta salva no lançamento de ontem não é um endereço durável: o sidecar solicita uma porta efêmera cada vez que é iniciado. Um token de sessão de conta, um token de emparelhamento complementar e um token de ponte de ação com escopo têm finalidades diferentes e não podem substituir o portador do aplicativo nesta rota de chat.
Antes de enviar uma curva, leia GET /api/ai/status, /api/ai/providerse opcionalmente /api/ai/models?configuredOnly=true. Uma credencial configurada significa que uma chave está presente; isso não prova que o provedor aceitará a solicitação ou que um terminal oferece suporte ao modelo selecionado. O host padrão é o nikcli incorporado. O modo direto pode transmitir uma resposta de modelo, mas não fornece o loop de ferramentas persistente do host incorporado ou o agendador especializado.
Este guia usa CADCAMFUN_URL e CADCAMFUN_TOKEN como variáveis do shell do cliente. Forneça-os por meio do gerenciamento de conexão existente. Veja o Referência da API de IA para os esquemas exatos e Sidecar para comportamento de lançamento. Os exemplos não instalam credenciais nem escolhem um modelo em seu nome.
Escolha o projeto, o desenho e a identidade da sessão
O cliente pode fornecer um sessionId no primeiro turno e reutilizá-lo nos turnos posteriores. O serviço mapeia esse identificador de cliente para a sessão de host real e verifica a ligação antes de cada prompt. Retenha a identidade do evento/sessão como estado de conversa, em vez de criar um identificador diferente sempre que a IU se reconectar.
Um verdadeiro projectId dá ao turno acesso aos recursos permitidos daquele projeto. Um opcional drawingId deve pertencer a esse projeto; ele vincula as ferramentas do turno a esse desenho. Um desenho não pode ser nomeado sem o seu projeto. Uma sessão que já pertence a outra conta ou projeto é recusada. Estas são verificações de admissão antes que o modelo possa executar as ferramentas do projeto, e não dicas em um prompt do sistema.
Para uma conversa explicativa geral, omita projeto e desenho. O aplicativo usa seu contexto padrão. Para trabalho de geometria, carregue os IDs reais do projeto/desenho selecionados por meio de bate-papo, recuperação de estado, fila, cancelamento e solicitações de controle. Uma troca de conta não deve adotar silenciosamente a sessão ativa da conta anterior.
Envie um turno de streaming
POST /api/ai/chat/stream retorna um corpo SSE. Requer uma mensagem que não esteja em branco, embora o esquema de transporte torne vários campos opcionais para que os manipuladores possam retornar mensagens de validação legíveis. A questão geral a seguir desativa a injeção de contexto armazenado, tornando-a adequada para verificar o transporte independentemente da geometria do projeto.
curl --no-buffer --fail-with-body \
-H "Authorization: Bearer $CADCAMFUN_TOKEN" \
-H "Content-Type: application/json" \
--data '{"message":"Explain the difference between G0 and G1.","sessionId":"inspection-demo","includeContext":false,"mode":"gcode"}' \
"$CADCAMFUN_URL/api/ai/chat/stream" Adicionar projectId e drawingId para trabalhar em um desenho específico. mode aceita geral, cad, cam, gcode ou percurso da ferramenta. UM model o seletor pode conter provedor, modelo, nível e variante; os níveis são rápidos, equilibrados ou melhores. Descubra IDs de provedor/modelo no catálogo em execução, em vez de codificar uma suposição sobre qual fornecedor está disponível.
permissionMode aceita aprovar, perguntar ou automático. Grupos de ferramentas destrutivas mantêm a aprovação humana em todos os modos. O agent campo seleciona uma função de assistente configurada; ler GET /api/ai/agents em vez de adivinhar os IDs das funções.
Leia frames de dados SSE completos
A rota usa o modo de dados SSE do Effect: cada carga útil de dados SSE é um registro de evento JSON com um type discriminador. Um pedaço de rede não é um limite de evento. JSON, finais de linha e caracteres UTF-8 podem ser divididos entre leituras. Acumule texto decodificado e analise apenas quadros completos, unindo múltiplas linhas de dados quando presentes.
async function* events(response: Response): AsyncGenerator<Record<string, unknown>> {
if (!response.ok) throw new Error(await response.text())
if (!response.body) throw new Error("The response has no stream")
const reader = response.body.getReader()
const decoder = new TextDecoder()
let pending = ""
try {
while (true) {
const next = await reader.read()
pending += decoder.decode(next.value, { stream: !next.done })
let boundary: RegExpExecArray | null
while ((boundary = /\r?\n\r?\n/.exec(pending)) !== null) {
const frame = pending.slice(0, boundary.index)
pending = pending.slice(boundary.index + boundary[0].length)
const data = frame.split(/\r?\n/)
.filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).replace(/^ /, "")).join("\n")
if (!data) continue
const event: unknown = JSON.parse(data)
if (!event || typeof event !== "object" || !("type" in event)) {
throw new Error("Invalid assistant event")
}
yield event as Record<string, unknown>
}
if (next.done) break
}
} finally {
await reader.cancel().catch(() => {})
reader.releaseLock()
}
} Este leitor demonstra o enquadramento; um cliente de produção também deve validar cada evento em relação ao contrato compartilhado ou usar o cliente de streaming gerado digitado. Ele ignora intencionalmente os campos de comentário/controle SSE porque o estado do aplicativo desse endpoint reside em registros de dados JSON. Liberar um fluxo de cliente é uma limpeza de transporte, não uma solicitação explícita para abortar o trabalho no servidor.
Renderize o progresso sem duplicar o conteúdo
Inicie a busca com o mesmo portador e corpo JSON mostrados acima e, em seguida, itere o leitor. Acrescentar delta.text para texto de resposta visível e rota reasoning.text à sua própria visão. UM done o evento carrega o texto da resposta final; substitua ou reconcilie a resposta acumulada com o valor final em vez de anexar a resposta inteira novamente.
| Eventos | Estado para manter |
|---|---|
turn, session_status, step | Estado ocupado/nova tentativa e etapa atual; uma etapa de finalização pode ser seguida por outra etapa no mesmo turno. |
tool_call, tool_progress, tool_result | Um registro por callId com nome, entrada, progresso do argumento, saída e sucesso; agente identifica um especialista quando presente. |
tool_diagnostic | Descobertas de argumento/execução/parcial/verificação, gravidade, texto de recuperação e mutação opcionalApplied. |
todo, goal | Substitua o instantâneo atual do plano/meta em vez de anexar cópias repetidas. |
retry, compaction, compacted | Explique o trabalho contínuo preservando o texto atual da conversa e a identidade da sessão. |
heartbeat | Nenhum conteúdo visível; o servidor envia um a cada cinco segundos enquanto o fluxo de origem permanece ativo. |
Correlacionar chamadas de ferramenta e resultados por callId, não a posição da matriz. Uma ferramenta com falha pode retornar diagnósticos úteis enquanto o assistente continua. Um erro do provedor após uma ferramenta de edição bem-sucedida não reverte essa alteração de geometria.
Responder permissões e perguntas
A permission_request inclui sessionId, requestId, permissão e padrões. Apresentar a solicitação à pessoa e enviar a decisão selecionada para POST /api/ai/permission/reply. A ação de controle de sessão equivalente é reply_permission. O servidor verifica se a solicitação ainda está pendente na conversa ou em uma sessão filha admitida.
{
"sessionId": "inspection-demo",
"requestId": "request-id-from-the-event",
"decision": "once"
} A decisão é uma vez, sempre ou rejeitar. É uma decisão de permissão do servidor, distinta de um argumento de ação como confirmar. Um pendente question_request contém uma matriz de perguntas ordenadas. Responder através /api/ai/sessions/control com ação answer_question, o ID da solicitação e uma matriz de resposta por pergunta; rejeitá-lo com reject_question.
{
"sessionId": "inspection-demo",
"action": "answer_question",
"requestId": "question-id-from-the-event",
"answers": [["The selected option label"]]
} Toda resposta não deve ser vazia. Uma pergunta de escolha única rejeita múltiplas respostas e custom=false permite apenas rótulos listados. Preserve os rótulos exatos e a ordem das perguntas. Se o servidor disser que a solicitação não está mais pendente, atualize os controles em vez de postar repetidamente a resposta antiga.
Enfileirar, direcionar e interromper explicitamente o trabalho
Usar POST /api/ai/chat/queue para uma mensagem enviada enquanto o turno funciona. Leva sessionId, mensagem, projectId opcional e entrega. queue é o padrão e aguarda a próxima etapa segura; steer pede ao anfitrião para incorporá-lo imediatamente. UM user_message evento sinaliza que a mensagem entrou na conversa.
Usar POST /api/ai/chat/abort com a identidade da conversa quando a pessoa pressiona Parar. Isso interrompe o turno e se junta aos trabalhadores associados. Não implemente Stop apenas cancelando a busca: perder o fluxo HTTP não é a mesma operação. Uma sessão pode então aceitar outro turno sem criar uma nova identidade de projeto.
POST /api/ai/sessions/remove remove a sessão do host após cancelar e ingressar em seus filhos. Ele retorna um sinalizador removido. A remoção de uma conversa não é uma reversão das edições do projeto feitas por suas ferramentas; revise ou restaure-os por meio das APIs de projeto/histórico quando necessário.
Recuperar depois de perder o stream
Quando o leitor termina sem um evento final, trate a conclusão como desconhecida. Chamar POST /api/ai/sessions/state com o mesmo sessionId e projectId. A resposta contém ocupado e texto, com eventos, uso e tarefas opcionais de ferramentas/tarefas/diagnóstico recuperados. Reconstrua a UI a partir deste instantâneo; reproduzir eventos de ferramenta recuperados não os executa novamente.
Então solicite /api/ai/sessions/control com status de ação para inspecionar permissões/perguntas pendentes, tarefas de trabalho, meta e política de contexto. Uma curva ocupada aguardando uma pergunta sem resposta precisa de uma resposta, não de uma segunda cópia do prompt original. Uma sessão relatada como inativa com texto acumulado pode ser exibida como saída recuperada mesmo que a conexão TCP original tenha desaparecido.
Mantenha uma distinção entre falha de rede, evento de erro de provedor/configuração digitado, aborto explícito e remoção de sessão. Tente novamente a solicitação do modelo somente depois de verificar o que o primeiro turno realmente fez. Em particular, leia o desenho atual após um diagnóstico de mutação parcial ou trabalho de edição interrompido.
Dentro do servidor, uma conexão de modelo interrompida é retomada até cinco vezes com uma espera de 2 segundos que dobra para 30 segundos. Um cliente que se reconecta ao fluxo ainda deve ler o estado da sessão, porque essas retomadas podem concluir o trabalho que o cliente não viu no fluxo.
Contexto, anexos e compactação
includeContext: false desativa a aquisição de contexto armazenado; texto de contexto explícito ainda pode ser fornecido para esse turno. A montagem normal ordena requisitos permanentes, observações atuais, instantâneos de UI, memória e referências de suporte, desduplica-os e aplica um orçamento agregado de 24,000 caracteres. Blocos omitidos/truncados são relatados; este orçamento não é a janela de token do provedor.
Os anexos incluem nome, MIME opcional, dados e codificação opcional de texto/base64. São no máximo cinco por solicitação. As imagens são limitadas a 5 MiB cada, os PDFs a 10 MiB e o texto a 2,000,000 caracteres; textos curtos de até 6,000 caracteres também aparecem embutidos. O texto retido para ferramentas tem escopo de projeto na memória, portanto, reconectar é diferente de reiniciar o serviço. Arquivos não suportados são rejeitados com motivos.
O contexto da ação de controle solicita um detalhamento detalhado do token; a pesquisa de status comum evita o funcionamento do tokenizer. O Action Compact pede ao anfitrião para resumir o material da conversa mais antiga. A consolidação de memória é uma operação de host separada. Persista nas solicitações de desenho e fabricação de origem por meio de suas próprias APIs, em vez de depender de uma conversa compactada para reproduzir a geometria exata.
Separe os erros de transporte dos resultados do domínio
HTTP 401 significa que a solicitação não passou na autorização do portador. Um 400 antes do streaming geralmente significa uma mensagem vazia, identidade de sessão inacessível, combinação de projeto/desenho inválida ou corpo malformado. Depois que um fluxo é estabelecido, o manipulador pode emitir um evento de erro com configuração, provedor, abortado ou classificação interna; lidar com esse evento mesmo quando o status da resposta era 200.
Um resultado de ferramenta com ok=false é um resultado de ferramenta, não necessariamente um fluxo com falha. Mostre sua saída e diagnóstico para que o assistente ou pessoa possa se recuperar. Uma fila de trabalhadores também pode estar cheia: o agendador permite quatro tarefas ativas e dezesseis tarefas em espera. Uma leitora interrompida pode ser recuperada dentro de tentativas limitadas; o trabalho de edição é tratado como incerto, em vez de reproduzido automaticamente.
Leia os contratos executáveis em packages/api/src/groups/ai.ts e ai-events.ts; admissão e comportamento de fio em apps/sidecar/src/http/ai.ts; mapeamento de sessão em packages/ai/src/service.ts; validação de pergunta em nikcli/session-controls.ts; tempo de batimento cardíaco em heartbeat.ts. Essas fontes definem qual resultado o cliente deve apresentar, independentemente da prosa do assistente.
Referência de evento
Cada SSE data: frame é um objeto JSON com um type e um sessionId. Analise apenas quadros completos; um quadro pode chegar dividido em pedaços da rede.
| tipo | Campos-chave | Como lidar |
|---|---|---|
turn | phase: start, recovering, continuing, finish; opcional reason | Mostrar estado de progresso. recovering significa que a conexão com o modelo está sendo retomada. |
session_status | status: busy, idle, retry | Habilite ou desabilite o botão enviar e a fila. |
user_message | a mensagem do usuário armazenada | Reconcilie a UI otimista. |
delta | text | Anexe à resposta visível. |
reasoning | texto de raciocínio | Opcional; mostrar em uma área dobrável. |
step, todo, goal | planejar o progresso | Renderize a lista de tarefas ou plano. |
tool_call | callId, ferramenta, argumentos, opcional agent | Mostre qual ferramenta é executada e para qual especialista. |
tool_progress | callId, progresso | Atualize uma ferramenta de longa duração. |
tool_result | callId, resultado | Mostre o resultado; combine com callId. |
tool_diagnostic | detalhes incluindo mutationApplied | Decida se o modelo mudou. Leia-o antes de tentar novamente uma edição. |
permission_request / permission_resolved / permission_blocked | requestId; responder once, always, reject | Pergunte ao usuário, então POST /api/ai/permission/reply. |
question_request / question_resolved | dados da pergunta | Apresente a pergunta e envie a resposta. |
context_snapshot, compaction, compacted | estado de contexto | Informe ao usuário que o histórico mais antigo foi resumido. |
retry | attempt, message | Mostrar um aviso transitório; não reenvie o prompt. |
done | text, opcional usage, opcional recovered | Texto final da virada. Substitua os deltas acumulados por ele. |
error | code: provider, config, aborted, internal; message | Encerre o turno na UI com a mensagem; config geralmente significa falta de credenciais. |
heartbeat | nenhum | Mantenha-se ativo a cada 5 segundos; ignorar. |
Um cliente de streaming completo
const base = process.env.CADCAMFUN_URL! // e.g. http://127.0.0.1:53211
const token = process.env.CADCAMFUN_TOKEN!
const response = await fetch(`${base}/api/ai/chat/stream`, {
method: "POST",
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
body: JSON.stringify({ projectId: "p1", message: "Summarise this project", mode: "general" }),
})
if (!response.ok || !response.body) throw new Error(`HTTP ${response.status}`)
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader()
let buffer = "", answer = ""
for (;;) {
const { value, done } = await reader.read()
if (done) break
buffer += value
let end
while ((end = buffer.indexOf("\n\n")) >= 0) { // one SSE frame
const frame = buffer.slice(0, end); buffer = buffer.slice(end + 2)
const data = frame.split("\n").filter((l) => l.startsWith("data:"))
.map((l) => l.slice(5).trimStart()).join("\n")
if (!data) continue
const event = JSON.parse(data)
switch (event.type) {
case "delta": answer += event.text; break
case "permission_request": /* ask, then POST /api/ai/permission/reply */ break
case "done": answer = event.text; console.log(answer); break
case "error": throw new Error(`${event.code}: ${event.message}`)
}
}
} Se a conexão cair, não envie a mensagem novamente. Leia a sessão com POST /api/ai/sessions/state, porque o servidor pode ter retomado a conexão do modelo e finalizado o turno sem você. Usar /api/ai/chat/abort parar uma curva deliberadamente, e /api/ai/chat/queue para adicionar uma mensagem enquanto a sessão está ocupada.
Qual é a aparência de um stream sem um provedor
Sem nenhum provedor de IA configurado, o stream ainda abre e responde com um erro de digitação, seguido por uma pulsação. Capturado de um sidecar novo:
data: {"type":"error","sessionId":"","code":"config","message":"No AI provider is configured. Add an API key in the AI settings."}
data: {"type":"heartbeat"} - O status HTTP ainda é 200 e o tipo de conteúdo
text/event-stream: problemas de configuração são resultados de domínio, entregues como eventos. sessionIdestá vazio porque nenhuma sessão foi criada.heartbeatos frames mantêm a conexão ativa; ignore-os na renderização e não os conte como progresso.- Cada quadro termina com uma linha em branco. Analise dividindo em linhas em branco e juntando o
data:linhas de um quadro, como faz o iterador do SDK.
Teste seu cliente sem modelo
Aponte um cliente para um sidecar sem credenciais: ele exercita o caminho SSE completo e a ramificação de erro sem nenhum custo.