에이전트

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,
}

해석 순서

  1. wippy.agent:resolver 컨트랙트 시도 (바인딩된 경우)
  2. ID로 레지스트리 조회 시도
  3. 이름으로 레지스트리 조회 시도
  4. 찾지 못하면 오류 반환

커스텀 해석은 사용자 또는 워크스페이스 범위의 정의를 비롯해 프레임워크 레지스트리 밖에 있는 에이전트 정의를 불러올 수 있습니다.

참고 항목