Skip to content

feat(kit): add core skill plugin capabilities#368

Open
gene9831 wants to merge 5 commits into
opentiny:developfrom
gene9831:codex/skill-plugin-core
Open

feat(kit): add core skill plugin capabilities#368
gene9831 wants to merge 5 commits into
opentiny:developfrom
gene9831:codex/skill-plugin-core

Conversation

@gene9831

@gene9831 gene9831 commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

PR 描述

新增 core skillPlugin 与 skill capabilities,将已选择的 skills 转换为 system instructions 和 runtime tools,接入 message engine。

主要改动:

  • 新增 core message/plugins/skillPlugin.ts
  • 支持 manual / auto / none 三种 request-level skill selection。
  • manual 模式支持直接传入完整 SkillDefinition[],或通过 skillNames + getSkillByName 解析。
  • auto 模式支持候选 skill summaries、偏好 skill names、select_skills runtime tool,以及选择后解析完整 skill。
  • 新增 SkillRequestContext,记录 skillsskillNamesrequestedSkillNamesunresolvedSkillNamesruntimeTools 和 selection 状态。
  • 新增 resource capability,提供 list_skill_files / read_skill_file runtime tools,并在 system instruction 中提示先 list 再 read。
  • 新增 command capability 类型占位,暂不实现 execute_skill_command
  • 新增 getUniqueStringArray 通用工具函数。
  • 增加 resource capability 和 skillPlugin 关键流程测试。

验证:

pnpm -F @opentiny/tiny-robot-kit test -- --run src/skills/test/resourceCapability.test.ts src/skills/test/skillPlugin.test.ts src/message/test/toolPlugin.test.ts
pnpm build

Summary by CodeRabbit

  • New Features

    • Added skill selection support for manual, automatic, and disabled modes.
    • Added skill resource tools so available skill files can be listed and read during a conversation.
    • Exposed shared utilities for working with skill commands and safer argument parsing.
    • Added a helper for returning unique string lists.
  • Bug Fixes

    • Improved handling of invalid tool arguments and missing skill/resource inputs with clearer stable errors.
  • Tests

    • Added coverage for skill selection, resource access, and plugin message behavior.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

✅ Preview build completed successfully!

Click the image above to preview.
Preview will be automatically removed when this PR is closed.

@github-actions

github-actions Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

Adds a new skillPlugin message-engine plugin supporting manual, auto, and none skill-selection modes, backed by new capability modules for skill resource listing/reading, skill selection tools, and command types. Adds supporting utilities (parseToolArguments, getUniqueStringArray), re-exports, tests, and a minor formatting change in message/utils.ts.

Changes

Skill Plugin and Capabilities

Layer / File(s) Summary
Skill capability modules
packages/kit/src/skills/capabilities/selection.ts, resources.ts, commands.ts, utils.ts
Adds createSkillSelectionInstructionsMessage/createSkillSelectionRuntimeTools for the select_skills tool, hasSkillResources/createSkillResourceInstructionsMessage/createSkillResourceRuntimeTools for list_skill_files/read_skill_file tools, SkillCommandRequest/SkillCommandResult/SkillCommandExecutor types, and parseToolArguments for parsing tool call JSON arguments.
skillPlugin implementation and wiring
packages/kit/src/message/plugins/skillPlugin.ts, index.ts, packages/kit/src/utils.ts, packages/kit/src/message/utils.ts
Implements skillPlugin with provideTools, onTurnStart, and onBeforeRequest handling manual/auto/none selection modes, resolving skills, storing request context, and injecting instructions/tools into messages; adds getUniqueStringArray utility, re-exports from plugins index, and a minor formatting tweak in normalizeToAsyncGenerator.
Tests for capability tools and plugin behavior
packages/kit/src/skills/test/resourceCapability.test.ts, skillPlugin.test.ts
Vitest suites validate resource tool handlers (list/read files, error cases) and skillPlugin behavior across manual, auto, and none selection modes including hook invocation and message/tool injection.

Sequence Diagram(s)

sequenceDiagram
  participant Engine as Message Engine
  participant Plugin as skillPlugin
  participant Model
  participant Tools as Runtime Tools

  Engine->>Plugin: onTurnStart(selection options)
  alt manual
    Plugin->>Plugin: resolve skills by name
  else auto
    Plugin->>Plugin: normalize candidates, store selecting phase
  else none
    Plugin->>Plugin: store empty skill context
  end
  Engine->>Plugin: onBeforeRequest
  alt auto & selecting
    Plugin->>Model: append select_skills instructions
    Model->>Tools: call select_skills
    Tools->>Plugin: resolve skill names, update context
  end
  Plugin->>Model: append skill + resource instructions
  Model->>Tools: call list_skill_files / read_skill_file
  Tools->>Model: return file summaries / content
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • opentiny/tiny-robot#365: The new skillPlugin's runtime tools (select_skills, list_skill_files, read_skill_file) depend on this PR's toolPlugin runtime tool provisioning and dispatching mechanisms.

Suggested reviewers

  • SonyLeo

A rabbit hops through skills anew,
Selecting, reading, tools that flew.
Auto or manual, files unfurled,
Instructions whispered to the model's world.
🐰📂 Hop, parse, and choose with glee!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main addition: core skill plugin capabilities in kit.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gene9831 gene9831 marked this pull request as ready for review June 30, 2026 08:15
@gene9831

gene9831 commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/kit/src/skills/capabilities/selection.ts (1)

10-37: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an empty-candidates guard for consistency with createSkillResourceInstructionsMessage.

createSkillSelectionRuntimeTools already short-circuits when candidates.length === 0 (no tool is exposed), but createSkillSelectionInstructionsMessage has no equivalent guard — it will still instruct the model to "Use the select_skills tool before answering" even when no tool is actually registered. The resource capability module (resources.ts) applies the same guard (hasSkillResources) symmetrically to both its instructions and runtime-tools functions; this module doesn't.

If callers in skillPlugin.ts always supply non-empty candidates before invoking this function, this is currently unreachable, but adding the guard removes the fragile cross-file assumption.

♻️ Suggested guard
 export const createSkillSelectionInstructionsMessage = ({
   candidates,
   preferredSkillNames,
 }: {
   candidates: SkillCandidate[]
   preferredSkillNames?: string[]
 }): ChatCompletionSystemMessageParam => {
+  if (candidates.length === 0) {
+    return undefined
+  }
+
   const lines = [

Note: this would also require updating the return type to ChatCompletionSystemMessageParam | undefined, matching createSkillResourceInstructionsMessage's signature.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/kit/src/skills/capabilities/selection.ts` around lines 10 - 37,
`createSkillSelectionInstructionsMessage` should mirror the empty-candidates
guard used by `createSkillSelectionRuntimeTools` and
`createSkillResourceInstructionsMessage`. Add a check for `candidates.length ===
0` at the start of the function, return `undefined` in that case, and update the
return type accordingly to `ChatCompletionSystemMessageParam | undefined`. Keep
the rest of the message-building logic in `selection.ts` unchanged for non-empty
candidates.
packages/kit/src/skills/capabilities/resources.ts (1)

11-51: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Consider restricting skillName to the active skill names via enum, mirroring selection.ts.

select_skills's parameter schema constrains skillNames via enum: candidateNames (selection.ts:74), but list_skill_files/read_skill_file's skillName parameter here has no such constraint, even though only names present in the current skills array are ever valid (see findSkill). Since these tools are already built per-call inside createSkillResourceRuntimeTools(skills), the schema could be generated dynamically with enum: skills.map((skill) => skill.name) to reduce hallucinated skill names, consistent with the guidance already established in selection.ts.

Handler-side validation (skill_not_found) already covers correctness, so this is purely a model-steering improvement, not a functional gap.

Also applies to: 93-108

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/kit/src/skills/capabilities/resources.ts` around lines 11 - 51, The
`skillName` parameter in `skillResourceTools` is currently unconstrained, which
can lead the model to invent invalid names. Update
`createSkillResourceRuntimeTools(skills)` so the schemas for `listSkillFiles`
and `readSkillFile` set `skillName` to an `enum` built from `skills.map((skill)
=> skill.name)`, matching the pattern used in `selection.ts` for
`select_skills`. Keep the existing `findSkill`/`skill_not_found` runtime
validation unchanged, and apply the same schema change anywhere the same tool
definitions are duplicated.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/kit/src/message/plugins/skillPlugin.ts`:
- Around line 234-258: In resolveSkillsByNames, avoid marking a skill as
unresolved when getSkillByName returns a valid SkillDefinition whose skill.name
differs from the requested name; track resolution status from the actual lookup
result rather than only the requested name. Update the unresolvedSkillNames
bookkeeping so aliases/normalized names don’t create contradictory state, and
ensure onSkillsResolved/SkillRequestContext only receive truly unresolved
entries. Also distinguish resolver failures in the try/catch from “not found”
cases by preserving the failure path in the results object instead of collapsing
every exception into a generic failed flag.

---

Nitpick comments:
In `@packages/kit/src/skills/capabilities/resources.ts`:
- Around line 11-51: The `skillName` parameter in `skillResourceTools` is
currently unconstrained, which can lead the model to invent invalid names.
Update `createSkillResourceRuntimeTools(skills)` so the schemas for
`listSkillFiles` and `readSkillFile` set `skillName` to an `enum` built from
`skills.map((skill) => skill.name)`, matching the pattern used in `selection.ts`
for `select_skills`. Keep the existing `findSkill`/`skill_not_found` runtime
validation unchanged, and apply the same schema change anywhere the same tool
definitions are duplicated.

In `@packages/kit/src/skills/capabilities/selection.ts`:
- Around line 10-37: `createSkillSelectionInstructionsMessage` should mirror the
empty-candidates guard used by `createSkillSelectionRuntimeTools` and
`createSkillResourceInstructionsMessage`. Add a check for `candidates.length ===
0` at the start of the function, return `undefined` in that case, and update the
return type accordingly to `ChatCompletionSystemMessageParam | undefined`. Keep
the rest of the message-building logic in `selection.ts` unchanged for non-empty
candidates.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: db793450-50c7-4d95-8bf0-0c15f4acbbce

📥 Commits

Reviewing files that changed from the base of the PR and between 79556a0 and 1514455.

📒 Files selected for processing (10)
  • packages/kit/src/message/plugins/index.ts
  • packages/kit/src/message/plugins/skillPlugin.ts
  • packages/kit/src/message/utils.ts
  • packages/kit/src/skills/capabilities/commands.ts
  • packages/kit/src/skills/capabilities/resources.ts
  • packages/kit/src/skills/capabilities/selection.ts
  • packages/kit/src/skills/capabilities/utils.ts
  • packages/kit/src/skills/test/resourceCapability.test.ts
  • packages/kit/src/skills/test/skillPlugin.test.ts
  • packages/kit/src/utils.ts

Comment on lines +234 to +258
const resolveSkillsByNames = async (
skillNames: string[],
getSkillByName: SkillResolver['getSkillByName'],
context: BasePluginContext,
) => {
const results = await Promise.all(
skillNames.map(async (name) => {
try {
return { name, skill: await getSkillByName(name, context) }
} catch {
return { name, failed: true }
}
}),
)

const skills = results.map((result) => result.skill).filter((skill): skill is SkillDefinition => Boolean(skill))
const resolvedSkillNameSet = new Set(skills.map((skill) => skill.name))

return {
skills,
unresolvedSkillNames: results
.filter((result) => result.failed || !result.skill || !resolvedSkillNameSet.has(result.name))
.map((result) => result.name),
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Possible double-bookkeeping when getSkillByName returns a skill with a different .name than requested.

unresolvedSkillNames is derived via resolvedSkillNameSet.has(result.name) (the requested name), while skills is populated from result.skill regardless of whether skill.name matches the requested name. If a resolver ever normalizes/aliases names (e.g. case-insensitive lookup), the same skill would end up both enabled (skills) and reported as unresolved (unresolvedSkillNames) — contradictory state surfaced to onSkillsResolved/SkillRequestContext.

Separately, the catch block (lines 241-245) treats genuine resolver failures (e.g. storage unavailable, per the test at skillPlugin.test.ts:178-180) identically to "skill not found" with no error detail preserved — this can mask real operational issues from anything relying on unresolvedSkillNames alone.

🐛 Proposed fix for the name-mismatch bookkeeping issue
   const skills = results.map((result) => result.skill).filter((skill): skill is SkillDefinition => Boolean(skill))
-  const resolvedSkillNameSet = new Set(skills.map((skill) => skill.name))
+  const resolvedNameByRequestedName = new Map(
+    results.filter((result) => result.skill).map((result) => [result.name, result.skill]),
+  )

   return {
-    skills,
+    skills: [...resolvedNameByRequestedName.values()] as SkillDefinition[],
     unresolvedSkillNames: results
-      .filter((result) => result.failed || !result.skill || !resolvedSkillNameSet.has(result.name))
+      .filter((result) => result.failed || !result.skill)
       .map((result) => result.name),
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/kit/src/message/plugins/skillPlugin.ts` around lines 234 - 258, In
resolveSkillsByNames, avoid marking a skill as unresolved when getSkillByName
returns a valid SkillDefinition whose skill.name differs from the requested
name; track resolution status from the actual lookup result rather than only the
requested name. Update the unresolvedSkillNames bookkeeping so
aliases/normalized names don’t create contradictory state, and ensure
onSkillsResolved/SkillRequestContext only receive truly unresolved entries. Also
distinguish resolver failures in the try/catch from “not found” cases by
preserving the failure path in the results object instead of collapsing every
exception into a generic failed flag.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant