agent-ecosystem/src/shared/utils/inboxNoise.ts

158 lines
5.7 KiB
TypeScript

/**
* Inbox "noise" messages are structured JSON objects that represent internal coordination
* signals (idle/shutdown/etc.). They should not trigger user-facing notifications or
* automatic lead relays.
*/
export const INBOX_NOISE_TYPES = [
'idle_notification',
'shutdown_approved',
'teammate_terminated',
'shutdown_request',
] as const;
const INBOX_NOISE_SET = new Set<string>(INBOX_NOISE_TYPES);
export function parseInboxJson(text: string): Record<string, unknown> | null {
const trimmed = text.trim();
if (!trimmed.startsWith('{')) return null;
try {
const parsed = JSON.parse(trimmed) as unknown;
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>;
}
} catch {
// not JSON
}
return null;
}
export function getInboxJsonType(text: string): string | null {
const parsed = parseInboxJson(text);
if (!parsed) return null;
return typeof parsed.type === 'string' ? parsed.type : null;
}
export function isInboxNoiseMessage(text: string): boolean {
const type = getInboxJsonType(text);
return !!type && INBOX_NOISE_SET.has(type);
}
/**
* Returns true when a teammate message is substantive enough to count as a real
* bootstrap/check-in signal. Internal coordination noise like idle/shutdown
* JSON should keep the runtime marked alive, but must not be treated as a
* teammate "checked in" signal in launch UX.
*/
export function isMeaningfulBootstrapCheckInMessage(text: string): boolean {
return text.trim().length > 0 && !isInboxNoiseMessage(text);
}
// ---------------------------------------------------------------------------
// Teammate permission request parsing
// ---------------------------------------------------------------------------
/** A single permission suggestion from the teammate runtime. */
export interface PermissionSuggestion {
type: string;
rules?: { toolName: string }[];
behavior?: string;
destination?: string;
/** Permission mode name (for type: "setMode"). FACT: observed values: "acceptEdits", "bypassPermissions" */
mode?: string;
}
/** Parsed teammate permission request from inbox message. */
export interface ParsedPermissionRequest {
requestId: string;
agentId: string;
toolName: string;
toolUseId: string;
description: string;
input: Record<string, unknown>;
/** Suggestions from teammate runtime on how to resolve the permission.
* FACT: This field is populated by Claude Code runtime, not by the AI agent.
* FACT: Observed format: { type: "addRules", rules: [{toolName}], behavior: "allow", destination: "localSettings" }
*/
permissionSuggestions: PermissionSuggestion[];
}
/**
* Parses a `permission_request` JSON message from a teammate's inbox entry.
* Returns null if the text is not a valid permission_request.
*/
export function parsePermissionRequest(text: string): ParsedPermissionRequest | null {
const parsed = parseInboxJson(text);
if (parsed?.type !== 'permission_request') return null;
const requestId = typeof parsed.request_id === 'string' ? parsed.request_id : null;
const agentId = typeof parsed.agent_id === 'string' ? parsed.agent_id : null;
const toolName = typeof parsed.tool_name === 'string' ? parsed.tool_name : null;
if (!requestId || !agentId || !toolName) return null;
return {
requestId,
agentId,
toolName,
toolUseId: typeof parsed.tool_use_id === 'string' ? parsed.tool_use_id : '',
description: typeof parsed.description === 'string' ? parsed.description : '',
input:
parsed.input && typeof parsed.input === 'object' && !Array.isArray(parsed.input)
? (parsed.input as Record<string, unknown>)
: {},
permissionSuggestions: Array.isArray(parsed.permission_suggestions)
? (parsed.permission_suggestions as PermissionSuggestion[])
: [],
};
}
// ---------------------------------------------------------------------------
// Teammate-message XML block detection & stripping
// ---------------------------------------------------------------------------
const TEAMMATE_MESSAGE_BLOCK_RE = /<teammate-message\s[^>]*>[\s\S]*?<\/teammate-message>/g;
/**
* Removes `<teammate-message>` XML blocks from text.
* Used to clean protocol artifacts that leak into lead thoughts.
*/
export function stripTeammateMessageBlocks(text: string): string {
return text
.replace(TEAMMATE_MESSAGE_BLOCK_RE, '\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
/**
* Returns true if the entire text consists only of `<teammate-message>` blocks
* (possibly with whitespace between them) and no meaningful user-visible content.
*/
export function isOnlyTeammateMessageBlocks(text: string): boolean {
const stripped = stripTeammateMessageBlocks(text);
return stripped.length === 0;
}
// ---------------------------------------------------------------------------
// Combined protocol noise check for lead thoughts
// ---------------------------------------------------------------------------
/**
* Detects `<teammate-message>` opening tags (even without closing tag).
* Claude's lead model sometimes echoes raw teammate message XML in assistant
* text output — these are always protocol artifacts, never real user content.
*/
const TEAMMATE_MESSAGE_OPEN_RE = /^\s*<teammate-message\s/;
/**
* Returns true if a lead thought text is entirely protocol noise and should
* be hidden from the user. Covers:
* 1. Structured JSON noise (idle_notification, shutdown_*, etc.)
* 2. Text that consists solely of `<teammate-message>` XML blocks (closed tags)
* 3. Text starting with an unclosed `<teammate-message>` tag
*/
export function isThoughtProtocolNoise(text: string): boolean {
if (isInboxNoiseMessage(text)) return true;
if (isOnlyTeammateMessageBlocks(text)) return true;
if (TEAMMATE_MESSAGE_OPEN_RE.test(text)) return true;
return false;
}