Skip to content

Commit 5dd5c78

Browse files
committed
feat(frontend): inline executor failures via ExecutorCard; drop notification toast
koan_request_executor's tool result is now a JSON payload {status, exit_code, error} (instead of a flat status string), letting the frontend render the failing executor's terminal error directly in the ExecutorCard tied to that tool call's call_id. The 'agent_exited with error' notification toast is removed -- executor failures surface inline on the card, orchestrator failures surface via agent.error in the projection. - ExecutorCard renderer + .ktc--executor-failed styling - Drop projection 'agent_exited' notification append - mcp_endpoint emits JSON status payload from request_executor - test_projections updated to assert no notification on exit-with-error
1 parent a57ffd9 commit 5dd5c78

5 files changed

Lines changed: 73 additions & 18 deletions

File tree

frontend/src/components/molecules/KoanToolCard.css

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,29 @@
143143
color: var(--text-body);
144144
}
145145

146+
/* ---- ExecutorCard ---- */
147+
.ktc--executor-failed {
148+
border-left-color: var(--status-failed);
149+
}
150+
.ktc--executor-failed .ktc-label {
151+
color: var(--status-failed);
152+
}
153+
.ktc-executor-error {
154+
margin-top: 8px;
155+
padding: 10px 12px;
156+
background: var(--bg-thinking);
157+
border-radius: var(--radius-md);
158+
font-size: var(--type-prose);
159+
color: var(--text-primary);
160+
line-height: 1.5;
161+
}
162+
.ktc-executor-error-meta {
163+
font-family: var(--font-mono);
164+
font-size: var(--type-tool-path);
165+
color: var(--text-muted);
166+
margin-bottom: 4px;
167+
}
168+
146169
/* ---- FallbackCard ---- */
147170
/* Muted label distinguishes fallback from first-class renderers */
148171
.ktc--fallback .ktc-label {

frontend/src/components/molecules/KoanToolCard.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,39 @@ function AskQuestionCard({ toolInput, inFlight }: Omit<KoanToolCardProps, 'toolN
194194
)
195195
}
196196

197+
// ExecutorCard renders one koan_request_executor call. Each call has its own
198+
// ToolKoanEntry (correlated by call_id), so concurrent executors render as
199+
// independent cards. The executor's terminal error -- the failing agent's last
200+
// message before exit -- arrives in this entry's result and is shown inline,
201+
// replacing the prior toast-notification path.
202+
function ExecutorCard({ toolInput, result, inFlight }: Omit<KoanToolCardProps, 'toolName'>) {
203+
const artifacts = (toolInput?.artifacts as string[] | undefined) ?? []
204+
const status = (result?.status as string | undefined) ?? null
205+
const failed = status === 'failed'
206+
const errorText = (result?.error as string | undefined) ?? ''
207+
const exitCode = result?.exit_code as number | undefined
208+
const label = inFlight ? 'Running executor' : failed ? 'Executor failed' : 'Executor done'
209+
return (
210+
<div className={`ktc ktc--executor${failed ? ' ktc--executor-failed' : ''}`}>
211+
<div className="ktc-header">
212+
<span className="ktc-indicator">
213+
{inFlight ? <span className="ktc-running-dot" /> : <CheckSvg />}
214+
</span>
215+
<span className="ktc-label">{label}</span>
216+
{artifacts.length > 0 && (
217+
<span className="ktc-meta">{artifacts.length} artifact{artifacts.length === 1 ? '' : 's'}</span>
218+
)}
219+
</div>
220+
{failed && (
221+
<div className="ktc-executor-error">
222+
{exitCode != null && <div className="ktc-executor-error-meta">exit {exitCode}</div>}
223+
{errorText && <Md>{errorText}</Md>}
224+
</div>
225+
)}
226+
</div>
227+
)
228+
}
229+
197230
// FallbackCard is NOT in TOOL_RENDERERS -- it takes its own clean prop shape
198231
// and is invoked directly by the dispatch else-branch. This avoids threading
199232
// a label through the ToolRenderer signature.
@@ -219,6 +252,7 @@ const TOOL_RENDERERS: Record<string, ToolRenderer> = {
219252
koan_artifact_write: ArtifactWriteCard,
220253
koan_yield: YieldCard,
221254
koan_ask_question: AskQuestionCard,
255+
koan_request_executor: ExecutorCard,
222256
}
223257

224258
export function KoanToolCard(props: KoanToolCardProps): ReactElement | null {

koan/projections.py

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -898,19 +898,10 @@ def fold(projection: Projection, event: VersionedEvent) -> Projection:
898898
new_agents = dict(projection.run.agents)
899899
new_agents[agent_id] = new_agent
900900
new_run = projection.run.model_copy(update={"agents": new_agents})
901-
new_projection = projection.model_copy(update={"run": new_run})
902-
903-
# Append error notification
904-
if error:
905-
notif = Notification(
906-
message=f"Agent {agent_id} exited with error: {error}",
907-
level="error",
908-
timestamp_ms=int(datetime.now(timezone.utc).timestamp() * 1000),
909-
)
910-
new_projection = new_projection.model_copy(update={
911-
"notifications": [*new_projection.notifications, notif],
912-
})
913-
return new_projection
901+
# Executor failures surface in the orchestrator's koan_request_executor
902+
# tool result (see ExecutorCard); failed agent status persists on
903+
# agent.status/agent.error. No transient notification toast.
904+
return projection.model_copy(update={"run": new_run})
914905

915906
case "agent_spawn_failed":
916907
notif = Notification(

koan/web/mcp_endpoint.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1385,8 +1385,15 @@ async def koan_request_executor(
13851385
from ..subagent import spawn_subagent
13861386
result = await spawn_subagent(task, app_state)
13871387

1388-
status = "succeeded" if result.exit_code == 0 else f"failed (exit {result.exit_code})"
1389-
result_blocks = [_text_block(f"Executor {status}.")]
1388+
if result.exit_code == 0:
1389+
payload = {"status": "succeeded"}
1390+
else:
1391+
payload = {
1392+
"status": "failed",
1393+
"exit_code": result.exit_code,
1394+
"error": result.error or "",
1395+
}
1396+
result_blocks = [_text_block(json.dumps(payload))]
13901397
result_blocks, steer_manifest = _drain_and_append_steering(result_blocks, agent)
13911398
_push_tool_attachments(steer_manifest, agent)
13921399
return result_blocks

tests/test_projections.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,9 @@ def test_agent_exited_with_error_sets_failed(self):
303303
r = fold(p, _e("agent_exited", {"exit_code": 1, "error": "boom"}, agent_id="a1"))
304304
assert r.run.agents["a1"].status == "failed"
305305
assert r.run.agents["a1"].error == "boom"
306-
# Error notification appended
307-
assert len(r.notifications) == 1
308-
assert "boom" in r.notifications[0].message
306+
# Tracked-agent error surfaces inline (executor: koan_request_executor
307+
# tool result; orchestrator: agent.error). No notification toast.
308+
assert r.notifications == []
309309

310310
def test_agent_exited_accumulates_usage_into_conversation(self):
311311
p = _proj_with_primary("a1")

0 commit comments

Comments
 (0)