Deep Dive: Work Intake & Scheduling cho Agent Loops
Trigger đánh thức runtime; intake cấp quyền cho công việc Thesis
Một cron tick, webhook hay nút Run chỉ nói rằng hệ thống nên thức dậy. Nó không chứng minh event còn mới, item chưa được xử lý, dependency đã sẵn sàng hay actor được phép chạm workspace. Khi trigger bị dùng thẳng như work order, mọi lỗi delivery—duplicate, reorder, delay—đều chảy vào reasoning loop và bị hiểu nhầm thành vấn đề của model.
Intake là admission-control boundary. Nó chuẩn hóa tín hiệu, xác định stable work identity, kiểm freshness và eligibility, áp policy, lấy lease và chỉ sau đó mới tạo context cho agent. Đây là nơi intent của nguồn phát được biến thành một đơn vị công việc có thể dedupe, checkpoint, budget và audit.
Tách hai lớp còn tạo một thuộc tính quan trọng: cùng một intake policy có thể được gọi từ schedule, event hoặc manual recovery mà không thay đổi semantics của work item. Trigger trở thành transport; contract trở thành source of truth.
No-work cũng là outcome hợp lệ. Một scheduled scan không thấy item cần ghi receipt và exit sạch, thay vì gọi model để “tìm thêm việc”. Khả năng im lặng đúng lúc là kiểm soát chống invented work, cost leak và notification noise.
Control-flow model: normalize trước, reason sau Mental model
Pipeline intake tốt giảm entropy theo từng bước. Envelope delivery được chuẩn hóa; identity được ánh xạ sang domain; policy loại item không hợp lệ; dedupe loại bản sao; scheduler kiểm capacity; lease tạo ownership tạm thời. Chỉ payload đã qua chuỗi này mới trở thành prompt context.
Một intake pipeline tách delivery semantics khỏi domain semantics và execution capacity.
flowchart LR
T["Schedule / event / manual"] --> N["Normalize envelope"]
N --> I["Resolve work key + version"]
I --> F{"Fresh and eligible?"}
F -- No --> R["Reject / defer receipt"]
F -- Yes --> D{"Seen or active?"}
D -- Yes --> R
D -- No --> C{"Capacity available?"}
C -- No --> Q["Queue with backpressure"]
C -- Yes --> L["Acquire lease"]
L --> A["Agent act"]
A --> P["Persist + release / renew"]
Hai identity cùng tồn tại. Event ID nhận diện lần delivery; work key nhận diện công việc domain, ví dụ
repo/pr/check-name/input-sha. Dedupe chỉ theo event ID sẽ bỏ sót hai webhook khác nhau cho cùng commit;
dedupe chỉ theo PR sẽ nuốt mất phiên bản input mới.
Scheduler cũng không nên quyết định business priority một mình. Nó thực thi fairness, concurrency và rate limits trên priority đã được intake policy gán. Nếu trộn hai vai trò, việc đổi queue implementation có thể vô tình đổi policy.
Bốn kỹ thuật thiết kế intake Design
1. Định nghĩa trigger semantics như một transport contract
I.1Bốn trigger family trong taxonomy phục vụ bốn nhu cầu. Schedule bảo đảm cadence nhưng không biết có thay đổi; event có latency thấp nhưng phụ thuộc delivery; goal trigger chạy tới condition thay vì theo thời gian; manual trigger phù hợp recovery và exceptional work. Một loop có thể hỗ trợ nhiều family nhưng chúng phải hội tụ vào cùng intake.
Với mỗi trigger, contract cần trả lời timezone, jitter, missed tick, catch-up, replay window và payload authority. Scheduled run sau downtime có backfill mọi khoảng đã mất hay chỉ chạy latest? Webhook retry trong bao lâu? Manual run có bypass policy không? Những lựa chọn này thay đổi workload và blast radius.
| Trigger | Ưu thế | Delivery risk | Policy bắt buộc |
|---|---|---|---|
| Schedule | Cadence dự đoán được | Overlap, missed tick, thundering herd | Jitter + overlap + catch-up rule |
| Event | Phản ứng nhanh | Duplicate, reorder, delay | Envelope + dedupe + freshness |
| Goal | Bám condition | Polling vô hạn, stale observation | Observation source + stop bound |
| Manual | Recovery và exception rõ | Bypass/privilege escalation | Actor identity + same admission policy |
GitHub Actions cho thấy một workflow có thể được kích hoạt bởi nhiều event và các event đồng thời tạo nhiều run. Điều đó hữu ích như transport behavior, nhưng domain vẫn phải xác định các run có cùng work key hay độc lập.
Ưu điểm
- Trigger có semantics rõ nên replay và backfill dự đoán được.
- Nhiều transport dùng chung intake contract.
- Manual recovery không cần bypass guardrail.
Nhược điểm
- Cần mô hình hóa clock và delivery edge cases.
- Backfill có thể tạo burst lớn.
- Event schema evolution cần compatibility policy.
2. Dùng identity, version và freshness để chống duplicate lẫn stale work
I.2
CloudEvents cung cấp các trường như id, source, type, subject và
time. Chúng đủ để correlation và routing, nhưng không tự định nghĩa business uniqueness. Intake nên lưu
cả delivery identity và stable work identity trong receipt.
Freshness không chỉ là tuổi event. Item có thể mới được deliver nhưng trỏ tới commit cũ, ticket đã đóng hoặc policy version hết hiệu lực. Freshness gate phải đọc authoritative source và so input version trước khi lease.
Idempotency key cho side effect nên dẫn xuất từ work key, operation và target version; không dùng attempt number. Nếu crash rồi retry, attempt mới phải nhận ra operation cũ đã commit và chuyển sang reconcile thay vì write lại.
Identity bundle tối thiểu
- Delivery: event ID, source, type, emitted/received time.
- Domain: work key, target ID, input version, policy version.
- Execution: run ID, attempt, lease token, concurrency key.
- Mutation: operation key, external resource ID, commit status.
Dedupe window hữu hạn cần được chọn theo delivery contract và retention. Window quá ngắn để duplicate cũ lọt vào; window vô hạn có thể chặn work hợp lệ sau khi domain reset. Tombstone và explicit generation thường an toàn hơn một boolean “processed forever”.
Ưu điểm
- Chống duplicate model calls và external writes.
- Freshness gate ngăn agent làm đúng trên input cũ.
- Receipt có provenance đầy đủ để replay.
Nhược điểm
- Stable key sai gây false merge hoặc false split.
- Ledger cần retention và schema migration.
- Reconciliation với external systems làm implementation phức tạp hơn.
3. Chọn queue, scan hay reactive theo source of truth và backpressure
I.3Queue intake tốt khi producer biết rõ work item và có thể enqueue atomically. Nó cho visibility, retry và priority, nhưng queue có thể lệch khỏi source of truth. Scan intake đọc authoritative state nên chữa missed event tốt, đổi lại tốn query và phải có watermark. Reactive intake nhanh nhưng mang toàn bộ rủi ro delivery của event source.
Hybrid phổ biến là event để tăng tốc, queue để hấp thụ burst và periodic scan để reconcile. Ba path phải tạo cùng work key; nếu mỗi path có identity riêng, “reliability” vô tình nhân ba công việc.
| Mode | Backlog ở đâu | Failure điển hình | Khi nên dùng |
|---|---|---|---|
| Queue | Broker/database queue | Poison item, visibility timeout, starvation | Work explicit, cần ownership/fairness |
| Scan | Authoritative source | Stale watermark, expensive full scan | Missed-event repair, periodic reconciliation |
| Reactive | Event stream | Duplicate/reorder/drop | Low latency và event source đáng tin |
| Hybrid | Queue + source of truth | Identity mismatch giữa paths | Production cần latency lẫn recovery |
Backpressure phải tồn tại trước model. Khi capacity hết, intake defer hoặc queue; không spawn thêm agent rồi hy vọng budget tổng vẫn ổn. Concurrency key nên bám resource conflict—repository, customer, deployment target—chứ không chỉ bám tên workflow.
Fairness cần age, priority và per-tenant quota. Priority tuyệt đối dễ làm low-priority starvation; FIFO tuyệt đối có thể để một poison item chặn hàng. Dead-letter route phải kèm evidence và owner, không phải nghĩa địa không ai đọc.
Ưu điểm
- Mode được chọn theo source of truth thay vì thói quen runtime.
- Hybrid khôi phục missed events mà vẫn giữ latency thấp.
- Backpressure bảo vệ cost và downstream systems.
Nhược điểm
- Hybrid cần identity thống nhất.
- Priority/fairness policy phải được vận hành.
- Scan và reconciliation tăng load lên source system.
4. Lease, checkpoint và recovery đóng crash window
I.4Concurrency group hữu ích để serialize run theo key và có thể cancel run cũ, nhưng cancellation không đảo ngược side effect đã xảy ra. Domain lease cần token/version để stale worker không commit sau khi lease được cấp lại.
Lease có TTL, heartbeat và renewal bound. TTL quá ngắn gây duplicate ownership khi tool call dài; quá dài kéo dài recovery. Worker mất heartbeat phải ngừng mutation ngay cả khi process vẫn sống—đây là fencing, không chỉ monitoring.
Durable execution có thể resume control flow sau crash, nhưng external API không nằm trong transaction của runtime. Pattern an toàn là persist intent, gọi operation bằng idempotency key, persist result; nếu không biết call đã commit, reconcile bằng external ID trước khi retry.
Recovery policy phân biệt transient failure, poison work và infrastructure outage. Retry storm sau outage có thể nặng hơn outage gốc, nên cần exponential backoff, jitter, global circuit breaker và admission ramp thay vì thả toàn bộ backlog.
Ưu điểm
- Lease/fencing ngăn stale worker commit.
- Durable checkpoint giảm restart-from-zero.
- Reconciliation đóng ambiguous external-write window.
Nhược điểm
- TTL và heartbeat cần tuning theo workload.
- Không có distributed transaction cho mọi tool.
- Recovery burst cần capacity plan riêng.
Failure map: từ delivery tới side effect Runbook
Một symptom “job chạy hai lần” có thể xuất phát từ event duplicate, schedule overlap, lease expiry hoặc crash sau write. Chẩn đoán cần xem identity bundle và causal receipt trước khi sửa retry count.
| Triệu chứng | Evidence cần xem | Control |
|---|---|---|
| Cùng item chạy đồng thời | work key, lease token, concurrency key | Atomic lease + fencing |
| Item cũ được xử lý | event time, input version, source snapshot | Freshness gate + authoritative reread |
| External write lặp | operation key, external ID, checkpoint order | Idempotency + reconcile |
| Backlog tăng sau outage | arrival/service rate, retry schedule | Circuit breaker + jittered ramp |
| Không còn việc vẫn tốn token | no-work branch, model-call timestamp | Pre-model no-work exit |
Runbook nên drill ít nhất ba case: duplicate delivery, worker chết giữa write/checkpoint và backlog recovery. Nếu không tái hiện được ba case này, intake contract vẫn là mô tả chứ chưa là control.
Intake contract tối thiểu Recipe
Contract dưới đây thể hiện bốn outcome—accept, defer, reject và no-work—để scheduler không biến mọi tín hiệu thành retry. Mỗi outcome đều tạo receipt và cập nhật watermark hoặc ledger tương ứng.
Admission boundary trước mọi model call
type IntakeDecision =
| { kind: "accept"; item: WorkItem; lease: Lease }
| { kind: "defer"; reason: string; retryAfter: Date }
| { kind: "reject"; reason: string }
| { kind: "no-work"; watermark: string };
async function intake(signal: Trigger): Promise<IntakeDecision> {
const event = normalize(signal); // identity + source + time
if (!fresh(event) || seen(event.id)) return reject("stale-or-duplicate");
const item = await resolveStableWorkKey(event);
if (!eligible(item) || !withinScope(item)) return reject("policy");
return leaseAtomically(item, concurrencyKey(item));
}
- Stable work key và input version có thể giải thích.
- Duplicate, stale, no-work và capacity-exhausted đều có explicit branch.
- Lease có fencing; side effect có idempotency key và reconcile path.
- Schedule/event/manual cùng đi qua một admission policy.
Kết luận: intake tốt làm model ít việc hơn Conclusion
Work intake đáng tin không tối đa hóa số run; nó tối đa hóa tỷ lệ run có công việc hợp lệ và đủ điều kiện. Phần lớn duplicate, stale và overload nên bị loại trước prompt.
Khi trigger chỉ còn là transport, scheduler chỉ còn là capacity controller và intake là policy boundary, loop có thể thay runtime mà vẫn giữ semantics. Đó là dấu hiệu contract đã trở thành kiến trúc, không phải tài liệu.