Skip to content

Commit 2a7b4f6

Browse files
authored
Merge pull request #5028 from w31r4/feat/neo-skill-self-iteration
feat: 接入 Shipyard Neo 自迭代 Skill 闭环与管理能力
2 parents 4abea2b + 6e1be64 commit 2a7b4f6

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+6219
-173
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,7 @@ IFLOW.md
5454
# genie_tts data
5555
CharacterModels/
5656
GenieData/
57+
.agent/
58+
.codex/
59+
.opencode/
60+
.kilocode/

CONTRIBUTING.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,32 @@ ruff check .
4646

4747
如果您使用 VSCode,可以安装 `Ruff` 插件。
4848

49+
##### PR 功能完整性验证(推荐)
50+
51+
如果您希望在本地做一套接近 CI 的完整验证,可使用:
52+
53+
```bash
54+
make pr-test-neo
55+
```
56+
57+
该命令会执行:
58+
- `uv sync --group dev`
59+
- `ruff format --check .``ruff check .`
60+
- Neo 相关关键测试
61+
- `main.py` 启动 smoke test(检测 `http://localhost:6185`
62+
63+
需要全量验证时可使用:
64+
65+
```bash
66+
make pr-test-full
67+
```
68+
69+
如果只想快速重复执行(跳过依赖同步和 dashboard 构建):
70+
71+
```bash
72+
make pr-test-full-fast
73+
```
74+
4975

5076
## Contributing Guide
5177

@@ -88,3 +114,29 @@ We use Ruff as our code formatter and static analysis tool. Before submitting yo
88114
ruff format .
89115
ruff check .
90116
```
117+
118+
##### PR completeness checks (recommended)
119+
120+
To run a local validation flow close to CI, use:
121+
122+
```bash
123+
make pr-test-neo
124+
```
125+
126+
This command runs:
127+
- `uv sync --group dev`
128+
- `ruff format --check .` and `ruff check .`
129+
- Neo-related critical tests
130+
- a startup smoke test against `http://localhost:6185`
131+
132+
For full validation, use:
133+
134+
```bash
135+
make pr-test-full
136+
```
137+
138+
For faster repeated runs (skip dependency sync and dashboard build), use:
139+
140+
```bash
141+
make pr-test-full-fast
142+
```

Makefile

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
.PHONY: worktree worktree-add worktree-rm
1+
.PHONY: worktree worktree-add worktree-rm pr-test-neo pr-test-full pr-test-full-fast
22

33
WORKTREE_DIR ?= ../astrbot_worktree
44
BRANCH ?= $(word 2,$(MAKECMDGOALS))
@@ -27,6 +27,15 @@ endif
2727
echo "Worktree $(WORKTREE_DIR)/$(BRANCH) not found."; \
2828
fi
2929

30+
pr-test-neo:
31+
./scripts/pr_test_env.sh --profile neo
32+
33+
pr-test-full:
34+
./scripts/pr_test_env.sh --profile full
35+
36+
pr-test-full-fast:
37+
./scripts/pr_test_env.sh --profile full --skip-sync --no-dashboard
38+
3039
# Swallow extra args (branch/base) so make doesn't treat them as targets
3140
%:
3241
@true

astrbot/core/astr_main_agent.py

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,32 @@
2020
from astrbot.core.astr_agent_run_util import AgentRunner
2121
from astrbot.core.astr_agent_tool_exec import FunctionToolExecutor
2222
from astrbot.core.astr_main_agent_resources import (
23+
ANNOTATE_EXECUTION_TOOL,
24+
BROWSER_BATCH_EXEC_TOOL,
25+
BROWSER_EXEC_TOOL,
2326
CHATUI_SPECIAL_DEFAULT_PERSONA_PROMPT,
27+
CREATE_SKILL_CANDIDATE_TOOL,
28+
CREATE_SKILL_PAYLOAD_TOOL,
29+
EVALUATE_SKILL_CANDIDATE_TOOL,
2430
EXECUTE_SHELL_TOOL,
2531
FILE_DOWNLOAD_TOOL,
2632
FILE_UPLOAD_TOOL,
33+
GET_EXECUTION_HISTORY_TOOL,
34+
GET_SKILL_PAYLOAD_TOOL,
2735
KNOWLEDGE_BASE_QUERY_TOOL,
36+
LIST_SKILL_CANDIDATES_TOOL,
37+
LIST_SKILL_RELEASES_TOOL,
2838
LIVE_MODE_SYSTEM_PROMPT,
2939
LLM_SAFETY_MODE_SYSTEM_PROMPT,
3040
LOCAL_EXECUTE_SHELL_TOOL,
3141
LOCAL_PYTHON_TOOL,
42+
PROMOTE_SKILL_CANDIDATE_TOOL,
3243
PYTHON_TOOL,
44+
ROLLBACK_SKILL_RELEASE_TOOL,
45+
RUN_BROWSER_SKILL_TOOL,
3346
SANDBOX_MODE_PROMPT,
3447
SEND_MESSAGE_TO_USER_TOOL,
48+
SYNC_SKILL_RELEASE_TOOL,
3549
TOOL_CALL_PROMPT,
3650
TOOL_CALL_PROMPT_SKILLS_LIKE_MODE,
3751
retrieve_knowledge_base,
@@ -832,19 +846,73 @@ def _apply_sandbox_tools(
832846
) -> None:
833847
if req.func_tool is None:
834848
req.func_tool = ToolSet()
835-
if config.sandbox_cfg.get("booter") == "shipyard":
849+
booter = config.sandbox_cfg.get("booter", "shipyard_neo")
850+
if booter == "shipyard":
836851
ep = config.sandbox_cfg.get("shipyard_endpoint", "")
837852
at = config.sandbox_cfg.get("shipyard_access_token", "")
838853
if not ep or not at:
839854
logger.error("Shipyard sandbox configuration is incomplete.")
840855
return
841856
os.environ["SHIPYARD_ENDPOINT"] = ep
842857
os.environ["SHIPYARD_ACCESS_TOKEN"] = at
858+
843859
req.func_tool.add_tool(EXECUTE_SHELL_TOOL)
844860
req.func_tool.add_tool(PYTHON_TOOL)
845861
req.func_tool.add_tool(FILE_UPLOAD_TOOL)
846862
req.func_tool.add_tool(FILE_DOWNLOAD_TOOL)
847-
req.system_prompt = f"{req.system_prompt}\n{SANDBOX_MODE_PROMPT}\n"
863+
if booter == "shipyard_neo":
864+
# Neo-specific path rule: filesystem tools operate relative to sandbox
865+
# workspace root. Do not prepend "/workspace".
866+
req.system_prompt += (
867+
"\n[Shipyard Neo File Path Rule]\n"
868+
"When using sandbox filesystem tools (upload/download/read/write/list/delete), "
869+
"always pass paths relative to the sandbox workspace root. "
870+
"Example: use `baidu_homepage.png` instead of `/workspace/baidu_homepage.png`.\n"
871+
)
872+
873+
req.system_prompt += (
874+
"\n[Neo Skill Lifecycle Workflow]\n"
875+
"When user asks to create/update a reusable skill in Neo mode, use lifecycle tools instead of directly writing local skill folders.\n"
876+
"Preferred sequence:\n"
877+
"1) Use `astrbot_create_skill_payload` to store canonical payload content and get `payload_ref`.\n"
878+
"2) Use `astrbot_create_skill_candidate` with `skill_key` + `source_execution_ids` (and optional `payload_ref`) to create a candidate.\n"
879+
"3) Use `astrbot_promote_skill_candidate` to release: `stage=canary` for trial; `stage=stable` for production.\n"
880+
"For stable release, set `sync_to_local=true` to sync `payload.skill_markdown` into local `SKILL.md`.\n"
881+
"Do not treat ad-hoc generated files as reusable Neo skills unless they are captured via payload/candidate/release.\n"
882+
"To update an existing skill, create a new payload/candidate and promote a new release version; avoid patching old local folders directly.\n"
883+
)
884+
885+
# Determine sandbox capabilities from an already-booted session.
886+
# If no session exists yet (first request), capabilities is None
887+
# and we register all tools conservatively.
888+
from astrbot.core.computer.computer_client import session_booter
889+
890+
sandbox_capabilities: list[str] | None = None
891+
existing_booter = session_booter.get(session_id)
892+
if existing_booter is not None:
893+
sandbox_capabilities = getattr(existing_booter, "capabilities", None)
894+
895+
# Browser tools: only register if profile supports browser
896+
# (or if capabilities are unknown because sandbox hasn't booted yet)
897+
if sandbox_capabilities is None or "browser" in sandbox_capabilities:
898+
req.func_tool.add_tool(BROWSER_EXEC_TOOL)
899+
req.func_tool.add_tool(BROWSER_BATCH_EXEC_TOOL)
900+
req.func_tool.add_tool(RUN_BROWSER_SKILL_TOOL)
901+
902+
# Neo-specific tools (always available for shipyard_neo)
903+
req.func_tool.add_tool(GET_EXECUTION_HISTORY_TOOL)
904+
req.func_tool.add_tool(ANNOTATE_EXECUTION_TOOL)
905+
req.func_tool.add_tool(CREATE_SKILL_PAYLOAD_TOOL)
906+
req.func_tool.add_tool(GET_SKILL_PAYLOAD_TOOL)
907+
req.func_tool.add_tool(CREATE_SKILL_CANDIDATE_TOOL)
908+
req.func_tool.add_tool(LIST_SKILL_CANDIDATES_TOOL)
909+
req.func_tool.add_tool(EVALUATE_SKILL_CANDIDATE_TOOL)
910+
req.func_tool.add_tool(PROMOTE_SKILL_CANDIDATE_TOOL)
911+
req.func_tool.add_tool(LIST_SKILL_RELEASES_TOOL)
912+
req.func_tool.add_tool(ROLLBACK_SKILL_RELEASE_TOOL)
913+
req.func_tool.add_tool(SYNC_SKILL_RELEASE_TOOL)
914+
915+
req.system_prompt = f"{req.system_prompt or ''}\n{SANDBOX_MODE_PROMPT}\n"
848916

849917

850918
def _proactive_cron_job_tools(req: ProviderRequest) -> None:

astrbot/core/astr_main_agent_resources.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,25 @@
1313
from astrbot.core.astr_agent_context import AstrAgentContext
1414
from astrbot.core.computer.computer_client import get_booter
1515
from astrbot.core.computer.tools import (
16+
AnnotateExecutionTool,
17+
BrowserBatchExecTool,
18+
BrowserExecTool,
19+
CreateSkillCandidateTool,
20+
CreateSkillPayloadTool,
21+
EvaluateSkillCandidateTool,
1622
ExecuteShellTool,
1723
FileDownloadTool,
1824
FileUploadTool,
25+
GetExecutionHistoryTool,
26+
GetSkillPayloadTool,
27+
ListSkillCandidatesTool,
28+
ListSkillReleasesTool,
1929
LocalPythonTool,
30+
PromoteSkillCandidateTool,
2031
PythonTool,
32+
RollbackSkillReleaseTool,
33+
RunBrowserSkillTool,
34+
SyncSkillReleaseTool,
2135
)
2236
from astrbot.core.message.message_event_result import MessageChain
2337
from astrbot.core.platform.message_session import MessageSession
@@ -449,6 +463,20 @@ async def retrieve_knowledge_base(
449463
LOCAL_PYTHON_TOOL = LocalPythonTool()
450464
FILE_UPLOAD_TOOL = FileUploadTool()
451465
FILE_DOWNLOAD_TOOL = FileDownloadTool()
466+
BROWSER_EXEC_TOOL = BrowserExecTool()
467+
BROWSER_BATCH_EXEC_TOOL = BrowserBatchExecTool()
468+
RUN_BROWSER_SKILL_TOOL = RunBrowserSkillTool()
469+
GET_EXECUTION_HISTORY_TOOL = GetExecutionHistoryTool()
470+
ANNOTATE_EXECUTION_TOOL = AnnotateExecutionTool()
471+
CREATE_SKILL_PAYLOAD_TOOL = CreateSkillPayloadTool()
472+
GET_SKILL_PAYLOAD_TOOL = GetSkillPayloadTool()
473+
CREATE_SKILL_CANDIDATE_TOOL = CreateSkillCandidateTool()
474+
LIST_SKILL_CANDIDATES_TOOL = ListSkillCandidatesTool()
475+
EVALUATE_SKILL_CANDIDATE_TOOL = EvaluateSkillCandidateTool()
476+
PROMOTE_SKILL_CANDIDATE_TOOL = PromoteSkillCandidateTool()
477+
LIST_SKILL_RELEASES_TOOL = ListSkillReleasesTool()
478+
ROLLBACK_SKILL_RELEASE_TOOL = RollbackSkillReleaseTool()
479+
SYNC_SKILL_RELEASE_TOOL = SyncSkillReleaseTool()
452480

453481
# we prevent astrbot from connecting to known malicious hosts
454482
# these hosts are base64 encoded

astrbot/core/computer/booters/base.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
from ..olayer import FileSystemComponent, PythonComponent, ShellComponent
1+
from ..olayer import (
2+
BrowserComponent,
3+
FileSystemComponent,
4+
PythonComponent,
5+
ShellComponent,
6+
)
27

38

49
class ComputerBooter:
@@ -11,6 +16,19 @@ def python(self) -> PythonComponent: ...
1116
@property
1217
def shell(self) -> ShellComponent: ...
1318

19+
@property
20+
def capabilities(self) -> tuple[str, ...] | None:
21+
"""Sandbox capabilities (e.g. ('python', 'shell', 'filesystem', 'browser')).
22+
23+
Returns None if the booter doesn't support capability introspection
24+
(backward-compatible default). Subclasses override after boot.
25+
"""
26+
return None
27+
28+
@property
29+
def browser(self) -> BrowserComponent | None:
30+
return None
31+
1432
async def boot(self, session_id: str) -> None: ...
1533

1634
async def shutdown(self) -> None: ...

0 commit comments

Comments
 (0)