feat(discord): show subagent progress (#95604)

* feat(discord): show subagent progress

Co-authored-by: Kyle Klouzal <kklouzal@users.noreply.github.com>

* fix(discord): serialize progress cleanup ownership

Co-authored-by: Kyle Klouzal <kklouzal@users.noreply.github.com>

* test(discord): type progress state fixtures

* refactor(discord): split subagent progress state

* fix(discord): persist terminal progress outcomes

* fix(discord): narrow persisted cleanup rows

* refactor(discord): keep progress internals private

* perf(discord): preserve lazy progress loading

* chore(plugin-sdk): refresh API baseline after rebase

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: Kyle Klouzal <kklouzal@users.noreply.github.com>
This commit is contained in:
Kyle Klouzal
2026-07-17 04:02:33 -07:00
committed by GitHub
parent b2e34b03b5
commit e316e1c440
32 changed files with 2465 additions and 39 deletions
@@ -31,7 +31,12 @@ const BUNDLED_TYPED_HOOK_REGISTRATION_GUARDS = {
"extensions/active-memory/index.ts": ["before_prompt_build"],
"extensions/codex/index.ts": ["after_compaction", "inbound_claim", "session_end"],
"extensions/diffs/src/plugin.ts": ["before_prompt_build"],
"extensions/discord/subagent-hooks-api.ts": ["subagent_delivery_target", "subagent_ended"],
"extensions/discord/subagent-hooks-api.ts": [
"gateway_start",
"subagent_delivery_target",
"subagent_ended",
"subagent_progress",
],
"extensions/feishu/subagent-hooks-api.ts": ["subagent_delivery_target", "subagent_ended"],
"extensions/matrix/subagent-hooks-api.ts": ["subagent_delivery_target", "subagent_ended"],
"extensions/memory-core/src/dreaming.ts": ["before_agent_reply", "gateway_start", "gateway_stop"],
+34 -6
View File
@@ -109,6 +109,7 @@ export type PluginHookName =
| "subagent_spawning"
| "subagent_delivery_target"
| "subagent_spawned"
| "subagent_progress"
| "subagent_ended"
/** @deprecated Use gateway_stop. */
| "deactivate"
@@ -153,6 +154,7 @@ const PLUGIN_HOOK_NAMES = [
"subagent_spawning",
"subagent_delivery_target",
"subagent_spawned",
"subagent_progress",
"subagent_ended",
"deactivate",
"gateway_start",
@@ -747,17 +749,23 @@ export type PluginHookSubagentContext = {
type PluginHookSubagentTargetKind = "subagent" | "acp";
type PluginHookSubagentRequester = {
channel?: string;
accountId?: string;
to?: string;
threadId?: string | number;
/** Native source channel/conversation id, when distinct from the routable target. */
channelId?: string | number;
/** Native source message that initiated the parent run, when available. */
messageId?: string | number;
};
type PluginHookSubagentSpawnBase = {
childSessionKey: string;
agentId: string;
label?: string;
mode: "run" | "session";
requester?: {
channel?: string;
accountId?: string;
to?: string;
threadId?: string | number;
};
requester?: PluginHookSubagentRequester;
threadRequested: boolean;
};
@@ -835,6 +843,22 @@ export type PluginHookSubagentSpawnedEvent = PluginHookSubagentSpawnBase & {
resolvedProvider?: string;
};
/** Portable channel presentation signal for one background child run. */
export type PluginHookSubagentProgressEvent =
| {
phase: "started";
runId: string;
childSessionKey: string;
requester?: PluginHookSubagentRequester;
}
| {
phase: "ended";
runId: string;
childSessionKey: string;
outcome: "ok" | "error" | "timeout" | "killed" | "unknown";
requester?: PluginHookSubagentRequester;
};
export type PluginHookSubagentEndedEvent = {
targetSessionKey: string;
targetKind: PluginHookSubagentTargetKind;
@@ -1246,6 +1270,10 @@ export type PluginHookHandlerMap = {
event: PluginHookSubagentSpawnedEvent,
ctx: PluginHookSubagentContext,
) => Promise<void> | void;
subagent_progress: (
event: PluginHookSubagentProgressEvent,
ctx: PluginHookSubagentContext,
) => Promise<void> | void;
subagent_ended: (
event: PluginHookSubagentEndedEvent,
ctx: PluginHookSubagentContext,
+15 -1
View File
@@ -80,6 +80,7 @@ import type {
PluginHookSubagentSpawningEvent,
PluginHookSubagentSpawningResult,
PluginHookSubagentEndedEvent,
PluginHookSubagentProgressEvent,
PluginHookSubagentSpawnedEvent,
PluginHookToolContext,
PluginHookToolResultPersistContext,
@@ -1448,6 +1449,14 @@ export function createHookRunner(
return runVoidHook("subagent_spawned", event, ctx);
}
/** Run portable subagent progress presentation hooks. */
async function runSubagentProgress(
event: PluginHookSubagentProgressEvent,
ctx: PluginHookSubagentContext,
): Promise<void> {
return runVoidHook("subagent_progress", event, ctx);
}
/**
* Run subagent_ended hook.
* Runs in parallel (fire-and-forget).
@@ -1621,6 +1630,7 @@ export function createHookRunner(
runSubagentSpawning,
runSubagentDeliveryTarget,
runSubagentSpawned,
runSubagentProgress,
runSubagentEnded,
// Gateway hooks
runGatewayStart,
@@ -1641,6 +1651,10 @@ export type HookRunner = ReturnType<typeof createHookRunner>;
export type SubagentLifecycleHookRunner = Pick<
HookRunner,
"hasHooks" | "runSubagentSpawning" | "runSubagentSpawned" | "runSubagentEnded"
| "hasHooks"
| "runSubagentSpawning"
| "runSubagentSpawned"
| "runSubagentProgress"
| "runSubagentEnded"
>;
/* oxlint-disable max-lines -- TODO: split this grandfathered oversized file. */
+20 -4
View File
@@ -1,5 +1,5 @@
/**
* Test: subagent_spawning, subagent_delivery_target, subagent_spawned & subagent_ended hook wiring
* Test: subagent spawning, routing, progress, and terminal hook wiring.
*/
import { describe, expect, it, vi } from "vitest";
import { addStaticTestHooks, createHookRunnerWithRegistry } from "./hooks.test-fixtures.js";
@@ -22,6 +22,7 @@ describe("subagent hook runner methods", () => {
hookName:
| "subagent_spawning"
| "subagent_spawned"
| "subagent_progress"
| "subagent_delivery_target"
| "subagent_ended";
event: Record<string, unknown>;
@@ -38,9 +39,11 @@ describe("subagent hook runner methods", () => {
? await runner.runSubagentSpawning(params.event as never, params.ctx as never)
: params.hookName === "subagent_spawned"
? await runner.runSubagentSpawned(params.event as never, params.ctx as never)
: params.hookName === "subagent_delivery_target"
? await runner.runSubagentDeliveryTarget(params.event as never, params.ctx as never)
: await runner.runSubagentEnded(params.event as never, params.ctx as never);
: params.hookName === "subagent_progress"
? await runner.runSubagentProgress(params.event as never, params.ctx as never)
: params.hookName === "subagent_delivery_target"
? await runner.runSubagentDeliveryTarget(params.event as never, params.ctx as never)
: await runner.runSubagentEnded(params.event as never, params.ctx as never);
expect(handler).toHaveBeenCalledWith(params.event, params.ctx);
return result;
@@ -81,6 +84,18 @@ describe("subagent hook runner methods", () => {
},
ctx: baseSubagentCtx,
},
{
name: "runSubagentProgress invokes registered subagent_progress hooks",
hookName: "subagent_progress" as const,
methodName: "runSubagentProgress" as const,
event: {
phase: "started" as const,
runId: "run-1",
childSessionKey: "agent:main:subagent:child",
requester: { ...baseRequester, messageId: "message-1" },
},
ctx: baseSubagentCtx,
},
{
name: "runSubagentDeliveryTarget invokes registered subagent_delivery_target hooks",
hookName: "subagent_delivery_target" as const,
@@ -160,6 +175,7 @@ describe("subagent hook runner methods", () => {
expect(runner.hasHooks("subagent_spawning")).toBe(true);
expect(runner.hasHooks("subagent_delivery_target")).toBe(true);
expect(runner.hasHooks("subagent_spawned")).toBe(false);
expect(runner.hasHooks("subagent_progress")).toBe(false);
expect(runner.hasHooks("subagent_ended")).toBe(false);
});