aury-agent 0.0.12__py3-none-any.whl → 0.0.14__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aury/agents/backends/__init__.py +8 -0
- aury/agents/backends/hitl/__init__.py +8 -0
- aury/agents/backends/hitl/memory.py +100 -0
- aury/agents/backends/hitl/types.py +132 -0
- aury/agents/core/base.py +5 -0
- aury/agents/core/context.py +1 -0
- aury/agents/core/signals.py +37 -17
- aury/agents/core/types/__init__.py +0 -2
- aury/agents/core/types/block.py +6 -23
- aury/agents/core/types/session.py +10 -3
- aury/agents/core/types/tool.py +194 -18
- aury/agents/hitl/__init__.py +2 -0
- aury/agents/hitl/ask_user.py +59 -47
- aury/agents/hitl/exceptions.py +214 -13
- aury/agents/react/agent.py +47 -0
- aury/agents/react/context.py +51 -25
- aury/agents/react/factory.py +2 -0
- aury/agents/react/pause.py +13 -2
- aury/agents/react/step.py +39 -12
- aury/agents/react/tools.py +277 -147
- aury/agents/tool/builtin/ask_user.py +1 -5
- aury/agents/tool/builtin/delegate.py +3 -15
- aury/agents/tool/builtin/plan.py +1 -5
- aury/agents/tool/builtin/thinking.py +1 -6
- aury/agents/tool/builtin/yield_result.py +1 -6
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/METADATA +1 -1
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/RECORD +29 -26
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/WHEEL +0 -0
- {aury_agent-0.0.12.dist-info → aury_agent-0.0.14.dist-info}/entry_points.txt +0 -0
aury/agents/backends/__init__.py
CHANGED
|
@@ -29,6 +29,7 @@ from .invocation import InvocationBackend, InMemoryInvocationBackend
|
|
|
29
29
|
from .message import MessageBackend, InMemoryMessageBackend
|
|
30
30
|
from .memory import MemoryBackend, InMemoryMemoryBackend
|
|
31
31
|
from .artifact import ArtifactBackend, ArtifactSource, InMemoryArtifactBackend
|
|
32
|
+
from .hitl import HITLBackend, InMemoryHITLBackend
|
|
32
33
|
|
|
33
34
|
# State backend - simplified to key-value
|
|
34
35
|
from .state import StateBackend, StateStore, StoreBasedStateBackend, SQLiteStateBackend, MemoryStateBackend, FileStateBackend, CompositeStateBackend
|
|
@@ -87,6 +88,7 @@ class Backends:
|
|
|
87
88
|
memory: MemoryBackend | None = None
|
|
88
89
|
artifact: ArtifactBackend | None = None
|
|
89
90
|
state: StateBackend | None = None
|
|
91
|
+
hitl: HITLBackend | None = None
|
|
90
92
|
|
|
91
93
|
# Capability backends - optional
|
|
92
94
|
snapshot: SnapshotBackend | None = None
|
|
@@ -108,6 +110,7 @@ class Backends:
|
|
|
108
110
|
memory=InMemoryMemoryBackend(),
|
|
109
111
|
artifact=InMemoryArtifactBackend(),
|
|
110
112
|
state=MemoryStateBackend(),
|
|
113
|
+
hitl=InMemoryHITLBackend(),
|
|
111
114
|
)
|
|
112
115
|
|
|
113
116
|
@classmethod
|
|
@@ -127,6 +130,7 @@ class Backends:
|
|
|
127
130
|
memory=InMemoryMemoryBackend(),
|
|
128
131
|
artifact=InMemoryArtifactBackend(),
|
|
129
132
|
state=SQLiteStateBackend(db_path),
|
|
133
|
+
hitl=InMemoryHITLBackend(),
|
|
130
134
|
)
|
|
131
135
|
|
|
132
136
|
|
|
@@ -155,6 +159,10 @@ __all__ = [
|
|
|
155
159
|
"ArtifactSource",
|
|
156
160
|
"InMemoryArtifactBackend",
|
|
157
161
|
|
|
162
|
+
# HITL backend
|
|
163
|
+
"HITLBackend",
|
|
164
|
+
"InMemoryHITLBackend",
|
|
165
|
+
|
|
158
166
|
# State backend (key-value)
|
|
159
167
|
"StateBackend",
|
|
160
168
|
"StateStore",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""In-memory HITL backend implementation."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import time
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class InMemoryHITLBackend:
|
|
9
|
+
"""In-memory HITL backend for development/testing."""
|
|
10
|
+
|
|
11
|
+
def __init__(self) -> None:
|
|
12
|
+
self._records: dict[str, dict[str, Any]] = {}
|
|
13
|
+
|
|
14
|
+
async def create(
|
|
15
|
+
self,
|
|
16
|
+
*,
|
|
17
|
+
hitl_id: str,
|
|
18
|
+
hitl_type: str,
|
|
19
|
+
session_id: str,
|
|
20
|
+
invocation_id: str,
|
|
21
|
+
data: dict[str, Any] | None = None,
|
|
22
|
+
metadata: dict[str, Any] | None = None,
|
|
23
|
+
block_id: str | None = None,
|
|
24
|
+
resume_mode: str = "response",
|
|
25
|
+
tool_state: dict[str, Any] | None = None,
|
|
26
|
+
checkpoint_id: str | None = None,
|
|
27
|
+
tool_name: str | None = None,
|
|
28
|
+
tool_call_id: str | None = None,
|
|
29
|
+
node_id: str | None = None,
|
|
30
|
+
expires_at: int | None = None,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""Create a new HITL record."""
|
|
33
|
+
now = int(time.time())
|
|
34
|
+
self._records[hitl_id] = {
|
|
35
|
+
"hitl_id": hitl_id,
|
|
36
|
+
"hitl_type": hitl_type,
|
|
37
|
+
"session_id": session_id,
|
|
38
|
+
"invocation_id": invocation_id,
|
|
39
|
+
"data": data or {},
|
|
40
|
+
"metadata": metadata or {},
|
|
41
|
+
"block_id": block_id,
|
|
42
|
+
"status": "pending",
|
|
43
|
+
"resume_mode": resume_mode,
|
|
44
|
+
"tool_state": tool_state,
|
|
45
|
+
"checkpoint_id": checkpoint_id,
|
|
46
|
+
"tool_name": tool_name,
|
|
47
|
+
"tool_call_id": tool_call_id,
|
|
48
|
+
"node_id": node_id,
|
|
49
|
+
"expires_at": expires_at,
|
|
50
|
+
"user_response": None,
|
|
51
|
+
"responded_at": None,
|
|
52
|
+
"created_at": now,
|
|
53
|
+
"updated_at": now,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async def get(self, hitl_id: str) -> dict[str, Any] | None:
|
|
57
|
+
"""Get HITL record by ID."""
|
|
58
|
+
return self._records.get(hitl_id)
|
|
59
|
+
|
|
60
|
+
async def respond(
|
|
61
|
+
self,
|
|
62
|
+
hitl_id: str,
|
|
63
|
+
user_response: dict[str, Any],
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Record user response to HITL."""
|
|
66
|
+
if hitl_id in self._records:
|
|
67
|
+
now = int(time.time())
|
|
68
|
+
self._records[hitl_id]["status"] = "completed"
|
|
69
|
+
self._records[hitl_id]["user_response"] = user_response
|
|
70
|
+
self._records[hitl_id]["responded_at"] = now
|
|
71
|
+
self._records[hitl_id]["updated_at"] = now
|
|
72
|
+
|
|
73
|
+
async def cancel(self, hitl_id: str) -> None:
|
|
74
|
+
"""Cancel a pending HITL request."""
|
|
75
|
+
if hitl_id in self._records:
|
|
76
|
+
self._records[hitl_id]["status"] = "cancelled"
|
|
77
|
+
self._records[hitl_id]["updated_at"] = int(time.time())
|
|
78
|
+
|
|
79
|
+
async def get_pending_by_session(
|
|
80
|
+
self,
|
|
81
|
+
session_id: str,
|
|
82
|
+
) -> list[dict[str, Any]]:
|
|
83
|
+
"""Get all pending HITL for a session."""
|
|
84
|
+
return [
|
|
85
|
+
r for r in self._records.values()
|
|
86
|
+
if r["session_id"] == session_id and r["status"] == "pending"
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
async def get_pending_by_invocation(
|
|
90
|
+
self,
|
|
91
|
+
invocation_id: str,
|
|
92
|
+
) -> list[dict[str, Any]]:
|
|
93
|
+
"""Get all pending HITL for an invocation."""
|
|
94
|
+
return [
|
|
95
|
+
r for r in self._records.values()
|
|
96
|
+
if r["invocation_id"] == invocation_id and r["status"] == "pending"
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
__all__ = ["InMemoryHITLBackend"]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""HITL backend types and protocols."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from typing import Any, Protocol, runtime_checkable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@runtime_checkable
|
|
8
|
+
class HITLBackend(Protocol):
|
|
9
|
+
"""Protocol for HITL (Human-in-the-Loop) persistence.
|
|
10
|
+
|
|
11
|
+
Stores HITL requests and user responses for:
|
|
12
|
+
- ask_user, confirm, permission requests
|
|
13
|
+
- External auth callbacks
|
|
14
|
+
- Workflow human tasks
|
|
15
|
+
|
|
16
|
+
Example usage:
|
|
17
|
+
# Create HITL request
|
|
18
|
+
await backend.create(
|
|
19
|
+
hitl_id="hitl_123",
|
|
20
|
+
hitl_type="ask_user",
|
|
21
|
+
session_id="sess_456",
|
|
22
|
+
invocation_id="inv_789",
|
|
23
|
+
data={"message": "What do you want?", "options": ["A", "B"]},
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Get HITL
|
|
27
|
+
hitl = await backend.get("hitl_123")
|
|
28
|
+
|
|
29
|
+
# Respond
|
|
30
|
+
await backend.respond("hitl_123", {"answer": "A"})
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
async def create(
|
|
34
|
+
self,
|
|
35
|
+
*,
|
|
36
|
+
hitl_id: str,
|
|
37
|
+
hitl_type: str,
|
|
38
|
+
session_id: str,
|
|
39
|
+
invocation_id: str,
|
|
40
|
+
data: dict[str, Any] | None = None,
|
|
41
|
+
metadata: dict[str, Any] | None = None,
|
|
42
|
+
block_id: str | None = None,
|
|
43
|
+
resume_mode: str = "response",
|
|
44
|
+
tool_state: dict[str, Any] | None = None,
|
|
45
|
+
checkpoint_id: str | None = None,
|
|
46
|
+
tool_name: str | None = None,
|
|
47
|
+
tool_call_id: str | None = None,
|
|
48
|
+
node_id: str | None = None,
|
|
49
|
+
expires_at: int | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Create a new HITL record.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
hitl_id: Unique HITL identifier
|
|
55
|
+
hitl_type: Type of HITL (ask_user, confirm, permission, etc.)
|
|
56
|
+
session_id: Parent session ID
|
|
57
|
+
invocation_id: Parent invocation ID
|
|
58
|
+
data: Type-specific data (message, options, etc.)
|
|
59
|
+
metadata: Additional metadata
|
|
60
|
+
block_id: Associated UI block ID
|
|
61
|
+
resume_mode: How to resume ("response" or "continuation")
|
|
62
|
+
tool_state: Tool internal state for continuation
|
|
63
|
+
checkpoint_id: Checkpoint ID for continuation
|
|
64
|
+
tool_name: Tool that triggered HITL
|
|
65
|
+
tool_call_id: Tool call ID
|
|
66
|
+
node_id: Workflow node ID
|
|
67
|
+
expires_at: Expiration timestamp (unix)
|
|
68
|
+
"""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
async def get(self, hitl_id: str) -> dict[str, Any] | None:
|
|
72
|
+
"""Get HITL record by ID.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
hitl_id: HITL identifier
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
HITL data dict or None if not found
|
|
79
|
+
"""
|
|
80
|
+
...
|
|
81
|
+
|
|
82
|
+
async def respond(
|
|
83
|
+
self,
|
|
84
|
+
hitl_id: str,
|
|
85
|
+
user_response: dict[str, Any],
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Record user response to HITL.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
hitl_id: HITL identifier
|
|
91
|
+
user_response: User's response data
|
|
92
|
+
"""
|
|
93
|
+
...
|
|
94
|
+
|
|
95
|
+
async def cancel(self, hitl_id: str) -> None:
|
|
96
|
+
"""Cancel a pending HITL request.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
hitl_id: HITL identifier
|
|
100
|
+
"""
|
|
101
|
+
...
|
|
102
|
+
|
|
103
|
+
async def get_pending_by_session(
|
|
104
|
+
self,
|
|
105
|
+
session_id: str,
|
|
106
|
+
) -> list[dict[str, Any]]:
|
|
107
|
+
"""Get all pending HITL for a session.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
session_id: Session ID
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
List of pending HITL records
|
|
114
|
+
"""
|
|
115
|
+
...
|
|
116
|
+
|
|
117
|
+
async def get_pending_by_invocation(
|
|
118
|
+
self,
|
|
119
|
+
invocation_id: str,
|
|
120
|
+
) -> list[dict[str, Any]]:
|
|
121
|
+
"""Get all pending HITL for an invocation.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
invocation_id: Invocation ID
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
List of pending HITL records
|
|
128
|
+
"""
|
|
129
|
+
...
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
__all__ = ["HITLBackend"]
|
aury/agents/core/base.py
CHANGED
|
@@ -76,6 +76,11 @@ class AgentConfig:
|
|
|
76
76
|
# Tool injection mode
|
|
77
77
|
tool_mode: ToolInjectionMode = ToolInjectionMode.FUNCTION_CALL
|
|
78
78
|
|
|
79
|
+
# HITL state persistence (for resume support)
|
|
80
|
+
# If True, save agent_state when suspended for later resume
|
|
81
|
+
# If False, skip state persistence (lighter weight, but can't resume)
|
|
82
|
+
persist_hitl_state: bool = False
|
|
83
|
+
|
|
79
84
|
# Extra metadata for agent-specific config
|
|
80
85
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
81
86
|
|
aury/agents/core/context.py
CHANGED
aury/agents/core/signals.py
CHANGED
|
@@ -40,54 +40,74 @@ class HITLSuspend(SuspendSignal):
|
|
|
40
40
|
1. Display the request to user
|
|
41
41
|
2. Resume execution after user responds
|
|
42
42
|
|
|
43
|
+
Supports two resume modes:
|
|
44
|
+
- "response": (Default) User response is returned to LLM as tool output.
|
|
45
|
+
- "continuation": Tool execution is resumed from checkpoint,
|
|
46
|
+
user response is passed to tool to continue processing.
|
|
47
|
+
|
|
43
48
|
Attributes:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
options: Optional list of choices
|
|
49
|
+
hitl_id: Unique ID for matching response
|
|
50
|
+
hitl_type: Type of HITL (ask_user, confirm, external_auth, etc.)
|
|
51
|
+
data: Type-specific data (message, options, etc.)
|
|
48
52
|
node_id: Workflow node ID if triggered from workflow
|
|
49
53
|
tool_name: Tool name if triggered from tool
|
|
50
54
|
block_id: Associated UI block ID
|
|
51
55
|
metadata: Additional context
|
|
56
|
+
resume_mode: How to resume after user responds
|
|
57
|
+
tool_state: Tool internal state for continuation mode
|
|
58
|
+
checkpoint_id: Unique ID for tool checkpoint
|
|
52
59
|
"""
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
options: list[str] | None = None
|
|
60
|
+
hitl_id: str
|
|
61
|
+
hitl_type: str = "ask_user"
|
|
62
|
+
data: dict[str, Any] = field(default_factory=dict) # Type-specific: {message, options, ...}
|
|
57
63
|
node_id: str | None = None
|
|
58
64
|
tool_name: str | None = None
|
|
59
65
|
block_id: str | None = None
|
|
60
66
|
metadata: dict[str, Any] = field(default_factory=dict)
|
|
61
67
|
|
|
68
|
+
# Continuation support
|
|
69
|
+
resume_mode: str = "response" # "response" | "continuation"
|
|
70
|
+
tool_state: dict[str, Any] | None = None # Internal state for continuation
|
|
71
|
+
checkpoint_id: str | None = None # Checkpoint ID for restoration
|
|
72
|
+
|
|
62
73
|
def __post_init__(self):
|
|
63
74
|
# Initialize BaseException with a message
|
|
64
|
-
super().__init__(f"HITL suspend: {self.
|
|
75
|
+
super().__init__(f"HITL suspend: {self.hitl_type} ({self.hitl_id})")
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def is_continuation(self) -> bool:
|
|
79
|
+
"""Check if this suspend requires continuation mode."""
|
|
80
|
+
return self.resume_mode == "continuation"
|
|
65
81
|
|
|
66
82
|
def to_dict(self) -> dict[str, Any]:
|
|
67
83
|
"""Convert to dictionary for storage/transmission."""
|
|
68
84
|
return {
|
|
69
|
-
"
|
|
70
|
-
"
|
|
71
|
-
"
|
|
72
|
-
"options": self.options,
|
|
85
|
+
"hitl_id": self.hitl_id,
|
|
86
|
+
"hitl_type": self.hitl_type,
|
|
87
|
+
"data": self.data,
|
|
73
88
|
"node_id": self.node_id,
|
|
74
89
|
"tool_name": self.tool_name,
|
|
75
90
|
"block_id": self.block_id,
|
|
76
91
|
"metadata": self.metadata,
|
|
92
|
+
"resume_mode": self.resume_mode,
|
|
93
|
+
"tool_state": self.tool_state,
|
|
94
|
+
"checkpoint_id": self.checkpoint_id,
|
|
77
95
|
}
|
|
78
96
|
|
|
79
97
|
@classmethod
|
|
80
98
|
def from_dict(cls, data: dict[str, Any]) -> "HITLSuspend":
|
|
81
99
|
"""Create from dictionary."""
|
|
82
100
|
return cls(
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
options=data.get("options"),
|
|
101
|
+
hitl_id=data["hitl_id"],
|
|
102
|
+
hitl_type=data.get("hitl_type", "ask_user"),
|
|
103
|
+
data=data.get("data", {}),
|
|
87
104
|
node_id=data.get("node_id"),
|
|
88
105
|
tool_name=data.get("tool_name"),
|
|
89
106
|
block_id=data.get("block_id"),
|
|
90
107
|
metadata=data.get("metadata", {}),
|
|
108
|
+
resume_mode=data.get("resume_mode", "response"),
|
|
109
|
+
tool_state=data.get("tool_state"),
|
|
110
|
+
checkpoint_id=data.get("checkpoint_id"),
|
|
91
111
|
)
|
|
92
112
|
|
|
93
113
|
|
|
@@ -32,7 +32,6 @@ from .block import (
|
|
|
32
32
|
thinking_delta,
|
|
33
33
|
tool_use_block,
|
|
34
34
|
tool_use_patch,
|
|
35
|
-
tool_result_block,
|
|
36
35
|
error_block,
|
|
37
36
|
)
|
|
38
37
|
from .message import (
|
|
@@ -86,7 +85,6 @@ __all__ = [
|
|
|
86
85
|
"thinking_delta",
|
|
87
86
|
"tool_use_block",
|
|
88
87
|
"tool_use_patch",
|
|
89
|
-
"tool_result_block",
|
|
90
88
|
"error_block",
|
|
91
89
|
# Message
|
|
92
90
|
"MessageRole",
|
aury/agents/core/types/block.py
CHANGED
|
@@ -27,8 +27,11 @@ class BlockKind(str, Enum):
|
|
|
27
27
|
THINKING = "thinking" # LLM reasoning (collapsible)
|
|
28
28
|
|
|
29
29
|
# === Tool Execution ===
|
|
30
|
-
TOOL_USE
|
|
31
|
-
|
|
30
|
+
# TOOL_USE manages entire tool lifecycle via PATCH:
|
|
31
|
+
# APPLY → {name, call_id, arguments, status: "pending"}
|
|
32
|
+
# PATCH → {status: "running", progress: ...} (tool can emit during execution)
|
|
33
|
+
# PATCH → {status: "success"} (framework emits on completion)
|
|
34
|
+
TOOL_USE = "tool_use"
|
|
32
35
|
|
|
33
36
|
# === Agent ===
|
|
34
37
|
SUB_AGENT = "sub_agent" # Sub-agent delegation
|
|
@@ -40,7 +43,7 @@ class BlockKind(str, Enum):
|
|
|
40
43
|
NODE = "node" # Workflow node execution block
|
|
41
44
|
|
|
42
45
|
# === HITL ===
|
|
43
|
-
|
|
46
|
+
HITL = "hitl" # Human-in-the-loop (ask_user, confirm, permission, etc.)
|
|
44
47
|
|
|
45
48
|
# === Control Flow ===
|
|
46
49
|
YIELD = "yield" # Return control to parent
|
|
@@ -700,26 +703,6 @@ def tool_use_patch(block_id: str, args: dict[str, Any]) -> BlockEvent:
|
|
|
700
703
|
)
|
|
701
704
|
|
|
702
705
|
|
|
703
|
-
def tool_result_block(
|
|
704
|
-
block_id: str,
|
|
705
|
-
tool_use_id: str,
|
|
706
|
-
content: str,
|
|
707
|
-
is_error: bool = False,
|
|
708
|
-
parent_id: str | None = None,
|
|
709
|
-
) -> BlockEvent:
|
|
710
|
-
"""Create a tool result block."""
|
|
711
|
-
return BlockEvent(
|
|
712
|
-
block_id=block_id,
|
|
713
|
-
parent_id=parent_id,
|
|
714
|
-
kind=BlockKind.TOOL_RESULT,
|
|
715
|
-
op=BlockOp.APPLY,
|
|
716
|
-
data={
|
|
717
|
-
"tool_use_id": tool_use_id,
|
|
718
|
-
"content": content,
|
|
719
|
-
"is_error": is_error,
|
|
720
|
-
},
|
|
721
|
-
)
|
|
722
|
-
|
|
723
706
|
|
|
724
707
|
def error_block(
|
|
725
708
|
block_id: str,
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
"""Session and Invocation data structures."""
|
|
2
2
|
from __future__ import annotations
|
|
3
3
|
|
|
4
|
+
import random
|
|
5
|
+
import time
|
|
4
6
|
from dataclasses import dataclass, field
|
|
5
7
|
from datetime import datetime
|
|
6
8
|
from enum import Enum
|
|
7
9
|
from typing import Any
|
|
8
|
-
from uuid import uuid4
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def generate_id(prefix: str = "") -> str:
|
|
12
|
-
"""Generate a unique ID with optional prefix.
|
|
13
|
-
|
|
13
|
+
"""Generate a unique ID with optional prefix.
|
|
14
|
+
|
|
15
|
+
Uses time + random instead of uuid4 to avoid blocking on os.urandom().
|
|
16
|
+
"""
|
|
17
|
+
# 时间戳低 32 位 + 48 位随机数,避免 os.urandom 阻塞
|
|
18
|
+
ts = int(time.time() * 1000) & 0xFFFFFFFF
|
|
19
|
+
rand = random.getrandbits(48)
|
|
20
|
+
uid = f"{ts:08x}{rand:012x}"[:12]
|
|
14
21
|
return f"{prefix}_{uid}" if prefix else uid
|
|
15
22
|
|
|
16
23
|
|