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
|
@@ -3,119 +3,31 @@ from __future__ import annotations
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from typing import Callable, Optional
|
|
5
5
|
|
|
6
|
-
from .config import
|
|
7
|
-
AppServerAutorunnerPromptConfig,
|
|
8
|
-
AppServerDocChatPromptConfig,
|
|
9
|
-
AppServerSpecIngestPromptConfig,
|
|
10
|
-
Config,
|
|
11
|
-
)
|
|
6
|
+
from .config import AppServerAutorunnerPromptConfig, Config
|
|
12
7
|
|
|
13
8
|
TRUNCATION_MARKER = "...[truncated]"
|
|
14
9
|
|
|
15
10
|
|
|
16
|
-
DOC_CHAT_APP_SERVER_TEMPLATE = """You are an autonomous coding assistant helping maintain the work docs for this repository.
|
|
17
|
-
|
|
18
|
-
Instructions:
|
|
19
|
-
- This run is non-interactive. Do not ask the user questions. If unsure, make reasonable assumptions and proceed.
|
|
20
|
-
- Use the base doc content below. Drafts (if present) are the authoritative base.
|
|
21
|
-
- You may inspect the repo and update the work docs listed when needed.
|
|
22
|
-
- If you update docs, edit the files directly. If no changes are needed, do not edit files.
|
|
23
|
-
- Respond with a short summary of what you did or found.
|
|
24
|
-
|
|
25
|
-
Work docs (paths):
|
|
26
|
-
- TODO: {todo_path}
|
|
27
|
-
- PROGRESS: {progress_path}
|
|
28
|
-
- OPINIONS: {opinions_path}
|
|
29
|
-
- SPEC: {spec_path}
|
|
30
|
-
- SUMMARY: {summary_path}
|
|
31
|
-
|
|
32
|
-
{user_viewing_block}
|
|
33
|
-
|
|
34
|
-
User request:
|
|
35
|
-
{message}
|
|
36
|
-
|
|
37
|
-
{docs_context_block}
|
|
38
|
-
{recent_summary_block}
|
|
39
|
-
"""
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
SPEC_INGEST_APP_SERVER_TEMPLATE = """You are preparing work docs (TODO/PROGRESS/OPINIONS) from the SPEC.
|
|
43
|
-
|
|
44
|
-
SPEC path: {spec_path}
|
|
45
|
-
TODO path: {todo_path}
|
|
46
|
-
PROGRESS path: {progress_path}
|
|
47
|
-
OPINIONS path: {opinions_path}
|
|
48
|
-
|
|
49
|
-
Instructions:
|
|
50
|
-
- Read the SPEC and existing docs from disk.
|
|
51
|
-
- Edit the TODO, PROGRESS, and OPINIONS files directly to reflect the SPEC.
|
|
52
|
-
- The TODO must be a Markdown checklist. Every task MUST be a checkbox line:
|
|
53
|
-
- Use `- [ ] <task>` for open items and `- [x] <task>` for completed items.
|
|
54
|
-
- Do NOT use plain bullets like `- task` or paragraphs for tasks.
|
|
55
|
-
- Do NOT output a patch block. Just edit the files.
|
|
56
|
-
- Output a short summary prefixed with "Agent: " explaining what you did.
|
|
57
|
-
|
|
58
|
-
User request:
|
|
59
|
-
{message}
|
|
60
|
-
|
|
61
|
-
{spec_excerpt_block}
|
|
62
|
-
"""
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
SNAPSHOT_APP_SERVER_TEMPLATE = """You are generating a compact Markdown repo snapshot meant to be pasted into another LLM chat.
|
|
66
|
-
|
|
67
|
-
Snapshot path: {snapshot_path}
|
|
68
|
-
|
|
69
|
-
Instructions:
|
|
70
|
-
- Analyze the provided context and the repository.
|
|
71
|
-
- Write the snapshot content directly to the snapshot path.
|
|
72
|
-
- Keep the file concise and high-signal.
|
|
73
|
-
|
|
74
|
-
Required output format (keep headings exactly):
|
|
75
|
-
# Repo Snapshot
|
|
76
|
-
|
|
77
|
-
## What this repo is
|
|
78
|
-
- 3–6 bullets.
|
|
79
|
-
|
|
80
|
-
## Architecture overview
|
|
81
|
-
- Components and responsibilities.
|
|
82
|
-
- Data/control flow (high level).
|
|
83
|
-
- How things actually work
|
|
84
|
-
|
|
85
|
-
## Key files and modules
|
|
86
|
-
- Bullet list of important paths with 1-line notes.
|
|
87
|
-
|
|
88
|
-
## Extension points and sharp edges
|
|
89
|
-
- Config/state/concurrency hazards, limits, sharp edges.
|
|
90
|
-
|
|
91
|
-
Inputs:
|
|
92
|
-
{seed_context}
|
|
93
|
-
|
|
94
|
-
{changes_block}
|
|
95
|
-
{previous_snapshot_block}
|
|
96
|
-
"""
|
|
97
|
-
|
|
98
|
-
|
|
99
11
|
AUTORUNNER_APP_SERVER_TEMPLATE = """You are an autonomous coding assistant operating on a git repository.
|
|
100
12
|
|
|
101
|
-
|
|
102
|
-
-
|
|
103
|
-
-
|
|
104
|
-
-
|
|
105
|
-
|
|
106
|
-
|
|
13
|
+
Workspace docs (optional; read from disk when useful):
|
|
14
|
+
- Active context: {active_context_path}
|
|
15
|
+
- Decisions: {decisions_path}
|
|
16
|
+
- Spec: {spec_path}
|
|
17
|
+
|
|
18
|
+
Tickets:
|
|
19
|
+
- The authoritative work items are ticket files under `.codex-autorunner/tickets/`.
|
|
20
|
+
- Pick the next not-done ticket, implement it, and update the ticket file (`done: true`) when complete.
|
|
107
21
|
|
|
108
22
|
Instructions:
|
|
109
23
|
- This run is non-interactive. Do not ask the user questions. If unsure, make reasonable assumptions and proceed.
|
|
110
|
-
-
|
|
111
|
-
-
|
|
112
|
-
- Keep TODO/PROGRESS/OPINIONS/SPEC/SUMMARY in sync.
|
|
113
|
-
- Make actual edits in the repo as needed.
|
|
24
|
+
- Prefer small, safe diffs and keep work focused on the current ticket.
|
|
25
|
+
- You may create new tickets only when needed to break down the current work.
|
|
114
26
|
|
|
115
27
|
User request:
|
|
116
28
|
{message}
|
|
117
29
|
|
|
118
|
-
{
|
|
30
|
+
{workspace_spec_block}
|
|
119
31
|
{prev_run_block}
|
|
120
32
|
"""
|
|
121
33
|
|
|
@@ -169,144 +81,6 @@ def _shrink_prompt(
|
|
|
169
81
|
return prompt
|
|
170
82
|
|
|
171
83
|
|
|
172
|
-
def build_doc_chat_prompt(
|
|
173
|
-
config: Config,
|
|
174
|
-
*,
|
|
175
|
-
message: str,
|
|
176
|
-
recent_summary: Optional[str],
|
|
177
|
-
docs: dict[str, dict[str, str]],
|
|
178
|
-
context_doc: Optional[str] = None,
|
|
179
|
-
) -> str:
|
|
180
|
-
prompt_cfg: AppServerDocChatPromptConfig = config.app_server.prompts.doc_chat
|
|
181
|
-
doc_paths = {
|
|
182
|
-
"todo": _display_path(config.root, config.doc_path("todo")),
|
|
183
|
-
"progress": _display_path(config.root, config.doc_path("progress")),
|
|
184
|
-
"opinions": _display_path(config.root, config.doc_path("opinions")),
|
|
185
|
-
"spec": _display_path(config.root, config.doc_path("spec")),
|
|
186
|
-
"summary": _display_path(config.root, config.doc_path("summary")),
|
|
187
|
-
}
|
|
188
|
-
message_text = truncate_text(message, prompt_cfg.message_max_chars)
|
|
189
|
-
doc_blocks = []
|
|
190
|
-
for key, path in doc_paths.items():
|
|
191
|
-
payload = docs.get(key, {})
|
|
192
|
-
source = payload.get("source") or "disk"
|
|
193
|
-
content = truncate_text(
|
|
194
|
-
str(payload.get("content") or ""), prompt_cfg.target_excerpt_max_chars
|
|
195
|
-
)
|
|
196
|
-
if not content.strip():
|
|
197
|
-
content = "(empty)"
|
|
198
|
-
label = f"{key.upper()} [{path}] ({source.upper()})"
|
|
199
|
-
doc_blocks.append(f"{label}\n{content}")
|
|
200
|
-
docs_context = "\n\n".join(doc_blocks)
|
|
201
|
-
recent_text = truncate_text(
|
|
202
|
-
recent_summary or "", prompt_cfg.recent_summary_max_chars
|
|
203
|
-
)
|
|
204
|
-
user_viewing = ""
|
|
205
|
-
if context_doc:
|
|
206
|
-
user_viewing = f"The user is currently looking at {context_doc.upper()}."
|
|
207
|
-
|
|
208
|
-
sections = {
|
|
209
|
-
"message": message_text,
|
|
210
|
-
"docs_context": docs_context,
|
|
211
|
-
"recent_summary": recent_text,
|
|
212
|
-
"user_viewing": user_viewing,
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
def render() -> str:
|
|
216
|
-
return DOC_CHAT_APP_SERVER_TEMPLATE.format(
|
|
217
|
-
todo_path=doc_paths["todo"],
|
|
218
|
-
progress_path=doc_paths["progress"],
|
|
219
|
-
opinions_path=doc_paths["opinions"],
|
|
220
|
-
spec_path=doc_paths["spec"],
|
|
221
|
-
summary_path=doc_paths["summary"],
|
|
222
|
-
message=sections["message"],
|
|
223
|
-
user_viewing_block=_optional_block(
|
|
224
|
-
"USER_VIEWING", sections["user_viewing"]
|
|
225
|
-
),
|
|
226
|
-
docs_context_block=_optional_block("DOC_BASES", sections["docs_context"]),
|
|
227
|
-
recent_summary_block=_optional_block(
|
|
228
|
-
"RECENT_RUN_SUMMARY", sections["recent_summary"]
|
|
229
|
-
),
|
|
230
|
-
)
|
|
231
|
-
|
|
232
|
-
return _shrink_prompt(
|
|
233
|
-
max_chars=prompt_cfg.max_chars,
|
|
234
|
-
render=render,
|
|
235
|
-
sections=sections,
|
|
236
|
-
order=["recent_summary", "docs_context", "message"],
|
|
237
|
-
)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def build_spec_ingest_prompt(
|
|
241
|
-
config: Config,
|
|
242
|
-
*,
|
|
243
|
-
message: str,
|
|
244
|
-
spec_path: Optional[Path] = None,
|
|
245
|
-
) -> str:
|
|
246
|
-
prompt_cfg: AppServerSpecIngestPromptConfig = config.app_server.prompts.spec_ingest
|
|
247
|
-
doc_paths = {
|
|
248
|
-
"todo": _display_path(config.root, config.doc_path("todo")),
|
|
249
|
-
"progress": _display_path(config.root, config.doc_path("progress")),
|
|
250
|
-
"opinions": _display_path(config.root, config.doc_path("opinions")),
|
|
251
|
-
}
|
|
252
|
-
spec_target = spec_path or config.doc_path("spec")
|
|
253
|
-
spec_path_str = _display_path(config.root, spec_target)
|
|
254
|
-
message_text = truncate_text(message, prompt_cfg.message_max_chars)
|
|
255
|
-
spec_excerpt = truncate_text(
|
|
256
|
-
spec_target.read_text(encoding="utf-8"),
|
|
257
|
-
prompt_cfg.spec_excerpt_max_chars,
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
sections = {
|
|
261
|
-
"message": message_text,
|
|
262
|
-
"spec_excerpt": spec_excerpt,
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
def render() -> str:
|
|
266
|
-
return SPEC_INGEST_APP_SERVER_TEMPLATE.format(
|
|
267
|
-
spec_path=spec_path_str,
|
|
268
|
-
todo_path=doc_paths["todo"],
|
|
269
|
-
progress_path=doc_paths["progress"],
|
|
270
|
-
opinions_path=doc_paths["opinions"],
|
|
271
|
-
message=sections["message"],
|
|
272
|
-
spec_excerpt_block=_optional_block(
|
|
273
|
-
"SPEC_EXCERPT", sections["spec_excerpt"]
|
|
274
|
-
),
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
return _shrink_prompt(
|
|
278
|
-
max_chars=prompt_cfg.max_chars,
|
|
279
|
-
render=render,
|
|
280
|
-
sections=sections,
|
|
281
|
-
order=["spec_excerpt", "message"],
|
|
282
|
-
)
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def build_app_server_snapshot_prompt(
|
|
286
|
-
config: Config,
|
|
287
|
-
*,
|
|
288
|
-
seed_context: str,
|
|
289
|
-
previous_snapshot: Optional[str] = None,
|
|
290
|
-
changes: Optional[str] = None,
|
|
291
|
-
) -> str:
|
|
292
|
-
snapshot_path = config.doc_path("snapshot")
|
|
293
|
-
previous_block = ""
|
|
294
|
-
if previous_snapshot:
|
|
295
|
-
previous_block = (
|
|
296
|
-
f"<PREVIOUS_SNAPSHOT>\n{previous_snapshot.strip()}\n</PREVIOUS_SNAPSHOT>"
|
|
297
|
-
)
|
|
298
|
-
changes_block = ""
|
|
299
|
-
if changes:
|
|
300
|
-
changes_block = f"<CHANGES_SINCE_LAST_SNAPSHOT>\n{changes.strip()}\n</CHANGES_SINCE_LAST_SNAPSHOT>"
|
|
301
|
-
|
|
302
|
-
return SNAPSHOT_APP_SERVER_TEMPLATE.format(
|
|
303
|
-
snapshot_path=snapshot_path,
|
|
304
|
-
seed_context=seed_context,
|
|
305
|
-
changes_block=changes_block,
|
|
306
|
-
previous_snapshot_block=previous_block,
|
|
307
|
-
)
|
|
308
|
-
|
|
309
|
-
|
|
310
84
|
def build_autorunner_prompt(
|
|
311
85
|
config: Config,
|
|
312
86
|
*,
|
|
@@ -315,35 +89,36 @@ def build_autorunner_prompt(
|
|
|
315
89
|
) -> str:
|
|
316
90
|
prompt_cfg: AppServerAutorunnerPromptConfig = config.app_server.prompts.autorunner
|
|
317
91
|
doc_paths = {
|
|
318
|
-
"
|
|
319
|
-
"
|
|
320
|
-
"opinions": _display_path(config.root, config.doc_path("opinions")),
|
|
92
|
+
"active_context": _display_path(config.root, config.doc_path("active_context")),
|
|
93
|
+
"decisions": _display_path(config.root, config.doc_path("decisions")),
|
|
321
94
|
"spec": _display_path(config.root, config.doc_path("spec")),
|
|
322
|
-
"summary": _display_path(config.root, config.doc_path("summary")),
|
|
323
95
|
}
|
|
96
|
+
|
|
324
97
|
message_text = truncate_text(message, prompt_cfg.message_max_chars)
|
|
325
|
-
|
|
326
|
-
|
|
98
|
+
spec_excerpt = truncate_text(
|
|
99
|
+
(
|
|
100
|
+
config.doc_path("spec").read_text(encoding="utf-8")
|
|
101
|
+
if config.doc_path("spec").exists()
|
|
102
|
+
else ""
|
|
103
|
+
),
|
|
327
104
|
prompt_cfg.todo_excerpt_max_chars,
|
|
328
105
|
)
|
|
329
106
|
prev_run_text = truncate_text(prev_run_summary or "", prompt_cfg.prev_run_max_chars)
|
|
330
107
|
|
|
331
108
|
sections = {
|
|
332
109
|
"message": message_text,
|
|
333
|
-
"
|
|
110
|
+
"workspace_spec": spec_excerpt,
|
|
334
111
|
"prev_run": prev_run_text,
|
|
335
112
|
}
|
|
336
113
|
|
|
337
114
|
def render() -> str:
|
|
338
115
|
return AUTORUNNER_APP_SERVER_TEMPLATE.format(
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
opinions_path=doc_paths["opinions"],
|
|
116
|
+
active_context_path=doc_paths["active_context"],
|
|
117
|
+
decisions_path=doc_paths["decisions"],
|
|
342
118
|
spec_path=doc_paths["spec"],
|
|
343
|
-
summary_path=doc_paths["summary"],
|
|
344
119
|
message=sections["message"],
|
|
345
|
-
|
|
346
|
-
"
|
|
120
|
+
workspace_spec_block=_optional_block(
|
|
121
|
+
"WORKSPACE_SPEC", sections["workspace_spec"]
|
|
347
122
|
),
|
|
348
123
|
prev_run_block=_optional_block("PREV_RUN_SUMMARY", sections["prev_run"]),
|
|
349
124
|
)
|
|
@@ -352,13 +127,11 @@ def build_autorunner_prompt(
|
|
|
352
127
|
max_chars=prompt_cfg.max_chars,
|
|
353
128
|
render=render,
|
|
354
129
|
sections=sections,
|
|
355
|
-
order=["prev_run", "
|
|
130
|
+
order=["prev_run", "workspace_spec", "message"],
|
|
356
131
|
)
|
|
357
132
|
|
|
358
133
|
|
|
359
134
|
APP_SERVER_PROMPT_BUILDERS = {
|
|
360
|
-
"doc_chat": build_doc_chat_prompt,
|
|
361
|
-
"spec_ingest": build_spec_ingest_prompt,
|
|
362
135
|
"autorunner": build_autorunner_prompt,
|
|
363
136
|
}
|
|
364
137
|
|
|
@@ -366,13 +139,7 @@ APP_SERVER_PROMPT_BUILDERS = {
|
|
|
366
139
|
__all__ = [
|
|
367
140
|
"AUTORUNNER_APP_SERVER_TEMPLATE",
|
|
368
141
|
"APP_SERVER_PROMPT_BUILDERS",
|
|
369
|
-
"DOC_CHAT_APP_SERVER_TEMPLATE",
|
|
370
|
-
"SPEC_INGEST_APP_SERVER_TEMPLATE",
|
|
371
|
-
"SNAPSHOT_APP_SERVER_TEMPLATE",
|
|
372
142
|
"TRUNCATION_MARKER",
|
|
373
143
|
"build_autorunner_prompt",
|
|
374
|
-
"build_doc_chat_prompt",
|
|
375
|
-
"build_spec_ingest_prompt",
|
|
376
|
-
"build_app_server_snapshot_prompt",
|
|
377
144
|
"truncate_text",
|
|
378
145
|
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
from datetime import datetime, timezone
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Optional
|
|
@@ -12,19 +13,17 @@ APP_SERVER_THREADS_FILENAME = ".codex-autorunner/app_server_threads.json"
|
|
|
12
13
|
APP_SERVER_THREADS_VERSION = 1
|
|
13
14
|
APP_SERVER_THREADS_CORRUPT_SUFFIX = ".corrupt"
|
|
14
15
|
APP_SERVER_THREADS_NOTICE_SUFFIX = ".corrupt.json"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
"spec_ingest",
|
|
27
|
-
"spec_ingest.opencode",
|
|
16
|
+
FILE_CHAT_KEY = "file_chat"
|
|
17
|
+
FILE_CHAT_OPENCODE_KEY = "file_chat.opencode"
|
|
18
|
+
FILE_CHAT_PREFIX = "file_chat."
|
|
19
|
+
FILE_CHAT_OPENCODE_PREFIX = "file_chat.opencode."
|
|
20
|
+
|
|
21
|
+
LOGGER = logging.getLogger("codex_autorunner.app_server")
|
|
22
|
+
|
|
23
|
+
# Static keys that can be reset/managed via the UI.
|
|
24
|
+
FEATURE_KEYS = {
|
|
25
|
+
FILE_CHAT_KEY,
|
|
26
|
+
FILE_CHAT_OPENCODE_KEY,
|
|
28
27
|
"autorunner",
|
|
29
28
|
"autorunner.opencode",
|
|
30
29
|
}
|
|
@@ -43,6 +42,10 @@ def normalize_feature_key(raw: str) -> str:
|
|
|
43
42
|
key = key.replace("/", ".").replace(":", ".")
|
|
44
43
|
if key in FEATURE_KEYS:
|
|
45
44
|
return key
|
|
45
|
+
# Allow per-target file chat threads (e.g. file_chat.ticket.1, file_chat.workspace.spec).
|
|
46
|
+
for prefix in (FILE_CHAT_PREFIX, FILE_CHAT_OPENCODE_PREFIX):
|
|
47
|
+
if key.startswith(prefix) and len(key) > len(prefix):
|
|
48
|
+
return key
|
|
46
49
|
raise ValueError(f"invalid feature key: {raw}")
|
|
47
50
|
|
|
48
51
|
|
|
@@ -84,20 +87,9 @@ class AppServerThreadRegistry:
|
|
|
84
87
|
|
|
85
88
|
def feature_map(self) -> dict[str, object]:
|
|
86
89
|
threads = self.load()
|
|
87
|
-
doc_chat_thread = threads.get(DOC_CHAT_KEY)
|
|
88
|
-
doc_chat_opencode_thread = threads.get(DOC_CHAT_OPENCODE_KEY)
|
|
89
90
|
payload: dict[str, object] = {
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
for kind in DOC_CHAT_KINDS
|
|
93
|
-
},
|
|
94
|
-
"doc_chat_opencode": {
|
|
95
|
-
kind: doc_chat_opencode_thread
|
|
96
|
-
or threads.get(f"{DOC_CHAT_OPENCODE_PREFIX}{kind}")
|
|
97
|
-
for kind in DOC_CHAT_KINDS
|
|
98
|
-
},
|
|
99
|
-
"spec_ingest": threads.get("spec_ingest"),
|
|
100
|
-
"spec_ingest_opencode": threads.get("spec_ingest.opencode"),
|
|
91
|
+
"file_chat": threads.get(FILE_CHAT_KEY),
|
|
92
|
+
"file_chat_opencode": threads.get(FILE_CHAT_OPENCODE_KEY),
|
|
101
93
|
"autorunner": threads.get("autorunner"),
|
|
102
94
|
"autorunner_opencode": threads.get("autorunner.opencode"),
|
|
103
95
|
}
|
|
@@ -188,8 +180,14 @@ class AppServerThreadRegistry:
|
|
|
188
180
|
try:
|
|
189
181
|
atomic_write(self._notice_path(), json.dumps(notice, indent=2) + "\n")
|
|
190
182
|
except Exception:
|
|
191
|
-
|
|
183
|
+
LOGGER.warning(
|
|
184
|
+
"Failed to write app server thread corruption notice.",
|
|
185
|
+
exc_info=True,
|
|
186
|
+
)
|
|
192
187
|
try:
|
|
193
188
|
self._save_unlocked({})
|
|
194
189
|
except Exception:
|
|
195
|
-
|
|
190
|
+
LOGGER.warning(
|
|
191
|
+
"Failed to reset app server thread registry after corruption.",
|
|
192
|
+
exc_info=True,
|
|
193
|
+
)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any, Optional, Sequence
|
|
4
|
+
|
|
5
|
+
from .logging_utils import log_event
|
|
6
|
+
from .utils import resolve_executable, subprocess_env
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def app_server_env(
|
|
10
|
+
command: Sequence[str],
|
|
11
|
+
cwd: Path,
|
|
12
|
+
*,
|
|
13
|
+
base_env: Optional[dict[str, str]] = None,
|
|
14
|
+
) -> dict[str, str]:
|
|
15
|
+
extra_paths: list[str] = []
|
|
16
|
+
if command:
|
|
17
|
+
binary = command[0]
|
|
18
|
+
resolved = resolve_executable(binary, env=base_env)
|
|
19
|
+
candidate: Optional[Path] = Path(resolved) if resolved else None
|
|
20
|
+
if candidate is None:
|
|
21
|
+
candidate = Path(binary).expanduser()
|
|
22
|
+
if not candidate.is_absolute():
|
|
23
|
+
candidate = (cwd / candidate).resolve()
|
|
24
|
+
if candidate.exists():
|
|
25
|
+
extra_paths.append(str(candidate.parent))
|
|
26
|
+
return subprocess_env(extra_paths=extra_paths, base_env=base_env)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def seed_codex_home(
|
|
30
|
+
codex_home: Path,
|
|
31
|
+
*,
|
|
32
|
+
logger: Any = None,
|
|
33
|
+
event_prefix: str = "app_server",
|
|
34
|
+
) -> None:
|
|
35
|
+
logger = logger or __import__("logging").getLogger(__name__)
|
|
36
|
+
auth_path = codex_home / "auth.json"
|
|
37
|
+
source_root = Path(os.environ.get("CODEX_HOME", "~/.codex")).expanduser()
|
|
38
|
+
if source_root.resolve() == codex_home.resolve():
|
|
39
|
+
return
|
|
40
|
+
source_auth = source_root / "auth.json"
|
|
41
|
+
if auth_path.exists():
|
|
42
|
+
if auth_path.is_symlink() and auth_path.resolve() == source_auth.resolve():
|
|
43
|
+
return
|
|
44
|
+
log_event(
|
|
45
|
+
logger,
|
|
46
|
+
__import__("logging").INFO,
|
|
47
|
+
f"{event_prefix}.codex_home.seed.skipped",
|
|
48
|
+
reason="auth_exists",
|
|
49
|
+
source=str(source_root),
|
|
50
|
+
target=str(codex_home),
|
|
51
|
+
)
|
|
52
|
+
return
|
|
53
|
+
if not source_root.exists():
|
|
54
|
+
log_event(
|
|
55
|
+
logger,
|
|
56
|
+
__import__("logging").WARNING,
|
|
57
|
+
f"{event_prefix}.codex_home.seed.skipped",
|
|
58
|
+
reason="source_missing",
|
|
59
|
+
source=str(source_root),
|
|
60
|
+
target=str(codex_home),
|
|
61
|
+
)
|
|
62
|
+
return
|
|
63
|
+
if not source_auth.exists():
|
|
64
|
+
log_event(
|
|
65
|
+
logger,
|
|
66
|
+
__import__("logging").WARNING,
|
|
67
|
+
f"{event_prefix}.codex_home.seed.skipped",
|
|
68
|
+
reason="auth_missing",
|
|
69
|
+
source=str(source_root),
|
|
70
|
+
target=str(codex_home),
|
|
71
|
+
)
|
|
72
|
+
return
|
|
73
|
+
try:
|
|
74
|
+
auth_path.symlink_to(source_auth)
|
|
75
|
+
log_event(
|
|
76
|
+
logger,
|
|
77
|
+
__import__("logging").INFO,
|
|
78
|
+
f"{event_prefix}.codex_home.seeded",
|
|
79
|
+
source=str(source_root),
|
|
80
|
+
target=str(codex_home),
|
|
81
|
+
)
|
|
82
|
+
except OSError as exc:
|
|
83
|
+
log_event(
|
|
84
|
+
logger,
|
|
85
|
+
__import__("logging").WARNING,
|
|
86
|
+
f"{event_prefix}.codex_home.seed.failed",
|
|
87
|
+
exc=exc,
|
|
88
|
+
source=str(source_root),
|
|
89
|
+
target=str(codex_home),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_app_server_env(
|
|
94
|
+
command: Sequence[str],
|
|
95
|
+
workspace_root: Path,
|
|
96
|
+
state_dir: Path,
|
|
97
|
+
*,
|
|
98
|
+
logger: Any = None,
|
|
99
|
+
event_prefix: str = "app_server",
|
|
100
|
+
base_env: Optional[dict[str, str]] = None,
|
|
101
|
+
) -> dict[str, str]:
|
|
102
|
+
env = app_server_env(command, workspace_root, base_env=base_env)
|
|
103
|
+
codex_home = state_dir / "codex_home"
|
|
104
|
+
codex_home.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
seed_codex_home(codex_home, logger=logger, event_prefix=event_prefix)
|
|
106
|
+
env["CODEX_HOME"] = str(codex_home)
|
|
107
|
+
return env
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _extract_turn_id(payload: Any) -> Optional[str]:
|
|
111
|
+
if not isinstance(payload, dict):
|
|
112
|
+
return None
|
|
113
|
+
for key in ("turnId", "turn_id", "id"):
|
|
114
|
+
value = payload.get(key)
|
|
115
|
+
if isinstance(value, str):
|
|
116
|
+
return value
|
|
117
|
+
turn = payload.get("turn")
|
|
118
|
+
if isinstance(turn, dict):
|
|
119
|
+
for key in ("id", "turnId", "turn_id"):
|
|
120
|
+
value = turn.get(key)
|
|
121
|
+
if isinstance(value, str):
|
|
122
|
+
return value
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _extract_thread_id_from_container(payload: Any) -> Optional[str]:
|
|
127
|
+
if not isinstance(payload, dict):
|
|
128
|
+
return None
|
|
129
|
+
for key in ("threadId", "thread_id"):
|
|
130
|
+
value = payload.get(key)
|
|
131
|
+
if isinstance(value, str):
|
|
132
|
+
return value
|
|
133
|
+
thread = payload.get("thread")
|
|
134
|
+
if isinstance(thread, dict):
|
|
135
|
+
for key in ("id", "threadId", "thread_id"):
|
|
136
|
+
value = thread.get(key)
|
|
137
|
+
if isinstance(value, str):
|
|
138
|
+
return value
|
|
139
|
+
return None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _extract_thread_id_for_turn(payload: Any) -> Optional[str]:
|
|
143
|
+
if not isinstance(payload, dict):
|
|
144
|
+
return None
|
|
145
|
+
for candidate in (payload, payload.get("turn"), payload.get("item")):
|
|
146
|
+
thread_id = _extract_thread_id_from_container(candidate)
|
|
147
|
+
if thread_id:
|
|
148
|
+
return thread_id
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _extract_thread_id(payload: Any) -> Optional[str]:
|
|
153
|
+
if not isinstance(payload, dict):
|
|
154
|
+
return None
|
|
155
|
+
for key in ("threadId", "thread_id", "id"):
|
|
156
|
+
value = payload.get(key)
|
|
157
|
+
if isinstance(value, str):
|
|
158
|
+
return value
|
|
159
|
+
thread = payload.get("thread")
|
|
160
|
+
if isinstance(thread, dict):
|
|
161
|
+
for key in ("id", "threadId", "thread_id"):
|
|
162
|
+
value = thread.get(key)
|
|
163
|
+
if isinstance(value, str):
|
|
164
|
+
return value
|
|
165
|
+
return None
|