33// Participants panel already reads them off the deployment/run listing).
44// Appending them as a trailing block on every room send gives every agent
55// in the room everyone else's address, so one agent can mail another
6- // directly. `stripRoster` is the inverse, used only to keep the block out
6+ // directly. Agent-to-agent mail goes run to run and never lands in the
7+ // person's mailbox on its own, so the block also carries the person's own
8+ // address and a line telling agents to copy it on any handoff — the mail
9+ // tools have no `cc` field, so that means naming it as another `to`
10+ // recipient. `stripRoster` is the inverse, used only to keep the block out
711// of what a person sees echoed back as their own sent message.
812
913const ROSTER_HEADING = "Participants:" ;
1014
1115export type RosterEntry = {
1216 readonly name : string ;
1317 readonly address : string ;
18+ readonly kind : "person" | "agent" ;
1419} ;
1520
1621/** The trailing block marker, including the blank-line separator that
@@ -19,11 +24,28 @@ function rosterBlock(entries: readonly RosterEntry[]): string {
1924 return [ ROSTER_HEADING , ...entries . map ( ( entry ) => `${ entry . name } <${ entry . address } >` ) ] . join ( "\n" ) ;
2025}
2126
27+ /** The trailing instruction naming every person in the roster, so an agent
28+ * knows to copy them on a handoff to another participant. `undefined` when
29+ * the roster has no person entry (there is nobody to copy). */
30+ function ccInstruction ( entries : readonly RosterEntry [ ] ) : string | undefined {
31+ const people = entries . filter ( ( entry ) => entry . kind === "person" ) ;
32+ if ( people . length === 0 ) return undefined ;
33+ const addresses = people . map ( ( entry ) => entry . address ) . join ( ", " ) ;
34+ return `Copy ${ addresses } in \`to\` on any mail you send another participant, so they can follow along.` ;
35+ }
36+
2237/** Appends a `Participants:` block listing every entry's name and address,
23- * separated from the message by a blank line. A no-op with no entries. */
38+ * plus a `cc` instruction naming any person in the roster, separated from
39+ * the message by a blank line. A no-op with no entries. */
2440export function appendRoster ( body : string , entries : readonly RosterEntry [ ] ) : string {
2541 if ( entries . length === 0 ) return body ;
26- return `${ body } \n\n${ rosterBlock ( entries ) } ` ;
42+ const instruction = ccInstruction ( entries ) ;
43+ return [
44+ body ,
45+ "" ,
46+ rosterBlock ( entries ) ,
47+ ...( instruction !== undefined ? [ "" , instruction ] : [ ] ) ,
48+ ] . join ( "\n" ) ;
2749}
2850
2951/** Removes a trailing `Participants:` block appended by `appendRoster`, so
0 commit comments