Pi Agent 통합 구현: 메시지 파싱, 재시도 및 취소
Pi Agent 통합 구현: 메시지 파싱, 재시도 및 취소
CLI 형태의 AI agent를 연결하려면 세 가지를 피할 수 없습니다: 그 전용 이벤트 스트림을 안정적인 메시지로 변환하는 방법, 실패 후 누가 재시도를 담당하는지, 사용자가 취소를 클릭할 때 프로세스를 어떻게 깔끔하게 중지하는지. 사실 이 세 가지는 겉으로 보기엔 “역할 분리”일 뿐이지만, 직접 해보면 그 물이 얼마나 깊은지 알게 됩니다.
배경
최근 AI 코드 어시스턴트 프로젝트를 진행하면서 연결해야 하는 agent 중 하나는 pi입니다. 그 자체로 TUI/CLI 형태의 coding agent로, 실행 시 stdout에서 줄별로 JSON 이벤트를 출력합니다. 들어서는 간단해 보입니다—프로세스 실행, 출력 읽기, 파싱—하지만 실제로 시작하면 “agent CLI 연결”과 “일반 CLI 연결”이 완전히 다른 일이라는 것을 알게 됩니다.
일반 CLI는 stdout을 읽고 종료 코드를 받으면 그만입니다. 하지만 agent CLI에는 세 가지 골치 아픈 특징이 있습니다:
첫째, 그 이벤트 스트림은 전용 프로토콜입니다. turn_start, session, message_update, message_end, turn_end, agent_end—이것들은 pi가 자체적으로 정의한 것이지 업계 표준이 아닙니다. 이를 소비하려는 모든 상위 계층이 각자 처리해야 하므로 pi의 내부 세부사항이 곳곳으로 누출됩니다. 마치 거리에서 사람을 보는 것과 같습니다. 잘 본 것 같지만 사실은 그가 당신에게 보여주고 싶은 면만 본 것입니다.
둘째, 실패 의미가 특히 애매합니다. agent 실행 중에 네트워크가 흔들리거나 모델이 제한되거나 프로세스가 충돌할 수 있습니다. 이때 재시도해야 할까요? 어디서 재시도할까요? 재시도가 이미 반쯤 출력된 세션 상태를 망가뜨릴 수 있나요? 이것은 아키텍처 결정이지 그냥 for 루프를 작성해서 해결할 수 있는 것이 아닙니다.
셋째, 길고 중단 가능합니다. 하나의 turn은 몇십 초 또는 몇 분 동안 실행될 수 있으며, 그 사이에 사용자가 언제든 취소하고 싶어할 수 있습니다. 취소할 때 프로세스가 고아가 되어서는 안 되고, 도구 호출이 미완성 상태로 남아서도 안 되며, 이미 출력된 내용을 잃어서도 안 됩니다. 이 물은 상상보다 훨씬 깊습니다.
이러한 문제점을 해결하기 위해 우리는 연결 경로를 정리하는 데 시간을 투자했습니다. 나중에 구체적으로 설명하겠지만, 여기서 먼저 스포일러를 하자면: 진짜 어려운 점은 “프로세스 실행”이 아니라 “역할 분리”에 있습니다.
HagiCode에 대하여
이 문서에서 공유하는 솔루션은 HagiCode 프로젝트에서 옵니다—다중 모델, 다중 agent CLI 백엔드를 지원하는 AI 코드 어시스턴트. GitHub 저장소: HagiCode-org/site, 별점을 주시면 감사하겠습니다. 아래에서 논의하는 모든 코드, 모든 시행착오는 이 프로젝트에서 실제로 실행되고 있는 것입니다. 사실 적어두는 것은 자신을 위한 추억일 뿐입니다.
전체 계층 구조
HagiCode는 AI 능력 통합을 두 계층으로 분리합니다:
- 하위 계층은
Hagicode.Libs로, 재사용 가능한 provider 프리미티브ICliProvider<TOptions>를 제공하고 “CLI agent 실행 및 출력을 공유 메시지 스트림으로 정규화”를 담당합니다. - 상위 계층은
hagicode-core로, 프로젝트 수준의 얇은 어댑터IAIProvider를 제공하고 “비즈니스 요청을 provider 매개변수로 변환, 공유 메시지 스트림 소비, 통합 스트리밍 청크 노출”을 담당합니다.
Pi 통합은 이 경로를 따릅니다. 하위 계층 PiProvider가 pi 프로세스를 실행하고 JSON 이벤트 스트림을 읽고 공유 메시지로 정규화하며, 상위 계층 PiCliProvider가 AIRequest를 PiOptions로 변환하고 CliMessage를 소비하며 AIStreamingChunk를 외부로 출력합니다.
이 세 가지 작업—메시지 파싱, 재시도, 취소—는 각각 세 개의 다른 위치에 있습니다: PiJsonEventMapper, 약간 이상해 보이는 보관 제안, 그리고 CliProcessManager. 아래에서 하나씩 설명하겠습니다.
메시지 파싱: Pi 전용 이벤트를 공유 메시지로 변환
pi는 --mode json --print 모드에서 줄별로 JSON 이벤트를 출력합니다. 이 이벤트 세트는 pi 전용이므로 상위 계층으로 직접 노출해서는 안 됩니다. 그렇지 않으면 모든 소비자가 pi의 내부 세부사항에 결합되고, pi가 이벤트 구조를 업그레이드하면 전체 프로젝트를 수정해야 합니다. 사실 이러한 누출은 마음을 얼굴에 쓰는 것과 다르지 않습니다—다른 사람은 지치고 자신도 편하지 않습니다.
우리는 PiJsonEventMapper를 사용하여 pi의 이벤트를 공유 CliMessage로 정규화하는 변환 계층을 만들었습니다. CliMessage는 HagiCode.Libs.Core/Transport/CliMessage.cs에 정의되어 있으며, 구조는 매우 간단한 (Type, Content) record입니다. 매핑 관계는 대략 다음과 같습니다:
| pi 이벤트 | 공유 메시지 | 용도 |
|---|---|---|
session | session.started / session.resumed | 세션 라이프사이클 |
message_update (text 유형) | assistant | 스트리밍 본문 증분 |
message_update (thinking 유형) | assistant.thought | 사고 체인 |
message_update (tool 유형) | tool.call / tool.update | 도구 호출 시작 |
message_end / turn_end (toolResult) | tool.completed / tool.failed | 도구 결과 |
turn_end / agent_end | terminal.completed | 현재 라운드 종료 |
| 0이 아닌 종료 / 파싱 실패 | terminal.failed | 최종 실패 |
이 표는 빠른 참조용이며, 안에는 두 가지 핵심 기술이 있습니다. 이것들은 시행착오를 겪은 후에摸索된 것이므로 자세히 설명할 가치가 있습니다.
기술 1: cumulative snapshot을 delta로 변환
이것이 가장 쉽게 문제가 발생하는 지점입니다. pi의 message_update 이벤트는 증분이 아니라 누적 전체 텍스트를 전송합니다—각 token이 올 때마다 “지금까지의 전체 텍스트”를 다시 보냅니다.
받은 내용을 그대로 프론트엔드로 전달하면 사용자가 내용이 반복되는 것을 보게 됩니다: 첫 번째는 “你”, 두 번째는 “你好”, 세 번째는 “你好,”, 네 번째는 “你好,世”… 프론트엔드는 이것을 네 개의 독립적인 출력으로 생각할 것입니다. 사실 반복 같은 것은 한 번 보면 신선하지만 열 번 보면 지루함일 뿐입니다.
해결 방법은 접두사 비교를 사용하여 진짜 증분을 계산하는 것입니다:
// 핵심: pi는 증분이 아니라 누적 스냅샷을 보냅니다// 접두사 비교를 사용하여 증분을 추출하지 않으면 프론트엔드가 반복 내용을 보게 됩니다if (text.StartsWith(_lastAssistantTextSnapshot, StringComparison.Ordinal)){ var delta = text[_lastAssistantTextSnapshot.Length..]; _lastAssistantTextSnapshot = text; return delta.Length == 0 ? null : delta;}여기에는 숨겨진 함정이 있습니다: 跨 turn 접두사 재생. pi는 도구 호출이 끝나고 assistant가 다시 말하기 시작할 때 이전 텍스트를 다시 처음부터 보냅니다. 전역 스냅샷만 기록하면 재생된 내용을 증분으로 간주하여 도구 호출 후에 반복이 나타납니다. PiProviderTests에는 ExecuteAsync_deduplicates_replayed_assistant_prefix_after_tool_turns라는 테스트 케이스가 있어 이 시나리오를 다룹니다. 즉, 도구 호출 전후의 스냅샷을 정렬하여 처리해야 하며 각자 처리해서는 안 됩니다.
기술 2: thinking을 turn 종료까지 버퍼링했다가 전송
사고 체인(thinking)은 각 token을 받을 때마다 외부로 내보내면 안 됩니다. pi는 도구 호출 중간에 생각 조각을 끼워 넣습니다. 실시간으로 전달하면 스트림 순서가 엉망이 됩니다—한 번은 assistant 본문, 한 번은 생각 조각, 한 번은 tool.call. 의미가 있나요? 사실 의미가 없으며 혼란을 증가시킬 뿐입니다.
우리의 접근 방식은: thinking 이벤트를 받을 때 BufferThinkingSnapshot에 일시적으로 저장하고, message_end 또는 turn_end이고 stopReason != "toolUse"일 때 통합 DrainBufferedThinkingMessages를 실행합니다. 이렇게 하면 도구 호출 중간의 생각 조각이 메인 스트림을 오염시키지 않고 turn 종료 시 완전한 생각 과정을 한 번에 제공합니다.
오류 처리: 잘못된 행이 스트림을 중단시켜서는 안 됩니다
agent CLI는 교과서의 이상적인 시스템이 아닙니다. 때로는 비 JSON 행이나 type 필드가 없는 JSON을 출력할 수 있습니다. 여기서 예외를 던지면 전체 스트림이 죽고 사용자는 아무것도 볼 수 없습니다. 현실 세계에는 항상 불완전한 것이 있으니 매 행이 규칙적이라고 누가 보장할 수 있나요?
우리의 전략은: 모든 행 파싱 실패는 스트림을 중단하지 않고 _invalidOutputLines에 수집합니다. 프로세스 종료 후 Complete()에서 이 “나쁜 행”들을 terminal.failed 진단 텍스트에 합칩니다. 이렇게 하면 사용자가 오류를 볼 때 pi가 정확히 어떤 엉망인 것을 출력했는지 직접 볼 수 있으며 건조한 “parse error”가 아닙니다.
재시도: provider 계층에서 하지 않으면 누가 하나요?
이것은 전체 통합에서 가장 쉽게 빠지는 함정입니다. 직관적으로 “CLI 통합은 재시도를 포함해야 한다”고 생각하지만 HagiCode는 보관 제안에서 provider 계층의 모든 자동 재시도를 능동적으로 제거했습니다. 제안 이름은 remove-provider-auto-retry-support입니다.
왜 자동 재시도를 하지 않나요
제안 배경은 매우 직설적입니다. 재시도 로직은 원래 두 곳에 흩어져 있었습니다: Hagicode.Libs에 하나(OpenCode 스타일의 fresh-runtime replay), hagicode-core에 또 하나(ProviderErrorAutoRetryCoordinator). 양쪽이 각자 하여 “재시도 여부”가 provider 내부에 숨겨진 암시적 동작이 되어 실패 타이밍, 세션 재실행 방식, 채팅 상태 스트림을 몰래 변경합니다.
생각해 보면 머리가 아픕니다: 사용자가 메시지를 보내면 provider 내부에서 스스로 세 번 재시도하고, 처음 두 번은 실패하고 세 번째는 성공합니다. 상위 계층은 중간에 무슨 일이 일어났는지 전혀 모르며 세션 상태, 토큰 수, UI 진행률이 모두 맞지 않습니다. 이러한 암시적 동작은 아키텍처의 만성적인 독약입니다.
따라서 경계는 한 문장으로 수렴되었습니다:
provider는 단일 시도 의미로 수렴하고, 호출자는 재시도 없는 상태를 정상 단일 실행 결과로 간주해야 합니다.
PiProvider에 적용하면 어떻게 되나요
코드로 적용하면 세 가지입니다:
PiOptions에는 재시도 관련 필드가 전혀 없습니다—maxAttempts,retryDelay,retryClassifier가 없습니다.ExecuteAsync는 pi 프로세스가 한 번 실행되면 종료되며 실패 시 직접terminal.failed를 줍니다.- 이전에 자동 재시도를 위해 사용된 분류기(
ClaudeCodeRetryableTerminalFailureClassifier,CodexRetryableTerminalFailureClassifier등)는 순수하게 자동 재시도를 위한 것이라면 활성 경로에서 모두 제거됩니다.
하지만 참고하세요, 재시도 능력은 사라진 것이 아니라 상위로 이동했습니다. 제안에는 “더 높은 계층에서 재시도를 통일하여接管할 수 있도록 안정적인 경계를 남겨둡니다”라고 명시되어 있습니다. 구성 항목 providerErrorAutoRetry의 DTO, 정규화, 직렬화, 프론트엔드 설정 페이지 round-trip은 모두 보존되지만 provider 실행을 더 이상 구동하지 않습니다. 사실 어떤 것은 진짜 필요 없는 것이 아니라 다른 방식으로 보존되는 것일 뿐입니다.
그럼 재시도는 어떻게 하나요
pi 위에 재시도를 추가하려면 올바른 방법은 PiCliProvider의 호출자에서 하는 것입니다—예를 들어 세션 오케스트레이션 계층(HagiCode에서는 Orleans의 SessionGrain, 프론트엔드는 채팅 오케스트레이션 계층일 수 있음). terminal.failed를 받은 후 직접 재시도 가능 여부를 판단하고 지연과 횟수를 결정한 후 다시 ExecuteAsync를 보냅니다.
최소 사용 가능 패턴은 다음과 같습니다:
// 재시도 로직은 호출자에 두고 PiProvider에 다시 넣지 마세요// 그렇지 않으면 방금 확립된 provider의 "단일 시도" 경계를 깨뜨립니다async Task<AIResponse> ExecuteWithRetryAsync(AIRequest req, int maxAttempts, CancellationToken ct){ for (var attempt = 1; ; attempt++) { var response = await provider.ExecuteAsync(req, ct);
// 성공하거나 상한에 도달하면 반환 if (response.FinishReason != FinishReason.Unknown || attempt >= maxAttempts) return response;
// 재시도 가능한 최종 실패(네트워크, 5xx, 프로세스 충돌)만 재시도 // model rejected, auth failure 같은 경우 재시도는 무의미하므로 재시도하지 마세요 await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, attempt)), ct); }}“재시도 가능”을 판단하는 분류 로직은 이제 provider 내부에 없으며 호출자가 직접 정의합니다. providerErrorAutoRetry 구성(maxAttempts, retryDelay, enabled)은 여전히 프론트엔드 설정 페이지에서 읽을 수 있지만 진짜 재시도를 구동하는 것은 오케스트레이션 계층이지 PiProvider가 아닙니다. 이 점을 세 번 반복해서 외우세요.
취소: token 전달 + 3단계 중지
취소 작업은 PiProvider 자체가 거의 구현하지 않고 모두 CliProcessManager에 위임하며, PiProvider는 두 가지만 담당합니다: CancellationToken을 전달하고 예외 발생 시 뒷정리를 합니다.
전체 체인 전달
체인은 다음과 같이 끝까지 전달됩니다:
호출자 CancellationToken → PiCliProvider.StreamCoreAsync(cancellationToken) → PiProvider.ExecuteProcessAsync([EnumeratorCancellation] cancellationToken) → ReadLineAsync(cancellationToken) / WaitForExitAsync(cancellationToken) → 예외 시 _processManager.StopAsync(handle, CancellationToken.None)마지막 줄에 주의하세요: 정리할 때 사용하는 것은 CancellationToken.None이지 사용자가 전달한 token이 아닙니다. 이것은 세부사항이지만 매우 중요합니다.
이유는: 사용자의 token은 이미 취소되었습니다. 이미 취소된 token으로 정리하면 정리 작업이 즉시 취소되고 프로세스가 고아가 됩니다—pi가 백그라운드에서 계속 실행되고 아무도 수거하지 않으며 CPU와 메모리를 낭비합니다. 따라서 정리는 반드시 CancellationToken.None을 사용하여 정리 작업이 확실히 완료되도록 해야 합니다. 사실 사람과 같습니다. 어떤 일은 완전히 멈춘 후에 뒷정리를 해야 하지 그렇지 않으면 뒤가 엉망이 됩니다.
3단계 점진적 중지
CliProcessManager.StopProcessAsync는 3단계 점진적 중지 프로세스이며, 시간 상수는 파일 상단에 정의되어 있습니다:
// 우아한 중지의 인내: 프로세스가 스스로 정리할 시간을 먼저 줍니다private static readonly TimeSpan GracefulStopTimeout = TimeSpan.FromSeconds(2);// 강제 kill 후 프로세스가 실제로 종료할 때까지 기다리는 인내private static readonly TimeSpan StopWaitTimeout = TimeSpan.FromSeconds(5);세 단계는 다음과 같이 점진적입니다:
- 중단 신호.
TryInterruptAsync는 먼저 stdin에\u0003(Ctrl+C 문자)을 쓰고, Unix에서는 추가로kill -INT <pid>를 실행합니다. 이 단계는 pi가 스스로 우아하게 정리할 수 있게 합니다—중단을 감지하고 쓰고 있는 것을 마무리할 수 있습니다. - 우아한 대기. 최대 2초까지 기다려 프로세스가 스스로 종료하는지 확인합니다.
- 강제 kill. 아직 종료하지 않으면 직접
Process.Kill(entireProcessTree: true)를 실행하여 전체 프로세스 트리를 죽이고 최대 5초까지 기다려 진짜 죽었는지 확인합니다.
왜 entireProcessTree: true를 하나요? pi가 도구를 실행할 때 자식 프로세스를 파생합니다—예를 들어 provider가 라우팅하는 로컬 모델 프로세스, 실행 중인 bash 자식 프로세스. 부모 프로세스만 죽이면 자식 프로세스는 고아가 되어 계속 실행됩니다. 전체 트리를 함께 죽여야 깔끔합니다.
Windows에는 SIGINT가 없으므로 Ctrl+C 문자에만 의존할 수 있으며 따라서 크로스 플랫폼 동작에 차이가 있을 수 있습니다. 이 점을 유의하세요.
PiProvider의 예외 뒷정리
PiProvider의 ExecuteProcessAsync는 ReadLineAsync에서 예외를 던질 때 ExceptionDispatchInfo.Capture를 사용하여 예외를 일시적으로 저장하고, 루프를 빠져 나온 후 StopAsync를 호출하여 프로세스를 정리한 다음 pendingException.Throw()로 원래 예외를 상위 계층으로 다시 던집니다.
왜 임시 저장했다가 던질까요? 직접 던지면 프로세스가 수거될 시간이 없어 고아가 되고, StopAsync 전에 던지면 정리 로직이 실행되지 않습니다. 임시 저장하여 프로세스가 반드시 수거되도록 보장한 다음 원래 OperationCanceledException 의미를 완전히 보존하여 호출자에게 전달합니다—호출자가 이 예외를 받으면 “아, 사용자가 적극적으로 취소했다”고 판단할 수 있으며 “오류가 발생했다”가 아닙니다.
시작 실패의 통합 계약
또 다른 세부사항을 언급할 가치가 있습니다. 프로세스 시작 실패—예를 들어 pi 실행 파일이 없거나 권한이 맞지 않을 때—PiProvider는 예외를 던지지 않고 terminal.failed 메시지를 합성한 다음 yield break를 실행합니다.
왜 이렇게 하나요? 예외를 던지면 상위 소비자는 두 가지 완전히 다른 의미를 처리해야 합니다: 하나는 “스트리밍 소비 과정 중의 정상 메시지”, 다른 하나는 “스트림이 시작되기도 전에 던진 예외”. 이것은 소비자의 await foreach를 매우 어렵게 만듭니다.
“항상 먼저 메시지를 주고 스트림을 종료”로 통합하면 소비자의 논리가 일관됩니다: terminal.failed를 받으면 실패, terminal.completed를 받으면 성공으로 간주하며 try/catch 분기 처리가 필요 없습니다. 작지만 중요한 설계 결정이며 계약을 안정화합니다.
실습: 스트림 소비의 올바른 방법
HagiCode의 PiScenarioMessageReader(libs의 콘솔 테스트 시나리오)와 PiCliProvider.StreamCoreAsync(core의 얇은 어댑터)를 참조하면 소비자는 대략 다음과 같습니다:
await foreach (var message in provider.ExecuteAsync(options, prompt, cancellationToken)){ // 1. 실패는 우선 단락시키고 후속 메시지를 처리하지 마세요 if (NormalizedAcpCliAdapter.TryGetFailureMessage(message.Content, out var failure)) { yield return new AIStreamingChunk { Type = StreamingChunkType.Error, ErrorMessage = failure }; yield break; // terminal.failed 후 스트림이 종료됩니다 }
// 2. assistant 텍스트는 cumulative 스냅샷이므로 직접 다시 증분 계산을 수행하세요 if (message.Type == "assistant" && TryGetText(message.Content, out var text)) { var delta = ReconcileSnapshot(text); // 접두사 비교 if (!string.IsNullOrEmpty(delta)) yield return Chunk(delta); }
// 3. terminal.completed는 유일한 신뢰할 수 있는 "종료" 신호입니다 if (message.Type == "terminal.completed") break;}일반적인 함정 빠른 참조
이 과정에서 겪은 함정을 표로 정리하여 후속 사람을 돕습니다:
| 현상 | 원인 | 처리 |
|---|---|---|
| 프론트엔드에서 assistant 텍스트 반복 표시 | cumulative를 delta로 변환하지 않음 | ReconcileAssistantTextSnapshot을 사용하여 접두사 비교 수행 |
| 취소 후 프로세스가 계속 실행 | 정리에 이미 취소된 token 사용 | 정리에 CancellationToken.None 사용 |
| 재시도가 작동하지 않음 | 재시도를 PiProvider에 작성했지만 provider는 단일 시도 의미 | 호출자 오케스트레이션 계층으로 상위 이동 |
| pi 오류 정보 손실 | terminal.failed 진단 필드를 읽지 않음 | text / invalid_output_lines / stderr 완전 전달 |
| 도구 호출 중간에 생각 조각 수신 | thinking 이벤트를 직접 전달 | turn 종료까지 버퍼링 후 DrainBufferedThinkingMessages |
검증 방법
libs 계층은 StubCliProcessManager로 프로세스를 mock하고 단위 테스트가 매개변수 구성, 이벤트 정규화, 증분 중복 제거, 실패 전달과 같은 순수 논리를 다룹니다. 실제 CLI 경로는 HAGICODE_REAL_CLI_TESTS 환경 변수로 opt-in하며 실제 모델로 trip 시나리오를 실행합니다. core 계층의 PiCliProviderTests는 얇은 어댑터의 AIStreamingChunk 투영 및 session binding을 검증합니다.
# Hagicode.Libs 저장소에서 Pi 관련 단위 테스트 실행dotnet test --filter "FullyQualifiedName~PiProviderTests"
# 실제 CLI 통합 테스트 실행(로컬에 pi 설치 필요)HAGICODE_REAL_CLI_TESTS=1 dotnet test --filter "FullyQualifiedName~PiProviderTests.RealCli"요약
이 세 가지를 연결하면 pi 통합의 멘탈 모델은 사실 한 문장입니다: 각 계층이 자신의 일만 하게 하세요.
- 메시지 파싱은
PiJsonEventMapper에 맡기세요: 전용 이벤트를 공유CliMessage로 정규화하고, cumulative 스냅샷을 delta로 변환하며, thinking을 turn 종료까지 버퍼링합니다. - 재시도는 호출자에 맡기세요: provider는 단일 시도하고, 재시도하려는 사람은 상위 계층에서 직접 하며, 구성은 보존하지만 provider를 더 이상 구동하지 않습니다.
- 취소는
CliProcessManager에 맡기세요:CancellationToken을 전체 체인으로 전달하고, 정리에는CancellationToken.None을 사용하며, 3단계 점진적 중지(중단 신호 → 우아한 대기 → 강제 kill 전체 프로세스 트리).
이러한 경계를 명확히 설정한 후 새 agent CLI 통합은 거의 파이프라인 작업이 되었습니다—새로운 XxxProvider와 XxxJsonEventMapper만 작성하면 재시도, 취소, 메시지 계약, 오류 처리와 같은 횡단 논리를 모두 재사용할 수 있습니다. 이것이 HagiCode가 여러 agent CLI 백엔드(claude code, codex, pi, gemini cli 등)를 동시에 지원하면서도 엉망이 되지 않는 근본적인 이유입니다.
마지막으로 가장 중요한 경계를 다시 말씀드리겠습니다: provider 계층에 재시도를 추가하지 마세요. 이 점을 이해하면 agent CLI 통합 작업이 절반 이상 넘어갑니다…
요약
“Pi Agent 통합 구현: 메시지 파싱, 재시도 및 취소”라는 주제로 돌아가서, 진짜 반복해서 확인해야 할 것은 산재된 기술이 아니라 제약 조건, 구현 경계 및 엔지니어링 트레이드오프가 이미 명확해졌는지입니다.
문서의 판단 근거를 안정적인 확인 항목으로 정착시키면, 나중에 유사한 문제에 직면했을 때 더 빨리 신뢰할 수 있는 결정을 내릴 수 있습니다.
开始使用 HagiCode
一次安装,几分钟上手
HagiCode for Windows 在 Microsoft Store 免费提供。打开商店即可安装并保持更新;也可以先对比各版本与定价,再决定从哪个渠道开始。