mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-23 01:54:52 +02:00
[eric] agents: AskUserQuestion swaps to AskUI when openswarm-ui is live (nudges lost to the trained prior; live-proven haiku turn) + lenient unknown-key strip before strict zod fallback
This commit is contained in:
@@ -122,6 +122,14 @@ def build_effective_tool_lists(
|
||||
for wt_name in ("WebSearch", "WebFetch"):
|
||||
if wt_name not in effective_disallowed:
|
||||
effective_disallowed.append(wt_name)
|
||||
# With the openswarm-ui server live, the built-in AskUserQuestion is swapped for AskUI (same
|
||||
# Agent->SpawnAgent playbook: prompt nudges lose to the trained prior, a hard deny doesn't).
|
||||
# AskUI's option-list/question-flow cover the flat-choice cases; denying the built-in is what
|
||||
# actually routes questions through the rich components.
|
||||
if "openswarm-ui" in mcp_servers:
|
||||
effective_allowed = [t for t in effective_allowed if t != "AskUserQuestion"]
|
||||
if "AskUserQuestion" not in effective_disallowed:
|
||||
effective_disallowed.append("AskUserQuestion")
|
||||
# Claude's internal Cron* scheduler is denied in favour of the visible native one; withhold it from the SDK so the model doesn't even reach for it.
|
||||
for bt in path_gate.CLAUDE_INTERNAL_SCHEDULER_TOOLS:
|
||||
if bt not in effective_disallowed:
|
||||
|
||||
@@ -100,7 +100,9 @@ def compose_turn_system_prompt(
|
||||
"- ShowUI for any structured result: tables, stats, links, plans, progress, code, diffs, "
|
||||
"charts, maps, media, posts, receipts. Render the component, then add one line of text.\n"
|
||||
"- AskUI for ANY question with enumerable choices, an approval, or tunable values: render "
|
||||
"it and wait for the answer instead of asking in prose.\n"
|
||||
"it and wait for the answer instead of asking in prose. When you would reach for "
|
||||
"AskUserQuestion and the choices are a flat list, call AskUI with an option-list instead; "
|
||||
"keep AskUserQuestion only for multi-question forms.\n"
|
||||
"Describing structured data in plain text when a component fits is the worse answer.\n"
|
||||
"</rich_ui>"
|
||||
)
|
||||
|
||||
@@ -9,14 +9,35 @@ interface VendoredToolUiProps {
|
||||
extraProps?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
type Gate = 'pending' | 'ok' | 'bad';
|
||||
type Gate =
|
||||
| { state: 'pending' }
|
||||
| { state: 'ok'; parsed: Record<string, unknown> }
|
||||
| { state: 'bad'; problem: string };
|
||||
|
||||
/** Models pad payloads with invented keys; strip ONLY unrecognized-key issues and retry once, so
|
||||
sloppiness self-heals while genuinely wrong shapes still fall back loudly. */
|
||||
function parseLeniently(schema: { safeParse: (v: unknown) => any }, props: Record<string, unknown>): Gate {
|
||||
let result = schema.safeParse(props);
|
||||
if (!result.success) {
|
||||
const issues: Array<{ code: string; keys?: string[]; path: Array<string | number>; message: string }> = result.error.issues;
|
||||
if (issues.every((i) => i.code === 'unrecognized_keys')) {
|
||||
const cleaned: Record<string, unknown> = { ...props };
|
||||
for (const issue of issues) {
|
||||
for (const key of issue.keys || []) delete cleaned[key];
|
||||
}
|
||||
result = schema.safeParse(cleaned);
|
||||
}
|
||||
}
|
||||
if (result.success) return { state: 'ok', parsed: result.data as Record<string, unknown> };
|
||||
const issues = result.error.issues.slice(0, 2).map((i: { path: Array<string | number>; message: string }) => `${i.path.join('.')}: ${i.message}`).join('; ');
|
||||
return { state: 'bad', problem: issues };
|
||||
}
|
||||
|
||||
/** Validates against the upstream zod contract, then renders the vendored component inside the scoped theme. */
|
||||
function VendoredToolUi({ name, props, extraProps }: VendoredToolUiProps): React.ReactElement | null {
|
||||
const { mode } = useThemeMode();
|
||||
const entry = TOOL_UI_REGISTRY[name];
|
||||
const [gate, setGate] = useState<Gate>('pending');
|
||||
const [problem, setProblem] = useState<string>('');
|
||||
const [gate, setGate] = useState<Gate>({ state: 'pending' });
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
@@ -24,35 +45,28 @@ function VendoredToolUi({ name, props, extraProps }: VendoredToolUiProps): React
|
||||
entry
|
||||
.loadSchema()
|
||||
.then((schema) => {
|
||||
if (cancelled) return;
|
||||
const result = schema.safeParse(props);
|
||||
if (result.success) {
|
||||
setGate('ok');
|
||||
} else {
|
||||
setGate('bad');
|
||||
setProblem(result.error.issues.slice(0, 2).map((i) => `${i.path.join('.')}: ${i.message}`).join('; '));
|
||||
}
|
||||
if (!cancelled) setGate(parseLeniently(schema, props));
|
||||
})
|
||||
.catch(() => { if (!cancelled) { setGate('bad'); setProblem('component failed to load'); } });
|
||||
.catch(() => { if (!cancelled) setGate({ state: 'bad', problem: 'component failed to load' }); });
|
||||
return () => { cancelled = true; };
|
||||
}, [entry, props]);
|
||||
|
||||
if (!entry) return null;
|
||||
if (gate === 'bad') {
|
||||
if (gate.state === 'bad') {
|
||||
return (
|
||||
<div style={{ fontSize: '0.75rem', opacity: 0.55, padding: '4px 0' }}>
|
||||
{name} payload didn't validate ({problem})
|
||||
{name} payload didn't validate ({gate.problem})
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (gate === 'pending') {
|
||||
if (gate.state === 'pending') {
|
||||
return <div style={{ height: 48, width: 280, borderRadius: 12, background: 'rgba(127,127,127,0.12)' }} />;
|
||||
}
|
||||
const Component = entry.Component;
|
||||
return (
|
||||
<div className={`tool-ui-scope${mode === 'dark' ? ' dark' : ''}`}>
|
||||
<Suspense fallback={<div style={{ height: 48, width: 280, borderRadius: 12, background: 'rgba(127,127,127,0.12)' }} />}>
|
||||
<Component {...props} {...(extraProps || {})} />
|
||||
<Component {...gate.parsed} {...(extraProps || {})} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user