Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/skill-management-activities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@perstack/core": patch
"@perstack/tui-components": patch
---

Add dedicated Activity types for skill management tools (addSkill, removeSkill, addDelegate, removeDelegate, createExpert)
90 changes: 90 additions & 0 deletions packages/core/src/schemas/activity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,86 @@ export const generalToolActivitySchema = baseActivitySchema.extend({
})
generalToolActivitySchema satisfies z.ZodType<GeneralToolActivity>

/** 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<AddSkillActivity>

/** 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<RemoveSkillActivity>

/** 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<AddDelegateActivity>

/** 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<RemoveDelegateActivity>

/** 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<CreateExpertActivity>

/** Union of all activity types */
export type Activity =
| QueryActivity
Expand All @@ -330,6 +410,11 @@ export type Activity =
| DelegationCompleteActivity
| InteractiveToolActivity
| GeneralToolActivity
| AddSkillActivity
| RemoveSkillActivity
| AddDelegateActivity
| RemoveDelegateActivity
| CreateExpertActivity

export const activitySchema = z.discriminatedUnion("type", [
queryActivitySchema,
Expand All @@ -349,6 +434,11 @@ export const activitySchema = z.discriminatedUnion("type", [
delegationCompleteActivitySchema,
interactiveToolActivitySchema,
generalToolActivitySchema,
addSkillActivitySchema,
removeSkillActivitySchema,
addDelegateActivitySchema,
removeDelegateActivitySchema,
createExpertActivitySchema,
])
activitySchema satisfies z.ZodType<Activity>

Expand Down
244 changes: 244 additions & 0 deletions packages/core/src/utils/activity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Loading