openhands-sdk 1.9.1__py3-none-any.whl → 1.11.0__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.
- openhands/sdk/agent/agent.py +90 -16
- openhands/sdk/agent/base.py +33 -46
- openhands/sdk/context/condenser/base.py +36 -3
- openhands/sdk/context/condenser/llm_summarizing_condenser.py +65 -24
- openhands/sdk/context/condenser/prompts/summarizing_prompt.j2 +1 -5
- openhands/sdk/context/prompts/templates/system_message_suffix.j2 +2 -1
- openhands/sdk/context/skills/skill.py +2 -25
- openhands/sdk/context/view.py +108 -122
- openhands/sdk/conversation/__init__.py +2 -0
- openhands/sdk/conversation/conversation.py +18 -3
- openhands/sdk/conversation/exceptions.py +18 -0
- openhands/sdk/conversation/impl/local_conversation.py +211 -36
- openhands/sdk/conversation/impl/remote_conversation.py +151 -12
- openhands/sdk/conversation/stuck_detector.py +18 -9
- openhands/sdk/critic/impl/api/critic.py +10 -7
- openhands/sdk/event/condenser.py +52 -2
- openhands/sdk/git/cached_repo.py +19 -0
- openhands/sdk/hooks/__init__.py +2 -0
- openhands/sdk/hooks/config.py +44 -4
- openhands/sdk/hooks/executor.py +2 -1
- openhands/sdk/llm/__init__.py +16 -0
- openhands/sdk/llm/auth/__init__.py +28 -0
- openhands/sdk/llm/auth/credentials.py +157 -0
- openhands/sdk/llm/auth/openai.py +762 -0
- openhands/sdk/llm/llm.py +222 -33
- openhands/sdk/llm/message.py +65 -27
- openhands/sdk/llm/options/chat_options.py +2 -1
- openhands/sdk/llm/options/responses_options.py +8 -7
- openhands/sdk/llm/utils/model_features.py +2 -0
- openhands/sdk/mcp/client.py +53 -6
- openhands/sdk/mcp/tool.py +24 -21
- openhands/sdk/mcp/utils.py +31 -23
- openhands/sdk/plugin/__init__.py +12 -1
- openhands/sdk/plugin/fetch.py +118 -14
- openhands/sdk/plugin/loader.py +111 -0
- openhands/sdk/plugin/plugin.py +155 -13
- openhands/sdk/plugin/types.py +163 -1
- openhands/sdk/secret/secrets.py +13 -1
- openhands/sdk/utils/__init__.py +2 -0
- openhands/sdk/utils/async_utils.py +36 -1
- openhands/sdk/utils/command.py +28 -1
- openhands/sdk/workspace/remote/base.py +8 -3
- openhands/sdk/workspace/remote/remote_workspace_mixin.py +40 -7
- {openhands_sdk-1.9.1.dist-info → openhands_sdk-1.11.0.dist-info}/METADATA +1 -1
- {openhands_sdk-1.9.1.dist-info → openhands_sdk-1.11.0.dist-info}/RECORD +47 -43
- {openhands_sdk-1.9.1.dist-info → openhands_sdk-1.11.0.dist-info}/WHEEL +1 -1
- {openhands_sdk-1.9.1.dist-info → openhands_sdk-1.11.0.dist-info}/top_level.txt +0 -0
openhands/sdk/context/view.py
CHANGED
|
@@ -11,7 +11,6 @@ from pydantic import BaseModel, computed_field
|
|
|
11
11
|
from openhands.sdk.event import (
|
|
12
12
|
Condensation,
|
|
13
13
|
CondensationRequest,
|
|
14
|
-
CondensationSummaryEvent,
|
|
15
14
|
LLMConvertibleEvent,
|
|
16
15
|
)
|
|
17
16
|
from openhands.sdk.event.base import Event, EventID
|
|
@@ -86,32 +85,6 @@ class View(BaseModel):
|
|
|
86
85
|
def __len__(self) -> int:
|
|
87
86
|
return len(self.events)
|
|
88
87
|
|
|
89
|
-
@property
|
|
90
|
-
def most_recent_condensation(self) -> Condensation | None:
|
|
91
|
-
"""Return the most recent condensation, or None if no condensations exist."""
|
|
92
|
-
return self.condensations[-1] if self.condensations else None
|
|
93
|
-
|
|
94
|
-
@property
|
|
95
|
-
def summary_event_index(self) -> int | None:
|
|
96
|
-
"""Return the index of the summary event, or None if no summary exists."""
|
|
97
|
-
recent_condensation = self.most_recent_condensation
|
|
98
|
-
if (
|
|
99
|
-
recent_condensation is not None
|
|
100
|
-
and recent_condensation.summary is not None
|
|
101
|
-
and recent_condensation.summary_offset is not None
|
|
102
|
-
):
|
|
103
|
-
return recent_condensation.summary_offset
|
|
104
|
-
return None
|
|
105
|
-
|
|
106
|
-
@property
|
|
107
|
-
def summary_event(self) -> CondensationSummaryEvent | None:
|
|
108
|
-
"""Return the summary event, or None if no summary exists."""
|
|
109
|
-
if self.summary_event_index is not None:
|
|
110
|
-
event = self.events[self.summary_event_index]
|
|
111
|
-
if isinstance(event, CondensationSummaryEvent):
|
|
112
|
-
return event
|
|
113
|
-
return None
|
|
114
|
-
|
|
115
88
|
@computed_field # type: ignore[prop-decorator]
|
|
116
89
|
@cached_property
|
|
117
90
|
def manipulation_indices(self) -> list[int]:
|
|
@@ -291,46 +264,53 @@ class View(BaseModel):
|
|
|
291
264
|
|
|
292
265
|
@staticmethod
|
|
293
266
|
def _enforce_batch_atomicity(
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
) ->
|
|
297
|
-
"""Ensure that if any ActionEvent in a batch is removed
|
|
298
|
-
in that batch are removed.
|
|
267
|
+
view_events: Sequence[LLMConvertibleEvent],
|
|
268
|
+
all_events: Sequence[Event],
|
|
269
|
+
) -> list[LLMConvertibleEvent]:
|
|
270
|
+
"""Ensure that if any ActionEvent in a batch is removed from the view,
|
|
271
|
+
all ActionEvents in that batch are removed.
|
|
299
272
|
|
|
300
273
|
This prevents partial batches from being sent to the LLM, which can cause
|
|
301
274
|
API errors when thinking blocks are separated from their tool calls.
|
|
302
275
|
|
|
303
276
|
Args:
|
|
304
|
-
|
|
305
|
-
|
|
277
|
+
view_events: The list of events that are being kept in the view
|
|
278
|
+
all_events: The complete original list of all events
|
|
306
279
|
|
|
307
280
|
Returns:
|
|
308
|
-
|
|
309
|
-
ActionEvents
|
|
281
|
+
Filtered list of view events with batch atomicity enforced
|
|
282
|
+
(removing all ActionEvents from batches where any ActionEvent
|
|
283
|
+
was already removed)
|
|
310
284
|
"""
|
|
311
|
-
action_batch = ActionBatch.from_events(
|
|
285
|
+
action_batch = ActionBatch.from_events(all_events)
|
|
312
286
|
|
|
313
287
|
if not action_batch.batches:
|
|
314
|
-
return
|
|
288
|
+
return list(view_events)
|
|
289
|
+
|
|
290
|
+
# Get set of event IDs currently in the view
|
|
291
|
+
view_event_ids = {event.id for event in view_events}
|
|
315
292
|
|
|
316
|
-
|
|
293
|
+
# Track which event IDs should be removed due to batch atomicity
|
|
294
|
+
ids_to_remove: set[EventID] = set()
|
|
317
295
|
|
|
318
296
|
for llm_response_id, batch_event_ids in action_batch.batches.items():
|
|
319
|
-
# Check if any ActionEvent in this batch is
|
|
320
|
-
if any(event_id in
|
|
297
|
+
# Check if any ActionEvent in this batch is missing from view
|
|
298
|
+
if any(event_id not in view_event_ids for event_id in batch_event_ids):
|
|
321
299
|
# If so, remove all ActionEvents in this batch
|
|
322
|
-
|
|
300
|
+
ids_to_remove.update(batch_event_ids)
|
|
323
301
|
logger.debug(
|
|
324
302
|
f"Enforcing batch atomicity: removing entire batch "
|
|
325
303
|
f"with llm_response_id={llm_response_id} "
|
|
326
304
|
f"({len(batch_event_ids)} events)"
|
|
327
305
|
)
|
|
328
306
|
|
|
329
|
-
|
|
307
|
+
# Filter out events that need to be removed
|
|
308
|
+
return [event for event in view_events if event.id not in ids_to_remove]
|
|
330
309
|
|
|
331
310
|
@staticmethod
|
|
332
|
-
def
|
|
333
|
-
|
|
311
|
+
def _filter_unmatched_tool_calls(
|
|
312
|
+
view_events: Sequence[LLMConvertibleEvent],
|
|
313
|
+
all_events: Sequence[Event],
|
|
334
314
|
) -> list[LLMConvertibleEvent]:
|
|
335
315
|
"""Filter out unmatched tool call events.
|
|
336
316
|
|
|
@@ -338,49 +318,58 @@ class View(BaseModel):
|
|
|
338
318
|
but don't have matching pairs. Also enforces batch atomicity - if any
|
|
339
319
|
ActionEvent in a batch is filtered out, all ActionEvents in that batch
|
|
340
320
|
are also filtered out.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
view_events: The list of events to filter
|
|
324
|
+
all_events: The complete original list of all events
|
|
325
|
+
|
|
326
|
+
Returns:
|
|
327
|
+
Filtered list of events with unmatched tool calls removed
|
|
341
328
|
"""
|
|
342
|
-
action_tool_call_ids = View._get_action_tool_call_ids(
|
|
343
|
-
observation_tool_call_ids = View._get_observation_tool_call_ids(
|
|
329
|
+
action_tool_call_ids = View._get_action_tool_call_ids(view_events)
|
|
330
|
+
observation_tool_call_ids = View._get_observation_tool_call_ids(view_events)
|
|
344
331
|
|
|
345
332
|
# Build batch info for batch atomicity enforcement
|
|
346
|
-
|
|
333
|
+
batch = ActionBatch.from_events(all_events)
|
|
347
334
|
|
|
348
|
-
# First pass:
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
335
|
+
# First pass: filter out events that don't match based on tool call pairing
|
|
336
|
+
kept_events = [
|
|
337
|
+
event
|
|
338
|
+
for event in view_events
|
|
339
|
+
if View._should_keep_event(
|
|
352
340
|
event, action_tool_call_ids, observation_tool_call_ids
|
|
353
|
-
)
|
|
354
|
-
|
|
341
|
+
)
|
|
342
|
+
]
|
|
355
343
|
|
|
356
344
|
# Second pass: enforce batch atomicity for ActionEvents
|
|
357
345
|
# If any ActionEvent in a batch is removed, all ActionEvents in that
|
|
358
346
|
# batch should also be removed
|
|
359
|
-
|
|
347
|
+
kept_events = View._enforce_batch_atomicity(kept_events, all_events)
|
|
360
348
|
|
|
361
349
|
# Third pass: also remove ObservationEvents whose ActionEvents were removed
|
|
362
350
|
# due to batch atomicity
|
|
363
|
-
|
|
364
|
-
for
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
if
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
351
|
+
# Find which action IDs are now missing after batch atomicity enforcement
|
|
352
|
+
kept_event_ids = {event.id for event in kept_events}
|
|
353
|
+
tool_call_ids_to_remove: set[ToolCallID] = {
|
|
354
|
+
tool_call_id
|
|
355
|
+
for action_id, tool_call_id in batch.action_id_to_tool_call_id.items()
|
|
356
|
+
if action_id not in kept_event_ids
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
# Filter out ObservationEvents whose ActionEvents were removed
|
|
360
|
+
result = [
|
|
361
|
+
event
|
|
362
|
+
for event in kept_events
|
|
363
|
+
if not (
|
|
364
|
+
isinstance(event, ObservationBaseEvent)
|
|
365
|
+
and event.tool_call_id in tool_call_ids_to_remove
|
|
366
|
+
)
|
|
367
|
+
]
|
|
379
368
|
|
|
380
369
|
return result
|
|
381
370
|
|
|
382
371
|
@staticmethod
|
|
383
|
-
def _get_action_tool_call_ids(events:
|
|
372
|
+
def _get_action_tool_call_ids(events: Sequence[Event]) -> set[ToolCallID]:
|
|
384
373
|
"""Extract tool_call_ids from ActionEvents."""
|
|
385
374
|
tool_call_ids = set()
|
|
386
375
|
for event in events:
|
|
@@ -390,7 +379,7 @@ class View(BaseModel):
|
|
|
390
379
|
|
|
391
380
|
@staticmethod
|
|
392
381
|
def _get_observation_tool_call_ids(
|
|
393
|
-
events:
|
|
382
|
+
events: Sequence[Event],
|
|
394
383
|
) -> set[ToolCallID]:
|
|
395
384
|
"""Extract tool_call_ids from ObservationEvents."""
|
|
396
385
|
tool_call_ids = set()
|
|
@@ -404,7 +393,7 @@ class View(BaseModel):
|
|
|
404
393
|
|
|
405
394
|
@staticmethod
|
|
406
395
|
def _should_keep_event(
|
|
407
|
-
event:
|
|
396
|
+
event: Event,
|
|
408
397
|
action_tool_call_ids: set[ToolCallID],
|
|
409
398
|
observation_tool_call_ids: set[ToolCallID],
|
|
410
399
|
) -> bool:
|
|
@@ -434,67 +423,64 @@ class View(BaseModel):
|
|
|
434
423
|
return idx
|
|
435
424
|
return threshold
|
|
436
425
|
|
|
426
|
+
@staticmethod
|
|
427
|
+
def unhandled_condensation_request_exists(
|
|
428
|
+
events: Sequence[Event],
|
|
429
|
+
) -> bool:
|
|
430
|
+
"""Check if there is an unhandled condensation request in the list of events.
|
|
431
|
+
|
|
432
|
+
An unhandled condensation request is defined as a CondensationRequest event
|
|
433
|
+
that appears after the most recent Condensation event in the list.
|
|
434
|
+
"""
|
|
435
|
+
for event in reversed(events):
|
|
436
|
+
if isinstance(event, Condensation):
|
|
437
|
+
return False
|
|
438
|
+
if isinstance(event, CondensationRequest):
|
|
439
|
+
return True
|
|
440
|
+
return False
|
|
441
|
+
|
|
437
442
|
@staticmethod
|
|
438
443
|
def from_events(events: Sequence[Event]) -> View:
|
|
439
444
|
"""Create a view from a list of events, respecting the semantics of any
|
|
440
445
|
condensation events.
|
|
441
446
|
"""
|
|
442
|
-
|
|
447
|
+
output: list[LLMConvertibleEvent] = []
|
|
443
448
|
condensations: list[Condensation] = []
|
|
449
|
+
|
|
450
|
+
# Generate the LLMConvertibleEvent objects the agent can send to the LLM by
|
|
451
|
+
# removing un-sendable events and applying condensations in order.
|
|
444
452
|
for event in events:
|
|
453
|
+
# By the time we come across a Condensation event, the output list should
|
|
454
|
+
# already reflect the events seen by the agent up to that point. We can
|
|
455
|
+
# therefore apply the condensation semantics directly to the output list.
|
|
445
456
|
if isinstance(event, Condensation):
|
|
446
457
|
condensations.append(event)
|
|
447
|
-
|
|
448
|
-
# Make sure we also forget the condensation action itself
|
|
449
|
-
forgotten_event_ids.add(event.id)
|
|
450
|
-
if isinstance(event, CondensationRequest):
|
|
451
|
-
forgotten_event_ids.add(event.id)
|
|
452
|
-
|
|
453
|
-
# Enforce batch atomicity: if any event in a multi-action batch is forgotten,
|
|
454
|
-
# forget all events in that batch to prevent partial batches with thinking
|
|
455
|
-
# blocks separated from their tool calls
|
|
456
|
-
forgotten_event_ids = View._enforce_batch_atomicity(events, forgotten_event_ids)
|
|
457
|
-
|
|
458
|
-
kept_events = [
|
|
459
|
-
event
|
|
460
|
-
for event in events
|
|
461
|
-
if event.id not in forgotten_event_ids
|
|
462
|
-
and isinstance(event, LLMConvertibleEvent)
|
|
463
|
-
]
|
|
458
|
+
output = event.apply(output)
|
|
464
459
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
summary_offset: int | None = None
|
|
460
|
+
elif isinstance(event, LLMConvertibleEvent):
|
|
461
|
+
output.append(event)
|
|
468
462
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
logger.debug(f"Inserting summary at offset {summary_offset}")
|
|
480
|
-
|
|
481
|
-
_new_summary_event = CondensationSummaryEvent(summary=summary)
|
|
482
|
-
kept_events.insert(summary_offset, _new_summary_event)
|
|
483
|
-
|
|
484
|
-
# Check for an unhandled condensation request -- these are events closer to the
|
|
485
|
-
# end of the list than any condensation action.
|
|
486
|
-
unhandled_condensation_request = False
|
|
487
|
-
|
|
488
|
-
for event in reversed(events):
|
|
489
|
-
if isinstance(event, Condensation):
|
|
490
|
-
break
|
|
463
|
+
# If the event isn't related to condensation and isn't LLMConvertible, it
|
|
464
|
+
# should not be in the resulting view. Examples include certain internal
|
|
465
|
+
# events used for state tracking that the LLM does not need to see -- see,
|
|
466
|
+
# for example, ConversationStateUpdateEvent, PauseEvent, and (relevant here)
|
|
467
|
+
# CondensationRequest.
|
|
468
|
+
else:
|
|
469
|
+
logger.debug(
|
|
470
|
+
f"Skipping non-LLMConvertibleEvent of type {type(event)} "
|
|
471
|
+
f"in View.from_events"
|
|
472
|
+
)
|
|
491
473
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
474
|
+
# Enforce batch atomicity: if any event in a multi-action batch is removed,
|
|
475
|
+
# remove all events in that batch to prevent partial batches with thinking
|
|
476
|
+
# blocks separated from their tool calls
|
|
477
|
+
output = View._enforce_batch_atomicity(output, events)
|
|
478
|
+
output = View._filter_unmatched_tool_calls(output, events)
|
|
495
479
|
|
|
496
480
|
return View(
|
|
497
|
-
events=
|
|
498
|
-
unhandled_condensation_request=
|
|
481
|
+
events=output,
|
|
482
|
+
unhandled_condensation_request=View.unhandled_condensation_request_exists(
|
|
483
|
+
events
|
|
484
|
+
),
|
|
499
485
|
condensations=condensations,
|
|
500
486
|
)
|
|
@@ -2,6 +2,7 @@ from openhands.sdk.conversation.base import BaseConversation
|
|
|
2
2
|
from openhands.sdk.conversation.conversation import Conversation
|
|
3
3
|
from openhands.sdk.conversation.event_store import EventLog
|
|
4
4
|
from openhands.sdk.conversation.events_list_base import EventsListBase
|
|
5
|
+
from openhands.sdk.conversation.exceptions import WebSocketConnectionError
|
|
5
6
|
from openhands.sdk.conversation.impl.local_conversation import LocalConversation
|
|
6
7
|
from openhands.sdk.conversation.impl.remote_conversation import RemoteConversation
|
|
7
8
|
from openhands.sdk.conversation.response_utils import get_agent_final_response
|
|
@@ -37,4 +38,5 @@ __all__ = [
|
|
|
37
38
|
"RemoteConversation",
|
|
38
39
|
"EventsListBase",
|
|
39
40
|
"get_agent_final_response",
|
|
41
|
+
"WebSocketConnectionError",
|
|
40
42
|
]
|
|
@@ -16,6 +16,7 @@ from openhands.sdk.conversation.visualizer import (
|
|
|
16
16
|
)
|
|
17
17
|
from openhands.sdk.hooks import HookConfig
|
|
18
18
|
from openhands.sdk.logger import get_logger
|
|
19
|
+
from openhands.sdk.plugin import PluginSource
|
|
19
20
|
from openhands.sdk.secret import SecretValue
|
|
20
21
|
from openhands.sdk.workspace import LocalWorkspace, RemoteWorkspace
|
|
21
22
|
|
|
@@ -40,9 +41,14 @@ class Conversation:
|
|
|
40
41
|
|
|
41
42
|
Example:
|
|
42
43
|
>>> from openhands.sdk import LLM, Agent, Conversation
|
|
44
|
+
>>> from openhands.sdk.plugin import PluginSource
|
|
43
45
|
>>> llm = LLM(model="claude-sonnet-4-20250514", api_key=SecretStr("key"))
|
|
44
46
|
>>> agent = Agent(llm=llm, tools=[])
|
|
45
|
-
>>> conversation = Conversation(
|
|
47
|
+
>>> conversation = Conversation(
|
|
48
|
+
... agent=agent,
|
|
49
|
+
... workspace="./workspace",
|
|
50
|
+
... plugins=[PluginSource(source="github:org/security-plugin", ref="v1.0")],
|
|
51
|
+
... )
|
|
46
52
|
>>> conversation.send_message("Hello!")
|
|
47
53
|
>>> conversation.run()
|
|
48
54
|
"""
|
|
@@ -53,6 +59,7 @@ class Conversation:
|
|
|
53
59
|
agent: AgentBase,
|
|
54
60
|
*,
|
|
55
61
|
workspace: str | Path | LocalWorkspace = "workspace/project",
|
|
62
|
+
plugins: list[PluginSource] | None = None,
|
|
56
63
|
persistence_dir: str | Path | None = None,
|
|
57
64
|
conversation_id: ConversationID | None = None,
|
|
58
65
|
callbacks: list[ConversationCallbackType] | None = None,
|
|
@@ -67,6 +74,7 @@ class Conversation:
|
|
|
67
74
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
68
75
|
) = DefaultConversationVisualizer,
|
|
69
76
|
secrets: dict[str, SecretValue] | dict[str, str] | None = None,
|
|
77
|
+
delete_on_close: bool = False,
|
|
70
78
|
) -> "LocalConversation": ...
|
|
71
79
|
|
|
72
80
|
@overload
|
|
@@ -75,6 +83,7 @@ class Conversation:
|
|
|
75
83
|
agent: AgentBase,
|
|
76
84
|
*,
|
|
77
85
|
workspace: RemoteWorkspace,
|
|
86
|
+
plugins: list[PluginSource] | None = None,
|
|
78
87
|
conversation_id: ConversationID | None = None,
|
|
79
88
|
callbacks: list[ConversationCallbackType] | None = None,
|
|
80
89
|
token_callbacks: list[ConversationTokenCallbackType] | None = None,
|
|
@@ -88,6 +97,7 @@ class Conversation:
|
|
|
88
97
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
89
98
|
) = DefaultConversationVisualizer,
|
|
90
99
|
secrets: dict[str, SecretValue] | dict[str, str] | None = None,
|
|
100
|
+
delete_on_close: bool = False,
|
|
91
101
|
) -> "RemoteConversation": ...
|
|
92
102
|
|
|
93
103
|
def __new__(
|
|
@@ -95,6 +105,7 @@ class Conversation:
|
|
|
95
105
|
agent: AgentBase,
|
|
96
106
|
*,
|
|
97
107
|
workspace: str | Path | LocalWorkspace | RemoteWorkspace = "workspace/project",
|
|
108
|
+
plugins: list[PluginSource] | None = None,
|
|
98
109
|
persistence_dir: str | Path | None = None,
|
|
99
110
|
conversation_id: ConversationID | None = None,
|
|
100
111
|
callbacks: list[ConversationCallbackType] | None = None,
|
|
@@ -109,6 +120,7 @@ class Conversation:
|
|
|
109
120
|
type[ConversationVisualizerBase] | ConversationVisualizerBase | None
|
|
110
121
|
) = DefaultConversationVisualizer,
|
|
111
122
|
secrets: dict[str, SecretValue] | dict[str, str] | None = None,
|
|
123
|
+
delete_on_close: bool = False,
|
|
112
124
|
) -> BaseConversation:
|
|
113
125
|
from openhands.sdk.conversation.impl.local_conversation import LocalConversation
|
|
114
126
|
from openhands.sdk.conversation.impl.remote_conversation import (
|
|
@@ -116,14 +128,14 @@ class Conversation:
|
|
|
116
128
|
)
|
|
117
129
|
|
|
118
130
|
if isinstance(workspace, RemoteWorkspace):
|
|
119
|
-
# For RemoteConversation, persistence_dir should not be used
|
|
120
|
-
# Only check if it was explicitly set to something other than the default
|
|
131
|
+
# For RemoteConversation, persistence_dir should not be used.
|
|
121
132
|
if persistence_dir is not None:
|
|
122
133
|
raise ValueError(
|
|
123
134
|
"persistence_dir should not be set when using RemoteConversation"
|
|
124
135
|
)
|
|
125
136
|
return RemoteConversation(
|
|
126
137
|
agent=agent,
|
|
138
|
+
plugins=plugins,
|
|
127
139
|
conversation_id=conversation_id,
|
|
128
140
|
callbacks=callbacks,
|
|
129
141
|
token_callbacks=token_callbacks,
|
|
@@ -134,10 +146,12 @@ class Conversation:
|
|
|
134
146
|
visualizer=visualizer,
|
|
135
147
|
workspace=workspace,
|
|
136
148
|
secrets=secrets,
|
|
149
|
+
delete_on_close=delete_on_close,
|
|
137
150
|
)
|
|
138
151
|
|
|
139
152
|
return LocalConversation(
|
|
140
153
|
agent=agent,
|
|
154
|
+
plugins=plugins,
|
|
141
155
|
conversation_id=conversation_id,
|
|
142
156
|
callbacks=callbacks,
|
|
143
157
|
token_callbacks=token_callbacks,
|
|
@@ -149,4 +163,5 @@ class Conversation:
|
|
|
149
163
|
workspace=workspace,
|
|
150
164
|
persistence_dir=persistence_dir,
|
|
151
165
|
secrets=secrets,
|
|
166
|
+
delete_on_close=delete_on_close,
|
|
152
167
|
)
|
|
@@ -4,6 +4,24 @@ from openhands.sdk.conversation.types import ConversationID
|
|
|
4
4
|
ISSUE_URL = "https://github.com/OpenHands/software-agent-sdk/issues/new"
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
class WebSocketConnectionError(RuntimeError):
|
|
8
|
+
"""Raised when WebSocket connection fails to establish within the timeout."""
|
|
9
|
+
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
conversation_id: ConversationID,
|
|
13
|
+
timeout: float,
|
|
14
|
+
message: str | None = None,
|
|
15
|
+
) -> None:
|
|
16
|
+
self.conversation_id = conversation_id
|
|
17
|
+
self.timeout = timeout
|
|
18
|
+
default_msg = (
|
|
19
|
+
f"WebSocket subscription did not complete within {timeout} seconds "
|
|
20
|
+
f"for conversation {conversation_id}. Events may be missed."
|
|
21
|
+
)
|
|
22
|
+
super().__init__(message or default_msg)
|
|
23
|
+
|
|
24
|
+
|
|
7
25
|
class ConversationRunError(RuntimeError):
|
|
8
26
|
"""Raised when a conversation run fails.
|
|
9
27
|
|