local-coding-agent 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.
- local_coding_agent/__init__.py +314 -0
- local_coding_agent/__main__.py +5 -0
- local_coding_agent/acp_server/__init__.py +38 -0
- local_coding_agent/acp_server/_codec.py +95 -0
- local_coding_agent/acp_server/_protocol.py +13 -0
- local_coding_agent/acp_server/_server.py +663 -0
- local_coding_agent/acp_server/_session.py +24 -0
- local_coding_agent/acp_server/_tools.py +126 -0
- local_coding_agent/ast_compactor.py +130 -0
- local_coding_agent/atomizer.py +77 -0
- local_coding_agent/benchmark/__init__.py +57 -0
- local_coding_agent/benchmark/_cases.py +307 -0
- local_coding_agent/benchmark/_judge.py +175 -0
- local_coding_agent/benchmark/_oracles.py +157 -0
- local_coding_agent/benchmark/_runner.py +259 -0
- local_coding_agent/benchmark/_stats.py +22 -0
- local_coding_agent/benchmark/_summary.py +143 -0
- local_coding_agent/benchmark_oracle_worker.py +169 -0
- local_coding_agent/calibration.py +154 -0
- local_coding_agent/capability.py +450 -0
- local_coding_agent/chat_compaction.py +276 -0
- local_coding_agent/cli/__init__.py +12 -0
- local_coding_agent/cli/_dispatch.py +377 -0
- local_coding_agent/cli/_handlers.py +320 -0
- local_coding_agent/cli/_handlers2.py +725 -0
- local_coding_agent/cli/_input.py +61 -0
- local_coding_agent/cli/_main.py +159 -0
- local_coding_agent/cli/_parser.py +327 -0
- local_coding_agent/context_manager.py +155 -0
- local_coding_agent/controller/__init__.py +18 -0
- local_coding_agent/controller/_constants.py +108 -0
- local_coding_agent/controller/_controller.py +1008 -0
- local_coding_agent/controller/_post_apply.py +216 -0
- local_coding_agent/delegator.py +180 -0
- local_coding_agent/desktop/__init__.py +8 -0
- local_coding_agent/desktop/app.py +100 -0
- local_coding_agent/desktop/assets/lucide.min.js +12 -0
- local_coding_agent/desktop/assets/tailwind.css +1 -0
- local_coding_agent/desktop/client_js.py +1271 -0
- local_coding_agent/desktop/components.py +555 -0
- local_coding_agent/desktop/server/__init__.py +37 -0
- local_coding_agent/desktop/server/_handlers.py +2178 -0
- local_coding_agent/desktop/server/_models.py +204 -0
- local_coding_agent/desktop/server/_server.py +347 -0
- local_coding_agent/desktop/server/_telemetry.py +87 -0
- local_coding_agent/desktop/styles.py +76 -0
- local_coding_agent/desktop/ui.py +53 -0
- local_coding_agent/doctor.py +413 -0
- local_coding_agent/hooks/__init__.py +19 -0
- local_coding_agent/hooks/_adapters.py +458 -0
- local_coding_agent/hooks/_bridge.py +346 -0
- local_coding_agent/hooks/_decision.py +26 -0
- local_coding_agent/lsp/__init__.py +60 -0
- local_coding_agent/lsp/_client.py +322 -0
- local_coding_agent/lsp/_fallback.py +297 -0
- local_coding_agent/lsp/_manager.py +173 -0
- local_coding_agent/lsp/_mock.py +200 -0
- local_coding_agent/lsp/_types.py +398 -0
- local_coding_agent/mcp_config.py +398 -0
- local_coding_agent/mcp_server.py +377 -0
- local_coding_agent/memory.py +140 -0
- local_coding_agent/mode_router.py +200 -0
- local_coding_agent/model_scanner.py +525 -0
- local_coding_agent/monitor.py +635 -0
- local_coding_agent/observation_policy.py +184 -0
- local_coding_agent/ollama_adapter.py +736 -0
- local_coding_agent/plan_mode.py +653 -0
- local_coding_agent/prescriptions.py +278 -0
- local_coding_agent/profiles.py +239 -0
- local_coding_agent/repository_tools.py +782 -0
- local_coding_agent/ripgrep.py +326 -0
- local_coding_agent/semantic_linter.py +122 -0
- local_coding_agent/service.py +405 -0
- local_coding_agent/session_events.py +684 -0
- local_coding_agent/session_query.py +578 -0
- local_coding_agent/skill_config.py +426 -0
- local_coding_agent/smoke.py +268 -0
- local_coding_agent/speculative_racing.py +85 -0
- local_coding_agent/spill.py +258 -0
- local_coding_agent/stats.py +255 -0
- local_coding_agent/stdio.py +132 -0
- local_coding_agent/subagent.py +516 -0
- local_coding_agent/task.py +90 -0
- local_coding_agent/task_store.py +142 -0
- local_coding_agent/tasks.py +240 -0
- local_coding_agent/terminal/__init__.py +35 -0
- local_coding_agent/terminal/_manager.py +156 -0
- local_coding_agent/terminal/_process.py +148 -0
- local_coding_agent/terminal/_session.py +452 -0
- local_coding_agent/terminal/_tools.py +233 -0
- local_coding_agent/terminal/_types.py +60 -0
- local_coding_agent/tool_call_parser.py +178 -0
- local_coding_agent/validators.py +563 -0
- local_coding_agent/vram_fit.py +240 -0
- local_coding_agent/worker_pool.py +516 -0
- local_coding_agent-1.1.0.dist-info/METADATA +202 -0
- local_coding_agent-1.1.0.dist-info/RECORD +101 -0
- local_coding_agent-1.1.0.dist-info/WHEEL +5 -0
- local_coding_agent-1.1.0.dist-info/entry_points.txt +3 -0
- local_coding_agent-1.1.0.dist-info/licenses/LICENSE +21 -0
- local_coding_agent-1.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
"""Bounded controller components for delegating small coding tasks."""
|
|
2
|
+
|
|
3
|
+
__version__ = "1.1.0"
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
from .acp_server import (
|
|
7
|
+
ACP_TOOLS,
|
|
8
|
+
AcpCodec,
|
|
9
|
+
AcpServer,
|
|
10
|
+
AcpSession,
|
|
11
|
+
)
|
|
12
|
+
from .ast_compactor import skeletonize_file, skeletonize_python
|
|
13
|
+
from .atomizer import Decomposition, PreflightReport, TaskBudget, decompose, preflight
|
|
14
|
+
|
|
15
|
+
from .calibration import calibrate_for_model, calibrate_workers, model_vram_bytes
|
|
16
|
+
from .context_manager import (
|
|
17
|
+
ContextAssembler,
|
|
18
|
+
HarnessState,
|
|
19
|
+
compact_tool_exchanges,
|
|
20
|
+
purge_diff_residues,
|
|
21
|
+
)
|
|
22
|
+
from .controller import Controller
|
|
23
|
+
from .delegator import DelegatingAgent, DecompositionTemplate, is_decomposable_failure
|
|
24
|
+
from .doctor import (
|
|
25
|
+
CheckResult,
|
|
26
|
+
DoctorFixReport,
|
|
27
|
+
DoctorReport,
|
|
28
|
+
diagnose_environment,
|
|
29
|
+
remediate_environment,
|
|
30
|
+
)
|
|
31
|
+
from .mcp_config import generate_mcp_config_dict, get_client_config_path, integrate_mcp_config
|
|
32
|
+
from .mcp_server import build_server
|
|
33
|
+
from .memory import LoadedModel, MemoryBudgetError, MemoryOperationError, MemorySnapshot, ModelMemoryManager
|
|
34
|
+
from .mode_router import (
|
|
35
|
+
DEFAULT_MODE_ROUTER_PROFILE,
|
|
36
|
+
MODES,
|
|
37
|
+
VALID_ROUTED,
|
|
38
|
+
ModeName,
|
|
39
|
+
build_mode_router,
|
|
40
|
+
classify_fast,
|
|
41
|
+
classify_mode,
|
|
42
|
+
)
|
|
43
|
+
from .monitor import MonitorServer
|
|
44
|
+
from .lsp import (
|
|
45
|
+
FallbackLspEngine,
|
|
46
|
+
LspClient,
|
|
47
|
+
LspConnectionError,
|
|
48
|
+
LspError,
|
|
49
|
+
LspHoverResult,
|
|
50
|
+
LspLocation,
|
|
51
|
+
LspManager,
|
|
52
|
+
LspPosition,
|
|
53
|
+
LspRange,
|
|
54
|
+
LspResponseError,
|
|
55
|
+
LspSymbol,
|
|
56
|
+
LspTimeoutError,
|
|
57
|
+
MockLspServer,
|
|
58
|
+
)
|
|
59
|
+
from .observation_policy import (
|
|
60
|
+
FsObservationError,
|
|
61
|
+
FsObservationGate,
|
|
62
|
+
is_observed,
|
|
63
|
+
observe_file,
|
|
64
|
+
reset_session,
|
|
65
|
+
verify_edit_intent,
|
|
66
|
+
)
|
|
67
|
+
from .plan_mode import (
|
|
68
|
+
AskUserQuestionTool,
|
|
69
|
+
PlanArtifact,
|
|
70
|
+
PlanModeController,
|
|
71
|
+
PlanModeError,
|
|
72
|
+
PlanModePolicyError,
|
|
73
|
+
PlanModeState,
|
|
74
|
+
QuestionAnswer,
|
|
75
|
+
QuestionItem,
|
|
76
|
+
TodoChecklist,
|
|
77
|
+
TodoItem,
|
|
78
|
+
TodoStatus,
|
|
79
|
+
)
|
|
80
|
+
from .ollama_adapter import (
|
|
81
|
+
ModelProfile,
|
|
82
|
+
OllamaClient,
|
|
83
|
+
OllamaError,
|
|
84
|
+
OpenAICompatibleClient,
|
|
85
|
+
build_client,
|
|
86
|
+
)
|
|
87
|
+
from .ripgrep import RipgrepMatch, ripgrep_files, ripgrep_search
|
|
88
|
+
from .semantic_linter import (
|
|
89
|
+
LinterDiagnostic,
|
|
90
|
+
LinterReport,
|
|
91
|
+
lint_patch_in_memory,
|
|
92
|
+
lint_source_code,
|
|
93
|
+
)
|
|
94
|
+
from .service import DelegationRequest, DelegationService
|
|
95
|
+
from .smoke import run_smoke_test
|
|
96
|
+
from .speculative_racing import SpeculativeRacer
|
|
97
|
+
from .spill import (
|
|
98
|
+
DEFAULT_MAX_BYTES,
|
|
99
|
+
DEFAULT_MAX_LINES,
|
|
100
|
+
SpillRef,
|
|
101
|
+
SpillStore,
|
|
102
|
+
maybe_spill,
|
|
103
|
+
read_spill,
|
|
104
|
+
save_text,
|
|
105
|
+
)
|
|
106
|
+
from .stats import DelegationStats, JsonlStatsSink, TimedDelegationStats
|
|
107
|
+
from .stdio import StdioDelegationAdapter
|
|
108
|
+
from .hooks import (
|
|
109
|
+
ClaudeCodeHookAdapter,
|
|
110
|
+
CodexHookAdapter,
|
|
111
|
+
HookBridge,
|
|
112
|
+
HookDecision,
|
|
113
|
+
)
|
|
114
|
+
from .subagent import (
|
|
115
|
+
MailboxMessage,
|
|
116
|
+
SubagentContext,
|
|
117
|
+
SubagentCoordinator,
|
|
118
|
+
SubagentReport,
|
|
119
|
+
)
|
|
120
|
+
from .task import TaskEnvelope
|
|
121
|
+
from .task_store import JsonFileTaskStore, TaskRecord, TaskStore
|
|
122
|
+
from .terminal import (
|
|
123
|
+
TerminalError,
|
|
124
|
+
TerminalManager,
|
|
125
|
+
TerminalProcessExitedError,
|
|
126
|
+
TerminalSession,
|
|
127
|
+
TerminalSessionExistsError,
|
|
128
|
+
TerminalSessionInfo,
|
|
129
|
+
TerminalSessionNotFoundError,
|
|
130
|
+
TerminalTimeoutError,
|
|
131
|
+
execute_terminal_tool,
|
|
132
|
+
get_terminal_tool_schemas,
|
|
133
|
+
kill_process_tree,
|
|
134
|
+
)
|
|
135
|
+
from .validators import ValidationReport, validate_candidate
|
|
136
|
+
from .worker_pool import BoundedWorkerPool
|
|
137
|
+
from .session_events import (
|
|
138
|
+
ModelTurnEvent,
|
|
139
|
+
PrescriptionEvent,
|
|
140
|
+
SessionCompletedEvent,
|
|
141
|
+
SessionCreatedEvent,
|
|
142
|
+
SessionEvent,
|
|
143
|
+
SessionLog,
|
|
144
|
+
ToolCallEvent,
|
|
145
|
+
ToolResultEvent,
|
|
146
|
+
UserPromptEvent,
|
|
147
|
+
derive_messages,
|
|
148
|
+
event_from_dict,
|
|
149
|
+
event_to_dict,
|
|
150
|
+
fork_session,
|
|
151
|
+
)
|
|
152
|
+
from .session_query import (
|
|
153
|
+
SessionQueryEngine,
|
|
154
|
+
get_session_trace,
|
|
155
|
+
sanitize_fts5_query,
|
|
156
|
+
search_events,
|
|
157
|
+
search_sessions,
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
try:
|
|
161
|
+
from .tasks import TASKS_IDENTIFIER, TasksExtension
|
|
162
|
+
except ImportError: # pragma: no cover - mcp is an optional dependency
|
|
163
|
+
TasksExtension = None # type: ignore[assignment]
|
|
164
|
+
TASKS_IDENTIFIER = "io.modelcontextprotocol/tasks"
|
|
165
|
+
|
|
166
|
+
__all__ = [
|
|
167
|
+
"Controller",
|
|
168
|
+
"DelegationRequest",
|
|
169
|
+
"DelegationService",
|
|
170
|
+
"DelegationStats",
|
|
171
|
+
"JsonlStatsSink",
|
|
172
|
+
"TimedDelegationStats",
|
|
173
|
+
"BoundedWorkerPool",
|
|
174
|
+
"StdioDelegationAdapter",
|
|
175
|
+
"DelegatingAgent",
|
|
176
|
+
"DecompositionTemplate",
|
|
177
|
+
"is_decomposable_failure",
|
|
178
|
+
"Decomposition",
|
|
179
|
+
"PreflightReport",
|
|
180
|
+
"TaskBudget",
|
|
181
|
+
"decompose",
|
|
182
|
+
"preflight",
|
|
183
|
+
"calibrate_for_model",
|
|
184
|
+
"calibrate_workers",
|
|
185
|
+
"model_vram_bytes",
|
|
186
|
+
"LoadedModel",
|
|
187
|
+
"MemoryBudgetError",
|
|
188
|
+
"MemoryOperationError",
|
|
189
|
+
"MemorySnapshot",
|
|
190
|
+
"ModelMemoryManager",
|
|
191
|
+
"ModeName",
|
|
192
|
+
"MODES",
|
|
193
|
+
"VALID_ROUTED",
|
|
194
|
+
"build_mode_router",
|
|
195
|
+
"DEFAULT_MODE_ROUTER_PROFILE",
|
|
196
|
+
"classify_fast",
|
|
197
|
+
"classify_mode",
|
|
198
|
+
"ModelProfile",
|
|
199
|
+
"OllamaClient",
|
|
200
|
+
"OllamaError",
|
|
201
|
+
"OpenAICompatibleClient",
|
|
202
|
+
"build_client",
|
|
203
|
+
"build_server",
|
|
204
|
+
"TaskEnvelope",
|
|
205
|
+
"ValidationReport",
|
|
206
|
+
"validate_candidate",
|
|
207
|
+
"TasksExtension",
|
|
208
|
+
"TASKS_IDENTIFIER",
|
|
209
|
+
"diagnose_environment",
|
|
210
|
+
"DoctorReport",
|
|
211
|
+
"DoctorFixReport",
|
|
212
|
+
"remediate_environment",
|
|
213
|
+
"CheckResult",
|
|
214
|
+
"generate_mcp_config_dict",
|
|
215
|
+
"get_client_config_path",
|
|
216
|
+
"integrate_mcp_config",
|
|
217
|
+
"run_smoke_test",
|
|
218
|
+
"MonitorServer",
|
|
219
|
+
"TaskStore",
|
|
220
|
+
"TaskRecord",
|
|
221
|
+
"JsonFileTaskStore",
|
|
222
|
+
"HarnessState",
|
|
223
|
+
"ContextAssembler",
|
|
224
|
+
"compact_tool_exchanges",
|
|
225
|
+
"purge_diff_residues",
|
|
226
|
+
"skeletonize_python",
|
|
227
|
+
"skeletonize_file",
|
|
228
|
+
"LinterDiagnostic",
|
|
229
|
+
"LinterReport",
|
|
230
|
+
"lint_source_code",
|
|
231
|
+
"lint_patch_in_memory",
|
|
232
|
+
"SpeculativeRacer",
|
|
233
|
+
"FsObservationError",
|
|
234
|
+
"FsObservationGate",
|
|
235
|
+
"observe_file",
|
|
236
|
+
"is_observed",
|
|
237
|
+
"verify_edit_intent",
|
|
238
|
+
"reset_session",
|
|
239
|
+
"RipgrepMatch",
|
|
240
|
+
"ripgrep_search",
|
|
241
|
+
"ripgrep_files",
|
|
242
|
+
"DEFAULT_MAX_BYTES",
|
|
243
|
+
"DEFAULT_MAX_LINES",
|
|
244
|
+
"SpillRef",
|
|
245
|
+
"SpillStore",
|
|
246
|
+
"save_text",
|
|
247
|
+
"read_spill",
|
|
248
|
+
"maybe_spill",
|
|
249
|
+
"LspPosition",
|
|
250
|
+
"LspRange",
|
|
251
|
+
"LspLocation",
|
|
252
|
+
"LspHoverResult",
|
|
253
|
+
"LspSymbol",
|
|
254
|
+
"LspClient",
|
|
255
|
+
"MockLspServer",
|
|
256
|
+
"FallbackLspEngine",
|
|
257
|
+
"LspManager",
|
|
258
|
+
"LspError",
|
|
259
|
+
"LspTimeoutError",
|
|
260
|
+
"LspConnectionError",
|
|
261
|
+
"LspResponseError",
|
|
262
|
+
"TerminalSession",
|
|
263
|
+
"TerminalSessionInfo",
|
|
264
|
+
"TerminalManager",
|
|
265
|
+
"TerminalError",
|
|
266
|
+
"TerminalSessionNotFoundError",
|
|
267
|
+
"TerminalSessionExistsError",
|
|
268
|
+
"TerminalProcessExitedError",
|
|
269
|
+
"TerminalTimeoutError",
|
|
270
|
+
"get_terminal_tool_schemas",
|
|
271
|
+
"execute_terminal_tool",
|
|
272
|
+
"kill_process_tree",
|
|
273
|
+
"PlanModeState",
|
|
274
|
+
"PlanArtifact",
|
|
275
|
+
"PlanModeController",
|
|
276
|
+
"PlanModeError",
|
|
277
|
+
"PlanModePolicyError",
|
|
278
|
+
"QuestionItem",
|
|
279
|
+
"QuestionAnswer",
|
|
280
|
+
"AskUserQuestionTool",
|
|
281
|
+
"TodoStatus",
|
|
282
|
+
"TodoItem",
|
|
283
|
+
"TodoChecklist",
|
|
284
|
+
"HookBridge",
|
|
285
|
+
"HookDecision",
|
|
286
|
+
"CodexHookAdapter",
|
|
287
|
+
"ClaudeCodeHookAdapter",
|
|
288
|
+
"SubagentCoordinator",
|
|
289
|
+
"SubagentContext",
|
|
290
|
+
"MailboxMessage",
|
|
291
|
+
"SubagentReport",
|
|
292
|
+
"AcpServer",
|
|
293
|
+
"AcpSession",
|
|
294
|
+
"AcpCodec",
|
|
295
|
+
"ACP_TOOLS",
|
|
296
|
+
"SessionCreatedEvent",
|
|
297
|
+
"UserPromptEvent",
|
|
298
|
+
"ModelTurnEvent",
|
|
299
|
+
"ToolCallEvent",
|
|
300
|
+
"ToolResultEvent",
|
|
301
|
+
"PrescriptionEvent",
|
|
302
|
+
"SessionCompletedEvent",
|
|
303
|
+
"SessionEvent",
|
|
304
|
+
"SessionLog",
|
|
305
|
+
"derive_messages",
|
|
306
|
+
"fork_session",
|
|
307
|
+
"event_to_dict",
|
|
308
|
+
"event_from_dict",
|
|
309
|
+
"SessionQueryEngine",
|
|
310
|
+
"search_sessions",
|
|
311
|
+
"search_events",
|
|
312
|
+
"get_session_trace",
|
|
313
|
+
"sanitize_fts5_query",
|
|
314
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Universal Agent Client Protocol (ACP) Server & Interop Gateway (R29).
|
|
2
|
+
|
|
3
|
+
Adapted from DeepSeek Harness @deepseek-ai/dsh-acp.
|
|
4
|
+
Implements the standard JSON-RPC 2.0 Agent Client Protocol over stdio with
|
|
5
|
+
both Content-Length header and newline-delimited JSONL framing support.
|
|
6
|
+
Exposes session lifecycle, prompting, cancellation, and repo tools to
|
|
7
|
+
external AI-native editors (Zed, Cursor, VS Code, JetBrains, OpenCode).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._codec import AcpCodec
|
|
11
|
+
from ._protocol import (
|
|
12
|
+
JSONRPC_INTERNAL_ERROR,
|
|
13
|
+
JSONRPC_INVALID_PARAMS,
|
|
14
|
+
JSONRPC_INVALID_REQUEST,
|
|
15
|
+
JSONRPC_METHOD_NOT_FOUND,
|
|
16
|
+
JSONRPC_PARSE_ERROR,
|
|
17
|
+
PROTOCOL_VERSION,
|
|
18
|
+
SERVER_NAME,
|
|
19
|
+
SERVER_VERSION,
|
|
20
|
+
)
|
|
21
|
+
from ._server import AcpServer
|
|
22
|
+
from ._session import AcpSession
|
|
23
|
+
from ._tools import ACP_TOOLS
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"PROTOCOL_VERSION",
|
|
27
|
+
"SERVER_NAME",
|
|
28
|
+
"SERVER_VERSION",
|
|
29
|
+
"JSONRPC_PARSE_ERROR",
|
|
30
|
+
"JSONRPC_INVALID_REQUEST",
|
|
31
|
+
"JSONRPC_METHOD_NOT_FOUND",
|
|
32
|
+
"JSONRPC_INVALID_PARAMS",
|
|
33
|
+
"JSONRPC_INTERNAL_ERROR",
|
|
34
|
+
"ACP_TOOLS",
|
|
35
|
+
"AcpCodec",
|
|
36
|
+
"AcpSession",
|
|
37
|
+
"AcpServer",
|
|
38
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""Stdio framing codec (Content-Length / JSONL)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AcpCodec:
|
|
10
|
+
"""Header & line framing codec for JSON-RPC 2.0 stdio."""
|
|
11
|
+
|
|
12
|
+
@staticmethod
|
|
13
|
+
def read_message(stream: Any, max_bytes: int = 10 * 1024 * 1024) -> tuple[dict[str, Any] | None, str]:
|
|
14
|
+
"""Read a single JSON-RPC message from a binary or text stream.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
(message_dict, framing_type) where framing_type is "content-length" or "jsonl".
|
|
18
|
+
Returns (None, "") on EOF.
|
|
19
|
+
"""
|
|
20
|
+
while True:
|
|
21
|
+
line = stream.readline()
|
|
22
|
+
if not line:
|
|
23
|
+
return None, ""
|
|
24
|
+
if isinstance(line, bytes):
|
|
25
|
+
line_str = line.decode("utf-8", errors="replace").strip()
|
|
26
|
+
else:
|
|
27
|
+
line_str = str(line).strip()
|
|
28
|
+
if line_str:
|
|
29
|
+
break
|
|
30
|
+
|
|
31
|
+
# Check for Content-Length header
|
|
32
|
+
if line_str.lower().startswith("content-length:"):
|
|
33
|
+
try:
|
|
34
|
+
parts = line_str.split(":", 1)
|
|
35
|
+
if len(parts) < 2:
|
|
36
|
+
raise ValueError(f"Invalid Content-Length header: {line_str}")
|
|
37
|
+
content_length = int(parts[1].strip())
|
|
38
|
+
except ValueError:
|
|
39
|
+
raise ValueError(f"Invalid Content-Length header: {line_str}")
|
|
40
|
+
|
|
41
|
+
if content_length < 0:
|
|
42
|
+
raise ValueError(f"Content-Length must be non-negative: {content_length}")
|
|
43
|
+
if content_length > max_bytes:
|
|
44
|
+
raise ValueError(f"Content-Length {content_length} exceeds max allowed {max_bytes} bytes")
|
|
45
|
+
|
|
46
|
+
# Read remaining headers until empty line (bounded against header floods)
|
|
47
|
+
header_count = 0
|
|
48
|
+
while True:
|
|
49
|
+
header_line = stream.readline()
|
|
50
|
+
if not header_line:
|
|
51
|
+
break
|
|
52
|
+
header_count += 1
|
|
53
|
+
if header_count > 50:
|
|
54
|
+
raise ValueError("Too many header lines in request (max 50)")
|
|
55
|
+
if isinstance(header_line, bytes):
|
|
56
|
+
h_str = header_line.decode("utf-8", errors="replace").strip()
|
|
57
|
+
else:
|
|
58
|
+
h_str = str(header_line).strip()
|
|
59
|
+
if not h_str:
|
|
60
|
+
break
|
|
61
|
+
|
|
62
|
+
# Read exact content_length bytes
|
|
63
|
+
body = stream.read(content_length)
|
|
64
|
+
if isinstance(body, bytes):
|
|
65
|
+
if len(body) < content_length:
|
|
66
|
+
raise ValueError(f"Unexpected EOF reading message body: expected {content_length} bytes, got {len(body)}")
|
|
67
|
+
body_str = body.decode("utf-8", errors="replace")
|
|
68
|
+
else:
|
|
69
|
+
body_str = str(body)
|
|
70
|
+
if len(body_str.encode("utf-8")) < content_length and len(body_str) < content_length:
|
|
71
|
+
raise ValueError(f"Unexpected EOF reading message body: expected {content_length} bytes")
|
|
72
|
+
|
|
73
|
+
parsed = json.loads(body_str)
|
|
74
|
+
if not isinstance(parsed, dict):
|
|
75
|
+
raise ValueError("JSON-RPC message must be an object")
|
|
76
|
+
return parsed, "content-length"
|
|
77
|
+
else:
|
|
78
|
+
# JSONL framing
|
|
79
|
+
if len(line_str.encode("utf-8")) > max_bytes:
|
|
80
|
+
raise ValueError(f"JSONL line exceeds max allowed {max_bytes} bytes")
|
|
81
|
+
parsed = json.loads(line_str)
|
|
82
|
+
if not isinstance(parsed, dict):
|
|
83
|
+
raise ValueError("JSON-RPC message must be an object")
|
|
84
|
+
return parsed, "jsonl"
|
|
85
|
+
|
|
86
|
+
@staticmethod
|
|
87
|
+
def format_message(message: dict[str, Any], framing: str = "jsonl") -> bytes:
|
|
88
|
+
"""Format a JSON-RPC message dictionary into framed bytes."""
|
|
89
|
+
raw_json = json.dumps(message, ensure_ascii=False)
|
|
90
|
+
encoded = raw_json.encode("utf-8")
|
|
91
|
+
if framing == "content-length":
|
|
92
|
+
header = f"Content-Length: {len(encoded)}\r\n\r\n".encode("ascii")
|
|
93
|
+
return header + encoded
|
|
94
|
+
else:
|
|
95
|
+
return encoded + b"\n"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""ACP protocol constants and error codes."""
|
|
2
|
+
|
|
3
|
+
from .. import __version__
|
|
4
|
+
|
|
5
|
+
PROTOCOL_VERSION: str = "2026-08-20"
|
|
6
|
+
SERVER_NAME: str = "local-coding-agent-acp"
|
|
7
|
+
SERVER_VERSION: str = __version__
|
|
8
|
+
|
|
9
|
+
JSONRPC_PARSE_ERROR: int = -32700
|
|
10
|
+
JSONRPC_INVALID_REQUEST: int = -32600
|
|
11
|
+
JSONRPC_METHOD_NOT_FOUND: int = -32601
|
|
12
|
+
JSONRPC_INVALID_PARAMS: int = -32602
|
|
13
|
+
JSONRPC_INTERNAL_ERROR: int = -32603
|