feat(coding-agent/session): added IRC relay forwarding for main session transcript
- Added agent identity and registry fields to session configuration and session creation, enabling relay routing metadata for agent sessions. - Implemented non-persistent IRC relay emission to forward incoming and reply observations from non-main agents into the main session UI. - Updated IRC UI rendering to support `irc:relay` messages with participant-aware arrow formatting and body display.
This commit is contained in:
@@ -2,6 +2,10 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- Added IRC relay observation in the main agent UI so every IRC exchange between agents is rendered in the main transcript, even when the main agent is not a direct participant
|
||||
|
||||
## [14.5.3] - 2026-04-27
|
||||
|
||||
### Added
|
||||
|
||||
@@ -127,14 +127,38 @@ export class UiHelpers {
|
||||
this.ctx.chatContainer.addChild(component);
|
||||
break;
|
||||
}
|
||||
if (message.customType === "irc:incoming" || message.customType === "irc:autoreply") {
|
||||
if (
|
||||
message.customType === "irc:incoming" ||
|
||||
message.customType === "irc:autoreply" ||
|
||||
message.customType === "irc:relay"
|
||||
) {
|
||||
const details = (
|
||||
message as CustomMessage<{ from?: string; to?: string; message?: string; reply?: string }>
|
||||
message as CustomMessage<{
|
||||
from?: string;
|
||||
to?: string;
|
||||
message?: string;
|
||||
reply?: string;
|
||||
body?: string;
|
||||
kind?: "message" | "reply";
|
||||
}>
|
||||
).details;
|
||||
const isIncoming = message.customType === "irc:incoming";
|
||||
const peer = isIncoming ? (details?.from ?? "?") : (details?.to ?? "?");
|
||||
const body = isIncoming ? (details?.message ?? "") : (details?.reply ?? "");
|
||||
const arrow = isIncoming ? `\u21e6 ${peer}` : `\u21e8 ${peer} (auto)`;
|
||||
let arrow: string;
|
||||
let body: string;
|
||||
if (message.customType === "irc:incoming") {
|
||||
const peer = details?.from ?? "?";
|
||||
body = details?.message ?? "";
|
||||
arrow = `\u21e6 ${peer}`;
|
||||
} else if (message.customType === "irc:autoreply") {
|
||||
const peer = details?.to ?? "?";
|
||||
body = details?.reply ?? "";
|
||||
arrow = `\u21e8 ${peer} (auto)`;
|
||||
} else {
|
||||
const from = details?.from ?? "?";
|
||||
const to = details?.to ?? "?";
|
||||
body = details?.body ?? "";
|
||||
const suffix = details?.kind === "reply" ? " (auto)" : "";
|
||||
arrow = `${from} \u21e8 ${to}${suffix}`;
|
||||
}
|
||||
const header = `${theme.fg("accent", `[IRC] ${arrow}`)}`;
|
||||
this.ctx.chatContainer.addChild(new Text(header, 1, 0));
|
||||
if (body) {
|
||||
|
||||
@@ -1601,6 +1601,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
ttsrManager,
|
||||
obfuscator,
|
||||
asyncJobManager,
|
||||
agentId: resolvedAgentId,
|
||||
agentRegistry,
|
||||
});
|
||||
hasSession = true;
|
||||
|
||||
|
||||
@@ -130,6 +130,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
|
||||
type: "text",
|
||||
};
|
||||
import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
|
||||
import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
|
||||
import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
|
||||
import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
|
||||
import { assertEditableFile } from "../tools/auto-generated-guard";
|
||||
@@ -263,6 +264,10 @@ export interface AgentSessionConfig {
|
||||
obfuscator?: SecretObfuscator;
|
||||
/** Logical owner for retained Python kernels created by this session. */
|
||||
pythonKernelOwnerId?: string;
|
||||
/** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
|
||||
agentId?: string;
|
||||
/** Shared agent registry (for forwarding IRC observations to the main session UI). */
|
||||
agentRegistry?: AgentRegistry;
|
||||
}
|
||||
|
||||
/** Options for AgentSession.prompt() */
|
||||
@@ -478,6 +483,9 @@ export class AgentSession {
|
||||
// Drained into history (via emitExternalEvent) once the recipient becomes idle.
|
||||
#pendingBackgroundExchanges: CustomMessage[][] = [];
|
||||
#scheduledBackgroundExchangeFlush = false;
|
||||
// Agent identity + registry for IRC relay forwarding to the main session UI.
|
||||
#agentId: string | undefined;
|
||||
#agentRegistry: AgentRegistry | undefined;
|
||||
// Extension system
|
||||
#extensionRunner: ExtensionRunner | undefined = undefined;
|
||||
#turnIndex = 0;
|
||||
@@ -611,6 +619,8 @@ export class AgentSession {
|
||||
);
|
||||
this.#ttsrManager = config.ttsrManager;
|
||||
this.#obfuscator = config.obfuscator;
|
||||
this.#agentId = config.agentId;
|
||||
this.#agentRegistry = config.agentRegistry;
|
||||
this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
|
||||
const event: AgentEvent = {
|
||||
type: "message_update",
|
||||
@@ -5999,6 +6009,13 @@ export class AgentSession {
|
||||
timestamp: incomingTimestamp,
|
||||
};
|
||||
void this.#emitSessionEvent({ type: "irc_message", message: incomingRecord });
|
||||
this.#forwardIrcRelayToMain({
|
||||
from: args.from,
|
||||
to: this.#agentId ?? "?",
|
||||
body: args.message,
|
||||
kind: "message",
|
||||
timestamp: incomingTimestamp,
|
||||
});
|
||||
|
||||
if (!awaitReply) {
|
||||
this.#queueBackgroundExchangeInjection([incomingRecord]);
|
||||
@@ -6024,11 +6041,59 @@ export class AgentSession {
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
|
||||
this.#forwardIrcRelayToMain({
|
||||
from: this.#agentId ?? "?",
|
||||
to: args.from,
|
||||
body: replyText,
|
||||
kind: "reply",
|
||||
timestamp: replyRecord.timestamp,
|
||||
});
|
||||
this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
|
||||
|
||||
return { replyText };
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward an IRC exchange observation to the main agent's session UI so the
|
||||
* user can see every IRC conversation in the main transcript, even when the
|
||||
* main agent is not a direct participant. The relay record is display-only:
|
||||
* it is NOT injected into the main agent's persisted history.
|
||||
*/
|
||||
#forwardIrcRelayToMain(args: {
|
||||
from: string;
|
||||
to: string;
|
||||
body: string;
|
||||
kind: "message" | "reply";
|
||||
timestamp: number;
|
||||
}): void {
|
||||
const registry = this.#agentRegistry;
|
||||
if (!registry) return;
|
||||
// If this session is the main agent, the local emit already reached the main UI.
|
||||
if (this.#agentId === MAIN_AGENT_ID) return;
|
||||
const mainRef = registry.get(MAIN_AGENT_ID);
|
||||
const mainSession = mainRef?.session;
|
||||
if (!mainSession || mainSession === this) return;
|
||||
const arrow = args.kind === "reply" ? "\u2192 (auto)" : "\u2192";
|
||||
const relayRecord: CustomMessage = {
|
||||
role: "custom",
|
||||
customType: "irc:relay",
|
||||
content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
|
||||
display: true,
|
||||
details: { from: args.from, to: args.to, body: args.body, kind: args.kind },
|
||||
attribution: "agent",
|
||||
timestamp: args.timestamp,
|
||||
};
|
||||
mainSession.emitIrcRelayObservation(relayRecord);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an IRC relay observation event on this session for UI rendering only.
|
||||
* Does not persist the record to history. Public so other sessions can forward.
|
||||
*/
|
||||
emitIrcRelayObservation(record: CustomMessage): void {
|
||||
void this.#emitSessionEvent({ type: "irc_message", message: record });
|
||||
}
|
||||
|
||||
/**
|
||||
* Run a single ephemeral side-channel turn against this session's current
|
||||
* model + system prompt + history. No tools are used; the side request
|
||||
|
||||
Reference in New Issue
Block a user