Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
139 commits
Select commit Hold shift + click to select a range
8aa17b1
feat(adk): runner-managed session
shentongmartin May 13, 2026
5388e10
feat(serialization): add HumanReadableSerializer with improved perfor…
shentongmartin Jan 22, 2026
60dcb33
fix(adk): harden managed session persistence
shentongmartin May 20, 2026
9d3b9ee
fix(adk): close session review gaps
shentongmartin May 20, 2026
3d5b6ad
fix(adk): avoid go121 slices package
shentongmartin May 20, 2026
32a1a5d
fix(adk): satisfy ci lint checks
shentongmartin May 20, 2026
4a39bae
fix(serialization): preserve human readable edge cases
shentongmartin May 20, 2026
13b2b3f
fix(adk): defer interrupt checkpoint until session events are durable
shentongmartin May 20, 2026
b075caa
fix(adk): preserve resumability of pre-envelope agent_tool checkpoints
shentongmartin May 20, 2026
4942054
refactor(adk): unify LoadEvents cursor naming into single After field
shentongmartin May 21, 2026
2f9797c
refactor(adk): rename LoadEventsOptions to LoadEventsRequest
shentongmartin May 21, 2026
83bc84e
docs(adk): fix middleware lifecycle comments and remove unused GobSer…
shentongmartin May 21, 2026
79b0a30
feat(adk): add retry-with-backoff to session event persister and conf…
shentongmartin May 21, 2026
8da356c
feat(adk): utilize persisted TurnEndState.ToolInfos for prompt cache …
shentongmartin May 22, 2026
93628b8
feat(adk): add timestamps to agent and session events
shentongmartin May 22, 2026
84fca31
fix(adk): deduplicate system message in defaultGenModelInput for sess…
shentongmartin May 22, 2026
61d4719
refactor(serialization): consolidate HumanReadableSerializer tests in…
shentongmartin May 22, 2026
9b5b4b5
refactor(adk): simplify SessionStore to append-only event log
shentongmartin May 22, 2026
278b850
feat(adk): use SessionEvent EventID as SessionStore cursor
shentongmartin May 23, 2026
410fe12
feat(adk): share EventID between AgentEvent and SessionEvent
shentongmartin May 23, 2026
53bbedb
feat(adk): add session timeline observation events
shentongmartin May 24, 2026
e05042e
feat(adk): add managed interrupt resume mode
shentongmartin May 24, 2026
7ed2515
refactor(adk): consolidate session event lane
shentongmartin May 24, 2026
f49960e
fix(adk): preserve checkpoint before fresh turn
shentongmartin May 24, 2026
f85d5c1
feat(middlewares): add permission middleware
shentongmartin May 24, 2026
fe15adc
feat(middlewares): refine permission resume resolution
shentongmartin May 24, 2026
109f220
fix(adk): harden permission middleware
shentongmartin May 24, 2026
5076e12
feat(adk): add file session store
shentongmartin May 24, 2026
c0b4860
fix(adk): resolve session loop lint failures
shentongmartin May 25, 2026
88bbc32
refactor(adk): auto-abandon pending checkpoint on fresh Run
shentongmartin May 25, 2026
0f1e8b9
refactor(adk): make SessionStore format-agnostic via SessionEventPayload
shentongmartin May 25, 2026
2387930
fix(adk): revert FileStore to raw line format for HumanReadableSerial…
shentongmartin May 25, 2026
28a8611
refactor(adk): remove RunID and recover inFlightTurnID on Resume
shentongmartin May 25, 2026
5b29da3
refactor(adk): cleanup session timeline event structs
shentongmartin May 25, 2026
cd72975
fix(adk): add omitempty to MessagesReplaced json tag
shentongmartin May 25, 2026
b90520b
refactor(adk): rename SessionPersistenceConfig to SessionConfig
shentongmartin May 25, 2026
86992f8
feat(adk): replace tool observation events with tool span events
shentongmartin May 26, 2026
99f7d01
feat(adk): persistence-aware tool spans across interrupt/resume
shentongmartin May 26, 2026
bd256aa
feat(adk): add kind-aware session store payloads and load filters
shentongmartin May 26, 2026
f96905c
feat(adk): persist agent interrupt as SessionEventAgentInterrupt
shentongmartin May 26, 2026
dadb61a
test(adk): cover session log terminal paths
shentongmartin May 26, 2026
653b85c
docs(adk): clarify session event cursor semantics
shentongmartin May 26, 2026
d12ef1b
feat(adk): add sync session persistence mode
shentongmartin May 26, 2026
fba8348
refactor(adk): simplify AgentInterruptEvent to flat context slice
shentongmartin May 26, 2026
17dc6e9
feat(adk): backfill SessionEvent on live message events
shentongmartin May 26, 2026
4650a8c
fix(adk): emit user input timeline events
shentongmartin May 27, 2026
d8be7ec
fix(adk): guard concurrent store.events access in sync-mode tests
shentongmartin May 27, 2026
84204af
feat(adk): add extensible session timeline events
shentongmartin May 27, 2026
2d31662
refactor(adk): rename session config field
shentongmartin May 28, 2026
016b405
fix(adk): avoid turn end requirement after fatal errors
shentongmartin May 28, 2026
cd010b8
fix(adk): set streaming meta for agentic tool chunks
shentongmartin May 28, 2026
20f4d76
fix(adk): harden session reduction persistence
shentongmartin May 28, 2026
984675d
fix(adk): close managed interrupt resume race
shentongmartin May 28, 2026
9479038
fix(compose): enrich checkpoint set errors
shentongmartin May 29, 2026
4b71d48
fix(adk): handle managed resume and synthetic rerun inputs
shentongmartin May 29, 2026
b5b8ada
feat(adk): add session rollback
shentongmartin May 29, 2026
d683470
fix(middlewares): clean permission interrupt payload
shentongmartin May 29, 2026
1701b74
refactor(adk): use typed session service
shentongmartin May 31, 2026
6bd020c
fix(middlewares): update permission session test
shentongmartin May 31, 2026
c176d7e
fix(adk): handle typed-nil CheckpointStore and add configurable Event…
shentongmartin Jun 1, 2026
36480c5
feat(adk): pass context to EventIDGenerator for request-scoped ID gen…
shentongmartin Jun 1, 2026
791bcf3
feat(adk): add opt-in model call timeout with per-phase budgets
shentongmartin Jun 1, 2026
b8283c9
feat(deep): propagate ModelTimeoutConfig to deep agent and general su…
shentongmartin Jun 1, 2026
cdba1e9
refactor(adk): move session ownership to session events
shentongmartin Jun 2, 2026
306303e
feat(adk): export prompt language wrappers
shentongmartin Jun 2, 2026
65ce068
fix(deep): propagate model retry config to task tool
shentongmartin Jun 2, 2026
60826fc
feat(middlewares): normalize patch tool call history
shentongmartin Jun 2, 2026
77db87b
chore: ignore external working trees
shentongmartin Jun 2, 2026
24e8af3
feat(adk/filesystem): support rich execute configuration
shentongmartin Jun 3, 2026
09e0f03
feat(adk): add fenced session service
shentongmartin Jun 8, 2026
3deeb27
refactor(adk): simplify session fencing ownership
shentongmartin Jun 8, 2026
c36166d
feat(adk): support session event id generator
shentongmartin Jun 9, 2026
226572e
feat(adk): add resume wait timeout
shentongmartin Jun 10, 2026
ca83a47
fix(adk): keep memory store go1.18 compatible
shentongmartin Jun 10, 2026
9ac6d53
fix(adk): stabilize session message ids
shentongmartin Jun 10, 2026
c319887
test(serialization): avoid duplicate gob registration
shentongmartin Jun 10, 2026
415321c
test(serialization): use concrete gob benchmark payloads
shentongmartin Jun 10, 2026
41c2803
test(adk): cover session adapter edge cases
shentongmartin Jun 10, 2026
3b76836
test(adk): cover file session store edges
shentongmartin Jun 10, 2026
1801c96
refactor(adk): simplify session persistence flow
shentongmartin Jun 10, 2026
5f7f9d3
fix(adk): preserve loaded session tail
shentongmartin Jun 10, 2026
a347e37
test(adk): stabilize cancel resume timeout test
shentongmartin Jun 11, 2026
07a9c70
test(adk): request cancel before resume safepoint
shentongmartin Jun 11, 2026
274fbaa
test(adk): stabilize resume stream cancel test
shentongmartin Jun 11, 2026
ac67d73
test(adk): cover model timeout edge paths
shentongmartin Jun 11, 2026
cae4f13
test(adk): stabilize turn loop cancel mock
shentongmartin Jun 11, 2026
9a5e528
feat(adk): record permission resume decisions (#1070)
shentongmartin Jun 12, 2026
a5b7e30
refactor(adk): drop AgentInterruptCause classification (#1072)
shentongmartin Jun 12, 2026
aab6f7e
fix(adk): allow business interrupt resume through permission gate (#1…
shentongmartin Jun 12, 2026
0022ba4
feat(adk): auto memory middleware (#987)
N3kox Jun 15, 2026
743224a
feat(adk): automemory rebuild memory instruction & emit session event…
N3kox Jun 15, 2026
cf72e91
fix(adk): auto memory TypedSendEvent wrong kind (#1082)
N3kox Jun 15, 2026
79eef75
fix(adk): persist BeforeAgent session events (#1083)
shentongmartin Jun 16, 2026
972c4cc
refactor(adk): simplify session service ownership (#1079)
shentongmartin Jun 17, 2026
4d4687d
fix(adk): drop errored streams from session persistence (#1088)
shentongmartin Jun 17, 2026
4bb0286
refactor: simplify load session (#1086)
YellowDusk04 Jun 18, 2026
543656c
feat(adk): auto memory support multi memory store (#1087)
N3kox Jun 18, 2026
28cc930
chore(adk): memory glob prompt (#1089)
N3kox Jun 18, 2026
a72d9ea
refactor(adk): extract model timeout middleware (#1095)
shentongmartin Jun 22, 2026
1afd7b3
fix(adk): persist leading system messages (#1096)
shentongmartin Jun 22, 2026
177033b
feat(adk): extract memory instruction customize (#1097)
N3kox Jun 22, 2026
78ad9db
refactor(adk): remove turn end session event (#1098)
shentongmartin Jun 22, 2026
b7906d3
fix(adk): deduplicate deep agent system prompts (#1101)
shentongmartin Jun 23, 2026
7d6ad71
fix(adk): delete consumed resume checkpoint (#1102)
shentongmartin Jun 23, 2026
8b29a7f
fix(adk): snapshot leading system sync state (#1103)
shentongmartin Jun 24, 2026
8958ee9
feat(adk): persist incomplete stream prefixes (#1105)
shentongmartin Jun 24, 2026
dbfe9a9
refactor(adk): introduce SessionEventVariant and simplify session eve…
shentongmartin Jun 25, 2026
33648fd
feat(adk): rollback memory stores to single memory dir (#1109)
N3kox Jun 25, 2026
a77de0d
fix(adk): dedupe empty model context snapshots (#1115)
shentongmartin Jun 26, 2026
26bc5ca
feat(adk): background-task manager with subagent/filesystem/deep wiri…
hi-pender Jun 29, 2026
f4cdeb9
feat(adk): add session timeline observation events
shentongmartin May 24, 2026
efd3ece
feat(adk): add agent teams middleware with mailbox-based multi-agent …
fanlv Mar 26, 2026
528c270
docs: add agents.md
fanlv Jun 22, 2026
848a097
refactor(adk): move team middleware to prebuilt directory
fanlv Jun 22, 2026
44ac19c
fix(adk/team): harden name validation, owner checks, lock lifecycle, …
fanlv Jun 22, 2026
7542013
fix(adk/team): correct reminder default, tighten API surface, harden …
fanlv Jun 22, 2026
7c69792
fix(adk/team): validate dedup names, surface TeamCreate rollback erro…
fanlv Jun 22, 2026
c1b5d6e
fix(adk/team): remove dead leader-exit chain, expose broadcast result…
fanlv Jun 22, 2026
917f02c
fix(adk/team): surface task-assignment notify failures, harden shutdo…
fanlv Jun 22, 2026
900546f
fix(adk/team): drop dead tool.json, unexport inboxMessage, fix stale …
fanlv Jun 22, 2026
b81e7a5
fix(adk/team): guard tasks before TeamCreate, length-safe team dedup,…
fanlv Jun 22, 2026
32c0325
fix(adk/team): guard nil config read, keep busy/idle in-process, doc …
fanlv Jun 22, 2026
3eefd1b
fix(adk/team): lint shadows, atomic TeamCreate, no team_name bypass, …
fanlv Jun 22, 2026
7258a53
test(adk/team): deterministically keep teammate alive in FullFlow
fanlv Jun 22, 2026
b956f2c
fix(adk/team): serialize Agent/TeamDelete via teamOpLock, render cont…
fanlv Jun 22, 2026
e4bec8e
fix(adk/team): guard nil RunnerConfig, order inbox delete before memb…
fanlv Jun 22, 2026
c2626b7
fix(adk/team): stop loop on abnormal pump exit, observable reminder d…
fanlv Jun 22, 2026
9a26772
fix(adk/team): no-leak teammate wait, warn on undeliverable DMs
fanlv Jun 22, 2026
c8d1dc4
fix(adk/team): drop dead dispatch helper, observe shutdown drops, fas…
fanlv Jun 22, 2026
67747af
fix(adk): atomic TaskUpdate graph writes, recoverable TeamDelete orde…
fanlv Jun 23, 2026
cb8e446
fix(adk/team): align team_name schema, drop unused IsActive, clarify …
fanlv Jun 23, 2026
bf1e7e8
fix(adk/team): harden teammate lifecycle, mailbox cleanup, and send/l…
fanlv Jun 23, 2026
85a39ae
fix(adk/team): gate task auto-clear to single-agent, drop dead busy/i…
fanlv Jun 23, 2026
f64841f
fix(adk/team): stop point-to-point send from resurrecting torn-down i…
fanlv Jun 23, 2026
77e74e2
docs(adk/team): add demo
fanlv Jun 23, 2026
1162e1e
feat(adk): remove TeamCreate and TeamDelete for teams (#1104)
N3kox Jun 23, 2026
1bab211
feat(adk): customizable teammate roles (#1108)
N3kox Jun 24, 2026
a8978e9
chore(adk/team): update README & demo
N3kox Jun 26, 2026
cfdf249
fix(adk/team): escape '<'/'&' in teammate envelope body to block tag-…
N3kox Jun 30, 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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,10 @@ CLAUDE.md

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

# External working trees
/examples/
/ext/

*.local.md
Comment thread
N3kox marked this conversation as resolved.
Comment thread
N3kox marked this conversation as resolved.
**/settings.local.json
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@./AGENTS.local.md
103 changes: 91 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,41 @@ func newTypedUserMessages[M MessageType](text string) []M {
}
}

func stampAgentToolSessionEvent[M MessageType](event *TypedAgentEvent[M], childSessionID string) {
if event == nil || childSessionID == "" {
return
}
if event.SessionEventVariant == nil && event.Output != nil && event.Output.MessageOutput != nil {
ts := newEventTimestamp()
if event.Output.MessageOutput.IsStreaming {
event.SessionEventVariant = &SessionEventVariant[M]{
MessageStreamRef: &MessageStreamRef{
Timestamp: ts,
Kind: SessionEventMessage,
},
}
} else if !isNilMessage(event.Output.MessageOutput.Message) {
event.SessionEventVariant = &SessionEventVariant[M]{
Event: &SessionEvent[M]{
Timestamp: ts,
Kind: SessionEventMessage,
Message: event.Output.MessageOutput.Message,
},
}
}
}
if event.SessionEventVariant != nil {
event.SessionEventVariant.SessionID = childSessionID
}
}

// 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 SessionEventVariant) 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
38 changes: 38 additions & 0 deletions adk/agent_tool_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,44 @@ 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.SessionEventVariant.Event)
assert.Equal(t, "agent_tool:child", event.SessionEventVariant.SessionID)
assert.Empty(t, event.SessionEventVariant.Event.EventID)
assert.False(t, event.SessionEventVariant.Event.Timestamp.IsZero())
assert.Equal(t, SessionEventMessage, event.SessionEventVariant.Event.Kind)
}

func TestStampAgentToolSessionEvent_Streaming(t *testing.T) {
event := &AgentEvent{
Output: &AgentOutput{
MessageOutput: &MessageVariant{
IsStreaming: true,
MessageStream: schema.StreamReaderFromArray([]Message{schema.AssistantMessage("child", nil)}),
Role: schema.Assistant,
},
},
}

stampAgentToolSessionEvent(event, "agent_tool:child")

ref := event.SessionEventVariant.MessageStreamRef
require.NotNil(t, ref)
assert.Equal(t, "agent_tool:child", event.SessionEventVariant.SessionID)
assert.Empty(t, ref.EventID)
assert.False(t, ref.Timestamp.IsZero())
assert.Equal(t, SessionEventMessage, ref.Kind)
}

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

Expand Down
109 changes: 109 additions & 0 deletions adk/backgroundtask/id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2026 CloudWeGo Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package backgroundtask

import (
"math/rand"
"time"
)

// Task-id layout: a positive int64 (63 usable bits) packed as
//
// [ 41 bits ms timestamp ][ 12 bits sequence ][ 10 bits random ]
//
// Uniqueness within a process is guaranteed by (timestamp, sequence): the
// sequence resets each millisecond and increments for every id minted within the
// same millisecond, all under the Manager lock. If more than 2^12 ids are minted
// in a single millisecond the generator spins to the next millisecond rather than
// wrapping the sequence, so (timestamp, sequence) never repeats. The random low
// bits only make ids look unordered/unpredictable; they are not relied upon for
// uniqueness.
//
// 41 bits of milliseconds covers ~69 years; 12 bits allows 4096 ids per
// millisecond before the generator advances to the next millisecond.
const (
idSeqBits = 12
idRandomBits = 10
idSeqLimit = 1 << idSeqBits
idRandomMask = (1 << idRandomBits) - 1
)

// nextRawID packs the next task id integer. Must be called with m.mu held, as it
// reads and advances m.seq / m.lastMs.
func (m *Manager) nextRawID() int64 {
ms := time.Now().UnixMilli()
switch {
case ms > m.lastMs:
m.lastMs = ms
m.seq = 0
default:
// Same millisecond (or a backward clock step): keep the id monotonic by
// staying on lastMs and advancing the sequence. On sequence overflow, move
// to the next millisecond so (timestamp, sequence) stays unique.
ms = m.lastMs
m.seq++
if m.seq >= idSeqLimit {
ms = m.waitNextMs(m.lastMs)
m.lastMs = ms
m.seq = 0
}
}

//nolint:gosec // non-cryptographic: random bits only diffuse the id's look.
r := int64(rand.Intn(idRandomMask + 1))
return (ms << (idSeqBits + idRandomBits)) | (m.seq << idRandomBits) | r
}

// waitNextMs busy-waits until the wall clock advances past prevMs. Reached only
// when more than 2^12 ids are minted within one millisecond.
func (m *Manager) waitNextMs(prevMs int64) int64 {
ms := time.Now().UnixMilli()
for ms <= prevMs {
ms = time.Now().UnixMilli()
}
return ms
}

// base62 encodes a non-negative int64 using [0-9A-Za-z]. It is the compact,
// URL-safe textual form of a task id's integer.
func base62(n int64) string {
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
if n == 0 {
return "0"
}
var buf [11]byte // ceil(63 / log2(62)) = 11
i := len(buf)
for n > 0 {
i--
buf[i] = alphabet[n%62]
n /= 62
}
return string(buf[i:])
}

// defaultTaskIDPrefix is used when a task has no Type tag.
const defaultTaskIDPrefix = "task"

// taskIDPrefix returns the id prefix for a task type, falling back to a generic
// prefix when the type is empty. The type tag (e.g. "bash", "subagent") makes ids
// self-describing: "bash_3Fa9...".
func taskIDPrefix(taskType string) string {
if taskType == "" {
return defaultTaskIDPrefix
}
return taskType
}
Loading
Loading