refactor(agents): bind subagent threads in core (#88416) · openclaw/openclaw@3fc0df9
steipete
·
2026-05-31
·
via Recent Commits to openclaw:main
| Original file line number | Diff line number | Diff line change |
|---|
@@ -141,7 +141,9 @@ observation-only.
|
141 | 141 | |
142 | 142 | **Subagents** |
143 | 143 | |
144 | | -- `subagent_spawning` / `subagent_delivery_target` / `subagent_spawned` / `subagent_ended` - coordinate subagent routing and completion delivery |
| 144 | +- `subagent_spawned` / `subagent_ended` - observe subagent launch and completion. |
| 145 | +- `subagent_delivery_target` - compatibility hook for completion delivery when no core session binding can project a route. |
| 146 | +- `subagent_spawning` - deprecated compatibility hook. Core now prepares `thread: true` subagent bindings through channel session-binding adapters before `subagent_spawned` fires. |
145 | 147 | - `subagent_spawned` includes `resolvedModel` and `resolvedProvider` when OpenClaw has resolved the child session's native model before launch. |
146 | 148 | |
147 | 149 | **Lifecycle** |
@@ -464,6 +466,10 @@ before the next major release:
|
464 | 466 | - **`before_agent_start`** remains for compatibility. New plugins should use |
465 | 467 | `before_model_resolve` and `before_prompt_build` instead of the combined |
466 | 468 | phase. |
| 469 | +- **`subagent_spawning`** remains for compatibility with older plugins, but |
| 470 | + new plugins should not return thread routing from it. Core prepares |
| 471 | +`thread: true` subagent bindings through channel session-binding adapters |
| 472 | + before `subagent_spawned` fires. |
467 | 473 | - **`deactivate`** remains as a deprecated cleanup compatibility alias until |
468 | 474 | after 2026-08-16. New plugins should use `gateway_stop`. |
469 | 475 | - **`onResolution` in `before_tool_call`** now uses the typed |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -792,6 +792,35 @@ canonical replacement.
|
792 | 792 | |
793 | 793 | </Accordion> |
794 | 794 | |
| 795 | +<Accordion title="subagent_spawning hook → core thread binding"> |
| 796 | +**Old**: `api.on("subagent_spawning", handler)` returning |
| 797 | +`threadBindingReady` or `deliveryOrigin`. |
| 798 | + |
| 799 | +**New**: let core prepare `thread: true` subagent bindings through the |
| 800 | +channel session-binding adapter. Use `api.on("subagent_spawned", handler)` |
| 801 | +only for post-launch observation. |
| 802 | + |
| 803 | +```typescript |
| 804 | +// Before |
| 805 | +api.on("subagent_spawning", async () => ({ |
| 806 | + status: "ok", |
| 807 | + threadBindingReady: true, |
| 808 | + deliveryOrigin: { channel: "discord", to: "channel:123", threadId: "456" }, |
| 809 | +})); |
| 810 | + |
| 811 | +// After |
| 812 | +api.on("subagent_spawned", async (event) => { |
| 813 | + await observeSubagentLaunch(event); |
| 814 | +}); |
| 815 | +``` |
| 816 | + |
| 817 | +`subagent_spawning`, `PluginHookSubagentSpawningEvent`, |
| 818 | +`PluginHookSubagentSpawningResult`, and |
| 819 | +`SubagentLifecycleHookRunner.runSubagentSpawning(...)` remain only as |
| 820 | +deprecated compatibility surfaces while external plugins migrate. |
| 821 | + |
| 822 | +</Accordion> |
| 823 | + |
795 | 824 | <Accordion title="Provider discovery types → provider catalog types"> |
796 | 825 | Four discovery type aliases are now thin wrappers over the |
797 | 826 | catalog-era types: |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -291,14 +291,12 @@ same sub-agent session.
|
291 | 291 | |
292 | 292 | ### Thread supporting channels |
293 | 293 | |
294 | | -**Discord** is currently the only supported channel. It supports |
295 | | -persistent thread-bound subagent sessions (`sessions_spawn` with |
296 | | -`thread: true`), manual thread controls (`/focus`, `/unfocus`, `/agents`, |
297 | | -`/session idle`, `/session max-age`), and adapter keys |
298 | | -`channels.discord.threadBindings.enabled`, |
299 | | -`channels.discord.threadBindings.idleHours`, |
300 | | -`channels.discord.threadBindings.maxAgeHours`, and |
301 | | -`channels.discord.threadBindings.spawnSessions`. |
| 294 | +Any channel with a session-binding adapter can support persistent |
| 295 | +thread-bound subagent sessions (`sessions_spawn` with `thread: true`). |
| 296 | +Bundled adapters currently include Discord threads, Matrix threads, |
| 297 | +Telegram forum topics, and current-conversation bindings for Feishu. |
| 298 | +Use the per-channel `threadBindings` config keys for enablement, |
| 299 | +timeouts, and `spawnSessions`. |
302 | 300 | |
303 | 301 | ### Quick flow |
304 | 302 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -4,6 +4,7 @@ import {
|
4 | 4 | } from "openclaw/plugin-sdk/channel-test-helpers"; |
5 | 5 | import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core"; |
6 | 6 | import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; |
| 7 | +import { handleDiscordSubagentSpawning } from "./subagent-hooks.js"; |
7 | 8 | |
8 | 9 | type ThreadBindingRecord = { |
9 | 10 | accountId: string; |
@@ -85,7 +86,10 @@ function registerHandlersForTest(
|
85 | 86 | ) { |
86 | 87 | return registerHookHandlersForTest<OpenClawPluginApi>({ |
87 | 88 | config, |
88 | | -register: registerDiscordSubagentHooks, |
| 89 | +register: (api) => { |
| 90 | +registerDiscordSubagentHooks(api); |
| 91 | +api.on("subagent_spawning", (event) => handleDiscordSubagentSpawning(api, event)); |
| 92 | +}, |
89 | 93 | }); |
90 | 94 | } |
91 | 95 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -12,10 +12,6 @@ function loadDiscordSubagentHooksModule() {
|
12 | 12 | // Subagent hooks live behind a dedicated barrel so the bundled entry can |
13 | 13 | // register one stable hook wiring path while keeping the handler module lazy. |
14 | 14 | export function registerDiscordSubagentHooks(api: OpenClawPluginApi): void { |
15 | | -api.on("subagent_spawning", async (event) => { |
16 | | -const { handleDiscordSubagentSpawning } = await loadDiscordSubagentHooksModule(); |
17 | | -return await handleDiscordSubagentSpawning(api, event); |
18 | | -}); |
19 | 15 | api.on("subagent_ended", async (event) => { |
20 | 16 | const { handleDiscordSubagentEnded } = await loadDiscordSubagentHooksModule(); |
21 | 17 | handleDiscordSubagentEnded(event); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -5,6 +5,7 @@ import {
|
5 | 5 | import { beforeEach, describe, expect, it } from "vitest"; |
6 | 6 | import type { ClawdbotConfig, OpenClawPluginApi } from "../runtime-api.js"; |
7 | 7 | import { registerFeishuSubagentHooks } from "../subagent-hooks-api.js"; |
| 8 | +import { handleFeishuSubagentSpawning } from "./subagent-hooks.js"; |
8 | 9 | import { |
9 | 10 | createFeishuThreadBindingManager, |
10 | 11 | testing as threadBindingTesting, |
@@ -18,7 +19,10 @@ const baseConfig: ClawdbotConfig = {
|
18 | 19 | function registerHandlersForTest(config: Record<string, unknown> = baseConfig) { |
19 | 20 | return registerHookHandlersForTest<OpenClawPluginApi>({ |
20 | 21 | config, |
21 | | -register: registerFeishuSubagentHooks, |
| 22 | +register: (api) => { |
| 23 | +registerFeishuSubagentHooks(api); |
| 24 | +api.on("subagent_spawning", (event, ctx) => handleFeishuSubagentSpawning(event, ctx)); |
| 25 | +}, |
22 | 26 | }); |
23 | 27 | } |
24 | 28 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -10,10 +10,6 @@ function loadFeishuSubagentHooksModule() {
|
10 | 10 | } |
11 | 11 | |
12 | 12 | export function registerFeishuSubagentHooks(api: OpenClawPluginApi): void { |
13 | | -api.on("subagent_spawning", async (event, ctx) => { |
14 | | -const { handleFeishuSubagentSpawning } = await loadFeishuSubagentHooksModule(); |
15 | | -return await handleFeishuSubagentSpawning(event, ctx); |
16 | | -}); |
17 | 13 | api.on("subagent_delivery_target", async (event) => { |
18 | 14 | const { handleFeishuSubagentDeliveryTarget } = await loadFeishuSubagentHooksModule(); |
19 | 15 | return handleFeishuSubagentDeliveryTarget(event); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -135,17 +135,14 @@ describe("matrix plugin", () => {
|
135 | 135 | |
136 | 136 | expect(runtimeMocks.ensureMatrixCryptoRuntime).not.toHaveBeenCalled(); |
137 | 137 | expect(on.mock.calls.map(([hookName]) => hookName)).toEqual([ |
138 | | -"subagent_spawning", |
139 | 138 | "subagent_ended", |
140 | 139 | "subagent_delivery_target", |
141 | 140 | ]); |
142 | 141 | const handlers = Object.fromEntries(on.mock.calls); |
143 | | -await expect(handlers.subagent_spawning({ id: "spawn" })).resolves.toBe("spawned"); |
144 | 142 | await expect(handlers.subagent_ended({ id: "ended" })).resolves.toBeUndefined(); |
145 | 143 | await expect(handlers.subagent_delivery_target({ id: "target" })).resolves.toBe( |
146 | 144 | "delivery-target", |
147 | 145 | ); |
148 | | -expect(runtimeMocks.handleMatrixSubagentSpawning).toHaveBeenCalledWith(api, { id: "spawn" }); |
149 | 146 | expect(runtimeMocks.handleMatrixSubagentEnded).toHaveBeenCalledWith({ id: "ended" }); |
150 | 147 | expect(runtimeMocks.handleMatrixSubagentDeliveryTarget).toHaveBeenCalledWith({ id: "target" }); |
151 | 148 | }); |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -55,7 +55,10 @@ const fakeApi = { config: {} } as never;
|
55 | 55 | function registerHandlersForTest(config: Record<string, unknown> = {}) { |
56 | 56 | return registerHookHandlersForTest<MatrixEntryPluginApi>({ |
57 | 57 | config, |
58 | | -register: registerMatrixSubagentHooks, |
| 58 | +register: (api) => { |
| 59 | +registerMatrixSubagentHooks(api); |
| 60 | +api.on("subagent_spawning", (event) => handleMatrixSubagentSpawning(api, event)); |
| 61 | +}, |
59 | 62 | }); |
60 | 63 | } |
61 | 64 | |
|
| Original file line number | Diff line number | Diff line change |
|---|
@@ -10,10 +10,6 @@ function loadMatrixSubagentHooksModule() {
|
10 | 10 | } |
11 | 11 | |
12 | 12 | export function registerMatrixSubagentHooks(api: OpenClawPluginApi): void { |
13 | | -api.on("subagent_spawning", async (event) => { |
14 | | -const { handleMatrixSubagentSpawning } = await loadMatrixSubagentHooksModule(); |
15 | | -return await handleMatrixSubagentSpawning(api, event); |
16 | | -}); |
17 | 13 | api.on("subagent_ended", async (event) => { |
18 | 14 | const { handleMatrixSubagentEnded } = await loadMatrixSubagentHooksModule(); |
19 | 15 | await handleMatrixSubagentEnded(event); |
|
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。