codex-autorunner 0.1.1__py3-none-any.whl → 1.0.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/__main__.py +4 -0
- codex_autorunner/agents/__init__.py +20 -0
- codex_autorunner/agents/base.py +2 -2
- codex_autorunner/agents/codex/harness.py +1 -1
- codex_autorunner/agents/opencode/__init__.py +4 -0
- codex_autorunner/agents/opencode/agent_config.py +104 -0
- codex_autorunner/agents/opencode/client.py +305 -28
- codex_autorunner/agents/opencode/harness.py +71 -20
- codex_autorunner/agents/opencode/logging.py +225 -0
- codex_autorunner/agents/opencode/run_prompt.py +261 -0
- codex_autorunner/agents/opencode/runtime.py +1202 -132
- codex_autorunner/agents/opencode/supervisor.py +194 -68
- codex_autorunner/agents/registry.py +258 -0
- codex_autorunner/agents/types.py +2 -2
- codex_autorunner/api.py +25 -0
- codex_autorunner/bootstrap.py +19 -40
- codex_autorunner/cli.py +234 -151
- codex_autorunner/core/about_car.py +44 -32
- codex_autorunner/core/adapter_utils.py +21 -0
- codex_autorunner/core/app_server_events.py +15 -6
- codex_autorunner/core/app_server_logging.py +55 -15
- codex_autorunner/core/app_server_prompts.py +28 -259
- codex_autorunner/core/app_server_threads.py +15 -26
- codex_autorunner/core/circuit_breaker.py +183 -0
- codex_autorunner/core/codex_runner.py +6 -0
- codex_autorunner/core/config.py +555 -133
- codex_autorunner/core/docs.py +54 -9
- codex_autorunner/core/drafts.py +82 -0
- codex_autorunner/core/engine.py +828 -274
- codex_autorunner/core/exceptions.py +60 -0
- codex_autorunner/core/flows/__init__.py +25 -0
- codex_autorunner/core/flows/controller.py +178 -0
- codex_autorunner/core/flows/definition.py +82 -0
- codex_autorunner/core/flows/models.py +75 -0
- codex_autorunner/core/flows/runtime.py +351 -0
- codex_autorunner/core/flows/store.py +485 -0
- codex_autorunner/core/flows/transition.py +133 -0
- codex_autorunner/core/flows/worker_process.py +242 -0
- codex_autorunner/core/hub.py +21 -13
- codex_autorunner/core/locks.py +118 -1
- codex_autorunner/core/logging_utils.py +9 -6
- codex_autorunner/core/path_utils.py +123 -0
- codex_autorunner/core/prompt.py +15 -7
- codex_autorunner/core/redaction.py +29 -0
- codex_autorunner/core/retry.py +61 -0
- codex_autorunner/core/review.py +888 -0
- codex_autorunner/core/review_context.py +161 -0
- codex_autorunner/core/run_index.py +223 -0
- codex_autorunner/core/runner_controller.py +44 -1
- codex_autorunner/core/runner_process.py +30 -1
- codex_autorunner/core/sqlite_utils.py +32 -0
- codex_autorunner/core/state.py +273 -44
- codex_autorunner/core/static_assets.py +55 -0
- codex_autorunner/core/supervisor_utils.py +67 -0
- codex_autorunner/core/text_delta_coalescer.py +43 -0
- codex_autorunner/core/update.py +20 -11
- codex_autorunner/core/update_runner.py +2 -0
- codex_autorunner/core/usage.py +107 -75
- codex_autorunner/core/utils.py +167 -3
- codex_autorunner/discovery.py +3 -3
- codex_autorunner/flows/ticket_flow/__init__.py +3 -0
- codex_autorunner/flows/ticket_flow/definition.py +91 -0
- codex_autorunner/integrations/agents/__init__.py +27 -0
- codex_autorunner/integrations/agents/agent_backend.py +142 -0
- codex_autorunner/integrations/agents/codex_backend.py +307 -0
- codex_autorunner/integrations/agents/opencode_backend.py +325 -0
- codex_autorunner/integrations/agents/run_event.py +71 -0
- codex_autorunner/integrations/app_server/client.py +708 -153
- codex_autorunner/integrations/app_server/supervisor.py +59 -33
- codex_autorunner/integrations/telegram/adapter.py +474 -185
- codex_autorunner/integrations/telegram/api_schemas.py +120 -0
- codex_autorunner/integrations/telegram/config.py +239 -1
- codex_autorunner/integrations/telegram/constants.py +19 -1
- codex_autorunner/integrations/telegram/dispatch.py +44 -8
- codex_autorunner/integrations/telegram/doctor.py +47 -0
- codex_autorunner/integrations/telegram/handlers/approvals.py +12 -10
- codex_autorunner/integrations/telegram/handlers/callbacks.py +15 -1
- codex_autorunner/integrations/telegram/handlers/commands/__init__.py +29 -0
- codex_autorunner/integrations/telegram/handlers/commands/approvals.py +173 -0
- codex_autorunner/integrations/telegram/handlers/commands/execution.py +2595 -0
- codex_autorunner/integrations/telegram/handlers/commands/files.py +1408 -0
- codex_autorunner/integrations/telegram/handlers/commands/flows.py +227 -0
- codex_autorunner/integrations/telegram/handlers/commands/formatting.py +81 -0
- codex_autorunner/integrations/telegram/handlers/commands/github.py +1688 -0
- codex_autorunner/integrations/telegram/handlers/commands/shared.py +190 -0
- codex_autorunner/integrations/telegram/handlers/commands/voice.py +112 -0
- codex_autorunner/integrations/telegram/handlers/commands/workspace.py +2043 -0
- codex_autorunner/integrations/telegram/handlers/commands_runtime.py +954 -5689
- codex_autorunner/integrations/telegram/handlers/{commands.py → commands_spec.py} +11 -4
- codex_autorunner/integrations/telegram/handlers/messages.py +374 -49
- codex_autorunner/integrations/telegram/handlers/questions.py +389 -0
- codex_autorunner/integrations/telegram/handlers/selections.py +6 -4
- codex_autorunner/integrations/telegram/handlers/utils.py +171 -0
- codex_autorunner/integrations/telegram/helpers.py +90 -18
- codex_autorunner/integrations/telegram/notifications.py +126 -35
- codex_autorunner/integrations/telegram/outbox.py +214 -43
- codex_autorunner/integrations/telegram/progress_stream.py +42 -19
- codex_autorunner/integrations/telegram/runtime.py +24 -13
- codex_autorunner/integrations/telegram/service.py +500 -129
- codex_autorunner/integrations/telegram/state.py +1278 -330
- codex_autorunner/integrations/telegram/ticket_flow_bridge.py +322 -0
- codex_autorunner/integrations/telegram/transport.py +37 -4
- codex_autorunner/integrations/telegram/trigger_mode.py +53 -0
- codex_autorunner/integrations/telegram/types.py +22 -2
- codex_autorunner/integrations/telegram/voice.py +14 -15
- codex_autorunner/manifest.py +2 -0
- codex_autorunner/plugin_api.py +22 -0
- codex_autorunner/routes/__init__.py +25 -14
- codex_autorunner/routes/agents.py +18 -78
- codex_autorunner/routes/analytics.py +239 -0
- codex_autorunner/routes/base.py +142 -113
- codex_autorunner/routes/file_chat.py +836 -0
- codex_autorunner/routes/flows.py +980 -0
- codex_autorunner/routes/messages.py +459 -0
- codex_autorunner/routes/repos.py +17 -0
- codex_autorunner/routes/review.py +148 -0
- codex_autorunner/routes/sessions.py +16 -8
- codex_autorunner/routes/settings.py +22 -0
- codex_autorunner/routes/shared.py +33 -3
- codex_autorunner/routes/system.py +22 -1
- codex_autorunner/routes/usage.py +87 -0
- codex_autorunner/routes/voice.py +5 -13
- codex_autorunner/routes/workspace.py +271 -0
- codex_autorunner/server.py +2 -1
- codex_autorunner/static/agentControls.js +9 -1
- codex_autorunner/static/agentEvents.js +248 -0
- codex_autorunner/static/app.js +27 -22
- codex_autorunner/static/autoRefresh.js +29 -1
- 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 +162 -150
- 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 +67 -126
- codex_autorunner/static/index.html +788 -807
- codex_autorunner/static/liveUpdates.js +59 -0
- codex_autorunner/static/loader.js +1 -0
- codex_autorunner/static/messages.js +470 -0
- codex_autorunner/static/mobileCompact.js +2 -1
- codex_autorunner/static/settings.js +24 -205
- codex_autorunner/static/styles.css +7577 -3758
- codex_autorunner/static/tabs.js +28 -5
- codex_autorunner/static/terminal.js +14 -0
- codex_autorunner/static/terminalManager.js +53 -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 +750 -0
- codex_autorunner/static/ticketVoice.js +9 -0
- codex_autorunner/static/tickets.js +1315 -0
- codex_autorunner/static/utils.js +32 -3
- codex_autorunner/static/voice.js +21 -7
- codex_autorunner/static/workspace.js +672 -0
- codex_autorunner/static/workspaceApi.js +53 -0
- codex_autorunner/static/workspaceFileBrowser.js +504 -0
- codex_autorunner/tickets/__init__.py +20 -0
- codex_autorunner/tickets/agent_pool.py +377 -0
- codex_autorunner/tickets/files.py +85 -0
- codex_autorunner/tickets/frontmatter.py +55 -0
- codex_autorunner/tickets/lint.py +102 -0
- codex_autorunner/tickets/models.py +95 -0
- codex_autorunner/tickets/outbox.py +232 -0
- codex_autorunner/tickets/replies.py +179 -0
- codex_autorunner/tickets/runner.py +823 -0
- codex_autorunner/tickets/spec_ingest.py +77 -0
- codex_autorunner/voice/capture.py +7 -7
- codex_autorunner/voice/service.py +51 -9
- codex_autorunner/web/app.py +419 -199
- codex_autorunner/web/hub_jobs.py +13 -2
- codex_autorunner/web/middleware.py +47 -13
- codex_autorunner/web/pty_session.py +26 -13
- codex_autorunner/web/schemas.py +114 -109
- codex_autorunner/web/static_assets.py +55 -42
- codex_autorunner/web/static_refresh.py +86 -0
- codex_autorunner/workspace/__init__.py +40 -0
- codex_autorunner/workspace/paths.py +319 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/METADATA +20 -21
- codex_autorunner-1.0.0.dist-info/RECORD +251 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/WHEEL +1 -1
- codex_autorunner/core/doc_chat.py +0 -1415
- 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 -118
- codex_autorunner/spec_ingest.py +0 -788
- codex_autorunner/static/docChatActions.js +0 -279
- 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 -274
- 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 -442
- codex_autorunner/static/logs.js +0 -640
- codex_autorunner/static/runs.js +0 -409
- codex_autorunner/static/snapshot.js +0 -124
- codex_autorunner/static/state.js +0 -86
- codex_autorunner/static/todoPreview.js +0 -27
- codex_autorunner/workspace.py +0 -16
- codex_autorunner-0.1.1.dist-info/RECORD +0 -191
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/entry_points.txt +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {codex_autorunner-0.1.1.dist-info → codex_autorunner-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -17,6 +17,26 @@ ABOUT_CAR_REL_PATH = Path(".codex-autorunner") / ABOUT_CAR_BASENAME
|
|
|
17
17
|
# If this marker is present, codex-autorunner may safely refresh the file content.
|
|
18
18
|
ABOUT_CAR_GENERATED_MARKER = "<!-- CAR:AUTOGENERATED -->"
|
|
19
19
|
|
|
20
|
+
CAR_CONTEXT_KEYWORDS = (
|
|
21
|
+
"car",
|
|
22
|
+
"codex",
|
|
23
|
+
"spec",
|
|
24
|
+
"autorunner",
|
|
25
|
+
"workspace",
|
|
26
|
+
"ticket",
|
|
27
|
+
"tickets",
|
|
28
|
+
"context",
|
|
29
|
+
"decision",
|
|
30
|
+
"decisions",
|
|
31
|
+
"handoff",
|
|
32
|
+
"dispatch",
|
|
33
|
+
"inbox",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
CAR_CONTEXT_HINT = (
|
|
37
|
+
"Context: read .codex-autorunner/ABOUT_CAR.md for repo-specific rules."
|
|
38
|
+
)
|
|
39
|
+
|
|
20
40
|
|
|
21
41
|
def _display_path(repo_root: Path, path: Path) -> str:
|
|
22
42
|
try:
|
|
@@ -28,11 +48,9 @@ def _display_path(repo_root: Path, path: Path) -> str:
|
|
|
28
48
|
def build_about_car_markdown(
|
|
29
49
|
*,
|
|
30
50
|
repo_root: Path,
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
opinions_path: Path,
|
|
51
|
+
active_context_path: Path,
|
|
52
|
+
decisions_path: Path,
|
|
34
53
|
spec_path: Path,
|
|
35
|
-
summary_path: Path,
|
|
36
54
|
hub_config_path: Optional[Path] = None,
|
|
37
55
|
repo_override_path: Optional[Path] = None,
|
|
38
56
|
) -> str:
|
|
@@ -44,11 +62,9 @@ def build_about_car_markdown(
|
|
|
44
62
|
repo_override_path = repo_override_path or (repo_root / REPO_OVERRIDE_FILENAME)
|
|
45
63
|
root_config_path = repo_root / ROOT_CONFIG_FILENAME
|
|
46
64
|
root_override_path = repo_root / ROOT_OVERRIDE_FILENAME
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
opinions_disp = _display_path(repo_root, opinions_path)
|
|
65
|
+
active_context_disp = _display_path(repo_root, active_context_path)
|
|
66
|
+
decisions_disp = _display_path(repo_root, decisions_path)
|
|
50
67
|
spec_disp = _display_path(repo_root, spec_path)
|
|
51
|
-
summary_disp = _display_path(repo_root, summary_path)
|
|
52
68
|
hub_config_disp = _display_path(repo_root, hub_config_path)
|
|
53
69
|
repo_override_disp = _display_path(repo_root, repo_override_path)
|
|
54
70
|
root_config_disp = _display_path(repo_root, root_config_path)
|
|
@@ -58,25 +74,25 @@ def build_about_car_markdown(
|
|
|
58
74
|
f"{ABOUT_CAR_GENERATED_MARKER}\n"
|
|
59
75
|
"# ABOUT_CAR — Codex Autorunner (CAR)\n\n"
|
|
60
76
|
"You are running inside **Codex Autorunner (CAR)**.\n\n"
|
|
61
|
-
"CAR uses a
|
|
62
|
-
"
|
|
63
|
-
"
|
|
64
|
-
"
|
|
65
|
-
|
|
66
|
-
"
|
|
67
|
-
|
|
68
|
-
"
|
|
69
|
-
|
|
70
|
-
"
|
|
71
|
-
f"`{spec_disp}`\n"
|
|
72
|
-
"- **SUMMARY** — user-facing report + external/user action items: "
|
|
73
|
-
f"`{summary_disp}`\n\n"
|
|
77
|
+
"CAR uses a ticket-first workflow.\n\n"
|
|
78
|
+
"## Required for operation\n"
|
|
79
|
+
"- Tickets live under `.codex-autorunner/tickets/`.\n\n"
|
|
80
|
+
"## Optional workspace docs\n"
|
|
81
|
+
"- **Active context**: "
|
|
82
|
+
f"`{active_context_disp}`\n"
|
|
83
|
+
"- **Decisions**: "
|
|
84
|
+
f"`{decisions_disp}`\n"
|
|
85
|
+
"- **Spec**: "
|
|
86
|
+
f"`{spec_disp}`\n\n"
|
|
74
87
|
"## Critical rules\n"
|
|
75
|
-
|
|
76
|
-
"- Do **not** create new copies of TODO/PROGRESS/OPINIONS/SPEC/SUMMARY elsewhere in the repo.\n"
|
|
88
|
+
"- Do **not** create new copies of workspace docs elsewhere in the repo.\n"
|
|
77
89
|
"- Treat `.codex-autorunner/` as intentional project structure even though it is hidden/gitignored.\n\n"
|
|
90
|
+
"## Agent Flow\n"
|
|
91
|
+
"- **Dispatch**: An update or message from the agent.\n"
|
|
92
|
+
"- **Handoff**: Passing control from agent to user (or vice versa).\n"
|
|
93
|
+
"- **Inbox**: Where the agent receives files/messages.\n\n"
|
|
78
94
|
"## How CAR works (short)\n"
|
|
79
|
-
"-
|
|
95
|
+
"- The web UI provides ticket editing + unified file chat.\n"
|
|
80
96
|
"- `car serve` starts the hub web UI. The **Terminal** tab launches the configured `codex` binary in a PTY.\n"
|
|
81
97
|
f"- Hub config lives at `{hub_config_disp}` (generated).\n"
|
|
82
98
|
f"- Repo overrides (optional) live at `{repo_override_disp}`.\n"
|
|
@@ -101,11 +117,9 @@ def ensure_about_car_file_for_repo(
|
|
|
101
117
|
|
|
102
118
|
content = build_about_car_markdown(
|
|
103
119
|
repo_root=repo_root,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
opinions_path=doc_paths["opinions"],
|
|
120
|
+
active_context_path=doc_paths["active_context"],
|
|
121
|
+
decisions_path=doc_paths["decisions"],
|
|
107
122
|
spec_path=doc_paths["spec"],
|
|
108
|
-
summary_path=doc_paths["summary"],
|
|
109
123
|
)
|
|
110
124
|
if content and not content.endswith("\n"):
|
|
111
125
|
content += "\n"
|
|
@@ -129,10 +143,8 @@ def ensure_about_car_file(config: Config, *, force: bool = False) -> Path:
|
|
|
129
143
|
"""Config-aware wrapper that uses configured doc paths."""
|
|
130
144
|
repo_root = config.root
|
|
131
145
|
docs = {
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"opinions": config.doc_path("opinions"),
|
|
146
|
+
"active_context": config.doc_path("active_context"),
|
|
147
|
+
"decisions": config.doc_path("decisions"),
|
|
135
148
|
"spec": config.doc_path("spec"),
|
|
136
|
-
"summary": config.doc_path("summary"),
|
|
137
149
|
}
|
|
138
150
|
return ensure_about_car_file_for_repo(repo_root, doc_paths=docs, force=force)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import Any, Callable
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def handle_agent_output(
|
|
7
|
+
log_app_server_output: Callable[[int, list[str]], None],
|
|
8
|
+
write_run_artifact: Callable[[int, str, str], Any],
|
|
9
|
+
merge_run_index_entry: Callable[[int, dict[str, Any]], None],
|
|
10
|
+
run_id: int,
|
|
11
|
+
output: str | list[str],
|
|
12
|
+
) -> None:
|
|
13
|
+
if isinstance(output, str):
|
|
14
|
+
messages = [output]
|
|
15
|
+
else:
|
|
16
|
+
messages = output
|
|
17
|
+
log_app_server_output(run_id, messages)
|
|
18
|
+
output_text = "\n\n".join(messages).strip() if messages else ""
|
|
19
|
+
if output_text:
|
|
20
|
+
output_path = write_run_artifact(run_id, "output.txt", output_text)
|
|
21
|
+
merge_run_index_entry(run_id, {"artifacts": {"output_path": str(output_path)}})
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import json
|
|
3
|
+
import threading
|
|
3
4
|
import time
|
|
4
5
|
from dataclasses import dataclass, field
|
|
5
6
|
from typing import Any, AsyncIterator, Dict, Optional
|
|
@@ -45,11 +46,19 @@ class AppServerEventBuffer:
|
|
|
45
46
|
) -> None:
|
|
46
47
|
self._entries: dict[TurnKey, TurnEventEntry] = {}
|
|
47
48
|
self._turn_index: dict[str, str] = {}
|
|
48
|
-
self._lock
|
|
49
|
+
self._lock: Optional[asyncio.Lock] = None
|
|
50
|
+
self._lock_init = threading.Lock()
|
|
49
51
|
self._max_events_per_turn = max_events_per_turn
|
|
50
52
|
self._max_turns = max_turns
|
|
51
53
|
self._turn_ttl_seconds = turn_ttl_seconds
|
|
52
54
|
|
|
55
|
+
def _ensure_lock(self) -> asyncio.Lock:
|
|
56
|
+
if self._lock is None:
|
|
57
|
+
with self._lock_init:
|
|
58
|
+
if self._lock is None:
|
|
59
|
+
self._lock = asyncio.Lock()
|
|
60
|
+
return self._lock
|
|
61
|
+
|
|
53
62
|
async def register_turn(
|
|
54
63
|
self,
|
|
55
64
|
thread_id: str,
|
|
@@ -60,7 +69,7 @@ class AppServerEventBuffer:
|
|
|
60
69
|
if not thread_id or not turn_id:
|
|
61
70
|
return
|
|
62
71
|
entry = await self._ensure_entry(thread_id, turn_id)
|
|
63
|
-
async with self.
|
|
72
|
+
async with self._ensure_lock():
|
|
64
73
|
self._turn_index[turn_id] = thread_id
|
|
65
74
|
if context:
|
|
66
75
|
entry.context.update(context)
|
|
@@ -83,7 +92,7 @@ class AppServerEventBuffer:
|
|
|
83
92
|
entry.last_event_at = time.monotonic()
|
|
84
93
|
entry.condition.notify_all()
|
|
85
94
|
context = dict(entry.context) if entry.context else {}
|
|
86
|
-
async with self.
|
|
95
|
+
async with self._ensure_lock():
|
|
87
96
|
self._turn_index[turn_id] = thread_id
|
|
88
97
|
self._prune_locked()
|
|
89
98
|
self._emit_log_lines(context, message)
|
|
@@ -96,7 +105,7 @@ class AppServerEventBuffer:
|
|
|
96
105
|
heartbeat_interval: float = 15.0,
|
|
97
106
|
) -> AsyncIterator[str]:
|
|
98
107
|
entry = await self._ensure_entry(thread_id, turn_id)
|
|
99
|
-
async with self.
|
|
108
|
+
async with self._ensure_lock():
|
|
100
109
|
entry.active_streams += 1
|
|
101
110
|
self._turn_index[turn_id] = thread_id
|
|
102
111
|
last_id = 0
|
|
@@ -116,12 +125,12 @@ class AppServerEventBuffer:
|
|
|
116
125
|
last_id = event["id"]
|
|
117
126
|
yield format_sse("app-server", event)
|
|
118
127
|
finally:
|
|
119
|
-
async with self.
|
|
128
|
+
async with self._ensure_lock():
|
|
120
129
|
entry.active_streams = max(0, entry.active_streams - 1)
|
|
121
130
|
|
|
122
131
|
async def _ensure_entry(self, thread_id: str, turn_id: str) -> TurnEventEntry:
|
|
123
132
|
key = (thread_id, turn_id)
|
|
124
|
-
async with self.
|
|
133
|
+
async with self._ensure_lock():
|
|
125
134
|
entry = self._entries.get(key)
|
|
126
135
|
if entry is None:
|
|
127
136
|
entry = TurnEventEntry(thread_id=thread_id, turn_id=turn_id)
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
from typing import Any
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from .redaction import redact_text
|
|
6
|
+
from .text_delta_coalescer import TextDeltaCoalescer
|
|
4
7
|
|
|
5
8
|
|
|
6
9
|
def _coerce_dict(value: Any) -> dict[str, Any]:
|
|
@@ -67,11 +70,14 @@ def _extract_error_message(params: Any) -> str:
|
|
|
67
70
|
|
|
68
71
|
|
|
69
72
|
class AppServerEventFormatter:
|
|
70
|
-
def __init__(self) -> None:
|
|
73
|
+
def __init__(self, redact_enabled: bool = True) -> None:
|
|
74
|
+
self._redact_enabled = redact_enabled
|
|
71
75
|
self._thinking_items: set[str] = set()
|
|
76
|
+
self._reasoning_coalescers: dict[str, TextDeltaCoalescer] = {}
|
|
72
77
|
|
|
73
78
|
def reset(self) -> None:
|
|
74
79
|
self._thinking_items.clear()
|
|
80
|
+
self._reasoning_coalescers.clear()
|
|
75
81
|
|
|
76
82
|
def format_event(self, message: Any) -> list[str]:
|
|
77
83
|
if not isinstance(message, dict):
|
|
@@ -86,23 +92,41 @@ class AppServerEventFormatter:
|
|
|
86
92
|
delta = params.get("delta")
|
|
87
93
|
if not isinstance(delta, str) or not delta:
|
|
88
94
|
return []
|
|
95
|
+
has_valid_item_id = isinstance(item_id, str) and item_id
|
|
96
|
+
if has_valid_item_id and item_id not in self._thinking_items:
|
|
97
|
+
lines.append("thinking")
|
|
98
|
+
self._thinking_items.add(cast(str, item_id))
|
|
99
|
+
if has_valid_item_id:
|
|
100
|
+
if item_id not in self._reasoning_coalescers:
|
|
101
|
+
self._reasoning_coalescers[cast(str, item_id)] = (
|
|
102
|
+
TextDeltaCoalescer()
|
|
103
|
+
)
|
|
104
|
+
self._reasoning_coalescers[cast(str, item_id)].add(delta)
|
|
105
|
+
else:
|
|
106
|
+
lines.append("thinking")
|
|
107
|
+
for line in delta.splitlines() or [""]:
|
|
108
|
+
if line:
|
|
109
|
+
lines.append(f"**{line}**")
|
|
110
|
+
else:
|
|
111
|
+
lines.append("")
|
|
112
|
+
return lines
|
|
113
|
+
|
|
114
|
+
if method == "item/reasoning/summaryPartAdded":
|
|
89
115
|
if (
|
|
90
116
|
isinstance(item_id, str)
|
|
91
117
|
and item_id
|
|
92
|
-
and item_id
|
|
118
|
+
and item_id in self._reasoning_coalescers
|
|
93
119
|
):
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
120
|
+
coalescer = self._reasoning_coalescers[item_id]
|
|
121
|
+
buffer = coalescer.flush_all()
|
|
122
|
+
for line in buffer:
|
|
123
|
+
if line:
|
|
124
|
+
lines.append(f"**{line}**")
|
|
125
|
+
else:
|
|
126
|
+
lines.append("")
|
|
127
|
+
self._reasoning_coalescers[item_id].clear()
|
|
101
128
|
return lines
|
|
102
129
|
|
|
103
|
-
if method == "item/reasoning/summaryPartAdded":
|
|
104
|
-
return []
|
|
105
|
-
|
|
106
130
|
if method in ("turn/completed", "error"):
|
|
107
131
|
self.reset()
|
|
108
132
|
|
|
@@ -134,6 +158,20 @@ class AppServerEventFormatter:
|
|
|
134
158
|
if files:
|
|
135
159
|
lines.append("file update")
|
|
136
160
|
lines.extend([f"M {path}" for path in files])
|
|
161
|
+
elif item_type == "reasoning":
|
|
162
|
+
if (
|
|
163
|
+
isinstance(item_id, str)
|
|
164
|
+
and item_id
|
|
165
|
+
and item_id in self._reasoning_coalescers
|
|
166
|
+
):
|
|
167
|
+
coalescer = self._reasoning_coalescers[item_id]
|
|
168
|
+
buffer = coalescer.flush_all()
|
|
169
|
+
self._reasoning_coalescers.pop(item_id, None)
|
|
170
|
+
for line in buffer:
|
|
171
|
+
if line:
|
|
172
|
+
lines.append(f"**{line}**")
|
|
173
|
+
else:
|
|
174
|
+
lines.append("")
|
|
137
175
|
elif item_type == "tool":
|
|
138
176
|
tool_name = item.get("name") or item.get("tool") or item.get("id")
|
|
139
177
|
if isinstance(tool_name, str) and tool_name:
|
|
@@ -151,7 +189,8 @@ class AppServerEventFormatter:
|
|
|
151
189
|
or params.get("value")
|
|
152
190
|
)
|
|
153
191
|
if isinstance(diff, str) and diff:
|
|
154
|
-
|
|
192
|
+
diff_text = redact_text(diff) if self._redact_enabled else diff
|
|
193
|
+
lines.extend(diff_text.splitlines())
|
|
155
194
|
return lines
|
|
156
195
|
|
|
157
196
|
if method == "error":
|
|
@@ -163,7 +202,8 @@ class AppServerEventFormatter:
|
|
|
163
202
|
if "outputdelta" in method.lower():
|
|
164
203
|
delta = params.get("delta") or params.get("text") or params.get("output")
|
|
165
204
|
if isinstance(delta, str) and delta:
|
|
166
|
-
|
|
205
|
+
delta_text = redact_text(delta) if self._redact_enabled else delta
|
|
206
|
+
lines.extend(delta_text.splitlines())
|
|
167
207
|
return lines
|
|
168
208
|
|
|
169
209
|
return lines
|