mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-03 01:40:21 +08:00
fix(kiro): 修复工具schema兼容性、消息交替和重复内容问题
- 递归清理工具schema中Kiro不支持的additionalProperties和空required字段 - 将thinking prefix注入从history移到currentMessage,仅作用于当前轮次 - 修复连续assistant消息缺少user消息导致角色不交替的问题 - 增加流式content事件去重,跳过Kiro发送的重复内容
This commit is contained in:
@@ -189,6 +189,29 @@ def _process_message_content(
|
|||||||
return "".join(text_parts), images, tool_results
|
return "".join(text_parts), images, tool_results
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_tool_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Recursively remove fields that Kiro API rejects.
|
||||||
|
|
||||||
|
Kiro returns 400 "Improperly formed request" when tool schemas contain
|
||||||
|
``additionalProperties`` (any value) or empty ``required: []`` arrays.
|
||||||
|
"""
|
||||||
|
if not isinstance(schema, dict):
|
||||||
|
return schema # type: ignore[return-value]
|
||||||
|
result: dict[str, Any] = {}
|
||||||
|
for key, value in schema.items():
|
||||||
|
if key == "additionalProperties":
|
||||||
|
continue
|
||||||
|
if key == "required" and isinstance(value, list) and not value:
|
||||||
|
continue
|
||||||
|
if isinstance(value, dict):
|
||||||
|
result[key] = _clean_tool_schema(value)
|
||||||
|
elif isinstance(value, list):
|
||||||
|
result[key] = [_clean_tool_schema(v) if isinstance(v, dict) else v for v in value]
|
||||||
|
else:
|
||||||
|
result[key] = value
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _convert_tools(tools: Any) -> list[dict[str, Any]]:
|
def _convert_tools(tools: Any) -> list[dict[str, Any]]:
|
||||||
if not isinstance(tools, list):
|
if not isinstance(tools, list):
|
||||||
return []
|
return []
|
||||||
@@ -216,6 +239,8 @@ def _convert_tools(tools: Any) -> list[dict[str, Any]]:
|
|||||||
if not isinstance(input_schema, dict):
|
if not isinstance(input_schema, dict):
|
||||||
input_schema = {}
|
input_schema = {}
|
||||||
|
|
||||||
|
input_schema = _clean_tool_schema(input_schema)
|
||||||
|
|
||||||
out.append(
|
out.append(
|
||||||
{
|
{
|
||||||
"toolSpecification": {
|
"toolSpecification": {
|
||||||
@@ -236,11 +261,8 @@ def _create_placeholder_tool(name: str) -> dict[str, Any]:
|
|||||||
"description": "Tool used in conversation history",
|
"description": "Tool used in conversation history",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"json": {
|
"json": {
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
"required": [],
|
|
||||||
"additionalProperties": True,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -348,8 +370,6 @@ def convert_claude_messages_to_conversation_state(
|
|||||||
if system_text:
|
if system_text:
|
||||||
# Append chunked-write policy so the model silently obeys tool limits.
|
# Append chunked-write policy so the model silently obeys tool limits.
|
||||||
final_system = f"{system_text}\n{_SYSTEM_CHUNKED_POLICY}"
|
final_system = f"{system_text}\n{_SYSTEM_CHUNKED_POLICY}"
|
||||||
if thinking_prefix and not _has_thinking_tags(system_text):
|
|
||||||
final_system = f"{thinking_prefix}\n{final_system}"
|
|
||||||
history.append(
|
history.append(
|
||||||
{
|
{
|
||||||
"userInputMessage": {
|
"userInputMessage": {
|
||||||
@@ -362,19 +382,6 @@ def convert_claude_messages_to_conversation_state(
|
|||||||
history.append(
|
history.append(
|
||||||
{"assistantResponseMessage": {"content": "I will follow these instructions."}}
|
{"assistantResponseMessage": {"content": "I will follow these instructions."}}
|
||||||
)
|
)
|
||||||
elif thinking_prefix:
|
|
||||||
history.append(
|
|
||||||
{
|
|
||||||
"userInputMessage": {
|
|
||||||
"content": thinking_prefix,
|
|
||||||
"modelId": model_id,
|
|
||||||
"origin": "AI_EDITOR",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
history.append(
|
|
||||||
{"assistantResponseMessage": {"content": "I will follow these instructions."}}
|
|
||||||
)
|
|
||||||
|
|
||||||
# Build history from messages.
|
# Build history from messages.
|
||||||
# If the last message is assistant, include it in history (Kiro currentMessage
|
# If the last message is assistant, include it in history (Kiro currentMessage
|
||||||
@@ -437,9 +444,22 @@ def convert_claude_messages_to_conversation_state(
|
|||||||
user_item = _flush_user_buffer()
|
user_item = _flush_user_buffer()
|
||||||
if user_item is not None:
|
if user_item is not None:
|
||||||
history.append(user_item)
|
history.append(user_item)
|
||||||
assistant_item = _convert_assistant_message(msg)
|
elif not history or "assistantResponseMessage" in history[-1]:
|
||||||
if assistant_item is not None:
|
# No preceding user message: insert synthetic user message
|
||||||
history.append({"assistantResponseMessage": assistant_item})
|
# to maintain alternating roles required by Kiro API.
|
||||||
|
history.append(
|
||||||
|
{
|
||||||
|
"userInputMessage": {
|
||||||
|
"content": "Continue.",
|
||||||
|
"modelId": model_id,
|
||||||
|
"origin": "AI_EDITOR",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
assistant_item = _convert_assistant_message(msg)
|
||||||
|
if assistant_item is not None:
|
||||||
|
history.append({"assistantResponseMessage": assistant_item})
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# trailing unpaired user messages in history
|
# trailing unpaired user messages in history
|
||||||
@@ -561,6 +581,11 @@ def convert_claude_messages_to_conversation_state(
|
|||||||
if validated_tool_results:
|
if validated_tool_results:
|
||||||
user_ctx["toolResults"] = validated_tool_results
|
user_ctx["toolResults"] = validated_tool_results
|
||||||
|
|
||||||
|
# Inject thinking tags into currentMessage (not history) so the
|
||||||
|
# instruction applies to the current turn only.
|
||||||
|
if thinking_prefix and not _has_thinking_tags(text_content):
|
||||||
|
text_content = f"{thinking_prefix}\n{text_content}"
|
||||||
|
|
||||||
user_input: dict[str, Any] = {
|
user_input: dict[str, Any] = {
|
||||||
"userInputMessageContext": user_ctx,
|
"userInputMessageContext": user_ctx,
|
||||||
"content": text_content,
|
"content": text_content,
|
||||||
|
|||||||
@@ -142,6 +142,7 @@ class _KiroStreamState:
|
|||||||
has_tool_use: bool = False
|
has_tool_use: bool = False
|
||||||
stop_reason_override: str | None = None
|
stop_reason_override: str | None = None
|
||||||
had_error: bool = False
|
had_error: bool = False
|
||||||
|
_last_content: str = ""
|
||||||
|
|
||||||
def generate_initial_events(self) -> list[dict[str, Any]]:
|
def generate_initial_events(self) -> list[dict[str, Any]]:
|
||||||
events: list[dict[str, Any]] = []
|
events: list[dict[str, Any]] = []
|
||||||
@@ -287,6 +288,11 @@ class _KiroStreamState:
|
|||||||
if not content:
|
if not content:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
# Kiro may send duplicate content events; skip exact repeats.
|
||||||
|
if content == self._last_content:
|
||||||
|
return []
|
||||||
|
self._last_content = content
|
||||||
|
|
||||||
self.output_tokens += _estimate_tokens(content)
|
self.output_tokens += _estimate_tokens(content)
|
||||||
|
|
||||||
if not self.thinking_enabled:
|
if not self.thinking_enabled:
|
||||||
|
|||||||
Reference in New Issue
Block a user