codex-autorunner 1.0.0__py3-none-any.whl → 1.2.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/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/client.py +113 -4
- codex_autorunner/agents/opencode/constants.py +3 -0
- codex_autorunner/agents/opencode/harness.py +6 -1
- codex_autorunner/agents/opencode/runtime.py +59 -18
- codex_autorunner/agents/opencode/supervisor.py +4 -0
- codex_autorunner/agents/registry.py +36 -7
- codex_autorunner/bootstrap.py +226 -4
- codex_autorunner/cli.py +5 -1174
- codex_autorunner/codex_cli.py +20 -84
- codex_autorunner/core/__init__.py +20 -0
- codex_autorunner/core/about_car.py +119 -1
- codex_autorunner/core/app_server_ids.py +59 -0
- codex_autorunner/core/app_server_threads.py +17 -2
- codex_autorunner/core/app_server_utils.py +165 -0
- codex_autorunner/core/archive.py +349 -0
- codex_autorunner/core/codex_runner.py +6 -2
- codex_autorunner/core/config.py +433 -4
- codex_autorunner/core/context_awareness.py +38 -0
- codex_autorunner/core/docs.py +0 -122
- codex_autorunner/core/drafts.py +58 -4
- codex_autorunner/core/exceptions.py +4 -0
- codex_autorunner/core/filebox.py +265 -0
- codex_autorunner/core/flows/controller.py +96 -2
- codex_autorunner/core/flows/models.py +13 -0
- codex_autorunner/core/flows/reasons.py +52 -0
- codex_autorunner/core/flows/reconciler.py +134 -0
- codex_autorunner/core/flows/runtime.py +57 -4
- codex_autorunner/core/flows/store.py +142 -7
- codex_autorunner/core/flows/transition.py +27 -15
- codex_autorunner/core/flows/ux_helpers.py +272 -0
- codex_autorunner/core/flows/worker_process.py +32 -6
- codex_autorunner/core/git_utils.py +62 -0
- codex_autorunner/core/hub.py +291 -20
- codex_autorunner/core/lifecycle_events.py +253 -0
- codex_autorunner/core/notifications.py +14 -2
- codex_autorunner/core/path_utils.py +2 -1
- codex_autorunner/core/pma_audit.py +224 -0
- codex_autorunner/core/pma_context.py +496 -0
- codex_autorunner/core/pma_dispatch_interceptor.py +284 -0
- codex_autorunner/core/pma_lifecycle.py +527 -0
- codex_autorunner/core/pma_queue.py +367 -0
- codex_autorunner/core/pma_safety.py +221 -0
- codex_autorunner/core/pma_state.py +115 -0
- codex_autorunner/core/ports/__init__.py +28 -0
- codex_autorunner/{integrations/agents → core/ports}/agent_backend.py +13 -8
- codex_autorunner/core/ports/backend_orchestrator.py +41 -0
- codex_autorunner/{integrations/agents → core/ports}/run_event.py +23 -6
- codex_autorunner/core/prompt.py +0 -80
- codex_autorunner/core/prompts.py +56 -172
- codex_autorunner/core/redaction.py +0 -4
- codex_autorunner/core/review_context.py +11 -9
- codex_autorunner/core/runner_controller.py +35 -33
- codex_autorunner/core/runner_state.py +147 -0
- codex_autorunner/core/runtime.py +829 -0
- codex_autorunner/core/sqlite_utils.py +13 -4
- codex_autorunner/core/state.py +7 -10
- codex_autorunner/core/state_roots.py +62 -0
- codex_autorunner/core/supervisor_protocol.py +15 -0
- codex_autorunner/core/templates/__init__.py +39 -0
- codex_autorunner/core/templates/git_mirror.py +234 -0
- codex_autorunner/core/templates/provenance.py +56 -0
- codex_autorunner/core/templates/scan_cache.py +120 -0
- codex_autorunner/core/text_delta_coalescer.py +54 -0
- codex_autorunner/core/ticket_linter_cli.py +218 -0
- codex_autorunner/core/ticket_manager_cli.py +494 -0
- codex_autorunner/core/time_utils.py +11 -0
- codex_autorunner/core/types.py +18 -0
- codex_autorunner/core/update.py +4 -5
- codex_autorunner/core/update_paths.py +28 -0
- codex_autorunner/core/usage.py +164 -12
- codex_autorunner/core/utils.py +125 -15
- codex_autorunner/flows/review/__init__.py +17 -0
- codex_autorunner/{core/review.py → flows/review/service.py} +37 -34
- codex_autorunner/flows/ticket_flow/definition.py +52 -3
- codex_autorunner/integrations/agents/__init__.py +11 -19
- codex_autorunner/integrations/agents/backend_orchestrator.py +302 -0
- codex_autorunner/integrations/agents/codex_adapter.py +90 -0
- codex_autorunner/integrations/agents/codex_backend.py +177 -25
- codex_autorunner/integrations/agents/opencode_adapter.py +108 -0
- codex_autorunner/integrations/agents/opencode_backend.py +305 -32
- codex_autorunner/integrations/agents/runner.py +86 -0
- codex_autorunner/integrations/agents/wiring.py +279 -0
- codex_autorunner/integrations/app_server/client.py +7 -60
- 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/telegram/adapter.py +65 -0
- codex_autorunner/integrations/telegram/config.py +46 -0
- codex_autorunner/integrations/telegram/constants.py +1 -1
- codex_autorunner/integrations/telegram/doctor.py +228 -6
- codex_autorunner/integrations/telegram/handlers/callbacks.py +7 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +236 -74
- codex_autorunner/integrations/telegram/handlers/commands/files.py +314 -75
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +1496 -71
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +498 -37
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +206 -48
- codex_autorunner/integrations/telegram/handlers/commands_spec.py +20 -3
- codex_autorunner/integrations/telegram/handlers/messages.py +27 -1
- codex_autorunner/integrations/telegram/handlers/selections.py +61 -1
- codex_autorunner/integrations/telegram/helpers.py +22 -1
- codex_autorunner/integrations/telegram/runtime.py +9 -4
- codex_autorunner/integrations/telegram/service.py +45 -10
- codex_autorunner/integrations/telegram/state.py +38 -0
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +338 -43
- codex_autorunner/integrations/telegram/transport.py +13 -4
- codex_autorunner/integrations/templates/__init__.py +27 -0
- codex_autorunner/integrations/templates/scan_agent.py +312 -0
- codex_autorunner/routes/__init__.py +37 -76
- codex_autorunner/routes/agents.py +2 -137
- codex_autorunner/routes/analytics.py +2 -238
- codex_autorunner/routes/app_server.py +2 -131
- codex_autorunner/routes/base.py +2 -596
- codex_autorunner/routes/file_chat.py +4 -833
- codex_autorunner/routes/flows.py +4 -977
- codex_autorunner/routes/messages.py +4 -456
- 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 -193
- codex_autorunner/routes/usage.py +2 -86
- codex_autorunner/routes/voice.py +2 -119
- codex_autorunner/routes/workspace.py +2 -270
- codex_autorunner/server.py +4 -4
- codex_autorunner/static/agentControls.js +61 -16
- codex_autorunner/static/app.js +126 -14
- codex_autorunner/static/archive.js +826 -0
- codex_autorunner/static/archiveApi.js +37 -0
- codex_autorunner/static/autoRefresh.js +7 -7
- codex_autorunner/static/chatUploads.js +137 -0
- codex_autorunner/static/dashboard.js +224 -171
- codex_autorunner/static/docChatCore.js +185 -13
- codex_autorunner/static/fileChat.js +68 -40
- codex_autorunner/static/fileboxUi.js +159 -0
- codex_autorunner/static/hub.js +114 -131
- codex_autorunner/static/index.html +375 -49
- codex_autorunner/static/messages.js +568 -87
- codex_autorunner/static/notifications.js +255 -0
- codex_autorunner/static/pma.js +1167 -0
- codex_autorunner/static/preserve.js +17 -0
- codex_autorunner/static/settings.js +128 -6
- codex_autorunner/static/smartRefresh.js +52 -0
- codex_autorunner/static/streamUtils.js +57 -0
- codex_autorunner/static/styles.css +9798 -6143
- codex_autorunner/static/tabs.js +152 -11
- codex_autorunner/static/templateReposSettings.js +225 -0
- codex_autorunner/static/terminal.js +18 -0
- codex_autorunner/static/ticketChatActions.js +165 -3
- codex_autorunner/static/ticketChatStream.js +17 -119
- codex_autorunner/static/ticketEditor.js +137 -15
- codex_autorunner/static/ticketTemplates.js +798 -0
- codex_autorunner/static/tickets.js +821 -98
- codex_autorunner/static/turnEvents.js +27 -0
- codex_autorunner/static/turnResume.js +33 -0
- codex_autorunner/static/utils.js +39 -0
- codex_autorunner/static/workspace.js +389 -82
- codex_autorunner/static/workspaceFileBrowser.js +15 -13
- codex_autorunner/surfaces/__init__.py +5 -0
- codex_autorunner/surfaces/cli/__init__.py +6 -0
- codex_autorunner/surfaces/cli/cli.py +2534 -0
- codex_autorunner/surfaces/cli/codex_cli.py +20 -0
- codex_autorunner/surfaces/cli/pma_cli.py +817 -0
- codex_autorunner/surfaces/telegram/__init__.py +3 -0
- codex_autorunner/surfaces/web/__init__.py +1 -0
- codex_autorunner/surfaces/web/app.py +2223 -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 +82 -0
- codex_autorunner/surfaces/web/routes/agents.py +138 -0
- codex_autorunner/surfaces/web/routes/analytics.py +284 -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 +1117 -0
- codex_autorunner/surfaces/web/routes/filebox.py +227 -0
- codex_autorunner/surfaces/web/routes/flows.py +1354 -0
- codex_autorunner/surfaces/web/routes/messages.py +490 -0
- codex_autorunner/surfaces/web/routes/pma.py +1652 -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 +277 -0
- codex_autorunner/surfaces/web/routes/system.py +196 -0
- codex_autorunner/surfaces/web/routes/templates.py +634 -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 +469 -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 +8 -1
- codex_autorunner/tickets/agent_pool.py +53 -4
- codex_autorunner/tickets/files.py +37 -16
- codex_autorunner/tickets/lint.py +50 -0
- codex_autorunner/tickets/models.py +6 -1
- codex_autorunner/tickets/outbox.py +50 -2
- codex_autorunner/tickets/runner.py +396 -57
- codex_autorunner/web/__init__.py +5 -1
- codex_autorunner/web/app.py +2 -1949
- codex_autorunner/web/hub_jobs.py +2 -191
- codex_autorunner/web/middleware.py +2 -586
- codex_autorunner/web/pty_session.py +2 -369
- codex_autorunner/web/runner_manager.py +2 -24
- codex_autorunner/web/schemas.py +2 -376
- codex_autorunner/web/static_assets.py +4 -441
- codex_autorunner/web/static_refresh.py +2 -85
- codex_autorunner/web/terminal_sessions.py +2 -77
- codex_autorunner/workspace/paths.py +49 -33
- codex_autorunner-1.2.0.dist-info/METADATA +150 -0
- codex_autorunner-1.2.0.dist-info/RECORD +339 -0
- codex_autorunner/core/adapter_utils.py +0 -21
- codex_autorunner/core/engine.py +0 -2653
- codex_autorunner/core/static_assets.py +0 -55
- codex_autorunner-1.0.0.dist-info/METADATA +0 -246
- codex_autorunner-1.0.0.dist-info/RECORD +0 -251
- /codex_autorunner/{routes → surfaces/web/routes}/terminal_images.py +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/WHEEL +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-1.0.0.dist-info → codex_autorunner-1.2.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
|
|
@@ -21,6 +21,7 @@ class OpenCodeApiProfile:
|
|
|
21
21
|
|
|
22
22
|
supports_prompt_async: bool = True
|
|
23
23
|
supports_global_endpoints: bool = True
|
|
24
|
+
max_text_chars: Optional[int] = None
|
|
24
25
|
spec_fetched: bool = False
|
|
25
26
|
|
|
26
27
|
|
|
@@ -83,6 +84,7 @@ class OpenCodeClient:
|
|
|
83
84
|
*,
|
|
84
85
|
auth: Optional[tuple[str, str]] = None,
|
|
85
86
|
timeout: Optional[float] = None,
|
|
87
|
+
max_text_chars: Optional[int] = None,
|
|
86
88
|
logger: Optional[logging.Logger] = None,
|
|
87
89
|
) -> None:
|
|
88
90
|
self._client = httpx.AsyncClient(
|
|
@@ -93,6 +95,10 @@ class OpenCodeClient:
|
|
|
93
95
|
self._logger = logger or logging.getLogger(__name__)
|
|
94
96
|
self._api_profile: Optional[OpenCodeApiProfile] = None
|
|
95
97
|
self._api_profile_lock = asyncio.Lock()
|
|
98
|
+
self._max_text_chars_override = (
|
|
99
|
+
int(max_text_chars) if isinstance(max_text_chars, int) else None
|
|
100
|
+
)
|
|
101
|
+
self._max_text_chars_cache: Optional[int] = None
|
|
96
102
|
|
|
97
103
|
async def close(self) -> None:
|
|
98
104
|
await self._client.aclose()
|
|
@@ -121,6 +127,7 @@ class OpenCodeClient:
|
|
|
121
127
|
profile.supports_global_endpoints = self.has_endpoint(
|
|
122
128
|
spec, "get", "/global/health"
|
|
123
129
|
) or self.has_endpoint(spec, "get", "/global/event")
|
|
130
|
+
profile.max_text_chars = self._extract_max_text_chars(spec)
|
|
124
131
|
|
|
125
132
|
log_event(
|
|
126
133
|
self._logger,
|
|
@@ -147,6 +154,103 @@ class OpenCodeClient:
|
|
|
147
154
|
return OpenCodeApiProfile()
|
|
148
155
|
return self._api_profile
|
|
149
156
|
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _extract_max_text_chars(spec: dict[str, Any]) -> Optional[int]:
|
|
159
|
+
if not isinstance(spec, dict):
|
|
160
|
+
return None
|
|
161
|
+
components = spec.get("components")
|
|
162
|
+
if not isinstance(components, dict):
|
|
163
|
+
return None
|
|
164
|
+
schemas = components.get("schemas")
|
|
165
|
+
if not isinstance(schemas, dict):
|
|
166
|
+
return None
|
|
167
|
+
candidates: list[int] = []
|
|
168
|
+
for schema in schemas.values():
|
|
169
|
+
max_len = OpenCodeClient._find_text_max_length(schema)
|
|
170
|
+
if isinstance(max_len, int) and max_len > 0:
|
|
171
|
+
candidates.append(max_len)
|
|
172
|
+
return min(candidates) if candidates else None
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def _find_text_max_length(schema: Any) -> Optional[int]:
|
|
176
|
+
if not isinstance(schema, dict):
|
|
177
|
+
return None
|
|
178
|
+
candidates: list[int] = []
|
|
179
|
+
properties = schema.get("properties")
|
|
180
|
+
if isinstance(properties, dict) and "text" in properties:
|
|
181
|
+
text_schema = properties.get("text")
|
|
182
|
+
if isinstance(text_schema, dict):
|
|
183
|
+
max_len = text_schema.get("maxLength")
|
|
184
|
+
if isinstance(max_len, int) and max_len > 0:
|
|
185
|
+
candidates.append(max_len)
|
|
186
|
+
for key in ("allOf", "anyOf", "oneOf"):
|
|
187
|
+
seq = schema.get(key)
|
|
188
|
+
if isinstance(seq, list):
|
|
189
|
+
for item in seq:
|
|
190
|
+
item_len = OpenCodeClient._find_text_max_length(item)
|
|
191
|
+
if isinstance(item_len, int) and item_len > 0:
|
|
192
|
+
candidates.append(item_len)
|
|
193
|
+
return min(candidates) if candidates else None
|
|
194
|
+
|
|
195
|
+
def set_max_text_chars(self, value: Optional[int]) -> None:
|
|
196
|
+
self._max_text_chars_override = int(value) if isinstance(value, int) else None
|
|
197
|
+
self._max_text_chars_cache = None
|
|
198
|
+
|
|
199
|
+
async def _resolve_max_text_chars(
|
|
200
|
+
self, profile: Optional[OpenCodeApiProfile] = None
|
|
201
|
+
) -> Optional[int]:
|
|
202
|
+
if self._max_text_chars_cache is not None:
|
|
203
|
+
return self._max_text_chars_cache
|
|
204
|
+
if profile is None:
|
|
205
|
+
profile = await self.detect_api_shape()
|
|
206
|
+
detected = (
|
|
207
|
+
profile.max_text_chars
|
|
208
|
+
if isinstance(profile.max_text_chars, int) and profile.max_text_chars > 0
|
|
209
|
+
else None
|
|
210
|
+
)
|
|
211
|
+
override = (
|
|
212
|
+
self._max_text_chars_override
|
|
213
|
+
if isinstance(self._max_text_chars_override, int)
|
|
214
|
+
and self._max_text_chars_override > 0
|
|
215
|
+
else None
|
|
216
|
+
)
|
|
217
|
+
if override is None:
|
|
218
|
+
resolved = detected
|
|
219
|
+
elif detected is None:
|
|
220
|
+
resolved = override
|
|
221
|
+
else:
|
|
222
|
+
resolved = min(override, detected)
|
|
223
|
+
self._max_text_chars_cache = resolved
|
|
224
|
+
return resolved
|
|
225
|
+
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _split_text(text: str, max_chars: int) -> list[str]:
|
|
228
|
+
if max_chars <= 0 or len(text) <= max_chars:
|
|
229
|
+
return [text]
|
|
230
|
+
parts: list[str] = []
|
|
231
|
+
start = 0
|
|
232
|
+
length = len(text)
|
|
233
|
+
while start < length:
|
|
234
|
+
end = min(start + max_chars, length)
|
|
235
|
+
if end < length:
|
|
236
|
+
split = text.rfind("\n", start, end)
|
|
237
|
+
if split <= start:
|
|
238
|
+
split = text.rfind(" ", start, end)
|
|
239
|
+
if split > start:
|
|
240
|
+
end = split + 1
|
|
241
|
+
parts.append(text[start:end])
|
|
242
|
+
start = end
|
|
243
|
+
return [part for part in parts if part]
|
|
244
|
+
|
|
245
|
+
async def _build_text_parts(
|
|
246
|
+
self, message: str, profile: Optional[OpenCodeApiProfile] = None
|
|
247
|
+
) -> list[dict[str, str]]:
|
|
248
|
+
limit = await self._resolve_max_text_chars(profile)
|
|
249
|
+
if limit is None:
|
|
250
|
+
return [{"type": "text", "text": message}]
|
|
251
|
+
chunks = self._split_text(message, limit)
|
|
252
|
+
return [{"type": "text", "text": chunk} for chunk in chunks]
|
|
253
|
+
|
|
150
254
|
def _dir_params(self, directory: Optional[str]) -> dict[str, str]:
|
|
151
255
|
return {"directory": directory} if directory else {}
|
|
152
256
|
|
|
@@ -275,8 +379,10 @@ class OpenCodeClient:
|
|
|
275
379
|
model: Optional[dict[str, str]] = None,
|
|
276
380
|
variant: Optional[str] = None,
|
|
277
381
|
) -> Any:
|
|
382
|
+
profile = await self.detect_api_shape()
|
|
383
|
+
parts = await self._build_text_parts(message, profile)
|
|
278
384
|
payload: dict[str, Any] = {
|
|
279
|
-
"parts":
|
|
385
|
+
"parts": parts,
|
|
280
386
|
}
|
|
281
387
|
if agent:
|
|
282
388
|
payload["agent"] = agent
|
|
@@ -300,8 +406,10 @@ class OpenCodeClient:
|
|
|
300
406
|
model: Optional[dict[str, str]] = None,
|
|
301
407
|
variant: Optional[str] = None,
|
|
302
408
|
) -> Any:
|
|
409
|
+
profile = await self.detect_api_shape()
|
|
410
|
+
parts = await self._build_text_parts(message, profile)
|
|
303
411
|
payload: dict[str, Any] = {
|
|
304
|
-
"parts":
|
|
412
|
+
"parts": parts,
|
|
305
413
|
}
|
|
306
414
|
if agent:
|
|
307
415
|
payload["agent"] = agent
|
|
@@ -310,7 +418,6 @@ class OpenCodeClient:
|
|
|
310
418
|
if variant:
|
|
311
419
|
payload["variant"] = variant
|
|
312
420
|
|
|
313
|
-
profile = await self.detect_api_shape()
|
|
314
421
|
if profile.supports_prompt_async:
|
|
315
422
|
return await self._request(
|
|
316
423
|
"POST",
|
|
@@ -335,8 +442,10 @@ class OpenCodeClient:
|
|
|
335
442
|
model: Optional[dict[str, str]] = None,
|
|
336
443
|
variant: Optional[str] = None,
|
|
337
444
|
) -> Any:
|
|
445
|
+
profile = await self.detect_api_shape()
|
|
446
|
+
parts = await self._build_text_parts(message, profile)
|
|
338
447
|
payload: dict[str, Any] = {
|
|
339
|
-
"parts":
|
|
448
|
+
"parts": parts,
|
|
340
449
|
}
|
|
341
450
|
if agent:
|
|
342
451
|
payload["agent"] = agent
|
|
@@ -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:
|
|
@@ -122,6 +122,12 @@ def extract_session_id(
|
|
|
122
122
|
value = payload.get(key)
|
|
123
123
|
if isinstance(value, str) and value:
|
|
124
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
|
|
125
131
|
if allow_fallback_id:
|
|
126
132
|
value = payload.get("id")
|
|
127
133
|
if isinstance(value, str) and value:
|
|
@@ -132,6 +138,12 @@ def extract_session_id(
|
|
|
132
138
|
value = properties.get(key)
|
|
133
139
|
if isinstance(value, str) and value:
|
|
134
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
|
|
135
147
|
part = properties.get("part")
|
|
136
148
|
if isinstance(part, dict):
|
|
137
149
|
for key in ("sessionID", "sessionId", "session_id"):
|
|
@@ -757,8 +769,9 @@ async def collect_opencode_output_from_events(
|
|
|
757
769
|
error: Optional[str] = None
|
|
758
770
|
message_roles: dict[str, str] = {}
|
|
759
771
|
message_roles_seen = False
|
|
760
|
-
last_role_seen: Optional[str] = None
|
|
761
772
|
pending_text: dict[str, list[str]] = {}
|
|
773
|
+
pending_no_id: list[str] = []
|
|
774
|
+
no_id_role: Optional[str] = None
|
|
762
775
|
fallback_message: Optional[tuple[Optional[str], Optional[str], str]] = None
|
|
763
776
|
last_usage_total: Optional[int] = None
|
|
764
777
|
last_context_window: Optional[int] = None
|
|
@@ -793,7 +806,7 @@ async def collect_opencode_output_from_events(
|
|
|
793
806
|
return None
|
|
794
807
|
|
|
795
808
|
def _register_message_role(payload: Any) -> tuple[Optional[str], Optional[str]]:
|
|
796
|
-
nonlocal
|
|
809
|
+
nonlocal message_roles_seen
|
|
797
810
|
if not isinstance(payload, dict):
|
|
798
811
|
return None, None
|
|
799
812
|
info = payload.get("info")
|
|
@@ -806,18 +819,27 @@ async def collect_opencode_output_from_events(
|
|
|
806
819
|
if isinstance(role, str) and msg_id:
|
|
807
820
|
message_roles[msg_id] = role
|
|
808
821
|
message_roles_seen = True
|
|
809
|
-
last_role_seen = role
|
|
810
822
|
return msg_id, role if isinstance(role, str) else None
|
|
811
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
|
+
|
|
812
835
|
def _append_text_for_message(message_id: Optional[str], text: str) -> None:
|
|
813
836
|
if not text:
|
|
814
837
|
return
|
|
815
838
|
if message_id is None:
|
|
816
|
-
if
|
|
817
|
-
text_parts.append(text)
|
|
818
|
-
return
|
|
819
|
-
if last_role_seen != "user":
|
|
839
|
+
if no_id_role == "assistant":
|
|
820
840
|
text_parts.append(text)
|
|
841
|
+
else:
|
|
842
|
+
pending_no_id.append(text)
|
|
821
843
|
return
|
|
822
844
|
role = message_roles.get(message_id)
|
|
823
845
|
if role == "user":
|
|
@@ -839,12 +861,32 @@ async def collect_opencode_output_from_events(
|
|
|
839
861
|
text_parts.extend(pending)
|
|
840
862
|
|
|
841
863
|
def _flush_all_pending_text() -> None:
|
|
842
|
-
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()
|
|
843
885
|
return
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
886
|
+
if role == "user":
|
|
887
|
+
_flush_pending_text(message_id)
|
|
888
|
+
_discard_pending_no_id()
|
|
889
|
+
no_id_role = None
|
|
848
890
|
|
|
849
891
|
async def _resolve_session_model_ids() -> tuple[Optional[str], Optional[str]]:
|
|
850
892
|
nonlocal session_model_ids
|
|
@@ -1002,7 +1044,7 @@ async def collect_opencode_output_from_events(
|
|
|
1002
1044
|
status_type=status_type,
|
|
1003
1045
|
idle_seconds=idle_seconds,
|
|
1004
1046
|
)
|
|
1005
|
-
if not text_parts and pending_text:
|
|
1047
|
+
if not text_parts and (pending_text or pending_no_id):
|
|
1006
1048
|
_flush_all_pending_text()
|
|
1007
1049
|
break
|
|
1008
1050
|
if last_primary_completion_at is not None:
|
|
@@ -1079,7 +1121,7 @@ async def collect_opencode_output_from_events(
|
|
|
1079
1121
|
status_type=status_type,
|
|
1080
1122
|
idle_seconds=idle_seconds,
|
|
1081
1123
|
)
|
|
1082
|
-
if not text_parts and pending_text:
|
|
1124
|
+
if not text_parts and (pending_text or pending_no_id):
|
|
1083
1125
|
_flush_all_pending_text()
|
|
1084
1126
|
break
|
|
1085
1127
|
if last_primary_completion_at is not None:
|
|
@@ -1296,8 +1338,7 @@ async def collect_opencode_output_from_events(
|
|
|
1296
1338
|
if event.event in ("message.updated", "message.completed"):
|
|
1297
1339
|
if is_primary_session:
|
|
1298
1340
|
msg_id, role = _register_message_role(payload)
|
|
1299
|
-
|
|
1300
|
-
_flush_pending_text(msg_id)
|
|
1341
|
+
_handle_role_update(msg_id, role)
|
|
1301
1342
|
if event.event == "message.part.updated":
|
|
1302
1343
|
properties = (
|
|
1303
1344
|
payload.get("properties") if isinstance(payload, dict) else None
|
|
@@ -1470,7 +1511,7 @@ async def collect_opencode_output_from_events(
|
|
|
1470
1511
|
):
|
|
1471
1512
|
if not is_primary_session:
|
|
1472
1513
|
continue
|
|
1473
|
-
if not text_parts and pending_text:
|
|
1514
|
+
if not text_parts and (pending_text or pending_no_id):
|
|
1474
1515
|
_flush_all_pending_text()
|
|
1475
1516
|
break
|
|
1476
1517
|
if event.event == "message.completed" and is_primary_session:
|
|
@@ -1485,7 +1526,7 @@ async def collect_opencode_output_from_events(
|
|
|
1485
1526
|
resolved_role = message_roles.get(msg_id)
|
|
1486
1527
|
if resolved_role == "assistant":
|
|
1487
1528
|
_append_text_for_message(msg_id, text)
|
|
1488
|
-
if pending_text:
|
|
1529
|
+
if pending_text or pending_no_id:
|
|
1489
1530
|
_flush_all_pending_text()
|
|
1490
1531
|
|
|
1491
1532
|
return OpenCodeTurnOutput(text="".join(text_parts).strip(), error=error)
|
|
@@ -55,6 +55,7 @@ class OpenCodeSupervisor:
|
|
|
55
55
|
base_url: Optional[str] = None,
|
|
56
56
|
subagent_models: Optional[Mapping[str, str]] = None,
|
|
57
57
|
session_stall_timeout_seconds: Optional[float] = None,
|
|
58
|
+
max_text_chars: Optional[int] = None,
|
|
58
59
|
) -> None:
|
|
59
60
|
self._command = [str(arg) for arg in command]
|
|
60
61
|
self._logger = logger or logging.getLogger(__name__)
|
|
@@ -70,6 +71,7 @@ class OpenCodeSupervisor:
|
|
|
70
71
|
self._base_env = base_env
|
|
71
72
|
self._base_url = base_url
|
|
72
73
|
self._subagent_models = subagent_models or {}
|
|
74
|
+
self._max_text_chars = max_text_chars
|
|
73
75
|
self._handles: dict[str, OpenCodeHandle] = {}
|
|
74
76
|
self._lock: Optional[asyncio.Lock] = None
|
|
75
77
|
|
|
@@ -275,6 +277,7 @@ class OpenCodeSupervisor:
|
|
|
275
277
|
base_url,
|
|
276
278
|
auth=self._auth,
|
|
277
279
|
timeout=self._request_timeout,
|
|
280
|
+
max_text_chars=self._max_text_chars,
|
|
278
281
|
logger=self._logger,
|
|
279
282
|
)
|
|
280
283
|
try:
|
|
@@ -344,6 +347,7 @@ class OpenCodeSupervisor:
|
|
|
344
347
|
base_url,
|
|
345
348
|
auth=self._auth,
|
|
346
349
|
timeout=self._request_timeout,
|
|
350
|
+
max_text_chars=self._max_text_chars,
|
|
347
351
|
logger=self._logger,
|
|
348
352
|
)
|
|
349
353
|
try:
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import importlib.metadata
|
|
4
4
|
import logging
|
|
5
|
+
import threading
|
|
5
6
|
from dataclasses import dataclass
|
|
6
7
|
from typing import Any, Callable, Iterable, Literal, Optional
|
|
7
8
|
|
|
@@ -102,6 +103,9 @@ _BUILTIN_AGENTS: dict[str, AgentDescriptor] = {
|
|
|
102
103
|
# Lazy-loaded cache of built-in + plugin agents.
|
|
103
104
|
_AGENT_CACHE: Optional[dict[str, AgentDescriptor]] = None
|
|
104
105
|
|
|
106
|
+
# Lock to protect cache initialization and reload from concurrent access.
|
|
107
|
+
_AGENT_CACHE_LOCK = threading.Lock()
|
|
108
|
+
|
|
105
109
|
|
|
106
110
|
def _select_entry_points(group: str) -> Iterable[importlib.metadata.EntryPoint]:
|
|
107
111
|
"""Compatibility wrapper for `importlib.metadata.entry_points()` across py versions."""
|
|
@@ -163,14 +167,36 @@ def _load_agent_plugins() -> dict[str, AgentDescriptor]:
|
|
|
163
167
|
)
|
|
164
168
|
continue
|
|
165
169
|
|
|
166
|
-
|
|
170
|
+
api_version_raw = getattr(descriptor, "plugin_api_version", None)
|
|
171
|
+
if api_version_raw is None:
|
|
172
|
+
api_version = None
|
|
173
|
+
else:
|
|
174
|
+
try:
|
|
175
|
+
api_version = int(api_version_raw)
|
|
176
|
+
except Exception:
|
|
177
|
+
api_version = None
|
|
178
|
+
if api_version is None:
|
|
167
179
|
_logger.warning(
|
|
168
|
-
"Ignoring agent plugin %s
|
|
180
|
+
"Ignoring agent plugin %s: invalid api_version %s",
|
|
169
181
|
agent_id,
|
|
170
|
-
|
|
182
|
+
api_version_raw,
|
|
183
|
+
)
|
|
184
|
+
continue
|
|
185
|
+
if api_version > CAR_PLUGIN_API_VERSION:
|
|
186
|
+
_logger.warning(
|
|
187
|
+
"Ignoring agent plugin %s (api_version=%s) requires newer core (%s)",
|
|
188
|
+
agent_id,
|
|
189
|
+
api_version,
|
|
171
190
|
CAR_PLUGIN_API_VERSION,
|
|
172
191
|
)
|
|
173
192
|
continue
|
|
193
|
+
if api_version < CAR_PLUGIN_API_VERSION:
|
|
194
|
+
_logger.info(
|
|
195
|
+
"Loaded agent plugin %s with older api_version=%s (current=%s)",
|
|
196
|
+
agent_id,
|
|
197
|
+
api_version,
|
|
198
|
+
CAR_PLUGIN_API_VERSION,
|
|
199
|
+
)
|
|
174
200
|
|
|
175
201
|
if agent_id in _BUILTIN_AGENTS:
|
|
176
202
|
_logger.warning(
|
|
@@ -196,9 +222,11 @@ def _load_agent_plugins() -> dict[str, AgentDescriptor]:
|
|
|
196
222
|
def _all_agents() -> dict[str, AgentDescriptor]:
|
|
197
223
|
global _AGENT_CACHE
|
|
198
224
|
if _AGENT_CACHE is None:
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
225
|
+
with _AGENT_CACHE_LOCK:
|
|
226
|
+
if _AGENT_CACHE is None:
|
|
227
|
+
agents = _BUILTIN_AGENTS.copy()
|
|
228
|
+
agents.update(_load_agent_plugins())
|
|
229
|
+
_AGENT_CACHE = agents
|
|
202
230
|
return _AGENT_CACHE
|
|
203
231
|
|
|
204
232
|
|
|
@@ -209,7 +237,8 @@ def reload_agents() -> dict[str, AgentDescriptor]:
|
|
|
209
237
|
"""
|
|
210
238
|
|
|
211
239
|
global _AGENT_CACHE
|
|
212
|
-
|
|
240
|
+
with _AGENT_CACHE_LOCK:
|
|
241
|
+
_AGENT_CACHE = None
|
|
213
242
|
return get_registered_agents()
|
|
214
243
|
|
|
215
244
|
|