에이전트
wippy/agent 모듈은 에이전트를 선언적으로 정의하고 컨텍스트와 러너를 통해 실행합니다. 에이전트는 도구를 사용하고, 응답을 스트리밍하고, 작업을 위임하고, 트레이트를 적용하고, 메모리를 회상할 수 있습니다.
이 페이지는 조합 가능한 레퍼런스 코드 조각을 담은 API 입문서이며 독립 실행형 튜토리얼이 아닙니다. 기존 Wippy 프로젝트, 등록된 LLM 모델과 공급자, 설정된 공급자 자격 증명, 각 예제가 참조하는 에이전트·도구·resolver 엔트리가 있다고 가정합니다. 뒤쪽 예제는 앞선 섹션에서 만든 ctx, runner, conversation 같은 변수를 이어서 사용합니다. 완전한 실행 프로젝트는 LLM 에이전트 구축을 참고하세요.
설정
프로젝트에 모듈을 추가합니다:
wippy add wippy/agent
wippy install
에이전트 모듈은 자체적으로 wippy/llm 의존성을 선언합니다. 아직 없다면 소스에 에이전트 의존성을 추가하세요.
version: "1.0"
namespace: app
entries:
- name: dep.agent
kind: ns.dependency
component: wippy/agent
version: "*"
에이전트 정의
에이전트는 meta.type: agent.gen1을 가진 레지스트리 엔트리입니다:
entries:
- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
title: Assistant
comment: A helpful chat assistant
prompt: |
You are a helpful assistant. Be concise and direct.
Answer questions clearly.
model: gpt-4o
max_tokens: 1024
temperature: 0.7
에이전트 필드
| 필드 | 타입 | 설명 |
|---|---|---|
meta.type |
string | agent.gen1이어야 합니다 |
meta.name |
string | 에이전트 식별자 |
prompt |
string | 시스템 프롬프트 |
model |
string | 모델 이름 또는 클래스 |
max_tokens |
number | 최대 출력 토큰 수 (기본값 512) |
temperature |
number | 선택적 샘플링 온도. 기본적으로 생략되며 범위와 지원 여부는 공급자가 결정 |
thinking_effort |
number | > 0일 때만 모델로 전달됨 (공급자 정의 스케일) |
tools |
array | 도구 레지스트리 ID |
traits |
array | 트레이트 참조 |
delegates |
array | 위임 에이전트 참조 |
memory |
array | 정적 메모리 항목 (문자열) |
memory_contract |
table | 동적 메모리 설정 |
에이전트 컨텍스트
에이전트 컨텍스트는 주요 진입점입니다. 컨텍스트를 생성하고, 선택적으로 설정한 뒤, 에이전트를 로드합니다:
imports:
agent_context: wippy.agent:context
prompt: wippy.llm:prompt
local agent_context = require("agent_context")
local ctx = agent_context.new()
local runner, err = ctx:load_agent("app:assistant")
if err then
error("Failed to load agent: " .. tostring(err))
end
컨텍스트 메서드
| 메서드 | 설명 |
|---|---|
agent_context.new(options?) |
새 컨텍스트 생성 |
:add_tools(specs) |
런타임에 도구 추가 |
:add_delegates(specs) |
위임 에이전트 추가 |
:configure_delegate_tools(config) |
위임자가 도구로 노출되는 방식 설정 |
:set_memory_contract(config) |
동적 메모리 설정 |
:set_context_merger(fn) |
런타임 컨텍스트 업데이트를 병합하는 함수 제공 |
:update_context(updates) |
런타임 컨텍스트 업데이트 |
:load_agent(spec_or_id, options?) |
에이전트를 로드하고 컴파일하여 러너 반환 |
:switch_to_agent(id, options?) |
다른 에이전트로 전환, (boolean, string?) 반환 |
:switch_to_model(name) |
현재 에이전트의 모델 변경, (boolean, string?) 반환 |
:get_current_agent() |
현재 러너 가져오기 |
:get_config() |
컨텍스트 구성 요약 반환 |
컨텍스트 옵션
local ctx = agent_context.new({
context = { session_id = "abc", user_id = "u1" },
delegate_tools = { enabled = true },
enable_cache = true,
})
| 옵션 | 설명 |
|---|---|
context |
도구 및 위임자에게 전달되는 기본 런타임 컨텍스트 |
delegate_tools |
기본 위임-도구 구성 (configure_delegate_tools로 재정의 가능) |
enable_cache |
Claude 모델의 프롬프트 캐시 마커 설정. 현재 구현은 이 옵션이 false여도 항상 마커를 활성화 |
인라인 스펙으로 로드
레지스트리 엔트리 없이 에이전트를 로드합니다:
local runner, err = ctx:load_agent({
id = "inline-agent",
name = "helper",
prompt = "You are a helpful assistant.",
model = "gpt-4o",
max_tokens = 1024,
tools = { "app.tools:search" },
})
스텝 실행
러너는 단일 추론 스텝을 실행합니다. 대화가 담긴 프롬프트 빌더를 전달합니다:
local prompt = require("prompt")
local conversation = prompt.new()
conversation:add_user("What is the capital of France?")
local response, err = runner:step(conversation)
if err then
error(tostring(err))
end
print(response.result)
스텝 옵션
local self_pid, pid_err = process.pid()
if pid_err then
error("Failed to get process PID: " .. tostring(pid_err))
end
local response, err = runner:step(conversation, {
context = { session_id = "abc" },
stream_target = { reply_to = self_pid, topic = "stream" },
tool_call = "auto",
})
if err then
error("Agent step failed: " .. tostring(err))
end
| 옵션 | 타입 | 설명 |
|---|---|---|
context |
table | 에이전트 컨텍스트와 병합되는 런타임 컨텍스트 |
stream_target |
table | 스트리밍: { reply_to, topic } |
tool_call |
string | "auto", "any", "none" 또는 도구 이름 |
스텝 응답
| 필드 | 타입 | 설명 |
|---|---|---|
result |
string | 생성된 텍스트 |
tokens |
table | 토큰 사용량 |
finish_reason |
string | 중지 이유 |
tool_calls |
table? | 실행할 도구 호출 |
delegate_calls |
table? | 위임 호출 |
러너 통계
local stats = runner:get_stats()
-- stats.id, stats.name, stats.total_tokens
도구 정의
도구는 meta.type: tool을 가진 function.lua 엔트리입니다. 별도의 _index.yaml에 정의합니다:
version: "1.0"
namespace: app.tools
entries:
- 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.
source: file://calculate.lua
modules: [expr]
method: handler
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 }
도구 메타데이터
| 필드 | 타입 | 설명 |
|---|---|---|
meta.type |
string | tool이어야 합니다 |
meta.input_schema |
string/table | 도구 인자를 위한 JSON Schema |
meta.llm_alias |
string | LLM에 노출되는 이름 |
meta.llm_description |
string | LLM에 노출되는 설명 |
meta.exclusive |
boolean | true이면 동시 도구 호출을 취소합니다 |
에이전트에서 도구 참조
에이전트 정의에 도구 레지스트리 ID를 나열합니다:
- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
prompt: You are a helpful assistant with tools.
model: gpt-4o
max_tokens: 1024
tools:
- app.tools:calculate
- app.tools:search
- app.tools:* # wildcard: all tools in namespace
도구는 사용자 정의 별칭과 컨텍스트를 지정하여 참조할 수도 있습니다:
tools:
- id: app.tools:search
alias: web_search
context:
api_key: "${SEARCH_API_KEY}"
도구 실행
에이전트 스텝이 tool_calls를 반환하면, 도구를 실행하고 결과를 다시 전달합니다:
local json = require("json")
local funcs = require("funcs")
local function execute_and_continue(runner, conversation)
while true do
local response, err = runner:step(conversation)
if err then return nil, err end
local tool_calls = response.tool_calls
if not tool_calls or #tool_calls == 0 then
return response.result, nil
end
for _, tc in ipairs(tool_calls) do
local result, call_err = funcs.call(tc.registry_id, tc.arguments)
local result_str
if call_err then
result_str = json.encode({ error = tostring(call_err) })
else
result_str = json.encode(result)
end
conversation:add_function_call(tc.name, json.encode(tc.arguments), tc.id)
conversation:add_function_result(tc.name, result_str, tc.id)
end
end
end
도구 호출 필드
| 필드 | 타입 | 설명 |
|---|---|---|
id |
string | 고유 호출 식별자 |
name |
string | 도구 이름 (별칭 또는 llm_alias) |
arguments |
table | 파싱된 인자 |
registry_id |
string | funcs.call()용 전체 레지스트리 ID |
funcs.call(tc.registry_id, tc.arguments)을 사용하여 도구를 실행합니다. registry_id 필드는 레지스트리의 도구 엔트리에 직접 매핑됩니다.
에이전트 도구 접근과 관측 가능성이 어떻게 보호되는지는 보안 모델을 참조하세요.
스트리밍
stream_target을 사용하여 에이전트 응답을 실시간으로 스트리밍합니다:
local TOPIC = "agent_stream"
local function stream_step(runner, conversation)
local stream_ch, listen_err = process.listen(TOPIC)
if listen_err then
return nil, nil, listen_err
end
local function finish(text, response, err)
local ok, cleanup_err = process.unlisten(stream_ch)
if not ok then
cleanup_err = cleanup_err or "Failed to remove agent stream listener"
if err then
return text, nil, tostring(err) .. "; cleanup failed: " .. tostring(cleanup_err)
end
return text, nil, cleanup_err
end
if err then
return text, nil, err
end
return text, response, nil
end
local self_pid, pid_err = process.pid()
if pid_err then
return finish("", nil, pid_err)
end
local done_ch = channel.new(1)
coroutine.spawn(function()
local response, err = runner:step(conversation, {
stream_target = {
reply_to = self_pid,
topic = TOPIC,
},
})
done_ch:send({ response = response, err = err })
end)
local full_text = ""
local step_result = nil
local stream_done = false
local stream_err = nil
while true do
local cases = {}
if not stream_done then
table.insert(cases, stream_ch:case_receive())
end
if not step_result then
table.insert(cases, done_ch:case_receive())
end
local result = channel.select(cases)
if not result.ok then
return finish(full_text, nil, "Agent stream closed before completion")
end
if result.channel == done_ch then
step_result = result.value
if step_result.err then
return finish(full_text, nil, step_result.err)
end
if stream_done then
return finish(full_text, step_result.response, stream_err)
end
else
local chunk = result.value
if chunk.type == "chunk" then
local content = chunk.content or ""
print(content)
full_text = full_text .. content
elseif chunk.type == "error" then
stream_done = true
stream_err = chunk.error and chunk.error.message or "Agent stream failed"
elseif chunk.type == "done" then
stream_done = true
end
if stream_done and step_result then
return finish(full_text, step_result.response, stream_err)
end
end
end
end
스트림은 직접 LLM 스트리밍과 동일한 청크 타입을 사용합니다: "chunk", "thinking", "tool_call", "error", "done".
coroutine.spawn을 사용하여 runner:step()을 별도의 코루틴에서 실행하면 스트림 청크를 동시에 수신할 수 있습니다. channel.select를 사용하여 스트림 채널과 완료 채널을 멀티플렉싱합니다.
위임
에이전트는 다른 에이전트에게 위임할 수 있습니다. 위임 에이전트는 부모 에이전트에게 도구로 표시됩니다:
- name: coordinator
kind: registry.entry
meta:
type: agent.gen1
name: coordinator
prompt: Route questions to the right specialist.
model: gpt-4o
max_tokens: 1024
delegates:
- id: app:code_agent
name: ask_coder
rule: for programming questions
- id: app:math_agent
name: ask_mathematician
rule: for math problems
위임 호출은 response.delegate_calls에 나타납니다:
local response, err = runner:step(conversation)
if err then
error("Delegate step failed: " .. tostring(err))
end
if response.delegate_calls then
for _, dc in ipairs(response.delegate_calls) do
-- dc.agent_id - target agent registry ID
-- dc.name - delegate tool name
-- dc.arguments - forwarded message
end
end
위임 에이전트는 런타임에도 추가할 수 있습니다:
ctx:add_delegates({
{ id = "app:specialist", name = "ask_specialist", rule = "for domain questions" },
})
트레이트
트레이트는 에이전트에 프롬프트, 도구, 동작을 제공하는 재사용 가능한 정의입니다:
- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
prompt: You are a helpful assistant.
model: gpt-4o
traits:
- time_aware
- id: custom_trait
context:
key: value
내장 트레이트
| 트레이트 | 설명 |
|---|---|
time_aware |
현재 날짜와 시간을 프롬프트에 주입합니다 |
time_aware 트레이트는 컨텍스트 옵션을 받습니다:
traits:
- id: time_aware
context:
timezone: America/New_York
time_interval: 15
사용자 정의 트레이트
트레이트는 meta.type: agent.trait을 가진 레지스트리 엔트리입니다. 다음을 제공할 수 있습니다:
- prompt - 시스템 프롬프트에 추가되는 정적 텍스트
- build_func_id - 컴파일 시 도구, 프롬프트, 위임을 제공하기 위해 호출되는 함수
- prompt_func_id - 각 스텝에서 동적 콘텐츠를 주입하기 위해 호출되는 함수
- step_func_id - 각 스텝에서 부수 효과를 위해 호출되는 함수
메모리
정적 메모리
시스템 프롬프트에 추가되는 간단한 메모리 항목입니다:
- name: assistant
kind: registry.entry
meta:
type: agent.gen1
name: assistant
prompt: You are a helpful assistant.
model: gpt-4o
memory:
- "User prefers concise answers"
- "Always cite sources when possible"
동적 메모리 컨트랙트
외부 소스에서 동적 메모리 회상을 설정합니다:
memory_contract:
implementation_id: app:memory_store
context:
user_id: "${user_id}"
options:
max_items: 3
max_length: 1000
recall_cooldown: 1
min_conversation_length: 2
메모리 컨트랙트는 runner:step() 실행 중에 대화 컨텍스트를 기반으로 관련 항목을 회상하기 위해 호출됩니다. 결과는 개발자 메시지로 주입됩니다.
| 옵션 | 기본값 | 설명 |
|---|---|---|
max_items |
3 |
회상당 최대 메모리 항목 수 |
max_length |
1000 |
최대 총 문자 수 |
recall_cooldown |
1 |
회상 사이의 최소 스텝 수 |
min_conversation_length |
2 |
첫 회상 전 최소 대화 턴 수 |
리졸버 컨트랙트
load_agent()가 문자열 식별자를 받으면, 먼저 wippy.agent:resolver 컨트랙트를 통해 해석을 시도합니다. 리졸버가 바인딩되지 않았거나 리졸버가 nil을 반환하면, 레지스트리 조회로 대체됩니다.
이를 통해 애플리케이션은 데이터베이스에서 에이전트 정의를 로드하는 등의 사용자 정의 에이전트 해석을 구현할 수 있습니다.
리졸버 바인딩
리졸버 함수를 정의하고 컨트랙트에 바인딩합니다:
entries:
- name: agent_resolver.resolve
kind: function.lua
source: file://agent_resolver.lua
method: resolve
modules:
- logger
imports:
agent_registry: wippy.agent.discovery:registry
- name: agent_resolver_binding
kind: contract.binding
contracts:
- contract: wippy.agent:resolver
default: true
methods:
resolve: app:agent_resolver.resolve
리졸버 구현
리졸버는 { agent_id = "..." }를 받고 에이전트 스펙 테이블 또는 nil을 반환합니다:
local agent_registry = require("agent_registry")
local CUSTOM_PREFIX = "custom:"
function resolve(args)
local agent_id = args.agent_id
if not agent_id then
return nil, "agent_id is required"
end
if agent_id:sub(1, #CUSTOM_PREFIX) == CUSTOM_PREFIX then
local id = agent_id:sub(#CUSTOM_PREFIX + 1)
-- load from database, config file, or any other source
return {
id = agent_id,
name = "custom-agent",
prompt = "You are a custom agent.",
model = "class:balanced",
max_tokens = 1024,
tools = {},
}
end
-- fall back to registry
local spec, err = agent_registry.get_by_id(agent_id)
if not spec then
spec, err = agent_registry.get_by_name(agent_id)
end
return spec, err
end
return {
resolve = resolve,
}
해석 순서
wippy.agent:resolver컨트랙트 시도 (바인딩된 경우)- ID로 레지스트리 조회 시도
- 이름으로 레지스트리 조회 시도
- 찾지 못하면 오류 반환
커스텀 해석은 사용자 또는 워크스페이스 범위의 정의를 비롯해 프레임워크 레지스트리 밖에 있는 에이전트 정의를 불러올 수 있습니다.
참고 항목
- LLM — 기반 모델 인터페이스
- LLM 에이전트 만들기 — 단계별 에이전트 구축
- 프레임워크 개요 — 프레임워크 모듈 설치 및 import