도구 호출
도구 호출 (Tool Calls)
에이전트가 호출을 어떻게 제안하고, 인자를 스트리밍하며, 결과를 받는지 설명해 드릴게요 — 1.0 기준이에요. 도구 호출은 에이전트가 무언가가 되기를 요청하는 것입니다.
출처: 문서
본문
도구 호출은 에이전트가 무언가가 되기를 요청하는 것이에요. 도구가 run input에서 애플리케이션이 광고한 것일 때, 애플리케이션이 그것을 실행합니다 — 그래서 도구 호출이 프로토콜의 인간-인-더-루프 핵심이 돼요: 에이전트가 제안하고, 애플리케이션이 처분합니다.
사용자 상호작용 모델
애플리케이션은 보통 도구 호출을 스트리밍되는 대로 표면화해요 — 도구를 이름 붙이는 카드, 채워지는 인자 — 그리고 부작용이 있는 도구에 대해서는 실행 전에 사용자에게 묻습니다. 프로토콜은 어떤 특정 상호작용 모델도 강요하지 않지만, 아래 Security Considerations을 참고하세요.
이벤트
도구 호출은 toolCallId로 짝지어지는 스트리밍 패턴을 따라요.
TOOL_CALL_START
호출을 엽니다.
{
"type": "TOOL_CALL_START",
"toolCallId": "call-1",
"toolCallName": "search",
"parentMessageId": "msg-1"
}
toolCallName은 호출되는 도구를 이름 붙여요.parentMessageId는 OPTIONAL이고 호출을 그것을 담는 어시스턴트 메시지에 붙입니다. 부모 메시지가 서브에이전트에 귀속되면, 호출은 그 귀속과 일치해야 해요(MUST) — 도구 호출은 그것을 담는 메시지에 속합니다(Subagents 참고).
TOOL_CALL_ARGS
열린 호출을 확장합니다. delta는 호출 인자의 다음 조각을 담아요; 연결된 델타들이 호출의 인자 텍스트를 형성하고, 관례적으로 JSON 문서예요 — 하지만 프로토콜은 그것을 텍스트로 담고 검증하지 않으며, 그것은 의도적이에요: 프로바이더는 형식이 잘못된 인자 문자열을 내보내고, 그것으로 무엇을 할지 결정하는 애플리케이션이 전송이 실행을 죽이는 것보다 낫습니다.
컨슈머는 TOOL_CALL_END 전에 인자에 대해 행동하면 안 됩니다(MUST NOT): 호출이 닫힐 때까지 그 텍스트는 프로듀서가 보내는 것의 프리픽스이지, 그것 자체가 아니에요.
TOOL_CALL_END
호출을 닫습니다. 인자 텍스트가 완전해요 — 닫는 것은 완전함을 확립하지, 유효함을 확립하지 않습니다. 도구를 실행하는 누구든 그것을 파싱하고, 파싱되지 않는 텍스트로 무엇을 할지는 프로토콜 위반이 아니라 애플리케이션의 결정이에요.
닫힌 호출은 닫힌 메시지처럼, 같은 toolCallId를 가진 새 TOOL_CALL_START로 재열릴 수 있고(MAY), 추가 인자가 이어붙습니다. 재여는 시작은 그것이 재여는 호출과 일치해야 해요(MUST) — 같은 toolCallName, 같은 parentMessageId, 같은 주인. 컨슈머는 불일치를 감지하도록 요구되지 않고, 재열린 텍스트 메시지와 달리, 프로토콜은 위반을 놓친 컨슈머가 두 값 중 어느 것을 결국 들게 될지에 대해 어떤 약속도 하지 않아요.
TOOL_CALL_CHUNK
컴팩트한 철자예요. 컨슈머는 스트리밍 패턴이 명시하는 대로 청크를 확장해야 해요(MUST): 첫 청크는 toolCallId와 toolCallName을 담아야 하고, 충돌하는 값으로 toolCallName이나 parentMessageId를 반복하는 연속은 치명적이에요.
TOOL_CALL_RESULT
호출의 결과를 담아요. 그것은 그 자체로 메시지예요 — 자기만의 messageId를 가진 도구 메시지 — 그리고 그것이 답하는 호출을 재열지 않아요.
{
"type": "TOOL_CALL_RESULT",
"messageId": "msg-2",
"toolCallId": "call-1",
"content": "3 results found."
}
결과는 그것의 호출과 같은 실행에서 도착할 수 있어요(MAY)(에이전트 실행 도구), 또는 스트림에 전혀 도착하지 않을 수도 있어요: 클라이언트 실행 도구의 결과는 대신 다음 실행 입력의 도구 메시지로 프로듀서에 돌아옵니다.
결과 콘텐츠
content는 문자열이거나 ContentPart의 정렬된 목록이어야 해요(MUST) — 사용자 메시지가 담는 같은 파트: text, image, audio, video, document, 각 미디어 파트는 인라인 데이터, URL, 프로바이더 파일 핸들 중 하나인 source를 가집니다. 이벤트가 만드는 도구 메시지는 동일한 형태를 가지므로, 결과는 다음 실행의 messages로 바뀌지 않고 여행합니다.
{
"type": "TOOL_CALL_RESULT",
"messageId": "msg-2",
"toolCallId": "call-1",
"content": [
{ "type": "text", "text": "Invoice INV-2291 attached." },
{
"type": "document",
"source": { "type": "url", "value": "https://example.com/INV-2291.pdf", "mimeType": "application/pdf" },
"metadata": { "title": "INV-2291" }
}
]
}
- 구조화된 데이터 — JSON 객체 같은 — 를 반환하는 도구는 그것을 문자열 형태나
text파트로 직렬화합니다. 프로토콜에는 JSON 파트가 없어요: 모든 프로바이더는 도구 결과를 텍스트로 받아들이고, 타입화된 객체는 어쨌든 프로바이더 경계에서 텍스트가 되어야 하니까요. - 파트가 모델링하지 않는 무엇이든 — 검색 히트의 소스와 제목, 문서의 파일 이름 — 은 파트의
metadata에 실려요. 프로토콜은 프로바이더별 결과 블록(인용 가능한 검색 결과, 브라우저 상태)을 정의하지 않아요; 그것이 필요한 프로듀서는 그것을 이 파트들에 매핑하거나 passthrough로 실어 나릅니다. - 그 출력을 모델 프로바이더에 업로드한 도구 — 생성된 리포트가 이제 프로바이더의 파일 저장소에 놓인 — 은 바이트를 다시 보내기보다 핸들을
file소스로 반환해요; 입력에서와 같은 규칙이 적용되고, 결과를 렌더링하는 컨슈머는 핸들을 불투명하게 취급합니다. - 그것의 모델이 받을 수 없는 파트를 받은 프로듀서 — 텍스트 전용 모델에 이미지, 도구 결과에 아무것도 받지 않는 프로바이더에 오디오, 다른 프로바이더가 발급한 파일 핸들 — 는 그것 때문에 실행을 실패시켜서는 안 됩니다(MUST NOT). run input 규칙이 사용자 콘텐츠에 대해 이미 말하듯 파트를 버리고 계속하며, 여전히 호출에 답해야 해요(MUST): 모든 파트가 버려진 결과는 빈 문자열로 답하는데, 답하지 않은 채 둔 도구 호출은 대부분의 모델이 노골적으로 거부하는 것이기 때문이에요.
- 문자열만 들 수 있는 컨슈머 — 렌더러, 저장소, 레거시 브리지 — 는 파트 목록을 순서대로 연결된 그것의
text파트로 렌더링하고 나머지는 무시합니다. 그렇게 하는 것은 손실이 있고, 어떤 다운그레이드든 그러하듯 공지되어야 해요(SHOULD)(Versioning). - 콘텐츠 파트가 존재하기 전의 피어는
protocolVersion을 보내지 않아요; 그것과 대화 중임을 아는 프로듀서는 내보내기 전에 결과를 같은 방식으로 평탄화할 수 있고(MAY), 평탄화가 파트를 버렸을 때 경고해야 해요(MUST). 버려진 파트의 자리에 플레이스홀더를 넣으면 안 됩니다(MUST NOT): 다운그레이드는 형태를 바꾸지, 발명하지 않아요.
프런트엔드 도구
run input의 tools 목록은 애플리케이션의 것이에요: 에이전트가 호출을 제안하고, 애플리케이션이 실행합니다. 프로토콜에는 컨슈머로부터의 실행 중 채널이 없으므로, 답은 오직 실행 경계만 건널 수 있어요 — 그것이 왕복에 형태를 줍니다:
- 프런트엔드 도구를 호출하는 프로듀서는 그것에 답해서는 안 됩니다(MUST NOT):
TOOL_CALL_RESULT도, 조작된 도구 메시지도 없어요. 결과는 애플리케이션이 만들 몫이에요. - 프로듀서는 호출에 답하지 않은 채 실행을 끝냅니다. 그것은 성공 outcome이나 없음을 사용해야 하고(MUST), 실행을 인터럽트된 것으로 보고해서는 안 되며(MUST NOT), 결과에 의존하지 않는 것이 아무것도 남지 않으면 신속히 끝내야 해요(SHOULD). 여러 프런트엔드 호출이 한 실행에 의해 답하지 않은 채 남을 수 있고(MAY), 애플리케이션이 그것들 모두를 한 번에 답합니다.
- 성공 outcome의
pendingToolCallIds는 답하지 않은 채 둔 호출을, 만들어진 순서대로 이름 붙여요. 프로듀서는 실행이 호출을 대기시킨 채로 둘 때 그것을 보내야 해요(SHOULD); 보낼 때 그 목록은 실행이 시작하고TOOL_CALL_RESULT로 답하지 않은 도구 호출만을 정확히 담아야 합니다(MUST).RUN_FINISHED만으로는 실행이 애플리케이션을 위해 작업을 남겼는지 말하지 않아요: 목록이 없거나 비어 있을 때, 알아야 하는 컨슈머는 그것을 스트림에서 유도합니다 — 실행이 시작하고 결과를 받지 못한 모든 도구 호출 — 그리고 부재를 "대기 중인 것 없음"으로 읽어서는 안 됩니다(MUST NOT). 인터럽트된 실행에서도 대기 호출은 같은 방식으로 유도됩니다; 인터럽트 outcome은 그것들을 담지 않아요. - 실행이 끝난 후, 애플리케이션은 각 답하지 않은 프런트엔드 호출을 처분합니다 — 실행하거나, 거절하거나, 자기 규칙이 요구하는 동의와 함께. 계속되는 스레드는 그것들 각자를 먼저 답해야 해요(MUST): 다음 실행의
messages가toolCallId로 키되는 호출당 도구 메시지 하나를 담아요 — 실패도error가 설정된 도구 메시지로서 여전히 답이고, 사용자가 거절한 호출은 그렇게 말함으로써 답합니다. 도구 메시지의content는TOOL_CALL_RESULT에서와 정확히 같은 문자열이나 파트 목록이에요: 스크린샷을 만들거나 파일을 고른 프런트엔드 도구는 그것의 설명이 아니라 미디어 파트로 답합니다. 답하지 않은 호출은 에이전트를 생각 중간에 남기고, 매달린 호출이 있는 히스토리는 많은 모델이 노골적으로 거부하는 것입니다. 스레드를 버리는 것은 아무것도 답하지 않지만 아무것도 위반하지 않아요 — 그 규칙은 재개 커버리지가 그러하듯 연속을 묶습니다.
이것은 인터럽트와 다른 왕복이에요: 인터럽트는 프로듀서가 명시적으로 멈춰 묻는 것이고 재개 항목으로 답하며, 프런트엔드 도구 호출은 평범한 메시지 루프를 타고 대화 히스토리로 답합니다.
프런트엔드 도구 호출에서 멈추는 실행은 완료된 실행이지 인터럽트된 것이 아니고, 세 번째 종류의 종료도 아니에요. outcome은 실행이 왜 끝났는지에 대해 프로듀서가 아는 것을 보고하며, 여기서 프로듀서는 기다리고 있는지 모릅니다: 프런트엔드 도구는 흔히 종단 효과 — 차트 렌더링, 탐색, 하이라이트 — 이고, 그것의 결과가 다른 실행을 시작하는지는 애플리케이션의 결정이며, 그 도구에 대한 애플리케이션의 규칙으로 만들어집니다. 그래서 프로듀서는 아는 것을 말해요 — 실행은 완료됐고 이 호출들은 답하지 않았다 — 를 성공 outcome의 세부로, 필드를 떼어내는 더 오래된 컨슈머가 여전히 성공적인 실행을 읽고 대기 호출을 항상 해왔듯 유도하는 곳에서요.
프로듀서는 광고된 목록에서만 프런트엔드 도구를 호출해야 해요(SHOULD); 입력이 광고하지 않은 도구를 이름 붙이는 호출은 그 자체로 프로토콜 위반이 아니에요 — 그것으로 무엇을 할지는 컨슈머의 결정입니다. 에이전트 측에서 실행되는 프로듀서 자신의 도구는 광고가 필요 없고 TOOL_CALL_RESULT로 스트림 안에서 답합니다.
메시지 흐름
클라이언트 실행 도구는 두 실행에 걸칩니다:
sequenceDiagram
participant Agent
participant Application
Agent->>Application: TOOL_CALL_START (call-1, "confirm_order")
Agent->>Application: TOOL_CALL_ARGS (…)
Agent->>Application: TOOL_CALL_END
Agent->>Application: RUN_FINISHED
Note over Application: executes the tool<br/>(with user consent where due)
Application->>Agent: next RunAgentInput (messages include the tool result)
데이터 타입
이벤트 형태는 schema reference로 정의됩니다: ToolCallStartEvent, ToolCallArgsEvent, ToolCallEndEvent, ToolCallChunkEvent, ToolCallResultEvent. 대화 히스토리에서 호출은 AssistantMessage의 ToolCall로 나타나고, 결과는 콘텐츠가 문자열이나 ContentPart 목록인 ToolMessage로 나타납니다. 광고된 도구는 RunAgentInput의 Tool 객체입니다.
파트들은 여행 방향이 아니라 그것이 무엇인지로 이름 붙여집니다 — TextPart, ImagePart — 같은 파트가 사용자 메시지 안에서 모델로 들어가고 도구 결과 안에서 스트림 밖으로 나오기 때문이에요. 이름 ReasoningPart, ToolCallPart, AssistantPart는 어시스턴트 메시지도 파트를 담게 되는 이후 사소한 버전에 예약됩니다; 아직 아무것도 그것들을 정의하지 않고, 프로듀서는 그것들을 내보내면 안 됩니다(MUST NOT).
오류 처리
스트리밍 패턴의 순서 규칙이 바뀌지 않고 적용됩니다: 열리지 않은 호출을 계속하거나 닫거나, 열린 것을 재여는 것은 치명적이에요. 실행이 끝날 때 열린 채 남은 호출은 위반입니다.
보안 고려사항
도구 호출은 프로토콜의 가장 큰 공격 표면이에요, 모델 출력을 행동으로 바꾸기 때문이에요.
- 인자는 모델이 생성한 것이고 신뢰할 수 없는 입력으로 취급되어야 해요(MUST): 광고된 스키마가 선언된 도구의 파라미터 스키마에 대해 검증하고, 없으면 어떤 신뢰할 수 없는 페이로드처럼 심사하며, 셸 명령·쿼리·마크업에 이스케이프 없이 보간해서는 절대 안 됩니다.
- 애플리케이션은 부작용이 있는 도구 호출을 실행하기 전에 사용자 동의를 얻어야 하고(SHOULD), 승인되지 않은 호출을 사용자가 승인한 것으로 나타내서는 안 됩니다(MUST NOT).
- 도구 결과는 도구가 그것을 얻은 어디서든의 데이터예요. 컨슈머는 결과 안의 텍스트를 프로토콜 자료나 사용자의 권위를 담은 지시로 취급해서는 안 됩니다(MUST NOT).
- 광고되지 않은 도구를 이름 붙이는 호출은 새 도구가 받을 것과 같은 심사 없이 실행되어서는 안 돼요(SHOULD NOT).
더 알아보기 (Learn more)
- 스펙 개요 — AG-UI 1.0 프로토콜의 공식 요구사항을 확인해 보세요.
- 실행과 스텝 (Lifecycle) — 도구 호출이 실행 라이프사이클에 어떻게 들어맞는지 살펴보세요.
- 인터럽트와 재개 — 프런트엔드 도구와 인터럽트의 차이를 확인해 보세요.