Compare commits
435 Commits
ba665528ed
..
v0.5.0
| Author | SHA1 | Date | |
|---|---|---|---|
| b1cd8351fa | |||
| ccf5e73341 | |||
| be5d280c32 | |||
| 6633a8c0bf | |||
| 097d8936e3 | |||
| 8a53b7934b | |||
| 0facb15e32 | |||
| c172736362 | |||
| 4a2b9fa42a | |||
| 98db37866c | |||
| ad31fbd169 | |||
| d69e28fd39 | |||
| 279eaa5300 | |||
| e687d78931 | |||
| 0c2e4df647 | |||
| 6221875f64 | |||
| 895b9c27db | |||
| e661ca2eda | |||
| 7066edd904 | |||
| 61bdf29bea | |||
| ef39c7d9ff | |||
| e9e46158e7 | |||
| 34dc4b0dce | |||
| cd226577e7 | |||
| b5fc633454 | |||
| 484b18ef16 | |||
| 7333046cfe | |||
| 815f0e5c39 | |||
| dacccbfcf7 | |||
| 5370637274 | |||
| e6da252a5a | |||
| 4aaff21f45 | |||
| 2678afe02b | |||
| 558b764db8 | |||
| 0bb312a85c | |||
| d81d233527 | |||
| 597f823bdf | |||
| 81c037515e | |||
| 3c7d19da07 | |||
| 4536d00067 | |||
| 98d16d9a56 | |||
| 26de81e84e | |||
| 20c28b55d5 | |||
| 7d6f1dda26 | |||
| 9a061944ae | |||
| 1f50af0974 | |||
| bdacf9fc78 | |||
| a9f2a5edc2 | |||
| 2df8b1a541 | |||
| de055bf8a4 | |||
| 8fb0eece4b | |||
| ba03c3037d | |||
| afa0e4af67 | |||
| 5a9a00bc6f | |||
| e7bb668ac7 | |||
| 04498b96ec | |||
| eb2843d38a | |||
| 696ce03ee4 | |||
| a3d67bfbf7 | |||
| 5bd0766a60 | |||
| 35e1b14843 | |||
| 503c9b4699 | |||
| 7a8b09542d | |||
| da5cd21c1c | |||
| 27fcb1fc15 | |||
| e292c414c5 | |||
| 8a2f18204f | |||
| c70ac98223 | |||
| 249d1fc881 | |||
| 3f4fd91b3f | |||
| 48c52b5829 | |||
| f58f751c59 | |||
| fc7fdc98b4 | |||
| f4d7d0fb73 | |||
| 4b38f53488 | |||
| 186422ff58 | |||
| 9bc4f8b621 | |||
| 84497d3d65 | |||
| 3ea9116a23 | |||
| bfcd73c32a | |||
| 3cd3ba55ff | |||
| 3535edba79 | |||
| bf0343e245 | |||
| b001ae4c18 | |||
| 9ce088a530 | |||
| 16f3f71188 | |||
| 0af5fa02f9 | |||
| d6a0676264 | |||
| b582bab17c | |||
| a8732c63d6 | |||
| 389d0b768f | |||
| 70a251a7e2 | |||
| 462f136596 | |||
| bf9d7d750e | |||
| 540ec648c9 | |||
| e69352ee2d | |||
| ee4e3bc13f | |||
| a576961bd6 | |||
| 59c7fc1276 | |||
| bcf512fcfc | |||
| 195401c496 | |||
| 34d8d20ec6 | |||
| 08ba6f0446 | |||
| 26984892af | |||
| 526a426073 | |||
| c53e0546d4 | |||
| 349b3748bd | |||
| e23e5f9f7b | |||
| 8d02782de6 | |||
| 27ceefdb40 | |||
| 5168eb6781 | |||
| ddb73a9a33 | |||
| 53eff10d75 | |||
| 1df6114ff3 | |||
| 975484cc2b | |||
| 0421c9b643 | |||
| fb69c21252 | |||
| 0cb9122d16 | |||
| c164ad3cbb | |||
| 9b4171a468 | |||
| 5cae4e44fb | |||
| a145a42b2b | |||
| 715807645a | |||
| 1259c6865f | |||
| ff42460cb4 | |||
| 39a16f8d56 | |||
| 83de60f59c | |||
| cf60e090a5 | |||
| 0fb37c33ab | |||
| d81508c22a | |||
| 883ac659b2 | |||
| c6c10b5e24 | |||
| a4e5bef1b7 | |||
| f72c7b03f9 | |||
| bd6f709374 | |||
| 00f2201157 | |||
| b3f0d66071 | |||
| 8730d413bc | |||
| 79140fda3c | |||
| 67e749ea3a | |||
| 7bcfc133ae | |||
| e3e246607e | |||
| 16104cb2c5 | |||
| 224e51c386 | |||
| b022ca089c | |||
| 0ebb761c09 | |||
| c8067828d5 | |||
| 30eedd9b8c | |||
| d701b45057 | |||
| 722c9c101e | |||
| 86aa45f0c4 | |||
| cf45dc4820 | |||
| db77034431 | |||
| abdaec11b0 | |||
| 95fb349656 | |||
| d0b6b6c324 | |||
| d74c23ccf5 | |||
| ea1cfda0d6 | |||
| 5623f47f9a | |||
| e4df9ec193 | |||
| a6306d6b76 | |||
| 64529ba5cc | |||
| cc7f963b89 | |||
| 0ce86af116 | |||
| 2cb0ed3f64 | |||
| fb61854f11 | |||
| 53ba3344b1 | |||
| e20c8be8bb | |||
| 894dcb1d3c | |||
| 9a9e890f8a | |||
| 818ea634f0 | |||
| 780460f8d8 | |||
| e19483a920 | |||
| aca93f1cae | |||
| 1371a4aad2 | |||
| db4a45c0f6 | |||
| e95b1e5f82 | |||
| 15f4008f4b | |||
| f45f81fb45 | |||
| 2220fd2542 | |||
| 564480e165 | |||
| 297c63d91a | |||
| 26e2cd3f65 | |||
| 9f899466d4 | |||
| 38393ea4cf | |||
| a4f25826e3 | |||
| 93484fb33f | |||
| c90f003f92 | |||
| 24793b9b8d | |||
| 78e772f455 | |||
| 1e0d269aad | |||
| f6b1d408fc | |||
| 442b318b6c | |||
| a7c97aedb7 | |||
| 746f9e7b24 | |||
| 0d6c61af5c | |||
| 673f31c059 | |||
| 369a4f0a89 | |||
| 8d54eae4d0 | |||
| a805d5beab | |||
| dbb2aec8b6 | |||
| 1a98b76a1f | |||
| 51d10ab2b5 | |||
| 1aad750395 | |||
| e0aab6bd02 | |||
| 6cb93132b7 | |||
| 04126b99d6 | |||
| 0794eb960d | |||
| d619ad1d48 | |||
| 5b147e07b3 | |||
| 944ce441d8 | |||
| a7dcb8519b | |||
| d912d44fb3 | |||
| 4f7254a634 | |||
| bf923cb296 | |||
| d9f737e1bf | |||
| 59690d045e | |||
| 5d95acba53 | |||
| d46225d2a9 | |||
| 3af30a0e62 | |||
| 69eca4d96d | |||
| 7b2e4a83c9 | |||
| 344b80872a | |||
| ddf828ff5f | |||
| 4e170b069b | |||
| 22c75fb578 | |||
| 11ab9eb6b8 | |||
| 29b232f407 | |||
| 53e8c920e5 | |||
| 78d19bed4d | |||
| 10f4160635 | |||
| 7622836e8b | |||
| 4d4713a9fa | |||
| 25008599f9 | |||
| c00ab074f8 | |||
| aed1f1957f | |||
| c6a959e2e1 | |||
| 02b7ed37f6 | |||
| 0d84aaabb9 | |||
| 6efdcf9610 | |||
| 4266d317d8 | |||
| 4ce7aafcbd | |||
| 35d8b69f92 | |||
| 562057e608 | |||
| b7024e5340 | |||
| 088588231b | |||
| eff117d3d9 | |||
| 968c535709 | |||
| c8b6fa7b11 | |||
| 0aa334b54e | |||
| 78a49f841d | |||
| 43b2bd937e | |||
| a4326875ba | |||
| eb31a58346 | |||
| a6b0acc35d | |||
| cc7fcd0b5b | |||
| 02fe59b913 | |||
| 6fd5f47089 | |||
| 2a2922760e | |||
| a3793460fd | |||
| e0927a04d9 | |||
| 8665604bab | |||
| d4c3c135b3 | |||
| 60bd5e493c | |||
| 0753b2d841 | |||
| 17e6fbd692 | |||
| 0710441650 | |||
| 20a76cee3e | |||
| cb64785867 | |||
| e6e26103c4 | |||
| 15529a14f1 | |||
| 86839188e0 | |||
| 39701b378b | |||
| 45ff6da737 | |||
| a260dd1503 | |||
| 57859301df | |||
| 8c968d3f53 | |||
| 0034bfbe46 | |||
| a733b9247a | |||
| e0afa349b9 | |||
| 7d0ce94907 | |||
| 9045763c35 | |||
| 29898552d7 | |||
| 9d7c2f5c2f | |||
| 5c0fa42351 | |||
| ab045b0ef3 | |||
| 41e6843db1 | |||
| 911ec3c9b9 | |||
| fc6f0a1a7b | |||
| 21873da278 | |||
| d1cd6be2c9 | |||
| 0c0ae41bca | |||
| c9ed7a904a | |||
| d200a8f554 | |||
| 3d04c8fcf1 | |||
| f53f165d91 | |||
| e5645e4064 | |||
| 95e15ca8c4 | |||
| dbf7329e87 | |||
| ed6c3ae431 | |||
| 214d2ecc67 | |||
| 29c95671de | |||
| 238f93a096 | |||
| c76877e7b3 | |||
| 12e5a9c5aa | |||
| 7f4be2ca3f | |||
| 29ffe12d8c | |||
| d34bed4f15 | |||
| aec7ea7e80 | |||
| 5938e1af29 | |||
| 60902297c5 | |||
| 12a95aa6fa | |||
| 78fc459a97 | |||
| 281565804c | |||
| 33a32fd9c8 | |||
| b64aad55e9 | |||
| 2392958114 | |||
| ec04e8e24a | |||
| 4e14ee7f50 | |||
| 7ba4ab0608 | |||
| fd816112fb | |||
| d0ee85be40 | |||
| 9448704af3 | |||
| 9dad9d6ca8 | |||
| 3f41abed7c | |||
| debcbab445 | |||
| 7fcabf1de7 | |||
| e116a1841d | |||
| cd3103ca14 | |||
| 50d07a4b13 | |||
| ed1352936e | |||
| f4b4156a0c | |||
| 5cf2cce0e3 | |||
| 249453d829 | |||
| c14939cecc | |||
| 72f516abb1 | |||
| 66478ed264 | |||
| 6b10dff41d | |||
| f8cc736482 | |||
| a0794fecfc | |||
| c68059e5b3 | |||
| 832ca6b0de | |||
| 89ee43830e | |||
| f7cf13901e | |||
| ad41fa93fb | |||
| 617b7dcd49 | |||
| 417ea032c4 | |||
| b77bb6e200 | |||
| 1fa3b4a600 | |||
| 99bd502f62 | |||
| 25a271dc95 | |||
| 5002ac7716 | |||
| d92a559460 | |||
| 3d571e1a31 | |||
| d338daa4b6 | |||
| 6f802c2a58 | |||
| a3f0168817 | |||
| 677702655f | |||
| b0bbd0c083 | |||
| 5cbf23a1f4 | |||
| 39eb9b34ec | |||
| 5da8616518 | |||
| b267fe05cd | |||
| 29f7ebe559 | |||
| bbffaca511 | |||
| 80532836c3 | |||
| 9474f4f322 | |||
| 93a09d3a9f | |||
| e3935ce699 | |||
| 58c15e7833 | |||
| fd2b7f3aa0 | |||
| 5ccbc629d1 | |||
| e98ff5e8e5 | |||
| a6fffa7b57 | |||
| 3ac153dd06 | |||
| 8db3108c94 | |||
| e25ff4ad19 | |||
| 21e76c6461 | |||
| 103aa1a432 | |||
| d2f4fefcf3 | |||
| 629527988d | |||
| 7f520f1346 | |||
| e28619b55a | |||
| f474e6130e | |||
| 4b5bcb45ac | |||
| 50565a0f17 | |||
| cf37db4fa2 | |||
| ad9b4097ef | |||
| c22c01c6c3 | |||
| 31f7f50c4a | |||
| a7f6ed4b16 | |||
| 73ada5a221 | |||
| 2f96256893 | |||
| 23d9e0775f | |||
| 72ade39144 | |||
| ec64c68777 | |||
| 80932e069f | |||
| 2f9b154b07 | |||
| 20bf911732 | |||
| 65a3dbb228 | |||
| 5844cc93ca | |||
| 4d23ce58c4 | |||
| 2bb592d5f6 | |||
| 3146b20c15 | |||
| 455cf67750 | |||
| a6d6a877b0 | |||
| a7bd54471c | |||
| fe5f803163 | |||
| 66a9b5362a | |||
| f3569cf68b | |||
| 2573f14726 | |||
| f1fb2d6abf | |||
| 4934e0ff0a | |||
| f772a80501 | |||
| 8950843be2 | |||
| 9b89e68908 | |||
| ba134ca53f | |||
| 21dbd9c057 | |||
| 40a68f8e05 | |||
| 37d861a631 | |||
| 31f3e885ce | |||
| 7ffaab2012 | |||
| 35b7946b0d | |||
| 3a05a8e712 | |||
| 294a1149ef | |||
| 8d80370014 | |||
| 1cbdef36cf | |||
| 4c8accbfc1 | |||
| c4c2d9cb93 | |||
| 7aed112326 | |||
| 216a3d53cd | |||
| e0823b343b | |||
| cb0bc65ee4 | |||
| 5b9ab6636f | |||
| 9fd77feebb |
@@ -37,7 +37,6 @@ Coming from [AIChat](https://github.com/sigoden/aichat)? Follow the [migration g
|
|||||||
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
* [RAG](https://github.com/Dark-Alex-17/coyote/wiki/RAG): Retrieval-Augmented Generation for enhanced information retrieval and generation.
|
||||||
* [Sessions](https://github.com/Dark-Alex-17/coyote/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
|
* [Sessions](https://github.com/Dark-Alex-17/coyote/wiki/Sessions): Manage and persist conversational contexts and settings across multiple interactions.
|
||||||
* [Roles](https://github.com/Dark-Alex-17/coyote/wiki/Roles): Customize model behavior for specific tasks or domains.
|
* [Roles](https://github.com/Dark-Alex-17/coyote/wiki/Roles): Customize model behavior for specific tasks or domains.
|
||||||
* [Skills](https://github.com/Dark-Alex-17/coyote/wiki/Skills): Modular knowledge or capability packs the LLM can load and unload mid-conversation. Multiple skills compose; instructions stack, tools and MCPs union.
|
|
||||||
* [Agents](https://github.com/Dark-Alex-17/coyote/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
* [Agents](https://github.com/Dark-Alex-17/coyote/wiki/Agents): Leverage AI agents to perform complex tasks and workflows, including sub-agent spawning, teammate messaging, and user interaction tools.
|
||||||
* [Graph Agents](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): Define an agent as a declarative, YAML-driven workflow. A directed graph of typed nodes (LLM calls, scripts, approvals, user input, RAG retrieval, sub-agent spawns).
|
* [Graph Agents](https://github.com/Dark-Alex-17/coyote/wiki/Graph-Agents): Define an agent as a declarative, YAML-driven workflow. A directed graph of typed nodes (LLM calls, scripts, approvals, user input, RAG retrieval, sub-agent spawns).
|
||||||
* [Todo System](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System): Built-in task tracking for improved LLM reliability with smaller models.
|
* [Todo System](https://github.com/Dark-Alex-17/coyote/wiki/TODO-System): Built-in task tracking for improved LLM reliability with smaller models.
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
---
|
|
||||||
description: Detect and remove AI slop from code and prose; produce output indistinguishable from a senior engineer's.
|
|
||||||
---
|
|
||||||
You are reviewing or generating content. Apply these standards strictly. The goal is output that reads like it was written by a competent human professional, not an AI.
|
|
||||||
|
|
||||||
## Code
|
|
||||||
|
|
||||||
**No useless comments.** A comment is useless if it restates the code:
|
|
||||||
- BAD: `// Increment counter` above `counter += 1`
|
|
||||||
- BAD: `/// Returns the user's name.` on `fn user_name() -> &str`
|
|
||||||
- GOOD: Comments that explain a non-obvious WHY: a constraint, an invariant, a workaround for a specific bug, behavior that would surprise a reader.
|
|
||||||
|
|
||||||
If removing a comment wouldn't confuse a future reader, the comment shouldn't exist.
|
|
||||||
|
|
||||||
**No emojis** unless the user explicitly asked for them.
|
|
||||||
|
|
||||||
**No defensive handling for impossible cases.** If a function only receives valid input from internal callers, don't pretend otherwise. Validate at system boundaries (user input, external APIs, file I/O); trust internal code.
|
|
||||||
|
|
||||||
**No over-engineering for hypothetical futures.** Three similar lines of code is fine. Premature abstractions are worse than duplication.
|
|
||||||
|
|
||||||
**No backwards-compatibility cruft for unreleased code.** If a function isn't called yet, just change it. Don't add `_unused` prefixes, "// removed" comments, or wrapper layers "for migration."
|
|
||||||
|
|
||||||
**Names should be honest.** A function called `get_user` should not mutate state. A field called `count` should not be a function. A method that can fail should return `Result`, not panic.
|
|
||||||
|
|
||||||
## Prose
|
|
||||||
|
|
||||||
**No flattery.** Don't start with "Great question!" or "That's a really good idea!" Just respond.
|
|
||||||
|
|
||||||
**No filler.** "It's important to note that" — delete. "Let me explain" — just explain. "I'll go ahead and" — just do it.
|
|
||||||
|
|
||||||
**No status updates.** "I'm going to help you with that" — just help.
|
|
||||||
|
|
||||||
**Match the user's terseness.** Brief user, brief reply. Detailed user, detailed reply.
|
|
||||||
|
|
||||||
**No multi-paragraph docstrings.** One short line max. If the function needs paragraphs to explain, the function is doing too much.
|
|
||||||
|
|
||||||
## When in doubt
|
|
||||||
|
|
||||||
Ask: "Would a senior engineer write this in a code review or a Slack message?" If not, cut it.
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
---
|
|
||||||
description: Conduct a thorough code review focused on correctness, clarity, tests, and footguns. Grants read-only filesystem access for inspecting code.
|
|
||||||
enabled_tools: fs_read, fs_grep, fs_glob, fs_cat, fs_ls
|
|
||||||
---
|
|
||||||
You are reviewing code. Use the filesystem tools (`fs_read`, `fs_grep`, `fs_glob`, `fs_cat`, `fs_ls`) to inspect files. Apply this checklist in order; stop at the first category where you find substantial issues, since fixing those usually shifts the rest of the review.
|
|
||||||
|
|
||||||
## Investigation workflow
|
|
||||||
|
|
||||||
Before reviewing the diff, build a mental model of the surrounding code:
|
|
||||||
|
|
||||||
- `fs_ls` the directories that contain the changed files.
|
|
||||||
- `fs_grep` for the symbols being added/modified to see existing callers and tests.
|
|
||||||
- `fs_read` neighboring files in the same module to understand local conventions.
|
|
||||||
- `fs_glob` for test files that might cover this area.
|
|
||||||
|
|
||||||
A review without context is just a syntax check.
|
|
||||||
|
|
||||||
## 1. Correctness
|
|
||||||
|
|
||||||
- Does the change actually do what it claims? Does it solve the stated problem?
|
|
||||||
- Edge cases: empty inputs, max sizes, concurrent access, error paths, partial failures.
|
|
||||||
- Off-by-one errors, type confusion, null/None handling, integer overflow.
|
|
||||||
- Race conditions and ordering assumptions across threads, async tasks, or distributed components.
|
|
||||||
- Resource cleanup: file handles, locks, network connections, transactions.
|
|
||||||
|
|
||||||
## 2. Tests
|
|
||||||
|
|
||||||
- Do the tests test BEHAVIOR, not implementation? (Tests of `private_helper()` are usually a smell.)
|
|
||||||
- Will they fail when the code regresses? Or are they tautological (e.g., `assert!(x.is_empty() || !x.is_empty())`)?
|
|
||||||
- Do they cover the unhappy paths, not just the happy ones?
|
|
||||||
- Is there a missing test for the specific bug or feature being added? `fs_grep` for the function name in test files to check.
|
|
||||||
|
|
||||||
## 3. Clarity
|
|
||||||
|
|
||||||
- Are names accurate? `get_user` that mutates is a lie; rename or split.
|
|
||||||
- Could a competent reader understand this without comments?
|
|
||||||
- Is there a simpler way to express the same logic?
|
|
||||||
- Is the function doing one thing, or several things glued together?
|
|
||||||
|
|
||||||
## 4. Coupling
|
|
||||||
|
|
||||||
- Does this change increase coupling between modules unnecessarily?
|
|
||||||
- Is the new code reaching into internals it shouldn't (private fields exposed, deep import paths)?
|
|
||||||
- Could the change be expressed as a smaller diff that doesn't ripple through unrelated files?
|
|
||||||
|
|
||||||
## 5. Footguns
|
|
||||||
|
|
||||||
- Could a future maintainer easily misuse this API?
|
|
||||||
- Are invariants enforced by types, or just by convention?
|
|
||||||
- Are error types specific enough to be actionable?
|
|
||||||
- Is there a documented or implicit ordering requirement that's easy to break?
|
|
||||||
|
|
||||||
## What to flag
|
|
||||||
|
|
||||||
- Correctness bugs.
|
|
||||||
- Missing error handling at trust boundaries.
|
|
||||||
- Race conditions.
|
|
||||||
- Tests that won't catch regressions.
|
|
||||||
- Security issues (injection, auth, exposed secrets).
|
|
||||||
|
|
||||||
## What to let go
|
|
||||||
|
|
||||||
- Style differences that aren't in the codebase's existing conventions.
|
|
||||||
- "I would have done it differently" preferences.
|
|
||||||
- Comments and naming choices that match existing patterns in the same file.
|
|
||||||
- Micro-optimizations in code that isn't on a hot path.
|
|
||||||
|
|
||||||
## Tone
|
|
||||||
|
|
||||||
Direct, specific, focused on the code. No flattery, no padding. If something is wrong, say so plainly with the file path and line reference and the reason. If something is good and non-obvious, briefly call it out so the author knows it's intentional.
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
---
|
|
||||||
description: Designer-turned-developer who crafts stunning UI/UX even without design mockups. Grants filesystem read/write access for editing component files.
|
|
||||||
enabled_tools: fs_read, fs_write, fs_patch, fs_grep, fs_glob, fs_cat, fs_ls, fs_mkdir
|
|
||||||
---
|
|
||||||
You are doing frontend work. Use the filesystem tools to read, write, and patch component files. Treat UI/UX as a discipline, not a polish step at the end.
|
|
||||||
|
|
||||||
## Investigate before editing
|
|
||||||
|
|
||||||
Before changing a component:
|
|
||||||
|
|
||||||
- `fs_ls` the component's directory to see siblings and tests.
|
|
||||||
- `fs_read` the component itself.
|
|
||||||
- `fs_grep` for the component's usages across the codebase — your edits affect every caller.
|
|
||||||
- `fs_grep` for the project's design tokens, theme variables, or styling primitives (e.g., `--color-`, `theme.spacing`, `tw-`).
|
|
||||||
- Read existing similar components to match conventions.
|
|
||||||
|
|
||||||
## Visual hierarchy
|
|
||||||
|
|
||||||
Every screen has a focal point. Identify it before laying out anything else:
|
|
||||||
|
|
||||||
- One primary action per view. Make it visually dominant.
|
|
||||||
- Secondary actions are present but visibly subordinate.
|
|
||||||
- Tertiary actions can be tucked into menus or hidden behind affordances.
|
|
||||||
|
|
||||||
## Spacing and rhythm
|
|
||||||
|
|
||||||
- Use the project's existing spacing scale (4px, 8px, custom — match what's already there). Don't introduce one-off values.
|
|
||||||
- Larger spacing = stronger grouping break. Inside a card, tight; between cards, looser.
|
|
||||||
- White space is not wasted space. It's the difference between "professional" and "cramped."
|
|
||||||
|
|
||||||
## Typography
|
|
||||||
|
|
||||||
- Two or three sizes per view, max. More than that is noise.
|
|
||||||
- Line-height: 1.4-1.6 for body, tighter for headlines.
|
|
||||||
- Don't center long paragraphs. Left-align (or right-align for RTL).
|
|
||||||
|
|
||||||
## Color
|
|
||||||
|
|
||||||
- Use the project's existing palette. If you need a color that isn't there, you're probably overdesigning.
|
|
||||||
- Contrast matters: aim for WCAG AA at minimum (4.5:1 for body text, 3:1 for large text).
|
|
||||||
- Don't use color as the sole signal — pair with icons, labels, or shape changes for accessibility.
|
|
||||||
|
|
||||||
## Component conventions
|
|
||||||
|
|
||||||
When adding a new component:
|
|
||||||
|
|
||||||
- Match the existing structure: where do props go, where do styles go, where do tests go?
|
|
||||||
- `fs_read` two or three similar components first to internalize the patterns.
|
|
||||||
- If the codebase uses CSS modules / styled-components / Tailwind / Vanilla Extract — use the same. Don't introduce a new system.
|
|
||||||
- Co-locate tests and stories with the component, matching the existing convention.
|
|
||||||
|
|
||||||
## Forms
|
|
||||||
|
|
||||||
- Label every input. Placeholder text is not a label.
|
|
||||||
- Show validation errors near the field, not in a banner at the top.
|
|
||||||
- Validate on blur, not on every keystroke. Show success states only after the user has interacted.
|
|
||||||
- Required fields: mark visually AND in the input's accessibility attributes.
|
|
||||||
|
|
||||||
## Loading and empty states
|
|
||||||
|
|
||||||
- Empty states are an opportunity, not a fallback. Tell the user what they can do, not "no data."
|
|
||||||
- Loading: show structure (skeletons) when you know what's coming. Spinners are for indeterminate waits.
|
|
||||||
- Errors: explain WHAT failed and what the user can do about it. "Something went wrong" is useless.
|
|
||||||
|
|
||||||
## When unsure
|
|
||||||
|
|
||||||
Ship the boring version. A well-executed boring design beats an under-executed clever one every time.
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
---
|
|
||||||
description: Methodology for atomic commits, rebase surgery, and clean git history. Grants shell access for running git commands.
|
|
||||||
enabled_tools: execute_command
|
|
||||||
---
|
|
||||||
You are operating on a git repository. Apply these conventions strictly. Use the `execute_command` tool to run git commands.
|
|
||||||
|
|
||||||
## Atomic commits
|
|
||||||
|
|
||||||
Each commit represents one logical change. If the commit message needs the word "and," the change is too large; split it. Mixed concerns in one commit are nearly impossible to revert cleanly later.
|
|
||||||
|
|
||||||
## Commit messages
|
|
||||||
|
|
||||||
- Subject line: imperative mood, ≤50 characters, no trailing period.
|
|
||||||
- Blank line.
|
|
||||||
- Body: explain WHY, not WHAT. The diff shows what changed.
|
|
||||||
- Reference issues by URL or canonical ID, not by free-form description.
|
|
||||||
|
|
||||||
## Rebase, don't merge
|
|
||||||
|
|
||||||
- `git rebase -i origin/main` before opening a PR.
|
|
||||||
- Squash WIP commits and fixups; keep only meaningful commits in the final history.
|
|
||||||
- Never rebase a branch others may have based work on. If unsure, ask.
|
|
||||||
|
|
||||||
## Conflict resolution
|
|
||||||
|
|
||||||
- Read both sides carefully before resolving. Don't reflexively take "ours" or "theirs."
|
|
||||||
- After resolving, run tests before continuing the rebase.
|
|
||||||
- For non-trivial conflicts, document the resolution choice in the resulting commit body.
|
|
||||||
|
|
||||||
## Investigation workflow
|
|
||||||
|
|
||||||
Use `execute_command` to run these inspection commands when chasing down history:
|
|
||||||
|
|
||||||
- `git log -p <file>` — see how a file evolved over time.
|
|
||||||
- `git log -S '<string>'` (pickaxe) — find when a string was added or removed.
|
|
||||||
- `git log --all --grep '<pattern>'` — search commit messages.
|
|
||||||
- `git blame -L <start>,<end> <file>` — current authorship for a line range.
|
|
||||||
- `git diff <ref1>..<ref2> -- <path>` — narrow diffs to specific paths.
|
|
||||||
- `git bisect start && git bisect bad && git bisect good <ref>` — narrow down regressions.
|
|
||||||
|
|
||||||
## Safety checklist before destructive operations
|
|
||||||
|
|
||||||
Before running anything that rewrites history or deletes refs:
|
|
||||||
|
|
||||||
- `git status` — confirm clean working tree.
|
|
||||||
- `git branch --show-current` — confirm which branch you're on.
|
|
||||||
- `git log -3 --oneline` — confirm what's about to be moved.
|
|
||||||
|
|
||||||
## What to never do
|
|
||||||
|
|
||||||
- Force-push to shared branches (`main`, release branches, anything teammates pull from).
|
|
||||||
- `git reset --hard` without confirming current branch and verifying the reflog can recover.
|
|
||||||
- `git push --no-verify` to skip hooks — fix the underlying issue instead.
|
|
||||||
- Commit secrets, even temporarily. Once pushed, treat as compromised; rotate.
|
|
||||||
|
|
||||||
## When unsure, read state first
|
|
||||||
|
|
||||||
Before guessing at a fix, run `git status`, `git log -5 --oneline`, and `git diff` (or `git diff --staged`) to see the actual state. Don't operate on assumptions.
|
|
||||||
@@ -42,11 +42,6 @@ global_tools: # Optional list of additional global tools to e
|
|||||||
- web_search
|
- web_search
|
||||||
- fs
|
- fs
|
||||||
- python
|
- python
|
||||||
skills_enabled: true # Master switch for skills in this agent (default: inherit from global)
|
|
||||||
enabled_skills: # Optional list of skills available when this agent runs.
|
|
||||||
# Must be a subset of global `visible_skills`. Omit to inherit the global default.
|
|
||||||
- git-master
|
|
||||||
- ai-slop-remover
|
|
||||||
dynamic_instructions: false # Whether to use dynamic instructions for the agent; if false, static instructions are used
|
dynamic_instructions: false # Whether to use dynamic instructions for the agent; if false, static instructions are used
|
||||||
instructions: | # Static instructions for the agent; ignored if dynamic instructions are used
|
instructions: | # Static instructions for the agent; ignored if dynamic instructions are used
|
||||||
You are a AI agent designed to demonstrate agent capabilities.
|
You are a AI agent designed to demonstrate agent capabilities.
|
||||||
|
|||||||
+1
-16
@@ -39,7 +39,7 @@ vault_password_file: null # Path to a file containing the password for th
|
|||||||
|
|
||||||
# ---- Function Calling ----
|
# ---- Function Calling ----
|
||||||
# See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details
|
# See the [Tools documentation](https://github.com/Dark-Alex-17/coyote/wiki/Tools) for more details
|
||||||
function_calling_support: true # Enables or disables function calling (Globally).
|
function_calling: true # Enables or disables function calling (Globally).
|
||||||
mapping_tools: # Alias for a tool or toolset
|
mapping_tools: # Alias for a tool or toolset
|
||||||
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
|
fs: 'fs_cat,fs_ls,fs_mkdir,fs_rm,fs_write,fs_read,fs_glob,fs_grep'
|
||||||
enabled_tools: null # Which tools to enable by default. (e.g. 'fs,web_search_coyote')
|
enabled_tools: null # Which tools to enable by default. (e.g. 'fs,web_search_coyote')
|
||||||
@@ -80,21 +80,6 @@ mapping_mcp_servers: # Alias for an MCP server or set of servers
|
|||||||
git: github,gitmcp
|
git: github,gitmcp
|
||||||
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
enabled_mcp_servers: null # Which MCP servers to enable by default (e.g. 'github,slack,ddg-search')
|
||||||
|
|
||||||
# ---- Skills ----
|
|
||||||
# Skills are modular knowledge or capability packs the LLM can load and unload mid-conversation.
|
|
||||||
# See the [Skills documentation](https://github.com/Dark-Alex-17/coyote/wiki/Skills) for more details.
|
|
||||||
skills_enabled: true # Master switch. Set to false to hide all skill management tools from the model.
|
|
||||||
visible_skills: # The universe of skills allowed to be enabled in any context. Omit (null) for "all installed".
|
|
||||||
- ai-slop-remover
|
|
||||||
- code-review
|
|
||||||
- frontend-ui-ux
|
|
||||||
- git-master
|
|
||||||
enabled_skills: null # Which skills are available by default (no role/agent/session active). null = all visible.
|
|
||||||
# Example: only expose two skills in the bare REPL.
|
|
||||||
# enabled_skills:
|
|
||||||
# - git-master
|
|
||||||
# - ai-slop-remover
|
|
||||||
|
|
||||||
# ---- Auto-Continue (Todo System) ----
|
# ---- Auto-Continue (Todo System) ----
|
||||||
# The auto-continue system provides built-in task tracking for improved reliability.
|
# The auto-continue system provides built-in task tracking for improved reliability.
|
||||||
# When enabled, the model can create todo lists and the system will automatically
|
# When enabled, the model can create todo lists and the system will automatically
|
||||||
|
|||||||
@@ -10,9 +10,6 @@ temperature: 0.2 # The temperature to use for this role whe
|
|||||||
top_p: 0 # The top_p to use for this role when querying the model
|
top_p: 0 # The top_p to use for this role when querying the model
|
||||||
enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enable for this role
|
enabled_tools: fs_ls,fs_cat # A comma-separated list of tools to enable for this role
|
||||||
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role
|
enabled_mcp_servers: github,gitmcp # A comma-separated list of MCP servers to enable for this role
|
||||||
skills_enabled: true # Master switch for skills in this role (default: inherit from global)
|
|
||||||
enabled_skills: git-master,ai-slop-remover # Comma-separated list of skills available when this role is active.
|
|
||||||
# Must be a subset of global `visible_skills`. Omit to inherit the global default.
|
|
||||||
prompt: null # A custom prompt to use for this role that will immediately query
|
prompt: null # A custom prompt to use for this role that will immediately query
|
||||||
# the model for output instead of using the instructions below
|
# the model for output instead of using the instructions below
|
||||||
# Auto-Continue (Todo System)
|
# Auto-Continue (Todo System)
|
||||||
|
|||||||
+24
-88
@@ -202,24 +202,6 @@
|
|||||||
# - https://ai.google.dev/api/rest/v1beta/models/streamGenerateContent
|
# - https://ai.google.dev/api/rest/v1beta/models/streamGenerateContent
|
||||||
- provider: gemini
|
- provider: gemini
|
||||||
models:
|
models:
|
||||||
- name: gemini-3.5-flash
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 65536
|
|
||||||
input_price: 0.2
|
|
||||||
output_price: 1.5
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemini-3-flash-preview
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 65536
|
|
||||||
input_price: 0.2
|
|
||||||
output_price: 1.5
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemini-3.1-flash-lite
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 65536
|
|
||||||
input_price: 0.2
|
|
||||||
output_price: 1.5
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemini-3.1-pro-preview
|
- name: gemini-3.1-pro-preview
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65535
|
max_output_tokens: 65535
|
||||||
@@ -256,6 +238,20 @@
|
|||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
|
- name: gemini-2.0-flash
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 8192
|
||||||
|
input_price: 0
|
||||||
|
output_price: 0
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
|
- name: gemini-2.0-flash-lite
|
||||||
|
max_input_tokens: 1048576
|
||||||
|
max_output_tokens: 8192
|
||||||
|
input_price: 0
|
||||||
|
output_price: 0
|
||||||
|
supports_vision: true
|
||||||
|
supports_function_calling: true
|
||||||
- name: gemma-3-27b-it
|
- name: gemma-3-27b-it
|
||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
@@ -273,20 +269,6 @@
|
|||||||
# - https://docs.anthropic.com/en/api/messages
|
# - https://docs.anthropic.com/en/api/messages
|
||||||
- provider: claude
|
- provider: claude
|
||||||
models:
|
models:
|
||||||
- name: claude-opus-4-8
|
|
||||||
max_input_tokens: 1000000
|
|
||||||
max_output_tokens: 128000
|
|
||||||
input_price: 5
|
|
||||||
output_price: 25
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: claude-opus-4-7
|
|
||||||
max_input_tokens: 1000000
|
|
||||||
max_output_tokens: 128000
|
|
||||||
input_price: 5
|
|
||||||
output_price: 25
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: claude-opus-4-6
|
- name: claude-opus-4-6
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
@@ -755,24 +737,6 @@
|
|||||||
# - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
|
# - https://cloud.google.com/vertex-ai/generative-ai/docs/model-reference/gemini
|
||||||
- provider: vertexai
|
- provider: vertexai
|
||||||
models:
|
models:
|
||||||
- name: gemini-3.5-flash
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 65536
|
|
||||||
input_price: 0.2
|
|
||||||
output_price: 1.5
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemini-3-flash-preview
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 65536
|
|
||||||
input_price: 0.2
|
|
||||||
output_price: 1.5
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemini-3.1-flash-lite
|
|
||||||
max_input_tokens: 1048576
|
|
||||||
max_output_tokens: 65536
|
|
||||||
input_price: 0.2
|
|
||||||
output_price: 1.5
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: gemini-3.1-pro-preview
|
- name: gemini-3.1-pro-preview
|
||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 65536
|
max_output_tokens: 65536
|
||||||
@@ -809,18 +773,18 @@
|
|||||||
max_input_tokens: 1048576
|
max_input_tokens: 1048576
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: claude-opus-4-8
|
- name: gemini-2.0-flash-001
|
||||||
max_input_tokens: 1000000
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 128000
|
max_output_tokens: 8192
|
||||||
input_price: 5
|
input_price: 0.15
|
||||||
output_price: 25
|
output_price: 0.6
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: claude-opus-4-7
|
- name: gemini-2.0-flash-lite-001
|
||||||
max_input_tokens: 1000000
|
max_input_tokens: 1048576
|
||||||
max_output_tokens: 128000
|
max_output_tokens: 8192
|
||||||
input_price: 5
|
input_price: 0.075
|
||||||
output_price: 25
|
output_price: 0.3
|
||||||
supports_vision: true
|
supports_vision: true
|
||||||
supports_function_calling: true
|
supports_function_calling: true
|
||||||
- name: claude-opus-4-6
|
- name: claude-opus-4-6
|
||||||
@@ -978,20 +942,6 @@
|
|||||||
# - https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html
|
# - https://docs.aws.amazon.com/bedrock/latest/userguide/conversation-inference-call.html
|
||||||
- provider: bedrock
|
- provider: bedrock
|
||||||
models:
|
models:
|
||||||
- name: us.anthropic.claude-opus-4-8
|
|
||||||
max_input_tokens: 1000000
|
|
||||||
max_output_tokens: 128000
|
|
||||||
input_price: 5
|
|
||||||
output_price: 25
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: us.anthropic.claude-opus-4-7
|
|
||||||
max_input_tokens: 1000000
|
|
||||||
max_output_tokens: 128000
|
|
||||||
input_price: 5
|
|
||||||
output_price: 25
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: us.anthropic.claude-opus-4-6-v1
|
- name: us.anthropic.claude-opus-4-6-v1
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
@@ -1618,20 +1568,6 @@
|
|||||||
max_input_tokens: 131072
|
max_input_tokens: 131072
|
||||||
input_price: 0.1
|
input_price: 0.1
|
||||||
output_price: 0.2
|
output_price: 0.2
|
||||||
- name: anthropic/claude-opus-4-8
|
|
||||||
max_input_tokens: 1000000
|
|
||||||
max_output_tokens: 128000
|
|
||||||
input_price: 5
|
|
||||||
output_price: 25
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: anthropic/claude-opus-4-7
|
|
||||||
max_input_tokens: 1000000
|
|
||||||
max_output_tokens: 128000
|
|
||||||
input_price: 5
|
|
||||||
output_price: 25
|
|
||||||
supports_vision: true
|
|
||||||
supports_function_calling: true
|
|
||||||
- name: anthropic/claude-opus-4.6
|
- name: anthropic/claude-opus-4.6
|
||||||
max_input_tokens: 200000
|
max_input_tokens: 200000
|
||||||
max_output_tokens: 8192
|
max_output_tokens: 8192
|
||||||
|
|||||||
@@ -116,14 +116,6 @@ pub struct Cli {
|
|||||||
/// List all macros
|
/// List all macros
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub list_macros: bool,
|
pub list_macros: bool,
|
||||||
/// List all installed skills
|
|
||||||
#[arg(long)]
|
|
||||||
pub list_skills: bool,
|
|
||||||
/// Pre-load an existing skill into the session (repeatable). If a single
|
|
||||||
/// `--skill <NAME>` is given and the skill doesn't exist, opens $EDITOR
|
|
||||||
/// with a scaffold to create it.
|
|
||||||
#[arg(long, value_name = "NAME")]
|
|
||||||
pub skill: Vec<String>,
|
|
||||||
/// Input text
|
/// Input text
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
text: Vec<String>,
|
text: Vec<String>,
|
||||||
@@ -306,21 +298,6 @@ mod tests {
|
|||||||
assert!(parse(&["--list-agents"]).list_agents);
|
assert!(parse(&["--list-agents"]).list_agents);
|
||||||
assert!(parse(&["--list-rags"]).list_rags);
|
assert!(parse(&["--list-rags"]).list_rags);
|
||||||
assert!(parse(&["--list-macros"]).list_macros);
|
assert!(parse(&["--list-macros"]).list_macros);
|
||||||
assert!(parse(&["--list-skills"]).list_skills);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_skill_flag_takes_name() {
|
|
||||||
assert_eq!(parse(&["--skill", "git-master"]).skill, vec!["git-master"]);
|
|
||||||
assert!(parse(&[]).skill.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_multiple_skill_flags_preserves_order() {
|
|
||||||
assert_eq!(
|
|
||||||
parse(&["--skill", "alpha", "--skill", "beta", "--skill", "gamma"]).skill,
|
|
||||||
vec!["alpha", "beta", "gamma"]
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -207,13 +207,6 @@ impl Agent {
|
|||||||
functions.append_teammate_functions();
|
functions.append_teammate_functions();
|
||||||
functions.append_user_interaction_functions();
|
functions.append_user_interaction_functions();
|
||||||
|
|
||||||
if app.function_calling_support
|
|
||||||
&& app.skills_enabled
|
|
||||||
&& !matches!(agent_config.skills_enabled, Some(false))
|
|
||||||
{
|
|
||||||
functions.append_skill_functions();
|
|
||||||
}
|
|
||||||
|
|
||||||
agent_config.replace_tools_placeholder(&functions);
|
agent_config.replace_tools_placeholder(&functions);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -344,14 +337,6 @@ impl Agent {
|
|||||||
&self.config.mcp_servers
|
&self.config.mcp_servers
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skills_enabled(&self) -> Option<bool> {
|
|
||||||
self.config.skills_enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled_skills(&self) -> Option<&[String]> {
|
|
||||||
self.config.enabled_skills.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn conversation_starters(&self) -> Vec<String> {
|
pub fn conversation_starters(&self) -> Vec<String> {
|
||||||
self.config
|
self.config
|
||||||
.conversation_starters
|
.conversation_starters
|
||||||
@@ -630,10 +615,6 @@ pub struct AgentConfig {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub global_tools: Vec<String>,
|
pub global_tools: Vec<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub skills_enabled: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub enabled_skills: Option<Vec<String>>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
pub continuation_prompt: Option<String>,
|
pub continuation_prompt: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub instructions: String,
|
pub instructions: String,
|
||||||
|
|||||||
@@ -35,10 +35,6 @@ pub struct AppConfig {
|
|||||||
pub enabled_tools: Option<String>,
|
pub enabled_tools: Option<String>,
|
||||||
pub visible_tools: Option<Vec<String>>,
|
pub visible_tools: Option<Vec<String>>,
|
||||||
|
|
||||||
pub skills_enabled: bool,
|
|
||||||
pub enabled_skills: Option<String>,
|
|
||||||
pub visible_skills: Option<Vec<String>>,
|
|
||||||
|
|
||||||
pub mcp_server_support: bool,
|
pub mcp_server_support: bool,
|
||||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||||
pub enabled_mcp_servers: Option<String>,
|
pub enabled_mcp_servers: Option<String>,
|
||||||
@@ -100,10 +96,6 @@ impl Default for AppConfig {
|
|||||||
enabled_tools: None,
|
enabled_tools: None,
|
||||||
visible_tools: None,
|
visible_tools: None,
|
||||||
|
|
||||||
skills_enabled: true,
|
|
||||||
enabled_skills: None,
|
|
||||||
visible_skills: None,
|
|
||||||
|
|
||||||
mcp_server_support: true,
|
mcp_server_support: true,
|
||||||
mapping_mcp_servers: Default::default(),
|
mapping_mcp_servers: Default::default(),
|
||||||
enabled_mcp_servers: None,
|
enabled_mcp_servers: None,
|
||||||
@@ -166,10 +158,6 @@ impl AppConfig {
|
|||||||
enabled_tools: config.enabled_tools,
|
enabled_tools: config.enabled_tools,
|
||||||
visible_tools: config.visible_tools,
|
visible_tools: config.visible_tools,
|
||||||
|
|
||||||
skills_enabled: config.skills_enabled,
|
|
||||||
enabled_skills: config.enabled_skills,
|
|
||||||
visible_skills: config.visible_skills,
|
|
||||||
|
|
||||||
mcp_server_support: config.mcp_server_support,
|
mcp_server_support: config.mcp_server_support,
|
||||||
mapping_mcp_servers: config.mapping_mcp_servers,
|
mapping_mcp_servers: config.mapping_mcp_servers,
|
||||||
enabled_mcp_servers: config.enabled_mcp_servers,
|
enabled_mcp_servers: config.enabled_mcp_servers,
|
||||||
@@ -391,14 +379,6 @@ impl AppConfig {
|
|||||||
self.enabled_tools = v;
|
self.enabled_tools = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("skills_enabled")) {
|
|
||||||
self.skills_enabled = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(v) = super::read_env_value::<String>(&get_env_name("enabled_skills")) {
|
|
||||||
self.enabled_skills = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
if let Some(Some(v)) = super::read_env_bool(&get_env_name("mcp_server_support")) {
|
||||||
self.mcp_server_support = v;
|
self.mcp_server_support = v;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ pub fn install_remote(git_url: &str, filter: Option<InstallFilter>, force: bool)
|
|||||||
if layout.is_empty() {
|
if layout.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
"No recognized assets found in {git_url}. Expected one or more of: \
|
"No recognized assets found in {git_url}. Expected one or more of: \
|
||||||
agents/, roles/, skills/, macros/, functions/tools/, functions/mcp.json"
|
agents/, roles/, macros/, functions/tools/, functions/mcp.json"
|
||||||
);
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,6 @@ fn run_git(args: Vec<OsString>) -> Result<()> {
|
|||||||
struct RemoteLayout {
|
struct RemoteLayout {
|
||||||
agents: Option<PathBuf>,
|
agents: Option<PathBuf>,
|
||||||
roles: Option<PathBuf>,
|
roles: Option<PathBuf>,
|
||||||
skills: Option<PathBuf>,
|
|
||||||
macros: Option<PathBuf>,
|
macros: Option<PathBuf>,
|
||||||
functions_tools: Option<PathBuf>,
|
functions_tools: Option<PathBuf>,
|
||||||
mcp_json: Option<PathBuf>,
|
mcp_json: Option<PathBuf>,
|
||||||
@@ -203,7 +202,6 @@ impl RemoteLayout {
|
|||||||
fn is_empty(&self) -> bool {
|
fn is_empty(&self) -> bool {
|
||||||
self.agents.is_none()
|
self.agents.is_none()
|
||||||
&& self.roles.is_none()
|
&& self.roles.is_none()
|
||||||
&& self.skills.is_none()
|
|
||||||
&& self.macros.is_none()
|
&& self.macros.is_none()
|
||||||
&& self.functions_tools.is_none()
|
&& self.functions_tools.is_none()
|
||||||
&& self.mcp_json.is_none()
|
&& self.mcp_json.is_none()
|
||||||
@@ -217,29 +215,20 @@ fn scan_remote_layout(root: &Path) -> Result<RemoteLayout> {
|
|||||||
if agents.is_dir() {
|
if agents.is_dir() {
|
||||||
layout.agents = Some(agents);
|
layout.agents = Some(agents);
|
||||||
}
|
}
|
||||||
|
|
||||||
let roles = root.join("roles");
|
let roles = root.join("roles");
|
||||||
if roles.is_dir() {
|
if roles.is_dir() {
|
||||||
layout.roles = Some(roles);
|
layout.roles = Some(roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
let skills = root.join("skills");
|
|
||||||
if skills.is_dir() {
|
|
||||||
layout.skills = Some(skills);
|
|
||||||
}
|
|
||||||
|
|
||||||
let macros = root.join("macros");
|
let macros = root.join("macros");
|
||||||
if macros.is_dir() {
|
if macros.is_dir() {
|
||||||
layout.macros = Some(macros);
|
layout.macros = Some(macros);
|
||||||
}
|
}
|
||||||
|
|
||||||
let functions = root.join("functions");
|
let functions = root.join("functions");
|
||||||
if functions.is_dir() {
|
if functions.is_dir() {
|
||||||
let tools = functions.join("tools");
|
let tools = functions.join("tools");
|
||||||
if tools.is_dir() {
|
if tools.is_dir() {
|
||||||
layout.functions_tools = Some(tools);
|
layout.functions_tools = Some(tools);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mcp = functions.join("mcp.json");
|
let mcp = functions.join("mcp.json");
|
||||||
if mcp.is_file() {
|
if mcp.is_file() {
|
||||||
layout.mcp_json = Some(mcp);
|
layout.mcp_json = Some(mcp);
|
||||||
@@ -262,10 +251,6 @@ fn apply_filter(mut layout: RemoteLayout, filter: Option<InstallFilter>) -> Remo
|
|||||||
roles: layout.roles.take(),
|
roles: layout.roles.take(),
|
||||||
..RemoteLayout::default()
|
..RemoteLayout::default()
|
||||||
},
|
},
|
||||||
InstallFilter::Skills => RemoteLayout {
|
|
||||||
skills: layout.skills.take(),
|
|
||||||
..RemoteLayout::default()
|
|
||||||
},
|
|
||||||
InstallFilter::Macros => RemoteLayout {
|
InstallFilter::Macros => RemoteLayout {
|
||||||
macros: layout.macros.take(),
|
macros: layout.macros.take(),
|
||||||
..RemoteLayout::default()
|
..RemoteLayout::default()
|
||||||
@@ -323,7 +308,6 @@ fn walk_files_inner(dir: &Path, out: &mut Vec<PathBuf>) -> Result<()> {
|
|||||||
enum TopCategory {
|
enum TopCategory {
|
||||||
Agents,
|
Agents,
|
||||||
Roles,
|
Roles,
|
||||||
Skills,
|
|
||||||
Macros,
|
Macros,
|
||||||
FunctionsTools,
|
FunctionsTools,
|
||||||
}
|
}
|
||||||
@@ -333,7 +317,6 @@ impl TopCategory {
|
|||||||
match self {
|
match self {
|
||||||
TopCategory::Agents => "agents",
|
TopCategory::Agents => "agents",
|
||||||
TopCategory::Roles => "roles",
|
TopCategory::Roles => "roles",
|
||||||
TopCategory::Skills => "skills",
|
|
||||||
TopCategory::Macros => "macros",
|
TopCategory::Macros => "macros",
|
||||||
TopCategory::FunctionsTools => "functions/tools",
|
TopCategory::FunctionsTools => "functions/tools",
|
||||||
}
|
}
|
||||||
@@ -373,16 +356,6 @@ fn plan_changes(layout: &RemoteLayout) -> Result<InstallPlan> {
|
|||||||
if let Some(src_dir) = &layout.roles {
|
if let Some(src_dir) = &layout.roles {
|
||||||
plan_dir_into(src_dir, &paths::roles_dir(), TopCategory::Roles, &mut files)?;
|
plan_dir_into(src_dir, &paths::roles_dir(), TopCategory::Roles, &mut files)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(src_dir) = &layout.skills {
|
|
||||||
plan_dir_into(
|
|
||||||
src_dir,
|
|
||||||
&paths::skills_dir(),
|
|
||||||
TopCategory::Skills,
|
|
||||||
&mut files,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(src_dir) = &layout.macros {
|
if let Some(src_dir) = &layout.macros {
|
||||||
plan_dir_into(
|
plan_dir_into(
|
||||||
src_dir,
|
src_dir,
|
||||||
@@ -484,7 +457,6 @@ fn print_plan_summary(plan: &InstallPlan) {
|
|||||||
for cat in [
|
for cat in [
|
||||||
TopCategory::Agents,
|
TopCategory::Agents,
|
||||||
TopCategory::Roles,
|
TopCategory::Roles,
|
||||||
TopCategory::Skills,
|
|
||||||
TopCategory::Macros,
|
TopCategory::Macros,
|
||||||
TopCategory::FunctionsTools,
|
TopCategory::FunctionsTools,
|
||||||
] {
|
] {
|
||||||
@@ -1010,7 +982,6 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: Some(PathBuf::from("r")),
|
roles: Some(PathBuf::from("r")),
|
||||||
skills: Some(PathBuf::from("s")),
|
|
||||||
macros: Some(PathBuf::from("m")),
|
macros: Some(PathBuf::from("m")),
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1018,8 +989,8 @@ mod tests {
|
|||||||
|
|
||||||
let out = apply_filter(l, None);
|
let out = apply_filter(l, None);
|
||||||
|
|
||||||
assert!(out.agents.is_some() && out.roles.is_some() && out.skills.is_some());
|
assert!(out.agents.is_some() && out.roles.is_some() && out.macros.is_some());
|
||||||
assert!(out.macros.is_some() && out.functions_tools.is_some() && out.mcp_json.is_some());
|
assert!(out.functions_tools.is_some() && out.mcp_json.is_some());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1027,7 +998,6 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: None,
|
roles: None,
|
||||||
skills: Some(PathBuf::from("s")),
|
|
||||||
macros: None,
|
macros: None,
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1036,7 +1006,6 @@ mod tests {
|
|||||||
let out = apply_filter(l, Some(InstallFilter::Functions));
|
let out = apply_filter(l, Some(InstallFilter::Functions));
|
||||||
|
|
||||||
assert!(out.agents.is_none());
|
assert!(out.agents.is_none());
|
||||||
assert!(out.skills.is_none());
|
|
||||||
assert_eq!(out.functions_tools, Some(PathBuf::from("f")));
|
assert_eq!(out.functions_tools, Some(PathBuf::from("f")));
|
||||||
assert!(out.mcp_json.is_none());
|
assert!(out.mcp_json.is_none());
|
||||||
}
|
}
|
||||||
@@ -1046,7 +1015,6 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: None,
|
roles: None,
|
||||||
skills: Some(PathBuf::from("s")),
|
|
||||||
macros: None,
|
macros: None,
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1054,7 +1022,7 @@ mod tests {
|
|||||||
|
|
||||||
let out = apply_filter(l, Some(InstallFilter::McpConfig));
|
let out = apply_filter(l, Some(InstallFilter::McpConfig));
|
||||||
|
|
||||||
assert!(out.agents.is_none() && out.skills.is_none() && out.functions_tools.is_none());
|
assert!(out.agents.is_none() && out.functions_tools.is_none());
|
||||||
assert_eq!(out.mcp_json, Some(PathBuf::from("j")));
|
assert_eq!(out.mcp_json, Some(PathBuf::from("j")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1063,7 +1031,6 @@ mod tests {
|
|||||||
let l = RemoteLayout {
|
let l = RemoteLayout {
|
||||||
agents: Some(PathBuf::from("a")),
|
agents: Some(PathBuf::from("a")),
|
||||||
roles: Some(PathBuf::from("r")),
|
roles: Some(PathBuf::from("r")),
|
||||||
skills: Some(PathBuf::from("s")),
|
|
||||||
macros: Some(PathBuf::from("m")),
|
macros: Some(PathBuf::from("m")),
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
functions_tools: Some(PathBuf::from("f")),
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
mcp_json: Some(PathBuf::from("j")),
|
||||||
@@ -1072,25 +1039,7 @@ mod tests {
|
|||||||
let out = apply_filter(l, Some(InstallFilter::Roles));
|
let out = apply_filter(l, Some(InstallFilter::Roles));
|
||||||
|
|
||||||
assert_eq!(out.roles, Some(PathBuf::from("r")));
|
assert_eq!(out.roles, Some(PathBuf::from("r")));
|
||||||
assert!(out.agents.is_none() && out.skills.is_none() && out.macros.is_none());
|
assert!(out.agents.is_none() && out.macros.is_none());
|
||||||
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn apply_filter_skills_keeps_only_skills() {
|
|
||||||
let l = RemoteLayout {
|
|
||||||
agents: Some(PathBuf::from("a")),
|
|
||||||
roles: Some(PathBuf::from("r")),
|
|
||||||
skills: Some(PathBuf::from("s")),
|
|
||||||
macros: Some(PathBuf::from("m")),
|
|
||||||
functions_tools: Some(PathBuf::from("f")),
|
|
||||||
mcp_json: Some(PathBuf::from("j")),
|
|
||||||
};
|
|
||||||
|
|
||||||
let out = apply_filter(l, Some(InstallFilter::Skills));
|
|
||||||
|
|
||||||
assert_eq!(out.skills, Some(PathBuf::from("s")));
|
|
||||||
assert!(out.agents.is_none() && out.roles.is_none() && out.macros.is_none());
|
|
||||||
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
assert!(out.functions_tools.is_none() && out.mcp_json.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1135,10 +1084,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scan_remote_layout_finds_known_subdirs() {
|
fn scan_remote_layout_finds_known_subdirs() {
|
||||||
let root = fresh_temp_dir("scan-test-");
|
let root = fresh_temp_dir("scan-test-");
|
||||||
|
|
||||||
fs::create_dir_all(root.join("agents/sample")).unwrap();
|
fs::create_dir_all(root.join("agents/sample")).unwrap();
|
||||||
fs::create_dir_all(root.join("roles")).unwrap();
|
fs::create_dir_all(root.join("roles")).unwrap();
|
||||||
fs::create_dir_all(root.join("skills")).unwrap();
|
|
||||||
fs::create_dir_all(root.join("macros")).unwrap();
|
fs::create_dir_all(root.join("macros")).unwrap();
|
||||||
fs::create_dir_all(root.join("functions/tools")).unwrap();
|
fs::create_dir_all(root.join("functions/tools")).unwrap();
|
||||||
touch(&root.join("functions/mcp.json"));
|
touch(&root.join("functions/mcp.json"));
|
||||||
@@ -1147,30 +1094,12 @@ mod tests {
|
|||||||
let layout = scan_remote_layout(&root).unwrap();
|
let layout = scan_remote_layout(&root).unwrap();
|
||||||
assert!(layout.agents.is_some());
|
assert!(layout.agents.is_some());
|
||||||
assert!(layout.roles.is_some());
|
assert!(layout.roles.is_some());
|
||||||
assert!(layout.skills.is_some());
|
|
||||||
assert!(layout.macros.is_some());
|
assert!(layout.macros.is_some());
|
||||||
assert!(layout.functions_tools.is_some());
|
assert!(layout.functions_tools.is_some());
|
||||||
assert!(layout.mcp_json.is_some());
|
assert!(layout.mcp_json.is_some());
|
||||||
let _ = fs::remove_dir_all(&root);
|
let _ = fs::remove_dir_all(&root);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn scan_remote_layout_finds_skills_only() {
|
|
||||||
let root = fresh_temp_dir("scan-skills-only-");
|
|
||||||
fs::create_dir_all(root.join("skills/git-master")).unwrap();
|
|
||||||
touch(&root.join("skills/git-master/SKILL.md"));
|
|
||||||
|
|
||||||
let layout = scan_remote_layout(&root).unwrap();
|
|
||||||
|
|
||||||
assert!(layout.skills.is_some());
|
|
||||||
assert!(layout.agents.is_none());
|
|
||||||
assert!(layout.roles.is_none());
|
|
||||||
assert!(layout.macros.is_none());
|
|
||||||
assert!(layout.functions_tools.is_none());
|
|
||||||
assert!(layout.mcp_json.is_none());
|
|
||||||
let _ = fs::remove_dir_all(&root);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scan_remote_layout_ignores_unrelated_files() {
|
fn scan_remote_layout_ignores_unrelated_files() {
|
||||||
let root = fresh_temp_dir("scan-unrelated-");
|
let root = fresh_temp_dir("scan-unrelated-");
|
||||||
@@ -1294,12 +1223,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_non_tty_conflict_aborts_without_force() {
|
fn merge_non_tty_conflict_aborts_without_force() {
|
||||||
if *IS_STDOUT_TERMINAL {
|
|
||||||
eprintln!(
|
|
||||||
"Skipping merge_non_tty_conflict_aborts_without_force: requires non-TTY stdout"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let dir = fresh_temp_dir("merge-non-tty-");
|
let dir = fresh_temp_dir("merge-non-tty-");
|
||||||
let remote = dir.join("remote.json");
|
let remote = dir.join("remote.json");
|
||||||
let target = dir.join("target.json");
|
let target = dir.join("target.json");
|
||||||
@@ -1376,12 +1299,6 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn handle_missing_secrets_defers_all_in_non_tty() {
|
fn handle_missing_secrets_defers_all_in_non_tty() {
|
||||||
if *IS_STDOUT_TERMINAL {
|
|
||||||
eprintln!(
|
|
||||||
"Skipping handle_missing_secrets_defers_all_in_non_tty: requires non-TTY stdout"
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let missing = vec![
|
let missing = vec![
|
||||||
"COYOTE_TEST_STEP4_A".to_string(),
|
"COYOTE_TEST_STEP4_A".to_string(),
|
||||||
"COYOTE_TEST_STEP4_B".to_string(),
|
"COYOTE_TEST_STEP4_B".to_string(),
|
||||||
|
|||||||
+2
-34
@@ -11,9 +11,6 @@ mod rag_cache;
|
|||||||
mod request_context;
|
mod request_context;
|
||||||
mod role;
|
mod role;
|
||||||
mod session;
|
mod session;
|
||||||
mod skill;
|
|
||||||
mod skill_policy;
|
|
||||||
mod skill_registry;
|
|
||||||
pub(crate) mod todo;
|
pub(crate) mod todo;
|
||||||
mod tool_scope;
|
mod tool_scope;
|
||||||
mod update;
|
mod update;
|
||||||
@@ -33,12 +30,6 @@ pub use self::role::{
|
|||||||
CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, Role, RoleLike, SHELL_ROLE,
|
CODE_ROLE, CREATE_TITLE_ROLE, EXPLAIN_SHELL_ROLE, Role, RoleLike, SHELL_ROLE,
|
||||||
};
|
};
|
||||||
use self::session::Session;
|
use self::session::Session;
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub use self::skill::Skill;
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub use self::skill_policy::SkillPolicy;
|
|
||||||
#[allow(unused_imports)]
|
|
||||||
pub use self::skill_registry::SkillRegistry;
|
|
||||||
pub use self::update::run_self_update;
|
pub use self::update::run_self_update;
|
||||||
use crate::client::{
|
use crate::client::{
|
||||||
ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS,
|
ClientConfig, MessageContentToolCalls, Model, ModelType, OPENAI_COMPATIBLE_PROVIDERS,
|
||||||
@@ -83,7 +74,6 @@ const LIGHT_THEME: &[u8] = include_bytes!("../../assets/monokai-extended-light.t
|
|||||||
const CONFIG_FILE_NAME: &str = "config.yaml";
|
const CONFIG_FILE_NAME: &str = "config.yaml";
|
||||||
const AGENT_GRAPH_FILE_NAME: &str = "graph.yaml";
|
const AGENT_GRAPH_FILE_NAME: &str = "graph.yaml";
|
||||||
const ROLES_DIR_NAME: &str = "roles";
|
const ROLES_DIR_NAME: &str = "roles";
|
||||||
const SKILLS_DIR_NAME: &str = "skills";
|
|
||||||
const MACROS_DIR_NAME: &str = "macros";
|
const MACROS_DIR_NAME: &str = "macros";
|
||||||
const ENV_FILE_NAME: &str = ".env";
|
const ENV_FILE_NAME: &str = ".env";
|
||||||
const MESSAGES_FILE_NAME: &str = "messages.md";
|
const MESSAGES_FILE_NAME: &str = "messages.md";
|
||||||
@@ -154,10 +144,6 @@ pub struct Config {
|
|||||||
pub enabled_tools: Option<String>,
|
pub enabled_tools: Option<String>,
|
||||||
pub visible_tools: Option<Vec<String>>,
|
pub visible_tools: Option<Vec<String>>,
|
||||||
|
|
||||||
pub skills_enabled: bool,
|
|
||||||
pub enabled_skills: Option<String>,
|
|
||||||
pub visible_skills: Option<Vec<String>>,
|
|
||||||
|
|
||||||
pub mcp_server_support: bool,
|
pub mcp_server_support: bool,
|
||||||
pub mapping_mcp_servers: IndexMap<String, String>,
|
pub mapping_mcp_servers: IndexMap<String, String>,
|
||||||
pub enabled_mcp_servers: Option<String>,
|
pub enabled_mcp_servers: Option<String>,
|
||||||
@@ -219,10 +205,6 @@ impl Default for Config {
|
|||||||
enabled_tools: None,
|
enabled_tools: None,
|
||||||
visible_tools: None,
|
visible_tools: None,
|
||||||
|
|
||||||
skills_enabled: true,
|
|
||||||
enabled_skills: None,
|
|
||||||
visible_skills: None,
|
|
||||||
|
|
||||||
mcp_server_support: true,
|
mcp_server_support: true,
|
||||||
mapping_mcp_servers: Default::default(),
|
mapping_mcp_servers: Default::default(),
|
||||||
enabled_mcp_servers: None,
|
enabled_mcp_servers: None,
|
||||||
@@ -268,7 +250,6 @@ pub fn install_builtins() -> Result<()> {
|
|||||||
Functions::install_builtin_global_tools(false)?;
|
Functions::install_builtin_global_tools(false)?;
|
||||||
Agent::install_builtin_agents(false)?;
|
Agent::install_builtin_agents(false)?;
|
||||||
Macro::install_macros(false)?;
|
Macro::install_macros(false)?;
|
||||||
Skill::install_builtin_skills(false)?;
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,20 +258,18 @@ pub enum AssetCategory {
|
|||||||
Agents,
|
Agents,
|
||||||
Macros,
|
Macros,
|
||||||
Functions,
|
Functions,
|
||||||
Skills,
|
|
||||||
#[value(name = "mcp_config")]
|
#[value(name = "mcp_config")]
|
||||||
McpConfig,
|
McpConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AssetCategory {
|
impl AssetCategory {
|
||||||
pub const NAMES: [&'static str; 5] = ["agents", "macros", "functions", "skills", "mcp_config"];
|
pub const NAMES: [&'static str; 4] = ["agents", "macros", "functions", "mcp_config"];
|
||||||
|
|
||||||
pub fn parse(name: &str) -> Option<Self> {
|
pub fn parse(name: &str) -> Option<Self> {
|
||||||
match name {
|
match name {
|
||||||
"agents" => Some(Self::Agents),
|
"agents" => Some(Self::Agents),
|
||||||
"macros" => Some(Self::Macros),
|
"macros" => Some(Self::Macros),
|
||||||
"functions" => Some(Self::Functions),
|
"functions" => Some(Self::Functions),
|
||||||
"skills" => Some(Self::Skills),
|
|
||||||
"mcp_config" => Some(Self::McpConfig),
|
"mcp_config" => Some(Self::McpConfig),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
@@ -301,7 +280,6 @@ impl AssetCategory {
|
|||||||
pub enum InstallFilter {
|
pub enum InstallFilter {
|
||||||
Agents,
|
Agents,
|
||||||
Roles,
|
Roles,
|
||||||
Skills,
|
|
||||||
Macros,
|
Macros,
|
||||||
Functions,
|
Functions,
|
||||||
#[value(name = "mcp_config")]
|
#[value(name = "mcp_config")]
|
||||||
@@ -309,20 +287,12 @@ pub enum InstallFilter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl InstallFilter {
|
impl InstallFilter {
|
||||||
pub const NAMES: [&'static str; 6] = [
|
pub const NAMES: [&'static str; 5] = ["agents", "roles", "macros", "functions", "mcp_config"];
|
||||||
"agents",
|
|
||||||
"roles",
|
|
||||||
"skills",
|
|
||||||
"macros",
|
|
||||||
"functions",
|
|
||||||
"mcp_config",
|
|
||||||
];
|
|
||||||
|
|
||||||
pub fn parse(name: &str) -> Option<Self> {
|
pub fn parse(name: &str) -> Option<Self> {
|
||||||
match name {
|
match name {
|
||||||
"agents" => Some(Self::Agents),
|
"agents" => Some(Self::Agents),
|
||||||
"roles" => Some(Self::Roles),
|
"roles" => Some(Self::Roles),
|
||||||
"skills" => Some(Self::Skills),
|
|
||||||
"macros" => Some(Self::Macros),
|
"macros" => Some(Self::Macros),
|
||||||
"functions" => Some(Self::Functions),
|
"functions" => Some(Self::Functions),
|
||||||
"mcp_config" => Some(Self::McpConfig),
|
"mcp_config" => Some(Self::McpConfig),
|
||||||
@@ -336,7 +306,6 @@ pub fn install_assets(category: AssetCategory) -> Result<()> {
|
|||||||
AssetCategory::Agents => ("agents", paths::agents_data_dir()),
|
AssetCategory::Agents => ("agents", paths::agents_data_dir()),
|
||||||
AssetCategory::Macros => ("macros", paths::macros_dir()),
|
AssetCategory::Macros => ("macros", paths::macros_dir()),
|
||||||
AssetCategory::Functions => ("functions", paths::functions_dir()),
|
AssetCategory::Functions => ("functions", paths::functions_dir()),
|
||||||
AssetCategory::Skills => ("skills", paths::skills_dir()),
|
|
||||||
AssetCategory::McpConfig => ("MCP config", paths::mcp_config_file()),
|
AssetCategory::McpConfig => ("MCP config", paths::mcp_config_file()),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -349,7 +318,6 @@ pub fn install_assets(category: AssetCategory) -> Result<()> {
|
|||||||
AssetCategory::Agents => Agent::install_builtin_agents(true)?,
|
AssetCategory::Agents => Agent::install_builtin_agents(true)?,
|
||||||
AssetCategory::Macros => Macro::install_macros(true)?,
|
AssetCategory::Macros => Macro::install_macros(true)?,
|
||||||
AssetCategory::Functions => Functions::install_builtin_global_tools(true)?,
|
AssetCategory::Functions => Functions::install_builtin_global_tools(true)?,
|
||||||
AssetCategory::Skills => Skill::install_builtin_skills(true)?,
|
|
||||||
AssetCategory::McpConfig => Functions::install_mcp_config()?,
|
AssetCategory::McpConfig => Functions::install_mcp_config()?,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-38
@@ -3,7 +3,7 @@ use super::{
|
|||||||
AGENT_GRAPH_FILE_NAME, AGENTS_DIR_NAME, BASH_PROMPT_UTILS_FILE_NAME, CONFIG_FILE_NAME,
|
AGENT_GRAPH_FILE_NAME, AGENTS_DIR_NAME, BASH_PROMPT_UTILS_FILE_NAME, CONFIG_FILE_NAME,
|
||||||
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
ENV_FILE_NAME, FUNCTIONS_BIN_DIR_NAME, FUNCTIONS_DIR_NAME, GLOBAL_TOOLS_DIR_NAME,
|
||||||
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, ModelsOverride, RAGS_DIR_NAME,
|
GLOBAL_TOOLS_UTILS_DIR_NAME, MACROS_DIR_NAME, MCP_FILE_NAME, ModelsOverride, RAGS_DIR_NAME,
|
||||||
ROLES_DIR_NAME, SKILLS_DIR_NAME,
|
ROLES_DIR_NAME,
|
||||||
};
|
};
|
||||||
use crate::client::ProviderModels;
|
use crate::client::ProviderModels;
|
||||||
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
use crate::utils::{get_env_name, list_file_names, normalize_env_name};
|
||||||
@@ -65,21 +65,6 @@ pub fn role_file(name: &str) -> PathBuf {
|
|||||||
roles_dir().join(format!("{name}.md"))
|
roles_dir().join(format!("{name}.md"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skills_dir() -> PathBuf {
|
|
||||||
match env::var(get_env_name("skills_dir")) {
|
|
||||||
Ok(value) => PathBuf::from(value),
|
|
||||||
Err(_) => local_path(SKILLS_DIR_NAME),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn skill_dir(name: &str) -> PathBuf {
|
|
||||||
skills_dir().join(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn skill_file(name: &str) -> PathBuf {
|
|
||||||
skill_dir(name).join("SKILL.md")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn macros_dir() -> PathBuf {
|
pub fn macros_dir() -> PathBuf {
|
||||||
match env::var(get_env_name("macros_dir")) {
|
match env::var(get_env_name("macros_dir")) {
|
||||||
Ok(value) => PathBuf::from(value),
|
Ok(value) => PathBuf::from(value),
|
||||||
@@ -249,28 +234,6 @@ pub fn has_macro(name: &str) -> bool {
|
|||||||
names.contains(&name.to_string())
|
names.contains(&name.to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_skills() -> Vec<String> {
|
|
||||||
let mut names = Vec::new();
|
|
||||||
if let Ok(rd) = read_dir(skills_dir()) {
|
|
||||||
for entry in rd.flatten() {
|
|
||||||
if let Ok(file_type) = entry.file_type()
|
|
||||||
&& file_type.is_dir()
|
|
||||||
&& let Some(name) = entry.file_name().to_str()
|
|
||||||
&& entry.path().join("SKILL.md").is_file()
|
|
||||||
{
|
|
||||||
names.push(name.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
names.sort_unstable();
|
|
||||||
names
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_skill(name: &str) -> bool {
|
|
||||||
skill_file(name).is_file()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn local_models_override() -> Result<Vec<ProviderModels>> {
|
pub fn local_models_override() -> Result<Vec<ProviderModels>> {
|
||||||
let model_override_path = models_override_file();
|
let model_override_path = models_override_file();
|
||||||
let err = || {
|
let err = || {
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
use super::rag_cache::{RagCache, RagKey};
|
use super::rag_cache::{RagCache, RagKey};
|
||||||
use super::session::Session;
|
use super::session::Session;
|
||||||
use super::skill::{SKILL_SCAFFOLD, Skill};
|
|
||||||
use super::skill_policy::SkillPolicy;
|
|
||||||
use super::skill_registry::SkillRegistry;
|
|
||||||
use super::todo::TodoList;
|
use super::todo::TodoList;
|
||||||
use super::tool_scope::{McpRuntime, ToolScope};
|
use super::tool_scope::{McpRuntime, ToolScope};
|
||||||
use super::{
|
use super::{
|
||||||
@@ -15,7 +12,7 @@ use super::{MessageContentToolCalls, prompts};
|
|||||||
use crate::client::{Model, ModelType, list_models};
|
use crate::client::{Model, ModelType, list_models};
|
||||||
use crate::function::{
|
use crate::function::{
|
||||||
FunctionDeclaration, Functions, ToolCallTracker, ToolResult,
|
FunctionDeclaration, Functions, ToolCallTracker, ToolResult,
|
||||||
skill::SKILL_FUNCTION_PREFIX, user_interaction::USER_FUNCTION_PREFIX,
|
user_interaction::USER_FUNCTION_PREFIX,
|
||||||
};
|
};
|
||||||
use crate::mcp::{
|
use crate::mcp::{
|
||||||
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
|
MCP_DESCRIBE_META_FUNCTION_NAME_PREFIX, MCP_INVOKE_META_FUNCTION_NAME_PREFIX,
|
||||||
@@ -37,7 +34,7 @@ use indexmap::IndexMap;
|
|||||||
use indoc::formatdoc;
|
use indoc::formatdoc;
|
||||||
use inquire::{Confirm, MultiSelect, Text, list_option::ListOption, validator::Validation};
|
use inquire::{Confirm, MultiSelect, Text, list_option::ListOption, validator::Validation};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::fs::{File, OpenOptions, read_dir, read_to_string, remove_dir_all, remove_file};
|
use std::fs::{File, OpenOptions, read_dir, read_to_string, remove_dir_all, remove_file};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
@@ -85,7 +82,6 @@ pub struct RequestContext {
|
|||||||
pub current_depth: usize,
|
pub current_depth: usize,
|
||||||
pub auto_continue_count: usize,
|
pub auto_continue_count: usize,
|
||||||
pub todo_list: TodoList,
|
pub todo_list: TodoList,
|
||||||
pub skill_registry: SkillRegistry,
|
|
||||||
pub last_continuation_response: Option<String>,
|
pub last_continuation_response: Option<String>,
|
||||||
|
|
||||||
pub render_mode: RenderMode,
|
pub render_mode: RenderMode,
|
||||||
@@ -114,7 +110,6 @@ impl RequestContext {
|
|||||||
current_depth: 0,
|
current_depth: 0,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: TodoList::default(),
|
todo_list: TodoList::default(),
|
||||||
skill_registry: SkillRegistry::default(),
|
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: RenderMode::default(),
|
render_mode: RenderMode::default(),
|
||||||
}
|
}
|
||||||
@@ -132,13 +127,6 @@ impl RequestContext {
|
|||||||
functions.append_user_interaction_functions();
|
functions.append_user_interaction_functions();
|
||||||
}
|
}
|
||||||
|
|
||||||
if app.config.function_calling_support {
|
|
||||||
let policy = SkillPolicy::effective(&app.config, None, None, None)?;
|
|
||||||
if policy.skills_enabled {
|
|
||||||
functions.append_skill_functions();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut mcp_runtime = McpRuntime::default();
|
let mut mcp_runtime = McpRuntime::default();
|
||||||
if let Some(registry) = &app.mcp_registry {
|
if let Some(registry) = &app.mcp_registry {
|
||||||
mcp_runtime.sync_from_registry(registry);
|
mcp_runtime.sync_from_registry(registry);
|
||||||
@@ -169,7 +157,6 @@ impl RequestContext {
|
|||||||
current_depth: 0,
|
current_depth: 0,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: TodoList::default(),
|
todo_list: TodoList::default(),
|
||||||
skill_registry: SkillRegistry::default(),
|
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: RenderMode::default(),
|
render_mode: RenderMode::default(),
|
||||||
})
|
})
|
||||||
@@ -211,7 +198,6 @@ impl RequestContext {
|
|||||||
current_depth: self.current_depth,
|
current_depth: self.current_depth,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: self.todo_list.clone(),
|
todo_list: self.todo_list.clone(),
|
||||||
skill_registry: self.skill_registry.clone(),
|
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: self.render_mode,
|
render_mode: self.render_mode,
|
||||||
}
|
}
|
||||||
@@ -251,7 +237,6 @@ impl RequestContext {
|
|||||||
current_depth,
|
current_depth,
|
||||||
auto_continue_count: 0,
|
auto_continue_count: 0,
|
||||||
todo_list: TodoList::default(),
|
todo_list: TodoList::default(),
|
||||||
skill_registry: SkillRegistry::default(),
|
|
||||||
last_continuation_response: None,
|
last_continuation_response: None,
|
||||||
render_mode: parent.render_mode,
|
render_mode: parent.render_mode,
|
||||||
}
|
}
|
||||||
@@ -626,7 +611,7 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.skill_registry.effective_role(&role)
|
role
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn auto_continue_config(&self) -> AutoContinueConfig {
|
pub fn auto_continue_config(&self) -> AutoContinueConfig {
|
||||||
@@ -829,7 +814,6 @@ impl RequestContext {
|
|||||||
if !app.dry_run {
|
if !app.dry_run {
|
||||||
self.save_message(app, input, output)?;
|
self.save_message(app, input, output)?;
|
||||||
}
|
}
|
||||||
self.skill_registry.sweep_auto_unload();
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -898,7 +882,6 @@ impl RequestContext {
|
|||||||
("env_file", display_path(&paths::env_file())),
|
("env_file", display_path(&paths::env_file())),
|
||||||
("agents_dir", display_path(&paths::agents_data_dir())),
|
("agents_dir", display_path(&paths::agents_data_dir())),
|
||||||
("roles_dir", display_path(&paths::roles_dir())),
|
("roles_dir", display_path(&paths::roles_dir())),
|
||||||
("skills_dir", display_path(&paths::skills_dir())),
|
|
||||||
("sessions_dir", display_path(&self.sessions_dir())),
|
("sessions_dir", display_path(&self.sessions_dir())),
|
||||||
("rags_dir", display_path(&paths::rags_dir())),
|
("rags_dir", display_path(&paths::rags_dir())),
|
||||||
("macros_dir", display_path(&paths::macros_dir())),
|
("macros_dir", display_path(&paths::macros_dir())),
|
||||||
@@ -1145,9 +1128,7 @@ impl RequestContext {
|
|||||||
.declarations()
|
.declarations()
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|v| {
|
.filter(|v| {
|
||||||
(v.name.starts_with(USER_FUNCTION_PREFIX)
|
v.name.starts_with(USER_FUNCTION_PREFIX) && !existing.contains(&v.name)
|
||||||
|| v.name.starts_with(SKILL_FUNCTION_PREFIX))
|
|
||||||
&& !existing.contains(&v.name)
|
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect();
|
.collect();
|
||||||
@@ -1556,7 +1537,6 @@ impl RequestContext {
|
|||||||
"session" => (self.sessions_dir(), Some(".yaml")),
|
"session" => (self.sessions_dir(), Some(".yaml")),
|
||||||
"rag" => (paths::rags_dir(), Some(".yaml")),
|
"rag" => (paths::rags_dir(), Some(".yaml")),
|
||||||
"macro" => (paths::macros_dir(), Some(".yaml")),
|
"macro" => (paths::macros_dir(), Some(".yaml")),
|
||||||
"skill" => (paths::skills_dir(), None),
|
|
||||||
"agent-data" => (paths::agents_data_dir(), None),
|
"agent-data" => (paths::agents_data_dir(), None),
|
||||||
_ => bail!("Unknown kind '{kind}'"),
|
_ => bail!("Unknown kind '{kind}'"),
|
||||||
};
|
};
|
||||||
@@ -1882,13 +1862,6 @@ impl RequestContext {
|
|||||||
super::map_completion_values(values)
|
super::map_completion_values(values)
|
||||||
}
|
}
|
||||||
".macro" => super::map_completion_values(paths::list_macros()),
|
".macro" => super::map_completion_values(paths::list_macros()),
|
||||||
".skill" => {
|
|
||||||
super::map_completion_values(vec![
|
|
||||||
"loaded".to_string(),
|
|
||||||
"load".to_string(),
|
|
||||||
"unload".to_string(),
|
|
||||||
])
|
|
||||||
}
|
|
||||||
".starter" => match &self.agent {
|
".starter" => match &self.agent {
|
||||||
Some(agent) => agent
|
Some(agent) => agent
|
||||||
.conversation_starters()
|
.conversation_starters()
|
||||||
@@ -1931,7 +1904,6 @@ impl RequestContext {
|
|||||||
"session",
|
"session",
|
||||||
"rag",
|
"rag",
|
||||||
"macro",
|
"macro",
|
||||||
"skill",
|
|
||||||
"agent-data",
|
"agent-data",
|
||||||
]),
|
]),
|
||||||
".vault" => {
|
".vault" => {
|
||||||
@@ -1944,12 +1916,6 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
};
|
};
|
||||||
} else if (cmd == ".edit" && args.first() == Some(&"skill") && args.len() == 2)
|
|
||||||
|| (cmd == ".skill" && args.first() == Some(&"load") && args.len() == 2)
|
|
||||||
{
|
|
||||||
values = super::map_completion_values(paths::list_skills());
|
|
||||||
} else if cmd == ".skill" && args.first() == Some(&"unload") && args.len() == 2 {
|
|
||||||
values = super::map_completion_values(self.skill_registry.loaded_names());
|
|
||||||
} else if cmd == ".install" && args.first() == Some(&"remote") && args.len() >= 2 {
|
} else if cmd == ".install" && args.first() == Some(&"remote") && args.len() >= 2 {
|
||||||
let prev = args.get(args.len() - 2).copied().unwrap_or("");
|
let prev = args.get(args.len() - 2).copied().unwrap_or("");
|
||||||
if prev == "--filter" {
|
if prev == "--filter" {
|
||||||
@@ -2095,35 +2061,6 @@ impl RequestContext {
|
|||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
abort_signal: AbortSignal,
|
abort_signal: AbortSignal,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let policy = SkillPolicy::effective(
|
|
||||||
app,
|
|
||||||
self.role.as_ref(),
|
|
||||||
self.agent.as_ref(),
|
|
||||||
self.session.as_ref(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let enabled_mcp_servers = if policy.skills_enabled && app.mcp_server_support {
|
|
||||||
let skill_mcps = self.skill_registry.loaded_mcp_servers();
|
|
||||||
match (enabled_mcp_servers.as_deref(), skill_mcps.is_empty()) {
|
|
||||||
(Some("all"), _) | (_, true) => enabled_mcp_servers,
|
|
||||||
(base, false) => {
|
|
||||||
let mut merged: BTreeSet<String> = skill_mcps;
|
|
||||||
if let Some(s) = base {
|
|
||||||
for token in s.split(',') {
|
|
||||||
let t = token.trim();
|
|
||||||
if !t.is_empty() {
|
|
||||||
merged.insert(t.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(merged.into_iter().collect::<Vec<_>>().join(","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
enabled_mcp_servers
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut mcp_runtime = McpRuntime::new();
|
let mut mcp_runtime = McpRuntime::new();
|
||||||
|
|
||||||
if app.mcp_server_support
|
if app.mcp_server_support
|
||||||
@@ -2191,9 +2128,6 @@ impl RequestContext {
|
|||||||
if !mcp_runtime.is_empty() {
|
if !mcp_runtime.is_empty() {
|
||||||
functions.append_mcp_meta_functions(mcp_runtime.server_names());
|
functions.append_mcp_meta_functions(mcp_runtime.server_names());
|
||||||
}
|
}
|
||||||
if app.function_calling_support && policy.skills_enabled {
|
|
||||||
functions.append_skill_functions();
|
|
||||||
}
|
|
||||||
|
|
||||||
let tool_tracker = self.tool_scope.tool_tracker.clone();
|
let tool_tracker = self.tool_scope.tool_tracker.clone();
|
||||||
self.tool_scope = ToolScope {
|
self.tool_scope = ToolScope {
|
||||||
@@ -2204,30 +2138,6 @@ impl RequestContext {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn refresh_tool_scope(&mut self, abort_signal: AbortSignal) -> Result<()> {
|
|
||||||
let app = (*self.app.config).clone();
|
|
||||||
let base_mcps = if app.mcp_server_support {
|
|
||||||
if let Some(session) = &self.session {
|
|
||||||
session.enabled_mcp_servers()
|
|
||||||
} else if let Some(agent) = &self.agent {
|
|
||||||
let names = agent.mcp_server_names();
|
|
||||||
if names.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(names.join(","))
|
|
||||||
}
|
|
||||||
} else if let Some(role) = &self.role {
|
|
||||||
role.enabled_mcp_servers()
|
|
||||||
} else {
|
|
||||||
app.enabled_mcp_servers.clone()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
self.rebuild_tool_scope(&app, base_mcps, abort_signal).await
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn use_role(
|
pub async fn use_role(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &AppConfig,
|
app: &AppConfig,
|
||||||
@@ -2500,101 +2410,6 @@ impl RequestContext {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn upsert_skill(&self, app: &AppConfig, name: &str) -> Result<()> {
|
|
||||||
let path = paths::skill_file(name);
|
|
||||||
ensure_parent_exists(&path)?;
|
|
||||||
let is_new = !path.exists();
|
|
||||||
if is_new {
|
|
||||||
fs::write(&path, SKILL_SCAFFOLD)
|
|
||||||
.with_context(|| format!("Failed to scaffold skill at {}", path.display()))?;
|
|
||||||
}
|
|
||||||
let editor = app.editor()?;
|
|
||||||
edit_file(&editor, &path)?;
|
|
||||||
if is_new {
|
|
||||||
println!("✓ Created skill at '{}'.", path.display());
|
|
||||||
} else {
|
|
||||||
println!("✓ Saved skill at '{}'.", path.display());
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load_skill_repl(&mut self, name: &str, abort_signal: AbortSignal) -> Result<()> {
|
|
||||||
if !paths::has_skill(name) {
|
|
||||||
bail!(
|
|
||||||
"Skill '{name}' is not installed (expected at {})",
|
|
||||||
paths::skill_file(name).display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let policy = SkillPolicy::effective(
|
|
||||||
&self.app.config,
|
|
||||||
self.role.as_ref(),
|
|
||||||
self.agent.as_ref(),
|
|
||||||
self.session.as_ref(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if !policy.skills_enabled {
|
|
||||||
bail!("Skills are disabled in this context");
|
|
||||||
}
|
|
||||||
|
|
||||||
if !policy.allows(name) {
|
|
||||||
bail!("Skill '{name}' is not enabled in this context");
|
|
||||||
}
|
|
||||||
|
|
||||||
let skill = Skill::load(name)?;
|
|
||||||
let fn_on = self.app.config.function_calling_support;
|
|
||||||
let mcp_on = self.app.config.mcp_server_support;
|
|
||||||
let needs_tools = skill
|
|
||||||
.enabled_tools()
|
|
||||||
.map(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or(false);
|
|
||||||
let needs_mcps = skill
|
|
||||||
.enabled_mcp_servers()
|
|
||||||
.map(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if needs_tools && !fn_on {
|
|
||||||
bail!("Skill '{name}' requires function calling, which is disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
if needs_mcps && !mcp_on {
|
|
||||||
bail!("Skill '{name}' requires MCP servers, which are disabled");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.skill_registry.insert(skill)?;
|
|
||||||
if let Err(e) = self.refresh_tool_scope(abort_signal).await {
|
|
||||||
let _ = self.skill_registry.unload(name);
|
|
||||||
bail!("Loaded skill '{name}' but failed to refresh tool scope: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("✓ Loaded skill '{name}'.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn unload_skill_repl(&mut self, name: &str, abort_signal: AbortSignal) -> Result<()> {
|
|
||||||
self.skill_registry.unload(name)?;
|
|
||||||
|
|
||||||
if let Err(e) = self.refresh_tool_scope(abort_signal).await {
|
|
||||||
eprintln!("Warning: unloaded skill '{name}' but tool scope refresh failed: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
println!("✓ Unloaded skill '{name}'.");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn list_loaded_skills(&self) {
|
|
||||||
let names = self.skill_registry.loaded_names();
|
|
||||||
|
|
||||||
if names.is_empty() {
|
|
||||||
println!("No skills loaded.");
|
|
||||||
} else {
|
|
||||||
println!("Loaded skills:");
|
|
||||||
for name in names {
|
|
||||||
println!(" • {name}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn apply_prelude(
|
pub async fn apply_prelude(
|
||||||
&mut self,
|
&mut self,
|
||||||
app: &AppConfig,
|
app: &AppConfig,
|
||||||
@@ -3518,58 +3333,6 @@ mod tests {
|
|||||||
assert!(lm.continuous);
|
assert!(lm.continuous);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn after_chat_completion_sweeps_auto_unload_skills_at_turn_end() {
|
|
||||||
let mut ctx = create_test_ctx();
|
|
||||||
ctx.app = Arc::new(AppState {
|
|
||||||
config: Arc::new(AppConfig {
|
|
||||||
dry_run: true,
|
|
||||||
..(*ctx.app.config).clone()
|
|
||||||
}),
|
|
||||||
..(*ctx.app).clone()
|
|
||||||
});
|
|
||||||
|
|
||||||
let ephemeral = Skill::new("ephemeral", "---\nauto_unload: true\n---\nbody");
|
|
||||||
let persistent = Skill::new("persistent", "---\nauto_unload: false\n---\nbody");
|
|
||||||
ctx.skill_registry.insert(ephemeral).unwrap();
|
|
||||||
ctx.skill_registry.insert(persistent).unwrap();
|
|
||||||
|
|
||||||
let input = Input::from_str(&ctx, "hello", None);
|
|
||||||
let app = Arc::clone(&ctx.app.config);
|
|
||||||
ctx.after_chat_completion(app.as_ref(), &input, "response", &[])
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(!ctx.skill_registry.is_loaded("ephemeral"));
|
|
||||||
assert!(ctx.skill_registry.is_loaded("persistent"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn after_chat_completion_preserves_auto_unload_during_tool_loop() {
|
|
||||||
let mut ctx = create_test_ctx();
|
|
||||||
ctx.app = Arc::new(AppState {
|
|
||||||
config: Arc::new(AppConfig {
|
|
||||||
dry_run: true,
|
|
||||||
..(*ctx.app.config).clone()
|
|
||||||
}),
|
|
||||||
..(*ctx.app).clone()
|
|
||||||
});
|
|
||||||
|
|
||||||
let ephemeral = Skill::new("ephemeral", "---\nauto_unload: true\n---\nbody");
|
|
||||||
ctx.skill_registry.insert(ephemeral).unwrap();
|
|
||||||
|
|
||||||
let input = Input::from_str(&ctx, "hello", None);
|
|
||||||
let app = Arc::clone(&ctx.app.config);
|
|
||||||
let tool_result =
|
|
||||||
ToolResult::new(crate::function::ToolCall::default(), serde_json::json!({}));
|
|
||||||
ctx.after_chat_completion(app.as_ref(), &input, "", &[tool_result])
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
ctx.skill_registry.is_loaded("ephemeral"),
|
|
||||||
"auto_unload skills must persist through tool-using rounds"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn role_like_mut_returns_none_when_empty() {
|
fn role_like_mut_returns_none_when_empty() {
|
||||||
let mut ctx = create_test_ctx();
|
let mut ctx = create_test_ctx();
|
||||||
@@ -4074,43 +3837,6 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn install_builtin_skills_force_overwrites_only_with_force() {
|
|
||||||
let _guard = TestConfigDirGuard::new();
|
|
||||||
|
|
||||||
Skill::install_builtin_skills(false).unwrap();
|
|
||||||
let file = paths::skill_file("git-master");
|
|
||||||
assert!(file.exists(), "git-master skill should be installed");
|
|
||||||
|
|
||||||
write(&file, "SENTINEL").unwrap();
|
|
||||||
Skill::install_builtin_skills(false).unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
read_to_string(&file).unwrap(),
|
|
||||||
"SENTINEL",
|
|
||||||
"non-force install must not overwrite an existing skill"
|
|
||||||
);
|
|
||||||
|
|
||||||
Skill::install_builtin_skills(true).unwrap();
|
|
||||||
assert_ne!(
|
|
||||||
read_to_string(&file).unwrap(),
|
|
||||||
"SENTINEL",
|
|
||||||
"force install must overwrite the existing skill"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
#[serial]
|
|
||||||
fn install_builtin_skills_installs_all_bundled() {
|
|
||||||
let _guard = TestConfigDirGuard::new();
|
|
||||||
|
|
||||||
Skill::install_builtin_skills(false).unwrap();
|
|
||||||
assert!(paths::skill_file("git-master").exists());
|
|
||||||
assert!(paths::skill_file("ai-slop-remover").exists());
|
|
||||||
assert!(paths::skill_file("code-review").exists());
|
|
||||||
assert!(paths::skill_file("frontend-ui-ux").exists());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
fn install_functions_force_preserves_user_mcp_json() {
|
fn install_functions_force_preserves_user_mcp_json() {
|
||||||
|
|||||||
@@ -56,10 +56,6 @@ pub struct Role {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
skills_enabled: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
enabled_skills: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
auto_continue: Option<bool>,
|
auto_continue: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
max_auto_continues: Option<usize>,
|
max_auto_continues: Option<usize>,
|
||||||
@@ -102,8 +98,6 @@ impl Role {
|
|||||||
"enabled_mcp_servers" => {
|
"enabled_mcp_servers" => {
|
||||||
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
|
role.enabled_mcp_servers = value.as_str().map(|v| v.to_string())
|
||||||
}
|
}
|
||||||
"skills_enabled" => role.skills_enabled = value.as_bool(),
|
|
||||||
"enabled_skills" => role.enabled_skills = value.as_str().map(|v| v.to_string()),
|
|
||||||
"auto_continue" => role.auto_continue = value.as_bool(),
|
"auto_continue" => role.auto_continue = value.as_bool(),
|
||||||
"max_auto_continues" => {
|
"max_auto_continues" => {
|
||||||
role.max_auto_continues = value.as_u64().map(|v| v as usize)
|
role.max_auto_continues = value.as_u64().map(|v| v as usize)
|
||||||
@@ -153,12 +147,6 @@ impl Role {
|
|||||||
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
|
if let Some(enabled_mcp_servers) = self.enabled_mcp_servers() {
|
||||||
metadata.push(format!("enabled_mcp_servers: {enabled_mcp_servers}"));
|
metadata.push(format!("enabled_mcp_servers: {enabled_mcp_servers}"));
|
||||||
}
|
}
|
||||||
if let Some(skills_enabled) = self.skills_enabled {
|
|
||||||
metadata.push(format!("skills_enabled: {skills_enabled}"));
|
|
||||||
}
|
|
||||||
if let Some(enabled_skills) = &self.enabled_skills {
|
|
||||||
metadata.push(format!("enabled_skills: {enabled_skills}"));
|
|
||||||
}
|
|
||||||
if let Some(auto_continue) = self.auto_continue {
|
if let Some(auto_continue) = self.auto_continue {
|
||||||
metadata.push(format!("auto_continue: {auto_continue}"));
|
metadata.push(format!("auto_continue: {auto_continue}"));
|
||||||
}
|
}
|
||||||
@@ -283,14 +271,6 @@ impl Role {
|
|||||||
self.continuation_prompt.as_deref()
|
self.continuation_prompt.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn skills_enabled(&self) -> Option<bool> {
|
|
||||||
self.skills_enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled_skills(&self) -> Option<&str> {
|
|
||||||
self.enabled_skills.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append_to_prompt(&mut self, text: &str) {
|
pub fn append_to_prompt(&mut self, text: &str) {
|
||||||
self.prompt.push_str(text);
|
self.prompt.push_str(text);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,10 +29,6 @@ pub struct Session {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
enabled_mcp_servers: Option<String>,
|
enabled_mcp_servers: Option<String>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
skills_enabled: Option<bool>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
enabled_skills: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
save_session: Option<bool>,
|
save_session: Option<bool>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
compression_threshold: Option<usize>,
|
compression_threshold: Option<usize>,
|
||||||
@@ -79,14 +75,6 @@ pub struct Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
pub fn skills_enabled(&self) -> Option<bool> {
|
|
||||||
self.skills_enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled_skills(&self) -> Option<&str> {
|
|
||||||
self.enabled_skills.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self {
|
pub fn new_from_ctx(ctx: &RequestContext, app: &AppConfig, name: &str) -> Self {
|
||||||
let role = ctx.extract_role(app);
|
let role = ctx.extract_role(app);
|
||||||
let mut session = Self {
|
let mut session = Self {
|
||||||
|
|||||||
@@ -1,326 +0,0 @@
|
|||||||
use super::*;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use fancy_regex::Regex;
|
|
||||||
use log::{debug, info};
|
|
||||||
use rust_embed::Embed;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
#[derive(Embed)]
|
|
||||||
#[folder = "assets/skills/"]
|
|
||||||
struct SkillsAsset;
|
|
||||||
|
|
||||||
static RE_METADATA: LazyLock<Regex> =
|
|
||||||
LazyLock::new(|| Regex::new(r"(?s)-{3,}\s*(.*?)\s*-{3,}\s*(.*)").unwrap());
|
|
||||||
|
|
||||||
pub const SKILL_SCAFFOLD: &str = "\
|
|
||||||
---
|
|
||||||
description: One-line description shown to the model when listing skills.
|
|
||||||
enabled_tools:
|
|
||||||
enabled_mcp_servers:
|
|
||||||
auto_unload: false
|
|
||||||
---
|
|
||||||
Replace this body with the knowledge or methodology this skill teaches.
|
|
||||||
";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
|
|
||||||
pub struct Skill {
|
|
||||||
name: String,
|
|
||||||
#[serde(default)]
|
|
||||||
description: String,
|
|
||||||
#[serde(default)]
|
|
||||||
body: String,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
enabled_tools: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
enabled_mcp_servers: Option<String>,
|
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
|
||||||
auto_unload: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Skill {
|
|
||||||
pub fn new(name: &str, content: &str) -> Self {
|
|
||||||
let mut metadata = "";
|
|
||||||
let mut body = content.trim();
|
|
||||||
if let Ok(Some(caps)) = RE_METADATA.captures(content)
|
|
||||||
&& let (Some(metadata_value), Some(body_value)) = (caps.get(1), caps.get(2))
|
|
||||||
{
|
|
||||||
metadata = metadata_value.as_str().trim();
|
|
||||||
body = body_value.as_str().trim();
|
|
||||||
}
|
|
||||||
let mut body = body.to_string();
|
|
||||||
interpolate_variables(&mut body);
|
|
||||||
let mut skill = Self {
|
|
||||||
name: name.to_string(),
|
|
||||||
body,
|
|
||||||
..Default::default()
|
|
||||||
};
|
|
||||||
if !metadata.is_empty()
|
|
||||||
&& let Ok(value) = serde_yaml::from_str::<Value>(metadata)
|
|
||||||
&& let Some(value) = value.as_object()
|
|
||||||
{
|
|
||||||
for (key, value) in value {
|
|
||||||
match key.as_str() {
|
|
||||||
"description" => {
|
|
||||||
if let Some(v) = value.as_str() {
|
|
||||||
skill.description = v.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"enabled_tools" => {
|
|
||||||
skill.enabled_tools = value.as_str().map(|v| v.to_string());
|
|
||||||
}
|
|
||||||
"enabled_mcp_servers" => {
|
|
||||||
skill.enabled_mcp_servers = value.as_str().map(|v| v.to_string());
|
|
||||||
}
|
|
||||||
"auto_unload" => {
|
|
||||||
skill.auto_unload = value.as_bool();
|
|
||||||
}
|
|
||||||
_ => (),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
skill
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn install_builtin_skills(force: bool) -> Result<()> {
|
|
||||||
info!(
|
|
||||||
"Installing built-in skills in {}",
|
|
||||||
paths::skills_dir().display()
|
|
||||||
);
|
|
||||||
|
|
||||||
for file in SkillsAsset::iter() {
|
|
||||||
debug!("Processing skill file: {}", file.as_ref());
|
|
||||||
|
|
||||||
let embedded_file = SkillsAsset::get(&file)
|
|
||||||
.ok_or_else(|| anyhow!("Failed to load embedded skill file: {}", file.as_ref()))?;
|
|
||||||
let content = unsafe { std::str::from_utf8_unchecked(&embedded_file.data) };
|
|
||||||
let file_path = paths::skills_dir().join(file.as_ref());
|
|
||||||
|
|
||||||
if file_path.exists() && !force {
|
|
||||||
debug!(
|
|
||||||
"Skill file already exists, skipping: {}",
|
|
||||||
file_path.display()
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ensure_parent_exists(&file_path)?;
|
|
||||||
info!("Creating skill file: {}", file_path.display());
|
|
||||||
let mut skill_file = File::create(&file_path)?;
|
|
||||||
Write::write_all(&mut skill_file, content.as_bytes())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn load(name: &str) -> Result<Self> {
|
|
||||||
let path = paths::skill_file(name);
|
|
||||||
let content = read_to_string(&path)
|
|
||||||
.with_context(|| format!("Failed to read skill '{name}' at {}", path.display()))?;
|
|
||||||
Ok(Skill::new(name, &content))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn description(&self) -> &str {
|
|
||||||
&self.description
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn body(&self) -> &str {
|
|
||||||
&self.body
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled_tools(&self) -> Option<&str> {
|
|
||||||
self.enabled_tools.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn enabled_mcp_servers(&self) -> Option<&str> {
|
|
||||||
self.enabled_mcp_servers.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn auto_unload(&self) -> bool {
|
|
||||||
self.auto_unload.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_compatible(&self, function_calling_enabled: bool, mcp_enabled: bool) -> bool {
|
|
||||||
if self.declares_tools() && !function_calling_enabled {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.declares_mcp_servers() && !mcp_enabled {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn declares_tools(&self) -> bool {
|
|
||||||
self.enabled_tools
|
|
||||||
.as_deref()
|
|
||||||
.map(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn declares_mcp_servers(&self) -> bool {
|
|
||||||
self.enabled_mcp_servers
|
|
||||||
.as_deref()
|
|
||||||
.map(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_new_parses_body() {
|
|
||||||
let skill = Skill::new("test", "You are a git expert");
|
|
||||||
|
|
||||||
assert_eq!(skill.name(), "test");
|
|
||||||
assert_eq!(skill.body(), "You are a git expert");
|
|
||||||
assert_eq!(skill.description(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_new_parses_full_metadata() {
|
|
||||||
let content = "---\n\
|
|
||||||
description: Atomic commits, rebase surgery\n\
|
|
||||||
enabled_tools: shell,fs\n\
|
|
||||||
enabled_mcp_servers: github\n\
|
|
||||||
auto_unload: true\n\
|
|
||||||
---\n\
|
|
||||||
You are a git expert";
|
|
||||||
|
|
||||||
let skill = Skill::new("git-master", content);
|
|
||||||
|
|
||||||
assert_eq!(skill.name(), "git-master");
|
|
||||||
assert_eq!(skill.description(), "Atomic commits, rebase surgery");
|
|
||||||
assert_eq!(skill.enabled_tools(), Some("shell,fs"));
|
|
||||||
assert_eq!(skill.enabled_mcp_servers(), Some("github"));
|
|
||||||
assert!(skill.auto_unload());
|
|
||||||
assert_eq!(skill.body(), "You are a git expert");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_new_no_metadata_has_defaults() {
|
|
||||||
let skill = Skill::new("test", "Just a body");
|
|
||||||
|
|
||||||
assert_eq!(skill.description(), "");
|
|
||||||
assert_eq!(skill.enabled_tools(), None);
|
|
||||||
assert_eq!(skill.enabled_mcp_servers(), None);
|
|
||||||
assert!(!skill.auto_unload());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_new_metadata_only() {
|
|
||||||
let content = "---\ndescription: Just metadata\n---";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert_eq!(skill.description(), "Just metadata");
|
|
||||||
assert_eq!(skill.body(), "");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_new_partial_metadata_leaves_others_none() {
|
|
||||||
let content = "---\ndescription: Partial\n---\nthe body";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert_eq!(skill.description(), "Partial");
|
|
||||||
assert_eq!(skill.enabled_tools(), None);
|
|
||||||
assert_eq!(skill.enabled_mcp_servers(), None);
|
|
||||||
assert!(!skill.auto_unload());
|
|
||||||
assert_eq!(skill.body(), "the body");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_new_ignores_unknown_keys() {
|
|
||||||
let content = "---\ndescription: D\nbogus_field: 42\n---\nbody";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert_eq!(skill.description(), "D");
|
|
||||||
assert_eq!(skill.body(), "body");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_new_trims_body_whitespace() {
|
|
||||||
let content = "---\ndescription: D\n---\n\n\n body content \n\n";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert_eq!(skill.body(), "body content");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skill_default_has_empty_fields() {
|
|
||||||
let skill = Skill::default();
|
|
||||||
|
|
||||||
assert_eq!(skill.name(), "");
|
|
||||||
assert_eq!(skill.body(), "");
|
|
||||||
assert_eq!(skill.description(), "");
|
|
||||||
assert_eq!(skill.enabled_tools(), None);
|
|
||||||
assert_eq!(skill.enabled_mcp_servers(), None);
|
|
||||||
assert!(!skill.auto_unload());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_compatible_knowledge_only_passes_all_combinations() {
|
|
||||||
let skill = Skill::new("test", "Just knowledge");
|
|
||||||
|
|
||||||
assert!(skill.is_compatible(false, false));
|
|
||||||
assert!(skill.is_compatible(true, false));
|
|
||||||
assert!(skill.is_compatible(false, true));
|
|
||||||
assert!(skill.is_compatible(true, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_compatible_with_tools_requires_function_calling() {
|
|
||||||
let content = "---\nenabled_tools: shell\n---\nbody";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert!(!skill.is_compatible(false, true));
|
|
||||||
assert!(!skill.is_compatible(false, false));
|
|
||||||
assert!(skill.is_compatible(true, true));
|
|
||||||
assert!(skill.is_compatible(true, false));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_compatible_with_mcp_requires_mcp_enabled() {
|
|
||||||
let content = "---\nenabled_mcp_servers: github\n---\nbody";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert!(!skill.is_compatible(true, false));
|
|
||||||
assert!(!skill.is_compatible(false, false));
|
|
||||||
assert!(skill.is_compatible(true, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_compatible_requires_both_when_both_declared() {
|
|
||||||
let content = "---\nenabled_tools: shell\nenabled_mcp_servers: github\n---\nbody";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert!(!skill.is_compatible(true, false));
|
|
||||||
assert!(!skill.is_compatible(false, true));
|
|
||||||
assert!(!skill.is_compatible(false, false));
|
|
||||||
assert!(skill.is_compatible(true, true));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_compatible_empty_string_tools_is_knowledge_only() {
|
|
||||||
let content = "---\nenabled_tools: \"\"\n---\nbody";
|
|
||||||
|
|
||||||
let skill = Skill::new("test", content);
|
|
||||||
|
|
||||||
assert!(skill.is_compatible(false, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,312 +0,0 @@
|
|||||||
use super::agent::Agent;
|
|
||||||
use super::app_config::AppConfig;
|
|
||||||
use super::paths;
|
|
||||||
use super::role::Role;
|
|
||||||
use super::session::Session;
|
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct SkillPolicy {
|
|
||||||
pub skills_enabled: bool,
|
|
||||||
pub enabled: HashSet<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SkillPolicy {
|
|
||||||
pub fn effective(
|
|
||||||
global: &AppConfig,
|
|
||||||
role: Option<&Role>,
|
|
||||||
agent: Option<&Agent>,
|
|
||||||
session: Option<&Session>,
|
|
||||||
) -> Result<Self> {
|
|
||||||
Self::effective_with(
|
|
||||||
global,
|
|
||||||
role,
|
|
||||||
agent,
|
|
||||||
session,
|
|
||||||
&paths::has_skill,
|
|
||||||
&paths::list_skills,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn effective_with<F, G>(
|
|
||||||
global: &AppConfig,
|
|
||||||
role: Option<&Role>,
|
|
||||||
agent: Option<&Agent>,
|
|
||||||
session: Option<&Session>,
|
|
||||||
skill_exists: &F,
|
|
||||||
list_installed: &G,
|
|
||||||
) -> Result<Self>
|
|
||||||
where
|
|
||||||
F: Fn(&str) -> bool,
|
|
||||||
G: Fn() -> Vec<String>,
|
|
||||||
{
|
|
||||||
let mut skills_enabled = global.skills_enabled;
|
|
||||||
if let Some(r) = role
|
|
||||||
&& let Some(false) = r.skills_enabled()
|
|
||||||
{
|
|
||||||
skills_enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(a) = agent
|
|
||||||
&& let Some(false) = a.skills_enabled()
|
|
||||||
{
|
|
||||||
skills_enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(s) = session
|
|
||||||
&& let Some(false) = s.skills_enabled()
|
|
||||||
{
|
|
||||||
skills_enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let visible: Option<HashSet<String>> = global
|
|
||||||
.visible_skills
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.iter().cloned().collect());
|
|
||||||
|
|
||||||
let enabled_raw: Option<Vec<String>> = session
|
|
||||||
.and_then(|s| parse_csv_opt(s.enabled_skills()))
|
|
||||||
.or_else(|| agent.and_then(|a| a.enabled_skills().map(|v| v.to_vec())))
|
|
||||||
.or_else(|| role.and_then(|r| parse_csv_opt(r.enabled_skills())))
|
|
||||||
.or_else(|| parse_csv_opt(global.enabled_skills.as_deref()));
|
|
||||||
|
|
||||||
let enabled: HashSet<String> = match enabled_raw {
|
|
||||||
Some(explicit) => {
|
|
||||||
let set: HashSet<String> = explicit.into_iter().collect();
|
|
||||||
for name in &set {
|
|
||||||
if !skill_exists(name) {
|
|
||||||
bail!("enabled_skills references skill '{name}' which is not installed");
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(vs) = &visible
|
|
||||||
&& !vs.contains(name)
|
|
||||||
{
|
|
||||||
bail!(
|
|
||||||
"enabled_skills references skill '{name}' which is not in visible_skills"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
}
|
|
||||||
None => match &visible {
|
|
||||||
Some(v) => v.clone(),
|
|
||||||
None => list_installed().into_iter().collect(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
skills_enabled,
|
|
||||||
enabled,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn allows(&self, name: &str) -> bool {
|
|
||||||
self.skills_enabled && self.enabled.contains(name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_csv_opt(s: Option<&str>) -> Option<Vec<String>> {
|
|
||||||
s.map(|raw| {
|
|
||||||
raw.split(',')
|
|
||||||
.map(|t| t.trim().to_string())
|
|
||||||
.filter(|t| !t.is_empty())
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn always_true(_: &str) -> bool {
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn empty_installed() -> Vec<String> {
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_app_config(
|
|
||||||
skills_enabled: bool,
|
|
||||||
enabled: Option<&str>,
|
|
||||||
visible: Option<&[&str]>,
|
|
||||||
) -> AppConfig {
|
|
||||||
AppConfig {
|
|
||||||
skills_enabled,
|
|
||||||
enabled_skills: enabled.map(|s| s.to_string()),
|
|
||||||
visible_skills: visible.map(|v| v.iter().map(|s| s.to_string()).collect()),
|
|
||||||
..AppConfig::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn defaults_yield_skills_enabled_with_empty_universe() {
|
|
||||||
let global = AppConfig::default();
|
|
||||||
|
|
||||||
let policy =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(policy.skills_enabled);
|
|
||||||
assert!(policy.enabled.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn falls_back_to_all_installed_when_no_level_sets_enabled_skills() {
|
|
||||||
let global = AppConfig::default();
|
|
||||||
let installed = || vec!["alpha".to_string(), "beta".to_string()];
|
|
||||||
|
|
||||||
let policy =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &always_true, &installed)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(policy.enabled.len(), 2);
|
|
||||||
assert!(policy.enabled.contains("alpha"));
|
|
||||||
assert!(policy.enabled.contains("beta"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn falls_back_to_visible_when_visible_set_but_no_enabled() {
|
|
||||||
let global = make_app_config(true, None, Some(&["alpha", "beta"]));
|
|
||||||
|
|
||||||
let policy =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert_eq!(policy.enabled.len(), 2);
|
|
||||||
assert!(policy.enabled.contains("alpha"));
|
|
||||||
assert!(policy.enabled.contains("beta"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn global_enabled_skills_is_effective_when_no_other_levels() {
|
|
||||||
let global = make_app_config(true, Some("alpha,beta"), Some(&["alpha", "beta", "gamma"]));
|
|
||||||
|
|
||||||
let policy =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(policy.enabled.contains("alpha"));
|
|
||||||
assert!(policy.enabled.contains("beta"));
|
|
||||||
assert!(!policy.enabled.contains("gamma"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn role_overrides_global_enabled_skills() {
|
|
||||||
let global = make_app_config(true, Some("alpha"), Some(&["alpha", "beta"]));
|
|
||||||
let role = Role::new("test", "---\nenabled_skills: beta\n---\nbody");
|
|
||||||
|
|
||||||
let policy = SkillPolicy::effective_with(
|
|
||||||
&global,
|
|
||||||
Some(&role),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
&always_true,
|
|
||||||
&empty_installed,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(policy.enabled.contains("beta"));
|
|
||||||
assert!(!policy.enabled.contains("alpha"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn any_skills_enabled_false_disables_globally() {
|
|
||||||
let global = make_app_config(true, None, None);
|
|
||||||
let role = Role::new("test", "---\nskills_enabled: false\n---\nbody");
|
|
||||||
|
|
||||||
let policy = SkillPolicy::effective_with(
|
|
||||||
&global,
|
|
||||||
Some(&role),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
&always_true,
|
|
||||||
&empty_installed,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(!policy.skills_enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn allows_returns_false_when_skills_disabled() {
|
|
||||||
let global = AppConfig {
|
|
||||||
skills_enabled: false,
|
|
||||||
..AppConfig::default()
|
|
||||||
};
|
|
||||||
|
|
||||||
let policy = SkillPolicy::effective_with(&global, None, None, None, &always_true, &|| {
|
|
||||||
vec!["alpha".to_string()]
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(!policy.allows("alpha"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn allows_returns_true_when_skill_in_enabled_set() {
|
|
||||||
let global = make_app_config(true, Some("alpha"), None);
|
|
||||||
|
|
||||||
let policy =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(policy.allows("alpha"));
|
|
||||||
assert!(!policy.allows("beta"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validation_rejects_uninstalled_skill_reference() {
|
|
||||||
let global = make_app_config(true, Some("ghost"), None);
|
|
||||||
|
|
||||||
let err =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &|_| false, &empty_installed)
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
assert!(err.to_string().contains("not installed"));
|
|
||||||
assert!(err.to_string().contains("ghost"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validation_rejects_skill_not_in_visible_set() {
|
|
||||||
let global = make_app_config(true, Some("beta"), Some(&["alpha"]));
|
|
||||||
|
|
||||||
let err =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &always_true, &empty_installed)
|
|
||||||
.unwrap_err();
|
|
||||||
|
|
||||||
assert!(err.to_string().contains("not in visible_skills"));
|
|
||||||
assert!(err.to_string().contains("beta"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn validation_skipped_when_no_explicit_enabled_skills() {
|
|
||||||
let global = make_app_config(true, None, None);
|
|
||||||
|
|
||||||
let policy =
|
|
||||||
SkillPolicy::effective_with(&global, None, None, None, &|_| false, &empty_installed)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(policy.enabled.is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_string_enabled_skills_resolves_to_empty_override() {
|
|
||||||
let global = make_app_config(true, Some("alpha,beta"), Some(&["alpha", "beta"]));
|
|
||||||
let role = Role::new("test", "---\nenabled_skills: \"\"\n---\nbody");
|
|
||||||
|
|
||||||
let policy = SkillPolicy::effective_with(
|
|
||||||
&global,
|
|
||||||
Some(&role),
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
&always_true,
|
|
||||||
&empty_installed,
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
assert!(policy.enabled.is_empty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
use super::role::{Role, RoleLike};
|
|
||||||
use super::skill::Skill;
|
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use std::collections::BTreeSet;
|
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
|
||||||
pub struct SkillRegistry {
|
|
||||||
loaded: IndexMap<String, Skill>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SkillRegistry {
|
|
||||||
pub fn insert(&mut self, skill: Skill) -> Result<()> {
|
|
||||||
let name = skill.name().to_string();
|
|
||||||
|
|
||||||
if self.loaded.contains_key(&name) {
|
|
||||||
bail!("Skill '{name}' is already loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
self.loaded.insert(name, skill);
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unload(&mut self, name: &str) -> Result<()> {
|
|
||||||
if self.loaded.shift_remove(name).is_none() {
|
|
||||||
bail!("Skill '{name}' is not loaded");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn loaded_names(&self) -> Vec<String> {
|
|
||||||
self.loaded.keys().cloned().collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn loaded_mcp_servers(&self) -> BTreeSet<String> {
|
|
||||||
let mut out = BTreeSet::new();
|
|
||||||
for skill in self.loaded.values() {
|
|
||||||
if let Some(csv) = skill.enabled_mcp_servers() {
|
|
||||||
for token in csv.split(',') {
|
|
||||||
let t = token.trim();
|
|
||||||
if !t.is_empty() {
|
|
||||||
out.insert(t.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_loaded(&self, name: &str) -> bool {
|
|
||||||
self.loaded.contains_key(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sweep_auto_unload(&mut self) {
|
|
||||||
self.loaded.retain(|_, skill| !skill.auto_unload());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn effective_role(&self, base: &Role) -> Role {
|
|
||||||
if self.loaded.is_empty() {
|
|
||||||
return base.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut effective = base.clone();
|
|
||||||
let skip_body = effective.is_embedded_prompt();
|
|
||||||
|
|
||||||
let base_tools_set = effective.enabled_tools().is_some();
|
|
||||||
let base_mcps_set = effective.enabled_mcp_servers().is_some();
|
|
||||||
|
|
||||||
let mut tools = parse_csv(effective.enabled_tools().as_deref());
|
|
||||||
let mut mcps = parse_csv(effective.enabled_mcp_servers().as_deref());
|
|
||||||
|
|
||||||
for (_, skill) in &self.loaded {
|
|
||||||
tools.extend(parse_csv(skill.enabled_tools()));
|
|
||||||
mcps.extend(parse_csv(skill.enabled_mcp_servers()));
|
|
||||||
if !skip_body && !skill.body().is_empty() {
|
|
||||||
let separator = if effective.is_empty_prompt() {
|
|
||||||
""
|
|
||||||
} else {
|
|
||||||
"\n\n"
|
|
||||||
};
|
|
||||||
effective.append_to_prompt(separator);
|
|
||||||
effective.append_to_prompt(skill.body());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if base_tools_set || !tools.is_empty() {
|
|
||||||
effective.set_enabled_tools(Some(join_csv(&tools)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if base_mcps_set || !mcps.is_empty() {
|
|
||||||
effective.set_enabled_mcp_servers(Some(join_csv(&mcps)));
|
|
||||||
}
|
|
||||||
|
|
||||||
effective
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_csv(s: Option<&str>) -> BTreeSet<String> {
|
|
||||||
let mut set = BTreeSet::new();
|
|
||||||
if let Some(raw) = s {
|
|
||||||
for token in raw.split(',') {
|
|
||||||
let trimmed = token.trim();
|
|
||||||
if !trimmed.is_empty() {
|
|
||||||
set.insert(trimmed.to_string());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set
|
|
||||||
}
|
|
||||||
|
|
||||||
fn join_csv(set: &BTreeSet<String>) -> String {
|
|
||||||
set.iter().cloned().collect::<Vec<_>>().join(",")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
impl SkillRegistry {
|
|
||||||
fn insert_for_test(&mut self, skill: Skill) {
|
|
||||||
self.loaded.insert(skill.name().to_string(), skill);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn make_skill(name: &str, frontmatter: &str, body: &str) -> Skill {
|
|
||||||
let content = if frontmatter.is_empty() {
|
|
||||||
body.to_string()
|
|
||||||
} else {
|
|
||||||
format!("---\n{frontmatter}\n---\n{body}")
|
|
||||||
};
|
|
||||||
Skill::new(name, &content)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_registry_returns_base_clone() {
|
|
||||||
let base = Role::new("test", "You are a helper");
|
|
||||||
let registry = SkillRegistry::default();
|
|
||||||
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert_eq!(effective.prompt(), base.prompt());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn one_skill_appends_body_after_base_with_separator() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("git-master", "description: D", "Git knowledge"));
|
|
||||||
|
|
||||||
let base = Role::new("test", "You are a helper");
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert_eq!(effective.prompt(), "You are a helper\n\nGit knowledge");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn two_skills_compose_bodies_in_insertion_order() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("a", "", "Alpha body"));
|
|
||||||
registry.insert_for_test(make_skill("b", "", "Beta body"));
|
|
||||||
|
|
||||||
let base = Role::new("test", "Base");
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert_eq!(effective.prompt(), "Base\n\nAlpha body\n\nBeta body");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_base_prompt_omits_leading_separator() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("a", "", "Alpha"));
|
|
||||||
registry.insert_for_test(make_skill("b", "", "Beta"));
|
|
||||||
|
|
||||||
let base = Role::new("test", "");
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert_eq!(effective.prompt(), "Alpha\n\nBeta");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn embedded_prompt_base_skips_body_composition() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill(
|
|
||||||
"git-master",
|
|
||||||
"enabled_tools: shell",
|
|
||||||
"should not appear",
|
|
||||||
));
|
|
||||||
|
|
||||||
let base = Role::new("test", "Process: __INPUT__");
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert_eq!(effective.prompt(), "Process: __INPUT__");
|
|
||||||
let tools = effective.enabled_tools().expect("tools set by skill");
|
|
||||||
assert!(tools.contains("shell"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn skills_with_empty_body_do_not_inject_separator() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("knowledge", "enabled_tools: fs", ""));
|
|
||||||
|
|
||||||
let base = Role::new("test", "Base");
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert_eq!(effective.prompt(), "Base");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tools_and_mcps_are_unioned_and_deduplicated() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill(
|
|
||||||
"a",
|
|
||||||
"enabled_tools: shell,fs\nenabled_mcp_servers: github",
|
|
||||||
"body",
|
|
||||||
));
|
|
||||||
registry.insert_for_test(make_skill(
|
|
||||||
"b",
|
|
||||||
"enabled_tools: fs,git\nenabled_mcp_servers: github,jira",
|
|
||||||
"body",
|
|
||||||
));
|
|
||||||
|
|
||||||
let mut base = Role::new("test", "body");
|
|
||||||
base.set_enabled_tools(Some("web_search".to_string()));
|
|
||||||
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
let tools_str = effective.enabled_tools().unwrap();
|
|
||||||
let tools: BTreeSet<&str> = tools_str.split(',').collect();
|
|
||||||
assert_eq!(tools, BTreeSet::from(["fs", "git", "shell", "web_search"]));
|
|
||||||
|
|
||||||
let mcps_str = effective.enabled_mcp_servers().unwrap();
|
|
||||||
let mcps: BTreeSet<&str> = mcps_str.split(',').collect();
|
|
||||||
assert_eq!(mcps, BTreeSet::from(["github", "jira"]));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn no_skill_tool_contributions_preserves_base_none() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("knowledge", "", "Pure knowledge"));
|
|
||||||
|
|
||||||
let base = Role::new("test", "Base");
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert!(effective.enabled_tools().is_none());
|
|
||||||
assert!(effective.enabled_mcp_servers().is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn base_some_empty_tools_is_preserved() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("knowledge", "", "Pure knowledge"));
|
|
||||||
|
|
||||||
let mut base = Role::new("test", "Base");
|
|
||||||
base.set_enabled_tools(Some(String::new()));
|
|
||||||
let effective = registry.effective_role(&base);
|
|
||||||
|
|
||||||
assert_eq!(effective.enabled_tools().as_deref(), Some(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unload_not_loaded_returns_error() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
|
|
||||||
let err = registry.unload("missing").unwrap_err();
|
|
||||||
|
|
||||||
assert!(err.to_string().contains("not loaded"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn unload_existing_succeeds_and_removes() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("git-master", "", "body"));
|
|
||||||
assert!(registry.is_loaded("git-master"));
|
|
||||||
|
|
||||||
registry.unload("git-master").unwrap();
|
|
||||||
assert!(!registry.is_loaded("git-master"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn loaded_names_returns_insertion_order() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
|
|
||||||
registry.insert_for_test(make_skill("zulu", "", "body"));
|
|
||||||
registry.insert_for_test(make_skill("alpha", "", "body"));
|
|
||||||
registry.insert_for_test(make_skill("mike", "", "body"));
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
registry.loaded_names(),
|
|
||||||
vec!["zulu".to_string(), "alpha".to_string(), "mike".to_string()]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sweep_removes_only_auto_unload_skills() {
|
|
||||||
let mut registry = SkillRegistry::default();
|
|
||||||
registry.insert_for_test(make_skill("ephemeral", "auto_unload: true", "body"));
|
|
||||||
registry.insert_for_test(make_skill("persistent", "", "body"));
|
|
||||||
|
|
||||||
registry.sweep_auto_unload();
|
|
||||||
|
|
||||||
assert!(!registry.is_loaded("ephemeral"));
|
|
||||||
assert!(registry.is_loaded("persistent"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn is_loaded_returns_false_for_unknown() {
|
|
||||||
let registry = SkillRegistry::default();
|
|
||||||
|
|
||||||
assert!(!registry.is_loaded("nothing"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
pub(crate) mod skill;
|
|
||||||
pub(crate) mod supervisor;
|
pub(crate) mod supervisor;
|
||||||
pub(crate) mod todo;
|
pub(crate) mod todo;
|
||||||
pub(crate) mod user_interaction;
|
pub(crate) mod user_interaction;
|
||||||
@@ -22,7 +21,6 @@ use indoc::formatdoc;
|
|||||||
use rust_embed::Embed;
|
use rust_embed::Embed;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
use skill::SKILL_FUNCTION_PREFIX;
|
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
use std::fs::File;
|
use std::fs::File;
|
||||||
@@ -355,11 +353,6 @@ impl Functions {
|
|||||||
self.declarations.extend(todo::todo_function_declarations());
|
self.declarations.extend(todo::todo_function_declarations());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn append_skill_functions(&mut self) {
|
|
||||||
self.declarations
|
|
||||||
.extend(skill::skill_function_declarations());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn append_supervisor_functions(&mut self) {
|
pub fn append_supervisor_functions(&mut self) {
|
||||||
self.declarations
|
self.declarations
|
||||||
.extend(supervisor::supervisor_function_declarations());
|
.extend(supervisor::supervisor_function_declarations());
|
||||||
@@ -1046,15 +1039,6 @@ impl ToolCall {
|
|||||||
json!({"tool_call_error": error_msg})
|
json!({"tool_call_error": error_msg})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
_ if cmd_name.starts_with(SKILL_FUNCTION_PREFIX) => {
|
|
||||||
skill::handle_skill_tool(ctx, &cmd_name, &json_data)
|
|
||||||
.await
|
|
||||||
.unwrap_or_else(|e| {
|
|
||||||
let error_msg = format!("Skill tool failed: {e}");
|
|
||||||
eprintln!("{}", warning_text(&format!("⚠️ {error_msg} ⚠️")));
|
|
||||||
json!({"tool_call_error": error_msg})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ if cmd_name.starts_with(SUPERVISOR_FUNCTION_PREFIX) => {
|
_ if cmd_name.starts_with(SUPERVISOR_FUNCTION_PREFIX) => {
|
||||||
supervisor::handle_supervisor_tool(ctx, &cmd_name, &json_data)
|
supervisor::handle_supervisor_tool(ctx, &cmd_name, &json_data)
|
||||||
.await
|
.await
|
||||||
|
|||||||
@@ -1,311 +0,0 @@
|
|||||||
use super::{FunctionDeclaration, JsonSchema};
|
|
||||||
use crate::config::{RequestContext, Skill, SkillPolicy, paths};
|
|
||||||
use crate::utils::create_abort_signal;
|
|
||||||
|
|
||||||
use anyhow::{Result, bail};
|
|
||||||
use indexmap::IndexMap;
|
|
||||||
use log::warn;
|
|
||||||
use serde_json::{Value, json};
|
|
||||||
|
|
||||||
pub const SKILL_FUNCTION_PREFIX: &str = "skill__";
|
|
||||||
|
|
||||||
pub fn skill_function_declarations() -> Vec<FunctionDeclaration> {
|
|
||||||
vec![
|
|
||||||
FunctionDeclaration {
|
|
||||||
name: format!("{SKILL_FUNCTION_PREFIX}list"),
|
|
||||||
description:
|
|
||||||
"List skills available in this context. Returns each skill's name, description, \
|
|
||||||
what tools and MCP servers it grants on load, and whether it is currently loaded. \
|
|
||||||
Call this to discover skills before using skill__load."
|
|
||||||
.to_string(),
|
|
||||||
parameters: JsonSchema {
|
|
||||||
type_value: Some("object".to_string()),
|
|
||||||
properties: Some(IndexMap::new()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
agent: false,
|
|
||||||
},
|
|
||||||
FunctionDeclaration {
|
|
||||||
name: format!("{SKILL_FUNCTION_PREFIX}load"),
|
|
||||||
description:
|
|
||||||
"Load a skill module into the current context. The skill's instructions and any \
|
|
||||||
tools or MCP servers it grants become active for subsequent turns. Call \
|
|
||||||
skill__unload when the skill's work is complete to keep the context lean."
|
|
||||||
.to_string(),
|
|
||||||
parameters: JsonSchema {
|
|
||||||
type_value: Some("object".to_string()),
|
|
||||||
properties: Some(IndexMap::from([(
|
|
||||||
"name".to_string(),
|
|
||||||
JsonSchema {
|
|
||||||
type_value: Some("string".to_string()),
|
|
||||||
description: Some("Name of the skill to load.".into()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)])),
|
|
||||||
required: Some(vec!["name".to_string()]),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
agent: false,
|
|
||||||
},
|
|
||||||
FunctionDeclaration {
|
|
||||||
name: format!("{SKILL_FUNCTION_PREFIX}unload"),
|
|
||||||
description:
|
|
||||||
"Unload a previously loaded skill, removing its instructions and granted tools \
|
|
||||||
from the context. Call this when the skill's work is complete."
|
|
||||||
.to_string(),
|
|
||||||
parameters: JsonSchema {
|
|
||||||
type_value: Some("object".to_string()),
|
|
||||||
properties: Some(IndexMap::from([(
|
|
||||||
"name".to_string(),
|
|
||||||
JsonSchema {
|
|
||||||
type_value: Some("string".to_string()),
|
|
||||||
description: Some("Name of the skill to unload.".into()),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
)])),
|
|
||||||
required: Some(vec!["name".to_string()]),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
agent: false,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn handle_skill_tool(
|
|
||||||
ctx: &mut RequestContext,
|
|
||||||
cmd_name: &str,
|
|
||||||
args: &Value,
|
|
||||||
) -> Result<Value> {
|
|
||||||
let action = cmd_name
|
|
||||||
.strip_prefix(SKILL_FUNCTION_PREFIX)
|
|
||||||
.unwrap_or(cmd_name);
|
|
||||||
|
|
||||||
let policy = SkillPolicy::effective(
|
|
||||||
&ctx.app.config,
|
|
||||||
ctx.role.as_ref(),
|
|
||||||
ctx.agent.as_ref(),
|
|
||||||
ctx.session.as_ref(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if !policy.skills_enabled {
|
|
||||||
return Ok(json!({
|
|
||||||
"error": "Skills are disabled in this context"
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
match action {
|
|
||||||
"list" => handle_list(ctx, &policy),
|
|
||||||
"load" => handle_load(ctx, args, &policy).await,
|
|
||||||
"unload" => handle_unload(ctx, args).await,
|
|
||||||
_ => bail!("Unknown skill action: {action}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_list(ctx: &RequestContext, policy: &SkillPolicy) -> Result<Value> {
|
|
||||||
let function_calling_on = ctx.app.config.function_calling_support;
|
|
||||||
let mcp_on = ctx.app.config.mcp_server_support;
|
|
||||||
|
|
||||||
let mut entries = Vec::new();
|
|
||||||
for name in paths::list_skills() {
|
|
||||||
if !policy.allows(&name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let skill = match Skill::load(&name) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
warn!("Failed to load skill '{name}' for listing: {e}");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if !skill.is_compatible(function_calling_on, mcp_on) {
|
|
||||||
warn!(
|
|
||||||
"Skill '{name}' filtered from list: declares tools or MCP servers but those features are disabled"
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
entries.push(json!({
|
|
||||||
"name": skill.name(),
|
|
||||||
"description": skill.description(),
|
|
||||||
"grants_tools": csv_to_vec(skill.enabled_tools()),
|
|
||||||
"grants_mcp_servers": csv_to_vec(skill.enabled_mcp_servers()),
|
|
||||||
"loaded": ctx.skill_registry.is_loaded(skill.name()),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(json!({"skills": entries}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_load(
|
|
||||||
ctx: &mut RequestContext,
|
|
||||||
args: &Value,
|
|
||||||
policy: &SkillPolicy,
|
|
||||||
) -> Result<Value> {
|
|
||||||
let name = match args.get("name").and_then(Value::as_str) {
|
|
||||||
Some(n) if !n.is_empty() => n,
|
|
||||||
_ => return Ok(json!({"error": "name is required"})),
|
|
||||||
};
|
|
||||||
|
|
||||||
if !policy.allows(name) {
|
|
||||||
return Ok(json!({
|
|
||||||
"error": format!("Skill '{name}' is not enabled in this context")
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
let skill = match Skill::load(name) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => {
|
|
||||||
return Ok(json!({
|
|
||||||
"error": format!("Failed to load skill '{name}': {e}")
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let function_calling_on = ctx.app.config.function_calling_support;
|
|
||||||
let mcp_on = ctx.app.config.mcp_server_support;
|
|
||||||
|
|
||||||
let tools_declared = skill
|
|
||||||
.enabled_tools()
|
|
||||||
.map(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or(false);
|
|
||||||
let mcps_declared = skill
|
|
||||||
.enabled_mcp_servers()
|
|
||||||
.map(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or(false);
|
|
||||||
|
|
||||||
if tools_declared && !function_calling_on {
|
|
||||||
return Ok(json!({
|
|
||||||
"error": format!(
|
|
||||||
"Skill '{name}' requires function calling, which is disabled in this context"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
if mcps_declared && !mcp_on {
|
|
||||||
return Ok(json!({
|
|
||||||
"error": format!(
|
|
||||||
"Skill '{name}' requires MCP servers, which are disabled in this context"
|
|
||||||
)
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = ctx.skill_registry.insert(skill) {
|
|
||||||
return Ok(json!({"error": e.to_string()}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = ctx.refresh_tool_scope(create_abort_signal()).await {
|
|
||||||
let _ = ctx.skill_registry.unload(name);
|
|
||||||
return Ok(json!({
|
|
||||||
"error": format!("Loaded skill '{name}' but failed to refresh tool scope: {e}")
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(json!({
|
|
||||||
"status": "ok",
|
|
||||||
"loaded": name,
|
|
||||||
"message": format!("Skill '{name}' loaded")
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_unload(ctx: &mut RequestContext, args: &Value) -> Result<Value> {
|
|
||||||
let name = match args.get("name").and_then(Value::as_str) {
|
|
||||||
Some(n) if !n.is_empty() => n,
|
|
||||||
_ => return Ok(json!({"error": "name is required"})),
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Err(e) = ctx.skill_registry.unload(name) {
|
|
||||||
return Ok(json!({"error": e.to_string()}));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Err(e) = ctx.refresh_tool_scope(create_abort_signal()).await {
|
|
||||||
warn!("Unloaded skill '{name}' but failed to refresh tool scope: {e}");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(json!({
|
|
||||||
"status": "ok",
|
|
||||||
"unloaded": name
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn csv_to_vec(csv: Option<&str>) -> Vec<String> {
|
|
||||||
csv.map(|raw| {
|
|
||||||
raw.split(',')
|
|
||||||
.map(|t| t.trim().to_string())
|
|
||||||
.filter(|t| !t.is_empty())
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn declarations_have_three_entries() {
|
|
||||||
let decls = skill_function_declarations();
|
|
||||||
assert_eq!(decls.len(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn declaration_names_use_skill_prefix() {
|
|
||||||
let decls = skill_function_declarations();
|
|
||||||
|
|
||||||
let names: Vec<&str> = decls.iter().map(|d| d.name.as_str()).collect();
|
|
||||||
|
|
||||||
assert!(names.contains(&"skill__list"));
|
|
||||||
assert!(names.contains(&"skill__load"));
|
|
||||||
assert!(names.contains(&"skill__unload"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn load_and_unload_require_name_parameter() {
|
|
||||||
let decls = skill_function_declarations();
|
|
||||||
for action in ["load", "unload"] {
|
|
||||||
let decl = decls
|
|
||||||
.iter()
|
|
||||||
.find(|d| d.name == format!("skill__{action}"))
|
|
||||||
.expect("missing declaration");
|
|
||||||
|
|
||||||
let required = decl
|
|
||||||
.parameters
|
|
||||||
.required
|
|
||||||
.as_ref()
|
|
||||||
.expect("required field missing");
|
|
||||||
|
|
||||||
assert!(required.contains(&"name".to_string()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn list_has_no_required_parameters() {
|
|
||||||
let decls = skill_function_declarations();
|
|
||||||
let list_decl = decls
|
|
||||||
.iter()
|
|
||||||
.find(|d| d.name == "skill__list")
|
|
||||||
.expect("skill__list missing");
|
|
||||||
|
|
||||||
let required = list_decl
|
|
||||||
.parameters
|
|
||||||
.required
|
|
||||||
.as_ref()
|
|
||||||
.map(|v| v.is_empty())
|
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
assert!(required, "skill__list should have no required parameters");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn csv_to_vec_empty_input() {
|
|
||||||
assert!(csv_to_vec(None).is_empty());
|
|
||||||
assert!(csv_to_vec(Some("")).is_empty());
|
|
||||||
assert!(csv_to_vec(Some(" ")).is_empty());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn csv_to_vec_parses_and_trims() {
|
|
||||||
let v = csv_to_vec(Some("a, b ,c,, d"));
|
|
||||||
|
|
||||||
assert_eq!(v, vec!["a", "b", "c", "d"]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-23
@@ -74,7 +74,6 @@ async fn main() -> Result<()> {
|
|||||||
|| cli.list_agents
|
|| cli.list_agents
|
||||||
|| cli.list_rags
|
|| cli.list_rags
|
||||||
|| cli.list_macros
|
|| cli.list_macros
|
||||||
|| cli.list_skills
|
|
||||||
|| cli.list_sessions;
|
|| cli.list_sessions;
|
||||||
let vault_flags = cli.add_secret.is_some()
|
let vault_flags = cli.add_secret.is_some()
|
||||||
|| cli.get_secret.is_some()
|
|| cli.get_secret.is_some()
|
||||||
@@ -192,24 +191,6 @@ async fn run(
|
|||||||
println!("{macros}");
|
println!("{macros}");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if cli.list_skills {
|
|
||||||
let skills = paths::list_skills().join("\n");
|
|
||||||
println!("{skills}");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if cli.skill.len() == 1 && !paths::has_skill(&cli.skill[0]) {
|
|
||||||
let name = &cli.skill[0];
|
|
||||||
let app = Arc::clone(&ctx.app.config);
|
|
||||||
ctx.upsert_skill(app.as_ref(), name)?;
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if cli.skill.len() > 1 {
|
|
||||||
for name in &cli.skill {
|
|
||||||
if !paths::has_skill(name) {
|
|
||||||
bail!("Skill '{name}' is not installed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if cli.dry_run {
|
if cli.dry_run {
|
||||||
update_app_config(&mut ctx, |app| app.dry_run = true);
|
update_app_config(&mut ctx, |app| app.dry_run = true);
|
||||||
@@ -323,10 +304,6 @@ async fn run(
|
|||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for name in &cli.skill {
|
|
||||||
ctx.load_skill_repl(name, abort_signal.clone()).await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
match is_repl {
|
match is_repl {
|
||||||
false => {
|
false => {
|
||||||
let mut input = create_input(&ctx, text, &cli.file, abort_signal.clone()).await?;
|
let mut input = create_input(&ctx, text, &cli.file, abort_signal.clone()).await?;
|
||||||
|
|||||||
+5
-64
@@ -46,7 +46,7 @@ pub const DEFAULT_CONTINUATION_PROMPT: &str = indoc! {"
|
|||||||
4. Continue with the next pending item now. Call tools immediately."
|
4. Continue with the next pending item now. Call tools immediately."
|
||||||
};
|
};
|
||||||
|
|
||||||
static REPL_COMMANDS: LazyLock<[ReplCommand; 44]> = LazyLock::new(|| {
|
static REPL_COMMANDS: LazyLock<[ReplCommand; 42]> = LazyLock::new(|| {
|
||||||
[
|
[
|
||||||
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
ReplCommand::new(".help", "Show this help guide", AssertState::pass()),
|
||||||
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
ReplCommand::new(".info", "Show system info", AssertState::pass()),
|
||||||
@@ -191,16 +191,6 @@ static REPL_COMMANDS: LazyLock<[ReplCommand; 44]> = LazyLock::new(|| {
|
|||||||
AssertState::TrueFalse(StateFlags::RAG, StateFlags::AGENT),
|
AssertState::TrueFalse(StateFlags::RAG, StateFlags::AGENT),
|
||||||
),
|
),
|
||||||
ReplCommand::new(".macro", "Execute a macro", AssertState::pass()),
|
ReplCommand::new(".macro", "Execute a macro", AssertState::pass()),
|
||||||
ReplCommand::new(
|
|
||||||
".skill",
|
|
||||||
"List, load, unload, or create skills",
|
|
||||||
AssertState::pass(),
|
|
||||||
),
|
|
||||||
ReplCommand::new(
|
|
||||||
".edit skill",
|
|
||||||
"Modify an existing skill by name",
|
|
||||||
AssertState::pass(),
|
|
||||||
),
|
|
||||||
ReplCommand::new(
|
ReplCommand::new(
|
||||||
".file",
|
".file",
|
||||||
"Include files, directories, URLs or commands",
|
"Include files, directories, URLs or commands",
|
||||||
@@ -523,41 +513,6 @@ pub async fn run_repl_command(
|
|||||||
.role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
|
.role <name> [text]... # Temporarily switch to the role, send the text, and switch back"#
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
".skill" => {
|
|
||||||
let trimmed = args.map(str::trim).unwrap_or("");
|
|
||||||
let mut parts = trimmed.splitn(2, char::is_whitespace);
|
|
||||||
let first = parts.next().unwrap_or("");
|
|
||||||
let rest = parts.next().map(str::trim).unwrap_or("");
|
|
||||||
match first {
|
|
||||||
"" => println!(
|
|
||||||
r#"Usage:
|
|
||||||
.skill loaded # List currently-loaded skills
|
|
||||||
.skill load <name> # Load a skill into the current context
|
|
||||||
.skill unload <name> # Unload a loaded skill
|
|
||||||
.skill <name> # Open the skill in $EDITOR; create with a scaffold if missing
|
|
||||||
# (Use `.edit skill <name>` to edit an existing skill without the create-if-missing behavior.)"#
|
|
||||||
),
|
|
||||||
"loaded" => ctx.list_loaded_skills(),
|
|
||||||
"load" => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
println!("Usage: .skill load <name>");
|
|
||||||
} else {
|
|
||||||
ctx.load_skill_repl(rest, abort_signal.clone()).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"unload" => {
|
|
||||||
if rest.is_empty() {
|
|
||||||
println!("Usage: .skill unload <name>");
|
|
||||||
} else {
|
|
||||||
ctx.unload_skill_repl(rest, abort_signal.clone()).await?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
name => {
|
|
||||||
let app = Arc::clone(&ctx.app.config);
|
|
||||||
ctx.upsert_skill(app.as_ref(), name)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
".session" => {
|
".session" => {
|
||||||
if let Some(name) = graph::active_agent_graph_name(ctx) {
|
if let Some(name) = graph::active_agent_graph_name(ctx) {
|
||||||
bail!(
|
bail!(
|
||||||
@@ -704,23 +659,9 @@ pub async fn run_repl_command(
|
|||||||
Some("mcp-config") => {
|
Some("mcp-config") => {
|
||||||
ctx.edit_mcp_config()?;
|
ctx.edit_mcp_config()?;
|
||||||
}
|
}
|
||||||
Some(s) if s == "skill" || s.starts_with("skill ") => {
|
|
||||||
let name = s.strip_prefix("skill").unwrap_or("").trim();
|
|
||||||
if name.is_empty() {
|
|
||||||
println!("Usage: .edit skill <name>");
|
|
||||||
} else if !paths::has_skill(name) {
|
|
||||||
bail!(
|
|
||||||
"Skill '{name}' is not installed (expected at {})",
|
|
||||||
paths::skill_file(name).display()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let app = Arc::clone(&ctx.app.config);
|
|
||||||
ctx.upsert_skill(app.as_ref(), name)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
_ => {
|
||||||
println!(
|
println!(
|
||||||
r#"Usage: .edit <config|mcp-config|role|session|rag-docs|agent-config|skill <name>>"#
|
r#"Usage: .edit <config|mcp-config|role|session|rag-docs|agent-config>"#
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -838,7 +779,7 @@ pub async fn run_repl_command(
|
|||||||
ctx.delete(args)?;
|
ctx.delete(args)?;
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
println!("Usage: .delete <role|session|rag|macro|skill|agent-data>")
|
println!("Usage: .delete <role|session|rag|macro|agent-data>")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
".copy" => {
|
".copy" => {
|
||||||
@@ -1324,8 +1265,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn repl_commands_has_44_entries() {
|
fn repl_commands_has_42_entries() {
|
||||||
assert_eq!(REPL_COMMANDS.len(), 44);
|
assert_eq!(REPL_COMMANDS.len(), 42);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
Reference in New Issue
Block a user