* docs(code-execution): document HERMES_* env narrowing + passthrough workaround
The execute_code sandbox-child env scrub (108397726, #27303) deliberately
dropped the broad HERMES_ prefix passthrough, keeping only an operational
4-var allowlist (HERMES_HOME/PROFILE/CONFIG/ENV). A script that relied on a
non-secret HERMES_* var (HERMES_BASE_URL, HERMES_KANBAN_DB, HERMES_*_WEBHOOK,
or a plugin-defined one) now sees it unset in the child.
Document the behavior change and the two recovery routes (terminal.env_passthrough
in config.yaml, or required_environment_variables in skill frontmatter), plus
the debug log line that surfaces the drop for diagnosis.
* fix(api_server): emit per-turn transcript on run.completed (#34703)
WebUI clients lost intermediate (pre-tool-call) assistant text after
switching session pages mid-stream. The session-chat SSE stream delivers
all assistant text as assistant.delta events under one message_id
interleaved with tool.* events, then a single assistant.completed
carrying only the final reply — so a client accumulating deltas into one
buffer cannot reconstruct intermediate text segments that preceded tool
calls, and they vanish from the live view (state.db persists them
correctly).
run.completed now carries the authoritative per-turn transcript
(assistant + tool messages for this turn, in client-safe shape) so any
SSE consumer can reconcile its live view against ground truth without a
separate GET /messages round-trip. Purely additive — clients that ignore
the field are unaffected.
This commit is contained in:
@@ -1605,6 +1605,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
)
|
||||
final_response = result.get("final_response", "") if isinstance(result, dict) else ""
|
||||
effective_session_id = result.get("session_id", session_id) if isinstance(result, dict) else session_id
|
||||
turn_messages = self._turn_transcript_messages(history, user_message, result) if isinstance(result, dict) else []
|
||||
await queue.put(_event_payload("assistant.completed", {
|
||||
"session_id": effective_session_id,
|
||||
"message_id": message_id,
|
||||
@@ -1617,6 +1618,7 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
"session_id": effective_session_id,
|
||||
"message_id": message_id,
|
||||
"completed": True,
|
||||
"messages": turn_messages,
|
||||
"usage": usage,
|
||||
}))
|
||||
except Exception as exc:
|
||||
@@ -3329,6 +3331,44 @@ class APIServerAdapter(BasePlatformAdapter):
|
||||
return len(prior)
|
||||
return 0
|
||||
|
||||
@classmethod
|
||||
def _turn_transcript_messages(
|
||||
cls,
|
||||
conversation_history: List[Dict[str, Any]],
|
||||
user_message: Any,
|
||||
result: Dict[str, Any],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""Return this turn's assistant/tool messages in client-safe shape.
|
||||
|
||||
The streaming SSE contract delivers all assistant text as
|
||||
``assistant.delta`` events under one ``message_id`` interleaved with
|
||||
``tool.*`` events, and a single ``assistant.completed`` carrying only
|
||||
the final reply. A client that accumulates deltas into one buffer
|
||||
cannot reconstruct *intermediate* assistant text segments that preceded
|
||||
tool calls — so when the page is re-opened mid/post-stream those
|
||||
segments appear lost, even though state.db persisted them correctly.
|
||||
|
||||
Emitting the authoritative per-turn transcript on ``run.completed`` lets
|
||||
any SSE consumer reconcile its live view against ground truth without a
|
||||
separate ``GET /messages`` round-trip. Purely additive: clients that
|
||||
ignore the field are unaffected. Refs #34703.
|
||||
"""
|
||||
agent_messages = result.get("messages") if isinstance(result, dict) else None
|
||||
if not isinstance(agent_messages, list) or not agent_messages:
|
||||
return []
|
||||
start = cls._response_messages_turn_start_index(
|
||||
conversation_history, user_message, result
|
||||
)
|
||||
turn = agent_messages[start:]
|
||||
out: List[Dict[str, Any]] = []
|
||||
for msg in turn:
|
||||
if not isinstance(msg, dict):
|
||||
continue
|
||||
if msg.get("role") not in {"assistant", "tool"}:
|
||||
continue
|
||||
out.append(cls._message_response(msg))
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _extract_output_items(result: Dict[str, Any], start_index: int = 0) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user