Repository navigation
feat(agent): implement Skill, SkillSet, and SkillGenerator - #3397
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces 'Skill', 'SkillSet', and 'SkillGenerator' to compile A2UI inference formats and component catalogs into monolithic or modular 'SKILL.md' packages, alongside comprehensive unit and conformance tests. The review feedback highlights a potential path traversal vulnerability in 'SkillSet.exportToDirectory' when deleting directories, recommends using 'Object.hasOwn' instead of the 'in' operator to prevent prototype lookup issues, points out a redundant check on 'rawId' in 'cleanCatalogName', and suggests removing unnecessary casting of 'fmt' in 'resolveCatalogsList'.
706adfb to
27a53fe
Compare
27a53fe to
2f2fbee
Compare
This comment was marked as outdated.
This comment was marked as outdated.
2f2fbee to
afe60be
Compare
|
/gemini review |
There was a problem hiding this comment.
Code Review
This pull request implements the skill_generator feature for the TypeScript A2UI Agent SDK, introducing the Skill, SkillSet, and SkillGenerator classes to compile inference formats and component catalogs into monolithic or modular SKILL.md markdown documents with YAML frontmatter. It also integrates the skill.yaml conformance suite and adds comprehensive unit tests. The review feedback suggests improving the robustness of the SkillGenerator API by using nullish coalescing to fall back to positional arguments when options are partially provided, and refining the directory cleanup logic in SkillSet.exportToDirectory by removing the isDirectory() check to prevent potential EEXIST errors if a file exists where a directory is expected.
226bdc5 to
6ecad46
Compare
nan-yu
left a comment
There was a problem hiding this comment.
Thanks for porting this! A few things before merge, some tied to the plan in #3416:
- Assembly drift:
generateSkillreimplements the body instead of sharing it withPromptGenerator.generate(), and the two already differ in TS (Express:### Examples:only in the skill; Direct JSON:## Workflow Description:and the schema block). Fine to land, but please don't claim parity in the description, and link #3416 so the shared assembler follows. - #3526 in TS:
DirectJsonPromptGenerator.generateCatalogInstructions(catalog)includes the protocol schemas, so a two-catalog Direct JSON monolithic skill repeats them. Either move them intogenerateBaseRules()here, or I'll cover TS in #3526. A Direct JSON two-catalog test would catch it either way. InferenceFormat.catalogs: not needed, sincepromptGenerator.catalogsis already public on the base class. Please revert the interface change and keep the formatcatalogsprivate.- API surface: can we go options-object only, with one name per method (
generateSkillSet), and droplength,items(), the positionalSkillconstructor and fuzzyget()? #3416 will reshape this API, and every alias becomes a deprecation. exportToDirectory: removal is guarded, but writing isn't.filename: '../x/SKILL.md'writes outsideoutputDir. Validatefilenameonce and use it for both.- CHANGELOG: the diff condenses existing entries and drops several BREAKING notes (Express positional args, Express validation,
A2uiRequestProcessorstate). Could this PR only add the skill line?
|
I also left my thoughts in #3416 and some action items along with the proposal. Please take a look and let me know if they all make sense to you. |
… snippet Add from_format_matches_snippet cases to conformance/agent/skill.yaml for Express and Direct JSON, with one and two catalogs. Each builds a format, generates the monolithic skill and the prompt snippet, and checks that the skill's body equals the snippet. The Python runner reads a catalogs list and the direct_json format, and fails on an unhandled action. The TS harness skips the new action until skill generation lands there (a2ui-project#3397). Refs a2ui-project#3416, a2ui-project#3464 Signed-off-by: Nan Yu <nanyu@google.com>
nan-yu
left a comment
There was a problem hiding this comment.
Thanks for the quick turnaround, everything from the last round looks good. Two small things, then I'm happy to approve:
Skill.filenamecan be changed after construction, soSkillSet.add()can take a path that skippedvalidateSkillFilename. Since thermSyncguard was removed, that now deletes outsideoutputDir(details inline).- A few TypeScript members are second names for the same thing (inline).
ca466b0 to
4d3ea55
Compare
… snippet Add from_format_matches_snippet cases to conformance/agent/skill.yaml for Express and Direct JSON, with one and two catalogs. Each builds a format, generates the monolithic skill and the prompt snippet, and checks that the skill's body equals the snippet. The Python runner reads a catalogs list and the direct_json format, and fails on an unhandled action. The TS harness skips the new action until skill generation lands there (a2ui-project#3397). Refs a2ui-project#3416, a2ui-project#3464 Signed-off-by: Nan Yu <nanyu@google.com>
…sitional fallbacks
- SkillGenerator methods take an options object only; the CatalogApi/string overloads and the structural isCatalogApi check are gone, and a missing catalog throws A2uiCatalogError. - Skill.filename and SkillSet keys are validated on construction (relative, no empty, '.' or '..' segments), so exportToDirectory relies on that instead of guarding each path. - InferenceFormat no longer declares catalogs; resolveCatalogsList reads promptGenerator.catalogs, and the formats keep their catalogs private. - Express base rules match express_base_rules.txt byte for byte; rule 15's root example is filled from the active catalogs as Python does, so the golden substitutions in the tests are gone. - The skill_set conformance case compares each skill's content against a golden via expected_skill_files.
…d, and keep one spelling per API member - Skill takes an options object only and its filename is read-only; SkillSet.add validates the filename again so a JavaScript caller who reassigns it cannot make exportToDirectory remove or write outside the output directory. - Removed the second spellings carried over from Python: generateSkillset (keep generateSkillSet), SkillSet.length (keep size) and SkillSet.items() (keep entries()).
A skill directory whose parent is a symlink out of the output directory used to be followed by rmSync, so exporting src/skills/nested/SKILL.md with out/src -> victim removed victim/skills/nested. exportToDirectory now resolves each skill directory's nearest existing ancestor and aborts, before touching anything, when it leaves the output directory. A symlink at the skill directory path itself is still removed as a link, as rmSync does, and replaced by a real directory.
c116242 to
41c0ec8
Compare
… snippet Add from_format_matches_snippet cases to conformance/agent/skill.yaml for Express and Direct JSON, with one and two catalogs. Each builds a format, generates the monolithic skill and the prompt snippet, and checks that the skill's body equals the snippet. The Python runner reads a catalogs list and the direct_json format, and fails on an unhandled action. The TS harness skips the new action until skill generation lands there (a2ui-project#3397). Refs a2ui-project#3416, a2ui-project#3464 Signed-off-by: Nan Yu <nanyu@google.com>
… snippet Add from_format_matches_snippet cases to conformance/agent/skill.yaml for Express and Direct JSON, with one and two catalogs. Each builds a format, generates the monolithic skill and the prompt snippet, and checks that the skill's body equals the snippet. The Python runner reads a catalogs list and the direct_json format, and fails on an unhandled action. The TS harness skips the new action until skill generation lands there (a2ui-project#3397). Refs a2ui-project#3416, a2ui-project#3464 Signed-off-by: Nan Yu <nanyu@google.com>
…TypeScript harness SkillGenerator landed in TypeScript (a2ui-project#3397), so the TS skill harness now runs the cases instead of skipping them: it reads a catalogs list and compares generateSkill() with promptGenerator.generate(). Also drop a duplicate unhandled-action branch left in the Python runner by the rebase. Signed-off-by: Nan Yu <nanyu@google.com>
… snippet Add from_format_matches_snippet cases to conformance/agent/skill.yaml for Express and Direct JSON, with one and two catalogs. Each builds a format, generates the monolithic skill and the prompt snippet, and checks that the skill's body equals the snippet. The Python runner reads a catalogs list and the direct_json format, and fails on an unhandled action. The TS harness skips the new action until skill generation lands there (a2ui-project#3397). Refs a2ui-project#3416, a2ui-project#3464 Signed-off-by: Nan Yu <nanyu@google.com>
…TypeScript harness SkillGenerator landed in TypeScript (a2ui-project#3397), so the TS skill harness now runs the cases instead of skipping them: it reads a catalogs list and compares generateSkill() with promptGenerator.generate(). Also drop a duplicate unhandled-action branch left in the Python runner by the rebase. Signed-off-by: Nan Yu <nanyu@google.com>
… snippet Add from_format_matches_snippet cases to conformance/agent/skill.yaml for Express and Direct JSON, with one and two catalogs. Each builds a format, generates the monolithic skill and the prompt snippet, and checks that the skill's body equals the snippet. The Python runner reads a catalogs list and the direct_json format, and fails on an unhandled action. The TS harness skips the new action until skill generation lands there (a2ui-project#3397). Refs a2ui-project#3416, a2ui-project#3464 Signed-off-by: Nan Yu <nanyu@google.com>
…TypeScript harness SkillGenerator landed in TypeScript (a2ui-project#3397), so the TS skill harness now runs the cases instead of skipping them: it reads a catalogs list and compares generateSkill() with promptGenerator.generate(). Also drop a duplicate unhandled-action branch left in the Python runner by the rebase. Signed-off-by: Nan Yu <nanyu@google.com>
… snippet Add from_format_matches_snippet cases to conformance/agent/skill.yaml for Express and Direct JSON, with one and two catalogs. Each builds a format, generates the monolithic skill and the prompt snippet, and checks that the skill's body equals the snippet. The Python runner reads a catalogs list and the direct_json format, and fails on an unhandled action. The TS harness skips the new action until skill generation lands there (a2ui-project#3397). Refs a2ui-project#3416, a2ui-project#3464 Signed-off-by: Nan Yu <nanyu@google.com>
…TypeScript harness SkillGenerator landed in TypeScript (a2ui-project#3397), so the TS skill harness now runs the cases instead of skipping them: it reads a catalogs list and compares generateSkill() with promptGenerator.generate(). Also drop a duplicate unhandled-action branch left in the Python runner by the rebase. Signed-off-by: Nan Yu <nanyu@google.com>
Implements the
skill_generatorfeature blueprint in@a2ui/agent, addingSkill,SkillSet, andSkillGeneratorso A2UI inference formats and component catalogs can be compiled into monolithic or modularSKILL.mdpackages.Changes
typescript/agents/a2ui_agent/src/skill/skill.tsandtypescript/agents/a2ui_agent/src/skill/generator.tsimplementSkill,SkillSet, andSkillGenerator:SkillGenerator.generateSkill(...)builds a single monolithicSkillfrom an initializedInferenceFormat(generateBaseRules()+generateCatalogInstructions()+generateExamples()).SkillGenerator.generateCoreSkill(...)builds a syntax-onlySkill(a2ui-core) containing the format's base rules without catalog schemas.SkillGenerator.generateCatalogSkill(...)builds a catalog-scopedSkill(a2ui-<catalog>) containing a single catalog's schema instructions and optional examples.SkillGenerator.generateSkillset(...)returns aSkillSetpairinga2ui-corewith oneSkillper catalog.Skill.toMarkdown()renders the YAML frontmatter (name,description, and optionalmetadata) followed by the Markdown body, andSkillSet.exportToDirectory(outputDir)writes<outputDir>/<skill.filename>to disk after cleaning existing subdirectories for skills in the set.typescript/agents/a2ui_agent/src/inference-format.ts,ExpressFormat, andDirectJsonFormatexposereadonly catalogs?: readonly CatalogApi[]onInferenceFormatsoSkillGeneratorcan default to the format's registered catalogs.typescript/agents/a2ui_agent/tests/conformance/loader.tsandtypescript/agents/a2ui_agent/tests/conformance/skill-conformance.test.tsenable all four conformance actions inconformance/agent/skill.yaml(from_format,core_syntax,from_catalog, andskill_set), alongside unit tests intypescript/agents/a2ui_agent/tests/unit/skill/skill-generator.test.ts.blueprints/codebases/typescript/agents/a2ui_agent/codebase.blueprint.md,KNOWN_GAPS.md, andCHANGELOG.mdrecordskill_generatorunderimplemented_featuresand document the new exports and conformance coverage.Verification
yarn --cwd typescript/agents/a2ui_agent buildyarn --cwd typescript/agents/a2ui_agent testyarn --cwd typescript/agents/a2ui_agent lintyarn --cwd typescript/agents/a2ui_agent format:checkpython3 blueprints/validate_blueprints.pyOne time:
For this PR:
If you need help, consider asking for advice on the discussion board.