# How the Intercom Bridge Enables Child-to-Parent Communication in pi-subagents

> Learn how the intercom bridge facilitates child-to-parent communication in pi-subagents by translating events and forwarding data to the parent orchestrator.

- Repository: [Nico Bailon/pi-subagents](https://github.com/nicobailon/pi-subagents)
- Tags: internals
- Published: 2026-06-01

---

**The intercom bridge enables child-to-parent communication by listening for intercom tool events on the child subagent’s session, translating the generic "orchestrator" target into the child’s specific session name, and forwarding the payload to the parent orchestrator’s pipe.**

In the pi-subagents framework, hierarchical agent workflows require that child subagents stream status updates and data back to their parent orchestrators. The intercom bridge facilitates this communication by acting as a bidirectional gateway that wires the child’s intercom target to the parent’s listening pipe, ensuring messages flow securely across the agent boundary.

## Architecture of the Intercom Bridge

### Configuration and Extension Loading

Before any messages can flow, the bridge must resolve its runtime configuration. The `resolveIntercomBridge` function in [`src/intercom/intercom-bridge.ts`](https://github.com/nicobailon/pi-subagents/blob/main/src/intercom/intercom-bridge.ts) reads the repository-wide configuration from [`intercom/config.json`](https://github.com/nicobailon/pi-subagents/blob/main/intercom/config.json) and dynamically imports the `pi-intercom` extension from [`extensions/pi-intercom/index.ts`](https://github.com/nicobailon/pi-subagents/blob/main/extensions/pi-intercom/index.ts).

If the configuration sets `enabled: true` and the extension exposes an `intercom` object, the bridge activates and becomes ready to handle traffic.

```typescript
export const resolveIntercomBridge = async (
  config: IntercomBridgeConfig,
  root: string,
  logger?: Logger,
): Promise<IntercomBridgeInfo> => {
  const mode = config?.mode ?? "always";
  if (mode === "never") {
    return { active: false };
  }
  
  const configPath = config?.configPath ?? path.join(root, "intercom", "config.json");
  const configContent = readFileSync(configPath, "utf-8");
  const parsed = JSON.parse(configContent);
  
  if (!parsed.enabled) {
    return { active: false, intercomConfigEnabled: false };
  }
  
  const extension = await import(path.resolve(extensionsDir, "index.ts"));
  return { active: true, intercom: extension.intercom };
};

```

### Wiring the Child Session

When a child subagent spawns, the runtime calls `createIntercomBridge` to establish the communication link. This function attaches an event listener to the child’s `Session` object on the designated `notifyChannel` (typically `"intercom"`). It specifically filters for messages where `action` equals `"send"` and `to` equals `"orchestrator"`.

Upon capturing such a message, the bridge rewrites the target from the generic `"orchestrator"` string to the child’s specific `childSessionName` (e.g., `subagent-worker-<run-id>-1`), then writes the transformed payload to the `parentPipe`.

```typescript
export const createIntercomBridge = (
  parentPipe: Pipe,
  childSession: Session,
  childSessionName: string,
  notifyChannel: string,
): Promise<void> => {
  return new Promise((resolve) => {
    const onMessage = async (msg: any) => {
      if (msg?.action === "send" && msg?.to === "orchestrator") {
        await parentPipe.write({
          tool: "intercom",
          payload: {
            action: "send",
            to: childSessionName,
            payload: msg.payload,
          },
        });
        resolve();
      }
    };
    childSession.on(notifyChannel, onMessage);
    return () => childSession.removeListener(notifyChannel, onMessage);
  });
};

```

### Parent-Side Message Handling

On the parent side, `intercomBridgeHandler` maintains the incoming message stream. It resolves the bridge configuration to obtain the loaded extension’s `intercom` iterator, then continuously pulls messages from that stream and writes them into the parent’s pipe. This ensures the orchestrator receives every child-sent payload as a standardized `intercom` tool event.

```typescript
export const intercomBridgeHandler = async (
  parentPipe: Pipe,
  root: string,
  logger?: Logger,
): Promise<void> => {
  const bridgeInfo = await resolveIntercomBridge({ mode: "always" }, root, logger);
  if (!bridgeInfo.active) return;

  const { intercom } = bridgeInfo;
  await asyncIterate(intercom, async (msg) => {
    await parentPipe.write({
      tool: "intercom",
      payload: msg,
    });
  });
};

```

## Implementation in the Runtime

The bridge integrates into the agent lifecycle inside [`src/agent-runtime.ts`](https://github.com/nicobailon/pi-subagents/blob/main/src/agent-runtime.ts). After spawning a child session, the runtime checks for the presence of a `notifyChannel` and immediately wires the bridge before the child begins execution. Meanwhile, the parent’s main loop starts `intercomBridgeHandler` to listen for any incoming child traffic.

```typescript
// Inside agent-runtime.ts
if (notifyChannel && childSession && childSessionName) {
  await createIntercomBridge(parentPipe, childSession, childSessionName, notifyChannel);
}

// Setup parent-side listener
await intercomBridgeHandler(parentPipe, root, logger);

```

## End-to-End Message Flow

1. **Child emits**: The subagent calls the intercom tool with `{ action: "send", to: "orchestrator", payload: data }`.
2. **Bridge captures**: `createIntercomBridge` detects the event on the child’s `notifyChannel`.
3. **Target translation**: The bridge swaps `"orchestrator"` for the child’s unique session identifier.
4. **Parent receives**: The payload flows through `parentPipe` to the orchestrator as an `intercom` tool event.
5. **Parent processes**: `intercomBridgeHandler` ensures the message enters the parent’s event loop for handling.

## Summary

- The **intercom bridge** relies on `resolveIntercomBridge` to dynamically load the `pi-intercom` extension and validate configuration.
- **Child-to-parent** traffic flows through `createIntercomBridge`, which translates generic orchestrator targets into specific session names.
- The parent consumes these messages via `intercomBridgeHandler`, which iterates the extension’s message stream and writes to the parent pipe.
- This architecture enables **asynchronous, isolated communication** between hierarchical agents without shared memory.

## Frequently Asked Questions

### What naming convention does the intercom target use for child sessions?

Child sessions receive an **intercom target** string formatted as `subagent-worker-<run-id>-<suffix>`. The bridge preserves this identifier in the `to` field when forwarding messages, allowing the parent to distinguish between multiple concurrent subagents.

### Can multiple child agents communicate with the parent simultaneously?

Yes. Each child subagent receives its own `createIntercomBridge` instance attached to its unique `Session` and `childSessionName`. The parent’s `intercomBridgeHandler` aggregates messages from all active children into a single stream, enabling parallel subagent orchestration.

### What happens if the pi-intercom extension is not installed?

If `resolveIntercomBridge` cannot locate the extension at [`extensions/pi-intercom/index.ts`](https://github.com/nicobailon/pi-subagents/blob/main/extensions/pi-intercom/index.ts) or the configuration has `enabled: false`, the function returns `{ active: false }`. The runtime skips bridge wiring, and child intercom calls operate as no-ops without throwing errors.

### Is the child-to-parent communication synchronous or asynchronous?

The communication is **fully asynchronous**. `createIntercomBridge` returns a `Promise` that resolves when the first matching message is forwarded, and `intercomBridgeHandler` uses an async iterator to process messages without blocking the parent’s main execution loop.