LastValue: el canal por defecto de escritura sobrescribiente
Responsabilidades
LastValue es la subclase más simple de BaseChannel: almacena un único valor y cada escritura sobrescribe el anterior (LastValue class:20-21). Es «el canal que se usa por defecto cuando un campo de state no lleva ningún Annotated»; si en StateGraph escribes class State(TypedDict): count: int, el campo count que ve el compilador es por dentro un LastValue(int) (fallback LastValue:1857-1859).
Su restricción clave es «solo se puede escribir un valor por paso». Cuando update recibe una secuencia de longitud mayor que 1, no fusiona ni sobrescribe: lanza directamente InvalidUpdateError (update:56-67), indicando al usuario que debe usar Annotated[int, reducer] para expresar «cómo fold cuando varios nodos escriben en el mismo paso». Esta restricción no es técnica sino semántica: un estado escalar no tiene forma razonable de fusionarse, así que mejor explotar antes que perder valores en silencio.
Además, este archivo define una variante LastValueAfterFinish (LastValueAfterFinish:81-152): se comporta como LastValue, pero los valores escritos solo son visibles después de que toda la ejecución Pregel invoque finish(); tras un consumo se vacía. Se usa en canales internos como «señal de interrupción» o «señal de fin» que solo surten efecto al final de la ejecución.
Motivación de diseño
- Sobrescribir es el único default razonable para estado escalar: enteros, cadenas, booleanos no tienen semántica de «fusión». Para un campo
count: int, sobrescribir el valor viejo con el nuevo es el comportamiento más intuitivo, por esoStateGraphcompila los campos sin reducer anotado comoLastValueenfallback:1857. - Rechazar la pérdida silenciosa de valores: si dos nodos escriben
counten el mismo superpaso,LastValueno elige arbitrariamente un ganador; lanzaINVALID_CONCURRENT_GRAPH_UPDATE(InvalidUpdateError:60-64). El mensaje indica explícitamente «Use an Annotated key to handle multiple values», empujando al usuario hacia la solución correcta: añadir un reducer, ver /channels/topic-binop. MISSINGen lugar deNone: el valor inicial eslanggraph._internal._typing.MISSING(value = MISSING:29), de modo queNonees un valor legítimamente almacenable.get()lanzaEmptyChannelErrorcuandoMISSING(get:69-72). Esto es coherente con la implementación por defecto deBaseChannel, pero aquíis_availablese sobreescribe como una sola líneaself.value is not MISSING(is_available:74-75) para evitar try/except en cada llamada.copymás barato quefrom_checkpoint: aunque la clase base por defecto hacecopy = from_checkpoint(checkpoint()), aquí se sobreescribecopy(copy:44-48) para compartir directamente la misma referencia avalue— un escalar no necesita deep-copy.from_checkpoint(from_checkpoint:50-54) decide según la ramaMISSINGsi asigna o no.- Visibilidad retrasada en
LastValueAfterFinish: ciertas señales de control (p. ej. el goto deCommand) no pueden hacerse visibles aprepare_next_tasksinmediatamente al escribir, de lo contrario se autodispararían dentro del mismo paso formando un ciclo.LastValueAfterFinishutiliza un flagfinished(finished flag:93-95);get()solo devuelve cuandofinished=True(get gated:145-148),finish()lo invoca Pregel al cierre (finish:138-143) yconsume()vacía tras un único consumo (consume:130-136).
Archivos clave
LastValue class:20-25— definición de la clase, genéricaBaseChannel[Value, Value, Value]: tipo de valor almacenado, tipo de escritura y tipo de checkpoint son todosValue.__init__:27-29— al construir,value = MISSING; solo deja de serMISSINGtras una escritura.ValueType / UpdateType:34-42— ambos devuelvenself.typ, el tipo proporcionado en la declaración.copy:44-48— reutiliza directamente la referenciavalue; un escalar no necesita deep-copy.from_checkpoint:50-54— reconstruye una instancia desde un valor de checkpoint;MISSINGindica empezar desde vacío.update:56-67— la restricción central: secuencia vacía devuelveFalse, longitud mayor que 1 lanzaInvalidUpdateError, en caso contrario guarda el último valor y devuelveTrue.get / is_available:69-75— interfaz de lectura;MISSINGlanzaEmptyChannelError;is_availablese sobreescribe como una comprobación de una línea.LastValueAfterFinish:81-95— variante: añade el flagfinished, retrasa la visibilidad hasta después definish().LastValueAfterFinish.update:122-128— al escribir, reseteafinisheda la vez, lo que significa que una nueva escritura revoca la visibilidad anterior.consume / finish / get:130-148— los tres hooks coordinan la semántica «visible una vez, luego se vacía».fallback LastValue:1857-1859— al compilar,StateGraphrecurre aLastValue(annotation)para campos sin reducer anotado.
Flujo de datos
Toda la lógica de LastValue.update está en estas líneas (update:56-67):
def update(self, values: Sequence[Value]) -> bool:
if len(values) == 0:
return False
if len(values) != 1:
msg = create_error_message(
message=f"At key '{self.key}': Can receive only one value per step. Use an Annotated key to handle multiple values.",
error_code=ErrorCode.INVALID_CONCURRENT_GRAPH_UPDATE,
)
raise InvalidUpdateError(msg)
self.value = values[-1]
return TrueNótese que en la tercera rama se escribe values[-1] en vez de values[0] — como ya se comprobó len != 1, esta línea solo se ejecuta en el caso len == 1, donde [-1] y [0] son equivalentes. Devolver False para la secuencia vacía es crucial: se coordina con la llamada update(EMPTY_SEQ) sobre canales no modificados dentro de apply_writes (EMPTY_SEQ update:329), para que LastValue devuelva correctamente «sin cambios» cuando se le invoca vacío.
Cómo StateGraph mapea los campos sin anotación a LastValue en compilación se describe en field_to_channel:1840-1859:
if manager := _is_field_managed_value(name, annotation):
if allow_managed:
return manager
else:
raise ValueError(f"This {annotation} not allowed in this position")
elif channel := _is_field_channel(annotation):
channel.key = name
return channel
elif channel := _is_field_binop(annotation):
channel.key = name
return channel
fallback: LastValue = LastValue(annotation)
fallback.key = name
return fallbackSe intenta en orden: managed value (gestión especial como en MessagesState) → instancia explícita de BaseChannel → Annotated[..., reducer] (BinaryOperatorAggregate, ver /channels/topic-binop) → si nada coincide, se recurre a LastValue.
La secuencia completa de lectura/escritura:
Límites y fallos
- Escritura concurrente multi-valor: error directo:
multi-value error:59-64lanzaInvalidUpdateErrorcon códigoINVALID_CONCURRENT_GRAPH_UPDATE. Es la trampa más habitual para principiantes: dos nodos escriben en paralelo en el mismo campo escalar. La solución es declarar el campo con reducer, p. ej.Annotated[int, lambda a, b: a + b]. - Escritura vacía devuelve
False:empty seq:57-58devuelveFalse, coherente con el contrato deBaseChannel;apply_writeslo utiliza para saltar la actualización dechannel_versions. __eq__no mira el valor sino solo el tipo:__eq__:31-32devuelveisinstance(value, LastValue), lo que significa que dos instanciasLastValuese consideran iguales sin importar lo que almacenan. Está pensado para la deduplicación durante la compilación del grafo; no lo uses para comparar valores.MISSINGfrente aNone:Nonees un valor legítimamente almacenable; soloMISSINGindica «nunca escrito» (get / is_available:70-75). Si tu campo de negocio admiteNone, escríbelo sin problema: el canal no lo tratará como vacío.- Una nueva escritura en
LastValueAfterFinishrevoca la visibilidad:LAF.update:122-128comienza conself.finished = False. Es decir, si trasfinish()llega otra escritura, el canal vuelve al estado «no publicado» y debe esperar al siguientefinish()para poder leerse. LastValueAfterFinish.consumees no-op cuandofinishedes falso: verLAF.consume:130-136. Así, un valor «escrito pero aún nofinish-ado» no se vacía; solo se vacía tras haberse consumido una vez — exactamente lo que requiere la semántica de «señal de fin».
Resumen
LastValue es la forma por defecto del estado de LangGraph: almacena un escalar, se actualiza por sobrescritura, y la restricción de un valor por paso explota al recibir múltiples en vez de perder valores en silencio. Entenderlo es entender cómo mutan en runtime todos los campos sin reducer anotado. Para ver cómo se fusionan correctamente escrituras concurrentes, continúa con /channels/topic-binop; la definición de la interfaz del canal vuelve a /channels/base-channel; el momento exacto en que se invoca update se describe en /pregel/algo.
Véase la documentación oficial: LangGraph docs · README