Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions s13_agent_teams/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,9 @@ idle_notification: "Waiting for more work."

IDLE のチームメイトは終了しない。直接メッセージか ready task を受けると WORK に戻り、`shutdown_request` を受けると段階的な shutdown handshake を始める。

### 6. IDLE は受信箱を先に確認し、その後 ready task を探す
### 6. IDLE は受信箱を確認し、自分の作業を再開してから ready task を探す

IDLE ではメッセージを優先し、その後に共有タスクボードを確認する
IDLE ではメッセージを優先する。メッセージがなければ、自分の未完了 assignment を再開してから、共有タスクボードで新しい作業を探す

```python
while True:
Expand All @@ -182,16 +182,20 @@ while True:
break
continue

task = claim_next_task(name)
task = resumable_task(name)
resuming = task is not None
if not task:
task = claim_next_task(name)
if task:
label = "Resume" if resuming else "Auto-claimed"
messages.append({
"role": "user",
"content": f"[Auto-claimed task {task.id}] {task.subject}",
"content": f"[{label} task {task.id}] {task.subject}",
})
break
```

shutdown、計画承認、Lead からの直接指示は、空き時間に見つけた仕事より先に扱う。メッセージも ready task もなければ、チームメイトは IDLE を続ける。別のチームメイトが前提タスクを完了すると、blocked task が ready になることもある。
shutdown、計画承認、Lead からの直接指示は task work より先に扱う。未完了 assignment も空き時間に見つけた仕事より優先し、IDLE のチームメイトが自分の進行中 task を取り残さないようにする。メッセージ、未完了 assignment、ready task のいずれもなければ、チームメイトは IDLE を続ける。別のチームメイトが前提タスクを完了すると、blocked task が ready になることもある。

### 7. 発見と Claim を分け、Claim はアトミックに行う

Expand Down
15 changes: 10 additions & 5 deletions s13_agent_teams/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,9 +169,10 @@ idle_notification: "Waiting for more work."

An idle teammate does not exit. A direct message or a ready task returns it to WORK; a `shutdown_request` starts a graceful shutdown handshake.

### 6. IDLE checks the mailbox before looking for ready tasks
### 6. IDLE checks the mailbox, resumes owned work, then looks for ready tasks

IDLE gives messages priority, then checks the shared task board:
IDLE gives messages priority. If no message arrives, it resumes the teammate's
unfinished assignment before checking the shared task board for new work:

```python
while True:
Expand All @@ -182,16 +183,20 @@ while True:
break
continue

task = claim_next_task(name)
task = resumable_task(name)
resuming = task is not None
if not task:
task = claim_next_task(name)
if task:
label = "Resume" if resuming else "Auto-claimed"
messages.append({
"role": "user",
"content": f"[Auto-claimed task {task.id}] {task.subject}",
"content": f"[{label} task {task.id}] {task.subject}",
})
break
```

Shutdown, plan approval, and direct instructions from Lead should arrive before opportunistic work. If there is no message and no ready task, the teammate remains IDLE. A blocked task may become ready after another teammate completes its prerequisite.
Shutdown, plan approval, and direct instructions from Lead should arrive before task work. An unfinished assignment stays ahead of opportunistic work so an IDLE teammate cannot strand its own in-progress task. If there is no message, unfinished assignment, or ready task, the teammate remains IDLE. A blocked task may become ready after another teammate completes its prerequisite.

### 7. Discovery and claim are separate, and claim is atomic

Expand Down
14 changes: 9 additions & 5 deletions s13_agent_teams/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,9 @@ idle_notification: "Waiting for more work."

空闲队友不会退出。直接消息或 ready task 会让它回到 WORK,`shutdown_request` 则会启动平滑关机握手。

### 6. IDLE 先看收件箱,再找 ready task
### 6. IDLE 先看收件箱,再恢复自己的任务,最后找 ready task

队友进入 IDLE 后优先处理消息,然后检查共享任务板
队友进入 IDLE 后优先处理消息。没有消息时,先恢复自己尚未完成的 assignment,再到共享任务板寻找新工作

```python
while True:
Expand All @@ -181,16 +181,20 @@ while True:
break
continue

task = claim_next_task(name)
task = resumable_task(name)
resuming = task is not None
if not task:
task = claim_next_task(name)
if task:
label = "Resume" if resuming else "Auto-claimed"
messages.append({
"role": "user",
"content": f"[Auto-claimed task {task.id}] {task.subject}",
"content": f"[{label} task {task.id}] {task.subject}",
})
break
```

关机、计划审批和 Lead 的直接指令应该先于临时发现的工作。如果没有消息,也没有 ready task,队友会保持 IDLE。前置任务完成后,当前受阻的任务可能变为 ready。
关机、计划审批和 Lead 的直接指令应该先于任务工作。尚未完成的 assignment 又优先于临时发现的工作,避免 IDLE 队友遗留自己进行中的任务。如果没有消息、未完成的 assignment 或 ready task,队友会保持 IDLE。前置任务完成后,当前受阻的任务可能变为 ready。

### 7. 发现和认领分成两步,认领必须原子执行

Expand Down
19 changes: 15 additions & 4 deletions s13_agent_teams/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,12 @@ def scan_unclaimed_tasks() -> list[Task]:
return ready


def resumable_task(owner: str) -> Task | None:
"""Return the owner's unfinished Task before looking for new work."""
with task_lock:
return _owner_in_progress(owner)


def claim_next_task(name: str) -> Task | None:
"""Claim the first still-available task, never a second assignment."""
with task_lock:
Expand Down Expand Up @@ -1314,7 +1320,7 @@ def work(self) -> str:
return "idle"

def wait_for_work(self) -> bool:
"""Wait for a message or atomically claim the next ready Task."""
"""Wait for a message, resume owned work, or claim a ready Task."""
while True:
inbox = BUS.wait_for_messages(self.name, IDLE_SCAN_INTERVAL)
if inbox:
Expand All @@ -1325,18 +1331,23 @@ def wait_for_work(self) -> bool:
return True
continue

task = claim_next_task(self.name)
task = resumable_task(self.name)
resuming = task is not None
if not task:
task = claim_next_task(self.name)
if not task:
continue
cwd = assignment_cwd(self.name)
self.messages.append({
"role": "user",
"content": (
f"[Auto-claimed task {task.id}] {task.subject}\n"
f"[{'Resume' if resuming else 'Auto-claimed'} task "
f"{task.id}] {task.subject}\n"
f"{task.description}\nWork directory: {cwd}"
),
})
print(f" [idle] {self.name} claimed {task.id}: {task.subject}")
action = "resuming" if resuming else "claimed"
print(f" [idle] {self.name} {action} {task.id}: {task.subject}")
return True

def run(self):
Expand Down
2 changes: 1 addition & 1 deletion s15_integrated_harness/README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ S15 には 2 層の plan がある:
S15 には 2 種類の delegation がある:

- `task`: one-shot subagent。独立した `messages[]` を使い、中間 context を捨て、final summary だけ返す。
- `spawn_teammate`: persistent teammate thread。ready `task_id` を渡すと、runtime は thread 開始前に Claim する。省略した場合、teammate は IDLE で後続 Task を待てる。assignment がない teammate は file tool と Shell tool を使えない。固定の tool round 上限なしで `WORK → result → IDLE` を続け、model または dispatch の失敗は `error` を送り、thread cleanup は未完了 assignment を task board へ戻す。model call の前には毎回 inbox を読み、direct message や shutdown request が連続する tool-use round の後ろで待ち続けないようにする。idle 中はまず `MessageBus` を待ち、timeout 後だけ ready task を scan して最大 1 件を atomic に claim する。
- `spawn_teammate`: persistent teammate thread。ready `task_id` を渡すと、runtime は thread 開始前に Claim する。省略した場合、teammate は IDLE で後続 Task を待てる。assignment がない teammate は file tool と Shell tool を使えない。固定の tool round 上限なしで `WORK → result → IDLE` を続け、model または dispatch の失敗は `error` を送り、thread cleanup は未完了 assignment を task board へ戻す。model call の前には毎回 inbox を読み、direct message や shutdown request が連続する tool-use round の後ろで待ち続けないようにする。idle 中はまず `MessageBus` を待ち、timeout 後は自分の未完了 assignment を再開してから ready task を scan し、最大 1 件を atomic に claim する。

Lead は teammate を起動した後、model loop 内で status を繰り返し確認せず、現在の turn を終了する。Lead の受信箱に team event が入ると runtime が次の turn を開始する。

Expand Down
2 changes: 1 addition & 1 deletion s15_integrated_harness/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ Task graph construction remains two-phase in the integrated host: the Lead creat
S15 has two kinds of delegation:

- `task`: one-shot subagent. It uses an isolated `messages[]`, discards intermediate context, and returns only a final summary.
- `spawn_teammate`: persistent teammate thread. When given a ready `task_id`, the runtime claims it before the thread starts; without one, the teammate can wait in IDLE for later work. A teammate without an assignment cannot use file or Shell tools. It follows `WORK → result → IDLE` without a fixed tool-round cap; model or dispatch failures emit an `error`, and thread cleanup releases an unfinished assignment back to the task board. It drains its inbox before every model call, so direct messages and shutdown requests cannot wait behind an unbroken tool-use sequence. While idle it waits for `MessageBus` delivery first, then scans ready tasks only after the wait times out and atomically claims at most one.
- `spawn_teammate`: persistent teammate thread. When given a ready `task_id`, the runtime claims it before the thread starts; without one, the teammate can wait in IDLE for later work. A teammate without an assignment cannot use file or Shell tools. It follows `WORK → result → IDLE` without a fixed tool-round cap; model or dispatch failures emit an `error`, and thread cleanup releases an unfinished assignment back to the task board. It drains its inbox before every model call, so direct messages and shutdown requests cannot wait behind an unbroken tool-use sequence. While idle it waits for `MessageBus` delivery first, resumes its own unfinished assignment after the wait times out, and only then scans ready tasks and atomically claims at most one.

After spawning a teammate, Lead ends the current turn instead of repeatedly querying its status inside the model loop. A team event in Lead's mailbox makes the runtime start the next turn.

Expand Down
2 changes: 1 addition & 1 deletion s15_integrated_harness/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ S15 同时保留两层计划:
S15 有两种 delegation:

- `task`:一次性 subagent。独立 `messages[]`,中间过程丢弃,只返回最终摘要。
- `spawn_teammate`:持久队友线程。传入 ready `task_id` 时,运行时会在线程启动前完成认领;不传时,队友可以在 IDLE 中等待后续任务。没有 assignment 的队友不能使用文件或 Shell 工具。它按 `WORK → result → IDLE` 运行,不设固定的工具轮数上限;模型或分发失败会发出 `error`,线程清理会把未完成 assignment 释放回任务板。每次调用模型前都会先读取收件箱,因此直接消息和关机请求不会被连续的 tool-use 轮次饿死。idle 时先等待 `MessageBus` 消息,只在超时后扫描就绪 task,并以原子操作最多认领一个。
- `spawn_teammate`:持久队友线程。传入 ready `task_id` 时,运行时会在线程启动前完成认领;不传时,队友可以在 IDLE 中等待后续任务。没有 assignment 的队友不能使用文件或 Shell 工具。它按 `WORK → result → IDLE` 运行,不设固定的工具轮数上限;模型或分发失败会发出 `error`,线程清理会把未完成 assignment 释放回任务板。每次调用模型前都会先读取收件箱,因此直接消息和关机请求不会被连续的 tool-use 轮次饿死。idle 时先等待 `MessageBus` 消息,超时后先恢复自己尚未完成的 assignment,最后才扫描就绪 task,并以原子操作最多认领一个。

Lead 启动队友后结束当前轮次,不在模型循环里反复查询状态。队友事件进入 Lead 收件箱后,运行时会自动唤醒下一轮。

Expand Down
17 changes: 14 additions & 3 deletions s15_integrated_harness/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,12 @@ def scan_unclaimed_tasks() -> list[Task]:
return ready


def resumable_task(owner: str) -> Task | None:
"""Return the owner's unfinished Task before looking for new work."""
with task_lock:
return _owner_in_progress(owner)


def claim_next_task(name: str) -> Task | None:
"""Claim the first still-available task, never a second assignment."""
with task_lock:
Expand Down Expand Up @@ -1601,7 +1607,10 @@ def _run_complete_task(task_id: str):
break
continue

task = claim_next_task(name)
task = resumable_task(name)
resuming = task is not None
if not task:
task = claim_next_task(name)
if not task:
continue
try:
Expand All @@ -1611,12 +1620,14 @@ def _run_complete_task(task_id: str):
messages.append({
"role": "user",
"content": (
f"[Auto-claimed task {task.id}] "
f"[{'Resume' if resuming else 'Auto-claimed'} task "
f"{task.id}] "
f"{task.subject}\n{task.description}\n"
f"Work directory: {workdir}"
),
})
print(f" \033[32m[idle] {name} claimed "
action = "resuming" if resuming else "claimed"
print(f" \033[32m[idle] {name} {action} "
f"{task.id}: {task.subject}\033[0m")
break

Expand Down
43 changes: 43 additions & 0 deletions tests/test_agent_teams_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -1508,6 +1508,49 @@ def claim(name):
if result is not None)
self.assertEqual(lesson.load_task(task.id).owner, winner)

def test_idle_teammate_resumes_owned_task_before_claiming_new_work(self):
for lesson_path in RUNTIME_LESSONS:
with self.subTest(lesson=lesson_path.parent.name):
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp), lesson_path)
lesson.IDLE_SCAN_INTERVAL = 0.01
owned = lesson.create_task("Finish the report")
pending = lesson.create_task("Start a different task")
calls = []

def respond(**kwargs):
calls.append(kwargs["messages"][-1]["content"])
return types.SimpleNamespace(
stop_reason="end_turn",
content=[types.SimpleNamespace(
type="text", text="Work is not complete yet."
)],
)

lesson.client.messages.create = respond
lesson.spawn_teammate_thread(
"alice", "writer", "Continue your work.",
task_id=owned.id,
)

self.assertTrue(wait_until(lambda: len(calls) >= 2))
self.assertIn(f"[Resume task {owned.id}]", str(calls[1]))
still_owned = lesson.load_task(owned.id)
self.assertEqual(still_owned.status, "in_progress")
self.assertEqual(still_owned.owner, "alice")
self.assertEqual(
lesson.teammate_assignments["alice"]["task_id"],
owned.id,
)
untouched = lesson.load_task(pending.id)
self.assertEqual(untouched.status, "pending")
self.assertIsNone(untouched.owner)

lesson.run_request_shutdown("alice")
self.assertTrue(wait_until(
lambda: "alice" not in lesson.active_teammates
))

def test_assignment_enforces_one_task_and_owner_only_completion(self):
with tempfile.TemporaryDirectory() as tmp:
lesson = load_lesson(Path(tmp))
Expand Down
12 changes: 6 additions & 6 deletions web/src/data/generated/docs.json

Large diffs are not rendered by default.

Loading