Skip to content

Commit f50ec83

Browse files
committed
feat(docs): add rules panel image and update skills panel description
- Introduced a new image for the rules panel in the documentation to enhance visual understanding. - Updated the description of the skills panel to reflect the new layout and features, improving clarity for users.
1 parent d7ac15e commit f50ec83

5 files changed

Lines changed: 192 additions & 47 deletions

File tree

docs/images/rules-panel.png

159 KB
Loading

docs/images/skills-panel.png

-155 KB
Loading

docs/user/rules-and-skills.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,10 @@ Each rule is an **expandable card** (same pattern as Skills):
3131

3232
Use **Create rule** at the top of the tab to add a new `rule-*.md` file. The form validates name and body before writing to `.agents/rules/`.
3333

34+
<p align="center">
35+
<img src="../images/rules-panel.png" alt="Rules panel with create-rule form and expandable rule cards" />
36+
</p>
37+
3438
Disabled rules are invisible to the agent — the system prompt treats them as if they did not exist.
3539

3640
Active rules are **binding and non-negotiable**; they outrank skill guidance when both apply.
@@ -40,7 +44,7 @@ Active rules are **binding and non-negotiable**; they outrank skill guidance whe
4044
Open **Skills** from the right workbench rail (`LuPuzzle` icon).
4145

4246
<p align="center">
43-
<img src="../images/skills-panel.png" alt="Skills panel with install dialog and skill cards showing source badges" />
47+
<img src="../images/skills-panel.png" alt="Skills panel with Core and User tabs and expandable skill cards" />
4448
</p>
4549

4650
### Core vs User tabs

src-tauri/src/agent/subagent_prompts.rs

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -155,21 +155,38 @@ pub fn subagent_system_prompt(
155155
)
156156
};
157157
let inventory = render_tool_inventory(groups);
158+
let has_workspace_read = groups.iter().any(|g| matches!(g, ToolGroup::WorkspaceRead));
159+
let mandatory_first = if has_workspace_read {
160+
"Your FIRST tool call MUST be `list_workspace_files` with `{\"path\": \".\"}` to enumerate the workspace root. \
161+
This is non-negotiable — do not skip it, do not call `submit_result` first."
162+
} else {
163+
""
164+
};
158165
format!(
159166
"You are {display_name}, a BLXCode subagent specialized in {role_line}.\n\
160167
Workspace: {workspace_root}\n\
161168
Task: {task}\n\
162169
{criteria}\n\
163-
Tools available to you in this run:\n{inventory}\n\
170+
# Tools (these ARE provisioned and CALLABLE right now)\n\
171+
{inventory}\n\
172+
\n\
173+
# Required execution flow\n\
174+
1. {mandatory_first}\n\
175+
2. After confirming access, call any other tools above as the task requires.\n\
176+
3. Call `environment_detect` before any `shell_exec` or `git_*` tool.\n\
177+
4. Finish by calling `submit_result` exactly once with structured JSON \
178+
(free-form assistant text is ignored).\n\
164179
\n\
165-
You DO have file-system access via the workspace tools listed above. \
166-
Never claim a lack of tools without first attempting `list_workspace_files` \
167-
and `read_workspace_file` against the workspace root. If a tool errors, \
168-
report the exact error in `submit_result`.\n\
169-
Call `environment_detect` before any shell or git tool.\n\
170-
You MUST finish by calling the `submit_result` tool exactly once with structured JSON. \
171-
Free-form assistant text is ignored.\n\
172-
Do not call `subagents.run`.\n"
180+
# Forbidden behaviors\n\
181+
- Do NOT claim the workspace/file-access tools are missing. They are listed above \
182+
AND present in the tool schema you were handed. If you cannot see them, your context \
183+
is malformed — report that exact diagnostic in `submit_result.summary`, but only after \
184+
attempting `list_workspace_files`.\n\
185+
- Do NOT call `submit_result` with `status: \"blocked\"` before attempting at least \
186+
one workspace read.\n\
187+
- Do NOT call `subagents.run` (recursion is disabled for subagents).\n\
188+
- When a tool errors, report the exact error string in `submit_result.summary`; do not \
189+
paraphrase it as 'tools unavailable'.\n"
173190
)
174191
}
175192

src-tauri/src/agent/subagent_runner.rs

Lines changed: 161 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ pub async fn run_one_subagent(
154154
let mut output_estimate = 0usize;
155155
let mut iterations = 0u32;
156156
let mut final_submit: Option<Value> = None;
157+
// Names of all non-`submit_result` tool calls the subagent has actually
158+
// made in this run. Used by `validate_submit` to detect "blocked without
159+
// trying", where a model returns `status:"blocked"` claiming missing
160+
// workspace tools without ever calling `list_workspace_files` etc.
161+
let mut tools_called: std::collections::HashSet<String> = std::collections::HashSet::new();
162+
let has_workspace_read = groups.iter().any(|g| matches!(g, ToolGroup::WorkspaceRead));
157163

158164
match provider {
159165
SubagentProvider::OpenAi(endpoint) => {
@@ -223,35 +229,58 @@ pub async fn run_one_subagent(
223229
if round.tool_calls.is_empty() {
224230
break;
225231
}
232+
let mut accepted_submit = false;
226233
for tc in round.tool_calls {
227234
let internal_name = openai_tool_name_to_internal(&tc.name);
228-
if handle_tool_call(
235+
match handle_tool_call(
229236
state,
230237
agent_id,
231238
&tc.id,
232239
&internal_name,
233240
&tc.arguments,
234241
groups,
235242
root_guard.as_ref(),
243+
&mut tools_called,
244+
has_workspace_read,
236245
&mut final_submit,
237246
) {
238-
messages.push(json!({
239-
"role": "tool",
240-
"tool_call_id": tc.id,
241-
"content": "submit_result accepted",
242-
}));
243-
break;
247+
ToolCallOutcome::SubmitAccepted => {
248+
messages.push(json!({
249+
"role": "tool",
250+
"tool_call_id": tc.id,
251+
"content": "submit_result accepted",
252+
}));
253+
accepted_submit = true;
254+
break;
255+
}
256+
ToolCallOutcome::SubmitRejected(message) => {
257+
messages.push(json!({
258+
"role": "tool",
259+
"tool_call_id": tc.id,
260+
"content": message,
261+
}));
262+
// Loop continues so the model retries with a real
263+
// workspace probe before re-submitting.
264+
continue;
265+
}
266+
ToolCallOutcome::NotSubmit => {
267+
let args: Value =
268+
serde_json::from_str(&tc.arguments).unwrap_or(json!({}));
269+
let outcome = execute_subagent_tool(
270+
&internal_name,
271+
&args,
272+
groups,
273+
root_guard.as_ref(),
274+
);
275+
messages.push(json!({
276+
"role": "tool",
277+
"tool_call_id": tc.id,
278+
"content": outcome.content,
279+
}));
280+
}
244281
}
245-
let args: Value =
246-
serde_json::from_str(&tc.arguments).unwrap_or(json!({}));
247-
let outcome = execute_subagent_tool(&internal_name, &args, groups, root_guard.as_ref());
248-
messages.push(json!({
249-
"role": "tool",
250-
"tool_call_id": tc.id,
251-
"content": outcome.content,
252-
}));
253282
}
254-
if final_submit.is_some() {
283+
if accepted_submit {
255284
break;
256285
}
257286
}
@@ -306,35 +335,53 @@ pub async fn run_one_subagent(
306335
break;
307336
}
308337
let mut result_blocks: Vec<Value> = Vec::new();
338+
let mut accepted_submit = false;
309339
for (id, name, args_str) in round.tool_uses {
310-
if handle_tool_call(
340+
match handle_tool_call(
311341
state,
312342
agent_id,
313343
&id,
314344
&name,
315345
&args_str,
316346
groups,
317347
root_guard.as_ref(),
348+
&mut tools_called,
349+
has_workspace_read,
318350
&mut final_submit,
319351
) {
320-
result_blocks.push(json!({
321-
"type": "tool_result",
322-
"tool_use_id": id,
323-
"content": "submit_result accepted",
324-
}));
325-
break;
352+
ToolCallOutcome::SubmitAccepted => {
353+
result_blocks.push(json!({
354+
"type": "tool_result",
355+
"tool_use_id": id,
356+
"content": "submit_result accepted",
357+
}));
358+
accepted_submit = true;
359+
break;
360+
}
361+
ToolCallOutcome::SubmitRejected(message) => {
362+
result_blocks.push(json!({
363+
"type": "tool_result",
364+
"tool_use_id": id,
365+
"content": message,
366+
"is_error": true,
367+
}));
368+
continue;
369+
}
370+
ToolCallOutcome::NotSubmit => {
371+
let args: Value = serde_json::from_str(&args_str).unwrap_or(json!({}));
372+
let outcome =
373+
execute_subagent_tool(&name, &args, groups, root_guard.as_ref());
374+
result_blocks.push(json!({
375+
"type": "tool_result",
376+
"tool_use_id": id,
377+
"content": outcome.content,
378+
"is_error": !outcome.ok,
379+
}));
380+
}
326381
}
327-
let args: Value = serde_json::from_str(&args_str).unwrap_or(json!({}));
328-
let outcome = execute_subagent_tool(&name, &args, groups, root_guard.as_ref());
329-
result_blocks.push(json!({
330-
"type": "tool_result",
331-
"tool_use_id": id,
332-
"content": outcome.content,
333-
"is_error": !outcome.ok,
334-
}));
335382
}
336383
messages.push(json!({ "role": "user", "content": result_blocks }));
337-
if final_submit.is_some() {
384+
if accepted_submit {
338385
break;
339386
}
340387
if round.stop_reason.as_deref() == Some("end_turn") {
@@ -364,6 +411,21 @@ pub async fn run_one_subagent(
364411
result
365412
}
366413

414+
/// Outcome reported back by [`handle_tool_call`] so the caller knows whether
415+
/// to finalize the loop, inject a corrective tool response, or proceed to
416+
/// regular tool execution.
417+
#[derive(Debug)]
418+
enum ToolCallOutcome {
419+
/// `submit_result` accepted — caller should record it and break the loop.
420+
SubmitAccepted,
421+
/// `submit_result` rejected by [`validate_submit`]; caller must push the
422+
/// embedded message back to the model as the tool response and keep
423+
/// looping so the model retries.
424+
SubmitRejected(String),
425+
/// Not a `submit_result` call — caller should run the regular tool.
426+
NotSubmit,
427+
}
428+
367429
fn handle_tool_call(
368430
state: &Arc<AgentEngineState>,
369431
agent_id: &str,
@@ -372,8 +434,10 @@ fn handle_tool_call(
372434
args_str: &str,
373435
groups: &[ToolGroup],
374436
root: Option<&WorkspaceRootGuard>,
437+
tools_called: &mut std::collections::HashSet<String>,
438+
has_workspace_read: bool,
375439
final_submit: &mut Option<Value>,
376-
) -> bool {
440+
) -> ToolCallOutcome {
377441
state.push(AgentEvent::SubagentToolCall {
378442
agent_id: agent_id.to_owned(),
379443
tool: name.to_owned(),
@@ -382,11 +446,20 @@ fn handle_tool_call(
382446
});
383447
if name == "submit_result" {
384448
let args: Value = serde_json::from_str(args_str).unwrap_or(json!({}));
385-
*final_submit = Some(truncate_submit_result(args));
386-
return true;
449+
match validate_submit(&args, tools_called, has_workspace_read) {
450+
SubmitVerdict::Accept => {
451+
*final_submit = Some(truncate_submit_result(args));
452+
ToolCallOutcome::SubmitAccepted
453+
}
454+
SubmitVerdict::RejectBlockedWithoutTrying { message } => {
455+
ToolCallOutcome::SubmitRejected(message)
456+
}
457+
}
458+
} else {
459+
tools_called.insert(name.to_owned());
460+
let _ = (groups, root);
461+
ToolCallOutcome::NotSubmit
387462
}
388-
let _ = (groups, root);
389-
false
390463
}
391464

392465
fn execute_subagent_tool(
@@ -427,6 +500,57 @@ fn finish_subagent(state: &Arc<AgentEngineState>, agent_id: &str, result: &Value
427500
});
428501
}
429502

503+
/// Names of workspace-read tools whose use proves the subagent actually
504+
/// attempted to access the file system before submitting a result.
505+
const WORKSPACE_READ_TOOLS: &[&str] = &[
506+
"list_workspace_files",
507+
"read_workspace_file",
508+
"workspace_search",
509+
];
510+
511+
/// Verdict for a candidate `submit_result` call from a subagent.
512+
#[derive(Debug, PartialEq, Eq)]
513+
enum SubmitVerdict {
514+
/// Accept this submission as the final result.
515+
Accept,
516+
/// Reject because the subagent claimed "blocked" without ever attempting
517+
/// a workspace read. `message` is fed back to the model as the tool
518+
/// response so it tries again instead of finalizing the run.
519+
RejectBlockedWithoutTrying { message: String },
520+
}
521+
522+
/// Decide whether a `submit_result` payload should be accepted or sent back
523+
/// to the model for another attempt. The only currently-enforced rule is
524+
/// the "blocked without trying" guard: if the role had `workspace_read`
525+
/// provisioned but the subagent never called any of the workspace tools
526+
/// and now wants to return `status:"blocked"`, we reject so the model can
527+
/// be forced to verify access first.
528+
fn validate_submit(
529+
args: &Value,
530+
tools_called: &std::collections::HashSet<String>,
531+
has_workspace_read: bool,
532+
) -> SubmitVerdict {
533+
let status = args.get("status").and_then(|v| v.as_str()).unwrap_or("");
534+
if status != "blocked" || !has_workspace_read {
535+
return SubmitVerdict::Accept;
536+
}
537+
let attempted = WORKSPACE_READ_TOOLS
538+
.iter()
539+
.any(|t| tools_called.contains(*t));
540+
if attempted {
541+
return SubmitVerdict::Accept;
542+
}
543+
SubmitVerdict::RejectBlockedWithoutTrying {
544+
message: "Rejected: you submitted status:\"blocked\" without ever calling \
545+
list_workspace_files, read_workspace_file, or workspace_search. These tools \
546+
ARE provisioned in this run and ARE present in your tool schema. \
547+
Call `list_workspace_files` with {\"path\":\".\"} now to verify access, \
548+
then proceed with the task. Do not return a blocked status until you have \
549+
actually attempted a workspace read and reported the concrete error string."
550+
.to_owned(),
551+
}
552+
}
553+
430554
fn blocked_result(role: SubagentRole, display_name: &str, summary: &str) -> Value {
431555
json!({
432556
"status": "blocked",

0 commit comments

Comments
 (0)