mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 17:08:34 +08:00
fix(chat): render InvokeSkill action/observation with skill name (#283)
Previously, an agent's invoke_skill tool call rendered as the literal 'INVOKESKILL' in the chat — the default fallback in getActionEventTitle was uppercasing the action kind because InvokeSkillAction wasn't a recognized kind. Add InvokeSkillAction / InvokeSkillObservation to the core type unions and route them through the same translation-key machinery as the other built-in tools, so the title now reads 'Invoking skill <name>' / 'Invoked skill <name>' and the expanded card shows the rendered skill content instead of a JSON blob. Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
committed by
GitHub
co-authored by
openhands
parent
400be5a3d4
commit
0eb0ca5dfa
@@ -180,4 +180,65 @@ describe("getEventContent", () => {
|
||||
expect(screen.getByText("Check repository status")).toBeInTheDocument();
|
||||
expect(screen.queryByText("$ git status")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders InvokeSkillAction with the skill name instead of 'INVOKESKILL'", () => {
|
||||
const invokeSkillAction: ActionEvent = {
|
||||
id: "action-skill",
|
||||
timestamp: new Date().toISOString(),
|
||||
source: "agent",
|
||||
thought: [],
|
||||
thinking_blocks: [],
|
||||
action: {
|
||||
kind: "InvokeSkillAction",
|
||||
name: "worktree-switch",
|
||||
},
|
||||
tool_name: "invoke_skill",
|
||||
tool_call_id: "tool-skill",
|
||||
tool_call: {
|
||||
id: "tool-skill",
|
||||
type: "function",
|
||||
function: {
|
||||
name: "invoke_skill",
|
||||
arguments: '{"name":"worktree-switch"}',
|
||||
},
|
||||
},
|
||||
llm_response_id: "response-skill",
|
||||
security_risk: SecurityRisk.LOW,
|
||||
};
|
||||
|
||||
const { title, details } = getEventContent(invokeSkillAction);
|
||||
|
||||
render(<span>{title}</span>);
|
||||
// Without i18n loaded, the translation key renders as the raw key —
|
||||
// the important thing is that we no longer fall back to "INVOKESKILL".
|
||||
expect(screen.getByText("ACTION_MESSAGE$INVOKE_SKILL")).toBeInTheDocument();
|
||||
expect(screen.queryByText("INVOKESKILL")).not.toBeInTheDocument();
|
||||
expect(details).toContain("worktree-switch");
|
||||
});
|
||||
|
||||
it("renders InvokeSkillObservation with the skill name", () => {
|
||||
const invokeSkillObservation: ObservationEvent = {
|
||||
id: "obs-skill",
|
||||
timestamp: new Date().toISOString(),
|
||||
source: "environment",
|
||||
tool_name: "invoke_skill",
|
||||
tool_call_id: "tool-skill",
|
||||
action_id: "action-skill",
|
||||
observation: {
|
||||
kind: "InvokeSkillObservation",
|
||||
skill_name: "worktree-switch",
|
||||
content: [{ type: "text", text: "# Skill content" }],
|
||||
},
|
||||
};
|
||||
|
||||
const { title, details } = getEventContent(invokeSkillObservation);
|
||||
|
||||
render(<span>{title}</span>);
|
||||
expect(
|
||||
screen.getByText("OBSERVATION_MESSAGE$INVOKE_SKILL"),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText("INVOKESKILL")).not.toBeInTheDocument();
|
||||
expect(details).toContain("worktree-switch");
|
||||
expect(details).toContain("# Skill content");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
BrowserCloseTabAction,
|
||||
GlobAction,
|
||||
GrepAction,
|
||||
InvokeSkillAction,
|
||||
} from "#/types/agent-server/core/base/action";
|
||||
|
||||
const getRiskText = (risk: SecurityRisk) => {
|
||||
@@ -41,6 +42,10 @@ const getRiskText = (risk: SecurityRisk) => {
|
||||
|
||||
const getNoContentActionContent = (): string => "";
|
||||
|
||||
// Invoke-skill actions
|
||||
const getInvokeSkillActionContent = (action: InvokeSkillAction): string =>
|
||||
action.name ? `**Skill:** \`${action.name}\`` : getNoContentActionContent();
|
||||
|
||||
// Grep/Glob search actions
|
||||
const getSearchActionContent = (
|
||||
event: ActionEvent<GlobAction | GrepAction>,
|
||||
@@ -254,6 +259,9 @@ export const getActionContent = (event: ActionEvent): string => {
|
||||
event as ActionEvent<GlobAction | GrepAction>,
|
||||
);
|
||||
|
||||
case "InvokeSkillAction":
|
||||
return getInvokeSkillActionContent(action as InvokeSkillAction);
|
||||
|
||||
default:
|
||||
return getDefaultEventContent(event);
|
||||
}
|
||||
|
||||
@@ -114,6 +114,12 @@ const getActionEventTitle = (event: OpenHandsEvent): React.ReactNode => {
|
||||
mcp_tool_name: event.tool_name,
|
||||
};
|
||||
break;
|
||||
case "InvokeSkillAction":
|
||||
actionKey = "ACTION_MESSAGE$INVOKE_SKILL";
|
||||
actionValues = {
|
||||
name: event.action.name,
|
||||
};
|
||||
break;
|
||||
case "ThinkAction":
|
||||
actionKey = "ACTION_MESSAGE$THINK";
|
||||
break;
|
||||
@@ -213,6 +219,12 @@ const getObservationEventTitle = (
|
||||
mcp_tool_name: event.observation.tool_name,
|
||||
};
|
||||
break;
|
||||
case "InvokeSkillObservation":
|
||||
observationKey = "OBSERVATION_MESSAGE$INVOKE_SKILL";
|
||||
observationValues = {
|
||||
name: event.observation.skill_name,
|
||||
};
|
||||
break;
|
||||
case "BrowserObservation":
|
||||
observationKey = "OBSERVATION_MESSAGE$BROWSE";
|
||||
break;
|
||||
|
||||
+31
@@ -14,6 +14,7 @@ import {
|
||||
TaskTrackerObservation,
|
||||
GlobObservation,
|
||||
GrepObservation,
|
||||
InvokeSkillObservation,
|
||||
} from "#/types/agent-server/core/base/observation";
|
||||
|
||||
// File Editor Observations
|
||||
@@ -145,6 +146,31 @@ const getMCPToolObservationContent = (
|
||||
return content;
|
||||
};
|
||||
|
||||
// Invoke-skill observations
|
||||
const getInvokeSkillObservationContent = (
|
||||
event: ObservationEvent<InvokeSkillObservation>,
|
||||
): string => {
|
||||
const { observation } = event;
|
||||
|
||||
const textContent = observation.content
|
||||
.filter((c) => c.type === "text")
|
||||
.map((c) => c.text)
|
||||
.join("\n");
|
||||
|
||||
const header = observation.skill_name
|
||||
? `**Skill:** \`${observation.skill_name}\`\n\n`
|
||||
: "";
|
||||
const body = observation.is_error
|
||||
? `**Error:**\n${textContent}`
|
||||
: textContent;
|
||||
|
||||
let content = `${header}${body}`;
|
||||
if (content.length > MAX_CONTENT_LENGTH) {
|
||||
content = `${content.slice(0, MAX_CONTENT_LENGTH)}...(truncated)`;
|
||||
}
|
||||
return content;
|
||||
};
|
||||
|
||||
// Complex Observations
|
||||
const getTaskTrackerObservationContent = (
|
||||
event: ObservationEvent<TaskTrackerObservation>,
|
||||
@@ -342,6 +368,11 @@ export const getObservationContent = (event: ObservationEvent): string => {
|
||||
event as ObservationEvent<GrepObservation>,
|
||||
);
|
||||
|
||||
case "InvokeSkillObservation":
|
||||
return getInvokeSkillObservationContent(
|
||||
event as ObservationEvent<InvokeSkillObservation>,
|
||||
);
|
||||
|
||||
default:
|
||||
return getDefaultEventContent(event);
|
||||
}
|
||||
|
||||
@@ -12920,6 +12920,40 @@
|
||||
"uk": "Пошук файлів: <cmd>{{pattern}}</cmd>",
|
||||
"ca": "Cerca fitxers: <cmd>{{pattern}}</cmd>"
|
||||
},
|
||||
"ACTION_MESSAGE$INVOKE_SKILL": {
|
||||
"en": "Invoking skill <cmd>{{name}}</cmd>",
|
||||
"ja": "スキルを呼び出し中: <cmd>{{name}}</cmd>",
|
||||
"zh-CN": "调用技能 <cmd>{{name}}</cmd>",
|
||||
"zh-TW": "呼叫技能 <cmd>{{name}}</cmd>",
|
||||
"ko-KR": "스킬 호출 중 <cmd>{{name}}</cmd>",
|
||||
"no": "Kaller ferdighet <cmd>{{name}}</cmd>",
|
||||
"it": "Invocazione skill <cmd>{{name}}</cmd>",
|
||||
"pt": "Invocando skill <cmd>{{name}}</cmd>",
|
||||
"es": "Invocando skill <cmd>{{name}}</cmd>",
|
||||
"ar": "استدعاء المهارة <cmd>{{name}}</cmd>",
|
||||
"fr": "Invocation de la compétence <cmd>{{name}}</cmd>",
|
||||
"tr": "Yetenek çağrılıyor <cmd>{{name}}</cmd>",
|
||||
"de": "Skill wird aufgerufen <cmd>{{name}}</cmd>",
|
||||
"uk": "Виклик навички <cmd>{{name}}</cmd>",
|
||||
"ca": "Invocant skill <cmd>{{name}}</cmd>"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$INVOKE_SKILL": {
|
||||
"en": "Invoked skill <cmd>{{name}}</cmd>",
|
||||
"ja": "スキルを呼び出しました: <cmd>{{name}}</cmd>",
|
||||
"zh-CN": "已调用技能 <cmd>{{name}}</cmd>",
|
||||
"zh-TW": "已呼叫技能 <cmd>{{name}}</cmd>",
|
||||
"ko-KR": "스킬 호출됨 <cmd>{{name}}</cmd>",
|
||||
"no": "Kalte ferdighet <cmd>{{name}}</cmd>",
|
||||
"it": "Skill invocata <cmd>{{name}}</cmd>",
|
||||
"pt": "Skill invocada <cmd>{{name}}</cmd>",
|
||||
"es": "Skill invocada <cmd>{{name}}</cmd>",
|
||||
"ar": "تم استدعاء المهارة <cmd>{{name}}</cmd>",
|
||||
"fr": "Compétence invoquée <cmd>{{name}}</cmd>",
|
||||
"tr": "Yetenek çağrıldı <cmd>{{name}}</cmd>",
|
||||
"de": "Skill aufgerufen <cmd>{{name}}</cmd>",
|
||||
"uk": "Навичку викликано <cmd>{{name}}</cmd>",
|
||||
"ca": "Skill invocada <cmd>{{name}}</cmd>"
|
||||
},
|
||||
"OBSERVATION_MESSAGE$RUN": {
|
||||
"en": "Ran <cmd>{{command}}</cmd>",
|
||||
"zh-CN": "运行 <cmd>{{command}}</cmd>",
|
||||
|
||||
@@ -270,6 +270,13 @@ export interface GrepAction extends ActionBase<"GrepAction"> {
|
||||
include: string | null;
|
||||
}
|
||||
|
||||
export interface InvokeSkillAction extends ActionBase<"InvokeSkillAction"> {
|
||||
/**
|
||||
* Name of the loaded skill to invoke.
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export type Action =
|
||||
| MCPToolAction
|
||||
| FinishAction
|
||||
@@ -291,4 +298,5 @@ export type Action =
|
||||
| BrowserSwitchTabAction
|
||||
| BrowserCloseTabAction
|
||||
| GlobAction
|
||||
| GrepAction;
|
||||
| GrepAction
|
||||
| InvokeSkillAction;
|
||||
|
||||
@@ -7,7 +7,8 @@ type EventType =
|
||||
| "FileEditor"
|
||||
| "StrReplaceEditor"
|
||||
| "TaskTracker"
|
||||
| "PlanningFileEditor";
|
||||
| "PlanningFileEditor"
|
||||
| "InvokeSkill";
|
||||
|
||||
type ActionOnlyType =
|
||||
| "BrowserNavigate"
|
||||
|
||||
@@ -275,6 +275,21 @@ export interface GrepObservation extends ObservationBase<"GrepObservation"> {
|
||||
truncated: boolean;
|
||||
}
|
||||
|
||||
export interface InvokeSkillObservation extends ObservationBase<"InvokeSkillObservation"> {
|
||||
/**
|
||||
* Name of the skill this observation corresponds to.
|
||||
*/
|
||||
skill_name: string;
|
||||
/**
|
||||
* Rendered skill content returned to the agent.
|
||||
*/
|
||||
content: Array<TextContent | ImageContent>;
|
||||
/**
|
||||
* Whether the invocation resulted in an error.
|
||||
*/
|
||||
is_error?: boolean;
|
||||
}
|
||||
|
||||
export type Observation =
|
||||
| MCPToolObservation
|
||||
| FinishObservation
|
||||
@@ -287,4 +302,5 @@ export type Observation =
|
||||
| TaskTrackerObservation
|
||||
| PlanningFileEditorObservation
|
||||
| GlobObservation
|
||||
| GrepObservation;
|
||||
| GrepObservation
|
||||
| InvokeSkillObservation;
|
||||
|
||||
Reference in New Issue
Block a user