Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
100 commits
Select commit Hold shift + click to select a range
966b238
feat(adk): runner-managed session
shentongmartin May 13, 2026
b99fe85
feat(serialization): add HumanReadableSerializer with improved perfor…
shentongmartin Jan 22, 2026
94779a8
fix(adk): harden managed session persistence
shentongmartin May 20, 2026
0b888ae
fix(adk): close session review gaps
shentongmartin May 20, 2026
2ddbccf
fix(adk): avoid go121 slices package
shentongmartin May 20, 2026
d9f34ef
fix(adk): satisfy ci lint checks
shentongmartin May 20, 2026
d867e33
fix(serialization): preserve human readable edge cases
shentongmartin May 20, 2026
a3f261f
fix(adk): defer interrupt checkpoint until session events are durable
shentongmartin May 20, 2026
4c49166
fix(adk): preserve resumability of pre-envelope agent_tool checkpoints
shentongmartin May 20, 2026
8cf7add
refactor(adk): unify LoadEvents cursor naming into single After field
shentongmartin May 21, 2026
952fd07
refactor(adk): rename LoadEventsOptions to LoadEventsRequest
shentongmartin May 21, 2026
f1e834e
docs(adk): fix middleware lifecycle comments and remove unused GobSer…
shentongmartin May 21, 2026
e4f5a85
feat(adk): add retry-with-backoff to session event persister and conf…
shentongmartin May 21, 2026
1c84f91
feat(adk): utilize persisted TurnEndState.ToolInfos for prompt cache …
shentongmartin May 22, 2026
7aae391
feat(adk): add timestamps to agent and session events
shentongmartin May 22, 2026
a33c834
fix(adk): deduplicate system message in defaultGenModelInput for sess…
shentongmartin May 22, 2026
62c6078
refactor(serialization): consolidate HumanReadableSerializer tests in…
shentongmartin May 22, 2026
e1d9ffb
refactor(adk): simplify SessionStore to append-only event log
shentongmartin May 22, 2026
0bfa106
feat(adk): use SessionEvent EventID as SessionStore cursor
shentongmartin May 23, 2026
8444ef7
feat(adk): share EventID between AgentEvent and SessionEvent
shentongmartin May 23, 2026
11a8950
feat(adk): add session timeline observation events
shentongmartin May 24, 2026
94ede0d
feat(adk): add managed interrupt resume mode
shentongmartin May 24, 2026
07f7482
refactor(adk): consolidate session event lane
shentongmartin May 24, 2026
cac787e
fix(adk): preserve checkpoint before fresh turn
shentongmartin May 24, 2026
cd71266
feat(middlewares): add permission middleware
shentongmartin May 24, 2026
333b7d1
feat(middlewares): refine permission resume resolution
shentongmartin May 24, 2026
643ad28
fix(adk): harden permission middleware
shentongmartin May 24, 2026
3a49452
feat(adk): add file session store
shentongmartin May 24, 2026
0e18abc
fix(adk): resolve session loop lint failures
shentongmartin May 25, 2026
4668ccd
refactor(adk): auto-abandon pending checkpoint on fresh Run
shentongmartin May 25, 2026
217947c
refactor(adk): make SessionStore format-agnostic via SessionEventPayload
shentongmartin May 25, 2026
d0ca0e7
fix(adk): revert FileStore to raw line format for HumanReadableSerial…
shentongmartin May 25, 2026
82a3a52
refactor(adk): remove RunID and recover inFlightTurnID on Resume
shentongmartin May 25, 2026
f6a8cbe
refactor(adk): cleanup session timeline event structs
shentongmartin May 25, 2026
f3186af
fix(adk): add omitempty to MessagesReplaced json tag
shentongmartin May 25, 2026
1a1db52
refactor(adk): rename SessionPersistenceConfig to SessionConfig
shentongmartin May 25, 2026
0e421d5
feat(adk): replace tool observation events with tool span events
shentongmartin May 26, 2026
457d102
feat(adk): persistence-aware tool spans across interrupt/resume
shentongmartin May 26, 2026
81a98ce
feat(adk): add kind-aware session store payloads and load filters
shentongmartin May 26, 2026
4474ecd
feat(adk): persist agent interrupt as SessionEventAgentInterrupt
shentongmartin May 26, 2026
4acd7d0
test(adk): cover session log terminal paths
shentongmartin May 26, 2026
5edf29b
docs(adk): clarify session event cursor semantics
shentongmartin May 26, 2026
f369667
feat(adk): add sync session persistence mode
shentongmartin May 26, 2026
9cd3ad7
refactor(adk): simplify AgentInterruptEvent to flat context slice
shentongmartin May 26, 2026
680feae
feat(adk): backfill SessionEvent on live message events
shentongmartin May 26, 2026
6d6c05d
fix(adk): emit user input timeline events
shentongmartin May 27, 2026
b0bc188
fix(adk): guard concurrent store.events access in sync-mode tests
shentongmartin May 27, 2026
43610d5
feat(adk): add extensible session timeline events
shentongmartin May 27, 2026
3ea9e2d
refactor(adk): rename session config field
shentongmartin May 28, 2026
ed23e2a
fix(adk): avoid turn end requirement after fatal errors
shentongmartin May 28, 2026
76e1982
fix(adk): set streaming meta for agentic tool chunks
shentongmartin May 28, 2026
2efa7a2
fix(adk): harden session reduction persistence
shentongmartin May 28, 2026
a44e0c2
fix(adk): close managed interrupt resume race
shentongmartin May 28, 2026
781e7d1
fix(compose): enrich checkpoint set errors
shentongmartin May 29, 2026
ca4442f
fix(adk): handle managed resume and synthetic rerun inputs
shentongmartin May 29, 2026
9c293a9
feat(adk): add session rollback
shentongmartin May 29, 2026
0fb69d4
fix(middlewares): clean permission interrupt payload
shentongmartin May 29, 2026
cdf70bd
refactor(adk): use typed session service
shentongmartin May 31, 2026
f102fa7
fix(middlewares): update permission session test
shentongmartin May 31, 2026
625669e
fix(adk): handle typed-nil CheckpointStore and add configurable Event…
shentongmartin Jun 1, 2026
4db5e5c
feat(adk): pass context to EventIDGenerator for request-scoped ID gen…
shentongmartin Jun 1, 2026
3602e95
feat(adk): add opt-in model call timeout with per-phase budgets
shentongmartin Jun 1, 2026
117b37f
feat(deep): propagate ModelTimeoutConfig to deep agent and general su…
shentongmartin Jun 1, 2026
68de173
refactor(adk): move session ownership to session events
shentongmartin Jun 2, 2026
0ea222d
feat(adk): export prompt language wrappers
shentongmartin Jun 2, 2026
61f0ea4
fix(deep): propagate model retry config to task tool
shentongmartin Jun 2, 2026
5147227
feat(middlewares): normalize patch tool call history
shentongmartin Jun 2, 2026
4e541ee
chore: ignore external working trees
shentongmartin Jun 2, 2026
5431f70
feat(adk/filesystem): support rich execute configuration
shentongmartin Jun 3, 2026
96c1736
feat(adk): add fenced session service
shentongmartin Jun 8, 2026
7b26a59
refactor(adk): simplify session fencing ownership
shentongmartin Jun 8, 2026
d658394
feat(adk): support session event id generator
shentongmartin Jun 9, 2026
d2e4828
feat(adk): add resume wait timeout
shentongmartin Jun 10, 2026
af249c7
fix(adk): keep memory store go1.18 compatible
shentongmartin Jun 10, 2026
a88f79c
fix(adk): stabilize session message ids
shentongmartin Jun 10, 2026
7972ed5
test(serialization): avoid duplicate gob registration
shentongmartin Jun 10, 2026
c109250
test(serialization): use concrete gob benchmark payloads
shentongmartin Jun 10, 2026
f97e31e
test(adk): cover session adapter edge cases
shentongmartin Jun 10, 2026
21f9288
test(adk): cover file session store edges
shentongmartin Jun 10, 2026
c571164
refactor(adk): simplify session persistence flow
shentongmartin Jun 10, 2026
6f9dd97
fix(adk): preserve loaded session tail
shentongmartin Jun 10, 2026
ffe5d04
test(adk): stabilize cancel resume timeout test
shentongmartin Jun 11, 2026
8064fb0
test(adk): request cancel before resume safepoint
shentongmartin Jun 11, 2026
b0d374a
test(adk): stabilize resume stream cancel test
shentongmartin Jun 11, 2026
b7bff48
test(adk): cover model timeout edge paths
shentongmartin Jun 11, 2026
fa23433
test(adk): stabilize turn loop cancel mock
shentongmartin Jun 11, 2026
6fabdf4
feat(adk): record permission resume decisions (#1070)
shentongmartin Jun 12, 2026
1270607
refactor(adk): drop AgentInterruptCause classification (#1072)
shentongmartin Jun 12, 2026
6cc4d0f
fix(adk): allow business interrupt resume through permission gate (#1…
shentongmartin Jun 12, 2026
a183e04
feat(adk): auto memory middleware (#987)
N3kox Jun 15, 2026
a4d94d8
feat(adk): automemory rebuild memory instruction & emit session event…
N3kox Jun 15, 2026
930d64d
fix(adk): auto memory TypedSendEvent wrong kind (#1082)
N3kox Jun 15, 2026
59df9d8
fix(adk): persist BeforeAgent session events (#1083)
shentongmartin Jun 16, 2026
fd92075
refactor(adk): simplify session service ownership (#1079)
shentongmartin Jun 17, 2026
859cb34
fix(adk): drop errored streams from session persistence (#1088)
shentongmartin Jun 17, 2026
9675635
refactor: simplify load session (#1086)
YellowDusk04 Jun 18, 2026
c05e8e3
feat(adk): auto memory support multi memory store (#1087)
N3kox Jun 18, 2026
8e22a2d
chore(adk): memory glob prompt (#1089)
N3kox Jun 18, 2026
9c768eb
chore: make code more readable
YellowDusk04 Jun 20, 2026
2b791f4
chore: improve the comment
YellowDusk04 Jun 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,7 @@ CLAUDE.md

# Internal dev setup (not for public repo)
/scripts/dev_setup_internal.sh

# External working trees
/examples/
/ext/
105 changes: 93 additions & 12 deletions adk/agent_tool.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ package adk

import (
"context"
"encoding/json"
"errors"
"fmt"

"github.com/bytedance/sonic"
"github.com/google/uuid"

"github.com/cloudwego/eino/components/tool"
"github.com/cloudwego/eino/compose"
Expand Down Expand Up @@ -151,6 +153,15 @@ func (at *typedAgentTool[M]) Info(ctx context.Context) (*schema.ToolInfo, error)
}, nil
}

// agentToolInterruptState is the JSON-encoded state captured when an AgentTool
// invocation is interrupted. It wraps the bridge checkpoint bytes alongside
// the synthetic child session ID so resume preserves SessionID-based event
// filtering across interrupt/resume.
type agentToolInterruptState struct {
ChildSessionID string `json:"child_session_id"`
BridgeCheckpoint []byte `json:"bridge_checkpoint"`
}

func (at *typedAgentTool[M]) InvokableRun(ctx context.Context, argumentsInJSON string, opts ...tool.Option) (string, error) {
if cancelCtx := getCancelContext(ctx); cancelCtx != nil {
cancelCtx.markAgentToolDescendant()
Expand All @@ -161,19 +172,42 @@ func (at *typedAgentTool[M]) InvokableRun(ctx context.Context, argumentsInJSON s
var iter *AsyncIterator[*TypedAgentEvent[M]]
var err error

wasInterrupted, hasState, state := tool.GetInterruptState[[]byte](ctx)
wasInterrupted, hasState, rawState := tool.GetInterruptState[[]byte](ctx)

var childSessionID string
var bridgeCheckpoint []byte

if !wasInterrupted {
// First invocation — generate a globally-unique child session ID.
// Synthetic UUID avoids collisions with model-assigned tool call IDs
// (which may be reused across turns) and with user-assigned session IDs.
childSessionID = "agent_tool:" + uuid.NewString()
} else if !hasState {
return "", fmt.Errorf("agent tool '%s' interrupt has happened, but cannot find interrupt state", at.agent.Name(ctx))
} else {
// Resume — try the JSON envelope (introduced when SessionID-based event
// filtering landed). If the envelope does not parse or carries no bridge
// checkpoint, the rawState is from a pre-envelope version: treat the
// raw bytes as the bridge checkpoint and synthesize a fresh
// childSessionID. Pre-envelope checkpoints predate session persistence,
// so the synthesized ID has no parent-session filter to coordinate with.
var wrapped agentToolInterruptState
if json.Unmarshal(rawState, &wrapped) == nil && len(wrapped.BridgeCheckpoint) > 0 {
childSessionID = wrapped.ChildSessionID
bridgeCheckpoint = wrapped.BridgeCheckpoint
} else {
childSessionID = "agent_tool:" + uuid.NewString()
bridgeCheckpoint = rawState
}
}

if !wasInterrupted {
ms = newBridgeStore()

var input []M
if at.fullChatHistoryAsInput {
var zero M
if _, ok := any(zero).(*schema.Message); !ok {
// fullChatHistoryAsInput is only supported for *schema.Message agents and will not
// be extended to *schema.AgenticMessage. The chat history format and role semantics
// differ fundamentally between Message and AgenticMessage, and the history rewriting
// logic (role attribution, system message filtering, transfer messages) is specific
// to the Message model.
return "", fmt.Errorf("fullChatHistoryAsInput is only supported for *schema.Message agents")
}
msgInput, histErr := getReactChatHistory(ctx, at.agent.Name(ctx))
Expand All @@ -197,11 +231,7 @@ func (at *typedAgentTool[M]) InvokableRun(ctx context.Context, argumentsInJSON s
iter = runner.Run(ctx, input,
append(extractAndDeriveAgentToolCancelCtx(ctx, at.agent.Name(ctx), opts), WithCheckPointID(bridgeCheckpointID), withSharedParentSession())...)
} else {
if !hasState {
return "", fmt.Errorf("agent tool '%s' interrupt has happened, but cannot find interrupt state", at.agent.Name(ctx))
}

ms = newResumeBridgeStore(bridgeCheckpointID, state)
ms = newResumeBridgeStore(bridgeCheckpointID, bridgeCheckpoint)

agentOpts := extractAndDeriveAgentToolCancelCtx(ctx, at.agent.Name(ctx), opts)
agentOpts = append(agentOpts, withSharedParentSession())
Expand Down Expand Up @@ -239,6 +269,10 @@ func (at *typedAgentTool[M]) InvokableRun(ctx context.Context, argumentsInJSON s
rp = append(rp, event.RunPath...)
event.RunPath = rp
}
// Tag forwarded events with the child session ID so live consumers
// can distinguish child timeline events and the parent's persistence
// loop can skip them.
stampAgentToolSessionEvent(event, childSessionID)
tmp := copyTypedAgentEvent(event)
gen.Send(event)
event = tmp
Expand All @@ -257,7 +291,17 @@ func (at *typedAgentTool[M]) InvokableRun(ctx context.Context, argumentsInJSON s
return "", fmt.Errorf("interrupt has happened, but cannot find interrupt info")
}

return "", tool.CompositeInterrupt(ctx, "agent tool interrupt", data,
// Wrap bridge checkpoint with childSessionID so resume can recover it.
wrapped := agentToolInterruptState{
ChildSessionID: childSessionID,
BridgeCheckpoint: data,
}
wrappedBytes, mErr := json.Marshal(wrapped)
if mErr != nil {
return "", fmt.Errorf("agent_tool: failed to encode interrupt state: %w", mErr)
}

return "", tool.CompositeInterrupt(ctx, "agent tool interrupt", wrappedBytes,
lastEvent.Action.internalInterrupted)
}

Expand Down Expand Up @@ -408,6 +452,43 @@ func newTypedUserMessages[M MessageType](text string) []M {
}
}

func stampAgentToolSessionEvent[M MessageType](event *TypedAgentEvent[M], childSessionID string) {
if event == nil || childSessionID == "" {
return
}
if event.EventID == "" {
event.EventID = uuid.NewString()
}
if event.Timestamp.IsZero() {
event.Timestamp = newEventTimestamp()
}
if event.SessionEvent == nil {
event.SessionEvent = &SessionEvent[M]{
SessionID: childSessionID,
EventID: event.EventID,
Timestamp: event.Timestamp,
}
if event.Output != nil && event.Output.MessageOutput != nil {
event.SessionEvent.Kind = SessionEventMessage
}
return
}
event.SessionEvent.SessionID = childSessionID
if event.SessionEvent.EventID == "" {
event.SessionEvent.EventID = event.EventID
}
if event.SessionEvent.Timestamp.IsZero() {
event.SessionEvent.Timestamp = event.Timestamp
}
}

// newTypedInvokableAgentToolRunner creates a runner for the inner agent without
// SessionEventStore. The child's events are forwarded to the parent's live stream
// (tagged with childSessionID on SessionEvent) and filtered out of the parent's persistence.
// The child's durability relies solely on the bridge checkpoint stored inside
// agentToolInterruptState — there is no independent child session log.
// This may change in the future if AgentTool needs cross-turn context
// continuation or audit-level event logging for the child session.
func newTypedInvokableAgentToolRunner[M MessageType](agent TypedAgent[M], store compose.CheckPointStore, enableStreaming bool) *TypedRunner[M] {
return &TypedRunner[M]{
a: agent,
Expand Down
17 changes: 17 additions & 0 deletions adk/agent_tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,23 @@ func TestAgentTool_InvokableRun_StreamingVariant(t *testing.T) {
}
}

func TestStampAgentToolSessionEvent(t *testing.T) {
msg := schema.AssistantMessage("child", nil)
event := &AgentEvent{
Output: &AgentOutput{
MessageOutput: &MessageVariant{Message: msg, Role: schema.Assistant},
},
}

stampAgentToolSessionEvent(event, "agent_tool:child")

require.NotNil(t, event.SessionEvent)
assert.Equal(t, "agent_tool:child", event.SessionEvent.SessionID)
assert.Equal(t, event.EventID, event.SessionEvent.EventID)
assert.Equal(t, event.Timestamp, event.SessionEvent.Timestamp)
assert.Equal(t, SessionEventMessage, event.SessionEvent.Kind)
}

func TestSequentialWorkflow_WithChatModelAgentTool_NestedRunPathAndSessions(t *testing.T) {
ctx := context.Background()

Expand Down
51 changes: 45 additions & 6 deletions adk/call_option.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,16 @@ package adk
import "github.com/cloudwego/eino/callbacks"

type options struct {
sharedParentSession bool
sessionValues map[string]any
checkPointID *string
skipTransferMessages bool
handlers []callbacks.Handler
cancelCtx *cancelContext
sharedParentSession bool
sessionValues map[string]any
checkPointID *string
skipTransferMessages bool
enableSessionEvents bool
enableTimelineEvents bool
enableInternalTimelineEvents bool
handlers []callbacks.Handler
cancelCtx *cancelContext
refreshToolInfos bool
}

// AgentRunOption is the call option for adk Agent.
Expand Down Expand Up @@ -55,6 +59,28 @@ func WithSessionValues(v map[string]any) AgentRunOption {
})
}

func withEnableSessionEvents() AgentRunOption {
return WrapImplSpecificOptFn(func(o *options) {
o.enableSessionEvents = true
})
}

// WithTimelineEvents exposes the first-class SessionEvent timeline envelope on
// live AgentEvents. Without this option, lifecycle/span/observation-only events
// are still produced for managed-session persistence but are stripped from the
// user-facing stream.
func WithTimelineEvents() AgentRunOption {
return WrapImplSpecificOptFn(func(o *options) {
o.enableTimelineEvents = true
})
}

func withEnableInternalTimelineEvents() AgentRunOption {
return WrapImplSpecificOptFn(func(o *options) {
o.enableInternalTimelineEvents = true
})
}

// WithSkipTransferMessages disables forwarding transfer messages during execution.
//
// NOT RECOMMENDED: Agent transfer with full context sharing between agents has not proven
Expand All @@ -81,6 +107,19 @@ func WithCallbacks(handlers ...callbacks.Handler) AgentRunOption {
})
}

// WithRefreshToolInfos forces the agent to re-derive its tool list from the current
// BaseTool set instead of using the persisted TurnEndState.ToolInfos from the previous turn.
//
// By default, when a SessionEventStore is configured, the Runner reuses the exact tool list
// from the previous turn's end to preserve the model's prompt cache. Use this option when
// you have added, removed, or updated tools between turns and need the model to see the
// changes immediately (accepting a cache miss).
func WithRefreshToolInfos() AgentRunOption {
return WrapImplSpecificOptFn(func(o *options) {
o.refreshToolInfos = true
})
}

// WrapImplSpecificOptFn is the option to wrap the implementation specific option function.
func WrapImplSpecificOptFn[T any](optFn func(*T)) AgentRunOption {
return AgentRunOption{
Expand Down
15 changes: 13 additions & 2 deletions adk/cancel_edge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1435,9 +1435,20 @@ func TestWithCancel_CancelImmediate_StreamableToolAborted(t *testing.T) {
// ErrStreamCanceled appears on the tool's MessageStream.Recv()
if e.Output != nil && e.Output.MessageOutput != nil && e.Output.MessageOutput.IsStreaming &&
e.Output.MessageOutput.Role == schema.Tool {
// Signal that the tool stream event has been received.
close(toolStreamReady)
stream := e.Output.MessageOutput.MessageStream
// Consume the first chunk so we are sure the stream is active,
// then signal readiness. This ensures cancel fires while we are
// blocked inside Recv(), preventing a race where cancel completes
// before we start consuming.
if _, firstErr := stream.Recv(); firstErr == nil {
close(toolStreamReady)
} else {
if errors.Is(firstErr, ErrStreamCanceled) {
r.foundStreamCanceled = true
}
close(toolStreamReady)
continue
}
for {
_, recvErr := stream.Recv()
if recvErr != nil {
Expand Down
Loading
Loading