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:
can1357
2026-04-27 14:32:13 +02:00
parent 3f819ac0b6
commit 697e4aa543
4 changed files with 101 additions and 6 deletions
+4
View File
@@ -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) {
+2
View File
@@ -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