fix: session resume, config_set schema, metrics chart, subagent logging
- Fix session resume: remove messages.length>1 guard so sessionStorage
ID is used as resumeSessionId on first message (prevents new session
creation when user sends "继续")
- Fix config_set tool schema: add missing `items: {}` to array type in
oneOf (Azure OpenAI strict validation rejects it otherwise)
- Metrics chart: use actual target set filename from trigger text
instead of generic "target_subset" key
- Add sub-agent skill call logging to agent_runtime.py (full
prompt/response, no truncation)
- Various metric enrichment and dedup fixes in server.py
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -154,6 +154,119 @@ def _prepend_runtime_context(prompt: str, runtime_context: str | None) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _log_child_skill_calls(
|
||||
child_result: 'AgentRunResult',
|
||||
child_agent: 'LocalCodingAgent',
|
||||
subtask_label: str,
|
||||
) -> None:
|
||||
"""Extract Skill tool calls from a child agent's transcript and write to a log file.
|
||||
|
||||
Logs each Skill invocation with the args sent and the agent's response,
|
||||
so we can audit whether sub-agents actually called skills like label-master.
|
||||
"""
|
||||
from .agent_types import AgentRunResult # noqa: F811
|
||||
|
||||
transcript = child_result.transcript
|
||||
if not transcript:
|
||||
return
|
||||
log_dir = child_agent.runtime_config.session_directory / '_subagent_logs'
|
||||
try:
|
||||
log_dir.mkdir(parents=True, exist_ok=True)
|
||||
except OSError:
|
||||
return
|
||||
session_id = child_result.session_id or 'unknown'
|
||||
log_path = log_dir / f'{session_id}.jsonl'
|
||||
|
||||
skill_calls: list[dict[str, object]] = []
|
||||
# Build a map of tool_call_id -> skill args from assistant messages with tool_calls
|
||||
pending_skills: dict[str, dict[str, str]] = {}
|
||||
for entry in transcript:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
role = entry.get('role')
|
||||
if role == 'assistant':
|
||||
tool_calls = entry.get('tool_calls')
|
||||
if isinstance(tool_calls, list):
|
||||
for tc in tool_calls:
|
||||
if not isinstance(tc, dict):
|
||||
continue
|
||||
fn = tc.get('function') or {}
|
||||
if not isinstance(fn, dict):
|
||||
continue
|
||||
if fn.get('name') == 'Skill':
|
||||
tc_id = tc.get('id', '')
|
||||
raw_args = fn.get('arguments', '{}')
|
||||
try:
|
||||
args_dict = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
args_dict = {'raw': raw_args}
|
||||
pending_skills[tc_id] = {
|
||||
'skill': args_dict.get('skill', ''),
|
||||
'args': args_dict.get('args', ''),
|
||||
}
|
||||
elif role == 'tool':
|
||||
tc_id = entry.get('tool_call_id', '')
|
||||
if tc_id in pending_skills:
|
||||
skill_info = pending_skills.pop(tc_id)
|
||||
skill_calls.append({
|
||||
'skill': skill_info['skill'],
|
||||
'args': skill_info['args'],
|
||||
'prompt_injected': entry.get('content') or '',
|
||||
'ts': datetime.now(timezone.utc).isoformat(),
|
||||
})
|
||||
|
||||
# Also capture the assistant responses that follow skill injections
|
||||
# by looking at assistant messages after tool messages
|
||||
assistant_after_skill: list[str] = []
|
||||
saw_skill_tool = False
|
||||
for entry in transcript:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
role = entry.get('role')
|
||||
if role == 'tool' and entry.get('tool_call_id', '') in [
|
||||
tc.get('id', '') for tc in _all_skill_tool_call_ids(transcript)
|
||||
]:
|
||||
saw_skill_tool = True
|
||||
elif role == 'assistant' and saw_skill_tool:
|
||||
content = entry.get('content', '')
|
||||
if content:
|
||||
assistant_after_skill.append(content)
|
||||
saw_skill_tool = False
|
||||
|
||||
# Merge assistant responses into skill_calls
|
||||
for i, response_text in enumerate(assistant_after_skill):
|
||||
if i < len(skill_calls):
|
||||
skill_calls[i]['agent_response'] = response_text
|
||||
|
||||
if not skill_calls:
|
||||
return
|
||||
try:
|
||||
with open(log_path, 'a', encoding='utf-8') as f:
|
||||
for call in skill_calls:
|
||||
call['subtask_label'] = subtask_label
|
||||
f.write(json.dumps(call, ensure_ascii=False) + '\n')
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def _all_skill_tool_call_ids(transcript: tuple[dict[str, object], ...]) -> list[dict[str, str]]:
|
||||
"""Collect all tool_call entries for Skill from assistant messages."""
|
||||
results = []
|
||||
for entry in transcript:
|
||||
if not isinstance(entry, dict) or entry.get('role') != 'assistant':
|
||||
continue
|
||||
tool_calls = entry.get('tool_calls')
|
||||
if not isinstance(tool_calls, list):
|
||||
continue
|
||||
for tc in tool_calls:
|
||||
if not isinstance(tc, dict):
|
||||
continue
|
||||
fn = tc.get('function') or {}
|
||||
if isinstance(fn, dict) and fn.get('name') == 'Skill':
|
||||
results.append(tc)
|
||||
return results
|
||||
|
||||
|
||||
@dataclass
|
||||
class LocalCodingAgent:
|
||||
model_config: ModelConfig
|
||||
@@ -3052,6 +3165,11 @@ class LocalCodingAgent:
|
||||
managed_child_index=index,
|
||||
managed_label=subtask_label,
|
||||
)
|
||||
if self.tool_context.jupyter_runtime is not None:
|
||||
child_agent.tool_context = replace(
|
||||
child_agent.tool_context,
|
||||
jupyter_runtime=self.tool_context.jupyter_runtime,
|
||||
)
|
||||
if group_id is not None and child_agent.managed_agent_id is not None:
|
||||
self.agent_manager.register_group_child(
|
||||
group_id,
|
||||
@@ -3118,9 +3236,11 @@ class LocalCodingAgent:
|
||||
break
|
||||
continue
|
||||
child_result = child_agent.resume(child_prompt, stored_child_session)
|
||||
_log_child_skill_calls(child_result, child_agent, subtask_label)
|
||||
resume_used = True
|
||||
else:
|
||||
child_result = child_agent.run(child_prompt)
|
||||
_log_child_skill_calls(child_result, child_agent, subtask_label)
|
||||
if group_id is not None and child_agent.managed_agent_id is not None:
|
||||
self.agent_manager.register_group_child(
|
||||
group_id,
|
||||
|
||||
+1
-1
@@ -340,7 +340,7 @@ def default_tool_registry() -> dict[str, AgentTool]:
|
||||
{'type': 'number'},
|
||||
{'type': 'integer'},
|
||||
{'type': 'boolean'},
|
||||
{'type': 'array'},
|
||||
{'type': 'array', 'items': {}},
|
||||
{'type': 'object'},
|
||||
{'type': 'null'},
|
||||
]
|
||||
|
||||
@@ -268,10 +268,12 @@ class JupyterRuntimeSession:
|
||||
'远端 jupyter pod 未挂载 /mnt/wangsenhao 或不可写:'
|
||||
+ (probe.stdout.strip() or probe.stderr.strip() or 'unknown error')
|
||||
)
|
||||
ws_output = f'{shlex.quote(self.binding.workspace_cwd)}/output'
|
||||
chat_output_link = f'{shlex.quote(chat_root)}/output'
|
||||
result = self.run_command(
|
||||
(
|
||||
'mkdir -p '
|
||||
f'{shlex.quote(self.binding.workspace_cwd)}/output '
|
||||
f'{ws_output} '
|
||||
f'{shlex.quote(self.binding.workspace_cwd)}/input '
|
||||
f'{shlex.quote(self.binding.workspace_cwd)}/scratchpad '
|
||||
f'{shlex.quote(self.binding.workspace_root)}/.runtime/uploads '
|
||||
@@ -279,7 +281,12 @@ class JupyterRuntimeSession:
|
||||
f'{shlex.quote(self.account_runtime_root)}/python '
|
||||
f'{shlex.quote(chat_root)}/results '
|
||||
f'{shlex.quote(chat_root)}/sft_output '
|
||||
f'{shlex.quote(chat_root)}/scripts'
|
||||
f'{shlex.quote(chat_root)}/scripts && '
|
||||
f'if [ -d {chat_output_link} ] && [ ! -L {chat_output_link} ]; then '
|
||||
f'cp -a {chat_output_link}/* {ws_output}/ 2>/dev/null; '
|
||||
f'rm -rf {chat_output_link}; '
|
||||
f'fi && '
|
||||
f'ln -sfnT {ws_output} {chat_output_link}'
|
||||
),
|
||||
timeout_seconds=timeout_seconds,
|
||||
max_output_chars=4000,
|
||||
|
||||
Reference in New Issue
Block a user