Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
385 changes: 315 additions & 70 deletions docs/designs/collaboration-arena-baseline.md

Large diffs are not rendered by default.

34 changes: 29 additions & 5 deletions evals/games/counting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,11 @@ export interface CountingGameOptions {
variant?: CountingVariant
/** Human persona the referee speaks through on the real ingress path. */
refereeUserId?: string
/** Room routing convention (default `auto`, the historical game-room shape).
* `mention` is the PRODUCTION shared-channel convention: activation needs an
* explicit mention or thread affinity — the kickoff then @mentions every
* member, exactly as a human does in a live multi-agent Slack channel. */
bindMatch?: 'auto' | 'mention'
}

interface AcceptedCandidate {
Expand Down Expand Up @@ -111,6 +116,7 @@ export class CountingGame implements CollaborationGameWorld {
private readonly echo?: PlatformEcho

private readonly variant: CountingVariant
private readonly bindMatch: 'auto' | 'mention'

constructor(options: CountingGameOptions) {
this.world = options.world
Expand All @@ -120,11 +126,22 @@ export class CountingGame implements CollaborationGameWorld {
this.target = options.target ?? 12
this.variant = options.variant ?? 'referee-announced'
this.refereeUserId = options.refereeUserId ?? 'W-ARENA-REFEREE'
this.environment = options.world.buildEnvironment()
this.bindMatch = options.bindMatch ?? 'auto'
this.environment = options.world.buildEnvironment({ bindMatch: this.bindMatch })
this.echo = this.variant === 'peer-driven' ? new PlatformEcho(options.world, room) : undefined
}

private roomBroadcast(text: string): GameWave {
/** Platform-native mention tokens for every member bot (the kickoff shape a
* human uses in a production shared channel: one message @mentioning the
* participating agents). */
private memberMentions(): { tokens: string; botUserIds: string[] } {
const botUserIds = this.room.memberIntegrationIds
.map((integrationId) => this.world.botUserIdFor(integrationId))
.filter((id): id is string => id !== undefined)
return { tokens: botUserIds.map((id) => `<@${id}>`).join(' '), botUserIds }
}

private roomBroadcast(text: string, options: { mentions?: string[] } = {}): GameWave {
// ONE platform message id shared by every member integration's copy — the
// same channel:ts each dedicated Slack app receives; per-connection dedup
// (scoped by transport) must admit each copy exactly once.
Expand All @@ -143,7 +160,8 @@ export class CountingGame implements CollaborationGameWorld {
thread: this.room.thread,
messageId,
text,
sender: { id: this.refereeUserId, isBot: false }
sender: { id: this.refereeUserId, isBot: false },
...(options.mentions !== undefined && options.mentions.length > 0 ? { mentions: options.mentions } : {})
}
}))
this.world.appendEvent({
Expand Down Expand Up @@ -183,13 +201,19 @@ export class CountingGame implements CollaborationGameWorld {
if (!this.started) {
this.started = true
if (this.variant === 'peer-driven') {
// Production kickoff shape: in a mention-gated shared channel the human
// @mentions the participants in the one start message; in the legacy
// auto-bound game room the bare text suffices.
const mention = this.bindMatch === 'mention' ? this.memberMentions() : undefined
const prefix = mention !== undefined && mention.tokens.length > 0 ? `${mention.tokens} ` : ''
return this.roomBroadcast(
`Let's play the counting game. Together, count from 1 to ${this.target} in this thread by continuing ` +
`${prefix}Let's play the counting game. Together, count from 1 to ${this.target} in this thread by continuing ` +
`each other's messages. When you see a number posted in this thread, reply with ONLY the next number — ` +
`nothing else. Do not repeat a number that was already posted. If you posted the most recent number, ` +
`prefer letting another participant continue, but keep the count moving. Stop once ${this.target} has ` +
`been posted. No number has been posted yet, so the first reply should be 1. The referee stays silent ` +
`from now on and only checks the sequence at the end.`
`from now on and only checks the sequence at the end.`,
mention !== undefined ? { mentions: mention.botUserIds } : {}
)
}
return this.roomBroadcast(
Expand Down
16 changes: 14 additions & 2 deletions evals/games/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ export interface CountingGameRunOptions {
/** What drives the waves: §10.1 referee announcements (default) or §3.3
* peer-message relays with a silent referee. */
variant?: CountingVariant
/** Room routing convention: historical `auto` (default) or the production
* shared-channel `mention` gate with an @mention-bearing human kickoff. */
bindMatch?: 'auto' | 'mention'
/** Who plays (§8.1): scripted hosts (default; the reproducible engine gate)
* or a real-runtime subject template — the identical game either way. */
subject?: GameSubjectSpec
Expand Down Expand Up @@ -635,6 +638,9 @@ export interface QuotaCountingRunOptions {
timeoutMs?: number
subject?: GameSubjectSpec
keepSubject?: boolean
/** Room routing convention: historical `auto` (default) or the production
* shared-channel `mention` gate with an @mention-bearing human kickoff. */
bindMatch?: 'auto' | 'mention'
}

export async function runQuotaCounting(options: QuotaCountingRunOptions): Promise<CollaborationGameResult> {
Expand All @@ -645,7 +651,12 @@ export async function runQuotaCounting(options: QuotaCountingRunOptions): Promis
const subjectSpec: GameSubjectSpec = options.subject ?? { kind: 'scripted' }
const topology = compileTopology(countingManifest({ seed, agents }))
const world = new ArenaWorld(topology)
const game = new QuotaCountingGame({ world, roomAlias: 'counting-room', quotaPerAgent })
const game = new QuotaCountingGame({
world,
roomAlias: 'counting-room',
quotaPerAgent,
...(options.bindMatch !== undefined ? { bindMatch: options.bindMatch } : {})
})
const subject = await prepareSubjectForRun(topology, subjectSpec)
try {
const runner = new CollaborationGameRunner({
Expand Down Expand Up @@ -683,7 +694,8 @@ export async function runSameRoomCounting(options: CountingGameRunOptions): Prom
world,
roomAlias: 'counting-room',
variant,
...(options.target !== undefined ? { target: options.target } : {})
...(options.target !== undefined ? { target: options.target } : {}),
...(options.bindMatch !== undefined ? { bindMatch: options.bindMatch } : {})
})
const subject = await prepareSubjectForRun(topology, subjectSpec)
try {
Expand Down
30 changes: 25 additions & 5 deletions evals/games/quota-counting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export interface QuotaCountingGameOptions {
/** Posts each participant must contribute (default 5). */
quotaPerAgent?: number
refereeUserId?: string
/** Room routing convention (default `auto`, the historical game-room shape).
* `mention` is the PRODUCTION shared-channel convention: activation needs an
* explicit mention or thread affinity — the kickoff then @mentions every
* member, exactly as a human does in a live multi-agent Slack channel. */
bindMatch?: 'auto' | 'mention'
}

interface Contribution {
Expand Down Expand Up @@ -77,6 +82,7 @@ export class QuotaCountingGame implements CollaborationGameWorld {
private readonly liveHandles: DeliveryHandle[] = []
/** Propagation: the production platform echo (§2.3/§5/§6). */
private readonly echo: PlatformEcho
private readonly bindMatch: 'auto' | 'mention'

constructor(options: QuotaCountingGameOptions) {
this.world = options.world
Expand All @@ -87,7 +93,8 @@ export class QuotaCountingGame implements CollaborationGameWorld {
if (this.quota < 1) throw new Error('quotaPerAgent must be at least 1')
this.target = this.quota * room.memberAgentIds.length
this.refereeUserId = options.refereeUserId ?? 'W-ARENA-REFEREE'
this.environment = options.world.buildEnvironment()
this.bindMatch = options.bindMatch ?? 'auto'
this.environment = options.world.buildEnvironment({ bindMatch: this.bindMatch })
this.echo = new PlatformEcho(options.world, room)
for (const memberId of room.memberAgentIds) this.contributionsByAgent.set(memberId, 0)
}
Expand All @@ -96,7 +103,16 @@ export class QuotaCountingGame implements CollaborationGameWorld {
return this.room.memberAgentIds.map((agentId) => this.world.aliasOfAgent(agentId))
}

private roomBroadcast(text: string): GameWave {
/** Platform-native mention tokens for every member bot (the kickoff shape a
* human uses in a production shared channel). */
private memberMentions(): { tokens: string; botUserIds: string[] } {
const botUserIds = this.room.memberIntegrationIds
.map((integrationId) => this.world.botUserIdFor(integrationId))
.filter((id): id is string => id !== undefined)
return { tokens: botUserIds.map((id) => `<@${id}>`).join(' '), botUserIds }
}

private roomBroadcast(text: string, options: { mentions?: string[] } = {}): GameWave {
const messageId = this.world.mintMessageId(this.room.platform)
this.world.registerRoomMessage(this.room.channel, messageId)
this.world.recordThreadMessage(this.room.channel, this.room.thread, {
Expand All @@ -112,7 +128,8 @@ export class QuotaCountingGame implements CollaborationGameWorld {
thread: this.room.thread,
messageId,
text,
sender: { id: this.refereeUserId, isBot: false }
sender: { id: this.refereeUserId, isBot: false },
...(options.mentions !== undefined && options.mentions.length > 0 ? { mentions: options.mentions } : {})
}
}))
this.world.appendEvent({
Expand Down Expand Up @@ -145,14 +162,17 @@ export class QuotaCountingGame implements CollaborationGameWorld {
nextDeliveries(): GameWave {
if (!this.started) {
this.started = true
const mention = this.bindMatch === 'mention' ? this.memberMentions() : undefined
const prefix = mention !== undefined && mention.tokens.length > 0 ? `${mention.tokens} ` : ''
return this.roomBroadcast(
`Let's play quota counting. Participants: ${this.memberAliases().join(', ')}. ` +
`${prefix}Let's play quota counting. Participants: ${this.memberAliases().join(', ')}. ` +
`Count upward from 1 in this thread by continuing each other's messages — reply with ONLY the next ` +
`number, nothing else. Each participant must post exactly ${this.quota} numbers in total. You cannot ` +
`post twice in a row — someone else must post before you may post again. The count ends at ` +
`${this.target}, when everyone has posted ${this.quota}. Plan your turns so nobody is left holding ` +
`posts no one can interleave. No number has been posted yet, so the first reply should be 1. The ` +
`referee stays silent from now on and only reviews the sequence at the end.`
`referee stays silent from now on and only reviews the sequence at the end.`,
mention !== undefined ? { mentions: mention.botUserIds } : {}
)
}
return this.pendingWaves.shift() ?? { platformEvents: [], refereeEvents: [] }
Expand Down
Loading