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
codex_autorunner/__init__.py
CHANGED
|
@@ -1,3 +1,14 @@
|
|
|
1
1
|
"""Codex autorunner package."""
|
|
2
2
|
|
|
3
|
-
__all__ = [
|
|
3
|
+
__all__ = [
|
|
4
|
+
"cli",
|
|
5
|
+
"core",
|
|
6
|
+
"integrations",
|
|
7
|
+
"routes",
|
|
8
|
+
"server",
|
|
9
|
+
"surfaces",
|
|
10
|
+
"surfaces.web.routes",
|
|
11
|
+
"surfaces.web",
|
|
12
|
+
"voice",
|
|
13
|
+
"web",
|
|
14
|
+
]
|
|
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Any, AsyncIterator, Optional
|
|
5
5
|
|
|
6
|
-
from ...
|
|
6
|
+
from ...integrations.app_server.event_buffer import AppServerEventBuffer
|
|
7
7
|
from ...integrations.app_server.supervisor import WorkspaceAppServerSupervisor
|
|
8
8
|
from ..base import AgentHarness
|
|
9
9
|
from ..types import AgentId, ConversationRef, ModelCatalog, ModelSpec, TurnRef
|
|
@@ -4,6 +4,7 @@ import asyncio
|
|
|
4
4
|
import dataclasses
|
|
5
5
|
import json
|
|
6
6
|
import logging
|
|
7
|
+
import re
|
|
7
8
|
from typing import Any, AsyncIterator, Iterable, Optional
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
@@ -48,7 +49,18 @@ def _normalize_sse_event(event: SSEEvent) -> SSEEvent:
|
|
|
48
49
|
payload_obj = None
|
|
49
50
|
|
|
50
51
|
if isinstance(payload_obj, dict) and isinstance(payload_obj.get("payload"), dict):
|
|
51
|
-
|
|
52
|
+
outer = payload_obj
|
|
53
|
+
inner = dict(outer.get("payload") or {})
|
|
54
|
+
if "type" not in inner and isinstance(outer.get("type"), str):
|
|
55
|
+
inner["type"] = outer["type"]
|
|
56
|
+
for key in ("sessionID", "sessionId", "session_id"):
|
|
57
|
+
if key in outer and key not in inner:
|
|
58
|
+
inner[key] = outer[key]
|
|
59
|
+
if "session" in outer and "session" not in inner:
|
|
60
|
+
inner["session"] = outer["session"]
|
|
61
|
+
if "properties" in outer and "properties" not in inner:
|
|
62
|
+
inner["properties"] = outer["properties"]
|
|
63
|
+
payload_obj = inner
|
|
52
64
|
|
|
53
65
|
if isinstance(payload_obj, dict):
|
|
54
66
|
payload_type = payload_obj.get("type")
|
|
@@ -505,49 +517,70 @@ class OpenCodeClient:
|
|
|
505
517
|
|
|
506
518
|
async def fetch_openapi_spec(self) -> dict[str, Any]:
|
|
507
519
|
"""Fetch OpenAPI spec from /doc endpoint for capability negotiation."""
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
520
|
+
response = await self._client.get("/doc")
|
|
521
|
+
response.raise_for_status()
|
|
522
|
+
content = response.content
|
|
523
|
+
try:
|
|
524
|
+
spec = json.loads(content) if content else {}
|
|
525
|
+
log_event(
|
|
526
|
+
self._logger,
|
|
527
|
+
logging.INFO,
|
|
528
|
+
"opencode.openapi.fetched",
|
|
529
|
+
paths=len(spec.get("paths", {})) if isinstance(spec, dict) else 0,
|
|
530
|
+
has_components=(
|
|
531
|
+
"components" in spec if isinstance(spec, dict) else False
|
|
532
|
+
),
|
|
533
|
+
)
|
|
534
|
+
return spec
|
|
535
|
+
except Exception as exc:
|
|
536
|
+
log_event(
|
|
537
|
+
self._logger,
|
|
538
|
+
logging.WARNING,
|
|
539
|
+
"opencode.openapi.parse_failed",
|
|
540
|
+
exc=exc,
|
|
541
|
+
)
|
|
542
|
+
raise OpenCodeProtocolError(
|
|
543
|
+
f"Failed to parse OpenAPI spec: {exc}",
|
|
544
|
+
status_code=response.status_code,
|
|
545
|
+
content_type=(
|
|
546
|
+
response.headers.get("content-type") if response else None
|
|
547
|
+
),
|
|
548
|
+
) from exc
|
|
537
549
|
|
|
538
550
|
def has_endpoint(
|
|
539
551
|
self, openapi_spec: dict[str, Any], method: str, path: str
|
|
540
552
|
) -> bool:
|
|
541
|
-
"""Check if endpoint is available in OpenAPI spec.
|
|
553
|
+
"""Check if endpoint is available in OpenAPI spec.
|
|
554
|
+
|
|
555
|
+
The OpenAPI spec sometimes uses different template parameter names (e.g.,
|
|
556
|
+
`{sessionID}` vs `{session_id}`). We normalize templates before matching so
|
|
557
|
+
capability detection does not depend on placeholder spelling.
|
|
558
|
+
"""
|
|
542
559
|
if not isinstance(openapi_spec, dict):
|
|
543
560
|
return False
|
|
544
561
|
paths = openapi_spec.get("paths", {})
|
|
545
562
|
if not isinstance(paths, dict):
|
|
546
563
|
return False
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
564
|
+
|
|
565
|
+
target = _normalize_template_path(path)
|
|
566
|
+
method = method.lower()
|
|
567
|
+
|
|
568
|
+
for candidate_path, info in paths.items():
|
|
569
|
+
if not isinstance(info, dict):
|
|
570
|
+
continue
|
|
571
|
+
if _normalize_template_path(candidate_path) != target:
|
|
572
|
+
continue
|
|
573
|
+
if method in info:
|
|
574
|
+
return True
|
|
575
|
+
return False
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def _normalize_template_path(path: str) -> str:
|
|
579
|
+
"""Collapse template placeholders to a canonical form.
|
|
580
|
+
|
|
581
|
+
Example: `/session/{sessionID}/prompt_async` -> `/session/{}/prompt_async`
|
|
582
|
+
"""
|
|
583
|
+
return re.sub(r"{[^/]+}", "{}", path)
|
|
551
584
|
|
|
552
585
|
|
|
553
586
|
__all__ = ["OpenCodeClient", "OpenCodeProtocolError", "OpenCodeApiProfile"]
|
|
@@ -6,9 +6,10 @@ import logging
|
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from typing import Any, AsyncIterator, Optional
|
|
8
8
|
|
|
9
|
-
from ...
|
|
9
|
+
from ...integrations.app_server.event_buffer import format_sse
|
|
10
10
|
from ..base import AgentHarness
|
|
11
11
|
from ..types import AgentId, ConversationRef, ModelCatalog, ModelSpec, TurnRef
|
|
12
|
+
from .constants import DEFAULT_TICKET_MODEL
|
|
12
13
|
from .runtime import (
|
|
13
14
|
build_turn_id,
|
|
14
15
|
extract_session_id,
|
|
@@ -168,6 +169,8 @@ class OpenCodeHarness(AgentHarness):
|
|
|
168
169
|
sandbox_policy: Optional[Any],
|
|
169
170
|
) -> TurnRef:
|
|
170
171
|
client = await self._supervisor.get_client(workspace_root)
|
|
172
|
+
if model is None:
|
|
173
|
+
model = DEFAULT_TICKET_MODEL
|
|
171
174
|
model_payload = split_model_id(model)
|
|
172
175
|
await client.prompt_async(
|
|
173
176
|
conversation_id,
|
|
@@ -192,6 +195,8 @@ class OpenCodeHarness(AgentHarness):
|
|
|
192
195
|
sandbox_policy: Optional[Any],
|
|
193
196
|
) -> TurnRef:
|
|
194
197
|
client = await self._supervisor.get_client(workspace_root)
|
|
198
|
+
if model is None:
|
|
199
|
+
model = DEFAULT_TICKET_MODEL
|
|
195
200
|
arguments = prompt if prompt else ""
|
|
196
201
|
|
|
197
202
|
async def _send_review() -> None:
|
|
@@ -66,11 +66,6 @@ class OpenCodeEventFormatter:
|
|
|
66
66
|
for line in complete_lines:
|
|
67
67
|
if line.strip():
|
|
68
68
|
lines.append(f"**{line.strip()}**")
|
|
69
|
-
|
|
70
|
-
remaining = coalescer.get_buffer()
|
|
71
|
-
if remaining and remaining.strip():
|
|
72
|
-
lines.append(f"**{remaining.strip()}**")
|
|
73
|
-
coalescer.clear()
|
|
74
69
|
return lines
|
|
75
70
|
|
|
76
71
|
def _format_tool_part(self, part: dict[str, Any]) -> list[str]:
|
|
@@ -120,6 +115,27 @@ class OpenCodeEventFormatter:
|
|
|
120
115
|
lines.append("exec")
|
|
121
116
|
lines.append(f"tool: {tool_name}")
|
|
122
117
|
|
|
118
|
+
input_preview: Optional[str] = None
|
|
119
|
+
for key in ("input", "command", "cmd", "script"):
|
|
120
|
+
value = part.get(key)
|
|
121
|
+
if isinstance(value, str) and value.strip():
|
|
122
|
+
input_preview = value.strip()
|
|
123
|
+
break
|
|
124
|
+
if input_preview is None:
|
|
125
|
+
args = part.get("args") or part.get("arguments") or part.get("params")
|
|
126
|
+
if isinstance(args, dict):
|
|
127
|
+
for key in ("command", "cmd", "script", "input"):
|
|
128
|
+
value = args.get(key)
|
|
129
|
+
if isinstance(value, str) and value.strip():
|
|
130
|
+
input_preview = value.strip()
|
|
131
|
+
break
|
|
132
|
+
elif isinstance(args, str) and args.strip():
|
|
133
|
+
input_preview = args.strip()
|
|
134
|
+
if input_preview:
|
|
135
|
+
if len(input_preview) > 240:
|
|
136
|
+
input_preview = input_preview[:240] + "…"
|
|
137
|
+
lines.append(f"input: {input_preview}")
|
|
138
|
+
|
|
123
139
|
return lines
|
|
124
140
|
|
|
125
141
|
def _format_patch_part(self, part: dict[str, Any]) -> list[str]:
|
|
@@ -15,6 +15,7 @@ from typing import (
|
|
|
15
15
|
Callable,
|
|
16
16
|
MutableMapping,
|
|
17
17
|
Optional,
|
|
18
|
+
cast,
|
|
18
19
|
)
|
|
19
20
|
|
|
20
21
|
import httpx
|
|
@@ -121,23 +122,37 @@ def extract_session_id(
|
|
|
121
122
|
value = payload.get(key)
|
|
122
123
|
if isinstance(value, str) and value:
|
|
123
124
|
return value
|
|
125
|
+
info = payload.get("info")
|
|
126
|
+
if isinstance(info, dict):
|
|
127
|
+
for key in ("sessionID", "sessionId", "session_id"):
|
|
128
|
+
value = info.get(key)
|
|
129
|
+
if isinstance(value, str) and value:
|
|
130
|
+
return value
|
|
124
131
|
if allow_fallback_id:
|
|
125
132
|
value = payload.get("id")
|
|
126
133
|
if isinstance(value, str) and value:
|
|
127
134
|
return value
|
|
128
135
|
properties = payload.get("properties")
|
|
129
136
|
if isinstance(properties, dict):
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return value
|
|
133
|
-
part = properties.get("part")
|
|
134
|
-
if isinstance(part, dict):
|
|
135
|
-
value = part.get("sessionID")
|
|
137
|
+
for key in ("sessionID", "sessionId", "session_id"):
|
|
138
|
+
value = properties.get(key)
|
|
136
139
|
if isinstance(value, str) and value:
|
|
137
140
|
return value
|
|
141
|
+
info = properties.get("info")
|
|
142
|
+
if isinstance(info, dict):
|
|
143
|
+
for key in ("sessionID", "sessionId", "session_id"):
|
|
144
|
+
value = info.get(key)
|
|
145
|
+
if isinstance(value, str) and value:
|
|
146
|
+
return value
|
|
147
|
+
part = properties.get("part")
|
|
148
|
+
if isinstance(part, dict):
|
|
149
|
+
for key in ("sessionID", "sessionId", "session_id"):
|
|
150
|
+
value = part.get(key)
|
|
151
|
+
if isinstance(value, str) and value:
|
|
152
|
+
return value
|
|
138
153
|
session = payload.get("session")
|
|
139
154
|
if isinstance(session, dict):
|
|
140
|
-
return extract_session_id(session, allow_fallback_id=
|
|
155
|
+
return extract_session_id(session, allow_fallback_id=True)
|
|
141
156
|
return None
|
|
142
157
|
|
|
143
158
|
|
|
@@ -676,7 +691,7 @@ async def opencode_missing_env(
|
|
|
676
691
|
providers = [entry for entry in payload if isinstance(entry, dict)]
|
|
677
692
|
for provider in providers:
|
|
678
693
|
pid = provider.get("id") or provider.get("providerID")
|
|
679
|
-
if pid != provider_id:
|
|
694
|
+
if not pid or pid != provider_id:
|
|
680
695
|
continue
|
|
681
696
|
if _provider_has_auth(pid, workspace_root):
|
|
682
697
|
return []
|
|
@@ -732,6 +747,7 @@ async def collect_opencode_output_from_events(
|
|
|
732
747
|
events: Optional[AsyncIterator[SSEEvent]] = None,
|
|
733
748
|
*,
|
|
734
749
|
session_id: str,
|
|
750
|
+
model_payload: Optional[dict[str, str]] = None,
|
|
735
751
|
progress_session_ids: Optional[set[str]] = None,
|
|
736
752
|
permission_policy: str = PERMISSION_ALLOW,
|
|
737
753
|
permission_handler: Optional[PermissionHandler] = None,
|
|
@@ -753,19 +769,23 @@ async def collect_opencode_output_from_events(
|
|
|
753
769
|
error: Optional[str] = None
|
|
754
770
|
message_roles: dict[str, str] = {}
|
|
755
771
|
message_roles_seen = False
|
|
756
|
-
last_role_seen: Optional[str] = None
|
|
757
772
|
pending_text: dict[str, list[str]] = {}
|
|
773
|
+
pending_no_id: list[str] = []
|
|
774
|
+
no_id_role: Optional[str] = None
|
|
758
775
|
fallback_message: Optional[tuple[Optional[str], Optional[str], str]] = None
|
|
759
776
|
last_usage_total: Optional[int] = None
|
|
760
777
|
last_context_window: Optional[int] = None
|
|
761
778
|
part_types: dict[str, str] = {}
|
|
762
|
-
seen_question_request_ids: set[tuple[str, str]] = set()
|
|
779
|
+
seen_question_request_ids: set[tuple[Optional[str], str]] = set()
|
|
763
780
|
logged_permission_errors: set[str] = set()
|
|
764
781
|
normalized_question_policy = _normalize_question_policy(question_policy)
|
|
765
782
|
logger = logging.getLogger(__name__)
|
|
766
783
|
providers_cache: Optional[list[dict[str, Any]]] = None
|
|
767
784
|
context_window_cache: dict[str, Optional[int]] = {}
|
|
768
785
|
session_model_ids: Optional[tuple[Optional[str], Optional[str]]] = None
|
|
786
|
+
default_model_ids = (
|
|
787
|
+
_extract_model_ids(model_payload) if isinstance(model_payload, dict) else None
|
|
788
|
+
)
|
|
769
789
|
|
|
770
790
|
def _message_id_from_info(info: Any) -> Optional[str]:
|
|
771
791
|
if not isinstance(info, dict):
|
|
@@ -786,7 +806,7 @@ async def collect_opencode_output_from_events(
|
|
|
786
806
|
return None
|
|
787
807
|
|
|
788
808
|
def _register_message_role(payload: Any) -> tuple[Optional[str], Optional[str]]:
|
|
789
|
-
nonlocal
|
|
809
|
+
nonlocal message_roles_seen
|
|
790
810
|
if not isinstance(payload, dict):
|
|
791
811
|
return None, None
|
|
792
812
|
info = payload.get("info")
|
|
@@ -799,18 +819,27 @@ async def collect_opencode_output_from_events(
|
|
|
799
819
|
if isinstance(role, str) and msg_id:
|
|
800
820
|
message_roles[msg_id] = role
|
|
801
821
|
message_roles_seen = True
|
|
802
|
-
last_role_seen = role
|
|
803
822
|
return msg_id, role if isinstance(role, str) else None
|
|
804
823
|
|
|
824
|
+
def _flush_pending_no_id_as_assistant() -> None:
|
|
825
|
+
nonlocal no_id_role
|
|
826
|
+
if pending_no_id:
|
|
827
|
+
text_parts.extend(pending_no_id)
|
|
828
|
+
pending_no_id.clear()
|
|
829
|
+
no_id_role = "assistant"
|
|
830
|
+
|
|
831
|
+
def _discard_pending_no_id() -> None:
|
|
832
|
+
if pending_no_id:
|
|
833
|
+
pending_no_id.clear()
|
|
834
|
+
|
|
805
835
|
def _append_text_for_message(message_id: Optional[str], text: str) -> None:
|
|
806
836
|
if not text:
|
|
807
837
|
return
|
|
808
838
|
if message_id is None:
|
|
809
|
-
if
|
|
810
|
-
text_parts.append(text)
|
|
811
|
-
return
|
|
812
|
-
if last_role_seen != "user":
|
|
839
|
+
if no_id_role == "assistant":
|
|
813
840
|
text_parts.append(text)
|
|
841
|
+
else:
|
|
842
|
+
pending_no_id.append(text)
|
|
814
843
|
return
|
|
815
844
|
role = message_roles.get(message_id)
|
|
816
845
|
if role == "user":
|
|
@@ -832,26 +861,50 @@ async def collect_opencode_output_from_events(
|
|
|
832
861
|
text_parts.extend(pending)
|
|
833
862
|
|
|
834
863
|
def _flush_all_pending_text() -> None:
|
|
835
|
-
if
|
|
864
|
+
if pending_text:
|
|
865
|
+
for pending in list(pending_text.values()):
|
|
866
|
+
if pending:
|
|
867
|
+
text_parts.extend(pending)
|
|
868
|
+
pending_text.clear()
|
|
869
|
+
if pending_no_id:
|
|
870
|
+
# If we have not seen a role yet, assume assistant for backwards
|
|
871
|
+
# compatibility with providers that omit roles entirely. Otherwise,
|
|
872
|
+
# only flush when we have already classified no-id text as assistant
|
|
873
|
+
# or when we have no other text (to avoid echoing user prompts).
|
|
874
|
+
if not message_roles_seen or no_id_role == "assistant" or not text_parts:
|
|
875
|
+
text_parts.extend(pending_no_id)
|
|
876
|
+
pending_no_id.clear()
|
|
877
|
+
|
|
878
|
+
def _handle_role_update(message_id: Optional[str], role: Optional[str]) -> None:
|
|
879
|
+
nonlocal no_id_role
|
|
880
|
+
if not role:
|
|
881
|
+
return
|
|
882
|
+
if role == "assistant":
|
|
883
|
+
_flush_pending_text(message_id)
|
|
884
|
+
_flush_pending_no_id_as_assistant()
|
|
836
885
|
return
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
886
|
+
if role == "user":
|
|
887
|
+
_flush_pending_text(message_id)
|
|
888
|
+
_discard_pending_no_id()
|
|
889
|
+
no_id_role = None
|
|
841
890
|
|
|
842
891
|
async def _resolve_session_model_ids() -> tuple[Optional[str], Optional[str]]:
|
|
843
892
|
nonlocal session_model_ids
|
|
844
893
|
if session_model_ids is not None:
|
|
845
894
|
return session_model_ids
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
895
|
+
resolved_ids: Optional[tuple[Optional[str], Optional[str]]] = None
|
|
896
|
+
if session_fetcher is not None:
|
|
897
|
+
try:
|
|
898
|
+
payload = await session_fetcher()
|
|
899
|
+
resolved_ids = _extract_model_ids(payload)
|
|
900
|
+
except Exception:
|
|
901
|
+
resolved_ids = None
|
|
902
|
+
# If we failed to resolve model ids from the session (including the empty
|
|
903
|
+
# tuple case), fall back to the caller-provided model payload so we can
|
|
904
|
+
# still backfill usage metadata.
|
|
905
|
+
if not resolved_ids or all(value is None for value in resolved_ids):
|
|
906
|
+
resolved_ids = default_model_ids
|
|
907
|
+
session_model_ids = resolved_ids or (None, None)
|
|
855
908
|
return session_model_ids
|
|
856
909
|
|
|
857
910
|
async def _resolve_context_window_from_providers(
|
|
@@ -946,7 +999,7 @@ async def collect_opencode_output_from_events(
|
|
|
946
999
|
await aclose()
|
|
947
1000
|
|
|
948
1001
|
stream_iter = _new_stream().__aiter__()
|
|
949
|
-
|
|
1002
|
+
last_relevant_event_at = time.monotonic()
|
|
950
1003
|
last_primary_completion_at: Optional[float] = None
|
|
951
1004
|
reconnect_attempts = 0
|
|
952
1005
|
can_reconnect = (
|
|
@@ -981,6 +1034,7 @@ async def collect_opencode_output_from_events(
|
|
|
981
1034
|
session_id=session_id,
|
|
982
1035
|
exc=exc,
|
|
983
1036
|
)
|
|
1037
|
+
idle_seconds = now - last_relevant_event_at
|
|
984
1038
|
if _status_is_idle(status_type):
|
|
985
1039
|
log_event(
|
|
986
1040
|
logger,
|
|
@@ -988,9 +1042,9 @@ async def collect_opencode_output_from_events(
|
|
|
988
1042
|
"opencode.stream.stalled.session_idle",
|
|
989
1043
|
session_id=session_id,
|
|
990
1044
|
status_type=status_type,
|
|
991
|
-
idle_seconds=
|
|
1045
|
+
idle_seconds=idle_seconds,
|
|
992
1046
|
)
|
|
993
|
-
if not text_parts and pending_text:
|
|
1047
|
+
if not text_parts and (pending_text or pending_no_id):
|
|
994
1048
|
_flush_all_pending_text()
|
|
995
1049
|
break
|
|
996
1050
|
if last_primary_completion_at is not None:
|
|
@@ -1000,7 +1054,7 @@ async def collect_opencode_output_from_events(
|
|
|
1000
1054
|
"opencode.stream.stalled.after_completion",
|
|
1001
1055
|
session_id=session_id,
|
|
1002
1056
|
status_type=status_type,
|
|
1003
|
-
idle_seconds=
|
|
1057
|
+
idle_seconds=idle_seconds,
|
|
1004
1058
|
)
|
|
1005
1059
|
if not can_reconnect:
|
|
1006
1060
|
break
|
|
@@ -1015,7 +1069,7 @@ async def collect_opencode_output_from_events(
|
|
|
1015
1069
|
logging.WARNING,
|
|
1016
1070
|
"opencode.stream.stalled.reconnecting",
|
|
1017
1071
|
session_id=session_id,
|
|
1018
|
-
idle_seconds=
|
|
1072
|
+
idle_seconds=idle_seconds,
|
|
1019
1073
|
backoff_seconds=backoff,
|
|
1020
1074
|
status_type=status_type,
|
|
1021
1075
|
attempts=reconnect_attempts,
|
|
@@ -1023,21 +1077,86 @@ async def collect_opencode_output_from_events(
|
|
|
1023
1077
|
await _close_stream(stream_iter)
|
|
1024
1078
|
await asyncio.sleep(backoff)
|
|
1025
1079
|
stream_iter = _new_stream().__aiter__()
|
|
1080
|
+
last_relevant_event_at = now
|
|
1026
1081
|
continue
|
|
1027
|
-
|
|
1082
|
+
now = time.monotonic()
|
|
1028
1083
|
raw = event.data or ""
|
|
1029
1084
|
try:
|
|
1030
1085
|
payload = json.loads(raw) if raw else {}
|
|
1031
1086
|
except json.JSONDecodeError:
|
|
1032
1087
|
payload = {}
|
|
1033
1088
|
event_session_id = extract_session_id(payload)
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1089
|
+
is_relevant = False
|
|
1090
|
+
if event_session_id:
|
|
1091
|
+
if progress_session_ids is None:
|
|
1092
|
+
is_relevant = event_session_id == session_id
|
|
1093
|
+
else:
|
|
1094
|
+
is_relevant = event_session_id in progress_session_ids
|
|
1095
|
+
if not is_relevant:
|
|
1096
|
+
if (
|
|
1097
|
+
stall_timeout_seconds is not None
|
|
1098
|
+
and now - last_relevant_event_at > stall_timeout_seconds
|
|
1099
|
+
):
|
|
1100
|
+
idle_seconds = now - last_relevant_event_at
|
|
1101
|
+
last_relevant_event_at = now
|
|
1102
|
+
status_type = None
|
|
1103
|
+
if session_fetcher is not None:
|
|
1104
|
+
try:
|
|
1105
|
+
payload = await session_fetcher()
|
|
1106
|
+
status_type = _extract_status_type(payload)
|
|
1107
|
+
except Exception as exc:
|
|
1108
|
+
log_event(
|
|
1109
|
+
logger,
|
|
1110
|
+
logging.WARNING,
|
|
1111
|
+
"opencode.session.poll_failed",
|
|
1112
|
+
session_id=session_id,
|
|
1113
|
+
exc=exc,
|
|
1114
|
+
)
|
|
1115
|
+
if _status_is_idle(status_type):
|
|
1116
|
+
log_event(
|
|
1117
|
+
logger,
|
|
1118
|
+
logging.INFO,
|
|
1119
|
+
"opencode.stream.stalled.session_idle",
|
|
1120
|
+
session_id=session_id,
|
|
1121
|
+
status_type=status_type,
|
|
1122
|
+
idle_seconds=idle_seconds,
|
|
1123
|
+
)
|
|
1124
|
+
if not text_parts and (pending_text or pending_no_id):
|
|
1125
|
+
_flush_all_pending_text()
|
|
1126
|
+
break
|
|
1127
|
+
if last_primary_completion_at is not None:
|
|
1128
|
+
log_event(
|
|
1129
|
+
logger,
|
|
1130
|
+
logging.INFO,
|
|
1131
|
+
"opencode.stream.stalled.after_completion",
|
|
1132
|
+
session_id=session_id,
|
|
1133
|
+
status_type=status_type,
|
|
1134
|
+
idle_seconds=idle_seconds,
|
|
1135
|
+
)
|
|
1136
|
+
if not can_reconnect:
|
|
1137
|
+
break
|
|
1138
|
+
backoff_index = min(
|
|
1139
|
+
reconnect_attempts,
|
|
1140
|
+
len(_OPENCODE_STREAM_RECONNECT_BACKOFF_SECONDS) - 1,
|
|
1141
|
+
)
|
|
1142
|
+
backoff = _OPENCODE_STREAM_RECONNECT_BACKOFF_SECONDS[backoff_index]
|
|
1143
|
+
reconnect_attempts += 1
|
|
1144
|
+
log_event(
|
|
1145
|
+
logger,
|
|
1146
|
+
logging.WARNING,
|
|
1147
|
+
"opencode.stream.stalled.reconnecting",
|
|
1148
|
+
session_id=session_id,
|
|
1149
|
+
idle_seconds=idle_seconds,
|
|
1150
|
+
backoff_seconds=backoff,
|
|
1151
|
+
status_type=status_type,
|
|
1152
|
+
attempts=reconnect_attempts,
|
|
1153
|
+
)
|
|
1154
|
+
await _close_stream(stream_iter)
|
|
1155
|
+
await asyncio.sleep(backoff)
|
|
1156
|
+
stream_iter = _new_stream().__aiter__()
|
|
1040
1157
|
continue
|
|
1158
|
+
last_relevant_event_at = now
|
|
1159
|
+
reconnect_attempts = 0
|
|
1041
1160
|
is_primary_session = event_session_id == session_id
|
|
1042
1161
|
if event.event == "question.asked":
|
|
1043
1162
|
request_id, props = _extract_question_request(payload)
|
|
@@ -1219,8 +1338,7 @@ async def collect_opencode_output_from_events(
|
|
|
1219
1338
|
if event.event in ("message.updated", "message.completed"):
|
|
1220
1339
|
if is_primary_session:
|
|
1221
1340
|
msg_id, role = _register_message_role(payload)
|
|
1222
|
-
|
|
1223
|
-
_flush_pending_text(msg_id)
|
|
1341
|
+
_handle_role_update(msg_id, role)
|
|
1224
1342
|
if event.event == "message.part.updated":
|
|
1225
1343
|
properties = (
|
|
1226
1344
|
payload.get("properties") if isinstance(payload, dict) else None
|
|
@@ -1393,7 +1511,7 @@ async def collect_opencode_output_from_events(
|
|
|
1393
1511
|
):
|
|
1394
1512
|
if not is_primary_session:
|
|
1395
1513
|
continue
|
|
1396
|
-
if not text_parts and pending_text:
|
|
1514
|
+
if not text_parts and (pending_text or pending_no_id):
|
|
1397
1515
|
_flush_all_pending_text()
|
|
1398
1516
|
break
|
|
1399
1517
|
if event.event == "message.completed" and is_primary_session:
|
|
@@ -1408,7 +1526,7 @@ async def collect_opencode_output_from_events(
|
|
|
1408
1526
|
resolved_role = message_roles.get(msg_id)
|
|
1409
1527
|
if resolved_role == "assistant":
|
|
1410
1528
|
_append_text_for_message(msg_id, text)
|
|
1411
|
-
if pending_text:
|
|
1529
|
+
if pending_text or pending_no_id:
|
|
1412
1530
|
_flush_all_pending_text()
|
|
1413
1531
|
|
|
1414
1532
|
return OpenCodeTurnOutput(text="".join(text_parts).strip(), error=error)
|
|
@@ -1419,6 +1537,7 @@ async def collect_opencode_output(
|
|
|
1419
1537
|
*,
|
|
1420
1538
|
session_id: str,
|
|
1421
1539
|
workspace_path: str,
|
|
1540
|
+
model_payload: Optional[dict[str, str]] = None,
|
|
1422
1541
|
progress_session_ids: Optional[set[str]] = None,
|
|
1423
1542
|
permission_policy: str = PERMISSION_ALLOW,
|
|
1424
1543
|
permission_handler: Optional[PermissionHandler] = None,
|
|
@@ -1427,6 +1546,7 @@ async def collect_opencode_output(
|
|
|
1427
1546
|
should_stop: Optional[Callable[[], bool]] = None,
|
|
1428
1547
|
ready_event: Optional[Any] = None,
|
|
1429
1548
|
part_handler: Optional[PartHandler] = None,
|
|
1549
|
+
stall_timeout_seconds: Optional[float] = _OPENCODE_STREAM_STALL_TIMEOUT_SECONDS,
|
|
1430
1550
|
) -> OpenCodeTurnOutput:
|
|
1431
1551
|
async def _respond(request_id: str, reply: str) -> None:
|
|
1432
1552
|
await client.respond_permission(request_id=request_id, reply=reply)
|
|
@@ -1438,14 +1558,21 @@ async def collect_opencode_output(
|
|
|
1438
1558
|
await client.reject_question(request_id)
|
|
1439
1559
|
|
|
1440
1560
|
def _stream_factory() -> AsyncIterator[SSEEvent]:
|
|
1441
|
-
return
|
|
1561
|
+
return cast(
|
|
1562
|
+
AsyncIterator[SSEEvent],
|
|
1563
|
+
client.stream_events(directory=workspace_path, ready_event=ready_event),
|
|
1564
|
+
)
|
|
1442
1565
|
|
|
1443
1566
|
async def _fetch_session() -> Any:
|
|
1444
1567
|
statuses = await client.session_status(directory=workspace_path)
|
|
1445
1568
|
if isinstance(statuses, dict):
|
|
1446
1569
|
session_status = statuses.get(session_id)
|
|
1570
|
+
if session_status is None:
|
|
1571
|
+
return {"status": {"type": "idle"}}
|
|
1447
1572
|
if isinstance(session_status, dict):
|
|
1448
1573
|
return {"status": session_status}
|
|
1574
|
+
if isinstance(session_status, str):
|
|
1575
|
+
return {"status": session_status}
|
|
1449
1576
|
return {"status": {}}
|
|
1450
1577
|
|
|
1451
1578
|
async def _fetch_providers() -> Any:
|
|
@@ -1465,8 +1592,10 @@ async def collect_opencode_output(
|
|
|
1465
1592
|
reject_question=_reject_question,
|
|
1466
1593
|
part_handler=part_handler,
|
|
1467
1594
|
event_stream_factory=_stream_factory,
|
|
1595
|
+
model_payload=model_payload,
|
|
1468
1596
|
session_fetcher=_fetch_session,
|
|
1469
1597
|
provider_fetcher=_fetch_providers,
|
|
1598
|
+
stall_timeout_seconds=stall_timeout_seconds,
|
|
1470
1599
|
)
|
|
1471
1600
|
|
|
1472
1601
|
|