بایگانی — خاطره در دره ماموران

۱. مقدمه

وسپر، مسئول بایگانی، پشت میز در طبقه همکف

هفته اول پرسیده شد چه کسی می‌گوید چه چیزی اجرا می‌شود . هفته دوم پرسیده شد چه کسی تصمیم می‌گیرد چه چیزی اجرا شود . هفته سوم پرسیده شد که یک اجرا چه چیزی را به جا می‌گذارد . این هفته مکالمه به پایان می‌رسد و سوال این است که چه چیزی از آن باقی می‌ماند - و معلوم می‌شود که نوع آن، تعیین می‌کند که به کجا برود.

بایگانی جایی است که وقتی مشکلی پیش می‌آید، دره به آنجا می‌رسد. برجی که همه چیز را می‌نویسد، و وسپر ، مسئول بایگانی، پشت میزی در طبقه همکف. فانوس شما شب‌ها خاموش می‌شود. شما می‌روید و می‌پرسید.

او همراه خوبی است. او همچنین، یک ماه بعد، از ملاقات شما خوشحال است.

یک رکورد، خاطره نیست.

این جمله کل آزمایشگاه است. شما از چهار پله بالا خواهید رفت و هر پله یک طبقه از برج است:

زنگوله

کف

آنچه را به یاد می‌آورد

چه چیزی آن را نگه می‌دارد

۱

میز تحریر، و طبقه اول

این بازدید

session.state · SqliteSessionService

۲

طبقه دوم، کشوی شما

این بازدیدکننده

user: پیشوند

۳

طبقه سوم، برج

چه گفته شد؟

بانک حافظه هوش مصنوعی ورتکس

۴

طبقه چهارم، فصل

تمام دره را

بیگ‌کوئری

هر پله‌ای که بالاتر می‌رود به این دلیل است که چیزی از پله‌ی پایینی افتاده است.

بایگانی، باز شده - پنج طبقه، و یک گروه کوچک که تک تک آنها را نگهداری می‌کنند

شما از قبل با پنج روش فراموش کردن توسط مسئول بایگانی آشنا هستید، زیرا هر یک از آنها در یک تماس پشتیبانی برای شما اتفاق افتاده است:

آنچه او می‌گوید

چه اشتباهی رخ داد؟

~~1~~

میشه دوباره اون مرجع رو بهم بدی؟ ~~

~~در عرض یک مکالمه فراموش کردم~~ — هفته سوم این مشکل را حل کردم

~~2~~

~~ من شما را منتقل کردم. لطفا از اول شروع کنید. ~~

~~تاریخ با این روند از بین رفت~~ — هفته سوم این مشکل را هم حل کرد

۳

خوش آمدید! چطور می‌توانم امروز به شما کمک کنم؟

به کسی که از ماه گذشته پرونده‌اش باز است

۴

میشه آسیب رو توصیف کنید؟

آخرین بار به فانوس نگاه کرد

۵

ما قبلاً هرگز این را ندیده‌ایم.

سی و یک نفر در این فصل سوال پرسیده اند

خط سوم مربوط به این هفته است. این خط به سه روش مختلف با شکست مواجه می‌شود - یک بازدید جدید (فصل ۲)، یک جمله بدون خط روی برگه (فصل ۳) و یک بایگانی که فقط درون فرآیند وجود دارد (فصل ۴) - و هر روش یک فصل است. خط چهارم به شما واگذار می‌شود: look() در archive/agent.py جایی است که او علامتی را از روی یک عکس می‌خواند. خط پنجم مربوط به فصل ۵ است و اختیاری است.

شما به هفته‌های اول تا سوم نیازی ندارید. یک نماینده، مدلی با یک دستورالعمل و چند ابزار است. هر چیز دیگری، در این هفته خودش را توضیح می‌دهد.

شما سه خط خواهید نوشت، و هر کدام از آنها یک مفهوم حافظه است :

خط

آنچه هست

tool_context.state[CASE] = case

وضعیت با کد نوشته می‌شود، نه با مکالمه

CASE = "user:case"

یک پیشوند یک عمر است

found = await ctx.search_memory(query)

سیاست فراخوان - ثبت شده به خاطر سپرده نمی‌شود

یک خط دیگر از قبل برای خواندن شما وجود دارد - سیاست نوشتن، await ctx.add_events_to_memory(...) - و فصل ۳ جایی است که قبل از نوشتن خطی که از آن استفاده می‌کند، عملکرد آن را می‌خوانید.

دو پله دیگر اصلاً ویرایش نیستند: شما یک برج (یک موتور عامل که میزبان بانک حافظه است) می‌سازید و همان کد به تنهایی به آن متصل می‌شود، و شما یک انبار (BigQuery) را بارگذاری می‌کنید ، یک گراف روی آن قرار می‌دهید و به همان عامل دو پرس‌وجوی کنترل‌شده می‌دهید - یکی بر اساس معنی، یکی بر اساس اتصال.

هر فصل یک حلقه‌ی یکسان را اجرا می‌کند، و این حلقه‌ای است که شما در ایجنت‌های خودتان استفاده خواهید کرد: کد را بخوانید → خط را بنویسید → آن را در برج تماشا کنید. هیچ چیز ابتدا خراب اجرا نمی‌شود، هیچ چیز دوباره شروع نمی‌شود و هیچ چیز Ctrl+C نمی‌شود: هر ویرایش در پیام بعدی دریافت می‌شود و اتصال برج یک خط در فایلی است که بایگانی در هر درخواست دوباره می‌خواند. برج یک نقاشی است که دوباره ترسیم می‌کند: همان فایل‌هایی را که ویرایش می‌کنید می‌خواند و سه دکمه دارد - 🌙 روز را تمام می‌کند، 🗓 بازدید بعدی را شروع می‌کند، 🔥 شما را فراموش می‌کند. هر کار دیگری که در کد و ترمینال انجام می‌دهید.

دو مکان، و آنها هرگز کار را تغییر نمی‌دهند: تب ۱ ترمینال است، تب ۲ بایگانی. میز کار ( adk web ) دقیقاً یک بار، در فصل ۱، برای یک نگاه به وضعیت خام جلسه ظاهر می‌شود، و سپس کار شما با آن تمام می‌شود. هر 👉 زیر نشان می‌دهد که کدام. 👉💻 تب ۱ است، 👉🖥 تب ۲ است، 👉🔬 میز کار است. 👉📖 باز کردن یک فایل و خواندن است - چیزی برای تغییر نیست - و 👉✏️ نوعی از مرحله است که ویرایش می‌کند. هر زمان که یک مرحله به شما می‌گوید چیزی به Vesper بگویید، پیام دقیق در بلوک درست زیر آن قرار دارد - آن را همانطور که هست کپی کنید. و بخشی که عنوان آن به - فقط خواندن ختم می‌شود، چیزی برای تایپ، چسباندن یا کلیک ندارد: کد موجود در آن از قبل در مخزن وجود دارد و در حال اجرا است، برای خواندن شما چاپ شده است.

جمله‌ی بالای درِ وسپر، جمله‌ی اول است. جمله‌ای که این هفته تمام می‌شود، جمله‌ی پایانی هفته‌ی چهارم کل فصل است:

حقایق و ترجیحات به فروشگاه‌های متفاوتی تعلق دارند.

۲. 🧰 تنظیمات

  • چه چیزی - به یک پروژه اشاره کنید یا یک کلید را جای‌گذاری کنید، نصب کنید و ثابت کنید که زنجیره ابزار سبز است.
  • چرا - هیچ چیز اینجا بوت نمی‌شود. هر سرور درست قبل از مرحله‌ای که به آن نیاز دارد شروع می‌شود، بنابراین وقتی صفحه‌ای باز می‌شود، می‌دانید که کدام دستور آن را دارد.
  • چگونه - یک اسکریپت که پروژه را می‌سازد، یکی که نصب می‌کند و شما را به Vertex AI هدایت می‌کند، و یک پیش‌پرده که کد شما را می‌خواند و به شما می‌گوید کجا هستید.

همه چیز در این آزمایشگاه در Cloud Shell اجرا می‌شود - پایتون، uv و gcloud از قبل در آنجا نصب شده‌اند و Gemini از طریق Vertex AI با استفاده از اعتبارنامه‌های شخصی شما قابل دسترسی است. هیچ کلید API در هیچ کجای این آزمایشگاه وجود ندارد و هیچ چیزی برای نصب روی لپ‌تاپ شما وجود ندارد.

یک چیز هست که این آزمایشگاه نمی‌تواند به شما بدهد: یک پروژه گوگل کلود به همراه صورتحساب. مرحله ۱ حسابی برای شما ایجاد می‌کند که می‌تواند هزینه آن را پرداخت کند؛ مرحله ۴ آن حساب را به یک پروژه تبدیل می‌کند - یا پروژه Agent Valley را که قبلاً در هفته اول، هفته دوم یا هفته سوم انجام داده‌اید، برمی‌دارد. آنها را به همین ترتیب انجام دهید - هر مرحله بعد از مرحله ۴، پروژه‌ای را که مرحله ۴ به آن اختصاص داده است، می‌خواند.

۱ · یک حساب کاربری با قابلیت پرداخت دریافت کنید (تقریباً ۱ دقیقه)

🌐 قبل از هر کاری این کار را انجام دهید. با یک حساب گوگل که قابلیت پرداخت صورتحساب در آن فعال است، وارد شوید. هر حسابی از این نوع کار می‌کند. اگر حساب شما فعال نیست، یک دوره آزمایشی رایگان را شروع کنید؛ این کار به شما یک حساب پرداخت صورتحساب می‌دهد که تمام چیزی است که در مرحله ۴ به آن نیاز دارید.

۲ · باز کردن پوسته ابری (تقریباً ۱ دقیقه)

🌐 روی گزینه‌ی «فعال کردن پوسته‌ی ابری» در بالای کنسول گوگل کلود کلیک کنید - این آیکون به شکل ترمینال در سربرگ قرار دارد.

فعال کردن Cloud Shell از سربرگ کنسول Google Cloud

🌐 روی گزینه Open Editor (آیکون باز کردن پوشه با مداد) کلیک کنید. این کار ویرایشگر Cloud Shell را به همراه یک فایل اکسپلورر در سمت چپ باز می‌کند - هر زمان که یک مرحله علامت‌گذاری شده باشد، در آنجا کار می‌کنید 👉✏️.

ویرایشگر Cloud Shell را باز کنید

🌐 اگر امروز در هر مرحله‌ای از شما خواسته شد که مجوز دهید، برای ادامه روی «مجوز دادن» کلیک کنید.

🌐 یک ترمینال درون ویرایشگر باز کنید - این تب ۱ است و هر 👉💻 مرحله در آنجا اجرا می‌شود، مگر اینکه خلاف آن ذکر شده باشد. تب ۲ بایگانی است و شما آن را در فصل ۱، درست قبل از مرحله‌ای که به آن نیاز دارید، باز می‌کنید. اگر ترمینال از قبل در پایین صفحه نیست، آن را با View → Terminal باز کنید:

یک ترمینال جدید در ویرایشگر Cloud Shell باز کنید

🌐 بررسی کنید که با کدام حساب کاربری وارد سیستم شده‌اید - باید همان حسابی باشد که در مرحله ۱ ذکر شد:

gcloud auth list

۳ · آزمایشگاه را آماده کنید (تقریباً ۱ دقیقه)

💻 مخزن را کپی کنید — این مخزن آرشیو است، نه مخزن هفته اول، هفته دوم یا هفته سوم:

cd ~
git clone https://github.com/cuppibla/agent-valley-archive
cd agent-valley-archive

۴ · پروژه خود را ایجاد کنید — یا از پروژه هفته قبل دوباره استفاده کنید (حدود ۲ دقیقه)

💻 یک اسکریپت حساب صورتحساب شما را پیدا می‌کند، یک پروژه روی آن ایجاد می‌کند و Cloud Shell را به آن پروژه ارجاع می‌دهد. اگر هفته اول، هفته دوم یا هفته سوم را انجام داده باشید، ابتدا پروژه Agent Valley را پیدا می‌کند و پیشنهاد استفاده مجدد از آن را می‌دهد - بله بگویید و هیچ پروژه قابل پرداخت دومی برای فکر کردن وجود ندارد:

cd ~/agent-valley-archive
./setup_project.sh

👀 انتظار یک ✓ برای حساب پرداختی که انتخاب کرده، یک ✓ برای پروژه‌ای که ایجاد یا دوباره استفاده کرده، و یک پایان مانند این را داشته باشید:

cd ~/agent-valley-archive
Project ready: agent-valley-4271
  Next:  ./setup_codelab.sh

agent-valley-4271 شناسه پروژه شماست - شماره شما ارقام متفاوتی خواهد داشت. چهار اتفاق در آنجا رخ می‌دهد:

صورتحساب

جدیدترین [YYYY-MM-DD] GDP Credit: اگر حساب دارید، و در غیر این صورت هر حساب صورتحساب باز دیگری

پروژه

به دنبال پروژه Agent Valley که از قبل دارید می‌گردد - هفته اول، هفته دوم یا هفته سوم - و پیشنهاد استفاده مجدد از آن را می‌دهد؛ در غیر این صورت agent-valley-XXXX را ایجاد می‌کند و آن حساب صورتحساب را به آن پیوند می‌دهد.

انتخاب می‌کند

شناسه را در ~/project_id.txt می‌نویسد و gcloud config set project اجرا می‌کند، بنابراین بعد از این از شما نمی‌خواهد آن را تایپ کنید.

منتظر می ماند

نظرسنجی‌ها تا زمانی که پروژه واقعاً اجازه ارائه خدمات داشته باشد، تا دو دقیقه

🌐 همان پروژه اکنون در کنسول است - شناسه آن در کارت اطلاعات پروژه در داشبورد قرار دارد:

شناسه پروژه شما در داشبورد کنسول

۵ · نصب و نشانه‌گیری به سمت Vertex AI (حدود ۳ دقیقه)

💻 uv را نصب کنید، سه متغیری که ADK را به Vertex AI ارسال می‌کنند را اکسپورت کنید و تنظیمات را اجرا کنید:

cd ~/agent-valley-archive
curl -LsSf https://astral.sh/uv/install.sh | sh
source ~/.local/bin/env
export GOOGLE_GENAI_USE_VERTEXAI=TRUE
export GOOGLE_CLOUD_PROJECT=$(gcloud config get project)
export GOOGLE_CLOUD_LOCATION=global
./setup_codelab.sh

$(gcloud config get project) ‎ چیزی برای برگرداندن دارد زیرا مرحله ۴ آن را تنظیم کرده است. به همین دلیل است که مرحله ۴ اول می‌آید.

setup_codelab.sh فایل خود-توان (idempotent) است - اجرای مجدد آن به هر تعداد بار که بخواهید، بی‌خطر است. این فایل به ترتیب پنج کار انجام می‌دهد:

API

پروژه مرحله ۴ انتخاب شده را می‌خواند و aiplatform.googleapis.com (هوش مصنوعی Vertex و موتور عامل که میزبان بانک حافظه است) و bigquery.googleapis.com (انبار داده فصل ۵) را روی آن فعال می‌کند.

محیط UV

uv venv فایل .venv را ایجاد می‌کند، یا از فایلی که از قبل وجود دارد دوباره استفاده می‌کند، سپس uv sync دقیقاً همان چیزی را که uv.lock پین می‌کند نصب می‌کند - google-adk ، google-genai و هر چیزی که در زیر آنها قرار دارد.

سایت

npm ci در site/ اجرا می‌کند - خودِ آرشیو یک برنامه Next.js است - بنابراین bash valley.sh در فصل ۱ چیزی برای دانلود ندارد؛ وقتی بسته‌ها از قبل وجود دارند، از آن صرف نظر می‌شود

.env

متغیرهایی را که آزمایشگاه می‌خواند می‌نویسد - GOOGLE_GENAI_USE_VERTEXAI ، GOOGLE_CLOUD_PROJECT ، GOOGLE_CLOUD_LOCATION ، MEMORY_BANK_LOCATION و BQ_DATASET ، و هیچ کلید API در هیچ کجا وجود ندارد

بررسی مدل

یک تماس واقعی با Gemini برقرار می‌کند، بنابراین یک پروژه بد همینجا در زمان راه‌اندازی شکست می‌خورد، نه بعداً در یک فصل

۶ · ثابت کنید سبز است (~۱ دقیقه)

💻 پیش‌پرواز را اجرا کنید - هر تیک یک کاوش واقعی است، نه یک حدس:

cd ~/agent-valley-archive
uv run python scripts/preflight.py

👀 پیش‌پرواز از مدل یک سوال می‌پرسد تا صحت اعتبارنامه‌های شما را تأیید کند، بررسی می‌کند که بسته‌های آرشیو در جای خود قرار دارند، سپس کد شما را می‌خواند و نوار پیشرفت این هفته را چاپ می‌کند:

cd ~/agent-valley-archive
  desk      ▢ the slip      · write_down writes nothing         chapter 1 · EDIT ONE
  floor 1   ✓ the file      · SqliteSessionService              chapter 1, read only
  floor 2   ▢ the drawer    · CASE = "case"                     chapter 2 · EDIT TWO
  floor 3   ✓ what was said · closing time files the day        chapter 3, read only
  floor 3   ▢ the asking    · recall never looks                chapter 3 · EDIT THREE
  floor 3   ▢ the tower     · InMemoryMemoryService             chapter 4 · built, not edited
  floor 3   ✓ the rules     · USER_PREFERENCES, LANTERN_CONTEXT chapter 4, read only

سه مورد از قبل تیک خورده‌اند. اینها مواردی هستند که شما فقط می‌خوانید. سه مورد خطوطی هستند که می‌نویسید؛ یکی برجی است که می‌سازید. دوباره پیش‌پرواز را در هر نقطه‌ای از آزمایشگاه اجرا کنید و به شما خواهد گفت که کجا هستید - فایل‌های شما را می‌خواند، نه یک چک‌لیست.

۳. جلسه، وضعیت، سرویس SqliteSession

  • چی؟ - یک خط بنویس؛ یک بار میز کار را روشن کن و پر شدن برگه را تماشا کن؛ بعد همین کار را در برج انجام بده؛ بعد بخوان که چرا تا شب دوام آورده است.
  • چرا - اولین چیزی که در مورد حافظه باید دید این است که هیچ چیز به خودی خود به خاطر سپرده نمی‌شود. کسی آن را می‌نویسد.
  • چگونه - یک خط در agent.py ، سپس هیچ چیز دیگری را تغییر ندهید.

ویرایش یک

👉📖 archive/agent.py را باز کنید و write_down پیدا کنید. 👉 را بخوانید:

    case = dict(tool_context.state.get(CASE, {}))
    if case.get(field):
        return {"already_written": {field: case[field]}}
    case[field] = value
    # 👉 EDIT ONE — chapter 1. She heard you. Nothing was written. Delete the
    #    "# " in front of the next line, then save — the slip is session state,
    #    and state is written by code, never by the conversation:
    # tool_context.state[CASE] = case
    return {"written": {field: value}}

این تابع، فایل لغزش را در یک dict محلی ایجاد می‌کند و سپس آن را برمی‌گرداند - و یک dict محلی با فراخوانی از بین می‌رود. این را همانطور که هست اجرا کنید و Vesper کاملاً صدای شما را می‌شنود: چهار فراخوانی write_down اجرا می‌شوند، هر کدام به "written" پاسخ می‌دهند و حالت جلسه خالی می‌ماند، زیرا هیچ چیز به آن نرسیده است. او صدای شما را شنید. هیچ چیزی نوشته نشده است.

👉✏️ هنوز در archive/agent.py ، داخل write_down : علامت # را از قبل از یک خط حذف کنید - علامت # و فاصله‌ی بعد از آن. قبل از:

    case[field] = value
    # 👉 EDIT ONE — chapter 1. She heard you. Nothing was written. Delete the
    #    "# " in front of the next line, then save — the slip is session state,
    #    and state is written by code, never by the conversation:
    # tool_context.state[CASE] = case
    return {"written": {field: value}}

بعد از:

    case[field] = value
    # 👉 EDIT ONE — chapter 1. She heard you. Nothing was written. Delete the
    #    "# " in front of the next line, then save — the slip is session state,
    #    and state is written by code, never by the conversation:
    tool_context.state[CASE] = case
    return {"written": {field: value}}

ذخیره کنید.

لغزش پر می‌شود

👉💻 میز کار را شروع کنید. این تنها باری است که در آزمایشگاه آن را باز می‌کنید - این تنها پنجره‌ای است که وضعیت جلسه را به صورت JSON خام نشان می‌دهد:

cd ~/agent-valley-archive
uv run adk web . --allow_origins="*"

👉🔬 میز کار را با پیش‌نمایش وب → پیش‌نمایش روی پورت ۸۰۰۰ باز کنید (دکمه پیش‌نمایش وب، آیکون چشم روی پنجره در سمت راست بالای Cloud Shell است). بایگانی را انتخاب کنید و قبل از تایپ هر چیزی، روی وضعیت در پنل سمت چپ کلیک کنید.

👀 خالی. هنوز کسی چیزی نگفته است.

👉🔬 حالا اینو بگو، همه‌ش تو یه پیام:

Hi! My lantern keeps going out at night. My case number is k7f2, and there's a little q7 stamped on the base.

👀 چهار تراشه write_down فعال می‌شوند - روی یکی از آنها کلیک کنید و JSON را بخوانید: "written": {"no": "k7f2"} . حالا دوباره روی State کلیک کنید.

چهار چیز، چهار فیلد، یک پیام

case   {"no": "k7f2", "item": "lantern", "mark": "q7",
        "symptom": "keeps going out at night"}

👀 خالی، سپس پر، درون یک پیام. این چیزی است که او با آن به خاطر می‌آورد، و به خودی خود ظاهر نشده است. شما خطی را که آن را می‌نویسد نوشته‌اید. کل مکانیسم همین است: یک تابع ساده که در tool_context.state می‌نویسد، و یک دستورالعمل که به او می‌گوید هر زمان که بازدیدکننده چیزی می‌گوید که با خط روی برگه مطابقت دارد، از آن استفاده کند.

👉🔬 از او بپرسید:

What's my case number?

👀 k7f2. او آن را از روی برگه می‌خواند. بدون ویرایش اول، او از شما درخواست می‌کرد - در یک مکالمه، آن را در لیست پشتیبانی خط بزنید.

دو نگهبان، برگه را به شکل یک فرم کاغذی نشان می‌دهند: چهار خط کوتاه که هر کدام یک بار نوشته شده‌اند. آنها بیش از آنچه به نظر می‌رسند، اهمیت دارند. بدون آنها، مدل متوجه می‌شود که می‌تواند کل مکالمه را در قالب symptom بریزد و فصل ۳ - فصلی که تمام نکته آن برگه است و هیچ خطی برای آن ندارد - بی‌سروصدا دیگر واقعی نیست. شکل یک فروشگاه توسط کدی که آن را می‌نویسد، اعمال می‌شود، یا اعمال نمی‌شود.

حالا برو به برج

👉💻 کار شما با میز کار تمام شد — آن را در حال اجرا بگذارید یا تب آن را ببندید؛ بقیه آزمایشگاه در بایگانی قرار دارد. یک تب ترمینال دوم (تب ۲ — بایگانی) باز کنید:

cd ~/agent-valley-archive
bash valley.sh

👉🖥 بایگانی را با پیش‌نمایش وب باز کنید → تغییر پورت ، ۳۴۴۰ را تایپ کنید و روی تغییر و پیش‌نمایش کلیک کنید. ۳۴۴۰ در فهرست سریع پیش‌نمایش وب نیست - ۸۰۸۰ پیش‌فرض آن است - بنابراین باید این بار آن را دستی تنظیم کنید. ▶ شروع ، سپس بایگانی را فشار دهید. شما پشت در هستید. هر کسی را که آمده تا بپرسد - یک نام و یک گونه - انتخاب کنید و وارد شوید.

برج، شکافته شده

👀 یک ساختمان. وسپر پشت میز طبقه همکف است و شما آنجا صحبت می‌کنید. بالای سر او: طبقه اول، طبقه دوم، طبقه سوم و یک اتاق زیرشیروانی تاریک. چهار پله، چهار طبقه هستند. آشنای شما روی طبقه همکف نشسته است، چون تا اینجا بالا رفته‌اید. ستون کنار نقاشی، محتویات هر طبقه را در حال حاضر مشخص می‌کند - همان چهار مغازه‌ای که روی میز مقدمه بود، به همان ترتیب. هیچ چیز در سمت راست دکمه نیست.

👉🖥 همان چهار مورد را در کادر زیر مکالمه - دقیقاً همین پیام - جایگذاری کنید و Enter را فشار دهید:

Hi! My lantern keeps going out at night. My case number is k7f2, and there's a little q7 stamped on the base.

چهار خط روی میز، و همان چهار خط کنارش

👀 به میز نگاه کنید. برگه خودش را می‌نویسد، یک خط در هر زمان - روی نقاشی، و به طور کامل زیر برگه کنارش. همان ابزار، همان حالت: برج فقط همان چیزی را که write_down نوشته است، نقاشی می‌کند و این کار را تا آخر آزمایش ادامه خواهد داد.

او یک شبه به یاد می‌آورد — فقط خواندنی

آنچه نوشتید و آنچه هست

خط

آنچه هست

کجا زندگی می‌کند

tool_context.state[CASE] = case

وضعیت جلسه - نوشته شده توسط کد، خوانده شده توسط دستورالعمل

میز · session.state

SqliteSessionService(DB) - شما فقط آن را می‌خوانید

محل ذخیره‌سازی جلسه - بازدیدی در یک فایل، نه در یک فرآیند

طبقه اول · archive.db

این پله به یک بازدید می‌رسد و تا زمان بسته شدن در باقی می‌ماند. اما در بازدید بعدی دوام نمی‌آورد.

۴. وضعیت کاربر

  • چی ؟ - یه ماه بعد برگرد؛ اون برگه دیگه نیست. پنج تا کاراکتر داره، و دنبالت میاد.
  • چرا - یک جلسه، یک بازدید است. بازدید بعدی یک جلسه جدید است و هرگز جلسه قبلی را ندیده است.
  • چطور — پنج کاراکتر در state.py .

یک ماه بعد. از آشنایی با شما خوشبختم.

اینجا را فشار دهید

👉🖥 یک ماه بعد دکمه 🗓 را فشار دهید. این یک جلسه جدید برای همان بازدیدکننده است - روشی که برای شروع یک مکالمه جدید در هفته سوم استفاده شد.

به آرشیو خوش آمدید. امروز چطور می‌توانم به شما کمک کنم؟

👉🖥 به برج نگاه کن. طبقه اول یک کتاب دیگر روی آن دارد - این بازدید. کشوی طبقه دوم خالی است. برگه روی میز هم خالی است.

کتاب دیگری در طبقه اول؛ کشو خالی؛ برگه سفید

👀 چرا: این رسید در session state نوشته شده است، و یک جلسه همان این بازدید است. بازدیدهای تغییر، جلسات تغییر. هیچ چیز منتقل نمی‌شود زیرا چیزی به آن گفته نشده است. آخرین بازدید هنوز در فایل است - این اولین دفتر طبقه اول است - اما رسید تحت کلیدی نوشته شده است که برای یک بازدید معتبر است.

ویرایش دو

👉✏️ archive/state.py را باز کنید. علامت 👉 را پیدا کنید. قبل از:

# 👉 EDIT TWO — chapter 2. Five characters. A key with no prefix lives for one
#    visit; `user:` follows the visitor into every visit after this.
CASE = "case"

بعد از:

CASE = "user:case"

ذخیره کنید. نیازی به راه‌اندازی مجدد نیست — بایگانی کد شما را در هر پیام دوباره وارد می‌کند.

👉🖥 دوباره همان چهار مورد را پیست کنید — دقیقاً همین پیام:

Hi! My lantern keeps going out at night. My case number is k7f2, and there's a little q7 stamped on the base.

👉🖥 سپس یک ماه بعد دوباره 🗓 را فشار دهید - همان دکمه قبلی.

کشویی که اسمی روی آن نوشته شده است

👀 اسم شما روی کشوی طبقه دوم است و چهار فیلد داخل آن قرار دارند. برگه روی میز همان چهار خط است - وقتی وارد شدید از کشو بیرون کشیده شد. فامیل شما یک طبقه بالاتر رفته است. همان کلید، هر بار مراجعه، یک مقدار: این پیشوند است که کار خودش را می‌کند.

پیشوند یک عمر است.

پیشوند

چقدر عمر می‌کند

کدام طبقه

(هیچکدام)

این بازدید

میز تحریر

user:

هر بازدیدی که این بازدیدکننده انجام می‌دهد

طبقه دوم، کشوی شما

app:

همه، برای همیشه

بخشی از طبقه چهارم

temp:

فقط این نوبت — هرگز حتی از پله‌ها بالا نمی‌رود

—

یک میز، چهار کشو. این فصل به یکی از آنها می‌پردازد.

الان تو رو میشناسه

👉🖥 این ملاقات کاملاً جدید است و شما هنوز چیزی در آن نگفته‌اید. از او بپرسید:

What did I bring in last time?

👀 من یادداشت کرده‌ام که شما یک فانوس، مورد k7f2، آورده‌اید. با q7 مشخص شده است و شما اشاره کردید که شب‌ها مدام خاموش می‌شود. هر کلمه از آن از کشو بیرون آمده است، نه مکالمه - این بازدید فقط یک پیام قدیمی است و پیام هیچ چیزی را نامگذاری نکرده است. این وضعیت سطح کاربر است که کار خود را انجام می‌دهد: یک کلید user: قبل از اینکه بازدیدکننده کلمه‌ای بگوید، در هر بازدیدی که انجام می‌دهد، آنجاست.

کشو چهار خطی که روی برگه نوشته شده را نگه می‌دارد، و نه چیز دیگری. آنچه می‌گویید و روی برگه خط ندارد - چه تلاشی کرده‌اید، چرا برایتان مهم است، آیا برگه جدیدی می‌خواهید یا نه - جایی برای رفتن ندارد، و یک ماه بعد از بین می‌رود. کشو از ماه عبور می‌کند. یک جمله عبور نمی‌کند. فصل ۳ در مورد همین است.

آنچه نوشتید و آنچه هست

خط

آنچه هست

کجا زندگی می‌کند

CASE = "user:case"

یک پیشوند یک طول عمر است - همان کلید، که به جای بازدید، به بازدیدکننده محدود شده است.

طبقه دوم · user: ایالت

این پله به یکی از بازدیدکنندگان می‌رسد. کشو به دنبال شما می‌آید؛ اما مکالمه همچنان ادامه دارد.

۵. سرویس حافظه داخلی

  • چه چیزی - سطری را که روز را ثبت می‌کند بخوانید، سطری را که می‌پرسد بنویسید؛ سپس یک زمان پایانی و یک سوال.
  • چرا ؟ - این برگه یک فرم است. یک فرم شامل خطوطی است که شما به آن داده‌اید. مردم به صورت خطی صحبت نمی‌کنند - و بایگانی کردن آنچه گفته شده تنها نیمی از به خاطر سپردن آن است.
  • چگونه — خواندن file ؛ یک خط در recall ؛ سپس یکی 🌙، یکی 🗓، یک سوال.

وقت بسته شدن است. او از کنار قفسه‌ها برمی‌گردد.

کاری که او در راه خانه انجام می‌دهد - فقط برای خواندن

ثبت شده به خاطر سپرده نمی‌شود

آن خط روز را روی قفسه می‌گذارد. چیزی از آن کم نمی‌کند. یک قفسه پر از کارت دقیقاً به اندازه یک قفسه خالی مفید است اگر کسی نگاه نکند - و همانطور که کد ارسال می‌شود، هیچ کس نگاه نمی‌کند: recall ، گره‌ای که قبل از Vesper در هر سوال اجرا می‌شود، از برج چیزی نمی‌پرسد. ذخیره‌سازی که نمی‌توانید در لحظه مناسب به خاطر بیاورید، حافظه نیست؛ بلکه یک گزارش است. نگاه کردن نیمه دیگر است و نوشتن آن بر عهده شماست.

ویرایش سوم - پرسیدن

👉📖 در archive/agent.py ، تابع find recall پیدا کنید - تابعی که درست بالای file قرار دارد. این یک گره تابع ساده است که قبل از Vesper روی هر سوال اجرا می‌شود - نه اینکه مدل تصمیم بگیرد چه زمانی جستجو کند، به این معنی که زمان‌هایی که چیزی پیدا نمی‌کند نیز قابل مشاهده هستند. برای 👉 بخوانید:

    if query.strip():
        try:
            # 👉 EDIT THREE — chapter 3. Filed is not remembered: the cards are on
            #    the shelf and nobody is looking at them. Delete the "# " in front
            #    of the next line, then save:
            # found = await ctx.search_memory(query)
            pass
        except ValueError:
            # no memory service wired on this runner — degrade, do not crash
            pass

👉✏️ هنوز در archive/agent.py هستید، داخل recall : علامت # را از قبل از یک خط حذف کنید - علامت # و فاصله‌ی بعد از آن. قبل از:

        try:
            # 👉 EDIT THREE — chapter 3. Filed is not remembered: the cards are on
            #    the shelf and nobody is looking at them. Delete the "# " in front
            #    of the next line, then save:
            # found = await ctx.search_memory(query)
            pass
        except ValueError:

بعد از:

        try:
            # 👉 EDIT THREE — chapter 3. Filed is not remembered: the cards are on
            #    the shelf and nobody is looking at them. Delete the "# " in front
            #    of the next line, then save:
            found = await ctx.search_memory(query)
            pass
        except ValueError:

pass را همان جایی که هست بگذارید - الان هیچ کاری نمی‌کند، زیرا found از قبل در بالا None است.

ذخیره کنید. بدون نیاز به راه‌اندازی مجدد.

دو خط، دو سیاست - و شما یکی از آنها را نوشتید. سیاست نوشتن تصمیم می‌گیرد که چه چیزی و چه زمانی نگه داشته شود (زمان بسته شدن، کل روز). سیاست فراخوانی تصمیم می‌گیرد که چه زمانی و با چه چیزی نگاه کنید (هر سوال، خود سوال). ADK افعال را به شما می‌دهد - add_events_to_memory ، search_memory - و از سیاست خارج می‌ماند. این تقسیم‌بندی درست است، و به همین دلیل است که نوشتن درخواست بر عهده شما بود.

زمان بسته شدن

👉🖥 سه چیزی که در برگه قید نشده را بگویید. یک یا سه پیام:

I read by it at night, so night is exactly when I need it.
I've tried hanging it higher. No change.
I don't want a new one — it was a gift.

اینجا را فشار دهید

👉🖥 پس دکمه 🌙 رو فشار بدید، همین برای امروز کافیه .

کلمات اوج می‌گیرند

👀 به میز نگاه کن. سه چیزی که گفتی از روی تخته بلند میشن و بالا میان و روی طبقه سوم فرود میان. زیر کارت‌های کنار برج، لیستشون نوشته شده - یک کارت برای هر پیامی که فرستادی، یه quote علامت‌گذاری شده، یه جمله کلمه به کلمه، و در این فرآیند بالای اونها. چراغ روی میز تا وقتی که فرود بیان، روشن میمونه.

یک ماه بعد، او می‌داند

👉🖥 یک ماه بعد دکمه 🗓 را فشار دهید و بپرسید:

What have I already tried?

👀 سعی کردی آن را بالاتر آویزان کنی - کمکی نکرد. او می‌داند. برگه هیچ خطی برای آن ندارد و هرگز هم نداشته، بنابراین کشو نمی‌توانسته به این سوال پاسخ دهد؛ recall کارت را پیدا کرد، آن را در ctx.state["recalled"] نوشت، و دستورالعمل او این را می‌خواند. یک خط تفاوت است.

و همه چیز در ذهن اوست

زیر کارت‌های کنار برج را نگاه کنید: در این فرآیند . در حال حاضر حافظه InMemoryMemoryService است - یک دیکشنری درون bash valley.sh ، و دقیقاً تا زمانی که آن فرآیند وجود دارد، زنده می‌ماند. امشب برج را ببندید و فردا آن را باز کنید: کتاب‌ها در طبقه اول هستند، کشو در طبقه دوم است و طبقه سوم تاریک است. پرونده در فایل باقی مانده است. آنچه شما گفتید در فرآیند زنده مانده است و فرآیند از بین رفته است.

ماندگاری، حافظه نیست.

دو چیز در مورد طبقه سوم به صورت فعلی اشتباه است: در حال انجام است، و به قول خودتان - یک نقل قول است، نه یک واقعیت. فصل بعدی هر دو را بدون هیچ کدی اصلاح می‌کند.

آنچه خواندید، آنچه نوشتید و آنچه هست

خط

آنچه هست

کجا زندگی می‌کند

await ctx.add_events_to_memory(events, custom_metadata=FILING) — شما فقط آن را می‌خوانید

سیاست نوشتن - یک روز کامل، در زمان بسته شدن

طبقه سوم · سرویس حافظه

found = await ctx.search_memory(query) — ویرایش سوم

سیاست فراخوان - هر سوال، قبل از اینکه او پاسخ دهد

recall() → ctx.state["recalled"]

این پله به آنچه گفته شد می‌رسد - و آن را در یک دیکشنری درون یک فرآیند، به قول خودتان، نگه می‌دارد. هر دو نیمه‌ی آن اشتباه هستند و فصل بعدی هر دو را بدون هیچ کدی اصلاح می‌کند.

6. بانک حافظه

  • چه چیزی - یک اسکریپت یک بانک حافظه می‌سازد؛ دو خطی که از قبل دارید شروع به نوشتن حقایق در آن و خواندن مجدد آنها می‌کنند.
  • چرا - یک سرویس حافظه، یک سرویس است. محل قرارگیری آرشیو، یک تصمیم استقرار است، نه یک عامل - شما آن را بدون دست زدن به عامل تغییر می‌دهید.
  • چطور — make_tower.py اجرا کنید، سه جمله بگویید، 🌙 را فشار دهید، 🗓 را فشار دهید، یک سوال بپرسید. سپس نگاهی فقط خواندنی به نوشتن و خواندن بیندازید.

برج را بسازید

طبقه سوم فصل سوم یک دیکشنری پایتون درون bash valley.sh بود. Memory Bank نسخه مدیریت‌شده آن طبقه است: این طبقه در یک نمونه Agent Engine در پروژه شما قرار دارد، حقایق را به جای جملات شما، از روز استخراج می‌کند و آنها را به جای کلمات مشترک، بر اساس معنی پیدا می‌کند. برای استفاده از Vesper نیازی به استقرار آن در یک Agent Engine ندارید - نمونه‌ای که هیچ چیزی روی آن مستقر نشده باشد، همچنان میزبان یک Memory Bank است و این چیزی است که اسکریپت می‌سازد: برجی که هیچ‌کس در آن زندگی نمی‌کند و تنها وظیفه‌اش نگهداری کارت‌ها است.

👉💻 در تب ۱ (حدود یک دقیقه):

cd ~/agent-valley-archive
uv run python scripts/make_tower.py

👀 انتظار:

  building the tower in YOUR_PROJECT / us-central1 — about a minute...
  built, with the two topics in archive/topics.py configured on it.

  resource: projects/.../locations/us-central1/reasoningEngines/6792...
  AGENT_ENGINE written to .env

  Next — nothing to restart. The Archive reads .env on every message:
    look at floor three — under the cards it now says Memory Bank.
    (adk web, if you use it:  --memory_service_uri "agentengine://projects/.../reasoningEngines/6792...")

یک فراخوانی SDK، agent_engines.create() و یک خط نوشته شده در .env . بایگانی آن فایل را در هر درخواست دوباره می‌خواند، بنابراین چیزی برای راه‌اندازی مجدد وجود ندارد.

عملکردش را تماشا کنید

👉🖥 به طبقه سوم نگاه کنید - برج ظرف چند ثانیه خود به خود دوباره ترسیم می‌شود. حالا زیر کارت‌ها نوشته شده است بانک حافظه ، و قفسه خالی است: سه نقل قول از فصل ۳ در حال انجام بودند، و این روند نیست. سه جمله را دوباره بگویید، یک یا سه پیام:

I read by it at night, so night is exactly when I need it.
I've tried hanging it higher. No change.
I don't want a new one — it was a gift.

👉🖥 دکمه 🌙 رو فشار بدید، همین برای امروز کافیه . الان حدود هشت ثانیه طول می‌کشه تا بسته بشه - برج داره آب میشه. بعد به طبقه سوم نگاه کنید.

چپ: آنچه در ذهنش نگه داشته بود. راست: آنچه برج نگه داشته است.

👀 کارت‌ها جملات شما نیستند. چیزهایی می‌گویند مثل اینکه من از فانوسم برای مطالعه در شب استفاده می‌کنم، که همان زمانی است که مدام خاموش می‌شود و من قبلاً سعی کرده‌ام فانوسم را بالاتر آویزان کنم ، و هر fact با تاریخ مشخص کرده‌ام. خلاصه. آنچه وارد شد یک مکالمه بود. آنچه بیرون آمد واقعیت است.

👉🖥 یک ماه بعد - یک ملاقات جدید، بنابراین دیگر هیچ چیز از آنچه که گفتید در مکالمه نیست - دکمه 🗓 را فشار دهید و این را از او بپرسید، سوالی که حتی یک کلمه هم با کارت‌ها مشترک نیست:

Should I just get a different lamp?

👀 از آنجایی که هدیه بود، اشاره کردید که نمی‌خواهید فانوس خود را تعویض کنید - و قبلاً سعی کرده‌اید آن را بالاتر آویزان کنید. هر کلمه از این حرف از کارت آمده است، و فقط کارت می‌توانسته به او بگوید: این بازدید یک پیام قدیمی است. جستجوی شباهت. برج با معنی مطابقت دارد، نه با کلمات - سوال گفت: لامپ و متفاوت ، کارت می‌گوید فانوس و جایگزین . و اکنون خارج از فرآیند است: امشب برج را ببندید و کارت‌ها فردا آنجا هستند، زیرا آنها در موتور عامل زندگی می‌کنند، نه در bash valley.sh .

نوشتن و خواندن - فقط خواندنی

The right to be forgotten

Press it here

👉🖥 Press 🔥 forget me , under the message box.

👀 Three floors go dark and one does not:

what it holds

after 🔥

floor three, the cards

what she remembered — the facts Memory Bank distilled from your visits

gone — memories.delete , one card at a time

floor two, the drawer

user:case , the slip that follows this visitor from visit to visit

cleared

the desk, the slip

this visit's copy of the case

cleared

floor one, the books

the visits themselves — one session in archive.db per visit, every message of it

untouched — the count does not move

The books are the record : that a visit happened, on which day, and what was said in it. The cards and the drawer are the memory : what she made of those visits and carries into the next one. A request to be forgotten is about the memory — the facts derived about a person, and the state that follows them around. Whether the record of a visit also goes is a separate decision, made for separate reasons — an audit trail, a support history — and this lab keeps it on purpose, so you can watch the two come apart in front of you: the record and the memory are not the same thing, and forgetting one does not touch the other. That is the sentence over Vesper's door, seen from the other side.

The ADK memory abstraction has add_events_to_memory and search_memory . It has no delete. Forgetting is a governance action, not an agent action, so it lives in the application — archive/tower.py , memories.delete on the raw client — where the model cannot reach it by accident. Whether an agent can forget is something you design, exactly like whether it can remember.

What you built and read, and what it is

the thing

آنچه هست

where it lives

make_tower.py → AGENT_ENGINE in .env — built, not edited, nothing restarted

the memory service, as a service: a deployment decision, outside the agent

Vertex AI Memory Bank on an Agent Engine

tower_service(engine) — you only read it

the same Runner(memory_service=...) slot, a different home

archive/service.py

"allowed_topics": TOPICS in FILING — read only

what may be kept, decided at write time, not in the prompt

archive/topics.py + the tower's own config

🔥 → memories.delete — pressed, not written

forgetting is an application action; ADK's memory API has no delete

archive/tower.py

This rung reaches what was said. A day goes in; a fact comes out — distilled to what the topics allow — and the book on floor one never moved.

7. GraphRAG

Optional.

  • What — one script loads the valley's past into BigQuery, generates embeddings inside the warehouse and lays a graph over the tables without moving a row; then two questions to Vesper, who has two governed queries for it: one for what sounds alike, one for what is connected.
  • Why — sounds alike is not the same as is connected. A vector finds where to look; a graph finds what is attached; and the answer has to carry the path it walked.
  • How — bash scripts/season.sh , once. Everything else in this chapter is read only.

Vesper's tower is scoped to you — (app_name, user_id) — by construction. Other people's visits are not her memory, and searching it harder would not change that. They are the valley's, and the valley keeps them in a warehouse. Thirty-one visitors this season came about a q7 lantern going out at night, and one of them found the cause. This chapter loads that warehouse and hands her two fixed queries into it.

Graph RAG

RAG is retrieve, then generate : find the right records, hand them to the model, let it answer from them. Plain RAG retrieves by similarity — embed the question, pull the nearest records. That finds things that sound like the question, and it is where chapter 4's tower stopped. Graph RAG adds a second retrieval step that follows edges — keys, which a warehouse already has — from whatever similarity found, to whatever is attached to it. The two steps answer different questions, and this chapter makes you ask both:

گام

the question it answers

چگونه

در این فصل

شباهت

where should I look?

embed the question, VECTOR_SEARCH

season_search — when the visitor brings no mark

traversal

what is connected to that?

MATCH a path through a property graph

season_known_issue — one fixed MATCH

نسل

what do I tell the visitor?

the model, with the path as its citation

her answer

اجراش کن

The season ships as plain CSVs in season/ — visitors, items, asks, fixes, seasons — the way a support system would export them. One script takes them all the way into the warehouse, and it is the only thing you run in this chapter.

👉💻 In tab 1 (about three minutes; it prints each step as it goes, and stops on the first one that fails):

cd ~/agent-valley-archive
bash scripts/season.sh

👀 When it ends, floor four on the tower lights up, and the legend beside it says open .

What it built — read only

How she reads it — read only

Ask her

👉🖥 Floor four is open on the tower. Ask her the way any visitor would:

Has anyone else had a q7 lantern go out at night?

She answers, and the last line is the path

👀 She answers — thirty-one others... most said the flame sat low and went out after an hour... Marlow the otter found the wicks were dipped too shallow — and the last line of her answer is the path. This is her output — read it, do not send it back to her:

mark q7 → 31 visits this season → 10 different things said → the wicks in the q7 batch were dipped too shallow — they drown in the oil and starve in an hour

The path is the citation. Every hop in it is an edge in the graph, and an edge is a key, so anyone can walk it again and get the same thirty-one. A similarity score cannot be walked again by anyone. If she says, just before the path, that the rows came back through three joins because the project has no graph reservation, that is your project's edition refusing GRAPH_TABLE — the rows and the path are the same.

👉🖥 Now leave the mark out — send this:

Has anyone else had a lantern that keeps dying once the sun is down?

👀 She gets to the same thirty-one, and the same path. What changed is the road: season_search ran first, found five visitors who sound alike — each carrying q7 — took that mark, and only then walked. Similar is not connected ; similar is where to look.

What was loaded, and what it is

the thing

آنچه هست

where it lives

bq load with explicit schemas — run, not written

a warehouse: columns with names and types

archive.visitors / items / asks / fixes / seasons

ML.GENERATE_EMBEDDING + VECTOR_SEARCH

the similarity step — where to look

archive.ask_embeddings , the embedder model

CREATE PROPERTY GRAPH — no row moved

the traversal step — what is connected; tables become nodes and arrows

archive.season_graph

GQL in archive/season/__init__.py — one parameter

the only walk she may take; the path is the citation

season_known_issue(mark)

A few dozen visitors is not a reason to buy a warehouse. What this chapter teaches is the shape of the query — not scale. Grow this graph a thousand times and the MATCH does not change by a character.

8. Close

Three lines, one tower, one warehouse — four floors. Nothing restarted.

the rung

کاری که شما انجام دادید

what it bought

یکی

tool_context.state[CASE] = case — an edit

a slip that exists at all: state is written by code

TWO

CASE = "user:case" — an edit

a drawer that follows the visitor

the filing

await ctx.add_events_to_memory(..., custom_metadata=FILING) — read, not written

what was said, kept

سه

found = await ctx.search_memory(query) — an edit

what was kept, asked for — filed is not remembered

برج

make_tower.py , then AGENT_ENGINE in .env — built and connected by one line, nothing restarted

a tower that distills, and survives the night

the rules

allowed_topics in FILING — read, not written

only what should be kept

the season

season.sh — loaded, embedded in place, a graph laid over it, walked with one MATCH

everyone else's visits, by meaning and by connection

What each piece replaced

ADK 2

what you would write instead

what that costs you

session.state + one tool, kept by SqliteSessionService

a dict beside the chat loop, pickled to disk yourself

the model cannot read it, and you write the resume and the locking

the user: prefix

a users table and a join on every turn

deciding where a key belongs, every single time

add_events_to_memory + search_memory in a function node

a cron that dumps transcripts, and the model deciding when to look

it dumps, nothing distills — and you never see the times it didn't look

VertexAiMemoryBankService + allowed_topics

a vector store, an extraction prompt, a consolidation job, and "please don't remember trivia" in the prompt

three services, and the rule holds until the model feels otherwise

VECTOR_SEARCH + one fixed GQL MATCH

export, embed elsewhere, a graph database, a sync job, and the model writing SQL

the data leaves the warehouse to be understood, and you find out which joins it guessed when a customer does

Three questions, stop at the first yes

  1. Will it matter tomorrow? No → keep nothing. It lives and dies in the conversation.
  2. Can a function check it? Yes → the drawer. Code writes it, every turn reads it, and no sentence can change it.
  3. Otherwise → the tower. Distilled from what was said, looked up when it is asked for, revised by what is said next.

The one sentence

A record is not a memory. A memory is a record plus two decisions — what to keep, and when to look — and what kind of thing it is decides which floor it goes on.

Facts and preferences belong in different stores.

👀 Every floor is lit. Your familiar is at the top. The Archive stamp is in the passport, and week five opens the Night Market — where Nix runs the stage out loud, in real time, and everything the tower knows comes with you.