bone-agent 1.4.0 → 2.0.1
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.
- package/bin/bone.js +39 -0
- package/package.json +25 -39
- package/LICENSE +0 -21
- package/README.md +0 -201
- package/bin/npm-wrapper.js +0 -235
- package/bin/rg +0 -0
- package/bin/rg.exe +0 -0
- package/config.yaml.example +0 -144
- package/prompts/main/ask_questions.md +0 -31
- package/prompts/main/batch_independent_calls.md +0 -5
- package/prompts/main/casual_interactions.md +0 -11
- package/prompts/main/code_references.md +0 -8
- package/prompts/main/communication_style.md +0 -12
- package/prompts/main/context_reliability.md +0 -12
- package/prompts/main/conversational_tool_calling.md +0 -15
- package/prompts/main/dream.md +0 -50
- package/prompts/main/editing_pattern.md +0 -13
- package/prompts/main/error_handling.md +0 -6
- package/prompts/main/exploration_pattern.md +0 -21
- package/prompts/main/intro.md +0 -1
- package/prompts/main/obsidian.md +0 -16
- package/prompts/main/obsidian_project.md +0 -79
- package/prompts/main/professional_objectivity.md +0 -3
- package/prompts/main/skills.md +0 -3
- package/prompts/main/targeted_searching.md +0 -10
- package/prompts/main/task_lists_pattern.md +0 -8
- package/prompts/main/temp_folder.md +0 -9
- package/prompts/main/think_before_acting.md +0 -10
- package/prompts/main/tone_and_style.md +0 -4
- package/prompts/main/tool_preferences.md +0 -24
- package/prompts/main/trust_subagent_context.md +0 -21
- package/prompts/main/when_to_use_sub_agent.md +0 -7
- package/prompts/micro/ask_questions.md +0 -1
- package/prompts/micro/batch_independent_calls.md +0 -1
- package/prompts/micro/casual_interactions.md +0 -1
- package/prompts/micro/code_references.md +0 -1
- package/prompts/micro/communication_style.md +0 -1
- package/prompts/micro/context_reliability.md +0 -1
- package/prompts/micro/conversational_tool_calling.md +0 -1
- package/prompts/micro/editing_pattern.md +0 -1
- package/prompts/micro/error_handling.md +0 -1
- package/prompts/micro/exploration_pattern.md +0 -1
- package/prompts/micro/intro.md +0 -1
- package/prompts/micro/obsidian.md +0 -4
- package/prompts/micro/obsidian_project.md +0 -5
- package/prompts/micro/professional_objectivity.md +0 -1
- package/prompts/micro/skills.md +0 -1
- package/prompts/micro/targeted_searching.md +0 -1
- package/prompts/micro/task_lists_pattern.md +0 -1
- package/prompts/micro/temp_folder.md +0 -1
- package/prompts/micro/think_before_acting.md +0 -5
- package/prompts/micro/tone_and_style.md +0 -1
- package/prompts/micro/tool_preferences.md +0 -1
- package/prompts/micro/trust_subagent_context.md +0 -1
- package/prompts/micro/when_to_use_sub_agent.md +0 -1
- package/requirements.txt +0 -9
- package/src/__init__.py +0 -11
- package/src/core/__init__.py +0 -1
- package/src/core/agentic.py +0 -1085
- package/src/core/chat_manager.py +0 -1577
- package/src/core/config_manager.py +0 -260
- package/src/core/cron.py +0 -578
- package/src/core/cron_allowlist.py +0 -118
- package/src/core/memory.py +0 -145
- package/src/core/metadata.py +0 -75
- package/src/core/retry.py +0 -71
- package/src/core/skills.py +0 -463
- package/src/core/sub_agent.py +0 -376
- package/src/core/tool_approval.py +0 -220
- package/src/core/tool_feedback.py +0 -789
- package/src/exceptions.py +0 -79
- package/src/llm/__init__.py +0 -1
- package/src/llm/client.py +0 -176
- package/src/llm/codex_provider.py +0 -350
- package/src/llm/config.py +0 -536
- package/src/llm/prompts.py +0 -494
- package/src/llm/providers.py +0 -438
- package/src/llm/streaming.py +0 -163
- package/src/llm/token_tracker.py +0 -399
- package/src/tools/__init__.py +0 -151
- package/src/tools/constants.py +0 -59
- package/src/tools/create_file.py +0 -136
- package/src/tools/directory.py +0 -389
- package/src/tools/edit.py +0 -549
- package/src/tools/file_reader.py +0 -322
- package/src/tools/helpers/__init__.py +0 -99
- package/src/tools/helpers/base.py +0 -599
- package/src/tools/helpers/converters.py +0 -44
- package/src/tools/helpers/file_helpers.py +0 -189
- package/src/tools/helpers/formatters.py +0 -411
- package/src/tools/helpers/loader.py +0 -145
- package/src/tools/helpers/parallel_executor.py +0 -231
- package/src/tools/helpers/path_resolver.py +0 -283
- package/src/tools/helpers/plugin_manifest.py +0 -185
- package/src/tools/obsidian.py +0 -96
- package/src/tools/review_sub_agent.py +0 -190
- package/src/tools/rg_search.py +0 -477
- package/src/tools/search_plugins.py +0 -177
- package/src/tools/select_option.py +0 -600
- package/src/tools/shell.py +0 -302
- package/src/tools/sub_agent.py +0 -139
- package/src/tools/task_list.py +0 -269
- package/src/tools/web_search.py +0 -61
- package/src/ui/__init__.py +0 -1
- package/src/ui/banner.py +0 -87
- package/src/ui/commands.py +0 -3131
- package/src/ui/displays.py +0 -239
- package/src/ui/loader.py +0 -284
- package/src/ui/main.py +0 -643
- package/src/ui/prompt_utils.py +0 -113
- package/src/ui/setting_selector.py +0 -590
- package/src/ui/setup_wizard.py +0 -294
- package/src/ui/sub_agent_panel.py +0 -234
- package/src/ui/tool_confirmation.py +0 -226
- package/src/utils/__init__.py +0 -1
- package/src/utils/citation_parser.py +0 -199
- package/src/utils/editor.py +0 -207
- package/src/utils/gitignore_filter.py +0 -149
- package/src/utils/logger.py +0 -254
- package/src/utils/paths.py +0 -30
- package/src/utils/result_parsers.py +0 -108
- package/src/utils/safe_commands.py +0 -243
- package/src/utils/settings.py +0 -195
- package/src/utils/user_message_logger.py +0 -120
- package/src/utils/validation.py +0 -201
- package/src/utils/web_search.py +0 -173
package/src/exceptions.py
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
"""Custom exception hierarchy for bone-agent."""
|
|
2
|
-
|
|
3
|
-
class BoneAgentError(Exception):
|
|
4
|
-
"""Base exception for all bone-agent application errors.
|
|
5
|
-
|
|
6
|
-
All custom exceptions should inherit from this class.
|
|
7
|
-
Provides consistent error handling and allows catching
|
|
8
|
-
all bone-agent-specific errors with a single except clause.
|
|
9
|
-
"""
|
|
10
|
-
def __init__(self, message: str, *, details: dict = None):
|
|
11
|
-
"""Initialize exception with optional details.
|
|
12
|
-
|
|
13
|
-
Args:
|
|
14
|
-
message: Human-readable error message
|
|
15
|
-
details: Optional dictionary with additional error context
|
|
16
|
-
"""
|
|
17
|
-
super().__init__(message)
|
|
18
|
-
self.details = details or {}
|
|
19
|
-
|
|
20
|
-
def __str__(self):
|
|
21
|
-
base_msg = super().__str__()
|
|
22
|
-
if self.details:
|
|
23
|
-
# Skip multi-line values from the compact string representation
|
|
24
|
-
# (they're included separately in detailed error formatting)
|
|
25
|
-
single_line_details = {k: v for k, v in self.details.items() if "\n" not in str(v)}
|
|
26
|
-
if single_line_details:
|
|
27
|
-
details_str = ", ".join(f"{k}={v}" for k, v in single_line_details.items())
|
|
28
|
-
return f"{base_msg} ({details_str})"
|
|
29
|
-
return base_msg
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
class ConfigurationError(BoneAgentError):
|
|
33
|
-
"""Raised when configuration is invalid, missing, or cannot be loaded."""
|
|
34
|
-
pass
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class LLMError(BoneAgentError):
|
|
38
|
-
"""Raised when LLM API communication fails or returns unexpected data."""
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
class LLMConnectionError(LLMError):
|
|
43
|
-
"""Raised when network connection to LLM provider fails."""
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
class LLMResponseError(LLMError):
|
|
48
|
-
"""Raised when LLM response is malformed or invalid."""
|
|
49
|
-
pass
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
class ToolExecutionError(BoneAgentError):
|
|
53
|
-
"""Raised when tool execution fails."""
|
|
54
|
-
pass
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
class CommandExecutionError(ToolExecutionError):
|
|
58
|
-
"""Raised when shell command execution fails."""
|
|
59
|
-
pass
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class FileEditError(ToolExecutionError):
|
|
63
|
-
"""Raised when file edit operation fails."""
|
|
64
|
-
pass
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
class ValidationError(BoneAgentError):
|
|
68
|
-
"""Raised when input validation fails."""
|
|
69
|
-
pass
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
class PathValidationError(ValidationError):
|
|
73
|
-
"""Raised when path validation fails (blocked by gitignore, etc.)."""
|
|
74
|
-
pass
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
class CommandValidationError(ValidationError):
|
|
78
|
-
"""Raised when command validation fails (dangerous operators, etc.)."""
|
|
79
|
-
pass
|
package/src/llm/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""LLM integration layer for bone-agent."""
|
package/src/llm/client.py
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
"""LLM client for making API requests to various providers."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
|
|
5
|
-
import requests
|
|
6
|
-
from llm import config as config_module
|
|
7
|
-
from llm.config import PROVIDER_REGISTRY, get_provider_config, get_providers
|
|
8
|
-
from llm.providers import get_handler
|
|
9
|
-
from exceptions import LLMConnectionError, LLMResponseError, ConfigurationError
|
|
10
|
-
from utils.validation import validate_api_url
|
|
11
|
-
|
|
12
|
-
logger = logging.getLogger(__name__)
|
|
13
|
-
|
|
14
|
-
# Connection/read timeouts (seconds)
|
|
15
|
-
_CONNECT_TIMEOUT = 10
|
|
16
|
-
_READ_TIMEOUT = 120
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
class StreamWrapper:
|
|
20
|
-
"""Wraps streaming response generator with cleanup capability."""
|
|
21
|
-
|
|
22
|
-
def __init__(self, response, generator):
|
|
23
|
-
self._response = response
|
|
24
|
-
self._generator = generator
|
|
25
|
-
|
|
26
|
-
def __iter__(self):
|
|
27
|
-
return self
|
|
28
|
-
|
|
29
|
-
def __next__(self):
|
|
30
|
-
return next(self._generator)
|
|
31
|
-
|
|
32
|
-
def close(self):
|
|
33
|
-
"""Close underlying HTTP connection."""
|
|
34
|
-
if self._response:
|
|
35
|
-
self._response.close()
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class LLMClient:
|
|
39
|
-
def __init__(self, provider=None):
|
|
40
|
-
"""Initialize LLM client.
|
|
41
|
-
|
|
42
|
-
Args:
|
|
43
|
-
provider: Provider name. If None, uses global LLM_PROVIDER from config.
|
|
44
|
-
"""
|
|
45
|
-
self.provider = provider or config_module.LLM_PROVIDER
|
|
46
|
-
self.handler = get_handler(self.provider)
|
|
47
|
-
self.config = self._get_provider_config()
|
|
48
|
-
|
|
49
|
-
@property
|
|
50
|
-
def model(self) -> str:
|
|
51
|
-
"""Return configured model name, if any."""
|
|
52
|
-
return str(self.config.get("payload", {}).get("model") or "")
|
|
53
|
-
|
|
54
|
-
def _get_provider_config(self):
|
|
55
|
-
"""Build provider config from PROVIDER_REGISTRY."""
|
|
56
|
-
registry = get_provider_config(self.provider)
|
|
57
|
-
if not registry:
|
|
58
|
-
raise ConfigurationError(f"Unknown provider: {self.provider}")
|
|
59
|
-
|
|
60
|
-
# Build headers using handler
|
|
61
|
-
headers = self.handler.build_headers(registry)
|
|
62
|
-
|
|
63
|
-
# Build payload with model name
|
|
64
|
-
payload = {}
|
|
65
|
-
model_name = registry.get("api_model") or registry.get("model")
|
|
66
|
-
if model_name:
|
|
67
|
-
payload["model"] = model_name
|
|
68
|
-
|
|
69
|
-
url = f"{registry['api_base']}{registry['endpoint']}"
|
|
70
|
-
valid, err = validate_api_url(url)
|
|
71
|
-
if not valid:
|
|
72
|
-
raise ConfigurationError(
|
|
73
|
-
f"Insecure API endpoint for provider '{self.provider}': {err}"
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
"url": url,
|
|
78
|
-
"headers": headers,
|
|
79
|
-
"payload": payload,
|
|
80
|
-
"error_prefix": registry["error_prefix"],
|
|
81
|
-
"registry": registry
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
def chat_completion(self, messages, stream=True, tools=None):
|
|
85
|
-
"""Make a chat completion request.
|
|
86
|
-
|
|
87
|
-
Args:
|
|
88
|
-
messages: List of message dicts
|
|
89
|
-
stream: Whether to stream the response
|
|
90
|
-
tools: Optional list of tool definitions
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
StreamWrapper if stream=True, else response dict
|
|
94
|
-
"""
|
|
95
|
-
config = self.config
|
|
96
|
-
registry = config["registry"]
|
|
97
|
-
|
|
98
|
-
# Build payload using handler
|
|
99
|
-
payload = self.handler.build_payload(registry, messages, tools, stream)
|
|
100
|
-
|
|
101
|
-
try:
|
|
102
|
-
response = requests.post(
|
|
103
|
-
config["url"],
|
|
104
|
-
headers=config["headers"],
|
|
105
|
-
json=payload,
|
|
106
|
-
stream=stream,
|
|
107
|
-
verify=True,
|
|
108
|
-
timeout=(_CONNECT_TIMEOUT, _READ_TIMEOUT),
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
# For better debugging, include response text on 4xx errors
|
|
112
|
-
if not response.ok:
|
|
113
|
-
error_details = response.text if response.text else str(response.status_code)
|
|
114
|
-
raise LLMConnectionError(
|
|
115
|
-
f"Error communicating with {config['error_prefix']}",
|
|
116
|
-
details={
|
|
117
|
-
"provider": self.provider,
|
|
118
|
-
"original_error": error_details,
|
|
119
|
-
"status_code": response.status_code,
|
|
120
|
-
}
|
|
121
|
-
)
|
|
122
|
-
response.raise_for_status()
|
|
123
|
-
|
|
124
|
-
if stream:
|
|
125
|
-
return StreamWrapper(
|
|
126
|
-
response,
|
|
127
|
-
self.handler.parse_stream(response)
|
|
128
|
-
)
|
|
129
|
-
else:
|
|
130
|
-
try:
|
|
131
|
-
response_json = response.json()
|
|
132
|
-
return self.handler.parse_response(response_json)
|
|
133
|
-
except ValueError:
|
|
134
|
-
if getattr(self.handler, "supports_sse_response_fallback", False):
|
|
135
|
-
return self.handler.parse_sse_response(response.text)
|
|
136
|
-
raise
|
|
137
|
-
|
|
138
|
-
except requests.exceptions.RequestException as e:
|
|
139
|
-
raise LLMConnectionError(
|
|
140
|
-
f"Error communicating with {config['error_prefix']}",
|
|
141
|
-
details={"provider": self.provider, "original_error": str(e)}
|
|
142
|
-
)
|
|
143
|
-
|
|
144
|
-
def switch_provider(self, new_provider):
|
|
145
|
-
"""Switch to a different provider.
|
|
146
|
-
|
|
147
|
-
Args:
|
|
148
|
-
new_provider: Name of the provider to switch to.
|
|
149
|
-
|
|
150
|
-
Returns:
|
|
151
|
-
True if successful, False if provider not found.
|
|
152
|
-
"""
|
|
153
|
-
if new_provider in get_providers():
|
|
154
|
-
self.provider = new_provider
|
|
155
|
-
self.handler = get_handler(new_provider)
|
|
156
|
-
self.config = self._get_provider_config()
|
|
157
|
-
return True
|
|
158
|
-
return False
|
|
159
|
-
|
|
160
|
-
def sync_provider_from_config(self):
|
|
161
|
-
"""Sync this client's provider and config with the current config.
|
|
162
|
-
|
|
163
|
-
This should be called after config is reloaded from disk.
|
|
164
|
-
"""
|
|
165
|
-
current_provider = config_module.LLM_PROVIDER
|
|
166
|
-
if self.provider != current_provider:
|
|
167
|
-
self.provider = current_provider
|
|
168
|
-
self.handler = get_handler(current_provider)
|
|
169
|
-
self.config = self._get_provider_config()
|
|
170
|
-
return True
|
|
171
|
-
# Even if provider hasn't changed, config values (model, api_key) might have
|
|
172
|
-
self.config = self._get_provider_config()
|
|
173
|
-
return False
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
__all__ = ['LLMClient']
|
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
"""Codex provider adapter.
|
|
2
|
-
|
|
3
|
-
Codex is intentionally isolated from the normal provider handlers because it is
|
|
4
|
-
not a Chat Completions-compatible API. It targets the ChatGPT Codex Responses
|
|
5
|
-
backend and adapts that protocol back into vmCode's OpenAI-style internal shape.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import copy
|
|
9
|
-
import hashlib
|
|
10
|
-
import json
|
|
11
|
-
from typing import Any, Dict, Iterator, Optional
|
|
12
|
-
|
|
13
|
-
import requests
|
|
14
|
-
|
|
15
|
-
from exceptions import LLMResponseError
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class CodexResponsesHandler:
|
|
19
|
-
"""Adapter for the ChatGPT Codex Responses backend.
|
|
20
|
-
|
|
21
|
-
Codex-specific behavior kept here:
|
|
22
|
-
- Uses `instructions` + `input` instead of Chat Completions `messages`.
|
|
23
|
-
- Always sends `stream: true`; the backend returns SSE even for logical
|
|
24
|
-
non-streaming agent calls.
|
|
25
|
-
- Stores `_responses_output` replay metadata so tool-call turns can be sent
|
|
26
|
-
back in Responses-native form while using `store: false`.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
supports_sse_response_fallback = True
|
|
30
|
-
|
|
31
|
-
def build_headers(self, config: Dict[str, Any]) -> Dict[str, str]:
|
|
32
|
-
"""Build request headers."""
|
|
33
|
-
headers = {"Content-Type": "application/json"}
|
|
34
|
-
if config.get("type") == "api" and config.get("api_key"):
|
|
35
|
-
headers["Authorization"] = f"Bearer {config['api_key']}"
|
|
36
|
-
if "headers_extra" in config:
|
|
37
|
-
headers.update(config["headers_extra"])
|
|
38
|
-
return headers
|
|
39
|
-
|
|
40
|
-
def build_payload(self, config: Dict[str, Any], messages: list,
|
|
41
|
-
tools: Optional[list] = None, stream: bool = True) -> Dict[str, Any]:
|
|
42
|
-
"""Build request payload for Codex backend Responses API."""
|
|
43
|
-
system_parts = [m["content"] for m in messages if m.get("role") == "system"]
|
|
44
|
-
instructions = "\n".join(system_parts) if system_parts else "You are a helpful assistant."
|
|
45
|
-
|
|
46
|
-
codex_input = []
|
|
47
|
-
for m in messages:
|
|
48
|
-
if m.get("role") == "system":
|
|
49
|
-
continue
|
|
50
|
-
role = m.get("role", "user")
|
|
51
|
-
content = m.get("content", "")
|
|
52
|
-
|
|
53
|
-
if role == "assistant" and m.get("_responses_output"):
|
|
54
|
-
codex_input.extend(m.get("_responses_output") or [])
|
|
55
|
-
continue
|
|
56
|
-
|
|
57
|
-
if role == "assistant" and m.get("tool_calls"):
|
|
58
|
-
if content:
|
|
59
|
-
codex_input.append({
|
|
60
|
-
"role": "assistant",
|
|
61
|
-
"content": [{"type": "input_text", "text": content}]
|
|
62
|
-
})
|
|
63
|
-
for tool_call in m.get("tool_calls", []):
|
|
64
|
-
function = tool_call.get("function", {})
|
|
65
|
-
codex_input.append({
|
|
66
|
-
"type": "function_call",
|
|
67
|
-
"call_id": tool_call.get("id"),
|
|
68
|
-
"name": function.get("name", ""),
|
|
69
|
-
"arguments": function.get("arguments", "{}"),
|
|
70
|
-
})
|
|
71
|
-
continue
|
|
72
|
-
|
|
73
|
-
if role == "tool":
|
|
74
|
-
codex_input.append({
|
|
75
|
-
"type": "function_call_output",
|
|
76
|
-
"call_id": m.get("tool_call_id"),
|
|
77
|
-
"output": content,
|
|
78
|
-
})
|
|
79
|
-
continue
|
|
80
|
-
|
|
81
|
-
content_type = "output_text" if role == "assistant" else "input_text"
|
|
82
|
-
codex_input.append({
|
|
83
|
-
"role": role,
|
|
84
|
-
"content": [{"type": content_type, "text": content}]
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
payload = {
|
|
88
|
-
**config.get("payload", {}),
|
|
89
|
-
"instructions": instructions,
|
|
90
|
-
"input": codex_input,
|
|
91
|
-
"store": False,
|
|
92
|
-
"stream": True,
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if "model" not in payload:
|
|
96
|
-
model_name = config.get("api_model") or config.get("model")
|
|
97
|
-
if model_name:
|
|
98
|
-
payload["model"] = model_name
|
|
99
|
-
|
|
100
|
-
if tools:
|
|
101
|
-
payload["tools"] = [self._convert_tool_to_responses(tool) for tool in tools]
|
|
102
|
-
|
|
103
|
-
if "prompt_cache_key" not in payload:
|
|
104
|
-
model = payload.get("model") or "unknown-model"
|
|
105
|
-
payload["prompt_cache_key"] = self._build_prompt_cache_key(
|
|
106
|
-
model=model,
|
|
107
|
-
instructions=instructions,
|
|
108
|
-
tools=payload.get("tools"),
|
|
109
|
-
)
|
|
110
|
-
|
|
111
|
-
if "temperature" not in payload and config.get("allow_temperature", True):
|
|
112
|
-
payload["temperature"] = config.get("default_temperature", 0.1)
|
|
113
|
-
if "top_p" not in payload and config.get("allow_top_p", True):
|
|
114
|
-
payload["top_p"] = config.get("default_top_p", 0.9)
|
|
115
|
-
|
|
116
|
-
return payload
|
|
117
|
-
|
|
118
|
-
def _build_prompt_cache_key(
|
|
119
|
-
self,
|
|
120
|
-
*,
|
|
121
|
-
model: str,
|
|
122
|
-
instructions: str,
|
|
123
|
-
tools: Optional[list] = None,
|
|
124
|
-
) -> str:
|
|
125
|
-
"""Build a stable prompt-cache key for the reusable Codex prefix."""
|
|
126
|
-
cache_scope = {
|
|
127
|
-
"model": model,
|
|
128
|
-
"instructions": instructions,
|
|
129
|
-
"tools": tools or [],
|
|
130
|
-
}
|
|
131
|
-
canonical = json.dumps(
|
|
132
|
-
cache_scope,
|
|
133
|
-
sort_keys=True,
|
|
134
|
-
separators=(",", ":"),
|
|
135
|
-
ensure_ascii=True,
|
|
136
|
-
)
|
|
137
|
-
cache_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest()[:24]
|
|
138
|
-
return f"bone-agent:{cache_hash}"
|
|
139
|
-
|
|
140
|
-
def parse_response(self, response_json: Dict[str, Any]) -> Dict[str, Any]:
|
|
141
|
-
"""Parse Responses API output into Chat Completions format."""
|
|
142
|
-
return self._normalize_response(response_json)
|
|
143
|
-
|
|
144
|
-
def parse_sse_response(self, response_text: str) -> Dict[str, Any]:
|
|
145
|
-
"""Parse a full SSE response body into Chat Completions format."""
|
|
146
|
-
completed_response = None
|
|
147
|
-
output_items = []
|
|
148
|
-
|
|
149
|
-
for raw_line in response_text.splitlines():
|
|
150
|
-
line = raw_line.strip()
|
|
151
|
-
if not line.startswith("data: "):
|
|
152
|
-
continue
|
|
153
|
-
data_str = line[6:]
|
|
154
|
-
if data_str == "[DONE]":
|
|
155
|
-
break
|
|
156
|
-
try:
|
|
157
|
-
data = json.loads(data_str)
|
|
158
|
-
except json.JSONDecodeError as e:
|
|
159
|
-
raise LLMResponseError(
|
|
160
|
-
"Failed to decode SSE response from Codex backend",
|
|
161
|
-
details={"original_error": str(e)}
|
|
162
|
-
)
|
|
163
|
-
|
|
164
|
-
if data.get("type") == "response.output_item.done":
|
|
165
|
-
item = data.get("item")
|
|
166
|
-
if item:
|
|
167
|
-
output_items.append(item)
|
|
168
|
-
continue
|
|
169
|
-
|
|
170
|
-
if data.get("type") == "response.completed":
|
|
171
|
-
completed_response = data.get("response")
|
|
172
|
-
break
|
|
173
|
-
|
|
174
|
-
if completed_response is None:
|
|
175
|
-
raise LLMResponseError(
|
|
176
|
-
"Codex backend returned streaming data without a completed response event"
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
if not completed_response.get("output") and output_items:
|
|
180
|
-
completed_response = dict(completed_response)
|
|
181
|
-
completed_response["output"] = output_items
|
|
182
|
-
|
|
183
|
-
return self._normalize_response(completed_response)
|
|
184
|
-
|
|
185
|
-
def parse_stream(self, response: requests.Response) -> Iterator[Dict[str, Any]]:
|
|
186
|
-
"""Parse streaming Responses API."""
|
|
187
|
-
usage_data = None
|
|
188
|
-
|
|
189
|
-
for line in response.iter_lines():
|
|
190
|
-
if line:
|
|
191
|
-
line = line.decode('utf-8')
|
|
192
|
-
|
|
193
|
-
if line.startswith('data: '):
|
|
194
|
-
data_str = line[6:]
|
|
195
|
-
if data_str.strip() == '[DONE]':
|
|
196
|
-
break
|
|
197
|
-
|
|
198
|
-
try:
|
|
199
|
-
data = json.loads(data_str)
|
|
200
|
-
|
|
201
|
-
if 'error' in data:
|
|
202
|
-
error_msg = data.get('error', {}).get('message', 'Unknown streaming error')
|
|
203
|
-
raise LLMResponseError(
|
|
204
|
-
f"Streaming error: {error_msg}",
|
|
205
|
-
details={"error_data": data.get('error')}
|
|
206
|
-
)
|
|
207
|
-
|
|
208
|
-
event_type = data.get("type", "")
|
|
209
|
-
|
|
210
|
-
if event_type == "response.completed":
|
|
211
|
-
resp = data.get("response", {})
|
|
212
|
-
if "usage" in resp:
|
|
213
|
-
usage_data = self._normalize_usage(resp["usage"])
|
|
214
|
-
|
|
215
|
-
if event_type == "response.output_text.delta":
|
|
216
|
-
delta = data.get("delta", "")
|
|
217
|
-
if delta:
|
|
218
|
-
yield delta
|
|
219
|
-
|
|
220
|
-
except json.JSONDecodeError as e:
|
|
221
|
-
raise LLMResponseError(
|
|
222
|
-
f"Failed to decode streaming response",
|
|
223
|
-
details={"original_error": str(e)}
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
if usage_data:
|
|
227
|
-
yield {'__usage__': usage_data}
|
|
228
|
-
|
|
229
|
-
def _convert_tool_to_responses(self, tool: Dict[str, Any]) -> Dict[str, Any]:
|
|
230
|
-
"""Convert Chat Completions tool schema to Responses/Codex schema."""
|
|
231
|
-
if tool.get("type") == "function" and "function" in tool:
|
|
232
|
-
function = tool["function"]
|
|
233
|
-
return {
|
|
234
|
-
"type": "function",
|
|
235
|
-
"name": function.get("name", ""),
|
|
236
|
-
"description": function.get("description", ""),
|
|
237
|
-
"parameters": self._normalize_json_schema(function.get("parameters", {})),
|
|
238
|
-
"strict": False,
|
|
239
|
-
}
|
|
240
|
-
return tool
|
|
241
|
-
|
|
242
|
-
def _normalize_response(self, response_json: Dict[str, Any]) -> Dict[str, Any]:
|
|
243
|
-
"""Normalize Responses output into Chat Completions message shape."""
|
|
244
|
-
raw_usage = response_json.get("usage", {})
|
|
245
|
-
usage = self._normalize_usage(raw_usage)
|
|
246
|
-
|
|
247
|
-
output_items = response_json.get("output", [])
|
|
248
|
-
content_parts = []
|
|
249
|
-
tool_calls = []
|
|
250
|
-
|
|
251
|
-
for item in output_items:
|
|
252
|
-
item_type = item.get("type")
|
|
253
|
-
|
|
254
|
-
if item_type == "function_call":
|
|
255
|
-
call_id = item.get("call_id") or item.get("id")
|
|
256
|
-
tool_calls.append({
|
|
257
|
-
"id": call_id,
|
|
258
|
-
"type": "function",
|
|
259
|
-
"function": {
|
|
260
|
-
"name": item.get("name", ""),
|
|
261
|
-
"arguments": item.get("arguments", "{}"),
|
|
262
|
-
}
|
|
263
|
-
})
|
|
264
|
-
continue
|
|
265
|
-
|
|
266
|
-
if item_type != "message":
|
|
267
|
-
continue
|
|
268
|
-
|
|
269
|
-
for c in item.get("content", []):
|
|
270
|
-
if c.get("type") in {"output_text", "text"}:
|
|
271
|
-
text = c.get("text")
|
|
272
|
-
if text is not None:
|
|
273
|
-
content_parts.append(text)
|
|
274
|
-
|
|
275
|
-
message = {"role": "assistant"}
|
|
276
|
-
text_content = "\n".join(content_parts) if content_parts else ""
|
|
277
|
-
if tool_calls:
|
|
278
|
-
message["tool_calls"] = tool_calls
|
|
279
|
-
message["content"] = text_content or None
|
|
280
|
-
else:
|
|
281
|
-
message["content"] = text_content
|
|
282
|
-
|
|
283
|
-
replay_items = copy.deepcopy(output_items)
|
|
284
|
-
for item in replay_items:
|
|
285
|
-
item.pop("id", None)
|
|
286
|
-
message["_responses_output"] = replay_items
|
|
287
|
-
|
|
288
|
-
return {
|
|
289
|
-
"choices": [{
|
|
290
|
-
"message": message,
|
|
291
|
-
"finish_reason": "tool_calls" if tool_calls else "stop",
|
|
292
|
-
}],
|
|
293
|
-
"usage": usage,
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
def _normalize_usage(self, usage: Any) -> Dict[str, Any]:
|
|
297
|
-
"""Normalize Codex Responses usage into vmCode's OpenAI-style usage shape."""
|
|
298
|
-
if not isinstance(usage, dict):
|
|
299
|
-
return {}
|
|
300
|
-
|
|
301
|
-
normalized = dict(usage)
|
|
302
|
-
|
|
303
|
-
input_tokens = normalized.get("input_tokens")
|
|
304
|
-
output_tokens = normalized.get("output_tokens")
|
|
305
|
-
|
|
306
|
-
if normalized.get("prompt_tokens") is None and input_tokens is not None:
|
|
307
|
-
normalized["prompt_tokens"] = input_tokens
|
|
308
|
-
if normalized.get("completion_tokens") is None and output_tokens is not None:
|
|
309
|
-
normalized["completion_tokens"] = output_tokens
|
|
310
|
-
if normalized.get("total_tokens") is None:
|
|
311
|
-
prompt_tokens = normalized.get("prompt_tokens")
|
|
312
|
-
completion_tokens = normalized.get("completion_tokens")
|
|
313
|
-
if prompt_tokens is not None and completion_tokens is not None:
|
|
314
|
-
normalized["total_tokens"] = prompt_tokens + completion_tokens
|
|
315
|
-
|
|
316
|
-
input_details = normalized.get("input_tokens_details")
|
|
317
|
-
if isinstance(input_details, dict) and input_details.get("cached_tokens") is not None:
|
|
318
|
-
cached_tokens = input_details["cached_tokens"]
|
|
319
|
-
if normalized.get("prompt_tokens_details") is None:
|
|
320
|
-
normalized["prompt_tokens_details"] = {"cached_tokens": cached_tokens}
|
|
321
|
-
elif isinstance(normalized["prompt_tokens_details"], dict):
|
|
322
|
-
normalized["prompt_tokens_details"].setdefault("cached_tokens", cached_tokens)
|
|
323
|
-
normalized.setdefault("cached_tokens", cached_tokens)
|
|
324
|
-
|
|
325
|
-
return normalized
|
|
326
|
-
|
|
327
|
-
def _normalize_json_schema(self, schema: Any) -> Any:
|
|
328
|
-
"""Normalize JSON Schema for strict Responses function tools."""
|
|
329
|
-
if not isinstance(schema, dict):
|
|
330
|
-
return schema
|
|
331
|
-
|
|
332
|
-
normalized = dict(schema)
|
|
333
|
-
schema_type = normalized.get("type")
|
|
334
|
-
|
|
335
|
-
if schema_type == "object":
|
|
336
|
-
properties = normalized.get("properties", {})
|
|
337
|
-
normalized["properties"] = {
|
|
338
|
-
key: self._normalize_json_schema(value)
|
|
339
|
-
for key, value in properties.items()
|
|
340
|
-
}
|
|
341
|
-
normalized.setdefault("additionalProperties", False)
|
|
342
|
-
|
|
343
|
-
if schema_type == "array" and "items" in normalized:
|
|
344
|
-
normalized["items"] = self._normalize_json_schema(normalized["items"])
|
|
345
|
-
|
|
346
|
-
for key in ("anyOf", "oneOf", "allOf"):
|
|
347
|
-
if key in normalized and isinstance(normalized[key], list):
|
|
348
|
-
normalized[key] = [self._normalize_json_schema(item) for item in normalized[key]]
|
|
349
|
-
|
|
350
|
-
return normalized
|