refactor(topics): 멀티 에이전트용 지식 재편 — _Common(공통 기본기) + Domain_* 구조
에이전트 8종(대화형/프로그래머 C·S/디자이너/설계자/기획자/QA/PD/PM)에게 [공통 기본 능력 + 롤별 Specialty] 2층으로 지식을 주입하기 위한 재분류. 문서 내용·포맷은 무수정, 폴더 이동만 (6,372개 문서 수 보존 확인). - Topic_Programming → Domain_Programming (내부 구조 보존) - Topic_Graphic → Domain_Design - Topic_Business → Domain_Product - Topic_General → Domain_General - _Common 신설: Math(구 Topic_Math_Specialty), Reasoning(구 General/From_Thinking & Reasoning), Reasoning_Creativity(구 General/From_창의성), Communication(Poetic_Blog_Writing + From_writing) - 타 도메인의 From_* 폴더는 유지 (출처 표기일 뿐, 이미 도메인에 맞게 분류된 문서) - 빈 폴더 정리 (memory/procedures) - 에이전트→폴더 매핑은 workspace의 .astra/agent-knowledge-map.json (9개 에이전트) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,273 @@
|
||||
---
|
||||
id: wiki-2026-0508-autonomous-polling-wait
|
||||
title: Autonomous Polling & Wait Automation
|
||||
category: 10_Wiki/Topics
|
||||
status: verified
|
||||
canonical_id: self
|
||||
aliases: [폴링 자동화, async wait, agent loop, state polling, webhook fallback, hybrid wait]
|
||||
duplicate_of: none
|
||||
source_trust_level: B
|
||||
confidence_score: 0.85
|
||||
verification_status: applied
|
||||
tags: [agent, polling, async, automation, notebooklm, research-loop, state-machine, retry, exponential-backoff]
|
||||
raw_sources: []
|
||||
last_reinforced: 2026-05-10
|
||||
github_commit: pending
|
||||
tech_stack:
|
||||
language: TypeScript / Python
|
||||
framework: Async/Await / Promise
|
||||
---
|
||||
|
||||
# Autonomous Polling & Wait Automation
|
||||
|
||||
## 📌 한 줄 통찰
|
||||
> **"매 sleeping researcher"**. 매 long-running task (3-10 분) 의 완료 의 agent 가 자동 감지 + 매 next step 으로 transition. 매 manual button click 의 X. 매 10초 polling + 매 webhook fallback + 매 timeout 의 hybrid.
|
||||
|
||||
## 📖 핵심
|
||||
|
||||
### 매 polling pattern
|
||||
- 매 short interval (1-30 sec) 의 state check.
|
||||
- 매 max attempts / timeout.
|
||||
- 매 simple, 매 stateless.
|
||||
|
||||
### 매 vs webhook
|
||||
| 측면 | Polling | Webhook |
|
||||
|---|---|---|
|
||||
| Setup | Simple | Complex (public URL) |
|
||||
| Latency | Polling interval | Near-zero |
|
||||
| Server load | High (N polls) | Low (1 call) |
|
||||
| Reliability | Self-managed | Webhook 의 lost OK |
|
||||
| Use case | Behind firewall | Public service |
|
||||
|
||||
→ 매 hybrid 의 best.
|
||||
|
||||
### 매 polling strategies
|
||||
|
||||
#### Fixed interval
|
||||
- 매 simple.
|
||||
- 매 short job 의 OK.
|
||||
|
||||
#### Exponential backoff
|
||||
- 매 wait = base × 2^n.
|
||||
- 매 server-friendly.
|
||||
|
||||
#### Adaptive
|
||||
- 매 ETA estimate.
|
||||
- 매 progress-based.
|
||||
|
||||
#### Long polling
|
||||
- 매 server 의 hold connection.
|
||||
- 매 latency ↓.
|
||||
|
||||
### 매 long-running 의 pattern
|
||||
1. **Submit job** → 매 job_id.
|
||||
2. **Poll status** until complete.
|
||||
3. **Retrieve result** when ready.
|
||||
4. **Webhook** as fallback (optional).
|
||||
5. **Timeout + manual fallback**.
|
||||
|
||||
### 매 NotebookLM Deep Research case
|
||||
- 매 average 3-10 min.
|
||||
- 매 10 sec polling × 60 = 매 max 10 min.
|
||||
- 매 status: "queued" → "running" → "completed" / "error".
|
||||
- 매 completed → 매 result fetch.
|
||||
|
||||
### 매 design 의 challenge
|
||||
1. **Quota**: 매 too frequent → 매 API rate limit.
|
||||
2. **Stale state**: 매 status 의 update 의 lag.
|
||||
3. **Network failure**: 매 retry 의 idempotent.
|
||||
4. **Timeout**: 매 server-side retry 의 inflight.
|
||||
5. **Resource leak**: 매 polling 의 stop 보장.
|
||||
|
||||
### 매 best practice
|
||||
- **Initial delay**: 매 즉시 poll X.
|
||||
- **Exponential + cap**: 매 max interval.
|
||||
- **Jitter**: 매 thundering herd 방지.
|
||||
- **Cancellation**: 매 abort signal.
|
||||
- **Observability**: 매 attempt count log.
|
||||
- **Idempotency**: 매 result fetch 의 retry-safe.
|
||||
|
||||
## 💻 패턴
|
||||
|
||||
### Basic polling (TS)
|
||||
```ts
|
||||
async function pollUntilDone<T>(
|
||||
fetchStatus: () => Promise<{ done: boolean; result?: T }>,
|
||||
options: { intervalMs?: number; maxAttempts?: number; timeoutMs?: number } = {},
|
||||
): Promise<T> {
|
||||
const { intervalMs = 10_000, maxAttempts = 60, timeoutMs = 600_000 } = options;
|
||||
const start = Date.now();
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
if (Date.now() - start > timeoutMs) throw new Error('Timeout');
|
||||
|
||||
const status = await fetchStatus();
|
||||
if (status.done) return status.result!;
|
||||
|
||||
await new Promise(r => setTimeout(r, intervalMs));
|
||||
}
|
||||
throw new Error('Max attempts exceeded');
|
||||
}
|
||||
```
|
||||
|
||||
### Exponential backoff with jitter
|
||||
```ts
|
||||
async function pollWithBackoff<T>(
|
||||
fetchStatus: () => Promise<{ done: boolean; result?: T }>,
|
||||
options: { baseMs?: number; maxMs?: number; maxAttempts?: number } = {},
|
||||
): Promise<T> {
|
||||
const { baseMs = 1000, maxMs = 30_000, maxAttempts = 30 } = options;
|
||||
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
const status = await fetchStatus();
|
||||
if (status.done) return status.result!;
|
||||
|
||||
const delay = Math.min(maxMs, baseMs * 2 ** i);
|
||||
const jittered = delay * (0.5 + Math.random() * 0.5);
|
||||
await new Promise(r => setTimeout(r, jittered));
|
||||
}
|
||||
throw new Error('Max attempts');
|
||||
}
|
||||
```
|
||||
|
||||
### Hybrid (poll + webhook)
|
||||
```ts
|
||||
async function awaitJobHybrid(jobId: string, webhookUrl?: string): Promise<Result> {
|
||||
// 매 webhook 의 우선 setup
|
||||
const webhookPromise = webhookUrl
|
||||
? listenForWebhook(jobId, webhookUrl, { timeoutMs: 600_000 })
|
||||
: null;
|
||||
|
||||
// 매 polling 의 fallback
|
||||
const pollingPromise = pollUntilDone(
|
||||
() => api.getJobStatus(jobId),
|
||||
{ intervalMs: 10_000, timeoutMs: 600_000 },
|
||||
);
|
||||
|
||||
// 매 둘 다 race
|
||||
return Promise.race([webhookPromise, pollingPromise].filter(Boolean));
|
||||
}
|
||||
```
|
||||
|
||||
### Cancellation (AbortController)
|
||||
```ts
|
||||
async function pollCancellable<T>(
|
||||
fetchStatus: (signal: AbortSignal) => Promise<{ done: boolean; result?: T }>,
|
||||
signal: AbortSignal,
|
||||
): Promise<T> {
|
||||
while (!signal.aborted) {
|
||||
const status = await fetchStatus(signal);
|
||||
if (status.done) return status.result!;
|
||||
await sleep(10_000, signal);
|
||||
}
|
||||
throw new DOMException('Cancelled', 'AbortError');
|
||||
}
|
||||
|
||||
function sleep(ms: number, signal: AbortSignal): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const t = setTimeout(resolve, ms);
|
||||
signal.addEventListener('abort', () => {
|
||||
clearTimeout(t);
|
||||
reject(new DOMException('Cancelled', 'AbortError'));
|
||||
});
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Webhook handler (FastAPI)
|
||||
```python
|
||||
from fastapi import FastAPI, BackgroundTasks
|
||||
import asyncio
|
||||
|
||||
pending: dict[str, asyncio.Future] = {}
|
||||
|
||||
@app.post('/webhooks/job-done')
|
||||
async def job_done(payload: dict):
|
||||
job_id = payload['id']
|
||||
if job_id in pending:
|
||||
pending[job_id].set_result(payload)
|
||||
return {'ok': True}
|
||||
|
||||
async def wait_for_webhook(job_id: str, timeout: float = 600):
|
||||
future = asyncio.Future()
|
||||
pending[job_id] = future
|
||||
try:
|
||||
return await asyncio.wait_for(future, timeout=timeout)
|
||||
finally:
|
||||
pending.pop(job_id, None)
|
||||
```
|
||||
|
||||
### Idempotent result fetch
|
||||
```python
|
||||
def fetch_result_idempotent(job_id, max_retries=3):
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = api.get_result(job_id)
|
||||
return response.data
|
||||
except TransientError as e:
|
||||
if attempt == max_retries - 1: raise
|
||||
sleep(2 ** attempt)
|
||||
except PermanentError:
|
||||
raise
|
||||
```
|
||||
|
||||
### Progress-aware polling
|
||||
```python
|
||||
def poll_progress(job_id):
|
||||
last_progress = 0
|
||||
while True:
|
||||
status = api.get_status(job_id)
|
||||
if status.done: return status.result
|
||||
|
||||
if status.progress > last_progress:
|
||||
log(f'Job {job_id}: {status.progress*100:.1f}%')
|
||||
last_progress = status.progress
|
||||
|
||||
# 매 ETA 기반 의 dynamic
|
||||
remaining_eta = (1 - status.progress) * status.elapsed / max(status.progress, 0.01)
|
||||
next_poll = min(30, max(2, remaining_eta / 5))
|
||||
sleep(next_poll)
|
||||
```
|
||||
|
||||
## 🤔 결정 기준
|
||||
| 상황 | Pattern |
|
||||
|---|---|
|
||||
| Fast (1-30 sec) | Fixed 1-2 sec polling |
|
||||
| Medium (1-10 min) | 5-10 sec polling |
|
||||
| Long (10 min-hour) | Hybrid (webhook + polling) |
|
||||
| Variable | Exponential backoff |
|
||||
| Cancellable | AbortController |
|
||||
| Resource-constrained | Webhook only |
|
||||
| Behind firewall | Polling only |
|
||||
|
||||
**기본값**: Hybrid (webhook + 10 sec polling) + jitter + cancellation.
|
||||
|
||||
## 🔗 Graph
|
||||
- 부모: [[Async-Programming]] · [[API-Design]]
|
||||
- 변형: [[Server-Sent-Events]] · [[Exponential-Backoff]]
|
||||
- 응용: [[NotebookLM]] · [[Agent-Loop]]
|
||||
- Adjacent: [[Circuit-Breaker]] · [[AbortController]]
|
||||
|
||||
## 🤖 LLM 활용
|
||||
**언제**: 매 long-running job. 매 agent automation. 매 third-party API integration. 매 batch inference orchestration.
|
||||
**언제 X**: 매 streaming (SSE 가 better). 매 sub-second job.
|
||||
|
||||
## ❌ 안티패턴
|
||||
- **No timeout**: 매 무한 hang.
|
||||
- **No jitter**: 매 thundering herd.
|
||||
- **Too short interval**: 매 quota burn.
|
||||
- **No cancel**: 매 resource leak.
|
||||
- **No idempotent fetch**: 매 retry 의 corruption.
|
||||
- **Webhook only (firewall)**: 매 silent loss.
|
||||
- **Tight retry on permanent error**: 매 useless burn.
|
||||
|
||||
## 🧪 검증 / 중복
|
||||
- Verified (AWS / Stripe / Replicate / GitHub API patterns).
|
||||
- 신뢰도 B.
|
||||
- Related: [[Webhook-Pattern]] · [[Async-Job-Queue]] · [[Retry-with-Backoff]] · [[Agent-Loop]].
|
||||
|
||||
## 🕓 Changelog
|
||||
| 날짜 | 변경 |
|
||||
|---|---|
|
||||
| 2026-05-08 | Phase 1 |
|
||||
| 2026-05-10 | Manual cleanup — polling pattern + webhook + 매 TS / Python code (basic, backoff, hybrid, cancellation) |
|
||||
Reference in New Issue
Block a user