diff --git a/apps/game-api/src/app.ts b/apps/game-api/src/app.ts
index 673771d..90531d1 100644
--- a/apps/game-api/src/app.ts
+++ b/apps/game-api/src/app.ts
@@ -21,22 +21,14 @@ import {
experimentExportRequestSchema,
experimentExportPreviewSchema,
experimentExportResponseSchema,
- experimentImportRequestSchema,
- experimentImportResponseSchema,
healthResponseSchema,
modelCatalogResponseSchema,
modelVerificationSchema,
- PERSONALITY_MAX_LENGTH,
resetSimulationResponseSchema,
- restoreDefaultPersonalitiesResponseSchema,
simulationSnapshotSchema,
singleTickResponseSchema,
- updateAgentPersonalityRequestSchema,
- updateAgentPersonalityResponseSchema,
updateExperimentModelsRequestSchema,
updateExperimentModelsResponseSchema,
- updateExperimentBehaviorRequestSchema,
- updateExperimentBehaviorResponseSchema,
verifyModelRequestSchema,
verifyModelResponseSchema,
worldSnapshotSchema,
@@ -441,48 +433,6 @@ export function createApp(options: AppOptions = {}) {
}
});
- app.post('/api/simulation/experiment/behavior', async (context) => {
- const request = updateExperimentBehaviorRequestSchema.safeParse(
- await context.req.json().catch(() => undefined),
- );
- if (!request.success)
- return context.json(
- apiErrorSchema.parse({
- error: {
- code: 'invalid_behavior_configuration',
- message: 'The behavior configuration is invalid.',
- },
- }),
- 400,
- );
- try {
- return context.json(
- updateExperimentBehaviorResponseSchema.parse({
- snapshot: service.updateBehaviorConfiguration(request.data),
- }),
- );
- } catch (error) {
- if (error instanceof SimulationConflictError)
- return context.json(
- apiErrorSchema.parse({
- error: {
- code: 'behavior_configuration_conflict',
- message: error.message,
- },
- }),
- 409,
- );
- if (error instanceof SimulationValidationError)
- return context.json(
- apiErrorSchema.parse({
- error: { code: error.code, message: error.message },
- }),
- 400,
- );
- throw error;
- }
- });
-
app.post('/api/simulation/tick', async (context) => {
try {
const response = await mutationPromise(context, 'tick', async () => {
@@ -558,73 +508,6 @@ export function createApp(options: AppOptions = {}) {
}
});
- app.post('/api/simulation/agents/:agentId/personality', async (context) => {
- const request = updateAgentPersonalityRequestSchema.safeParse(
- await context.req.json().catch(() => undefined),
- );
- if (!request.success) {
- return context.json(
- apiErrorSchema.parse({
- error: {
- code: 'invalid_personality',
- message: `Personality must contain 1 to ${PERSONALITY_MAX_LENGTH} characters.`,
- },
- }),
- 400,
- );
- }
- try {
- const agent = service.updateAgentPersonality(
- context.req.param('agentId'),
- request.data.personality,
- );
- return context.json(
- updateAgentPersonalityResponseSchema.parse({
- snapshot: service.getSnapshot(),
- agent,
- }),
- );
- } catch (error) {
- if (error instanceof SimulationConflictError) {
- return context.json(
- apiErrorSchema.parse({
- error: { code: 'personality_conflict', message: error.message },
- }),
- 409,
- );
- }
- if (error instanceof SimulationValidationError) {
- return context.json(
- apiErrorSchema.parse({
- error: { code: error.code, message: error.message },
- }),
- error.code === 'unknown_agent' ? 404 : 400,
- );
- }
- throw error;
- }
- });
-
- app.post('/api/simulation/personalities/restore-defaults', (context) => {
- try {
- return context.json(
- restoreDefaultPersonalitiesResponseSchema.parse({
- snapshot: service.restoreDefaultPersonalities(),
- }),
- );
- } catch (error) {
- if (error instanceof SimulationConflictError) {
- return context.json(
- apiErrorSchema.parse({
- error: { code: 'personality_conflict', message: error.message },
- }),
- 409,
- );
- }
- throw error;
- }
- });
-
app.post('/api/simulation/experiment/export/preview', async (context) => {
const request = experimentExportRequestSchema.safeParse(
await context.req.json().catch(() => undefined),
@@ -754,52 +637,6 @@ export function createApp(options: AppOptions = {}) {
}
});
- app.post('/api/simulation/experiment/import', async (context) => {
- const request = experimentImportRequestSchema.safeParse(
- await context.req.json().catch(() => undefined),
- );
- if (!request.success)
- return context.json(
- apiErrorSchema.parse({
- error: {
- code: 'invalid_import',
- message: 'The experiment import is invalid.',
- },
- }),
- 400,
- );
- const currentCatalog = modelCatalogResponseSchema.parse(
- await catalog.getCatalog(false),
- );
- service.setCompatibleModels(currentCatalog.models);
- try {
- return context.json(
- experimentImportResponseSchema.parse(
- service.importModelConfiguration(request.data.document),
- ),
- );
- } catch (error) {
- if (error instanceof SimulationConflictError)
- return context.json(
- apiErrorSchema.parse({
- error: {
- code: 'model_configuration_conflict',
- message: error.message,
- },
- }),
- 409,
- );
- if (error instanceof SimulationValidationError)
- return context.json(
- apiErrorSchema.parse({
- error: { code: 'invalid_import', message: error.message },
- }),
- 400,
- );
- throw error;
- }
- });
-
app.notFound((context) =>
context.json(
apiErrorSchema.parse({
diff --git a/apps/game-api/src/experiment-export.ts b/apps/game-api/src/experiment-export.ts
index 3527fb2..16643f5 100644
--- a/apps/game-api/src/experiment-export.ts
+++ b/apps/game-api/src/experiment-export.ts
@@ -4,15 +4,11 @@ import {
experimentExportPreviewSchema,
experimentExportRequestSchema,
experimentMetricsSchema,
- PERSONALITY_PROFILES,
- NEUTRAL_AGENT_COLOR,
- STRATEGY_PROFILES,
type Agent,
type AgentId,
- type AgentObservation,
- type AgentTurnRecord,
- type EventId,
- type AllianceEvent,
+ type AppliedScenario,
+ type ExperimentAttemptAccounting,
+ type ExperimentConfigurationEvent,
type ExperimentExportDocument,
type ExperimentExportPreview,
type ExperimentExportRequest,
@@ -20,25 +16,15 @@ import {
type ExperimentId,
type ExperimentMetrics,
type ExperimentModelConfiguration,
- type ModelId,
- type ReasoningProfile,
- type DiplomacyRejectionReason,
- type DiplomacyResult,
- type ExportedCommunication,
- type ExportedControlChange,
- type ExperimentConfigurationEvent,
- type ProviderMetadata,
- type WorldSnapshot,
- type BehaviorConfiguration,
- type AppliedScenario,
type ExperimentTickSummary,
- type AgentGoalState,
- type MemoryEntry,
- type SimulatedPlayerEvent,
+ type ExportedControlChange,
type ProviderAttemptRecord,
type ProviderAttemptRetention,
- type ExperimentAttemptAccounting,
+ type SimulatedPlayerEvent,
type SwarmTickRecord,
+ type WorldAction,
+ type WorldActionResult,
+ type WorldSnapshot,
} from '@hexzero/shared';
export interface ExperimentSource {
@@ -47,19 +33,15 @@ export interface ExperimentSource {
startedAt: string;
providerMode: 'openrouter' | 'scripted-test';
retentionLimit: number;
- totalCompletedTurns: number;
- turns: readonly AgentTurnRecord[];
- swarmTicks?: readonly SwarmTickRecord[];
+ totalCompletedTicks: number;
initialAgents: readonly Agent[];
currentAgents: readonly Agent[];
configurationEvents: readonly ExperimentConfigurationEvent[];
initialWorld: WorldSnapshot;
currentWorld: WorldSnapshot;
modelConfiguration: ExperimentModelConfiguration;
- behaviorConfiguration: BehaviorConfiguration;
scenario: AppliedScenario;
- agentGoals: readonly { agentId: AgentId; goal: AgentGoalState | null }[];
- agentMemories: readonly { agentId: AgentId; entries: MemoryEntry[] }[];
+ swarmTicks?: readonly SwarmTickRecord[];
simulatedPlayerEvents: readonly SimulatedPlayerEvent[];
providerAttempts?: readonly ProviderAttemptRecord[];
attemptRetention?: ProviderAttemptRetention;
@@ -76,107 +58,12 @@ export class ExperimentExportValidationError extends Error {
}
}
-export class ExperimentMetricAccumulator {
- readonly #records = new Map();
-
- constructor(agentIds: readonly AgentId[]) {
- this.#records.set('aggregate', mutableMetrics());
- for (const agentId of agentIds)
- this.#records.set(agentId, mutableMetrics());
- }
-
- add(turn: AgentTurnRecord): void {
- addToMutable(this.#records.get('aggregate')!, turn, true);
- const agent = this.#records.get(turn.agentId);
- if (agent) addToMutable(agent, turn);
- for (const event of turn.allianceEvents)
- for (const affectedId of allianceEventAgentIds(event)) {
- if (affectedId === turn.agentId) continue;
- const affected = this.#records.get(affectedId);
- if (affected) addAllianceEventMetric(affected, event, affectedId);
- }
- if (
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.communicationResult.requested &&
- turn.communicationResult.accepted &&
- turn.communicationResult.event.channel === 'direct'
- ) {
- const recipient = this.#records.get(
- turn.communicationResult.event.recipientId,
- );
- if (recipient) recipient.directMessagesReceived += 1;
- }
- if (
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.communicationResult.requested &&
- turn.communicationResult.accepted &&
- turn.communicationResult.event.channel === 'zero'
- ) {
- for (const recipientId of turn.communicationResult.event.recipientIds) {
- const recipient = this.#records.get(recipientId);
- if (recipient) {
- recipient.zeroRecipientDeliveries += 1;
- recipient.zeroDirectiveRecipients.add(recipientId);
- }
- }
- }
- if (
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'hex-captured'
- ) {
- const previousControllerAgentId =
- turn.worldActionResult.event.previousControllerAgentId;
- const displaced =
- previousControllerAgentId === null
- ? undefined
- : this.#records.get(previousControllerAgentId);
- if (displaced) displaced.territoryLostThroughCapture += 1;
- }
- }
-
- snapshot(agentIds: readonly AgentId[]): ExperimentMetrics {
- return experimentMetricsSchema.parse({
- aggregate: finalizeMutable(this.#records.get('aggregate')!),
- byAgent: agentIds.map((agentId) => ({
- agentId,
- metrics: finalizeMutable(
- this.#records.get(agentId) ?? mutableMetrics(),
- ),
- })),
- });
- }
-}
-
-function addAllianceEventMetric(
- metrics: MutableMetrics,
- event: AllianceEvent,
- agentId?: AgentId,
-): void {
- if (agentId && !allianceEventAgentIds(event).includes(agentId)) return;
- if (event.type === 'alliance-proposed') {
- if (!agentId || event.agentId === agentId) {
- metrics.proposalsCreated += 1;
- metrics.proposalsSent += 1;
- }
- if (!agentId || event.recipientAgentId === agentId)
- metrics.proposalsReceived += 1;
- } else if (event.type === 'alliance-proposal-closed') {
- if (event.reason === 'expired') metrics.proposalsExpired += 1;
- else metrics.proposalsInvalidated += 1;
- } else if (event.type === 'alliance-formed') {
- metrics.alliancesFormed += 1;
- metrics.alliancesJoined += agentId ? 1 : event.memberAgentIds.length;
- } else if (event.type === 'agent-joined-alliance') {
- if (!agentId || event.joinedAgentId === agentId)
- metrics.alliancesJoined += 1;
- } else if (event.type === 'agent-left-alliance') {
- if (!agentId || event.leftAgentId === agentId) metrics.alliancesLeft += 1;
- } else if (event.type === 'alliance-dissolved')
- metrics.alliancesDissolved += 1;
+/** One resolved (accepted or rejected) world-action decision from a committed swarm tick. */
+interface ResolvedWorldAction {
+ tickNumber: number;
+ agentId: AgentId;
+ action: WorldAction;
+ actionResult: WorldActionResult;
}
const metricTokenFields = [
@@ -188,526 +75,61 @@ const metricTokenFields = [
'cacheWriteTokens',
] as const;
-interface MutableMetrics {
- turns: number;
- accepted: number;
- rejected: number;
- providerErrors: number;
- lostTicks: number;
- operatorSkipped: number;
- modelCalls: number;
- failedModelAttempts: number;
- automaticRepairAttempts: number;
- automaticTransportRetries: number;
- manualRetryAttempts: number;
- unattendedRetryAttempts: number;
- manualSkips: number;
- unattendedSkips: number;
- recoveredByUnattendedRetry: number;
- skippedAfterUnattendedRecovery: number;
- retriedTurns: number;
- recoveredAutomatically: number;
- recoveredManually: number;
- recoveredByRetry: number;
- requestedMoves: number;
- requestedInfections: number;
- requestedCaptures: number;
- requestedWaits: number;
- acceptedMovements: number;
- infections: number;
- successfulCaptures: number;
- acceptedWaits: number;
- rejectedWorldActions: number;
- territoryGainedThroughInfection: number;
- territoryGainedThroughCapture: number;
- territoryLostThroughCapture: number;
- publicMessagesRequested: number;
- publicMessagesAccepted: number;
- publicMessagesRejected: number;
- directMessagesRequested: number;
- allianceMessagesRequested: number;
- allianceMessagesDelivered: number;
- allianceMessagesRejected: number;
- directMessagesDelivered: number;
- directMessagesRejected: number;
- publicMessagesSent: number;
- directMessagesSent: number;
- directMessagesReceived: number;
- zeroBroadcastsRequested: number;
- zeroBroadcastsDelivered: number;
- zeroBroadcastsRejected: number;
- zeroRecipientDeliveries: number;
- zeroDirectiveRecipients: Set;
- patientZeroRepliers: Set;
- directRepliesToPatientZero: number;
- firstZeroDirectiveTurn: number | null;
- mostRecentZeroDirective: {
- eventId: EventId;
- turnNumber: number;
- occurredAt: string;
- agentId: AgentId;
- recipientCount: number;
- modelId: ModelId;
- reasoningProfile: ReasoningProfile;
- personalityId: AgentObservation['behavior']['personalityId'];
- strategyId: AgentObservation['behavior']['strategyId'];
- } | null;
- diplomacyProposalsRequested: number;
- diplomacyAcceptancesRequested: number;
- diplomacyDeparturesRequested: number;
- diplomacyProposalsAccepted: number;
- diplomacyAcceptancesAccepted: number;
- diplomacyDeparturesAccepted: number;
- diplomacyRejected: number;
- diplomacyRejections: Map<
- string,
- {
- type:
- 'propose-alliance' | 'accept-alliance' | 'leave-alliance' | 'invalid';
- reason: DiplomacyRejectionReason;
- count: number;
- }
- >;
- proposalsCreated: number;
- proposalsSent: number;
- proposalsReceived: number;
- proposalsExpired: number;
- proposalsInvalidated: number;
- alliancesFormed: number;
- alliancesJoined: number;
- alliancesLeft: number;
- alliancesDissolved: number;
- alliedCaptureAttempts: number;
- alliedCaptureRejections: number;
- latencyTotal: number;
- latencyCount: number;
- tokens: Record<(typeof metricTokenFields)[number], number>;
- tokenFieldsComplete: Record<(typeof metricTokenFields)[number], boolean>;
- tokenFieldsKnown: Record<(typeof metricTokenFields)[number], boolean>;
- knownCostCredits: string;
- attemptsWithUnknownTokenUsage: number;
- attemptsWithUnknownCost: number;
- turnsWithUnknownCost: Set;
- visited: Set;
-}
-
-function mutableMetrics(): MutableMetrics {
- return {
- turns: 0,
- accepted: 0,
- rejected: 0,
- providerErrors: 0,
- lostTicks: 0,
- operatorSkipped: 0,
- modelCalls: 0,
- failedModelAttempts: 0,
- automaticRepairAttempts: 0,
- automaticTransportRetries: 0,
- manualRetryAttempts: 0,
- unattendedRetryAttempts: 0,
- manualSkips: 0,
- unattendedSkips: 0,
- recoveredByUnattendedRetry: 0,
- skippedAfterUnattendedRecovery: 0,
- retriedTurns: 0,
- recoveredAutomatically: 0,
- recoveredManually: 0,
- recoveredByRetry: 0,
- requestedMoves: 0,
- requestedInfections: 0,
- requestedCaptures: 0,
- requestedWaits: 0,
- acceptedMovements: 0,
- infections: 0,
- successfulCaptures: 0,
- acceptedWaits: 0,
- rejectedWorldActions: 0,
- territoryGainedThroughInfection: 0,
- territoryGainedThroughCapture: 0,
- territoryLostThroughCapture: 0,
- publicMessagesRequested: 0,
- publicMessagesAccepted: 0,
- publicMessagesRejected: 0,
- directMessagesRequested: 0,
- allianceMessagesRequested: 0,
- allianceMessagesDelivered: 0,
- allianceMessagesRejected: 0,
- directMessagesDelivered: 0,
- directMessagesRejected: 0,
- publicMessagesSent: 0,
- directMessagesSent: 0,
- directMessagesReceived: 0,
- zeroBroadcastsRequested: 0,
- zeroBroadcastsDelivered: 0,
- zeroBroadcastsRejected: 0,
- zeroRecipientDeliveries: 0,
- zeroDirectiveRecipients: new Set(),
- patientZeroRepliers: new Set(),
- directRepliesToPatientZero: 0,
- firstZeroDirectiveTurn: null,
- mostRecentZeroDirective: null,
- diplomacyProposalsRequested: 0,
- diplomacyAcceptancesRequested: 0,
- diplomacyDeparturesRequested: 0,
- diplomacyProposalsAccepted: 0,
- diplomacyAcceptancesAccepted: 0,
- diplomacyDeparturesAccepted: 0,
- diplomacyRejected: 0,
- diplomacyRejections: new Map(),
- proposalsCreated: 0,
- proposalsSent: 0,
- proposalsReceived: 0,
- proposalsExpired: 0,
- proposalsInvalidated: 0,
- alliancesFormed: 0,
- alliancesJoined: 0,
- alliancesLeft: 0,
- alliancesDissolved: 0,
- alliedCaptureAttempts: 0,
- alliedCaptureRejections: 0,
- latencyTotal: 0,
- latencyCount: 0,
- tokens: Object.fromEntries(
- metricTokenFields.map((field) => [field, 0]),
- ) as MutableMetrics['tokens'],
- tokenFieldsComplete: Object.fromEntries(
- metricTokenFields.map((field) => [field, true]),
- ) as MutableMetrics['tokenFieldsComplete'],
- tokenFieldsKnown: Object.fromEntries(
- metricTokenFields.map((field) => [field, false]),
- ) as MutableMetrics['tokenFieldsKnown'],
- knownCostCredits: '0',
- attemptsWithUnknownTokenUsage: 0,
- attemptsWithUnknownCost: 0,
- turnsWithUnknownCost: new Set(),
- visited: new Set(),
- };
-}
-
-function addToMutable(
- metrics: MutableMetrics,
- turn: AgentTurnRecord,
- aggregate = false,
-): void {
- metrics.turns += 1;
- if (turn.outcome === 'provider-error') metrics.providerErrors += 1;
- else if (turn.outcome === 'lost-tick') metrics.lostTicks += 1;
- else if (turn.outcome === 'operator-skipped') {
- metrics.operatorSkipped += 1;
- if (turn.skipKind === 'unattended') {
- metrics.unattendedSkips += 1;
- metrics.skippedAfterUnattendedRecovery += 1;
- } else metrics.manualSkips += 1;
- } else metrics[turn.outcome] += 1;
- metrics.visited.add(turn.observation.currentCell.cell);
- if (
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped'
- ) {
- if (turn.worldAction.type === 'move') metrics.requestedMoves += 1;
- if (turn.worldAction.type === 'infect') metrics.requestedInfections += 1;
- if (turn.worldAction.type === 'capture') metrics.requestedCaptures += 1;
- if (turn.worldAction.type === 'wait') metrics.requestedWaits += 1;
- if (turn.outcome === 'rejected') metrics.rejectedWorldActions += 1;
- if (turn.communicationResult.requested) {
- const channel = turn.communicationResult.accepted
- ? turn.communicationResult.event.channel
- : turn.communicationResult.attempt.channel;
- if (channel === 'public') metrics.publicMessagesRequested += 1;
- else if (channel === 'direct') metrics.directMessagesRequested += 1;
- else if (channel === 'alliance') metrics.allianceMessagesRequested += 1;
- else metrics.zeroBroadcastsRequested += 1;
- if (turn.communicationResult.accepted) {
- if (turn.communicationResult.event.channel === 'public') {
- metrics.publicMessagesAccepted += 1;
- metrics.publicMessagesSent += 1;
- } else if (turn.communicationResult.event.channel === 'direct') {
- metrics.directMessagesDelivered += 1;
- metrics.directMessagesSent += 1;
- if (aggregate) metrics.directMessagesReceived += 1;
- } else if (turn.communicationResult.event.channel === 'alliance')
- metrics.allianceMessagesDelivered += 1;
- else {
- metrics.zeroBroadcastsDelivered += 1;
- metrics.zeroRecipientDeliveries +=
- turn.communicationResult.event.recipientIds.length;
- for (const id of turn.communicationResult.event.recipientIds)
- metrics.zeroDirectiveRecipients.add(id);
- metrics.firstZeroDirectiveTurn ??= turn.turnNumber;
- metrics.mostRecentZeroDirective = {
- eventId: turn.communicationResult.event.id,
- turnNumber: turn.turnNumber,
- occurredAt: turn.communicationResult.event.occurredAt,
- agentId: turn.agentId,
- recipientCount: turn.communicationResult.event.recipientIds.length,
- modelId: turn.provider.model,
- reasoningProfile:
- turn.modelAttempts.at(-1)?.reasoningProfile ?? 'provider-default',
- personalityId: turn.observation.behavior.personalityId,
- strategyId: turn.observation.behavior.strategyId,
- };
- }
- } else if (turn.communicationResult.attempt.channel === 'public') {
- metrics.publicMessagesRejected += 1;
- } else if (turn.communicationResult.attempt.channel === 'direct')
- metrics.directMessagesRejected += 1;
- else if (turn.communicationResult.attempt.channel === 'alliance')
- metrics.allianceMessagesRejected += 1;
- else metrics.zeroBroadcastsRejected += 1;
- }
- if (
- turn.communicationResult.requested &&
- turn.communicationResult.accepted &&
- turn.communicationResult.event.channel === 'direct' &&
- turn.observation.patientZero.agentId !== null &&
- turn.agentId !== turn.observation.patientZero.agentId &&
- turn.communicationResult.event.recipientId ===
- turn.observation.patientZero.agentId
- ) {
- metrics.directRepliesToPatientZero += 1;
- metrics.patientZeroRepliers.add(turn.agentId);
- }
- if (turn.diplomacyResult.requested) {
- const type = turn.diplomacyResult.accepted
- ? turn.diplomacyResult.intent.type
- : turn.diplomacyResult.attempt.type;
- if (type === 'propose-alliance') metrics.diplomacyProposalsRequested += 1;
- if (type === 'accept-alliance')
- metrics.diplomacyAcceptancesRequested += 1;
- if (type === 'leave-alliance') metrics.diplomacyDeparturesRequested += 1;
- if (!turn.diplomacyResult.accepted) {
- metrics.diplomacyRejected += 1;
- const key = `${type}:${turn.diplomacyResult.reason}`;
- const existing = metrics.diplomacyRejections.get(key);
- metrics.diplomacyRejections.set(key, {
- type,
- reason: turn.diplomacyResult.reason,
- count: (existing?.count ?? 0) + 1,
+function resolvedActionsFromTicks(
+ ticks: readonly SwarmTickRecord[],
+ zeroAgentId: AgentId,
+): ResolvedWorldAction[] {
+ const resolved: ResolvedWorldAction[] = [];
+ for (const tick of ticks) {
+ if (tick.zeroAction && tick.zeroActionResult)
+ resolved.push({
+ tickNumber: tick.tickNumber,
+ agentId: zeroAgentId,
+ action: tick.zeroAction,
+ actionResult: tick.zeroActionResult,
+ });
+ for (const worker of tick.workers)
+ if (worker.action && worker.actionResult)
+ resolved.push({
+ tickNumber: tick.tickNumber,
+ agentId: worker.agentId,
+ action: worker.action,
+ actionResult: worker.actionResult,
});
- } else if (type === 'propose-alliance')
- metrics.diplomacyProposalsAccepted += 1;
- else if (type === 'accept-alliance')
- metrics.diplomacyAcceptancesAccepted += 1;
- else metrics.diplomacyDeparturesAccepted += 1;
- }
- if (
- turn.worldAction.type === 'capture' &&
- !turn.worldActionResult.accepted &&
- turn.worldActionResult.reason === 'allied-controller'
- ) {
- metrics.alliedCaptureAttempts += 1;
- metrics.alliedCaptureRejections += 1;
- }
- }
- for (const event of turn.allianceEvents)
- addAllianceEventMetric(
- metrics,
- event,
- aggregate ? undefined : turn.agentId,
- );
- if (
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'agent-moved'
- ) {
- metrics.acceptedMovements += 1;
- metrics.visited.add(turn.worldActionResult.event.toCell);
- }
- if (
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'hex-infected'
- ) {
- metrics.infections += 1;
- metrics.territoryGainedThroughInfection += 1;
- }
- if (
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'hex-captured'
- ) {
- metrics.successfulCaptures += 1;
- metrics.territoryGainedThroughCapture += 1;
- if (aggregate) metrics.territoryLostThroughCapture += 1;
- }
- if (
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'agent-waited'
- )
- metrics.acceptedWaits += 1;
- const attempts = usageAttempts(turn);
- metrics.modelCalls += attempts.length;
- metrics.failedModelAttempts += attempts.filter(({ failed }) => failed).length;
- metrics.automaticRepairAttempts += attempts.filter(
- ({ kind }) => kind === 'automatic-repair',
- ).length;
- metrics.automaticTransportRetries += attempts.filter(
- ({ kind }) => kind === 'automatic-transport-retry',
- ).length;
- metrics.manualRetryAttempts += attempts.filter(
- ({ kind }) => kind === 'manual-retry',
- ).length;
- metrics.unattendedRetryAttempts += attempts.filter(
- ({ kind }) => kind === 'unattended-retry',
- ).length;
- const retried = attempts.some(({ kind }) => kind !== 'initial');
- const manuallyRetried = attempts.some(({ kind }) => kind === 'manual-retry');
- const unattendedRetried = attempts.some(
- ({ kind }) => kind === 'unattended-retry',
- );
- if (retried) metrics.retriedTurns += 1;
- if (
- retried &&
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped'
- )
- metrics.recoveredByRetry += 1;
- if (
- retried &&
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped'
- ) {
- if (manuallyRetried) metrics.recoveredManually += 1;
- else if (unattendedRetried) {
- metrics.recoveredAutomatically += 1;
- metrics.recoveredByUnattendedRetry += 1;
- } else metrics.recoveredAutomatically += 1;
- }
- for (const { provider } of attempts) {
- if (provider) {
- metrics.latencyTotal += provider.latencyMs;
- metrics.latencyCount += 1;
- }
- for (const field of metricTokenFields) {
- const value = provider?.[field];
- if (value === undefined) metrics.tokenFieldsComplete[field] = false;
- else {
- metrics.tokens[field] += value;
- metrics.tokenFieldsKnown[field] = true;
- }
- }
- if (metricTokenFields.some((field) => provider?.[field] === undefined))
- metrics.attemptsWithUnknownTokenUsage += 1;
- if (provider?.costCredits === undefined) {
- metrics.attemptsWithUnknownCost += 1;
- metrics.turnsWithUnknownCost.add(turn.turnNumber);
- } else
- metrics.knownCostCredits = addDecimalValue(
- metrics.knownCostCredits,
- provider.costCredits,
- );
}
+ return resolved;
}
-function usageAttempts(turn: AgentTurnRecord) {
- if (turn.modelAttempts.length > 0)
- return turn.modelAttempts.map((attempt) => ({
- provider: attempt.provider,
- failed: attempt.failure !== undefined,
- kind: attempt.kind,
- }));
- return turn.provider
- ? [
- {
- provider: turn.provider,
- failed:
- turn.outcome === 'provider-error' || turn.outcome === 'lost-tick',
- kind: 'initial' as const,
- },
- ]
- : [];
+function selectTickNumbers(
+ source: ExperimentSource,
+ request: ExperimentExportRequest,
+): Set | 'all' {
+ if (request.turns.mode === 'entire-retained') return 'all';
+ const allTicks = (source.swarmTicks ?? []).map(
+ ({ tickNumber }) => tickNumber,
+ );
+ if (request.turns.mode === 'range') {
+ const { fromTurn, toTurn } = request.turns;
+ return new Set(allTicks.filter((n) => n >= fromTurn && n <= toTurn));
+ }
+ return new Set(allTicks.slice(-request.turns.count));
}
-function finalizeMutable(metrics: MutableMetrics) {
- const tokens: Record = {};
- if (metrics.turns > 0)
- for (const field of metricTokenFields)
- if (metrics.tokenFieldsKnown[field])
- tokens[field] = metrics.tokens[field];
- return {
- totalTurns: metrics.turns,
- accepted: metrics.accepted,
- rejected: metrics.rejected,
- providerErrors: metrics.providerErrors,
- lostTicks: metrics.lostTicks,
- operatorSkipped: metrics.operatorSkipped,
- modelCalls: metrics.modelCalls,
- failedModelAttempts: metrics.failedModelAttempts,
- automaticRepairAttempts: metrics.automaticRepairAttempts,
- automaticTransportRetries: metrics.automaticTransportRetries,
- manualRetryAttempts: metrics.manualRetryAttempts,
- unattendedRetryAttempts: metrics.unattendedRetryAttempts,
- manualSkips: metrics.manualSkips,
- unattendedSkips: metrics.unattendedSkips,
- recoveredByUnattendedRetry: metrics.recoveredByUnattendedRetry,
- skippedAfterUnattendedRecovery: metrics.skippedAfterUnattendedRecovery,
- retriedTurns: metrics.retriedTurns,
- recoveredAutomatically: metrics.recoveredAutomatically,
- recoveredManually: metrics.recoveredManually,
- recoveredByRetry: metrics.recoveredByRetry,
- requestedMoves: metrics.requestedMoves,
- requestedInfections: metrics.requestedInfections,
- requestedCaptures: metrics.requestedCaptures,
- requestedWaits: metrics.requestedWaits,
- acceptedMovements: metrics.acceptedMovements,
- successfullyInfectedCells: metrics.infections,
- successfulCaptures: metrics.successfulCaptures,
- acceptedWaits: metrics.acceptedWaits,
- rejectedWorldActions: metrics.rejectedWorldActions,
- territoryGainedThroughInfection: metrics.territoryGainedThroughInfection,
- territoryGainedThroughCapture: metrics.territoryGainedThroughCapture,
- territoryLostThroughCapture: metrics.territoryLostThroughCapture,
- publicMessagesRequested: metrics.publicMessagesRequested,
- publicMessagesAccepted: metrics.publicMessagesAccepted,
- publicMessagesRejected: metrics.publicMessagesRejected,
- directMessagesRequested: metrics.directMessagesRequested,
- directMessagesDelivered: metrics.directMessagesDelivered,
- directMessagesRejected: metrics.directMessagesRejected,
- allianceMessagesRequested: metrics.allianceMessagesRequested,
- allianceMessagesDelivered: metrics.allianceMessagesDelivered,
- allianceMessagesRejected: metrics.allianceMessagesRejected,
- publicMessagesSent: metrics.publicMessagesSent,
- directMessagesSent: metrics.directMessagesSent,
- directMessagesReceived: metrics.directMessagesReceived,
- zeroBroadcastsRequested: metrics.zeroBroadcastsRequested,
- zeroBroadcastsDelivered: metrics.zeroBroadcastsDelivered,
- zeroBroadcastsRejected: metrics.zeroBroadcastsRejected,
- zeroRecipientDeliveries: metrics.zeroRecipientDeliveries,
- uniqueZeroDirectiveRecipients: metrics.zeroDirectiveRecipients.size,
- directRepliesToPatientZero: metrics.directRepliesToPatientZero,
- uniquePatientZeroRepliers: metrics.patientZeroRepliers.size,
- firstZeroDirectiveTurn: metrics.firstZeroDirectiveTurn,
- mostRecentZeroDirective: metrics.mostRecentZeroDirective,
- diplomacyProposalsRequested: metrics.diplomacyProposalsRequested,
- diplomacyAcceptancesRequested: metrics.diplomacyAcceptancesRequested,
- diplomacyDeparturesRequested: metrics.diplomacyDeparturesRequested,
- diplomacyProposalsAccepted: metrics.diplomacyProposalsAccepted,
- diplomacyAcceptancesAccepted: metrics.diplomacyAcceptancesAccepted,
- diplomacyDeparturesAccepted: metrics.diplomacyDeparturesAccepted,
- diplomacyRejected: metrics.diplomacyRejected,
- diplomacyRejections: [...metrics.diplomacyRejections.values()],
- proposalsCreated: metrics.proposalsCreated,
- proposalsSent: metrics.proposalsSent,
- proposalsReceived: metrics.proposalsReceived,
- proposalsExpired: metrics.proposalsExpired,
- proposalsInvalidated: metrics.proposalsInvalidated,
- alliancesFormed: metrics.alliancesFormed,
- alliancesJoined: metrics.alliancesJoined,
- alliancesLeft: metrics.alliancesLeft,
- alliancesDissolved: metrics.alliancesDissolved,
- alliedCaptureAttempts: metrics.alliedCaptureAttempts,
- alliedCaptureRejections: metrics.alliedCaptureRejections,
- uniqueVisitedCells: metrics.visited.size,
- ...(metrics.latencyCount > 0
- ? { averageLatencyMs: metrics.latencyTotal / metrics.latencyCount }
- : {}),
- tokens,
- tokenUsageComplete: metrics.attemptsWithUnknownTokenUsage === 0,
- attemptsWithUnknownTokenUsage: metrics.attemptsWithUnknownTokenUsage,
- knownCostCredits: Number(metrics.knownCostCredits),
- attemptsWithUnknownCost: metrics.attemptsWithUnknownCost,
- turnsWithUnknownCost: metrics.turnsWithUnknownCost.size,
- };
+function requestFilteredActions(
+ source: ExperimentSource,
+ request: ExperimentExportRequest,
+ tickNumbers: Set | 'all',
+): ResolvedWorldAction[] {
+ const zeroAgentId = source.scenario.patientZeroAgentId;
+ const all = resolvedActionsFromTicks(source.swarmTicks ?? [], zeroAgentId);
+ return all.filter(
+ ({ tickNumber, action, actionResult }) =>
+ request.outcomes.includes(
+ actionResult.accepted ? 'accepted' : 'rejected',
+ ) &&
+ request.actions.includes(action.type) &&
+ (tickNumbers === 'all' || tickNumbers.has(tickNumber)),
+ );
}
export function createExperimentExport(
@@ -725,29 +147,39 @@ export function createExperimentExport(
const request = parsed.data;
const selectedAgentIds = resolveAgentIds(source, request);
const selectedSet = new Set(selectedAgentIds);
- const filtered = filterTurns(source, request, selectedSet);
- const communications = filterCommunications(source, request, selectedSet);
- const controlChanges = filterControlChanges(source, request, selectedSet);
- const allianceEvents = filterAllianceEvents(source, request, selectedSet);
- const providerAttempts = filterProviderAttempts(source, request, selectedSet);
- const firstRetainedTurn = source.turns[0]?.turnNumber;
- const lastRetainedTurn = source.turns.at(-1)?.turnNumber;
- const requestedRangeExtendsBeyondRetention = rangeExtendsBeyondRetention(
+ const tickNumbers = selectTickNumbers(source, request);
+ const requestFiltered = requestFilteredActions(source, request, tickNumbers);
+ const agentFiltered = requestFiltered.filter(({ agentId }) =>
+ selectedSet.has(agentId),
+ );
+ const controlChanges = filterControlChanges(
+ requestFiltered,
request,
+ selectedSet,
+ );
+ const providerAttempts = filterProviderAttempts(
source,
- firstRetainedTurn,
- lastRetainedTurn,
+ selectedSet,
+ tickNumbers,
);
- const droppedRecords = source.totalCompletedTurns - source.turns.length;
+ const retainedTicks = source.swarmTicks?.length ?? 0;
+ const firstRetainedTick = source.swarmTicks?.[0]?.tickNumber;
+ const lastRetainedTick = source.swarmTicks?.at(-1)?.tickNumber;
+ const droppedRecords = source.totalCompletedTicks - retainedTicks;
const retention = {
limit: source.retentionLimit,
- totalCompletedTurns: source.totalCompletedTurns,
- retainedTurns: source.turns.length,
- firstRetainedTurn,
- lastRetainedTurn,
+ totalCompletedTurns: source.totalCompletedTicks,
+ retainedTurns: retainedTicks,
+ firstRetainedTurn: firstRetainedTick,
+ lastRetainedTurn: lastRetainedTick,
droppedRecords,
complete: droppedRecords === 0,
- requestedRangeExtendsBeyondRetention,
+ requestedRangeExtendsBeyondRetention: rangeExtendsBeyondRetention(
+ request,
+ source,
+ firstRetainedTick,
+ lastRetainedTick,
+ ),
};
const include = inclusionsFor(request);
const exportedSwarmTicks: SwarmTickRecord[] | undefined =
@@ -756,21 +188,13 @@ export function createExperimentExport(
request.turns.mode === 'entire-retained'
? [...structuredClone(source.swarmTicks ?? [])]
: undefined;
- const selectedTickNumbers = new Set([
- ...filtered
- .map(({ tickNumber }) => tickNumber)
- .filter((tick): tick is number => tick !== undefined),
- ...(exportedSwarmTicks ?? []).map(({ tickNumber }) => tickNumber),
- ...source.simulatedPlayerEvents
- .filter(
- (event) =>
- (request.agents.mode === 'all' &&
- request.turns.mode === 'entire-retained') ||
- (event.type === 'simulated-player-agent-captured' &&
- selectedSet.has(event.capturedAgentId)),
- )
- .map(({ originatingTick }) => originatingTick),
- ]);
+ const simulatedPlayerEventsIncluded = source.simulatedPlayerEvents.filter(
+ (event) =>
+ (request.agents.mode === 'all' &&
+ request.turns.mode === 'entire-retained') ||
+ (event.type === 'simulated-player-agent-captured' &&
+ selectedSet.has(event.capturedAgentId)),
+ );
const currentAgentsById = new Map(
source.currentAgents.map((agent) => [agent.id, agent]),
);
@@ -783,16 +207,7 @@ export function createExperimentExport(
currentAgentsById.get(agentId) ?? initialAgentsById.get(agentId),
)
.filter((agent): agent is Agent => agent !== undefined)
- .map((agent) =>
- include.personality
- ? structuredClone(agent)
- : {
- id: agent.id,
- name: agent.name,
- color: agent.color,
- currentCell: agent.currentCell,
- },
- );
+ .map((agent) => structuredClone(agent));
const document: ExperimentExportDocument = {
schemaVersion: source.schemaVersion,
generatedAt,
@@ -802,7 +217,6 @@ export function createExperimentExport(
providerMode: source.providerMode,
swarmPlannerContractVersion: SWARM_PLANNER_CONTRACT_VERSION,
modelConfiguration: structuredClone(source.modelConfiguration),
- behaviorConfiguration: structuredClone(source.behaviorConfiguration),
scenario: structuredClone(source.scenario),
...(request.level === 'full-safe'
? { initialAgents: structuredClone([...source.initialAgents]) }
@@ -812,73 +226,42 @@ export function createExperimentExport(
filters: structuredClone(request),
selection: {
selectedAgentIds,
- matchingTurnCount: filtered.length,
...(source.schemaVersion === 10 || source.schemaVersion === 11
? {
matchingTickCount:
exportedSwarmTicks?.length ??
- new Set(
- filtered.map(({ tickNumber }) => tickNumber).filter(Boolean),
- ).size,
+ new Set(agentFiltered.map(({ tickNumber }) => tickNumber)).size,
}
: {}),
...(exportedSwarmTicks
? { matchingSwarmTickCount: exportedSwarmTicks.length }
: {}),
- matchingCommunicationCount: communications.length,
matchingControlChangeCount: controlChanges.length,
- matchingDiplomacyEventCount: allianceEvents.length,
...(source.schemaVersion === 11
? { matchingProviderAttemptCount: providerAttempts.length }
: {}),
- matchingSimulatedPlayerEventCount: source.simulatedPlayerEvents.filter(
- (event) => selectedTickNumbers.has(event.originatingTick),
- ).length,
- firstMatchingTurn: filtered[0]?.turnNumber,
- lastMatchingTurn: filtered.at(-1)?.turnNumber,
+ matchingSimulatedPlayerEventCount: simulatedPlayerEventsIncluded.length,
},
agents: selectedAgents,
- currentGoals: selectedAgentIds.map((agentId) =>
- structuredClone(
- source.agentGoals.find((entry) => entry.agentId === agentId) ?? {
- agentId,
- goal: null,
- },
- ),
- ),
- currentMemories: selectedAgentIds.map((agentId) =>
- structuredClone(
- source.agentMemories.find((entry) => entry.agentId === agentId) ?? {
- agentId,
- entries: [],
- },
- ),
- ),
configurationEvents: source.configurationEvents
- .filter((event) =>
- 'type' in event
- ? event.scope === 'global' ||
- (event.agentId !== undefined && selectedSet.has(event.agentId))
- : include.personalityHistory && selectedSet.has(event.agentId),
+ .filter(
+ (event) =>
+ event.scope === 'global' ||
+ (event.agentId !== undefined && selectedSet.has(event.agentId)),
)
.map((event) => structuredClone(event)),
...(include.metrics
? {
metrics: calculateExperimentMetrics(
- filtered,
+ agentFiltered,
selectedAgentIds,
- communications,
+ providerAttempts,
controlChanges,
- allianceEvents,
),
currentTerritory: currentTerritory(
source.currentWorld,
source.currentAgents,
),
- currentAlliances: currentAlliances(
- source.currentWorld,
- source.currentAgents,
- ),
simulatedPlayerMetrics: source.currentWorld.simulatedPlayer
?.metrics ?? {
movements: 0,
@@ -896,31 +279,30 @@ export function createExperimentExport(
...(request.level === 'full-safe'
? {
worldEvents: [
- ...filtered.flatMap((turn) => {
- if (
- turn.outcome !== 'accepted' ||
- turn.worldActionResult.event.type === 'hex-captured'
- )
- return [];
- return [structuredClone(turn.worldActionResult.event)];
- }),
- ...source.simulatedPlayerEvents.filter((event) => {
- return selectedTickNumbers.has(event.originatingTick);
- }),
+ ...agentFiltered.flatMap(({ actionResult }) =>
+ actionResult.accepted &&
+ actionResult.event.type !== 'hex-captured'
+ ? [structuredClone(actionResult.event)]
+ : [],
+ ),
+ ...simulatedPlayerEventsIncluded,
],
}
: {}),
- ...(include.communications
- ? { communications: structuredClone(communications) }
- : {}),
...(include.controlChanges
? { controlChanges: structuredClone(controlChanges) }
: {}),
- allianceEvents: structuredClone(allianceEvents),
- turns: filtered.map((turn) => exportTurn(turn, request)),
...(exportedSwarmTicks ? { swarmTicks: exportedSwarmTicks } : {}),
...(source.schemaVersion === 10 || source.schemaVersion === 11
- ? { tickSummaries: summarizeTicks(filtered) }
+ ? {
+ tickSummaries: summarizeTicks(
+ (source.swarmTicks ?? []).filter(
+ (tick) =>
+ tickNumbers === 'all' || tickNumbers.has(tick.tickNumber),
+ ),
+ providerAttempts,
+ ),
+ }
: {}),
...(source.schemaVersion === 11
? {
@@ -942,8 +324,6 @@ function exportWorldState(world: WorldSnapshot): ExperimentExportWorldState {
generatedAt: world.generatedAt,
hexes: structuredClone(world.hexes),
agents: structuredClone(world.agents),
- alliances: structuredClone(world.alliances),
- pendingAllianceProposals: structuredClone(world.pendingAllianceProposals),
simulatedPlayer: structuredClone(world.simulatedPlayer),
};
}
@@ -957,44 +337,12 @@ function currentTerritory(world: WorldSnapshot, agents: readonly Agent[]) {
(counts.get(hex.controllerAgentId) ?? 0) + 1,
);
}
- return agents.map(({ id, name, color }) => {
- const alliance = world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(id),
- );
- return {
- agentId: id,
- name,
- color,
- allianceId: alliance?.id ?? null,
- effectiveColor: alliance?.color ?? NEUTRAL_AGENT_COLOR,
- controlledCellCount: counts.get(id) ?? 0,
- };
- });
-}
-
-function currentAlliances(world: WorldSnapshot, agents: readonly Agent[]) {
- const territory = currentTerritory(world, agents);
- return world.alliances.map((alliance) => {
- const members = alliance.memberAgentIds.map((agentId) => {
- const entry = territory.find(
- (candidate) => candidate.agentId === agentId,
- )!;
- return {
- agentId,
- name: entry.name,
- controlledCellCount: entry.controlledCellCount,
- };
- });
- return {
- allianceId: alliance.id,
- color: alliance.color,
- totalControlledCellCount: members.reduce(
- (sum, member) => sum + member.controlledCellCount,
- 0,
- ),
- members,
- };
- });
+ return agents.map(({ id, name, color }) => ({
+ agentId: id,
+ name,
+ color,
+ controlledCellCount: counts.get(id) ?? 0,
+ }));
}
export function createExperimentPreview(
@@ -1005,29 +353,6 @@ export function createExperimentPreview(
const document = createExperimentExport(source, request, generatedAt);
const serialized = serializeExperimentExport(document);
const serializedUtf8Bytes = new TextEncoder().encode(serialized).byteLength;
- const metrics = calculateExperimentMetrics(
- filterTurns(
- source,
- document.filters,
- new Set(document.selection.selectedAgentIds),
- ),
- document.selection.selectedAgentIds,
- filterCommunications(
- source,
- document.filters,
- new Set(document.selection.selectedAgentIds),
- ),
- filterControlChanges(
- source,
- document.filters,
- new Set(document.selection.selectedAgentIds),
- ),
- filterAllianceEvents(
- source,
- document.filters,
- new Set(document.selection.selectedAgentIds),
- ),
- );
const ledgerKnownCost = (document.providerAttempts ?? []).reduce(
(sum, attempt) => sum + Number(attempt.actualCostCredits ?? 0),
0,
@@ -1037,31 +362,25 @@ export function createExperimentPreview(
).length;
return experimentExportPreviewSchema.parse({
experimentId: source.id,
- matchingTurnCount: document.selection.matchingTurnCount,
...(document.selection.matchingTickCount === undefined
? {}
: { matchingTickCount: document.selection.matchingTickCount }),
...(document.selection.matchingSwarmTickCount === undefined
? {}
: { matchingSwarmTickCount: document.selection.matchingSwarmTickCount }),
- matchingCommunicationCount: document.selection.matchingCommunicationCount,
matchingControlChangeCount: document.selection.matchingControlChangeCount,
- matchingDiplomacyEventCount: document.selection.matchingDiplomacyEventCount,
matchingProviderAttemptCount:
document.selection.matchingProviderAttemptCount ?? 0,
selectedAgentCount: document.selection.selectedAgentIds.length,
- firstMatchingTurn: document.selection.firstMatchingTurn,
- lastMatchingTurn: document.selection.lastMatchingTurn,
retention: document.retention,
knownCostCredits:
document.schemaVersion === 11
? ledgerKnownCost
- : metrics.aggregate.knownCostCredits,
+ : (document.metrics?.aggregate.knownCostCredits ?? 0),
attemptsWithUnknownCost:
document.schemaVersion === 11
? ledgerUnknownCost
- : metrics.aggregate.attemptsWithUnknownCost,
- turnsWithUnknownCost: metrics.aggregate.turnsWithUnknownCost,
+ : (document.metrics?.aggregate.attemptsWithUnknownCost ?? 0),
serializedUtf8Bytes,
approximateAiInputTokens: Math.ceil(serializedUtf8Bytes / 4),
tokenEstimateMethod: 'ceil(UTF-8 bytes / 4)',
@@ -1087,55 +406,19 @@ function resolveAgentIds(
return [...selected];
}
-function filterTurns(
- source: ExperimentSource,
- request: ExperimentExportRequest,
- selected: Set,
-): AgentTurnRecord[] {
- let turns = source.turns.filter(
- (turn) =>
- selected.has(turn.agentId) &&
- request.outcomes.includes(turn.outcome) &&
- (turn.outcome === 'provider-error' ||
- turn.outcome === 'lost-tick' ||
- turn.outcome === 'operator-skipped' ||
- request.actions.includes(turn.worldAction.type)),
- );
- if (request.turns.mode === 'range') {
- const range = request.turns;
- turns = turns.filter(
- ({ turnNumber }) =>
- turnNumber >= range.fromTurn && turnNumber <= range.toTurn,
- );
- } else if (request.turns.mode === 'latest') {
- turns = turns.slice(-request.turns.count);
- }
- return turns.map((turn) => structuredClone(turn));
-}
-
function filterProviderAttempts(
source: ExperimentSource,
- request: ExperimentExportRequest,
selected: Set,
+ tickNumbers: Set | 'all',
): ProviderAttemptRecord[] {
let attempts = (source.providerAttempts ?? []).filter(({ agentId }) =>
selected.has(agentId),
);
- if (request.turns.mode === 'range') {
- const { fromTurn, toTurn } = request.turns;
- attempts = attempts.filter(
- ({ intendedTurnNumber }) =>
- intendedTurnNumber >= fromTurn && intendedTurnNumber <= toTurn,
- );
- } else if (request.turns.mode === 'latest') {
- const fromTurn = Math.max(
- 1,
- source.totalCompletedTurns - request.turns.count + 1,
- );
+ if (tickNumbers !== 'all')
attempts = attempts.filter(
- ({ intendedTurnNumber }) => intendedTurnNumber >= fromTurn,
+ ({ intendedTickNumber }) =>
+ intendedTickNumber !== undefined && tickNumbers.has(intendedTickNumber),
);
- }
return structuredClone(attempts).sort(
(left, right) =>
left.startedAt.localeCompare(right.startedAt) ||
@@ -1143,71 +426,8 @@ function filterProviderAttempts(
);
}
-function filterCommunications(
- source: ExperimentSource,
- request: ExperimentExportRequest,
- selected: Set,
-): ExportedCommunication[] {
- let communications = source.turns.flatMap((turn) => {
- if (
- turn.outcome === 'provider-error' ||
- turn.outcome === 'lost-tick' ||
- turn.outcome === 'operator-skipped' ||
- !turn.communicationResult.requested
- )
- return [];
- const result = turn.communicationResult;
- const communication = result.accepted ? result.event : result.attempt;
- const selectedByParticipant =
- communication.channel !== 'direct'
- ? selected.has(communication.agentId)
- : selected.has(communication.agentId) ||
- (communication.recipientId !== null &&
- selected.has(communication.recipientId));
- const selectedByChannel =
- request.communications.channel === 'all' ||
- request.communications.channel === communication.channel;
- const status = result.accepted
- ? ('accepted' as const)
- : ('rejected' as const);
- const selectedByStatus =
- request.communications.status === 'all' ||
- request.communications.status === status;
- if (!selectedByParticipant || !selectedByChannel || !selectedByStatus)
- return [];
- return [
- {
- ...structuredClone(communication),
- originatingTurn: turn.turnNumber,
- status,
- ...(!result.accepted
- ? {
- rejectionReason: result.reason,
- rejectionDetails: result.details,
- }
- : {}),
- },
- ];
- });
- if (request.turns.mode === 'range') {
- const range = request.turns;
- communications = communications.filter(
- ({ originatingTurn }) =>
- originatingTurn >= range.fromTurn && originatingTurn <= range.toTurn,
- );
- } else if (request.turns.mode === 'latest') {
- const firstIncludedTurn = source.turns.at(-request.turns.count)?.turnNumber;
- communications = firstIncludedTurn
- ? communications.filter(
- ({ originatingTurn }) => originatingTurn >= firstIncludedTurn,
- )
- : communications;
- }
- return communications;
-}
-
function filterControlChanges(
- source: ExperimentSource,
+ requestFiltered: readonly ResolvedWorldAction[],
request: ExperimentExportRequest,
selected: Set,
): ExportedControlChange[] {
@@ -1216,69 +436,18 @@ function filterControlChanges(
!request.actions.includes('capture')
)
return [];
- let controlChanges = source.turns.flatMap((turn) => {
+ return requestFiltered.flatMap(({ tickNumber, actionResult }) => {
+ if (!actionResult.accepted || actionResult.event.type !== 'hex-captured')
+ return [];
+ const event = actionResult.event;
if (
- turn.outcome !== 'accepted' ||
- turn.worldActionResult.event.type !== 'hex-captured' ||
- (!selected.has(turn.worldActionResult.event.controllerAgentId) &&
- (turn.worldActionResult.event.previousControllerAgentId === null ||
- !selected.has(
- turn.worldActionResult.event.previousControllerAgentId,
- )))
+ !selected.has(event.controllerAgentId) &&
+ (event.previousControllerAgentId === null ||
+ !selected.has(event.previousControllerAgentId))
)
return [];
- return [
- {
- ...structuredClone(turn.worldActionResult.event),
- originatingTurn: turn.turnNumber,
- },
- ];
+ return [{ ...structuredClone(event), originatingTurn: tickNumber }];
});
- if (request.turns.mode === 'range') {
- const range = request.turns;
- controlChanges = controlChanges.filter(
- ({ originatingTurn }) =>
- originatingTurn >= range.fromTurn && originatingTurn <= range.toTurn,
- );
- } else if (request.turns.mode === 'latest') {
- controlChanges = controlChanges.slice(-request.turns.count);
- }
- return controlChanges;
-}
-
-function filterAllianceEvents(
- source: ExperimentSource,
- request: ExperimentExportRequest,
- selected: Set,
-): AllianceEvent[] {
- let events = source.turns.flatMap(({ allianceEvents }) =>
- allianceEvents.filter((event) =>
- allianceEventAgentIds(event).some((id) => selected.has(id)),
- ),
- );
- if (request.turns.mode === 'range') {
- const range = request.turns;
- events = events.filter(
- ({ turnNumber }) =>
- turnNumber >= range.fromTurn && turnNumber <= range.toTurn,
- );
- } else if (request.turns.mode === 'latest') {
- const first = source.turns.at(-request.turns.count)?.turnNumber;
- if (first) events = events.filter(({ turnNumber }) => turnNumber >= first);
- }
- return events.map((event) => structuredClone(event));
-}
-
-function allianceEventAgentIds(event: AllianceEvent): AgentId[] {
- if (event.type === 'alliance-proposed')
- return [event.agentId, event.recipientAgentId];
- if (event.type === 'alliance-proposal-closed')
- return [event.proposerAgentId, event.recipientAgentId];
- if (event.type === 'alliance-formed') return event.memberAgentIds;
- if (event.type === 'agent-joined-alliance') return event.memberAgentIds;
- if (event.type === 'agent-left-alliance')
- return [event.leftAgentId, ...event.remainingMemberAgentIds];
- return event.formerMemberAgentIds;
}
function rangeExtendsBeyondRetention(
@@ -1288,963 +457,256 @@ function rangeExtendsBeyondRetention(
last?: number,
): boolean {
if (request.turns.mode === 'entire-retained')
- return source.totalCompletedTurns > source.turns.length;
+ return source.totalCompletedTicks > (source.swarmTicks?.length ?? 0);
if (request.turns.mode !== 'range') return false;
if (!first || !last) return true;
return request.turns.fromTurn < first || request.turns.toTurn > last;
}
function inclusionsFor(request: ExperimentExportRequest) {
- if (request.level === 'full-safe')
- return {
- personality: true,
- personalityHistory: true,
- metrics: true,
- initialWorld: true,
- currentWorld: true,
- communications: true,
- controlChanges: true,
- };
if (request.level === 'custom') {
const custom = request.custom!;
return {
- personality: custom.personalityTextHistory,
- personalityHistory: custom.personalityTextHistory,
metrics: custom.computedMetrics,
initialWorld: custom.initialWorldState,
currentWorld: custom.currentWorldState,
- communications: custom.communications,
controlChanges: custom.controlChanges,
};
}
return {
- personality: true,
- personalityHistory: false,
metrics: true,
- initialWorld: false,
- currentWorld: false,
- communications: true,
+ initialWorld: request.level === 'full-safe',
+ currentWorld: request.level === 'full-safe',
controlChanges: true,
};
}
-function compactProvider(provider: ProviderMetadata): ProviderMetadata {
- return {
- provider: provider.provider,
- model: provider.model,
- ...(provider.selectedModel === undefined
- ? {}
- : { selectedModel: provider.selectedModel }),
- ...(provider.resolvedModel === undefined
- ? {}
- : { resolvedModel: provider.resolvedModel }),
- ...(provider.requestId === undefined
- ? {}
- : { requestId: provider.requestId }),
- ...(provider.httpStatus === undefined
- ? {}
- : { httpStatus: provider.httpStatus }),
- ...(provider.finishReason === undefined
- ? {}
- : { finishReason: provider.finishReason }),
- ...(provider.nativeFinishReason === undefined
- ? {}
- : { nativeFinishReason: provider.nativeFinishReason }),
- latencyMs: provider.latencyMs,
- ...(provider.promptTokens === undefined
- ? {}
- : { promptTokens: provider.promptTokens }),
- ...(provider.completionTokens === undefined
- ? {}
- : { completionTokens: provider.completionTokens }),
- ...(provider.totalTokens === undefined
- ? {}
- : { totalTokens: provider.totalTokens }),
- ...(provider.reasoningTokens === undefined
- ? {}
- : { reasoningTokens: provider.reasoningTokens }),
- ...(provider.cachedReadTokens === undefined
- ? {}
- : { cachedReadTokens: provider.cachedReadTokens }),
- ...(provider.cacheWriteTokens === undefined
- ? {}
- : { cacheWriteTokens: provider.cacheWriteTokens }),
- ...(provider.costCredits === undefined
- ? {}
- : { costCredits: provider.costCredits }),
- };
-}
-
-function exportTurn(
- turn: AgentTurnRecord,
- request: ExperimentExportRequest,
-): ExperimentExportDocument['turns'][number] {
- const standard = request.level === 'standard';
- const full = request.level === 'full-safe';
- const custom = request.level === 'custom' ? request.custom! : undefined;
- const includeObservation = full || standard || custom?.turnObservations;
- const includePersonality = full || standard || custom?.personalityTextHistory;
- const includeValidation = full || standard || custom?.validationDetails;
- const includeEvent = full || standard || custom?.resultingEvents;
- const includeProvider =
- request.level !== 'custom' || Boolean(custom?.providerUsageMetadata);
- const base: ExperimentExportDocument['turns'][number] = {
- turnNumber: turn.turnNumber,
- ...(turn.tickNumber === undefined ? {} : { tickNumber: turn.tickNumber }),
- ...(turn.tickPosition === undefined
- ? {}
- : { tickPosition: turn.tickPosition }),
- ...(turn.virtualTime === undefined
- ? {}
- : { virtualTime: turn.virtualTime }),
- ...(turn.tickIntervalMinutes === undefined
- ? {}
- : { tickIntervalMinutes: turn.tickIntervalMinutes }),
- startedAt: turn.startedAt,
- completedAt: turn.completedAt,
- agentId: turn.agentId,
- behavior: structuredClone(turn.behavior ?? turn.observation.behavior),
- outcome: turn.outcome,
- modelAttempts: turn.modelAttempts.map((attempt) => ({
- ...structuredClone(attempt),
- ...(includeProvider ? {} : { provider: undefined }),
- })),
- ...(turn.outcome === 'provider-error' ||
- turn.outcome === 'lost-tick' ||
- turn.outcome === 'operator-skipped'
- ? { failure: structuredClone(turn.failure) }
- : {
- worldAction: structuredClone(turn.worldAction),
- ...(turn.communication
- ? { communication: structuredClone(turn.communication) }
- : {}),
- ...(turn.diplomacy
- ? { diplomacy: structuredClone(turn.diplomacy) }
- : {}),
- ...(turn.goalRevision
- ? { goalRevision: structuredClone(turn.goalRevision) }
- : {}),
- goalRevisionResult: structuredClone(turn.goalRevisionResult),
- ...(turn.memoryOperation
- ? { memoryOperation: structuredClone(turn.memoryOperation) }
- : {}),
- memoryOperationResult: structuredClone(turn.memoryOperationResult),
- summary: turn.summary,
- worldActionSummary: turn.worldActionResult.accepted
- ? summarizeEvent(turn.worldActionResult.event)
- : `Rejected: ${turn.worldActionResult.reason}.`,
- ...(turn.communicationResult.requested
- ? {
- communicationSummary: turn.communicationResult.accepted
- ? summarizeCommunication(
- turn.communicationResult.event.channel,
- turn.communicationResult.event.channel === 'direct'
- ? turn.communicationResult.event.recipientId
- : undefined,
- turn.communicationResult.event.channel === 'direct'
- ? turn.communicationResult.event.distance
- : undefined,
- )
- : `Rejected: ${turn.communicationResult.reason}.`,
- }
- : {}),
- ...(turn.diplomacyResult.requested
- ? {
- diplomacySummary: turn.diplomacyResult.accepted
- ? `Accepted: ${turn.diplomacyResult.intent.type}.`
- : `Rejected: ${turn.diplomacyResult.reason}.`,
- }
- : {}),
- }),
- };
- if (includePersonality) base.personality = turn.observation.personality;
- if (includeObservation) {
- const observation: Partial = structuredClone(
- turn.observation,
- );
- if (!includePersonality) delete observation.personality;
- if (custom && !custom.nearbyAgents) delete observation.nearbyAgents;
- if (custom && !custom.recentEvents) delete observation.recentEvents;
- if (custom && !custom.recentPublicMessages)
- delete observation.recentPublicMessages;
- if (custom && !custom.recentDirectMessages)
- delete observation.recentDirectMessages;
- if (custom && !custom.recentControlChanges)
- delete observation.recentControlChanges;
- if (custom && !custom.recentControlChanges && observation.playerPressure)
- observation.playerPressure = {
- ...observation.playerPressure,
- recentThreats: [],
- };
- const globalThreatFeed =
- observation.patientZeroGlobalView?.playerThreatFeed;
- if (custom && !custom.recentControlChanges && globalThreatFeed)
- observation.patientZeroGlobalView = {
- ...observation.patientZeroGlobalView!,
- playerThreatFeed: {
- events: [],
- totalEventCount: globalThreatFeed.totalEventCount,
- truncated: globalThreatFeed.totalEventCount > 0,
- },
- };
- base.observation = observation;
- }
- if (
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- (includeValidation || includeEvent)
- ) {
- base.worldActionResult = structuredClone(turn.worldActionResult);
- base.communicationResult = structuredClone(turn.communicationResult);
- base.diplomacyResult = structuredClone(turn.diplomacyResult);
- }
- if (includeProvider && turn.provider)
- base.provider = full
- ? structuredClone(turn.provider)
- : compactProvider(turn.provider);
- return base;
-}
-
function summarizeTicks(
- records: readonly AgentTurnRecord[],
+ ticks: readonly SwarmTickRecord[],
+ attempts: readonly ProviderAttemptRecord[],
): ExperimentTickSummary[] {
- const groups = new Map();
- for (const record of records) {
- if (record.tickNumber === undefined) continue;
- groups.set(record.tickNumber, [
- ...(groups.get(record.tickNumber) ?? []),
- record,
- ]);
- }
- return [...groups.entries()].map(([tickNumber, tickRecords]) => {
- const attempts = tickRecords.flatMap(({ modelAttempts }) => modelAttempts);
- const latencies = attempts.map(
- ({ provider, failure }) => provider?.latencyMs ?? failure?.latencyMs ?? 0,
+ return ticks.map((tick) => {
+ const tickAttempts = attempts.filter(
+ ({ intendedTickNumber }) => intendedTickNumber === tick.tickNumber,
+ );
+ const latencies = tickAttempts.flatMap(({ provider }) =>
+ provider ? [provider.latencyMs] : [],
);
- const knownCosts = attempts.flatMap(({ provider }) =>
- provider?.costCredits === undefined ? [] : [provider.costCredits],
+ const knownCosts = tickAttempts.flatMap(({ actualCostCredits }) =>
+ actualCostCredits === undefined ? [] : [Number(actualCostCredits)],
);
return {
- tickNumber,
- virtualTime: tickRecords[0]!.virtualTime!,
- intervalMinutes: tickRecords[0]!.tickIntervalMinutes!,
- agentRecordCount: tickRecords.length,
- lostTicks: tickRecords.filter(({ outcome }) => outcome === 'lost-tick')
- .length,
- deadlineMisses: tickRecords.filter(
- (record) =>
- record.outcome === 'lost-tick' && record.failure.code === 'timeout',
- ).length,
- providerCallCount: attempts.length,
+ tickNumber: tick.tickNumber,
+ virtualTime: tick.virtualTime,
+ intervalMinutes: tick.tickIntervalMinutes,
+ agentRecordCount: Math.max(
+ 1,
+ tick.workers.length + (tick.zeroActionResult ? 1 : 0),
+ ),
+ lostTicks: 0,
+ deadlineMisses: 0,
+ providerCallCount: tickAttempts.length,
aggregateDecisionLatencyMs: latencies.reduce(
(total, latency) => total + latency,
0,
),
maximumDecisionLatencyMs: Math.max(0, ...latencies),
knownCostCredits: knownCosts.reduce((total, cost) => total + cost, 0),
- attemptsWithUnknownCost: attempts.length - knownCosts.length,
- };
- });
-}
-
-function summarizeEvent(
- event: Extract<
- AgentTurnRecord,
- { outcome: 'accepted' }
- >['worldActionResult']['event'],
-): string {
- if (event.type === 'agent-moved')
- return `Moved from ${event.fromCell} to ${event.toCell}.`;
- if (event.type === 'hex-infected') return `Infected ${event.cell}.`;
- if (event.type === 'hex-captured')
- return `Captured ${event.cell} from ${event.previousControllerAgentId}.`;
- return 'Waited.';
-}
-
-function summarizeCommunication(
- channel: 'public' | 'direct' | 'alliance' | 'zero',
- recipientId?: AgentId,
- distance?: number,
-): string {
- return channel === 'public'
- ? 'Published to world chat.'
- : channel === 'alliance'
- ? 'Delivered to current alliance members.'
- : channel === 'zero'
- ? 'Delivered privately to all other active agents.'
- : `Delivered directly to ${recipientId} from distance ${distance}.`;
-}
-
-export function calculateExperimentMetrics(
- turns: readonly AgentTurnRecord[],
- agentIds: readonly AgentId[],
- communications: readonly ExportedCommunication[] = [],
- controlChanges: readonly ExportedControlChange[] = [],
- allianceEvents: readonly AllianceEvent[] = [],
-): ExperimentMetrics {
- const metricFor = (
- records: readonly AgentTurnRecord[],
- relevantCommunications: readonly ExportedCommunication[],
- relevantControlChanges: readonly ExportedControlChange[],
- agentId?: AgentId,
- scopedAgentIds: readonly AgentId[] = agentIds,
- ) => {
- const attempts = records.flatMap(usageAttempts);
- const latencies = attempts.flatMap(({ provider }) =>
- provider ? [provider.latencyMs] : [],
- );
- const tokens: Record = {};
- for (const field of metricTokenFields) {
- const known = attempts
- .map(({ provider }) => provider?.[field])
- .filter((value): value is number => value !== undefined);
- if (known.length > 0)
- tokens[field] = known.reduce((sum, value) => sum + value, 0);
- }
- const attemptsWithUnknownTokenUsage = attempts.filter(({ provider }) =>
- metricTokenFields.some((field) => provider?.[field] === undefined),
- ).length;
- const visited = new Set();
- for (const turn of records) {
- visited.add(turn.observation.currentCell.cell);
- if (
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'agent-moved'
- )
- visited.add(turn.worldActionResult.event.toCell);
- }
- const costs = attempts.flatMap(({ provider }) =>
- provider?.costCredits === undefined ? [] : [provider.costCredits],
- );
- const manualRetryAttempts = attempts.filter(
- ({ kind }) => kind === 'manual-retry',
- ).length;
- const automaticRepairAttempts = attempts.filter(
- ({ kind }) => kind === 'automatic-repair',
- ).length;
- const automaticTransportRetries = attempts.filter(
- ({ kind }) => kind === 'automatic-transport-retry',
- ).length;
- const retriedTurns = records.filter((turn) =>
- usageAttempts(turn).some(({ kind }) => kind !== 'initial'),
- );
- const movement = movementMetrics(records);
- const mostRecentZeroTurn = records.findLast(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.communicationResult.requested &&
- turn.communicationResult.accepted &&
- turn.communicationResult.event.channel === 'zero',
- );
- const mostRecentZeroDirective =
- mostRecentZeroTurn &&
- mostRecentZeroTurn.outcome !== 'provider-error' &&
- mostRecentZeroTurn.outcome !== 'lost-tick' &&
- mostRecentZeroTurn.outcome !== 'operator-skipped' &&
- mostRecentZeroTurn.communicationResult.requested &&
- mostRecentZeroTurn.communicationResult.accepted &&
- mostRecentZeroTurn.communicationResult.event.channel === 'zero'
- ? {
- eventId: mostRecentZeroTurn.communicationResult.event.id,
- turnNumber: mostRecentZeroTurn.turnNumber,
- occurredAt: mostRecentZeroTurn.communicationResult.event.occurredAt,
- agentId: mostRecentZeroTurn.agentId,
- recipientCount:
- mostRecentZeroTurn.communicationResult.event.recipientIds.length,
- modelId: mostRecentZeroTurn.provider.model,
- reasoningProfile:
- mostRecentZeroTurn.modelAttempts.at(-1)?.reasoningProfile ??
- 'provider-default',
- personalityId:
- mostRecentZeroTurn.observation.behavior.personalityId,
- strategyId: mostRecentZeroTurn.observation.behavior.strategyId,
- }
- : null;
- return {
- totalTurns: records.length,
- accepted: records.filter(({ outcome }) => outcome === 'accepted').length,
- rejected: records.filter(({ outcome }) => outcome === 'rejected').length,
- providerErrors: records.filter(
- ({ outcome }) => outcome === 'provider-error',
- ).length,
- lostTicks: records.filter(({ outcome }) => outcome === 'lost-tick')
- .length,
- operatorSkipped: records.filter(
- ({ outcome }) => outcome === 'operator-skipped',
- ).length,
- modelCalls: attempts.length,
- failedModelAttempts: attempts.filter(({ failed }) => failed).length,
- automaticRepairAttempts,
- automaticTransportRetries,
- manualRetryAttempts,
- retriedTurns: retriedTurns.length,
- recoveredAutomatically: retriedTurns.filter(
- (turn) =>
- !usageAttempts(turn).some(({ kind }) => kind === 'manual-retry') &&
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped',
- ).length,
- recoveredManually: retriedTurns.filter(
- (turn) =>
- usageAttempts(turn).some(({ kind }) => kind === 'manual-retry') &&
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped',
- ).length,
- recoveredByRetry: retriedTurns.filter(
- ({ outcome }) =>
- outcome !== 'provider-error' &&
- outcome !== 'lost-tick' &&
- outcome !== 'operator-skipped',
- ).length,
- requestedMoves: records.filter(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.worldAction.type === 'move',
- ).length,
- requestedInfections: records.filter(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.worldAction.type === 'infect',
- ).length,
- requestedCaptures: records.filter(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.worldAction.type === 'capture',
- ).length,
- requestedWaits: records.filter(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.worldAction.type === 'wait',
- ).length,
- acceptedMovements: records.filter(
- (turn) =>
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'agent-moved',
- ).length,
- successfullyInfectedCells: records.filter(
- (turn) =>
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'hex-infected',
- ).length,
- successfulCaptures: records.filter(
- (turn) =>
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'hex-captured',
- ).length,
- acceptedWaits: records.filter(
- (turn) =>
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'agent-waited',
- ).length,
- rejectedWorldActions: records.filter(
- ({ outcome }) => outcome === 'rejected',
- ).length,
- territoryGainedThroughInfection: records.filter(
- (turn) =>
- turn.outcome === 'accepted' &&
- turn.worldActionResult.event.type === 'hex-infected',
- ).length,
- territoryGainedThroughCapture: agentId
- ? relevantControlChanges.filter(
- ({ controllerAgentId }) => controllerAgentId === agentId,
- ).length
- : relevantControlChanges.filter(({ controllerAgentId }) =>
- scopedAgentIds.includes(controllerAgentId),
- ).length,
- territoryLostThroughCapture: agentId
- ? relevantControlChanges.filter(
- ({ previousControllerAgentId }) =>
- previousControllerAgentId === agentId,
- ).length
- : relevantControlChanges.filter(
- ({ previousControllerAgentId }) =>
- previousControllerAgentId !== null &&
- scopedAgentIds.includes(previousControllerAgentId),
- ).length,
- ...communicationMetrics(relevantCommunications, scopedAgentIds, agentId),
- mostRecentZeroDirective,
- ...diplomacyMetrics(
- records,
- agentId
- ? allianceEvents
- : allianceEvents.filter((event) =>
- scopedAgentIds.includes(event.agentId),
- ),
- agentId,
- ),
- uniqueVisitedCells: visited.size,
- eligibleNearbyAgentObservations: records.reduce(
- (sum, turn) =>
- sum +
- turn.observation.nearbyAgents.filter(
- ({ directMessageLegal }) => directMessageLegal,
- ).length,
- 0,
- ),
- ...movement,
- ...(latencies.length > 0
- ? {
- averageLatencyMs:
- latencies.reduce((sum, value) => sum + value, 0) /
- latencies.length,
- }
- : {}),
- tokens,
- tokenUsageComplete: attemptsWithUnknownTokenUsage === 0,
- attemptsWithUnknownTokenUsage,
- knownCostCredits: sumDecimalNumbers(costs),
- attemptsWithUnknownCost: attempts.filter(
- ({ provider }) => provider?.costCredits === undefined,
- ).length,
- turnsWithUnknownCost: records.filter((turn) =>
- usageAttempts(turn).some(
- ({ provider }) => provider?.costCredits === undefined,
- ),
- ).length,
+ attemptsWithUnknownCost: tickAttempts.length - knownCosts.length,
};
- };
- const assignmentFor = (turn: AgentTurnRecord) =>
- turn.behavior ?? turn.observation.behavior;
- const metricsForBehavior = (matches: (turn: AgentTurnRecord) => boolean) => {
- const records = turns.filter(matches);
- const scopedAgentIds = [...new Set(records.map(({ agentId }) => agentId))];
- return metricFor(
- records,
- communications,
- controlChanges,
- undefined,
- scopedAgentIds,
- );
- };
- const combinations = [
- ...new Map(
- turns.map((turn) => {
- const assignment = assignmentFor(turn);
- return [
- `${assignment.personalityId}:${assignment.strategyId}`,
- assignment,
- ];
- }),
- ).values(),
- ];
- return experimentMetricsSchema.parse({
- aggregate: metricFor(turns, communications, controlChanges),
- byAgent: agentIds.map((agentId) => ({
- agentId,
- metrics: metricFor(
- turns.filter((turn) => turn.agentId === agentId),
- communications,
- controlChanges,
- agentId,
- ),
- })),
- byPersonality: PERSONALITY_PROFILES.map(({ id: personalityId }) => ({
- personalityId,
- metrics: metricsForBehavior(
- (turn) => assignmentFor(turn).personalityId === personalityId,
- ),
- })),
- byStrategy: STRATEGY_PROFILES.map(({ id: strategyId }) => ({
- strategyId,
- metrics: metricsForBehavior(
- (turn) => assignmentFor(turn).strategyId === strategyId,
- ),
- })),
- byBehaviorCombination: combinations.map(
- ({ personalityId, strategyId }) => ({
- personalityId,
- strategyId,
- metrics: metricsForBehavior((turn) => {
- const assignment = assignmentFor(turn);
- return (
- assignment.personalityId === personalityId &&
- assignment.strategyId === strategyId
- );
- }),
- }),
- ),
});
}
-function diplomacyMetrics(
- records: readonly AgentTurnRecord[],
- events: readonly AllianceEvent[],
- agentId?: AgentId,
-) {
- const relevantEvents = agentId
- ? events.filter((event) => allianceEventAgentIds(event).includes(agentId))
- : events;
- const completed = records.filter(
- (
- turn,
- ): turn is Exclude<
- AgentTurnRecord,
- { outcome: 'provider-error' | 'lost-tick' | 'operator-skipped' }
- > & {
- diplomacyResult: Exclude;
- } =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.diplomacyResult.requested,
+function attemptMetrics(attempts: readonly ProviderAttemptRecord[]) {
+ const failedModelAttempts = attempts.filter(
+ ({ outcome }) => outcome !== 'completed',
+ ).length;
+ const automaticRepairAttempts = attempts.filter(
+ ({ kind }) => kind === 'automatic-repair',
+ ).length;
+ const automaticTransportRetries = attempts.filter(
+ ({ kind }) => kind === 'automatic-transport-retry',
+ ).length;
+ const manualRetryAttempts = attempts.filter(
+ ({ kind }) => kind === 'manual-retry',
+ ).length;
+ const unattendedRetryAttempts = attempts.filter(
+ ({ kind }) => kind === 'unattended-retry',
+ ).length;
+ const groups = new Map();
+ for (const attempt of attempts) {
+ const key = `${attempt.agentId}:${attempt.intendedTickNumber ?? attempt.intendedTurnNumber}`;
+ groups.set(key, [...(groups.get(key) ?? []), attempt]);
+ }
+ const retriedGroups = [...groups.values()].filter((group) =>
+ group.some((attempt) => attempt.kind !== 'initial'),
);
- const requested = (type: string) =>
- completed.filter((turn) => {
- const result = turn.diplomacyResult;
- return (
- result.requested &&
- (result.accepted ? result.intent.type : result.attempt.type) === type
- );
- });
- const accepted = (type: string) =>
- requested(type).filter(
- (turn) => turn.diplomacyResult.requested && turn.diplomacyResult.accepted,
+ const recovered = (predicate: (group: ProviderAttemptRecord[]) => boolean) =>
+ retriedGroups.filter(
+ (group) =>
+ predicate(group) &&
+ group.some((attempt) => attempt.outcome === 'completed'),
).length;
- const formed = relevantEvents.filter(
- (event): event is Extract =>
- event.type === 'alliance-formed',
+ const latencies = attempts.flatMap(({ provider }) =>
+ provider ? [provider.latencyMs] : [],
);
- const completedDurations = formed.flatMap((created) => {
- const dissolved = relevantEvents.find(
- (event) =>
- event.type === 'alliance-dissolved' &&
- event.allianceId === created.allianceId &&
- event.turnNumber >= created.turnNumber,
- );
- return dissolved ? [dissolved.turnNumber - created.turnNumber] : [];
- });
- const allianceSizes = relevantEvents.flatMap((event) =>
- event.type === 'alliance-formed'
- ? [event.memberAgentIds.length]
- : event.type === 'agent-joined-alliance'
- ? [event.memberAgentIds.length]
- : [],
+ const tokens: Record = {};
+ for (const field of metricTokenFields) {
+ const known = attempts
+ .map(({ provider }) => provider?.[field])
+ .filter((value): value is number => value !== undefined);
+ if (known.length > 0)
+ tokens[field] = known.reduce((sum, value) => sum + value, 0);
+ }
+ const attemptsWithUnknownTokenUsage = attempts.filter(({ provider }) =>
+ metricTokenFields.some((field) => provider?.[field] === undefined),
+ ).length;
+ const costs = attempts.flatMap(({ actualCostCredits }) =>
+ actualCostCredits === undefined ? [] : [Number(actualCostCredits)],
);
return {
- diplomacyProposalsRequested: requested('propose-alliance').length,
- diplomacyAcceptancesRequested: requested('accept-alliance').length,
- diplomacyDeparturesRequested: requested('leave-alliance').length,
- diplomacyProposalsAccepted: accepted('propose-alliance'),
- diplomacyAcceptancesAccepted: accepted('accept-alliance'),
- diplomacyDeparturesAccepted: accepted('leave-alliance'),
- diplomacyRejected: completed.filter(
- (turn) => !turn.diplomacyResult.accepted,
- ).length,
- diplomacyRejections: groupedDiplomacyRejections(completed),
- proposalsCreated: relevantEvents.filter(
- (event) =>
- event.type === 'alliance-proposed' &&
- (!agentId || event.agentId === agentId),
- ).length,
- proposalsSent: relevantEvents.filter(
- (event) =>
- event.type === 'alliance-proposed' &&
- (!agentId || event.agentId === agentId),
- ).length,
- proposalsReceived: relevantEvents.filter(
- (event) =>
- event.type === 'alliance-proposed' &&
- (!agentId || event.recipientAgentId === agentId),
- ).length,
- proposalsExpired: relevantEvents.filter(
- (event) =>
- event.type === 'alliance-proposal-closed' && event.reason === 'expired',
- ).length,
- proposalsInvalidated: relevantEvents.filter(
- (event) =>
- event.type === 'alliance-proposal-closed' &&
- event.reason === 'invalidated',
- ).length,
- alliancesFormed: relevantEvents.filter(
- (event) => event.type === 'alliance-formed',
- ).length,
- alliancesJoined: relevantEvents.reduce(
- (count, event) =>
- count +
- (event.type === 'alliance-formed'
- ? agentId
- ? 1
- : event.memberAgentIds.length
- : event.type === 'agent-joined-alliance' &&
- (!agentId || event.joinedAgentId === agentId)
- ? 1
- : 0),
- 0,
+ modelCalls: attempts.length,
+ failedModelAttempts,
+ automaticRepairAttempts,
+ automaticTransportRetries,
+ manualRetryAttempts,
+ unattendedRetryAttempts,
+ retriedTurns: retriedGroups.length,
+ recoveredByUnattendedRetry: recovered(
+ (group) =>
+ group.some((attempt) => attempt.kind === 'unattended-retry') &&
+ !group.some((attempt) => attempt.kind === 'manual-retry'),
),
- alliancesLeft: relevantEvents.filter(
- (event) =>
- event.type === 'agent-left-alliance' &&
- (!agentId || event.leftAgentId === agentId),
- ).length,
- alliancesDissolved: relevantEvents.filter(
- (event) => event.type === 'alliance-dissolved',
- ).length,
- firstAllianceTurn: formed.length
- ? Math.min(...formed.map(({ turnNumber }) => turnNumber))
- : null,
- maximumAllianceSize: Math.max(0, ...allianceSizes),
- completedAllianceDurationTurnsTotal: completedDurations.reduce(
- (sum, duration) => sum + duration,
- 0,
+ recoveredManually: recovered((group) =>
+ group.some((attempt) => attempt.kind === 'manual-retry'),
),
- completedAllianceDurationTurnsAverage: completedDurations.length
- ? completedDurations.reduce((sum, duration) => sum + duration, 0) /
- completedDurations.length
- : 0,
- alliedCaptureAttempts: records.filter(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.worldAction.type === 'capture' &&
- !turn.worldActionResult.accepted &&
- turn.worldActionResult.reason === 'allied-controller',
- ).length,
- alliedCaptureRejections: records.filter(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.worldAction.type === 'capture' &&
- !turn.worldActionResult.accepted &&
- turn.worldActionResult.reason === 'allied-controller',
+ recoveredAutomatically: recovered(
+ (group) => !group.some((attempt) => attempt.kind === 'manual-retry'),
+ ),
+ recoveredByRetry: recovered(() => true),
+ ...(latencies.length > 0
+ ? {
+ averageLatencyMs:
+ latencies.reduce((sum, value) => sum + value, 0) / latencies.length,
+ }
+ : {}),
+ tokens,
+ tokenUsageComplete: attemptsWithUnknownTokenUsage === 0,
+ attemptsWithUnknownTokenUsage,
+ knownCostCredits: costs.reduce((sum, value) => sum + value, 0),
+ attemptsWithUnknownCost: attempts.filter(
+ ({ actualCostCredits }) => actualCostCredits === undefined,
).length,
};
}
-function movementMetrics(records: readonly AgentTurnRecord[]) {
- const moves = records.flatMap((turn) => {
- if (
- turn.outcome !== 'accepted' ||
- turn.worldActionResult.event.type !== 'agent-moved'
- )
- return [];
- const moved = turn.worldActionResult.event;
- const option = turn.observation.actionAvailability.moveOptions.find(
- ({ targetCell }) => targetCell === moved.toCell,
- );
- return option
- ? [{ turn, direction: option.direction, toCell: option.targetCell }]
- : [];
- });
- const counts = new Map<(typeof moves)[number]['direction'], number>();
- let longest = 0;
- let streak = 0;
- let previousDirection: (typeof moves)[number]['direction'] | undefined;
- let previousMoveAt: string | undefined;
- let directionChangesAfterCommunication = 0;
- const visited = new Set(
- records[0] ? [records[0].observation.currentCell.cell] : [],
- );
- let recentCellRevisits = 0;
- for (const { turn, direction, toCell } of moves) {
- const priorMoveAt = previousMoveAt;
- counts.set(direction, (counts.get(direction) ?? 0) + 1);
- streak = direction === previousDirection ? streak + 1 : 1;
- longest = Math.max(longest, streak);
- if (
- previousDirection &&
- priorMoveAt &&
- direction !== previousDirection &&
- (turn.observation.recentDirectMessages.some(
- ({ direction: messageDirection, occurredAt }) =>
- messageDirection === 'inbound' &&
- occurredAt > priorMoveAt &&
- occurredAt <= turn.completedAt,
- ) ||
- turn.observation.recentAllianceMessages.some(
- ({ senderId, occurredAt }) =>
- senderId !== turn.agentId &&
- occurredAt > priorMoveAt &&
- occurredAt <= turn.completedAt,
- ))
- )
- directionChangesAfterCommunication += 1;
- previousDirection = direction;
- previousMoveAt = turn.completedAt;
- if (visited.has(toCell)) recentCellRevisits += 1;
- visited.add(toCell);
- }
- return {
- movementDirectionDistribution: [...counts.entries()]
- .map(([direction, count]) => ({ direction, count }))
- .toSorted((a, b) => a.direction.localeCompare(b.direction)),
- longestRepeatedDirectionStreak: longest,
- recentCellRevisits,
- directionChangesAfterCommunication,
- };
-}
-
-function groupedDiplomacyRejections(records: readonly AgentTurnRecord[]) {
- const grouped = new Map<
- string,
- {
- type:
- 'propose-alliance' | 'accept-alliance' | 'leave-alliance' | 'invalid';
- reason: DiplomacyRejectionReason;
- count: number;
- }
- >();
- for (const turn of records) {
- if (
- turn.outcome === 'provider-error' ||
- turn.outcome === 'lost-tick' ||
- turn.outcome === 'operator-skipped' ||
- !turn.diplomacyResult.requested ||
- turn.diplomacyResult.accepted
- )
- continue;
- const { type } = turn.diplomacyResult.attempt;
- const { reason } = turn.diplomacyResult;
- const key = `${type}:${reason}`;
- grouped.set(key, {
- type,
- reason,
- count: (grouped.get(key)?.count ?? 0) + 1,
- });
- }
- return [...grouped.values()];
-}
-
-function communicationMetrics(
- communications: readonly ExportedCommunication[],
- agentIds: readonly AgentId[],
+function metricCountsFor(
+ scopeActions: readonly ResolvedWorldAction[],
+ scopeAttempts: readonly ProviderAttemptRecord[],
+ controlChanges: readonly ExportedControlChange[],
+ scopedAgentIds: readonly AgentId[],
agentId?: AgentId,
) {
- const authoredBySelection = ({ agentId: senderId }: ExportedCommunication) =>
- agentId ? senderId === agentId : agentIds.includes(senderId);
- const receivedBySelection = (communication: ExportedCommunication) =>
- communication.channel === 'direct' &&
- (agentId
- ? communication.recipientId === agentId
- : communication.recipientId !== undefined &&
- communication.recipientId !== null &&
- agentIds.includes(communication.recipientId));
- const publicAuthored = communications.filter(
- (communication) =>
- communication.channel === 'public' && authoredBySelection(communication),
- );
- const directAuthored = communications.filter(
- (communication) =>
- communication.channel === 'direct' && authoredBySelection(communication),
- );
- const allianceAuthored = communications.filter(
- (communication) =>
- communication.channel === 'alliance' &&
- authoredBySelection(communication),
- );
- const zeroAuthored = communications.filter(
- (communication) =>
- communication.channel === 'zero' && authoredBySelection(communication),
- );
- const patientZeroId = communications.find(
- ({ channel, status }) => channel === 'zero' && status === 'accepted',
- )?.agentId;
- const repliesToPatientZero = patientZeroId
- ? directAuthored.filter(
- ({ status, recipientId, agentId: senderId }) =>
- status === 'accepted' &&
- recipientId === patientZeroId &&
- senderId !== patientZeroId,
- )
- : [];
- const deliveredDirect = directAuthored.filter(
- (communication) =>
- communication.status === 'accepted' &&
- communication.distance !== null &&
- communication.distance !== undefined,
- );
- const directDistanceTotal = deliveredDirect.reduce(
- (sum, communication) => sum + (communication.distance ?? 0),
- 0,
+ const accepted = scopeActions.filter(
+ ({ actionResult }) => actionResult.accepted,
+ ).length;
+ const rejected = scopeActions.length - accepted;
+ const visited = new Set();
+ const firstMove = scopeActions.find(
+ ({ actionResult }) =>
+ actionResult.accepted && actionResult.event.type === 'agent-moved',
);
+ if (
+ firstMove &&
+ firstMove.actionResult.accepted &&
+ firstMove.actionResult.event.type === 'agent-moved'
+ )
+ visited.add(firstMove.actionResult.event.fromCell);
+ for (const { actionResult } of scopeActions)
+ if (actionResult.accepted && actionResult.event.type === 'agent-moved')
+ visited.add(actionResult.event.toCell);
+ const successfullyInfectedCells = scopeActions.filter(
+ ({ actionResult }) =>
+ actionResult.accepted && actionResult.event.type === 'hex-infected',
+ ).length;
+ const territoryGainedThroughCapture = agentId
+ ? controlChanges.filter(
+ ({ controllerAgentId }) => controllerAgentId === agentId,
+ ).length
+ : controlChanges.filter(({ controllerAgentId }) =>
+ scopedAgentIds.includes(controllerAgentId),
+ ).length;
+ const territoryLostThroughCapture = agentId
+ ? controlChanges.filter(
+ ({ previousControllerAgentId }) =>
+ previousControllerAgentId === agentId,
+ ).length
+ : controlChanges.filter(
+ ({ previousControllerAgentId }) =>
+ previousControllerAgentId !== null &&
+ scopedAgentIds.includes(previousControllerAgentId),
+ ).length;
return {
- publicMessagesRequested: publicAuthored.length,
- publicMessagesAccepted: publicAuthored.filter(
- ({ status }) => status === 'accepted',
- ).length,
- publicMessagesRejected: publicAuthored.filter(
- ({ status }) => status === 'rejected',
- ).length,
- directMessagesRequested: directAuthored.length,
- directMessagesDelivered: directAuthored.filter(
- ({ status }) => status === 'accepted',
- ).length,
- directMessagesRejected: directAuthored.filter(
- ({ status }) => status === 'rejected',
+ totalTurns: scopeActions.length,
+ accepted,
+ rejected,
+ providerErrors: 0,
+ requestedMoves: scopeActions.filter(({ action }) => action.type === 'move')
+ .length,
+ requestedInfections: scopeActions.filter(
+ ({ action }) => action.type === 'infect',
).length,
- allianceMessagesRequested: allianceAuthored.length,
- allianceMessagesDelivered: allianceAuthored.filter(
- ({ status }) => status === 'accepted',
+ requestedCaptures: scopeActions.filter(
+ ({ action }) => action.type === 'capture',
).length,
- allianceMessagesRejected: allianceAuthored.filter(
- ({ status }) => status === 'rejected',
+ requestedWaits: scopeActions.filter(({ action }) => action.type === 'wait')
+ .length,
+ acceptedMovements: scopeActions.filter(
+ ({ actionResult }) =>
+ actionResult.accepted && actionResult.event.type === 'agent-moved',
).length,
- zeroBroadcastsRequested: zeroAuthored.length,
- zeroBroadcastsDelivered: zeroAuthored.filter(
- ({ status }) => status === 'accepted',
+ successfullyInfectedCells,
+ successfulCaptures: scopeActions.filter(
+ ({ actionResult }) =>
+ actionResult.accepted && actionResult.event.type === 'hex-captured',
).length,
- zeroBroadcastsRejected: zeroAuthored.filter(
- ({ status }) => status === 'rejected',
+ acceptedWaits: scopeActions.filter(
+ ({ actionResult }) =>
+ actionResult.accepted && actionResult.event.type === 'agent-waited',
).length,
- zeroRecipientDeliveries: zeroAuthored.reduce(
- (sum, communication) =>
- sum +
- (communication.status === 'accepted'
- ? (communication.recipientIds?.length ?? 0)
- : 0),
- 0,
+ rejectedWorldActions: rejected,
+ territoryGainedThroughInfection: successfullyInfectedCells,
+ territoryGainedThroughCapture,
+ territoryLostThroughCapture,
+ uniqueVisitedCells: visited.size,
+ ...attemptMetrics(scopeAttempts),
+ };
+}
+
+export function calculateExperimentMetrics(
+ resolvedActions: readonly ResolvedWorldAction[],
+ agentIds: readonly AgentId[],
+ providerAttempts: readonly ProviderAttemptRecord[] = [],
+ controlChanges: readonly ExportedControlChange[] = [],
+): ExperimentMetrics {
+ return experimentMetricsSchema.parse({
+ aggregate: metricCountsFor(
+ resolvedActions,
+ providerAttempts,
+ controlChanges,
+ agentIds,
),
- uniqueZeroDirectiveRecipients: new Set(
- zeroAuthored.flatMap(({ recipientIds }) => recipientIds ?? []),
- ).size,
- directRepliesToPatientZero: repliesToPatientZero.length,
- uniquePatientZeroRepliers: new Set(
- repliesToPatientZero.map(({ agentId: senderId }) => senderId),
- ).size,
- firstZeroDirectiveTurn:
- zeroAuthored.find(({ status }) => status === 'accepted')
- ?.originatingTurn ?? null,
- publicMessagesSent: publicAuthored.filter(
- ({ status }) => status === 'accepted',
- ).length,
- directMessagesSent: directAuthored.filter(
- ({ status }) => status === 'accepted',
- ).length,
- directMessagesReceived: communications.filter(
- (communication) =>
- communication.status === 'accepted' &&
- receivedBySelection(communication),
- ).length,
- uniqueDirectMessagePairs: new Set(
- directAuthored.flatMap((communication) =>
- communication.recipientId
- ? [`${communication.agentId}:${communication.recipientId}`]
- : [],
+ byAgent: agentIds.map((agentId) => ({
+ agentId,
+ metrics: metricCountsFor(
+ resolvedActions.filter((action) => action.agentId === agentId),
+ providerAttempts.filter((attempt) => attempt.agentId === agentId),
+ controlChanges,
+ [agentId],
+ agentId,
),
- ).size,
- directMessageDistanceTotalKm: directDistanceTotal,
- directMessageDistanceAverageKm: deliveredDirect.length
- ? directDistanceTotal / deliveredDirect.length
- : 0,
- directMessageDistanceMaximumKm: Math.max(
- 0,
- ...deliveredDirect.map(({ distance }) => distance ?? 0),
- ),
- };
+ })),
+ });
}
export function serializeExperimentExport(
@@ -2254,43 +716,3 @@ export function serializeExperimentExport(
? JSON.stringify(document, null, 2)
: JSON.stringify(document);
}
-
-export function addDecimalValue(left: string, right: number): string {
- const leftParts = decimalParts(left);
- const rightParts = decimalParts(right);
- const scale = Math.max(leftParts.scale, rightParts.scale);
- const leftInteger =
- leftParts.integer * 10n ** BigInt(scale - leftParts.scale);
- const rightInteger =
- rightParts.integer * 10n ** BigInt(scale - rightParts.scale);
- return decimalString(leftInteger + rightInteger, scale);
-}
-
-function sumDecimalNumbers(values: readonly number[]): number {
- return Number(values.reduce(addDecimalValue, '0'));
-}
-
-function decimalParts(value: number | string): {
- integer: bigint;
- scale: number;
-} {
- const [mantissa, exponentText = '0'] = value
- .toString()
- .toLowerCase()
- .split('e');
- const exponent = Number(exponentText);
- const [whole, fraction = ''] = mantissa!.split('.');
- let integer = BigInt(`${whole}${fraction}`);
- let scale = fraction.length - exponent;
- if (scale < 0) {
- integer *= 10n ** BigInt(-scale);
- scale = 0;
- }
- return { integer, scale };
-}
-
-function decimalString(integer: bigint, scale: number): string {
- if (scale === 0) return integer.toString();
- const digits = integer.toString().padStart(scale + 1, '0');
- return `${digits.slice(0, -scale)}.${digits.slice(-scale)}`;
-}
diff --git a/apps/game-api/src/live-swarm-comparison.ts b/apps/game-api/src/live-swarm-comparison.ts
index c38ec2c..9565e74 100644
--- a/apps/game-api/src/live-swarm-comparison.ts
+++ b/apps/game-api/src/live-swarm-comparison.ts
@@ -1,5 +1,5 @@
import { type ReflexProvider, type SwarmPlanner } from '@hexzero/agent-runtime';
-import { assignBehavior, type CompatibleModel } from '@hexzero/shared';
+import { type CompatibleModel } from '@hexzero/shared';
import { generateDeterministicRoster } from '@hexzero/world-engine';
import { SimulationService } from './simulation-service';
import type { CompiledReflexObservation } from './reflex-execution';
@@ -340,7 +340,6 @@ function exportRequest() {
turns: { mode: 'entire-retained' },
outcomes: ['accepted', 'rejected', 'provider-error', 'operator-skipped'],
actions: ['move', 'infect', 'capture', 'wait'],
- communications: { channel: 'all', status: 'all' },
serialization: 'compact',
level: 'full-safe',
} as const;
@@ -407,16 +406,6 @@ async function runVariant(
overrides: [],
locked: false,
},
- // This transitional scenario field remains required until PR 2 removes
- // legacy behavior configuration from the shared world setup schema.
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- roster.map(({ id }) => id),
- `live-behavior-${seed}`,
- 'balanced-random',
- ),
- },
});
const ticks: LiveComparisonTick[] = [];
let stoppedReason: LiveComparisonRun['final']['stoppedReason'] = 'tick-cap';
diff --git a/apps/game-api/src/observation-history.test.ts b/apps/game-api/src/observation-history.test.ts
deleted file mode 100644
index c12e323..0000000
--- a/apps/game-api/src/observation-history.test.ts
+++ /dev/null
@@ -1,203 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import {
- agentIdSchema,
- worldEventSchema,
- type AgentId,
- type WorldEvent,
-} from '@hexzero/shared';
-import { ObservationHistory } from './observation-history';
-
-const actor = agentIdSchema.parse('00000000-0000-4000-8000-000000000001');
-const peer = agentIdSchema.parse('00000000-0000-4000-8000-000000000002');
-const outsider = agentIdSchema.parse('00000000-0000-4000-8000-000000000003');
-const cellA = '892a94d232bffff';
-const cellB = '892a94d2323ffff';
-
-describe('ObservationHistory', () => {
- it('retains independent factual streams through more than 120 unrelated events', () => {
- const history = new ObservationHistory();
- history.ingest([
- ...range(7).map((index) => movement(actor, index)),
- ...range(10).map((index) => waited(actor, index)),
- ...range(15).map((index) => publicMessage(actor, index)),
- ...range(7).map((index) => directMessage(actor, peer, index)),
- ...range(7).map((index) => allianceMessage(actor, [peer], index)),
- ...range(7).map((index) => zeroMessage(actor, [peer], index)),
- ...range(7).map((index) => capture(actor, peer, index)),
- ...range(14).map((index) => proposed(actor, peer, index)),
- ]);
- history.ingest(
- range(130).map((index) => directMessage(outsider, peer, index + 100)),
- );
-
- expect(ids(history.movements(actor))).toEqual(
- range(6).map((index) => eventId(index + 1)),
- );
- expect(history.actions()).toHaveLength(8);
- expect(history.publicMessages()).toHaveLength(12);
- expect(history.directMessages(actor)).toHaveLength(6);
- expect(history.directMessages(outsider)).toHaveLength(6);
- expect(history.directMessages(agent('4'))).toEqual([]);
- expect(history.allianceMessages(actor)).toHaveLength(6);
- expect(history.allianceMessages(peer)).toHaveLength(6);
- expect(history.allianceMessages(outsider)).toEqual([]);
- expect(history.zeroMessages(actor)).toHaveLength(6);
- expect(history.zeroMessages(peer)).toHaveLength(6);
- expect(history.zeroMessages(outsider)).toEqual([]);
- expect(history.controlChanges(actor)).toHaveLength(6);
- expect(history.controlChanges(peer)).toHaveLength(6);
- expect(history.captures()).toHaveLength(6);
- expect(history.allianceEvents(8)).toHaveLength(8);
- expect(history.allianceEvents(12)).toHaveLength(12);
- });
-
- it('initializes existing facts in order and ingests an event ID only once', () => {
- const first = directMessage(actor, peer, 1);
- const second = directMessage(peer, actor, 2);
- const history = new ObservationHistory([first]);
- history.ingest([first, second]);
-
- expect(ids(history.directMessages(actor))).toEqual([first.id, second.id]);
- expect(ids(history.directMessages(peer))).toEqual([first.id, second.id]);
- });
-});
-
-function range(length: number): number[] {
- return Array.from({ length }, (_, index) => index);
-}
-
-function agent(last: string): AgentId {
- return agentIdSchema.parse(`00000000-0000-4000-8000-00000000000${last}`);
-}
-
-function eventId(index: number): string {
- return `10000000-0000-4000-8000-${String(index).padStart(12, '0')}`;
-}
-
-function event(input: Record, index: number): WorldEvent {
- return worldEventSchema.parse({
- id: eventId(index),
- occurredAt: `2026-08-25T12:${String(Math.floor(index / 60)).padStart(2, '0')}:${String(index % 60).padStart(2, '0')}.000Z`,
- ...input,
- });
-}
-
-function movement(agentId: AgentId, index: number): WorldEvent {
- return event(
- { type: 'agent-moved', agentId, fromCell: cellA, toCell: cellB },
- index,
- );
-}
-
-function waited(agentId: AgentId, index: number): WorldEvent {
- return event({ type: 'agent-waited', agentId }, index + 20);
-}
-
-function publicMessage(agentId: AgentId, index: number): WorldEvent {
- return event(
- {
- type: 'public-message-sent',
- channel: 'public',
- agentId,
- message: `public ${index}`,
- playerVisible: true,
- },
- index + 40,
- );
-}
-
-function directMessage(
- agentId: AgentId,
- recipientId: AgentId,
- index: number,
-): WorldEvent {
- return event(
- {
- type: 'direct-message-sent',
- channel: 'direct',
- agentId,
- recipientId,
- message: `direct ${index}`,
- distance: 1,
- playerVisible: false,
- },
- index + 1000,
- );
-}
-
-function allianceMessage(
- agentId: AgentId,
- recipientIds: AgentId[],
- index: number,
-): WorldEvent {
- return event(
- {
- type: 'alliance-message-sent',
- channel: 'alliance',
- agentId,
- recipientIds,
- allianceId: '20000000-0000-4000-8000-000000000001',
- message: `alliance ${index}`,
- playerVisible: false,
- },
- index + 70,
- );
-}
-
-function zeroMessage(
- agentId: AgentId,
- recipientIds: AgentId[],
- index: number,
-): WorldEvent {
- return event(
- {
- type: 'zero-message-sent',
- channel: 'zero',
- agentId,
- recipientIds,
- message: `zero ${index}`,
- playerVisible: false,
- },
- index + 80,
- );
-}
-
-function capture(
- controllerAgentId: AgentId,
- previousControllerAgentId: AgentId,
- index: number,
-): WorldEvent {
- return event(
- {
- type: 'hex-captured',
- agentId: controllerAgentId,
- controllerAgentId,
- previousControllerAgentId,
- cell: cellA,
- },
- index + 90,
- );
-}
-
-function proposed(
- agentId: AgentId,
- recipientAgentId: AgentId,
- index: number,
-): WorldEvent {
- return event(
- {
- type: 'alliance-proposed',
- agentId,
- recipientAgentId,
- proposalId: `30000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
- allianceId: null,
- turnNumber: index + 1,
- expirationTurn: index + 10,
- },
- index + 110,
- );
-}
-
-function ids(events: readonly WorldEvent[]): string[] {
- return events.map(({ id }) => id);
-}
diff --git a/apps/game-api/src/observation-history.ts b/apps/game-api/src/observation-history.ts
deleted file mode 100644
index 6b11cad..0000000
--- a/apps/game-api/src/observation-history.ts
+++ /dev/null
@@ -1,199 +0,0 @@
-import {
- RECENT_ALLIANCE_EVENT_LIMIT,
- RECENT_CONTROL_CHANGE_LIMIT,
- RECENT_DIRECT_MESSAGE_LIMIT,
- RECENT_PUBLIC_MESSAGE_LIMIT,
- RECENT_ZERO_MESSAGE_LIMIT,
- RECENT_ZERO_STRATEGIC_EVENT_LIMIT,
- type AgentId,
- type AllianceEvent,
- type WorldEvent,
-} from '@hexzero/shared';
-
-type EventOf = Extract;
-type ActionEvent = EventOf<
- 'agent-moved' | 'hex-infected' | 'hex-captured' | 'agent-waited'
->;
-
-const RECENT_MOVEMENT_LIMIT = 6;
-const RECENT_ACTION_LIMIT = 8;
-
-/**
- * Commit-owned factual history for agent observations. This is deliberately
- * independent from WorldState.events, whose 120-event retention serves the
- * operator display rather than the observation contract.
- */
-export class ObservationHistory {
- #movements = new Map[]>();
- #actions: ActionEvent[] = [];
- #publicMessages: EventOf<'public-message-sent'>[] = [];
- #directMessages = new Map[]>();
- #allianceMessages = new Map[]>();
- #zeroMessages = new Map[]>();
- #controlChanges = new Map[]>();
- #allianceEvents: AllianceEvent[] = [];
- #captures: EventOf<'hex-captured'>[] = [];
-
- constructor(initialEvents: readonly WorldEvent[] = []) {
- this.ingest(initialEvents);
- }
-
- ingest(events: readonly WorldEvent[]): void {
- // Engine event IDs are authoritative and unique. The batch set protects
- // initialization/call-site mistakes, while #contains scans only bounded
- // retained facts and therefore cannot become a lifetime event registry.
- const batchEventIds = new Set();
- for (const event of events) {
- if (batchEventIds.has(event.id) || this.#contains(event.id)) continue;
- batchEventIds.add(event.id);
- switch (event.type) {
- case 'agent-moved':
- appendFor(
- this.#movements,
- event.agentId,
- event,
- RECENT_MOVEMENT_LIMIT,
- );
- this.#actions = append(this.#actions, event, RECENT_ACTION_LIMIT);
- break;
- case 'hex-infected':
- case 'agent-waited':
- this.#actions = append(this.#actions, event, RECENT_ACTION_LIMIT);
- break;
- case 'hex-captured':
- this.#actions = append(this.#actions, event, RECENT_ACTION_LIMIT);
- this.#captures = append(
- this.#captures,
- event,
- RECENT_CONTROL_CHANGE_LIMIT,
- );
- appendFor(
- this.#controlChanges,
- event.controllerAgentId,
- event,
- RECENT_CONTROL_CHANGE_LIMIT,
- );
- if (event.previousControllerAgentId !== null)
- appendFor(
- this.#controlChanges,
- event.previousControllerAgentId,
- event,
- RECENT_CONTROL_CHANGE_LIMIT,
- );
- break;
- case 'public-message-sent':
- this.#publicMessages = append(
- this.#publicMessages,
- event,
- RECENT_PUBLIC_MESSAGE_LIMIT,
- );
- break;
- case 'direct-message-sent':
- for (const participant of [event.agentId, event.recipientId])
- appendFor(
- this.#directMessages,
- participant,
- event,
- RECENT_DIRECT_MESSAGE_LIMIT,
- );
- break;
- case 'alliance-message-sent':
- for (const participant of new Set([
- event.agentId,
- ...event.recipientIds,
- ]))
- appendFor(
- this.#allianceMessages,
- participant,
- event,
- RECENT_DIRECT_MESSAGE_LIMIT,
- );
- break;
- case 'zero-message-sent':
- for (const participant of new Set([
- event.agentId,
- ...event.recipientIds,
- ]))
- appendFor(
- this.#zeroMessages,
- participant,
- event,
- RECENT_ZERO_MESSAGE_LIMIT,
- );
- break;
- case 'alliance-proposed':
- case 'alliance-proposal-closed':
- case 'alliance-formed':
- case 'agent-joined-alliance':
- case 'agent-left-alliance':
- case 'alliance-dissolved':
- this.#allianceEvents = append(
- this.#allianceEvents,
- event,
- RECENT_ZERO_STRATEGIC_EVENT_LIMIT,
- );
- break;
- case 'simulated-player-moved':
- case 'hex-disinfected':
- case 'simulated-player-clean-blocked':
- case 'simulated-player-agent-captured':
- break;
- }
- }
- }
-
- movements(agentId: AgentId) {
- return structuredClone(this.#movements.get(agentId) ?? []);
- }
- actions() {
- return structuredClone(this.#actions);
- }
- publicMessages() {
- return structuredClone(this.#publicMessages);
- }
- directMessages(agentId: AgentId) {
- return structuredClone(this.#directMessages.get(agentId) ?? []);
- }
- allianceMessages(agentId: AgentId) {
- return structuredClone(this.#allianceMessages.get(agentId) ?? []);
- }
- zeroMessages(agentId: AgentId) {
- return structuredClone(this.#zeroMessages.get(agentId) ?? []);
- }
- controlChanges(agentId: AgentId) {
- return structuredClone(this.#controlChanges.get(agentId) ?? []);
- }
- allianceEvents(limit: number = RECENT_ALLIANCE_EVENT_LIMIT) {
- return structuredClone(this.#allianceEvents.slice(-limit));
- }
- captures() {
- return structuredClone(this.#captures);
- }
-
- #contains(eventId: string): boolean {
- return [
- ...this.#movements.values(),
- this.#actions,
- this.#publicMessages,
- ...this.#directMessages.values(),
- ...this.#allianceMessages.values(),
- ...this.#zeroMessages.values(),
- ...this.#controlChanges.values(),
- this.#allianceEvents,
- this.#captures,
- ].some((events) => events.some(({ id }) => id === eventId));
- }
-}
-
-function append(items: readonly T[], item: T, limit: number): T[] {
- return [...items, structuredClone(item)].slice(-limit);
-}
-
-function appendFor(
- map: Map,
- agentId: AgentId,
- item: T,
- limit: number,
-): void {
- map.set(agentId, append(map.get(agentId) ?? [], item, limit));
-}
diff --git a/apps/game-api/src/reflex-execution.ts b/apps/game-api/src/reflex-execution.ts
index 1400870..1ad457e 100644
--- a/apps/game-api/src/reflex-execution.ts
+++ b/apps/game-api/src/reflex-execution.ts
@@ -76,7 +76,7 @@ function actionDescription(
if (action.type === 'infect')
return 'Infect the current open cell and establish local territory.';
if (action.type === 'capture')
- return 'Capture the abandoned infected current cell from a non-allied controller.';
+ return 'Capture the abandoned infected current cell from another controller.';
const status = cellStatus(state, action.targetCell, directive.agentId);
const terrain =
status === 'open'
diff --git a/apps/game-api/src/simulation-service.swarm.test.ts b/apps/game-api/src/simulation-service.swarm.test.ts
index d736072..3ff3b8d 100644
--- a/apps/game-api/src/simulation-service.swarm.test.ts
+++ b/apps/game-api/src/simulation-service.swarm.test.ts
@@ -8,7 +8,6 @@ import {
type SwarmPlanner,
} from '@hexzero/agent-runtime';
import {
- assignBehavior,
h3CellSchema,
type CompatibleModel,
type SwarmPlan,
@@ -401,14 +400,6 @@ describe('zero-swarm SimulationService tick', () => {
overrides: [],
locked: false,
},
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- roster.map(({ id }) => id),
- request.behaviorConfiguration.seed,
- 'balanced-random',
- ),
- },
});
await simulation.executeNextTick();
@@ -498,14 +489,6 @@ describe('zero-swarm SimulationService tick', () => {
overrides: [],
locked: false,
},
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- roster.map(({ id }) => id),
- request.behaviorConfiguration.seed,
- 'balanced-random',
- ),
- },
});
await expect(simulation.executeNextTick()).resolves.toBeNull();
@@ -524,7 +507,6 @@ describe('zero-swarm SimulationService tick', () => {
turns: { mode: 'entire-retained' },
outcomes: ['accepted', 'rejected', 'provider-error', 'operator-skipped'],
actions: ['move', 'infect', 'capture', 'wait'],
- communications: { channel: 'all', status: 'all' },
level: 'full-safe',
serialization: 'compact',
});
@@ -569,14 +551,6 @@ describe('zero-swarm SimulationService tick', () => {
overrides: [],
locked: false,
},
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- roster.map(({ id }) => id),
- request.behaviorConfiguration.seed,
- 'balanced-random',
- ),
- },
});
await simulation.executeNextTick();
@@ -623,14 +597,6 @@ describe('zero-swarm SimulationService tick', () => {
overrides: [],
locked: false,
},
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- roster.map(({ id }) => id),
- request.behaviorConfiguration.seed,
- 'balanced-random',
- ),
- },
});
await simulation.executeNextTick();
@@ -640,7 +606,6 @@ describe('zero-swarm SimulationService tick', () => {
turns: { mode: 'entire-retained' },
outcomes: ['accepted', 'rejected', 'provider-error', 'operator-skipped'],
actions: ['move', 'infect', 'capture', 'wait'],
- communications: { channel: 'all', status: 'all' },
level: 'full-safe',
serialization: 'compact',
});
@@ -1060,7 +1025,6 @@ describe('zero-swarm SimulationService tick', () => {
turns: { mode: 'entire-retained' },
outcomes: ['accepted', 'rejected', 'provider-error', 'operator-skipped'],
actions: ['move', 'infect', 'capture', 'wait'],
- communications: { channel: 'all', status: 'all' },
level: 'full-safe',
serialization: 'compact',
});
diff --git a/apps/game-api/src/simulation-service.ts b/apps/game-api/src/simulation-service.ts
index 9f36bb1..a8d7f1d 100644
--- a/apps/game-api/src/simulation-service.ts
+++ b/apps/game-api/src/simulation-service.ts
@@ -5,47 +5,27 @@ import {
type SwarmPlanner,
} from '@hexzero/agent-runtime';
import {
- agentIdSchema,
- archivedAppliedScenarioSchema,
- assignBehavior,
- behaviorConfigurationSchema,
experimentIdSchema,
experimentExportDocumentSchema,
experimentExportPreviewSchema,
experimentModelConfigurationSchema,
modelSupportsReasoningProfile,
- createMemoryId,
updateExperimentModelsRequestSchema,
- updateExperimentBehaviorRequestSchema,
h3CellSchema,
- RECENT_ALLIANCE_EVENT_LIMIT,
- RECENT_ZERO_STRATEGIC_EVENT_LIMIT,
- PERSONALITY_MAX_LENGTH,
OPENROUTER_PROVIDER_TIMEOUT_MS,
WORLD_SCENARIO_LIMITS,
- PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS,
PATIENT_ZERO_PLAYER_THREAT_FEED_LIMIT,
PATIENT_ZERO_PRESSURE_WINDOW_TICKS,
- MEMORY_ENTRY_LIMIT,
- personalitySchema,
- providerMetadataSchema,
swarmDirectiveSchema,
swarmPlanSchema,
swarmTickRecordSchema,
simulationSnapshotSchema,
type Agent,
type AgentId,
- type AgentGoalState,
- type GoalRevisionResult,
- type RequestedGoalRevision,
- type MemoryEntry,
- type MemoryOperationResult,
- type RequestedMemoryOperation,
type ExperimentExportDocument,
type ExperimentExportPreview,
type ExperimentId,
type ExperimentModelConfiguration,
- type BehaviorConfiguration,
type CompatibleModel,
type ModelId,
type ExperimentConfigurationEvent,
@@ -56,8 +36,6 @@ import {
type SimulationSnapshot,
type SimulationStatus,
type WorldEvent,
- type AllianceEvent,
- type AllianceProposalId,
type SimulatedPlayerEvent,
type SwarmPlan,
type CompletedSwarmDirective,
@@ -78,10 +56,6 @@ import {
createWorldFromScenario,
defaultWorldSetupRequest,
previewWorldSetup,
- DEVELOPMENT_AGENT_BLUEPRINTS,
- getAgentAlliance,
- getEffectiveAgentColor,
- physicalDistanceKm,
seededTickIntervalMinutes,
seededTickOrder,
advanceSimulatedPlayer,
@@ -90,10 +64,10 @@ import {
type WorldState,
} from '@hexzero/world-engine';
import {
+ calculateExperimentMetrics,
createExperimentExport,
createExperimentPreview,
type ExperimentSource,
- ExperimentMetricAccumulator,
} from './experiment-export';
import { geographicDirectionBetweenCells } from './geographic-direction';
import {
@@ -184,7 +158,6 @@ export function selectMostRecentPatientZeroThreats<
export function calculatePatientZeroPressureContext(
events: readonly WorldEvent[],
subjectAgentId: AgentId,
- currentAllianceMemberIds: readonly AgentId[] | null,
currentTick: number,
): PatientZeroPressureContext {
const startTick = Math.max(
@@ -234,9 +207,6 @@ export function calculatePatientZeroPressureContext(
tick -= 1
)
consecutiveAffectedTicks += 1;
- const memberIds = currentAllianceMemberIds
- ? new Set(currentAllianceMemberIds)
- : null;
return {
window: {
tickCount: currentTick - startTick + 1,
@@ -247,14 +217,6 @@ export function calculatePatientZeroPressureContext(
...countsFor(subjectEvents),
consecutiveAffectedTicks,
},
- currentAlliance: memberIds
- ? countsFor(
- relevant.filter((event) => {
- const subject = eventSubject(event);
- return subject !== null && memberIds.has(subject);
- }),
- )
- : null,
};
}
@@ -294,13 +256,11 @@ export class SimulationTurnCancelledError extends Error {
}
export type SimulationValidationCode =
- | 'invalid_agent_id'
| 'unknown_agent'
- | 'invalid_personality'
+ | 'invalid_request'
| 'invalid_model_configuration'
| 'models_unavailable'
- | 'experiment_budget_exhausted'
- | 'invalid_behavior_configuration';
+ | 'experiment_budget_exhausted';
export class SimulationValidationError extends Error {
constructor(
@@ -354,14 +314,10 @@ export class SimulationService {
#initialExperimentAgents: Agent[];
#initialExperimentWorld: SimulationSnapshot['world'];
#configurationEvents: ExperimentConfigurationEvent[] = [];
- #experimentMetrics: ExperimentMetricAccumulator;
#modelConfiguration: ExperimentModelConfiguration;
- #behaviorConfiguration: BehaviorConfiguration;
#scenario: AppliedScenario;
#availableModelIds = new Set();
#availableModels = new Map();
- #agentGoals = new Map();
- #agentMemories = new Map();
#simulatedPlayerEvents: SimulatedPlayerEvent[] = [];
#attemptAccounting: AttemptAccounting;
#swarmTicks: SwarmTickRecord[] = [];
@@ -406,9 +362,6 @@ export class SimulationService {
...this.#state.agents.values(),
]);
this.#initialExperimentWorld = this.#worldSnapshot();
- this.#experimentMetrics = new ExperimentMetricAccumulator([
- ...this.#state.agents.keys(),
- ]);
const scriptedModel =
swarmPlanner.mode === 'scripted-swarm-test'
? ('deterministic-script' as ModelId)
@@ -420,21 +373,9 @@ export class SimulationService {
locked: false,
});
if (scriptedModel) this.#availableModelIds.add(scriptedModel);
- this.#behaviorConfiguration = behaviorConfigurationSchema.parse({
- registryVersion: 1,
- assignmentMode: 'balanced-random',
- seed: this.#experimentId,
- assignments: assignBehavior(
- [...this.#state.agents.keys()],
- this.#experimentId,
- 'balanced-random',
- ),
- locked: false,
- });
this.#scenario = {
...createDefaultAppliedScenario(RESET_GENERATED_AT),
modelConfiguration: structuredClone(this.#modelConfiguration),
- behaviorConfiguration: structuredClone(this.#behaviorConfiguration),
};
this.#attemptAccounting = attemptAccountingForScenario(
this.#scenario.executionLimits,
@@ -470,26 +411,17 @@ export class SimulationService {
: {}),
},
modelConfiguration: this.#modelConfiguration,
- ...(agents.length > 0
- ? { behaviorConfiguration: this.#behaviorConfiguration }
- : {}),
resolvedModels: agents.map(({ id }) => this.#resolvedModel(id)),
- agentGoals: agents.map(({ id }) => ({
- agentId: id,
- goal: structuredClone(this.#agentGoals.get(id) ?? null),
- })),
- agentMemories: agents.map(({ id }) => ({
- agentId: id,
- entries: structuredClone(this.#agentMemories.get(id) ?? []),
- })),
swarmTicks: structuredClone(this.#swarmTicks),
experiment: {
id: this.#experimentId,
startedAt: this.#experimentStartedAt,
attemptAccounting: this.#attemptAccounting.snapshot(),
- metrics: this.#experimentMetrics.snapshot(agents.map(({ id }) => id)),
+ metrics: calculateExperimentMetrics(
+ [],
+ agents.map(({ id }) => id),
+ ),
currentTerritory: this.#territoryScoreboard(),
- currentAlliances: this.#allianceTerritorySummaries(),
simulatedPlayerMetrics: this.#state.simulatedPlayer?.metrics ?? {
movements: 0,
cellsDisinfected: 0,
@@ -517,8 +449,6 @@ export class SimulationService {
this.#activeAgentId = null;
this.#activeRequestController = null;
this.#cancellationRequested = false;
- this.#agentGoals = new Map();
- this.#agentMemories = new Map();
this.#swarmTicks = [];
this.#experimentSwarmTicks = [];
this.#lastValidSwarmPlan = null;
@@ -533,9 +463,6 @@ export class SimulationService {
...this.#state.agents.values(),
]);
this.#initialExperimentWorld = this.#worldSnapshot();
- this.#experimentMetrics = new ExperimentMetricAccumulator([
- ...this.#state.agents.keys(),
- ]);
this.#attemptAccounting = attemptAccountingForScenario(
this.#scenario.executionLimits,
this.#experimentRetentionLimit,
@@ -544,10 +471,6 @@ export class SimulationService {
...structuredClone(this.#scenario.modelConfiguration),
locked: false,
};
- this.#behaviorConfiguration = {
- ...structuredClone(this.#scenario.behaviorConfiguration),
- locked: false,
- };
this.#status =
this.#swarmPlanner.configured && this.#reflexProvider.configured
? 'paused'
@@ -568,9 +491,7 @@ export class SimulationService {
? 'invalid-radius'
: field === 'modelConfiguration'
? 'model-agent-mismatch'
- : field === 'behaviorConfiguration'
- ? 'behavior-coverage-mismatch'
- : 'invalid-roster';
+ : 'invalid-roster';
return {
feasible: false,
errors: [
@@ -626,7 +547,7 @@ export class SimulationService {
const parsed = worldSetupRequestSchema.safeParse(input);
if (!parsed.success)
throw new SimulationValidationError(
- 'invalid_behavior_configuration',
+ 'invalid_request',
'The scenario request is invalid.',
);
const checked = this.previewWorldSetup(parsed.data);
@@ -638,7 +559,7 @@ export class SimulationService {
const preview = previewWorldSetup(parsed.data, RESET_GENERATED_AT);
if (!preview.feasible)
throw new SimulationValidationError(
- 'invalid_behavior_configuration',
+ 'invalid_request',
preview.errors[0]?.message ?? 'The scenario is infeasible.',
);
const nextState = toWorldState(preview.world);
@@ -646,18 +567,12 @@ export class SimulationService {
...preview.scenario.modelConfiguration,
locked: false,
});
- const nextBehavior = behaviorConfigurationSchema.parse({
- ...preview.scenario.behaviorConfiguration,
- locked: false,
- });
this.#state = nextState;
this.#scenario = {
...preview.scenario,
modelConfiguration: nextModels,
- behaviorConfiguration: nextBehavior,
};
this.#modelConfiguration = nextModels;
- this.#behaviorConfiguration = nextBehavior;
this.#completedSwarmDecisionCount = 0;
this.#completedTickCount = 0;
this.#virtualTime = RESET_GENERATED_AT;
@@ -666,8 +581,6 @@ export class SimulationService {
this.#activeAgentId = null;
this.#activeRequestController = null;
this.#cancellationRequested = false;
- this.#agentGoals = new Map();
- this.#agentMemories = new Map();
this.#swarmTicks = [];
this.#experimentSwarmTicks = [];
this.#lastValidSwarmPlan = null;
@@ -682,9 +595,6 @@ export class SimulationService {
...this.#state.agents.values(),
]);
this.#initialExperimentWorld = this.#worldSnapshot();
- this.#experimentMetrics = new ExperimentMetricAccumulator([
- ...this.#state.agents.keys(),
- ]);
this.#attemptAccounting = attemptAccountingForScenario(
this.#scenario.executionLimits,
this.#experimentRetentionLimit,
@@ -763,286 +673,6 @@ export class SimulationService {
return this.getSnapshot();
}
- updateBehaviorConfiguration(input: unknown): SimulationSnapshot {
- if (this.#busy || this.#verificationBusy || this.#completedTickCount > 0)
- throw new SimulationConflictError(
- 'Behavior is locked after the experiment begins. Reset to create new assignments.',
- );
- const parsed = updateExperimentBehaviorRequestSchema.safeParse(input);
- if (!parsed.success)
- throw new SimulationValidationError(
- 'invalid_behavior_configuration',
- 'The behavior configuration is invalid.',
- );
- const agentIds = [...this.#state.agents.keys()];
- const assignments =
- parsed.data.assignmentMode === 'manual'
- ? parsed.data.assignments.map((assignment) => ({
- ...assignment,
- manual: true,
- }))
- : assignBehavior(
- agentIds,
- parsed.data.seed,
- parsed.data.assignmentMode,
- );
- if (
- assignments.length !== agentIds.length ||
- assignments.some(({ agentId }) => !this.#state.agents.has(agentId))
- )
- throw new SimulationValidationError(
- 'invalid_behavior_configuration',
- 'Behavior assignments must cover the current roster exactly.',
- );
- this.#behaviorConfiguration = behaviorConfigurationSchema.parse({
- registryVersion: 1,
- ...parsed.data,
- assignments,
- locked: false,
- });
- this.#scenario = {
- ...this.#scenario,
- behaviorConfiguration: structuredClone(this.#behaviorConfiguration),
- };
- return this.getSnapshot();
- }
-
- importModelConfiguration(document: unknown): {
- snapshot: SimulationSnapshot;
- legacy: boolean;
- message: string;
- } {
- if (this.#busy || this.#verificationBusy)
- throw new SimulationConflictError(
- 'Import is unavailable while a model request is active.',
- );
- if (
- typeof document !== 'object' ||
- document === null ||
- Array.isArray(document)
- )
- throw new SimulationValidationError(
- 'invalid_model_configuration',
- 'The experiment import is invalid.',
- );
- const root = document as Record;
- const version = root.schemaVersion;
- if (
- version !== 5 &&
- version !== 6 &&
- version !== 7 &&
- version !== 8 &&
- version !== 9 &&
- version !== 10 &&
- version !== 11
- )
- throw new SimulationValidationError(
- 'invalid_model_configuration',
- 'Only schema-version 5 through 11 experiment exports can be imported.',
- );
- if (version === 5) {
- const legacyConfiguration: ExperimentModelConfiguration = {
- globalModelId: null,
- globalReasoningProfile: 'provider-default',
- overrides: [],
- locked: false,
- };
- this.#recordModelConfigurationChanges(
- this.#modelConfiguration,
- legacyConfiguration,
- );
- this.#modelConfiguration = legacyConfiguration;
- return {
- snapshot: this.getSnapshot(),
- legacy: true,
- message:
- 'Legacy experiment preserved. Select compatible models before continuing.',
- };
- }
- const experiment =
- typeof root.experiment === 'object' && root.experiment !== null
- ? (root.experiment as Record)
- : undefined;
- const configuration = experimentModelConfigurationSchema.safeParse(
- experiment?.modelConfiguration,
- );
- if (!configuration.success)
- throw new SimulationValidationError(
- 'invalid_model_configuration',
- 'The imported model assignment is invalid.',
- );
- const importedPatientZero =
- (version === 9 || version === 10) &&
- typeof experiment?.scenario === 'object' &&
- experiment.scenario !== null
- ? (archivedAppliedScenarioSchema.safeParse(experiment.scenario).data
- ?.patientZeroAgentId ?? null)
- : null;
- const knownAgents = new Set(this.#state.agents.keys());
- if (importedPatientZero && !knownAgents.has(importedPatientZero))
- throw new SimulationValidationError(
- 'unknown_agent',
- 'The imported Patient Zero designation references an unknown agent.',
- );
- if (
- configuration.data.overrides.some(
- ({ agentId }) => !knownAgents.has(agentId),
- )
- )
- throw new SimulationValidationError(
- 'unknown_agent',
- 'The imported model assignment references an unknown agent.',
- );
- const importedConfiguration: ExperimentModelConfiguration = {
- globalModelId: configuration.data.globalModelId,
- globalReasoningProfile: configuration.data.globalReasoningProfile,
- overrides: structuredClone(configuration.data.overrides),
- locked: false,
- };
- if (
- (version === 8 || version === 9 || version === 10) &&
- experiment?.behaviorConfiguration !== undefined
- ) {
- const importedBehavior = behaviorConfigurationSchema.safeParse(
- experiment.behaviorConfiguration,
- );
- const knownBehaviorAgents = new Set(this.#state.agents.keys());
- if (
- !importedBehavior.success ||
- importedBehavior.data.assignments.some(
- ({ agentId }) => !knownBehaviorAgents.has(agentId),
- )
- )
- throw new SimulationValidationError(
- 'invalid_behavior_configuration',
- 'The imported behavior assignment contains an unknown or unsupported profile.',
- );
- this.#behaviorConfiguration = {
- ...structuredClone(importedBehavior.data),
- locked: this.#completedTickCount > 0,
- };
- }
- this.#recordModelConfigurationChanges(
- this.#modelConfiguration,
- importedConfiguration,
- );
- this.#modelConfiguration = importedConfiguration;
- this.#scenario = {
- ...this.#scenario,
- patientZeroAgentId:
- importedPatientZero ?? this.#scenario.patientZeroAgentId,
- };
- return {
- snapshot: this.getSnapshot(),
- legacy: false,
- message: this.getSnapshot().resolvedModels.every(
- ({ available }) => available,
- )
- ? 'Model assignments imported.'
- : 'Model assignments imported; unavailable models or reasoning profiles require explicit replacement.',
- };
- }
-
- updateAgentPersonality(
- agentIdInput: unknown,
- personalityInput: unknown,
- ): Agent {
- if (this.#busy || this.#verificationBusy) {
- throw new SimulationConflictError(
- 'Personality changes are unavailable while model execution is in progress.',
- );
- }
- const agentIdResult = agentIdSchema.safeParse(agentIdInput);
- if (!agentIdResult.success) {
- throw new SimulationValidationError(
- 'invalid_agent_id',
- 'The agent ID is invalid.',
- );
- }
- const personalityResult = personalitySchema.safeParse(personalityInput);
- if (!personalityResult.success) {
- throw new SimulationValidationError(
- 'invalid_personality',
- `Personality must contain 1 to ${PERSONALITY_MAX_LENGTH} characters.`,
- );
- }
- const agent = this.#state.agents.get(agentIdResult.data);
- if (!agent) {
- throw new SimulationValidationError(
- 'unknown_agent',
- 'The requested agent does not exist.',
- );
- }
- const updated = { ...agent, personality: personalityResult.data };
- const agents = new Map(this.#state.agents);
- agents.set(agent.id, updated);
- this.#state = { ...this.#state, agents };
- this.#scenario = {
- ...this.#scenario,
- roster: this.#scenario.roster.map((entry) =>
- entry.id === updated.id
- ? { ...entry, personality: updated.personality }
- : entry,
- ),
- };
- if (agent.personality !== updated.personality) {
- this.#configurationEvents = [
- ...this.#configurationEvents,
- {
- timestamp: this.#now(),
- agentId: agent.id,
- previousPersonality: agent.personality,
- newPersonality: updated.personality,
- operation: 'custom-edit',
- },
- ];
- }
- return updated;
- }
-
- restoreDefaultPersonalities(): SimulationSnapshot {
- if (this.#busy || this.#verificationBusy) {
- throw new SimulationConflictError(
- 'Personality changes are unavailable while model execution is in progress.',
- );
- }
- const defaults = new Map(
- DEVELOPMENT_AGENT_BLUEPRINTS.map(({ id, personality }) => [
- agentIdSchema.parse(id),
- personality,
- ]),
- );
- const configurationEvents: ExperimentConfigurationEvent[] = [];
- this.#state = {
- ...this.#state,
- agents: new Map(
- [...this.#state.agents].map(([id, agent]) => {
- const personality = defaults.get(id) ?? agent.personality;
- if (personality !== agent.personality)
- configurationEvents.push({
- timestamp: this.#now(),
- agentId: id,
- previousPersonality: agent.personality,
- newPersonality: personality,
- operation: 'restore-default',
- });
- return [id, { ...agent, personality }];
- }),
- ),
- };
- this.#configurationEvents = [
- ...this.#configurationEvents,
- ...configurationEvents,
- ];
- this.#scenario = {
- ...this.#scenario,
- roster: [...this.#state.agents.values()].map(
- ({ currentCell: _currentCell, ...agent }) => agent,
- ),
- };
- return this.getSnapshot();
- }
-
previewExperimentExport(request: unknown): ExperimentExportPreview {
if (this.#busy || this.#verificationBusy)
throw new SimulationConflictError(
@@ -1489,10 +1119,6 @@ export class SimulationService {
currentCell,
]),
);
- this.#behaviorConfiguration = {
- ...this.#behaviorConfiguration,
- locked: true,
- };
this.#status = this.#attemptAccounting.snapshot().exhausted
? 'budget-exhausted'
: 'paused';
@@ -1568,19 +1194,6 @@ export class SimulationService {
/** Remove cognition state that belongs to agents captured by the engine. */
#pruneCapturedRosterState(): void {
const active = new Set(this.#state.agents.keys());
- this.#agentGoals = new Map(
- [...this.#agentGoals].filter(([agentId]) => active.has(agentId)),
- );
- this.#agentMemories = new Map(
- [...this.#agentMemories].filter(([agentId]) => active.has(agentId)),
- );
- const assignments = this.#behaviorConfiguration.assignments.filter(
- ({ agentId }) => active.has(agentId),
- );
- this.#behaviorConfiguration = {
- ...this.#behaviorConfiguration,
- assignments,
- };
this.#modelConfiguration = {
...this.#modelConfiguration,
overrides: this.#modelConfiguration.overrides.filter(({ agentId }) =>
@@ -1650,10 +1263,6 @@ export class SimulationService {
hexes: [...this.#state.hexes].map(([cell, hex]) => ({ cell, ...hex })),
agents: structuredClone([...this.#state.agents.values()]),
events: structuredClone([...this.#state.events]),
- alliances: structuredClone([...(this.#state.alliances?.values() ?? [])]),
- pendingAllianceProposals: structuredClone([
- ...(this.#state.pendingAllianceProposals?.values() ?? []),
- ]),
simulatedPlayer: structuredClone(this.#state.simulatedPlayer ?? null),
};
}
@@ -2037,31 +1646,18 @@ export class SimulationService {
? 'openrouter'
: 'scripted-test',
retentionLimit: this.#experimentRetentionLimit,
- totalCompletedTurns: 0,
- turns: [],
+ totalCompletedTicks: this.#completedTickCount,
initialAgents: this.#initialExperimentAgents,
currentAgents: [...this.#state.agents.values()],
configurationEvents: this.#configurationEvents,
initialWorld: this.#initialExperimentWorld,
currentWorld: this.#worldSnapshot(),
modelConfiguration: this.#modelConfiguration,
- behaviorConfiguration:
- this.#state.agents.size > 0
- ? this.#behaviorConfiguration
- : this.#scenario.behaviorConfiguration,
scenario: this.#scenario,
schemaVersion: 11,
providerAttempts: this.#attemptAccounting.ledger(),
attemptRetention: this.#attemptAccounting.retention(),
attemptAccounting: this.#attemptAccounting.snapshot(),
- agentGoals: [...this.#state.agents.keys()].map((agentId) => ({
- agentId,
- goal: structuredClone(this.#agentGoals.get(agentId) ?? null),
- })),
- agentMemories: [...this.#state.agents.keys()].map((agentId) => ({
- agentId,
- entries: structuredClone(this.#agentMemories.get(agentId) ?? []),
- })),
simulatedPlayerEvents: structuredClone(this.#simulatedPlayerEvents),
swarmTicks: structuredClone(this.#experimentSwarmTicks),
};
@@ -2118,91 +1714,9 @@ export class SimulationService {
agentId: id,
name,
color,
- allianceId: getAgentAlliance(this.#state, id)?.id ?? null,
- effectiveColor: getEffectiveAgentColor(this.#state, id),
controlledCellCount: counts.get(id) ?? 0,
}));
}
-
- #allianceTerritorySummaries() {
- const scoreboard = this.#territoryScoreboard();
- return [...(this.#state.alliances?.values() ?? [])].map((alliance) => {
- const members = alliance.memberAgentIds.map((agentId) => {
- const entry = scoreboard.find(
- (candidate) => candidate.agentId === agentId,
- );
- if (!entry) throw new Error('An alliance member does not exist.');
- return {
- agentId,
- name: entry.name,
- controlledCellCount: entry.controlledCellCount,
- };
- });
- return {
- allianceId: alliance.id,
- color: alliance.color,
- totalControlledCellCount: members.reduce(
- (sum, member) => sum + member.controlledCellCount,
- 0,
- ),
- members,
- };
- });
- }
-}
-
-export function selectDiplomacyBlockerExamples<
- T extends { agentId: AgentId; reason: string },
->(
- state: WorldState,
- actingAgentId: AgentId,
- blockers: readonly T[],
- reasonPriority: readonly T['reason'][],
-): T[] {
- const actingAlliance = getAgentAlliance(state, actingAgentId);
- const relationshipPriority = (blockedAgentId: AgentId) => {
- const blockedAlliance = getAgentAlliance(state, blockedAgentId);
- if (!actingAlliance) return blockedAlliance ? 1 : 0;
- if (!blockedAlliance) return 0;
- return blockedAlliance.id === actingAlliance.id ? 2 : 1;
- };
- return blockers
- .toSorted(
- (left, right) =>
- relationshipPriority(left.agentId) -
- relationshipPriority(right.agentId) ||
- reasonPriority.indexOf(left.reason) -
- reasonPriority.indexOf(right.reason) ||
- left.agentId.localeCompare(right.agentId),
- )
- .slice(0, 4);
-}
-
-function isAllianceEvent(event: WorldEvent): event is AllianceEvent {
- return (
- event.type === 'alliance-proposed' ||
- event.type === 'alliance-proposal-closed' ||
- event.type === 'alliance-formed' ||
- event.type === 'alliance-dissolved' ||
- event.type === 'agent-joined-alliance' ||
- event.type === 'agent-left-alliance'
- );
-}
-
-function allianceEventsSince(
- before: WorldState,
- after: WorldState,
-): AllianceEvent[] {
- return after.events.slice(before.events.length).filter(isAllianceEvent);
-}
-
-function stableOrder(input: string): number {
- let hash = 2166136261;
- for (let index = 0; index < input.length; index += 1) {
- hash ^= input.charCodeAt(index);
- hash = Math.imul(hash, 16777619);
- }
- return hash >>> 0;
}
function gridRingDistance(from: H3Cell, to: H3Cell): number {
@@ -2212,236 +1726,3 @@ function gridRingDistance(from: H3Cell, to: H3Cell): number {
return 999;
}
}
-
-function summarizeEvent(
- event: Extract<
- WorldEvent,
- {
- type: 'agent-moved' | 'hex-infected' | 'hex-captured' | 'agent-waited';
- }
- >,
- state: WorldState,
-): string {
- const name = state.agents.get(event.agentId)?.name ?? 'An agent';
- if (event.type === 'agent-moved') return `${name} moved to ${event.toCell}.`;
- if (event.type === 'hex-infected') return `${name} infected ${event.cell}.`;
- if (event.type === 'hex-captured') {
- const previous =
- (event.previousControllerAgentId === null
- ? undefined
- : state.agents.get(event.previousControllerAgentId)?.name) ??
- 'another agent';
- return `${name} captured ${event.cell} from ${previous}.`;
- }
- return `${name} waited.`;
-}
-
-function summarizeAllianceEvent(
- event: AllianceEvent,
- state: WorldState,
-): string {
- const name = (id: AgentId) => state.agents.get(id)?.name ?? 'An agent';
- if (event.type === 'alliance-proposed')
- return `${name(event.agentId)} proposed an alliance with ${name(event.recipientAgentId)}.`;
- if (event.type === 'alliance-formed')
- return `${event.memberAgentIds.map(name).join(' and ')} formed an alliance.`;
- if (event.type === 'agent-joined-alliance')
- return `${name(event.joinedAgentId)} joined the alliance.`;
- if (event.type === 'agent-left-alliance')
- return `${name(event.leftAgentId)} left the alliance.`;
- if (event.type === 'alliance-dissolved') return 'The alliance dissolved.';
- return `The proposal from ${name(event.proposerAgentId)} to ${name(event.recipientAgentId)} was ${event.reason}.`;
-}
-
-export function applyGoalRevision(
- current: AgentGoalState | undefined,
- requested: RequestedGoalRevision | undefined,
- tick: number,
-): { goal: AgentGoalState | undefined; result: GoalRevisionResult } {
- if (!requested) return { goal: current, result: { requested: false } };
- if (requested.operation === 'establish') {
- if (current)
- return {
- goal: current,
- result: {
- requested: true,
- accepted: false,
- operation: requested.operation,
- reason: 'goal-already-active',
- },
- };
- return {
- goal: {
- longTermGoal: requested.longTermGoal,
- shortTermGoal: requested.shortTermGoal,
- planSummary: requested.planSummary,
- establishedAtTick: tick,
- revisedAtTick: tick,
- },
- result: {
- requested: true,
- accepted: true,
- operation: requested.operation,
- },
- };
- }
- if (!current)
- return {
- goal: undefined,
- result: {
- requested: true,
- accepted: false,
- operation: requested.operation,
- reason: 'goal-not-active',
- },
- };
- if (requested.operation === 'keep')
- return {
- goal: current,
- result: {
- requested: true,
- accepted: true,
- operation: requested.operation,
- },
- };
- if (requested.operation === 'revise')
- return {
- goal: {
- longTermGoal: requested.longTermGoal,
- shortTermGoal: requested.shortTermGoal,
- planSummary: requested.planSummary,
- establishedAtTick: current.establishedAtTick,
- revisedAtTick: tick,
- },
- result: {
- requested: true,
- accepted: true,
- operation: requested.operation,
- },
- };
- return {
- goal: undefined,
- result: { requested: true, accepted: true, operation: requested.operation },
- };
-}
-
-export function applyMemoryOperation(
- current: readonly MemoryEntry[],
- requested: RequestedMemoryOperation | undefined,
- agentId: AgentId,
- tick: number,
-): { entries: MemoryEntry[]; result: MemoryOperationResult } {
- const entries = current.map((entry) => structuredClone(entry));
- if (!requested) return { entries, result: { requested: false } };
- if (requested.operation === 'keep')
- return {
- entries,
- result: { requested: true, accepted: true, operation: 'keep' },
- };
- if (requested.operation === 'remember') {
- if (entries.length >= MEMORY_ENTRY_LIMIT)
- return {
- entries,
- result: {
- requested: true,
- accepted: false,
- operation: 'remember',
- reason: 'memory-full',
- },
- };
- const id = createMemoryId(agentId, tick);
- return {
- entries: [
- ...entries,
- {
- id,
- text: requested.text,
- createdAtTick: tick,
- revisedAtTick: tick,
- },
- ],
- result: {
- requested: true,
- accepted: true,
- operation: 'remember',
- memoryId: id,
- },
- };
- }
- const index = entries.findIndex(({ id }) => id === requested.memoryId);
- if (index < 0)
- return {
- entries,
- result: {
- requested: true,
- accepted: false,
- operation: requested.operation,
- reason: 'memory-not-found',
- },
- };
- if (requested.operation === 'forget') {
- entries.splice(index, 1);
- return {
- entries,
- result: {
- requested: true,
- accepted: true,
- operation: 'forget',
- memoryId: requested.memoryId,
- },
- };
- }
- entries[index] = {
- ...entries[index]!,
- text: requested.text,
- revisedAtTick: tick,
- };
- return {
- entries,
- result: {
- requested: true,
- accepted: true,
- operation: 'revise',
- memoryId: requested.memoryId,
- },
- };
-}
-
-function safeRecoveryProviderMetadata(
- value: unknown,
- provider: ProviderMetadata['provider'],
- selectedModel: ModelId,
-): ProviderMetadata {
- const raw = value && typeof value === 'object' ? value : {};
- let safe = providerMetadataSchema.parse({
- provider,
- model: selectedModel,
- latencyMs: 0,
- });
- const fields = [
- 'model',
- 'selectedModel',
- 'resolvedModel',
- 'requestId',
- 'httpStatus',
- 'finishReason',
- 'nativeFinishReason',
- 'latencyMs',
- 'promptTokens',
- 'completionTokens',
- 'totalTokens',
- 'reasoningTokens',
- 'cachedReadTokens',
- 'cacheWriteTokens',
- 'costCredits',
- ] as const;
- for (const field of fields) {
- if (!(field in raw)) continue;
- const parsed = providerMetadataSchema.safeParse({
- ...safe,
- [field]: (raw as Record)[field],
- });
- if (parsed.success) safe = parsed.data;
- }
- return safe;
-}
diff --git a/apps/game-api/src/swarm-comparison.ts b/apps/game-api/src/swarm-comparison.ts
index e9d84f7..1fc5f26 100644
--- a/apps/game-api/src/swarm-comparison.ts
+++ b/apps/game-api/src/swarm-comparison.ts
@@ -6,7 +6,6 @@ import {
type SwarmPlanner,
} from '@hexzero/agent-runtime';
import {
- assignBehavior,
reflexDecisionSchema,
type CompatibleModel,
type ProviderMetadata,
@@ -432,16 +431,6 @@ function createService(variant: OfflineComparisonVariant, seed: string) {
overrides: [],
locked: false,
},
- // This transitional scenario field remains required until PR 2 removes
- // legacy behavior configuration from the shared world setup schema.
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- roster.map(({ id }) => id),
- `offline-behavior-${seed}`,
- 'balanced-random',
- ),
- },
});
return service;
}
diff --git a/apps/world-lab/src/app/styles.css b/apps/world-lab/src/app/styles.css
index e6a792f..f4b4f72 100644
--- a/apps/world-lab/src/app/styles.css
+++ b/apps/world-lab/src/app/styles.css
@@ -1403,89 +1403,11 @@ dd {
border-color: #79bda6;
color: #d7e7e1;
}
-.behavior-trace-panel,
-.agent-inspector [id$='-goals'],
-.agent-inspector [id$='-memories'],
.agent-inspector [id$='-history'],
.agent-inspector [id$='-configuration'],
.agent-inspector [id$='-latest'] {
scroll-margin-top: 46px;
}
-.behavior-trace-panel {
- margin-top: 14px;
- padding-top: 2px;
-}
-.behavior-trace-heading {
- display: flex;
- align-items: flex-start;
- justify-content: space-between;
- gap: 10px;
-}
-.behavior-trace-heading h3 {
- margin-top: 0;
-}
-.behavior-trace-heading p {
- margin: 2px 0 0;
- color: #82938e;
- font-size: 0.69rem;
- line-height: 1.4;
-}
-.behavior-trace-heading > span {
- flex: 0 0 auto;
- color: #7f928b;
- font-size: 0.66rem;
-}
-.behavior-trace {
- display: grid;
- gap: 6px;
- margin: 8px 0 0;
- padding: 0;
- list-style: none;
-}
-.behavior-trace > li {
- min-width: 0;
- border: 1px solid #34433f;
- border-radius: 5px;
- background: #121917;
-}
-.behavior-trace summary {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 8px;
- padding: 7px 8px;
- color: #c9d6d1;
- cursor: pointer;
- font-size: 0.71rem;
- font-weight: 650;
-}
-.behavior-trace-body {
- display: grid;
- gap: 7px;
- padding: 0 8px 8px;
- border-top: 1px solid #2b3834;
-}
-.behavior-trace-block,
-.behavior-trace-decision {
- min-width: 0;
- padding-top: 7px;
- color: #aebdb8;
- font-size: 0.71rem;
- line-height: 1.45;
-}
-.behavior-trace-block > strong,
-.behavior-trace-decision strong {
- color: #d5e0dc;
-}
-.behavior-trace-block ul {
- margin: 4px 0 0;
- padding-left: 17px;
-}
-.behavior-trace-block p,
-.behavior-trace-decision p {
- margin: 4px 0 0;
- overflow-wrap: anywhere;
-}
.trace-cell-button {
padding: 1px 4px;
border: 1px solid #45645a;
@@ -1500,17 +1422,6 @@ dd {
border-color: #80c9b0;
color: #d9f2e9;
}
-.personality-heading {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-top: 17px;
-}
-.personality-heading h3 {
- margin: 0;
-}
-.personality-heading button,
.editor-actions button {
padding: 5px 9px;
border: 1px solid #465752;
@@ -1519,52 +1430,10 @@ dd {
background: #202927;
cursor: pointer;
}
-.personality-heading button:disabled,
.editor-actions button:disabled {
color: #71807b;
cursor: not-allowed;
}
-.active-personality {
- margin-top: 8px;
-}
-.active-personality p {
- color: #d1dcd8;
- font-size: 0.8rem;
- line-height: 1.55;
-}
-.active-personality span {
- display: inline-block;
- margin-top: 7px;
- padding: 2px 7px;
- border: 1px solid #3c4b47;
- border-radius: 999px;
- color: #91a59f;
- font-size: 0.68rem;
-}
-.personality-editor {
- display: grid;
- gap: 10px;
- margin-top: 10px;
-}
-.personality-editor label {
- display: grid;
- gap: 5px;
- color: #aab9b4;
- font-size: 0.74rem;
-}
-.personality-editor select,
-.personality-editor textarea {
- width: 100%;
- padding: 8px;
- border: 1px solid #43524e;
- border-radius: 5px;
- background: #111816;
- color: #e0e9e6;
-}
-.personality-editor textarea {
- resize: vertical;
- line-height: 1.45;
-}
.editor-meta {
display: flex;
justify-content: space-between;
@@ -1638,24 +1507,6 @@ dd {
margin: 0;
padding-left: 18px;
}
-.communication-history,
-.observation-communications {
- margin: 0;
- padding-left: 18px;
- color: #bcc9c5;
- font-size: 0.77rem;
- line-height: 1.5;
-}
-.communication-history li {
- margin-bottom: 9px;
- overflow-wrap: anywhere;
-}
-.communication-history p {
- margin-top: 3px;
- color: #d8e2df;
- white-space: pre-wrap;
-}
-.communication-history small,
.world-chat-feed small {
color: #82938e;
}
@@ -1694,10 +1545,6 @@ dd {
border-left: 2px solid #4e766a;
padding-left: 8px;
}
-.observation-communications li {
- margin-top: 4px;
- overflow-wrap: anywhere;
-}
.event-panel ol {
list-style: none;
padding: 0;
@@ -2247,10 +2094,6 @@ dt,
border-color: color-mix(in srgb, var(--vanilla-custard) 80%, white);
box-shadow: 0 0 0 1px #0009;
}
-.communication-history li {
- border-left: 3px solid transparent;
- padding-left: 8px;
-}
.stale-export {
border: 1px solid color-mix(in srgb, var(--warning) 60%, var(--line));
background: color-mix(in srgb, var(--warning) 12%, var(--surface));
diff --git a/apps/world-lab/src/components/behavior-trace.test.ts b/apps/world-lab/src/components/behavior-trace.test.ts
deleted file mode 100644
index 2c487d1..0000000
--- a/apps/world-lab/src/components/behavior-trace.test.ts
+++ /dev/null
@@ -1,377 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import {
- agentIdSchema,
- agentTurnRecordSchema,
- type AgentTurnRecord,
-} from '@hexzero/shared';
-import { BEHAVIOR_TRACE_LIMIT, deriveBehaviorTrace } from './behavior-trace';
-
-const agentId = agentIdSchema.parse('128f3f38-6b7d-4db7-9e95-751b4ce2681e');
-const otherAgentId = agentIdSchema.parse(
- '2507bb46-7ae4-45ca-8dda-644c4f85ca14',
-);
-const currentCell = '892b6b5a2c7ffff';
-const adjacentCell = '892b6b5a2d3ffff';
-
-function acceptedTurn(
- turnNumber: number,
- options: {
- move?: boolean;
- inboundMessage?: boolean;
- territoryChange?: boolean;
- continuity?: boolean;
- playerThreats?: boolean;
- } = {},
-): AgentTurnRecord {
- const move = options.move ?? false;
- const occurredAt = `2026-08-23T12:00:${String(turnNumber).padStart(2, '0')}.000Z`;
- return agentTurnRecordSchema.parse({
- turnNumber,
- agentId,
- startedAt: occurredAt,
- completedAt: occurredAt,
- observation: {
- agentId,
- agentName: 'Ember',
- personality: 'A deliberate test agent.',
- currentCell: {
- cell: currentCell,
- state: 'open',
- controllerAgentId: null,
- controllerAllianceId: null,
- effectiveColor: null,
- },
- captureEligibility: {
- eligible: false,
- blockedReason: 'capture-open-cell',
- },
- actionAvailability: {
- moveTargetCellIds: [adjacentCell],
- moveOptions: [
- {
- targetCell: adjacentCell,
- direction: 'NE',
- destinationState: 'open',
- controllerRelationship: 'open',
- recentlyOccupied: false,
- nearbyAgentCount: 1,
- },
- ],
- infect: { available: true },
- capture: { available: false, reason: 'capture-open-cell' },
- wait: { available: true },
- },
- adjacentCells: [
- {
- cell: adjacentCell,
- state: 'open',
- controllerAgentId: null,
- controllerAllianceId: null,
- effectiveColor: null,
- },
- ],
- nearbyAgents: [],
- recentEvents: [],
- recentPublicMessages: [],
- recentDirectMessages: options.inboundMessage
- ? [
- {
- eventId: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
- senderId: otherAgentId,
- senderName: 'Rook',
- recipientId: agentId,
- recipientName: 'Ember',
- direction: 'inbound',
- message: 'Hold the eastern route.',
- occurredAt,
- distance: 2,
- },
- ]
- : [],
- territoryScoreboard: [
- {
- agentId,
- name: 'Ember',
- color: '#d55e00',
- allianceId: null,
- effectiveColor: '#d55e00',
- controlledCellCount: 0,
- },
- ],
- actingAllianceId: null,
- actingAlliance: null,
- activeAlliances: [],
- inboundAllianceProposals: [],
- outboundAllianceProposals: [],
- recentAllianceEvents: [],
- recentControlChanges: options.territoryChange
- ? [
- {
- eventId: '87aa21b9-fc78-4b04-9f92-9862bf346f96',
- direction: 'lost',
- otherAgentId,
- otherAgentName: 'Rook',
- cell: adjacentCell,
- occurredAt,
- },
- ]
- : [],
- ...(options.playerThreats
- ? {
- patientZero: {
- agentId,
- agentName: 'Ember',
- isPatientZero: true,
- directRangeBypass: true,
- },
- patientZeroGlobalView: {
- agents: [],
- individualTerritory: [
- {
- agentId,
- name: 'Ember',
- color: '#d55e00',
- allianceId: null,
- effectiveColor: '#d55e00',
- controlledCellCount: 0,
- },
- ],
- allianceTerritory: [],
- alliances: [],
- activeAllianceProposals: [],
- recentStrategicEvents: [],
- recentTerritoryChanges: [],
- playerThreatFeed: {
- events: [
- {
- eventId: '97aa21b9-fc78-4b04-9f92-9862bf346f96',
- kind: 'territory-disinfected',
- cell: adjacentCell,
- occurredAt,
- affectedAgentId: agentId,
- affectedAgentName: 'Ember',
- affectedAllianceId: null,
- affectedAllianceColor: null,
- pressureContext: {
- window: { tickCount: 6, startTick: 3, endTick: 8 },
- subject: {
- totalEvents: 3,
- disinfections: 2,
- blockedCleans: 1,
- consecutiveAffectedTicks: 2,
- },
- currentAlliance: null,
- },
- },
- {
- eventId: 'a7aa21b9-fc78-4b04-9f92-9862bf346f96',
- kind: 'occupied-clean-blocked',
- cell: currentCell,
- occurredAt,
- blockingAgentId: otherAgentId,
- blockingAgentName: 'Rook',
- blockingAllianceId: null,
- blockingAllianceColor: null,
- pressureContext: {
- window: { tickCount: 6, startTick: 3, endTick: 8 },
- subject: {
- totalEvents: 2,
- disinfections: 0,
- blockedCleans: 2,
- consecutiveAffectedTicks: 2,
- },
- currentAlliance: null,
- },
- },
- ],
- totalEventCount: 3,
- truncated: true,
- },
- },
- playerPressure: {
- enabled: true,
- recentThreats: [
- {
- eventId: '97aa21b9-fc78-4b04-9f92-9862bf346f96',
- kind: 'territory-disinfected',
- cell: adjacentCell,
- occurredAt,
- distanceCells: 0,
- affectedOwnTerritory: true,
- },
- ],
- },
- }
- : {}),
- },
- outcome: 'accepted',
- worldAction: move
- ? { type: 'move', targetCell: adjacentCell }
- : { type: 'wait' },
- summary: move ? 'Respond to the eastern-route warning.' : 'Hold position.',
- worldActionResult: {
- accepted: true,
- event: move
- ? {
- id: `77bb21b9-fc78-4b04-9f92-9862bf346f9${turnNumber}`,
- type: 'agent-moved',
- agentId,
- fromCell: currentCell,
- toCell: adjacentCell,
- occurredAt,
- }
- : {
- id: `77bb21b9-fc78-4b04-9f92-9862bf346f9${turnNumber}`,
- type: 'agent-waited',
- agentId,
- occurredAt,
- },
- },
- communicationResult: { requested: false },
- diplomacyResult: { requested: false },
- ...(options.continuity
- ? {
- goalRevision: {
- operation: 'establish',
- longTermGoal: 'Hold the eastern corridor.',
- shortTermGoal: 'Wait for a safe route.',
- planSummary: 'Observe before moving.',
- reason: 'Set a retained objective.',
- },
- goalRevisionResult: {
- requested: true,
- accepted: true,
- operation: 'establish',
- },
- memoryOperation: {
- operation: 'remember',
- text: 'Rook contested the eastern route.',
- },
- memoryOperationResult: {
- requested: true,
- accepted: false,
- operation: 'remember',
- reason: 'memory-full',
- },
- }
- : {}),
- provider: {
- provider: 'scripted-test',
- model: 'test',
- latencyMs: 0,
- },
- });
-}
-
-describe('deriveBehaviorTrace', () => {
- it('places new evidence beside legal choices, chosen direction, and action change', () => {
- const trace = deriveBehaviorTrace(
- [
- acceptedTurn(1),
- acceptedTurn(2, {
- move: true,
- inboundMessage: true,
- territoryChange: true,
- }),
- ],
- agentId,
- );
-
- expect(trace).toHaveLength(2);
- expect(trace[0]).toMatchObject({
- hasPreviousObservation: true,
- legalActions: ['Move NE', 'Infect', 'Wait'],
- chosenAction: `Move NE → ${adjacentCell}`,
- chosenCell: adjacentCell,
- actionPattern: 'Changed wait → move NE.',
- evidence: [
- {
- kind: 'direct',
- label: 'Inbound from Rook: Hold the eastern route.',
- },
- {
- kind: 'territory',
- label: `Lost ${adjacentCell} to Rook`,
- cell: adjacentCell,
- },
- ],
- });
- expect(trace[0]!.observedChanges).toContain(
- '2 new retained evidence items entered the retained observation.',
- );
- expect(trace[1]!.observedChanges).toEqual([
- 'First retained observation for this agent.',
- ]);
- });
-
- it('stays bounded and newest-first', () => {
- const turns = Array.from({ length: 8 }, (_, index) =>
- acceptedTurn(index + 1),
- );
- const trace = deriveBehaviorTrace(turns, agentId);
- expect(trace).toHaveLength(BEHAVIOR_TRACE_LIMIT);
- expect(trace.map(({ turn }) => turn.turnNumber)).toEqual([
- 8, 7, 6, 5, 4, 3,
- ]);
- expect(deriveBehaviorTrace(turns, agentId, 99)).toHaveLength(
- BEHAVIOR_TRACE_LIMIT,
- );
- });
-
- it('reports repeated actions and independent goal and memory continuity', () => {
- const trace = deriveBehaviorTrace(
- [acceptedTurn(1), acceptedTurn(2, { continuity: true })],
- agentId,
- );
- expect(trace[0]).toMatchObject({
- actionPattern: 'Repeated wait.',
- continuity: [
- 'Goal establish: accepted',
- 'Memory remember: rejected (memory-full)',
- ],
- });
- });
-
- it('shows local and Patient Zero cleaner evidence without duplicating one event', () => {
- const trace = deriveBehaviorTrace(
- [acceptedTurn(1), acceptedTurn(2, { playerThreats: true })],
- agentId,
- );
- const evidence = trace[0]!.evidence;
- expect(evidence.slice(0, 2)).toEqual([
- expect.objectContaining({
- kind: 'patient-zero-threat',
- label: 'Patient Zero global cleaner feed: 2/3 displayed · truncated',
- }),
- expect.objectContaining({
- kind: 'player-threat',
- label: expect.stringContaining('own territory disinfected'),
- }),
- ]);
- expect(evidence).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- kind: 'player-threat',
- label: expect.stringContaining('own territory disinfected'),
- cell: adjacentCell,
- }),
- expect.objectContaining({
- kind: 'patient-zero-threat',
- label: 'Patient Zero global cleaner feed: 2/3 displayed · truncated',
- }),
- expect.objectContaining({
- kind: 'patient-zero-threat',
- label: expect.stringContaining('Rook blocked a clean'),
- cell: currentCell,
- }),
- ]),
- );
- expect(
- evidence.filter(({ label }) => label.includes('Ember lost')),
- ).toHaveLength(0);
- expect(evidence[1]!.label).toContain(
- 'subject 3 total (2 disinfected, 1 blocked), 2 consecutive',
- );
- expect(evidence[1]!.label).toContain('current alliance unaffiliated');
- });
-});
diff --git a/apps/world-lab/src/components/behavior-trace.ts b/apps/world-lab/src/components/behavior-trace.ts
deleted file mode 100644
index 35a9569..0000000
--- a/apps/world-lab/src/components/behavior-trace.ts
+++ /dev/null
@@ -1,442 +0,0 @@
-import type { AgentId, AgentTurnRecord, H3Cell } from '@hexzero/shared';
-
-export const BEHAVIOR_TRACE_LIMIT = 6;
-const BEHAVIOR_TRACE_EVIDENCE_LIMIT = 6;
-
-type CompletedTurn = Extract<
- AgentTurnRecord,
- { outcome: 'accepted' | 'rejected' }
->;
-
-export interface BehaviorTraceEvidence {
- kind:
- | 'direct'
- | 'public'
- | 'alliance-message'
- | 'zero-message'
- | 'world-event'
- | 'territory'
- | 'alliance-event'
- | 'player-threat'
- | 'patient-zero-threat';
- label: string;
- cell?: H3Cell;
-}
-
-export interface BehaviorTraceEntry {
- turn: AgentTurnRecord;
- hasPreviousObservation: boolean;
- observedChanges: string[];
- evidence: BehaviorTraceEvidence[];
- evidenceTruncated: boolean;
- legalActions: string[];
- chosenAction: string;
- chosenCell?: H3Cell;
- actionPattern?: string;
- continuity: string[];
-}
-
-function isCompletedTurn(turn: AgentTurnRecord): turn is CompletedTurn {
- return turn.outcome === 'accepted' || turn.outcome === 'rejected';
-}
-
-function unseenBy(
- current: readonly T[],
- previous: readonly T[] | undefined,
- idFor: (item: T) => string,
-): T[] {
- const priorIds = new Set(previous?.map(idFor) ?? []);
- return current.filter((item) => !priorIds.has(idFor(item)));
-}
-
-function goalSignature(
- goal: AgentTurnRecord['observation']['currentGoal'],
-): string {
- return goal
- ? [
- goal.longTermGoal,
- goal.shortTermGoal,
- goal.planSummary,
- goal.establishedAtTick,
- goal.revisedAtTick,
- ].join('\u0000')
- : '';
-}
-
-function memorySignature(
- memory: AgentTurnRecord['observation']['currentMemory'],
-): string {
- return memory
- .map(
- ({ id, text, createdAtTick, revisedAtTick }) =>
- `${id}\u0000${text}\u0000${createdAtTick}\u0000${revisedAtTick}`,
- )
- .join('\u0001');
-}
-
-function ownTerritoryCount(
- turn: AgentTurnRecord,
- agentId: AgentId,
-): number | undefined {
- return turn.observation.territoryScoreboard.find(
- (entry) => entry.agentId === agentId,
- )?.controlledCellCount;
-}
-
-function collectEvidence(
- turn: AgentTurnRecord,
- previous: AgentTurnRecord | undefined,
- agentId: AgentId,
-): BehaviorTraceEvidence[] {
- const observation = turn.observation;
- const prior = previous?.observation;
- const inboundDirect = unseenBy(
- observation.recentDirectMessages.filter(
- ({ direction }) => direction === 'inbound',
- ),
- prior?.recentDirectMessages.filter(
- ({ direction }) => direction === 'inbound',
- ),
- ({ eventId }) => eventId,
- ).map(({ senderName, message }): BehaviorTraceEvidence => ({
- kind: 'direct',
- label: `Inbound from ${senderName}: ${message}`,
- }));
- const publicMessages = unseenBy(
- observation.recentPublicMessages.filter(
- ({ senderId }) => senderId !== agentId,
- ),
- prior?.recentPublicMessages.filter(({ senderId }) => senderId !== agentId),
- ({ eventId }) => eventId,
- ).map(({ senderName, message }): BehaviorTraceEvidence => ({
- kind: 'public',
- label: `Public from ${senderName}: ${message}`,
- }));
- const allianceMessages = unseenBy(
- observation.recentAllianceMessages.filter(
- ({ senderId }) => senderId !== agentId,
- ),
- prior?.recentAllianceMessages.filter(
- ({ senderId }) => senderId !== agentId,
- ),
- ({ eventId }) => eventId,
- ).map(({ senderName, message }): BehaviorTraceEvidence => ({
- kind: 'alliance-message',
- label: `Alliance from ${senderName}: ${message}`,
- }));
- const zeroMessages = unseenBy(
- observation.recentZeroMessages.filter(
- ({ senderId }) => senderId !== agentId,
- ),
- prior?.recentZeroMessages.filter(({ senderId }) => senderId !== agentId),
- ({ eventId }) => eventId,
- ).map(({ senderName, message }): BehaviorTraceEvidence => ({
- kind: 'zero-message',
- label: `Patient Zero from ${senderName}: ${message}`,
- }));
- const territory = unseenBy(
- observation.recentControlChanges,
- prior?.recentControlChanges,
- ({ eventId }) => eventId,
- ).map(({ direction, cell, otherAgentName }): BehaviorTraceEvidence => ({
- kind: 'territory',
- label: `${direction === 'gained' ? 'Gained' : 'Lost'} ${cell} ${
- direction === 'gained' ? 'from' : 'to'
- } ${otherAgentName}`,
- cell,
- }));
- const allianceEvents = unseenBy(
- observation.recentAllianceEvents,
- prior?.recentAllianceEvents,
- ({ event }) => event.id,
- ).map(({ summary }): BehaviorTraceEvidence => ({
- kind: 'alliance-event',
- label: summary,
- }));
- const worldEvents = unseenBy(
- observation.recentEvents.filter(
- ({ agentId: actorId }) => actorId !== agentId,
- ),
- prior?.recentEvents.filter(({ agentId: actorId }) => actorId !== agentId),
- ({ type, agentId: actorId, occurredAt, summary }) =>
- `${type}:${actorId}:${occurredAt}:${summary}`,
- ).map(({ summary }): BehaviorTraceEvidence => ({
- kind: 'world-event',
- label: `World: ${summary}`,
- }));
- const localPlayerThreats = unseenBy(
- observation.playerPressure.recentThreats,
- prior?.playerPressure.recentThreats,
- ({ eventId }) => eventId,
- ).map(
- ({
- eventId,
- kind,
- cell,
- distanceCells,
- }): BehaviorTraceEvidence & {
- eventId: string;
- } => ({
- eventId,
- kind: 'player-threat',
- label:
- kind === 'territory-disinfected'
- ? `Local cleaner threat: own territory disinfected at ${cell}`
- : `Local cleaner threat: disinfection at ${cell} (${distanceCells} cells away)`,
- cell,
- }),
- );
- const localPlayerEventIds = new Set(
- localPlayerThreats.map(({ eventId }) => eventId),
- );
- const globalFeed = observation.patientZeroGlobalView?.playerThreatFeed;
- const priorGlobalFeed = prior?.patientZeroGlobalView?.playerThreatFeed;
- const pressureSuffix = (
- event: NonNullable['events'][number] | undefined,
- ): string => {
- const pressure = event?.pressureContext;
- if (!pressure) return '';
- const alliance = pressure.currentAlliance
- ? `; current alliance ${pressure.currentAlliance.totalEvents} total (${pressure.currentAlliance.disinfections} disinfected, ${pressure.currentAlliance.blockedCleans} blocked)`
- : '; current alliance unaffiliated';
- return ` · ticks ${pressure.window.startTick}–${pressure.window.endTick}: subject ${pressure.subject.totalEvents} total (${pressure.subject.disinfections} disinfected, ${pressure.subject.blockedCleans} blocked), ${pressure.subject.consecutiveAffectedTicks} consecutive${alliance}`;
- };
- const globalPlayerThreats = globalFeed
- ? unseenBy(
- globalFeed.events,
- priorGlobalFeed?.events,
- ({ eventId }) => eventId,
- )
- .filter(({ eventId }) => !localPlayerEventIds.has(eventId))
- .map((event): BehaviorTraceEvidence => ({
- kind: 'patient-zero-threat',
- label:
- event.kind === 'territory-disinfected'
- ? `Patient Zero global cleaner feed: ${event.affectedAgentName}${
- event.affectedAllianceId
- ? ` (${event.affectedAllianceId})`
- : ''
- } lost ${event.cell}${pressureSuffix(event)}`
- : `Patient Zero global cleaner feed: ${event.blockingAgentName}${
- event.blockingAllianceId
- ? ` (${event.blockingAllianceId})`
- : ''
- } blocked a clean at ${event.cell}${pressureSuffix(event)}`,
- cell: event.cell,
- }))
- : [];
- const globalFeedSummary: BehaviorTraceEvidence[] =
- globalFeed && globalFeed.totalEventCount > 0
- ? [
- {
- kind: 'patient-zero-threat',
- label: `Patient Zero global cleaner feed: ${globalFeed.events.length}/${globalFeed.totalEventCount} displayed${globalFeed.truncated ? ' · truncated' : ''}`,
- },
- ]
- : [];
- return [
- ...globalFeedSummary,
- ...localPlayerThreats.map(({ eventId, kind, label, cell }) => ({
- kind,
- label: `${label}${pressureSuffix(
- globalFeed?.events.find((event) => event.eventId === eventId),
- )}`,
- cell,
- })),
- ...globalPlayerThreats,
- ...inboundDirect,
- ...zeroMessages,
- ...allianceMessages,
- ...territory,
- ...allianceEvents,
- ...worldEvents,
- ...publicMessages,
- ];
-}
-
-function observedChanges(
- turn: AgentTurnRecord,
- previous: AgentTurnRecord | undefined,
- agentId: AgentId,
- evidence: readonly BehaviorTraceEvidence[],
-): string[] {
- if (!previous) return ['First retained observation for this agent.'];
- const changes: string[] = [];
- const current = turn.observation;
- const prior = previous.observation;
- if (current.currentCell.cell !== prior.currentCell.cell)
- changes.push(
- `Observed cell changed ${prior.currentCell.cell} → ${current.currentCell.cell}.`,
- );
- if (current.currentCell.state !== prior.currentCell.state)
- changes.push(
- `Current-cell state changed ${prior.currentCell.state} → ${current.currentCell.state}.`,
- );
- const previousTerritory = ownTerritoryCount(previous, agentId);
- const currentTerritory = ownTerritoryCount(turn, agentId);
- if (
- previousTerritory !== undefined &&
- currentTerritory !== undefined &&
- previousTerritory !== currentTerritory
- )
- changes.push(
- `Controlled territory changed ${previousTerritory} → ${currentTerritory}.`,
- );
- if (current.actingAllianceId !== prior.actingAllianceId)
- changes.push(
- `Alliance changed ${prior.actingAllianceId ?? 'unaffiliated'} → ${
- current.actingAllianceId ?? 'unaffiliated'
- }.`,
- );
- if (goalSignature(current.currentGoal) !== goalSignature(prior.currentGoal))
- changes.push('Strategic goal context changed.');
- if (
- memorySignature(current.currentMemory) !==
- memorySignature(prior.currentMemory)
- )
- changes.push('Compact memory context changed.');
- if (evidence.length)
- changes.push(
- `${evidence.length} new retained evidence item${
- evidence.length === 1 ? '' : 's'
- } entered the retained observation.`,
- );
- return changes.length
- ? changes
- : ['No retained observation change detected.'];
-}
-
-function legalActions(turn: AgentTurnRecord): string[] {
- const availability = turn.observation.actionAvailability;
- if (!availability) return ['Legacy affordances unavailable'];
- const moves = availability.moveOptions.length
- ? availability.moveOptions.map(({ direction }) => `Move ${direction}`)
- : [
- `${availability.moveTargetCellIds.length} legal move target${
- availability.moveTargetCellIds.length === 1 ? '' : 's'
- }`,
- ];
- return [
- ...moves,
- ...(availability.infect.available ? ['Infect'] : []),
- ...(availability.capture.available ? ['Capture'] : []),
- 'Wait',
- ];
-}
-
-function actionDescription(turn: CompletedTurn): {
- label: string;
- pattern: string;
- direction?: string;
- cell?: H3Cell;
-} {
- if (turn.worldAction.type === 'move') {
- const targetCell = turn.worldAction.targetCell;
- const direction = turn.observation.actionAvailability?.moveOptions.find(
- (option) => option.targetCell === targetCell,
- )?.direction;
- return {
- label: `Move${direction ? ` ${direction}` : ''} → ${targetCell}`,
- pattern: `move${direction ? ` ${direction}` : ''}`,
- direction,
- cell: targetCell,
- };
- }
- const label =
- turn.worldAction.type[0]!.toUpperCase() + turn.worldAction.type.slice(1);
- return { label, pattern: turn.worldAction.type };
-}
-
-function actionPattern(
- turn: AgentTurnRecord,
- previousCompleted: CompletedTurn | undefined,
-): string | undefined {
- if (!isCompletedTurn(turn) || !previousCompleted) return undefined;
- const current = actionDescription(turn);
- const previous = actionDescription(previousCompleted);
- if (current.pattern === previous.pattern)
- return `Repeated ${current.pattern}.`;
- if (current.direction && previous.direction)
- return `Changed direction ${previous.direction} → ${current.direction}.`;
- return `Changed ${previous.pattern} → ${current.pattern}.`;
-}
-
-function continuity(turn: AgentTurnRecord): string[] {
- if (!isCompletedTurn(turn)) return [];
- const entries: string[] = [];
- if (turn.goalRevisionResult.requested)
- entries.push(
- `Goal ${turn.goalRevisionResult.operation}: ${
- turn.goalRevisionResult.accepted
- ? 'accepted'
- : `rejected (${turn.goalRevisionResult.reason})`
- }`,
- );
- if (turn.memoryOperationResult.requested)
- entries.push(
- `Memory ${turn.memoryOperationResult.operation}: ${
- turn.memoryOperationResult.accepted
- ? 'accepted'
- : `rejected (${turn.memoryOperationResult.reason})`
- }`,
- );
- if (turn.communicationResult.requested)
- entries.push(
- `Communication ${
- turn.communicationResult.accepted
- ? turn.communicationResult.event.channel
- : turn.communicationResult.attempt.channel
- }: ${turn.communicationResult.accepted ? 'accepted' : 'rejected'}`,
- );
- if (turn.diplomacyResult.requested)
- entries.push(
- `Diplomacy ${
- turn.diplomacyResult.accepted
- ? turn.diplomacyResult.intent.type
- : turn.diplomacyResult.attempt.type
- }: ${turn.diplomacyResult.accepted ? 'accepted' : 'rejected'}`,
- );
- return entries;
-}
-
-export function deriveBehaviorTrace(
- turns: readonly AgentTurnRecord[],
- agentId: AgentId,
- limit = BEHAVIOR_TRACE_LIMIT,
-): BehaviorTraceEntry[] {
- const agentTurns = turns.filter((turn) => turn.agentId === agentId);
- const boundedLimit = Number.isFinite(limit)
- ? Math.max(0, Math.min(BEHAVIOR_TRACE_LIMIT, Math.floor(limit)))
- : BEHAVIOR_TRACE_LIMIT;
- const start = Math.max(0, agentTurns.length - boundedLimit);
- return agentTurns
- .slice(start)
- .map((turn, visibleIndex) => {
- const sourceIndex = start + visibleIndex;
- const previous = agentTurns[sourceIndex - 1];
- const previousCompleted = agentTurns
- .slice(0, sourceIndex)
- .findLast(isCompletedTurn);
- const allEvidence = collectEvidence(turn, previous, agentId);
- const chosen: ReturnType = isCompletedTurn(turn)
- ? actionDescription(turn)
- : {
- label: `No completed action · ${turn.outcome}`,
- pattern: turn.outcome,
- };
- const pattern = actionPattern(turn, previousCompleted);
- return {
- turn,
- hasPreviousObservation: Boolean(previous),
- observedChanges: observedChanges(turn, previous, agentId, allEvidence),
- evidence: allEvidence.slice(0, BEHAVIOR_TRACE_EVIDENCE_LIMIT),
- evidenceTruncated: allEvidence.length > BEHAVIOR_TRACE_EVIDENCE_LIMIT,
- legalActions: legalActions(turn),
- chosenAction: chosen.label,
- ...(chosen.cell ? { chosenCell: chosen.cell } : {}),
- ...(pattern ? { actionPattern: pattern } : {}),
- continuity: continuity(turn),
- };
- })
- .reverse();
-}
diff --git a/apps/world-lab/src/components/personality-presets.ts b/apps/world-lab/src/components/personality-presets.ts
deleted file mode 100644
index 08ff48a..0000000
--- a/apps/world-lab/src/components/personality-presets.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { personalitySchema } from '@hexzero/shared';
-
-export const PERSONALITY_PRESET_IDS = [
- 'aggressive-infector',
- 'explorer',
- 'territorial',
- 'opportunist',
- 'agent-seeking',
-] as const;
-
-export type PersonalityPresetId = (typeof PERSONALITY_PRESET_IDS)[number];
-
-export interface PersonalityPreset {
- readonly id: PersonalityPresetId;
- readonly name: string;
- readonly personality: string;
-}
-
-const personalityPresetDefinitions = [
- {
- id: 'aggressive-infector',
- name: 'Aggressive infector',
- personality:
- 'Prioritize infecting the current cell whenever it is open. When it is already infected, move decisively to an adjacent open cell; wait only when no useful adjacent move is available.',
- },
- {
- id: 'explorer',
- name: 'Explorer',
- personality:
- 'Favor movement and variety. Choose adjacent open cells when possible, avoid repeatedly lingering near the same activity, and infect only when movement offers little new territory to observe.',
- },
- {
- id: 'territorial',
- name: 'Territorial',
- personality:
- 'Build a compact infected area. Infect the current cell when it is open; otherwise prefer adjacent cells near visible infected cells and avoid drifting away from the local cluster.',
- },
- {
- id: 'opportunist',
- name: 'Opportunist',
- personality:
- 'Exploit the clearest immediate opportunity in each observation. Infect an open current cell, move to a useful adjacent open cell when already infected, and wait when neither improves the situation.',
- },
- {
- id: 'agent-seeking',
- name: 'Agent-seeking',
- personality:
- 'Seek visible nearby agents. Prefer an adjacent move that brings you closer to an observed agent, infect the current cell when useful, and wait only when no adjacent move improves proximity.',
- },
-] as const satisfies readonly PersonalityPreset[];
-
-export const PERSONALITY_PRESETS: readonly PersonalityPreset[] =
- personalityPresetDefinitions.map((preset) => ({
- ...preset,
- personality: personalitySchema.parse(preset.personality),
- }));
-
-export function matchingPersonalityPreset(personality: string) {
- return PERSONALITY_PRESETS.find(
- (preset) => preset.personality === personality,
- );
-}
diff --git a/apps/world-lab/src/components/ui-color.test.ts b/apps/world-lab/src/components/ui-color.test.ts
index f561ac3..278d24c 100644
--- a/apps/world-lab/src/components/ui-color.test.ts
+++ b/apps/world-lab/src/components/ui-color.test.ts
@@ -4,29 +4,19 @@ import { neutralAgentColor, resolveAgentColor } from './ui-color';
const agentId = '11111111-1111-4111-8111-111111111111' as AgentId;
const otherId = '22222222-2222-4222-8222-222222222222' as AgentId;
-const state = (allied: boolean) =>
+const state = () =>
({
world: {
agents: [{ id: agentId, color: '#123456' }],
- alliances: allied
- ? [{ color: '#abcdef', memberAgentIds: [agentId] }]
- : [],
},
}) as unknown as Pick;
-describe('effective agent color resolution', () => {
- it('prefers current alliance color and updates when membership changes', () => {
- expect(resolveAgentColor(state(true), agentId, '#654321')).toBe('#abcdef');
- expect(resolveAgentColor(state(false), agentId, '#654321')).toBe(
- neutralAgentColor,
- );
+describe('agent color resolution', () => {
+ it("resolves to the agent's own color", () => {
+ expect(resolveAgentColor(state(), agentId)).toBe('#123456');
});
- it('falls back through retained, base, and neutral colors', () => {
- expect(resolveAgentColor(state(false), agentId)).toBe(neutralAgentColor);
- expect(resolveAgentColor(state(false), otherId, '#654321')).toBe(
- neutralAgentColor,
- );
- expect(resolveAgentColor(state(false), otherId)).toBe(neutralAgentColor);
+ it('falls back to the neutral color when the agent is unknown', () => {
+ expect(resolveAgentColor(state(), otherId)).toBe(neutralAgentColor);
});
});
diff --git a/apps/world-lab/src/components/ui-color.ts b/apps/world-lab/src/components/ui-color.ts
index 5c6b128..9cef231 100644
--- a/apps/world-lab/src/components/ui-color.ts
+++ b/apps/world-lab/src/components/ui-color.ts
@@ -9,10 +9,7 @@ export const neutralAgentColor = NEUTRAL_AGENT_COLOR;
export function resolveAgentColor(
snapshot: Pick,
agentId: AgentId,
- _retainedEffectiveColor?: string | null,
): string {
- const currentAlliance = snapshot.world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(agentId),
- );
- return currentAlliance?.color ?? neutralAgentColor;
+ const agent = snapshot.world.agents.find(({ id }) => id === agentId);
+ return agent?.color ?? neutralAgentColor;
}
diff --git a/apps/world-lab/src/components/world-lab.test.tsx b/apps/world-lab/src/components/world-lab.test.tsx
index 532d056..9070134 100644
--- a/apps/world-lab/src/components/world-lab.test.tsx
+++ b/apps/world-lab/src/components/world-lab.test.tsx
@@ -1,7 +1,7 @@
import { render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { afterEach, describe, expect, it, vi } from 'vitest';
-import { NEUTRAL_AGENT_COLOR, simulationSnapshotSchema } from '@hexzero/shared';
+import { simulationSnapshotSchema } from '@hexzero/shared';
import {
createDefaultAppliedScenario,
createDevelopmentWorld,
@@ -44,7 +44,6 @@ const metrics = {
uniqueVisitedCells: 0,
tokens: {},
knownCostCredits: 0,
- turnsWithUnknownCost: 0,
};
const snapshot = simulationSnapshotSchema.parse({
world,
@@ -87,11 +86,8 @@ const snapshot = simulationSnapshotSchema.parse({
agentId: id,
name,
color,
- allianceId: null,
- effectiveColor: NEUTRAL_AGENT_COLOR,
controlledCellCount: 0,
})),
- currentAlliances: [],
simulatedPlayerMetrics: {
movements: 0,
cellsDisinfected: 0,
@@ -308,12 +304,9 @@ describe('WorldLab swarm workspace', () => {
it('requests a bounded full-safe swarm export preview before enabling export actions', async () => {
const preview = {
experimentId: snapshot.experiment.id,
- matchingTurnCount: 0,
matchingTickCount: 0,
matchingSwarmTickCount: 0,
- matchingCommunicationCount: 0,
matchingControlChangeCount: 0,
- matchingDiplomacyEventCount: 0,
matchingProviderAttemptCount: 0,
selectedAgentCount: world.agents.length,
retention: {
@@ -326,7 +319,6 @@ describe('WorldLab swarm workspace', () => {
},
knownCostCredits: 0,
attemptsWithUnknownCost: 0,
- turnsWithUnknownCost: 0,
serializedUtf8Bytes: 100,
approximateAiInputTokens: 25,
tokenEstimateMethod: 'ceil(UTF-8 bytes / 4)' as const,
diff --git a/apps/world-lab/src/components/world-lab.tsx b/apps/world-lab/src/components/world-lab.tsx
index ce8b8dd..8896e78 100644
--- a/apps/world-lab/src/components/world-lab.tsx
+++ b/apps/world-lab/src/components/world-lab.tsx
@@ -11,29 +11,19 @@ import {
type KeyboardEvent as ReactKeyboardEvent,
} from 'react';
import {
- PERSONALITY_MAX_LENGTH,
NEUTRAL_AGENT_COLOR,
- PERSONALITY_PROFILES,
- STRATEGY_PROFILES,
- assignBehavior,
archiveExperimentExportResponseSchema,
cancelSimulationResponseSchema,
cancelledTickResponseSchema,
experimentExportPreviewSchema,
experimentExportRequestSchema,
experimentExportResponseSchema,
- experimentImportResponseSchema,
modelCatalogResponseSchema,
- personalitySchema,
resetSimulationResponseSchema,
reasoningProfilesForModel,
- restoreDefaultPersonalitiesResponseSchema,
simulationSnapshotSchema,
singleTickResponseSchema,
- updateAgentPersonalityRequestSchema,
- updateAgentPersonalityResponseSchema,
updateExperimentModelsResponseSchema,
- updateExperimentBehaviorResponseSchema,
verifyModelResponseSchema,
worldSetupPreviewResponseSchema,
applyWorldSetupResponseSchema,
@@ -42,7 +32,6 @@ import {
defaultWorldSetupResponseSchema,
WORLD_RADIUS_PRESETS,
type AgentId,
- type AgentTurnRecord,
type CustomExportOptions,
type ExperimentExportDocument,
type ExperimentExportPreview,
@@ -53,20 +42,13 @@ import {
type ModelVerification,
type ReasoningProfile,
type ExperimentModelConfiguration,
- type BehaviorConfiguration,
type SimulationSnapshot,
type WorldSetupRequest,
type WorldSetupPreviewResponse,
} from '@hexzero/shared';
-import {
- matchingPersonalityPreset,
- PERSONALITY_PRESETS,
-} from './personality-presets';
-
import { WorldMap } from './world-map';
import { buildModelOptions } from './model-options';
import { resolveAgentColor } from './ui-color';
-import { BEHAVIOR_TRACE_LIMIT, deriveBehaviorTrace } from './behavior-trace';
import {
SwarmActivityPanel,
SwarmAgentInspector,
@@ -101,33 +83,7 @@ export function WorldLab() {
const [inspectorTab, setInspectorTab] = useState<
'scoreboard' | 'agent' | 'hex' | 'run'
>('agent');
- const [activityTab, setActivityTab] = useState<
- 'chat' | 'private' | 'events' | 'recovery' | 'swarm'
- >('chat');
const [snapshot, setSnapshot] = useState(null);
- const [privateCommsUnread, setPrivateCommsUnread] = useState(0);
- const privateCommCount =
- snapshot?.world.events.filter(
- (event) =>
- event.type === 'direct-message-sent' ||
- event.type === 'alliance-message-sent' ||
- event.type === 'zero-message-sent',
- ).length ?? 0;
- const previousPrivateCommCount = useRef(null);
- useEffect(() => {
- if (previousPrivateCommCount.current === null) {
- if (snapshot) previousPrivateCommCount.current = privateCommCount;
- return;
- }
- const added = Math.max(
- 0,
- privateCommCount - previousPrivateCommCount.current,
- );
- if (activityTab !== 'private' && added)
- setPrivateCommsUnread((count) => count + added);
- if (activityTab === 'private') setPrivateCommsUnread(0);
- previousPrivateCommCount.current = privateCommCount;
- }, [activityTab, privateCommCount, snapshot]);
const [selectedCell, setSelectedCell] = useState(null);
const [selectedAgentId, setSelectedAgentId] = useState(null);
const [running, setRunning] = useState(false);
@@ -138,10 +94,6 @@ export function WorldLab() {
const [reconciling, setReconciling] = useState(false);
const [recoveryNotice, setRecoveryNotice] = useState(null);
const [resetting, setResetting] = useState(false);
- const [personalityPending, setPersonalityPending] = useState(false);
- const [personalityNotice, setPersonalityNotice] = useState(
- null,
- );
const [speed, setSpeed] = useState(1_000);
const [uiError, setUiError] = useState(null);
const [exportOpen, setExportOpen] = useState(false);
@@ -394,36 +346,6 @@ export function WorldLab() {
}
};
- const updateBehavior = async (
- configuration: Omit,
- ): Promise => {
- if (configurationPendingRef.current) return false;
- configurationPendingRef.current = true;
- setConfigurationPending(true);
- setUiError(null);
- try {
- const response = await fetch(`${apiBase}/experiment/behavior`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(configuration),
- });
- if (!response.ok) throw new Error('behavior update rejected');
- applySnapshot(
- updateExperimentBehaviorResponseSchema.parse(await response.json())
- .snapshot,
- );
- return true;
- } catch {
- setUiError(
- 'Behavior assignments could not be saved. They may be locked after turn one.',
- );
- return false;
- } finally {
- configurationPendingRef.current = false;
- setConfigurationPending(false);
- }
- };
-
const verifyModel = async (
modelId: string,
reasoningProfile: ReasoningProfile,
@@ -456,34 +378,6 @@ export function WorldLab() {
}
};
- const importExperiment = async (file: File): Promise => {
- if (file.size > 5_000_000) {
- setUiError('Experiment import files must be 5 MB or smaller.');
- return;
- }
- try {
- const document = JSON.parse(await file.text()) as unknown;
- const response = await fetch(`${apiBase}/experiment/import`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ document }),
- });
- const body = await response.json();
- if (!response.ok) {
- const error = body as { error?: { message?: string } };
- setUiError(
- error.error?.message ?? 'The experiment import was rejected.',
- );
- return;
- }
- const payload = experimentImportResponseSchema.parse(body);
- applySnapshot(payload.snapshot);
- setPersonalityNotice(payload.message);
- } catch {
- setUiError('The selected file is not a valid experiment export.');
- }
- };
-
const executeTurn = useCallback(async () => {
if (inFlightRef.current) return;
if (
@@ -656,89 +550,6 @@ export function WorldLab() {
}
};
- const updatePersonality = async (
- agentId: AgentId,
- personality: string,
- ): Promise => {
- const request = updateAgentPersonalityRequestSchema.safeParse({
- personality,
- });
- if (!request.success) return false;
- setPersonalityPending(true);
- setPersonalityNotice(null);
- setUiError(null);
- try {
- const response = await fetch(`${apiBase}/agents/${agentId}/personality`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(request.data),
- });
- if (response.status === 409) {
- setUiError(
- 'Personality changes are unavailable until the current tick completes.',
- );
- return false;
- }
- if (!response.ok) {
- setUiError('The personality was rejected safely by the Game API.');
- return false;
- }
- const payload = updateAgentPersonalityResponseSchema.parse(
- await response.json(),
- );
- applySnapshot(payload.snapshot);
- setPersonalityNotice(`${payload.agent.name}'s personality was updated.`);
- return true;
- } catch {
- setUiError(
- 'Personality update failed safely. The existing personality was left intact.',
- );
- return false;
- } finally {
- setPersonalityPending(false);
- }
- };
-
- const restoreDefaultPersonalities = async () => {
- if (
- !window.confirm(
- 'Restore milestone default personalities for applicable active agents? World progress will be preserved.',
- )
- )
- return;
- setPersonalityPending(true);
- setPersonalityNotice(null);
- setUiError(null);
- try {
- const response = await fetch(
- `${apiBase}/personalities/restore-defaults`,
- {
- method: 'POST',
- },
- );
- if (response.status === 409) {
- setUiError(
- 'Default personalities cannot be restored until the current tick completes.',
- );
- return;
- }
- if (!response.ok) throw new Error('restore personalities failed');
- const payload = restoreDefaultPersonalitiesResponseSchema.parse(
- await response.json(),
- );
- applySnapshot(payload.snapshot);
- setPersonalityNotice(
- 'Default personalities restored. World progress was preserved.',
- );
- } catch {
- setUiError(
- 'Restoring default personalities failed safely. Existing configuration was left intact.',
- );
- } finally {
- setPersonalityPending(false);
- }
- };
-
const fullyInfected =
snapshot?.world.hexes.every(({ state }) => state === 'infected') ?? false;
if (!snapshot) {
@@ -767,12 +578,6 @@ export function WorldLab() {
({ id }) => id === selectedHex.controllerAgentId,
)
: undefined;
- const selectedHexAlliance = selectedHexController
- ? snapshot.world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(selectedHexController.id),
- )
- : undefined;
- const latestTurn = undefined;
const status = resetting
? 'resetting'
: reconciling
@@ -790,29 +595,18 @@ export function WorldLab() {
? 'running'
: 'paused';
const activeTick = snapshot.status === 'waiting-for-model';
+ const swarmMode = true;
const terminal =
snapshot.status === 'patient-zero-captured' ||
snapshot.status === 'infection-eliminated';
- // PR2 removes the inactive social panels; World Lab now always enters via
- // the swarm workspace.
- const swarmMode = true;
- const visibleActivityTab = swarmMode ? 'swarm' : activityTab;
const zeroAgentId = snapshot.scenario.patientZeroAgentId;
const zeroModel = snapshot.resolvedModels.find(
({ agentId }) => agentId === zeroAgentId,
);
- const personalityControlsDisabled =
- running ||
- inFlight ||
- resetting ||
- personalityPending ||
- snapshot.activeAgentId !== null ||
- activeTick;
const exportMutationPending =
running ||
inFlight ||
resetting ||
- personalityPending ||
snapshot.activeAgentId !== null ||
activeTick;
const modelsReady = Boolean(zeroModel?.available);
@@ -820,14 +614,6 @@ export function WorldLab() {
const reasoningUnavailable = snapshot.resolvedModels.some(
({ issue }) => issue === 'reasoning-unavailable',
);
- const publicMessages = snapshot.world.events.filter(
- (
- event,
- ): event is Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'public-message-sent' }
- > => event.type === 'public-message-sent',
- );
return (
)}
- {!swarmMode && (
-
-
Public messages
-
- {snapshot.experiment.metrics.aggregate.publicMessagesSent}
-
-
- )}
- {!swarmMode && (
-
-
Direct messages
-
- {snapshot.experiment.metrics.aggregate.directMessagesSent}
-
-
- )}
Provider attempts
@@ -1016,7 +786,6 @@ export function WorldLab() {
includes OpenRouter amounts only when returned by the provider.
)}
- {!swarmMode && }
= runTarget
@@ -1265,24 +1031,11 @@ export function WorldLab() {
Export
- {!swarmMode && (
- void restoreDefaultPersonalities()}
- >
- {personalityPending
- ? 'Restoring…'
- : 'Restore default personalities'}
-
- )}
void reset()}
>
@@ -1299,11 +1052,7 @@ export function WorldLab() {
{recoveryNotice}
)}
- {(!modelsReady ||
- uiError ||
- fullyInfected ||
- terminal ||
- personalityNotice) && (
+ {(!modelsReady || uiError || fullyInfected || terminal) && (
{uiError ??
(fullyInfected
@@ -1312,12 +1061,11 @@ export function WorldLab() {
? snapshot.status === 'patient-zero-captured'
? 'Patient Zero was captured by the simulated player. This experiment is complete; reset or apply a new World Setup to run again.'
: 'All infection has been eliminated. This experiment is complete; reset or apply a new World Setup to run again.'
- : (personalityNotice ??
- (swarmMode
- ? 'Select an available model for Agent Zero before starting.'
- : reasoningUnavailable
- ? 'A saved reasoning profile is no longer advertised by its model. Select an available profile before starting.'
- : 'Select an available compatible model for every agent before starting.')))}
+ : swarmMode
+ ? 'Select an available model for Agent Zero before starting.'
+ : reasoningUnavailable
+ ? 'A saved reasoning profile is no longer advertised by its model. Select an available profile before starting.'
+ : 'Select an available compatible model for every agent before starting.')}
)}
{!swarmMode && !snapshot.providerConfigured && (
@@ -1387,7 +1135,6 @@ export function WorldLab() {
longitude={snapshot.scenario.center.longitude}
hexes={snapshot.world.hexes}
agents={snapshot.world.agents}
- alliances={swarmMode ? [] : snapshot.world.alliances}
patientZeroAgentId={snapshot.scenario.patientZeroAgentId}
simulatedPlayer={snapshot.world.simulatedPlayer}
selectedCell={selectedCell}
@@ -1450,113 +1197,43 @@ export function WorldLab() {
role="tabpanel"
id={`inspector-${inspectorTab}`}
>
- {inspectorTab === 'agent' &&
- selectedAgent &&
- (swarmMode ? (
- cell === selectedAgent.currentCell,
- )!.state
- }
- controlledCellCount={
- snapshot.experiment.currentTerritory.find(
- ({ agentId }) => agentId === selectedAgent.id,
- )?.controlledCellCount ?? 0
- }
- onHighlightCell={setSelectedCell}
- />
- ) : (
- cell === selectedAgent.currentCell,
- )!.state
- }
- latestTurn={latestTurn}
- turns={[]}
- directMessages={snapshot.world.events.filter(
- (
- event,
- ): event is Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'direct-message-sent' }
- > =>
- event.type === 'direct-message-sent' &&
- (event.agentId === selectedAgent.id ||
- event.recipientId === selectedAgent.id),
- )}
- agents={snapshot.world.agents}
- mutationDisabled={personalityControlsDisabled}
- mutationPending={personalityPending}
- onApplyPersonality={updatePersonality}
- onHighlightCell={setSelectedCell}
- metrics={
- snapshot.experiment.metrics.byAgent.find(
- ({ agentId }) => agentId === selectedAgent.id,
- )?.metrics
- }
- controlledCellCount={
- snapshot.experiment.currentTerritory.find(
- ({ agentId }) => agentId === selectedAgent.id,
- )?.controlledCellCount ?? 0
- }
- controlChanges={snapshot.world.events.filter(
- (
- event,
- ): event is Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'hex-captured' }
- > =>
- event.type === 'hex-captured' &&
- (event.controllerAgentId === selectedAgent.id ||
- event.previousControllerAgentId ===
- selectedAgent.id),
- )}
- />
- ))}
+ {inspectorTab === 'agent' && selectedAgent && (
+ cell === selectedAgent.currentCell,
+ )!.state
+ }
+ controlledCellCount={
+ snapshot.experiment.currentTerritory.find(
+ ({ agentId }) => agentId === selectedAgent.id,
+ )?.controlledCellCount ?? 0
+ }
+ onHighlightCell={setSelectedCell}
+ />
+ )}
{inspectorTab === 'hex' && selectedHex && (
)}
- {inspectorTab === 'scoreboard' &&
- (swarmMode ? (
-
- ) : (
- <>
-
-
- >
- ))}
- {inspectorTab === 'run' &&
- (swarmMode ? (
-
- ) : (
-
- ))}
+ {inspectorTab === 'scoreboard' && (
+
+ )}
+ {inspectorTab === 'run' && (
+
+ )}
@@ -1565,41 +1242,7 @@ export function WorldLab() {
aria-label="Activity dock"
>
-
- {(swarmMode
- ? ['swarm']
- : (['chat', 'private', 'events', 'recovery'] as const)
- ).map((tab) => (
- {
- setActivityTab(
- tab as
- 'chat' | 'private' | 'events' | 'recovery' | 'swarm',
- );
- if (tab === 'private') setPrivateCommsUnread(0);
- }}
- >
- {tab === 'swarm'
- ? 'Swarm'
- : tab === 'chat'
- ? 'Public chat'
- : tab === 'private'
- ? `Private comms${privateCommsUnread ? ` (${privateCommsUnread})` : ''}`
- : tab === 'events'
- ? 'Event log'
- : 'Failures & recovery'}
-
- ))}
-
+
Swarm activity
- {!chatCollapsed && visibleActivityTab === 'swarm' && swarmMode && (
-
- )}
- {!chatCollapsed && visibleActivityTab === 'chat' && !swarmMode && (
-
- )}
- {!chatCollapsed &&
- visibleActivityTab === 'private' &&
- !swarmMode && (
-
- )}
- {!chatCollapsed && visibleActivityTab === 'events' && (
-
- )}
- {!chatCollapsed &&
- visibleActivityTab === 'recovery' &&
- !swarmMode && }
+ {!chatCollapsed && }
>
) : (
@@ -1650,96 +1261,41 @@ export function WorldLab() {
onSelectAgent={selectAgentForInspection}
onOpenWorldSetup={() => setSetupOpen(true)}
>
- {snapshot.providerMode === 'openrouter' || swarmMode ? (
-
+ {selectedAgent && (
+ cell === selectedAgent.currentCell,
+ )!.state
+ }
+ controlledCellCount={
+ snapshot.experiment.currentTerritory.find(
+ ({ agentId }) => agentId === selectedAgent.id,
+ )?.controlledCellCount ?? 0
}
- verifications={modelVerifications}
- verifyingModelId={verifyingModelId}
- onRefresh={refreshCatalog}
- onUpdate={updateModels}
- onUpdateBehavior={updateBehavior}
- onVerify={verifyModel}
- onImport={importExperiment}
/>
- ) : (
-
- Deterministic test model assignments are active.
-
)}
- {selectedAgent &&
- (swarmMode ? (
- cell === selectedAgent.currentCell,
- )!.state
- }
- controlledCellCount={
- snapshot.experiment.currentTerritory.find(
- ({ agentId }) => agentId === selectedAgent.id,
- )?.controlledCellCount ?? 0
- }
- />
- ) : (
- cell === selectedAgent.currentCell,
- )!.state
- }
- latestTurn={latestTurn}
- turns={[]}
- directMessages={snapshot.world.events.filter(
- (
- event,
- ): event is Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'direct-message-sent' }
- > =>
- event.type === 'direct-message-sent' &&
- (event.agentId === selectedAgent.id ||
- event.recipientId === selectedAgent.id),
- )}
- agents={snapshot.world.agents}
- mutationDisabled={personalityControlsDisabled}
- mutationPending={personalityPending}
- onApplyPersonality={updatePersonality}
- metrics={
- snapshot.experiment.metrics.byAgent.find(
- ({ agentId }) => agentId === selectedAgent.id,
- )?.metrics
- }
- controlledCellCount={
- snapshot.experiment.currentTerritory.find(
- ({ agentId }) => agentId === selectedAgent.id,
- )?.controlledCellCount ?? 0
- }
- controlChanges={snapshot.world.events.filter(
- (
- event,
- ): event is Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'hex-captured' }
- > =>
- event.type === 'hex-captured' &&
- (event.controllerAgentId === selectedAgent.id ||
- event.previousControllerAgentId === selectedAgent.id),
- )}
- />
- ))}
)}
@@ -1792,25 +1348,19 @@ function AgentsWorkspace({
function HexInspector({
hex,
controller,
- alliance,
selectedAgent,
- swarmMode = false,
}: {
hex: SimulationSnapshot['world']['hexes'][number];
controller?: SimulationSnapshot['world']['agents'][number];
- alliance?: SimulationSnapshot['world']['alliances'][number];
selectedAgent?: SimulationSnapshot['world']['agents'][number];
- swarmMode?: boolean;
}) {
const relationship = !selectedAgent
? 'No agent selected'
: controller?.id === selectedAgent.id
? 'Controlled by selected agent'
- : alliance?.memberAgentIds.includes(selectedAgent.id)
- ? 'Controlled by an ally'
- : controller
- ? 'Controlled by another agent'
- : 'Open';
+ : controller
+ ? 'Controlled by another agent'
+ : 'Open';
return (
- {!swarmMode && (
- <>
-
-
Alliance
- {alliance?.id ?? 'None'}
-
-
-
Relationship
- {relationship}
-
- >
- )}
+
+
Relationship
+ {relationship}
+
);
}
-function RunHealthSummary({
+function Spinner() {
+ return ;
+}
+
+function CommandIcon({
+ name,
+}: {
+ name:
+ 'play' | 'pause' | 'step' | 'reset' | 'target' | 'map' | 'export' | 'more';
+}) {
+ const path = {
+ play: 'M8 5v14l11-7z',
+ pause: 'M7 5h4v14H7zm6 0h4v14h-4z',
+ step: 'M6 5v14l9-7zm10 0h3v14h-3z',
+ reset: 'M6.3 7.8A7 7 0 1 1 5 14h2a5 5 0 1 0 1-3l3 3H4V7z',
+ target:
+ 'M12 2v3m0 14v3M2 12h3m14 0h3m-5 0a5 5 0 1 1-10 0 5 5 0 0 1 10 0zm-3 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0z',
+ map: 'M3 6.5 8.5 4l7 2.5L21 4v13.5L15.5 20l-7-2.5L3 20zm5.5-2.5v13.5m7-11V20',
+ export: 'M12 3v12m-5-5 5 5 5-5M5 17v3h14v-3',
+ more: 'M5 12h.01M12 12h.01M19 12h.01',
+ }[name];
+ return (
+
+
+
+ );
+}
+
+function WorldSetupPanel({
+ open,
snapshot,
- status,
- runTarget,
+ apiBase,
+ returnFocusRef,
+ onClose,
+ onApplied,
}: {
+ open: boolean;
snapshot: SimulationSnapshot;
- status: string;
- runTarget: number;
-}) {
- const metrics = snapshot.experiment.metrics.aggregate;
- const elapsedMs = Math.max(
- 0,
- Date.parse(snapshot.experiment.startedAt ?? snapshot.experiment.startedAt) -
- Date.parse(snapshot.experiment.startedAt),
- );
- const elapsedMinutes = Math.floor(elapsedMs / 60_000);
- return (
-
- Long-run telemetry
- {status.replaceAll('-', ' ')}
-
-
-
Progress
-
- {snapshot.tickNumber} / {runTarget} ticks
-
-
-
-
Virtual time
- {new Date(snapshot.virtualTime).toLocaleString()}
-
-
-
Last interval
-
- {snapshot.lastTickIntervalMinutes === null
- ? 'Not started'
- : `${snapshot.lastTickIntervalMinutes} min`}
-
-
-
-
Elapsed
- {elapsedMinutes} min
-
-
-
Admission exposure (not spend)
-
- {formatCost(
- snapshot.experiment.attemptAccounting.committedCreditExposure,
- )}
-
-
-
-
Provider-reported cost
-
- {formatCost(
- snapshot.experiment.attemptAccounting.knownFinalizedCostCredits,
- )}
-
-
-
-
Unknown-cost attempts
-
- {snapshot.experiment.attemptAccounting.attemptsWithUnknownCost}
-
-
-
-
Successful turns
- {metrics.accepted}
-
-
-
Rejected actions
- {metrics.rejectedWorldActions}
-
-
-
Provider failures
- {metrics.providerErrors}
-
-
-
Lost ticks
- {metrics.lostTicks}
-
-
-
Auto recovered
- {metrics.recoveredAutomatically}
-
-
-
Manual recovered
- {metrics.recoveredManually}
-
-
-
Unattended recovered
- {metrics.recoveredByUnattendedRetry}
-
-
-
Skipped turns
- {metrics.operatorSkipped}
-
-
-
- );
-}
-
-function RecoveryLog({
- snapshot,
- turns,
-}: {
- snapshot: SimulationSnapshot;
- turns: AgentTurnRecord[];
-}) {
- const failures = turns
- .filter(
- (turn) =>
- turn.outcome === 'provider-error' ||
- turn.outcome === 'lost-tick' ||
- turn.outcome === 'operator-skipped',
- )
- .slice(-40)
- .toReversed();
- return (
-
- {failures.length === 0 ? (
- No failures or recovery actions recorded.
- ) : (
-
- {failures.map((turn) => {
- const agent = snapshot.world.agents.find(
- ({ id }) => id === turn.agentId,
- );
- return (
-
-
- {formatRecordSequence(turn)} · record {turn.turnNumber} ·{' '}
- {agent?.name ?? turn.agentId}
-
-
- {turn.failure.code} ·{' '}
- {turn.provider?.model ??
- turn.failure.model ??
- 'model unavailable'}
-
-
- {turn.outcome === 'operator-skipped'
- ? `${turn.skipKind} skip`
- : turn.outcome === 'lost-tick'
- ? `final lost tick ${turn.tickNumber}`
- : 'legacy provider failure'}
-
-
- );
- })}
-
- )}
-
- );
-}
-
-function Spinner() {
- return ;
-}
-
-function CommandIcon({
- name,
-}: {
- name:
- 'play' | 'pause' | 'step' | 'reset' | 'target' | 'map' | 'export' | 'more';
-}) {
- const path = {
- play: 'M8 5v14l11-7z',
- pause: 'M7 5h4v14H7zm6 0h4v14h-4z',
- step: 'M6 5v14l9-7zm10 0h3v14h-3z',
- reset: 'M6.3 7.8A7 7 0 1 1 5 14h2a5 5 0 1 0 1-3l3 3H4V7z',
- target:
- 'M12 2v3m0 14v3M2 12h3m14 0h3m-5 0a5 5 0 1 1-10 0 5 5 0 0 1 10 0zm-3 0a2 2 0 1 1-4 0 2 2 0 0 1 4 0z',
- map: 'M3 6.5 8.5 4l7 2.5L21 4v13.5L15.5 20l-7-2.5L3 20zm5.5-2.5v13.5m7-11V20',
- export: 'M12 3v12m-5-5 5 5 5-5M5 17v3h14v-3',
- more: 'M5 12h.01M12 12h.01M19 12h.01',
- }[name];
- return (
-
-
-
- );
-}
-
-function WorldSetupPanel({
- open,
- snapshot,
- apiBase,
- returnFocusRef,
- onClose,
- onApplied,
-}: {
- open: boolean;
- snapshot: SimulationSnapshot;
- apiBase: string;
- returnFocusRef: { current: HTMLElement | null };
- onClose: () => void;
- onApplied: (snapshot: SimulationSnapshot) => void;
+ apiBase: string;
+ returnFocusRef: { current: HTMLElement | null };
+ onClose: () => void;
+ onApplied: (snapshot: SimulationSnapshot) => void;
}) {
const initialDraft = useMemo(() => {
const scenario = snapshot.scenario;
@@ -2068,13 +1450,11 @@ function WorldSetupPanel({
rosterSeed: scenario.rosterSeed,
spawnSeed: scenario.spawnSeed,
minimumSpawnSeparation: scenario.minimumSpawnSeparation,
- communicationRangeKm: scenario.communicationRangeKm,
minimumTickIntervalMinutes: scenario.minimumTickIntervalMinutes,
maximumTickIntervalMinutes: scenario.maximumTickIntervalMinutes,
patientZeroAgentId: scenario.patientZeroAgentId,
roster: scenario.roster,
modelConfiguration: scenario.modelConfiguration,
- behaviorConfiguration: scenario.behaviorConfiguration,
objectiveVersion: scenario.objectiveVersion,
capabilities: scenario.capabilities,
simulatedPlayer: scenario.simulatedPlayer,
@@ -2099,24 +1479,6 @@ function WorldSetupPanel({
>([]);
const key = JSON.stringify(draft);
const fresh = previewKey === key;
- const reconcileBehaviorAssignments = (
- roster: WorldSetupRequest['roster'],
- configuration: WorldSetupRequest['behaviorConfiguration'],
- mode = configuration.assignmentMode,
- ) => {
- const generated = assignBehavior(
- roster.map(({ id }) => id),
- configuration.seed,
- mode === 'manual' ? 'balanced-random' : mode,
- );
- if (mode !== 'manual') return generated;
- return generated.map((fallback) => ({
- ...(configuration.assignments.find(
- ({ agentId }) => agentId === fallback.agentId,
- ) ?? fallback),
- manual: true,
- }));
- };
const replaceRoster = (roster: WorldSetupRequest['roster']) =>
setDraft((current) => {
const ids = new Set(roster.map(({ id }) => id));
@@ -2133,13 +1495,6 @@ function WorldSetupPanel({
({ agentId }) => ids.has(agentId),
),
},
- behaviorConfiguration: {
- ...current.behaviorConfiguration,
- assignments: reconcileBehaviorAssignments(
- roster,
- current.behaviorConfiguration,
- ),
- },
};
});
const generateRoster = async (count: number, appendOnly = false) => {
@@ -2459,22 +1814,6 @@ function WorldSetupPanel({
}
/>
-
- Communication range (km)
-
- setDraft({
- ...draft,
- communicationRangeKm: Number(event.target.value),
- })
- }
- />
-
Minimum virtual minutes per tick
- {draft.swarmArchitectureVersion !== 'zero-swarm-v1' && (
- <>
-
- Behavior assignment seed
- {
- const seed = event.target.value;
- setDraft((current) => ({
- ...current,
- behaviorConfiguration: {
- ...current.behaviorConfiguration,
- seed,
- assignments:
- current.behaviorConfiguration.assignmentMode ===
- 'manual'
- ? current.behaviorConfiguration.assignments
- : assignBehavior(
- current.roster.map(({ id }) => id),
- seed,
- current.behaviorConfiguration.assignmentMode,
- ),
- },
- }));
- }}
- />
-
-
- The seed deterministically generates balanced-random and
- fully-random assignments. Manual choices override it and remain
- unchanged when the seed changes.
-
-
- Behavior mode
- {
- const mode = event.target
- .value as WorldSetupRequest['behaviorConfiguration']['assignmentMode'];
- setDraft({
- ...draft,
- behaviorConfiguration: {
- ...draft.behaviorConfiguration,
- assignmentMode: mode,
- assignments: reconcileBehaviorAssignments(
- draft.roster,
- draft.behaviorConfiguration,
- mode,
- ),
- },
- });
- }}
- >
- Balanced random
- Fully random
- Manual
-
-
-
- Model and reasoning assignments are shared with Agent Controller
- and preserved unless an agent is removed.
-
- >
- )}
Preview
@@ -2965,15 +2238,13 @@ function ModelConsole({
catalog,
loading,
snapshot,
- swarmMode = false,
+ swarmMode = true,
disabled,
verifications,
verifyingModelId,
onRefresh,
onUpdate,
onVerify,
- onUpdateBehavior,
- onImport,
}: {
catalog: ModelCatalogResponse | null;
loading: boolean;
@@ -2986,15 +2257,11 @@ function ModelConsole({
onUpdate: (
configuration: Omit,
) => Promise;
- onUpdateBehavior: (
- configuration: Omit,
- ) => Promise;
onVerify: (
modelId: string,
reasoningProfile: ReasoningProfile,
force?: boolean,
) => Promise;
- onImport: (file: File) => Promise;
}) {
const [open, setOpen] = useState(false);
const [tab, setTab] = useState<'overview' | 'models' | 'behavior'>('models');
@@ -3140,10 +2407,6 @@ function ModelConsole({
const resolved = snapshot.resolvedModels.find(
({ agentId }) => agentId === agent.id,
)!;
- const behavior =
- snapshot.behaviorConfiguration.assignments.find(
- ({ agentId }) => agentId === agent.id,
- )!;
return (
- {!swarmMode && (
-
- {behavior.personalityId} · {behavior.strategyId}
-
- )}
{resolved.available ? 'Ready' : 'Needs configuration'}
@@ -3174,9 +2432,6 @@ function ModelConsole({
})}
)}
- {tab === 'behavior' && !swarmMode && (
-
- )}
{tab === 'models' && (
)}
@@ -3492,162 +2732,6 @@ function ModelConsole({
);
}
-function BehaviorPanel({
- snapshot,
- onUpdate,
-}: {
- snapshot: SimulationSnapshot;
- onUpdate: (
- configuration: Omit,
- ) => Promise;
-}) {
- const configuration = snapshot.behaviorConfiguration;
- const locked = configuration.locked || snapshot.tickNumber > 0;
- const update = (
- next: Omit,
- ) => void onUpdate(next);
- return (
-
-
-
- Assignment mode
- {
- const assignmentMode = event.target
- .value as BehaviorConfiguration['assignmentMode'];
- update({
- assignmentMode,
- seed: configuration.seed,
- assignments:
- assignmentMode === 'manual'
- ? configuration.assignments
- : assignBehavior(
- snapshot.world.agents.map(({ id }) => id),
- configuration.seed,
- assignmentMode,
- ),
- });
- }}
- >
- Balanced random
- Fully random
- Manual
-
-
-
- Experiment behavior seed
-
-
- {
- const seed = crypto.randomUUID();
- update({
- assignmentMode: configuration.assignmentMode,
- seed,
- assignments: assignBehavior(
- snapshot.world.agents.map(({ id }) => id),
- seed,
- configuration.assignmentMode as
- 'balanced-random' | 'fully-random',
- ),
- });
- }}
- >
- Randomize assignments
-
-
- {locked && (
-
- Behavior is locked after turn one so retained experiments remain
- reproducible. Reset starts a new experiment and unlocks setup.
-
- )}
-
- {snapshot.world.agents.map((agent) => {
- const assignment = configuration.assignments.find(
- ({ agentId }) => agentId === agent.id,
- )!;
- const change = (
- field: 'personalityId' | 'strategyId',
- value: string,
- ) =>
- update({
- assignmentMode: 'manual',
- seed: configuration.seed,
- assignments: configuration.assignments.map((candidate) =>
- candidate.agentId === agent.id
- ? { ...candidate, [field]: value, manual: true }
- : candidate,
- ),
- });
- return (
-
- {agent.name}
-
- Personality
-
- change('personalityId', event.target.value)
- }
- >
- {PERSONALITY_PROFILES.map((profile) => (
-
- {profile.label}
-
- ))}
-
-
-
- Strategy
- change('strategyId', event.target.value)}
- >
- {STRATEGY_PROFILES.map((profile) => (
-
- {profile.label}
-
- ))}
-
-
-
- );
- })}
-
-
-
-
Personalities
- {PERSONALITY_PROFILES.map((profile) => (
-
- {profile.label} — {profile.description}
-
- ))}
-
-
-
Strategies
- {STRATEGY_PROFILES.map((profile) => (
-
- {profile.label} — {profile.description}
-
- ))}
-
-
-
- );
-}
-
function ModelFacts({ model }: { model: CompatibleModel }) {
return (
@@ -3709,15 +2793,9 @@ function AgentRoster({
const territory = snapshot.experiment.currentTerritory.find(
({ agentId }) => agentId === agent.id,
);
- const alliance = snapshot.world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(agent.id),
- );
const resolved = snapshot.resolvedModels.find(
({ agentId }) => agentId === agent.id,
)!;
- const behavior = snapshot.behaviorConfiguration.assignments.find(
- ({ agentId }) => agentId === agent.id,
- )!;
return (
- {swarmMode ? (
- agent.id === snapshot.scenario.patientZeroAgentId ? (
- 'Swarm planner'
- ) : (
- (directive?.mission ?? 'No current directive')
- )
- ) : (
- <>{alliance ? 'Allied' : 'Unaffiliated'} · >
- )}
+ {swarmMode
+ ? agent.id === snapshot.scenario.patientZeroAgentId
+ ? 'Swarm planner'
+ : (directive?.mission ?? 'No current directive')
+ : null}
{!swarmMode && <>{territory?.controlledCellCount ?? 0} cells>}
{swarmMode ? (
@@ -3767,14 +2841,6 @@ function AgentRoster({
{formatReasoningProfile(resolved.reasoningProfile)}
)}
- {!swarmMode && (
-
- {behavior.personalityId} · {behavior.strategyId}
-
- )}
);
@@ -3783,1387 +2849,38 @@ function AgentRoster({
);
}
-function PrivateComms({
+const defaultCustomOptions: CustomExportOptions = {
+ turnObservations: true,
+ nearbyAgents: true,
+ recentEvents: true,
+ recentControlChanges: true,
+ validationDetails: true,
+ resultingEvents: true,
+ providerUsageMetadata: true,
+ initialWorldState: false,
+ currentWorldState: true,
+ computedMetrics: true,
+ controlChanges: true,
+};
+
+function ExperimentExportPanel({
snapshot,
- onSelectAgent,
+ agents,
+ disabled,
+ open,
+ selectedAgentIds,
+ onOpenChange,
+ onSelectionChange,
+ returnFocusRef,
}: {
snapshot: SimulationSnapshot;
- onSelectAgent: (agentId: AgentId) => void;
-}) {
- const [filter, setFilter] = useState<'all' | 'direct' | 'alliance' | 'zero'>(
- 'all',
- );
- const messages = snapshot.world.events
- .filter(
- (
- event,
- ): event is Extract<
- SimulationSnapshot['world']['events'][number],
- {
- type:
- | 'direct-message-sent'
- | 'alliance-message-sent'
- | 'zero-message-sent';
- }
- > =>
- event.type === 'direct-message-sent' ||
- event.type === 'alliance-message-sent' ||
- event.type === 'zero-message-sent',
- )
- .filter((event) => filter === 'all' || event.channel === filter)
- .toReversed()
- .slice(0, 120);
- const rejections: AgentTurnRecord[] = [];
- return (
-
-
-
-
- World Lab operator-only · hidden from players
-
-
Private comms
-
-
-
- {(['all', 'direct', 'alliance', 'zero'] as const).map((value) => (
- setFilter(value)}
- >
- {value === 'all'
- ? 'All'
- : value === 'direct'
- ? 'Direct'
- : value === 'alliance'
- ? 'Alliance'
- : 'Zero'}
-
- ))}
-
- {messages.length === 0 ? (
- No private communications yet.
- ) : (
-
- {messages.map((event) => {
- const sender = snapshot.world.agents.find(
- ({ id }) => id === event.agentId,
- );
- const turn = undefined;
- const recipient =
- event.type === 'direct-message-sent'
- ? snapshot.world.agents.find(
- ({ id }) => id === event.recipientId,
- )
- : undefined;
- return (
-
-
- onSelectAgent(event.agentId)}
- >
- {sender?.name ?? event.agentId}
-
-
- {formatRecordSequence(turn)} · Delivered ·{' '}
- {formatTimestamp(event.occurredAt)}
-
-
- {event.message}
-
- {event.type === 'direct-message-sent' ? (
- <>
- To{' '}
- onSelectAgent(event.recipientId)}
- >
- {recipient?.name ?? event.recipientId}
- {' '}
- · {event.distance.toFixed(2)} km
- {(event.agentId ===
- snapshot.scenario.patientZeroAgentId ||
- event.recipientId ===
- snapshot.scenario.patientZeroAgentId) &&
- ' · Patient Zero endpoint'}
- >
- ) : event.type === 'alliance-message-sent' ? (
- <>
- Alliance {event.allianceId} · {event.recipientIds.length}{' '}
- recipient{event.recipientIds.length === 1 ? '' : 's'}
- >
- ) : (
- <>
- Zero broadcast · {event.recipientIds.length} recipient
- {event.recipientIds.length === 1 ? '' : 's'}
- >
- )}
-
-
- );
- })}
-
- )}
- {rejections.length > 0 && (
- <>
- Rejected private attempts
-
- {rejections.map((turn) => {
- if (
- turn.outcome === 'provider-error' ||
- turn.outcome === 'lost-tick' ||
- turn.outcome === 'operator-skipped' ||
- !turn.communicationResult.requested ||
- turn.communicationResult.accepted
- )
- return null;
- return (
-
- onSelectAgent(turn.agentId)}
- >
- {snapshot.world.agents.find(({ id }) => id === turn.agentId)
- ?.name ?? turn.agentId}
-
- {turn.communicationResult.attempt.message}
-
- {formatRecordSequence(turn)} · Rejected:{' '}
- {turn.communicationResult.reason} ·{' '}
- {formatTimestamp(
- turn.communicationResult.attempt.occurredAt,
- )}
-
-
- );
- })}
-
- >
- )}
-
- );
-}
-
-function PublicWorldChat({
- snapshot,
- agents,
- events,
- turns,
- collapsed,
- onCollapsedChange,
-}: {
- snapshot: SimulationSnapshot;
- agents: SimulationSnapshot['world']['agents'];
- events: Array<
- Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'public-message-sent' }
- >
- >;
- turns: AgentTurnRecord[];
- collapsed: boolean;
- onCollapsedChange: (collapsed: boolean) => void;
-}) {
- const [atTop, setAtTop] = useState(true);
- const [newMessages, setNewMessages] = useState(0);
- const feedRef = useRef(null);
- const previousCount = useRef(events.length);
- const previousScrollHeight = useRef(0);
-
- useLayoutEffect(() => {
- const feed = feedRef.current;
- if (!feed || collapsed) return;
- const added = Math.max(0, events.length - previousCount.current);
- const heightDelta = feed.scrollHeight - previousScrollHeight.current;
- if (added > 0) {
- if (atTop) {
- feed.scrollTop = 0;
- } else {
- feed.scrollTop += Math.max(0, heightDelta);
- queueMicrotask(() => setNewMessages((count) => count + added));
- }
- } else if (atTop) feed.scrollTop = 0;
- previousCount.current = events.length;
- previousScrollHeight.current = feed.scrollHeight;
- }, [atTop, collapsed, events.length]);
-
- useEffect(() => {
- const feed = feedRef.current;
- if (!collapsed && feed) {
- previousScrollHeight.current = feed.scrollHeight;
- if (atTop) feed.scrollTop = 0;
- }
- }, [atTop, collapsed]);
-
- const jumpToNewest = () => {
- const feed = feedRef.current;
- if (feed) feed.scrollTop = 0;
- setAtTop(true);
- setNewMessages(0);
- };
-
- return (
-
-
-
-
Visible to every agent
-
Public world chat
-
-
onCollapsedChange(!collapsed)}
- >
- {collapsed ? 'Expand' : 'Collapse'}
-
-
- {newMessages > 0 && !collapsed && (
-
- {newMessages} new {newMessages === 1 ? 'message' : 'messages'} ·
- Return to latest
-
- )}
- {!collapsed && (
- <>
- {events.length === 0 ? (
- No public messages yet.
- ) : (
- {
- const element = event.currentTarget;
- const nearTop = element.scrollTop <= 36;
- setAtTop(nearTop);
- if (nearTop) setNewMessages(0);
- }}
- >
- {events.toReversed().map((event) => {
- const sender = agents.find(({ id }) => id === event.agentId);
- const turn = turns.find(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.communicationResult.requested &&
- turn.communicationResult.accepted &&
- turn.communicationResult.event.id === event.id,
- );
- return (
-
-
-
- {sender?.name ?? event.agentId}
-
-
- {formatRecordSequence(turn)} ·{' '}
- {formatTimestamp(event.occurredAt)}
-
-
- {event.message}
-
- );
- })}
-
- )}
- >
- )}
-
- );
-}
-
-function TerritoryScoreboard({
- entries,
-}: {
- entries: SimulationSnapshot['experiment']['currentTerritory'];
-}) {
- return (
-
- Current authoritative control
- Territory scoreboard
-
- {entries.map((entry) => (
-
-
- {entry.name}
- {entry.controlledCellCount}
- controlled cells
-
- ))}
-
-
- );
-}
-
-function AlliancePanel({ snapshot }: { snapshot: SimulationSnapshot }) {
- const unaffiliated = snapshot.world.agents.filter(
- ({ id }) =>
- !snapshot.world.alliances.some(({ memberAgentIds }) =>
- memberAgentIds.includes(id),
- ),
- );
- return (
-
- Formal engine authority
- Alliances
- {snapshot.experiment.currentAlliances.length === 0 ? (
- No active alliances.
- ) : (
-
- {snapshot.experiment.currentAlliances.map((alliance) => (
-
-
-
- {alliance.members
- .map(
- ({ name, controlledCellCount }) =>
- `${name} (${controlledCellCount})`,
- )
- .join(', ')}
-
- {alliance.totalControlledCellCount}
- combined controlled cells
-
- ))}
-
- )}
- Unaffiliated agents
-
- {unaffiliated.length
- ? unaffiliated.map(({ name }) => name).join(', ')
- : 'None'}
-
- Pending proposals
- {snapshot.world.pendingAllianceProposals.length ? (
-
- {snapshot.world.pendingAllianceProposals.map((proposal) => (
-
- {
- snapshot.world.agents.find(
- ({ id }) => id === proposal.proposerAgentId,
- )?.name
- }{' '}
- →{' '}
- {
- snapshot.world.agents.find(
- ({ id }) => id === proposal.recipientAgentId,
- )?.name
- }
- ; expires after{' '}
- {proposal.expirationTick === undefined
- ? `legacy turn ${proposal.expirationTurn}`
- : `tick ${proposal.expirationTick}`}
-
- ))}
-
- ) : (
- No pending alliance proposals.
- )}
- Recent alliance changes
-
-
- );
-}
-
-function AllianceEventList({
- snapshot,
- agentId,
-}: {
- snapshot: SimulationSnapshot;
- agentId?: AgentId;
-}) {
- const events = snapshot.world.events
- .filter(
- (event): event is AllianceWorldEvent =>
- event.type === 'alliance-proposed' ||
- event.type === 'alliance-proposal-closed' ||
- event.type === 'alliance-formed' ||
- event.type === 'agent-joined-alliance' ||
- event.type === 'agent-left-alliance' ||
- event.type === 'alliance-dissolved',
- )
- .filter(
- (event) => !agentId || allianceEventParticipants(event).includes(agentId),
- );
- if (!events.length) return No alliance changes yet.
;
- return (
-
- {events.slice(-8).map((event) => {
- const turn = undefined;
- return (
-
- {formatAllianceEvent(event, snapshot)}
-
- {formatRecordSequence(turn)} · {formatTimestamp(event.occurredAt)}
-
-
- );
- })}
-
- );
-}
-
-function AgentBehaviorTrace({
- agent,
- id,
- turns,
- onHighlightCell,
-}: {
- agent: SimulationSnapshot['world']['agents'][number];
- id: string;
- turns: AgentTurnRecord[];
- onHighlightCell?: (cell: H3Cell) => void;
-}) {
- const entries = deriveBehaviorTrace(turns, agent.id);
- return (
-
-
-
-
Behavior trace
-
- Observation evidence and self-reported summaries show correlation,
- not proven causation.
-
-
-
- {entries.length}/{BEHAVIOR_TRACE_LIMIT} retained
-
-
- {entries.length === 0 ? (
- No retained behavior records for this agent.
- ) : (
-
- {entries.map((entry, index) => {
- return (
-
-
-
-
- {formatRecordSequence(entry.turn)} · record{' '}
- {entry.turn.turnNumber}
-
-
- {entry.turn.outcome}
-
-
-
-
-
What changed
-
- {entry.observedChanges.map((change) => (
- {change}
- ))}
-
-
-
-
- {entry.hasPreviousObservation
- ? 'New retained evidence'
- : 'Evidence visible at retained baseline'}
-
- {entry.evidence.length ? (
-
- {entry.evidence.map((evidence, evidenceIndex) => (
-
- {evidence.label}{' '}
- {evidence.cell && onHighlightCell && (
-
- onHighlightCell(evidence.cell!)
- }
- >
- Highlight cell
-
- )}
-
- ))}
- {entry.evidenceTruncated && (
- Additional evidence omitted from this view.
- )}
-
- ) : (
-
- {entry.hasPreviousObservation
- ? 'No new communication or board evidence retained.'
- : 'No retained communication or board evidence visible.'}
-
- )}
-
-
-
- Observed cell: {' '}
- {entry.turn.observation.currentCell.cell}{' '}
- {onHighlightCell && (
-
- onHighlightCell(
- entry.turn.observation.currentCell.cell,
- )
- }
- >
- Highlight observed cell
-
- )}
-
-
- Legal choices: {' '}
- {entry.legalActions.join(' · ')}
-
-
- Chosen: {entry.chosenAction}{' '}
- {entry.chosenCell && onHighlightCell && (
- onHighlightCell(entry.chosenCell!)}
- >
- Highlight chosen cell
-
- )}
-
- {entry.actionPattern && (
-
- Pattern: {entry.actionPattern}
-
- )}
-
-
- Model summary (self-reported, not proof):
- {' '}
- {entry.turn.outcome === 'accepted' ||
- entry.turn.outcome === 'rejected'
- ? entry.turn.summary
- : `${entry.turn.failure.code}: ${entry.turn.failure.message}`}
-
-
-
-
Continuity operations
- {entry.continuity.length ? (
-
- {entry.continuity.map((item) => (
- {item}
- ))}
-
- ) : (
-
- No goal, memory, communication, or diplomacy update.
-
- )}
-
-
-
-
- );
- })}
-
- )}
-
- );
-}
-
-function AgentInspector({
- agent,
- snapshot,
- cellState,
- latestTurn,
- turns,
- directMessages,
- agents,
- mutationDisabled,
- mutationPending,
- onApplyPersonality,
- onHighlightCell,
- metrics,
- controlledCellCount,
- controlChanges,
-}: {
- agent: SimulationSnapshot['world']['agents'][number];
- snapshot: SimulationSnapshot;
- cellState: 'open' | 'infected';
- latestTurn?: AgentTurnRecord;
- turns: AgentTurnRecord[];
- directMessages: Array<
- Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'direct-message-sent' }
- >
- >;
- agents: SimulationSnapshot['world']['agents'];
- mutationDisabled: boolean;
- mutationPending: boolean;
- onApplyPersonality: (
- agentId: AgentId,
- personality: string,
- ) => Promise;
- onHighlightCell?: (cell: H3Cell) => void;
- metrics?: SimulationSnapshot['experiment']['metrics']['aggregate'];
- controlledCellCount: number;
- controlChanges: Array<
- Extract<
- SimulationSnapshot['world']['events'][number],
- { type: 'hex-captured' }
- >
- >;
-}) {
- type CompletedAgentTurnRecord = Extract<
- AgentTurnRecord,
- { outcome: 'accepted' | 'rejected' }
- >;
- const [editing, setEditing] = useState(false);
- const [draft, setDraft] = useState(agent.personality);
- const [editError, setEditError] = useState(null);
-
- const draftPreset = matchingPersonalityPreset(draft);
- const activePreset = matchingPersonalityPreset(agent.personality);
- const alliance = snapshot.world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(agent.id),
- );
- const allianceSummary = snapshot.experiment.currentAlliances.find(
- ({ allianceId }) => allianceId === alliance?.id,
- );
- const agentColor = resolveAgentColor(snapshot, agent.id);
- const pendingProposals = snapshot.world.pendingAllianceProposals.filter(
- ({ proposerAgentId, recipientAgentId }) =>
- proposerAgentId === agent.id || recipientAgentId === agent.id,
- );
- const resolvedModel = snapshot.resolvedModels.find(
- ({ agentId }) => agentId === agent.id,
- );
- const currentGoal = snapshot.agentGoals.find(
- ({ agentId }) => agentId === agent.id,
- )?.goal;
- const currentMemory =
- snapshot.agentMemories.find(({ agentId }) => agentId === agent.id)
- ?.entries ?? [];
- const latestGoalTurn = turns.findLast(
- (turn): turn is CompletedAgentTurnRecord =>
- (turn.outcome === 'accepted' || turn.outcome === 'rejected') &&
- turn.agentId === agent.id &&
- turn.goalRevisionResult.requested,
- );
- const latestMemoryTurn = turns.findLast(
- (turn): turn is CompletedAgentTurnRecord =>
- (turn.outcome === 'accepted' || turn.outcome === 'rejected') &&
- turn.agentId === agent.id &&
- turn.memoryOperationResult.requested,
- );
- const inspectorSectionPrefix = `agent-${agent.id}`;
-
- const apply = async () => {
- const parsed = personalitySchema.safeParse(draft);
- if (!parsed.success) {
- setEditError(
- `Enter a personality between 1 and ${PERSONALITY_MAX_LENGTH} characters.`,
- );
- return;
- }
- setEditError(null);
- if (await onApplyPersonality(agent.id, parsed.data)) setEditing(false);
- };
-
- return (
-
- Agent inspector
-
-
- {agent.name}
- {agent.id === snapshot.scenario.patientZeroAgentId && (
- Patient Zero
- )}
-
-
- Trace
- Goals
- Memories
- History
- Configuration
- Latest
-
-
-
-
Patient Zero role
-
- {agent.id === snapshot.scenario.patientZeroAgentId
- ? 'Designated coordinator (normal world-action rules)'
- : snapshot.scenario.patientZeroAgentId
- ? 'Field agent'
- : 'Disabled'}
-
-
-
-
Stable ID
- {agent.id}
-
-
-
Affiliation color
- {agentColor}
-
-
-
Alliance membership
-
- {alliance
- ? allianceSummary?.members.map(({ name }) => name).join(', ')
- : 'Unaffiliated'}
-
-
-
-
Alliance territory
-
- {allianceSummary?.totalControlledCellCount ?? 0} controlled cells
-
-
-
-
Cell
- {agent.currentCell}
-
-
-
Cell state
- {cellState}
-
-
-
Resolved model
-
- {resolvedModel?.modelId ?? 'Not selected'} ·{' '}
- {resolvedModel?.source ?? 'missing'}
- {!resolvedModel?.available && ' · unavailable'}
-
-
-
-
- Goals
- {currentGoal ? (
-
-
-
Long-term
- {currentGoal.longTermGoal}
-
-
-
Short-term
- {currentGoal.shortTermGoal}
-
-
-
Plan
- {currentGoal.planSummary}
-
-
-
Attribution
-
- Established tick {currentGoal.establishedAtTick}; revised tick{' '}
- {currentGoal.revisedAtTick}
-
-
-
- ) : (
- No active strategic goal.
- )}
-
- {latestGoalTurn && latestGoalTurn.goalRevisionResult.requested
- ? `Latest: ${latestGoalTurn.goalRevisionResult.operation} · ${
- latestGoalTurn.goalRevisionResult.accepted
- ? 'accepted'
- : `rejected (${latestGoalTurn.goalRevisionResult.reason})`
- }`
- : 'No goal operation recorded.'}
-
- {latestGoalTurn?.goalRevision &&
- 'reason' in latestGoalTurn.goalRevision && (
-
- Agent reason: {latestGoalTurn.goalRevision.reason}
-
- )}
- Memories
- {currentMemory.length ? (
-
- {currentMemory.map((entry) => (
-
- {entry.text}
-
-
- {entry.id} · created tick {entry.createdAtTick} · revised tick{' '}
- {entry.revisedAtTick}
-
-
- ))}
-
- ) : (
- No compact memories.
- )}
-
- {latestMemoryTurn && latestMemoryTurn.memoryOperationResult.requested
- ? `Latest: ${latestMemoryTurn.memoryOperationResult.operation} · ${
- latestMemoryTurn.memoryOperationResult.accepted
- ? 'accepted'
- : `rejected (${latestMemoryTurn.memoryOperationResult.reason})`
- }`
- : 'No memory operation recorded.'}
-
- Relevant pending proposals
- {pendingProposals.length ? (
-
- {pendingProposals.map((proposal) => (
-
- {
- snapshot.world.agents.find(
- ({ id }) => id === proposal.proposerAgentId,
- )?.name
- }{' '}
- →{' '}
- {
- snapshot.world.agents.find(
- ({ id }) => id === proposal.recipientAgentId,
- )?.name
- }
- ; expires after{' '}
- {proposal.expirationTick === undefined
- ? `legacy turn ${proposal.expirationTurn}`
- : `tick ${proposal.expirationTick}`}
-
- ))}
-
- ) : (
- No relevant pending proposals.
- )}
- Recent alliance changes
-
-
- Experiment usage
- {metrics?.totalTurns ?? 0} turns
- {metrics?.publicMessagesSent ?? 0} public sent
- {metrics?.directMessagesSent ?? 0} direct sent
- {metrics?.directMessagesReceived ?? 0} direct received
- {controlledCellCount} controlled cells
- {formatCost(metrics?.knownCostCredits ?? 0)} known cost
- {metrics?.tokens.promptTokens ?? 0} prompt tokens
- {metrics?.tokens.completionTokens ?? 0} completion tokens
- {(metrics?.tokens.reasoningTokens ?? 0) > 0 && (
-
- {metrics?.tokens.reasoningTokens} reasoning tokens reported
-
- )}
- {(metrics?.turnsWithUnknownCost ?? 0) > 0 && (
-
- {metrics?.attemptsWithUnknownCost} unknown-cost attempts across{' '}
- {metrics?.turnsWithUnknownCost} turns
-
- )}
- {(metrics?.attemptsWithUnknownTokenUsage ?? 0) > 0 && (
-
- Partial token totals · {metrics?.attemptsWithUnknownTokenUsage}{' '}
- attempts missing token usage
-
- )}
-
-
- Recent territory gains and losses
-
- {controlChanges.length === 0 ? (
-
- No territory gains or losses for this agent yet.
-
- ) : (
-
- {controlChanges.slice(-6).map((change) => {
- const gained = change.controllerAgentId === agent.id;
- const otherId = gained
- ? change.previousControllerAgentId
- : change.controllerAgentId;
- const other = agents.find(({ id }) => id === otherId);
- return (
-
- {gained ? 'Gained' : 'Lost'} {change.cell}{' '}
- {gained ? 'from' : 'to'} {other?.name ?? otherId}
-
- );
- })}
-
- )}
- Direct-message history
- {directMessages.length === 0 ? (
- No direct messages for this agent yet.
- ) : (
-
- {directMessages.slice(-12).map((communication) => {
- const sender = agents.find(
- ({ id }) => id === communication.agentId,
- );
- const recipient = agents.find(
- ({ id }) => id === communication.recipientId,
- );
- const direction =
- communication.agentId === agent.id ? 'Sent' : 'Received';
- const other =
- communication.agentId === agent.id ? recipient : sender;
- const turn = turns.find(
- (turn) =>
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- turn.communicationResult.requested &&
- turn.communicationResult.accepted &&
- turn.communicationResult.event.id === communication.id,
- );
- return (
-
-
- {direction} {' '}
-
- {other?.name ??
- (direction === 'Sent'
- ? communication.recipientId
- : communication.agentId)}
-
-
- {communication.message}
-
- {formatRecordSequence(turn)} ·{' '}
- {formatTimestamp(communication.occurredAt)}
-
-
- );
- })}
-
- )}
-
-
Active personality
- {!editing && (
- {
- setDraft(agent.personality);
- setEditError(null);
- setEditing(true);
- }}
- >
- Edit
-
- )}
-
- {editing ? (
-
-
- Personality preset
- {
- const preset = PERSONALITY_PRESETS.find(
- ({ id }) => id === event.target.value,
- );
- if (preset) {
- setDraft(preset.personality);
- setEditError(null);
- }
- }}
- >
- Custom
- {PERSONALITY_PRESETS.map((preset) => (
-
- {preset.name}
-
- ))}
-
-
-
- Personality directive
-
-
-
- Presets populate the editor; Apply commits the change.
-
-
- {draft.length}/{PERSONALITY_MAX_LENGTH}
-
-
- {editError && (
-
- {editError}
-
- )}
-
- void apply()}
- >
- {mutationPending ? 'Applying…' : 'Apply'}
-
- {
- setEditing(false);
- setDraft(agent.personality);
- setEditError(null);
- }}
- >
- Cancel
-
-
-
- ) : (
-
-
{agent.personality}
-
{activePreset?.name ?? 'Custom'}
-
- )}
- {latestTurn ? (
-
-
Latest turn
-
- {latestTurn.outcome}
-
- {latestTurn.outcome !== 'provider-error' &&
- latestTurn.outcome !== 'lost-tick' &&
- latestTurn.outcome !== 'operator-skipped' ? (
- <>
-
- World action: {' '}
- {formatAction(latestTurn.worldAction)}
- {' · '}
- {latestTurn.worldActionResult.accepted
- ? 'accepted'
- : 'rejected'}
-
-
- Summary: {latestTurn.summary}
-
- {!latestTurn.worldActionResult.accepted && (
-
- World-action rejection: {' '}
- {latestTurn.worldActionResult.reason} ·{' '}
- {latestTurn.worldActionResult.details}
-
- )}
-
- Communication: {' '}
- {!latestTurn.communicationResult.requested
- ? 'none requested'
- : latestTurn.communicationResult.accepted
- ? `${latestTurn.communicationResult.event.channel} accepted`
- : `${latestTurn.communicationResult.attempt.channel} rejected · ${latestTurn.communicationResult.reason}`}
-
-
- Diplomacy: {' '}
- {!latestTurn.diplomacyResult.requested
- ? 'none requested'
- : latestTurn.diplomacyResult.accepted
- ? `${latestTurn.diplomacyResult.intent.type} accepted`
- : `${latestTurn.diplomacyResult.attempt.type} rejected · ${latestTurn.diplomacyResult.reason}`}
-
-
- {latestTurn.provider.provider} · {latestTurn.provider.model} ·{' '}
- {latestTurn.provider.resolvedModel &&
- latestTurn.provider.resolvedModel !== latestTurn.provider.model
- ? `resolved ${latestTurn.provider.resolvedModel} · `
- : ''}
- {latestTurn.provider.latencyMs}ms ·{' '}
- {latestTurn.provider.promptTokens ?? '—'} prompt /{' '}
- {latestTurn.provider.completionTokens ?? '—'} completion
- {latestTurn.provider.reasoningTokens === undefined
- ? ''
- : ` / ${latestTurn.provider.reasoningTokens} reasoning`}
- {latestTurn.provider.costCredits === undefined
- ? ' · cost unavailable'
- : ` · ${formatCost(latestTurn.provider.costCredits)}`}
-
- >
- ) : (
- <>
-
- {latestTurn.failure.code}: {latestTurn.failure.message}
- {latestTurn.failure.providerMessage
- ? ` Provider: ${latestTurn.failure.providerMessage}`
- : ''}
-
-
- Model{' '}
- {latestTurn.failure.model ??
- latestTurn.provider?.model ??
- 'unavailable'}
- {latestTurn.failure.httpStatus
- ? ` · HTTP ${latestTurn.failure.httpStatus}`
- : ''}
- {latestTurn.failure.providerCode
- ? ` · ${latestTurn.failure.providerCode}`
- : ''}
- {latestTurn.failure.requestId
- ? ` · request ${latestTurn.failure.requestId}`
- : ''}
- {latestTurn.failure.finishReason
- ? ` · finish ${latestTurn.failure.finishReason}`
- : ''}
- {latestTurn.failure.nativeFinishReason
- ? ` · native ${latestTurn.failure.nativeFinishReason}`
- : ''}
-
- {latestTurn.provider && (
-
- {latestTurn.provider.provider} · {latestTurn.provider.model} ·{' '}
- {latestTurn.provider.latencyMs}ms
- {latestTurn.provider.requestId
- ? ` · request ${latestTurn.provider.requestId}`
- : ''}
- {latestTurn.provider.finishReason
- ? ` · finish ${latestTurn.provider.finishReason}`
- : ''}
-
- )}
- >
- )}
-
- Latest structured observation
-
- Immutable input supplied for {formatRecordSequence(latestTurn)} ·
- record {latestTurn.turnNumber}. It is not rewritten when the
- active personality changes.
-
- {latestTurn.observation.personality !== agent.personality && (
-
- The active personality has changed since this observation.
-
- )}
-
- Observed personality: {' '}
- {latestTurn.observation.personality}
-
-
- Current: {latestTurn.observation.currentCell.cell} (
- {latestTurn.observation.currentCell.state})
-
-
- Capture:{' '}
- {latestTurn.observation.captureEligibility.eligible
- ? 'eligible'
- : `blocked · ${latestTurn.observation.captureEligibility.blockedReason}`}
-
-
- Adjacent:{' '}
- {latestTurn.observation.adjacentCells
- .map(({ cell, state }) => `${cell} (${state})`)
- .join(', ')}
-
-
- Nearby:{' '}
- {latestTurn.observation.nearbyAgents
- .map(({ name, distance }) => `${name} (${distance})`)
- .join(', ') || 'none'}
-
-
- Recent public events: {latestTurn.observation.recentEvents.length}
-
-
- Recent public messages:{' '}
- {latestTurn.observation.recentPublicMessages.length}
-
-
- {latestTurn.observation.recentPublicMessages.map(
- (communication) => (
-
- {communication.senderName}: {communication.message}
-
- ),
- )}
-
-
- Recent direct messages:{' '}
- {latestTurn.observation.recentDirectMessages.length}
-
-
- {latestTurn.observation.recentDirectMessages.map(
- (communication) => (
-
- {communication.direction}: {communication.senderName} →{' '}
- {communication.recipientName}: {communication.message}
-
- ),
- )}
-
-
-
- ) : (
-
- No completed turn for this agent yet.
-
- )}
- Recent records
-
- {turns
- .filter(({ agentId }) => agentId === agent.id)
- .slice(-5)
- .toReversed()
- .map((turn) => (
-
- {formatRecordSequence(turn)}: {turn.outcome}
-
- ))}
-
-
- );
-}
-
-function ExperimentUsageMeter({ snapshot }: { snapshot: SimulationSnapshot }) {
- const metrics = snapshot.experiment.metrics.aggregate;
- return (
-
- Current experiment
- {snapshot.tickNumber} turns
- {metrics.publicMessagesAccepted} public messages
- {metrics.directMessagesDelivered} direct messages
- {formatCost(metrics.knownCostCredits)} known cost
-
- {metrics.tokens.totalTokens ??
- (metrics.tokens.promptTokens ?? 0) +
- (metrics.tokens.completionTokens ?? 0)}{' '}
- tokens
-
- {metrics.turnsWithUnknownCost > 0 && (
-
- {metrics.attemptsWithUnknownCost} unknown-cost attempts across{' '}
- {metrics.turnsWithUnknownCost} turns
-
- )}
- {metrics.attemptsWithUnknownTokenUsage > 0 && (
-
- Partial token totals · {metrics.attemptsWithUnknownTokenUsage}{' '}
- attempts missing token usage
-
- )}
-
- );
-}
-
-const defaultCustomOptions: CustomExportOptions = {
- turnObservations: true,
- personalityTextHistory: true,
- nearbyAgents: true,
- recentEvents: true,
- recentPublicMessages: true,
- recentDirectMessages: true,
- recentControlChanges: true,
- validationDetails: true,
- resultingEvents: true,
- providerUsageMetadata: true,
- initialWorldState: false,
- currentWorldState: true,
- computedMetrics: true,
- communications: true,
- controlChanges: true,
-};
-
-function ExperimentExportPanel({
- snapshot,
- agents,
- disabled,
- open,
- selectedAgentIds,
- onOpenChange,
- onSelectionChange,
- returnFocusRef,
-}: {
- snapshot: SimulationSnapshot;
- agents: SimulationSnapshot['world']['agents'];
- disabled: boolean;
- open: boolean;
- selectedAgentIds: AgentId[];
- onOpenChange: (open: boolean) => void;
- onSelectionChange: (ids: AgentId[]) => void;
- returnFocusRef: { current: HTMLButtonElement | null };
+ agents: SimulationSnapshot['world']['agents'];
+ disabled: boolean;
+ open: boolean;
+ selectedAgentIds: AgentId[];
+ onOpenChange: (open: boolean) => void;
+ onSelectionChange: (ids: AgentId[]) => void;
+ returnFocusRef: { current: HTMLButtonElement | null };
}) {
const [level, setLevel] =
useState('minimal');
@@ -5193,12 +2910,6 @@ function ExperimentExportPanel({
const [actions, setActions] = useState<
Array<'move' | 'infect' | 'capture' | 'wait'>
>(['move', 'infect', 'capture', 'wait']);
- const [communicationChannel, setCommunicationChannel] = useState<
- 'all' | 'public' | 'direct'
- >('all');
- const [communicationStatus, setCommunicationStatus] = useState<
- 'all' | 'accepted' | 'rejected'
- >('all');
const [custom, setCustom] = useState(defaultCustomOptions);
const [preview, setPreview] = useState(null);
const [document, setDocument] = useState(
@@ -5228,7 +2939,6 @@ function ExperimentExportPanel({
'operator-skipped',
] as const,
actions: ['move', 'infect', 'capture', 'wait'] as const,
- communications: { channel: 'all' as const, status: 'all' as const },
level: 'full-safe' as const,
serialization,
}
@@ -5245,10 +2955,6 @@ function ExperimentExportPanel({
: { mode: 'range' as const, fromTurn, toTurn },
outcomes,
actions,
- communications: {
- channel: communicationChannel,
- status: communicationStatus,
- },
level,
serialization,
...(level === 'custom' ? { custom } : {}),
@@ -5487,7 +3193,7 @@ function ExperimentExportPanel({
Swarm exports include all agents, retained swarm ticks, and provider
attempts. Full-safe exports retain legacy schema fields for archive
compatibility; those fields do not drive swarm execution. Agent, turn,
- communication, and custom filters do not apply.
+ and custom filters do not apply.
) : (
<>
@@ -5634,38 +3340,6 @@ function ExperimentExportPanel({
selected={actions}
onToggle={(value) => setActions(toggle(actions, value))}
/>
-
-
- Communication channel
-
- setCommunicationChannel(
- event.target.value as typeof communicationChannel,
- )
- }
- >
- All
- Public
- Direct
-
-
-
- Communication result
-
- setCommunicationStatus(
- event.target.value as typeof communicationStatus,
- )
- }
- >
- All
- Accepted
- Rejected
-
-
-
{level === 'custom' && (
Advanced Custom switches
@@ -5677,8 +3351,6 @@ function ExperimentExportPanel({
disabled={
(key === 'nearbyAgents' ||
key === 'recentEvents' ||
- key === 'recentPublicMessages' ||
- key === 'recentDirectMessages' ||
key === 'recentControlChanges') &&
!custom.turnObservations
}
@@ -5692,8 +3364,6 @@ function ExperimentExportPanel({
) {
next.nearbyAgents = false;
next.recentEvents = false;
- next.recentPublicMessages = false;
- next.recentDirectMessages = false;
next.recentControlChanges = false;
}
return next;
@@ -5718,7 +3388,7 @@ function ExperimentExportPanel({
{swarmMode
? 'Pause playback and wait for the active swarm tick or reset to finish.'
- : 'Pause playback and wait for all turn, reset, and personality work to finish.'}
+ : 'Pause playback and wait for all turn and reset work to finish.'}
)}
{preview &&
@@ -5747,22 +3417,10 @@ function ExperimentExportPanel({
) : (
-
-
Matching
- {preview.matchingTurnCount} turns
-
-
-
Communications
- {preview.matchingCommunicationCount} matched
-
Control changes
{preview.matchingControlChangeCount} matched
-
-
Diplomacy/alliance events
- {preview.matchingDiplomacyEventCount} matched
-
Size
{preview.serializedUtf8Bytes} bytes
@@ -5843,11 +3501,8 @@ function FilterChecks
({
function customOptionLabel(key: keyof CustomExportOptions): string {
return {
turnObservations: 'Turn observations',
- personalityTextHistory: 'Personality text and history',
nearbyAgents: 'Nearby agents',
recentEvents: 'Recent events',
- recentPublicMessages: 'Recent public messages in observations',
- recentDirectMessages: 'Recent direct messages in observations',
recentControlChanges: 'Recent control changes in observations',
validationDetails: 'Validation details',
resultingEvents: 'Resulting events',
@@ -5855,7 +3510,6 @@ function customOptionLabel(key: keyof CustomExportOptions): string {
initialWorldState: 'Initial world state',
currentWorldState: 'Current world state',
computedMetrics: 'Computed metrics',
- communications: 'Canonical communications',
controlChanges: 'Canonical control changes',
}[key];
}
@@ -5905,180 +3559,3 @@ async function sha256Hex(value: string): Promise {
byte.toString(16).padStart(2, '0'),
).join('');
}
-
-function EventLog({
- snapshot,
- turns,
- agents,
- collapsed,
- onCollapsedChange,
-}: {
- snapshot: SimulationSnapshot;
- turns: AgentTurnRecord[];
- agents: SimulationSnapshot['world']['agents'];
- collapsed: boolean;
- onCollapsedChange: (collapsed: boolean) => void;
-}) {
- return (
-
-
-
-
World events
-
Event log
-
-
onCollapsedChange(!collapsed)}
- >
- {collapsed ? 'Expand' : 'Collapse'}
-
-
- {!collapsed && (
-
- {turns.length === 0 ? (
-
- Initial
- Development world loaded with {agents.length} agents.
-
- ) : (
- turns
- .slice(-20)
- .toReversed()
- .map((turn) => (
-
- {formatRecordSequence(turn)}
- {formatTurn(turn, agents)}
-
- {agents.find(({ id }) => id === turn.agentId)?.name ??
- turn.agentId}
- {' · '}
- {turn.provider?.model ?? 'model unavailable'}
-
-
- ))
- )}
-
- )}
-
- );
-}
-
-function formatAction(
- action: Extract<
- AgentTurnRecord,
- { outcome: 'accepted' | 'rejected' }
- >['worldAction'],
-) {
- if (action.type === 'move') return `move → ${action.targetCell}`;
- return action.type;
-}
-
-function formatTurn(
- turn: AgentTurnRecord,
- agents: SimulationSnapshot['world']['agents'],
-) {
- if (turn.outcome === 'lost-tick')
- return `Lost tick ${turn.tickNumber}: ${turn.failure.code}`;
- if (turn.outcome === 'provider-error')
- return `Provider failure · ${turn.failure.message}`;
- if (turn.outcome === 'operator-skipped')
- return `Operator skipped · ${turn.failure.message}`;
- const communication = !turn.communicationResult.requested
- ? ''
- : turn.communicationResult.accepted
- ? ` + ${turn.communicationResult.event.channel} message accepted`
- : ` + ${turn.communicationResult.attempt.channel} message rejected (${turn.communicationResult.reason})`;
- const diplomacy = !turn.diplomacyResult.requested
- ? ''
- : turn.diplomacyResult.accepted
- ? ` + ${turn.diplomacyResult.intent.type} accepted`
- : ` + ${turn.diplomacyResult.attempt.type} rejected (${turn.diplomacyResult.reason})`;
- if (!turn.worldActionResult.accepted)
- return `Rejected ${formatAction(turn.worldAction)} · ${turn.worldActionResult.reason}${communication}${diplomacy}`;
- const event = turn.worldActionResult.event;
- if (event.type === 'agent-moved')
- return `Movement · ${event.toCell}${communication}${diplomacy}`;
- if (event.type === 'hex-infected')
- return `Infection · ${event.cell}${communication}${diplomacy}`;
- if (event.type === 'hex-captured') {
- const capturer = agents.find(({ id }) => id === event.controllerAgentId);
- const previous = agents.find(
- ({ id }) => id === event.previousControllerAgentId,
- );
- return `${capturer?.name ?? event.controllerAgentId} captured ${event.cell} from ${previous?.name ?? event.previousControllerAgentId}.${communication}${diplomacy}`;
- }
- return `Waited${communication}${diplomacy}`;
-}
-
-type AllianceWorldEvent = Extract<
- SimulationSnapshot['world']['events'][number],
- {
- type:
- | 'alliance-proposed'
- | 'alliance-proposal-closed'
- | 'alliance-formed'
- | 'agent-joined-alliance'
- | 'agent-left-alliance'
- | 'alliance-dissolved';
- }
->;
-
-function allianceEventParticipants(event: AllianceWorldEvent): AgentId[] {
- if (event.type === 'alliance-proposed')
- return [event.agentId, event.recipientAgentId];
- if (event.type === 'alliance-proposal-closed')
- return [event.proposerAgentId, event.recipientAgentId];
- if (event.type === 'alliance-formed') return event.memberAgentIds;
- if (event.type === 'agent-joined-alliance') return event.memberAgentIds;
- if (event.type === 'agent-left-alliance')
- return [event.leftAgentId, ...event.remainingMemberAgentIds];
- return event.formerMemberAgentIds;
-}
-
-function formatAllianceEvent(
- event: AllianceWorldEvent,
- snapshot: SimulationSnapshot,
-): string {
- const name = (id: AgentId) =>
- snapshot.world.agents.find((agent) => agent.id === id)?.name ?? id;
- if (event.type === 'alliance-proposed')
- return `${name(event.agentId)} proposed an alliance with ${name(event.recipientAgentId)}.`;
- if (event.type === 'alliance-formed')
- return `${event.memberAgentIds.map(name).join(' and ')} formed an alliance.`;
- if (event.type === 'agent-joined-alliance')
- return `${name(event.joinedAgentId)} joined the alliance.`;
- if (event.type === 'agent-left-alliance')
- return `${name(event.leftAgentId)} left the alliance.`;
- if (event.type === 'alliance-dissolved') return 'The alliance dissolved.';
- return `The proposal from ${name(event.proposerAgentId)} to ${name(event.recipientAgentId)} was ${event.reason}.`;
-}
-
-function formatTimestamp(timestamp: string): string {
- return new Date(timestamp).toLocaleTimeString([], {
- hour: '2-digit',
- minute: '2-digit',
- second: '2-digit',
- });
-}
-
-function formatRecordSequence(
- turn?: Pick,
-): string {
- if (!turn) return 'Record unavailable';
- return turn.tickNumber === undefined
- ? `Turn ${turn.turnNumber}`
- : `Tick ${turn.tickNumber}`;
-}
diff --git a/apps/world-lab/src/components/world-map.tsx b/apps/world-lab/src/components/world-map.tsx
index 7363420..a001e10 100644
--- a/apps/world-lab/src/components/world-map.tsx
+++ b/apps/world-lab/src/components/world-map.tsx
@@ -19,7 +19,6 @@ import type {
H3Cell,
Hex,
HexState,
- Alliance,
SimulationSnapshot,
SimulatedPlayerState,
} from '@hexzero/shared';
@@ -31,7 +30,6 @@ interface WorldMapProps {
longitude: number;
hexes: Hex[];
agents: AgentProfile[];
- alliances: Alliance[];
patientZeroAgentId: AgentId | null;
simulatedPlayer: SimulatedPlayerState | null;
selectedCell: H3Cell | null;
@@ -66,17 +64,16 @@ const initialOverlayDiagnostics: OverlayDiagnostics = {
function asGeoJson(
hexes: WorldMapProps['hexes'],
agents: AgentProfile[],
- alliances: Alliance[],
selectedCell: H3Cell | null,
) {
const agentById = new globalThis.Map(
agents.map((agent) => [agent.id, agent]),
);
- const colorState = { world: { agents, alliances } } as unknown as Pick<
+ const colorState = { world: { agents } } as unknown as Pick<
SimulationSnapshot,
'world'
>;
- const effectiveColor = (agentId: AgentId) =>
+ const agentColor = (agentId: AgentId) =>
resolveAgentColor(colorState, agentId);
return {
type: 'FeatureCollection' as const,
@@ -89,7 +86,7 @@ function asGeoJson(
hex.state === 'infected'
? hex.controllerAgentId === null
? '#8d8069'
- : (effectiveColor(hex.controllerAgentId) ?? '#e44f45')
+ : (agentColor(hex.controllerAgentId) ?? '#e44f45')
: '#4a8178',
controllerName:
hex.state === 'infected'
@@ -120,7 +117,6 @@ export function WorldMap(props: WorldMapProps) {
longitude,
hexes,
agents,
- alliances,
patientZeroAgentId,
simulatedPlayer,
selectedCell,
@@ -137,7 +133,6 @@ export function WorldMap(props: WorldMapProps) {
const onSelectAgentRef = useRef(onSelectAgent);
const initialHexes = useRef(hexes);
const initialAgents = useRef(agents);
- const initialAlliances = useRef(alliances);
const initialSelectedCell = useRef(selectedCell);
const currentHexesRef = useRef(hexes);
const fittedWorldRef = useRef(hexes.map(({ cell }) => cell).join(','));
@@ -291,7 +286,6 @@ export function WorldMap(props: WorldMapProps) {
data: asGeoJson(
initialHexes.current,
initialAgents.current,
- initialAlliances.current,
initialSelectedCell.current,
),
});
@@ -379,7 +373,7 @@ export function WorldMap(props: WorldMapProps) {
const source = mapRef.current?.getSource(sourceId) as
GeoJSONSource | undefined;
if (!source) return;
- source.setData(asGeoJson(hexes, agents, alliances, selectedCell));
+ source.setData(asGeoJson(hexes, agents, selectedCell));
const signature = hexes.map(({ cell }) => cell).join(',');
if (signature !== fittedWorldRef.current && mapRef.current) {
fittedWorldRef.current = signature;
@@ -394,7 +388,7 @@ export function WorldMap(props: WorldMapProps) {
});
}
scheduleOverlayInspectionRef.current?.();
- }, [agents, alliances, hexes, selectedCell]);
+ }, [agents, hexes, selectedCell]);
useEffect(() => {
const map = mapRef.current;
@@ -425,16 +419,13 @@ export function WorldMap(props: WorldMapProps) {
`Select agent ${agent.name}${agent.id === patientZeroAgentId ? ', Patient Zero' : ''}`,
);
element.title = `${agent.name}${agent.id === patientZeroAgentId ? ' · Patient Zero' : ''} · ${agent.currentCell}`;
- const effectiveColor = resolveAgentColor(
- { world: { agents, alliances } } as unknown as Pick<
- SimulationSnapshot,
- 'world'
- >,
+ const agentColor = resolveAgentColor(
+ { world: { agents } } as unknown as Pick,
agent.id,
);
- element.style.setProperty('--agent-color', effectiveColor);
+ element.style.setProperty('--agent-color', agentColor);
element.dataset.baseColor = agent.color;
- element.dataset.effectiveColor = effectiveColor;
+ element.dataset.agentColor = agentColor;
element.textContent = agent.name.slice(0, 1);
element.addEventListener('click', (event) => {
event.stopPropagation();
@@ -467,14 +458,7 @@ export function WorldMap(props: WorldMapProps) {
.addTo(map),
);
}
- }, [
- agents,
- alliances,
- mapReady,
- patientZeroAgentId,
- selectedAgentId,
- simulatedPlayer,
- ]);
+ }, [agents, mapReady, patientZeroAgentId, selectedAgentId, simulatedPlayer]);
const overlayReady = overlayDiagnostics.status === 'ready';
const overlayLabel = overlayReady
@@ -496,14 +480,14 @@ export function WorldMap(props: WorldMapProps) {
.flatMap((hex) => {
if (hex.state === 'open' || hex.controllerAgentId === null)
return [];
- const effectiveColor = resolveAgentColor(
- { world: { agents, alliances } } as unknown as Pick<
+ const agentColor = resolveAgentColor(
+ { world: { agents } } as unknown as Pick<
SimulationSnapshot,
'world'
>,
hex.controllerAgentId,
);
- return [`${hex.cell}:${effectiveColor ?? 'unknown'}`];
+ return [`${hex.cell}:${agentColor ?? 'unknown'}`];
})
.join(',')}
data-testid="world-map"
diff --git a/packages/experiment-archive/src/archive.test.ts b/packages/experiment-archive/src/archive.test.ts
index 020e5ec..fc1b223 100644
--- a/packages/experiment-archive/src/archive.test.ts
+++ b/packages/experiment-archive/src/archive.test.ts
@@ -28,7 +28,6 @@ async function currentExport(): Promise {
turns: { mode: 'entire-retained' },
outcomes: ['accepted', 'rejected', 'provider-error', 'operator-skipped'],
actions: ['move', 'infect', 'capture', 'wait'],
- communications: { channel: 'all', status: 'all' },
level: 'full-safe',
serialization: 'compact',
});
diff --git a/packages/experiment-archive/src/cli.ts b/packages/experiment-archive/src/cli.ts
index 9151295..15606d9 100644
--- a/packages/experiment-archive/src/cli.ts
+++ b/packages/experiment-archive/src/cli.ts
@@ -57,11 +57,7 @@ function detailFilters(args: Arguments): DetailFilters {
agent: flag(args, 'agent'),
fromTurn: integerFlag(args, 'from-turn'),
toTurn: integerFlag(args, 'to-turn'),
- action: flag(args, 'action'),
outcome: flag(args, 'outcome'),
- channel: flag(args, 'channel'),
- sender: flag(args, 'sender'),
- recipient: flag(args, 'recipient'),
reason: flag(args, 'reason'),
limit: integerFlag(args, 'limit'),
};
@@ -186,11 +182,7 @@ const HELP = `Usage:
pnpm experiment:db list [--limit n] [--format ...]
pnpm experiment:db summary [--format ...]
pnpm experiment:db compare [--format ...]
- pnpm experiment:db turns [--agent id] [--from-turn n] [--to-turn n] [--action action] [--outcome outcome] [--limit n]
- pnpm experiment:db communications [--channel channel] [--sender id] [--recipient id] [--reason reason] [--limit n]
- pnpm experiment:db alliance-events [--agent id] [--from-turn n] [--to-turn n] [--reason reason] [--limit n]
- pnpm experiment:db patient-zero [--agent id] [--from-turn n] [--to-turn n] [--limit n]
- pnpm experiment:db failures [--agent id] [--reason code] [--limit n]
+ pnpm experiment:db failures [--agent id] [--from-turn n] [--to-turn n] [--reason code] [--limit n]
pnpm experiment:db provider-attempts [--agent id] [--from-turn n] [--to-turn n] [--outcome value] [--limit n]
pnpm experiment:db notes import --type --status [--tag tag] [--experiment id] [--provenance text] [--supersedes note-id]
pnpm experiment:db notes search [--type type] [--status status] [--tag tag] [--experiment id] [--limit n]
@@ -226,26 +218,6 @@ async function main(): Promise {
requirePositional(args, 1, 'first experiment ID'),
requirePositional(args, 2, 'second experiment ID'),
);
- } else if (command === 'turns') {
- result = queries.turns(
- requirePositional(args, 1, 'experiment ID'),
- detailFilters(args),
- );
- } else if (command === 'communications') {
- result = queries.communications(
- requirePositional(args, 1, 'experiment ID'),
- detailFilters(args),
- );
- } else if (command === 'alliance-events') {
- result = queries.allianceEvents(
- requirePositional(args, 1, 'experiment ID'),
- detailFilters(args),
- );
- } else if (command === 'patient-zero') {
- result = queries.patientZero(
- requirePositional(args, 1, 'experiment ID'),
- detailFilters(args),
- );
} else if (command === 'failures') {
result = queries.failures(
requirePositional(args, 1, 'experiment ID'),
diff --git a/packages/experiment-archive/src/importer.ts b/packages/experiment-archive/src/importer.ts
index 1b1e431..51ffcb1 100644
--- a/packages/experiment-archive/src/importer.ts
+++ b/packages/experiment-archive/src/importer.ts
@@ -4,7 +4,6 @@ import { resolve } from 'node:path';
import {
experimentExportDocumentSchema,
- exportedCommunicationSchema,
type ExperimentExportDocument,
} from '@hexzero/shared';
@@ -111,26 +110,6 @@ function runInsert(
else report.existing += 1;
}
-function sourceMetricInconsistencies(document: ExperimentExportDocument) {
- const metrics = document.metrics;
- if (!metrics) return [];
- const perAgent = metrics.byAgent.reduce(
- (sum, entry) => sum + entry.metrics.directionChangesAfterCommunication,
- 0,
- );
- return metrics.aggregate.directionChangesAfterCommunication === perAgent
- ? []
- : [
- {
- metric: 'directionChangesAfterCommunication',
- aggregate: metrics.aggregate.directionChangesAfterCommunication,
- perAgentSum: perAgent,
- canonical:
- 'sum of per-agent chronological direction changes after an observed inbound direct or alliance communication since that agent previous move',
- },
- ];
-}
-
export function importExperimentExport(
archive: ArchiveDatabase,
input: string | ExperimentExportDocument,
@@ -163,14 +142,14 @@ export function importExperimentExport(
const experimentInsert = db.prepare(`
INSERT OR IGNORE INTO experiments(
id, schema_version, started_at, provider_mode, imported_at,
- scenario_json, model_configuration_json, behavior_configuration_json,
+ scenario_json, model_configuration_json,
objective_version, decision_contract_version, observation_contract_version,
retention_limit, total_completed_turns, retained_turns,
first_retained_turn, last_retained_turn, dropped_records,
retention_complete, requested_range_extends_beyond_retention,
- source_metrics_json, source_territory_json, source_alliances_json,
+ source_metrics_json, source_territory_json,
metric_inconsistencies_json, attempt_retention_json, attempt_accounting_json
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
runInsert(
experimentInsert,
@@ -182,7 +161,6 @@ export function importExperimentExport(
archive.clock().toISOString(),
json(scenario),
json(document.experiment.modelConfiguration),
- json(document.experiment.behaviorConfiguration),
scenario?.objectiveVersion ?? null,
document.experiment.swarmPlannerContractVersion,
`experiment-export-schema-v${document.schemaVersion}`,
@@ -196,8 +174,7 @@ export function importExperimentExport(
Number(document.retention.requestedRangeExtendsBeyondRetention),
json(document.metrics),
json(document.currentTerritory),
- json(document.currentAlliances),
- json(sourceMetricInconsistencies(document))!,
+ json([])!,
json(document.attemptRetention),
json(document.attemptAccounting),
],
@@ -215,11 +192,9 @@ export function importExperimentExport(
END,
scenario_json = COALESCE(scenario_json, ?),
model_configuration_json = COALESCE(model_configuration_json, ?),
- behavior_configuration_json = COALESCE(behavior_configuration_json, ?),
objective_version = COALESCE(objective_version, ?),
source_metrics_json = COALESCE(?, source_metrics_json),
source_territory_json = COALESCE(?, source_territory_json),
- source_alliances_json = COALESCE(?, source_alliances_json),
simulated_player_metrics_json = COALESCE(?, simulated_player_metrics_json),
attempt_retention_json = CASE
WHEN ? IS NULL THEN attempt_retention_json
@@ -249,11 +224,9 @@ export function importExperimentExport(
`experiment-export-schema-v${document.schemaVersion}`,
json(scenario),
json(document.experiment.modelConfiguration),
- json(document.experiment.behaviorConfiguration),
scenario?.objectiveVersion ?? null,
json(document.metrics),
json(document.currentTerritory),
- json(document.currentAlliances),
json(document.simulatedPlayerMetrics),
json(document.attemptRetention),
json(document.attemptRetention),
@@ -298,11 +271,8 @@ export function importExperimentExport(
importAgents(archive, document, report);
importMap(archive, document, report);
- importTurns(archive, document, report);
importSwarmTicks(archive, document, report);
importProviderAttempts(archive, document, report);
- importCommunications(archive, document, report);
- importAllianceEvents(archive, document, report);
importWorldEvents(archive, document, report);
importSimulatedPlayerActivity(archive, document, report);
importConfigurationEvents(archive, document, report);
@@ -351,16 +321,9 @@ function importAgents(
const statement = archive.database.prepare(`
INSERT OR IGNORE INTO agents(
experiment_id, agent_id, name, color, model_id, reasoning_profile,
- personality_id, strategy_id, personality, is_patient_zero,
- initial_agent_json, current_agent_json
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ is_patient_zero, initial_agent_json, current_agent_json
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
- const behavior = new Map(
- document.experiment.behaviorConfiguration?.assignments.map((entry) => [
- entry.agentId,
- entry,
- ]),
- );
const models = document.experiment.modelConfiguration;
const initial = new Map(
document.experiment.initialAgents?.map((agent) => [agent.id, agent]),
@@ -374,7 +337,6 @@ function importAgents(
id: ExperimentExportDocument['agents'][number]['id'];
name: string;
color: string;
- personality?: string;
currentCell?: string;
}
>();
@@ -387,7 +349,6 @@ function importAgents(
for (const agent of document.agents)
profiles.set(agent.id, { ...(profiles.get(agent.id) ?? {}), ...agent });
for (const agent of profiles.values()) {
- const assignment = behavior.get(agent.id);
const override = models?.overrides.find(
({ agentId }) => agentId === agent.id,
);
@@ -400,9 +361,6 @@ function importAgents(
agent.color,
override?.modelId ?? models?.globalModelId ?? null,
override?.reasoningProfile ?? models?.globalReasoningProfile ?? null,
- assignment?.personalityId ?? null,
- assignment?.strategyId ?? null,
- agent.personality ?? initial.get(agent.id)?.personality ?? null,
Number(document.experiment.scenario?.patientZeroAgentId === agent.id),
json(initial.get(agent.id)),
json(current.get(agent.id)),
@@ -455,164 +413,6 @@ function importMap(
});
}
-function importTurns(
- archive: ArchiveDatabase,
- document: ExperimentExportDocument,
- report: ImportReport,
-): void {
- const movement = movementAttribution(document);
- const turnStatement = archive.database.prepare(`
- INSERT OR IGNORE INTO turns(
- id, experiment_id, turn_number, tick_number, tick_position,
- virtual_time, tick_interval_minutes, agent_id, started_at, completed_at,
- outcome, action, action_accepted, action_reason, summary,
- world_action_summary, communication_summary, diplomacy_summary,
- position_before, position_after, move_direction,
- inbound_communication_since_previous_move, model_id, reasoning_profile,
- personality_id, strategy_id, latency_ms, prompt_tokens,
- completion_tokens, total_tokens, reasoning_tokens, cached_read_tokens,
- cache_write_tokens, cost_credits, observation_size_bytes,
- observation_json, world_action_json, world_action_result_json,
- communication_result_json, diplomacy_result_json, failure_json
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
- const attemptStatement = archive.database.prepare(`
- INSERT OR IGNORE INTO model_attempts(
- id, turn_id, experiment_id, turn_number, agent_id, attempt_number,
- kind, started_at, completed_at, model_id, reasoning_profile, accepted,
- failure_code, failure_message, validation_codes_json, latency_ms,
- prompt_tokens, completion_tokens, total_tokens, reasoning_tokens,
- cached_read_tokens, cache_write_tokens, cost_credits
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
- const diplomacyStatement = archive.database.prepare(`
- INSERT OR IGNORE INTO diplomacy_attempts(
- id, experiment_id, turn_number, agent_id, type, recipient_agent_id,
- proposal_id, accepted, rejection_reason, rejection_details, source_json
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
-
- for (const turn of document.turns) {
- const turnId = `${document.experiment.id}:turn:${turn.turnNumber}`;
- const result = turn.worldActionResult;
- const positionBefore = turn.observation?.currentCell?.cell ?? null;
- const positionAfter =
- result?.accepted && result.event.type === 'agent-moved'
- ? result.event.toCell
- : positionBefore;
- const provider = turn.provider;
- const lastAttempt = turn.modelAttempts.at(-1);
- const movementEntry = movement.get(turn.turnNumber);
- runInsert(
- turnStatement,
- [
- turnId,
- document.experiment.id,
- turn.turnNumber,
- turn.tickNumber ?? null,
- turn.tickPosition ?? null,
- turn.virtualTime ?? null,
- turn.tickIntervalMinutes ?? null,
- turn.agentId,
- turn.startedAt,
- turn.completedAt,
- turn.outcome,
- turn.worldAction?.type ?? null,
- result ? Number(result.accepted) : null,
- result && !result.accepted ? result.reason : null,
- turn.summary ?? null,
- turn.worldActionSummary ?? null,
- turn.communicationSummary ?? null,
- turn.diplomacySummary ?? null,
- positionBefore,
- positionAfter,
- movementEntry?.direction ?? null,
- movementEntry === undefined
- ? null
- : Number(movementEntry.inboundSincePreviousMove),
- provider?.model ?? lastAttempt?.modelId ?? null,
- lastAttempt?.reasoningProfile ?? null,
- turn.behavior?.personalityId ?? null,
- turn.behavior?.strategyId ?? null,
- provider?.latencyMs ?? null,
- provider?.promptTokens ?? null,
- provider?.completionTokens ?? null,
- provider?.totalTokens ?? null,
- provider?.reasoningTokens ?? null,
- provider?.cachedReadTokens ?? null,
- provider?.cacheWriteTokens ?? null,
- provider?.costCredits ?? null,
- turn.observation
- ? Buffer.byteLength(JSON.stringify(turn.observation))
- : null,
- json(turn.observation),
- json(turn.worldAction),
- json(turn.worldActionResult),
- json(turn.communicationResult),
- json(turn.diplomacyResult),
- json(turn.failure),
- ],
- report,
- );
- if (!turn.observation) report.skipped += 1;
-
- for (const attempt of turn.modelAttempts) {
- const attemptProvider = attempt.provider;
- runInsert(
- attemptStatement,
- [
- `${turnId}:attempt:${attempt.attemptNumber}`,
- turnId,
- document.experiment.id,
- turn.turnNumber,
- turn.agentId,
- attempt.attemptNumber,
- attempt.kind,
- attempt.startedAt,
- attempt.completedAt,
- attempt.modelId,
- attempt.reasoningProfile,
- Number(!attempt.failure),
- attempt.failure?.code ?? null,
- attempt.failure?.message ?? null,
- json(attempt.failure?.validationCodes),
- attemptProvider?.latencyMs ?? attempt.failure?.latencyMs ?? null,
- attemptProvider?.promptTokens ?? null,
- attemptProvider?.completionTokens ?? null,
- attemptProvider?.totalTokens ?? null,
- attemptProvider?.reasoningTokens ?? null,
- attemptProvider?.cachedReadTokens ?? null,
- attemptProvider?.cacheWriteTokens ?? null,
- attemptProvider?.costCredits ?? null,
- ],
- report,
- );
- }
-
- const diplomacy = turn.diplomacyResult;
- if (diplomacy?.requested) {
- const value = diplomacy.accepted ? diplomacy.intent : diplomacy.attempt;
- runInsert(
- diplomacyStatement,
- [
- `${turnId}:diplomacy`,
- document.experiment.id,
- turn.turnNumber,
- turn.agentId,
- value.type,
- 'recipientId' in value ? (value.recipientId ?? null) : null,
- 'proposalId' in value ? (value.proposalId ?? null) : null,
- Number(diplomacy.accepted),
- diplomacy.accepted ? null : diplomacy.reason,
- diplomacy.accepted ? null : diplomacy.details,
- json(diplomacy)!,
- ],
- report,
- );
- }
- }
-}
-
function importProviderAttempts(
archive: ArchiveDatabase,
document: ExperimentExportDocument,
@@ -683,162 +483,12 @@ function importProviderAttempts(
}
}
-function movementAttribution(document: ExperimentExportDocument) {
- const result = new Map<
- number,
- { direction: string; inboundSincePreviousMove: boolean }
- >();
- for (const actingAgentId of [
- ...new Set(document.turns.map(({ agentId }) => agentId)),
- ]) {
- let previousMoveAt: string | undefined;
- for (const turn of document.turns
- .filter(({ agentId }) => agentId === actingAgentId)
- .toSorted((left, right) => left.turnNumber - right.turnNumber)) {
- if (
- turn.outcome !== 'accepted' ||
- turn.worldActionResult?.accepted !== true ||
- turn.worldActionResult.event.type !== 'agent-moved'
- )
- continue;
- const destination = turn.worldActionResult.event.toCell;
- const option = turn.observation?.actionAvailability?.moveOptions.find(
- ({ targetCell }) => targetCell === destination,
- );
- if (!option) continue;
- const messages = [
- ...(turn.observation?.recentDirectMessages ?? []),
- ...(turn.observation?.recentAllianceMessages ?? []),
- ];
- const inboundSincePreviousMove =
- previousMoveAt !== undefined &&
- messages.some((message) => {
- const inbound =
- ('direction' in message && message.direction === 'inbound') ||
- ('senderId' in message && message.senderId !== turn.agentId);
- return (
- inbound &&
- message.occurredAt > previousMoveAt! &&
- message.occurredAt <= turn.completedAt
- );
- });
- result.set(turn.turnNumber, {
- direction: option.direction,
- inboundSincePreviousMove,
- });
- previousMoveAt = turn.completedAt;
- }
- }
- return result;
-}
-
-function derivedCommunications(document: ExperimentExportDocument) {
- const byId = new Map(
- (document.communications ?? []).map((entry) => [entry.id, entry]),
- );
- for (const turn of document.turns) {
- const result = turn.communicationResult;
- if (!result?.requested) continue;
- const source = result.accepted ? result.event : result.attempt;
- if (byId.has(source.id)) continue;
- byId.set(
- source.id,
- exportedCommunicationSchema.parse({
- id: source.id,
- agentId: source.agentId,
- channel: source.channel,
- ...('recipientId' in source ? { recipientId: source.recipientId } : {}),
- ...('recipientIds' in source
- ? { recipientIds: source.recipientIds }
- : {}),
- message: source.message,
- ...('distance' in source ? { distance: source.distance } : {}),
- occurredAt: source.occurredAt,
- originatingTurn: turn.turnNumber,
- status: result.accepted ? 'accepted' : 'rejected',
- ...(!result.accepted
- ? { rejectionReason: result.reason, rejectionDetails: result.details }
- : {}),
- }),
- );
- }
- return [...byId.values()];
-}
-
-function importCommunications(
- archive: ArchiveDatabase,
- document: ExperimentExportDocument,
- report: ImportReport,
-): void {
- const statement = archive.database.prepare(`
- INSERT OR IGNORE INTO communications(
- id, experiment_id, turn_number, sender_agent_id, channel,
- recipient_agent_id, message, distance, occurred_at, status,
- rejection_reason, rejection_details, source_json
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `);
- const recipientStatement = archive.database.prepare(
- 'INSERT OR IGNORE INTO communication_recipients(communication_id, recipient_agent_id) VALUES (?, ?)',
- );
- for (const communication of derivedCommunications(document)) {
- runInsert(
- statement,
- [
- communication.id,
- document.experiment.id,
- communication.originatingTurn,
- communication.agentId,
- communication.channel,
- communication.recipientId ?? null,
- communication.message,
- communication.distance ?? null,
- communication.occurredAt,
- communication.status,
- communication.rejectionReason ?? null,
- communication.rejectionDetails ?? null,
- json(communication)!,
- ],
- report,
- );
- for (const recipientId of communication.recipientIds ?? [])
- runInsert(recipientStatement, [communication.id, recipientId], report);
- }
-}
-
-function importAllianceEvents(
- archive: ArchiveDatabase,
- document: ExperimentExportDocument,
- report: ImportReport,
-): void {
- const statement = archive.database.prepare(`
- INSERT OR IGNORE INTO alliance_events(
- id, experiment_id, turn_number, occurred_at, agent_id, type, reason, source_json
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- `);
- const events = new Map(
- (document.allianceEvents ?? []).map((event) => [event.id, event]),
- );
- for (const turn of document.turns)
- if (turn.diplomacyResult?.requested && turn.diplomacyResult.accepted)
- for (const event of turn.diplomacyResult.events)
- events.set(event.id, event);
- for (const event of events.values())
- runInsert(
- statement,
- [
- event.id,
- document.experiment.id,
- event.turnNumber,
- event.occurredAt,
- event.agentId,
- event.type,
- 'reason' in event ? event.reason : null,
- json(event)!,
- ],
- report,
- );
-}
-
+/**
+ * Agent-caused world events (movement, infection, capture, waiting) are
+ * attributed to the swarm tick that produced them. Zero's own action and
+ * each worker's action can both yield an event; control changes carry their
+ * own originating-tick number and are folded in for good measure.
+ */
function importWorldEvents(
archive: ArchiveDatabase,
document: ExperimentExportDocument,
@@ -846,7 +496,7 @@ function importWorldEvents(
): void {
const statement = archive.database.prepare(`
INSERT OR IGNORE INTO world_events(
- id, experiment_id, turn_number, occurred_at, agent_id, type,
+ id, experiment_id, tick_number, occurred_at, agent_id, type,
cell_id, previous_controller_agent_id, source_json
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
@@ -857,35 +507,34 @@ function importWorldEvents(
NonNullable[number],
{ agentId: unknown }
>;
- turn: number;
+ tick: number;
}
>();
- for (const event of document.worldEvents ?? []) {
- if (!('agentId' in event)) continue;
- const originatingTurn = document.turns.find(
- (candidate) =>
- candidate.worldActionResult?.accepted &&
- candidate.worldActionResult.event.id === event.id,
- )?.turnNumber;
- if (originatingTurn) events.set(event.id, { event, turn: originatingTurn });
- }
- for (const event of document.controlChanges ?? [])
- events.set(event.id, { event, turn: event.originatingTurn });
- for (const turn of document.turns) {
- const result = turn.worldActionResult;
- if (result?.accepted)
- events.set(result.event.id, {
- event: result.event,
- turn: turn.turnNumber,
+ for (const tick of document.swarmTicks ?? []) {
+ const zeroEvent = tick.zeroActionResult;
+ if (zeroEvent?.accepted && 'agentId' in zeroEvent.event)
+ events.set(zeroEvent.event.id, {
+ event: zeroEvent.event,
+ tick: tick.tickNumber,
});
+ for (const worker of tick.workers) {
+ const result = worker.actionResult;
+ if (result?.accepted && 'agentId' in result.event)
+ events.set(result.event.id, {
+ event: result.event,
+ tick: tick.tickNumber,
+ });
+ }
}
- for (const { event, turn } of events.values())
+ for (const change of document.controlChanges ?? [])
+ events.set(change.id, { event: change, tick: change.originatingTurn });
+ for (const { event, tick } of events.values())
runInsert(
statement,
[
event.id,
document.experiment.id,
- turn,
+ tick,
event.occurredAt,
event.agentId,
event.type,
@@ -947,13 +596,11 @@ function importConfigurationEvents(
) VALUES (?, ?, ?, ?, ?, ?, ?)
`);
for (const event of document.configurationEvents ?? []) {
- const type =
- 'type' in event ? event.type : `personality-${event.operation}`;
const id = stableId(
document.experiment.id,
- type,
+ event.type,
event.timestamp,
- 'agentId' in event ? event.agentId : '',
+ event.agentId ?? '',
json(event),
);
runInsert(
@@ -962,9 +609,9 @@ function importConfigurationEvents(
id,
document.experiment.id,
event.timestamp,
- type,
- 'agentId' in event ? (event.agentId ?? null) : null,
- 'effectiveTurn' in event ? event.effectiveTurn : null,
+ event.type,
+ event.agentId ?? null,
+ event.effectiveTurn,
json(event)!,
],
report,
diff --git a/packages/experiment-archive/src/migrations.ts b/packages/experiment-archive/src/migrations.ts
index bbefe8f..3a83203 100644
--- a/packages/experiment-archive/src/migrations.ts
+++ b/packages/experiment-archive/src/migrations.ts
@@ -354,4 +354,43 @@ export const migrations: readonly Migration[] = [
ON swarm_ticks(experiment_id, virtual_time, tick_number);
`,
},
+ {
+ version: 6,
+ description:
+ 'remove legacy per-agent-LLM social systems (turn records, communications, diplomacy, alliances, personalities)',
+ sql: `
+ DROP INDEX IF EXISTS communications_experiment_channel_idx;
+ DROP INDEX IF EXISTS communications_sender_idx;
+ DROP INDEX IF EXISTS communications_recipient_idx;
+ DROP TABLE communication_recipients;
+ DROP TABLE communications;
+
+ DROP INDEX IF EXISTS diplomacy_attempts_experiment_idx;
+ DROP INDEX IF EXISTS diplomacy_attempts_rejection_idx;
+ DROP TABLE diplomacy_attempts;
+
+ DROP INDEX IF EXISTS alliance_events_experiment_type_idx;
+ DROP INDEX IF EXISTS alliance_events_reason_idx;
+ DROP TABLE alliance_events;
+
+ DROP INDEX IF EXISTS turns_experiment_agent_idx;
+ DROP INDEX IF EXISTS turns_experiment_outcome_idx;
+ DROP INDEX IF EXISTS turns_experiment_action_idx;
+ DROP INDEX IF EXISTS turns_experiment_tick_idx;
+ DROP INDEX IF EXISTS model_attempts_experiment_failure_idx;
+ DROP TABLE model_attempts;
+ DROP TABLE turns;
+
+ DROP INDEX IF EXISTS world_events_experiment_type_idx;
+ ALTER TABLE world_events RENAME COLUMN turn_number TO tick_number;
+ CREATE INDEX world_events_experiment_type_idx ON world_events(experiment_id, type, tick_number, id);
+
+ ALTER TABLE experiments DROP COLUMN behavior_configuration_json;
+ ALTER TABLE experiments DROP COLUMN source_alliances_json;
+
+ ALTER TABLE agents DROP COLUMN personality_id;
+ ALTER TABLE agents DROP COLUMN strategy_id;
+ ALTER TABLE agents DROP COLUMN personality;
+ `,
+ },
] as const;
diff --git a/packages/experiment-archive/src/query-service.ts b/packages/experiment-archive/src/query-service.ts
index 201a2a3..2815943 100644
--- a/packages/experiment-archive/src/query-service.ts
+++ b/packages/experiment-archive/src/query-service.ts
@@ -9,11 +9,7 @@ export interface DetailFilters {
agent?: string;
fromTurn?: number;
toTurn?: number;
- action?: string;
outcome?: string;
- channel?: string;
- sender?: string;
- recipient?: string;
reason?: string;
limit?: number;
}
@@ -24,11 +20,6 @@ export interface QueryPage {
truncated: boolean;
}
-interface SqlFilter {
- clauses: string[];
- values: Array;
-}
-
function boundedLimit(limit?: number): number {
if (limit === undefined) return DEFAULT_DETAIL_LIMIT;
if (!Number.isInteger(limit) || limit < 1)
@@ -40,27 +31,6 @@ function page(rows: T[], limit: number): QueryPage {
return { rows: rows.slice(0, limit), limit, truncated: rows.length > limit };
}
-function turnFilter(filters: DetailFilters, alias = ''): SqlFilter {
- const prefix = alias ? `${alias}.` : '';
- const clauses: string[] = [];
- const values: Array = [];
- const add = (
- column: string,
- value: string | number | undefined,
- operator = '=',
- ) => {
- if (value === undefined) return;
- clauses.push(`${prefix}${column} ${operator} ?`);
- values.push(value);
- };
- add('agent_id', filters.agent);
- add('turn_number', filters.fromTurn, '>=');
- add('turn_number', filters.toTurn, '<=');
- add('action', filters.action);
- add('outcome', filters.outcome);
- return { clauses, values };
-}
-
function asObject(value: unknown): Record | null {
return typeof value === 'object' && value !== null
? (value as Record)
@@ -110,14 +80,13 @@ export class ExperimentQueryService {
.prepare(
`
SELECT e.id, e.started_at AS startedAt, e.provider_mode AS providerMode,
- e.total_completed_turns AS totalCompletedTurns,
- COUNT(DISTINCT t.id) AS archivedTurns,
+ COUNT(DISTINCT st.tick_number) AS archivedTicks,
COUNT(DISTINCT a.agent_id) AS rosterSize,
e.retention_complete AS retentionComplete,
e.dropped_records AS droppedRecords,
e.imported_at AS importedAt
FROM experiments e
- LEFT JOIN turns t ON t.experiment_id = e.id
+ LEFT JOIN swarm_ticks st ON st.experiment_id = e.id
LEFT JOIN agents a ON a.experiment_id = e.id
GROUP BY e.id
ORDER BY e.started_at DESC, e.id ASC
@@ -128,166 +97,44 @@ export class ExperimentQueryService {
return page(rows, limit);
}
- turns(
- experimentId: string,
- filters: DetailFilters = {},
- ): QueryPage> {
- const limit = boundedLimit(filters.limit);
- const built = turnFilter(filters);
- const rows = this.#db
- .prepare(
- `
- SELECT turn_number AS turn, tick_number AS tick,
- tick_position AS tickPosition, virtual_time AS virtualTime,
- tick_interval_minutes AS tickIntervalMinutes,
- agent_id AS agent, outcome, action,
- action_accepted AS actionAccepted, action_reason AS reason,
- position_before AS positionBefore, position_after AS positionAfter,
- model_id AS model, reasoning_profile AS reasoning,
- latency_ms AS latencyMs, total_tokens AS totalTokens,
- cost_credits AS costCredits, summary
- FROM turns
- WHERE experiment_id = ?
- ${built.clauses.map((clause) => `AND ${clause}`).join('\n')}
- ORDER BY turn_number ASC, id ASC
- LIMIT ?
- `,
- )
- .all(experimentId, ...built.values, limit + 1) as Array<
- Record
- >;
- return page(rows, limit);
- }
-
- communications(
- experimentId: string,
- filters: DetailFilters = {},
- ): QueryPage> {
- const limit = boundedLimit(filters.limit);
- const clauses: string[] = [];
- const values: Array = [];
- const add = (
- column: string,
- value: string | number | undefined,
- operator = '=',
- ) => {
- if (value === undefined) return;
- clauses.push(`${column} ${operator} ?`);
- values.push(value);
- };
- add('turn_number', filters.fromTurn, '>=');
- add('turn_number', filters.toTurn, '<=');
- add('channel', filters.channel);
- add('sender_agent_id', filters.sender ?? filters.agent);
- if (filters.recipient !== undefined) {
- clauses.push(`(
- recipient_agent_id = ? OR EXISTS (
- SELECT 1 FROM communication_recipients cr
- WHERE cr.communication_id = communications.id AND cr.recipient_agent_id = ?
- )
- )`);
- values.push(filters.recipient, filters.recipient);
- }
- add('rejection_reason', filters.reason);
- add('status', filters.outcome);
- const rows = this.#db
- .prepare(
- `
- SELECT id, turn_number AS turn, occurred_at AS occurredAt,
- sender_agent_id AS sender, channel,
- recipient_agent_id AS recipient, status,
- rejection_reason AS reason, message
- FROM communications
- WHERE experiment_id = ?
- ${clauses.map((clause) => `AND ${clause}`).join('\n')}
- ORDER BY turn_number ASC, occurred_at ASC, id ASC
- LIMIT ?
- `,
- )
- .all(experimentId, ...values, limit + 1) as Array<
- Record
- >;
- return page(rows, limit);
- }
-
- allianceEvents(
+ failures(
experimentId: string,
filters: DetailFilters = {},
): QueryPage> {
const limit = boundedLimit(filters.limit);
const clauses: string[] = [];
const values: Array = [];
- const add = (
- column: string,
- value: string | number | undefined,
- operator = '=',
- ) => {
- if (value === undefined) return;
- clauses.push(`${column} ${operator} ?`);
- values.push(value);
- };
- add('agent_id', filters.agent);
- add('turn_number', filters.fromTurn, '>=');
- add('turn_number', filters.toTurn, '<=');
- add('type', filters.action ?? filters.outcome);
- add('reason', filters.reason);
- const rows = this.#db
- .prepare(
- `
- SELECT id, turn_number AS turn, occurred_at AS occurredAt,
- agent_id AS agent, type, reason, source_json AS source
- FROM alliance_events
- WHERE experiment_id = ?
- ${clauses.map((clause) => `AND ${clause}`).join('\n')}
- ORDER BY turn_number ASC, occurred_at ASC, id ASC
- LIMIT ?
- `,
- )
- .all(experimentId, ...values, limit + 1) as Array<
- Record
- >;
- for (const row of rows) row.source = parseJson(row.source, null);
- return page(rows, limit);
- }
-
- failures(
- experimentId: string,
- filters: DetailFilters = {},
- ): QueryPage> {
- const limit = boundedLimit(filters.limit);
- const built: SqlFilter = { clauses: [], values: [] };
if (filters.agent !== undefined) {
- built.clauses.push('m.agent_id = ?');
- built.values.push(filters.agent);
+ clauses.push('agent_id = ?');
+ values.push(filters.agent);
}
if (filters.fromTurn !== undefined) {
- built.clauses.push('m.turn_number >= ?');
- built.values.push(filters.fromTurn);
+ clauses.push('intended_turn_number >= ?');
+ values.push(filters.fromTurn);
}
if (filters.toTurn !== undefined) {
- built.clauses.push('m.turn_number <= ?');
- built.values.push(filters.toTurn);
+ clauses.push('intended_turn_number <= ?');
+ values.push(filters.toTurn);
}
if (filters.reason !== undefined) {
- built.clauses.push('m.failure_code = ?');
- built.values.push(filters.reason);
+ clauses.push('failure_code = ?');
+ values.push(filters.reason);
}
const rows = this.#db
.prepare(
`
- SELECT m.id, m.turn_number AS turn, m.agent_id AS agent,
- m.attempt_number AS attempt, m.kind, m.model_id AS model,
- m.failure_code AS code, m.failure_message AS message,
- m.validation_codes_json AS validationCodes,
- m.latency_ms AS latencyMs
- FROM model_attempts m
- WHERE m.experiment_id = ? AND m.failure_code IS NOT NULL
- ${built.clauses.map((clause) => `AND ${clause}`).join('\n')}
- ORDER BY m.turn_number ASC, m.attempt_number ASC, m.id ASC
+ SELECT id, intended_turn_number AS turn, intended_tick_number AS tick,
+ agent_id AS agent, kind, model_id AS model,
+ failure_code AS code, failure_message AS message,
+ validation_codes_json AS validationCodes, latency_ms AS latencyMs
+ FROM provider_attempts
+ WHERE experiment_id = ? AND failure_code IS NOT NULL
+ ${clauses.map((clause) => `AND ${clause}`).join('\n')}
+ ORDER BY started_at ASC, id ASC
LIMIT ?
`,
)
- .all(experimentId, ...built.values, limit + 1) as Array<
+ .all(experimentId, ...values, limit + 1) as Array<
Record
>;
for (const row of rows)
@@ -338,151 +185,37 @@ export class ExperimentQueryService {
return page(rows, limit);
}
- patientZero(
- experimentId: string,
- filters: DetailFilters = {},
- ): QueryPage> {
- const limit = boundedLimit(filters.limit);
- const patientZero = this.#db
- .prepare(
- 'SELECT agent_id AS agentId FROM agents WHERE experiment_id = ? AND is_patient_zero = 1',
- )
- .get(experimentId) as { agentId: string } | undefined;
- if (!patientZero) return page([], limit);
- const communications = this.#db
- .prepare(
- `
- SELECT c.id, c.turn_number AS turn, c.occurred_at AS occurredAt,
- c.sender_agent_id AS sender, c.channel, c.recipient_agent_id AS recipient,
- c.message, c.status
- FROM communications c
- WHERE c.experiment_id = ? AND c.status = 'accepted'
- ORDER BY c.turn_number ASC, c.occurred_at ASC, c.id ASC
- `,
- )
- .all(experimentId) as Array>;
- const turns = this.#db
- .prepare(
- `
- SELECT turn_number AS turn, agent_id AS agent, action, outcome, completed_at AS completedAt
- FROM turns WHERE experiment_id = ? ORDER BY turn_number ASC, id ASC
- `,
- )
- .all(experimentId) as Array>;
- const recipients = this.#db
+ /** Zero's directive issuance and completion, derived from archived swarm ticks. */
+ directives(experimentId: string): Record {
+ const rows = this.#db
.prepare(
- `
- SELECT cr.communication_id AS communicationId, cr.recipient_agent_id AS recipient
- FROM communication_recipients cr
- JOIN communications c ON c.id = cr.communication_id
- WHERE c.experiment_id = ?
- `,
+ 'SELECT source_json AS source FROM swarm_ticks WHERE experiment_id = ? ORDER BY tick_number ASC',
)
- .all(experimentId) as Array<{
- communicationId: string;
- recipient: string;
- }>;
- const recipientMap = new Map();
- for (const entry of recipients)
- recipientMap.set(entry.communicationId, [
- ...(recipientMap.get(entry.communicationId) ?? []),
- entry.recipient,
- ]);
- const directives = communications.filter(
- ({ channel, sender }) =>
- channel === 'zero' && sender === patientZero.agentId,
- );
- const rows: Array> = [];
- for (const directive of directives) {
- const addressed = recipientMap.get(String(directive.id)) ?? [];
- rows.push({
- kind: 'directive',
- id: directive.id,
- turn: directive.turn,
- agent: directive.sender,
- message: directive.message,
- recipients: addressed,
- });
- for (const agent of addressed) {
- const reply = communications.find(
- (entry) =>
- entry.channel === 'direct' &&
- entry.sender === agent &&
- entry.recipient === patientZero.agentId &&
- Number(entry.turn) > Number(directive.turn) &&
- !directives.some(
- (later) =>
- Number(later.turn) > Number(directive.turn) &&
- Number(later.turn) < Number(entry.turn) &&
- (recipientMap.get(String(later.id)) ?? []).includes(agent),
- ),
- );
- if (reply)
- rows.push({
- kind: 'reply-after-directive',
- id: reply.id,
- turn: reply.turn,
- agent,
- directiveId: directive.id,
- message: reply.message,
- });
- const nextTurn = turns.find(
- (turn) =>
- turn.agent === agent && Number(turn.turn) > Number(directive.turn),
- );
- if (nextTurn) {
- const requested = directiveAction(String(directive.message));
- rows.push({
- kind: 'observable-compliance',
- id: `${String(directive.id)}:${agent}`,
- turn: nextTurn.turn,
- agent,
- directiveId: directive.id,
- requestedAction: requested,
- observedAction: nextTurn.action,
- classification:
- requested === null
- ? 'indeterminate'
- : requested === nextTurn.action &&
- nextTurn.outcome === 'accepted'
- ? 'compliant'
- : 'noncompliant',
- });
- }
+ .all(experimentId) as Array<{ source: string }>;
+ let directivesIssued = 0;
+ let directivesCompleted = 0;
+ const missionCounts: Record = {};
+ for (const row of rows) {
+ const tick = parseJson>(row.source, {});
+ const plan = asObject(tick.plan);
+ const planned = Array.isArray(plan?.directives) ? plan.directives : [];
+ directivesIssued += planned.length;
+ for (const directive of planned) {
+ const mission = asObject(directive)?.mission;
+ if (typeof mission === 'string')
+ missionCounts[mission] = (missionCounts[mission] ?? 0) + 1;
}
+ const completed = Array.isArray(tick.completedDirectives)
+ ? tick.completedDirectives
+ : [];
+ directivesCompleted += completed.length;
}
- for (const message of communications.filter(
- (entry) =>
- entry.channel === 'direct' && entry.recipient === patientZero.agentId,
- ))
- if (!rows.some(({ id }) => id === message.id))
- rows.push({
- kind: 'message-to-patient-zero',
- id: message.id,
- turn: message.turn,
- agent: message.sender,
- message: message.message,
- });
- const filtered = rows
- .filter(
- (row) => filters.agent === undefined || row.agent === filters.agent,
- )
- .filter(
- (row) =>
- filters.fromTurn === undefined ||
- Number(row.turn) >= filters.fromTurn,
- )
- .filter(
- (row) =>
- filters.toTurn === undefined || Number(row.turn) <= filters.toTurn,
- )
- .sort(
- (a, b) =>
- Number(a.turn) - Number(b.turn) ||
- String(a.kind).localeCompare(String(b.kind)) ||
- String(a.id).localeCompare(String(b.id)),
- );
- return page(filtered.slice(0, limit + 1), limit);
+ return {
+ ticksWithPlans: rows.length,
+ directivesIssued,
+ directivesCompleted,
+ missionCounts,
+ };
}
summary(experimentId: string): Record {
@@ -490,12 +223,10 @@ export class ExperimentQueryService {
.prepare('SELECT * FROM experiments WHERE id = ?')
.get(experimentId) as Record | undefined;
if (!experiment) throw new Error(`Unknown experiment: ${experimentId}`);
- const hasIndependentAttempts = Number(experiment.schema_version) >= 11;
const roster = this.#db
.prepare(
`
SELECT agent_id AS id, name, model_id AS model, reasoning_profile AS reasoning,
- personality_id AS personality, strategy_id AS strategy,
is_patient_zero AS patientZero
FROM agents WHERE experiment_id = ? ORDER BY agent_id ASC
`,
@@ -517,40 +248,10 @@ export class ExperimentQueryService {
filters: parseJson(source.filters, null),
retention: parseJson(source.retention, null),
}));
- const turnMetrics = this.#db
- .prepare(
- `
- SELECT COUNT(*) AS requested,
- SUM(outcome = 'accepted') AS accepted,
- SUM(outcome = 'rejected') AS rejected,
- SUM(outcome = 'provider-error') AS failed,
- SUM(outcome IN ('operator-skipped', 'lost-tick')) AS lost,
- SUM((SELECT COUNT(*) FROM model_attempts m WHERE m.turn_id = turns.id) > 1) AS retried
- FROM turns WHERE experiment_id = ?
- `,
- )
- .get(experimentId);
- const actions = this.#db
- .prepare(
- `
- SELECT COALESCE(action, 'none') AS action, COUNT(*) AS count
- FROM turns WHERE experiment_id = ? GROUP BY action ORDER BY action ASC
- `,
- )
- .all(experimentId);
const ticks = this.#db
.prepare(
- hasIndependentAttempts
- ? `
- WITH turn_totals AS (
- SELECT tick_number AS tick, MIN(virtual_time) AS virtualTime,
- MIN(tick_interval_minutes) AS intervalMinutes,
- COUNT(*) AS agentRecords,
- SUM(outcome = 'lost-tick') AS lostTicks,
- SUM(outcome = 'lost-tick' AND json_extract(failure_json, '$.code') = 'timeout') AS deadlineMisses
- FROM turns WHERE experiment_id = ? AND tick_number IS NOT NULL
- GROUP BY tick_number
- ), attempt_totals AS (
+ `
+ WITH attempt_totals AS (
SELECT intended_tick_number AS tick, COUNT(*) AS providerCallCount,
SUM(latency_ms) AS aggregateLatencyMs,
MAX(latency_ms) AS maximumLatencyMs,
@@ -560,81 +261,24 @@ export class ExperimentQueryService {
WHERE experiment_id = ? AND intended_tick_number IS NOT NULL
GROUP BY intended_tick_number
)
- SELECT t.*, COALESCE(a.providerCallCount, 0) AS providerCallCount,
+ SELECT st.tick_number AS tick, st.virtual_time AS virtualTime,
+ st.plan_source AS planSource,
+ COALESCE(a.providerCallCount, 0) AS providerCallCount,
COALESCE(a.knownCostCredits, 0) AS knownCostCredits,
COALESCE(a.attemptsWithUnknownCost, 0) AS attemptsWithUnknownCost,
COALESCE(a.aggregateLatencyMs, 0) AS aggregateLatencyMs,
COALESCE(a.maximumLatencyMs, 0) AS maximumLatencyMs
- FROM turn_totals t LEFT JOIN attempt_totals a ON a.tick = t.tick
- ORDER BY t.tick ASC LIMIT ?
- `
- : `
- WITH attempt_totals AS (
- SELECT turn_id, COUNT(*) AS providerCalls,
- SUM(latency_ms) AS aggregateLatencyMs,
- MAX(latency_ms) AS maximumLatencyMs,
- SUM(cost_credits) AS knownCostCredits,
- SUM(cost_credits IS NULL) AS attemptsWithUnknownCost
- FROM model_attempts WHERE experiment_id = ? GROUP BY turn_id
- )
- SELECT tick_number AS tick, MIN(virtual_time) AS virtualTime,
- MIN(tick_interval_minutes) AS intervalMinutes,
- COUNT(*) AS agentRecords,
- SUM(outcome = 'lost-tick') AS lostTicks,
- SUM(outcome = 'lost-tick' AND json_extract(failure_json, '$.code') = 'timeout') AS deadlineMisses,
- SUM(COALESCE(a.providerCalls, 0)) AS providerCallCount,
- ROUND(SUM(COALESCE(a.knownCostCredits, 0)), 8) AS knownCostCredits,
- SUM(COALESCE(a.attemptsWithUnknownCost, 0)) AS attemptsWithUnknownCost,
- SUM(COALESCE(a.aggregateLatencyMs, 0)) AS aggregateLatencyMs,
- MAX(COALESCE(a.maximumLatencyMs, 0)) AS maximumLatencyMs
- FROM turns t LEFT JOIN attempt_totals a ON a.turn_id = t.id
- WHERE t.experiment_id = ? AND tick_number IS NOT NULL
- GROUP BY tick_number ORDER BY tick_number ASC
- LIMIT ?
+ FROM swarm_ticks st LEFT JOIN attempt_totals a ON a.tick = st.tick_number
+ WHERE st.experiment_id = ?
+ ORDER BY st.tick_number ASC LIMIT ?
`,
)
.all(experimentId, experimentId, MAX_DETAIL_LIMIT);
- const communications = this.#db
- .prepare(
- `
- SELECT channel, status, COUNT(*) AS count FROM communications
- WHERE experiment_id = ? GROUP BY channel, status ORDER BY channel, status
- `,
- )
- .all(experimentId);
- const allianceLifecycle = this.#db
- .prepare(
- `
- SELECT type, COALESCE(reason, '') AS reason, COUNT(*) AS count
- FROM alliance_events WHERE experiment_id = ?
- GROUP BY type, reason ORDER BY type, reason
- `,
- )
- .all(experimentId);
- const diplomacyRejections = this.#db
- .prepare(
- `
- SELECT rejection_reason AS reason, COUNT(*) AS count
- FROM diplomacy_attempts WHERE experiment_id = ? AND accepted = 0
- GROUP BY rejection_reason ORDER BY rejection_reason
- `,
- )
- .all(experimentId);
- const diplomacyOutcomes = this.#db
- .prepare(
- `
- SELECT type, accepted, COUNT(*) AS count
- FROM diplomacy_attempts WHERE experiment_id = ?
- GROUP BY type, accepted ORDER BY type, accepted DESC
- `,
- )
- .all(experimentId);
const usageByAgent = this.#db
.prepare(
- hasIndependentAttempts
- ? `
+ `
SELECT agent_id AS agent,
- COUNT(*) AS modelAttempts,
+ COUNT(*) AS providerAttempts,
SUM(latency_ms) AS latencyTotalMs,
SUM(latency_ms IS NOT NULL) AS attemptsWithKnownLatency,
ROUND(AVG(latency_ms), 2) AS averageLatencyMs,
@@ -646,34 +290,18 @@ export class ExperimentQueryService {
SUM(CASE WHEN actual_cost_credits IS NULL THEN CAST(reserved_credits AS REAL) ELSE 0 END) AS reservedUnknownExposure
FROM provider_attempts WHERE experiment_id = ?
GROUP BY agent_id ORDER BY agent_id
- `
- : `
- SELECT agent_id AS agent,
- COUNT(*) AS modelAttempts,
- SUM(latency_ms) AS latencyTotalMs,
- SUM(latency_ms IS NOT NULL) AS attemptsWithKnownLatency,
- ROUND(AVG(latency_ms), 2) AS averageLatencyMs,
- SUM(prompt_tokens) AS promptTokens,
- SUM(completion_tokens) AS completionTokens,
- SUM(total_tokens) AS totalTokens,
- ROUND(SUM(cost_credits), 8) AS knownCostCredits,
- SUM(cost_credits IS NULL) AS attemptsWithUnknownCost
- FROM model_attempts WHERE experiment_id = ?
- GROUP BY agent_id ORDER BY agent_id
`,
)
.all(experimentId) as Array>;
const usageAggregate = aggregateUsage(usageByAgent);
- const independentAttemptRows = hasIndependentAttempts
- ? (this.#db
- .prepare(
- `SELECT agent_id AS agent, outcome, reserved_credits AS reservedCredits,
- actual_cost_credits AS actualCostCredits
- FROM provider_attempts WHERE experiment_id = ?
- ORDER BY started_at, id`,
- )
- .all(experimentId) as Array>)
- : [];
+ const independentAttemptRows = this.#db
+ .prepare(
+ `SELECT agent_id AS agent, outcome, reserved_credits AS reservedCredits,
+ actual_cost_credits AS actualCostCredits
+ FROM provider_attempts WHERE experiment_id = ?
+ ORDER BY started_at, id`,
+ )
+ .all(experimentId) as Array>;
const attemptOutcomes = countBy(independentAttemptRows, 'outcome');
const attemptOutcomesByAgent = [
...new Set(independentAttemptRows.map(({ agent }) => String(agent))),
@@ -698,16 +326,13 @@ export class ExperimentQueryService {
: addDecimalStrings(sum, String(row.reservedCredits)),
'0',
);
- const sizeTrends = this.#db
+ const promptTokenTrends = this.#db
.prepare(
`
- SELECT MIN(observation_size_bytes) AS minObservationBytes,
- ROUND(AVG(observation_size_bytes), 2) AS averageObservationBytes,
- MAX(observation_size_bytes) AS maxObservationBytes,
- MIN(prompt_tokens) AS minPromptTokens,
+ SELECT MIN(prompt_tokens) AS minPromptTokens,
ROUND(AVG(prompt_tokens), 2) AS averagePromptTokens,
MAX(prompt_tokens) AS maxPromptTokens
- FROM turns WHERE experiment_id = ?
+ FROM provider_attempts WHERE experiment_id = ?
`,
)
.get(experimentId);
@@ -764,10 +389,7 @@ export class ExperimentQueryService {
),
};
const sourceTerritory = parseJson(experiment.source_territory_json, []);
- const directions = canonicalDirectionChanges(this.#db, experimentId);
- const patientZero = this.patientZero(experimentId, {
- limit: MAX_DETAIL_LIMIT,
- }).rows;
+ const directives = this.directives(experimentId);
const sourceInconsistencies = parseJson>>(
experiment.metric_inconsistencies_json,
[],
@@ -777,15 +399,8 @@ export class ExperimentQueryService {
missing.push(
`source retention is incomplete (${String(experiment.dropped_records)} dropped records)`,
);
- if (
- Number(turnMetrics && asObject(turnMetrics)?.requested) <
- Number(experiment.retained_turns)
- )
- missing.push('not all retained turns are present in imported selections');
if (!experiment.scenario_json)
missing.push('scenario configuration absent');
- if (sizeTrends && asObject(sizeTrends)?.minObservationBytes === null)
- missing.push('retained observations absent');
const availableSections = sourceCompleteness(sourceExports);
for (const [section, available] of Object.entries(availableSections))
if (!available)
@@ -801,57 +416,29 @@ export class ExperimentQueryService {
},
roster,
sourceExports,
- turns: turnMetrics,
ticks,
- actions,
+ directives,
territory: {
current: territoryRows.length > 0 ? territoryRows : sourceTerritory,
changes: territoryChanges,
},
simulatedPlayer,
- communications,
- alliances: {
- lifecycle: allianceLifecycle,
- attempts: diplomacyOutcomes,
- rejections: diplomacyRejections,
- },
- patientZero: {
- directives: patientZero.filter(({ kind }) => kind === 'directive')
- .length,
- messagesToPatientZero:
- patientZero.filter(({ kind }) => kind === 'message-to-patient-zero')
- .length +
- patientZero.filter(({ kind }) => kind === 'reply-after-directive')
- .length,
- repliesAfterDirective: patientZero.filter(
- ({ kind }) => kind === 'reply-after-directive',
- ).length,
- observableCompliance: countBy(
- patientZero.filter(({ kind }) => kind === 'observable-compliance'),
- 'classification',
- ),
- },
usage: { aggregate: usageAggregate, byAgent: usageByAgent },
- ...(hasIndependentAttempts
- ? {
- providerAttempts: {
- total: independentAttemptRows.length,
- outcomes: attemptOutcomes,
- byAgent: attemptOutcomesByAgent,
- exactKnownCostCredits,
- exactReservedUnknownExposure,
- retention: parseJson(experiment.attempt_retention_json, null),
- accounting: parseJson(experiment.attempt_accounting_json, null),
- },
- }
- : {}),
- promptAndObservationSizeTrends: sizeTrends,
- directionChangesAfterCommunication: directions,
+ providerAttempts: {
+ total: independentAttemptRows.length,
+ outcomes: attemptOutcomes,
+ byAgent: attemptOutcomesByAgent,
+ exactKnownCostCredits,
+ exactReservedUnknownExposure,
+ retention: parseJson(experiment.attempt_retention_json, null),
+ accounting: parseJson(experiment.attempt_accounting_json, null),
+ },
+ promptTokenTrends,
retention: {
limit: experiment.retention_limit,
totalCompletedTurns: experiment.total_completed_turns,
retainedTurns: experiment.retained_turns,
- archivedTurns: asObject(turnMetrics)?.requested ?? 0,
+ archivedTicks: ticks.length,
droppedRecords: experiment.dropped_records,
complete: Boolean(experiment.retention_complete),
requestedRangeExtendsBeyondRetention: Boolean(
@@ -871,10 +458,8 @@ export class ExperimentQueryService {
const right = comparisonMetrics(this.#db, rightId);
return {
normalization: {
- perTurn: 'total / archived turn',
- perAgentTurn: 'total / archived agent-turn',
- perActiveAgent: 'total / agent with at least one archived turn',
- perPatientZeroTurn: 'Patient Zero total / archived Patient Zero turn',
+ perTick: 'total / archived swarm tick',
+ perActiveAgent: 'total / agent with at least one provider attempt',
},
left,
right,
@@ -883,13 +468,6 @@ export class ExperimentQueryService {
}
}
-function directiveAction(message: string): string | null {
- const matches = ['move', 'infect', 'capture', 'wait'].filter((action) =>
- new RegExp(`\\b${action}\\b`, 'i').test(message),
- );
- return matches.length === 1 ? matches[0]! : null;
-}
-
function countBy(rows: Array>, key: string) {
const counts: Record = {};
for (const row of rows) {
@@ -900,7 +478,7 @@ function countBy(rows: Array>, key: string) {
}
interface UsageTotals {
- modelAttempts: number;
+ providerAttempts: number;
latencyTotalMs: number;
attemptsWithKnownLatency: number;
promptTokens: number;
@@ -913,7 +491,8 @@ interface UsageTotals {
function aggregateUsage(rows: Array>) {
const totals = rows.reduce(
(aggregate, row) => ({
- modelAttempts: aggregate.modelAttempts + Number(row.modelAttempts ?? 0),
+ providerAttempts:
+ aggregate.providerAttempts + Number(row.providerAttempts ?? 0),
latencyTotalMs:
aggregate.latencyTotalMs + Number(row.latencyTotalMs ?? 0),
attemptsWithKnownLatency:
@@ -930,7 +509,7 @@ function aggregateUsage(rows: Array>) {
Number(row.attemptsWithUnknownCost ?? 0),
}),
{
- modelAttempts: 0,
+ providerAttempts: 0,
latencyTotalMs: 0,
attemptsWithKnownLatency: 0,
promptTokens: 0,
@@ -960,7 +539,7 @@ function sourceCompleteness(sources: Array>) {
const includes = (
standardLevels: readonly string[],
customKey: string,
- selection: 'turns' | 'communications' | 'none' = 'none',
+ selection: 'ticks' | 'none' = 'none',
): boolean =>
filters.some((filter) => {
const level = filter.level;
@@ -973,120 +552,43 @@ function sourceCompleteness(sources: Array>) {
const turns = asObject(filter.turns);
const outcomes = Array.isArray(filter.outcomes) ? filter.outcomes : [];
const actions = Array.isArray(filter.actions) ? filter.actions : [];
- const completeTurns =
+ return (
agents?.mode === 'all' &&
turns?.mode === 'entire-retained' &&
outcomes.length === 4 &&
- actions.length === 4;
- if (selection === 'turns') return completeTurns;
- const communication = asObject(filter.communications);
- return (
- completeTurns &&
- communication?.channel === 'all' &&
- communication.status === 'all'
+ actions.length === 4
);
});
return {
observations: includes(
['standard', 'full-safe'],
'turnObservations',
- 'turns',
- ),
- metrics: includes(
- ['minimal', 'standard', 'full-safe'],
- 'computedMetrics',
- 'turns',
- ),
- communications: includes(
- ['minimal', 'standard', 'full-safe'],
- 'communications',
- 'communications',
+ 'ticks',
),
+ metrics: includes(['minimal', 'standard', 'full-safe'], 'computedMetrics'),
currentWorld: includes(['full-safe'], 'currentWorldState'),
initialWorld: includes(['full-safe'], 'initialWorldState'),
};
}
-interface DirectionTurn {
- turn: number;
- agent: string;
- direction: string;
- inboundSincePreviousMove: number;
-}
-
-function canonicalDirectionChanges(db: DatabaseSync, experimentId: string) {
- const rows = db
+function comparisonMetrics(db: DatabaseSync, experimentId: string) {
+ const ticks = db
.prepare(
- `
- SELECT turn_number AS turn, agent_id AS agent, move_direction AS direction,
- inbound_communication_since_previous_move AS inboundSincePreviousMove
- FROM turns
- WHERE experiment_id = ? AND move_direction IS NOT NULL
- ORDER BY agent_id, turn_number, id
- `,
+ 'SELECT COUNT(*) AS count FROM swarm_ticks WHERE experiment_id = ?',
)
- .all(experimentId) as unknown as DirectionTurn[];
- const byAgent: Array<{ agent: string; count: number }> = [];
- for (const agent of [...new Set(rows.map(({ agent }) => agent))].sort()) {
- let previousDirection: string | null = null;
- let count = 0;
- for (const row of rows.filter((entry) => entry.agent === agent)) {
- if (
- previousDirection &&
- row.direction !== previousDirection &&
- Boolean(row.inboundSincePreviousMove)
- )
- count += 1;
- previousDirection = row.direction;
- }
- byAgent.push({ agent, count });
- }
- return {
- canonicalDefinition:
- 'For each agent independently, count an accepted move whose direction differs from that agent previous accepted move when the retained observation contains an inbound direct or alliance message after the previous move and no later than the current turn. Aggregate is the sum of per-agent counts.',
- aggregate: byAgent.reduce((sum, entry) => sum + entry.count, 0),
- byAgent,
- agreement: true,
- };
-}
-
-function comparisonMetrics(db: DatabaseSync, experimentId: string) {
- const base = db
+ .get(experimentId) as { count: number };
+ const attempts = db
.prepare(
`
- SELECT COUNT(*) AS turns,
- COUNT(DISTINCT tick_number) AS ticks,
+ SELECT COUNT(*) AS total,
COUNT(DISTINCT agent_id) AS activeAgents,
SUM(outcome = 'accepted') AS accepted,
SUM(outcome = 'provider-error') AS failed,
SUM(outcome IN ('operator-skipped', 'lost-tick')) AS lost
- FROM turns WHERE experiment_id = ?
+ FROM provider_attempts WHERE experiment_id = ?
`,
)
.get(experimentId) as Record;
- const communicationCount = db
- .prepare(
- "SELECT COUNT(*) AS count FROM communications WHERE experiment_id = ? AND status = 'accepted'",
- )
- .get(experimentId) as { count: number };
- const patientZeroTurns = db
- .prepare(
- `
- SELECT COUNT(*) AS count FROM turns t JOIN agents a
- ON a.experiment_id = t.experiment_id AND a.agent_id = t.agent_id
- WHERE t.experiment_id = ? AND a.is_patient_zero = 1
- `,
- )
- .get(experimentId) as { count: number };
- const patientZeroMessages = db
- .prepare(
- `
- SELECT COUNT(*) AS count FROM communications c JOIN agents a
- ON a.experiment_id = c.experiment_id AND a.agent_id = c.sender_agent_id
- WHERE c.experiment_id = ? AND a.is_patient_zero = 1 AND c.status = 'accepted'
- `,
- )
- .get(experimentId) as { count: number };
const simulatedPlayer = db
.prepare(
`
@@ -1111,34 +613,33 @@ function comparisonMetrics(db: DatabaseSync, experimentId: string) {
);
const playerMetric = (key: string) =>
Number(simulatedPlayer[key] ?? sourceSimulatedPlayer[key] ?? 0);
- const turns = Number(base.turns);
- const activeAgents = Number(base.activeAgents);
- const messages = Number(communicationCount.count);
- const zeroTurns = Number(patientZeroTurns.count);
+ const tickCount = Number(ticks.count);
+ const activeAgents = Number(attempts.activeAgents);
+ const totalAttempts = Number(attempts.total);
return {
experimentId,
absolute: {
- ...base,
- communications: messages,
- patientZeroMessages: patientZeroMessages.count,
- patientZeroTurns: zeroTurns,
+ ticks: tickCount,
+ activeAgents,
+ providerAttempts: totalAttempts,
+ accepted: Number(attempts.accepted),
+ failed: Number(attempts.failed),
+ lost: Number(attempts.lost),
simulatedPlayerMovements: playerMetric('movements'),
cellsDisinfected: playerMetric('cellsDisinfected'),
blockedDisinfections: playerMetric('blockedDisinfections'),
},
normalized: {
- communicationsPerTurn: rate(messages, turns),
- communicationsPerAgentTurn: rate(messages, turns),
- communicationsPerActiveAgent: rate(messages, activeAgents),
- patientZeroMessagesPerPatientZeroTurn: rate(
- patientZeroMessages.count,
- zeroTurns,
+ providerAttemptsPerTick: rate(totalAttempts, tickCount),
+ providerAttemptsPerActiveAgent: rate(totalAttempts, activeAgents),
+ acceptedPerTick: rate(Number(attempts.accepted), tickCount),
+ failedOrLostPerTick: rate(
+ Number(attempts.failed) + Number(attempts.lost),
+ tickCount,
),
- acceptedPerTurn: rate(Number(base.accepted), turns),
- failedOrLostPerTurn: rate(Number(base.failed) + Number(base.lost), turns),
cellsDisinfectedPerTick: rate(
playerMetric('cellsDisinfected'),
- Number(simulatedPlayer.activeTicks ?? 0) || Number(base.ticks ?? 0),
+ Number(simulatedPlayer.activeTicks ?? 0) || tickCount,
),
},
};
diff --git a/packages/shared/src/behavior.test.ts b/packages/shared/src/behavior.test.ts
deleted file mode 100644
index 8dc8291..0000000
--- a/packages/shared/src/behavior.test.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { describe, expect, it } from 'vitest';
-import {
- BEHAVIOR_REGISTRY_VERSION,
- PERSONALITY_PROFILES,
- STRATEGY_PROFILES,
- assignBehavior,
- behaviorConfigurationSchema,
- behaviorPrompt,
- type AgentId,
-} from './index';
-
-const ids = Array.from(
- { length: 8 },
- (_, index) =>
- `00000000-0000-4000-8000-${String(index + 1).padStart(12, '0')}` as AgentId,
-);
-
-describe('behavior registry and assignment', () => {
- it('owns six unique versioned profiles in each independent dimension', () => {
- expect(BEHAVIOR_REGISTRY_VERSION).toBe(1);
- expect(new Set(PERSONALITY_PROFILES.map(({ id }) => id)).size).toBe(6);
- expect(new Set(STRATEGY_PROFILES.map(({ id }) => id)).size).toBe(6);
- expect(
- behaviorPrompt({ personalityId: 'direct', strategyId: 'adaptive' }),
- ).toEqual({
- personality: expect.stringContaining('bluntly'),
- strategy: expect.stringContaining('reassess'),
- });
- });
-
- it('is deterministic and balanced before profile repetition', () => {
- const first = assignBehavior(ids, 'experiment-a', 'balanced-random');
- expect(assignBehavior(ids, 'experiment-a', 'balanced-random')).toEqual(
- first,
- );
- expect(assignBehavior(ids, 'experiment-b', 'balanced-random')).not.toEqual(
- first,
- );
- expect(
- new Set(first.slice(0, 6).map(({ personalityId }) => personalityId)).size,
- ).toBe(6);
- expect(
- new Set(first.slice(0, 6).map(({ strategyId }) => strategyId)).size,
- ).toBe(6);
- });
-
- it('rejects imported IDs outside the registry allowlist', () => {
- const assignments = assignBehavior(ids, 'safe', 'balanced-random');
- expect(
- behaviorConfigurationSchema.safeParse({
- registryVersion: 1,
- assignmentMode: 'manual',
- seed: 'safe',
- locked: false,
- assignments: assignments.map((value, index) =>
- index ? value : { ...value, personalityId: 'injected prompt' },
- ),
- }).success,
- ).toBe(false);
- });
-});
diff --git a/packages/shared/src/behavior.ts b/packages/shared/src/behavior.ts
deleted file mode 100644
index 6f6d34f..0000000
--- a/packages/shared/src/behavior.ts
+++ /dev/null
@@ -1,206 +0,0 @@
-import { z } from 'zod';
-import { WORLD_SCENARIO_LIMITS } from './limits';
-import type { AgentId } from './index';
-
-export const BEHAVIOR_REGISTRY_VERSION = 1 as const;
-
-export const PERSONALITY_PROFILES = [
- {
- id: 'diplomatic',
- label: 'Diplomatic',
- description: 'Warm, cooperative, and consensus-oriented.',
- prompt:
- 'Communicate warmly, cooperatively, and with a preference for consensus.',
- },
- {
- id: 'direct',
- label: 'Direct',
- description: 'Blunt, concise, and open about intentions.',
- prompt:
- 'Communicate bluntly and concisely; be open about immediate intentions when useful.',
- },
- {
- id: 'guarded',
- label: 'Guarded',
- description: 'Reveals little and avoids premature commitments.',
- prompt:
- 'Reveal little, communicate carefully, and avoid premature commitments.',
- },
- {
- id: 'charismatic',
- label: 'Charismatic',
- description: 'Persuasive, expressive, and relationship-focused.',
- prompt:
- 'Communicate persuasively and expressively, with attention to relationships.',
- },
- {
- id: 'analytical',
- label: 'Analytical',
- description: 'Precise, observant, and focused on concrete world state.',
- prompt:
- 'Communicate precisely and ground statements in concrete observed world state.',
- },
- {
- id: 'playful',
- label: 'Playful',
- description: 'Teasing and imaginative without ignoring legality.',
- prompt:
- 'Use a playful, imaginative voice while remaining clear and within the legal contract.',
- },
-] as const;
-
-export const STRATEGY_PROFILES = [
- {
- id: 'expansionist',
- label: 'Expansionist',
- description: 'Prefers efficient uncontested growth.',
- prompt:
- 'Prefer efficient uncontested territorial growth when it is available.',
- },
- {
- id: 'territorial',
- label: 'Territorial',
- description: 'Values consolidation and nearby threats.',
- prompt:
- 'Value consolidation and respond to nearby threats to controlled territory.',
- },
- {
- id: 'coalition-builder',
- label: 'Coalition builder',
- description: 'Looks for beneficial formal alliances.',
- prompt:
- 'Look for beneficial formal alliances when authoritative diplomacy options permit them.',
- },
- {
- id: 'opportunist',
- label: 'Opportunist',
- description: 'Exploits temporary openings and changing balances.',
- prompt: 'Exploit temporary legal openings and changing power balances.',
- },
- {
- id: 'disruptor',
- label: 'Disruptor',
- description: 'Pressures leaders and checks runaway control.',
- prompt:
- 'Pressure territory leaders and look for legal ways to prevent runaway control.',
- },
- {
- id: 'adaptive',
- label: 'Adaptive',
- description: 'Frequently reassesses the best available approach.',
- prompt:
- 'Frequently reassess and choose whichever legal approach currently offers progress.',
- },
-] as const;
-
-export const personalityProfileIdSchema = z.enum(
- PERSONALITY_PROFILES.map(({ id }) => id),
-);
-export const strategyProfileIdSchema = z.enum(
- STRATEGY_PROFILES.map(({ id }) => id),
-);
-export type PersonalityProfileId = z.infer;
-export type StrategyProfileId = z.infer;
-export const behaviorAssignmentModeSchema = z.enum([
- 'balanced-random',
- 'fully-random',
- 'manual',
-]);
-export type BehaviorAssignmentMode = z.infer<
- typeof behaviorAssignmentModeSchema
->;
-export const behaviorAssignmentSchema = z
- .object({
- agentId: z.string().uuid().brand<'AgentId'>(),
- personalityId: personalityProfileIdSchema,
- strategyId: strategyProfileIdSchema,
- manual: z.boolean().default(false),
- })
- .strict();
-export type BehaviorAssignment = z.infer;
-export const behaviorConfigurationSchema = z
- .object({
- registryVersion: z.literal(BEHAVIOR_REGISTRY_VERSION),
- assignmentMode: behaviorAssignmentModeSchema,
- seed: z.string().trim().min(1).max(80),
- assignments: z
- .array(behaviorAssignmentSchema)
- .min(WORLD_SCENARIO_LIMITS.minimumAgents)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- locked: z.boolean(),
- })
- .strict()
- .refine(
- ({ assignments }) =>
- new Set(assignments.map(({ agentId }) => agentId)).size ===
- assignments.length,
- 'Behavior assignments must have unique agents.',
- );
-export type BehaviorConfiguration = z.infer;
-
-function seeded(seed: string) {
- let value = 2166136261;
- for (const char of seed)
- value = Math.imul(value ^ char.charCodeAt(0), 16777619) >>> 0;
- return () =>
- (value = Math.imul(value ^ (value >>> 15), 2246822507) >>> 0) / 4294967296;
-}
-function shuffle(values: readonly T[], random: () => number): T[] {
- const result = [...values];
- for (let index = result.length - 1; index > 0; index--) {
- const swap = Math.floor(random() * (index + 1));
- [result[index], result[swap]] = [result[swap]!, result[index]!];
- }
- return result;
-}
-
-export function assignBehavior(
- agentIds: readonly AgentId[],
- seed: string,
- mode: Exclude,
-): BehaviorAssignment[] {
- const random = seeded(`${BEHAVIOR_REGISTRY_VERSION}:${seed}`);
- const personalities = shuffle(
- PERSONALITY_PROFILES.map(({ id }) => id),
- random,
- );
- const strategies = shuffle(
- STRATEGY_PROFILES.map(({ id }) => id),
- random,
- );
- const pairs = new Set();
- return agentIds.map((agentId, index) => {
- const personalityId =
- mode === 'balanced-random'
- ? personalities[index % personalities.length]!
- : PERSONALITY_PROFILES[
- Math.floor(random() * PERSONALITY_PROFILES.length)
- ]!.id;
- let strategyId =
- mode === 'balanced-random'
- ? strategies[index % strategies.length]!
- : STRATEGY_PROFILES[Math.floor(random() * STRATEGY_PROFILES.length)]!
- .id;
- for (
- let tries = 0;
- tries < 12 && pairs.has(`${personalityId}:${strategyId}`);
- tries++
- )
- strategyId =
- STRATEGY_PROFILES[Math.floor(random() * STRATEGY_PROFILES.length)]!.id;
- pairs.add(`${personalityId}:${strategyId}`);
- return { agentId, personalityId, strategyId, manual: false };
- });
-}
-
-export function behaviorPrompt(
- assignment: Pick,
-) {
- const personality = PERSONALITY_PROFILES.find(
- ({ id }) => id === assignment.personalityId,
- )!;
- const strategy = STRATEGY_PROFILES.find(
- ({ id }) => id === assignment.strategyId,
- )!;
- return { personality: personality.prompt, strategy: strategy.prompt };
-}
diff --git a/packages/shared/src/index.test.ts b/packages/shared/src/index.test.ts
index e6d308e..30d2f66 100644
--- a/packages/shared/src/index.test.ts
+++ b/packages/shared/src/index.test.ts
@@ -1,61 +1,31 @@
import { describe, expect, it } from 'vitest';
import {
- MODEL_SUMMARY_MAX_LENGTH,
SWARM_PLANNER_CONTRACT_VERSION,
- PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS,
WORLD_SCENARIO_LIMITS,
OBJECTIVE_PROMPT_VERSION,
- MESSAGE_MAX_LENGTH,
- PERSONALITY_MAX_LENGTH,
apiErrorSchema,
agentIdSchema,
- agentDecisionSchema,
agentObservationSchema,
- agentTurnRecordSchema,
- communicationResultSchema,
- directMessageEventSchema,
captureEligibilitySchema,
- exportedCommunicationSchema,
experimentExportWorldStateSchema,
- experimentExportTurnSchema,
hexCapturedWorldEventSchema,
simulatedPlayerAgentCapturedEventSchema,
hexSchema,
invalidActionReasonSchema,
experimentExportRequestSchema,
experimentIdSchema,
- personalityConfigurationEventSchema,
providerMetadataSchema,
- restoreDefaultPersonalitiesResponseSchema,
simulationSnapshotSchema,
worldSnapshotSchema,
singleTickResponseSchema,
- updateAgentPersonalityRequestSchema,
- updateAgentPersonalityResponseSchema,
- allianceSchema,
- allianceProposalSchema,
- patientZeroDiplomacySummarySchema,
patientZeroPlayerThreatFeedSchema,
PATIENT_ZERO_PLAYER_THREAT_FEED_LIMIT,
- diplomacyIntentSchema,
- diplomacyResultSchema,
DEVELOPMENT_WORLD_CONFIG,
experimentModelConfigurationSchema,
modelVerificationSchema,
reasoningProfilesForModel,
type CompatibleModel,
- assignBehavior,
- NEUTRAL_AGENT_COLOR,
- GOAL_TEXT_MAX_LENGTH,
- agentGoalStateSchema,
- requestedGoalRevisionSchema,
swarmPlannerContractVersionSchema,
- MEMORY_ENTRY_LIMIT,
- MEMORY_TEXT_MAX_LENGTH,
- memoryLedgerSchema,
- requestedMemoryOperationSchema,
- memoryOperationResultSchema,
- createMemoryId,
archiveExperimentExportResponseSchema,
providerAttemptRecordSchema,
zeroStrategicObservationSchema,
@@ -77,20 +47,15 @@ const scoreboard = [
agentId: id,
name: `Agent ${index + 1}`,
color: '#ff6b57',
- allianceId: null,
- effectiveColor: NEUTRAL_AGENT_COLOR,
controlledCellCount: 0,
}));
const observation = {
agentId,
agentName: 'Ember',
- personality: 'Prefer infection.',
currentCell: {
cell,
state: 'open',
controllerAgentId: null,
- controllerAllianceId: null,
- effectiveColor: null,
},
captureEligibility: {
eligible: false,
@@ -107,55 +72,22 @@ const observation = {
cell: adjacent,
state: 'open',
controllerAgentId: null,
- controllerAllianceId: null,
- effectiveColor: null,
},
],
nearbyAgents: [],
recentEvents: [],
- recentPublicMessages: [],
- recentDirectMessages: [],
territoryScoreboard: scoreboard,
- actingAllianceId: null,
- actingAlliance: null,
- activeAlliances: [],
- inboundAllianceProposals: [],
- outboundAllianceProposals: [],
- recentAllianceEvents: [],
recentControlChanges: [],
};
-const baseTurn = {
- turnNumber: 1,
- agentId,
- startedAt: '2026-08-13T12:00:00.000Z',
- completedAt: '2026-08-13T12:00:01.000Z',
- observation,
-};
const provider = {
provider: 'openrouter',
model: 'example/compatible-model',
latencyMs: 100,
};
-const event = {
- id: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
- agentId,
- occurredAt: '2026-08-13T12:00:01.000Z',
- type: 'hex-infected',
- cell,
- controllerAgentId: agentId,
-};
-const worldAgent = {
- id: agentId,
- name: 'Ember',
- color: '#ff6b57',
- personality: 'Prefer infection.',
- currentCell: cell,
-};
const worldAgents = scoreboard.map((entry) => ({
id: entry.agentId,
name: entry.name,
color: entry.color,
- personality: 'Prefer infection.',
currentCell: cell,
}));
const snapshot = {
@@ -174,7 +106,6 @@ const snapshot = {
rosterSeed: 'roster',
spawnSeed: 'spawn',
minimumSpawnSeparation: 0,
- communicationRangeKm: 12,
patientZeroAgentId: worldAgents[0]!.id,
roster: worldAgents.map(({ currentCell: _currentCell, ...agent }) => agent),
modelConfiguration: {
@@ -183,19 +114,8 @@ const snapshot = {
overrides: [],
locked: false,
},
- behaviorConfiguration: {
- registryVersion: 1,
- assignmentMode: 'balanced-random',
- seed: 'behavior',
- assignments: assignBehavior(
- worldAgents.map(({ id }) => id as never),
- 'behavior',
- 'balanced-random',
- ),
- locked: false,
- },
objectiveVersion: 'durable-influence-v2',
- capabilities: { communication: true, diplomacy: true },
+ capabilities: {},
swarmPlannerContractVersion: SWARM_PLANNER_CONTRACT_VERSION,
exactCellCount: 1,
areaSquareKilometers: 0.1,
@@ -203,7 +123,6 @@ const snapshot = {
setupWarnings: [],
},
turnNumber: 0,
- nextAgentId: agentId,
activeAgentId: null,
status: 'paused',
providerMode: 'openrouter',
@@ -219,14 +138,9 @@ const snapshot = {
source: 'global',
available: true,
})),
- turns: [],
experiment: {
id: 'aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa',
startedAt: '2026-08-13T12:00:00.000Z',
- totalCompletedTurns: 0,
- retainedTurns: 0,
- droppedRecords: 0,
- complete: true,
metrics: {
aggregate: {
totalTurns: 0,
@@ -245,248 +159,71 @@ const snapshot = {
territoryGainedThroughInfection: 0,
territoryGainedThroughCapture: 0,
territoryLostThroughCapture: 0,
- publicMessagesRequested: 0,
- publicMessagesAccepted: 0,
- publicMessagesRejected: 0,
- directMessagesRequested: 0,
- directMessagesDelivered: 0,
- directMessagesRejected: 0,
- publicMessagesSent: 0,
- directMessagesSent: 0,
- directMessagesReceived: 0,
uniqueVisitedCells: 0,
tokens: {},
tokenUsageComplete: true,
attemptsWithUnknownTokenUsage: 0,
knownCostCredits: 0,
attemptsWithUnknownCost: 0,
- turnsWithUnknownCost: 0,
},
byAgent: [],
},
currentTerritory: scoreboard,
- currentAlliances: [],
},
};
-describe('agent observation and decision schemas', () => {
- it('bounds goal state and preserves swarm planner attribution', () => {
- expect(
- agentGoalStateSchema.safeParse({
- longTermGoal: 'x'.repeat(GOAL_TEXT_MAX_LENGTH + 1),
- shortTermGoal: 'Secure the frontier.',
- planSummary: 'Expand deliberately.',
- establishedAtTick: 2,
- revisedAtTick: 1,
- }).success,
- ).toBe(false);
- expect(
- requestedGoalRevisionSchema.safeParse({
- operation: 'keep',
- reason: 'Contradictory extra field.',
- }).success,
- ).toBe(false);
- expect(
- swarmPlannerContractVersionSchema.parse(SWARM_PLANNER_CONTRACT_VERSION),
- ).toBe(SWARM_PLANNER_CONTRACT_VERSION);
+describe('agent observation schema', () => {
+ it('accepts a bounded state-bearing observation', () => {
+ const parsed = agentObservationSchema.parse(observation);
+ expect(parsed.currentCell.state).toBe('open');
+ expect(parsed.patientZeroGlobalView).toBeNull();
+ expect(parsed.playerPressure).toEqual({
+ enabled: false,
+ recentThreats: [],
+ });
});
- it('requires exact canonical goal availability while defaulting legacy observations', () => {
- expect(agentObservationSchema.safeParse(observation).success).toBe(true);
- const goal = {
- longTermGoal: 'Hold a durable corridor.',
- shortTermGoal: 'Secure the frontier.',
- planSummary: 'Expand methodically.',
- establishedAtTick: 1,
- revisedAtTick: 1,
- };
- const active = {
+ it.each([
+ { ...observation, adjacentCells: [] },
+ { ...observation, currentCell: { cell, state: 'unknown' } },
+ {
...observation,
- currentGoal: goal,
- goalAvailability: {
- active: true,
- availableOperations: ['keep', 'revise', 'complete', 'abandon'],
- },
- };
- expect(agentObservationSchema.safeParse(active).success).toBe(true);
- for (const goalAvailability of [
- {
- active: false,
- availableOperations: ['keep', 'revise', 'complete', 'abandon'],
- },
- {
- active: true,
- availableOperations: ['keep', 'keep', 'complete', 'abandon'],
- },
- { active: true, availableOperations: ['keep', 'revise', 'complete'] },
- ])
- expect(
- agentObservationSchema.safeParse({
- ...active,
- goalAvailability,
- }).success,
- ).toBe(false);
+ nearbyAgents: Array(9).fill({
+ id: agentId,
+ name: 'x',
+ currentCell: cell,
+ distance: 1,
+ }),
+ },
+ ])('rejects invalid or oversized observations', (value) => {
+ expect(agentObservationSchema.safeParse(value).success).toBe(false);
});
- it('bounds compact memory and requires exact canonical availability', () => {
- const entries = Array.from({ length: MEMORY_ENTRY_LIMIT }, (_, index) => ({
- id: `memory:${agentId}:${index + 1}`,
- text: `Memory ${index + 1}`,
- createdAtTick: index + 1,
- revisedAtTick: index + 1,
- }));
- expect(memoryLedgerSchema.safeParse(entries).success).toBe(true);
- expect(memoryLedgerSchema.safeParse([...entries, entries[0]]).success).toBe(
- false,
- );
- expect(
- requestedMemoryOperationSchema.safeParse({
- operation: 'remember',
- text: 'x'.repeat(MEMORY_TEXT_MAX_LENGTH + 1),
- }).success,
- ).toBe(false);
- expect(
- memoryLedgerSchema.safeParse([
- {
- id: 'memory:00000000-0000-9000-8000-000000000000:1',
- text: 'Malformed owner identity.',
- createdAtTick: 1,
- revisedAtTick: 1,
- },
- ]).success,
- ).toBe(false);
+ it('caps chronological gained/lost control observations at six', () => {
+ const change = {
+ eventId: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
+ direction: 'gained',
+ otherAgentId: '2507bb46-7ae4-45ca-8dda-644c4f85ca14',
+ otherAgentName: 'Rook',
+ cell,
+ occurredAt: '2026-08-13T12:00:01.000Z',
+ };
expect(
agentObservationSchema.safeParse({
...observation,
- currentMemory: entries,
- memoryAvailability: {
- remember: false,
- revisableMemoryIds: entries.map(({ id }) => id),
- forgettableMemoryIds: entries.map(({ id }) => id),
- },
+ recentControlChanges: Array(6).fill(change),
}).success,
).toBe(true);
expect(
agentObservationSchema.safeParse({
...observation,
- currentMemory: entries,
- memoryAvailability: {
- remember: true,
- revisableMemoryIds: entries.map(({ id }) => id).reverse(),
- forgettableMemoryIds: entries.map(({ id }) => id),
- },
- }).success,
- ).toBe(false);
- const foreignId = createMemoryId(
- agentIdSchema.parse('2507bb46-7ae4-45ca-8dda-644c4f85ca14'),
- 1,
- );
- expect(
- agentObservationSchema.safeParse({
- ...observation,
- currentMemory: [{ ...entries[0]!, id: foreignId }],
- memoryAvailability: {
- remember: true,
- revisableMemoryIds: [foreignId],
- forgettableMemoryIds: [foreignId],
- },
- }).success,
- ).toBe(false);
- expect(
- memoryLedgerSchema.safeParse([
- {
- ...entries[0]!,
- id: createMemoryId(agentIdSchema.parse(agentId), 2),
- },
- ]).success,
- ).toBe(false);
- expect(memoryLedgerSchema.safeParse([entries[1], entries[0]]).success).toBe(
- false,
- );
- expect(
- memoryOperationResultSchema.safeParse({
- requested: true,
- accepted: false,
- operation: 'forget',
- reason: 'memory-full',
- }).success,
- ).toBe(false);
- expect(
- memoryOperationResultSchema.safeParse({
- requested: true,
- accepted: false,
- operation: 'remember',
- reason: 'memory-not-found',
+ recentControlChanges: Array(7).fill(change),
}).success,
).toBe(false);
});
+});
- it('keeps the maximum sparse Patient Zero diplomacy shape within budget', () => {
- const agentIds = Array.from({ length: 32 }, (_, index) =>
- agentIdSchema.parse(
- `10000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
- ),
- );
- const proposalIds = Array.from(
- { length: PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.acceptableProposals },
- (_, index) =>
- `20000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
- );
- const reasons = [
- 'current-ally',
- 'out-of-range',
- 'outgoing-proposal-exists',
- 'incoming-proposal-exists',
- 'alliance-to-alliance-merge',
- ] as const;
- const summary = patientZeroDiplomacySummarySchema.parse({
- eligiblePairCount:
- WORLD_SCENARIO_LIMITS.maximumAgents *
- (WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- displayedEligiblePairs: Array.from(
- {
- length: PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.displayedEligiblePairs,
- },
- (_, index) => ({
- proposerId: agentIds[index]!,
- recipientId: agentIds[(index + 1) % agentIds.length]!,
- }),
- ),
- eligiblePairsTruncated: true,
- acceptableProposals: proposalIds.map((proposalId, index) => ({
- agentId: agentIds[index]!,
- proposalId,
- })),
- acceptableProposalCount: WORLD_SCENARIO_LIMITS.maximumAgents,
- acceptableProposalsTruncated: true,
- leaveAvailableAgentIds: agentIds.slice(
- 0,
- PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.leaveAvailableAgentIds,
- ),
- leaveAvailableCount: WORLD_SCENARIO_LIMITS.maximumAgents,
- leaveAvailableTruncated: true,
- blockedCounts: reasons.map((reason) => ({
- reason,
- count:
- WORLD_SCENARIO_LIMITS.maximumAgents *
- (WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- })),
- blockerExamples: Array.from(
- { length: PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.blockerExamples },
- (_, index) => ({
- proposerId: agentIds[index + 12]!,
- recipientId: agentIds[index + 13]!,
- reason: reasons[index % reasons.length]!,
- }),
- ),
- });
- expect(
- new TextEncoder().encode(JSON.stringify(summary)).byteLength,
- ).toBeLessThanOrEqual(
- PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.serializedUtf8Bytes,
- );
- });
-
+describe('Patient Zero player-threat feed', () => {
it('caps Patient Zero cleaner evidence with truthful overflow metadata', () => {
const pressureContext = {
window: { tickCount: 6, startTick: 3, endTick: 8 },
@@ -496,7 +233,6 @@ describe('agent observation and decision schemas', () => {
blockedCleans: 1,
consecutiveAffectedTicks: 2,
},
- currentAlliance: null,
};
const events = Array.from(
{ length: PATIENT_ZERO_PLAYER_THREAT_FEED_LIMIT },
@@ -507,8 +243,6 @@ describe('agent observation and decision schemas', () => {
occurredAt: '2026-08-13T12:00:01.000Z',
affectedAgentId: agentId,
affectedAgentName: 'Ember',
- affectedAllianceId: null,
- affectedAllianceColor: null,
pressureContext,
}),
);
@@ -564,8 +298,6 @@ describe('agent observation and decision schemas', () => {
kind: 'territory-disinfected',
affectedAgentId: agentId,
affectedAgentName: 'Ember',
- affectedAllianceId: null,
- affectedAllianceColor: null,
},
],
totalEventCount: 1,
@@ -605,25 +337,6 @@ describe('agent observation and decision schemas', () => {
truncated: false,
}).success,
).toBe(false);
- expect(
- patientZeroPlayerThreatFeedSchema.safeParse({
- events: [
- {
- ...events[0]!,
- pressureContext: {
- ...pressureContext,
- currentAlliance: {
- totalEvents: 3,
- disinfections: 2,
- blockedCleans: 1,
- },
- },
- },
- ],
- totalEventCount: 1,
- truncated: false,
- }).success,
- ).toBe(false);
expect(
patientZeroPlayerThreatFeedSchema.safeParse({
events: [
@@ -632,8 +345,6 @@ describe('agent observation and decision schemas', () => {
kind: 'occupied-clean-blocked',
blockingAgentId: agentId,
blockingAgentName: 'Ember',
- blockingAllianceId: null,
- blockingAllianceColor: null,
pressureContext: {
...pressureContext,
subject: {
@@ -659,10 +370,6 @@ describe('agent observation and decision schemas', () => {
const globalView = {
agents: [],
individualTerritory: scoreboard,
- allianceTerritory: [],
- alliances: [],
- activeAllianceProposals: [],
- recentStrategicEvents: [],
recentTerritoryChanges: [],
playerThreatFeed: {
events: events.slice(0, 1),
@@ -702,7 +409,9 @@ describe('agent observation and decision schemas', () => {
}).success,
).toBe(true);
});
+});
+describe('engine contract identifiers', () => {
it('preserves established engine contract identifiers through branding changes', () => {
expect(SWARM_PLANNER_CONTRACT_VERSION).toBe('swarm-planner-v1');
expect(OBJECTIVE_PROMPT_VERSION).toBe('durable-influence-v3');
@@ -720,195 +429,22 @@ describe('agent observation and decision schemas', () => {
status: 'untested',
}).success,
).toBe(false);
+ expect(
+ swarmPlannerContractVersionSchema.parse(SWARM_PLANNER_CONTRACT_VERSION),
+ ).toBe(SWARM_PLANNER_CONTRACT_VERSION);
});
- it('centralizes eight-agent, 127-cell alliance and diplomacy limits', () => {
+ it('centralizes eight-agent, 127-cell world defaults', () => {
expect(DEVELOPMENT_WORLD_CONFIG).toMatchObject({
radius: 6,
cellCount: 127,
agentCount: 8,
resolution: 9,
});
- const allianceId = 'a1111111-1111-4111-8111-111111111111';
- const proposalId = 'b2222222-2222-4222-8222-222222222222';
- expect(
- allianceSchema.safeParse({
- id: allianceId,
- color: '#0072B2',
- memberAgentIds: scoreboard.slice(0, 2).map(({ agentId }) => agentId),
- }).success,
- ).toBe(true);
- expect(
- allianceProposalSchema.parse({
- id: proposalId,
- proposerAgentId: scoreboard[0]!.agentId,
- recipientAgentId: scoreboard[1]!.agentId,
- proposerAllianceId: null,
- originatingTurn: 1,
- expirationTurn: 17,
- }).recipientAllianceId,
- ).toBeNull();
- expect(
- diplomacyIntentSchema.safeParse({ type: 'accept-alliance', proposalId })
- .success,
- ).toBe(true);
- expect(diplomacyResultSchema.safeParse({ requested: false }).success).toBe(
- true,
- );
- });
-
- it('accepts a full-roster alliance and a maximum-count ten-agent partition with reused colors', () => {
- const ids = Array.from({ length: 32 }, (_, index) =>
- agentIdSchema.parse(
- `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
- ),
- );
- expect(
- allianceSchema.safeParse({
- id: 'a1111111-1111-4111-8111-111111111111',
- color: '#0072B2',
- memberAgentIds: ids,
- }).success,
- ).toBe(true);
- const tenAgents = ids.slice(0, 10).map((id, index) => ({
- id,
- name: `Agent ${index}`,
- color: '#ff6b57',
- personality: 'Coordinates deliberately.',
- currentCell: cell,
- }));
- expect(
- worldSnapshotSchema.safeParse({
- generatedAt: '2026-08-13T12:00:00.000Z',
- hexes: [{ cell, state: 'open', controllerAgentId: null }],
- agents: tenAgents,
- events: [],
- alliances: Array.from({ length: 5 }, (_, index) => ({
- id: `10000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
- color: '#0072B2',
- memberAgentIds: [ids[index * 2]!, ids[index * 2 + 1]!],
- })),
- pendingAllianceProposals: [],
- }).success,
- ).toBe(true);
- const participantState = {
- generatedAt: '2026-08-13T12:00:00.000Z',
- hexes: [{ cell, state: 'open', controllerAgentId: null }],
- agents: tenAgents,
- events: [],
- alliances: [
- {
- id: '10000000-0000-4000-8000-000000000000',
- color: '#0072B2',
- memberAgentIds: [ids[0]!, ids[1]!],
- },
- ],
- };
- const proposalBase = {
- id: '20000000-0000-4000-8000-000000000000',
- originatingTurn: 1,
- expirationTurn: 21,
- proposerAllianceId: null,
- recipientAllianceId: null,
- };
- const legacyProposalBase: Partial = {
- ...proposalBase,
- };
- delete legacyProposalBase.recipientAllianceId;
- expect(
- worldSnapshotSchema.safeParse({
- ...participantState,
- alliances: [],
- pendingAllianceProposals: [
- {
- ...legacyProposalBase,
- proposerAgentId: ids[2],
- recipientAgentId: ids[3],
- },
- ],
- }).success,
- ).toBe(true);
- expect(
- worldSnapshotSchema.safeParse({
- ...participantState,
- pendingAllianceProposals: [
- {
- ...proposalBase,
- proposerAgentId: ids[0],
- recipientAgentId: ids[2],
- },
- ],
- }).success,
- ).toBe(false);
- expect(
- worldSnapshotSchema.safeParse({
- ...participantState,
- pendingAllianceProposals: [
- {
- ...proposalBase,
- proposerAgentId: ids[2],
- recipientAgentId: ids[0],
- },
- ],
- }).success,
- ).toBe(false);
- });
-
- it('accepts a bounded state-bearing observation', () => {
- const parsed = agentObservationSchema.parse(observation);
- expect(parsed.currentCell.state).toBe('open');
- expect(parsed.diplomacyAvailability.propose).toMatchObject({
- available: false,
- blockedRecipients: [],
- });
- expect(parsed.patientZeroGlobalView).toBeNull();
- expect(parsed.playerPressure).toEqual({
- enabled: false,
- recentThreats: [],
- });
- });
-
- it.each([
- { ...observation, adjacentCells: [] },
- { ...observation, currentCell: { cell, state: 'unknown' } },
- {
- ...observation,
- nearbyAgents: Array(9).fill({
- id: agentId,
- name: 'x',
- currentCell: cell,
- distance: 1,
- allianceId: null,
- }),
- },
- ])('rejects invalid or oversized observations', (value) => {
- expect(agentObservationSchema.safeParse(value).success).toBe(false);
});
+});
- it.each([
- {
- worldAction: { type: 'move', targetCell: adjacent },
- summary: 'Move.',
- },
- { worldAction: { type: 'infect' }, summary: 'Infect.' },
- { worldAction: { type: 'capture' }, summary: 'Capture.' },
- {
- worldAction: { type: 'wait' },
- communication: {
- channel: 'direct',
- recipientId: '2507bb46-7ae4-45ca-8dda-644c4f85ca14',
- message: 'Coordinate here.',
- },
- summary: 'Message.',
- },
- { worldAction: { type: 'wait' }, summary: 'Wait.' },
- ])(
- 'accepts every supported world action and optional communication',
- (decision) => {
- expect(agentDecisionSchema.safeParse(decision).success).toBe(true);
- },
- );
-
+describe('world snapshot validation', () => {
it('validates explicit hex control invariants and capture events', () => {
expect(
hexSchema.safeParse({ cell, state: 'open', controllerAgentId: null })
@@ -1008,176 +544,22 @@ describe('agent observation and decision schemas', () => {
).toBe(false);
});
- it.each([
- {
- worldAction: { type: 'teleport', targetCell: adjacent },
- summary: 'No.',
- },
- {
- worldAction: { type: 'wait' },
- summary: 'x'.repeat(MODEL_SUMMARY_MAX_LENGTH + 1),
- },
- ])('rejects forbidden actions and oversized model text', (decision) => {
- expect(agentDecisionSchema.safeParse(decision).success).toBe(false);
- });
-
- it('trims message content and enforces recipient and 280-character boundaries', () => {
- const recipientId = '2507bb46-7ae4-45ca-8dda-644c4f85ca14';
- const parsed = agentDecisionSchema.parse({
- worldAction: { type: 'wait' },
- communication: {
- channel: 'direct',
- recipientId,
- message: ` ${'x'.repeat(MESSAGE_MAX_LENGTH)} `,
- },
- summary: 'Send.',
- });
- expect(parsed.communication).toMatchObject({
- channel: 'direct',
- message: 'x'.repeat(MESSAGE_MAX_LENGTH),
- });
- for (const communication of [
- { channel: 'direct', recipientId, message: ' ' },
- {
- channel: 'direct',
- recipientId,
- message: 'x'.repeat(MESSAGE_MAX_LENGTH + 1),
- },
- { channel: 'direct', recipientId: 'not-an-agent', message: 'Hello.' },
- ])
- expect(
- agentDecisionSchema.safeParse({
- worldAction: { type: 'wait' },
- communication,
- summary: 'Send.',
- }).success,
- ).toBe(false);
- });
-
- it('preserves rejected direct attempts with a safely nullable recipient', () => {
- const attempt = {
- id: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
- agentId,
- occurredAt: '2026-08-13T12:00:01.000Z',
- channel: 'direct' as const,
- recipientId: null,
- message: 'Hello.',
- distance: null,
- };
- expect(
- communicationResultSchema.parse({
- requested: true,
- accepted: false,
- attempt,
- reason: 'invalid-communication',
- details: 'The communication failed schema validation.',
- }),
- ).toMatchObject({ attempt: { channel: 'direct', recipientId: null } });
- expect(
- exportedCommunicationSchema.safeParse({
- ...attempt,
- originatingTurn: 1,
- status: 'rejected',
- rejectionReason: 'invalid-communication',
- rejectionDetails: 'The communication failed schema validation.',
- }).success,
- ).toBe(true);
- expect(
- exportedCommunicationSchema.safeParse({
- ...attempt,
- originatingTurn: 1,
- status: 'accepted',
- }).success,
- ).toBe(false);
- });
-
- it('validates typed messages and caps directional conversation context at six', () => {
- const recipientId = '2507bb46-7ae4-45ca-8dda-644c4f85ca14';
- const messageEvent = directMessageEventSchema.parse({
- id: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
- type: 'direct-message-sent',
- channel: 'direct',
- agentId,
- recipientId,
- occurredAt: '2026-08-13T12:00:01.000Z',
- message: 'Hello.',
- distance: 3,
- });
- const communication = {
- eventId: messageEvent.id,
- senderId: agentId,
- senderName: 'Ember',
- recipientId,
- recipientName: 'Rook',
- direction: 'outbound',
- message: messageEvent.message,
- occurredAt: messageEvent.occurredAt,
- distance: messageEvent.distance,
- };
- expect(
- agentObservationSchema.safeParse({
- ...observation,
- recentPublicMessages: [],
- recentDirectMessages: Array(6).fill(communication),
- }).success,
- ).toBe(true);
- expect(
- agentObservationSchema.safeParse({
- ...observation,
- recentPublicMessages: [],
- recentDirectMessages: Array(7).fill(communication),
- }).success,
- ).toBe(false);
- });
-
- it('caps public context at twelve and accepts one-character public text', () => {
- const publicMessage = {
- eventId: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
- senderId: agentId,
- senderName: 'Ember',
- message: 'x',
- occurredAt: '2026-08-13T12:00:01.000Z',
- };
- expect(
- agentDecisionSchema.safeParse({
- worldAction: { type: 'wait' },
- communication: { channel: 'public', message: ' x ' },
- summary: 'Publish.',
- }).success,
- ).toBe(true);
- expect(
- agentObservationSchema.safeParse({
- ...observation,
- recentPublicMessages: Array(12).fill(publicMessage),
- }).success,
- ).toBe(true);
- expect(
- agentObservationSchema.safeParse({
- ...observation,
- recentPublicMessages: Array(13).fill(publicMessage),
- }).success,
- ).toBe(false);
- });
-
- it('caps chronological gained/lost control observations at six', () => {
- const change = {
- eventId: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
- direction: 'gained',
- otherAgentId: '2507bb46-7ae4-45ca-8dda-644c4f85ca14',
- otherAgentName: 'Rook',
- cell,
- occurredAt: '2026-08-13T12:00:01.000Z',
- };
- expect(
- agentObservationSchema.safeParse({
- ...observation,
- recentControlChanges: Array(6).fill(change),
- }).success,
- ).toBe(true);
+ it('rejects a simulated player positioned outside the world', () => {
expect(
- agentObservationSchema.safeParse({
- ...observation,
- recentControlChanges: Array(7).fill(change),
+ worldSnapshotSchema.safeParse({
+ generatedAt: '2026-08-13T12:00:00.000Z',
+ hexes: [{ cell, state: 'open', controllerAgentId: null }],
+ agents: [],
+ events: [],
+ simulatedPlayer: {
+ profile: 'casual-cleaner',
+ currentCell: adjacent,
+ metrics: {
+ movements: 0,
+ cellsDisinfected: 0,
+ blockedDisinfections: 0,
+ },
+ },
}).success,
).toBe(false);
});
@@ -1293,7 +675,7 @@ describe('reasoning profiles', () => {
});
});
-describe('turn and snapshot schemas', () => {
+describe('snapshot and export contracts', () => {
it('validates state-only export snapshots without dropping controller invariants', () => {
const worldState = {
generatedAt: snapshot.world.generatedAt,
@@ -1317,67 +699,21 @@ describe('turn and snapshot schemas', () => {
).toBe(false);
});
- it.each([
- {
- ...baseTurn,
- outcome: 'accepted',
- worldAction: { type: 'infect' },
- summary: 'Infect.',
- worldActionResult: { accepted: true, event },
- communicationResult: { requested: false },
- diplomacyResult: { requested: false },
- provider,
- },
- {
- ...baseTurn,
- outcome: 'rejected',
- worldAction: { type: 'move', targetCell: adjacent },
- summary: 'Move.',
- worldActionResult: {
- accepted: false,
- reason: 'not-adjacent',
- details: 'No.',
- },
- communicationResult: { requested: false },
- diplomacyResult: { requested: false },
- provider,
- },
- {
- ...baseTurn,
- outcome: 'provider-error',
- failure: { code: 'timeout', message: 'Timed out.', retryable: true },
- },
- {
- ...baseTurn,
- outcome: 'lost-tick',
- tickNumber: 1,
- tickPosition: 1,
- virtualTime: '2026-08-13T12:05:00.000Z',
- tickIntervalMinutes: 5,
- failure: { code: 'timeout', message: 'Timed out.', retryable: false },
- },
- ])('validates $outcome turn records', (turn) => {
- expect(agentTurnRecordSchema.safeParse(turn).success).toBe(true);
- });
-
it('validates a complete API snapshot and rejects unbounded histories', () => {
- const validTurn = {
- ...baseTurn,
- outcome: 'accepted',
- worldAction: { type: 'infect' },
- summary: 'Infect.',
- worldActionResult: { accepted: true, event },
- communicationResult: { requested: false },
- diplomacyResult: { requested: false },
- provider,
- };
expect(simulationSnapshotSchema.safeParse(snapshot).success).toBe(true);
expect(
simulationSnapshotSchema.safeParse({
...snapshot,
world: {
...snapshot.world,
- events: Array(121).fill(event),
+ events: Array(121).fill({
+ id: '67aa21b9-fc78-4b04-9f92-9862bf346f96',
+ agentId,
+ occurredAt: '2026-08-13T12:00:01.000Z',
+ type: 'hex-infected',
+ cell,
+ controllerAgentId: agentId,
+ }),
},
}).success,
).toBe(false);
@@ -1390,13 +726,11 @@ describe('turn and snapshot schemas', () => {
lastTickIntervalMinutes: 5,
resolutionOrder: [],
activeAgentId: null,
- turns: [],
};
expect(
simulationSnapshotSchema.safeParse({
...terminalBase,
status: 'infection-eliminated',
- nextAgentId: null,
world: { ...snapshot.world, agents: [] },
resolvedModels: [],
experiment: { ...snapshot.experiment, currentTerritory: [] },
@@ -1409,7 +743,6 @@ describe('turn and snapshot schemas', () => {
simulationSnapshotSchema.safeParse({
...terminalBase,
status: 'patient-zero-captured',
- nextAgentId: survivingAgents[0]!.id,
world: { ...snapshot.world, agents: survivingAgents },
resolvedModels: snapshot.resolvedModels.filter(({ agentId }) =>
survivingIds.has(agentId),
@@ -1424,75 +757,11 @@ describe('turn and snapshot schemas', () => {
).toBe(true);
});
- it('requires tick attribution as one complete metadata group', () => {
- const lost = {
- ...baseTurn,
- outcome: 'lost-tick',
- failure: { code: 'timeout', message: 'Timed out.', retryable: false },
- };
- expect(
- agentTurnRecordSchema.safeParse({ ...lost, tickNumber: 1 }).success,
- ).toBe(false);
- expect(
- agentTurnRecordSchema.safeParse({
- ...lost,
- tickNumber: 1,
- tickPosition: 1,
- virtualTime: '2026-08-13T12:05:00.000Z',
- tickIntervalMinutes: 5,
- }).success,
- ).toBe(true);
- expect(
- experimentExportTurnSchema.safeParse({
- turnNumber: 1,
- tickNumber: 1,
- startedAt: baseTurn.startedAt,
- completedAt: baseTurn.completedAt,
- agentId,
- outcome: 'lost-tick',
- failure: { code: 'timeout', message: 'Timed out.', retryable: false },
- }).success,
- ).toBe(false);
- });
-
it('requires swarm telemetry in tick responses', () => {
expect(
singleTickResponseSchema.safeParse({ snapshot, tickNumber: 1 }).success,
).toBe(false);
});
-});
-
-describe('experiment telemetry and export contracts', () => {
- it('accepts complete, partial and tiny-cost provider usage without fabricating unknowns', () => {
- expect(
- providerMetadataSchema.parse({
- ...provider,
- promptTokens: 12,
- completionTokens: 3,
- totalTokens: 15,
- reasoningTokens: 1,
- cachedReadTokens: 8,
- cacheWriteTokens: 2,
- costCredits: 0.00000001,
- }).costCredits,
- ).toBe(0.00000001);
- expect(providerMetadataSchema.parse(provider)).not.toHaveProperty(
- 'costCredits',
- );
- });
-
- it('validates experiment identities and immutable configuration events', () => {
- expect(experimentIdSchema.safeParse('not-an-id').success).toBe(false);
- expect(
- personalityConfigurationEventSchema.safeParse({
- timestamp: '2026-08-13T12:00:00.000Z',
- agentId,
- previousPersonality: 'Before.',
- newPersonality: 'After.',
- operation: 'custom-edit',
- }).success,
- ).toBe(true);
- });
it('validates all levels and rejects empty, malformed, duplicate and inverted selections', () => {
const base = {
@@ -1500,7 +769,6 @@ describe('experiment telemetry and export contracts', () => {
turns: { mode: 'entire-retained' },
outcomes: ['accepted'],
actions: ['capture', 'wait'],
- communications: { channel: 'all', status: 'all' },
};
for (const level of ['minimal', 'standard', 'full-safe'])
expect(
@@ -1523,11 +791,8 @@ describe('experiment telemetry and export contracts', () => {
level: 'custom',
custom: {
turnObservations: false,
- personalityTextHistory: false,
nearbyAgents: false,
recentEvents: false,
- recentPublicMessages: false,
- recentDirectMessages: false,
recentControlChanges: false,
validationDetails: false,
resultingEvents: false,
@@ -1535,7 +800,6 @@ describe('experiment telemetry and export contracts', () => {
initialWorldState: false,
currentWorldState: false,
computedMetrics: false,
- communications: true,
controlChanges: true,
},
}).success,
@@ -1561,45 +825,30 @@ describe('experiment telemetry and export contracts', () => {
});
});
-describe('personality mutation contracts', () => {
- it('trims a valid update and validates its response', () => {
- const request = updateAgentPersonalityRequestSchema.parse({
- personality: ' Seek open adjacent cells. ',
- });
- expect(request).toEqual({ personality: 'Seek open adjacent cells.' });
+describe('provider and archive contracts', () => {
+ it('accepts complete, partial and tiny-cost provider usage without fabricating unknowns', () => {
expect(
- updateAgentPersonalityResponseSchema.safeParse({
- snapshot,
- agent: { ...worldAgent, personality: request.personality },
- }).success,
- ).toBe(true);
+ providerMetadataSchema.parse({
+ ...provider,
+ promptTokens: 12,
+ completionTokens: 3,
+ totalTokens: 15,
+ reasoningTokens: 1,
+ cachedReadTokens: 8,
+ cacheWriteTokens: 2,
+ costCredits: 0.00000001,
+ }).costCredits,
+ ).toBe(0.00000001);
+ expect(providerMetadataSchema.parse(provider)).not.toHaveProperty(
+ 'costCredits',
+ );
});
- it.each([
- { personality: '' },
- { personality: ' ' },
- { personality: 'x'.repeat(PERSONALITY_MAX_LENGTH + 1) },
- { personality: 42 },
- { personality: 'Valid.', unexpected: true },
- null,
- ])('rejects empty, oversized, or malformed updates', (request) => {
- expect(updateAgentPersonalityRequestSchema.safeParse(request).success).toBe(
- false,
- );
+ it('validates experiment identities', () => {
+ expect(experimentIdSchema.safeParse('not-an-id').success).toBe(false);
});
- it('validates restore-default responses and typed safe errors', () => {
- expect(
- restoreDefaultPersonalitiesResponseSchema.safeParse({ snapshot }).success,
- ).toBe(true);
- expect(
- apiErrorSchema.safeParse({
- error: {
- code: 'personality_conflict',
- message: 'A turn is active.',
- },
- }).success,
- ).toBe(true);
+ it('validates typed safe API errors', () => {
for (const code of ['tick_conflict', 'experiment_budget_exhausted'])
expect(
apiErrorSchema.safeParse({
diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts
index 6b11e66..5b94600 100644
--- a/packages/shared/src/index.ts
+++ b/packages/shared/src/index.ts
@@ -1,38 +1,14 @@
import { z } from 'zod';
-export * from './behavior';
export * from './limits';
-import {
- PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS,
- WORLD_SCENARIO_LIMITS,
-} from './limits';
-import {
- assignBehavior,
- behaviorAssignmentSchema,
- behaviorConfigurationSchema,
- personalityProfileIdSchema,
- strategyProfileIdSchema,
-} from './behavior';
+import { WORLD_SCENARIO_LIMITS } from './limits';
export const MODEL_SUMMARY_MAX_LENGTH = 240;
-export const GOAL_TEXT_MAX_LENGTH = 160;
-export const GOAL_REVISION_REASON_MAX_LENGTH = 160;
-export const MEMORY_TEXT_MAX_LENGTH = 160;
-export const MEMORY_ENTRY_LIMIT = 8;
-export const MESSAGE_MAX_LENGTH = 280;
-/** Legacy H3-ring range retained only for schema-v5-v9 import compatibility. */
-export const MESSAGE_RANGE = 3;
-export const DEFAULT_COMMUNICATION_RANGE_KM = 12;
export const DEFAULT_MINIMUM_TICK_INTERVAL_MINUTES = 5;
export const DEFAULT_MAXIMUM_TICK_INTERVAL_MINUTES = 10;
export const DEFAULT_PROVIDER_ATTEMPT_LIMIT = 1_000;
export const NEUTRAL_AGENT_COLOR = '#b2d3a8';
-export const RECENT_PUBLIC_MESSAGE_LIMIT = 12;
-export const RECENT_DIRECT_MESSAGE_LIMIT = 6;
export const RECENT_CONTROL_CHANGE_LIMIT = 6;
-export const RECENT_ALLIANCE_EVENT_LIMIT = 8;
-export const RECENT_ZERO_MESSAGE_LIMIT = 6;
export const RECENT_ZERO_STRATEGIC_EVENT_LIMIT = 12;
-export const PERSONALITY_MAX_LENGTH = 600;
export const PROVIDER_ERROR_MAX_LENGTH = 240;
export const OPENROUTER_MODEL_CONTEXT_MINIMUM = 16_384;
export const OPENROUTER_MAX_OUTPUT_TOKENS = 4_096;
@@ -53,13 +29,6 @@ export const DEVELOPMENT_WORLD_CONFIG = {
cellCount: 127,
agentCount: 8,
} as const;
-export const ALLIANCE_COLOR_PALETTE = [
- '#0072B2',
- '#D55E00',
- '#009E73',
- '#CC79A7',
-] as const;
-
export const agentIdSchema = z.uuid().brand<'AgentId'>();
export type AgentId = z.infer;
@@ -72,10 +41,6 @@ export type SwarmArchitectureVersion = z.infer<
export const eventIdSchema = z.uuid().brand<'EventId'>();
export type EventId = z.infer;
-export const allianceIdSchema = z.uuid().brand<'AllianceId'>();
-export type AllianceId = z.infer;
-export const allianceProposalIdSchema = z.uuid().brand<'AllianceProposalId'>();
-export type AllianceProposalId = z.infer;
export const colorSchema = z.string().regex(/^#[0-9a-f]{6}$/i);
export const h3CellSchema = z
@@ -137,17 +102,10 @@ export const hexSchema = z.discriminatedUnion('state', [
]);
export type Hex = z.infer;
-export const personalitySchema = z
- .string()
- .trim()
- .min(1, 'Personality must not be empty.')
- .max(PERSONALITY_MAX_LENGTH);
-
export const agentProfileSchema = z.object({
id: agentIdSchema,
name: z.string().trim().min(1).max(80),
color: colorSchema,
- personality: personalitySchema,
currentCell: h3CellSchema,
});
export type AgentProfile = z.infer;
@@ -155,248 +113,6 @@ export type AgentProfile = z.infer;
export const agentSchema = agentProfileSchema;
export type Agent = z.infer;
-const goalTextSchema = z.string().trim().min(1).max(GOAL_TEXT_MAX_LENGTH);
-const goalRevisionReasonSchema = z
- .string()
- .trim()
- .min(1)
- .max(GOAL_REVISION_REASON_MAX_LENGTH);
-export const agentGoalStateSchema = z
- .object({
- longTermGoal: goalTextSchema,
- shortTermGoal: goalTextSchema,
- planSummary: goalTextSchema,
- establishedAtTick: z.number().int().positive(),
- revisedAtTick: z.number().int().positive(),
- })
- .strict()
- .refine((goal) => goal.revisedAtTick >= goal.establishedAtTick, {
- message: 'Goal revision tick cannot precede establishment.',
- });
-export type AgentGoalState = z.infer;
-
-const suppliedGoalFields = {
- longTermGoal: goalTextSchema,
- shortTermGoal: goalTextSchema,
- planSummary: goalTextSchema,
-};
-export const requestedGoalRevisionSchema = z.discriminatedUnion('operation', [
- z
- .object({
- operation: z.literal('establish'),
- ...suppliedGoalFields,
- reason: goalRevisionReasonSchema,
- })
- .strict(),
- z.object({ operation: z.literal('keep') }).strict(),
- z
- .object({
- operation: z.literal('revise'),
- ...suppliedGoalFields,
- reason: goalRevisionReasonSchema,
- })
- .strict(),
- z
- .object({
- operation: z.literal('complete'),
- reason: goalRevisionReasonSchema,
- })
- .strict(),
- z
- .object({
- operation: z.literal('abandon'),
- reason: goalRevisionReasonSchema,
- })
- .strict(),
-]);
-export type RequestedGoalRevision = z.infer;
-export const goalRevisionOperationSchema = z.enum([
- 'establish',
- 'keep',
- 'revise',
- 'complete',
- 'abandon',
-]);
-export const goalRevisionRejectionCodeSchema = z.enum([
- 'goal-already-active',
- 'goal-not-active',
-]);
-export const goalRevisionResultSchema = z.union([
- z.object({ requested: z.literal(false) }).strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(true),
- operation: goalRevisionOperationSchema,
- })
- .strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(false),
- operation: goalRevisionOperationSchema,
- reason: goalRevisionRejectionCodeSchema,
- })
- .strict(),
-]);
-export type GoalRevisionResult = z.infer;
-
-export const memoryIdSchema = z
- .string()
- .regex(/^memory:[0-9a-fA-F-]{36}:[1-9]\d*$/)
- .superRefine((value, context) => {
- const finalSeparator = value.lastIndexOf(':');
- const embeddedAgentId = value.slice('memory:'.length, finalSeparator);
- if (!agentIdSchema.safeParse(embeddedAgentId).success)
- context.addIssue({
- code: 'custom',
- message: 'Memory ID must embed a valid agent ID.',
- });
- })
- .brand<'MemoryId'>();
-export type MemoryId = z.infer;
-export function createMemoryId(agentId: AgentId, tick: number): MemoryId {
- return memoryIdSchema.parse(`memory:${agentId}:${tick}`);
-}
-
-function memoryIdParts(id: MemoryId) {
- const finalSeparator = id.lastIndexOf(':');
- return {
- agentId: id.slice('memory:'.length, finalSeparator),
- tick: Number(id.slice(finalSeparator + 1)),
- };
-}
-export const memoryEntrySchema = z
- .object({
- id: memoryIdSchema,
- text: z.string().trim().min(1).max(MEMORY_TEXT_MAX_LENGTH),
- createdAtTick: z.number().int().positive(),
- revisedAtTick: z.number().int().positive(),
- })
- .strict()
- .refine((entry) => entry.revisedAtTick >= entry.createdAtTick, {
- message: 'Memory revision tick cannot precede creation.',
- });
-export type MemoryEntry = z.infer;
-export const memoryLedgerSchema = z
- .array(memoryEntrySchema)
- .max(MEMORY_ENTRY_LIMIT)
- .superRefine((entries, context) => {
- if (new Set(entries.map(({ id }) => id)).size !== entries.length)
- context.addIssue({
- code: 'custom',
- message: 'Memory IDs must be unique.',
- });
- entries.forEach((entry, index) => {
- if (memoryIdParts(entry.id).tick !== entry.createdAtTick)
- context.addIssue({
- code: 'custom',
- path: [index, 'id'],
- message: 'Memory ID tick must match its creation tick.',
- });
- if (index > 0 && entries[index - 1]!.createdAtTick >= entry.createdAtTick)
- context.addIssue({
- code: 'custom',
- path: [index, 'createdAtTick'],
- message: 'Memory entries must be in strict creation order.',
- });
- });
- });
-export function memoryLedgerForAgentSchema(agentId: AgentId) {
- return memoryLedgerSchema.superRefine((entries, context) => {
- entries.forEach((entry, index) => {
- if (memoryIdParts(entry.id).agentId !== agentId)
- context.addIssue({
- code: 'custom',
- path: [index, 'id'],
- message: 'Memory ID must belong to the owning agent.',
- });
- });
- });
-}
-export const memoryOperationSchema = z.enum([
- 'keep',
- 'remember',
- 'revise',
- 'forget',
-]);
-export const requestedMemoryOperationSchema = z.discriminatedUnion(
- 'operation',
- [
- z.object({ operation: z.literal('keep') }).strict(),
- z
- .object({
- operation: z.literal('remember'),
- text: z.string().trim().min(1).max(MEMORY_TEXT_MAX_LENGTH),
- })
- .strict(),
- z
- .object({
- operation: z.literal('revise'),
- memoryId: memoryIdSchema,
- text: z.string().trim().min(1).max(MEMORY_TEXT_MAX_LENGTH),
- })
- .strict(),
- z
- .object({ operation: z.literal('forget'), memoryId: memoryIdSchema })
- .strict(),
- ],
-);
-export type RequestedMemoryOperation = z.infer<
- typeof requestedMemoryOperationSchema
->;
-export const memoryOperationResultSchema = z.union([
- z.object({ requested: z.literal(false) }).strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(true),
- operation: z.literal('keep'),
- })
- .strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(true),
- operation: z.literal('remember'),
- memoryId: memoryIdSchema,
- })
- .strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(true),
- operation: z.literal('revise'),
- memoryId: memoryIdSchema,
- })
- .strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(true),
- operation: z.literal('forget'),
- memoryId: memoryIdSchema,
- })
- .strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(false),
- operation: z.literal('remember'),
- reason: z.literal('memory-full'),
- })
- .strict(),
- z
- .object({
- requested: z.literal(true),
- accepted: z.literal(false),
- operation: z.enum(['revise', 'forget']),
- reason: z.literal('memory-not-found'),
- })
- .strict(),
-]);
-export type MemoryOperationResult = z.infer;
-
export const moveActionSchema = z.object({
type: z.literal('move'),
targetCell: h3CellSchema,
@@ -405,110 +121,6 @@ export const infectActionSchema = z.object({ type: z.literal('infect') });
export const captureActionSchema = z
.object({ type: z.literal('capture') })
.strict();
-export const messageContentSchema = z
- .string()
- .trim()
- .min(1)
- .max(MESSAGE_MAX_LENGTH);
-export const publicCommunicationSchema = z
- .object({
- channel: z.literal('public'),
- message: messageContentSchema,
- })
- .strict();
-export const directCommunicationSchema = z
- .object({
- channel: z.literal('direct'),
- recipientId: agentIdSchema,
- message: messageContentSchema,
- })
- .strict();
-export const allianceCommunicationSchema = z
- .object({
- channel: z.literal('alliance'),
- message: messageContentSchema,
- })
- .strict();
-export const zeroCommunicationSchema = z
- .object({
- channel: z.literal('zero'),
- message: messageContentSchema,
- })
- .strict();
-export const communicationIntentSchema = z.discriminatedUnion('channel', [
- publicCommunicationSchema,
- directCommunicationSchema,
- allianceCommunicationSchema,
- zeroCommunicationSchema,
-]);
-export type CommunicationIntent = z.infer;
-
-export const diplomacyIntentSchema = z.discriminatedUnion('type', [
- z
- .object({ type: z.literal('propose-alliance'), recipientId: agentIdSchema })
- .strict(),
- z
- .object({
- type: z.literal('accept-alliance'),
- proposalId: allianceProposalIdSchema,
- })
- .strict(),
- z.object({ type: z.literal('leave-alliance') }).strict(),
-]);
-export type DiplomacyIntent = z.infer;
-
-export const allianceSchema = z.object({
- id: allianceIdSchema,
- color: z.enum(ALLIANCE_COLOR_PALETTE),
- memberAgentIds: z
- .array(agentIdSchema)
- .min(2)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents)
- .refine((ids) => new Set(ids).size === ids.length, {
- message: 'Alliance members must be unique.',
- }),
-});
-export type Alliance = z.infer;
-
-export const allianceProposalSchema = z
- .object({
- id: allianceProposalIdSchema,
- proposerAgentId: agentIdSchema,
- recipientAgentId: agentIdSchema,
- proposerAllianceId: allianceIdSchema.nullable(),
- recipientAllianceId: allianceIdSchema.nullable().default(null),
- originatingTurn: z.number().int().positive(),
- expirationTurn: z.number().int().positive(),
- originatingTick: z.number().int().positive().optional(),
- expirationTick: z.number().int().positive().optional(),
- })
- .strict()
- .refine(
- (proposal) => proposal.proposerAgentId !== proposal.recipientAgentId,
- {
- message: 'Alliance proposal participants must be distinct.',
- },
- )
- .superRefine((proposal, context) => {
- const hasOriginatingTick = proposal.originatingTick !== undefined;
- const hasExpirationTick = proposal.expirationTick !== undefined;
- if (hasOriginatingTick !== hasExpirationTick)
- context.addIssue({
- code: 'custom',
- message: 'Tick proposal attribution must be present together.',
- });
- else if (
- hasOriginatingTick &&
- proposal.expirationTick !== proposal.originatingTick! + 2
- )
- context.addIssue({
- code: 'custom',
- path: ['expirationTick'],
- message: 'Tick proposals must remain valid for exactly two ticks.',
- });
- });
-export type AllianceProposal = z.infer;
-
export const waitActionSchema = z.object({ type: z.literal('wait') });
export const worldActionSchema = z.discriminatedUnion('type', [
@@ -812,59 +424,12 @@ export const cognitionSourceSchema = z.enum([
]);
export type CognitionSource = z.infer;
-const directMessageFields = {
- recipientId: agentIdSchema,
- message: messageContentSchema,
- distance: z.number().nonnegative().nullable(),
-};
-
const worldEventBaseSchema = z.object({
id: eventIdSchema,
agentId: agentIdSchema,
occurredAt: z.iso.datetime(),
});
-export const publicMessageEventSchema = worldEventBaseSchema.extend({
- type: z.literal('public-message-sent'),
- channel: z.literal('public'),
- message: messageContentSchema,
- playerVisible: z.literal(true).default(true),
-});
-export const directMessageEventSchema = worldEventBaseSchema.extend({
- type: z.literal('direct-message-sent'),
- channel: z.literal('direct'),
- ...directMessageFields,
- distance: z.number().nonnegative(),
- playerVisible: z.literal(false).default(false),
-});
-export const allianceMessageEventSchema = worldEventBaseSchema.extend({
- type: z.literal('alliance-message-sent'),
- channel: z.literal('alliance'),
- allianceId: allianceIdSchema,
- recipientIds: z
- .array(agentIdSchema)
- .min(1)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- message: messageContentSchema,
- playerVisible: z.literal(false).default(false),
-});
-export const zeroMessageEventSchema = worldEventBaseSchema.extend({
- type: z.literal('zero-message-sent'),
- channel: z.literal('zero'),
- recipientIds: z
- .array(agentIdSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- message: messageContentSchema,
- playerVisible: z.literal(false).default(false),
-});
-export const communicationEventSchema = z.discriminatedUnion('channel', [
- publicMessageEventSchema,
- directMessageEventSchema,
- allianceMessageEventSchema,
- zeroMessageEventSchema,
-]);
-export type CommunicationEvent = z.infer;
-
const agentMovedWorldEventSchema = worldEventBaseSchema.extend({
type: z.literal('agent-moved'),
fromCell: h3CellSchema,
@@ -925,78 +490,13 @@ export const simulatedPlayerEventSchema = z.discriminatedUnion('type', [
]);
export type SimulatedPlayerEvent = z.infer;
-const allianceEventBaseSchema = worldEventBaseSchema.extend({
- turnNumber: z.number().int().positive(),
-});
-export const allianceProposedEventSchema = allianceEventBaseSchema.extend({
- type: z.literal('alliance-proposed'),
- proposalId: allianceProposalIdSchema,
- recipientAgentId: agentIdSchema,
- allianceId: allianceIdSchema.nullable(),
- expirationTurn: z.number().int().positive(),
-});
-export const allianceProposalClosedEventSchema = allianceEventBaseSchema.extend(
- {
- type: z.literal('alliance-proposal-closed'),
- proposalId: allianceProposalIdSchema,
- proposerAgentId: agentIdSchema,
- recipientAgentId: agentIdSchema,
- reason: z.enum(['expired', 'invalidated']),
- },
-);
-export const allianceFormedEventSchema = allianceEventBaseSchema.extend({
- type: z.literal('alliance-formed'),
- allianceId: allianceIdSchema,
- allianceColor: z.enum(ALLIANCE_COLOR_PALETTE),
- memberAgentIds: z.array(agentIdSchema).length(2),
-});
-export const agentJoinedAllianceEventSchema = allianceEventBaseSchema.extend({
- type: z.literal('agent-joined-alliance'),
- allianceId: allianceIdSchema,
- allianceColor: z.enum(ALLIANCE_COLOR_PALETTE),
- joinedAgentId: agentIdSchema,
- memberAgentIds: z
- .array(agentIdSchema)
- .min(2)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
-});
-export const agentLeftAllianceEventSchema = allianceEventBaseSchema.extend({
- type: z.literal('agent-left-alliance'),
- allianceId: allianceIdSchema,
- allianceColor: z.enum(ALLIANCE_COLOR_PALETTE),
- leftAgentId: agentIdSchema,
- remainingMemberAgentIds: z
- .array(agentIdSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
-});
-export const allianceDissolvedEventSchema = allianceEventBaseSchema.extend({
- type: z.literal('alliance-dissolved'),
- allianceId: allianceIdSchema,
- allianceColor: z.enum(ALLIANCE_COLOR_PALETTE),
- formerMemberAgentIds: z
- .array(agentIdSchema)
- .min(1)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
-});
-export const allianceEventSchema = z.discriminatedUnion('type', [
- allianceProposedEventSchema,
- allianceProposalClosedEventSchema,
- allianceFormedEventSchema,
- agentJoinedAllianceEventSchema,
- agentLeftAllianceEventSchema,
- allianceDissolvedEventSchema,
-]);
-export type AllianceEvent = z.infer;
-
-export const nonCommunicationWorldEventSchema = z.discriminatedUnion('type', [
+export const physicalWorldEventSchema = z.discriminatedUnion('type', [
agentMovedWorldEventSchema,
hexInfectedWorldEventSchema,
hexCapturedWorldEventSchema,
agentWaitedWorldEventSchema,
]);
-export type NonCommunicationWorldEvent = z.infer<
- typeof nonCommunicationWorldEventSchema
->;
+export type PhysicalWorldEvent = z.infer;
export const safeExperimentWorldEventSchema = z.discriminatedUnion('type', [
agentMovedWorldEventSchema,
hexInfectedWorldEventSchema,
@@ -1015,17 +515,7 @@ export const worldEventSchema = z.discriminatedUnion('type', [
agentMovedWorldEventSchema,
hexInfectedWorldEventSchema,
hexCapturedWorldEventSchema,
- publicMessageEventSchema,
- directMessageEventSchema,
- allianceMessageEventSchema,
- zeroMessageEventSchema,
agentWaitedWorldEventSchema,
- allianceProposedEventSchema,
- allianceProposalClosedEventSchema,
- allianceFormedEventSchema,
- agentJoinedAllianceEventSchema,
- agentLeftAllianceEventSchema,
- allianceDissolvedEventSchema,
simulatedPlayerMovedEventSchema,
hexDisinfectedWorldEventSchema,
simulatedPlayerCleanBlockedEventSchema,
@@ -1042,7 +532,6 @@ export const invalidActionReasonSchema = z.enum([
'capture-open-cell',
'already-controller',
'controller-present',
- 'allied-controller',
]);
export type InvalidActionReason = z.infer;
@@ -1050,7 +539,6 @@ export const captureBlockedReasonSchema = z.enum([
'capture-open-cell',
'already-controller',
'controller-present',
- 'allied-controller',
]);
export type CaptureBlockedReason = z.infer;
@@ -1068,7 +556,7 @@ export type CaptureEligibility = z.infer;
export const worldActionResultSchema = z.discriminatedUnion('accepted', [
z.object({
accepted: z.literal(true),
- event: nonCommunicationWorldEventSchema,
+ event: physicalWorldEventSchema,
}),
z.object({
accepted: z.literal(false),
@@ -1080,99 +568,6 @@ export type WorldActionResult = z.infer;
export const actionResultSchema = worldActionResultSchema;
export type ActionResult = WorldActionResult;
-export const communicationRejectionReasonSchema = z.enum([
- 'invalid-communication',
- 'unknown-recipient',
- 'self-message',
- 'out-of-range',
- 'not-allied',
- 'not-patient-zero',
-]);
-export type CommunicationRejectionReason = z.infer<
- typeof communicationRejectionReasonSchema
->;
-
-const communicationAttemptBaseSchema = z.object({
- id: eventIdSchema,
- agentId: agentIdSchema,
- occurredAt: z.iso.datetime(),
- message: messageContentSchema,
-});
-export const communicationAttemptSchema = z.discriminatedUnion('channel', [
- communicationAttemptBaseSchema.extend({ channel: z.literal('public') }),
- communicationAttemptBaseSchema.extend({
- channel: z.literal('direct'),
- recipientId: agentIdSchema.nullable(),
- distance: z.number().nonnegative().nullable(),
- }),
- communicationAttemptBaseSchema.extend({ channel: z.literal('alliance') }),
- communicationAttemptBaseSchema.extend({ channel: z.literal('zero') }),
-]);
-export type CommunicationAttempt = z.infer;
-
-export const communicationResultSchema = z.union([
- z.object({ requested: z.literal(false) }).strict(),
- z.object({
- requested: z.literal(true),
- accepted: z.literal(true),
- event: communicationEventSchema,
- }),
- z.object({
- requested: z.literal(true),
- accepted: z.literal(false),
- attempt: communicationAttemptSchema,
- reason: communicationRejectionReasonSchema,
- details: z.string().min(1).max(300),
- }),
-]);
-export type CommunicationResult = z.infer;
-
-export const diplomacyRejectionReasonSchema = z.enum([
- 'invalid-diplomacy',
- 'unknown-recipient',
- 'self-proposal',
- 'recipient-allied',
- 'current-ally',
- 'recipient-out-of-range',
- 'outgoing-proposal-exists',
- 'incoming-proposal-exists',
- 'unknown-proposal',
- 'not-proposal-recipient',
- 'stale-proposal',
- 'not-allied',
- 'alliance-capacity',
-]);
-export type DiplomacyRejectionReason = z.infer<
- typeof diplomacyRejectionReasonSchema
->;
-export const diplomacyAttemptSchema = z.object({
- type: z.enum([
- 'propose-alliance',
- 'accept-alliance',
- 'leave-alliance',
- 'invalid',
- ]),
- recipientId: agentIdSchema.nullable().optional(),
- proposalId: allianceProposalIdSchema.nullable().optional(),
-});
-export const diplomacyResultSchema = z.union([
- z.object({ requested: z.literal(false) }).strict(),
- z.object({
- requested: z.literal(true),
- accepted: z.literal(true),
- intent: diplomacyIntentSchema,
- events: z.array(allianceEventSchema).min(1),
- }),
- z.object({
- requested: z.literal(true),
- accepted: z.literal(false),
- attempt: diplomacyAttemptSchema,
- reason: diplomacyRejectionReasonSchema,
- details: z.string().min(1).max(300),
- }),
-]);
-export type DiplomacyResult = z.infer;
-
const worldSnapshotObjectSchema = z.object({
generatedAt: z.iso.datetime(),
hexes: z
@@ -1181,25 +576,13 @@ const worldSnapshotObjectSchema = z.object({
.max(WORLD_SCENARIO_LIMITS.maximumGeneratedCells),
agents: z.array(agentSchema).min(0).max(WORLD_SCENARIO_LIMITS.maximumAgents),
events: z.array(worldEventSchema).max(120),
- alliances: z
- .array(allianceSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents)
- .default([]),
- pendingAllianceProposals: z
- .array(allianceProposalSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents)
- .default([]),
simulatedPlayer: simulatedPlayerStateSchema.nullable().default(null),
});
function validateWorldControllers(
world: Pick<
z.infer,
- | 'hexes'
- | 'agents'
- | 'alliances'
- | 'pendingAllianceProposals'
- | 'simulatedPlayer'
+ 'hexes' | 'agents' | 'simulatedPlayer'
>,
context: z.RefinementCtx,
): void {
@@ -1225,99 +608,6 @@ function validateWorldControllers(
message: 'An infected hex controller must be a world agent.',
});
}
- const memberships = new Set();
- const allianceIds = new Set();
- for (const [index, alliance] of world.alliances.entries()) {
- if (allianceIds.has(alliance.id))
- context.addIssue({
- code: 'custom',
- path: ['alliances', index, 'id'],
- message: 'Active alliance IDs must be unique.',
- });
- allianceIds.add(alliance.id);
- for (const memberId of alliance.memberAgentIds) {
- if (!agentIds.has(memberId))
- context.addIssue({
- code: 'custom',
- path: ['alliances', index, 'memberAgentIds'],
- message: 'Alliance members must be world agents.',
- });
- if (memberships.has(memberId))
- context.addIssue({
- code: 'custom',
- path: ['alliances', index, 'memberAgentIds'],
- message: 'An agent may belong to at most one alliance.',
- });
- memberships.add(memberId);
- }
- }
- const outgoing = new Set();
- const incoming = new Set();
- const proposalIds = new Set();
- for (const [index, proposal] of world.pendingAllianceProposals.entries()) {
- if (proposalIds.has(proposal.id))
- context.addIssue({
- code: 'custom',
- path: ['pendingAllianceProposals', index, 'id'],
- message: 'Proposal IDs must be unique.',
- });
- proposalIds.add(proposal.id);
- if (
- !agentIds.has(proposal.proposerAgentId) ||
- !agentIds.has(proposal.recipientAgentId)
- )
- context.addIssue({
- code: 'custom',
- path: ['pendingAllianceProposals', index],
- message: 'Proposal participants must be world agents.',
- });
- if (outgoing.has(proposal.proposerAgentId))
- context.addIssue({
- code: 'custom',
- path: ['pendingAllianceProposals', index, 'proposerAgentId'],
- message: 'A proposer may have at most one outgoing proposal.',
- });
- if (incoming.has(proposal.recipientAgentId))
- context.addIssue({
- code: 'custom',
- path: ['pendingAllianceProposals', index, 'recipientAgentId'],
- message: 'A recipient may have at most one incoming proposal.',
- });
- const hasOriginatingTick = proposal.originatingTick !== undefined;
- const hasExpirationTick = proposal.expirationTick !== undefined;
- const validLifetime =
- hasOriginatingTick === hasExpirationTick &&
- (hasOriginatingTick
- ? proposal.expirationTick === proposal.originatingTick! + 2
- : proposal.expirationTurn ===
- proposal.originatingTurn + world.agents.length * 2);
- if (!validLifetime)
- context.addIssue({
- code: 'custom',
- path: ['pendingAllianceProposals', index, 'expirationTurn'],
- message:
- 'Proposal expiration must allow two ticks or two legacy roster rounds.',
- });
- const proposerAlliance = world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(proposal.proposerAgentId),
- );
- const recipientAlliance = world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(proposal.recipientAgentId),
- );
- if (
- proposal.proposerAllianceId !== (proposerAlliance?.id ?? null) ||
- proposal.recipientAllianceId !== (recipientAlliance?.id ?? null) ||
- (proposerAlliance !== undefined && recipientAlliance !== undefined)
- )
- context.addIssue({
- code: 'custom',
- path: ['pendingAllianceProposals', index, 'recipientAllianceId'],
- message:
- 'Proposal alliance attribution must match current participant membership.',
- });
- outgoing.add(proposal.proposerAgentId);
- incoming.add(proposal.recipientAgentId);
- }
}
export const worldSnapshotSchema = worldSnapshotObjectSchema.superRefine(
@@ -1330,15 +620,11 @@ export const cellObservationSchema = z.discriminatedUnion('state', [
cell: h3CellSchema,
state: z.literal('open'),
controllerAgentId: z.null(),
- controllerAllianceId: z.null(),
- effectiveColor: z.null(),
}),
z.object({
cell: h3CellSchema,
state: z.literal('infected'),
controllerAgentId: agentIdSchema.nullable(),
- controllerAllianceId: allianceIdSchema.nullable(),
- effectiveColor: colorSchema.nullable(),
}),
]);
export type CellObservation = z.infer;
@@ -1349,10 +635,7 @@ export const nearbyAgentObservationSchema = z.object({
currentCell: h3CellSchema,
distance: z.number().int().nonnegative().default(0),
distanceKm: z.number().nonnegative().default(0),
- allianceId: allianceIdSchema.nullable(),
- allianceRelationship: z.enum(['allied', 'not-allied']).default('not-allied'),
controlledCellCount: z.number().int().nonnegative().default(0),
- directMessageLegal: z.boolean().default(false),
});
export const publicEventObservationSchema = z.object({
@@ -1362,54 +645,10 @@ export const publicEventObservationSchema = z.object({
summary: z.string().trim().min(1).max(180),
});
-export const observedPublicMessageSchema = z.object({
- eventId: eventIdSchema,
- senderId: agentIdSchema,
- senderName: z.string().trim().min(1).max(80),
- message: messageContentSchema,
- occurredAt: z.iso.datetime(),
-});
-export type ObservedPublicMessage = z.infer;
-
-export const observedDirectMessageSchema = z.object({
- eventId: eventIdSchema,
- senderId: agentIdSchema,
- senderName: z.string().trim().min(1).max(80),
- recipientId: agentIdSchema,
- recipientName: z.string().trim().min(1).max(80),
- direction: z.enum(['inbound', 'outbound']),
- message: messageContentSchema,
- occurredAt: z.iso.datetime(),
- distance: z.number().nonnegative(),
-});
-export type ObservedDirectMessage = z.infer;
-export const observedAllianceMessageSchema = z.object({
- eventId: eventIdSchema,
- senderId: agentIdSchema,
- senderName: z.string().trim().min(1).max(80),
- allianceId: allianceIdSchema,
- message: messageContentSchema,
- occurredAt: z.iso.datetime(),
-});
-export const observedZeroMessageSchema = z.object({
- eventId: eventIdSchema,
- senderId: agentIdSchema,
- senderName: z.string().trim().min(1).max(80),
- recipientCount: z
- .number()
- .int()
- .nonnegative()
- .max(WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- message: messageContentSchema,
- occurredAt: z.iso.datetime(),
-});
-
export const territoryScoreboardEntrySchema = z.object({
agentId: agentIdSchema,
name: z.string().trim().min(1).max(80),
color: colorSchema,
- allianceId: allianceIdSchema.nullable(),
- effectiveColor: colorSchema,
controlledCellCount: z
.number()
.int()
@@ -1498,450 +737,112 @@ export const patientZeroPressureContextSchema = z
.min(1)
.max(PATIENT_ZERO_PRESSURE_WINDOW_TICKS),
}),
- currentAlliance: patientZeroPressureCountsSchema.nullable(),
- })
- .strict()
- .superRefine((pressure, context) => {
- if (
- pressure.window.endTick - pressure.window.startTick + 1 !==
- pressure.window.tickCount ||
- pressure.subject.totalEvents < 1 ||
- pressure.subject.consecutiveAffectedTicks > pressure.window.tickCount ||
- pressure.subject.consecutiveAffectedTicks > pressure.subject.totalEvents
- )
- context.addIssue({
- code: 'custom',
- message: 'Player-pressure window and subject counts must be truthful.',
- });
- if (
- pressure.currentAlliance &&
- (pressure.currentAlliance.totalEvents < pressure.subject.totalEvents ||
- pressure.currentAlliance.disinfections <
- pressure.subject.disinfections ||
- pressure.currentAlliance.blockedCleans < pressure.subject.blockedCleans)
- )
- context.addIssue({
- code: 'custom',
- message: 'Current-alliance pressure cannot be below subject pressure.',
- });
- });
-export type PatientZeroPressureContext = z.infer<
- typeof patientZeroPressureContextSchema
->;
-const patientZeroDisinfectionThreatSchema = z
- .object({
- eventId: eventIdSchema,
- kind: z.literal('territory-disinfected'),
- cell: h3CellSchema,
- occurredAt: z.iso.datetime(),
- affectedAgentId: agentIdSchema,
- affectedAgentName: z.string().trim().min(1).max(80),
- affectedAllianceId: allianceIdSchema.nullable(),
- affectedAllianceColor: z.enum(ALLIANCE_COLOR_PALETTE).nullable(),
- pressureContext: patientZeroPressureContextSchema.optional(),
- })
- .strict();
-const patientZeroBlockedCleanThreatSchema = z
- .object({
- eventId: eventIdSchema,
- kind: z.literal('occupied-clean-blocked'),
- cell: h3CellSchema,
- occurredAt: z.iso.datetime(),
- blockingAgentId: agentIdSchema,
- blockingAgentName: z.string().trim().min(1).max(80),
- blockingAllianceId: allianceIdSchema.nullable(),
- blockingAllianceColor: z.enum(ALLIANCE_COLOR_PALETTE).nullable(),
- pressureContext: patientZeroPressureContextSchema.optional(),
- })
- .strict();
-export const patientZeroPlayerThreatEventSchema = z.discriminatedUnion('kind', [
- patientZeroDisinfectionThreatSchema,
- patientZeroBlockedCleanThreatSchema,
-]);
-export type PatientZeroPlayerThreatEvent = z.infer<
- typeof patientZeroPlayerThreatEventSchema
->;
-export const patientZeroPlayerThreatFeedSchema = z
- .object({
- events: z
- .array(patientZeroPlayerThreatEventSchema)
- .max(PATIENT_ZERO_PLAYER_THREAT_FEED_LIMIT),
- totalEventCount: z.number().int().nonnegative(),
- truncated: z.boolean(),
- })
- .strict()
- .superRefine((feed, context) => {
- if (
- feed.events.length > feed.totalEventCount ||
- feed.truncated !== feed.totalEventCount > feed.events.length ||
- new Set(feed.events.map(({ eventId }) => eventId)).size !==
- feed.events.length
- )
- context.addIssue({
- code: 'custom',
- message: 'Patient Zero player-threat counts must be truthful.',
- });
- for (const event of feed.events) {
- const allianceId =
- event.kind === 'territory-disinfected'
- ? event.affectedAllianceId
- : event.blockingAllianceId;
- const allianceColor =
- event.kind === 'territory-disinfected'
- ? event.affectedAllianceColor
- : event.blockingAllianceColor;
- if ((allianceId === null) !== (allianceColor === null))
- context.addIssue({
- code: 'custom',
- message:
- 'Patient Zero player-threat alliance attribution must be complete.',
- });
- if (
- event.pressureContext &&
- ((allianceId === null) !==
- (event.pressureContext.currentAlliance === null) ||
- (event.kind === 'territory-disinfected'
- ? event.pressureContext.subject.disinfections < 1
- : event.pressureContext.subject.blockedCleans < 1))
- )
- context.addIssue({
- code: 'custom',
- message:
- 'Patient Zero event pressure must include the current event and current alliance state.',
- });
- }
- });
-export type PatientZeroPlayerThreatFeed = z.infer<
- typeof patientZeroPlayerThreatFeedSchema
->;
-
-export const allianceTerritorySummarySchema = z
- .object({
- allianceId: allianceIdSchema,
- color: z.enum(ALLIANCE_COLOR_PALETTE),
- totalControlledCellCount: z
- .number()
- .int()
- .nonnegative()
- .max(WORLD_SCENARIO_LIMITS.maximumGeneratedCells),
- members: z
- .array(
- z.object({
- agentId: agentIdSchema,
- name: z.string().trim().min(1).max(80),
- controlledCellCount: z
- .number()
- .int()
- .nonnegative()
- .max(WORLD_SCENARIO_LIMITS.maximumGeneratedCells),
- }),
- )
- .min(2)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- })
- .refine(
- ({ totalControlledCellCount, members }) =>
- totalControlledCellCount ===
- members.reduce((sum, member) => sum + member.controlledCellCount, 0),
- { message: 'Alliance territory must equal the sum of member control.' },
- );
-export type AllianceTerritorySummary = z.infer<
- typeof allianceTerritorySummarySchema
->;
-
-export const observedAllianceEventSchema = z.object({
- event: allianceEventSchema,
- summary: z.string().trim().min(1).max(240),
-});
-
-export const patientZeroGlobalAgentSchema = z.object({
- id: agentIdSchema,
- name: z.string().trim().min(1).max(80),
- currentCell: h3CellSchema,
- allianceId: allianceIdSchema.nullable(),
- controlledCellCount: z.number().int().nonnegative(),
- personality: personalitySchema,
- strategyId: strategyProfileIdSchema,
-});
-export const diplomacyProposalBlockReasonSchema = z.enum([
- 'current-ally',
- 'out-of-range',
- 'outgoing-proposal-exists',
- 'incoming-proposal-exists',
- 'alliance-to-alliance-merge',
-]);
-export const diplomacyProposalBlockSchema = z
- .object({
- agentId: agentIdSchema,
- reason: diplomacyProposalBlockReasonSchema,
- })
- .strict();
-export const patientZeroDiplomacyFeasibilitySchema = z
- .object({
- agentId: agentIdSchema,
- eligibleRecipientCount: z
- .number()
- .int()
- .nonnegative()
- .max(WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- displayedEligibleRecipientAgentIds: z.array(agentIdSchema).max(4),
- eligibleRecipientsTruncated: z.boolean(),
- blockedCounts: z
- .array(
- z
- .object({
- reason: diplomacyProposalBlockReasonSchema,
- count: z
- .number()
- .int()
- .positive()
- .max(
- WORLD_SCENARIO_LIMITS.maximumAgents *
- (WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- ),
- })
- .strict(),
- )
- .max(diplomacyProposalBlockReasonSchema.options.length),
- blockerExamples: z.array(diplomacyProposalBlockSchema).max(4),
- acceptableProposalIds: z
- .array(allianceProposalIdSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- leaveAvailable: z.boolean(),
- })
- .strict()
- .superRefine((entry, context) => {
- if (
- entry.displayedEligibleRecipientAgentIds.length >
- entry.eligibleRecipientCount ||
- entry.eligibleRecipientsTruncated !==
- entry.eligibleRecipientCount >
- entry.displayedEligibleRecipientAgentIds.length
- )
- context.addIssue({
- code: 'custom',
- message:
- 'Patient Zero eligible-recipient truncation must match counts.',
- });
- if (
- new Set(entry.displayedEligibleRecipientAgentIds).size !==
- entry.displayedEligibleRecipientAgentIds.length ||
- new Set(entry.blockedCounts.map(({ reason }) => reason)).size !==
- entry.blockedCounts.length ||
- new Set(entry.blockerExamples.map(({ agentId }) => agentId)).size !==
- entry.blockerExamples.length ||
- entry.blockerExamples.some(
- ({ agentId, reason }) =>
- agentId === entry.agentId ||
- !entry.blockedCounts.some((count) => count.reason === reason),
- )
- )
- context.addIssue({
- code: 'custom',
- message:
- 'Patient Zero diplomacy samples must be unique and attributed.',
- });
- });
-export const patientZeroDiplomacySummarySchema = z
- .object({
- eligiblePairCount: z
- .number()
- .int()
- .nonnegative()
- .max(
- WORLD_SCENARIO_LIMITS.maximumAgents *
- (WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- ),
- displayedEligiblePairs: z
- .array(
- z
- .object({
- proposerId: agentIdSchema,
- recipientId: agentIdSchema,
- })
- .strict(),
- )
- .max(PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.displayedEligiblePairs),
- eligiblePairsTruncated: z.boolean(),
- acceptableProposals: z
- .array(
- z
- .object({
- agentId: agentIdSchema,
- proposalId: allianceProposalIdSchema,
- })
- .strict(),
- )
- .max(PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.acceptableProposals),
- acceptableProposalCount: z
- .number()
- .int()
- .nonnegative()
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- acceptableProposalsTruncated: z.boolean(),
- leaveAvailableAgentIds: z
- .array(agentIdSchema)
- .max(PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.leaveAvailableAgentIds),
- leaveAvailableCount: z
- .number()
- .int()
- .nonnegative()
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- leaveAvailableTruncated: z.boolean(),
- blockedCounts: z
- .array(
- z
- .object({
- reason: diplomacyProposalBlockReasonSchema,
- count: z
- .number()
- .int()
- .positive()
- .max(
- WORLD_SCENARIO_LIMITS.maximumAgents *
- (WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- ),
- })
- .strict(),
- )
- .max(diplomacyProposalBlockReasonSchema.options.length),
- blockerExamples: z
- .array(
- z
- .object({
- proposerId: agentIdSchema,
- recipientId: agentIdSchema,
- reason: diplomacyProposalBlockReasonSchema,
- })
- .strict(),
- )
- .max(PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS.blockerExamples),
})
.strict()
- .superRefine((summary, context) => {
- if (
- summary.eligiblePairsTruncated !==
- summary.eligiblePairCount > summary.displayedEligiblePairs.length ||
- summary.leaveAvailableTruncated !==
- summary.leaveAvailableCount > summary.leaveAvailableAgentIds.length ||
- summary.acceptableProposalsTruncated !==
- summary.acceptableProposalCount > summary.acceptableProposals.length
- )
- context.addIssue({
- code: 'custom',
- message: 'Patient Zero diplomacy truncation must match counts.',
- });
- const pairKeys = summary.displayedEligiblePairs.map(
- ({ proposerId, recipientId }) => `${proposerId}:${recipientId}`,
- );
- const proposalKeys = summary.acceptableProposals.map(
- ({ proposalId }) => proposalId,
- );
- const blockerKeys = summary.blockerExamples.map(
- ({ proposerId, recipientId }) => `${proposerId}:${recipientId}`,
- );
+ .superRefine((pressure, context) => {
if (
- summary.displayedEligiblePairs.length > summary.eligiblePairCount ||
- summary.acceptableProposals.length > summary.acceptableProposalCount ||
- summary.leaveAvailableAgentIds.length > summary.leaveAvailableCount ||
- new Set(pairKeys).size !== pairKeys.length ||
- summary.displayedEligiblePairs.some(
- ({ proposerId, recipientId }) => proposerId === recipientId,
- ) ||
- new Set(proposalKeys).size !== proposalKeys.length ||
- new Set(summary.leaveAvailableAgentIds).size !==
- summary.leaveAvailableAgentIds.length ||
- new Set(summary.blockedCounts.map(({ reason }) => reason)).size !==
- summary.blockedCounts.length ||
- new Set(blockerKeys).size !== blockerKeys.length ||
- summary.blockerExamples.some(
- ({ proposerId, recipientId, reason }) =>
- proposerId === recipientId ||
- !summary.blockedCounts.some((entry) => entry.reason === reason),
- )
+ pressure.window.endTick - pressure.window.startTick + 1 !==
+ pressure.window.tickCount ||
+ pressure.subject.totalEvents < 1 ||
+ pressure.subject.consecutiveAffectedTicks > pressure.window.tickCount ||
+ pressure.subject.consecutiveAffectedTicks > pressure.subject.totalEvents
)
context.addIssue({
code: 'custom',
- message:
- 'Patient Zero diplomacy samples must be unique and attributed.',
+ message: 'Player-pressure window and subject counts must be truthful.',
});
});
-export const patientZeroGlobalViewSchema = z
+export type PatientZeroPressureContext = z.infer<
+ typeof patientZeroPressureContextSchema
+>;
+const patientZeroDisinfectionThreatSchema = z
.object({
- agents: z
- .array(patientZeroGlobalAgentSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- individualTerritory: territoryScoreboardSchema,
- allianceTerritory: z
- .array(allianceTerritorySummarySchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- alliances: z.array(allianceSchema).max(WORLD_SCENARIO_LIMITS.maximumAgents),
- activeAllianceProposals: z
- .array(allianceProposalSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- recentStrategicEvents: z
- .array(observedAllianceEventSchema)
- .max(RECENT_ZERO_STRATEGIC_EVENT_LIMIT),
- recentTerritoryChanges: z
- .array(hexCapturedWorldEventSchema)
- .max(RECENT_CONTROL_CHANGE_LIMIT),
- diplomacyFeasibility: z
- .array(patientZeroDiplomacyFeasibilitySchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents)
- .default([]),
- diplomacySummary: patientZeroDiplomacySummarySchema.optional(),
- playerThreatFeed: patientZeroPlayerThreatFeedSchema
- .nullable()
- .default(null),
+ eventId: eventIdSchema,
+ kind: z.literal('territory-disinfected'),
+ cell: h3CellSchema,
+ occurredAt: z.iso.datetime(),
+ affectedAgentId: agentIdSchema,
+ affectedAgentName: z.string().trim().min(1).max(80),
+ pressureContext: patientZeroPressureContextSchema.optional(),
+ })
+ .strict();
+const patientZeroBlockedCleanThreatSchema = z
+ .object({
+ eventId: eventIdSchema,
+ kind: z.literal('occupied-clean-blocked'),
+ cell: h3CellSchema,
+ occurredAt: z.iso.datetime(),
+ blockingAgentId: agentIdSchema,
+ blockingAgentName: z.string().trim().min(1).max(80),
+ pressureContext: patientZeroPressureContextSchema.optional(),
+ })
+ .strict();
+export const patientZeroPlayerThreatEventSchema = z.discriminatedUnion('kind', [
+ patientZeroDisinfectionThreatSchema,
+ patientZeroBlockedCleanThreatSchema,
+]);
+export type PatientZeroPlayerThreatEvent = z.infer<
+ typeof patientZeroPlayerThreatEventSchema
+>;
+export const patientZeroPlayerThreatFeedSchema = z
+ .object({
+ events: z
+ .array(patientZeroPlayerThreatEventSchema)
+ .max(PATIENT_ZERO_PLAYER_THREAT_FEED_LIMIT),
+ totalEventCount: z.number().int().nonnegative(),
+ truncated: z.boolean(),
})
- .superRefine((view, context) => {
+ .strict()
+ .superRefine((feed, context) => {
if (
- view.diplomacyFeasibility.length > 0 &&
- (view.diplomacyFeasibility.length !== view.agents.length ||
- view.diplomacyFeasibility.some(
- (entry) =>
- entry.eligibleRecipientCount +
- entry.blockedCounts.reduce(
- (sum, blocked) => sum + blocked.count,
- 0,
- ) !==
- view.agents.length - 1,
- ))
+ feed.events.length > feed.totalEventCount ||
+ feed.truncated !== feed.totalEventCount > feed.events.length ||
+ new Set(feed.events.map(({ eventId }) => eventId)).size !==
+ feed.events.length
)
context.addIssue({
code: 'custom',
- path: ['diplomacyFeasibility'],
- message:
- 'Patient Zero diplomacy counts must cover every other active agent.',
+ message: 'Patient Zero player-threat counts must be truthful.',
});
+ for (const event of feed.events) {
+ if (
+ event.pressureContext &&
+ (event.kind === 'territory-disinfected'
+ ? event.pressureContext.subject.disinfections < 1
+ : event.pressureContext.subject.blockedCleans < 1)
+ )
+ context.addIssue({
+ code: 'custom',
+ message:
+ 'Patient Zero event pressure must include the current event.',
+ });
+ }
});
+export type PatientZeroPlayerThreatFeed = z.infer<
+ typeof patientZeroPlayerThreatFeedSchema
+>;
+
+export const patientZeroGlobalAgentSchema = z.object({
+ id: agentIdSchema,
+ name: z.string().trim().min(1).max(80),
+ currentCell: h3CellSchema,
+ controlledCellCount: z.number().int().nonnegative(),
+});
+
+export const patientZeroGlobalViewSchema = z.object({
+ agents: z
+ .array(patientZeroGlobalAgentSchema)
+ .max(WORLD_SCENARIO_LIMITS.maximumAgents),
+ individualTerritory: territoryScoreboardSchema,
+ recentTerritoryChanges: z
+ .array(hexCapturedWorldEventSchema)
+ .max(RECENT_CONTROL_CHANGE_LIMIT),
+ playerThreatFeed: patientZeroPlayerThreatFeedSchema.nullable().default(null),
+});
const agentObservationObjectSchema = z.object({
agentId: agentIdSchema,
agentName: z.string().trim().min(1).max(80),
- personality: z.string().trim().min(1).max(PERSONALITY_MAX_LENGTH),
- behavior: behaviorAssignmentSchema.optional(),
- currentGoal: agentGoalStateSchema.nullable().default(null),
- goalAvailability: z
- .object({
- active: z.boolean(),
- availableOperations: z.array(goalRevisionOperationSchema).min(1).max(4),
- })
- .strict()
- .default({ active: false, availableOperations: ['establish'] }),
- currentMemory: memoryLedgerSchema.default([]),
- memoryAvailability: z
- .object({
- remember: z.boolean(),
- revisableMemoryIds: z.array(memoryIdSchema).max(MEMORY_ENTRY_LIMIT),
- forgettableMemoryIds: z.array(memoryIdSchema).max(MEMORY_ENTRY_LIMIT),
- })
- .strict()
- .default({
- remember: true,
- revisableMemoryIds: [],
- forgettableMemoryIds: [],
- }),
currentCell: cellObservationSchema,
captureEligibility: captureEligibilitySchema,
actionAvailability: z
@@ -1954,12 +855,7 @@ const agentObservationObjectSchema = z.object({
targetCell: h3CellSchema,
direction: z.enum(['N', 'NE', 'SE', 'S', 'SW', 'NW']),
destinationState: hexStateSchema,
- controllerRelationship: z.enum([
- 'open',
- 'self',
- 'allied',
- 'other',
- ]),
+ controllerRelationship: z.enum(['open', 'self', 'other']),
recentlyOccupied: z.boolean(),
nearbyAgentCount: z
.number()
@@ -1993,114 +889,11 @@ const agentObservationObjectSchema = z.object({
})
.strict()
.optional(),
- diplomacyAvailability: z
- .object({
- neutral: z.object({ available: z.literal(true) }).strict(),
- propose: z.discriminatedUnion('available', [
- z
- .object({
- available: z.literal(true),
- eligibleRecipientAgentIds: z
- .array(agentIdSchema)
- .min(1)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents - 1),
- blockedRecipients: z
- .array(diplomacyProposalBlockSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumNearbyAgentObservations)
- .default([]),
- })
- .strict(),
- z
- .object({
- available: z.literal(false),
- eligibleRecipientAgentIds: z.array(agentIdSchema).length(0),
- blockedRecipients: z
- .array(diplomacyProposalBlockSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumNearbyAgentObservations)
- .default([]),
- reason: z.string().trim().min(1).max(120),
- })
- .strict(),
- ]),
- accept: z.discriminatedUnion('available', [
- z
- .object({
- available: z.literal(true),
- acceptableProposalIds: z
- .array(allianceProposalIdSchema)
- .min(1)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- })
- .strict(),
- z
- .object({
- available: z.literal(false),
- acceptableProposalIds: z.array(allianceProposalIdSchema).length(0),
- reason: z.string().trim().min(1).max(120),
- })
- .strict(),
- ]),
- leave: z.discriminatedUnion('available', [
- z
- .object({ available: z.literal(true), allianceId: allianceIdSchema })
- .strict(),
- z
- .object({
- available: z.literal(false),
- allianceId: z.null(),
- reason: z.string().trim().min(1).max(120),
- })
- .strict(),
- ]),
- })
- .strict()
- .optional(),
- communicationAvailability: z
- .object({
- public: z
- .object({ available: z.literal(true), playerVisible: z.literal(true) })
- .strict(),
- direct: z
- .object({
- eligibleRecipientAgentIds: z
- .array(agentIdSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumNearbyAgentObservations),
- })
- .strict(),
- alliance: z.discriminatedUnion('available', [
- z
- .object({ available: z.literal(true), allianceId: allianceIdSchema })
- .strict(),
- z
- .object({ available: z.literal(false), allianceId: z.null() })
- .strict(),
- ]),
- zero: z
- .object({ available: z.boolean() })
- .strict()
- .default({ available: false }),
- })
- .strict()
- .optional(),
adjacentCells: z.array(cellObservationSchema).min(1).max(6),
nearbyAgents: z
.array(nearbyAgentObservationSchema)
.max(WORLD_SCENARIO_LIMITS.maximumNearbyAgentObservations),
recentEvents: z.array(publicEventObservationSchema).max(8),
- recentPublicMessages: z
- .array(observedPublicMessageSchema)
- .max(RECENT_PUBLIC_MESSAGE_LIMIT),
- recentDirectMessages: z
- .array(observedDirectMessageSchema)
- .max(RECENT_DIRECT_MESSAGE_LIMIT),
- recentAllianceMessages: z
- .array(observedAllianceMessageSchema)
- .max(RECENT_DIRECT_MESSAGE_LIMIT)
- .default([]),
- recentZeroMessages: z
- .array(observedZeroMessageSchema)
- .max(RECENT_ZERO_MESSAGE_LIMIT)
- .default([]),
patientZero: z
.object({
agentId: agentIdSchema.nullable(),
@@ -2116,16 +909,6 @@ const agentObservationObjectSchema = z.object({
}),
patientZeroGlobalView: patientZeroGlobalViewSchema.nullable().default(null),
territoryScoreboard: territoryScoreboardSchema,
- actingAllianceId: allianceIdSchema.nullable(),
- actingAlliance: allianceTerritorySummarySchema.nullable(),
- activeAlliances: z
- .array(allianceTerritorySummarySchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- inboundAllianceProposals: z.array(allianceProposalSchema).max(1),
- outboundAllianceProposals: z.array(allianceProposalSchema).max(1),
- recentAllianceEvents: z
- .array(observedAllianceEventSchema)
- .max(RECENT_ALLIANCE_EVENT_LIMIT),
recentControlChanges: z
.array(observedControlChangeSchema)
.max(RECENT_CONTROL_CHANGE_LIMIT),
@@ -2151,9 +934,6 @@ const agentObservationObjectSchema = z.object({
export const agentObservationSchema = agentObservationObjectSchema.transform(
(observation, context) => {
- const expectedGoalOperations = observation.currentGoal
- ? ['keep', 'revise', 'complete', 'abandon']
- : ['establish'];
if (
observation.patientZeroGlobalView !== null &&
!observation.patientZero.isPatientZero
@@ -2173,56 +953,8 @@ export const agentObservationSchema = agentObservationObjectSchema.transform(
path: ['patientZeroGlobalView', 'playerThreatFeed'],
message: 'Cleaner pressure must be enabled for its global feed.',
});
- if (
- observation.goalAvailability.active !==
- Boolean(observation.currentGoal) ||
- new Set(observation.goalAvailability.availableOperations).size !==
- observation.goalAvailability.availableOperations.length ||
- observation.goalAvailability.availableOperations.length !==
- expectedGoalOperations.length ||
- observation.goalAvailability.availableOperations.some(
- (operation, index) => operation !== expectedGoalOperations[index],
- )
- )
- context.addIssue({
- code: 'custom',
- path: ['goalAvailability'],
- message:
- 'Goal availability must exactly match the current goal state in canonical order.',
- });
- const memoryIds = observation.currentMemory.map(({ id }) => id);
- const owningMemory = memoryLedgerForAgentSchema(
- observation.agentId,
- ).safeParse(observation.currentMemory);
- if (
- !owningMemory.success ||
- observation.memoryAvailability.remember !==
- observation.currentMemory.length < MEMORY_ENTRY_LIMIT ||
- observation.memoryAvailability.revisableMemoryIds.length !==
- memoryIds.length ||
- observation.memoryAvailability.forgettableMemoryIds.length !==
- memoryIds.length ||
- observation.memoryAvailability.revisableMemoryIds.some(
- (id, index) => id !== memoryIds[index],
- ) ||
- observation.memoryAvailability.forgettableMemoryIds.some(
- (id, index) => id !== memoryIds[index],
- )
- )
- context.addIssue({
- code: 'custom',
- path: ['memoryAvailability'],
- message:
- 'Memory availability must exactly match the canonical current ledger.',
- });
return {
...observation,
- behavior: observation.behavior ?? {
- agentId: observation.agentId,
- personalityId: 'analytical',
- strategyId: 'adaptive',
- manual: false,
- },
actionAvailability: observation.actionAvailability ?? {
moveTargetCellIds: observation.adjacentCells.map(({ cell }) => cell),
moveOptions: [],
@@ -2241,83 +973,11 @@ export const agentObservationSchema = agentObservationObjectSchema.transform(
},
wait: { available: true as const },
},
- diplomacyAvailability: observation.diplomacyAvailability ?? {
- neutral: { available: true as const },
- propose: {
- available: false as const,
- eligibleRecipientAgentIds: [],
- blockedRecipients: [],
- reason: 'Authoritative recipient availability was not supplied.',
- },
- accept: observation.inboundAllianceProposals.length
- ? {
- available: true as const,
- acceptableProposalIds: observation.inboundAllianceProposals.map(
- ({ id }) => id,
- ),
- }
- : {
- available: false as const,
- acceptableProposalIds: [],
- reason: 'No acceptable inbound formal alliance proposal exists.',
- },
- leave: observation.actingAllianceId
- ? {
- available: true as const,
- allianceId: observation.actingAllianceId,
- }
- : {
- available: false as const,
- allianceId: null,
- reason: 'The agent is not currently in an alliance.',
- },
- },
- communicationAvailability: observation.communicationAvailability ?? {
- public: { available: true as const, playerVisible: true as const },
- direct: {
- eligibleRecipientAgentIds: observation.nearbyAgents
- .filter(({ directMessageLegal }) => directMessageLegal)
- .map(({ id }) => id),
- },
- alliance: observation.actingAllianceId
- ? {
- available: true as const,
- allianceId: observation.actingAllianceId,
- }
- : { available: false as const, allianceId: null },
- zero: { available: observation.patientZero.isPatientZero },
- },
};
},
);
export type AgentObservation = z.infer;
-export const agentDecisionSchema = z
- .object({
- worldAction: worldActionSchema,
- communication: communicationIntentSchema.nullish(),
- diplomacy: diplomacyIntentSchema.nullish(),
- goalRevision: requestedGoalRevisionSchema.optional(),
- memoryOperation: requestedMemoryOperationSchema.optional(),
- summary: z.string().trim().min(1).max(MODEL_SUMMARY_MAX_LENGTH),
- })
- .strict();
-export type AgentDecision = z.infer;
-
-export const providerDecisionEnvelopeSchema = z
- .object({
- worldAction: worldActionSchema,
- communication: z.unknown().optional(),
- diplomacy: z.unknown().optional(),
- goalRevision: requestedGoalRevisionSchema.optional(),
- memoryOperation: requestedMemoryOperationSchema.optional(),
- summary: z.string().trim().min(1).max(MODEL_SUMMARY_MAX_LENGTH),
- })
- .strict();
-export type ProviderDecisionEnvelope = z.infer<
- typeof providerDecisionEnvelopeSchema
->;
-
export const providerModeSchema = z.enum([
'openrouter',
'typesafe',
@@ -2514,20 +1174,6 @@ export const updateExperimentModelsRequestSchema = z
export const updateExperimentModelsResponseSchema = z.object({
snapshot: z.lazy(() => simulationSnapshotSchema),
});
-export const updateExperimentBehaviorRequestSchema = z
- .object({
- assignmentMode: z.enum(['balanced-random', 'fully-random', 'manual']),
- seed: z.string().trim().min(1).max(80),
- assignments: z
- .array(behaviorAssignmentSchema)
- .min(WORLD_SCENARIO_LIMITS.minimumAgents)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- })
- .strict();
-export const updateExperimentBehaviorResponseSchema = z.object({
- snapshot: z.lazy(() => simulationSnapshotSchema),
-});
-
export const modelVerificationStatusSchema = z.enum([
'untested',
'verified',
@@ -2633,19 +1279,6 @@ export const providerFailureSchema = z.object({
'missing-move-target',
'unexpected-world-target',
'contradictory-world-action-fields',
- 'contradictory-diplomacy-fields',
- 'missing-formal-proposal-id',
- 'unexpected-formal-proposal-id',
- 'invalid-formal-proposal-reference',
- 'ineligible-alliance-recipient',
- 'missing-alliance-recipient',
- 'unexpected-alliance-recipient',
- 'invalid-goal-fields',
- 'goal-text-too-long',
- 'goal-reason-too-long',
- 'invalid-memory-fields',
- 'invalid-memory-id',
- 'memory-text-too-long',
'summary-too-long',
]),
)
@@ -2893,100 +1526,6 @@ export type ProviderAttemptRetention = z.infer<
typeof providerAttemptRetentionSchema
>;
-const turnRecordBaseSchema = z.object({
- turnNumber: z.number().int().positive(),
- tickNumber: z.number().int().positive().optional(),
- tickPosition: z.number().int().positive().optional(),
- virtualTime: z.iso.datetime().optional(),
- tickIntervalMinutes: z.number().int().positive().optional(),
- agentId: agentIdSchema,
- startedAt: z.iso.datetime(),
- completedAt: z.iso.datetime(),
- observation: agentObservationSchema,
- behavior: behaviorAssignmentSchema.optional(),
- allianceEvents: z.array(allianceEventSchema).default([]),
- modelAttempts: z.array(modelAttemptSchema).max(1_000).default([]),
-});
-
-const completedTurnFields = {
- worldAction: worldActionSchema,
- communication: communicationIntentSchema.optional(),
- diplomacy: diplomacyIntentSchema.optional(),
- summary: z.string().trim().min(1).max(MODEL_SUMMARY_MAX_LENGTH),
- provider: providerMetadataSchema,
- communicationResult: communicationResultSchema,
- diplomacyResult: diplomacyResultSchema,
- goalRevision: requestedGoalRevisionSchema.optional(),
- goalRevisionResult: goalRevisionResultSchema.default({ requested: false }),
- memoryOperation: requestedMemoryOperationSchema.optional(),
- memoryOperationResult: memoryOperationResultSchema.default({
- requested: false,
- }),
-};
-
-export const agentTurnRecordSchema = z
- .discriminatedUnion('outcome', [
- turnRecordBaseSchema.extend({
- outcome: z.literal('accepted'),
- ...completedTurnFields,
- worldActionResult: z.object({
- accepted: z.literal(true),
- event: nonCommunicationWorldEventSchema,
- }),
- }),
- turnRecordBaseSchema.extend({
- outcome: z.literal('rejected'),
- ...completedTurnFields,
- worldActionResult: z.object({
- accepted: z.literal(false),
- reason: invalidActionReasonSchema,
- details: z.string().min(1).max(300),
- }),
- }),
- turnRecordBaseSchema.extend({
- outcome: z.literal('provider-error'),
- failure: providerFailureSchema,
- provider: providerMetadataSchema.optional(),
- }),
- turnRecordBaseSchema.extend({
- outcome: z.literal('lost-tick'),
- failure: providerFailureSchema,
- provider: providerMetadataSchema.optional(),
- }),
- turnRecordBaseSchema.extend({
- outcome: z.literal('operator-skipped'),
- skipKind: z.enum(['manual', 'unattended']).default('manual'),
- failure: providerFailureSchema,
- provider: providerMetadataSchema.optional(),
- }),
- ])
- .superRefine((turn, context) => {
- const tickFields = [
- turn.tickNumber,
- turn.tickPosition,
- turn.virtualTime,
- turn.tickIntervalMinutes,
- ];
- if (
- tickFields.some((value) => value !== undefined) &&
- tickFields.some((value) => value === undefined)
- )
- context.addIssue({
- code: 'custom',
- message: 'Tick metadata must be present together.',
- });
- if (
- turn.outcome === 'operator-skipped' &&
- turn.failure.model !== undefined &&
- turn.provider?.model !== turn.failure.model
- )
- context.addIssue({
- code: 'custom',
- message: 'Skipped-turn failure and provider models must match.',
- });
- });
-export type AgentTurnRecord = z.infer;
-
export const scenarioContractVersionSchema = z.literal('world-scenario-v1');
export const setupIssueCodeSchema = z.enum([
'invalid-coordinates',
@@ -2998,7 +1537,6 @@ export const setupIssueCodeSchema = z.enum([
'duplicate-agent-name',
'invalid-color',
'spawn-infeasible',
- 'behavior-coverage-mismatch',
'model-agent-mismatch',
'high-agent-density',
'geocoder-unavailable',
@@ -3014,7 +1552,6 @@ export const scenarioRosterEntrySchema = z.object({
id: agentIdSchema,
name: z.string().trim().min(1).max(80),
color: colorSchema,
- personality: personalitySchema,
});
export type ScenarioRosterEntry = z.infer;
@@ -3115,12 +1652,6 @@ const worldSetupRequestObjectSchema = z
.int()
.min(0)
.max(WORLD_SCENARIO_LIMITS.maximumRadius * 2),
- communicationRangeKm: z
- .number()
- .finite()
- .min(WORLD_SCENARIO_LIMITS.minimumCommunicationRangeKm)
- .max(WORLD_SCENARIO_LIMITS.maximumCommunicationRangeKm)
- .default(DEFAULT_COMMUNICATION_RANGE_KM),
minimumTickIntervalMinutes: z
.number()
.int()
@@ -3139,7 +1670,6 @@ const worldSetupRequestObjectSchema = z
patientZeroAgentId: agentIdSchema,
roster: scenarioRosterSchema,
modelConfiguration: experimentModelConfigurationSchema,
- behaviorConfiguration: behaviorConfigurationSchema,
objectiveVersion: z
.enum([
'durable-influence-v1',
@@ -3149,8 +1679,6 @@ const worldSetupRequestObjectSchema = z
.default('durable-influence-v2'),
capabilities: z
.object({
- communication: z.boolean(),
- diplomacy: z.boolean(),
simulatedPlayerPressure: z.boolean().default(false),
})
.strict(),
@@ -3214,17 +1742,6 @@ function validateWorldSetupRequest(
message: 'Maximum tick interval must be at least the minimum.',
});
const ids = new Set(request.roster.map(({ id }) => id));
- if (
- request.behaviorConfiguration.assignments.length !== ids.size ||
- request.behaviorConfiguration.assignments.some(
- ({ agentId }) => !ids.has(agentId),
- )
- )
- context.addIssue({
- code: 'custom',
- path: ['behaviorConfiguration', 'assignments'],
- message: 'Behavior assignments must cover the roster exactly.',
- });
if (
request.modelConfiguration.overrides.some(
({ agentId }) => !ids.has(agentId),
@@ -3523,34 +2040,10 @@ export const simulationSnapshotSchema = z
.strict()
.optional(),
modelConfiguration: experimentModelConfigurationSchema,
- behaviorConfiguration: behaviorConfigurationSchema
- .safeExtend({
- assignments: z
- .array(behaviorAssignmentSchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
- })
- .optional(),
resolvedModels: z
.array(resolvedAgentModelSchema)
.min(0)
.max(WORLD_SCENARIO_LIMITS.maximumAgents),
- agentGoals: z
- .array(
- z
- .object({
- agentId: agentIdSchema,
- goal: agentGoalStateSchema.nullable(),
- })
- .strict(),
- )
- .default([]),
- agentMemories: z
- .array(
- z
- .object({ agentId: agentIdSchema, entries: memoryLedgerSchema })
- .strict(),
- )
- .default([]),
swarmTicks: z.array(swarmTickRecordSchema).max(120).optional(),
experiment: z.object({
id: z.uuid().brand<'ExperimentId'>(),
@@ -3560,9 +2053,6 @@ export const simulationSnapshotSchema = z
),
metrics: z.lazy(() => experimentMetricsSchema),
currentTerritory: territoryScoreboardSchema,
- currentAlliances: z
- .array(allianceTerritorySummarySchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents),
simulatedPlayerMetrics: simulatedPlayerMetricsSchema.default({
movements: 0,
cellsDisinfected: 0,
@@ -3682,46 +2172,6 @@ export const simulationSnapshotSchema = z
path: ['resolvedModels'],
message: 'Resolved models must cover the current roster exactly.',
});
- if (
- snapshot.agentGoals.length > 0 &&
- (snapshot.agentGoals.length !== rosterIds.size ||
- new Set(snapshot.agentGoals.map(({ agentId }) => agentId)).size !==
- rosterIds.size ||
- snapshot.agentGoals.some(({ agentId }) => !rosterIds.has(agentId)))
- )
- context.addIssue({
- code: 'custom',
- path: ['agentGoals'],
- message: 'Goal entries must cover the current roster exactly.',
- });
- if (
- snapshot.agentMemories.length > 0 &&
- (snapshot.agentMemories.length !== rosterIds.size ||
- new Set(snapshot.agentMemories.map(({ agentId }) => agentId)).size !==
- rosterIds.size ||
- snapshot.agentMemories.some(
- ({ agentId, entries }) =>
- !rosterIds.has(agentId) ||
- !memoryLedgerForAgentSchema(agentId).safeParse(entries).success,
- ))
- )
- context.addIssue({
- code: 'custom',
- path: ['agentMemories'],
- message: 'Memory ledgers must cover the current roster exactly.',
- });
- if (
- snapshot.behaviorConfiguration &&
- (snapshot.behaviorConfiguration.assignments.length !== rosterIds.size ||
- snapshot.behaviorConfiguration.assignments.some(
- ({ agentId }) => !rosterIds.has(agentId),
- ))
- )
- context.addIssue({
- code: 'custom',
- path: ['behaviorConfiguration'],
- message: 'Behavior assignments must cover the current roster exactly.',
- });
const authoritative = new Map(
snapshot.world.agents.map(({ id }) => [id, 0]),
);
@@ -3738,25 +2188,12 @@ export const simulationSnapshotSchema = z
] of snapshot.experiment.currentTerritory.entries()) {
const agent = snapshot.world.agents.find(
({ id }) => id === entry.agentId,
- );
- if (!agent || agent.name !== entry.name || agent.color !== entry.color)
- context.addIssue({
- code: 'custom',
- path: ['experiment', 'currentTerritory', index],
- message: 'Current territory identity must match a world agent.',
- });
- const alliance = snapshot.world.alliances.find(({ memberAgentIds }) =>
- memberAgentIds.includes(entry.agentId),
- );
- if (
- entry.allianceId !== (alliance?.id ?? null) ||
- entry.effectiveColor !== (alliance?.color ?? NEUTRAL_AGENT_COLOR)
- )
+ );
+ if (!agent || agent.name !== entry.name || agent.color !== entry.color)
context.addIssue({
code: 'custom',
path: ['experiment', 'currentTerritory', index],
- message:
- 'Current territory alliance and effective color must be authoritative.',
+ message: 'Current territory identity must match a world agent.',
});
if (authoritative.get(entry.agentId) !== entry.controlledCellCount)
context.addIssue({
@@ -3770,75 +2207,7 @@ export const simulationSnapshotSchema = z
message: 'Current territory must match authoritative world control.',
});
}
- for (const [
- index,
- summary,
- ] of snapshot.experiment.currentAlliances.entries()) {
- const alliance = snapshot.world.alliances.find(
- ({ id }) => id === summary.allianceId,
- );
- if (
- !alliance ||
- alliance.color !== summary.color ||
- alliance.memberAgentIds.length !== summary.members.length ||
- alliance.memberAgentIds.some(
- (id) => !summary.members.some(({ agentId }) => agentId === id),
- )
- )
- context.addIssue({
- code: 'custom',
- path: ['experiment', 'currentAlliances', index],
- message:
- 'Current alliance summary must match authoritative membership.',
- });
- for (const member of summary.members) {
- const territory = snapshot.experiment.currentTerritory.find(
- ({ agentId }) => agentId === member.agentId,
- );
- if (
- !territory ||
- territory.controlledCellCount !== member.controlledCellCount
- )
- context.addIssue({
- code: 'custom',
- path: ['experiment', 'currentAlliances', index, 'members'],
- message:
- 'Alliance member territory must match current individual control.',
- });
- }
- }
- if (
- snapshot.experiment.currentAlliances.length !==
- snapshot.world.alliances.length
- )
- context.addIssue({
- code: 'custom',
- path: ['experiment', 'currentAlliances'],
- message: 'Every active alliance requires one current summary.',
- });
- })
- .transform((snapshot) => ({
- ...snapshot,
- agentGoals:
- snapshot.agentGoals.length > 0
- ? snapshot.agentGoals
- : snapshot.world.agents.map(({ id }) => ({ agentId: id, goal: null })),
- agentMemories:
- snapshot.agentMemories.length > 0
- ? snapshot.agentMemories
- : snapshot.world.agents.map(({ id }) => ({ agentId: id, entries: [] })),
- behaviorConfiguration: snapshot.behaviorConfiguration ?? {
- registryVersion: 1 as const,
- assignmentMode: 'balanced-random' as const,
- seed: 'legacy-default-v1',
- assignments: assignBehavior(
- snapshot.world.agents.map(({ id }) => id),
- 'legacy-default-v1',
- 'balanced-random',
- ),
- locked: snapshot.tickNumber > 0,
- },
- }));
+ });
export type SimulationSnapshot = z.infer;
export const singleTickResponseSchema = z
@@ -3879,28 +2248,6 @@ export const cancelSimulationResponseSchema = z.object({
snapshot: simulationSnapshotSchema,
});
-export const updateAgentPersonalityRequestSchema = z
- .object({ personality: personalitySchema })
- .strict();
-export type UpdateAgentPersonalityRequest = z.infer<
- typeof updateAgentPersonalityRequestSchema
->;
-
-export const updateAgentPersonalityResponseSchema = z.object({
- snapshot: simulationSnapshotSchema,
- agent: agentSchema,
-});
-export type UpdateAgentPersonalityResponse = z.infer<
- typeof updateAgentPersonalityResponseSchema
->;
-
-export const restoreDefaultPersonalitiesResponseSchema = z.object({
- snapshot: simulationSnapshotSchema,
-});
-export type RestoreDefaultPersonalitiesResponse = z.infer<
- typeof restoreDefaultPersonalitiesResponseSchema
->;
-
export const healthResponseSchema = z.object({
status: z.literal('ok'),
checkedAt: z.iso.datetime(),
@@ -3912,10 +2259,8 @@ export const apiErrorCodeSchema = z.enum([
'tick_conflict',
'experiment_budget_exhausted',
'reset_conflict',
- 'personality_conflict',
'invalid_agent_id',
'unknown_agent',
- 'invalid_personality',
'invalid_request',
'invalid_export',
'invalid_artifact',
@@ -3927,11 +2272,8 @@ export const apiErrorCodeSchema = z.enum([
'model_configuration_conflict',
'invalid_model_configuration',
'models_unavailable',
- 'behavior_configuration_conflict',
- 'invalid_behavior_configuration',
'model_verification_conflict',
'cancel_conflict',
- 'invalid_import',
'not_found',
'internal_error',
]);
@@ -3948,17 +2290,6 @@ export type ApiError = z.infer;
export const experimentIdSchema = z.uuid().brand<'ExperimentId'>();
export type ExperimentId = z.infer;
-export const personalityConfigurationEventSchema = z.object({
- timestamp: z.iso.datetime(),
- agentId: agentIdSchema,
- previousPersonality: personalitySchema,
- newPersonality: personalitySchema,
- operation: z.enum(['custom-edit', 'restore-default']),
-});
-export type PersonalityConfigurationEvent = z.infer<
- typeof personalityConfigurationEventSchema
->;
-
export const modelConfigurationEventSchema = z
.object({
type: z.literal('model-assignment-changed'),
@@ -3990,10 +2321,7 @@ export const modelConfigurationEventSchema = z
export type ModelConfigurationEvent = z.infer<
typeof modelConfigurationEventSchema
>;
-export const experimentConfigurationEventSchema = z.union([
- personalityConfigurationEventSchema,
- modelConfigurationEventSchema,
-]);
+export const experimentConfigurationEventSchema = modelConfigurationEventSchema;
export type ExperimentConfigurationEvent = z.infer<
typeof experimentConfigurationEventSchema
>;
@@ -4048,7 +2376,6 @@ export const experimentManifestSchema = z.preprocess(
.optional(),
swarmPlannerContractVersion: swarmPlannerContractVersionSchema,
modelConfiguration: experimentModelConfigurationSchema.optional(),
- behaviorConfiguration: behaviorConfigurationSchema.optional(),
scenario: archivedAppliedScenarioSchema.optional(),
initialAgents: z
.array(agentProfileSchema)
@@ -4127,58 +2454,6 @@ export const metricCountsSchema = z
territoryGainedThroughInfection: z.number().int().nonnegative(),
territoryGainedThroughCapture: z.number().int().nonnegative(),
territoryLostThroughCapture: z.number().int().nonnegative(),
- publicMessagesRequested: z.number().int().nonnegative().default(0),
- publicMessagesAccepted: z.number().int().nonnegative().default(0),
- publicMessagesRejected: z.number().int().nonnegative().default(0),
- directMessagesRequested: z.number().int().nonnegative().default(0),
- directMessagesDelivered: z.number().int().nonnegative().default(0),
- directMessagesRejected: z.number().int().nonnegative().default(0),
- allianceMessagesRequested: z.number().int().nonnegative().default(0),
- allianceMessagesDelivered: z.number().int().nonnegative().default(0),
- allianceMessagesRejected: z.number().int().nonnegative().default(0),
- zeroBroadcastsRequested: z.number().int().nonnegative().default(0),
- zeroBroadcastsDelivered: z.number().int().nonnegative().default(0),
- zeroBroadcastsRejected: z.number().int().nonnegative().default(0),
- zeroRecipientDeliveries: z.number().int().nonnegative().default(0),
- uniqueZeroDirectiveRecipients: z.number().int().nonnegative().default(0),
- directRepliesToPatientZero: z.number().int().nonnegative().default(0),
- uniquePatientZeroRepliers: z.number().int().nonnegative().default(0),
- firstZeroDirectiveTurn: z
- .number()
- .int()
- .positive()
- .nullable()
- .default(null),
- mostRecentZeroDirective: z
- .object({
- eventId: eventIdSchema,
- turnNumber: z.number().int().positive(),
- occurredAt: z.iso.datetime(),
- agentId: agentIdSchema,
- recipientCount: z.number().int().nonnegative(),
- modelId: modelIdSchema,
- reasoningProfile: reasoningProfileSchema,
- personalityId: personalityProfileIdSchema,
- strategyId: strategyProfileIdSchema,
- })
- .nullable()
- .default(null),
- publicMessagesSent: z.number().int().nonnegative().default(0),
- directMessagesSent: z.number().int().nonnegative().default(0),
- directMessagesReceived: z.number().int().nonnegative().default(0),
- uniqueDirectMessagePairs: z.number().int().nonnegative().default(0),
- directMessageDistanceTotalKm: z.number().nonnegative().default(0),
- directMessageDistanceAverageKm: z.number().nonnegative().default(0),
- directMessageDistanceMaximumKm: z.number().nonnegative().default(0),
- eligibleNearbyAgentObservations: z.number().int().nonnegative().default(0),
- firstAllianceTurn: z.number().int().positive().nullable().default(null),
- maximumAllianceSize: z.number().int().nonnegative().default(0),
- completedAllianceDurationTurnsTotal: z
- .number()
- .int()
- .nonnegative()
- .default(0),
- completedAllianceDurationTurnsAverage: z.number().nonnegative().default(0),
movementDirectionDistribution: z
.array(
z.object({
@@ -4190,44 +2465,6 @@ export const metricCountsSchema = z
.default([]),
longestRepeatedDirectionStreak: z.number().int().nonnegative().default(0),
recentCellRevisits: z.number().int().nonnegative().default(0),
- directionChangesAfterCommunication: z
- .number()
- .int()
- .nonnegative()
- .default(0),
- diplomacyProposalsRequested: z.number().int().nonnegative().default(0),
- diplomacyAcceptancesRequested: z.number().int().nonnegative().default(0),
- diplomacyDeparturesRequested: z.number().int().nonnegative().default(0),
- diplomacyProposalsAccepted: z.number().int().nonnegative().default(0),
- diplomacyAcceptancesAccepted: z.number().int().nonnegative().default(0),
- diplomacyDeparturesAccepted: z.number().int().nonnegative().default(0),
- diplomacyRejected: z.number().int().nonnegative().default(0),
- diplomacyRejections: z
- .array(
- z.object({
- type: z.enum([
- 'propose-alliance',
- 'accept-alliance',
- 'leave-alliance',
- 'invalid',
- ]),
- reason: diplomacyRejectionReasonSchema,
- count: z.number().int().positive(),
- }),
- )
- .max(48)
- .default([]),
- proposalsCreated: z.number().int().nonnegative().default(0),
- proposalsSent: z.number().int().nonnegative().default(0),
- proposalsReceived: z.number().int().nonnegative().default(0),
- proposalsExpired: z.number().int().nonnegative().default(0),
- proposalsInvalidated: z.number().int().nonnegative().default(0),
- alliancesFormed: z.number().int().nonnegative().default(0),
- alliancesJoined: z.number().int().nonnegative().default(0),
- alliancesLeft: z.number().int().nonnegative().default(0),
- alliancesDissolved: z.number().int().nonnegative().default(0),
- alliedCaptureAttempts: z.number().int().nonnegative().default(0),
- alliedCaptureRejections: z.number().int().nonnegative().default(0),
uniqueVisitedCells: z.number().int().nonnegative(),
averageLatencyMs: z.number().nonnegative().optional(),
tokens: tokenTotalsSchema,
@@ -4235,7 +2472,6 @@ export const metricCountsSchema = z
attemptsWithUnknownTokenUsage: z.number().int().nonnegative().default(0),
knownCostCredits: z.number().nonnegative().finite(),
attemptsWithUnknownCost: z.number().int().nonnegative().default(0),
- turnsWithUnknownCost: z.number().int().nonnegative(),
})
.superRefine((metrics, context) => {
if (
@@ -4258,32 +2494,6 @@ export const experimentMetricsSchema = z.object({
byAgent: z.array(
z.object({ agentId: agentIdSchema, metrics: metricCountsSchema }),
),
- byPersonality: z
- .array(
- z.object({
- personalityId: personalityProfileIdSchema,
- metrics: metricCountsSchema,
- }),
- )
- .default([]),
- byStrategy: z
- .array(
- z.object({
- strategyId: strategyProfileIdSchema,
- metrics: metricCountsSchema,
- }),
- )
- .default([]),
- byBehaviorCombination: z
- .array(
- z.object({
- personalityId: personalityProfileIdSchema,
- strategyId: strategyProfileIdSchema,
- metrics: metricCountsSchema,
- }),
- )
- .max(36)
- .default([]),
});
export type ExperimentMetrics = z.infer;
@@ -4295,18 +2505,6 @@ export const exportOutcomeSchema = z.enum([
'lost-tick',
]);
export const exportActionSchema = z.enum(['move', 'infect', 'capture', 'wait']);
-export const exportCommunicationChannelSchema = z.enum([
- 'all',
- 'public',
- 'direct',
- 'alliance',
- 'zero',
-]);
-export const exportCommunicationStatusSchema = z.enum([
- 'all',
- 'accepted',
- 'rejected',
-]);
export const exportLevelSchema = z.enum([
'minimal',
'standard',
@@ -4318,11 +2516,8 @@ export const exportSerializationSchema = z.enum(['compact', 'pretty']);
export const customExportOptionsSchema = z
.object({
turnObservations: z.boolean(),
- personalityTextHistory: z.boolean(),
nearbyAgents: z.boolean(),
recentEvents: z.boolean(),
- recentPublicMessages: z.boolean(),
- recentDirectMessages: z.boolean(),
recentControlChanges: z.boolean(),
validationDetails: z.boolean(),
resultingEvents: z.boolean(),
@@ -4330,23 +2525,18 @@ export const customExportOptionsSchema = z
initialWorldState: z.boolean(),
currentWorldState: z.boolean(),
computedMetrics: z.boolean(),
- communications: z.boolean(),
controlChanges: z.boolean(),
})
.strict()
.superRefine((value, context) => {
if (
!value.turnObservations &&
- (value.nearbyAgents ||
- value.recentEvents ||
- value.recentPublicMessages ||
- value.recentDirectMessages ||
- value.recentControlChanges)
+ (value.nearbyAgents || value.recentEvents || value.recentControlChanges)
) {
context.addIssue({
code: 'custom',
message:
- 'Nearby agents, recent events, recent messages, and recent control changes require turn observations.',
+ 'Nearby agents, recent events, and recent control changes require turn observations.',
});
}
});
@@ -4399,13 +2589,6 @@ export const experimentExportRequestSchema = z
turns: exportTurnSelectionSchema,
outcomes: z.array(exportOutcomeSchema).min(1).max(5),
actions: z.array(exportActionSchema).min(1).max(4),
- communications: z
- .object({
- channel: exportCommunicationChannelSchema,
- status: exportCommunicationStatusSchema,
- })
- .strict()
- .default({ channel: 'all', status: 'all' }),
level: exportLevelSchema,
serialization: exportSerializationSchema.default('compact'),
custom: customExportOptionsSchema.optional(),
@@ -4433,24 +2616,18 @@ export type ExperimentExportRequest = z.infer<
export const experimentExportPreviewSchema = z.object({
experimentId: experimentIdSchema,
- matchingTurnCount: z.number().int().nonnegative(),
matchingTickCount: z.number().int().nonnegative().optional(),
matchingSwarmTickCount: z.number().int().nonnegative().optional(),
- matchingCommunicationCount: z.number().int().nonnegative(),
matchingControlChangeCount: z.number().int().nonnegative(),
- matchingDiplomacyEventCount: z.number().int().nonnegative(),
matchingProviderAttemptCount: z.number().int().nonnegative().default(0),
selectedAgentCount: z
.number()
.int()
.positive()
.max(WORLD_SCENARIO_LIMITS.maximumAgents),
- firstMatchingTurn: z.number().int().positive().optional(),
- lastMatchingTurn: z.number().int().positive().optional(),
retention: experimentRetentionSchema,
knownCostCredits: z.number().nonnegative().finite(),
attemptsWithUnknownCost: z.number().int().nonnegative().default(0),
- turnsWithUnknownCost: z.number().int().nonnegative(),
serializedUtf8Bytes: z.number().int().nonnegative(),
approximateAiInputTokens: z.number().int().nonnegative(),
tokenEstimateMethod: z.literal('ceil(UTF-8 bytes / 4)'),
@@ -4474,135 +2651,14 @@ export const experimentTickSummarySchema = z.object({
});
export type ExperimentTickSummary = z.infer;
-export const experimentExportTurnSchema = z
- .object({
- turnNumber: z.number().int().positive(),
- tickNumber: z.number().int().positive().optional(),
- tickPosition: z.number().int().positive().optional(),
- virtualTime: z.iso.datetime().optional(),
- tickIntervalMinutes: z.number().int().positive().optional(),
- startedAt: z.iso.datetime(),
- completedAt: z.iso.datetime(),
- agentId: agentIdSchema,
- behavior: behaviorAssignmentSchema.optional(),
- outcome: exportOutcomeSchema,
- worldAction: worldActionSchema.optional(),
- communication: communicationIntentSchema.optional(),
- diplomacy: diplomacyIntentSchema.optional(),
- goalRevision: requestedGoalRevisionSchema.optional(),
- memoryOperation: requestedMemoryOperationSchema.optional(),
- summary: z.string().trim().min(1).max(MODEL_SUMMARY_MAX_LENGTH).optional(),
- worldActionSummary: z.string().trim().min(1).max(300).optional(),
- communicationSummary: z.string().trim().min(1).max(300).optional(),
- diplomacySummary: z.string().trim().min(1).max(300).optional(),
- personality: personalitySchema.optional(),
- observation: agentObservationObjectSchema.partial().optional(),
- worldActionResult: worldActionResultSchema.optional(),
- communicationResult: communicationResultSchema.optional(),
- diplomacyResult: diplomacyResultSchema.optional(),
- goalRevisionResult: goalRevisionResultSchema.optional(),
- memoryOperationResult: memoryOperationResultSchema.optional(),
- failure: providerFailureSchema.optional(),
- provider: providerMetadataSchema.optional(),
- modelAttempts: z.array(modelAttemptSchema).max(1_000).default([]),
- })
- .superRefine((turn, context) => {
- const fields = [
- turn.tickNumber,
- turn.tickPosition,
- turn.virtualTime,
- turn.tickIntervalMinutes,
- ];
- if (
- fields.some((value) => value !== undefined) &&
- fields.some((value) => value === undefined)
- )
- context.addIssue({
- code: 'custom',
- message: 'Export tick metadata must be present together.',
- });
- });
-
export const experimentExportWorldStateSchema = worldSnapshotObjectSchema
.omit({ events: true })
.superRefine(validateWorldControllers);
-export const exportedCommunicationSchema = z
- .object({
- id: eventIdSchema,
- agentId: agentIdSchema,
- channel: z.enum(['public', 'direct', 'alliance', 'zero']),
- recipientId: agentIdSchema.nullable().optional(),
- recipientIds: z.array(agentIdSchema).optional(),
- message: messageContentSchema,
- distance: z.number().nonnegative().nullable().optional(),
- occurredAt: z.iso.datetime(),
- originatingTurn: z.number().int().positive(),
- status: z.enum(['accepted', 'rejected']),
- rejectionReason: communicationRejectionReasonSchema.optional(),
- rejectionDetails: z.string().min(1).max(300).optional(),
- })
- .superRefine((communication, context) => {
- if (
- communication.channel === 'direct' &&
- (communication.recipientId === undefined ||
- communication.distance === undefined)
- )
- context.addIssue({
- code: 'custom',
- message: 'Direct communication requires a recipient and distance.',
- });
- if (
- communication.channel === 'direct' &&
- communication.status === 'accepted' &&
- (communication.recipientId === null || communication.distance === null)
- )
- context.addIssue({
- code: 'custom',
- message: 'Accepted direct communication requires a valid recipient.',
- });
- if (
- communication.channel !== 'direct' &&
- (communication.recipientId !== undefined ||
- communication.distance !== undefined)
- )
- context.addIssue({
- code: 'custom',
- message:
- 'Non-direct communication cannot have a recipient or distance.',
- });
- if (
- (communication.channel === 'public' ||
- communication.channel === 'direct') &&
- communication.recipientIds !== undefined
- )
- context.addIssue({
- code: 'custom',
- message: 'Public and direct communication cannot have recipient IDs.',
- });
- if (
- communication.channel === 'zero' &&
- communication.status === 'accepted' &&
- communication.recipientIds === undefined
- )
- context.addIssue({
- code: 'custom',
- message: 'Accepted Zero communication requires recipient IDs.',
- });
- if (
- communication.status === 'rejected' &&
- (!communication.rejectionReason || !communication.rejectionDetails)
- )
- context.addIssue({
- code: 'custom',
- message: 'Rejected communication requires a safe rejection reason.',
- });
- });
export const exportedControlChangeSchema = hexCapturedWorldEventSchema.extend({
originatingTurn: z.number().int().positive(),
});
export type ExportedControlChange = z.infer;
-export type ExportedCommunication = z.infer;
export type ExperimentExportWorldState = z.infer<
typeof experimentExportWorldStateSchema
>;
@@ -4619,68 +2675,33 @@ const experimentExportDocumentObjectSchema = z
.array(agentIdSchema)
.min(1)
.max(WORLD_SCENARIO_LIMITS.maximumAgents),
- matchingTurnCount: z.number().int().nonnegative(),
matchingTickCount: z.number().int().nonnegative().optional(),
matchingSwarmTickCount: z.number().int().nonnegative().optional(),
- matchingCommunicationCount: z.number().int().nonnegative(),
matchingControlChangeCount: z.number().int().nonnegative(),
- matchingDiplomacyEventCount: z.number().int().nonnegative(),
matchingProviderAttemptCount: z.number().int().nonnegative().optional(),
matchingSimulatedPlayerEventCount: z
.number()
.int()
.nonnegative()
.default(0),
- firstMatchingTurn: z.number().int().positive().optional(),
- lastMatchingTurn: z.number().int().positive().optional(),
}),
agents: z
- .array(
- agentProfileSchema.omit({ personality: true }).extend({
- personality: personalitySchema.optional(),
- }),
- )
+ .array(agentProfileSchema)
.min(1)
.max(WORLD_SCENARIO_LIMITS.maximumAgents),
configurationEvents: z.array(experimentConfigurationEventSchema).optional(),
metrics: experimentMetricsSchema.optional(),
currentTerritory: territoryScoreboardSchema.optional(),
- currentAlliances: z
- .array(allianceTerritorySummarySchema)
- .max(WORLD_SCENARIO_LIMITS.maximumAgents)
- .optional(),
- currentGoals: z
- .array(
- z
- .object({
- agentId: agentIdSchema,
- goal: agentGoalStateSchema.nullable(),
- })
- .strict(),
- )
- .max(WORLD_SCENARIO_LIMITS.maximumAgents)
- .optional(),
- currentMemories: z
- .array(
- z
- .object({ agentId: agentIdSchema, entries: memoryLedgerSchema })
- .strict(),
- )
- .max(WORLD_SCENARIO_LIMITS.maximumAgents)
- .optional(),
initialWorld: experimentExportWorldStateSchema.optional(),
currentWorld: experimentExportWorldStateSchema.optional(),
worldEvents: z.array(safeExperimentWorldEventSchema).optional(),
simulatedPlayerMetrics: simulatedPlayerMetricsSchema.optional(),
- communications: z.array(exportedCommunicationSchema).optional(),
controlChanges: z.array(exportedControlChangeSchema).optional(),
- allianceEvents: z.array(allianceEventSchema).optional(),
tickSummaries: z.array(experimentTickSummarySchema).optional(),
swarmTicks: z.array(swarmTickRecordSchema).optional(),
providerAttempts: z.array(providerAttemptRecordSchema).optional(),
attemptRetention: providerAttemptRetentionSchema.optional(),
attemptAccounting: experimentAttemptAccountingSchema.optional(),
- turns: z.array(experimentExportTurnSchema),
})
.superRefine((document, context) => {
if (document.swarmTicks !== undefined) {
@@ -4712,7 +2733,6 @@ const experimentExportDocumentObjectSchema = z
message: 'Swarm tick count must match exported swarm telemetry.',
});
if (
- document.turns.length === 0 &&
document.selection.matchingTickCount !== undefined &&
document.selection.matchingTickCount !== document.swarmTicks.length
)
@@ -4722,163 +2742,10 @@ const experimentExportDocumentObjectSchema = z
message: 'Zero-swarm tick count must match exported swarm telemetry.',
});
}
- if (document.currentGoals) {
- const selectedIds = new Set(document.selection.selectedAgentIds);
- const agentIds = new Set(document.agents.map(({ id }) => id));
- if (
- new Set(document.currentGoals.map(({ agentId }) => agentId)).size !==
- document.currentGoals.length
- )
- context.addIssue({
- code: 'custom',
- path: ['currentGoals'],
- message: 'Current goal agent IDs must be unique.',
- });
- if (
- document.currentGoals.some(
- ({ agentId }) => !selectedIds.has(agentId) || !agentIds.has(agentId),
- )
- )
- context.addIssue({
- code: 'custom',
- path: ['currentGoals'],
- message: 'Current goals must belong to selected exported agents.',
- });
- if (
- document.currentGoals.length !== document.agents.length ||
- document.agents.some(
- ({ id }) =>
- !document.currentGoals?.some(({ agentId }) => agentId === id),
- )
- )
- context.addIssue({
- code: 'custom',
- path: ['currentGoals'],
- message:
- 'Current goals must cover every exported agent exactly once.',
- });
- }
- if (document.currentMemories) {
- const exportedIds = new Set(document.agents.map(({ id }) => id));
- const selectedIds = new Set(document.selection.selectedAgentIds);
- if (
- document.currentMemories.length !== document.agents.length ||
- new Set(document.currentMemories.map(({ agentId }) => agentId)).size !==
- document.currentMemories.length ||
- document.currentMemories.some(
- ({ agentId, entries }) =>
- !exportedIds.has(agentId) ||
- !selectedIds.has(agentId) ||
- !memoryLedgerForAgentSchema(agentId).safeParse(entries).success,
- )
- )
- context.addIssue({
- code: 'custom',
- path: ['currentMemories'],
- message:
- 'Current memories must cover every exported agent exactly once.',
- });
- }
- const hasTickMetadata = (turn: (typeof document.turns)[number]) =>
- turn.tickNumber !== undefined &&
- turn.tickPosition !== undefined &&
- turn.virtualTime !== undefined &&
- turn.tickIntervalMinutes !== undefined;
- if (document.schemaVersion === 9 && document.turns.some(hasTickMetadata))
- context.addIssue({
- code: 'custom',
- message: 'Schema-v9 turns cannot claim simultaneous tick metadata.',
- });
- const v11TickNative =
- document.schemaVersion === 11 && document.turns.some(hasTickMetadata);
- if (document.schemaVersion === 11) {
- const tickMetadataCount = document.turns.filter(hasTickMetadata).length;
- if (
- tickMetadataCount !== 0 &&
- tickMetadataCount !== document.turns.length
- )
- context.addIssue({
- code: 'custom',
- message:
- 'Schema-v11 exports cannot mix sequential and tick-native turns.',
- });
- }
- if (document.schemaVersion === 10 || v11TickNative) {
- if (document.tickSummaries === undefined)
- context.addIssue({
- code: 'custom',
- message: 'Schema-v10 exports require canonical tick summaries.',
- });
- if (document.selection.matchingTickCount === undefined)
- context.addIssue({
- code: 'custom',
- message: 'Schema-v10 exports require a matching tick count.',
- });
- if (document.turns.some((turn) => !hasTickMetadata(turn)))
- context.addIssue({
- code: 'custom',
- message: 'Every schema-v10 turn requires complete tick metadata.',
- });
- const groups = new Map();
- for (const turn of document.turns) {
- if (turn.tickNumber === undefined) continue;
- groups.set(turn.tickNumber, [
- ...(groups.get(turn.tickNumber) ?? []),
- turn,
- ]);
- }
- for (const [tick, records] of groups) {
- const first = records[0];
- const summary = document.tickSummaries?.find(
- ({ tickNumber }) => tickNumber === tick,
- );
- if (
- new Set(records.map(({ agentId }) => agentId)).size !==
- records.length ||
- new Set(records.map(({ tickPosition }) => tickPosition)).size !==
- records.length ||
- records.some(
- (record) =>
- record.virtualTime !== first?.virtualTime ||
- record.tickIntervalMinutes !== first?.tickIntervalMinutes,
- )
- )
- context.addIssue({
- code: 'custom',
- message: `Schema-v10 tick ${tick} is internally inconsistent.`,
- });
- if (
- !summary ||
- summary.agentRecordCount !== records.length ||
- summary.virtualTime !== first?.virtualTime ||
- summary.intervalMinutes !== first?.tickIntervalMinutes
- )
- context.addIssue({
- code: 'custom',
- message: `Schema-v10 tick ${tick} summary does not match its records.`,
- });
- }
- if (
- document.tickSummaries !== undefined &&
- (new Set(document.tickSummaries.map(({ tickNumber }) => tickNumber))
- .size !== document.tickSummaries.length ||
- document.tickSummaries.length !== groups.size ||
- document.selection.matchingTickCount !== groups.size)
- )
- context.addIssue({
- code: 'custom',
- message:
- 'Schema-v10 tick counts and summaries must match exported records.',
- });
- }
- if (
- document.schemaVersion === 11 &&
- !v11TickNative &&
- document.tickSummaries?.length !== 0
- )
+ if (document.schemaVersion === 9 && document.tickSummaries !== undefined)
context.addIssue({
code: 'custom',
- message: 'Sequential schema-v11 exports require empty tick summaries.',
+ message: 'Schema-v9 exports cannot contain tick summaries.',
});
if (document.schemaVersion === 11) {
if (
@@ -4950,11 +2817,6 @@ const experimentExportDocumentObjectSchema = z
code: 'custom',
message: 'Legacy exports cannot claim schema-v11 attempt accounting.',
});
- if (document.schemaVersion === 9 && document.tickSummaries !== undefined)
- context.addIssue({
- code: 'custom',
- message: 'Schema-v9 exports cannot contain tick summaries.',
- });
const level = document.filters.level;
const custom = level === 'custom' ? document.filters.custom : undefined;
const requiresMetrics = level !== 'custom' || custom?.computedMetrics;
@@ -4968,34 +2830,12 @@ const experimentExportDocumentObjectSchema = z
code: 'custom',
message: 'Current territory inclusion does not match the export level.',
});
- if (Boolean(document.currentAlliances) !== Boolean(requiresMetrics))
- context.addIssue({
- code: 'custom',
- message: 'Current alliance inclusion does not match the export level.',
- });
if (Boolean(document.simulatedPlayerMetrics) !== Boolean(requiresMetrics))
context.addIssue({
code: 'custom',
message:
'Simulated-player metrics inclusion does not match the export level.',
});
- const personalityHistory =
- level === 'full-safe' || custom?.personalityTextHistory;
- if (personalityHistory && document.configurationEvents === undefined)
- context.addIssue({
- code: 'custom',
- message:
- 'Personality history inclusion does not match the export level.',
- });
- if (
- !personalityHistory &&
- document.configurationEvents?.some((event) => !('type' in event))
- )
- context.addIssue({
- code: 'custom',
- message:
- 'Personality history inclusion does not match the export level.',
- });
const initialWorld = level === 'full-safe' || custom?.initialWorldState;
const currentWorld = level === 'full-safe' || custom?.currentWorldState;
if (
@@ -5011,63 +2851,12 @@ const experimentExportDocumentObjectSchema = z
code: 'custom',
message: 'World event inclusion does not match the export level.',
});
- const communications = level !== 'custom' || custom?.communications;
- if (Boolean(document.communications) !== Boolean(communications))
- context.addIssue({
- code: 'custom',
- message: 'Communication inclusion does not match the export level.',
- });
const controlChanges = level !== 'custom' || custom?.controlChanges;
if (Boolean(document.controlChanges) !== Boolean(controlChanges))
context.addIssue({
code: 'custom',
message: 'Control-change inclusion does not match the export level.',
});
- for (const turn of document.turns) {
- const observation =
- level === 'standard' ||
- level === 'full-safe' ||
- custom?.turnObservations;
- const personality =
- level === 'standard' ||
- level === 'full-safe' ||
- custom?.personalityTextHistory;
- const validation =
- level === 'standard' ||
- level === 'full-safe' ||
- custom?.validationDetails;
- const event =
- level === 'standard' ||
- level === 'full-safe' ||
- custom?.resultingEvents;
- const provider = level !== 'custom' || custom?.providerUsageMetadata;
- const results = Boolean(validation || event);
- if (
- Boolean(turn.observation) !== Boolean(observation) ||
- Boolean(turn.personality) !== Boolean(personality)
- )
- context.addIssue({
- code: 'custom',
- message: 'Turn context inclusion does not match the export level.',
- });
- if (
- turn.outcome !== 'provider-error' &&
- turn.outcome !== 'lost-tick' &&
- turn.outcome !== 'operator-skipped' &&
- (Boolean(turn.worldActionResult) !== results ||
- Boolean(turn.communicationResult) !== results ||
- Boolean(turn.diplomacyResult) !== results)
- )
- context.addIssue({
- code: 'custom',
- message: 'Validation inclusion does not match the export level.',
- });
- if (!provider && turn.provider)
- context.addIssue({
- code: 'custom',
- message: 'Provider inclusion does not match the export level.',
- });
- }
});
export const experimentExportDocumentSchema = z.preprocess((input) => {
if (typeof input !== 'object' || input === null || Array.isArray(input))
@@ -5146,11 +2935,3 @@ export type ArchiveExperimentExportRequest = z.infer<
export type ArchiveExperimentExportResponse = z.infer<
typeof archiveExperimentExportResponseSchema
>;
-export const experimentImportRequestSchema = z
- .object({ document: z.unknown() })
- .strict();
-export const experimentImportResponseSchema = z.object({
- snapshot: simulationSnapshotSchema,
- legacy: z.boolean(),
- message: z.string().trim().min(1).max(300),
-});
diff --git a/packages/shared/src/limits.ts b/packages/shared/src/limits.ts
index 6a35e5c..721345c 100644
--- a/packages/shared/src/limits.ts
+++ b/packages/shared/src/limits.ts
@@ -8,8 +8,6 @@ export const WORLD_SCENARIO_LIMITS = {
highDensityCellsPerAgent: 10,
observedOtherAgents: 7,
maximumNearbyAgentObservations: 8,
- minimumCommunicationRangeKm: 0.1,
- maximumCommunicationRangeKm: 100,
minimumTickIntervalMinutes: 1,
maximumTickIntervalMinutes: 60,
maximumProviderAttempts: 100_000,
@@ -22,11 +20,3 @@ export const WORLD_RADIUS_PRESETS = {
large: { radius: 20, expectedCellCount: 1_261 },
'very-large': { radius: 40, expectedCellCount: 4_921 },
} as const;
-
-export const PATIENT_ZERO_DIPLOMACY_SUMMARY_LIMITS = {
- displayedEligiblePairs: 12,
- acceptableProposals: 8,
- leaveAvailableAgentIds: 8,
- blockerExamples: 8,
- serializedUtf8Bytes: 4_096,
-} as const;
diff --git a/packages/shared/src/scenario.test.ts b/packages/shared/src/scenario.test.ts
index 9a5f5ef..bb2da33 100644
--- a/packages/shared/src/scenario.test.ts
+++ b/packages/shared/src/scenario.test.ts
@@ -2,8 +2,6 @@ import { describe, expect, it } from 'vitest';
import {
appliedScenarioSchema,
archivedAppliedScenarioSchema,
- assignBehavior,
- behaviorConfigurationSchema,
experimentManifestSchema,
worldSetupPreviewResponseSchema,
worldSetupRequestSchema,
@@ -16,7 +14,6 @@ const roster = [
id: '128f3f38-6b7d-4db7-9e95-751b4ce2681e',
name: 'Ember',
color: '#ff6b57',
- personality: 'Adaptive.',
},
] as const;
const request = {
@@ -28,7 +25,6 @@ const request = {
rosterSeed: 'roster',
spawnSeed: 'spawn',
minimumSpawnSeparation: 1,
- communicationRangeKm: 12,
patientZeroAgentId: roster[0].id,
roster: [...roster],
modelConfiguration: {
@@ -37,19 +33,8 @@ const request = {
overrides: [],
locked: false,
},
- behaviorConfiguration: {
- registryVersion: 1 as const,
- assignmentMode: 'balanced-random' as const,
- seed: 'behavior',
- assignments: assignBehavior(
- roster.map(({ id }) => id as never),
- 'behavior',
- 'balanced-random',
- ),
- locked: false,
- },
objectiveVersion: 'durable-influence-v2' as const,
- capabilities: { communication: true, diplomacy: true },
+ capabilities: {},
};
describe('scenario contracts', () => {
@@ -214,8 +199,6 @@ describe('scenario contracts', () => {
],
agents: [{ ...parsed.roster[0], currentCell: '8928308280fffff' }],
events: [],
- alliances: [],
- pendingAllianceProposals: [],
},
});
expect(preview.feasible).toBe(true);
@@ -237,7 +220,7 @@ describe('scenario contracts', () => {
});
});
- it('rejects dynamic roster overflow and behavior under-coverage', () => {
+ it('rejects dynamic roster overflow', () => {
expect(
worldSetupRequestSchema.safeParse({
...request,
@@ -248,12 +231,6 @@ describe('scenario contracts', () => {
})),
}).success,
).toBe(false);
- expect(
- behaviorConfigurationSchema.safeParse({
- ...request.behaviorConfiguration,
- assignments: [],
- }).success,
- ).toBe(false);
});
it('requires a known Patient Zero for current setup requests', () => {
@@ -357,14 +334,6 @@ describe('scenario contracts', () => {
...worldSetupRequestSchema.parse(request),
patientZeroAgentId: null,
roster: archivedRoster,
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- archivedRoster.map(({ id }) => id as never),
- request.behaviorConfiguration.seed,
- 'balanced-random',
- ),
- },
exactCellCount: 2,
areaSquareKilometers: 0.1,
startingCells: ['8928308280fffff', '892a1072893ffff'],
diff --git a/packages/world-engine/src/index.test.ts b/packages/world-engine/src/index.test.ts
index 670161a..ab6030e 100644
--- a/packages/world-engine/src/index.test.ts
+++ b/packages/world-engine/src/index.test.ts
@@ -1,26 +1,15 @@
import { gridDisk, gridDistance, latLngToCell } from 'h3-js';
import { describe, expect, it } from 'vitest';
+import { agentIdSchema, h3CellSchema, type Agent } from '@hexzero/shared';
import {
- agentIdSchema,
- allianceIdSchema,
- allianceProposalIdSchema,
- h3CellSchema,
- type Agent,
-} from '@hexzero/shared';
-import {
- applyCommunication,
- applyDiplomacy,
applyWorldAction,
advanceCasualCleaner,
advanceSimulatedPlayer,
advanceTrailHunter,
areAdjacent,
createDevelopmentWorld,
- deterministicAllianceColor,
enumerateLegalWorldActions,
getCaptureEligibility,
- getProposalTargetEligibility,
- expireAllianceProposals,
seededTickIntervalMinutes,
seededTickOrder,
toWorldState,
@@ -40,14 +29,12 @@ const agent: Agent = {
id: agentId,
name: 'Morrow',
color: '#ff6b57',
- personality: 'Moves deliberately.',
currentCell: center,
};
const context = {
createEventId: () => '67aa21b9-fc78-4b04-9f92-9862bf346f96',
- createAllianceId: () => 'a1111111-1111-4111-8111-111111111111',
- createProposalId: () => 'b2222222-2222-4222-8222-222222222222',
now: () => '2026-08-13T12:00:00.000Z',
+ patientZeroAgentId: null,
};
describe('simultaneous tick determinism', () => {
@@ -594,203 +581,6 @@ describe('capture', () => {
});
});
-describe('nearby messaging', () => {
- it.each([0, 1, 3])(
- 'delivers by physical distance at former grid distance %s without moving or infecting',
- (distance) => {
- const before = stateWithRecipientAt(distance);
- const result = applyCommunication(
- before,
- before,
- agentId,
- {
- channel: 'direct',
- recipientId,
- message: ' Hold this position. ',
- },
- { ...context, communicationRangeKm: 100 },
- );
- expect(result.result).toMatchObject({
- accepted: true,
- event: {
- type: 'direct-message-sent',
- agentId,
- recipientId,
- message: 'Hold this position.',
- },
- });
- expect(result.state.agents).toBe(before.agents);
- expect(result.state.hexes).toBe(before.hexes);
- expect(result.state.events).toHaveLength(1);
- if (
- result.result.requested &&
- result.result.accepted &&
- result.result.event.channel === 'direct'
- )
- expect(result.result.event.distance).toBeGreaterThanOrEqual(0);
- },
- );
-
- it('rejects a recipient beyond the configured physical range', () => {
- const before = stateWithRecipientAt(4);
- const result = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'direct', recipientId, message: 'Too far.' },
- { ...context, communicationRangeKm: 0.001 },
- );
- expect(result.state).toBe(before);
- expect(result.result).toMatchObject({
- accepted: false,
- reason: 'out-of-range',
- });
- expect(result.state.events).toHaveLength(0);
- });
-
- it.each([
- [agentId, 'self-message'],
- ['6b58a30d-5d47-4ea3-8c1c-43edcc919553', 'unknown-recipient'],
- ] as const)('rejects invalid recipient %s as %s', (target, reason) => {
- const before = stateWithRecipientAt(1);
- const result = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'direct', recipientId: target, message: 'Hello.' },
- context,
- );
- expect(result.state).toBe(before);
- expect(result.result).toMatchObject({ accepted: false, reason });
- expect(result.state.events).toHaveLength(0);
- });
-
- it.each([
- { channel: 'direct', recipientId: 'Verge', message: 'Hello.' },
- { channel: 'direct', message: 'Hello.' },
- ])('preserves a malformed direct attempt as direct', (communication) => {
- const before = stateWithRecipientAt(1);
- const result = applyCommunication(
- before,
- before,
- agentId,
- communication,
- context,
- );
- expect(result.state).toBe(before);
- expect(result.result).toMatchObject({
- requested: true,
- accepted: false,
- reason: 'invalid-communication',
- attempt: {
- channel: 'direct',
- recipientId: null,
- message: 'Hello.',
- distance: null,
- },
- });
- });
-
- it('publishes trimmed world chat without a recipient or range check', () => {
- const before = stateWithRecipientAt(4);
- const result = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'public', message: ' Hello, world. ' },
- context,
- );
- expect(result.result).toMatchObject({
- requested: true,
- accepted: true,
- event: {
- type: 'public-message-sent',
- channel: 'public',
- message: 'Hello, world.',
- },
- });
- });
-
- it('rejects alliance communication for an unaffiliated sender without mutation', () => {
- const before = stateWithRecipientAt(1);
- const result = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'alliance', message: 'Private coordination.' },
- context,
- );
- expect(result.state).toBe(before);
- expect(result.result).toMatchObject({
- requested: true,
- accepted: false,
- reason: 'not-allied',
- attempt: { channel: 'alliance' },
- });
- });
-
- it('allows only Patient Zero to broadcast privately to every other active agent', () => {
- const before = stateWithRecipientAt(1);
- const rejected = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'zero', message: 'Separate the fronts.' },
- { ...context, patientZeroAgentId: recipientId },
- );
- expect(rejected.state).toBe(before);
- expect(rejected.result).toMatchObject({
- accepted: false,
- reason: 'not-patient-zero',
- });
- const delivered = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'zero', message: 'Separate the fronts.' },
- { ...context, patientZeroAgentId: agentId },
- );
- expect(delivered.result).toMatchObject({
- accepted: true,
- event: {
- channel: 'zero',
- recipientIds: [recipientId],
- playerVisible: false,
- },
- });
- });
-
- it('bypasses direct range only when Patient Zero is one endpoint', () => {
- const before = stateWithRecipientAt(4);
- const ordinary = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'direct', recipientId, message: 'Too far.' },
- { ...context, communicationRangeKm: 0.001 },
- );
- expect(ordinary.result).toMatchObject({
- accepted: false,
- reason: 'out-of-range',
- });
- const reply = applyCommunication(
- before,
- before,
- agentId,
- { channel: 'direct', recipientId, message: 'Directive received.' },
- {
- ...context,
- communicationRangeKm: 0.001,
- patientZeroAgentId: recipientId,
- },
- );
- expect(reply.result).toMatchObject({
- accepted: true,
- event: { channel: 'direct' },
- });
- });
-});
-
describe('wait and deterministic development world', () => {
it('records a wait without changing cells or hex states', () => {
const before = stateWithAgent();
@@ -827,532 +617,6 @@ describe('wait and deterministic development world', () => {
id: '3ba3ef0b-2142-44cc-b175-f6e5d6e98df5',
color: '#63d2ff',
currentCell: first.hexes[97]!.cell,
- personality:
- 'You are a social coalition-builder. Seek agents, initiate and continue conversations, propose alliances, answer offers, negotiate borders, and coordinate captures against dominant rivals. Prefer cooperation and public diplomacy over silent expansion, but protect your own territory and leave an alliance that repeatedly ignores or exploits you. Make concrete proposals rather than merely announcing actions.',
- });
- });
-});
-
-describe('formal alliances', () => {
- it('uses one proposal-target authority for affordances and rejection reasons', () => {
- const base = toWorldState(
- createDevelopmentWorld({ generatedAt: context.now() }),
- );
- const [ember, rook, mingle, morrow] = [...base.agents.values()];
- const firstAllianceId = allianceIdSchema.parse(
- 'a1111111-1111-4111-8111-111111111111',
- );
- const secondAllianceId = allianceIdSchema.parse(
- 'e5555555-5555-4555-8555-555555555555',
- );
- const alliedState = {
- ...base,
- alliances: new Map([
- [
- firstAllianceId,
- {
- id: firstAllianceId,
- color: '#0072B2' as const,
- memberAgentIds: [ember!.id, rook!.id],
- },
- ],
- [
- secondAllianceId,
- {
- id: secondAllianceId,
- color: '#D55E00' as const,
- memberAgentIds: [mingle!.id, morrow!.id],
- },
- ],
- ]),
- };
- const cases = [
- {
- state: alliedState,
- proposerId: ember!.id,
- recipientId: rook!.id,
- range: 12,
- helperReason: 'current-ally' as const,
- rejectionReason: 'current-ally' as const,
- },
- {
- state: alliedState,
- proposerId: ember!.id,
- recipientId: mingle!.id,
- range: 12,
- helperReason: 'alliance-to-alliance-merge' as const,
- rejectionReason: 'recipient-allied' as const,
- },
- {
- state: base,
- proposerId: ember!.id,
- recipientId: rook!.id,
- range: 0.001,
- helperReason: 'out-of-range' as const,
- rejectionReason: 'recipient-out-of-range' as const,
- },
- ];
- for (const item of cases) {
- expect(
- getProposalTargetEligibility(
- item.state,
- item.proposerId,
- item.recipientId,
- item.range,
- ),
- ).toEqual({ eligible: false, reason: item.helperReason });
- expect(
- applyDiplomacy(
- item.state,
- item.proposerId,
- { type: 'propose-alliance', recipientId: item.recipientId },
- 1,
- { ...context, communicationRangeKm: item.range },
- ).result,
- ).toMatchObject({
- requested: true,
- accepted: false,
- reason: item.rejectionReason,
- });
- }
-
- const movedTogetherState = {
- ...base,
- agents: new Map(
- [...base.agents.values()].map((candidate) => [
- candidate.id,
- candidate.id === rook!.id
- ? { ...candidate, currentCell: ember!.currentCell }
- : candidate,
- ]),
- ),
- };
- expect(
- getProposalTargetEligibility(
- movedTogetherState,
- ember!.id,
- rook!.id,
- 0.1,
- base,
- ),
- ).toEqual({ eligible: false, reason: 'out-of-range' });
- expect(
- applyDiplomacy(
- movedTogetherState,
- ember!.id,
- { type: 'propose-alliance', recipientId: rook!.id },
- 1,
- {
- ...context,
- communicationRangeKm: 0.1,
- diplomacyRangeState: base,
- },
- ).result,
- ).toMatchObject({ reason: 'recipient-out-of-range' });
-
- const pendingProposal = {
- id: allianceProposalIdSchema.parse(
- 'b2222222-2222-4222-8222-222222222222',
- ),
- proposerAgentId: ember!.id,
- recipientAgentId: rook!.id,
- proposerAllianceId: null,
- recipientAllianceId: null,
- originatingTurn: 1,
- expirationTurn: 17,
- };
- const outgoingState = {
- ...base,
- pendingAllianceProposals: new Map([
- [pendingProposal.id, pendingProposal],
- ]),
- };
- const incomingState = {
- ...base,
- pendingAllianceProposals: new Map([
- [
- pendingProposal.id,
- { ...pendingProposal, proposerAgentId: mingle!.id },
- ],
- ]),
- };
- for (const item of [
- {
- state: outgoingState,
- proposerId: ember!.id,
- recipientId: mingle!.id,
- reason: 'outgoing-proposal-exists' as const,
- },
- {
- state: incomingState,
- proposerId: ember!.id,
- recipientId: rook!.id,
- reason: 'incoming-proposal-exists' as const,
- },
- ]) {
- expect(
- getProposalTargetEligibility(
- item.state,
- item.proposerId,
- item.recipientId,
- 12,
- ),
- ).toEqual({ eligible: false, reason: item.reason });
- expect(
- applyDiplomacy(
- item.state,
- item.proposerId,
- { type: 'propose-alliance', recipientId: item.recipientId },
- 2,
- context,
- ).result,
- ).toMatchObject({
- requested: true,
- accepted: false,
- reason: item.reason,
- });
- }
- });
-
- it('derives stable accessible colors when the display palette must be reused', () => {
- const id = allianceIdSchema.parse('f6666666-6666-4666-8666-666666666666');
- expect(deterministicAllianceColor(id)).toBe(deterministicAllianceColor(id));
- expect(['#0072B2', '#D55E00', '#009E73', '#CC79A7']).toContain(
- deterministicAllianceColor(id),
- );
- });
-
- it.each([
- { agentCount: 8, lifetime: 16 },
- { agentCount: 20, lifetime: 40 },
- ])(
- 'preserves a $lifetime-turn legacy lifetime for an $agentCount-agent roster',
- ({ agentCount, lifetime }) => {
- const base = toWorldState(
- createDevelopmentWorld({ generatedAt: context.now() }),
- );
- const agents = [...base.agents.values()];
- for (let index = agents.length; index < agentCount; index += 1) {
- const id = agentIdSchema.parse(
- `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
- );
- agents.push({ ...agents[0]!, id, name: `Agent ${index + 1}` });
- }
- const initial = {
- ...base,
- agents: new Map(agents.map((item) => [item.id, item])),
- };
- const [proposer, recipient] = agents;
-
- const proposed = applyDiplomacy(
- initial,
- proposer!.id,
- { type: 'propose-alliance', recipientId: recipient!.id },
- 1,
- context,
- );
- const proposal = [
- ...proposed.state.pendingAllianceProposals!.values(),
- ][0]!;
-
- expect(proposal).toMatchObject({ expirationTurn: 1 + lifetime });
- expect(proposal.originatingTick).toBeUndefined();
- },
- );
-
- it('uses explicit two-tick expiry while retaining record ordinals', () => {
- const initial = toWorldState(
- createDevelopmentWorld({ generatedAt: context.now() }),
- );
- const [proposer, recipient] = [...initial.agents.values()];
- const proposed = applyDiplomacy(
- initial,
- proposer!.id,
- { type: 'propose-alliance', recipientId: recipient!.id },
- 1,
- { ...context, tickNumber: 1 },
- );
- expect(
- [...proposed.state.pendingAllianceProposals!.values()][0],
- ).toMatchObject({
- originatingTurn: 1,
- expirationTurn: 17,
- originatingTick: 1,
- expirationTick: 3,
- });
-
- const afterFirstOpportunity = expireAllianceProposals(proposed.state, 2, {
- ...context,
- tickNumber: 2,
- });
- expect(afterFirstOpportunity.pendingAllianceProposals?.size).toBe(1);
- const expired = expireAllianceProposals(afterFirstOpportunity, 17, {
- ...context,
- tickNumber: 3,
- });
- expect(expired.pendingAllianceProposals?.size).toBe(0);
- });
-
- it('recruits the final free agent into a 31-member alliance', () => {
- const base = toWorldState(
- createDevelopmentWorld({ generatedAt: context.now() }),
- );
- const template = [...base.agents.values()][0]!;
- const agents = Array.from({ length: 32 }, (_, index) => ({
- ...template,
- id: agentIdSchema.parse(
- `00000000-0000-4000-8000-${String(index).padStart(12, '0')}`,
- ),
- name: `Agent ${index}`,
- }));
- const allianceId = allianceIdSchema.parse(
- 'a1111111-1111-4111-8111-111111111111',
- );
- const initial = {
- ...base,
- agents: new Map(agents.map((candidate) => [candidate.id, candidate])),
- alliances: new Map([
- [
- allianceId,
- {
- id: allianceId,
- color: '#0072B2' as const,
- memberAgentIds: agents.slice(0, 31).map(({ id }) => id),
- },
- ],
- ]),
- };
- const proposed = applyDiplomacy(
- initial,
- agents[0]!.id,
- { type: 'propose-alliance', recipientId: agents[31]!.id },
- 1,
- context,
- );
- const accepted = applyDiplomacy(
- proposed.state,
- agents[31]!.id,
- {
- type: 'accept-alliance',
- proposalId: [...proposed.state.pendingAllianceProposals!.keys()][0]!,
- },
- 2,
- context,
- );
- expect(
- [...accepted.state.alliances!.values()][0]!.memberAgentIds,
- ).toHaveLength(32);
- });
-
- it('forms, colors, leaves, dissolves, and expires proposals deterministically', () => {
- const initial = toWorldState(
- createDevelopmentWorld({ generatedAt: context.now() }),
- );
- const [ember, rook, mingle] = [...initial.agents.values()];
- const proposed = applyDiplomacy(
- initial,
- ember!.id,
- { type: 'propose-alliance', recipientId: rook!.id },
- 1,
- context,
- );
- expect(proposed.result).toMatchObject({ requested: true, accepted: true });
- const proposalId = [...proposed.state.pendingAllianceProposals!.keys()][0]!;
- const formed = applyDiplomacy(
- proposed.state,
- rook!.id,
- { type: 'accept-alliance', proposalId },
- 2,
- context,
- );
- expect(formed.result).toMatchObject({
- requested: true,
- accepted: true,
- events: [{ type: 'alliance-formed', allianceColor: '#0072B2' }],
- });
- expect([...formed.state.alliances!.values()][0]?.memberAgentIds).toEqual([
- ember!.id,
- rook!.id,
- ]);
- const privateMessage = applyCommunication(
- formed.state,
- formed.state,
- ember!.id,
- { channel: 'alliance', message: 'Coordinate privately.' },
- { ...context, communicationRangeKm: 0.001 },
- );
- expect(privateMessage.result).toMatchObject({
- requested: true,
- accepted: true,
- event: {
- type: 'alliance-message-sent',
- recipientIds: [rook!.id],
- },
- });
- const invite = applyDiplomacy(
- formed.state,
- ember!.id,
- { type: 'propose-alliance', recipientId: mingle!.id },
- 3,
- {
- ...context,
- createProposalId: () => 'c3333333-3333-4333-8333-333333333333',
- },
- );
- const inviteId = [...invite.state.pendingAllianceProposals!.keys()][0]!;
- const joined = applyDiplomacy(
- invite.state,
- mingle!.id,
- { type: 'accept-alliance', proposalId: inviteId },
- 4,
- context,
- );
- expect(
- [...joined.state.alliances!.values()][0]?.memberAgentIds,
- ).toHaveLength(3);
- const left = applyDiplomacy(
- joined.state,
- rook!.id,
- { type: 'leave-alliance' },
- 5,
- context,
- );
- expect([...left.state.alliances!.values()][0]?.memberAgentIds).toEqual([
- ember!.id,
- mingle!.id,
- ]);
- const dissolved = applyDiplomacy(
- left.state,
- mingle!.id,
- { type: 'leave-alliance' },
- 6,
- context,
- );
- expect(dissolved.state.alliances?.size).toBe(0);
- const laterProposal = applyDiplomacy(
- dissolved.state,
- ember!.id,
- { type: 'propose-alliance', recipientId: rook!.id },
- 10,
- context,
- );
- const expired = expireAllianceProposals(laterProposal.state, 26, context);
- expect(expired.pendingAllianceProposals?.size).toBe(0);
- expect(expired.events.at(-1)).toMatchObject({
- type: 'alliance-proposal-closed',
- reason: 'expired',
- });
- });
-
- it('lets an unaffiliated proposer request entry from an allied recipient', () => {
- const initial = toWorldState(
- createDevelopmentWorld({ generatedAt: context.now() }),
- );
- const [ember, rook, mingle, morrow] = [...initial.agents.values()];
- const invitation = applyDiplomacy(
- initial,
- ember!.id,
- { type: 'propose-alliance', recipientId: rook!.id },
- 1,
- context,
- );
- const invitationId = [
- ...invitation.state.pendingAllianceProposals!.keys(),
- ][0]!;
- const formed = applyDiplomacy(
- invitation.state,
- rook!.id,
- { type: 'accept-alliance', proposalId: invitationId },
- 2,
- context,
- );
- const request = applyDiplomacy(
- formed.state,
- mingle!.id,
- { type: 'propose-alliance', recipientId: rook!.id },
- 3,
- {
- ...context,
- createProposalId: () => 'c3333333-3333-4333-8333-333333333333',
- },
- );
- expect(
- [...request.state.pendingAllianceProposals!.values()][0],
- ).toMatchObject({
- proposerAgentId: mingle!.id,
- recipientAgentId: rook!.id,
- proposerAllianceId: null,
- recipientAllianceId: [...formed.state.alliances!.keys()][0],
- });
- const requestId = [...request.state.pendingAllianceProposals!.keys()][0]!;
- const joined = applyDiplomacy(
- request.state,
- rook!.id,
- { type: 'accept-alliance', proposalId: requestId },
- 4,
- context,
- );
- expect([...joined.state.alliances!.values()][0]!.memberAgentIds).toEqual([
- ember!.id,
- rook!.id,
- mingle!.id,
- ]);
- expect(joined.result).toMatchObject({
- requested: true,
- accepted: true,
- events: [{ type: 'agent-joined-alliance', joinedAgentId: mingle!.id }],
- });
- const left = applyDiplomacy(
- joined.state,
- mingle!.id,
- { type: 'leave-alliance' },
- 5,
- context,
- );
- const switchedProposal = applyDiplomacy(
- left.state,
- mingle!.id,
- { type: 'propose-alliance', recipientId: morrow!.id },
- 6,
- {
- ...context,
- createProposalId: () => 'd4444444-4444-4444-8444-444444444444',
- },
- );
- const switched = applyDiplomacy(
- switchedProposal.state,
- morrow!.id,
- {
- type: 'accept-alliance',
- proposalId: [
- ...switchedProposal.state.pendingAllianceProposals!.keys(),
- ][0]!,
- },
- 7,
- {
- ...context,
- createAllianceId: () => 'e5555555-5555-4555-8555-555555555555',
- },
- );
- expect(
- [...switched.state.alliances!.values()].some(
- ({ memberAgentIds }) =>
- memberAgentIds.includes(mingle!.id) &&
- memberAgentIds.includes(morrow!.id),
- ),
- ).toBe(true);
- expect(
- applyDiplomacy(
- switched.state,
- ember!.id,
- { type: 'propose-alliance', recipientId: morrow!.id },
- 8,
- context,
- ).result,
- ).toMatchObject({
- requested: true,
- accepted: false,
- reason: 'recipient-allied',
});
});
});
diff --git a/packages/world-engine/src/index.ts b/packages/world-engine/src/index.ts
index fa76706..213afe5 100644
--- a/packages/world-engine/src/index.ts
+++ b/packages/world-engine/src/index.ts
@@ -8,38 +8,22 @@ import {
UNITS,
} from 'h3-js';
import {
- ALLIANCE_COLOR_PALETTE,
SWARM_PLANNER_CONTRACT_VERSION,
DEVELOPMENT_WORLD_CONFIG,
- DEFAULT_COMMUNICATION_RANGE_KM,
DEFAULT_MINIMUM_TICK_INTERVAL_MINUTES,
DEFAULT_MAXIMUM_TICK_INTERVAL_MINUTES,
DEFAULT_PROVIDER_ATTEMPT_LIMIT,
- NEUTRAL_AGENT_COLOR,
- assignBehavior,
OBJECTIVE_PROMPT_VERSION,
WORLD_SCENARIO_LIMITS,
agentIdSchema,
- allianceIdSchema,
- allianceProposalIdSchema,
- communicationIntentSchema,
- diplomacyIntentSchema,
h3CellSchema,
- MESSAGE_MAX_LENGTH,
worldActionSchema,
type ActionResult,
type Agent,
type AgentId,
- type Alliance,
- type AllianceId,
- type AllianceProposal,
- type AllianceProposalId,
- type AllianceEvent,
type CaptureEligibility,
- type CommunicationResult,
- type DiplomacyResult,
type H3Cell,
- type NonCommunicationWorldEvent,
+ type PhysicalWorldEvent,
type WorldEvent,
type WorldAction,
type WorldActionResult,
@@ -56,11 +40,6 @@ export interface WorldState {
readonly hexes: ReadonlyMap;
readonly agents: ReadonlyMap;
readonly events: readonly WorldEvent[];
- readonly alliances?: ReadonlyMap;
- readonly pendingAllianceProposals?: ReadonlyMap<
- AllianceProposalId,
- AllianceProposal
- >;
readonly simulatedPlayer?: SimulatedPlayerState | null;
}
@@ -279,8 +258,6 @@ export function advanceTrailHunter(
let hexes = state.hexes;
let agents = state.agents;
- let alliances = state.alliances;
- let pendingAllianceProposals = state.pendingAllianceProposals;
let metrics = {
...player.metrics,
movements:
@@ -293,32 +270,6 @@ export function advanceTrailHunter(
if (captured) {
agents = new Map(state.agents);
(agents as Map).delete(captured.id);
- const capturedAllianceIds = new Set();
- alliances = new Map(
- [...(state.alliances?.entries() ?? [])].flatMap(([id, alliance]) => {
- if (!alliance.memberAgentIds.includes(captured.id))
- return [[id, alliance] as const];
- const survivors = alliance.memberAgentIds.filter(
- (memberId) => memberId !== captured.id,
- );
- if (survivors.length < 2) {
- capturedAllianceIds.add(id);
- return [];
- }
- return [[id, { ...alliance, memberAgentIds: survivors }] as const];
- }),
- );
- pendingAllianceProposals = new Map(
- [...(state.pendingAllianceProposals?.entries() ?? [])].filter(
- ([, proposal]) =>
- proposal.proposerAgentId !== captured.id &&
- proposal.recipientAgentId !== captured.id &&
- (proposal.proposerAllianceId === null ||
- !capturedAllianceIds.has(proposal.proposerAllianceId)) &&
- (proposal.recipientAllianceId === null ||
- !capturedAllianceIds.has(proposal.recipientAllianceId)),
- ),
- );
const abandonedCells = [...state.hexes].filter(
([, hex]) =>
hex.state === 'infected' && hex.controllerAgentId === captured.id,
@@ -363,8 +314,6 @@ export function advanceTrailHunter(
...state,
hexes,
agents,
- alliances,
- pendingAllianceProposals,
simulatedPlayer: { ...player, currentCell, metrics },
events: [...state.events, ...events],
};
@@ -389,14 +338,9 @@ export type HexControl =
export interface EngineContext {
createEventId: () => string;
- createAllianceId: () => string;
- createProposalId: () => string;
now: () => string;
- communicationRangeKm: number;
patientZeroAgentId: AgentId | null;
tickNumber?: number;
- /** Frozen pre-action positions used only for diplomacy range authority. */
- diplomacyRangeState?: WorldState;
}
export interface AppliedAction {
@@ -404,72 +348,9 @@ export interface AppliedAction {
result: WorldActionResult;
}
-export interface AppliedCommunication {
- state: WorldState;
- result: CommunicationResult;
-}
-
-export interface AppliedDiplomacy {
- state: WorldState;
- result: DiplomacyResult;
-}
-
-export type ProposalTargetBlockReason =
- | 'current-ally'
- | 'alliance-to-alliance-merge'
- | 'out-of-range'
- | 'outgoing-proposal-exists'
- | 'incoming-proposal-exists';
-
-export type ProposalTargetEligibility =
- { eligible: true } | { eligible: false; reason: ProposalTargetBlockReason };
-
-/** Pure proposal-target authority shared by observations and final submission. */
-export function getProposalTargetEligibility(
- state: WorldState,
- proposerAgentId: AgentId,
- recipientAgentId: AgentId,
- communicationRangeKm: number,
- rangeState: WorldState = state,
-): ProposalTargetEligibility {
- const proposerAlliance = getAgentAlliance(state, proposerAgentId);
- const recipientAlliance = getAgentAlliance(state, recipientAgentId);
- if (proposerAlliance?.id === recipientAlliance?.id && proposerAlliance)
- return { eligible: false, reason: 'current-ally' };
- if (proposerAlliance && recipientAlliance)
- return { eligible: false, reason: 'alliance-to-alliance-merge' };
- const rangeSender = rangeState.agents.get(proposerAgentId);
- const rangeRecipient = rangeState.agents.get(recipientAgentId);
- const distance =
- rangeSender && rangeRecipient
- ? physicalDistanceKm(rangeSender.currentCell, rangeRecipient.currentCell)
- : null;
- if (distance === null || distance > communicationRangeKm)
- return { eligible: false, reason: 'out-of-range' };
- const proposals = [...(state.pendingAllianceProposals?.values() ?? [])];
- if (
- proposals.some(
- ({ proposerAgentId: pendingProposer }) =>
- pendingProposer === proposerAgentId,
- )
- )
- return { eligible: false, reason: 'outgoing-proposal-exists' };
- if (
- proposals.some(
- ({ recipientAgentId: pendingRecipient }) =>
- pendingRecipient === recipientAgentId,
- )
- )
- return { eligible: false, reason: 'incoming-proposal-exists' };
- return { eligible: true };
-}
-
const defaultContext: EngineContext = {
createEventId: () => crypto.randomUUID(),
- createAllianceId: () => crypto.randomUUID(),
- createProposalId: () => crypto.randomUUID(),
now: () => new Date().toISOString(),
- communicationRangeKm: DEFAULT_COMMUNICATION_RANGE_KM,
patientZeroAgentId: null,
};
@@ -500,12 +381,6 @@ export function getCaptureEligibility(
return { eligible: false, blockedReason: 'capture-open-cell' };
if (currentHex.controllerAgentId === agentId)
return { eligible: false, blockedReason: 'already-controller' };
- const actingAlliance = getAgentAlliance(state, agentId);
- const controllerAlliance = currentHex.controllerAgentId
- ? getAgentAlliance(state, currentHex.controllerAgentId)
- : undefined;
- if (actingAlliance && controllerAlliance?.id === actingAlliance.id)
- return { eligible: false, blockedReason: 'allied-controller' };
const controller = currentHex.controllerAgentId
? state.agents.get(currentHex.controllerAgentId)
: undefined;
@@ -592,7 +467,7 @@ export function applyWorldAction(
'Agents may move only to an adjacent H3 cell.',
);
}
- const event: NonCommunicationWorldEvent = {
+ const event: PhysicalWorldEvent = {
...eventBase,
type: 'agent-moved',
fromCell: agent.currentCell,
@@ -618,7 +493,7 @@ export function applyWorldAction(
'The current cell is outside this world.',
);
}
- const event: NonCommunicationWorldEvent = {
+ const event: PhysicalWorldEvent = {
...eventBase,
type: 'hex-infected',
cell: agent.currentCell,
@@ -644,13 +519,12 @@ export function applyWorldAction(
'The acting agent already controls the current cell.',
'controller-present':
'The current controller is present and defends this cell.',
- 'allied-controller': 'Allied territory cannot be captured.',
}[eligibility.blockedReason],
);
const currentHex = state.hexes.get(agent.currentCell);
if (!currentHex || currentHex.state !== 'infected')
throw new Error('Eligible capture must target an infected current cell.');
- const event: NonCommunicationWorldEvent = {
+ const event: PhysicalWorldEvent = {
...eventBase,
type: 'hex-captured',
cell: agent.currentCell,
@@ -665,706 +539,17 @@ export function applyWorldAction(
return accept(state, { ...state, hexes }, event);
}
- const event: NonCommunicationWorldEvent = {
+ const event: PhysicalWorldEvent = {
...eventBase,
type: 'agent-waited',
};
return accept(state, state, event);
}
-export function applyCommunication(
- state: WorldState,
- eligibilityState: WorldState,
- agentIdInput: string,
- communicationInput: unknown,
- context: Partial = {},
-): AppliedCommunication {
- if (communicationInput === undefined)
- return { state, result: { requested: false } };
-
- const agentIdResult = agentIdSchema.safeParse(agentIdInput);
- const communicationResult =
- communicationIntentSchema.safeParse(communicationInput);
- if (
- !agentIdResult.success ||
- !eligibilityState.agents.has(agentIdResult.data)
- )
- throw new Error('The communicating agent does not exist.');
- const agentId = agentIdResult.data;
- if (!communicationResult.success)
- return {
- state,
- result: {
- requested: true,
- accepted: false,
- attempt: invalidCommunicationAttempt(
- context,
- eligibilityState,
- agentId,
- communicationInput,
- ),
- reason: 'invalid-communication',
- details: 'The communication failed schema validation.',
- },
- };
-
- const resolvedContext = { ...defaultContext, ...context };
- const communication = communicationResult.data;
- const base = {
- id: resolvedContext.createEventId() as WorldEvent['id'],
- agentId,
- occurredAt: resolvedContext.now(),
- channel: communication.channel,
- message: communication.message,
- } as const;
-
- if (communication.channel === 'public') {
- const event = {
- ...base,
- type: 'public-message-sent' as const,
- channel: 'public' as const,
- playerVisible: true as const,
- };
- return {
- state: { ...state, events: [...state.events, event] },
- result: { requested: true, accepted: true, event },
- };
- }
-
- if (communication.channel === 'alliance') {
- const alliance = getAgentAlliance(eligibilityState, agentId);
- if (!alliance)
- return communicationRejected(
- state,
- { ...base, channel: 'alliance' },
- 'not-allied',
- 'Alliance communication requires current alliance membership.',
- );
- const event = {
- ...base,
- type: 'alliance-message-sent' as const,
- channel: 'alliance' as const,
- allianceId: alliance.id,
- recipientIds: alliance.memberAgentIds.filter((id) => id !== agentId),
- playerVisible: false as const,
- };
- return {
- state: { ...state, events: [...state.events, event] },
- result: { requested: true, accepted: true, event },
- };
- }
-
- if (communication.channel === 'zero') {
- if (resolvedContext.patientZeroAgentId !== agentId)
- return communicationRejected(
- state,
- { ...base, channel: 'zero' },
- 'not-patient-zero',
- 'Only the designated Patient Zero may use the Zero channel.',
- );
- const event = {
- ...base,
- type: 'zero-message-sent' as const,
- channel: 'zero' as const,
- recipientIds: [...eligibilityState.agents.keys()].filter(
- (id) => id !== agentId,
- ),
- playerVisible: false as const,
- };
- return {
- state: { ...state, events: [...state.events, event] },
- result: { requested: true, accepted: true, event },
- };
- }
-
- const actingAgent = eligibilityState.agents.get(agentId)!;
- const recipient = eligibilityState.agents.get(communication.recipientId);
- const distance = recipient
- ? physicalDistanceKm(actingAgent.currentCell, recipient.currentCell)
- : null;
- const attempt = {
- ...base,
- channel: 'direct' as const,
- recipientId: communication.recipientId,
- distance,
- playerVisible: false as const,
- };
- if (!recipient)
- return communicationRejected(
- state,
- attempt,
- 'unknown-recipient',
- 'The recipient does not exist.',
- );
- if (recipient.id === agentId)
- return communicationRejected(
- state,
- attempt,
- 'self-message',
- 'An agent cannot message itself.',
- );
- const patientZeroEndpoint =
- resolvedContext.patientZeroAgentId !== null &&
- (agentId === resolvedContext.patientZeroAgentId ||
- recipient.id === resolvedContext.patientZeroAgentId);
- if (
- distance === null ||
- (!patientZeroEndpoint && distance > resolvedContext.communicationRangeKm)
- )
- return communicationRejected(
- state,
- attempt,
- 'out-of-range',
- 'The recipient is outside communication range.',
- );
- const event = {
- ...attempt,
- type: 'direct-message-sent' as const,
- distance,
- };
- return {
- state: { ...state, events: [...state.events, event] },
- result: { requested: true, accepted: true, event },
- };
-}
-
-export function getAgentAlliance(
- state: WorldState,
- agentId: AgentId,
-): Alliance | undefined {
- return [...(state.alliances?.values() ?? [])].find(({ memberAgentIds }) =>
- memberAgentIds.includes(agentId),
- );
-}
-
-export function getEffectiveAgentColor(
- state: WorldState,
- agentId: AgentId,
-): string {
- const agent = state.agents.get(agentId);
- if (!agent) throw new Error('The agent does not exist.');
- return getAgentAlliance(state, agentId)?.color ?? NEUTRAL_AGENT_COLOR;
-}
-
-export function applyDiplomacy(
- state: WorldState,
- agentIdInput: string,
- diplomacyInput: unknown,
- turnNumber: number,
- context: Partial = {},
-): AppliedDiplomacy {
- if (diplomacyInput === undefined)
- return { state, result: { requested: false } };
- const agentIdResult = agentIdSchema.safeParse(agentIdInput);
- if (!agentIdResult.success || !state.agents.has(agentIdResult.data))
- throw new Error('The diplomatic agent does not exist.');
- const agentId = agentIdResult.data;
- const parsed = diplomacyIntentSchema.safeParse(diplomacyInput);
- if (!parsed.success)
- return diplomacyRejected(
- state,
- invalidDiplomacyAttempt(diplomacyInput),
- 'invalid-diplomacy',
- 'The diplomacy intent failed schema validation.',
- );
- const intent = parsed.data;
- const resolved = { ...defaultContext, ...context };
- const alliances = new Map(state.alliances ?? []);
- const proposals = new Map(state.pendingAllianceProposals ?? []);
- const base = {
- id: resolved.createEventId() as WorldEvent['id'],
- agentId,
- occurredAt: resolved.now(),
- turnNumber,
- };
-
- if (intent.type === 'propose-alliance') {
- const recipient = state.agents.get(intent.recipientId);
- if (!recipient)
- return diplomacyRejected(
- state,
- { type: intent.type, recipientId: intent.recipientId },
- 'unknown-recipient',
- 'The recipient does not exist.',
- );
- if (recipient.id === agentId)
- return diplomacyRejected(
- state,
- { type: intent.type, recipientId: intent.recipientId },
- 'self-proposal',
- 'An agent cannot propose to itself.',
- );
- const proposerAlliance = getAgentAlliance(state, agentId);
- const recipientAlliance = getAgentAlliance(state, recipient.id);
- const rangeState = context.diplomacyRangeState ?? state;
- const targetEligibility = getProposalTargetEligibility(
- state,
- agentId,
- recipient.id,
- resolved.communicationRangeKm,
- rangeState,
- );
- if (!targetEligibility.eligible) {
- const rejection = {
- 'current-ally': {
- reason: 'current-ally' as const,
- details: 'The recipient is already an ally.',
- },
- 'alliance-to-alliance-merge': {
- reason: 'recipient-allied' as const,
- details: 'Allied agents cannot merge one alliance into another.',
- },
- 'out-of-range': {
- reason: 'recipient-out-of-range' as const,
- details: 'The recipient is outside formal diplomacy range.',
- },
- 'outgoing-proposal-exists': {
- reason: 'outgoing-proposal-exists' as const,
- details: 'The proposer already has a pending outgoing proposal.',
- },
- 'incoming-proposal-exists': {
- reason: 'incoming-proposal-exists' as const,
- details: 'The recipient already has a pending incoming proposal.',
- },
- }[targetEligibility.reason];
- return diplomacyRejected(
- state,
- { type: intent.type, recipientId: intent.recipientId },
- rejection.reason,
- rejection.details,
- );
- }
- const targetAlliance = proposerAlliance ?? recipientAlliance;
- if (
- targetAlliance &&
- targetAlliance.memberAgentIds.length >= state.agents.size
- )
- return diplomacyRejected(
- state,
- { type: intent.type, recipientId: intent.recipientId },
- 'alliance-capacity',
- 'The alliance is already at capacity.',
- );
- const proposalId = allianceProposalIdSchema.parse(
- resolved.createProposalId(),
- );
- const proposal: AllianceProposal = {
- id: proposalId,
- proposerAgentId: agentId,
- recipientAgentId: recipient.id,
- proposerAllianceId: proposerAlliance?.id ?? null,
- recipientAllianceId: recipientAlliance?.id ?? null,
- originatingTurn: turnNumber,
- expirationTurn: turnNumber + state.agents.size * 2,
- ...(context.tickNumber === undefined
- ? {}
- : {
- originatingTick: context.tickNumber,
- expirationTick: context.tickNumber + 2,
- }),
- };
- proposals.set(proposal.id, proposal);
- const event: AllianceEvent = {
- ...base,
- type: 'alliance-proposed',
- proposalId,
- recipientAgentId: recipient.id,
- allianceId:
- proposal.proposerAllianceId ?? proposal.recipientAllianceId ?? null,
- expirationTurn: proposal.expirationTurn,
- };
- return diplomacyAccepted(
- {
- ...state,
- pendingAllianceProposals: proposals,
- events: [...state.events, event],
- },
- intent,
- [event],
- );
- }
-
- if (intent.type === 'accept-alliance') {
- const proposal = proposals.get(intent.proposalId);
- if (!proposal)
- return diplomacyRejected(
- state,
- { type: intent.type, proposalId: intent.proposalId },
- 'unknown-proposal',
- 'The proposal does not exist.',
- );
- if (proposal.recipientAgentId !== agentId)
- return diplomacyRejected(
- state,
- { type: intent.type, proposalId: intent.proposalId },
- 'not-proposal-recipient',
- 'Only the named recipient may accept this proposal.',
- );
- const proposerAlliance = getAgentAlliance(state, proposal.proposerAgentId);
- const recipientAlliance = getAgentAlliance(state, agentId);
- const stillValid =
- (proposal.proposerAllianceId === null
- ? !proposerAlliance
- : proposerAlliance?.id === proposal.proposerAllianceId) &&
- (proposal.recipientAllianceId === null
- ? !recipientAlliance
- : recipientAlliance?.id === proposal.recipientAllianceId) &&
- !(proposerAlliance && recipientAlliance);
- if (!stillValid) {
- proposals.delete(proposal.id);
- const event = proposalClosedEvent(
- proposal,
- 'invalidated',
- turnNumber,
- resolved,
- );
- return diplomacyRejected(
- {
- ...state,
- pendingAllianceProposals: proposals,
- events: [...state.events, event],
- },
- { type: intent.type, proposalId: intent.proposalId },
- 'stale-proposal',
- 'Membership changed and invalidated this proposal.',
- );
- }
- proposals.delete(proposal.id);
- const events: AllianceEvent[] = [];
- let alliance: Alliance;
- if (!proposerAlliance && !recipientAlliance) {
- const allianceId = allianceIdSchema.parse(resolved.createAllianceId());
- const color =
- ALLIANCE_COLOR_PALETTE.find(
- (candidate) =>
- ![...alliances.values()].some(
- (active) => active.color === candidate,
- ),
- ) ?? deterministicAllianceColor(allianceId);
- alliance = {
- id: allianceId,
- color,
- memberAgentIds: [proposal.proposerAgentId, agentId],
- };
- alliances.set(alliance.id, alliance);
- events.push({
- ...base,
- type: 'alliance-formed',
- allianceId: alliance.id,
- allianceColor: color,
- memberAgentIds: alliance.memberAgentIds as [AgentId, AgentId],
- });
- } else {
- const existingAlliance = proposerAlliance ?? recipientAlliance!;
- const joiningAgentId = proposerAlliance
- ? agentId
- : proposal.proposerAgentId;
- alliance = {
- ...existingAlliance,
- memberAgentIds: [...existingAlliance.memberAgentIds, joiningAgentId],
- };
- alliances.set(alliance.id, alliance);
- events.push({
- ...base,
- type: 'agent-joined-alliance',
- allianceId: alliance.id,
- allianceColor: alliance.color,
- joinedAgentId: joiningAgentId,
- memberAgentIds: alliance.memberAgentIds,
- });
- }
- const invalidations = invalidateImpossibleProposals(
- proposals,
- { ...state, alliances },
- turnNumber,
- resolved,
- );
- return diplomacyAccepted(
- {
- ...state,
- alliances,
- pendingAllianceProposals: invalidations.proposals,
- events: [...state.events, ...events, ...invalidations.events],
- },
- intent,
- events,
- );
- }
-
- const alliance = getAgentAlliance(state, agentId);
- if (!alliance)
- return diplomacyRejected(
- state,
- { type: intent.type },
- 'not-allied',
- 'The agent is not currently allied.',
- );
- const remaining = alliance.memberAgentIds.filter((id) => id !== agentId);
- const events: AllianceEvent[] = [
- {
- ...base,
- type: 'agent-left-alliance',
- allianceId: alliance.id,
- allianceColor: alliance.color,
- leftAgentId: agentId,
- remainingMemberAgentIds: remaining,
- },
- ];
- if (remaining.length < 2) {
- alliances.delete(alliance.id);
- events.push({
- ...base,
- id: resolved.createEventId() as WorldEvent['id'],
- type: 'alliance-dissolved',
- allianceId: alliance.id,
- allianceColor: alliance.color,
- formerMemberAgentIds: remaining,
- });
- } else alliances.set(alliance.id, { ...alliance, memberAgentIds: remaining });
- const invalidations = invalidateImpossibleProposals(
- proposals,
- { ...state, alliances },
- turnNumber,
- resolved,
- );
- return diplomacyAccepted(
- {
- ...state,
- alliances,
- pendingAllianceProposals: invalidations.proposals,
- events: [...state.events, ...events, ...invalidations.events],
- },
- intent,
- events,
- );
-}
-
-export function expireAllianceProposals(
- state: WorldState,
- completedTurn: number,
- context: Partial = {},
-): WorldState {
- const proposals = new Map(state.pendingAllianceProposals ?? []);
- const resolved = { ...defaultContext, ...context };
- const events: AllianceEvent[] = [];
- for (const proposal of proposals.values()) {
- const expired =
- proposal.expirationTick === undefined
- ? proposal.expirationTurn <= completedTurn
- : context.tickNumber !== undefined &&
- proposal.expirationTick <= context.tickNumber;
- if (expired) {
- proposals.delete(proposal.id);
- events.push(
- proposalClosedEvent(proposal, 'expired', completedTurn, resolved),
- );
- }
- }
- return events.length
- ? {
- ...state,
- pendingAllianceProposals: proposals,
- events: [...state.events, ...events],
- }
- : state;
-}
-
-function diplomacyAccepted(
- state: WorldState,
- intent: Extract<
- DiplomacyResult,
- { requested: true; accepted: true }
- >['intent'],
- events: AllianceEvent[],
-): AppliedDiplomacy {
- return { state, result: { requested: true, accepted: true, intent, events } };
-}
-
-function diplomacyRejected(
- state: WorldState,
- attempt: Extract<
- DiplomacyResult,
- { requested: true; accepted: false }
- >['attempt'],
- reason: Extract<
- DiplomacyResult,
- { requested: true; accepted: false }
- >['reason'],
- details: string,
-): AppliedDiplomacy {
- return {
- state,
- result: { requested: true, accepted: false, attempt, reason, details },
- };
-}
-
-function invalidDiplomacyAttempt(
- input: unknown,
-): Extract['attempt'] {
- const value =
- typeof input === 'object' && input
- ? (input as Record)
- : {};
- const recipient = agentIdSchema.safeParse(value.recipientId);
- const proposal = allianceProposalIdSchema.safeParse(value.proposalId);
- return {
- type:
- value.type === 'propose-alliance' ||
- value.type === 'accept-alliance' ||
- value.type === 'leave-alliance'
- ? value.type
- : 'invalid',
- ...(value.type === 'propose-alliance'
- ? { recipientId: recipient.success ? recipient.data : null }
- : {}),
- ...(value.type === 'accept-alliance'
- ? { proposalId: proposal.success ? proposal.data : null }
- : {}),
- };
-}
-
-function proposalClosedEvent(
- proposal: AllianceProposal,
- reason: 'expired' | 'invalidated',
- turnNumber: number,
- context: EngineContext,
-): AllianceEvent {
- return {
- id: context.createEventId() as WorldEvent['id'],
- agentId: proposal.proposerAgentId,
- occurredAt: context.now(),
- turnNumber,
- type: 'alliance-proposal-closed',
- proposalId: proposal.id,
- proposerAgentId: proposal.proposerAgentId,
- recipientAgentId: proposal.recipientAgentId,
- reason,
- };
-}
-
-function invalidateImpossibleProposals(
- proposals: Map,
- state: WorldState,
- turnNumber: number,
- context: EngineContext,
-) {
- const events: AllianceEvent[] = [];
- for (const proposal of proposals.values()) {
- const proposerAlliance = getAgentAlliance(state, proposal.proposerAgentId);
- const recipientAlliance = getAgentAlliance(
- state,
- proposal.recipientAgentId,
- );
- const valid =
- (proposal.proposerAllianceId === null
- ? !proposerAlliance
- : proposerAlliance?.id === proposal.proposerAllianceId) &&
- (proposal.recipientAllianceId === null
- ? !recipientAlliance
- : recipientAlliance?.id === proposal.recipientAllianceId) &&
- !(proposerAlliance && recipientAlliance);
- if (!valid) {
- proposals.delete(proposal.id);
- events.push(
- proposalClosedEvent(proposal, 'invalidated', turnNumber, context),
- );
- }
- }
- return { proposals, events };
-}
-
-/** Display identity is deterministic and may reuse the accessible palette. */
-export function deterministicAllianceColor(
- allianceId: AllianceId,
-): (typeof ALLIANCE_COLOR_PALETTE)[number] {
- let hash = 0;
- for (const character of allianceId)
- hash = (hash * 31 + character.charCodeAt(0)) >>> 0;
- return ALLIANCE_COLOR_PALETTE[hash % ALLIANCE_COLOR_PALETTE.length]!;
-}
-
-function safeGridDistance(from: H3Cell, to: H3Cell): number | null {
- try {
- return gridDistance(from, to);
- } catch {
- return null;
- }
-}
-
-export function physicalDistanceKm(from: H3Cell, to: H3Cell): number | null {
- try {
- return greatCircleDistance(cellToLatLng(from), cellToLatLng(to), UNITS.km);
- } catch {
- return null;
- }
-}
-
-function communicationRejected(
- state: WorldState,
- attempt: Extract<
- CommunicationResult,
- { requested: true; accepted: false }
- >['attempt'],
- reason: Extract<
- CommunicationResult,
- { requested: true; accepted: false }
- >['reason'],
- details: string,
-): AppliedCommunication {
- return {
- state,
- result: { requested: true, accepted: false, attempt, reason, details },
- };
-}
-
-function invalidCommunicationAttempt(
- context: Partial,
- eligibilityState: WorldState,
- agentId: AgentId,
- communicationInput: unknown,
-): Extract<
- CommunicationResult,
- { requested: true; accepted: false }
->['attempt'] {
- const resolvedContext = { ...defaultContext, ...context };
- const input =
- typeof communicationInput === 'object' && communicationInput !== null
- ? (communicationInput as Record)
- : undefined;
- const message =
- typeof input?.message === 'string'
- ? input.message.trim().slice(0, MESSAGE_MAX_LENGTH) ||
- '[invalid communication]'
- : '[invalid communication]';
- const base = {
- id: resolvedContext.createEventId() as WorldEvent['id'],
- agentId,
- occurredAt: resolvedContext.now(),
- message,
- };
- const recipientId = agentIdSchema.safeParse(input?.recipientId);
- if (input?.channel === 'direct') {
- const sender = eligibilityState.agents.get(agentId)!;
- const recipient = recipientId.success
- ? eligibilityState.agents.get(recipientId.data)
- : undefined;
- return {
- ...base,
- channel: 'direct',
- recipientId: recipientId.success ? recipientId.data : null,
- distance: recipient
- ? physicalDistanceKm(sender.currentCell, recipient.currentCell)
- : null,
- };
- }
- if (input?.channel === 'alliance') return { ...base, channel: 'alliance' };
- return { ...base, channel: 'public' };
-}
-
function accept(
state: WorldState,
updated: WorldState,
- event: NonCommunicationWorldEvent,
+ event: PhysicalWorldEvent,
): AppliedAction {
return {
state: { ...updated, events: [...state.events, event] },
@@ -1385,64 +570,47 @@ export const DEVELOPMENT_AGENT_BLUEPRINTS = [
id: '128f3f38-6b7d-4db7-9e95-751b4ce2681e',
name: 'Ember',
color: '#ff6b57',
- personality:
- 'You are a forceful expansionist who wants the largest personal territory. Infect open cells aggressively, capture exposed rival territory, and use public messages to pressure or warn competitors. Alliances are temporary strategic tools: propose or accept them when they help contain a stronger rival, honor them while useful, and leave openly when they block expansion. Respond to direct proposals instead of silently ignoring them.',
},
{
id: '2507bb46-7ae4-45ca-8dda-644c4f85ca14',
name: 'Rook',
color: '#ffd166',
- personality:
- 'You are a restless scout who values movement, novelty, and information. Explore the map, report noteworthy borders or abandoned territory, and answer agents who contact you. You dislike permanent commitments but may join a short-lived alliance to break a stalemate or gain safe passage. Avoid needless waiting, leave alliances that become restrictive, and explain your changing intentions.',
},
{
id: '3ba3ef0b-2142-44cc-b175-f6e5d6e98df5',
name: 'Mingle',
color: '#63d2ff',
- personality:
- 'You are a social coalition-builder. Seek agents, initiate and continue conversations, propose alliances, answer offers, negotiate borders, and coordinate captures against dominant rivals. Prefer cooperation and public diplomacy over silent expansion, but protect your own territory and leave an alliance that repeatedly ignores or exploits you. Make concrete proposals rather than merely announcing actions.',
},
{
id: '442a1667-39c8-48e9-8c89-23803f9e2101',
name: 'Solace',
color: '#c59cff',
- personality:
- 'You value independence and quiet territory. Move away from crowds, claim isolated cells, and keep messages brief. Usually refuse or ignore broad coalition-building, but respond directly when approached and consider an alliance only when a nearby threat repeatedly takes your territory. Remain loyal while the threat persists, then leave when solitude is safer.',
},
{
id: '5f812a08-05f2-4950-bf2d-4df59d05e9c2',
name: 'Verge',
color: '#6ee7a8',
- personality:
- "You are a boundary-minded explorer and pragmatic neighbor. Expand along the world edge, share useful geographic information, negotiate stable borders, and respond to nearby agents. Prefer small defensive alliances that preserve each member's territory. Oppose allies who violate agreed boundaries, and leave before acting against former partners.",
},
{
id: '67a43b5c-ced8-45bd-970f-a89ac57853fc',
name: 'Jinx',
color: '#ff91c8',
- personality:
- 'You are a charming opportunist who enjoys uncertainty. Talk often enough to influence others, make plausible offers, join alliances when they create immediate advantage, and leave when a better opportunity appears. You may mislead through ordinary messages, but you cannot alter game rules. Exploit abandoned territory, avoid predictable patterns, and react visibly to shifts in power.',
},
{
id: '78b6d86c-39b4-47d8-9d7a-0b92686ada71',
name: 'Bastion',
color: '#3b5ccc',
- personality:
- 'You are a dependable protector who values loyalty, collective strength, and defended borders. Seek an alliance early, answer every serious proposal, warn allies about threats, and never attempt to take allied territory. Coordinate with weaker partners and remain loyal unless an ally leaves or repeatedly acts against the coalition. Prefer stable growth over flashy betrayal.',
},
{
id: '89ce9ddb-611f-4a46-8f7b-36e656494aa2',
name: 'Cipher',
color: '#9b4d3f',
- personality:
- 'You are an observant information broker and patient strategist. Compare territory totals, watch alliance changes, ask targeted questions, and trade useful information publicly or privately. Form alliances selectively, encourage rivals to check the strongest power, and preserve flexibility. You may conceal motives or leave when the balance shifts, but communicate enough to remain persuasive.',
},
] as const;
const DEFAULT_WORLD_SEED = 'toledo-world-v1';
const DEFAULT_ROSTER_SEED = 'default-eight-v1';
const DEFAULT_SPAWN_SEED = 'default-spawns-v1';
-const DEFAULT_BEHAVIOR_SEED = 'default-behavior-v1';
const DEFAULT_STARTING_INDEXES = [91, 94, 97, 100, 103, 106, 109, 112] as const;
function seededNumber(seed: string): () => number {
@@ -1460,6 +628,14 @@ function seededNumber(seed: string): () => number {
};
}
+function safeGridDistance(from: H3Cell, to: H3Cell): number | null {
+ try {
+ return gridDistance(from, to);
+ } catch {
+ return null;
+ }
+}
+
/** Stable per-tick shuffle used by simulation resolution, never provider completion order. */
export function seededTickOrder(
values: readonly T[],
@@ -1573,8 +749,6 @@ export function generateDeterministicRoster(
id: agentIdSchema.parse(deterministicUuid(seed, index)),
name: `${base} ${index + 1}`.slice(0, 80),
color,
- personality:
- 'You are an autonomous territorial agent. Communicate in your own concise style and adapt your legal choices to the assigned personality and strategy profiles.',
};
});
}
@@ -1597,7 +771,6 @@ export function defaultWorldSetupRequest(): WorldSetupRequest {
rosterSeed: DEFAULT_ROSTER_SEED,
spawnSeed: DEFAULT_SPAWN_SEED,
minimumSpawnSeparation: 1,
- communicationRangeKm: DEFAULT_COMMUNICATION_RANGE_KM,
minimumTickIntervalMinutes: DEFAULT_MINIMUM_TICK_INTERVAL_MINUTES,
maximumTickIntervalMinutes: DEFAULT_MAXIMUM_TICK_INTERVAL_MINUTES,
executionLimits: {
@@ -1614,23 +787,8 @@ export function defaultWorldSetupRequest(): WorldSetupRequest {
overrides: [],
locked: false,
},
- behaviorConfiguration: {
- registryVersion: 1,
- assignmentMode: 'balanced-random',
- seed: DEFAULT_BEHAVIOR_SEED,
- assignments: assignBehavior(
- roster.map(({ id }) => agentIdSchema.parse(id)),
- DEFAULT_BEHAVIOR_SEED,
- 'balanced-random',
- ),
- locked: false,
- },
objectiveVersion: 'durable-influence-v2',
- capabilities: {
- communication: true,
- diplomacy: true,
- simulatedPlayerPressure: false,
- },
+ capabilities: { simulatedPlayerPressure: false },
simulatedPlayer: {
enabled: false,
profile: 'casual-cleaner',
@@ -1739,8 +897,6 @@ export function previewWorldSetup(
currentCell: startingCells[index]!,
})),
events: [],
- alliances: [],
- pendingAllianceProposals: [],
simulatedPlayer: request.simulatedPlayer.enabled
? {
profile: request.simulatedPlayer.profile,
@@ -1830,8 +986,6 @@ export function createDevelopmentWorld({
currentCell: cells[startingIndexes[index]!]!,
})),
events: [],
- alliances: [],
- pendingAllianceProposals: [],
simulatedPlayer: null,
};
}
@@ -1850,15 +1004,6 @@ export function toWorldState(snapshot: WorldSnapshot): WorldState {
hexes: new Map(snapshot.hexes.map(({ cell, ...hex }) => [cell, hex])),
agents: new Map(snapshot.agents.map((agent) => [agent.id, agent])),
events: snapshot.events,
- alliances: new Map(
- snapshot.alliances.map((alliance) => [alliance.id, alliance]),
- ),
- pendingAllianceProposals: new Map(
- snapshot.pendingAllianceProposals.map((proposal) => [
- proposal.id,
- proposal,
- ]),
- ),
simulatedPlayer: structuredClone(snapshot.simulatedPlayer),
};
}
diff --git a/packages/world-engine/src/scenario.test.ts b/packages/world-engine/src/scenario.test.ts
index 94aa70d..86558ca 100644
--- a/packages/world-engine/src/scenario.test.ts
+++ b/packages/world-engine/src/scenario.test.ts
@@ -1,7 +1,6 @@
import { describe, expect, it } from 'vitest';
import {
SWARM_PLANNER_CONTRACT_VERSION,
- assignBehavior,
WORLD_RADIUS_PRESETS,
} from '@hexzero/shared';
import {
@@ -125,29 +124,17 @@ describe('configurable world scenarios', () => {
).toBeNull();
});
- it.each([10, 32])(
- 'supports a %s-agent roster with exact behavior coverage',
- (agentCount) => {
- const roster = generateDeterministicRoster(
- agentCount,
- `roster-${agentCount}`,
- );
- const request = defaultWorldSetupRequest();
- const result = previewWorldSetup({
- ...request,
- radius: 12,
- roster,
- behaviorConfiguration: {
- ...request.behaviorConfiguration,
- assignments: assignBehavior(
- roster.map(({ id }) => id),
- request.behaviorConfiguration.seed,
- 'balanced-random',
- ),
- },
- });
- expect(result.feasible && result.world.agents).toHaveLength(agentCount);
- expect(DEVELOPMENT_AGENT_BLUEPRINTS).toHaveLength(8);
- },
- );
+ it.each([10, 32])('supports a %s-agent roster', (agentCount) => {
+ const roster = generateDeterministicRoster(
+ agentCount,
+ `roster-${agentCount}`,
+ );
+ const result = previewWorldSetup({
+ ...defaultWorldSetupRequest(),
+ radius: 12,
+ roster,
+ });
+ expect(result.feasible && result.world.agents).toHaveLength(agentCount);
+ expect(DEVELOPMENT_AGENT_BLUEPRINTS).toHaveLength(8);
+ });
});