Files
langgraph/libs/langgraph-go/pregel/loop.go
T
2025-05-20 15:59:26 -07:00

501 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pregel
import (
"context"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"sync"
"time"
)
type GraphInterrupt struct {
Interrupts any
}
func (e GraphInterrupt) Error() string { return "graph interrupted" }
type GraphDelegate struct {
Payload map[string]any
}
func (e GraphDelegate) Error() string { return "graph delegation requested" }
func hashID(checkpointID string, parts ...string) string {
b, _ := hex.DecodeString(checkpointID)
h := sha256.New()
h.Write(b)
for _, p := range parts {
h.Write([]byte(p))
}
return hex.EncodeToString(h.Sum(nil))
}
type PregelLoop struct {
ctx context.Context
cancel context.CancelFunc
cfg RunnableConfig
store BaseStore
checkpoint Checkpoint
checkporter BaseCheckpointSaver
processes map[string]PregelNode
channels map[string]BaseChannel
managed ManagedValueMapping
step int
stop int
interruptBefore []string
interruptAfter []string
pendingWrites []WriteRecord
tasks map[string]*PregelExecutableTask
toInterrupt []*PregelExecutableTask
triggerToNodes map[string][]string
updatedChans map[string]struct{}
// synchronisation / workers
workers int
wg sync.WaitGroup
errMu sync.Mutex
runErr error
// Streaming
streamCh chan<- StreamChunk
streamMode StreamMode
pendingMu sync.Mutex
checkpointPendingWrites []PendingWrite
checkpointer Checkpointer // interface with PutWrites()
checkpointConfig RunnableConfig
emit func(task *PregelExecutableTask, writes []Write, cached bool)
}
type WriteRecord struct {
Task string
Chan string
Value any
}
// NewLoop initialises a fully-featured loop.
func NewLoop(
ctx context.Context,
checkpoint Checkpoint,
processes map[string]PregelNode,
channels map[string]BaseChannel,
managed ManagedValueMapping,
cfg RunnableConfig,
checkporter BaseCheckpointSaver,
store BaseStore,
) *PregelLoop {
c, cancel := context.WithCancel(ctx)
// Ensure checkpoint is properly initialized
if checkpoint.ChannelVersions == nil {
checkpoint = NewCheckpoint()
}
loop := &PregelLoop{
ctx: c,
cancel: cancel,
checkpoint: checkpoint,
processes: processes,
channels: channels,
managed: managed,
cfg: cfg,
checkporter: checkporter,
store: store,
step: 0,
stop: cfg.RecursionLimit,
workers: cfg.MaxConcurrency,
pendingWrites: make([]WriteRecord, 0, 16),
tasks: map[string]*PregelExecutableTask{},
}
if loop.workers <= 0 {
loop.workers = 1
}
return loop
}
// Run blocks until completion (or first error)
func (l *PregelLoop) Run() error {
defer l.cancel()
for {
more, err := l.tick(nil)
if err != nil {
if errors.As(err, &GraphInterrupt{}) {
return nil
}
return err
}
if !more {
break
}
}
return nil
}
// tick executes a single iteration of the Pregel loop.
// Returns true if more iterations are needed, false if done.
func (l *PregelLoop) tick(inputKeys []string) (bool, error) {
// TODO: Use inputKeys to get the first values.
// Check if we need to evaluate interrupts before execution
if err := l.evaluateInterrupt("before"); err != nil {
return false, err
}
// Build tasks
tasks, err := PrepareNextTasks(
l.ctx,
l.checkpoint,
convertPending(l.pendingWrites),
l.processes,
l.channels,
l.managed,
l.cfg,
l.step,
true,
l.store,
l.checkporter,
l.triggerToNodes,
l.updatedChans,
)
if err != nil {
return false, err
}
if len(tasks) == 0 {
return false, nil // done, no more tasks
}
l.tasks = make(map[string]*PregelExecutableTask)
for k, v := range tasks {
te := v.(PregelExecutableTask)
l.tasks[k] = &te
}
// parallel execute
workCh := make(chan *PregelExecutableTask)
errCh := make(chan error, l.workers)
for i := 0; i < l.workers; i++ {
go l.worker(workCh, errCh)
}
for _, t := range l.tasks {
if len(t.Writes) > 0 {
continue // already satisfied
}
workCh <- t
}
close(workCh)
for i := 0; i < l.workers; i++ {
if err := <-errCh; err != nil {
return false, err
}
}
// All tasks finished; apply writes
if err := l.applyWrites(); err != nil {
return false, err
}
// checkpoint
if err := l.saveCheckpoint(); err != nil {
return false, err
}
// Check if we need to evaluate interrupts after execution
if err := l.evaluateInterrupt("after"); err != nil {
return false, err
}
// Check if we've exceeded the recursion limit
l.step++
if l.step > l.stop {
return false, fmt.Errorf("exceeded recursion limit (%d)", l.stop)
}
return true, nil
}
// prepareAndExecuteStep is kept for backward compatibility
func (l *PregelLoop) prepareAndExecuteStep() error {
more, err := l.tick(nil)
if err != nil {
return err
}
if !more {
return nil
}
return nil
}
func (l *PregelLoop) worker(in <-chan *PregelExecutableTask, out chan<- error) {
for task := range in {
err := l.runTask(task)
out <- err
}
}
func (l *PregelLoop) runTask(t *PregelExecutableTask) error {
// retry loop
attempts := 0
max := 1
if p, ok := l.processes[t.Name]; ok {
max = maxAttempts(p.Retry)
}
for {
attempts++
select {
case <-l.ctx.Done():
return l.ctx.Err()
default:
}
writes, err := t.Node.Invoke(l.ctx, t.Input, t.Config, l)
if err == nil {
for _, w := range writes {
l.recordWrite(t.ID, w.Channel, w.Value)
}
t.Writes = writes
return nil
}
if attempts >= max {
return err
}
time.Sleep(backoffDelay(attempts))
}
}
// putWrites is called by PregelRunner (or nested tasks via the SEND helper)
// to persist writes produced by a task *during the current super-step*.
// It is safe for concurrent use.
func (l *PregelLoop) putWrites(taskID string, writes []Write) {
if len(writes) == 0 {
return
}
// ---------------------------------------------------------------------
// 1. Deduplicate if every write is for a “special” indexed channel.
// (“last one wins”, exactly like in TS / Python)
// ---------------------------------------------------------------------
allIndexed := true
for _, w := range writes {
if _, ok := WRITES_IDX_MAP[w.Channel]; !ok {
allIndexed = false
break
}
}
if allIndexed {
dedup := make(map[string]Write, len(writes))
for _, w := range writes {
dedup[w.Channel] = w
}
writes = make([]Write, 0, len(dedup))
for _, w := range dedup {
writes = append(writes, w)
}
}
// ---------------------------------------------------------------------
// 2. Merge into l.checkpointPendingWrites.
// We need a mutex because PregelRunner goroutines call us in parallel.
// ---------------------------------------------------------------------
l.pendingMu.Lock()
for _, w := range writes {
replaced := false
// If it is an indexed channel and an entry already exists for (task,channel),
// overwrite it (=> keep only the newest write).
if _, special := WRITES_IDX_MAP[w.Channel]; special {
for i := range l.checkpointPendingWrites {
pw := &l.checkpointPendingWrites[i]
if pw.TaskID == taskID && pw.Channel == w.Channel {
pw.Value = w.Value
replaced = true
break
}
}
}
// Otherwise (or if not found) just append.
if !replaced {
l.checkpointPendingWrites = append(
l.checkpointPendingWrites,
PendingWrite{TaskID: taskID, Channel: w.Channel, Value: w.Value},
)
}
}
l.pendingMu.Unlock()
// ---------------------------------------------------------------------
// 3. Forward the writes to the configured checkpointer (if any).
// We don’t block the caller – a quick “fire-and-forget” goroutine
// is fine because checkpointer.PutWrites() is thread-safe by design.
// ---------------------------------------------------------------------
// if l.checkpointer != nil {
// cfg := l.checkpointConfig // shallow copy is enough – we never mutate it
// go l.checkpointer.PutWrites(cfg, writes, taskID)
// }
// ---------------------------------------------------------------------
// 4. Emit stream/debug output if the loop is already running.
// ---------------------------------------------------------------------
if len(l.tasks) > 0 {
l.outputWrites(taskID, writes, false)
}
}
// outputWrites mirrors TS _outputWrites (omits hidden tasks & handles modes).
// This is a *minimal* version; extend if you need streaming/debug UI parity.
func (l *PregelLoop) outputWrites(taskID string, writes []Write, cached bool) {
task, ok := l.tasks[taskID]
if !ok {
return
}
for _, tag := range task.Config.Tags {
if tag == TAG_HIDDEN {
return
}
}
// TODO: implement streaming
// delegate to whatever streaming mechanism you implemented…
// if l.emit != nil {
// l.emit(task, writes, cached)
// }
}
func maxAttempts(r RetryPolicy) int {
if r.MaxAttempts <= 0 {
return 1
}
return r.MaxAttempts
}
func backoffDelay(at int) time.Duration { return time.Duration(at) * 50 * time.Millisecond }
func (l *PregelLoop) Send(taskID string, writes []Write) {
for _, w := range writes {
l.recordWrite(taskID, w.Channel, w.Value)
}
}
// Read returns a copy of current channel values
func (l *PregelLoop) Read(selectKeys []string) map[string]any {
out := map[string]any{}
for _, k := range selectKeys {
if ch, ok := l.channels[k]; ok {
out[k] = ch.Get()
}
}
return out
}
func (l *PregelLoop) AcceptPush(origin PregelExecutableTask, writeIdx int, call *Call) (*PregelExecutableTask, error) {
ppath := origin.Path
newPath := []interface{}{PUSH, ppath, writeIdx, origin.ID, call}
cpid, _ := hex.DecodeString(l.checkpoint.ID)
nullVer := -1
task, err := PrepareSingleTask(
l.ctx,
newPath,
"",
l.checkpoint,
cpid,
nullVer,
convertPending(l.pendingWrites),
l.processes,
l.channels,
l.managed,
l.cfg,
l.step,
true,
l.store,
l.checkporter,
)
if err != nil {
return nil, err
}
if task == nil {
return nil, nil
}
te := task.(PregelExecutableTask)
l.tasks[te.ID] = &te
return &te, nil
}
func (l *PregelLoop) recordWrite(taskID, ch string, val any) {
l.pendingWrites = append(l.pendingWrites, WriteRecord{taskID, ch, val})
}
func convertPending(ws []WriteRecord) []interface{} {
out := make([]interface{}, 0, len(ws))
for _, w := range ws {
out = append(out, []interface{}{w.Task, w.Chan, w.Value})
}
return out
}
func (l *PregelLoop) applyWrites() error {
if len(l.pendingWrites) == 0 {
return nil
}
for _, wr := range l.pendingWrites {
ch, ok := l.channels[wr.Chan]
if !ok {
ch = &simpleChan{}
l.channels[wr.Chan] = ch
}
ch.Set(wr.Value)
// TODO: Handle other version types.
if _, exists := l.checkpoint.ChannelVersions[wr.Chan]; !exists {
l.checkpoint.ChannelVersions[wr.Chan] = 0
}
l.checkpoint.ChannelVersions[wr.Chan]++
}
l.pendingWrites = l.pendingWrites[:0]
return nil
}
func (l *PregelLoop) saveCheckpoint() error {
if l.checkporter == nil {
return nil
}
md := map[string]any{
"step": l.step,
"source": "loop",
"time": time.Now().UTC().Format(time.RFC3339Nano),
}
return l.checkporter.Put(l.cfg, l.checkpoint, md, nil)
}
func (l *PregelLoop) evaluateInterrupt(stage string) error {
var conditions []string
if stage == "before" {
conditions = l.interruptBefore
} else {
conditions = l.interruptAfter
}
if len(conditions) == 0 {
return nil
}
seen := map[string]struct{}{}
for _, t := range l.tasks {
for _, trg := range t.Triggers {
seen[trg] = struct{}{}
}
}
for _, cond := range conditions {
if _, ok := seen[cond]; ok || cond == "*" {
return GraphInterrupt{}
}
}
return nil
}
type Result struct {
Err error
}