Sistem autonomous agent yang menjalankan long task planning (seperti ReAct, Plan-and-Solve, atau tree-of-thought) sering mengalami kegagalan pada sub-langkah tertentu. Ketika terjadi step failure, timeout jaringan, atau diskrepansi output, agent umumnya memicu mekanisme replanning atau step-level retry. Tanpa kontrak API yang tepat, pemanggilan ulang pada tool non-idempotent (misalnya pemotongan saldo, provisioning instance cloud, atau pengiriman webhook) akan memicu eksekusi ganda (duplicate side-effects).
Solusi standar seperti HTTP retry biasa tidak memadai untuk AI agent karena LLM cenderung merumuskan ulang request atau mengirimkannya kembali dengan jeda waktu signifikan di tengah sesi perencanaan. Artikel ini menguraikan desain kontrak API tool yang deterministik menggunakan header Idempotency-Key komposit, penanganan status in-flight, manajemen storage state, dan mitigasi token auth kedaluwarsa.
Anatomi Masalah: Duplicate Side-Effects pada Replanning
Pada pipeline eksekusi task panjang, arsitektur agent umumnya memecah tujuan akhir menjadi DAG (Directed Acyclic Graph) langkah eksekusi. Masalah muncul dalam skenario berikut:
- Agent mengeksekusi Step 3: Tool
provision_database. - Backend API menerima request, mulai membuat database, tetapi koneksi HTTP antara agent runner dan API mengalami timeout sebelum response
200 OKditerima agent. - Agent evaluator mendeteksi timeout sebagai error, lalu memicu replanning. Hasil replanning memutuskan untuk mengulang Step 3 dengan payload yang identik atau sedikit dimodifikasi.
- Jika API backend tidak memiliki proteksi idempotensi level-langkah, instance database kedua akan dibuat, menghasilkan kebocoran resource dan inkonsistensi state runtime agent.
Mengandalkan client-side UUID acak sebagai Idempotency-Key sering kali gagal pada sistem agent, karena loop agent yang memicu replan dapat menghasilkan ID baru jika prompt generation tidak dipaksa menghasilkan ID deterministik.
Desain Kunci: Step-Level Idempotency Key
Idempotency key harus terikat secara ketat pada siklus hidup task planning agent. Kunci dibentuk secara deterministik di level runtime orchestrator sebelum request dikirim ke tool endpoint:
Idempotency-Key: <plan_session_id>:<step_index>:<args_hash>Komponen pembentuk key:
plan_session_id: UUID sesi root planning task agent yang unik untuk satu siklus pekerjaan dari awal hingga selesai.step_index: Indeks urutan langkah deterministik di dalam rencana kerja (misal:step_003).args_hash: SHA-256 dari representasi JSON kanonikal (terurut berdasarkan key/lexicographically sorted) dari parameter eksekusi tool.
Mengapa menyertakan args_hash? Jika agent melakukan replanning dan memutuskan mengubah parameter kritis (misalnya mengganti region dari ap-southeast-1 ke us-east-1) pada step yang sama, kunci harus dianggap sebagai operasi baru yang berbeda, bukan replay dari operasi sebelumnya.
Siklus Hidup Eksekusi dan Penanganan Status In-Flight
State eksekusi tool di key-value store (seperti Redis) memerlukan mesin status minimal: IN_PROGRESS, COMPLETED, dan FAILED.
Penanganan Race Condition: 409 Conflict vs Waiting Polling
Ketika dua worker agent secara tidak sengaja memicu step yang sama, atau ketika retry tiba saat proses pertama masih berjalan di backend, backend memiliki dua opsi pendekatan arsitektural:
- Waiting/Polling: Connection ditahan (long-polling) sampai proses pertama selesai. Pendekatan ini rentan menghabiskan worker thread server dan memicu cascading timeout pada client agent.
- 409 Conflict (Direkomendasikan): Tolak eksekusi kedua secara instan dengan response
409 Conflictdan sertakan headerRetry-After. Pola ini mengembalikan kendali ke agent orchestrator untuk menentukan strategi backoff tanpa membebani resource I/O server.
Alur Status Redis
- Acquire Lock: Eksekusi perintah atomic
SET key "IN_PROGRESS" NX EX <ttl>. - Jika gagal (key sudah ada): Baca nilai key. Jika nilainya
IN_PROGRESS, kembalikan HTTP409 Conflict. Jika sudah ada data payload tersimpan, kembalikan response tersimpan langsung (HTTP 200/201 replay) dengan headerIdempotent-Replayed: true. - Jika berhasil: Jalankan operasi internal tool, simpan output serialisasi ke Redis, lalu kembalikan output ke client agent.
TTL State Idempotensi di Key-Value Store
Berbeda dengan cache API standar (1-5 menit), TTL idempotensi pada agent long-task planning harus dihitung berdasarkan batas atas runtime pekerjaan. Jika sebuah agent diizinkan menjalankan workflow hingga 2 jam (termasuk human-in-the-loop validation):
- TTL Status
IN_PROGRESS: Diberi batas waktu timeout operasi tool individual ditambah toleransi jaringan (misal: 60-120 detik). Menggunakan TTL pendek pada status lock ini mencegah deadlock permanen jika proses backend crash sebelum sempat mengupdate status. - TTL Status
COMPLETED/ Replay: Harus bertahan selama durasi maksimal sesi planning aktif ditambah buffer audit (misal: 24 hingga 48 jam). Setelah TTL ini expired, memory Redis dibersihkan otomatis.
Mitigasi Auth Token Kedaluwarsa pada Task Durasi Panjang
Eksekusi long task planning yang memakan waktu belasan menit hingga beberapa jam rentan menemui kendala 401 Unauthorized di tengah proses karena access token JWT user kedaluwarsa. Strategi mitigasi yang perlu diterapkan pada layer kontrak API:
- OBO (On-Behalf-Of) Token Exchange: Orchestrator agent memegang refresh token terenkripsi atau credentials machine-to-machine (m2m) berbasis OAuth2 Client Credentials dengan scope delegasi. Orchestrator memperbarui access token sebelum memanggil tool endpoint, tidak langsung menggunakan raw JWT user awal.
- Asynchronous Decoupled Execution: Untuk step yang memakan waktu di atas batas aman timeout HTTP (misal > 30 detik), kontrak tool harus dirancang asinkron: endpoint tool mengembalikan
202 Accepteddengan resource tracking URL. Agent melakukan polling status menggunakan token yang di-refresh secara berkala.
Implementasi Handler Tool API (FastAPI + Redis)
Berikut implementasi endpoint tool minimalis menggunakan Python, FastAPI, dan Redis dengan pola atomic check, lock, dan response replay.
import hashlib
import json
from typing import Any, Dict
from fastapi import FastAPI, Header, HTTPException, Response, status
import redis
app = FastAPI()
redis_client = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
LOCK_TTL_SECONDS = 120 # Lock timeout untuk mencegah dangling state
RESULT_TTL_SECONDS = 86400 # 24 jam untuk replay response agent
def generate_canonical_hash(payload: Dict[str, Any]) -> str:
canonical_json = json.dumps(payload, sort_keys=True, separators=(",", ":"))
return hashlib.sha256(canonical_json.encode("utf-8")).hexdigest()
@app.post("/tools/provision-database")
def provision_database(
payload: Dict[str, Any],
response: Response,
idempotency_key: str = Header(..., alias="Idempotency-Key")
):
# Validasi format key: plan_id:step_index:hash
parts = idempotency_key.split(":")
if len(parts) != 3:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Format Idempotency-Key harus: <plan_id>:<step_index>:<args_hash>"
)
expected_hash = generate_canonical_hash(payload)
if parts[2] != expected_hash:
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail="Payload tidak cocok dengan args_hash pada Idempotency-Key"
)
redis_key = f"idempotency:{idempotency_key}"
# 1. Atomic lock menggunakan SET NX
acquired = redis_client.set(redis_key, json.dumps({"status": "IN_PROGRESS"}), nx=True, ex=LOCK_TTL_SECONDS)
if not acquired:
raw_val = redis_client.get(redis_key)
if not raw_val:
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="State lookup failure")
cached_data = json.loads(raw_val)
# 2. Tangani state IN_PROGRESS
if cached_data.get("status" developments) == "IN_PROGRESS":
response.headers["Retry-After"] = "5"
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Task langkah ini sedang berjalan. Lakukan retry sesuai nilai Retry-After."
)
# 3. Replay cached response
response.headers["Idempotent-Replay"] = "true"
return cached_data.get("response")
# Eksekusi side-effect
try:
# Simulasi mutasi database/resource
result = {
"instance_id": f"db-{parts[0][:8]}",
"status": "ready",
"allocated_storage": payload.get("storage_gb", 20)
}
# Simpan hasil untuk replay
cache_payload = {
"status": "COMPLETED",
"response": result
}
redis_client.set(redis_key, json.dumps(cache_payload), ex=RESULT_TTL_SECONDS)
return result
except Exception as exc:
# Hapus lock agar step dapat dicoba ulang jika kegagalan bersifat internal
redis_client.delete(redis_key)
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=str(exc))
Trade-offs dan Pertimbangan Sistem
- Penyimpanan Payload Respons: Menyimpan respons berukuran besar di Redis dapat menghabiskan memori. Simpan respons di object storage (S3/GCS) dan letakkan URI referensinya di Redis jika ukuran payload tool melebihi ambang batas aman (> 100 KB).
- Deterministic Serializer: Penyusunan
args_hashwajib menggunakan JSON serializer yang mengurutkan dictionary keys secara konsisten (sort_keys=True) untuk mencegah miss hash pada object JSON dengan urutan property acak. - Graceful Recovery: Selalu pastikan blok exception handling menghapus lock status
IN_PROGRESSjika terjadi error infrastruktur lokal sebelum mutasi dimulai. Jika mutasi sudah separuh jalan, tandai status sebagaiFAILEDagar agent dapat melakukan intervensi korektif yang tepat.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!