Uso del tiempo de vida para mensajes y recuerdos

Los mensajes y los recuerdos pueden seguir siendo buscables después de que termine la conversación que los creó. Time-to-live permite que las aplicaciones caduquen automáticamente los registros después de un período definido, por ejemplo, para cumplir con los requisitos de conformidad, eliminar información que probablemente esté obsoleta o ahorrar espacio de almacenamiento.

En esta guía se muestra cómo configurar la caducidad del registro para los mensajes y memorias respaldados por Oracle DB. También explica los trabajos de depuración y las sustituciones de tiempo de actividad por registro.

La memoria del agente de Oracle aplica el tiempo de actividad en dos capas:

Al seleccionar un anclaje por registro, utilice TimeToLiveAnchor.CREATED_AT para realizar el recuento desde el momento en que Oracle almacena la fila, o bien TimeToLiveAnchor.TIMESTAMP para realizar el recuento desde el registro de hora del evento del registro.

Nota: Utilice el tiempo de actividad cuando los registros deban permanecer disponibles durante un período definido y, a continuación, caduquen automáticamente. Por ejemplo, una aplicación puede conservar los detalles de soporte durante 30 días o la información de tarea temporal durante una semana.

Nota: Hay que tener en cuenta algunas reglas de retención:

Indicación: Para la configuración del paquete, consulte Get Started with Agent Memory. Si necesita una instancia local de Oracle AI Database para este ejemplo, siga Ejecución local de Oracle AI Database.

Configuración de Valores por Defecto de Retención de Nivel de Esquema

Cree un cliente OracleAgentMemory con schema_policy=SchemaPolicy.CREATE_IF_NECESSARY cuando desee que el SDK cree o actualice el esquema de Oracle gestionado y mantenga una política de retención junto con él.

from datetime import datetime, timezone

import oracledb

from oracleagentmemory.apis import TimeToLiveAnchor
from oracleagentmemory.core import (
    MemoryRetentionConfig,
    OracleAgentMemory,
    SchemaPolicy,
)
from oracleagentmemory.core.embedders.embedder import Embedder
from oracleagentmemory.core.llms.llm import Llm

embedder = Embedder(model="YOUR_EMBEDDING_MODEL")
llm = Llm(
    model="YOUR_MEMORY_LLM_MODEL",
    api_base="YOUR_MEMORY_LLM_BASE_URL",
    api_key="YOUR_MEMORY_LLM_API_KEY",
)
db_pool = oracledb.SessionPool(
    user="YOUR DB USER",
    password="YOUR DB PASSWORD",
    dsn="localhost:1521/...",
)
memory_store_id = "T_TIME_TO_LIVE"

memory = OracleAgentMemory(
    connection=db_pool,
    embedder=embedder,
    llm=llm,
    schema_policy=SchemaPolicy.CREATE_IF_NECESSARY,
    memory_store_id=memory_store_id,
    memory_retention_config=MemoryRetentionConfig(
        default_ttl_days=30,
        max_ttl_days=90,
    ),
)

En esta configuración:

Referencia de API: OracleAgentMemory

Agregar mensajes y recuerdos con el tiempo de vida

Utilice escrituras de thread para basarse en el TTL por defecto del esquema para algunos registros al definir un TTL diferente para otros.

ttl_thread = memory.create_thread(
    thread_id="ttl_demo_thread",
    user_id="user_123",
)

message_ids = ttl_thread.add_messages(
    [
        {
            "id": "msg-ttl-1",
            "role": "user",
            "content": (
                "I opened ticket 1042 yesterday because the laptop battery failed."
            ),
            "timestamp": "2026-04-01T09:00:00Z",
        },
        {
            "id": "msg-ttl-2",
            "role": "assistant",
            "content": (
                "I will keep ticket 1042 active and send a replacement checklist."
            ),
            "timestamp": "2026-04-01T09:01:00Z",
        },
    ]
)

timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
#^ for instance, 2026-04-01T09:00:00Z

backfilled_memory_id = ttl_thread.add_memory(
    "Ticket 1042 battery-failure report was filed on 2026-04-01.",
    memory_id="mem-ttl-backfill",
    timestamp=timestamp,
    ttl_days=90,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)

short_lived_memory_id = ttl_thread.add_memory(
    "Replacement checklist should be revisited within one week.",
    memory_id="mem-ttl-short",
    ttl_days=7,
)

print(message_ids)
print(backfilled_memory_id)
print(short_lived_memory_id)
#['msg-ttl-1', 'msg-ttl-2']
#mem-ttl-backfill
#mem-ttl-short

En este ejemplo:

Referencia de API: OracleThread

Refrescar tiempo de actividad en registros existentes

Utilice update_message() o update_memory() para refrescar una caducidad existente sin sustituir el contenido almacenado.

ttl_thread.update_message("msg-ttl-1", ttl_days=14)

ttl_thread.update_memory(
    "mem-ttl-backfill",
    ttl_days=60,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)

Estas llamadas mantienen el contenido y los metadatos almacenados en su lugar mientras se vuelve a calcular la ventana de caducidad. Recuerde que las actualizaciones conservan la caducidad actual a menos que pase ttl_days o ttl_anchor.

Referencia de API: OracleThread

Asegúrese de que existe el trabajo de depuración gestionado

Cuando la memoria del agente de Oracle crea o actualiza su esquema de Oracle gestionado, también crea dos trabajos DBMS_SCHEDULER diarios. Una suprime las filas de memoria y mensajes caducados y sus fragmentos de recuperación. El resto suprime fragmentos de recuperación huérfanos cuya fila de origen ya no existe o ya no coincide con su tipo de origen. Ambos trabajos depuran filas en lotes en lugar de emitir una gran supresión, confirmación entre lotes y capturan un registro de hora de referencia por adelantado para que cada ejecución utilice un corte de caducidad consistente.

Cada definición del programador define schedule_limit en un día para que se pueda omitir una ejecución que se inicie demasiado tarde en lugar de ejecutarse arbitrariamente tarde. Cada cuerpo de trabajo deja de tomar lotes nuevos por separado después de un día desde la hora de inicio de ejecución. Ese límite de tiempo de ejecución se comprueba entre lotes, por lo que un lote que ya está en curso puede finalizar y confirmar. Al final de una ejecución, cada trabajo escribe un breve resumen DBMS_OUTPUT que incluye si se ha alcanzado el límite de tiempo de ejecución y cuántas filas se han suprimido.

Cuando SchemaPolicy.CREATE_IF_NECESSARY necesita crear cualquier trabajo pero el usuario de esquema carece de CREATE JOB, la configuración se completa con una advertencia. Los registros caducados permanecen ocultos en las lecturas y búsquedas, y los fragmentos huérfanos permanecen hasta que un usuario con privilegios crea el trabajo que falta.

Si define memory_store_id explícitamente, agregue un prefijo a los nombres de trabajo y tabla con ese ID más un guion bajo. Si sigue utilizando el table_name_prefix en desuso, aplique ese prefijo en su lugar. Los nombres de trabajos gestionados por defecto son PURGE_EXPIRED_RECORDS_J y PURGE_ORPHANED_CHUNKS_J.

Consejo: utilice las siguientes opciones de DBA cuando el propietario del esquema no tenga CREATE JOB.

Otorgar Privilegios de Trabajo del Planificador

GRANT CREATE JOB TO app_schema;
-- Run OracleAgentMemory schema setup as app_schema.
REVOKE CREATE JOB FROM app_schema;

Creación Manual del Trabajo de Depuración

Sustituya APP_SCHEMA por el propietario del esquema gestionado y ajuste los nombres de objeto si se configura un prefijo de nombre de tabla. El siguiente bloque PL/SQL es intencionalmente compacto, por lo que permanece dentro del límite de longitud job_action de Oracle Scheduler incluso cuando los nombres de objeto con prefijo son largos:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    job_type => 'PLSQL_BLOCK',
    job_action => q'[
      DECLARE
        bs CONSTANT PLS_INTEGER := 1000;
        n PLS_INTEGER;
        mc PLS_INTEGER := 0;
        xc PLS_INTEGER := 0;
        mr PLS_INTEGER := 0;
        xr PLS_INTEGER := 0;
        tl PLS_INTEGER := 0;
        rt TIMESTAMP(6) WITH TIME ZONE := SYSTIMESTAMP;
        dl TIMESTAMP(6) WITH TIME ZONE := rt + INTERVAL '1 00:00:00' DAY TO SECOND;
      BEGIN
        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.RECORD_CHUNKS c
          WHERE c.rowid IN (
            SELECT rid
            FROM (
              SELECT c.rowid AS rid
              FROM APP_SCHEMA.RECORD_CHUNKS c
              WHERE c.source_record_type = 'message'
                AND EXISTS (
                  SELECT 1
                  FROM APP_SCHEMA.MESSAGE m
                  WHERE m.record_id = c.source_id
                    AND m.expires_at IS NOT NULL
                    AND m.expires_at <= rt
                )
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          mc := mc + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.RECORD_CHUNKS c
          WHERE c.rowid IN (
            SELECT rid
            FROM (
              SELECT c.rowid AS rid
              FROM APP_SCHEMA.RECORD_CHUNKS c
              WHERE c.source_record_type IN ('fact', 'guideline', 'memory', 'preference')
                AND EXISTS (
                  SELECT 1
                  FROM APP_SCHEMA.MEMORY m
                  WHERE m.record_id = c.source_id
                    AND m.memory_type = c.source_record_type
                    AND m.expires_at IS NOT NULL
                    AND m.expires_at <= rt
                )
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          xc := xc + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.MESSAGE
          WHERE rowid IN (
            SELECT rid
            FROM (
              SELECT rowid AS rid
              FROM APP_SCHEMA.MESSAGE
              WHERE expires_at IS NOT NULL
                AND expires_at <= rt
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          mr := mr + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.MEMORY
          WHERE rowid IN (
            SELECT rid
            FROM (
              SELECT rowid AS rid
              FROM APP_SCHEMA.MEMORY
              WHERE expires_at IS NOT NULL
                AND expires_at <= rt
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          xr := xr + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;

        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge reference_time='
          || TO_CHAR(rt, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge run_deadline='
          || TO_CHAR(dl, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge runtime_limit_reached='
          || CASE WHEN tl = 1 THEN 'true' ELSE 'false' END
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted message chunk rows='
          || TO_CHAR(mc)
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted fact/guideline/memory/preference chunk rows='
          || TO_CHAR(xc)
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted message rows='
          || TO_CHAR(mr)
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory purge deleted fact/guideline/memory/preference rows='
          || TO_CHAR(xr)
        );
      END;
    ]',
    start_date => SYSTIMESTAMP,
    repeat_interval => 'FREQ=DAILY;INTERVAL=1',
    enabled => FALSE,
    auto_drop => FALSE,
    comments => 'OracleAgentMemory expired-record purge'
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    attribute => 'logging_level',
    value => DBMS_SCHEDULER.LOGGING_RUNS
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    attribute => 'store_output',
    value => TRUE
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    attribute => 'schedule_limit',
    value => INTERVAL '1 00:00:00' DAY TO SECOND
  );
  DBMS_SCHEDULER.ENABLE('APP_SCHEMA.PURGE_EXPIRED_RECORDS_J');
END;
/

La limpieza de fragmentos huérfanos se ejecuta como un trabajo diario independiente:

BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    job_type => 'PLSQL_BLOCK',
    job_action => q'[
      DECLARE
        bs CONSTANT PLS_INTEGER := 1000;
        n PLS_INTEGER;
        oc PLS_INTEGER := 0;
        tl PLS_INTEGER := 0;
        rt TIMESTAMP(6) WITH TIME ZONE := SYSTIMESTAMP;
        dl TIMESTAMP(6) WITH TIME ZONE := rt + INTERVAL '1 00:00:00' DAY TO SECOND;
      BEGIN
        LOOP
          IF SYSTIMESTAMP >= dl THEN
            tl := 1;
            EXIT;
          END IF;
          DELETE FROM APP_SCHEMA.RECORD_CHUNKS c
          WHERE c.rowid IN (
            SELECT rid
            FROM (
              SELECT c.rowid AS rid
              FROM APP_SCHEMA.RECORD_CHUNKS c
              WHERE NOT (
                (
                  c.source_record_type = 'message'
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.MESSAGE m
                    WHERE m.record_id = c.source_id
                  )
                )
                OR (
                  c.source_record_type IN ('fact', 'guideline', 'memory', 'preference')
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.MEMORY m
                    WHERE m.record_id = c.source_id
                      AND m.memory_type = c.source_record_type
                  )
                )
                OR (
                  c.source_record_type = 'user_profile'
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.ACTOR_PROFILE a
                    WHERE a.actor_id = c.source_id
                      AND a.actor_type = 'user'
                  )
                )
                OR (
                  c.source_record_type = 'agent_profile'
                  AND EXISTS (
                    SELECT 1
                    FROM APP_SCHEMA.ACTOR_PROFILE a
                    WHERE a.actor_id = c.source_id
                      AND a.actor_type = 'agent'
                  )
                )
              )
              FETCH FIRST bs ROWS ONLY
            )
          );
          n := SQL%ROWCOUNT;
          oc := oc + n;
          EXIT WHEN n = 0;
          COMMIT;
        END LOOP;
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup reference_time='
          || TO_CHAR(rt, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup run_deadline='
          || TO_CHAR(dl, 'YYYY-MM-DD"T"HH24:MI:SS.FF3TZH:TZM')
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup runtime_limit_reached='
          || CASE WHEN tl = 1 THEN 'true' ELSE 'false' END
        );
        DBMS_OUTPUT.PUT_LINE(
          'OracleAgentMemory orphan cleanup deleted chunk rows='
          || TO_CHAR(oc)
        );
      END;
    ]',
    start_date => SYSTIMESTAMP,
    repeat_interval => 'FREQ=DAILY;INTERVAL=1',
    enabled => FALSE,
    auto_drop => FALSE,
    comments => 'OracleAgentMemory orphaned-chunk cleanup'
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    attribute => 'logging_level',
    value => DBMS_SCHEDULER.LOGGING_RUNS
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    attribute => 'store_output',
    value => TRUE
  );
  DBMS_SCHEDULER.SET_ATTRIBUTE(
    name => 'APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J',
    attribute => 'schedule_limit',
    value => INTERVAL '1 00:00:00' DAY TO SECOND
  );
  DBMS_SCHEDULER.ENABLE('APP_SCHEMA.PURGE_ORPHANED_CHUNKS_J');
END;
/

Resulta útil confirmar la definición del programador e inspeccionar el historial de ejecuciones recientes. Sustituya APP_SCHEMA si es necesario y aplique el prefijo de nombre de tabla configurado al nombre de trabajo cuando el despliegue utilice uno.

Compruebe que el trabajo de depuración gestionado existe y mantiene la configuración de inspección esperada:

SELECT owner,
       job_name,
       enabled,
       state,
       repeat_interval,
       schedule_limit,
       logging_level,
       store_output,
       start_date,
       last_start_date,
       next_run_date
FROM   all_scheduler_jobs
WHERE  owner = 'APP_SCHEMA'
AND    job_name IN (
         'PURGE_EXPIRED_RECORDS_J',
         'PURGE_ORPHANED_CHUNKS_J'
       );

Compruebe las ejecuciones recientes cuando necesite confirmar que las ejecuciones de depuración se están realizando y finalizan correctamente:

SELECT log_date,
       status,
       run_duration,
       output,
       additional_info
FROM   all_scheduler_job_run_details
WHERE  owner = 'APP_SCHEMA'
AND    job_name IN (
         'PURGE_EXPIRED_RECORDS_J',
         'PURGE_ORPHANED_CHUNKS_J'
       )
ORDER BY log_date DESC;

Ejecute cualquier trabajo una vez manualmente si desea validar la instalación inmediatamente:

BEGIN
  DBMS_SCHEDULER.RUN_JOB(
    job_name => 'APP_SCHEMA.PURGE_EXPIRED_RECORDS_J',
    use_current_session => TRUE
  );
END;
/

Conclusión

En esta guía hemos aprendido cómo los valores por defecto de retención de nivel de esquema, los valores ttl_days por registro y TimeToLiveAnchor funcionan conjuntamente, cómo los trabajos gestionados eliminan filas caducadas y huérfanas y cómo refrescar las vencimientos existentes de Python.

→ Después de haber aprendido a controlar la duración del registro, ahora puede continuar con Use Metadata and Metadata Filtering (Usar filtrado de metadatos y metadatos).

Código Completo

Copie el código completo que aparece a continuación.

#Copyright © 2026 Oracle and/or its affiliates.
#This software is under the Apache License 2.0
#(LICENSE-APACHE or http://www.apache.org/licenses/LICENSE-2.0) or Universal Permissive License
#(UPL) 1.0 (LICENSE-UPL or https://oss.oracle.com/licenses/upl), at your option.

#Oracle Agent Memory Code Example - Use Time-to-Live for Messages and Memories
#-----------------------------------------------------------------------------

##Create a retention aware client

from datetime import datetime, timezone

import oracledb

from oracleagentmemory.apis import TimeToLiveAnchor
from oracleagentmemory.core import (
    MemoryRetentionConfig,
    OracleAgentMemory,
    SchemaPolicy,
)
from oracleagentmemory.core.embedders.embedder import Embedder
from oracleagentmemory.core.llms.llm import Llm

embedder = Embedder(model="YOUR_EMBEDDING_MODEL")
llm = Llm(
    model="YOUR_MEMORY_LLM_MODEL",
    api_base="YOUR_MEMORY_LLM_BASE_URL",
    api_key="YOUR_MEMORY_LLM_API_KEY",
)
db_pool = oracledb.SessionPool(
    user="YOUR DB USER",
    password="YOUR DB PASSWORD",
    dsn="localhost:1521/...",
)
memory_store_id = "T_TIME_TO_LIVE"

memory = OracleAgentMemory(
    connection=db_pool,
    embedder=embedder,
    llm=llm,
    schema_policy=SchemaPolicy.CREATE_IF_NECESSARY,
    memory_store_id=memory_store_id,
    memory_retention_config=MemoryRetentionConfig(
        default_ttl_days=30,
        max_ttl_days=90,
    ),
)

##Add messages and memories with time to live

ttl_thread = memory.create_thread(
    thread_id="ttl_demo_thread",
    user_id="user_123",
)

message_ids = ttl_thread.add_messages(
    [
        {
            "id": "msg-ttl-1",
            "role": "user",
            "content": (
                "I opened ticket 1042 yesterday because the laptop battery failed."
            ),
            "timestamp": "2026-04-01T09:00:00Z",
        },
        {
            "id": "msg-ttl-2",
            "role": "assistant",
            "content": (
                "I will keep ticket 1042 active and send a replacement checklist."
            ),
            "timestamp": "2026-04-01T09:01:00Z",
        },
    ]
)

timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
#^ for instance, 2026-04-01T09:00:00Z

backfilled_memory_id = ttl_thread.add_memory(
    "Ticket 1042 battery-failure report was filed on 2026-04-01.",
    memory_id="mem-ttl-backfill",
    timestamp=timestamp,
    ttl_days=90,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)

short_lived_memory_id = ttl_thread.add_memory(
    "Replacement checklist should be revisited within one week.",
    memory_id="mem-ttl-short",
    ttl_days=7,
)

print(message_ids)
print(backfilled_memory_id)
print(short_lived_memory_id)
#['msg-ttl-1', 'msg-ttl-2']
#mem-ttl-backfill
#mem-ttl-short

##Refresh time to live on existing records

ttl_thread.update_message("msg-ttl-1", ttl_days=14)

ttl_thread.update_memory(
    "mem-ttl-backfill",
    ttl_days=60,
    ttl_anchor=TimeToLiveAnchor.TIMESTAMP,
)