feat(hub): add singleton pattern and subagent spawning

Add global Hub singleton for cross-module access by subagent tools.
Add createSubagent() method to Hub for spawning ephemeral child agents
with isSubagent flag and custom system prompts.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
yushen 2026-02-03 16:49:05 +08:00
parent 85f71e0084
commit 5b6a1c6953
2 changed files with 54 additions and 0 deletions

28
src/hub/hub-singleton.ts Normal file
View file

@ -0,0 +1,28 @@
/**
* Global Hub singleton for cross-module access.
*
* Used by subagent tools and announce flow to interact with the Hub
* without threading references through the entire call chain.
*/
import type { Hub } from "./hub.js";
let _hub: Hub | undefined;
/** Set the global Hub instance. Called once during Hub construction. */
export function setHub(hub: Hub): void {
_hub = hub;
}
/** Get the global Hub instance. Throws if not yet initialized. */
export function getHub(): Hub {
if (!_hub) {
throw new Error("[Hub] Hub singleton not initialized. Ensure Hub is constructed before accessing.");
}
return _hub;
}
/** Check if the Hub singleton has been initialized. */
export function isHubInitialized(): boolean {
return _hub !== undefined;
}

View file

@ -9,7 +9,9 @@ import {
type ResponseErrorPayload,
} from "@multica/sdk";
import { AsyncAgent } from "../agent/async-agent.js";
import type { AgentOptions } from "../agent/types.js";
import { getHubId } from "./hub-identity.js";
import { setHub } from "./hub-singleton.js";
import { loadAgentRecords, addAgentRecord, removeAgentRecord } from "./agent-store.js";
import { RpcDispatcher, RpcError } from "./rpc/dispatcher.js";
import { createGetAgentMessagesHandler } from "./rpc/handlers/get-agent-messages.js";
@ -48,6 +50,9 @@ export class Hub {
this.rpc.register("deleteAgent", createDeleteAgentHandler(this));
this.rpc.register("updateGateway", createUpdateGatewayHandler(this));
// Register as global singleton for cross-module access (subagent tools, announce flow)
setHub(this);
this.client = this.createClient(this.url);
this.client.connect();
this.restoreAgents();
@ -243,6 +248,27 @@ export class Hub {
}
}
/** Create a subagent with specific options (isSubagent, systemPrompt, model) */
createSubagent(sessionId: string, options: Omit<AgentOptions, "sessionId"> = {}): AsyncAgent {
const existing = this.agents.get(sessionId);
if (existing && !existing.closed) {
return existing;
}
const agent = new AsyncAgent({
...options,
sessionId,
isSubagent: true,
});
this.agents.set(agent.sessionId, agent);
// Subagents are ephemeral — don't persist to agent store
void this.consumeAgent(agent);
console.log(`[Hub] Subagent created: ${agent.sessionId}`);
return agent;
}
getAgent(id: string): AsyncAgent | undefined {
return this.agents.get(id);
}