codex-autorunner 0.1.2__py3-none-any.whl → 1.1.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.
- codex_autorunner/__init__.py +12 -1
- codex_autorunner/__main__.py +4 -0
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +68 -35
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/logging.py +21 -5
- codex_autorunner/agents/opencode/run_prompt.py +1 -0
- codex_autorunner/agents/opencode/runtime.py +176 -47
- codex_autorunner/agents/opencode/supervisor.py +36 -48
- codex_autorunner/agents/registry.py +155 -8
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +22 -37
- codex_autorunner/cli.py +5 -1156
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +4 -0
- codex_autorunner/core/about_car.py +49 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_logging.py +7 -3
- codex_autorunner/core/app_server_prompts.py +27 -260
- codex_autorunner/core/app_server_threads.py +26 -28
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +12 -2
- codex_autorunner/core/config.py +587 -103
- codex_autorunner/core/docs.py +10 -2
- codex_autorunner/core/drafts.py +136 -0
- codex_autorunner/core/engine.py +1531 -866
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +202 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +88 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +131 -0
- codex_autorunner/core/flows/runtime.py +382 -0
- codex_autorunner/core/flows/store.py +568 -0
- codex_autorunner/core/flows/transition.py +138 -0
- codex_autorunner/core/flows/ux_helpers.py +257 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +136 -16
- codex_autorunner/core/locks.py +4 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/core/ports/agent_backend.py +150 -0
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/core/ports/run_event.py +91 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/review_context.py +5 -8
- codex_autorunner/core/run_index.py +6 -0
- codex_autorunner/core/runner_process.py +5 -2
- codex_autorunner/core/state.py +0 -88
- codex_autorunner/core/state_roots.py +57 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +201 -0
- codex_autorunner/core/ticket_manager_cli.py +432 -0
- codex_autorunner/core/update.py +24 -16
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +120 -11
- codex_autorunner/discovery.py +2 -4
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +15 -10
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +98 -0
- codex_autorunner/integrations/agents/__init__.py +17 -0
- codex_autorunner/integrations/agents/backend_orchestrator.py +284 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +448 -0
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +598 -0
- codex_autorunner/integrations/agents/runner.py +91 -0
- codex_autorunner/integrations/agents/wiring.py +271 -0
- codex_autorunner/integrations/app_server/client.py +583 -152
- codex_autorunner/integrations/app_server/env.py +2 -107
- codex_autorunner/{core/app_server_events.py → integrations/app_server/event_buffer.py} +15 -8
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +204 -165
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +221 -0
- codex_autorunner/integrations/telegram/constants.py +17 -2
- codex_autorunner/integrations/telegram/dispatch.py +17 -0
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -4
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +2 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +53 -57
- codex_autorunner/integrations/telegram/handlers/commands/files.py +2 -6
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1364 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +1 -1
- codex_autorunner/integrations/telegram/handlers/commands/github.py +41 -582
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +8 -8
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +137 -478
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +17 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +121 -9
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +111 -16
- codex_autorunner/integrations/telegram/outbox.py +208 -37
- codex_autorunner/integrations/telegram/progress_stream.py +3 -10
- codex_autorunner/integrations/telegram/service.py +221 -42
- codex_autorunner/integrations/telegram/state.py +100 -2
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +611 -0
- codex_autorunner/integrations/telegram/transport.py +39 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +37 -67
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +3 -0
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -624
- codex_autorunner/routes/file_chat.py +7 -0
- codex_autorunner/routes/flows.py +7 -0
- codex_autorunner/routes/messages.py +7 -0
- codex_autorunner/routes/repos.py +2 -196
- codex_autorunner/routes/review.py +2 -147
- codex_autorunner/routes/sessions.py +2 -175
- codex_autorunner/routes/settings.py +2 -168
- codex_autorunner/routes/shared.py +2 -275
- codex_autorunner/routes/system.py +4 -188
- codex_autorunner/routes/usage.py +3 -0
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +3 -0
- codex_autorunner/server.py +3 -2
- codex_autorunner/static/agentControls.js +41 -11
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +35 -24
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +36 -8
- codex_autorunner/static/bootstrap.js +1 -0
- codex_autorunner/static/bus.js +1 -0
- codex_autorunner/static/cache.js +1 -0
- codex_autorunner/static/constants.js +20 -4
- codex_autorunner/static/dashboard.js +344 -325
- codex_autorunner/static/diffRenderer.js +37 -0
- codex_autorunner/static/docChatCore.js +324 -0
- codex_autorunner/static/docChatStorage.js +65 -0
- codex_autorunner/static/docChatVoice.js +65 -0
- codex_autorunner/static/docEditor.js +133 -0
- codex_autorunner/static/env.js +1 -0
- codex_autorunner/static/eventSummarizer.js +166 -0
- codex_autorunner/static/fileChat.js +182 -0
- codex_autorunner/static/health.js +155 -0
- codex_autorunner/static/hub.js +126 -185
- codex_autorunner/static/index.html +839 -863
- codex_autorunner/static/liveUpdates.js +1 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +873 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +149 -217
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/styles.css +8850 -3876
- codex_autorunner/static/tabs.js +175 -11
- codex_autorunner/static/terminal.js +32 -0
- codex_autorunner/static/terminalManager.js +34 -59
- codex_autorunner/static/ticketChatActions.js +333 -0
- codex_autorunner/static/ticketChatEvents.js +16 -0
- codex_autorunner/static/ticketChatStorage.js +16 -0
- codex_autorunner/static/ticketChatStream.js +264 -0
- codex_autorunner/static/ticketEditor.js +844 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1988 -0
- codex_autorunner/static/utils.js +43 -3
- codex_autorunner/static/voice.js +1 -0
- codex_autorunner/static/workspace.js +765 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +1224 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2019 -0
- codex_autorunner/surfaces/web/hub_jobs.py +192 -0
- codex_autorunner/surfaces/web/middleware.py +587 -0
- codex_autorunner/surfaces/web/pty_session.py +370 -0
- codex_autorunner/surfaces/web/review.py +6 -0
- codex_autorunner/surfaces/web/routes/__init__.py +78 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +277 -0
- codex_autorunner/surfaces/web/routes/app_server.py +132 -0
- codex_autorunner/surfaces/web/routes/archive.py +357 -0
- codex_autorunner/surfaces/web/routes/base.py +615 -0
- codex_autorunner/surfaces/web/routes/file_chat.py +836 -0
- codex_autorunner/surfaces/web/routes/flows.py +1164 -0
- codex_autorunner/surfaces/web/routes/messages.py +459 -0
- codex_autorunner/surfaces/web/routes/repos.py +197 -0
- codex_autorunner/surfaces/web/routes/review.py +148 -0
- codex_autorunner/surfaces/web/routes/sessions.py +176 -0
- codex_autorunner/surfaces/web/routes/settings.py +169 -0
- codex_autorunner/surfaces/web/routes/shared.py +280 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/usage.py +89 -0
- codex_autorunner/surfaces/web/routes/voice.py +120 -0
- codex_autorunner/surfaces/web/routes/workspace.py +271 -0
- codex_autorunner/surfaces/web/runner_manager.py +25 -0
- codex_autorunner/surfaces/web/schemas.py +417 -0
- codex_autorunner/surfaces/web/static_assets.py +490 -0
- codex_autorunner/surfaces/web/static_refresh.py +86 -0
- codex_autorunner/surfaces/web/terminal_sessions.py +78 -0
- codex_autorunner/tickets/__init__.py +27 -0
- codex_autorunner/tickets/agent_pool.py +399 -0
- codex_autorunner/tickets/files.py +89 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +97 -0
- codex_autorunner/tickets/outbox.py +244 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +881 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1771
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -587
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -396
- codex_autorunner/web/static_assets.py +4 -484
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +335 -0
- codex_autorunner-1.1.0.dist-info/METADATA +154 -0
- codex_autorunner-1.1.0.dist-info/RECORD +308 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/WHEEL +1 -1
- codex_autorunner/agents/execution/policy.py +0 -292
- codex_autorunner/agents/factory.py +0 -52
- codex_autorunner/agents/orchestrator.py +0 -358
- codex_autorunner/core/doc_chat.py +0 -1446
- codex_autorunner/core/snapshot.py +0 -580
- codex_autorunner/integrations/github/chatops.py +0 -268
- codex_autorunner/integrations/github/pr_flow.py +0 -1314
- codex_autorunner/routes/docs.py +0 -381
- codex_autorunner/routes/github.py +0 -327
- codex_autorunner/routes/runs.py +0 -250
- codex_autorunner/spec_ingest.py +0 -812
- codex_autorunner/static/docChatActions.js +0 -287
- codex_autorunner/static/docChatEvents.js +0 -300
- codex_autorunner/static/docChatRender.js +0 -205
- codex_autorunner/static/docChatStream.js +0 -361
- codex_autorunner/static/docs.js +0 -20
- codex_autorunner/static/docsClipboard.js +0 -69
- codex_autorunner/static/docsCrud.js +0 -257
- codex_autorunner/static/docsDocUpdates.js +0 -62
- codex_autorunner/static/docsDrafts.js +0 -16
- codex_autorunner/static/docsElements.js +0 -69
- codex_autorunner/static/docsInit.js +0 -285
- codex_autorunner/static/docsParse.js +0 -160
- codex_autorunner/static/docsSnapshot.js +0 -87
- codex_autorunner/static/docsSpecIngest.js +0 -263
- codex_autorunner/static/docsState.js +0 -127
- codex_autorunner/static/docsThreadRegistry.js +0 -44
- codex_autorunner/static/docsUi.js +0 -153
- codex_autorunner/static/docsVoice.js +0 -56
- codex_autorunner/static/github.js +0 -504
- codex_autorunner/static/logs.js +0 -678
- codex_autorunner/static/review.js +0 -157
- codex_autorunner/static/runs.js +0 -418
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -94
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.2.dist-info/METADATA +0 -249
- codex_autorunner-0.1.2.dist-info/RECORD +0 -222
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.2.dist-info → codex_autorunner-1.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,598 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import contextlib
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, AsyncGenerator, Dict, Optional
|
|
7
|
+
|
|
8
|
+
from ...agents.opencode.client import OpenCodeClient
|
|
9
|
+
from ...agents.opencode.events import SSEEvent
|
|
10
|
+
from ...agents.opencode.logging import OpenCodeEventFormatter
|
|
11
|
+
from ...agents.opencode.runtime import (
|
|
12
|
+
OpenCodeTurnOutput,
|
|
13
|
+
build_turn_id,
|
|
14
|
+
collect_opencode_output,
|
|
15
|
+
extract_session_id,
|
|
16
|
+
map_approval_policy_to_permission,
|
|
17
|
+
opencode_missing_env,
|
|
18
|
+
parse_message_response,
|
|
19
|
+
split_model_id,
|
|
20
|
+
)
|
|
21
|
+
from ...agents.opencode.supervisor import OpenCodeSupervisor
|
|
22
|
+
from ...core.ports.agent_backend import (
|
|
23
|
+
AgentBackend,
|
|
24
|
+
AgentEvent,
|
|
25
|
+
AgentEventType,
|
|
26
|
+
now_iso,
|
|
27
|
+
)
|
|
28
|
+
from ...core.ports.run_event import (
|
|
29
|
+
Completed,
|
|
30
|
+
Failed,
|
|
31
|
+
OutputDelta,
|
|
32
|
+
RunEvent,
|
|
33
|
+
RunNotice,
|
|
34
|
+
Started,
|
|
35
|
+
TokenUsage,
|
|
36
|
+
ToolCall,
|
|
37
|
+
)
|
|
38
|
+
from ...core.text_delta_coalescer import StreamingTextCoalescer
|
|
39
|
+
|
|
40
|
+
_logger = logging.getLogger(__name__)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OpenCodeBackend(AgentBackend):
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
*,
|
|
47
|
+
base_url: Optional[str] = None,
|
|
48
|
+
supervisor: Optional[OpenCodeSupervisor] = None,
|
|
49
|
+
workspace_root: Optional[Path] = None,
|
|
50
|
+
auth: Optional[tuple[str, str]] = None,
|
|
51
|
+
timeout: Optional[float] = None,
|
|
52
|
+
agent: Optional[str] = None,
|
|
53
|
+
model: Optional[str] = None,
|
|
54
|
+
reasoning: Optional[str] = None,
|
|
55
|
+
approval_policy: Optional[str] = None,
|
|
56
|
+
session_stall_timeout_seconds: Optional[float] = None,
|
|
57
|
+
logger: Optional[logging.Logger] = None,
|
|
58
|
+
):
|
|
59
|
+
self._supervisor = supervisor
|
|
60
|
+
self._workspace_root = Path(workspace_root) if workspace_root else None
|
|
61
|
+
self._client: Optional[OpenCodeClient]
|
|
62
|
+
if base_url:
|
|
63
|
+
self._client = OpenCodeClient(
|
|
64
|
+
base_url=base_url,
|
|
65
|
+
auth=auth,
|
|
66
|
+
timeout=timeout,
|
|
67
|
+
logger=logger,
|
|
68
|
+
)
|
|
69
|
+
else:
|
|
70
|
+
self._client = None
|
|
71
|
+
self._agent = agent
|
|
72
|
+
self._model = model
|
|
73
|
+
self._reasoning = reasoning
|
|
74
|
+
self._approval_policy = approval_policy
|
|
75
|
+
self._session_stall_timeout_seconds = session_stall_timeout_seconds
|
|
76
|
+
self._logger = logger or _logger
|
|
77
|
+
|
|
78
|
+
self._session_id: Optional[str] = None
|
|
79
|
+
self._message_count: int = 0
|
|
80
|
+
self._final_messages: list[str] = []
|
|
81
|
+
self._last_turn_id: Optional[str] = None
|
|
82
|
+
self._last_token_total: Optional[dict[str, Any]] = None
|
|
83
|
+
self._event_formatter = OpenCodeEventFormatter()
|
|
84
|
+
|
|
85
|
+
def configure(
|
|
86
|
+
self,
|
|
87
|
+
*,
|
|
88
|
+
model: Optional[str],
|
|
89
|
+
reasoning: Optional[str],
|
|
90
|
+
approval_policy: Optional[str],
|
|
91
|
+
) -> None:
|
|
92
|
+
self._model = model
|
|
93
|
+
self._reasoning = reasoning
|
|
94
|
+
self._approval_policy = approval_policy
|
|
95
|
+
|
|
96
|
+
async def start_session(self, target: dict, context: dict) -> str:
|
|
97
|
+
client = await self._ensure_client()
|
|
98
|
+
workspace_root = self._workspace_root or Path(context.get("workspace") or ".")
|
|
99
|
+
resume_session = context.get("session_id") or context.get("thread_id")
|
|
100
|
+
if isinstance(resume_session, str) and resume_session:
|
|
101
|
+
try:
|
|
102
|
+
await client.get_session(resume_session)
|
|
103
|
+
self._session_id = resume_session
|
|
104
|
+
except Exception:
|
|
105
|
+
self._session_id = None
|
|
106
|
+
|
|
107
|
+
if not self._session_id:
|
|
108
|
+
result = await client.create_session(
|
|
109
|
+
title=f"Flow session {self._message_count}",
|
|
110
|
+
directory=str(workspace_root),
|
|
111
|
+
)
|
|
112
|
+
self._session_id = extract_session_id(result, allow_fallback_id=True)
|
|
113
|
+
|
|
114
|
+
if not self._session_id:
|
|
115
|
+
raise RuntimeError("Failed to create OpenCode session: missing session ID")
|
|
116
|
+
|
|
117
|
+
_logger.info("Started OpenCode session: %s", self._session_id)
|
|
118
|
+
|
|
119
|
+
return self._session_id
|
|
120
|
+
|
|
121
|
+
async def run_turn(
|
|
122
|
+
self, session_id: str, message: str
|
|
123
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
124
|
+
client = await self._ensure_client()
|
|
125
|
+
if session_id:
|
|
126
|
+
self._session_id = session_id
|
|
127
|
+
if not self._session_id:
|
|
128
|
+
self._session_id = await self.start_session(target={}, context={})
|
|
129
|
+
|
|
130
|
+
_logger.info("Sending message to session %s", self._session_id)
|
|
131
|
+
|
|
132
|
+
yield AgentEvent.stream_delta(content=message, delta_type="user_message")
|
|
133
|
+
|
|
134
|
+
await client.send_message(
|
|
135
|
+
self._session_id,
|
|
136
|
+
message=message,
|
|
137
|
+
agent=self._agent,
|
|
138
|
+
model=split_model_id(self._model) if self._model else None,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
self._message_count += 1
|
|
142
|
+
async for event in self._yield_events_until_completion():
|
|
143
|
+
yield event
|
|
144
|
+
|
|
145
|
+
async def run_turn_events(
|
|
146
|
+
self, session_id: str, message: str
|
|
147
|
+
) -> AsyncGenerator[RunEvent, None]:
|
|
148
|
+
client = await self._ensure_client()
|
|
149
|
+
workspace_root = self._workspace_root or Path(".")
|
|
150
|
+
|
|
151
|
+
if session_id:
|
|
152
|
+
self._session_id = session_id
|
|
153
|
+
if not self._session_id:
|
|
154
|
+
self._session_id = await self.start_session(
|
|
155
|
+
target={},
|
|
156
|
+
context={"workspace": str(workspace_root)},
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
_logger.info("Running turn events on session %s", self._session_id)
|
|
160
|
+
|
|
161
|
+
self._last_turn_id = build_turn_id(self._session_id)
|
|
162
|
+
|
|
163
|
+
yield Started(timestamp=now_iso(), session_id=self._session_id)
|
|
164
|
+
|
|
165
|
+
yield OutputDelta(
|
|
166
|
+
timestamp=now_iso(), content=message, delta_type="user_message"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
model_payload = split_model_id(self._model) if self._model else None
|
|
170
|
+
missing_env = await opencode_missing_env(
|
|
171
|
+
client, str(workspace_root), model_payload
|
|
172
|
+
)
|
|
173
|
+
if missing_env:
|
|
174
|
+
provider_id = model_payload.get("providerID") if model_payload else None
|
|
175
|
+
missing_label = ", ".join(missing_env)
|
|
176
|
+
yield Failed(
|
|
177
|
+
timestamp=now_iso(),
|
|
178
|
+
error_message=(
|
|
179
|
+
"OpenCode provider "
|
|
180
|
+
f"{provider_id or 'selected'} requires env vars: {missing_label}"
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
return
|
|
184
|
+
|
|
185
|
+
permission_policy = map_approval_policy_to_permission(
|
|
186
|
+
self._approval_policy, default="allow"
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
event_queue: asyncio.Queue[RunEvent] = asyncio.Queue()
|
|
190
|
+
self._event_formatter.reset()
|
|
191
|
+
assistant_stream_coalescer = StreamingTextCoalescer()
|
|
192
|
+
|
|
193
|
+
async def _enqueue_lines(lines: list[str]) -> None:
|
|
194
|
+
for line in lines:
|
|
195
|
+
await event_queue.put(
|
|
196
|
+
OutputDelta(
|
|
197
|
+
timestamp=now_iso(), content=line, delta_type="log_line"
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
async def _part_handler(
|
|
202
|
+
part_type: str, part: dict[str, Any], delta_text: Optional[str]
|
|
203
|
+
) -> None:
|
|
204
|
+
if part_type == "usage" and isinstance(part, dict):
|
|
205
|
+
self._last_token_total = _usage_to_token_total(part)
|
|
206
|
+
await event_queue.put(TokenUsage(timestamp=now_iso(), usage=dict(part)))
|
|
207
|
+
await _enqueue_lines(self._event_formatter.format_usage(part))
|
|
208
|
+
else:
|
|
209
|
+
await _enqueue_lines(
|
|
210
|
+
self._event_formatter.format_part(part_type, part, delta_text)
|
|
211
|
+
)
|
|
212
|
+
if part_type == "text" and isinstance(delta_text, str) and delta_text:
|
|
213
|
+
for chunk in assistant_stream_coalescer.add(delta_text):
|
|
214
|
+
await event_queue.put(
|
|
215
|
+
OutputDelta(
|
|
216
|
+
timestamp=now_iso(),
|
|
217
|
+
content=chunk,
|
|
218
|
+
delta_type="assistant_stream",
|
|
219
|
+
)
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
ready_event = asyncio.Event()
|
|
223
|
+
output_task = asyncio.create_task(
|
|
224
|
+
collect_opencode_output(
|
|
225
|
+
client,
|
|
226
|
+
session_id=self._session_id,
|
|
227
|
+
workspace_path=str(workspace_root),
|
|
228
|
+
model_payload=model_payload,
|
|
229
|
+
permission_policy=permission_policy,
|
|
230
|
+
part_handler=_part_handler,
|
|
231
|
+
ready_event=ready_event,
|
|
232
|
+
stall_timeout_seconds=self._session_stall_timeout_seconds,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
try:
|
|
236
|
+
await asyncio.wait_for(ready_event.wait(), timeout=2.0)
|
|
237
|
+
except asyncio.TimeoutError:
|
|
238
|
+
await event_queue.put(
|
|
239
|
+
RunNotice(
|
|
240
|
+
timestamp=now_iso(),
|
|
241
|
+
kind="ready_timeout",
|
|
242
|
+
message="OpenCode stream readiness wait timed out",
|
|
243
|
+
data={"timeout_seconds": 2.0},
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
prompt_response: Any = None
|
|
248
|
+
prompt_task: Optional[asyncio.Task[Any]] = asyncio.create_task(
|
|
249
|
+
client.prompt_async(
|
|
250
|
+
self._session_id,
|
|
251
|
+
message=message,
|
|
252
|
+
agent=self._agent,
|
|
253
|
+
model=model_payload,
|
|
254
|
+
variant=self._reasoning,
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
output_result = None
|
|
259
|
+
try:
|
|
260
|
+
while True:
|
|
261
|
+
queue_task = asyncio.create_task(event_queue.get())
|
|
262
|
+
tasks = {output_task, queue_task}
|
|
263
|
+
if prompt_task is not None:
|
|
264
|
+
tasks.add(prompt_task)
|
|
265
|
+
done, pending = await asyncio.wait(
|
|
266
|
+
tasks, return_when=asyncio.FIRST_COMPLETED
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
if queue_task in done:
|
|
270
|
+
yield queue_task.result()
|
|
271
|
+
else:
|
|
272
|
+
queue_task.cancel()
|
|
273
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
274
|
+
await queue_task
|
|
275
|
+
|
|
276
|
+
if prompt_task is not None and prompt_task in done:
|
|
277
|
+
try:
|
|
278
|
+
prompt_response = prompt_task.result()
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
output_task.cancel()
|
|
281
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
282
|
+
await output_task
|
|
283
|
+
yield Failed(timestamp=now_iso(), error_message=str(exc))
|
|
284
|
+
return
|
|
285
|
+
prompt_task = None
|
|
286
|
+
|
|
287
|
+
if output_task in done:
|
|
288
|
+
output_result = await output_task
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
finally:
|
|
292
|
+
if prompt_task is not None:
|
|
293
|
+
with contextlib.suppress(asyncio.CancelledError):
|
|
294
|
+
await prompt_task
|
|
295
|
+
for line in self._event_formatter.flush_all_reasoning():
|
|
296
|
+
await event_queue.put(
|
|
297
|
+
OutputDelta(
|
|
298
|
+
timestamp=now_iso(), content=line, delta_type="log_line"
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
for chunk in assistant_stream_coalescer.flush():
|
|
302
|
+
await event_queue.put(
|
|
303
|
+
OutputDelta(
|
|
304
|
+
timestamp=now_iso(),
|
|
305
|
+
content=chunk,
|
|
306
|
+
delta_type="assistant_stream",
|
|
307
|
+
)
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
while not event_queue.empty():
|
|
311
|
+
yield event_queue.get_nowait()
|
|
312
|
+
|
|
313
|
+
if output_result is None:
|
|
314
|
+
yield Failed(timestamp=now_iso(), error_message="OpenCode output failed")
|
|
315
|
+
return
|
|
316
|
+
|
|
317
|
+
if prompt_response is not None and not output_result.text:
|
|
318
|
+
fallback = parse_message_response(prompt_response)
|
|
319
|
+
if fallback.text:
|
|
320
|
+
output_result = OpenCodeTurnOutput(
|
|
321
|
+
text=fallback.text, error=output_result.error
|
|
322
|
+
)
|
|
323
|
+
if fallback.error and not output_result.error:
|
|
324
|
+
output_result = OpenCodeTurnOutput(
|
|
325
|
+
text=output_result.text, error=fallback.error
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
if output_result.text:
|
|
329
|
+
yield Completed(timestamp=now_iso(), final_message=output_result.text)
|
|
330
|
+
elif output_result.error:
|
|
331
|
+
yield Failed(timestamp=now_iso(), error_message=output_result.error)
|
|
332
|
+
else:
|
|
333
|
+
yield Completed(timestamp=now_iso(), final_message="")
|
|
334
|
+
|
|
335
|
+
async def stream_events(self, session_id: str) -> AsyncGenerator[AgentEvent, None]:
|
|
336
|
+
if session_id:
|
|
337
|
+
self._session_id = session_id
|
|
338
|
+
if not self._session_id:
|
|
339
|
+
raise RuntimeError("Session not started. Call start_session() first.")
|
|
340
|
+
|
|
341
|
+
client = await self._ensure_client()
|
|
342
|
+
async for sse in client.stream_events(directory=None):
|
|
343
|
+
for agent_event in self._convert_sse_to_agent_event(sse):
|
|
344
|
+
yield agent_event
|
|
345
|
+
|
|
346
|
+
async def interrupt(self, session_id: str) -> None:
|
|
347
|
+
target_session = session_id or self._session_id
|
|
348
|
+
if target_session:
|
|
349
|
+
client = await self._ensure_client()
|
|
350
|
+
try:
|
|
351
|
+
await client.abort(target_session)
|
|
352
|
+
_logger.info("Interrupted OpenCode session %s", target_session)
|
|
353
|
+
except Exception as e:
|
|
354
|
+
_logger.warning("Failed to interrupt session: %s", e)
|
|
355
|
+
|
|
356
|
+
async def final_messages(self, session_id: str) -> list[str]:
|
|
357
|
+
return self._final_messages
|
|
358
|
+
|
|
359
|
+
async def request_approval(
|
|
360
|
+
self, description: str, context: Optional[Dict[str, Any]] = None
|
|
361
|
+
) -> bool:
|
|
362
|
+
raise NotImplementedError("Approvals not implemented for OpenCodeBackend")
|
|
363
|
+
|
|
364
|
+
async def _yield_events_until_completion(self) -> AsyncGenerator[AgentEvent, None]:
|
|
365
|
+
paths = ["/event", "/global/event"]
|
|
366
|
+
if self._session_id:
|
|
367
|
+
paths.insert(0, f"/session/{self._session_id}/event")
|
|
368
|
+
try:
|
|
369
|
+
client = await self._ensure_client()
|
|
370
|
+
async for sse in client.stream_events(
|
|
371
|
+
directory=None,
|
|
372
|
+
paths=paths,
|
|
373
|
+
):
|
|
374
|
+
if not self._sse_matches_session(sse):
|
|
375
|
+
continue
|
|
376
|
+
for agent_event in self._convert_sse_to_agent_event(sse):
|
|
377
|
+
yield agent_event
|
|
378
|
+
if agent_event.event_type in {
|
|
379
|
+
AgentEventType.MESSAGE_COMPLETE,
|
|
380
|
+
AgentEventType.SESSION_ENDED,
|
|
381
|
+
}:
|
|
382
|
+
if agent_event.event_type == AgentEventType.MESSAGE_COMPLETE:
|
|
383
|
+
self._final_messages.append(
|
|
384
|
+
agent_event.data.get("final_message", "")
|
|
385
|
+
)
|
|
386
|
+
return
|
|
387
|
+
except Exception as e:
|
|
388
|
+
_logger.warning("Error in event collection: %s", e)
|
|
389
|
+
yield AgentEvent.error(error_message=str(e))
|
|
390
|
+
|
|
391
|
+
async def _yield_run_events_until_completion(
|
|
392
|
+
self,
|
|
393
|
+
) -> AsyncGenerator[RunEvent, None]:
|
|
394
|
+
paths = ["/event", "/global/event"]
|
|
395
|
+
if self._session_id:
|
|
396
|
+
paths.insert(0, f"/session/{self._session_id}/event")
|
|
397
|
+
try:
|
|
398
|
+
client = await self._ensure_client()
|
|
399
|
+
async for sse in client.stream_events(
|
|
400
|
+
directory=None,
|
|
401
|
+
paths=paths,
|
|
402
|
+
):
|
|
403
|
+
if not self._sse_matches_session(sse):
|
|
404
|
+
continue
|
|
405
|
+
for run_event in self._convert_sse_to_run_event(sse):
|
|
406
|
+
yield run_event
|
|
407
|
+
if isinstance(run_event, (Completed, Failed)):
|
|
408
|
+
if isinstance(run_event, Completed):
|
|
409
|
+
self._final_messages.append(run_event.final_message)
|
|
410
|
+
return
|
|
411
|
+
except Exception as e:
|
|
412
|
+
_logger.warning("Error in run event collection: %s", e)
|
|
413
|
+
yield Failed(timestamp=now_iso(), error_message=str(e))
|
|
414
|
+
|
|
415
|
+
def _convert_sse_to_run_event(self, sse: SSEEvent) -> list[RunEvent]:
|
|
416
|
+
events: list[RunEvent] = []
|
|
417
|
+
|
|
418
|
+
try:
|
|
419
|
+
payload = json.loads(sse.data) if sse.data else {}
|
|
420
|
+
except json.JSONDecodeError:
|
|
421
|
+
return events
|
|
422
|
+
|
|
423
|
+
payload_type = payload.get("type", "")
|
|
424
|
+
|
|
425
|
+
if payload_type == "textDelta":
|
|
426
|
+
text = payload.get("text", "")
|
|
427
|
+
events.append(
|
|
428
|
+
OutputDelta(
|
|
429
|
+
timestamp=now_iso(), content=text, delta_type="assistant_stream"
|
|
430
|
+
)
|
|
431
|
+
)
|
|
432
|
+
|
|
433
|
+
elif payload_type == "toolCall":
|
|
434
|
+
tool_name = payload.get("toolName", "")
|
|
435
|
+
tool_input = payload.get("toolInput", {})
|
|
436
|
+
events.append(
|
|
437
|
+
ToolCall(
|
|
438
|
+
timestamp=now_iso(), tool_name=tool_name, tool_input=tool_input
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
elif payload_type == "toolCallEnd":
|
|
443
|
+
pass
|
|
444
|
+
|
|
445
|
+
elif payload_type == "messageEnd":
|
|
446
|
+
final_message = payload.get("message", "")
|
|
447
|
+
events.append(Completed(timestamp=now_iso(), final_message=final_message))
|
|
448
|
+
|
|
449
|
+
elif payload_type == "error":
|
|
450
|
+
error_message = payload.get("message", "Unknown error")
|
|
451
|
+
events.append(Failed(timestamp=now_iso(), error_message=error_message))
|
|
452
|
+
|
|
453
|
+
elif payload_type == "sessionEnd":
|
|
454
|
+
# Prefer messageEnd content if we already saw it; otherwise treat as failure.
|
|
455
|
+
final_message = payload.get("message") or ""
|
|
456
|
+
if final_message:
|
|
457
|
+
events.append(
|
|
458
|
+
Completed(timestamp=now_iso(), final_message=final_message)
|
|
459
|
+
)
|
|
460
|
+
else:
|
|
461
|
+
events.append(
|
|
462
|
+
Failed(
|
|
463
|
+
timestamp=now_iso(),
|
|
464
|
+
error_message=payload.get("reason", "Session ended early"),
|
|
465
|
+
)
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
return events
|
|
469
|
+
|
|
470
|
+
def _convert_sse_to_agent_event(self, sse: SSEEvent) -> list[AgentEvent]:
|
|
471
|
+
events: list[AgentEvent] = []
|
|
472
|
+
|
|
473
|
+
try:
|
|
474
|
+
payload = json.loads(sse.data) if sse.data else {}
|
|
475
|
+
except json.JSONDecodeError:
|
|
476
|
+
return events
|
|
477
|
+
|
|
478
|
+
payload_type = payload.get("type", "")
|
|
479
|
+
session_id = self._extract_session_id(payload)
|
|
480
|
+
|
|
481
|
+
if payload_type == "textDelta":
|
|
482
|
+
text = payload.get("text", "")
|
|
483
|
+
event = AgentEvent.stream_delta(content=text, delta_type="assistant_stream")
|
|
484
|
+
if session_id:
|
|
485
|
+
event.data["session_id"] = session_id
|
|
486
|
+
events.append(event)
|
|
487
|
+
|
|
488
|
+
elif payload_type == "toolCall":
|
|
489
|
+
tool_name = payload.get("toolName", "")
|
|
490
|
+
tool_input = payload.get("toolInput", {})
|
|
491
|
+
event = AgentEvent.tool_call(tool_name=tool_name, tool_input=tool_input)
|
|
492
|
+
if session_id:
|
|
493
|
+
event.data["session_id"] = session_id
|
|
494
|
+
events.append(event)
|
|
495
|
+
|
|
496
|
+
elif payload_type == "toolCallEnd":
|
|
497
|
+
tool_name = payload.get("toolName", "")
|
|
498
|
+
result = payload.get("result")
|
|
499
|
+
error = payload.get("error")
|
|
500
|
+
event = AgentEvent.tool_result(
|
|
501
|
+
tool_name=tool_name, result=result, error=error
|
|
502
|
+
)
|
|
503
|
+
if session_id:
|
|
504
|
+
event.data["session_id"] = session_id
|
|
505
|
+
events.append(event)
|
|
506
|
+
|
|
507
|
+
elif payload_type == "messageEnd":
|
|
508
|
+
final_message = payload.get("message", "")
|
|
509
|
+
event = AgentEvent.message_complete(final_message=final_message)
|
|
510
|
+
if session_id:
|
|
511
|
+
event.data["session_id"] = session_id
|
|
512
|
+
events.append(event)
|
|
513
|
+
|
|
514
|
+
elif payload_type == "error":
|
|
515
|
+
error_message = payload.get("message", "Unknown error")
|
|
516
|
+
event = AgentEvent.error(error_message=error_message)
|
|
517
|
+
if session_id:
|
|
518
|
+
event.data["session_id"] = session_id
|
|
519
|
+
events.append(event)
|
|
520
|
+
|
|
521
|
+
elif payload_type == "sessionEnd":
|
|
522
|
+
events.append(
|
|
523
|
+
AgentEvent(
|
|
524
|
+
type=AgentEventType.SESSION_ENDED.value,
|
|
525
|
+
timestamp=now_iso(),
|
|
526
|
+
data={
|
|
527
|
+
"reason": payload.get("reason", "unknown"),
|
|
528
|
+
"session_id": session_id,
|
|
529
|
+
},
|
|
530
|
+
)
|
|
531
|
+
)
|
|
532
|
+
|
|
533
|
+
return events
|
|
534
|
+
|
|
535
|
+
def _extract_session_id(self, payload: dict[str, Any]) -> Optional[str]:
|
|
536
|
+
for key in ("session", "sessionId", "sessionID", "session_id"):
|
|
537
|
+
value = payload.get(key)
|
|
538
|
+
if isinstance(value, str):
|
|
539
|
+
return value
|
|
540
|
+
return None
|
|
541
|
+
|
|
542
|
+
def _sse_matches_session(self, sse: SSEEvent) -> bool:
|
|
543
|
+
if not self._session_id:
|
|
544
|
+
return True
|
|
545
|
+
try:
|
|
546
|
+
payload = json.loads(sse.data) if sse.data else {}
|
|
547
|
+
except json.JSONDecodeError:
|
|
548
|
+
return True
|
|
549
|
+
session_id = self._extract_session_id(payload)
|
|
550
|
+
if session_id is None:
|
|
551
|
+
# If server does not tag events, do not drop them.
|
|
552
|
+
return True
|
|
553
|
+
return session_id == self._session_id
|
|
554
|
+
|
|
555
|
+
async def _ensure_client(self) -> OpenCodeClient:
|
|
556
|
+
if self._client is not None:
|
|
557
|
+
return self._client
|
|
558
|
+
if self._supervisor is None or self._workspace_root is None:
|
|
559
|
+
raise RuntimeError("OpenCode client unavailable: supervisor not configured")
|
|
560
|
+
client = await self._supervisor.get_client(self._workspace_root)
|
|
561
|
+
self._client = client
|
|
562
|
+
return client
|
|
563
|
+
|
|
564
|
+
@property
|
|
565
|
+
def last_turn_id(self) -> Optional[str]:
|
|
566
|
+
return self._last_turn_id
|
|
567
|
+
|
|
568
|
+
@property
|
|
569
|
+
def last_token_total(self) -> Optional[dict[str, Any]]:
|
|
570
|
+
return self._last_token_total
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
def _usage_to_token_total(usage: dict[str, Any]) -> Optional[dict[str, int]]:
|
|
574
|
+
if not isinstance(usage, dict):
|
|
575
|
+
return None
|
|
576
|
+
|
|
577
|
+
def _int(key: str) -> int:
|
|
578
|
+
value = usage.get(key)
|
|
579
|
+
return int(value) if isinstance(value, (int, float)) else 0
|
|
580
|
+
|
|
581
|
+
total = usage.get("totalTokens")
|
|
582
|
+
total_tokens = int(total) if isinstance(total, (int, float)) else None
|
|
583
|
+
input_tokens = _int("inputTokens")
|
|
584
|
+
cached_tokens = _int("cachedInputTokens")
|
|
585
|
+
output_tokens = _int("outputTokens")
|
|
586
|
+
reasoning_tokens = _int("reasoningTokens")
|
|
587
|
+
if total_tokens is None:
|
|
588
|
+
total_tokens = input_tokens + cached_tokens + output_tokens + reasoning_tokens
|
|
589
|
+
return {
|
|
590
|
+
"total": total_tokens,
|
|
591
|
+
"input_tokens": input_tokens,
|
|
592
|
+
"prompt_tokens": input_tokens,
|
|
593
|
+
"cached_input_tokens": cached_tokens,
|
|
594
|
+
"output_tokens": output_tokens,
|
|
595
|
+
"completion_tokens": output_tokens,
|
|
596
|
+
"reasoning_tokens": reasoning_tokens,
|
|
597
|
+
"reasoning_output_tokens": reasoning_tokens,
|
|
598
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from typing import AsyncGenerator, Callable, Optional
|
|
5
|
+
|
|
6
|
+
from ...core.ports.agent_backend import AgentBackend, AgentEvent
|
|
7
|
+
from ...core.ports.run_event import (
|
|
8
|
+
Failed,
|
|
9
|
+
OutputDelta,
|
|
10
|
+
RunEvent,
|
|
11
|
+
Started,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
_logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
LogHandler = Callable[[str], None]
|
|
17
|
+
EventCallback = Callable[[RunEvent], None]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
async def run_turn_with_backend(
|
|
21
|
+
backend: AgentBackend,
|
|
22
|
+
message: str,
|
|
23
|
+
session_id: Optional[str],
|
|
24
|
+
*,
|
|
25
|
+
log_handler: Optional[LogHandler] = None,
|
|
26
|
+
event_callback: Optional[EventCallback] = None,
|
|
27
|
+
) -> int:
|
|
28
|
+
"""
|
|
29
|
+
Execute a turn using the AgentBackend protocol.
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Exit code (0 for success, non-zero for failure)
|
|
33
|
+
"""
|
|
34
|
+
try:
|
|
35
|
+
if not session_id:
|
|
36
|
+
session_id = await backend.start_session(target={}, context={})
|
|
37
|
+
|
|
38
|
+
if event_callback:
|
|
39
|
+
event_callback(Started(timestamp=timestamp(), session_id=session_id))
|
|
40
|
+
|
|
41
|
+
if log_handler:
|
|
42
|
+
log_handler(message)
|
|
43
|
+
|
|
44
|
+
events_consumed = False
|
|
45
|
+
if hasattr(backend, "run_turn_events"):
|
|
46
|
+
async for run_event in backend.run_turn_events(session_id, message):
|
|
47
|
+
events_consumed = True
|
|
48
|
+
if event_callback:
|
|
49
|
+
event_callback(run_event)
|
|
50
|
+
if log_handler and isinstance(run_event, OutputDelta):
|
|
51
|
+
log_handler(run_event.content)
|
|
52
|
+
|
|
53
|
+
if not events_consumed:
|
|
54
|
+
async for agent_event in backend.run_turn(session_id, message):
|
|
55
|
+
if isinstance(agent_event, AgentEvent):
|
|
56
|
+
if log_handler:
|
|
57
|
+
if agent_event.data.get("content"):
|
|
58
|
+
log_handler(agent_event.data["content"])
|
|
59
|
+
elif isinstance(agent_event, str):
|
|
60
|
+
if log_handler:
|
|
61
|
+
log_handler(agent_event)
|
|
62
|
+
|
|
63
|
+
return 0
|
|
64
|
+
except Exception as exc:
|
|
65
|
+
_logger.error("Turn execution failed: %s", exc)
|
|
66
|
+
if event_callback:
|
|
67
|
+
event_callback(Failed(timestamp=timestamp(), error_message=str(exc)))
|
|
68
|
+
return 1
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def stream_turn_events(
|
|
72
|
+
backend: AgentBackend,
|
|
73
|
+
session_id: str,
|
|
74
|
+
) -> AsyncGenerator[AgentEvent, None]:
|
|
75
|
+
"""
|
|
76
|
+
Stream events from a backend for an existing session.
|
|
77
|
+
|
|
78
|
+
This is used for external streaming (e.g., WebSocket UI) where the turn
|
|
79
|
+
has already been initiated and we want to stream events as they arrive.
|
|
80
|
+
"""
|
|
81
|
+
if hasattr(backend, "stream_events"):
|
|
82
|
+
async for event in backend.stream_events(session_id):
|
|
83
|
+
yield event
|
|
84
|
+
else:
|
|
85
|
+
yield AgentEvent.stream_delta(content="", delta_type="noop")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def timestamp() -> str:
|
|
89
|
+
from datetime import datetime, timezone
|
|
90
|
+
|
|
91
|
+
return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|