Agente LLM
Construa um agente de chat no terminal passo a passo, progredindo de uma simples chamada LLM ate um agente com streaming e ferramentas.
O Que Vamos Construir
Um agente de chat no terminal que:
- Gera texto com um LLM
- Mantem conversas com multiplos turnos
- Transmite respostas em tempo real
- Usa ferramentas para acessar capacidades externas
Estrutura do Projeto
llm-agent/
├── .wippy.yaml
├── wippy.lock
└── src/
├── _index.yaml
├── ask.lua
├── chat.lua
└── tools/
├── _index.yaml
├── current_time.lua
└── calculate.lua
Fase 1: Geracao Simples
Comece com uma funcao basica que chama llm.generate() com um prompt em string.
Criar o Projeto
mkdir llm-agent && cd llm-agent
mkdir -p src
Definicoes de Entrada
Crie src/_index.yaml:
version: "1.0"
namespace: app
entries:
- name: os_env
kind: env.storage.os
- name: processes
kind: process.host
lifecycle:
auto_start: true
- name: dep.llm
kind: ns.dependency
component: wippy/llm
version: "*"
parameters:
- name: env_storage
value: app:os_env
- name: process_host
value: app:processes
- name: ask
kind: function.lua
source: file://ask.lua
method: handler
imports:
llm: wippy.llm:llm
O modulo LLM precisa de duas entradas de infraestrutura:
env.storage.osfornece chaves de API a partir de variaveis de ambienteprocess.hostfornece o runtime de processos que o modulo LLM usa internamente
Codigo de Geracao
Crie src/ask.lua:
local llm = require("llm")
local function handler(input)
local response, err = llm.generate(input, {
model = "gpt-4.1-nano",
temperature = 0.7,
max_tokens = 512,
})
if err then
return nil, err
end
return response.result
end
return { handler = handler }
Definicao do Modelo
O modulo LLM resolve modelos a partir do registro. Adicione uma entrada de modelo ao _index.yaml:
- name: gpt-4.1-nano
kind: registry.entry
meta:
name: gpt-4.1-nano
type: llm.model
title: GPT-4.1 Nano
comment: Fast, affordable model
capabilities:
- generate
- tool_use
- structured_output
class:
- fast
priority: 100
max_tokens: 1047576
output_tokens: 32768
pricing:
input: 0.1
output: 0.4
providers:
- id: wippy.llm.openai:provider
provider_model: gpt-4.1-nano
Inicializar e Testar
wippy init
wippy run -x app:ask "What is the capital of France?"
Isso chama a funcao diretamente e imprime o resultado. A definicao do modelo diz ao modulo LLM qual provedor usar e qual nome de modelo enviar para a API.
Fase 2: Conversas
Evolua de uma unica chamada para uma conversa com multiplos turnos usando o construtor de prompt. Altere a entrada de uma funcao para um processo com I/O de terminal.
Atualizar Definicoes de Entrada
Substitua a entrada ask por um processo chat e adicione a dependencia do terminal:
- name: dep.terminal
kind: ns.dependency
component: wippy/terminal
version: "*"
- name: chat
kind: process.lua
meta:
command:
name: chat
short: Start a terminal chat
source: file://chat.lua
method: main
modules:
- io
- process
imports:
llm: wippy.llm:llm
prompt: wippy.llm:prompt
Processo de Chat
Crie src/chat.lua:
local io = require("io")
local llm = require("llm")
local prompt = require("prompt")
local function main()
io.print("Chat (type 'quit' to exit)")
io.print("")
local conversation = prompt.new()
conversation:add_system("You are a helpful assistant. Be concise and direct.")
while true do
io.write("> ")
io.flush()
local input = io.readline()
if not input or input == "quit" or input == "exit" then break end
if input == "" then goto continue end
conversation:add_user(input)
local response, err = llm.generate(conversation, {
model = "gpt-4.1-nano",
temperature = 0.7,
max_tokens = 1024,
})
if err then
io.print("Error: " .. tostring(err))
goto continue
end
io.print(response.result)
io.print("")
conversation:add_assistant(response.result)
::continue::
end
io.print("Bye!")
end
return { main = main }
Executar
wippy update
wippy run chat
O construtor de prompt mantem o historico completo da conversa. Cada turno adiciona a mensagem do usuario e a resposta do assistente, fornecendo ao modelo o contexto das trocas anteriores.
Fase 3: Framework de Agentes
O modulo de agentes fornece uma abstracao de nivel mais alto sobre chamadas LLM brutas. Agentes sao definidos declarativamente com um prompt, modelo e ferramentas, e depois carregados e executados atraves de um padrao de contexto/runner.
Adicionar Dependencia do Agente
Adicione ao _index.yaml:
- name: dep.agent
kind: ns.dependency
component: wippy/agent
version: "*"
parameters:
- name: process_host
value: app:processes
Definir um Agente
Adicione uma entrada de agente:
- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
title: Assistant
comment: Terminal chat agent
prompt: |
You are a helpful terminal assistant. Be concise and direct.
Answer questions clearly. If you don't know something, say so.
Do not use emoji in responses.
model: gpt-4.1-nano
max_tokens: 1024
temperature: 0.7
Atualizar o Processo de Chat
Mude para o framework de agentes. Atualize as importacoes da entrada:
- name: chat
kind: process.lua
meta:
command:
name: chat
short: Start a terminal chat
source: file://chat.lua
method: main
modules:
- io
- process
imports:
prompt: wippy.llm:prompt
agent_context: wippy.agent:context
Atualize src/chat.lua:
local io = require("io")
local prompt = require("prompt")
local agent_context = require("agent_context")
local function main()
io.print("Chat (type 'quit' to exit)")
io.print("")
local ctx = agent_context.new()
local runner, err = ctx:load_agent("app:assistant")
if err then
io.print("Failed to load agent: " .. tostring(err))
return
end
local conversation = prompt.new()
while true do
io.write("> ")
io.flush()
local input = io.readline()
if not input or input == "quit" or input == "exit" then break end
if input == "" then goto continue end
conversation:add_user(input)
local response, gen_err = runner:step(conversation)
if gen_err then
io.print("Error: " .. tostring(gen_err))
goto continue
end
io.print(response.result)
io.print("")
conversation:add_assistant(response.result)
::continue::
end
io.print("Bye!")
end
return { main = main }
O framework de agentes separa a definicao do agente (prompt, modelo, parametros) da logica de execucao. O mesmo agente pode ser carregado com diferentes contextos, ferramentas e modelos em tempo de execucao.
Fase 4: Streaming
Transmita respostas token por token em vez de esperar pela resposta completa.
Atualizar Modulos
Adicione channel aos modulos do processo:
modules:
- io
- process
- channel
Implementacao de Streaming
Atualize src/chat.lua:
local io = require("io")
local prompt = require("prompt")
local agent_context = require("agent_context")
local STREAM_TOPIC = "stream"
local function stream_response(runner, conversation, stream_ch)
local done_ch = channel.new(1)
coroutine.spawn(function()
local response, err = runner:step(conversation, {
stream_target = {
reply_to = process.pid(),
topic = STREAM_TOPIC,
},
})
done_ch:send({ response = response, err = err })
end)
local full_text = ""
while true do
local result = channel.select({
stream_ch:case_receive(),
done_ch:case_receive(),
})
if not result.ok then break end
if result.channel == done_ch then
local r = result.value
return full_text, r.response, r.err
end
local chunk = result.value
if chunk.type == "chunk" then
io.write(chunk.content or "")
full_text = full_text .. (chunk.content or "")
elseif chunk.type == "done" then
local r, ok = done_ch:receive()
if ok and r then
return full_text, r.response, r.err
end
return full_text, nil, nil
elseif chunk.type == "error" then
return nil, nil, chunk.error and chunk.error.message or "stream error"
end
end
return full_text, nil, nil
end
local function main()
io.print("Chat (type 'quit' to exit)")
io.print("")
local ctx = agent_context.new()
local runner, err = ctx:load_agent("app:assistant")
if err then
io.print("Failed to load agent: " .. tostring(err))
return
end
local conversation = prompt.new()
local stream_ch = process.listen(STREAM_TOPIC)
while true do
io.write("> ")
io.flush()
local input = io.readline()
if not input or input == "quit" or input == "exit" then break end
if input == "" then goto continue end
conversation:add_user(input)
local text, _, gen_err = stream_response(runner, conversation, stream_ch)
if gen_err then
io.print("Error: " .. tostring(gen_err))
goto continue
end
io.print("")
if text and text ~= "" then
conversation:add_assistant(text)
end
::continue::
end
process.unlisten(stream_ch)
io.print("Bye!")
end
return { main = main }
Padroes principais:
coroutine.spawnexecutarunner:step()em uma coroutine separada para que a coroutine principal possa processar chunks do streamchannel.selectmultiplexa o canal de stream e o canal de conclusao- Um unico
process.listen()e criado uma vez e reutilizado entre os turnos - O texto e acumulado para adicionar ao historico da conversa
Fase 5: Ferramentas
Forneca ao agente ferramentas que ele pode chamar para acessar capacidades externas.
Definir Ferramentas
Crie src/tools/_index.yaml:
version: "1.0"
namespace: app.tools
entries:
- name: current_time
kind: function.lua
meta:
type: tool
title: Current Time
input_schema: |
{ "type": "object", "properties": {}, "additionalProperties": false }
llm_alias: get_current_time
llm_description: Get the current date and time in UTC.
source: file://current_time.lua
modules: [time]
method: handler
- name: calculate
kind: function.lua
meta:
type: tool
title: Calculate
input_schema: |
{
"type": "object",
"properties": {
"expression": {
"type": "string",
"description": "Math expression to evaluate"
}
},
"required": ["expression"],
"additionalProperties": false
}
llm_alias: calculate
llm_description: Evaluate a mathematical expression and return the result.
source: file://calculate.lua
modules: [expr]
method: handler
Os metadados da ferramenta informam ao LLM o que a ferramenta faz:
input_schemae um JSON Schema definindo os argumentosllm_aliase o nome da funcao que o LLM vellm_descriptionexplica quando usar a ferramenta
Implementar Ferramentas
Crie src/tools/current_time.lua:
local time = require("time")
local function handler()
local now = time.now()
return {
utc = now:format("2006-01-02T15:04:05Z"),
unix = now:unix(),
}
end
return { handler = handler }
Crie src/tools/calculate.lua:
local expr = require("expr")
local function handler(args)
local result, err = expr.eval(args.expression)
if err then
return { error = tostring(err) }
end
return { result = result }
end
return { handler = handler }
Registrar Ferramentas no Agente
Atualize a entrada do agente em src/_index.yaml para referenciar as ferramentas:
- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
title: Assistant
comment: Terminal chat agent
prompt: |
You are a helpful terminal assistant. Be concise and direct.
Answer questions clearly. If you don't know something, say so.
Use tools when they help answer the question.
Do not use emoji in responses.
model: gpt-4.1-nano
max_tokens: 1024
temperature: 0.7
tools:
- app.tools:current_time
- app.tools:calculate
Adicionar Execucao de Ferramentas
Atualize os modulos do processo de chat para incluir json e funcs:
modules:
- io
- json
- process
- channel
- funcs
Atualize src/chat.lua com a execucao de ferramentas:
local io = require("io")
local json = require("json")
local funcs = require("funcs")
local prompt = require("prompt")
local agent_context = require("agent_context")
local STREAM_TOPIC = "stream"
local function stream_response(runner, conversation, stream_ch)
local done_ch = channel.new(1)
coroutine.spawn(function()
local response, err = runner:step(conversation, {
stream_target = {
reply_to = process.pid(),
topic = STREAM_TOPIC,
},
})
done_ch:send({ response = response, err = err })
end)
local full_text = ""
while true do
local result = channel.select({
stream_ch:case_receive(),
done_ch:case_receive(),
})
if not result.ok then break end
if result.channel == done_ch then
local r = result.value
return full_text, r.response, r.err
end
local chunk = result.value
if chunk.type == "chunk" then
io.write(chunk.content or "")
full_text = full_text .. (chunk.content or "")
elseif chunk.type == "done" then
local r, ok = done_ch:receive()
if ok and r then
return full_text, r.response, r.err
end
return full_text, nil, nil
elseif chunk.type == "error" then
return nil, nil, chunk.error and chunk.error.message or "stream error"
end
end
return full_text, nil, nil
end
local function execute_tools(tool_calls)
local results = {}
for _, tc in ipairs(tool_calls) do
local args = tc.arguments
if type(args) == "string" then
args = json.decode(args) or {}
end
io.write("[" .. tc.name .. "] ")
io.flush()
local result, err = funcs.call(tc.registry_id, args)
if err then
results[tc.id] = { error = tostring(err) }
io.print("error")
else
results[tc.id] = result
io.print("done")
end
end
return results
end
local function run_turn(runner, conversation, stream_ch)
while true do
local text, response, err = stream_response(runner, conversation, stream_ch)
if err then
io.print("")
return nil, err
end
if text and text ~= "" then
io.print("")
end
local tool_calls = response and response.tool_calls
if not tool_calls or #tool_calls == 0 then
return text, nil
end
if text and text ~= "" then
conversation:add_assistant(text)
end
local results = execute_tools(tool_calls)
for _, tc in ipairs(tool_calls) do
local result = results[tc.id]
local result_str = json.encode(result) or "{}"
conversation:add_function_call(tc.name, tc.arguments, tc.id)
conversation:add_function_result(tc.name, result_str, tc.id)
end
end
end
local function main()
io.print("Terminal Agent (type 'quit' to exit)")
io.print("")
local ctx = agent_context.new()
local runner, err = ctx:load_agent("app:assistant")
if err then
io.print("Failed to load agent: " .. tostring(err))
return
end
local conversation = prompt.new()
local stream_ch = process.listen(STREAM_TOPIC)
while true do
io.write("> ")
io.flush()
local input = io.readline()
if not input or input == "quit" or input == "exit" then break end
if input == "" then goto continue end
conversation:add_user(input)
local text, gen_err = run_turn(runner, conversation, stream_ch)
if gen_err then
io.print("Error: " .. tostring(gen_err))
goto continue
end
if text and text ~= "" then
conversation:add_assistant(text)
end
::continue::
end
process.unlisten(stream_ch)
io.print("Bye!")
end
return { main = main }
O loop de execucao de ferramentas:
- Chama
runner:step()com streaming - Se a resposta contem
tool_calls, executa cada ferramenta viafuncs.call() - Adiciona as chamadas de ferramentas e resultados a conversa
- Volta ao passo 1 para o agente incorporar os resultados
- Quando nao ha mais chamadas de ferramentas, retorna o texto final
Executar o Agente
wippy update
wippy run chat
Terminal Agent (type 'quit' to exit)
> what time is it?
[get_current_time] done
The current time is 17:20 UTC on February 12, 2026.
> what is 125 * 16?
[calculate] done
125 * 16 = 2000.
> quit
Bye!
Proximos Passos
- Modulo LLM - Referencia completa da API do LLM
- Modulo de Agentes - Referencia do framework de agentes
- Aplicacoes CLI - Padroes de I/O de terminal
- Processos - Modelo de processos e comunicacao