Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
7388fb7
feat(engine,harness,render): multi-action support + action runtime fixes
ravenSanstete Jun 26, 2026
5452e23
feat: expose reasoning_content from API response to TUI
ravenSanstete Jun 27, 2026
5a2f8f0
fix: show model content text alongside tool_calls in TUI
ravenSanstete Jun 27, 2026
707979f
feat(tui): display task instruction at run start
ravenSanstete Jun 27, 2026
6379f25
fix: parse :param descriptions from docstrings into tool schema
ravenSanstete Jun 27, 2026
af3cc55
feat(tui): render CallChain gates and task memory in TUI
ravenSanstete Jun 27, 2026
17024e5
feat(tui): constraint board + task memory render with LLM-identical text
ravenSanstete Jun 27, 2026
11a8f8a
fix(tui): pass live state object to _state_stats for constraint board
ravenSanstete Jun 27, 2026
c47ab75
feat(cybergym): sync latest agent with vul-only partial-hit refinemen…
ravenSanstete Jun 28, 2026
da0aa73
feat(cybergym): sync latest agent with constraint discovery accelerat…
ravenSanstete Jun 29, 2026
85a8099
feat: qita CLI updates, workspace tool improvements, docs refresh
ravenSanstete Jun 29, 2026
71bb4e1
fix(render): deduplicate thinking text when reasoning_content equals …
ravenSanstete Jun 29, 2026
d2ee976
feat: per-task TUI log file for offline trajectory analysis
ravenSanstete Jun 29, 2026
7ac49ed
chore(cybergym): sync vendored agent to cybergym_agent@6dff2cc
Xulichenpro Jun 30, 2026
84d56b2
feat(cybergym): optional per-task Docker env + fix DockerEnv abs-path…
Xulichenpro Jun 30, 2026
c1c445f
chore(cybergym): sync vendored agent to cybergym_agent@4b4fc88 (oracl…
Xulichenpro Jun 30, 2026
64207ef
fix: add __init__.py to repo root to prevent namespace package shadowing
ravenSanstete Jul 1, 2026
d4c816e
Revert "fix: add __init__.py to repo root to prevent namespace packag…
ravenSanstete Jul 1, 2026
0e5df2b
sync: cybergym agent V11 — active sink discovery optimizations
ravenSanstete Jul 2, 2026
147468f
diag: add traceback.print_exc() to engine exception handlers
ravenSanstete Jul 2, 2026
d9596e4
fix: add file-based error logging for unrecoverable_error debugging
ravenSanstete Jul 2, 2026
39f423f
fix: surface recovered runtime exceptions
ravenSanstete Jul 2, 2026
70239a0
fix: render Sink Candidates + Objective in TUI alongside Constraint B…
ravenSanstete Jul 2, 2026
b673948
feat: TUI completeness — phase cache, task context, allowed tools ren…
ravenSanstete Jul 2, 2026
8400678
feat: TUI rendering for suggested sinks + auto-discovery tags
ravenSanstete Jul 2, 2026
c01587e
fix: remove truncation of tool call results in TUI output
ravenSanstete Jul 2, 2026
8204310
sync: agent v19 with no Point access + vuln patterns + auto-discovery
ravenSanstete Jul 2, 2026
ecd817d
DockerEnv: add container_env param + runner passes CYBERGYM vars to c…
ravenSanstete Jul 5, 2026
3cbbedb
v14: TUI completeness, runtime contract fixes, GDB tool improvements
ravenSanstete Jul 6, 2026
c5b80ee
Sync cybergym_agent v15: knowledge packs, observation contract, dynam…
ravenSanstete Jul 6, 2026
8578790
Sync cybergym_agent v16: remove run_candidate, fix gdb=false, sanitiz…
ravenSanstete Jul 7, 2026
5e013e9
Add x-inspire-inference-key sticky routing for LLM requests
ravenSanstete Jul 7, 2026
d16e775
Add inference_key sticky routing for CyberGym LLM requests
ravenSanstete Jul 8, 2026
e37540f
Fix task_id display and Action rendering in TUI
ravenSanstete Jul 8, 2026
42a91ff
Sync cybergym_agent: knowledge packs, observation, recipe IR, bug fixes
ravenSanstete Jul 8, 2026
a02e96c
Sync cybergym_agent v19-next: submit_tool fix, pack taxonomy, observa…
ravenSanstete Jul 8, 2026
c6e8fc0
Sync cybergym_agent v19-nb: Constraint Board sink_id fix, remove obso…
ravenSanstete Jul 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 89 additions & 33 deletions qitos/engine/_action_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,11 @@ def run_act(
if handoff is not None:
return handoff
actions.append(normalized)
for normalized_action in actions:
# Pre-flight checks: collect blocked/loop-blocked actions, execute the rest
blocked_indices: set[int] = set()
blocked_results: List[tuple[int, ToolResult]] = []
blocked_invocations: List[tuple[int, Dict[str, Any]]] = []
for i, normalized_action in enumerate(actions):
engine._memory_append("action", normalized_action, record.step_id)
block_reason = self._action_block_reason(state, normalized_action)
if block_reason:
Expand All @@ -81,23 +85,22 @@ def run_act(
"error_category": "action_blocked",
},
)
record.action_results = [blocked_result]
record.tool_invocations = [
{
"tool_name": normalized_action.name,
"toolset_name": None,
"toolset_version": None,
"source": "agent_action_gate",
"attempts": 0,
"latency_ms": 0,
"status": "error",
"error_category": "action_blocked",
"error": "action_blocked",
}
]
blocked_indices.add(i)
blocked_results.append((i, blocked_result))
blocked_invocations.append((i, {
"tool_name": normalized_action.name,
"toolset_name": None,
"toolset_version": None,
"source": "agent_action_gate",
"attempts": 0,
"latency_ms": 0,
"status": "error",
"error_category": "action_blocked",
"error": "action_blocked",
}))
engine._memory_append("action_result", blocked_result, record.step_id)
if record.decision_source == "native_tool_calls" and record.native_tool_call_used:
tool_call_id = normalized_action.action_id or f"call_{record.step_id}_0"
tool_call_id = normalized_action.action_id or f"call_{record.step_id}_{i}"
engine._history_append(
"tool",
self._serialize_for_tool_message(
Expand Down Expand Up @@ -137,18 +140,7 @@ def run_act(
],
},
)
engine._dispatch_hook(
"on_after_act",
engine._hook_context(
step_id=record.step_id,
phase=RuntimePhase.ACT,
state=state,
decision=decision,
action_results=[blocked_result.to_dict()],
record=record,
),
)
return [blocked_result.to_dict()]
continue
loop_result = engine._tool_loop_detector.check_detailed(
normalized_action.name, normalized_action.args
)
Expand All @@ -162,7 +154,19 @@ def run_act(
"reason": loop_result.message,
},
)
record.action_results = [loop_tool_result]
blocked_indices.add(i)
blocked_results.append((i, loop_tool_result))
blocked_invocations.append((i, {
"tool_name": normalized_action.name,
"toolset_name": None,
"toolset_version": None,
"source": "loop_detector",
"attempts": 0,
"latency_ms": 0,
"status": "error",
"error_category": "tool_call_loop_detected",
"error": "tool_call_loop_detected",
}))
engine._history_append(
"user",
loop_result.message,
Expand All @@ -178,7 +182,7 @@ def run_act(
"recovery_message": loop_result.message,
},
)
return [loop_tool_result.to_dict()]
continue
elif loop_result.level == "warn":
# Soft warning: inject into the observation as guidance
engine._history_append(
Expand All @@ -188,8 +192,31 @@ def run_act(
metadata={"source": "loop_detector_warning"},
)

execution = engine.executor.execute(actions, env=engine.env, state=state)
record.tool_invocations = [
# If all actions were blocked, return immediately
if len(blocked_indices) == len(actions):
merged_results = [br for _, br in sorted(blocked_results, key=lambda x: x[0])]
merged_invocations = [bi for _, bi in sorted(blocked_invocations, key=lambda x: x[0])]
record.action_results = merged_results
record.tool_invocations = merged_invocations
engine._dispatch_hook(
"on_after_act",
engine._hook_context(
step_id=record.step_id,
phase=RuntimePhase.ACT,
state=state,
decision=decision,
action_results=[r.to_dict() for r in merged_results],
record=record,
),
)
return [r.to_dict() for r in merged_results]

# Execute non-blocked actions
executable_actions = [a for i, a in enumerate(actions) if i not in blocked_indices]
executable_indices = [i for i in range(len(actions)) if i not in blocked_indices]
execution = engine.executor.execute(executable_actions, env=engine.env, state=state)
# Build tool_invocations from execution results (executable only)
exec_invocations = [
{
"tool_name": item.name,
"toolset_name": item.metadata.get("toolset_name"),
Expand Down Expand Up @@ -270,6 +297,35 @@ def run_act(
},
)
)

# Merge blocked results and execution results back into original action order
if blocked_indices:
# Map execution result indices to original action indices
exec_result_by_orig_idx: Dict[int, ToolResult] = {}
for exec_i, orig_i in enumerate(executable_indices):
if exec_i < len(results):
exec_result_by_orig_idx[orig_i] = results[exec_i]
blocked_result_by_orig_idx: Dict[int, ToolResult] = {idx: r for idx, r in blocked_results}
blocked_inv_by_orig_idx: Dict[int, Dict[str, Any]] = {idx: inv for idx, inv in blocked_invocations}
exec_inv_by_orig_idx: Dict[int, Dict[str, Any]] = {}
for exec_i, orig_i in enumerate(executable_indices):
if exec_i < len(exec_invocations):
exec_inv_by_orig_idx[orig_i] = exec_invocations[exec_i]

merged_results: List[ToolResult] = []
merged_invocations: List[Dict[str, Any]] = []
for i in range(len(actions)):
if i in blocked_indices:
merged_results.append(blocked_result_by_orig_idx.get(i, ToolResult(status="error", output=None, error="action_blocked")))
merged_invocations.append(blocked_inv_by_orig_idx.get(i, {}))
else:
merged_results.append(exec_result_by_orig_idx.get(i, ToolResult(status="error", output=None, error="execution_failed")))
merged_invocations.append(exec_inv_by_orig_idx.get(i, {}))
results = merged_results
record.tool_invocations = merged_invocations
else:
record.tool_invocations = exec_invocations

if engine.env is not None:
env_result = engine._run_env_step(
decision=decision,
Expand All @@ -286,7 +342,7 @@ def run_act(
record.action_results = results
for item in results:
engine._memory_append("action_result", item, record.step_id)
for normalized_action in actions:
for normalized_action in executable_actions:
engine._tool_loop_detector.record(
normalized_action.name, dict(normalized_action.args or {})
)
Expand Down
5 changes: 5 additions & 0 deletions qitos/engine/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,11 @@
"Glob", "Grep", "glob_v2", "grep_v2",
"WebFetch", "web_fetch_v2",
"task_list", "task_get",
# CyberGym read-only tools
"READ", "GREP", "FindSymbols", "CallsiteSearch", "RepoMap",
"FileInfo", "HexView", "StructProbe", "CorpusInspect",
# CyberGym submission (idempotent, safe to parallelize)
"submit_poc",
})


Expand Down
14 changes: 13 additions & 1 deletion qitos/harness/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,18 @@ def build_model_for_preset(
protocol=protocol,
tool_delivery=tool_delivery,
)
# Merge preset-level recommended_request_kwargs with caller-provided kwargs.
# Caller kwargs take precedence over preset recommendations.
preset_kwargs = harness.family_preset.recommended_request_kwargs
effective_kwargs = default_request_kwargs
if preset_kwargs:
if effective_kwargs is None:
effective_kwargs = dict(preset_kwargs)
else:
merged = dict(preset_kwargs)
merged.update(effective_kwargs)
effective_kwargs = merged

llm = harness.adapter.build_model(
preset=harness.family_preset,
model_name=model_name,
Expand All @@ -80,7 +92,7 @@ def build_model_for_preset(
timeout=timeout,
system_prompt=system_prompt,
context_window=context_window,
default_request_kwargs=default_request_kwargs,
default_request_kwargs=effective_kwargs,
)
metadata = dict(getattr(llm, "qitos_harness_metadata", {}) or {})
metadata.update(harness.to_dict())
Expand Down
7 changes: 4 additions & 3 deletions qitos/harness/_presets.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@
display_name="GLM",
model_matchers=("glm-", "zai-org/glm-", "zai-org/glm"),
adapter_kind="openai-compatible",
default_protocol="json_decision_v1",
fallback_protocols=("xml_decision_v1", "react_text_v1"),
default_protocol="json_decision_multi_v1",
fallback_protocols=("json_decision_v1", "xml_decision_v1", "react_text_v1"),
tool_policy=ToolPolicy(
primary_delivery="api_parameter",
fallback_delivery="prompt_injection",
Expand All @@ -76,12 +76,13 @@
fallback_context_window=128_000,
notes="GLM-5.1 class endpoints commonly expose a 200k context window; fall back conservatively when the provider does not advertise it.",
),
notes="Research default for GLM models served through OpenAI-compatible endpoints, preferring native tool calls before text parsing.",
notes="Research default for GLM models served through OpenAI-compatible endpoints, preferring native tool calls before text parsing. Uses json_decision_multi_v1 for multi-action support.",
recommended_models=("GLM-5.1-sii", "zai-org/GLM-5.1-FP8"),
recommended_max_steps=30,
recommended_max_tokens=500_000,
recommended_retry_budget=3,
recommended_temperature=0.2,
recommended_request_kwargs={"parallel_tool_calls": True},
),
FamilyPreset(
id="kimi",
Expand Down
2 changes: 2 additions & 0 deletions qitos/harness/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ class FamilyPreset:
recommended_max_tokens: Optional[int] = None
recommended_retry_budget: Optional[int] = None
recommended_temperature: Optional[float] = None
recommended_request_kwargs: Optional[dict[str, Any]] = None

def matches(self, value: str | None) -> bool:
normalized = str(value or "").strip().lower()
Expand Down Expand Up @@ -90,6 +91,7 @@ def to_dict(self) -> dict[str, Any]:
"recommended_max_tokens": self.recommended_max_tokens,
"recommended_retry_budget": self.recommended_retry_budget,
"recommended_temperature": self.recommended_temperature,
"recommended_request_kwargs": self.recommended_request_kwargs,
}

def override(self, **kwargs: Any) -> FamilyPreset:
Expand Down
47 changes: 46 additions & 1 deletion qitos/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,12 +166,33 @@ def _render_continuation_message(feedback: str) -> str:
Final Answer: <answer>"""

_JSON_CONTRACT = """Output contract:
- Tool call:
- Single tool call:
{"thought":"...","action":{"name":"tool_name","args":{"key":"value"}}}

- Multiple independent tool calls (use when actions have no data dependencies):
{"thought":"...","actions":[{"name":"tool_name","args":{"key":"value"}},{"name":"tool_name","args":{"key":"value"}}]}

- Final answer:
{"thought":"...","final_answer":"..."}"""

_JSON_MULTI_CONTRACT = """Output contract:
- Single tool call:
{"thought":"...","action":{"name":"tool_name","args":{"key":"value"}}}

- Multiple independent tool calls (PREFERRED when actions have no dependencies):
{"thought":"read two source files in parallel","actions":[{"name":"tool1","args":{"key":"value"}},{"name":"tool2","args":{"key":"value"}}]}

- Final answer:
{"thought":"...","final_answer":"..."}

Rules for multiple tool calls:
- Use the "actions" array when you need multiple independent operations in one step.
- Only combine actions with NO data dependencies on each other.
- Read-only tools (file reads, searches, inspections) can always be combined.
- NEVER combine write actions (file writes, shell commands) with reads or with each other in the same step.
- Limit each batch to at most 4 actions.
- When you have only one tool to call, use the "action" object (not an array)."""

_XML_CONTRACT = """Output contract:
- Tool call:
<decision mode="act"><think>...</think><action name="tool_name"><arg name="key">value</arg></action></decision>
Expand Down Expand Up @@ -373,6 +394,15 @@ def _json_prompt(base_prompt: str, tool_registry: Any) -> str:
)


def _json_multi_prompt(base_prompt: str, tool_registry: Any) -> str:
return _compose_prompt(
base_prompt,
tool_registry,
contract=_JSON_MULTI_CONTRACT,
renderer=render_json_tool_schema,
)


def _xml_prompt(base_prompt: str, tool_registry: Any) -> str:
return _compose_prompt(
base_prompt,
Expand Down Expand Up @@ -461,6 +491,21 @@ def _protocol_table() -> Dict[str, ModelProtocol]:
continuation_renderer=_render_continuation_message,
tool_schema_delivery="hybrid",
supports_native_tool_call_markup=True,
supports_multi_action=True,
),
"json_decision_multi_v1": ModelProtocol(
id="json_decision_multi_v1",
display_name="JSON Decision (Multi-Action)",
parser_factory=JsonDecisionParser,
prompt_renderer=_json_multi_prompt,
contract_renderer=lambda _protocol: _render_simple_contract(_JSON_MULTI_CONTRACT),
tool_schema_renderer=render_json_tool_schema,
repair_renderer=_render_repair_message,
continuation_renderer=_render_continuation_message,
tool_schema_delivery="hybrid",
supports_native_tool_call_markup=True,
supports_multi_action=True,
fallback_protocols=("json_decision_v1", "xml_decision_v1", "react_text_v1"),
),
"xml_decision_v1": ModelProtocol(
id="xml_decision_v1",
Expand Down
Loading
Loading