目录 · 第 21 / 28 章
EinoPart IV · 支撑 ADK 的编排引擎

21流机制与状态管理

Copy 的 copy-on-read 链表、MergeNamedStreamReaders、装箱、ProcessState 按类型逐级查找。

schema/stream.go:261schema/stream.go:837schema/stream.go:912compose/state.go:165

两块压轴基石:流与状态

Part IV 走到这里,执行内核(循环 + channel)和三个构建器(Graph/Chain/Workflow)都拆完了。但还有两块基石没碰,而它们恰恰是 ADK 里”流式回复”和”跨轮记忆”的底层支撑:stream(流)state(状态)。这一章我们把它们一起讲清楚——因为它们体现了同一种设计取向:用最省的机制,兑现最强的语义。

flowchart TB
  subgraph SRC["五种来源"]
    ST["stream · Pipe 管道"]
    AR["arrayReader · 数组伪装"]
    MSR["multiStreamReader · 合并"]
    SRW["…WithConvert · 类型转换"]
    CSR["childStreamReader · Copy 子流"]
  end
  SRC --> SR["StreamReader[T]<br/>tagged union · 按 typ 分派"]
  SR --> RECV["Recv() 读一次即干涸<br/>close 恰好一次"]

StreamReader:五种来源,对外同一个类型

流不是数据,是”一次性的取水管”

先纠正一个直觉。Eino 的 StreamReader[T](schema/stream.go:168)不是一个装着数据的切片,而是一根读一次就干涸的管子。文档写得很直白:read-once,只能一个 goroutine 调 Recv,而且必须 close 恰好一次。

// StreamReader is the consumer side of an Eino stream.
//
// A StreamReader is read-once: only one goroutine should call Recv, and the
// reader must be closed exactly once (whether the loop finishes normally or
// exits early via break or return).
//
// Typical usage:
//
// defer sr.Close() // always close, even after io.EOF
// for {
// chunk, err := sr.Recv()
// if errors.Is(err, io.EOF) {
// break
// }
// if err != nil {
// return err
// }
// process(chunk)
// }
//
// To fan-out a single stream to N independent consumers, call [StreamReader.Copy]
// before any Recv; the original reader becomes unusable after the call.
//
// StreamReaders are created by [Pipe], [StreamReaderFromArray],
// [MergeStreamReaders], [MergeNamedStreamReaders], and [StreamReaderWithConvert].
type StreamReader[T any] struct {
typ readerType
st *stream[T]
ar *arrayReader[T]
msr *multiStreamReader[T]
srw *streamReaderWithConvert[T]
csr *childStreamReader[T]
}

它的入口是 Pipe(schema/stream.go:99),给你一对 reader/writer;writer 那头 Send,reader 这头 Recvio.EOF 为止。但真正精妙的是 StreamReader 的内部结构——它是一个带标签的联合类型(tagged union):

// Pipe creates a new stream with the given capacity that represented with StreamWriter and StreamReader.
// The capacity is the maximum number of items that can be buffered in the stream.
// e.g.
//
// sr, sw := schema.Pipe[string](3)
// go func() { // send data
// defer sw.Close()
// for i := 0; i < 10; i++ {
// sw.Send(i, nil)
// }
// }
//
// defer sr.Close()
// for {
// chunk, err := sr.Recv()
// if errors.Is(err, io.EOF) {
// break
// }
// if err != nil {
// panic(err)
// }
// fmt.Println(chunk)
// }
func Pipe[T any](cap int) (*StreamReader[T], *StreamWriter[T]) {
stm := newStream[T](cap)
return stm.asReader(), &StreamWriter[T]{stm: stm}
}
type StreamReader[T any] struct {
typ readerType // 我到底是哪一种 reader?
st *stream[T] // 真·管道(Pipe 来的)
ar *arrayReader[T] // 数组伪装成的流
msr *multiStreamReader[T] // 合并出来的多路流
srw *streamReaderWithConvert[T] // 带类型转换的流
csr *childStreamReader[T] // Copy 出来的子流
}

Recv 就是一个按 typ 分发的 switch。为什么要这么设计?因为 Eino 里”流”有五种来源(管道、数组、合并、转换、复制),但对使用者必须是同一个类型。用一个 union + 一个 tag,既避免了接口装箱的开销,又让五种流对外完全一致。这是整章的第一个设计信号:统一对外类型,内部按需分派。

Copy:读时复制的单链表

流是读一次就没的,那”一个大模型的输出,既要给用户看、又要喂给下一个节点”怎么办?答案是 Copy(n)(schema/stream.go:261)。

// Copy creates n independent StreamReaders that each receive every element of
// the original stream. The original StreamReader becomes unusable after Copy.
//
// Use Copy when two or more pipeline branches need the same stream —
// for example, when a stream must be fed to both a callback handler and the
// next node in a graph:
//
// copies := sr.Copy(2)
// sr1, sr2 := copies[0], copies[1]
// defer sr1.Close()
// defer sr2.Close()
//
// // sr1 and sr2 independently read the same elements
//
// n must be at least 1. If n < 2, the original reader is returned unchanged.
func (sr *StreamReader[T]) Copy(n int) []*StreamReader[T] {
if n < 2 {
return []*StreamReader[T]{sr}
}
if sr.typ == readerTypeArray {
ret := make([]*StreamReader[T], n)
for i, ar := range sr.ar.copy(n) {
ret[i] = &StreamReader[T]{typ: readerTypeArray, ar: ar}
}
return ret
}
return copyStreamReaders[T](sr, n)
}

难点在于:上游流有多长你事先不知道,而 n 个消费者各读各的、进度不一。Eino 的解法非常漂亮——一个读时复制(copy-on-read)的单向链表。核心类型是 cpStreamElement(schema/stream.go:784):

type cpStreamElement[T any] struct {
once sync.Once // 保证这一格只从源头读一次
next *cpStreamElement[T] // 指向下一格(读到才创建)
item streamItem[T] // 缓存下来的那一块数据
}

Copy 造出一个 parentStreamReader(schema/stream.go:823)和 n 个 childStreamReader(schema/stream.go:883)。每个 child 记着自己走到链表的哪一格。当某个 child 要 Recv,它落到 peek(schema/stream.go:837),核心就一段:

type parentStreamReader[T any] struct {
// sr is the original StreamReader.
sr *StreamReader[T]
// subStreamList maps each child's index to its latest read chunk.
// Each value comes from a hidden linked list of cpStreamElement.
subStreamList []*cpStreamElement[T]
// closedNum is the count of closed children.
closedNum uint32
}
type childStreamReader[T any] struct {
parent *parentStreamReader[T]
index int
}
elem.once.Do(func() {
t, err = p.sr.Recv() // 只有第一个到这格的 child 真的去源头读
elem.item = streamItem[T]{chunk: t, err: err}
if err != io.EOF {
elem.next = &cpStreamElement[T]{} // 顺手把下一格铺好
p.subStreamList[idx] = elem.next
}
})
// 后到的 child 直接读缓存好的 elem.item,不碰源头

sync.Once 是这里的灵魂:**同一格,无论多少个 child 经过,只有第一个真去源头 Recv 一次,其余的读缓存。**跑得快的 child 沿着链表往前铺路,跑得慢的沿着同一条链追,谁都不会重复消费源头,也不用等齐。等所有 child 都 close,源头才被 close(close 里用 closedNum 计数,schema/stream.go:868)。

func (p *parentStreamReader[T]) close(idx int) {
if p.subStreamList[idx] == nil {
return // avoid close multiple times
}
p.subStreamList[idx] = nil
curClosedNum := atomic.AddUint32(&p.closedNum, 1)
allClosed := int(curClosedNum) == len(p.subStreamList)
if allClosed {
p.sr.Close()
}
}

🔑 本章的设计钥匙

流与状态,共享同一条设计哲学:用最小的机制承载最强的语义,并把成本推迟到真正需要的那一刻。 流的 Copy 不预读、不缓冲整条流,而是用 sync.Once 守护的单链表做惰性的读时复制——快的消费者铺路、慢的追赶,源头每格只读一次;合并用 select 按到达顺序交织而非轮询,顺其自然。状态则不做全局字典,而是把每层图的 state 串成一条 parent 链,ProcessState[S]类型断言逐级上溯去找匹配 S 的那一层。两者都拒绝”预先算好、集中保管”,都选择”用时才求值、按需才分派”——这正是支撑 ADK 流式与记忆而不拖垮性能的关键。

Merge:按到达顺序交织

Copy 是一变多,Merge 是多变一。MergeStreamReaders(schema/stream.go:912)把 N 路流合成一路。注意它的语义:按到达顺序交织(arrival order),不是轮询,也不保证顺序——哪路先来数据就先吐哪路。只有所有源都 EOF,合并流才 EOF。

// MergeStreamReaders fans in multiple StreamReaders into a single StreamReader.
// Elements from all source streams are interleaved in arrival order (non-deterministic).
// The merged reader reaches EOF only after every source stream has been exhausted.
//
// Callers must still close the merged reader; it propagates the close signal
// to all underlying sources.
//
// Use [MergeNamedStreamReaders] instead when you need to know which source
// stream ended first (it emits a [SourceEOF] per-source EOF rather than
// silently discarding them).
//
// Returns nil if srs is empty.
func MergeStreamReaders[T any](srs []*StreamReader[T]) *StreamReader[T] {
if len(srs) < 1 {
return nil
}
if len(srs) < 2 {
return srs[0]
}
var arr []T
var ss []*stream[T]
for _, sr := range srs {
switch sr.typ {
case readerTypeStream:
ss = append(ss, sr.st)
case readerTypeArray:
arr = append(arr, sr.ar.arr[sr.ar.index:]...)
case readerTypeMultiStream:
ss = append(ss, sr.msr.nonClosedStreams()...)
case readerTypeWithConvert:
ss = append(ss, sr.srw.toStream())
case readerTypeChild:
ss = append(ss, sr.csr.toStream())
default:
panic("impossible")
}
}
if len(ss) == 0 {
return &StreamReader[T]{
typ: readerTypeArray,
ar: &arrayReader[T]{
arr: arr,
index: 0,
},
// … 省略 13 行;完整声明 L900–960,点击上方「浏览完整文件」

实现上是 multiStreamReader,它的 recv(schema/stream.go:538)分两条路:源少时(≤5)走一组硬编码的 select(schema/select.go:21),源多时(>5)才动用 reflect.Select。这个 5 的阈值就是 maxSelectNum(schema/select.go:19):

func (msr *multiStreamReader[T]) recv() (T, error) {
for len(msr.nonClosed) > 0 {
var chosen int
var ok bool
if len(msr.nonClosed) > maxSelectNum {
var recv reflect.Value
chosen, recv, ok = reflect.Select(msr.itemsCases)
if ok {
item := recv.Interface().(streamItem[T])
return item.chunk, item.err
}
msr.itemsCases[chosen].Chan = reflect.Value{}
} else {
var item *streamItem[T]
chosen, item, ok = receiveN(msr.nonClosed, msr.sts)
if ok {
return item.chunk, item.err
}
}
// delete the closed stream
for i := range msr.nonClosed {
if msr.nonClosed[i] == chosen {
msr.nonClosed = append(msr.nonClosed[:i], msr.nonClosed[i+1:]...)
break
}
}
if len(msr.sourceReaderNames) > 0 {
var t T
return t, &SourceEOF{msr.sourceReaderNames[chosen]}
}
}
var t T
return t, io.EOF
}
func receiveN[T any](chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) {
return []func(chosenList []int, ss []*stream[T]) (index int, item *streamItem[T], ok bool){
nil,
func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) {
item, ok := <-ss[chosenList[0]].items
return chosenList[0], &item, ok
},
func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) {
select {
case item, ok := <-ss[chosenList[0]].items:
return chosenList[0], &item, ok
case item, ok := <-ss[chosenList[1]].items:
return chosenList[1], &item, ok
}
},
func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) {
select {
case item, ok := <-ss[chosenList[0]].items:
return chosenList[0], &item, ok
case item, ok := <-ss[chosenList[1]].items:
return chosenList[1], &item, ok
case item, ok := <-ss[chosenList[2]].items:
return chosenList[2], &item, ok
}
},
func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) {
select {
case item, ok := <-ss[chosenList[0]].items:
return chosenList[0], &item, ok
case item, ok := <-ss[chosenList[1]].items:
return chosenList[1], &item, ok
case item, ok := <-ss[chosenList[2]].items:
return chosenList[2], &item, ok
case item, ok := <-ss[chosenList[3]].items:
return chosenList[3], &item, ok
}
},
func(chosenList []int, ss []*stream[T]) (int, *streamItem[T], bool) {
select {
case item, ok := <-ss[chosenList[0]].items:
return chosenList[0], &item, ok
case item, ok := <-ss[chosenList[1]].items:
return chosenList[1], &item, ok
case item, ok := <-ss[chosenList[2]].items:
return chosenList[2], &item, ok
case item, ok := <-ss[chosenList[3]].items:
return chosenList[3], &item, ok
case item, ok := <-ss[chosenList[4]].items:
// … 省略 5 行;完整声明 L21–73,点击上方「浏览完整文件」
const maxSelectNum = 5
const maxSelectNum = 5

为什么要分两条路?reflect.Select 灵活但慢;而绝大多数合并场景源都不多,于是 Eino 为 2~5 路手写了展开的 select,把反射开销省掉。这又是一次”常见路径手工优化、罕见路径才降级到反射”的取舍——和上一章类型检查里”编译期能拦就编译期拦”如出一辙。

还有一个变体 MergeNamedStreamReaders(schema/stream.go:990):当某一路 EOF 时,它吐一个 SourceEOF 告诉你是哪一路结束了,而不是默默咽下。多智能体里”等 A 说完再处理 B”就靠它。底层都汇进 InternalMergeNamedStreamReaders(schema/stream.go:1010)。

// MergeNamedStreamReaders merges multiple named StreamReaders into one.
// Unlike [MergeStreamReaders], when a source stream reaches EOF the merged
// reader emits a [SourceEOF] error (instead of silently continuing) so you can
// detect exactly which source finished. Use [GetSourceName] to retrieve the
// name from a SourceEOF error. The merged reader itself signals io.EOF only
// after all named sources are exhausted.
//
// This is useful when downstream logic must react differently to each source
// completing — for example, draining one agent's output before proceeding:
//
// namedStreams := map[string]*schema.StreamReader[string]{
// "agent_a": srA,
// "agent_b": srB,
// }
// merged := schema.MergeNamedStreamReaders(namedStreams)
// defer merged.Close()
// for {
// chunk, err := merged.Recv()
// if errors.Is(err, io.EOF) { break }
// if name, ok := schema.GetSourceName(err); ok {
// fmt.Printf("%s finished\n", name)
// continue
// }
// if err != nil { return err }
// process(chunk)
// }
//
// Returns nil if srs is empty.
func MergeNamedStreamReaders[T any](srs map[string]*StreamReader[T]) *StreamReader[T] {
if len(srs) < 1 {
return nil
}
ss := make([]*StreamReader[T], len(srs))
names := make([]string, len(srs))
i := 0
for name, sr := range srs {
ss[i] = sr
names[i] = name
i++
}
return InternalMergeNamedStreamReaders(ss, names)
}
// InternalMergeNamedStreamReaders merges multiple readers with their names
// into a single multi-stream reader.
func InternalMergeNamedStreamReaders[T any](srs []*StreamReader[T], names []string) *StreamReader[T] {
ss := make([]*stream[T], len(srs))
for i, sr := range srs {
ss[i] = sr.toStream()
}
msr := newMultiStreamReader(ss)
msr.sourceReaderNames = names
return &StreamReader[T]{
typ: readerTypeMultiStream,
msr: msr,
}
}

装箱与拆箱:流与非流的边界

节点有的吐流、有的吐单值,编排引擎怎么抹平这个差异?靠一对互逆操作。

装箱:把一个已经算好的值伪装成流,用 StreamReaderFromArray(schema/stream.go:461)——它其实就是塞一个 arrayReader,Recv 时逐个吐出、最后 io.EOF。一个非流节点的输出,就这样被”装箱”成下游期待的流。

// StreamReaderFromArray creates a StreamReader from a given slice of elements.
// It takes an array of type T and returns a pointer to a StreamReader[T].
// This allows for streaming the elements of the array in a controlled manner.
// eg.
//
// sr := schema.StreamReaderFromArray([]int{1, 2, 3})
// defer sr.Close()
//
// for {
// chunk, err := sr.Recv()
// if errors.Is(err, io.EOF) {
// break
// }
// if err != nil {
// panic(err)
// }
// fmt.Println(chunk)
// }
func StreamReaderFromArray[T any](arr []T) *StreamReader[T] {
return &StreamReader[T]{ar: &arrayReader[T]{arr: arr}, typ: readerTypeArray}
}

拆箱:反过来,把一个流收敛成单值,用 concatStreamReader(compose/stream_concat.go:50)——它把流里所有 chunk 收齐,交给 internal.ConcatItems(internal/concat.go:91)按类型拼接(map 合并、slice 拼接、或用你注册的 concat 函数)。大模型流式吐出的一堆 Message 分片,就是这样被拼回一条完整消息的。

func concatStreamReader[T any](sr *schema.StreamReader[T]) (T, error) {
defer sr.Close()
var items []T
for {
chunk, err := sr.Recv()
if err != nil {
if err == io.EOF {
break
}
if _, ok := schema.GetSourceName(err); ok {
continue
}
var t T
return t, newStreamReadError(err)
}
items = append(items, chunk)
}
if len(items) == 0 {
var t T
return t, emptyStreamConcatErr
}
if len(items) == 1 {
return items[0], nil
}
res, err := internal.ConcatItems(items)
if err != nil {
var t T
return t, err
}
return res, nil
}
// ConcatItems the caller should ensure len(items) > 1
func ConcatItems[T any](items []T) (T, error) {
typ := generic.TypeOf[T]()
v := reflect.ValueOf(items)
var cv reflect.Value
var err error
// handle map kind
if typ.Kind() == reflect.Map {
cv, err = concatMaps(v)
} else {
cv, err = ConcatSliceValue(v)
}
if err != nil {
var t T
return t, err
}
return cv.Interface().(T), nil
}

装箱/拆箱是 Eino 处理”流与非流混排”的统一手法:引擎永远可以把任意节点的输出规整成下游想要的形态,使用者因此几乎感觉不到”这个节点到底是不是流式的”。

State:按类型逐级上溯的一条链

最后是状态。ADK 的记忆、跨节点共享数据,底层都落在 compose 的 state 上。

状态怎么存?图在运行时,如果注册了 WithGenLocalState(compose/generic_graph.go:37),就在 runCtx 里造一个 internalState 塞进 context(compose/graph.go:842)。关键在这个结构(compose/state.go:34)带一个 parent 指针:

// WithGenLocalState registers a function to generate per-run local state
// that can be shared across nodes in the graph.
func WithGenLocalState[S any](gls GenLocalState[S]) NewGraphOption {
return func(ngo *newGraphOptions) {
ngo.withState = func(ctx context.Context) any {
return gls(ctx)
}
ngo.stateType = generic.TypeOf[S]()
}
}
func (g *graph) compile(ctx context.Context, opt *graphCompileOptions) (*composableRunnable, error) {
if g.buildError != nil {
return nil, g.buildError
}
// get run type
runType := runTypePregel
cb := pregelChannelBuilder
if isChain(g.cmp) || isWorkflow(g.cmp) {
if opt != nil && opt.nodeTriggerMode != "" {
return nil, errors.New(fmt.Sprintf("%s doesn't support node trigger mode option", g.cmp))
}
}
if (opt != nil && opt.nodeTriggerMode == AllPredecessor) || isWorkflow(g.cmp) {
runType = runTypeDAG
cb = dagChannelBuilder
}
// get eager type
eager := false
if isWorkflow(g.cmp) || runType == runTypeDAG {
eager = true
}
if opt != nil && opt.eagerDisabled {
eager = false
}
if len(g.startNodes) == 0 {
return nil, errors.New("start node not set")
}
if len(g.endNodes) == 0 {
return nil, errors.New("end node not set")
}
// toValidateMap isn't empty means there are nodes that cannot infer type
for _, v := range g.toValidateMap {
if len(v) > 0 {
return nil, fmt.Errorf("some node's input or output types cannot be inferred: %v", g.toValidateMap)
}
}
for key := range g.fieldMappingRecords {
// not allowed to map multiple fields to the same field
toMap := make(map[string]bool)
for _, mapping := range g.fieldMappingRecords[key] {
if _, ok := toMap[mapping.to]; ok {
return nil, fmt.Errorf("duplicate mapping target field: %s of node[%s]", mapping.to, key)
}
// … 省略 171 行;完整声明 L674–892,点击上方「浏览完整文件」
type internalState struct {
state any
mu sync.Mutex
parent *internalState // 指向外层图的 state
}

于是当图嵌套图(比如一个 ADK agent 内部又是一张图),state 就串成一条从内到外的链。你用 ProcessState[S](compose/state.go:165)读状态时,它调 getState[S](compose/state.go:175)沿着这条链逐级做类型断言:

// ProcessState processes the state from the context in a concurrency-safe way.
// This is the recommended way to access and modify state in custom nodes.
// The provided function handler will be executed with exclusive access to the state (protected by mutex).
//
// State Lookup Behavior:
// - If the requested state type exists in the current graph, it will be returned
// - If not found in current graph, ProcessState will search in parent graph states (for nested graphs)
// - This enables nested graphs to access state from their parent graphs
// - Follows lexical scoping: inner state of the same type shadows outer state
//
// Concurrency Safety:
// - ProcessState automatically locks the mutex of the state being accessed (current or parent level)
// - Each state level has its own mutex, allowing concurrent access to different levels
// - The lock is held for the entire duration of the handler function
//
// Note: This method will report an error if the state type doesn't match or state is not found in the context chain.
//
// Example - Basic usage in a single graph:
//
// lambdaFunc := func(ctx context.Context, in string, opts ...any) (string, error) {
// err := compose.ProcessState[*MyState](ctx, func(ctx context.Context, state *MyState) error {
// // Safely modify state
// state.Count++
// return nil
// })
// if err != nil {
// return "", err
// }
// return in, nil
// }
//
// Example - Nested graph accessing parent state:
//
// // In an inner graph node
// innerNode := func(ctx context.Context, input string) (string, error) {
// // Access parent graph's state
// err := compose.ProcessState[*OuterState](ctx, func(ctx context.Context, s *OuterState) error {
// s.Counter++ // Safely modify parent state
// return nil
// })
// if err != nil {
// return "", err
// }
//
// // Also access inner graph's own state
// err = compose.ProcessState[*InnerState](ctx, func(ctx context.Context, s *InnerState) error {
// s.Data = "processed"
// return nil
// … 省略 12 行;完整声明 L114–173,点击上方「浏览完整文件」
for interState != nil {
if cState, ok := interState.state.(S); ok {
return cState, &interState.mu, nil // 找到类型匹配的那一层
}
interState = interState.parent // 没匹配,上溯外层
}

这个设计很有意思:**状态不是按名字找,而是按类型找。**你在内层图里 ProcessState[*MyState],它会从最内层开始一路往外,找到第一个 state 类型正好是 *MyState 的那层图,返回它以及它那把独立的锁(每层一把 mu,compose/state.go:36)。这让”内层能读到外层共享的状态”变得自然,而无需任何全局注册表——类型本身就是寻址的钥匙。

type internalState struct {
state any
mu sync.Mutex
parent *internalState
}

💡 动手

做个实验感受 Copy 的惰性:Pipe 出一个流,Copy(2),让其中一个 child 一口气读完,另一个隔一会儿再读。你会发现慢的那个仍能读到全部数据——因为快的那个把链表铺好了(schema/stream.go:837)。再把 sync.Once 的作用想一遍:如果去掉它,同一格会被两个 child 各读一次源头,数据就会错乱。

本章小结

  • 流是读一次的管子,StreamReader(schema/stream.go:168)是带 tag 的联合类型,五种来源对外统一,Recv 按 tag 分派。
  • Copy(schema/stream.go:261)用 sync.Once 守护的读时复制单链表(schema/stream.go:784)做扇出:每格只从源头读一次,快者铺路慢者追赶,全 close 才关源。
  • Merge(schema/stream.go:912)按到达顺序交织;≤5 路走手写 select,>5 路才用 reflect.Select(阈值 maxSelectNum=5,schema/select.go:19)。
  • 装箱(schema/stream.go:461)把值伪装成流,拆箱(compose/stream_concat.go:50internal/concat.go:91)把流拼回值,抹平流与非流的边界。
  • State(compose/state.go:34)是一条带 parent 的链,ProcessState[S](compose/state.go:165)按类型逐级上溯,每层各有一把锁——类型即寻址。
  • 设计钥匙:流与状态都奉行”最小机制、最强语义、成本后置”——这正是 ADK 流式与记忆的地基。

至此 Part IV 收官:你已经看清 compose 从执行内核、三个构建器到流与状态的全貌。下一部分我们把镜头拉远,回到全局——把 ADK、compose、components 三层如何咬合成一个完整的框架,做一次总览与延展。

源码

正在读取完整文件…