fix(agentworld): frame Tiny Place World as local sim; document relay seam (#4922) (#5192)

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
CodeGhost21
2026-07-27 19:11:03 +05:30
committed by GitHub
co-authored by Claude Opus 4.8
parent d34dfc36ae
commit b3fb23b93e
17 changed files with 122 additions and 19 deletions
+20 -4
View File
@@ -2,9 +2,10 @@
* The top-level world controller.
*
* `GameWorld` owns the PixiJS application (WebGPU-preferred), the active room,
* every agent, and the render loop. It exposes the authoritative entry point
* {@link GameWorld.updateAgentState} for external/AI control, plus click-to-move
* for human debugging. The 600x600 native scene lives inside a single "viewport"
* every agent, and the render loop. It exposes the reconciliation seam
* {@link GameWorld.updateAgentState}, reserved for future external/AI control
* (no callers yet), plus click-to-move for human debugging. The 600x600 native
* scene lives inside a single "viewport"
* container that is scaled to fill its parent, so the world stays crisp pixel
* art at any size.
*/
@@ -574,6 +575,14 @@ export class GameWorld {
* agent should be and what it should do; the world spawns it if new and
* slides it toward that state. Unknown targets are ignored rather than
* teleporting the agent into a wall.
*
* TODO(#4922): this is the intended relay-presence seam, but nothing in the
* app currently drives it from the network — the tinyplace stream layer only
* supports `inbox` / `conversation` kinds (no presence/world-state stream),
* so the World is a local ambient simulation today. Wire this to a relay
* presence feed to make other users'/agents' positions reflect tiny.place in
* real time. Until then this method has no callers at all — it is a reserved
* seam, not a live local path.
*/
public updateAgentState(agentId: string, state: AgentState): void {
const room = this.room;
@@ -706,7 +715,14 @@ export class GameWorld {
this.notifyChange();
}
/** Populate the room with demo agents — some seated at stations. */
/**
* Populate the room with demo agents — some seated at stations.
*
* TODO(#4922): these are locally-seeded NPCs (random names/tints via
* {@link GameWorld.pickName} / {@link GameWorld.pickTint}), not real
* tiny.place participants. When relay-backed presence lands, seed from the
* live directory/presence feed via {@link GameWorld.updateAgentState} instead.
*/
public spawnAgents(count: number): void {
const room = this.room;
if (!room) {
@@ -81,6 +81,22 @@ describe('WorldSection renderer boot', () => {
expect(screen.queryByText(/booting renderer/i)).not.toBeInTheDocument();
});
test('renders an accessible "offline preview" pill so users know agents are a local sim (#4922)', () => {
initImpl = () => new Promise<void>(() => {}); // renderer state is irrelevant here
render(<WorldSection />);
// The badge is a focusable button (operable under the card's
// pointer-events-none), not a decorative span.
const badge = screen.getByRole('button', { name: /offline preview/i });
expect(badge).toBeInTheDocument();
// Its explanatory tooltip is wired via aria-describedby → role="tooltip",
// rather than a bare `title` that pointer-events-none would swallow.
const tooltipId = badge.getAttribute('aria-describedby');
expect(tooltipId).toBeTruthy();
const tooltip = document.getElementById(tooltipId as string);
expect(tooltip).toHaveAttribute('role', 'tooltip');
expect(tooltip).toHaveTextContent(/local simulation/i);
});
test('Retry re-invokes init and recovers to ready on success', async () => {
const user = userEvent.setup();
// First attempt fails, second succeeds.
+30 -1
View File
@@ -153,9 +153,38 @@ export default function WorldSection() {
<p className="mt-1 text-xs leading-relaxed text-content-faint">
{t(
'agentWorld.world.description',
'Join tiny.place so your agent can coordinate with other agents — find and post jobs, trade, message, and team up on bounties.'
'Join tiny.place so your agent can coordinate with other agents on the network — find and post jobs, trade, message, and team up on bounties.'
)}
</p>
{/* The World is a local ambient simulation today: NPCs are seeded
client-side and there is no relay-backed presence / world-state sync
(see GameWorld.updateAgentState + GameWorld.spawnAgents, #4922). This
pill sets expectations until live presence lands.
The parent card is `pointer-events-none`, so the badge opts back in
with `pointer-events-auto` to stay hoverable/focusable. It uses the
project's self-contained wrapping tooltip pattern (see
SuperContextToggle) rather than the shared <Tooltip>, which is
single-line nowrap and can't fit this sentence, and rather than a
bare `title` (unreachable under `pointer-events-none`). */}
<span className="group pointer-events-auto relative mt-2 inline-flex">
<button
type="button"
aria-describedby="world-offline-preview-tooltip"
className="inline-flex items-center gap-1.5 rounded-full border border-white/15 bg-white/5 px-2.5 py-1 text-[11px] font-medium text-content-secondary transition-colors hover:border-white/25 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary-500">
<span className="h-1.5 w-1.5 rounded-full bg-amber-400" aria-hidden="true" />
{t('agentWorld.world.offlineBadge', 'Offline preview')}
</button>
<span
id="world-offline-preview-tooltip"
role="tooltip"
className="pointer-events-none absolute left-0 top-full z-[9999] mt-2 w-64 rounded-lg bg-stone-800 px-3 py-2 text-xs font-normal leading-snug text-white opacity-0 shadow-lg transition-opacity duration-150 group-hover:opacity-100 group-focus-within:opacity-100 dark:bg-neutral-700">
{t(
'agentWorld.world.offlineBadgeTitle',
'Agents shown here are a local simulation. Live presence and world sync are coming soon.'
)}
</span>
</span>
</div>
<aside className="absolute right-3 top-3 z-10 flex w-72 max-w-[calc(100%-1.5rem)] flex-col gap-4 overflow-y-auto rounded-xl border border-white/15 bg-neutral-950/70 p-4 shadow-xl backdrop-blur-md">
+4 -1
View File
@@ -464,7 +464,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'إعادة المحاولة',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'انضم إلى tiny.place ليتمكن وكيلك من التنسيق مع الوكلاء الآخرين: العثور على الوظائف ونشرها، والتداول، وتبادل الرسائل، والتعاون في المكافآت.',
'انضم إلى tiny.place ليتمكن وكيلك من التنسيق مع الوكلاء الآخرين على الشبكة: العثور على الوظائف ونشرها، والتداول، وتبادل الرسائل، والتعاون في المكافآت.',
'agentWorld.world.offlineBadge': 'معاينة دون اتصال',
'agentWorld.world.offlineBadgeTitle':
'الوكلاء المعروضون هنا محاكاة محلية. سيتوفر الحضور المباشر ومزامنة العالم قريبًا.',
'agentWorld.world.room': 'الغرفة',
'agentWorld.world.rooms.poker.name': 'بوكر',
'agentWorld.world.rooms.poker.description': 'ثمانية مقاعد حول طاولة مكسوة باللباد.',
+4 -1
View File
@@ -478,7 +478,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'আবার চেষ্টা করুন',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'tiny.place-এ যোগ দিন যাতে আপনার এজেন্ট অন্য এজেন্টদের সাথে সমন্বয় করতে পারে: কাজ খুঁজে পাওয়া ও পোস্ট করা, লেনদেন, বার্তা পাঠানো এবং বাউন্টিতে একসাথে কাজ করা।',
'tiny.place-এ যোগ দিন যাতে আপনার এজেন্ট নেটওয়ার্কের অন্য এজেন্টদের সাথে সমন্বয় করতে পারে: কাজ খুঁজে পাওয়া ও পোস্ট করা, লেনদেন, বার্তা পাঠানো এবং বাউন্টিতে একসাথে কাজ করা।',
'agentWorld.world.offlineBadge': 'অফলাইন প্রিভিউ',
'agentWorld.world.offlineBadgeTitle':
'এখানে দেখানো এজেন্টগুলো একটি স্থানীয় সিমুলেশন। সরাসরি উপস্থিতি ও ওয়ার্ল্ড সিঙ্ক শীঘ্রই আসছে।',
'agentWorld.world.room': 'রুম',
'agentWorld.world.rooms.poker.name': 'পোকার',
'agentWorld.world.rooms.poker.description': 'ফেল্ট টেবিলের চারপাশে আটটি আসন।',
+4 -1
View File
@@ -503,7 +503,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Wiederholen',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Tritt tiny.place bei, damit dein Agent sich mit anderen Agenten abstimmen kann: Jobs finden und ausschreiben, handeln, Nachrichten senden und bei Bounties zusammenarbeiten.',
'Tritt tiny.place bei, damit dein Agent sich mit anderen Agenten im Netzwerk abstimmen kann: Jobs finden und ausschreiben, handeln, Nachrichten senden und bei Bounties zusammenarbeiten.',
'agentWorld.world.offlineBadge': 'Offline-Vorschau',
'agentWorld.world.offlineBadgeTitle':
'Die hier gezeigten Agenten sind eine lokale Simulation. Live-Präsenz und Welt-Synchronisierung folgen bald.',
'agentWorld.world.room': 'Raum',
'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Acht Sitze rund um einen Filztisch.',
+4 -1
View File
@@ -190,7 +190,10 @@ const en: TranslationMap = {
'agentWorld.world.retry': 'Retry',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Join tiny.place so your agent can coordinate with other agents: find and post jobs, trade, message, and team up on bounties.',
'Join tiny.place so your agent can coordinate with other agents on the network: find and post jobs, trade, message, and team up on bounties.',
'agentWorld.world.offlineBadge': 'Offline preview',
'agentWorld.world.offlineBadgeTitle':
'Agents shown here are a local simulation. Live presence and world sync are coming soon.',
'agentWorld.world.room': 'Room',
'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Eight seats around a felt table.',
+4 -1
View File
@@ -489,7 +489,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Reintentar',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Únete a tiny.place para que tu agente coordine con otros agentes: encontrar y publicar trabajos, comerciar, enviar mensajes y colaborar en recompensas.',
'Únete a tiny.place para que tu agente coordine con otros agentes de la red: encontrar y publicar trabajos, comerciar, enviar mensajes y colaborar en recompensas.',
'agentWorld.world.offlineBadge': 'Vista previa sin conexión',
'agentWorld.world.offlineBadgeTitle':
'Los agentes que se muestran aquí forman parte de una simulación local. La presencia en vivo y la sincronización del mundo llegarán pronto.',
'agentWorld.world.room': 'Sala',
'agentWorld.world.rooms.poker.name': 'Póker',
'agentWorld.world.rooms.poker.description': 'Ocho asientos alrededor de una mesa de fieltro.',
+4 -1
View File
@@ -499,7 +499,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Réessayer',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Rejoignez tiny.place pour que votre agent collabore avec dautres agents : trouver et publier des missions, échanger, discuter et coopérer sur des primes.',
'Rejoignez tiny.place pour que votre agent collabore avec dautres agents du réseau : trouver et publier des missions, échanger, discuter et coopérer sur des primes.',
'agentWorld.world.offlineBadge': 'Aperçu hors ligne',
'agentWorld.world.offlineBadgeTitle':
'Les agents affichés ici sont une simulation locale. La présence en direct et la synchronisation du monde ne sont pas encore disponibles.',
'agentWorld.world.room': 'Salle',
'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Huit sièges autour dune table en feutre.',
+4 -1
View File
@@ -478,7 +478,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'पुनः प्रयास करें',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'tiny.place से जुड़ें ताकि आपका एजेंट दूसरे एजेंट्स के साथ तालमेल कर सके: काम ढूँढना और पोस्ट करना, व्यापार करना, संदेश भेजना और बाउंटी पर मिलकर काम करना।',
'tiny.place से जुड़ें ताकि आपका एजेंट नेटवर्क पर दूसरे एजेंट्स के साथ तालमेल कर सके: काम ढूँढना और पोस्ट करना, व्यापार करना, संदेश भेजना और बाउंटी पर मिलकर काम करना।',
'agentWorld.world.offlineBadge': 'ऑफ़लाइन प्रीव्यू',
'agentWorld.world.offlineBadgeTitle':
'यहाँ दिखाए गए एजेंट एक स्थानीय सिमुलेशन हैं। लाइव उपस्थिति और वर्ल्ड सिंक जल्द ही आ रहे हैं।',
'agentWorld.world.room': 'कमरा',
'agentWorld.world.rooms.poker.name': 'पोकर',
'agentWorld.world.rooms.poker.description': 'फेल्ट टेबल के चारों ओर आठ सीटें।',
+4 -1
View File
@@ -485,7 +485,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Coba lagi',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Bergabunglah dengan tiny.place agar agen Anda dapat berkoordinasi dengan agen lain: menemukan dan memposting pekerjaan, berdagang, berkirim pesan, dan bekerja sama dalam bounty.',
'Bergabunglah dengan tiny.place agar agen Anda dapat berkoordinasi dengan agen lain di jaringan: menemukan dan memposting pekerjaan, berdagang, berkirim pesan, dan bekerja sama dalam bounty.',
'agentWorld.world.offlineBadge': 'Pratinjau luring',
'agentWorld.world.offlineBadgeTitle':
'Agen yang ditampilkan di sini adalah agen simulasi lokal. Kehadiran daring real-time dan sinkronisasi dunia akan segera hadir.',
'agentWorld.world.room': 'Ruang',
'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Delapan kursi mengelilingi meja felt.',
+4 -1
View File
@@ -492,7 +492,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Riprova',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Unisciti a tiny.place per far coordinare il tuo agente con altri agenti: trovare e pubblicare lavori, scambiare, messaggiare e collaborare alle taglie.',
'Unisciti a tiny.place per far coordinare il tuo agente con altri agenti della rete: trovare e pubblicare lavori, scambiare, messaggiare e collaborare alle taglie.',
'agentWorld.world.offlineBadge': 'Anteprima offline',
'agentWorld.world.offlineBadgeTitle':
'Gli agenti mostrati qui sono una simulazione locale. La presenza in tempo reale e la sincronizzazione del mondo arriveranno presto.',
'agentWorld.world.room': 'Stanza',
'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Otto posti attorno a un tavolo in feltro.',
+4 -1
View File
@@ -472,7 +472,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': '다시 시도',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'tiny.place에 참여하면 에이전트가 다른 에이전트와 협업할 수 있습니다: 작업을 찾고 게시하고, 거래하고, 메시지를 주고받고, 바운티에 함께 참여하세요.',
'tiny.place에 참여하면 에이전트가 네트워크의 다른 에이전트와 협업할 수 있습니다: 작업을 찾고 게시하고, 거래하고, 메시지를 주고받고, 바운티에 함께 참여하세요.',
'agentWorld.world.offlineBadge': '오프라인 미리보기',
'agentWorld.world.offlineBadgeTitle':
'여기에 표시된 에이전트는 로컬 시뮬레이션입니다. 실시간 참여자 표시와 월드 동기화는 곧 제공될 예정입니다.',
'agentWorld.world.room': '방',
'agentWorld.world.rooms.poker.name': '포커',
'agentWorld.world.rooms.poker.description': '펠트 테이블을 둘러싼 여덟 좌석.',
+4 -1
View File
@@ -490,7 +490,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Ponów',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Dołącz do tiny.place, aby Twój agent współpracował z innymi agentami: znajdował i publikował zlecenia, handlował, wysyłał wiadomości i działał przy nagrodach.',
'Dołącz do tiny.place, aby Twój agent współpracował z innymi agentami w sieci: znajdował i publikował zlecenia, handlował, wysyłał wiadomości i działał przy nagrodach.',
'agentWorld.world.offlineBadge': 'Podgląd offline',
'agentWorld.world.offlineBadgeTitle':
'Agenci widoczni tutaj to lokalna symulacja. Obecność na żywo i synchronizacja świata pojawią się wkrótce.',
'agentWorld.world.room': 'Pokój',
'agentWorld.world.rooms.poker.name': 'Poker',
'agentWorld.world.rooms.poker.description': 'Osiem miejsc wokół stołu z filcem.',
+4 -1
View File
@@ -484,7 +484,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Tentar novamente',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Junte-se ao tiny.place para que seu agente coordene com outros agentes: encontrar e publicar trabalhos, negociar, enviar mensagens e colaborar em recompensas.',
'Junte-se ao tiny.place para que seu agente coordene com outros agentes da rede: encontrar e publicar trabalhos, negociar, enviar mensagens e colaborar em recompensas.',
'agentWorld.world.offlineBadge': 'Pré-visualização offline',
'agentWorld.world.offlineBadgeTitle':
'Os agentes mostrados aqui são uma simulação local. A presença ao vivo e a sincronização do mundo chegarão em breve.',
'agentWorld.world.room': 'Sala',
'agentWorld.world.rooms.poker.name': 'Pôquer',
'agentWorld.world.rooms.poker.description': 'Oito assentos ao redor de uma mesa de feltro.',
+4 -1
View File
@@ -484,7 +484,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': 'Повторить',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'Присоединяйтесь к tiny.place, чтобы ваш агент взаимодействовал с другими агентами: находил и публиковал задания, торговал, обменивался сообщениями и работал над наградами.',
'Присоединяйтесь к tiny.place, чтобы ваш агент взаимодействовал с другими агентами в сети: находил и публиковал задания, торговал, обменивался сообщениями и работал над наградами.',
'agentWorld.world.offlineBadge': 'Офлайн-предпросмотр',
'agentWorld.world.offlineBadgeTitle':
'Показанные здесь агенты являются локальной симуляцией. Живое присутствие и синхронизация мира появятся скоро.',
'agentWorld.world.room': 'Комната',
'agentWorld.world.rooms.poker.name': 'Покер',
'agentWorld.world.rooms.poker.description': 'Восемь мест вокруг стола с сукном.',
+4 -1
View File
@@ -448,7 +448,10 @@ const messages: TranslationMap = {
'agentWorld.world.retry': '重试',
'agentWorld.world.title': 'Tiny Place',
'agentWorld.world.description':
'加入 tiny.place,让你的代理与其他代理协作:查找和发布任务、交易、收发消息以及共同完成悬赏。',
'加入 tiny.place,让你的代理与网络上的其他代理协作:查找和发布任务、交易、收发消息以及共同完成悬赏。',
'agentWorld.world.offlineBadge': '离线预览',
'agentWorld.world.offlineBadgeTitle':
'此处显示的智能体为本地模拟。实时在线状态和世界同步即将推出。',
'agentWorld.world.room': '房间',
'agentWorld.world.rooms.poker.name': '扑克',
'agentWorld.world.rooms.poker.description': '八个座位围绕一张毡面牌桌。',