openhands-sdk 1.0.0__tar.gz
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.
- openhands_sdk-1.0.0/PKG-INFO +16 -0
- openhands_sdk-1.0.0/openhands/sdk/__init__.py +97 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/__init__.py +8 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/agent.py +456 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/base.py +420 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/in_context_learning_example.j2 +169 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/in_context_learning_example_suffix.j2 +3 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/security_policy.j2 +22 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/security_risk_assessment.j2 +21 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/system_prompt.j2 +113 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/system_prompt_interactive.j2 +14 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/system_prompt_long_horizon.j2 +40 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/system_prompt_planning.j2 +40 -0
- openhands_sdk-1.0.0/openhands/sdk/agent/prompts/system_prompt_tech_philosophy.j2 +122 -0
- openhands_sdk-1.0.0/openhands/sdk/context/__init__.py +26 -0
- openhands_sdk-1.0.0/openhands/sdk/context/agent_context.py +177 -0
- openhands_sdk-1.0.0/openhands/sdk/context/condenser/__init__.py +18 -0
- openhands_sdk-1.0.0/openhands/sdk/context/condenser/base.py +95 -0
- openhands_sdk-1.0.0/openhands/sdk/context/condenser/llm_summarizing_condenser.py +84 -0
- openhands_sdk-1.0.0/openhands/sdk/context/condenser/no_op_condenser.py +13 -0
- openhands_sdk-1.0.0/openhands/sdk/context/condenser/pipeline_condenser.py +55 -0
- openhands_sdk-1.0.0/openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +59 -0
- openhands_sdk-1.0.0/openhands/sdk/context/prompts/__init__.py +6 -0
- openhands_sdk-1.0.0/openhands/sdk/context/prompts/prompt.py +74 -0
- openhands_sdk-1.0.0/openhands/sdk/context/prompts/templates/skill_knowledge_info.j2 +8 -0
- openhands_sdk-1.0.0/openhands/sdk/context/prompts/templates/system_message_suffix.j2 +16 -0
- openhands_sdk-1.0.0/openhands/sdk/context/skills/__init__.py +24 -0
- openhands_sdk-1.0.0/openhands/sdk/context/skills/exceptions.py +11 -0
- openhands_sdk-1.0.0/openhands/sdk/context/skills/skill.py +359 -0
- openhands_sdk-1.0.0/openhands/sdk/context/skills/trigger.py +36 -0
- openhands_sdk-1.0.0/openhands/sdk/context/skills/types.py +48 -0
- openhands_sdk-1.0.0/openhands/sdk/context/view.py +243 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/__init__.py +32 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/base.py +213 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/conversation.py +123 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/conversation_stats.py +101 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/event_store.py +157 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/events_list_base.py +17 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/exceptions.py +25 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/fifo_lock.py +127 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/impl/__init__.py +5 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/impl/local_conversation.py +436 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/impl/remote_conversation.py +678 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/persistence_const.py +9 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/response_utils.py +41 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/secret_registry.py +131 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/secret_source.py +86 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/serialization_diff.py +0 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/state.py +336 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/stuck_detector.py +275 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/title_utils.py +191 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/types.py +10 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/visualizer/__init__.py +12 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/visualizer/base.py +86 -0
- openhands_sdk-1.0.0/openhands/sdk/conversation/visualizer/default.py +318 -0
- openhands_sdk-1.0.0/openhands/sdk/event/__init__.py +38 -0
- openhands_sdk-1.0.0/openhands/sdk/event/base.py +149 -0
- openhands_sdk-1.0.0/openhands/sdk/event/condenser.py +71 -0
- openhands_sdk-1.0.0/openhands/sdk/event/conversation_state.py +96 -0
- openhands_sdk-1.0.0/openhands/sdk/event/llm_convertible/__init__.py +20 -0
- openhands_sdk-1.0.0/openhands/sdk/event/llm_convertible/action.py +139 -0
- openhands_sdk-1.0.0/openhands/sdk/event/llm_convertible/message.py +135 -0
- openhands_sdk-1.0.0/openhands/sdk/event/llm_convertible/observation.py +141 -0
- openhands_sdk-1.0.0/openhands/sdk/event/llm_convertible/system.py +65 -0
- openhands_sdk-1.0.0/openhands/sdk/event/types.py +11 -0
- openhands_sdk-1.0.0/openhands/sdk/event/user_action.py +21 -0
- openhands_sdk-1.0.0/openhands/sdk/git/exceptions.py +43 -0
- openhands_sdk-1.0.0/openhands/sdk/git/git_changes.py +249 -0
- openhands_sdk-1.0.0/openhands/sdk/git/git_diff.py +129 -0
- openhands_sdk-1.0.0/openhands/sdk/git/models.py +21 -0
- openhands_sdk-1.0.0/openhands/sdk/git/utils.py +189 -0
- openhands_sdk-1.0.0/openhands/sdk/io/__init__.py +6 -0
- openhands_sdk-1.0.0/openhands/sdk/io/base.py +48 -0
- openhands_sdk-1.0.0/openhands/sdk/io/local.py +82 -0
- openhands_sdk-1.0.0/openhands/sdk/io/memory.py +54 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/__init__.py +42 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/exceptions/__init__.py +45 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/exceptions/classifier.py +50 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/exceptions/mapping.py +54 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/exceptions/types.py +101 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/llm.py +1095 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/llm_registry.py +153 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/llm_response.py +59 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/message.py +585 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/mixins/fn_call_converter.py +1050 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/mixins/non_native_fc.py +93 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/options/__init__.py +1 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/options/chat_options.py +87 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/options/common.py +19 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/options/responses_options.py +57 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/router/__init__.py +10 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/router/base.py +114 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/router/impl/multimodal.py +76 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/router/impl/random.py +22 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/utils/metrics.py +312 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/utils/model_features.py +114 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/utils/retry_mixin.py +123 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/utils/telemetry.py +317 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/utils/unverified_models.py +156 -0
- openhands_sdk-1.0.0/openhands/sdk/llm/utils/verified_models.py +61 -0
- openhands_sdk-1.0.0/openhands/sdk/logger/__init__.py +22 -0
- openhands_sdk-1.0.0/openhands/sdk/logger/logger.py +188 -0
- openhands_sdk-1.0.0/openhands/sdk/logger/rolling.py +113 -0
- openhands_sdk-1.0.0/openhands/sdk/mcp/__init__.py +21 -0
- openhands_sdk-1.0.0/openhands/sdk/mcp/client.py +76 -0
- openhands_sdk-1.0.0/openhands/sdk/mcp/definition.py +106 -0
- openhands_sdk-1.0.0/openhands/sdk/mcp/tool.py +271 -0
- openhands_sdk-1.0.0/openhands/sdk/mcp/utils.py +63 -0
- openhands_sdk-1.0.0/openhands/sdk/observability/__init__.py +4 -0
- openhands_sdk-1.0.0/openhands/sdk/observability/laminar.py +166 -0
- openhands_sdk-1.0.0/openhands/sdk/observability/utils.py +20 -0
- openhands_sdk-1.0.0/openhands/sdk/py.typed +0 -0
- openhands_sdk-1.0.0/openhands/sdk/security/__init__.py +6 -0
- openhands_sdk-1.0.0/openhands/sdk/security/analyzer.py +111 -0
- openhands_sdk-1.0.0/openhands/sdk/security/confirmation_policy.py +61 -0
- openhands_sdk-1.0.0/openhands/sdk/security/llm_analyzer.py +29 -0
- openhands_sdk-1.0.0/openhands/sdk/security/risk.py +100 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/__init__.py +34 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/builtins/__init__.py +34 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/builtins/finish.py +106 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/builtins/think.py +117 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/registry.py +161 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/schema.py +276 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/spec.py +39 -0
- openhands_sdk-1.0.0/openhands/sdk/tool/tool.py +464 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/__init__.py +14 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/async_executor.py +106 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/async_utils.py +39 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/cipher.py +68 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/command.py +90 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/json.py +48 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/models.py +302 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/pydantic_diff.py +85 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/pydantic_secrets.py +64 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/truncate.py +44 -0
- openhands_sdk-1.0.0/openhands/sdk/utils/visualize.py +23 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/__init__.py +15 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/base.py +143 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/local.py +183 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/models.py +29 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/remote/__init__.py +8 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/remote/async_remote_workspace.py +128 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/remote/base.py +149 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/remote/remote_workspace_mixin.py +321 -0
- openhands_sdk-1.0.0/openhands/sdk/workspace/workspace.py +49 -0
- openhands_sdk-1.0.0/openhands_sdk.egg-info/PKG-INFO +16 -0
- openhands_sdk-1.0.0/openhands_sdk.egg-info/SOURCES.txt +294 -0
- openhands_sdk-1.0.0/openhands_sdk.egg-info/dependency_links.txt +1 -0
- openhands_sdk-1.0.0/openhands_sdk.egg-info/requires.txt +12 -0
- openhands_sdk-1.0.0/openhands_sdk.egg-info/top_level.txt +1 -0
- openhands_sdk-1.0.0/pyproject.toml +34 -0
- openhands_sdk-1.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openhands-sdk
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: OpenHands SDK - Core functionality for building AI agents
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: fastmcp>=2.11.3
|
|
7
|
+
Requires-Dist: httpx>=0.27.0
|
|
8
|
+
Requires-Dist: litellm>=1.77.7.dev9
|
|
9
|
+
Requires-Dist: pydantic>=2.11.7
|
|
10
|
+
Requires-Dist: python-frontmatter>=1.1.0
|
|
11
|
+
Requires-Dist: python-json-logger>=3.3.0
|
|
12
|
+
Requires-Dist: tenacity>=9.1.2
|
|
13
|
+
Requires-Dist: websockets>=12
|
|
14
|
+
Requires-Dist: lmnr>=0.7.20
|
|
15
|
+
Provides-Extra: boto3
|
|
16
|
+
Requires-Dist: boto3>=1.35.0; extra == "boto3"
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
|
|
3
|
+
from openhands.sdk.agent import Agent, AgentBase
|
|
4
|
+
from openhands.sdk.context import AgentContext
|
|
5
|
+
from openhands.sdk.context.condenser import (
|
|
6
|
+
LLMSummarizingCondenser,
|
|
7
|
+
)
|
|
8
|
+
from openhands.sdk.conversation import (
|
|
9
|
+
BaseConversation,
|
|
10
|
+
Conversation,
|
|
11
|
+
ConversationCallbackType,
|
|
12
|
+
LocalConversation,
|
|
13
|
+
RemoteConversation,
|
|
14
|
+
)
|
|
15
|
+
from openhands.sdk.conversation.conversation_stats import ConversationStats
|
|
16
|
+
from openhands.sdk.event import Event, LLMConvertibleEvent
|
|
17
|
+
from openhands.sdk.event.llm_convertible import MessageEvent
|
|
18
|
+
from openhands.sdk.io import FileStore, LocalFileStore
|
|
19
|
+
from openhands.sdk.llm import (
|
|
20
|
+
LLM,
|
|
21
|
+
ImageContent,
|
|
22
|
+
LLMRegistry,
|
|
23
|
+
Message,
|
|
24
|
+
RedactedThinkingBlock,
|
|
25
|
+
RegistryEvent,
|
|
26
|
+
TextContent,
|
|
27
|
+
ThinkingBlock,
|
|
28
|
+
)
|
|
29
|
+
from openhands.sdk.logger import get_logger
|
|
30
|
+
from openhands.sdk.mcp import (
|
|
31
|
+
MCPClient,
|
|
32
|
+
MCPToolDefinition,
|
|
33
|
+
MCPToolObservation,
|
|
34
|
+
create_mcp_tools,
|
|
35
|
+
)
|
|
36
|
+
from openhands.sdk.tool import (
|
|
37
|
+
Action,
|
|
38
|
+
Observation,
|
|
39
|
+
Tool,
|
|
40
|
+
ToolDefinition,
|
|
41
|
+
list_registered_tools,
|
|
42
|
+
register_tool,
|
|
43
|
+
resolve_tool,
|
|
44
|
+
)
|
|
45
|
+
from openhands.sdk.workspace import (
|
|
46
|
+
LocalWorkspace,
|
|
47
|
+
RemoteWorkspace,
|
|
48
|
+
Workspace,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
__version__ = version("openhands-sdk")
|
|
54
|
+
except PackageNotFoundError:
|
|
55
|
+
__version__ = "0.0.0" # fallback for editable/unbuilt environments
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"LLM",
|
|
59
|
+
"LLMRegistry",
|
|
60
|
+
"ConversationStats",
|
|
61
|
+
"RegistryEvent",
|
|
62
|
+
"Message",
|
|
63
|
+
"TextContent",
|
|
64
|
+
"ImageContent",
|
|
65
|
+
"ThinkingBlock",
|
|
66
|
+
"RedactedThinkingBlock",
|
|
67
|
+
"Tool",
|
|
68
|
+
"ToolDefinition",
|
|
69
|
+
"AgentBase",
|
|
70
|
+
"Agent",
|
|
71
|
+
"Action",
|
|
72
|
+
"Observation",
|
|
73
|
+
"MCPClient",
|
|
74
|
+
"MCPToolDefinition",
|
|
75
|
+
"MCPToolObservation",
|
|
76
|
+
"MessageEvent",
|
|
77
|
+
"create_mcp_tools",
|
|
78
|
+
"get_logger",
|
|
79
|
+
"Conversation",
|
|
80
|
+
"BaseConversation",
|
|
81
|
+
"LocalConversation",
|
|
82
|
+
"RemoteConversation",
|
|
83
|
+
"ConversationCallbackType",
|
|
84
|
+
"Event",
|
|
85
|
+
"LLMConvertibleEvent",
|
|
86
|
+
"AgentContext",
|
|
87
|
+
"LLMSummarizingCondenser",
|
|
88
|
+
"FileStore",
|
|
89
|
+
"LocalFileStore",
|
|
90
|
+
"register_tool",
|
|
91
|
+
"resolve_tool",
|
|
92
|
+
"list_registered_tools",
|
|
93
|
+
"Workspace",
|
|
94
|
+
"LocalWorkspace",
|
|
95
|
+
"RemoteWorkspace",
|
|
96
|
+
"__version__",
|
|
97
|
+
]
|
|
@@ -0,0 +1,456 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from pydantic import ValidationError
|
|
4
|
+
|
|
5
|
+
import openhands.sdk.security.risk as risk
|
|
6
|
+
from openhands.sdk.agent.base import AgentBase
|
|
7
|
+
from openhands.sdk.context.view import View
|
|
8
|
+
from openhands.sdk.conversation import (
|
|
9
|
+
ConversationCallbackType,
|
|
10
|
+
ConversationState,
|
|
11
|
+
LocalConversation,
|
|
12
|
+
)
|
|
13
|
+
from openhands.sdk.conversation.state import ConversationExecutionStatus
|
|
14
|
+
from openhands.sdk.event import (
|
|
15
|
+
ActionEvent,
|
|
16
|
+
AgentErrorEvent,
|
|
17
|
+
LLMConvertibleEvent,
|
|
18
|
+
MessageEvent,
|
|
19
|
+
ObservationEvent,
|
|
20
|
+
SystemPromptEvent,
|
|
21
|
+
)
|
|
22
|
+
from openhands.sdk.event.condenser import Condensation, CondensationRequest
|
|
23
|
+
from openhands.sdk.llm import (
|
|
24
|
+
Message,
|
|
25
|
+
MessageToolCall,
|
|
26
|
+
ReasoningItemModel,
|
|
27
|
+
RedactedThinkingBlock,
|
|
28
|
+
TextContent,
|
|
29
|
+
ThinkingBlock,
|
|
30
|
+
)
|
|
31
|
+
from openhands.sdk.llm.exceptions import (
|
|
32
|
+
FunctionCallValidationError,
|
|
33
|
+
LLMContextWindowExceedError,
|
|
34
|
+
)
|
|
35
|
+
from openhands.sdk.logger import get_logger
|
|
36
|
+
from openhands.sdk.observability.laminar import (
|
|
37
|
+
maybe_init_laminar,
|
|
38
|
+
observe,
|
|
39
|
+
should_enable_observability,
|
|
40
|
+
)
|
|
41
|
+
from openhands.sdk.observability.utils import extract_action_name
|
|
42
|
+
from openhands.sdk.security.confirmation_policy import NeverConfirm
|
|
43
|
+
from openhands.sdk.security.llm_analyzer import LLMSecurityAnalyzer
|
|
44
|
+
from openhands.sdk.tool import (
|
|
45
|
+
Action,
|
|
46
|
+
Observation,
|
|
47
|
+
)
|
|
48
|
+
from openhands.sdk.tool.builtins import (
|
|
49
|
+
FinishAction,
|
|
50
|
+
FinishTool,
|
|
51
|
+
ThinkAction,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
logger = get_logger(__name__)
|
|
56
|
+
maybe_init_laminar()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Agent(AgentBase):
|
|
60
|
+
"""Main agent implementation for OpenHands.
|
|
61
|
+
|
|
62
|
+
The Agent class provides the core functionality for running AI agents that can
|
|
63
|
+
interact with tools, process messages, and execute actions. It inherits from
|
|
64
|
+
AgentBase and implements the agent execution logic.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> from openhands.sdk import LLM, Agent, Tool
|
|
68
|
+
>>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key"))
|
|
69
|
+
>>> tools = [Tool(name="TerminalTool"), Tool(name="FileEditorTool")]
|
|
70
|
+
>>> agent = Agent(llm=llm, tools=tools)
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def _add_security_risk_prediction(self) -> bool:
|
|
75
|
+
return isinstance(self.security_analyzer, LLMSecurityAnalyzer)
|
|
76
|
+
|
|
77
|
+
def init_state(
|
|
78
|
+
self,
|
|
79
|
+
state: ConversationState,
|
|
80
|
+
on_event: ConversationCallbackType,
|
|
81
|
+
) -> None:
|
|
82
|
+
super().init_state(state, on_event=on_event)
|
|
83
|
+
# TODO(openhands): we should add test to test this init_state will actually
|
|
84
|
+
# modify state in-place
|
|
85
|
+
|
|
86
|
+
# Validate security analyzer configuration once during initialization
|
|
87
|
+
if self._add_security_risk_prediction and isinstance(
|
|
88
|
+
state.confirmation_policy, NeverConfirm
|
|
89
|
+
):
|
|
90
|
+
# If security analyzer is enabled, we always need a policy that is not
|
|
91
|
+
# NeverConfirm, otherwise we are just predicting risks without using them,
|
|
92
|
+
# and waste tokens!
|
|
93
|
+
logger.warning(
|
|
94
|
+
"LLM security analyzer is enabled but confirmation "
|
|
95
|
+
"policy is set to NeverConfirm"
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
llm_convertible_messages = [
|
|
99
|
+
event for event in state.events if isinstance(event, LLMConvertibleEvent)
|
|
100
|
+
]
|
|
101
|
+
if len(llm_convertible_messages) == 0:
|
|
102
|
+
# Prepare system message
|
|
103
|
+
event = SystemPromptEvent(
|
|
104
|
+
source="agent",
|
|
105
|
+
system_prompt=TextContent(text=self.system_message),
|
|
106
|
+
tools=[
|
|
107
|
+
t.to_openai_tool(
|
|
108
|
+
add_security_risk_prediction=self._add_security_risk_prediction
|
|
109
|
+
)
|
|
110
|
+
for t in self.tools_map.values()
|
|
111
|
+
],
|
|
112
|
+
)
|
|
113
|
+
on_event(event)
|
|
114
|
+
|
|
115
|
+
def _execute_actions(
|
|
116
|
+
self,
|
|
117
|
+
conversation: LocalConversation,
|
|
118
|
+
action_events: list[ActionEvent],
|
|
119
|
+
on_event: ConversationCallbackType,
|
|
120
|
+
):
|
|
121
|
+
for action_event in action_events:
|
|
122
|
+
self._execute_action_event(conversation, action_event, on_event=on_event)
|
|
123
|
+
|
|
124
|
+
@observe(name="agent.step", ignore_inputs=["state", "on_event"])
|
|
125
|
+
def step(
|
|
126
|
+
self,
|
|
127
|
+
conversation: LocalConversation,
|
|
128
|
+
on_event: ConversationCallbackType,
|
|
129
|
+
) -> None:
|
|
130
|
+
state = conversation.state
|
|
131
|
+
# Check for pending actions (implicit confirmation)
|
|
132
|
+
# and execute them before sampling new actions.
|
|
133
|
+
pending_actions = ConversationState.get_unmatched_actions(state.events)
|
|
134
|
+
if pending_actions:
|
|
135
|
+
logger.info(
|
|
136
|
+
"Confirmation mode: Executing %d pending action(s)",
|
|
137
|
+
len(pending_actions),
|
|
138
|
+
)
|
|
139
|
+
self._execute_actions(conversation, pending_actions, on_event)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# If a condenser is registered with the agent, we need to give it an
|
|
143
|
+
# opportunity to transform the events. This will either produce a list
|
|
144
|
+
# of events, exactly as expected, or a new condensation that needs to be
|
|
145
|
+
# processed before the agent can sample another action.
|
|
146
|
+
if self.condenser is not None:
|
|
147
|
+
view = View.from_events(state.events)
|
|
148
|
+
condensation_result = self.condenser.condense(view)
|
|
149
|
+
|
|
150
|
+
match condensation_result:
|
|
151
|
+
case View():
|
|
152
|
+
llm_convertible_events = condensation_result.events
|
|
153
|
+
|
|
154
|
+
case Condensation():
|
|
155
|
+
on_event(condensation_result)
|
|
156
|
+
return None
|
|
157
|
+
|
|
158
|
+
else:
|
|
159
|
+
llm_convertible_events = [
|
|
160
|
+
e for e in state.events if isinstance(e, LLMConvertibleEvent)
|
|
161
|
+
]
|
|
162
|
+
|
|
163
|
+
# Get LLM Response (Action)
|
|
164
|
+
_messages = LLMConvertibleEvent.events_to_messages(llm_convertible_events)
|
|
165
|
+
logger.debug(
|
|
166
|
+
"Sending messages to LLM: "
|
|
167
|
+
f"{json.dumps([m.model_dump() for m in _messages[1:]], indent=2)}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
if self.llm.uses_responses_api():
|
|
172
|
+
llm_response = self.llm.responses(
|
|
173
|
+
messages=_messages,
|
|
174
|
+
tools=list(self.tools_map.values()),
|
|
175
|
+
include=None,
|
|
176
|
+
store=False,
|
|
177
|
+
add_security_risk_prediction=self._add_security_risk_prediction,
|
|
178
|
+
extra_body=self.llm.litellm_extra_body,
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
llm_response = self.llm.completion(
|
|
182
|
+
messages=_messages,
|
|
183
|
+
tools=list(self.tools_map.values()),
|
|
184
|
+
extra_body=self.llm.litellm_extra_body,
|
|
185
|
+
add_security_risk_prediction=self._add_security_risk_prediction,
|
|
186
|
+
)
|
|
187
|
+
except FunctionCallValidationError as e:
|
|
188
|
+
logger.warning(f"LLM generated malformed function call: {e}")
|
|
189
|
+
error_message = MessageEvent(
|
|
190
|
+
source="user",
|
|
191
|
+
llm_message=Message(
|
|
192
|
+
role="user",
|
|
193
|
+
content=[TextContent(text=str(e))],
|
|
194
|
+
),
|
|
195
|
+
)
|
|
196
|
+
on_event(error_message)
|
|
197
|
+
return
|
|
198
|
+
except LLMContextWindowExceedError:
|
|
199
|
+
# If condenser is available and handles requests, trigger condensation
|
|
200
|
+
if (
|
|
201
|
+
self.condenser is not None
|
|
202
|
+
and self.condenser.handles_condensation_requests()
|
|
203
|
+
):
|
|
204
|
+
logger.warning(
|
|
205
|
+
"LLM raised context window exceeded error, triggering condensation"
|
|
206
|
+
)
|
|
207
|
+
on_event(CondensationRequest())
|
|
208
|
+
return
|
|
209
|
+
# No condenser available; re-raise for client handling
|
|
210
|
+
raise
|
|
211
|
+
|
|
212
|
+
# LLMResponse already contains the converted message and metrics snapshot
|
|
213
|
+
message: Message = llm_response.message
|
|
214
|
+
|
|
215
|
+
if message.tool_calls and len(message.tool_calls) > 0:
|
|
216
|
+
if not all(isinstance(c, TextContent) for c in message.content):
|
|
217
|
+
logger.warning(
|
|
218
|
+
"LLM returned tool calls but message content is not all "
|
|
219
|
+
"TextContent - ignoring non-text content"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Generate unique batch ID for this LLM response
|
|
223
|
+
thought_content = [c for c in message.content if isinstance(c, TextContent)]
|
|
224
|
+
|
|
225
|
+
action_events: list[ActionEvent] = []
|
|
226
|
+
for i, tool_call in enumerate(message.tool_calls):
|
|
227
|
+
action_event = self._get_action_event(
|
|
228
|
+
tool_call,
|
|
229
|
+
llm_response_id=llm_response.id,
|
|
230
|
+
on_event=on_event,
|
|
231
|
+
thought=thought_content
|
|
232
|
+
if i == 0
|
|
233
|
+
else [], # Only first gets thought
|
|
234
|
+
# Only first gets reasoning content
|
|
235
|
+
reasoning_content=message.reasoning_content if i == 0 else None,
|
|
236
|
+
# Only first gets thinking blocks
|
|
237
|
+
thinking_blocks=list(message.thinking_blocks) if i == 0 else [],
|
|
238
|
+
responses_reasoning_item=message.responses_reasoning_item
|
|
239
|
+
if i == 0
|
|
240
|
+
else None,
|
|
241
|
+
)
|
|
242
|
+
if action_event is None:
|
|
243
|
+
continue
|
|
244
|
+
action_events.append(action_event)
|
|
245
|
+
|
|
246
|
+
# Handle confirmation mode - exit early if actions need confirmation
|
|
247
|
+
if self._requires_user_confirmation(state, action_events):
|
|
248
|
+
return
|
|
249
|
+
|
|
250
|
+
if action_events:
|
|
251
|
+
self._execute_actions(conversation, action_events, on_event)
|
|
252
|
+
|
|
253
|
+
else:
|
|
254
|
+
logger.info("LLM produced a message response - awaits user input")
|
|
255
|
+
state.execution_status = ConversationExecutionStatus.FINISHED
|
|
256
|
+
msg_event = MessageEvent(
|
|
257
|
+
source="agent",
|
|
258
|
+
llm_message=message,
|
|
259
|
+
llm_response_id=llm_response.id,
|
|
260
|
+
)
|
|
261
|
+
on_event(msg_event)
|
|
262
|
+
|
|
263
|
+
def _requires_user_confirmation(
|
|
264
|
+
self, state: ConversationState, action_events: list[ActionEvent]
|
|
265
|
+
) -> bool:
|
|
266
|
+
"""
|
|
267
|
+
Decide whether user confirmation is needed to proceed.
|
|
268
|
+
|
|
269
|
+
Rules:
|
|
270
|
+
1. Confirmation mode is enabled
|
|
271
|
+
2. Every action requires confirmation
|
|
272
|
+
3. A single `FinishAction` never requires confirmation
|
|
273
|
+
4. A single `ThinkAction` never requires confirmation
|
|
274
|
+
"""
|
|
275
|
+
# A single `FinishAction` or `ThinkAction` never requires confirmation
|
|
276
|
+
if len(action_events) == 1 and isinstance(
|
|
277
|
+
action_events[0].action, (FinishAction, ThinkAction)
|
|
278
|
+
):
|
|
279
|
+
return False
|
|
280
|
+
|
|
281
|
+
# If there are no actions there is nothing to confirm
|
|
282
|
+
if len(action_events) == 0:
|
|
283
|
+
return False
|
|
284
|
+
|
|
285
|
+
# If a security analyzer is registered, use it to grab the risks of the actions
|
|
286
|
+
# involved. If not, we'll set the risks to UNKNOWN.
|
|
287
|
+
if self.security_analyzer is not None:
|
|
288
|
+
risks = [
|
|
289
|
+
risk
|
|
290
|
+
for _, risk in self.security_analyzer.analyze_pending_actions(
|
|
291
|
+
action_events
|
|
292
|
+
)
|
|
293
|
+
]
|
|
294
|
+
else:
|
|
295
|
+
risks = [risk.SecurityRisk.UNKNOWN] * len(action_events)
|
|
296
|
+
|
|
297
|
+
# Grab the confirmation policy from the state and pass in the risks.
|
|
298
|
+
if any(state.confirmation_policy.should_confirm(risk) for risk in risks):
|
|
299
|
+
state.execution_status = (
|
|
300
|
+
ConversationExecutionStatus.WAITING_FOR_CONFIRMATION
|
|
301
|
+
)
|
|
302
|
+
return True
|
|
303
|
+
|
|
304
|
+
return False
|
|
305
|
+
|
|
306
|
+
def _get_action_event(
|
|
307
|
+
self,
|
|
308
|
+
tool_call: MessageToolCall,
|
|
309
|
+
llm_response_id: str,
|
|
310
|
+
on_event: ConversationCallbackType,
|
|
311
|
+
thought: list[TextContent] = [],
|
|
312
|
+
reasoning_content: str | None = None,
|
|
313
|
+
thinking_blocks: list[ThinkingBlock | RedactedThinkingBlock] = [],
|
|
314
|
+
responses_reasoning_item: ReasoningItemModel | None = None,
|
|
315
|
+
) -> ActionEvent | None:
|
|
316
|
+
"""Converts a tool call into an ActionEvent, validating arguments.
|
|
317
|
+
|
|
318
|
+
NOTE: state will be mutated in-place.
|
|
319
|
+
"""
|
|
320
|
+
tool_name = tool_call.name
|
|
321
|
+
tool = self.tools_map.get(tool_name, None)
|
|
322
|
+
# Handle non-existing tools
|
|
323
|
+
if tool is None:
|
|
324
|
+
available = list(self.tools_map.keys())
|
|
325
|
+
err = f"Tool '{tool_name}' not found. Available: {available}"
|
|
326
|
+
logger.error(err)
|
|
327
|
+
# Persist assistant function_call so next turn has matching call_id
|
|
328
|
+
tc_event = ActionEvent(
|
|
329
|
+
source="agent",
|
|
330
|
+
thought=thought,
|
|
331
|
+
reasoning_content=reasoning_content,
|
|
332
|
+
thinking_blocks=thinking_blocks,
|
|
333
|
+
responses_reasoning_item=responses_reasoning_item,
|
|
334
|
+
tool_call=tool_call,
|
|
335
|
+
tool_name=tool_call.name,
|
|
336
|
+
tool_call_id=tool_call.id,
|
|
337
|
+
llm_response_id=llm_response_id,
|
|
338
|
+
action=None,
|
|
339
|
+
)
|
|
340
|
+
on_event(tc_event)
|
|
341
|
+
event = AgentErrorEvent(
|
|
342
|
+
error=err,
|
|
343
|
+
tool_name=tool_name,
|
|
344
|
+
tool_call_id=tool_call.id,
|
|
345
|
+
)
|
|
346
|
+
on_event(event)
|
|
347
|
+
return
|
|
348
|
+
|
|
349
|
+
# Validate arguments
|
|
350
|
+
security_risk: risk.SecurityRisk = risk.SecurityRisk.UNKNOWN
|
|
351
|
+
try:
|
|
352
|
+
arguments = json.loads(tool_call.arguments)
|
|
353
|
+
|
|
354
|
+
# if the tool has a security_risk field (when security analyzer is set),
|
|
355
|
+
# pop it out as it's not part of the tool's action schema
|
|
356
|
+
if (
|
|
357
|
+
_predicted_risk := arguments.pop("security_risk", None)
|
|
358
|
+
) is not None and self.security_analyzer is not None:
|
|
359
|
+
try:
|
|
360
|
+
security_risk = risk.SecurityRisk(_predicted_risk)
|
|
361
|
+
except ValueError:
|
|
362
|
+
logger.warning(
|
|
363
|
+
f"Invalid security_risk value from LLM: {_predicted_risk}"
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
assert "security_risk" not in arguments, (
|
|
367
|
+
"Unexpected 'security_risk' key found in tool arguments"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
action: Action = tool.action_from_arguments(arguments)
|
|
371
|
+
except (json.JSONDecodeError, ValidationError) as e:
|
|
372
|
+
err = (
|
|
373
|
+
f"Error validating args {tool_call.arguments} for tool "
|
|
374
|
+
f"'{tool.name}': {e}"
|
|
375
|
+
)
|
|
376
|
+
# Persist assistant function_call so next turn has matching call_id
|
|
377
|
+
tc_event = ActionEvent(
|
|
378
|
+
source="agent",
|
|
379
|
+
thought=thought,
|
|
380
|
+
reasoning_content=reasoning_content,
|
|
381
|
+
thinking_blocks=thinking_blocks,
|
|
382
|
+
responses_reasoning_item=responses_reasoning_item,
|
|
383
|
+
tool_call=tool_call,
|
|
384
|
+
tool_name=tool_call.name,
|
|
385
|
+
tool_call_id=tool_call.id,
|
|
386
|
+
llm_response_id=llm_response_id,
|
|
387
|
+
action=None,
|
|
388
|
+
)
|
|
389
|
+
on_event(tc_event)
|
|
390
|
+
event = AgentErrorEvent(
|
|
391
|
+
error=err,
|
|
392
|
+
tool_name=tool_name,
|
|
393
|
+
tool_call_id=tool_call.id,
|
|
394
|
+
)
|
|
395
|
+
on_event(event)
|
|
396
|
+
return
|
|
397
|
+
|
|
398
|
+
action_event = ActionEvent(
|
|
399
|
+
action=action,
|
|
400
|
+
thought=thought,
|
|
401
|
+
reasoning_content=reasoning_content,
|
|
402
|
+
thinking_blocks=thinking_blocks,
|
|
403
|
+
responses_reasoning_item=responses_reasoning_item,
|
|
404
|
+
tool_name=tool.name,
|
|
405
|
+
tool_call_id=tool_call.id,
|
|
406
|
+
tool_call=tool_call,
|
|
407
|
+
llm_response_id=llm_response_id,
|
|
408
|
+
security_risk=security_risk,
|
|
409
|
+
)
|
|
410
|
+
on_event(action_event)
|
|
411
|
+
return action_event
|
|
412
|
+
|
|
413
|
+
@observe(ignore_inputs=["state", "on_event"])
|
|
414
|
+
def _execute_action_event(
|
|
415
|
+
self,
|
|
416
|
+
conversation: LocalConversation,
|
|
417
|
+
action_event: ActionEvent,
|
|
418
|
+
on_event: ConversationCallbackType,
|
|
419
|
+
):
|
|
420
|
+
"""Execute an action event and update the conversation state.
|
|
421
|
+
|
|
422
|
+
It will call the tool's executor and update the state & call callback fn
|
|
423
|
+
with the observation.
|
|
424
|
+
"""
|
|
425
|
+
state = conversation.state
|
|
426
|
+
tool = self.tools_map.get(action_event.tool_name, None)
|
|
427
|
+
if tool is None:
|
|
428
|
+
raise RuntimeError(
|
|
429
|
+
f"Tool '{action_event.tool_name}' not found. This should not happen "
|
|
430
|
+
"as it was checked earlier."
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
# Execute actions!
|
|
434
|
+
if should_enable_observability():
|
|
435
|
+
tool_name = extract_action_name(action_event)
|
|
436
|
+
observation: Observation = observe(name=tool_name, span_type="TOOL")(tool)(
|
|
437
|
+
action_event.action, conversation
|
|
438
|
+
)
|
|
439
|
+
else:
|
|
440
|
+
observation = tool(action_event.action, conversation)
|
|
441
|
+
assert isinstance(observation, Observation), (
|
|
442
|
+
f"Tool '{tool.name}' executor must return an Observation"
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
obs_event = ObservationEvent(
|
|
446
|
+
observation=observation,
|
|
447
|
+
action_id=action_event.id,
|
|
448
|
+
tool_name=tool.name,
|
|
449
|
+
tool_call_id=action_event.tool_call.id,
|
|
450
|
+
)
|
|
451
|
+
on_event(obs_event)
|
|
452
|
+
|
|
453
|
+
# Set conversation state
|
|
454
|
+
if tool.name == FinishTool.name:
|
|
455
|
+
state.execution_status = ConversationExecutionStatus.FINISHED
|
|
456
|
+
return obs_event
|