From be65c401fe76930a84bdc3b29c3cffc93a9f5193 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 18 Feb 2026 07:29:05 +0000 Subject: [PATCH 1/2] feat: add dedicated Activity types for skill management tools Replace GeneralToolActivity fallback for addSkill, removeSkill, addDelegate, removeDelegate, and createExpert with dedicated Activity types that provide structured fields and human-readable TUI display. Co-Authored-By: Claude Opus 4.6 --- packages/core/src/schemas/activity.ts | 90 +++++++ packages/core/src/utils/activity.test.ts | 244 ++++++++++++++++++ packages/core/src/utils/activity.ts | 59 +++++ .../src/components/checkpoint-action-row.tsx | 47 ++++ 4 files changed, 440 insertions(+) diff --git a/packages/core/src/schemas/activity.ts b/packages/core/src/schemas/activity.ts index c69830a5..a395749e 100644 --- a/packages/core/src/schemas/activity.ts +++ b/packages/core/src/schemas/activity.ts @@ -311,6 +311,86 @@ export const generalToolActivitySchema = baseActivitySchema.extend({ }) generalToolActivitySchema satisfies z.ZodType +/** Add skill activity - Dynamically adding an MCP skill */ +export interface AddSkillActivity extends BaseActivity { + type: "addSkill" + name: string + skillType: string + tools?: string[] + error?: string +} + +export const addSkillActivitySchema = baseActivitySchema.extend({ + type: z.literal("addSkill"), + name: z.string(), + skillType: z.string(), + tools: z.array(z.string()).optional(), + error: z.string().optional(), +}) +addSkillActivitySchema satisfies z.ZodType + +/** Remove skill activity - Dynamically removing an MCP skill */ +export interface RemoveSkillActivity extends BaseActivity { + type: "removeSkill" + skillName: string + error?: string +} + +export const removeSkillActivitySchema = baseActivitySchema.extend({ + type: z.literal("removeSkill"), + skillName: z.string(), + error: z.string().optional(), +}) +removeSkillActivitySchema satisfies z.ZodType + +/** Add delegate activity - Dynamically adding a delegate expert */ +export interface AddDelegateActivity extends BaseActivity { + type: "addDelegate" + targetExpertKey: string + delegateToolName?: string + error?: string +} + +export const addDelegateActivitySchema = baseActivitySchema.extend({ + type: z.literal("addDelegate"), + targetExpertKey: z.string(), + delegateToolName: z.string().optional(), + error: z.string().optional(), +}) +addDelegateActivitySchema satisfies z.ZodType + +/** Remove delegate activity - Dynamically removing a delegate expert */ +export interface RemoveDelegateActivity extends BaseActivity { + type: "removeDelegate" + expertName: string + error?: string +} + +export const removeDelegateActivitySchema = baseActivitySchema.extend({ + type: z.literal("removeDelegate"), + expertName: z.string(), + error: z.string().optional(), +}) +removeDelegateActivitySchema satisfies z.ZodType + +/** Create expert activity - Dynamically creating an expert definition */ +export interface CreateExpertActivity extends BaseActivity { + type: "createExpert" + targetKey: string + description?: string + resultExpertKey?: string + error?: string +} + +export const createExpertActivitySchema = baseActivitySchema.extend({ + type: z.literal("createExpert"), + targetKey: z.string(), + description: z.string().optional(), + resultExpertKey: z.string().optional(), + error: z.string().optional(), +}) +createExpertActivitySchema satisfies z.ZodType + /** Union of all activity types */ export type Activity = | QueryActivity @@ -330,6 +410,11 @@ export type Activity = | DelegationCompleteActivity | InteractiveToolActivity | GeneralToolActivity + | AddSkillActivity + | RemoveSkillActivity + | AddDelegateActivity + | RemoveDelegateActivity + | CreateExpertActivity export const activitySchema = z.discriminatedUnion("type", [ queryActivitySchema, @@ -349,6 +434,11 @@ export const activitySchema = z.discriminatedUnion("type", [ delegationCompleteActivitySchema, interactiveToolActivitySchema, generalToolActivitySchema, + addSkillActivitySchema, + removeSkillActivitySchema, + addDelegateActivitySchema, + removeDelegateActivitySchema, + createExpertActivitySchema, ]) activitySchema satisfies z.ZodType diff --git a/packages/core/src/utils/activity.test.ts b/packages/core/src/utils/activity.test.ts index 07abc98b..c1d9b682 100644 --- a/packages/core/src/utils/activity.test.ts +++ b/packages/core/src/utils/activity.test.ts @@ -995,6 +995,250 @@ describe("getActivities", () => { }) }) + describe("skill management activities", () => { + it("returns addSkill activity with tools", () => { + const checkpoint = createBaseCheckpoint() + const step = createBaseStep({ + toolCalls: [ + createToolCall({ + toolName: "addSkill", + args: { name: "my-skill", type: "mcpStdioSkill" }, + }), + ], + toolResults: [ + createToolResult({ + toolName: "addSkill", + result: [ + { + type: "textPart", + id: "tp-1", + text: JSON.stringify({ tools: ["toolA", "toolB", "toolC"] }), + }, + ], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(1) + expect(activities[0].type).toBe("addSkill") + if (activities[0].type === "addSkill") { + expect(activities[0].name).toBe("my-skill") + expect(activities[0].skillType).toBe("mcpStdioSkill") + expect(activities[0].tools).toEqual(["toolA", "toolB", "toolC"]) + expect(activities[0].error).toBeUndefined() + } + }) + + it("returns addSkill activity with error", () => { + const checkpoint = createBaseCheckpoint() + const step = createBaseStep({ + toolCalls: [ + createToolCall({ + toolName: "addSkill", + args: { name: "bad-skill", type: "mcpStdioSkill" }, + }), + ], + toolResults: [ + createToolResult({ + toolName: "addSkill", + result: [ + { + type: "textPart", + id: "tp-1", + text: JSON.stringify({ error: "Connection failed" }), + }, + ], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(1) + expect(activities[0].type).toBe("addSkill") + if (activities[0].type === "addSkill") { + expect(activities[0].name).toBe("bad-skill") + expect(activities[0].error).toBe("Connection failed") + expect(activities[0].tools).toBeUndefined() + } + }) + + it("returns removeSkill activity", () => { + const checkpoint = createBaseCheckpoint() + const step = createBaseStep({ + toolCalls: [ + createToolCall({ + toolName: "removeSkill", + args: { skillName: "old-skill" }, + }), + ], + toolResults: [ + createToolResult({ + toolName: "removeSkill", + result: [ + { + type: "textPart", + id: "tp-1", + text: JSON.stringify({ removed: "old-skill" }), + }, + ], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(1) + expect(activities[0].type).toBe("removeSkill") + if (activities[0].type === "removeSkill") { + expect(activities[0].skillName).toBe("old-skill") + expect(activities[0].error).toBeUndefined() + } + }) + + it("returns addDelegate activity", () => { + const checkpoint = createBaseCheckpoint() + const step = createBaseStep({ + toolCalls: [ + createToolCall({ + toolName: "addDelegate", + args: { expertKey: "math-expert@1.0.0" }, + }), + ], + toolResults: [ + createToolResult({ + toolName: "addDelegate", + result: [ + { + type: "textPart", + id: "tp-1", + text: JSON.stringify({ delegateToolName: "delegateToMathExpert" }), + }, + ], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(1) + expect(activities[0].type).toBe("addDelegate") + if (activities[0].type === "addDelegate") { + expect(activities[0].targetExpertKey).toBe("math-expert@1.0.0") + expect(activities[0].delegateToolName).toBe("delegateToMathExpert") + expect(activities[0].error).toBeUndefined() + } + }) + + it("returns removeDelegate activity", () => { + const checkpoint = createBaseCheckpoint() + const step = createBaseStep({ + toolCalls: [ + createToolCall({ + toolName: "removeDelegate", + args: { expertName: "math-expert" }, + }), + ], + toolResults: [ + createToolResult({ + toolName: "removeDelegate", + result: [ + { + type: "textPart", + id: "tp-1", + text: JSON.stringify({ removed: "math-expert" }), + }, + ], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(1) + expect(activities[0].type).toBe("removeDelegate") + if (activities[0].type === "removeDelegate") { + expect(activities[0].expertName).toBe("math-expert") + expect(activities[0].error).toBeUndefined() + } + }) + + it("returns createExpert activity with description", () => { + const checkpoint = createBaseCheckpoint() + const step = createBaseStep({ + toolCalls: [ + createToolCall({ + toolName: "createExpert", + args: { + key: "new-expert", + instruction: "You are an expert", + description: "A dynamically created expert", + }, + }), + ], + toolResults: [ + createToolResult({ + toolName: "createExpert", + result: [ + { + type: "textPart", + id: "tp-1", + text: JSON.stringify({ expertKey: "new-expert@1.0.0" }), + }, + ], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(1) + expect(activities[0].type).toBe("createExpert") + if (activities[0].type === "createExpert") { + expect(activities[0].targetKey).toBe("new-expert") + expect(activities[0].description).toBe("A dynamically created expert") + expect(activities[0].resultExpertKey).toBe("new-expert@1.0.0") + expect(activities[0].error).toBeUndefined() + } + }) + + it("returns createExpert activity without optional fields", () => { + const checkpoint = createBaseCheckpoint() + const step = createBaseStep({ + toolCalls: [ + createToolCall({ + toolName: "createExpert", + args: { key: "minimal-expert", instruction: "Do stuff" }, + }), + ], + toolResults: [ + createToolResult({ + toolName: "createExpert", + result: [ + { + type: "textPart", + id: "tp-1", + text: JSON.stringify({ expertKey: "minimal-expert@1.0.0" }), + }, + ], + }), + ], + }) + + const activities = getActivities({ checkpoint, step }) + + expect(activities).toHaveLength(1) + expect(activities[0].type).toBe("createExpert") + if (activities[0].type === "createExpert") { + expect(activities[0].targetKey).toBe("minimal-expert") + expect(activities[0].description).toBeUndefined() + expect(activities[0].resultExpertKey).toBe("minimal-expert@1.0.0") + } + }) + }) + describe("unknown base tool fallback", () => { it("uses actual skillName for unknown base tools", () => { const checkpoint = createBaseCheckpoint() diff --git a/packages/core/src/utils/activity.ts b/packages/core/src/utils/activity.ts index 9f89fd0d..ba09a583 100644 --- a/packages/core/src/utils/activity.ts +++ b/packages/core/src/utils/activity.ts @@ -370,6 +370,51 @@ export function createBaseToolActivity( stderr: parseStringField(resultContents, "stderr"), } + case "addSkill": + return { + type: "addSkill", + ...baseFields, + name: String(args["name"] ?? ""), + skillType: String(args["type"] ?? ""), + tools: parseStringArrayField(resultContents, "tools"), + error: errorText, + } + + case "removeSkill": + return { + type: "removeSkill", + ...baseFields, + skillName: String(args["skillName"] ?? ""), + error: errorText, + } + + case "addDelegate": + return { + type: "addDelegate", + ...baseFields, + targetExpertKey: String(args["expertKey"] ?? ""), + delegateToolName: parseStringField(resultContents, "delegateToolName"), + error: errorText, + } + + case "removeDelegate": + return { + type: "removeDelegate", + ...baseFields, + expertName: String(args["expertName"] ?? ""), + error: errorText, + } + + case "createExpert": + return { + type: "createExpert", + ...baseFields, + targetKey: String(args["key"] ?? ""), + description: typeof args["description"] === "string" ? args["description"] : undefined, + resultExpertKey: parseStringField(resultContents, "expertKey"), + error: errorText, + } + default: // Use actual skillName from toolCall, not the constant return createGeneralToolActivity( @@ -445,6 +490,20 @@ function parseNumberField(result: MessagePart[], field: string): number | undefi } } +function parseStringArrayField(result: MessagePart[], field: string): string[] | undefined { + const textPart = result.find((p) => p.type === "textPart") + if (!textPart?.text) return undefined + try { + const parsed = JSON.parse(textPart.text) + if (Array.isArray(parsed[field])) { + return parsed[field].map(String) + } + } catch { + // Ignore parse errors + } + return undefined +} + function parseRemainingTodosFromResult( result: MessagePart[], ): Array<{ id: number; title: string; completed: boolean }> | undefined { diff --git a/packages/tui-components/src/components/checkpoint-action-row.tsx b/packages/tui-components/src/components/checkpoint-action-row.tsx index 3dc3f998..69fe1a03 100644 --- a/packages/tui-components/src/components/checkpoint-action-row.tsx +++ b/packages/tui-components/src/components/checkpoint-action-row.tsx @@ -163,6 +163,53 @@ function renderAction(action: Activity): React.ReactNode { ) + case "addSkill": { + const skillColor = action.error ? colors.destructive : colors.accent + const toolsSummary = action.tools + ? `${action.tools.length} tool${action.tools.length !== 1 ? "s" : ""}: ${action.tools.join(", ")}` + : undefined + if (toolsSummary) { + return ( + + {truncateText(toolsSummary, UI_CONSTANTS.TRUNCATE_TEXT_MEDIUM)} + + ) + } + return + } + + case "removeSkill": + return ( + + ) + + case "addDelegate": + return ( + + ) + + case "removeDelegate": + return ( + + ) + + case "createExpert": { + const expertColor = action.error ? colors.destructive : colors.accent + const summary = action.description + ? `${action.targetKey} - ${truncateText(action.description, UI_CONSTANTS.TRUNCATE_TEXT_MEDIUM)}` + : action.targetKey + return + } + case "generalTool": return ( From 24391596eda29007dfe58b1b3777c2bf0e9757f8 Mon Sep 17 00:00:00 2001 From: HiranoMasaaki Date: Wed, 18 Feb 2026 07:31:36 +0000 Subject: [PATCH 2/2] chore: add changeset for skill management activity types Co-Authored-By: Claude Opus 4.6 --- .changeset/skill-management-activities.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/skill-management-activities.md diff --git a/.changeset/skill-management-activities.md b/.changeset/skill-management-activities.md new file mode 100644 index 00000000..e7dc6a8d --- /dev/null +++ b/.changeset/skill-management-activities.md @@ -0,0 +1,6 @@ +--- +"@perstack/core": patch +"@perstack/tui-components": patch +--- + +Add dedicated Activity types for skill management tools (addSkill, removeSkill, addDelegate, removeDelegate, createExpert)