Some fancy stuff

This commit is contained in:
William Fu-Hinthorn
2025-05-20 15:59:26 -07:00
parent 0a8f54ca4a
commit 78239caed0
9 changed files with 1739 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
.PHONY: all format build test
# Default target executed when no arguments are given to make.
all: help
format:
go fmt ./...
build:
go build ./...
test:
go test ./...
######################
# HELP
######################
help:
@echo '===================='
@echo '-- DOCUMENTATION --'
@echo '-- LINTING --'
@echo 'format - run code formatters'
@echo 'build - build the project'
@echo 'test - run unit tests'
+10
View File
@@ -0,0 +1,10 @@
module langchain.dev/langgraph
go 1.23.0
toolchain go1.23.9
require (
github.com/google/uuid v1.6.0 // indirect
golang.org/x/sync v0.14.0 // indirect
)
+4
View File
@@ -0,0 +1,4 @@
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
+522
View File
@@ -0,0 +1,522 @@
package pregel
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"reflect"
"sort"
"strconv"
"strings"
)
func PrepareNextTasks(
ctx context.Context,
checkpoint Checkpoint,
pendingWrites []interface{},
processes map[string]PregelNode,
channels map[string]BaseChannel,
managed ManagedValueMapping,
config RunnableConfig,
step int,
forExecution bool,
store BaseStore,
checkpointer BaseCheckpointSaver,
// ─ optimisation hints (optional) ─
triggerToNodes map[string][]string,
updatedChannels map[string]struct{},
) (map[string]interface{}, error) {
// Decode checkpoint.id (UUID/xxhash) into raw bytes for deterministic task-id hashing.
cleanID := strings.ReplaceAll(checkpoint.ID, "-", "")
checkpointIDBytes, err := hex.DecodeString(cleanID)
if err != nil {
return nil, err
}
nullVersion := checkpointNullVersion(checkpoint)
tasks := make(map[string]interface{})
// Consume pending sends
for idx := range checkpoint.PendingSends {
task, err := PrepareSingleTask(
ctx,
[]interface{}{PUSH, idx},
"",
checkpoint,
checkpointIDBytes,
nullVersion,
pendingWrites,
processes,
channels,
managed,
config,
step,
forExecution,
store,
checkpointer,
)
if err != nil {
return nil, err
}
if task == nil {
continue
}
if id, ok := taskID(task); ok {
tasks[id] = task
}
}
var candidateNodes []string
if len(updatedChannels) > 0 && len(triggerToNodes) > 0 {
nodeSet := map[string]struct{}{}
for ch := range updatedChannels {
for _, n := range triggerToNodes[ch] {
nodeSet[n] = struct{}{}
}
}
for n := range nodeSet {
candidateNodes = append(candidateNodes, n)
}
sort.Strings(candidateNodes) // deterministic order
} else if len(checkpoint.ChannelVersions) == 0 {
candidateNodes = nil
} else {
for n := range processes {
candidateNodes = append(candidateNodes, n)
}
sort.Strings(candidateNodes)
}
for _, name := range candidateNodes {
task, err := PrepareSingleTask(
ctx,
[]interface{}{PULL, name},
"", // checksum only used when resuming a partial step
checkpoint,
checkpointIDBytes,
nullVersion,
pendingWrites,
processes,
channels,
managed,
config,
step,
forExecution,
store,
checkpointer,
)
if err != nil {
return nil, err
}
if task == nil {
continue
}
if id, ok := taskID(task); ok {
tasks[id] = task
}
}
return tasks, nil
}
func PrepareSingleTask(
ctx context.Context,
taskPath []interface{}, // e.g. [PUSH, idx] OR [PULL, "node"]
taskIDChecksum string, // optional used when resuming
checkpoint Checkpoint, // state captured at end of previous step
checkpointIDBytes []byte, // checkpoint.id as bytes (uuid / xxhash)
checkpointNullVersion interface{}, // sentinel “null” version value
pendingWrites []interface{}, // successful writes from *this* step so far
processes map[string]PregelNode, // graph definition
channels map[string]BaseChannel, // live channel values
managed ManagedValueMapping, // placeholder resolver
config RunnableConfig, // config inherited from graph.Invoke()
step int, // current super-step (n+1)
forExecution bool, // false = planning pass, true = exec pass
store BaseStore, // needed for reads/writes
checkpointer BaseCheckpointSaver, // used only when executing
) (interface{}, error) {
// Ensure checkpoint.ChannelVersions is initialized
if checkpoint.ChannelVersions == nil {
checkpoint.ChannelVersions = make(map[string]int64)
}
cfgSection := config.Configurable
if cfgSection == nil {
cfgSection = map[string]interface{}{}
}
parentNS, _ := cfgSection[CONFIG_KEY_CHECKPOINT_NS].(string)
emitConfig := func(base RunnableConfig, md map[string]interface{}) RunnableConfig {
// Make a shallow copy of the struct
out := base
if out.Configurable == nil {
out.Configurable = map[string]interface{}{}
}
confClone := make(map[string]interface{}, len(out.Configurable))
for k, v := range out.Configurable {
confClone[k] = v
}
confClone[CONFIG_KEY_SCRATCHPAD] = createScratchpad(
out.Configurable[CONFIG_KEY_SCRATCHPAD].(map[string]interface{}),
pendingWrites,
md["langgraph_checkpoint_ns"].(string),
md["langgraph_checkpoint_ns"].(string),
out.Configurable[CONFIG_KEY_RESUME_MAP].(map[string]interface{}),
)
confClone[CONFIG_KEY_CHECKPOINTER] = checkpointer
out.Configurable = confClone
if out.Metadata == nil {
out.Metadata = map[string]interface{}{}
}
for k, v := range md {
out.Metadata[k] = v
}
return out
}
// Convenience for checksum comparison
checkSumMatch := func(need string) error {
if taskIDChecksum != "" && taskIDChecksum != need {
return fmt.Errorf("%s != %s", need, taskIDChecksum)
}
return nil
}
// PUSH
if len(taskPath) > 0 && taskPath[0] == PUSH {
// PUSH triggered via explicit Call (happens during node execution)
// taskPath shape: [PUSH, parentPath, writeIdx, parentTaskID, Call]
if len(taskPath) >= 5 {
call, ok := taskPath[4].(Call)
if ok {
name, isStr := call.Func.(string)
if !isStr {
name = "unknown"
}
// Hash-stable checkpoint namespace
var checkpointNS string
if parentNS == "" {
checkpointNS = name
} else {
checkpointNS = parentNS + NS_SEP + name
}
// Deterministic task-id
taskID := taskIDFunc(
checkpointIDBytes,
checkpointNS,
strconv.Itoa(step),
name,
PUSH,
taskPathStr(taskPath[1]),
fmt.Sprintf("%v", taskPath[2]),
)
if err := checkSumMatch(taskID); err != nil {
return nil, err
}
taskCheckpointNS := checkpointNS + NS_END + taskID
metadata := map[string]interface{}{
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": []string{PUSH},
"langgraph_path": taskPath[:3],
"langgraph_checkpoint_ns": taskCheckpointNS,
}
if forExecution {
var node NodeRunnable
if proc, ok := processes[name]; ok {
node = proc.Node
}
return PregelExecutableTask{
PregelTask: PregelTask{
ID: taskID,
Name: name,
Path: taskPath[:3],
},
Input: call.Input,
Node: node,
Writes: []Write{},
Config: emitConfig(config, metadata),
Triggers: []string{PUSH},
}, nil
}
return PregelTask{ID: taskID, Name: name, Path: taskPath[:3]}, nil
}
}
// ---------------------------------------------------------------------
// 1b. Standard pending-send packet: taskPath shape [PUSH, idx]
// ---------------------------------------------------------------------
if len(taskPath) == 2 {
idx, ok := taskPath[1].(int)
if !ok || idx >= len(checkpoint.PendingSends) {
return nil, nil
}
packet := checkpoint.PendingSends[idx]
proc, ok := processes[packet.Node]
if !ok || proc.Node == nil {
return nil, nil
}
checkpointNS := parentNS
if checkpointNS != "" {
checkpointNS += NS_SEP + packet.Node
} else {
checkpointNS = packet.Node
}
taskID := taskIDFunc(
checkpointIDBytes,
checkpointNS,
strconv.Itoa(step),
packet.Node,
PUSH,
strconv.Itoa(idx),
)
if err := checkSumMatch(taskID); err != nil {
return nil, err
}
taskCheckpointNS := checkpointNS + NS_END + taskID
metadata := map[string]interface{}{
"langgraph_step": step,
"langgraph_node": packet.Node,
"langgraph_triggers": []string{PUSH},
"langgraph_path": taskPath,
"langgraph_checkpoint_ns": taskCheckpointNS,
}
if forExecution {
return PregelExecutableTask{
PregelTask: PregelTask{
ID: taskID,
Name: packet.Node,
Path: taskPath,
},
Input: packet.Arg,
Node: proc.Node,
Writes: nil,
Config: emitConfig(config, metadata),
Triggers: []string{PUSH},
}, nil
}
return PregelTask{ID: taskID, Name: packet.Node, Path: taskPath}, nil
}
// An ill-formed PUSH path nothing to schedule
return nil, nil
}
// PULL branch
if len(taskPath) > 0 && taskPath[0] == PULL {
if len(taskPath) < 2 {
return nil, nil
}
name, ok := taskPath[1].(string)
if !ok {
return nil, nil
}
proc, ok := processes[name]
if !ok || proc.Node == nil {
return nil, nil
}
seen := map[string]interface{}{}
if v, _ := checkpoint.VersionsSeen[name].(map[string]interface{}); v != nil {
for k, vv := range v { // shallow copy
seen[k] = vv
}
}
var triggers []string
for _, ch := range proc.Triggers {
cv, exists := checkpoint.ChannelVersions[ch]
if !exists {
cv = checkpointNullVersion.(int64) // use the provided null version
}
sv, _ := seen[ch].(int64) // default to 0 if not exists or wrong type
if compareVersion(cv, sv) > 0 {
triggers = append(triggers, ch)
}
}
if len(triggers) == 0 {
return nil, nil // not ready
}
sort.Strings(triggers)
input := map[string]interface{}{}
for _, ch := range proc.Triggers {
if v, ok := channels[ch]; ok {
input[ch] = v
}
}
checkpointNS := parentNS
if checkpointNS != "" {
checkpointNS += NS_SEP + name
} else {
checkpointNS = name
}
taskID := taskIDFunc(
checkpointIDBytes,
checkpointNS,
strconv.Itoa(step),
name,
PULL,
// join triggers to guarantee deterministic id
fmt.Sprintf("%v", triggers),
)
if err := checkSumMatch(taskID); err != nil {
return nil, err
}
taskCheckpointNS := checkpointNS + NS_END + taskID
metadata := map[string]interface{}{
"langgraph_step": step,
"langgraph_node": name,
"langgraph_triggers": triggers,
"langgraph_path": taskPath,
"langgraph_checkpoint_ns": taskCheckpointNS,
}
if forExecution {
return PregelExecutableTask{
PregelTask: PregelTask{
ID: taskID,
Name: name,
Path: taskPath,
},
Input: input,
Node: proc.Node,
Writes: nil,
Config: emitConfig(config, metadata),
Triggers: triggers,
}, nil
}
return PregelTask{ID: taskID, Name: name, Path: taskPath}, nil
}
return nil, nil
}
// Private / Helpers
// taskIDFunc deterministically hashes the checkpoint-scoped information that
// must be unique for a task in a given super-step.
func taskIDFunc(checkpointIDBytes []byte, parts ...string) string {
h := sha256.New()
_, _ = h.Write(checkpointIDBytes)
for _, p := range parts {
_, _ = h.Write([]byte(p))
}
return hex.EncodeToString(h.Sum(nil))
}
// taskPathStr is only used so the path element contributes to the hash in a
// deterministic textual form.
func taskPathStr(path interface{}) string {
return fmt.Sprintf("%v", path)
}
// createScratchpad returns an *immutable* copy of the scratchpad that will
// be injected into the task-local Config. We:
//
// 1. start from the previous scratchpad (if any),
// 2. merge in any successful writes from earlier tasks in this super-step,
// 3. copy-on-write so individual tasks never share interior maps.
//
// The logic below is intentionally simple; extend as needed.
func createScratchpad(
current map[string]interface{},
pendingWrites []interface{},
taskID string,
checkpointHash string,
resumeMap map[string]interface{},
) map[string]interface{} {
out := map[string]interface{}{}
for k, v := range current {
out[k] = v
}
if len(pendingWrites) > 0 {
out["pending_writes"] = append([]interface{}{}, pendingWrites...)
}
if checkpointHash != "" {
out["checkpoint_hash"] = checkpointHash
}
if resumeMap != nil {
out["resume_map"] = resumeMap
}
out["task_id"] = taskID
return out
}
func checkpointNullVersion(_ Checkpoint) interface{} {
// Return the zero value for int64 as the null version
return int64(0)
}
func taskID(t interface{}) (string, bool) {
switch v := t.(type) {
case PregelTask:
return v.ID, true
case PregelExecutableTask:
return v.ID, true
default:
return "", false
}
}
func compareVersion(a, b interface{}) int {
switch av := a.(type) {
case int:
bv, _ := b.(int)
return av - bv
case int64:
var bv int64
switch bvVal := b.(type) {
case int64:
bv = bvVal
case int:
bv = int64(bvVal)
default:
bv = 0
}
if av == bv {
return 0
}
if av < bv {
return -1
}
return 1
case string:
bv, _ := b.(string)
if av == bv {
return 0
}
if av < bv {
return -1
}
return 1
// Fallback to reflect.DeepEqual comparison: not perfect but safe.
default:
if reflect.DeepEqual(a, b) {
return 0
}
return 1
}
}
+28
View File
@@ -0,0 +1,28 @@
package pregel
import (
"context"
"time"
"github.com/google/uuid"
)
func EmptyCheckpoint() (*Checkpoint, error) {
uid, err := uuid.NewV6()
if err != nil {
return nil, err
}
return &Checkpoint{
Version: 1,
ID: uid.String(),
Timestamp: time.Now().Format(time.RFC3339),
ChannelValues: map[string]interface{}{},
ChannelVersions: map[string]int64{},
VersionsSeen: map[string]interface{}{},
PendingSends: []Send{},
}, nil
}
type Checkpointer interface {
PutWrites(ctx context.Context, checkpoint Checkpoint, writes []Write) error
}
+138
View File
@@ -0,0 +1,138 @@
package pregel
import (
"context"
"errors"
)
// Pregel is the top-level graph object.
type Pregel struct {
Name string
Nodes map[string]PregelNode
Channels map[string]BaseChannel
LoopCfg RunnableConfig
Checkptr BaseCheckpointSaver
Store BaseStore
Debug bool
}
func (g *Pregel) Stream(
input any,
cfg RunnableConfig,
opts *StreamOptions,
) (<-chan StreamChunk, <-chan error) {
eventCh := make(chan StreamChunk, 16)
errCh := make(chan error, 1)
if opts == nil {
opts = &StreamOptions{}
}
ctx := opts.Context
if ctx == nil {
ctx = context.Background()
}
mode := opts.Mode
if mode == "" {
mode = StreamValues
}
// TODO: opts.Debug
// ensure output channels are set / valid
outChans := opts.OutputChannels
if len(outChans) == 0 {
for k := range g.Channels {
if _, ok := g.Channels[k]; ok {
outChans = append(outChans, k)
}
}
}
if opts.MaxConcurrency > 0 {
cfg.MaxConcurrency = opts.MaxConcurrency
}
if cfg.MaxConcurrency == 0 {
cfg.MaxConcurrency = 4
}
if cfg.RecursionLimit == 0 {
cfg.RecursionLimit = 25
}
if opts.CheckpointDuring != nil {
cfg.Configurable[CONFIG_KEY_CHECKPOINT_DURING] = *opts.CheckpointDuring
}
checkpoint, err := EmptyCheckpoint()
if err != nil {
errCh <- err
return nil, errCh
}
loop := NewLoop(
ctx,
*checkpoint,
g.Nodes,
g.channelsAsConcrete(),
nil, // managed values
cfg,
nil, // g.checkpointer, // may be nil
nil, // g.store,
)
loop.interruptBefore = opts.InterruptBefore
loop.interruptAfter = opts.InterruptAfter
loop.streamCh = eventCh
loop.streamMode = mode
// loop.debug = debug
go func() {
defer close(eventCh)
defer close(errCh)
// Create a runner to execute tasks
runner := NewPregelRunner(loop, nil)
// Use the tick method in a loop instead of Run()
for {
more, err := loop.tick(outChans)
if err != nil {
// Check if this is a GraphInterrupt error
var interrupt GraphInterrupt
if errors.As(err, &interrupt) {
// Handle interrupt gracefully
break
}
// Otherwise, it's a real error
errCh <- err
return
}
runnerOpts := TickOptions{
MaxConcurrency: cfg.MaxConcurrency,
}
if opts.Debug != nil && *opts.Debug {
runnerOpts.OnStepWrite = func(step int, writes []Write) {
// TODO: Handle debugging info
}
}
if err := runner.tick(runnerOpts); err != nil {
errCh <- err
return
}
// No more iterations needed, we're done
if !more {
break
}
}
}()
return eventCh, errCh
}
func (g *Pregel) channelsAsConcrete() map[string]BaseChannel {
out := make(map[string]BaseChannel, len(g.Channels))
for k, v := range g.Channels {
if ch, ok := v.(BaseChannel); ok {
out[k] = ch
}
}
return out
}
+500
View File
@@ -0,0 +1,500 @@
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 dont 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
}
+217
View File
@@ -0,0 +1,217 @@
// runner.go
package pregel
import (
"context"
"errors"
"sync"
"time"
"golang.org/x/sync/errgroup"
)
// PregelRunner is responsible for executing the set of tasks that a
// PregelLoop prepared for the *current super-step*. It runs them with
// respect to retry-policy, max-concurrency, timeouts, cancellation and
// Pregel-specific error semantics (GraphInterrupt / GraphBubbleUp).
type PregelRunner struct {
loop *PregelLoop
nodeFinished func(string) // Optional user-callback
}
// NewPregelRunner links the runner to its parent loop.
func NewPregelRunner(loop *PregelLoop, nodeFinished func(string)) *PregelRunner {
return &PregelRunner{loop: loop, nodeFinished: nodeFinished}
}
// TickOptions mirrors the semantics in the TS/Python implementations.
type TickOptions struct {
Timeout time.Duration // Deadline for the whole super-step
RetryPolicy RetryPolicy // Per-task retry policy
OnStepWrite func(int, []Write) // Hook after *all* writes are committed
MaxConcurrency int // ≤0 ⇒ unlimited
Ctx context.Context // Root ctx (optional)
}
// Tick executes every task whose Writes slice is still empty.
// It returns when *all* tasks have completed (successfully or not) **or**
// when the first non-interrupt error bubbles up.
func (r *PregelRunner) tick(opt TickOptions) error {
// Choose base context
ctx := opt.Ctx
if ctx == nil {
ctx = context.Background()
}
// We cancel siblings on first fatal error
ctx, cancel := context.WithCancel(ctx)
defer cancel()
// Optional global timeout
if opt.Timeout > 0 {
ctx, cancel = context.WithTimeout(ctx, opt.Timeout)
defer cancel()
}
// Gather tasks that still need to run in this super-step
var pending []*PregelExecutableTask
for _, t := range r.loop.tasks {
if len(t.Writes) == 0 {
pending = append(pending, t)
}
}
if len(pending) == 0 {
return nil // nothing to do
}
// errgroup manages goroutines and collects the first returned error
g, gctx := errgroup.WithContext(ctx)
maxConc := opt.MaxConcurrency
if maxConc <= 0 {
maxConc = len(pending)
}
sem := make(chan struct{}, maxConc)
var mu sync.Mutex
for _, task := range pending {
task := task // capture
sem <- struct{}{}
g.Go(func() error {
defer func() { <-sem }()
err := runWithRetry(gctx, opt.RetryPolicy, func(c context.Context) error {
// NOTE: Node.Run must honour ctx for cancellation / deadlines.
writes, runErr := task.Node.Invoke(c, task.Input, task.Config, r.loop)
if runErr == nil {
task.Writes = writes
}
return runErr
})
r.commit(task, err)
switch {
case err == nil:
return nil
case errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded):
return err // propagate
}
var gi GraphInterrupt
if errors.As(err, &gi) {
mu.Lock()
defer mu.Unlock()
// kep track so that loop can raise combined interrupt later
return gi
}
cancel()
return err
})
}
// Wait for all goroutines (or first fatal error)
if err := g.Wait(); err != nil {
return err
}
// Step-level callback after *all* commits
if opt.OnStepWrite != nil {
var all []Write
for _, t := range r.loop.tasks {
all = append(all, t.Writes...)
}
opt.OnStepWrite(r.loop.step, all)
}
return nil
}
// commit replicates the Python/TS commit semantics.
func (r *PregelRunner) commit(task *PregelExecutableTask, execErr error) {
// On success ensure at least one NO_WRITES marker so loop knows it's done.
if execErr == nil && len(task.Writes) == 0 {
task.Writes = append(task.Writes, Write{Channel: NO_WRITES})
}
// Persist writes (or error) through the loops thread-safe adaptor.
switch {
case execErr == nil:
r.loop.putWrites(task.ID, task.Writes)
case errors.As(execErr, new(GraphInterrupt)):
// Interrupt carries its own writes payload
r.loop.putWrites(task.ID, task.Writes)
default:
// Record generic error
r.loop.putWrites(task.ID, []Write{{Channel: ERROR, Value: execErr}})
}
// optional callback
if execErr == nil && r.nodeFinished != nil {
r.nodeFinished(task.Name)
}
}
// runWithRetry is a minimal exponential-back-off retry helper.
func runWithRetry(ctx context.Context, pol RetryPolicy, fn func(context.Context) error) error {
if pol.MaxAttempts <= 0 {
pol.MaxAttempts = 1
}
// if pol.Backoff == nil {
// // default: exponential capped at 2 s
// pol.Backoff = func(attempt int) time.Duration {
// d := time.Duration(math.Pow(2, float64(attempt))) * 50 * time.Millisecond
// if d > 2*time.Second {
// d = 2 * time.Second
// }
// return d
// }
// }
// if pol.Retryable == nil {
// pol.Retryable = func(error) bool { return true }
// }
var err error
for attempt := 0; attempt < pol.MaxAttempts; attempt++ {
if err = fn(ctx); err == nil { // || !pol.Retryable(err) {
return err
}
// // wait before next try
// wait := pol.Backoff(attempt)
// select {
// case <-time.After(wait):
// case <-ctx.Done():
// return ctx.Err()
// }
}
return err
}
/* --------------------------------------------------------------------------
Missing symbols? If your project does not yet declare the following items
just add minimal stubs like the ones below (remove before wiring in
real implementations to avoid duplicates).
// Constants that mark write types
const (
ERROR = "error"
NO_WRITES = "no_writes"
)
// GraphInterrupt / BubbleUp marker errors
type GraphInterrupt struct{ Msg string }
func (g GraphInterrupt) Error() string { return g.Msg }
type GraphBubbleUp struct{ error }
// Minimal Write + RetryPolicy
type Write struct{ Channel string; Value any }
type RetryPolicy struct {
MaxAttempts int
Backoff func(attempt int) time.Duration
Retryable func(error) bool
}
// PregelExecutableTask, PregelLoop, etc. should exist elsewhere.
// -------------------------------------------------------------------------- */
+292
View File
@@ -0,0 +1,292 @@
package pregel
import (
"context"
"sync"
)
// Constants for task types and reserved keys
const (
// Task types
PUSH = "__pregel_push" // Denotes push-style tasks, ie. those created by Send objects
PULL = "__pregel_pull" // Denotes pull-style tasks, ie. those triggered by edges
// Reserved write keys
INPUT = "__input__" // For values passed as input to the graph
INTERRUPT = "__interrupt__" // For dynamic interrupts raised by nodes
RESUME = "__resume__" // For values passed to resume a node after an interrupt
ERROR = "__error__" // For errors raised by nodes
NO_WRITES = "__no_writes__" // Marker to signal node didn't write anything
SCHEDULED = "__scheduled__" // Marker to signal node was scheduled (in distributed mode)
TASKS = "__pregel_tasks" // For Send objects returned by nodes/edges
RETURN = "__return__" // For writes of a task where we simply record the return value
// Public constants
START = "__start__" // The first (maybe virtual) node in graph-style Pregel
END = "__end__" // The last (maybe virtual) node in graph-style Pregel
SELF = "__self__" // The implicit branch that handles each node's Control values
PREVIOUS = "__previous__" // Previous value
// Other constants
NS_SEP = "|" // For checkpoint_ns, separates each level (ie. graph|subgraph|subsubgraph)
NS_END = ":" // For checkpoint_ns, for each level, separates the namespace from the task_id
NULL_TASK_ID = "00000000-0000-0000-0000-000000000000" // The task_id to use for writes that are not associated with a task
CONF = "configurable" // Key for the configurable dict in RunnableConfig
// Reserved config.configurable keys
CONFIG_KEY_SEND = "__pregel_send" // Holds the `write` function that accepts writes to state/edges/reserved keys
CONFIG_KEY_READ = "__pregel_read" // Holds the `read` function that returns a copy of the current state
CONFIG_KEY_CALL = "__pregel_call" // Holds the `call` function that accepts a node/func, args and returns a future
CONFIG_KEY_CHECKPOINTER = "__pregel_checkpointer" // Holds a `BaseCheckpointSaver` passed from parent graph to child graphs
CONFIG_KEY_STREAM = "__pregel_stream" // Holds a `StreamProtocol` passed from parent graph to child graphs
CONFIG_KEY_STREAM_WRITER = "__pregel_stream_writer" // Holds a `StreamWriter` for stream_mode=custom
CONFIG_KEY_STORE = "__pregel_store" // Holds a `BaseStore` made available to managed values
CONFIG_KEY_CACHE = "__pregel_cache" // Holds a `BaseCache` made available to subgraphs
CONFIG_KEY_RESUMING = "__pregel_resuming" // Holds a boolean indicating if subgraphs should resume from a previous checkpoint
CONFIG_KEY_TASK_ID = "__pregel_task_id" // Holds the task ID for the current task
CONFIG_KEY_DEDUPE_TASKS = "__pregel_dedupe_tasks" // Holds a boolean indicating if tasks should be deduplicated (for distributed mode)
CONFIG_KEY_ENSURE_LATEST = "__pregel_ensure_latest" // Holds a boolean indicating whether to assert the requested checkpoint is the latest
CONFIG_KEY_DELEGATE = "__pregel_delegate" // Holds a boolean indicating whether to delegate subgraphs (for distributed mode)
CONFIG_KEY_THREAD_ID = "thread_id" // Holds the thread ID for the current invocation
CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map" // Holds a mapping of checkpoint_ns -> checkpoint_id for parent graphs
CONFIG_KEY_CHECKPOINT_ID = "checkpoint_id" // Holds the current checkpoint_id, if any
CONFIG_KEY_CHECKPOINT_NS = "checkpoint_ns" // Holds the current checkpoint_ns, "" for root graph
CONFIG_KEY_NODE_FINISHED = "__pregel_node_finished" // Holds a callback to be called when a node is finished
CONFIG_KEY_SCRATCHPAD = "__pregel_scratchpad" // Holds a mutable dict for temporary storage scoped to the current task
CONFIG_KEY_PREVIOUS = "__pregel_previous" // Holds the previous return value from a stateful Pregel graph
CONFIG_KEY_RUNNER_SUBMIT = "__pregel_runner_submit" // Holds a function that receives tasks from runner, executes them and returns results
CONFIG_KEY_CHECKPOINT_DURING = "__pregel_checkpoint_during" // Holds a boolean indicating whether to checkpoint during the run (or only at the end)
CONFIG_KEY_RESUME_MAP = "__pregel_resume_map" // Holds a mapping of task ns -> resume value for resuming tasks
TAG_HIDDEN = "langsmith:hidden" // Holds a boolean indicating whether to hide a node/edge from certain tracing/streaming environments.
)
// StreamMode defines how the graph streams its output
type StreamMode string
// WRITES_IDX_MAP maps special channel names to negative indices
// to avoid conflicts with regular writes.
var WRITES_IDX_MAP = map[string]int{
ERROR: -1,
SCHEDULED: -2,
INTERRUPT: -3,
RESUME: -4,
}
// TS
// export type PendingWriteValue = unknown;
// export type PendingWrite<Channel = string> = [Channel, PendingWriteValue];
// export type CheckpointPendingWrite<TaskId = string> = [
// TaskId,
// ...PendingWrite<string>
// ];
// Py
// PendingWrite = Tuple[str, str, Any]
type PendingWrite struct {
TaskID string
Channel string
Value interface{}
}
const (
// StreamValues emits all values in the state after each step
StreamValues StreamMode = "values"
// StreamUpdates emits only the node or task names and updates
StreamUpdates StreamMode = "updates"
// StreamCustom emits custom data from inside nodes or tasks
StreamCustom StreamMode = "custom"
// StreamMessages emits LLM messages token-by-token
StreamMessages StreamMode = "messages"
// StreamDebug emits debug events with as much information as possible
StreamDebug StreamMode = "debug"
)
// PregelTask represents a task in the Pregel system
type PregelTask struct {
ID string
Name string
Path []interface{}
Error error
Interrupts []interface{}
Result interface{}
}
// PregelExecutableTask represents a task that can be executed
type PregelExecutableTask struct {
PregelTask
Input interface{}
Node NodeRunnable
Writes []Write
Config RunnableConfig
Triggers []string
RetryPolicy interface{}
CacheKey *CacheKey
Writers map[string]interface{} // Flat writers
Subgraphs map[string]interface{} // Subgraphs
}
// StreamChunk is what the consumer receives.
type StreamChunk struct {
Namespace []string // sub-graph path (reserved for future use)
Mode StreamMode
Payload any
}
type StreamOptions struct {
Mode StreamMode
OutputChannels []string // defaults to all non-context channels
InterruptBefore []string // interrupt gate (before)
InterruptAfter []string // interrupt gate (after)
MaxConcurrency int // overrides config[ "max_concurrency" ]
CheckpointDuring *bool // nil → inherit config
Debug *bool // nil → inherit graph.debug
Context context.Context // optional, default = context.Background()
}
// CacheKey represents a key for caching
type CacheKey struct {
Namespace []string
Key string
TTL int64
}
type PregelNode struct {
Node NodeRunnable
Triggers []string
Metadata map[string]interface{}
Tags []string
CachePolicy interface{} // CachePolicy equivalent
RetryPolicy interface{} // RetryPolicy equivalent
FlatWriters map[string]interface{}
Subgraphs map[string]interface{}
Retry RetryPolicy
}
type NodeRunnable interface {
Invoke(ctx context.Context, input any, cfg RunnableConfig, loop LoopCallback) ([]Write, error)
}
type Write struct {
Channel string
Value any
}
// Checkpoint represents a checkpoint in the Pregel system
type Checkpoint struct {
ID string
ChannelValues map[string]interface{} `json:"channel_values,omitempty"`
ChannelVersions map[string]int64 `json:"channel_versions,omitempty"`
VersionsSeen map[string]interface{} `json:"versions_seen,omitempty"`
PendingSends []Send `json:"pending_sends,omitempty"`
Version int `json:"version,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
}
// NewCheckpoint creates a new Checkpoint with all fields properly initialized
func NewCheckpoint() Checkpoint {
return Checkpoint{
ChannelValues: make(map[string]interface{}),
ChannelVersions: make(map[string]int64),
VersionsSeen: make(map[string]interface{}),
PendingSends: make([]Send, 0),
}
}
// Send represents a message to be sent to a node
type Send struct {
Node string
Arg interface{}
}
// Call represents a function call
type Call struct {
Func interface{} // Function to call
Input []interface{} // Arguments
Callbacks interface{} // Callbacks
CachePolicy interface{} // CachePolicy
Retry interface{} // RetryPolicy
}
// PregelTaskWrites represents writes from a task
type PregelTaskWrites struct {
Path []interface{}
Name string
Writes []interface{} // Deque in Python
Triggers []string
}
// ---------------------------------------------------------------------------
// Interfaces from previous snippets (slim versions here)
// ---------------------------------------------------------------------------
type RetryPolicy struct {
MaxAttempts int
BackoffMs int
}
type BaseChannel interface {
Set(v any)
Get() any
}
type simpleChan struct{ val atomicValue }
type atomicValue struct {
mu sync.RWMutex
v any
}
func (a *atomicValue) Store(v any) {
a.mu.Lock()
a.v = v
a.mu.Unlock()
}
func (a *atomicValue) Load() (v any) { a.mu.RLock(); v = a.v; a.mu.RUnlock(); return }
func (c *simpleChan) Set(v any) { c.val.Store(v) }
func (c *simpleChan) Get() any { return c.val.Load() }
// Managed values -------------------------------------------------------------
type WritableManagedValue interface {
Update([]any) error
}
type ManagedValueMapping map[string]WritableManagedValue
type SendPacket struct {
Node string
Arg any
}
type BaseCheckpointSaver interface {
Put(cfg RunnableConfig, cp Checkpoint, md map[string]any, newVers map[string]int) error
GetTuple(cfg RunnableConfig) (*Checkpoint, error)
}
// Stores ---------------------------------------------------------------------
type BaseStore interface{}
// Loop callback interface passed to Nodes for localWrite / localRead
type LoopCallback interface {
Send(taskID string, writes []Write)
Read(selectKeys []string) map[string]any
AcceptPush(originTask PregelExecutableTask, writeIdx int, call *Call) (*PregelExecutableTask, error)
}
// RunnableConfig represents configuration for a Runnable.
// Fields are optional
type RunnableConfig struct {
Tags []string `json:"tags,omitempty"` // Tags for this call and sub-calls.
Metadata map[string]interface{} `json:"metadata,omitempty"` // Metadata for this call and sub-calls.
Callbacks interface{} `json:"callbacks,omitempty"` // Callbacks for this call and sub-calls.
RunName *string `json:"run_name,omitempty"` // Name for the tracer run for this call.
MaxConcurrency int `json:"max_concurrency,omitempty"` // Max number of parallel calls.
RecursionLimit int `json:"recursion_limit,omitempty"` // Max recursion depth.
Configurable map[string]interface{} `json:"configurable,omitempty"` // Runtime values for configurable attributes.
RunID *string `json:"run_id,omitempty"` // Unique identifier for the tracer run (UUID as string).
}