docs/en/s15-scheduled-tasks.mdXem bản gốc Bản dịch đầy đủ theo đúng cấu trúc tài liệu; code, liên kết và định danh kỹ thuật được giữ nguyên.
s15: Nhiệm vụ theo lịch trình
s00 > s01 > s02 > s03 > s04 > s05 > s06 > s07 > s08 > s09 > s10 > s11 > s12 > s13 > s14 > [ s15 ] > s16 > s17 > s18 > s19 > s20 > s21 > s22 > s23 > s24 > s25 > s26 > s27
Các agent từ s01-s14 hoàn toàn phản ứng -- chúng chỉ hành động khi người dùng gửi tin nhắn. Chương này dạy agent "ghi nhớ những việc cần làm trong tương lai" và hành động khi thời điểm đến.

Chương này giải quyết vấn đề gì
Cho đến s14, agent hoạt động như thế này: người dùng nói điều gì đó -> agent hành động -> trả lời. Nếu người dùng không nói gì, agent sẽ không hoạt động.
Nhưng trong các tình huống thực tế, người dùng sẽ nói những câu như:
"Trong 30 phút, hãy kiểm tra xem việc triển khai đó có thành công hay không."
"Cứ 2 giờ, hãy kiểm tra mức sử dụng đĩa máy chủ và cho tôi biết nếu nó vượt quá 80%."
"Vào lúc 9 giờ sáng các ngày trong tuần, tóm tắt git log của ngày hôm qua cho tôi."
Ba yêu cầu, một chủ đề chung: người dùng muốn tổng đài viên chủ động thực hiện điều gì đó vào thời điểm trong tương lai.
Nếu không có hệ thống nhiệm vụ theo lịch trình, nhân viên chỉ có thể nói "Chắc chắn rồi, vui lòng nhắc tôi khi đến lúc" -- điều đó có khác gì đồng hồ báo thức không?
Hướng dẫn kịch bản: toàn bộ vòng đời của một nhiệm vụ đã lên lịch
Một người dùng nói với agent trên Telegram: "Hãy kiểm tra trạng thái triển khai cho tôi sau 30 phút."
1. The agent understands the intent and calls cron_tool:
create_job(schedule="30m", prompt="Check deployment status, run kubectl get pods")
2. cron_tool parses "30m":
- Current time: 14:00
- next_fire = 14:30
- one_shot = true (delete after one execution)
3. JobStore writes this job to jobs.json
4. The agent replies: "Got it, I'll check the deployment status in 30 minutes."
5. The background JobScheduler scans jobs.json every 30 seconds...
- 14:00:30 -- not time yet, skip
- 14:01:00 -- not time yet, skip
- ...
- 14:30:00 -- time's up!
6. JobScheduler constructs a MessageEvent:
text = "Check deployment status, run kubectl get pods"
source.platform = "cron"
source.chat_id = the original Telegram user's chat_id
7. This MessageEvent is fed into GatewayRunner._handle_message()
-> follows the exact same path as a real user message
8. The agent runs kubectl get pods, gets the result, and replies on Telegram
9. Because one_shot=true, JobStore automatically deletes this job
Thông tin chi tiết quan trọng: Tác vụ đã lên lịch không phải là lộ trình thực thi mới. Nó chỉ đơn giản là một cơ chế "giả vờ người dùng nói điều gì đó" vào một thời điểm trong tương lai. Từ GatewayRunner trở đi, tất cả logic đều được sử dụng lại hoàn toàn.
Đề xuất đọc
s02-tool-system.md-- cron_tool cắm vào thông qua tự đăng kýs12-gateway-architecture.md-- khi một công việc được kích hoạt, nó sẽ đi qua Cổng_handle_messages14-terminal-backends.md-- các lệnh được kích hoạt bởi công việc sẽ sử dụng lại cùng một chương trình phụ trợ
Thuật ngữ chính
CronJob là gì
Một mô tả đầy đủ về một nhiệm vụ theo lịch trình. Nó bao gồm: khi nào kích hoạt, thông báo nào sẽ gửi cho tổng đài viên khi nó kích hoạt và nó thuộc về phiên nào.
@dataclass
class CronJob:
job_id: str # Unique identifier
schedule: str # Original schedule expression: "30m" / "every 2h" / "0 9 * * 1-5"
prompt: str # Message to send to the agent when the job fires
session_key: str # Which session this belongs to
created_at: str # Creation time
next_fire: float # Unix timestamp of the next fire time
one_shot: bool # True = delete after one execution, False = recurring
Ba định dạng lịch trình
| Định dạng | Ví dụ | Ý nghĩa | one_shot |
|---|---|---|---|
| Trì hoãn | 30m | Thực hiện một lần trong 30 phút | Đúng |
| Khoảng thời gian | every 2h | Thực hiện cứ sau 2 giờ, định kỳ | Sai |
| Cron | 0 9 * * 1-5 | Các ngày trong tuần lúc 9 giờ sáng | Sai |
Tại sao không chỉ sử dụng biểu thức cron? Bởi vì người dùng nói "nhắc tôi sau 30 phút" thường xuyên hơn nhiều so với "thiết lập cron cho tôi". Định dạng trễ làm cho kịch bản phổ biến nhất trở nên đơn giản nhất.
Cửa hàng việc làm là gì
Lớp kiên trì cho các nhiệm vụ theo lịch trình. Chịu trách nhiệm về các hoạt động CRUD và ghi vào đĩa.
Tại sao sử dụng jobs.json thay vì SQLite?
- Số lượng tác vụ ít -- một người dùng thường có không quá 20 tác vụ đang hoạt động
- Con người có thể đọc được -- bạn có thể mở và kiểm tra trực tiếp khi gỡ lỗi
- Không cần tìm kiếm toàn văn hoặc viết đồng thời
SQLite được thiết kế cho khối lượng lớn thông báo phiên. Sử dụng nó để lưu trữ 20 công việc là quá mức cần thiết.
Trình lập lịch công việc là gì
Một luồng nền sẽ thức dậy sau mỗi 30 giây, quét tất cả công việc và thực hiện bất kỳ công việc nào đến hạn.
Tại sao lại là một chủ đề thay vì asyncio? Bởi vì bộ lập lịch cần chạy ở cả chế độ CLI (đồng bộ) và chế độ Cổng (không đồng bộ). Một luồng là mẫu số chung thấp nhất cho cả hai chế độ.
Mô hình tinh thần tối thiểu
User (or the agent itself)
|
| cron_tool: create_job(schedule="every 2h", prompt="Check disk")
v
JobStore
| Writes to jobs.json
|
| +-------------- Background ----------------+
| | JobScheduler (thread, scans every 30s) |
| | |
| | for job in jobs: |
| | if now >= job.next_fire: |
| | fire(job) |
| +-------------------------------------------+
|
| fire(job):
v
Construct MessageEvent(text=job.prompt, platform="cron")
|
| Feed into GatewayRunner._handle_message()
v
Follows the exact same path as a real user message
|
| Agent executes and replies
v
Result sent back to the original platform (Telegram / Discord / CLI)
Cấu trúc dữ liệu chính
việc làm.json
[
{
"job_id": "a1b2c3",
"schedule": "every 2h",
"prompt": "Run df -h. Alert if any partition exceeds 80%.",
"session_key": "main:telegram:alice",
"created_at": "2025-01-15T14:00:00",
"next_fire": 1736953200.0,
"one_shot": false
},
{
"job_id": "d4e5f6",
"schedule": "0 9 * * 1-5",
"prompt": "Summarize yesterday's git log, main branch only",
"session_key": "main:telegram:alice",
"created_at": "2025-01-15T14:05:00",
"next_fire": 1737007200.0,
"one_shot": false
}
]
Bắt đầu với việc thực hiện đơn giản nhất
Thêm một time.sleep thăm dò bên trong vòng lặp agent:
# Simplest approach: poll in the main loop
while True:
user_input = input("You: ")
if user_input:
handle_message(user_input)
# Check scheduled tasks
for job in jobs:
if time.time() >= job.next_fire:
handle_message(job.prompt) # Pretend the user said this
update_next_fire(job)
Điều này hoạt động, nhưng có hai vấn đề.
Vấn đề 1: Chặn
input() khối. Nếu người dùng không gõ bất cứ thứ gì, các tác vụ đã lên lịch sẽ không bao giờ được kiểm tra.
Bạn có thể nghĩ: “Sử dụng select hoặc threading làm input không chặn." Chắc chắn rồi, nhưng điều đó dẫn đến Vấn đề 2.
Vấn đề 2: Còn chế độ Gateway thì sao
Chế độ cổng không đồng bộ - không có input(). Bạn cần một cơ chế lập lịch độc lập với chế độ nhập.
Giải pháp: Đặt bộ lập lịch trong luồng nền riêng của nó. Cả CLI và Gateway đều bắt đầu cùng một luồng và khi đến hạn công việc, luồng sẽ tạo một MessageEvent và đưa nó vào quy trình xử lý.
Thực hiện tối thiểu
Lên lịch phân tích biểu thức
Logic phân tích cú pháp cho ba định dạng:
def parse_schedule(expr: str) -> tuple[float, bool]:
"""
Parse a schedule expression. Returns (next_fire_timestamp, one_shot).
Supports:
"30m" -> 30 minutes from now, one-shot
"2h" -> 2 hours from now, one-shot
"every 30m" -> every 30 minutes, recurring
"every 2h" -> every 2 hours, recurring
"0 9 * * 1-5" -> cron expression, recurring
"""
expr = expr.strip()
now = time.time()
# Format 1: "every Xm" / "every Xh" -> recurring interval
if expr.startswith("every "):
seconds = _parse_duration(expr[6:])
return now + seconds, False
# Format 2: "Xm" / "Xh" -> one-shot delay
try:
seconds = _parse_duration(expr)
return now + seconds, True
except ValueError:
pass
# Format 3: cron expression
next_ts = _next_cron_fire(expr)
return next_ts, False
_parse_duration rất đơn giản -- lấy đơn vị ở cuối chuỗi, số ở phía trước:
_UNITS = {"s": 1, "m": 60, "h": 3600, "d": 86400}
def _parse_duration(s: str) -> float:
s = s.strip()
unit = s[-1].lower()
if unit not in _UNITS:
raise ValueError(f"unknown unit: {unit}")
return float(s[:-1]) * _UNITS[unit]
Phân tích biểu thức Cron
Cron năm trường không cần thư viện của bên thứ ba -- định dạng chuẩn là đủ:
minute hour day month weekday
0 9 * * 1-5 <- weekdays at 9 AM
Ý tưởng cốt lõi: bắt đầu từ "bây giờ", tua đi từng phút cho đến khi bạn tìm thấy thời gian khớp với tất cả năm trường.
def _next_cron_fire(expr: str) -> float:
"""Starting from the current time, find the next time matching the cron expression."""
fields = expr.split()
if len(fields) != 5:
raise ValueError(f"cron needs 5 fields, got {len(fields)}: {expr}")
matchers = [_parse_cron_field(f, r) for f, r in
zip(fields, [(0,59), (0,23), (1,31), (1,12), (0,6)])]
# Start from the next minute, search up to 366 days
t = datetime.now().replace(second=0, microsecond=0)
t += timedelta(minutes=1)
for _ in range(366 * 24 * 60):
if (matchers[0](t.minute) and matchers[1](t.hour)
and matchers[2](t.day) and matchers[3](t.month)
and matchers[4](t.weekday())):
# Python weekday(): 0=Mon, cron: 0=Sun -> conversion needed
return t.timestamp()
t += timedelta(minutes=1)
raise ValueError(f"no match in 366 days for: {expr}")
_parse_cron_field xử lý bốn cú pháp: *, */5, 1-5, 1,3,5, và trả về một int -> bool vị ngữ. Đây là phần phức tạp nhất của phân tích cú pháp cron, nhưng nó không phải là trọng tâm của chương này -- hãy xem mã nguồn để biết chi tiết.
Cửa hàng việc làm
class JobStore:
"""Task persistence: CRUD + write to disk."""
def __init__(self, path: str = "jobs.json"):
self._path = Path(path)
self._jobs: dict[str, CronJob] = {}
self._lock = threading.Lock()
self._load()
def add(self, job: CronJob):
with self._lock:
self._jobs[job.job_id] = job
self._save()
def remove(self, job_id: str) -> bool:
with self._lock:
if job_id in self._jobs:
del self._jobs[job_id]
self._save()
return True
return False
def list_all(self) -> list[CronJob]:
with self._lock:
return list(self._jobs.values())
def get_due(self) -> list[CronJob]:
"""Return all jobs that are due."""
now = time.time()
with self._lock:
return [j for j in self._jobs.values() if now >= j.next_fire]
def advance(self, job: CronJob):
"""Update next_fire (recurring tasks) or delete (one-shot tasks)."""
with self._lock:
if job.one_shot:
self._jobs.pop(job.job_id, None)
else:
next_ts, _ = parse_schedule(job.schedule)
job.next_fire = next_ts
self._save()
def _save(self):
data = [vars(j) for j in self._jobs.values()]
self._path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
def _load(self):
if not self._path.exists():
return
for item in json.loads(self._path.read_text()):
job = CronJob(**item)
self._jobs[job.job_id] = job
Tại sao lại khóa? Bởi vì JobStore được truy cập bởi hai luồng: luồng chính (cron_tool write) và luồng nền (JobScheduler đọc). Không có khóa -> điều kiện cuộc đua.
Lập lịch công việc
class JobScheduler:
"""Background thread that periodically checks for due tasks and fires them."""
def __init__(self, store: JobStore, fire_callback):
self._store = store
self._fire = fire_callback # def fire(job: CronJob) -> None
self._interval = 30 # seconds
self._running = False
self._thread: threading.Thread | None = None
def start(self):
self._running = True
self._thread = threading.Thread(target=self._loop, daemon=True)
self._thread.start()
def stop(self):
self._running = False
def _loop(self):
while self._running:
for job in self._store.get_due():
try:
self._fire(job)
except Exception as e:
print(f" [scheduler] job {job.job_id} failed: {e}")
self._store.advance(job)
time.sleep(self._interval)
daemon=True rất quan trọng: luồng nền tự động kết thúc khi quá trình chính thoát, tránh bị treo.
cron_tool: giao diện của agent
def handle_cron_tool(args: dict, *, store: JobStore, session_key: str, **kw):
action = args.get("action", "list")
if action == "create":
schedule = args["schedule"]
prompt = args["prompt"]
next_fire, one_shot = parse_schedule(schedule)
job = CronJob(
job_id=uuid.uuid4().hex[:8],
schedule=schedule,
prompt=prompt,
session_key=session_key,
created_at=datetime.now().isoformat(),
next_fire=next_fire,
one_shot=one_shot,
)
store.add(job)
fire_time = datetime.fromtimestamp(next_fire).strftime("%Y-%m-%d %H:%M")
return f"Job {job.job_id} created. Next fire: {fire_time}"
elif action == "list":
jobs = store.list_all()
if not jobs:
return "No scheduled jobs."
lines = []
for j in jobs:
fire_time = datetime.fromtimestamp(j.next_fire).strftime("%m-%d %H:%M")
kind = "once" if j.one_shot else "recurring"
lines.append(f" {j.job_id} {j.schedule:15s} {kind:9s} "
f"next: {fire_time} {j.prompt[:40]}")
return "Jobs:\n" + "\n".join(lines)
elif action == "delete":
job_id = args["job_id"]
if store.remove(job_id):
return f"Job {job_id} deleted."
return f"Job {job_id} not found."
return f"Unknown action: {action}"
Định nghĩa công cụ đã đăng ký với mô hình:
CRON_TOOL_SCHEMA = {
"type": "function",
"function": {
"name": "cron",
"description": "Create, list, or delete scheduled tasks.",
"parameters": {
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["create", "list", "delete"],
},
"schedule": {
"type": "string",
"description": 'Schedule expression: "30m", "every 2h", or "0 9 * * 1-5"',
},
"prompt": {
"type": "string",
"description": "What the agent should do when the job fires.",
},
"job_id": {
"type": "string",
"description": "Job ID to delete (for action=delete).",
},
},
"required": ["action"],
},
},
}
Tình huống 2: Cách kết quả của tác vụ định kỳ được trả về cho người dùng
Một người dùng nói trên Telegram: "Hãy kiểm tra đĩa cho tôi 2 giờ một lần."
1. The agent calls cron_tool: create_job(schedule="every 2h", prompt="Run df -h, alert if over 80%")
2. JobStore writes to jobs.json, next_fire = 2 hours from now
=== 2 hours later ===
3. JobScheduler finds the job is due
4. Constructs a MessageEvent:
text = "Run df -h, alert if over 80%"
source.platform = "cron"
source.chat_id = "main:telegram:alice" <- the original user's session_key
5. Fed into GatewayRunner._handle_message()
6. The agent runs df -h (via the terminal backend from s14)
7. Finds /data partition at 85% -> the agent replies with an alert
8. GatewayRunner sends the reply back to Telegram
9. JobStore updates next_fire = 2 more hours from now
=== Another 2 hours later ===
10. Steps 3-9 repeat
Lưu ý bước 4: JobScheduler không thực thi lệnh trực tiếp -- nó xây dựng một thông báo và đưa nó vào Cổng. Điều này có nghĩa là việc thực thi công việc được hưởng lợi từ tất cả các khả năng của agent -- lệnh gọi công cụ, khôi phục lỗi, kiểm tra quyền, nén ngữ cảnh -- tất cả đều được sử dụng lại mà không cần viết thêm một dòng nào.
Tình huống 3: Cách thực hiện công việc ở chế độ CLI
Chế độ cổng có _handle_message để nhận MessageEvents. Chế độ CLI thì không.
Giải pháp: Trong chế độ CLI, JobScheduler's fire_callback gọi trực tiếp run_conversation():
# Gateway mode
def fire_gateway(job):
event = MessageEvent(
text=job.prompt,
source=SessionSource(platform="cron", chat_id=..., ...),
)
asyncio.run_coroutine_threadsafe(
gateway._handle_message(event), loop
)
# CLI mode
def fire_cli(job):
result = run_conversation(job.prompt, conn, session_id, cached_prompt)
print(f"\n[cron] {job.job_id}: {result['final_response']}\n")
Hai chế độ, cùng một JobScheduler, khác nhau fire_callback triển khai.
Cách nó cắm vào vòng lặp chính
At startup
|
+-- CLI mode
| JobStore("jobs.json")
| JobScheduler(store, fire_callback=fire_cli)
| scheduler.start() <- background thread starts
| while True:
| input() -> run_conversation() <- main thread handles user input
|
+-- Gateway mode
JobStore("jobs.json")
JobScheduler(store, fire_callback=fire_gateway)
scheduler.start() <- background thread starts
GatewayRunner.start() <- asyncio event loop
At shutdown
scheduler.stop()
Khi đăng ký tool, bạn cần phải vượt qua store và session_key:
# When registering the cron tool, bind store via closure
def make_cron_handler(store, session_key):
def handler(args, **kw):
return handle_cron_tool(args, store=store, session_key=session_key, **kw)
return handler
tool_registry.register("cron", make_cron_handler(store, current_session_key))
Thiết kế độc đáo của Hermes Agent
Nhiệm vụ dưới dạng tin nhắn
Hầu hết các hệ thống nhiệm vụ theo lịch trình đều gọi trực tiếp một chức năng khi công việc đến hạn. agent Hermes không -- khi một công việc được kích hoạt, nó sẽ xây dựng một MessageEvent, ngụy trang dưới dạng tin nhắn của người dùng và đưa nó vào Cổng.
Điều này mang lại ba lợi ích:
- Miễn phí toàn bộ khả năng của agent. Lệnh gọi công cụ, khắc phục lỗi, kiểm tra quyền, ủy quyền agent phụ -- tất cả đều tự động có hiệu lực.
- Lịch sử thực thi vẫn tự động tồn tại. Các cuộc hội thoại do tác vụ kích hoạt được lưu trữ trong SQLite, có thể tìm kiếm được giống như các cuộc hội thoại do người dùng bắt đầu.
- Các câu trả lời được định tuyến tự động. Kết quả được gửi lại cho người dùng thông qua nền tảng ban đầu (Telegram / Discord) mà không cần nối dây thêm.
Thiết kế sự cân bằng giữa ba dạng lịch trình
Tại sao không chỉ sử dụng biểu thức cron?
Người dùng nói "nhắc tôi sau 30 phút" thường xuyên hơn nhiều so với "thiết lập cron". Nếu bạn buộc định dạng cron:
- Người dùng nói gì: "trong 30 phút"
- Điều mà agent phải tính: thời gian hiện tại là 14h00, vậy là
30 14 15 1 *(2025-01-15 14:30) - Ngoài ra, bạn phải xử lý việc chuyển đổi ngày, chuyển đổi tháng và múi giờ
Với định dạng độ trễ, đó là một lớp lót: parse_schedule("30m") -> (now + 1800, True).
Giữ các kịch bản đơn giản ở mức đơn giản; sử dụng cron cho những cái phức tạp.
Những lỗi thường gặp của người mới bắt đầu
1. Kiểm tra các tác vụ đã lên lịch trong luồng chính
while True:
user_input = input() # <- blocks
check_cron_jobs() # <- never reached
input() chặn luồng chính nên các tác vụ đã lên lịch sẽ không bao giờ được kích hoạt.
Khắc phục: Đặt bộ lập lịch vào chuỗi riêng của nó.
2. Quên khóa
JobStore được truy cập bởi hai luồng: luồng chính (cron_tool write) và luồng nền (JobScheduler đọc). Không khóa -> từ điển thay đổi kích thước trong quá trình lặp -> RuntimeError.
Khắc phục: Mọi phương thức JobStore đều sử dụng with self._lock.
3. Không xóa các tác vụ một lần
A one_shot=True sa thải công việc nhưng không bị đuổi khỏi cửa hàng. 30 giây sau, bộ lập lịch tìm lại nó và kích hoạt nó lần thứ hai.
Sửa chữa: Trong advance(), các công việc một lần sẽ xuất hiện, không được cập nhật với next_fire mới.
4. Viết sai ngày trong tuần trong biểu thức cron
của Python datetime.weekday() trả về 0=Thứ Hai ... 6=Chủ Nhật. Cron tiêu chuẩn sử dụng 0=Chủ Nhật.
cron's 5 = Friday
Python's 5 = Saturday
Sự khác biệt một chữ số khiến nhiệm vụ thực hiện không đúng ngày.
Khắc phục: Chuyển đổi trong khi phân tích cú pháp bằng (python_weekday + 1) % 7hoặc xử lý ánh xạ khi khớp các trường cron.
5. Không xử lý tham nhũng jobs.json
Quá trình gặp sự cố khi đang viết jobs.json, để lại một tập tin viết được một nửa. Trong lần khởi động tiếp theo, json.loads ném một lỗi.
Khắc phục: Trước tiên ghi vào tệp tạm thời, sau đó đổi tên (thao tác nguyên tử). Khi đọc, bắt json.JSONDecodeError và quay trở lại danh sách trống.
Phạm vi của chương này
Chương này bao gồm ba điều:
- Tại sao cần có các nhiệm vụ theo lịch trình -- xuất phát từ điểm khó khăn là "nhân viên chỉ có thể phản ứng một cách thụ động"
- Ba định dạng lịch trình -- phân tích độ trễ, khoảng thời gian và biểu thức cron
- Nhiệm vụ dưới dạng tin nhắn -- khi một công việc được kích hoạt, nó sẽ được ngụy trang dưới dạng MessageEvent và được đưa vào Cổng
Không được bảo hiểm:
- Lập lịch phân phối (cách ngăn nhiều phiên bản Cổng thực hiện cùng một công việc) -> tối ưu hóa sản xuất
- Chuỗi phụ thuộc nhiệm vụ (công việc B chỉ thực hiện sau khi công việc A hoàn thành) -> quá gần với công cụ xử lý công việc
- Xử lý múi giờ -> hướng dẫn này sử dụng giờ địa phương; môi trường sản xuất cần UTC + múi giờ của người dùng
- Thời gian chờ thực hiện tác vụ và chiến lược thử lại -> đã được khắc phục lỗi của s06
Chương này liên quan đến những chương khác như thế nào
- s02 đã đăng ký cron_tool -> tự đăng ký, giống như mọi công cụ khác
- s03's SQLite -> các cuộc hội thoại được kích hoạt bởi các tác vụ đã lên lịch cũng được lưu trữ trong các phiên
- Khôi phục lỗi của s06 -> nếu vòng lặp agent được kích hoạt bởi một công việc bị lỗi, logic thử lại tương tự sẽ được áp dụng
- s12's Gateway -> khi một công việc được thực hiện, nó sẽ đi qua
_handle_message, cùng đường dẫn với tin nhắn nền tảng - s14's terminal backend -> các lệnh trong công việc sử dụng lại cùng một backend
s15 là chương cuối cùng của Giai đoạn 3. Tại thời điểm này, agent Hermes có thể:
- Nhận tin nhắn từ hàng tá nền tảng nhắn tin (s12-s13)
- Chạy lệnh trong các môi trường thực thi khác nhau (s14)
- Chủ động hành động vào thời điểm trong tương lai (s15)
Giai đoạn 4 chuyển sang các khả năng nâng cao: MCP, tự động hóa trình duyệt, giọng nói và tầm nhìn.
Sau khi kết thúc chương này, bạn sẽ có thể trả lời
- Tại sao tác vụ theo lịch trình lại tạo MessageEvent khi nó kích hoạt thay vì trực tiếp thực thi lệnh?
- Khi người dùng nói "nhắc tôi sau 30 phút", tại sao không phân tích nó thành biểu thức cron?
- Tại sao JobScheduler là một chuỗi chứ không phải là một tác vụ không đồng bộ?
- Đâu là sự khác biệt giữa cách
advance()xử lý các nhiệm vụ một lần so với các nhiệm vụ định kỳ? - Đường dẫn thực hiện công việc khác nhau như thế nào giữa chế độ CLI và chế độ Cổng?
Một câu cần nhớ: Tác vụ đã lên lịch không phải là một đường dẫn thực thi mới -- nó chỉ đơn giản là một cơ chế "giả vờ người dùng đã nói điều gì đó" vào một thời điểm trong tương lai. Từ Cổng trở đi, tất cả logic đều được tái sử dụng hoàn toàn.