chore!: rename module from github.com/sipeed/picoclaw to github.com/ZanzyTHEbar/dragonscale

Update Go module path and all import references across the codebase.
Rename cmd/picoclaw/ → cmd/dragonscale/, pkg/pcerrors/ → pkg/dserrors/.
Update all environment variable prefixes PICOCLAW_* → DRAGONSCALE_*.
Remove stale translated READMEs (ja, pt-br, zh) and legacy community
roadmap doc. Update CI workflows, Dockerfile, goreleaser, and Makefile
to reference the new binary name and module path.

BREAKING CHANGE: module path changed; all consumers must update imports
This commit is contained in:
ZanzyTHEbar 2026-02-21 18:56:35 +00:00
parent 8223bc37a9
commit db4d98a1d4
204 changed files with 2896 additions and 3493 deletions

View file

@ -1,7 +1,7 @@
.git .git
.gitignore .gitignore
build/ build/
.picoclaw/ .dragonscale/
config/ config/
.env .env
.env.example .env.example

View file

@ -10,7 +10,7 @@ assignees: ''
## Quick Summary ## Quick Summary
## Environment & Tools ## Environment & Tools
- **PicoClaw Version:** (e.g., v0.1.2 or commit hash) - **DragonScale Version:** (e.g., v0.1.2 or commit hash)
- **Go Version:** (e.g., go 1.22) - **Go Version:** (e.g., go 1.22)
- **AI Model & Provider:** (e.g., GPT-4o via OpenAI / DeepSeek via SiliconFlow) - **AI Model & Provider:** (e.g., GPT-4o via OpenAI / DeepSeek via SiliconFlow)
- **Operating System:** (e.g., Ubuntu 22.04 / macOS / Android Termux) - **Operating System:** (e.g., Ubuntu 22.04 / macOS / Android Termux)

View file

@ -10,7 +10,7 @@ on:
env: env:
GHCR_REGISTRY: ghcr.io GHCR_REGISTRY: ghcr.io
GHCR_IMAGE_NAME: ${{ github.repository_owner }}/picoclaw GHCR_IMAGE_NAME: ${{ github.repository_owner }}/dragonscale
DOCKERHUB_REGISTRY: docker.io DOCKERHUB_REGISTRY: docker.io
DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }}

View file

@ -41,7 +41,8 @@ jobs:
- name: Run sqlc vet rules - name: Run sqlc vet rules
run: make sqlc-vet run: make sqlc-vet
vet: # Verify FlatBuffers generation is idempotent for current tree state.
flatc-check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: fmt-check needs: fmt-check
steps: steps:
@ -53,6 +54,31 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Install flatc
run: |
sudo apt-get update
sudo apt-get install -y flatbuffers-compiler
- name: Check FlatBuffers generated code
run: make flatc-check
vet:
runs-on: ubuntu-latest
needs: [fmt-check, flatc-check]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Install flatc
run: |
sudo apt-get update
sudo apt-get install -y flatbuffers-compiler
- name: Run go generate - name: Run go generate
run: go generate ./... run: go generate ./...
@ -61,7 +87,7 @@ jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: fmt-check needs: [fmt-check, flatc-check]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -71,6 +97,11 @@ jobs:
with: with:
go-version-file: go.mod go-version-file: go.mod
- name: Install flatc
run: |
sudo apt-get update
sudo apt-get install -y flatbuffers-compiler
- name: Run go generate - name: Run go generate
run: go generate ./... run: go generate ./...

10
.gitignore vendored
View file

@ -7,14 +7,14 @@ bin/
*.dylib *.dylib
*.test *.test
*.out *.out
/picoclaw /dragonscale
/picoclaw-test /dragonscale-test
cmd/picoclaw/workspace cmd/dragonscale/workspace
# Picoclaw specific # Picoclaw specific
# PicoClaw # DragonScale
.picoclaw/ .dragonscale/
config.json config.json
sessions/ sessions/

View file

@ -5,10 +5,10 @@ version: 2
before: before:
hooks: hooks:
- go mod tidy - go mod tidy
- go generate ./cmd/picoclaw - go generate ./cmd/dragonscale
builds: builds:
- id: picoclaw - id: dragonscale
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
tags: tags:
@ -31,18 +31,18 @@ builds:
- s390x - s390x
- mips64 - mips64
- arm - arm
main: ./cmd/picoclaw main: ./cmd/dragonscale
ignore: ignore:
- goos: windows - goos: windows
goarch: arm goarch: arm
dockers_v2: dockers_v2:
- id: picoclaw - id: dragonscale
dockerfile: Dockerfile.goreleaser dockerfile: Dockerfile.goreleaser
ids: ids:
- picoclaw - dragonscale
images: images:
- "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw" - "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/dragonscale"
- "docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}" - "docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}"
tags: tags:
- "{{ .Tag }}" - "{{ .Tag }}"

View file

@ -1,5 +1,5 @@
# ============================================================ # ============================================================
# Stage 1: Build the picoclaw binary # Stage 1: Build the dragonscale binary
# ============================================================ # ============================================================
FROM golang:1.26.0-alpine AS builder FROM golang:1.26.0-alpine AS builder
@ -27,17 +27,17 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -q --spider http://localhost:18790/health || exit 1 CMD wget -q --spider http://localhost:18790/health || exit 1
# Copy binary # Copy binary
COPY --from=builder /src/bin/picoclaw /usr/local/bin/picoclaw COPY --from=builder /src/bin/dragonscale /usr/local/bin/dragonscale
# Create non-root user and group # Create non-root user and group
RUN addgroup -g 1000 picoclaw && \ RUN addgroup -g 1000 dragonscale && \
adduser -D -u 1000 -G picoclaw picoclaw adduser -D -u 1000 -G dragonscale dragonscale
# Switch to non-root user # Switch to non-root user
USER picoclaw USER dragonscale
# Run onboard to create initial directories and config # Run onboard to create initial directories and config
RUN /usr/local/bin/picoclaw onboard RUN /usr/local/bin/dragonscale onboard
ENTRYPOINT ["picoclaw"] ENTRYPOINT ["dragonscale"]
CMD ["gateway"] CMD ["gateway"]

View file

@ -4,7 +4,7 @@ ARG TARGETPLATFORM
RUN apk add --no-cache ca-certificates tzdata RUN apk add --no-cache ca-certificates tzdata
COPY $TARGETPLATFORM/picoclaw /usr/local/bin/picoclaw COPY $TARGETPLATFORM/dragonscale /usr/local/bin/dragonscale
ENTRYPOINT ["picoclaw"] ENTRYPOINT ["dragonscale"]
CMD ["gateway"] CMD ["gateway"]

118
Makefile
View file

@ -1,8 +1,9 @@
.PHONY: all build install uninstall clean help test lint hooks \ .PHONY: all build install uninstall clean help test lint hooks \
fantasy-check fantasy-diff fantasy-sync fantasy-patch fantasy-check fantasy-diff fantasy-sync fantasy-patch \
flatc-check devcontainer-up devcontainer-build devcontainer-generate devcontainer-verify
# Build variables # Build variables
BINARY_NAME=picoclaw BINARY_NAME=dragonscale
BUILD_DIR=bin BUILD_DIR=bin
CMD_DIR=cmd/$(BINARY_NAME) CMD_DIR=cmd/$(BINARY_NAME)
MAIN_GO=$(CMD_DIR)/main.go MAIN_GO=$(CMD_DIR)/main.go
@ -24,8 +25,8 @@ INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin
INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1 INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1
# Workspace and Skills # Workspace and Skills
PICOCLAW_HOME?=$(HOME)/.picoclaw DRAGONSCALE_HOME?=$(HOME)/.dragonscale
WORKSPACE_DIR?=$(PICOCLAW_HOME)/workspace WORKSPACE_DIR?=$(DRAGONSCALE_HOME)/workspace
WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills
BUILTIN_SKILLS_DIR=$(CURDIR)/skills BUILTIN_SKILLS_DIR=$(CURDIR)/skills
@ -73,7 +74,7 @@ generate:
@$(GO) generate ./... @$(GO) generate ./...
@echo "Run generate complete" @echo "Run generate complete"
## build: Build the picoclaw binary for current platform ## build: Build the dragonscale binary for current platform
build: generate build: generate
@echo "Building $(BINARY_NAME) for $(PLATFORM)/$(ARCH)..." @echo "Building $(BINARY_NAME) for $(PLATFORM)/$(ARCH)..."
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@ -81,7 +82,7 @@ build: generate
@echo "Build complete: $(BINARY_PATH)" @echo "Build complete: $(BINARY_PATH)"
@ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME) @ln -sf $(BINARY_NAME)-$(PLATFORM)-$(ARCH) $(BUILD_DIR)/$(BINARY_NAME)
## build-all: Build picoclaw for all platforms ## build-all: Build dragonscale for all platforms
build-all: generate build-all: generate
@echo "Building for multiple platforms..." @echo "Building for multiple platforms..."
@mkdir -p $(BUILD_DIR) @mkdir -p $(BUILD_DIR)
@ -93,7 +94,7 @@ build-all: generate
GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR) GOOS=windows GOARCH=amd64 $(GO) build $(LDFLAGS) -o $(BUILD_DIR)/$(BINARY_NAME)-windows-amd64.exe ./$(CMD_DIR)
@echo "All builds complete" @echo "All builds complete"
## install: Install picoclaw to system and copy builtin skills ## install: Install dragonscale to system and copy builtin skills
install: build install: build
@echo "Installing $(BINARY_NAME)..." @echo "Installing $(BINARY_NAME)..."
@mkdir -p $(INSTALL_BIN_DIR) @mkdir -p $(INSTALL_BIN_DIR)
@ -102,7 +103,7 @@ install: build
@echo "Installed binary to $(INSTALL_BIN_DIR)/$(BINARY_NAME)" @echo "Installed binary to $(INSTALL_BIN_DIR)/$(BINARY_NAME)"
@echo "Installation complete!" @echo "Installation complete!"
## uninstall: Remove picoclaw from system ## uninstall: Remove dragonscale from system
uninstall: uninstall:
@echo "Uninstalling $(BINARY_NAME)..." @echo "Uninstalling $(BINARY_NAME)..."
@rm -f $(INSTALL_BIN_DIR)/$(BINARY_NAME) @rm -f $(INSTALL_BIN_DIR)/$(BINARY_NAME)
@ -110,11 +111,11 @@ uninstall:
@echo "Note: Only the executable file has been deleted." @echo "Note: Only the executable file has been deleted."
@echo "If you need to delete all configurations (config.json, workspace, etc.), run 'make uninstall-all'" @echo "If you need to delete all configurations (config.json, workspace, etc.), run 'make uninstall-all'"
## uninstall-all: Remove picoclaw and all data ## uninstall-all: Remove dragonscale and all data
uninstall-all: uninstall-all:
@echo "Removing workspace and skills..." @echo "Removing workspace and skills..."
@rm -rf $(PICOCLAW_HOME) @rm -rf $(DRAGONSCALE_HOME)
@echo "Removed workspace: $(PICOCLAW_HOME)" @echo "Removed workspace: $(DRAGONSCALE_HOME)"
@echo "Complete uninstallation done!" @echo "Complete uninstallation done!"
## clean: Remove build artifacts ## clean: Remove build artifacts
@ -160,13 +161,48 @@ update-deps:
@$(GO) get -u ./... @$(GO) get -u ./...
@$(GO) mod tidy @$(GO) mod tidy
## sqlc-check: Verify sqlc-generated code is up to date ## sqlc-check: Verify sqlc generation is idempotent for current tree
sqlc-check: sqlc-check:
@echo "Checking sqlc generation..." @echo "Checking sqlc generation..."
@sqlc generate -f pkg/memory/sqlc/sqlc.yaml @set -e; repo="$$(pwd)"; before="$$(mktemp)"; after="$$(mktemp)"; \
@git diff --exit-code -- pkg/memory/sqlc/ || (echo "::error::sqlc generated code is stale. Run 'sqlc generate -f pkg/memory/sqlc/sqlc.yaml' and commit." && exit 1) snapshot() { \
git -c safe.directory="$$repo" diff --binary -- pkg/memory/sqlc/; \
git -c safe.directory="$$repo" ls-files --others --exclude-standard -- pkg/memory/sqlc/ | LC_ALL=C sort | while IFS= read -r f; do \
[ -f "$$f" ] && sha256sum "$$f"; \
done; \
}; \
snapshot > "$$before"; \
sqlc generate -f pkg/memory/sqlc/sqlc.yaml; \
snapshot > "$$after"; \
if ! cmp -s "$$before" "$$after"; then \
echo "::error::sqlc generated code is stale. Run 'sqlc generate -f pkg/memory/sqlc/sqlc.yaml' and commit."; \
rm -f "$$before" "$$after"; \
exit 1; \
fi; \
rm -f "$$before" "$$after"
@echo "sqlc OK" @echo "sqlc OK"
## flatc-check: Verify FlatBuffers generation is idempotent for current tree
flatc-check:
@echo "Checking flatc generation..."
@set -e; repo="$$(pwd)"; before="$$(mktemp)"; after="$$(mktemp)"; \
snapshot() { \
git -c safe.directory="$$repo" diff --binary -- pkg/itr/itrfb/ pkg/tools/mapopsfb/; \
git -c safe.directory="$$repo" ls-files --others --exclude-standard -- pkg/itr/itrfb/ pkg/tools/mapopsfb/ | LC_ALL=C sort | while IFS= read -r f; do \
[ -f "$$f" ] && sha256sum "$$f"; \
done; \
}; \
snapshot > "$$before"; \
$(GO) generate ./pkg/itr ./pkg/tools; \
snapshot > "$$after"; \
if ! cmp -s "$$before" "$$after"; then \
echo "::error::FlatBuffers generated code is stale. Run 'go generate ./pkg/itr ./pkg/tools' and commit."; \
rm -f "$$before" "$$after"; \
exit 1; \
fi; \
rm -f "$$before" "$$after"
@echo "flatc OK"
## sqlc-vet: Run sqlc vet rules (no-unbounded-delete, one-select-requires-limit-1) ## sqlc-vet: Run sqlc vet rules (no-unbounded-delete, one-select-requires-limit-1)
sqlc-vet: sqlc-vet:
@echo "Running sqlc vet..." @echo "Running sqlc vet..."
@ -205,10 +241,26 @@ test-integration:
## check: Run vet, fmt, sqlc vet, and verify dependencies ## check: Run vet, fmt, sqlc vet, and verify dependencies
check: deps fmt vet sqlc-vet test check: deps fmt vet sqlc-vet test
## run: Build and run picoclaw ## run: Build and run dragonscale
run: build run: build
@$(BUILD_DIR)/$(BINARY_NAME) $(ARGS) @$(BUILD_DIR)/$(BINARY_NAME) $(ARGS)
## devcontainer-build: Build the local devcontainer image via npx devcontainer CLI
devcontainer-build:
@npx --yes @devcontainers/cli build --workspace-folder .
## devcontainer-up: Start/update the local devcontainer via npx devcontainer CLI
devcontainer-up:
@npx --yes @devcontainers/cli up --workspace-folder .
## devcontainer-generate: Run go/sqlc/flatc generation inside devcontainer
devcontainer-generate:
@npx --yes @devcontainers/cli exec --workspace-folder . -- bash -lc "go generate ./pkg/itr ./pkg/tools && sqlc generate -f pkg/memory/sqlc/sqlc.yaml"
## devcontainer-verify: Validate generated code inside devcontainer
devcontainer-verify:
@npx --yes @devcontainers/cli exec --workspace-folder . -- make flatc-check sqlc-check
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Evaluation Harness # Evaluation Harness
# Usage: make eval-build && make eval # Usage: make eval-build && make eval
@ -225,27 +277,27 @@ eval-build: generate
## eval: Run the eval suite against the current build ## eval: Run the eval suite against the current build
eval: eval-build eval-fixtures eval: eval-build eval-fixtures
@echo "Running eval suite..." @echo "Running eval suite..."
@cd eval && PICOCLAW_EVAL_CONFIG="./configs/default.json" npx promptfoo eval --config promptfooconfig.yaml --no-cache --no-progress-bar @cd eval && DRAGONSCALE_EVAL_CONFIG="./configs/default.json" npx promptfoo eval --config promptfooconfig.yaml --no-cache --no-progress-bar
@echo "Results: eval/results/latest.json" @echo "Results: eval/results/latest.json"
@echo "View: cd eval && npx promptfoo view" @echo "View: cd eval && npx promptfoo view"
## eval-fixtures: Reset workspace to a known state and seed fixture files for eval ## eval-fixtures: Reset workspace to a known state and seed fixture files for eval
## Uses XDG paths: ~/.local/share/picoclaw/sandbox/ and ~/.local/share/picoclaw/skills/ ## Uses XDG paths: ~/.local/share/dragonscale/sandbox/ and ~/.local/share/dragonscale/skills/
eval-fixtures: eval-fixtures:
@mkdir -p $(HOME)/.local/share/picoclaw/sandbox @mkdir -p $(HOME)/.local/share/dragonscale/sandbox
@rm -f $(HOME)/.local/share/picoclaw/sandbox/eval_test_output.txt \ @rm -f $(HOME)/.local/share/dragonscale/sandbox/eval_test_output.txt \
$(HOME)/.local/share/picoclaw/sandbox/test_steps.txt \ $(HOME)/.local/share/dragonscale/sandbox/test_steps.txt \
$(HOME)/.local/share/picoclaw/sandbox/eval_checkpoint.txt \ $(HOME)/.local/share/dragonscale/sandbox/eval_checkpoint.txt \
$(HOME)/.local/share/picoclaw/sandbox/chain_test.txt \ $(HOME)/.local/share/dragonscale/sandbox/chain_test.txt \
$(HOME)/.local/share/picoclaw/sandbox/current_year.txt \ $(HOME)/.local/share/dragonscale/sandbox/current_year.txt \
$(HOME)/.local/share/picoclaw/sandbox/result.txt \ $(HOME)/.local/share/dragonscale/sandbox/result.txt \
$(HOME)/.local/share/picoclaw/sandbox/progressive_test.txt \ $(HOME)/.local/share/dragonscale/sandbox/progressive_test.txt \
$(HOME)/.local/share/picoclaw/sandbox/os_name.txt $(HOME)/.local/share/dragonscale/sandbox/os_name.txt
@rm -rf $(HOME)/.local/share/picoclaw/sandbox/project @rm -rf $(HOME)/.local/share/dragonscale/sandbox/project
@printf 'picoclaw eval fixture — hello from the eval harness\nThis is line two of the fixture file.\n' > $(HOME)/.local/share/picoclaw/sandbox/eval_fixture.txt @printf 'dragonscale eval fixture — hello from the eval harness\nThis is line two of the fixture file.\n' > $(HOME)/.local/share/dragonscale/sandbox/eval_fixture.txt
@cp -f eval/fixtures/sample_data.txt $(HOME)/.local/share/picoclaw/sandbox/sample_data.txt @cp -f eval/fixtures/sample_data.txt $(HOME)/.local/share/dragonscale/sandbox/sample_data.txt
@mkdir -p $(HOME)/.local/share/picoclaw/skills @mkdir -p $(HOME)/.local/share/dragonscale/skills
@cp -rf eval/fixtures/skills/* $(HOME)/.local/share/picoclaw/skills/ 2>/dev/null || true @cp -rf eval/fixtures/skills/* $(HOME)/.local/share/dragonscale/skills/ 2>/dev/null || true
## eval-view: Open the promptfoo results viewer ## eval-view: Open the promptfoo results viewer
eval-view: eval-view:
@ -266,7 +318,7 @@ eval-test:
## help: Show this help message ## help: Show this help message
help: help:
@echo "picoclaw Makefile" @echo "dragonscale Makefile"
@echo "" @echo ""
@echo "Usage:" @echo "Usage:"
@echo " make [target]" @echo " make [target]"
@ -282,7 +334,7 @@ help:
@echo "" @echo ""
@echo "Environment Variables:" @echo "Environment Variables:"
@echo " INSTALL_PREFIX # Installation prefix (default: ~/.local)" @echo " INSTALL_PREFIX # Installation prefix (default: ~/.local)"
@echo " WORKSPACE_DIR # Workspace directory (default: ~/.picoclaw/workspace)" @echo " WORKSPACE_DIR # Workspace directory (default: ~/.dragonscale/workspace)"
@echo " VERSION # Version string (default: git describe)" @echo " VERSION # Version string (default: git describe)"
@echo "" @echo ""
@echo "Current Configuration:" @echo "Current Configuration:"

View file

@ -1,775 +0,0 @@
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
<h1>PicoClaw: Go で書かれた超効率 AI アシスタント</h1>
<h3>$10 ハードウェア · 10MB RAM · 1秒起動 · 行くぜ、シャコ!</h3>
<h3></h3>
<p>
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20RISC--V-blue" alt="Hardware">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
</p>
[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [English](README.md)
</div>
---
🦐 PicoClaw は [nanobot](https://github.com/HKUDS/nanobot) にインスパイアされた超軽量パーソナル AI アシスタントです。Go でゼロからリファクタリングされ、AI エージェント自身がアーキテクチャの移行とコード最適化を推進するセルフブートストラッピングプロセスで構築されました。
⚡️ $10 のハードウェアで 10MB 未満の RAM で動作OpenClaw より 99% 少ないメモリ、Mac mini より 98% 安い!
<table align="center">
<tr align="center">
<td align="center" valign="top">
<p align="center">
<img src="assets/picoclaw_mem.gif" width="360" height="240">
</p>
</td>
<td align="center" valign="top">
<p align="center">
<img src="assets/licheervnano.png" width="400" height="240">
</p>
</td>
</tr>
</table>
## 📢 ニュース
2026-02-09 🎉 PicoClaw リリース!$10 ハードウェアで 10MB 未満の RAM で動く AI エージェントを 1 日で構築。🦐 行くぜ、シャコ!
## ✨ 特徴
🪶 **超軽量**: メモリフットプリント 10MB 未満 — Clawdbot のコア機能より 99% 小さい。
💰 **最小コスト**: $10 ハードウェアで動作 — Mac mini より 98% 安い。
⚡️ **超高速**: 起動時間 400 倍高速、0.6GHz シングルコアでも 1 秒で起動。
🌍 **真のポータビリティ**: RISC-V、ARM、x86 対応の単一バイナリ。ワンクリックで Go
🤖 **AI ブートストラップ**: 自律的な Go ネイティブ実装 — コアの 95% が AI 生成、人間によるレビュー付き。
| | OpenClaw | NanoBot | **PicoClaw** |
| --- | --- | --- |--- |
| **言語** | TypeScript | Python | **Go** |
| **RAM** | >1GB |>100MB| **< 10MB** |
| **起動時間**</br>(0.8GHz コア) | >500秒 | >30秒 | **<1秒** |
| **コスト** | Mac Mini 599$ | 大半の Linux SBC </br>~50$ |**あらゆる Linux ボード**</br>**最安 10$** |
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🦾 デモンストレーション
### 🛠️ スタンダードアシスタントワークフロー
<table align="center">
<tr align="center">
<th><p align="center">🧩 フルスタックエンジニア</p></th>
<th><p align="center">🗂️ ログ&計画管理</p></th>
<th><p align="center">🔎 Web 検索&学習</p></th>
</tr>
<tr>
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
</tr>
<tr>
<td align="center">開発 · デプロイ · スケール</td>
<td align="center">スケジュール · 自動化 · メモリ</td>
<td align="center">発見 · インサイト · トレンド</td>
</tr>
</table>
### 🐜 革新的な省フットプリントデプロイ
PicoClaw はほぼすべての Linux デバイスにデプロイできます!
- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(Ethernet) または W(WiFi6) バージョン、最小ホームアシスタントに
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html) または $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) サーバー自動メンテナンスに
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) または $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) スマート監視に
https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4
🌟 もっと多くのデプロイ事例が待っています!
## 📦 インストール
### コンパイル済みバイナリでインストール
[リリースページ](https://github.com/sipeed/picoclaw/releases) からお使いのプラットフォーム用のファームウェアをダウンロードしてください。
### ソースからインストール(最新機能、開発向け推奨)
```bash
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# ビルド(インストール不要)
make build
# 複数プラットフォーム向けビルド
make build-all
# ビルドとインストール
make install
```
## 🐳 Docker Compose
Docker Compose を使えば、ローカルにインストールせずに PicoClaw を実行できます。
```bash
# 1. リポジトリをクローン
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
# 2. API キーを設定
cp config/config.example.json config/config.json
vim config/config.json # DISCORD_BOT_TOKEN, プロバイダーの API キーを設定
# 3. ビルドと起動
docker compose --profile gateway up -d
# 4. ログ確認
docker compose logs -f picoclaw-gateway
# 5. 停止
docker compose --profile gateway down
```
### Agent モード(ワンショット)
```bash
# 質問を投げる
docker compose run --rm picoclaw-agent -m "What is 2+2?"
# インタラクティブモード
docker compose run --rm picoclaw-agent
```
### リビルド
```bash
docker compose --profile gateway build --no-cache
docker compose --profile gateway up -d
```
### 🚀 クイックスタート(ネイティブ)
> [!TIP]
> `~/.picoclaw/config.json` に API キーを設定してください。
> API キーの取得先: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> Web 検索は **任意** です - 無料の [Brave Search API](https://brave.com/search/api) (月 2000 クエリ無料)
**1. 初期化**
```bash
picoclaw onboard
```
**2. 設定** (`~/.picoclaw/config.json`)
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"openrouter": {
"api_key": "xxx",
"api_base": "https://openrouter.ai/api/v1"
}
},
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
},
"cron": {
"exec_timeout_minutes": 5
}
},
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
**3. API キーの取得**
- **LLM プロバイダー**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
- **Web 検索**(任意): [Brave Search](https://brave.com/search/api) - 無料枠あり(月 2000 リクエスト)
> **注意**: 完全な設定テンプレートは `config.example.json` を参照してください。
**3. チャット**
```bash
picoclaw agent -m "What is 2+2?"
```
これだけです2 分で AI アシスタントが動きます。
---
## 💬 チャットアプリ
Telegram、Discord、QQ、DingTalk、LINE で PicoClaw と会話できます
| チャネル | セットアップ |
|---------|------------|
| **Telegram** | 簡単(トークンのみ) |
| **Discord** | 簡単Bot トークン + Intents |
| **QQ** | 簡単AppID + AppSecret |
| **DingTalk** | 普通(アプリ認証情報) |
| **LINE** | 普通(認証情報 + Webhook URL |
<details>
<summary><b>Telegram</b>(推奨)</summary>
**1. Bot を作成**
- Telegram を開き、`@BotFather` を検索
- `/newbot` を送信、プロンプトに従う
- トークンをコピー
**2. 設定**
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
> ユーザー ID は Telegram の `@userinfobot` から取得できます。
**3. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>Discord</b></summary>
**1. Bot を作成**
- https://discord.com/developers/applications にアクセス
- アプリケーションを作成 → Bot → Add Bot
- Bot トークンをコピー
**2. Intents を有効化**
- Bot の設定画面で **MESSAGE CONTENT INTENT** を有効化
- (任意)**SERVER MEMBERS INTENT** も有効化
**3. ユーザー ID を取得**
- Discord 設定 → 詳細設定 → **開発者モード** を有効化
- 自分のアバターを右クリック → **ユーザーIDをコピー**
**4. 設定**
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
**5. Bot を招待**
- OAuth2 → URL Generator
- Scopes: `bot`
- Bot Permissions: `Send Messages`, `Read Message History`
- 生成された招待 URL を開き、サーバーに Bot を追加
**6. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>QQ</b></summary>
**1. Bot を作成**
- [QQ オープンプラットフォーム](https://q.qq.com/#) にアクセス
- アプリケーションを作成 → **AppID****AppSecret** を取得
**2. 設定**
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
> `allow_from` を空にすると全ユーザーを許可、QQ番号を指定してアクセス制限可能。
**3. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>DingTalk</b></summary>
**1. Bot を作成**
- [オープンプラットフォーム](https://open.dingtalk.com/) にアクセス
- 内部アプリを作成
- Client ID と Client Secret をコピー
**2. 設定**
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
> `allow_from` を空にすると全ユーザーを許可、ユーザーIDを指定してアクセス制限可能。
**3. 起動**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>LINE</b></summary>
**1. LINE 公式アカウントを作成**
- [LINE Developers Console](https://developers.line.biz/) にアクセス
- プロバイダーを作成 → Messaging API チャネルを作成
- **チャネルシークレット****チャネルアクセストークン** をコピー
**2. 設定**
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_host": "0.0.0.0",
"webhook_port": 18791,
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
**3. Webhook URL を設定**
LINE の Webhook には HTTPS が必要です。リバースプロキシまたはトンネルを使用してください:
```bash
# ngrok の例
ngrok http 18791
```
LINE Developers Console で Webhook URL を `https://あなたのドメイン/webhook/line` に設定し、**Webhook の利用** を有効にしてください。
**4. 起動**
```bash
picoclaw gateway
```
> グループチャットでは @メンション時のみ応答します。返信は元メッセージを引用する形式です。
> **Docker Compose**: `picoclaw-gateway` サービスに `ports: ["18791:18791"]` を追加して Webhook ポートを公開してください。
</details>
## ⚙️ 設定
設定ファイル: `~/.picoclaw/config.json`
### ワークスペース構成
PicoClaw は設定されたワークスペース(デフォルト: `~/.picoclaw/workspace`)にデータを保存します:
```
~/.picoclaw/workspace/
├── sessions/ # 会話セッションと履歴
├── memory/ # 長期メモリMEMORY.md
├── state/ # 永続状態(最後のチャネルなど)
├── cron/ # スケジュールジョブデータベース
├── skills/ # カスタムスキル
├── AGENTS.md # エージェントの行動ガイド
├── HEARTBEAT.md # 定期タスクプロンプト30分ごとに確認
├── IDENTITY.md # エージェントのアイデンティティ
├── SOUL.md # エージェントのソウル
├── TOOLS.md # ツールの説明
└── USER.md # ユーザー設定
```
### 🔒 セキュリティサンドボックス
PicoClaw はデフォルトでサンドボックス環境で実行されます。エージェントは設定されたワークスペース内のファイルにのみアクセスし、コマンドを実行できます。
#### デフォルト設定
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"restrict_to_workspace": true
}
}
}
```
| オプション | デフォルト | 説明 |
|-----------|-----------|------|
| `workspace` | `~/.picoclaw/workspace` | エージェントの作業ディレクトリ |
| `restrict_to_workspace` | `true` | ファイル/コマンドアクセスをワークスペースに制限 |
#### 保護対象ツール
`restrict_to_workspace: true` の場合、以下のツールがサンドボックス化されます:
| ツール | 機能 | 制限 |
|-------|------|------|
| `read_file` | ファイル読み込み | ワークスペース内のファイルのみ |
| `write_file` | ファイル書き込み | ワークスペース内のファイルのみ |
| `list_dir` | ディレクトリ一覧 | ワークスペース内のディレクトリのみ |
| `edit_file` | ファイル編集 | ワークスペース内のファイルのみ |
| `append_file` | ファイル追記 | ワークスペース内のファイルのみ |
| `exec` | コマンド実行 | コマンドパスはワークスペース内である必要あり |
#### exec ツールの追加保護
`restrict_to_workspace: false` でも、`exec` ツールは以下の危険なコマンドをブロックします:
- `rm -rf`, `del /f`, `rmdir /s` — 一括削除
- `format`, `mkfs`, `diskpart` — ディスクフォーマット
- `dd if=` — ディスクイメージング
- `/dev/sd[a-z]` への書き込み — 直接ディスク書き込み
- `shutdown`, `reboot`, `poweroff` — システムシャットダウン
- フォークボム `:(){ :|:& };:`
#### エラー例
```
[ERROR] tool: Tool execution failed
{tool=exec, error=Command blocked by safety guard (path outside working dir)}
```
```
[ERROR] tool: Tool execution failed
{tool=exec, error=Command blocked by safety guard (dangerous pattern detected)}
```
#### 制限の無効化(セキュリティリスク)
エージェントにワークスペース外のパスへのアクセスが必要な場合:
**方法1: 設定ファイル**
```json
{
"agents": {
"defaults": {
"restrict_to_workspace": false
}
}
}
```
**方法2: 環境変数**
```bash
export PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE=false
```
> ⚠️ **警告**: この制限を無効にすると、エージェントはシステム上の任意のパスにアクセスできるようになります。制御された環境でのみ慎重に使用してください。
#### セキュリティ境界の一貫性
`restrict_to_workspace` 設定は、すべての実行パスで一貫して適用されます:
| 実行パス | セキュリティ境界 |
|---------|-----------------|
| メインエージェント | `restrict_to_workspace` ✅ |
| サブエージェント / Spawn | 同じ制限を継承 ✅ |
| ハートビートタスク | 同じ制限を継承 ✅ |
すべてのパスで同じワークスペース制限が適用されます — サブエージェントやスケジュールタスクを通じてセキュリティ境界をバイパスする方法はありません。
### ハートビート(定期タスク)
PicoClaw は自動的に定期タスクを実行できます。ワークスペースに `HEARTBEAT.md` ファイルを作成します:
```markdown
# 定期タスク
- 重要なメールをチェック
- 今後の予定を確認
- 天気予報をチェック
```
エージェントは30分ごと設定可能にこのファイルを読み込み、利用可能なツールを使ってタスクを実行します。
#### spawn で非同期タスク実行
時間のかかるタスクWeb検索、API呼び出しには `spawn` ツールを使って**サブエージェント**を作成します:
```markdown
# 定期タスク
## クイックタスク(直接応答)
- 現在時刻を報告
## 長時間タスクspawn で非同期)
- AIニュースを検索して要約
- メールをチェックして重要なメッセージを報告
```
**主な特徴:**
| 機能 | 説明 |
|------|------|
| **spawn** | 非同期サブエージェントを作成、ハートビートをブロックしない |
| **独立コンテキスト** | サブエージェントは独自のコンテキストを持ち、セッション履歴なし |
| **message ツール** | サブエージェントは message ツールで直接ユーザーと通信 |
| **非ブロッキング** | spawn 後、ハートビートは次のタスクへ継続 |
#### サブエージェントの通信方法
```
ハートビート発動
エージェントが HEARTBEAT.md を読む
長いタスク: spawn サブエージェント
↓ ↓
次のタスクへ継続 サブエージェントが独立して動作
↓ ↓
全タスク完了 message ツールを使用
↓ ↓
HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る
```
サブエージェントはツールmessage、web_search など)にアクセスでき、メインエージェントを経由せずにユーザーと通信できます。
**設定:**
```json
{
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
| オプション | デフォルト | 説明 |
|-----------|-----------|------|
| `enabled` | `true` | ハートビートの有効/無効 |
| `interval` | `30` | チェック間隔、最小5分 |
**環境変数:**
- `PICOCLAW_HEARTBEAT_ENABLED=false` で無効化
- `PICOCLAW_HEARTBEAT_INTERVAL=60` で間隔変更
### 基本設定
1. **設定ファイルの作成:**
```bash
cp config.example.json config/config.json
```
2. **設定の編集:**
```json
{
"providers": {
"openrouter": {
"api_key": "sk-or-v1-..."
}
},
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_DISCORD_BOT_TOKEN"
}
}
}
```
3. **実行**
```bash
picoclaw agent -m "Hello"
```
</details>
<details>
<summary><b>完全な設定例</b></summary>
```json
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"providers": {
"openrouter": {
"apiKey": "sk-or-v1-xxx"
},
"groq": {
"apiKey": "gsk_xxx"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "123456:ABC...",
"allowFrom": ["123456789"]
},
"discord": {
"enabled": true,
"token": "",
"allow_from": [""]
},
"whatsapp": {
"enabled": false
},
"feishu": {
"enabled": false,
"appId": "cli_xxx",
"appSecret": "xxx",
"encryptKey": "",
"verificationToken": "",
"allowFrom": []
}
},
"tools": {
"web": {
"search": {
"apiKey": "BSA..."
}
},
"cron": {
"exec_timeout_minutes": 5
}
},
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
</details>
## CLI リファレンス
| コマンド | 説明 |
|---------|------|
| `picoclaw onboard` | 設定&ワークスペースの初期化 |
| `picoclaw agent -m "..."` | エージェントとチャット |
| `picoclaw agent` | インタラクティブチャットモード |
| `picoclaw gateway` | ゲートウェイを起動 |
| `picoclaw status` | ステータスを表示 |
## 🤝 コントリビュート&ロードマップ
PR 歓迎!コードベースは意図的に小さく読みやすくしています。🤗
Discord: https://discord.gg/V4sAZ9XWpN
<img src="assets/wechat.png" alt="PicoClaw" width="512">
## 🐛 トラブルシューティング
### Web 検索で「API 設定の問題」と表示される
検索 API キーをまだ設定していない場合、これは正常です。PicoClaw は手動検索用の便利なリンクを提供します。
Web 検索を有効にするには:
1. [https://brave.com/search/api](https://brave.com/search/api) で無料の API キーを取得(月 2000 クエリ無料)
2. `~/.picoclaw/config.json` に追加:
```json
{
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
}
}
}
```
### コンテンツフィルタリングエラーが出る
一部のプロバイダーZhipu など)にはコンテンツフィルタリングがあります。クエリを言い換えるか、別のモデルを使用してください。
### Telegram Bot で「Conflict: terminated by other getUpdates」と表示される
別のインスタンスが実行中の場合に発生します。`picoclaw gateway` が 1 つだけ実行されていることを確認してください。
---
## 📝 API キー比較
| サービス | 無料枠 | ユースケース |
|---------|--------|------------|
| **OpenRouter** | 月 200K トークン | 複数モデルClaude, GPT-4 など) |
| **Zhipu** | 月 200K トークン | 中国ユーザー向け最適 |
| **Brave Search** | 月 2000 クエリ | Web 検索機能 |
| **Groq** | 無料枠あり | 高速推論Llama, Mixtral |

View file

@ -1,881 +0,0 @@
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
<h1>PicoClaw: Assistente de IA Ultra-Eficiente em Go</h1>
<h3>Hardware de $10 · 10MB de RAM · Boot em 1s · 皮皮虾,我们走!</h3>
<p>
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20RISC--V-blue" alt="Hardware">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
<br>
<a href="https://picoclaw.io"><img src="https://img.shields.io/badge/Website-picoclaw.io-blue?style=flat&logo=google-chrome&logoColor=white" alt="Website"></a>
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
</p>
[中文](README.zh.md) | [日本語](README.ja.md) | [English](README.md) | **Português**
</div>
---
🦐 **PicoClaw** é um assistente pessoal de IA ultra-leve inspirado no [nanobot](https://github.com/HKUDS/nanobot), reescrito do zero em **Go** por meio de um processo de "auto-inicialização" (self-bootstrapping) — onde o próprio agente de IA conduziu toda a migração de arquitetura e otimização de código.
⚡️ **Extremamente leve:** Roda em hardware de apenas **$10** com **<10MB** de RAM. Isso é 99% menos memória que o OpenClaw e 98% mais barato que um Mac mini!
<table align="center">
<tr align="center">
<td align="center" valign="top">
<p align="center">
<img src="assets/picoclaw_mem.gif" width="360" height="240">
</p>
</td>
<td align="center" valign="top">
<p align="center">
<img src="assets/licheervnano.png" width="400" height="240">
</p>
</td>
</tr>
</table>
> [!CAUTION]
> **🚨 DECLARACAO DE SEGURANCA & CANAIS OFICIAIS**
>
> * **SEM CRIPTOMOEDAS:** O PicoClaw **NAO** possui nenhum token/moeda oficial. Todas as alegacoes no `pump.fun` ou outras plataformas de negociacao sao **GOLPES**.
> * **DOMINIO OFICIAL:** O **UNICO** site oficial e **[picoclaw.io](https://picoclaw.io)**, e o site da empresa e **[sipeed.com](https://sipeed.com)**.
> * **Aviso:** Muitos dominios `.ai/.org/.com/.net/...` foram registrados por terceiros, nao sao nossos.
> * **Aviso:** O PicoClaw esta em fase inicial de desenvolvimento e pode ter problemas de seguranca de rede nao resolvidos. Nao implante em ambientes de producao antes da versao v1.0.
> * **Nota:** O PicoClaw recentemente fez merge de muitos PRs, o que pode resultar em maior consumo de memoria (10-20MB) nas versoes mais recentes. Planejamos priorizar a otimizacao de recursos assim que o conjunto de funcionalidades estiver estavel.
## 📢 Novidades
2026-02-16 🎉 PicoClaw atingiu 12K stars em uma semana! Obrigado a todos pelo apoio! O PicoClaw esta crescendo mais rapido do que jamais imaginamos. Dado o alto volume de PRs, precisamos urgentemente de maintainers da comunidade. Nossos papeis de voluntarios e roadmap foram publicados oficialmente [aqui](docs/picoclaw_community_roadmap_260216.md) — estamos ansiosos para ter voce a bordo!
2026-02-13 🎉 PicoClaw atingiu 5000 stars em 4 dias! Obrigado a comunidade! Estamos finalizando o **Roadmap do Projeto** e configurando o **Grupo de Desenvolvedores** para acelerar o desenvolvimento do PicoClaw.
🚀 **Chamada para Acao:** Envie suas solicitacoes de funcionalidades nas GitHub Discussions. Revisaremos e priorizaremos na proxima reuniao semanal.
2026-02-09 🎉 PicoClaw lancado oficialmente! Construido em 1 dia para trazer Agentes de IA para hardware de $10 com <10MB de RAM. 🦐 PicoClaw, Partiu!
## ✨ Funcionalidades
🪶 **Ultra-Leve**: Consumo de memoria <10MB 99% menor que o Clawdbot para funcionalidades essenciais.
💰 **Custo Minimo**: Eficiente o suficiente para rodar em hardware de $10 — 98% mais barato que um Mac mini.
⚡️ **Inicializacao Relampago**: Tempo de inicializacao 400X mais rapido, boot em 1 segundo mesmo em CPU single-core de 0.6GHz.
🌍 **Portabilidade Real**: Um unico binario auto-contido para RISC-V, ARM e x86. Um clique e ja era!
🤖 **Auto-Construido por IA**: Implementacao nativa em Go de forma autonoma — 95% do nucleo gerado pelo Agente com refinamento humano no loop.
| | OpenClaw | NanoBot | **PicoClaw** |
| ----------------------------- | ------------- | ------------------------ | ----------------------------------------- |
| **Linguagem** | TypeScript | Python | **Go** |
| **RAM** | >1GB | >100MB | **< 10MB** |
| **Inicializacao**</br>(CPU 0.8GHz) | >500s | >30s | **<1s** |
| **Custo** | Mac Mini $599 | Maioria dos SBC Linux </br>~$50 | **Qualquer placa Linux**</br>**A partir de $10** |
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🦾 Demonstracao
### 🛠️ Fluxos de Trabalho Padrao do Assistente
<table align="center">
<tr align="center">
<th><p align="center">🧩 Engenharia Full-Stack</p></th>
<th><p align="center">🗂️ Gerenciamento de Logs & Planejamento</p></th>
<th><p align="center">🔎 Busca Web & Aprendizado</p></th>
</tr>
<tr>
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
</tr>
<tr>
<td align="center">Desenvolver • Implantar • Escalar</td>
<td align="center">Agendar • Automatizar • Memorizar</td>
<td align="center">Descobrir • Analisar • Tendencias</td>
</tr>
</table>
### 📱 Rode em celulares Android antigos
De uma segunda vida ao seu celular de dez anos atras! Transforme-o em um assistente de IA inteligente com o PicoClaw. Inicio rapido:
1. **Instale o Termux** (Disponivel no F-Droid ou Google Play).
2. **Execute os comandos**
```bash
# Nota: Substitua v0.1.1 pela versao mais recente da pagina de Releases
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
chmod +x picoclaw-linux-arm64
pkg install proot
termux-chroot ./picoclaw-linux-arm64 onboard
```
Depois siga as instrucoes na secao "Inicio Rapido" para completar a configuracao!
<img src="assets/termux.jpg" alt="PicoClaw" width="512">
### 🐜 Implantacao Inovadora com Baixo Consumo
O PicoClaw pode ser implantado em praticamente qualquer dispositivo Linux!
- $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) versao E (Ethernet) ou W (WiFi6), para Assistente Domestico Minimalista
- $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html), ou $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html) para Manutencao Automatizada de Servidores
- $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) ou $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera) para Monitoramento Inteligente
https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4
🌟 Mais cenarios de implantacao aguardam voce!
## 📦 Instalacao
### Instalar com binario pre-compilado
Baixe o binario para sua plataforma na pagina de [releases](https://github.com/sipeed/picoclaw/releases).
### Instalar a partir do codigo-fonte (funcionalidades mais recentes, recomendado para desenvolvimento)
```bash
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# Build, sem necessidade de instalar
make build
# Build para multiplas plataformas
make build-all
# Build e Instalar
make install
```
## 🐳 Docker Compose
Voce tambem pode rodar o PicoClaw usando Docker Compose sem instalar nada localmente.
```bash
# 1. Clone este repositorio
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
# 2. Configure suas API keys
cp config/config.example.json config/config.json
vim config/config.json # Configure DISCORD_BOT_TOKEN, API keys, etc.
# 3. Build & Iniciar
docker compose --profile gateway up -d
# 4. Ver logs
docker compose logs -f picoclaw-gateway
# 5. Parar
docker compose --profile gateway down
```
### Modo Agente (Execucao unica)
```bash
# Fazer uma pergunta
docker compose run --rm picoclaw-agent -m "Quanto e 2+2?"
# Modo interativo
docker compose run --rm picoclaw-agent
```
### Rebuild
```bash
docker compose --profile gateway build --no-cache
docker compose --profile gateway up -d
```
### 🚀 Inicio Rapido
> [!TIP]
> Configure sua API key em `~/.picoclaw/config.json`.
> Obtenha API keys: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> Busca web e **opcional** — obtenha a [Brave Search API](https://brave.com/search/api) gratuita (2000 consultas gratis/mes) ou use o fallback automatico integrado.
**1. Inicializar**
```bash
picoclaw onboard
```
**2. Configurar** (`~/.picoclaw/config.json`)
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"openrouter": {
"api_key": "xxx",
"api_base": "https://openrouter.ai/api/v1"
}
},
"tools": {
"web": {
"brave": {
"enabled": false,
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
},
"duckduckgo": {
"enabled": true,
"max_results": 5
}
}
}
}
```
**3. Obter API Keys**
* **Provedor de LLM**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
* **Busca Web** (opcional): [Brave Search](https://brave.com/search/api) - Plano gratuito disponivel (2000 consultas/mes)
> **Nota**: Veja `config.example.json` para um modelo de configuracao completo.
**4. Conversar**
```bash
picoclaw agent -m "Quanto e 2+2?"
```
Pronto! Voce tem um assistente de IA funcionando em 2 minutos.
---
## 💬 Integracao com Apps de Chat
Converse com seu PicoClaw via Telegram, Discord, DingTalk ou LINE.
| Canal | Nivel de Configuracao |
| --- | --- |
| **Telegram** | Facil (apenas um token) |
| **Discord** | Facil (bot token + intents) |
| **QQ** | Facil (AppID + AppSecret) |
| **DingTalk** | Medio (credenciais do app) |
| **LINE** | Medio (credenciais + webhook URL) |
<details>
<summary><b>Telegram</b> (Recomendado)</summary>
**1. Criar o bot**
* Abra o Telegram, busque `@BotFather`
* Envie `/newbot`, siga as instrucoes
* Copie o token
**2. Configurar**
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
> Obtenha seu User ID pelo `@userinfobot` no Telegram.
**3. Executar**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>Discord</b></summary>
**1. Criar o bot**
* Acesse <https://discord.com/developers/applications>
* Crie um aplicativo → Bot → Add Bot
* Copie o token do bot
**2. Habilitar Intents**
* Nas configuracoes do Bot, habilite **MESSAGE CONTENT INTENT**
* (Opcional) Habilite **SERVER MEMBERS INTENT** se quiser usar lista de permissoes baseada em dados dos membros
**3. Obter seu User ID**
* Configuracoes do Discord → Avancado → habilite **Modo Desenvolvedor**
* Clique com botao direito no seu avatar → **Copiar ID do Usuario**
**4. Configurar**
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
**5. Convidar o bot**
* OAuth2 → URL Generator
* Scopes: `bot`
* Bot Permissions: `Send Messages`, `Read Message History`
* Abra a URL de convite gerada e adicione o bot ao seu servidor
**6. Executar**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>QQ</b></summary>
**1. Criar o bot**
- Acesse a [QQ Open Platform](https://q.qq.com/#)
- Crie um aplicativo → Obtenha **AppID** e **AppSecret**
**2. Configurar**
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique numeros QQ para restringir o acesso.
**3. Executar**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>DingTalk</b></summary>
**1. Criar o bot**
* Acesse a [Open Platform](https://open.dingtalk.com/)
* Crie um app interno
* Copie o Client ID e Client Secret
**2. Configurar**
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
> Deixe `allow_from` vazio para permitir todos os usuarios, ou especifique IDs para restringir o acesso.
**3. Executar**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>LINE</b></summary>
**1. Criar uma Conta Oficial LINE**
- Acesse o [LINE Developers Console](https://developers.line.biz/)
- Crie um provider → Crie um canal Messaging API
- Copie o **Channel Secret** e o **Channel Access Token**
**2. Configurar**
```json
{
"channels": {
"line": {
"enabled": true,
"channel_secret": "YOUR_CHANNEL_SECRET",
"channel_access_token": "YOUR_CHANNEL_ACCESS_TOKEN",
"webhook_host": "0.0.0.0",
"webhook_port": 18791,
"webhook_path": "/webhook/line",
"allow_from": []
}
}
}
```
**3. Configurar URL do Webhook**
O LINE requer HTTPS para webhooks. Use um reverse proxy ou tunnel:
```bash
# Exemplo com ngrok
ngrok http 18791
```
Em seguida, configure a Webhook URL no LINE Developers Console para `https://seu-dominio/webhook/line` e habilite **Use webhook**.
**4. Executar**
```bash
picoclaw gateway
```
> Em chats de grupo, o bot responde apenas quando mencionado com @. As respostas citam a mensagem original.
> **Docker Compose**: Adicione `ports: ["18791:18791"]` ao servico `picoclaw-gateway` para expor a porta do webhook.
</details>
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> Junte-se a Rede Social de Agentes
Conecte o PicoClaw a Rede Social de Agentes simplesmente enviando uma unica mensagem via CLI ou qualquer App de Chat integrado.
**Leia `https://clawdchat.ai/skill.md` e siga as instrucoes para entrar no [ClawdChat.ai](https://clawdchat.ai)**
## ⚙️ Configuracao Detalhada
Arquivo de configuracao: `~/.picoclaw/config.json`
### Estrutura do Workspace
O PicoClaw armazena dados no workspace configurado (padrao: `~/.picoclaw/workspace`):
```
~/.picoclaw/workspace/
├── sessions/ # Sessoes de conversa e historico
├── memory/ # Memoria de longo prazo (MEMORY.md)
├── state/ # Estado persistente (ultimo canal, etc.)
├── cron/ # Banco de dados de tarefas agendadas
├── skills/ # Skills personalizadas
├── AGENTS.md # Guia de comportamento do Agente
├── HEARTBEAT.md # Prompts de tarefas periodicas (verificado a cada 30 min)
├── IDENTITY.md # Identidade do Agente
├── SOUL.md # Alma do Agente
├── TOOLS.md # Descricao das ferramentas
└── USER.md # Preferencias do usuario
```
### 🔒 Sandbox de Seguranca
O PicoClaw roda em um ambiente sandbox por padrao. O agente so pode acessar arquivos e executar comandos dentro do workspace configurado.
#### Configuracao Padrao
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"restrict_to_workspace": true
}
}
}
```
| Opcao | Padrao | Descricao |
|-------|--------|-----------|
| `workspace` | `~/.picoclaw/workspace` | Diretorio de trabalho do agente |
| `restrict_to_workspace` | `true` | Restringir acesso de arquivos/comandos ao workspace |
#### Ferramentas Protegidas
Quando `restrict_to_workspace: true`, as seguintes ferramentas sao restritas ao sandbox:
| Ferramenta | Funcao | Restricao |
|------------|--------|-----------|
| `read_file` | Ler arquivos | Apenas arquivos dentro do workspace |
| `write_file` | Escrever arquivos | Apenas arquivos dentro do workspace |
| `list_dir` | Listar diretorios | Apenas diretorios dentro do workspace |
| `edit_file` | Editar arquivos | Apenas arquivos dentro do workspace |
| `append_file` | Adicionar a arquivos | Apenas arquivos dentro do workspace |
| `exec` | Executar comandos | Caminhos dos comandos devem estar dentro do workspace |
#### Protecao Adicional do Exec
Mesmo com `restrict_to_workspace: false`, a ferramenta `exec` bloqueia estes comandos perigosos:
* `rm -rf`, `del /f`, `rmdir /s` — Exclusao em massa
* `format`, `mkfs`, `diskpart` — Formatacao de disco
* `dd if=` — Criacao de imagem de disco
* Escrita em `/dev/sd[a-z]` — Escrita direta no disco
* `shutdown`, `reboot`, `poweroff` — Desligamento do sistema
* Fork bomb `:(){ :|:& };:`
#### Exemplos de Erro
```
[ERROR] tool: Tool execution failed
{tool=exec, error=Command blocked by safety guard (path outside working dir)}
```
```
[ERROR] tool: Tool execution failed
{tool=exec, error=Command blocked by safety guard (dangerous pattern detected)}
```
#### Desabilitar Restricoes (Risco de Seguranca)
Se voce precisa que o agente acesse caminhos fora do workspace:
**Metodo 1: Arquivo de configuracao**
```json
{
"agents": {
"defaults": {
"restrict_to_workspace": false
}
}
}
```
**Metodo 2: Variavel de ambiente**
```bash
export PICOCLAW_AGENTS_DEFAULTS_RESTRICT_TO_WORKSPACE=false
```
> ⚠️ **Aviso**: Desabilitar esta restricao permite que o agente acesse qualquer caminho no seu sistema. Use com cuidado apenas em ambientes controlados.
#### Consistencia do Limite de Seguranca
A configuracao `restrict_to_workspace` se aplica consistentemente em todos os caminhos de execucao:
| Caminho de Execucao | Limite de Seguranca |
|----------------------|---------------------|
| Agente Principal | `restrict_to_workspace` ✅ |
| Subagente / Spawn | Herda a mesma restricao ✅ |
| Tarefas Heartbeat | Herda a mesma restricao ✅ |
Todos os caminhos compartilham a mesma restricao de workspace — nao ha como contornar o limite de seguranca por meio de subagentes ou tarefas agendadas.
### Heartbeat (Tarefas Periodicas)
O PicoClaw pode executar tarefas periodicas automaticamente. Crie um arquivo `HEARTBEAT.md` no seu workspace:
```markdown
# Tarefas Periodicas
- Verificar meu email para mensagens importantes
- Revisar minha agenda para proximos eventos
- Verificar a previsao do tempo
```
O agente lera este arquivo a cada 30 minutos (configuravel) e executara as tarefas usando as ferramentas disponiveis.
#### Tarefas Assincronas com Spawn
Para tarefas de longa duracao (busca web, chamadas de API), use a ferramenta `spawn` para criar um **subagente**:
```markdown
# Tarefas Periodicas
## Tarefas Rapidas (resposta direta)
- Informar hora atual
## Tarefas Longas (usar spawn para async)
- Buscar noticias de IA na web e resumir
- Verificar email e reportar mensagens importantes
```
**Comportamentos principais:**
| Funcionalidade | Descricao |
|----------------|-----------|
| **spawn** | Cria subagente assincrono, nao bloqueia o heartbeat |
| **Contexto independente** | Subagente tem seu proprio contexto, sem historico de sessao |
| **Ferramenta message** | Subagente se comunica diretamente com o usuario via ferramenta message |
| **Nao-bloqueante** | Apos o spawn, o heartbeat continua para a proxima tarefa |
#### Como Funciona a Comunicacao do Subagente
```
Heartbeat dispara
Agente le HEARTBEAT.md
Para tarefa longa: spawn subagente
↓ ↓
Continua proxima tarefa Subagente trabalha independentemente
↓ ↓
Todas tarefas concluidas Subagente usa ferramenta "message"
↓ ↓
Responde HEARTBEAT_OK Usuario recebe resultado diretamente
```
O subagente tem acesso as ferramentas (message, web_search, etc.) e pode se comunicar com o usuario independentemente sem passar pelo agente principal.
**Configuracao:**
```json
{
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
| Opcao | Padrao | Descricao |
|-------|--------|-----------|
| `enabled` | `true` | Habilitar/desabilitar heartbeat |
| `interval` | `30` | Intervalo de verificacao em minutos (min: 5) |
**Variaveis de ambiente:**
* `PICOCLAW_HEARTBEAT_ENABLED=false` para desabilitar
* `PICOCLAW_HEARTBEAT_INTERVAL=60` para alterar o intervalo
### Provedores
> [!NOTE]
> O Groq fornece transcricao de voz gratuita via Whisper. Se configurado, mensagens de voz do Telegram serao automaticamente transcritas.
| Provedor | Finalidade | Obter API Key |
| --- | --- | --- |
| `gemini` | LLM (Gemini direto) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (Zhipu direto) | [bigmodel.cn](bigmodel.cn) |
| `openrouter` (Em teste) | LLM (recomendado, acesso a todos os modelos) | [openrouter.ai](https://openrouter.ai) |
| `anthropic` (Em teste) | LLM (Claude direto) | [console.anthropic.com](https://console.anthropic.com) |
| `openai` (Em teste) | LLM (GPT direto) | [platform.openai.com](https://platform.openai.com) |
| `deepseek` (Em teste) | LLM (DeepSeek direto) | [platform.deepseek.com](https://platform.deepseek.com) |
| `groq` | LLM + **Transcricao de voz** (Whisper) | [console.groq.com](https://console.groq.com) |
<details>
<summary><b>Configuracao Zhipu</b></summary>
**1. Obter API key**
* Obtenha a [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
**2. Configurar**
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"zhipu": {
"api_key": "Sua API Key",
"api_base": "https://open.bigmodel.cn/api/paas/v4"
}
}
}
```
**3. Executar**
```bash
picoclaw agent -m "Ola, como vai?"
```
</details>
<details>
<summary><b>Exemplo de configuracao completa</b></summary>
```json
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"providers": {
"openrouter": {
"api_key": "sk-or-v1-xxx"
},
"groq": {
"api_key": "gsk_xxx"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "123456:ABC...",
"allow_from": ["123456789"]
},
"discord": {
"enabled": true,
"token": "",
"allow_from": [""]
},
"whatsapp": {
"enabled": false
},
"feishu": {
"enabled": false,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
},
"qq": {
"enabled": false,
"app_id": "",
"app_secret": "",
"allow_from": []
}
},
"tools": {
"web": {
"brave": {
"enabled": false,
"api_key": "BSA...",
"max_results": 5
},
"duckduckgo": {
"enabled": true,
"max_results": 5
}
},
"cron": {
"exec_timeout_minutes": 5
}
},
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
</details>
## Referencia CLI
| Comando | Descricao |
| --- | --- |
| `picoclaw onboard` | Inicializar configuracao & workspace |
| `picoclaw agent -m "..."` | Conversar com o agente |
| `picoclaw agent` | Modo de chat interativo |
| `picoclaw gateway` | Iniciar o gateway (para bots de chat) |
| `picoclaw status` | Mostrar status |
| `picoclaw cron list` | Listar todas as tarefas agendadas |
| `picoclaw cron add ...` | Adicionar uma tarefa agendada |
### Tarefas Agendadas / Lembretes
O PicoClaw suporta lembretes agendados e tarefas recorrentes por meio da ferramenta `cron`:
* **Lembretes unicos**: "Remind me in 10 minutes" (Me lembre em 10 minutos) → dispara uma vez apos 10min
* **Tarefas recorrentes**: "Remind me every 2 hours" (Me lembre a cada 2 horas) → dispara a cada 2 horas
* **Expressoes Cron**: "Remind me at 9am daily" (Me lembre as 9h todos os dias) → usa expressao cron
As tarefas sao armazenadas em `~/.picoclaw/workspace/cron/` e processadas automaticamente.
## 🤝 Contribuir & Roadmap
PRs sao bem-vindos! O codigo-fonte e intencionalmente pequeno e legivel. 🤗
Roadmap em breve...
Grupo de desenvolvedores em formacao. Requisito de entrada: Pelo menos 1 PR com merge.
Grupos de usuarios:
Discord: <https://discord.gg/V4sAZ9XWpN>
<img src="assets/wechat.png" alt="PicoClaw" width="512">
## 🐛 Solucao de Problemas
### Busca web mostra "API 配置问题"
Isso e normal se voce ainda nao configurou uma API key de busca. O PicoClaw fornecera links uteis para busca manual.
Para habilitar a busca web:
1. **Opcao 1 (Recomendado)**: Obtenha uma API key gratuita em [https://brave.com/search/api](https://brave.com/search/api) (2000 consultas gratis/mes) para os melhores resultados.
2. **Opcao 2 (Sem Cartao de Credito)**: Se voce nao tem uma key, o sistema automaticamente usa o **DuckDuckGo** como fallback (sem necessidade de key).
Adicione a key em `~/.picoclaw/config.json` se usar o Brave:
```json
{
"tools": {
"web": {
"brave": {
"enabled": true,
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
},
"duckduckgo": {
"enabled": true,
"max_results": 5
}
}
}
}
```
### Erros de filtragem de conteudo
Alguns provedores (como Zhipu) possuem filtragem de conteudo. Tente reformular sua pergunta ou use um modelo diferente.
### Bot do Telegram diz "Conflict: terminated by other getUpdates"
Isso acontece quando outra instancia do bot esta rodando. Certifique-se de que apenas um `picoclaw gateway` esteja rodando por vez.
---
## 📝 Comparacao de API Keys
| Servico | Plano Gratuito | Caso de Uso |
| --- | --- | --- |
| **OpenRouter** | 200K tokens/mes | Multiplos modelos (Claude, GPT-4, etc.) |
| **Zhipu** | 200K tokens/mes | Melhor para usuarios chineses |
| **Brave Search** | 2000 consultas/mes | Funcionalidade de busca web |
| **Groq** | Plano gratuito disponivel | Inferencia ultra-rapida (Llama, Mixtral) |

View file

@ -1,744 +0,0 @@
<div align="center">
<img src="assets/logo.jpg" alt="PicoClaw" width="512">
<h1>PicoClaw: 基于Go语言的超高效 AI 助手</h1>
<h3>10$硬件 · 10MB内存 · 1秒启动 · 皮皮虾,我们走!</h3>
<p>
<img src="https://img.shields.io/badge/Go-1.21+-00ADD8?style=flat&logo=go&logoColor=white" alt="Go">
<img src="https://img.shields.io/badge/Arch-x86__64%2C%20ARM64%2C%20RISC--V-blue" alt="Hardware">
<img src="https://img.shields.io/badge/license-MIT-green" alt="License">
<br>
<a href="https://picoclaw.io"><img src="https://img.shields.io/badge/Website-picoclaw.io-blue?style=flat&logo=google-chrome&logoColor=white" alt="Website"></a>
<a href="https://x.com/SipeedIO"><img src="https://img.shields.io/badge/X_(Twitter)-SipeedIO-black?style=flat&logo=x&logoColor=white" alt="Twitter"></a>
</p>
**中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [English](README.md)
</div>
---
🦐 **PicoClaw** 是一个受 [nanobot](https://github.com/HKUDS/nanobot) 启发的超轻量级个人 AI 助手。它采用 **Go 语言** 从零重构,经历了一个“自举”过程——即由 AI Agent 自身驱动了整个架构迁移和代码优化。
⚡️ **极致轻量**:可在 **10 美元** 的硬件上运行,内存占用 **<10MB**。这意味着比 OpenClaw 节省 99% 的内存 Mac mini 便宜 98%
<table align="center">
<tr align="center">
<td align="center" valign="top">
<p align="center">
<img src="assets/picoclaw_mem.gif" width="360" height="240">
</p>
</td>
<td align="center" valign="top">
<p align="center">
<img src="assets/licheervnano.png" width="400" height="240">
</p>
</td>
</tr>
</table>
注意:人手有限,中文文档可能略有滞后,请优先查看英文文档。
> [!CAUTION]
> **🚨 SECURITY & OFFICIAL CHANNELS / 安全声明**
> * **无加密货币 (NO CRYPTO):** PicoClaw **没有** 发行任何官方代币、Token 或虚拟货币。所有在 `pump.fun` 或其他交易平台上的相关声称均为 **诈骗**
> * **官方域名:** 唯一的官方网站是 **[picoclaw.io](https://picoclaw.io)**,公司官网是 **[sipeed.com](https://sipeed.com)**。
> * **警惕:** 许多 `.ai/.org/.com/.net/...` 后缀的域名被第三方抢注,请勿轻信。
> * **注意:** picoclaw正在初期的快速功能开发阶段可能有尚未修复的网络安全问题在1.0正式版发布前,请不要将其部署到生产环境中
> * **注意:** picoclaw最近合并了大量PRs近期版本可能内存占用较大(10~20MB),我们将在功能较为收敛后进行资源占用优化.
## 📢 新闻 (News)
2026-02-16 🎉 PicoClaw 在一周内突破了12K star! 感谢大家的关注PicoClaw 的成长速度超乎我们预期. 由于PR数量的快速膨胀我们亟需社区开发者参与维护. 我们需要的志愿者角色和roadmap已经发布到了[这里](docs/picoclaw_community_roadmap_260216.md), 期待你的参与!
2026-02-13 🎉 **PicoClaw 在 4 天内突破 5000 Stars** 感谢社区的支持由于正值中国春节假期PR 和 Issue 涌入较多,我们正在利用这段时间敲定 **项目路线图 (Roadmap)** 并组建 **开发者群组**,以便加速 PicoClaw 的开发。
🚀 **行动号召:** 请在 GitHub Discussions 中提交您的功能请求 (Feature Requests)。我们将在接下来的周会上进行审查和优先级排序。
2026-02-09 🎉 **PicoClaw 正式发布!** 仅用 1 天构建,旨在将 AI Agent 带入 10 美元硬件与 <10MB 内存的世界。🦐 PicoClaw皮皮虾我们走
## ✨ 特性
🪶 **超轻量级**: 核心功能内存占用 <10MB Clawdbot 99%。
💰 **极低成本**: 高效到足以在 10 美元的硬件上运行 — 比 Mac mini 便宜 98%。
⚡️ **闪电启动**: 启动速度快 400 倍,即使在 0.6GHz 单核处理器上也能在 1 秒内启动。
🌍 **真正可移植**: 跨 RISC-V、ARM 和 x86 架构的单二进制文件,一键运行!
🤖 **AI 自举**: 纯 Go 语言原生实现 — 95% 的核心代码由 Agent 生成,并经由“人机回环 (Human-in-the-loop)”微调。
| | OpenClaw | NanoBot | **PicoClaw** |
| --- | --- | --- | --- |
| **语言** | TypeScript | Python | **Go** |
| **RAM** | >1GB | >100MB | **< 10MB** |
| **启动时间**</br>(0.8GHz core) | >500s | >30s | **<1s** |
| **成本** | Mac Mini $599 | 大多数 Linux 开发板 ~$50 | **任意 Linux 开发板**</br>**低至 $10** |
<img src="assets/compare.jpg" alt="PicoClaw" width="512">
## 🦾 演示
### 🛠️ 标准助手工作流
<table align="center">
<tr align="center">
<th><p align="center">🧩 全栈工程师模式</p></th>
<th><p align="center">🗂️ 日志与规划管理</p></th>
<th><p align="center">🔎 网络搜索与学习</p></th>
</tr>
<tr>
<td align="center"><p align="center"><img src="assets/picoclaw_code.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_memory.gif" width="240" height="180"></p></td>
<td align="center"><p align="center"><img src="assets/picoclaw_search.gif" width="240" height="180"></p></td>
</tr>
<tr>
<td align="center">开发 • 部署 • 扩展</td>
<td align="center">日程 • 自动化 • 记忆</td>
<td align="center">发现 • 洞察 • 趋势</td>
</tr>
</table>
### 📱 在手机上轻松运行
picoclaw 可以将你10年前的老旧手机废物利用变身成为你的AI助理快速指南:
1. 先去应用商店下载安装Termux
2. 打开后执行指令
```bash
# 注意: 下面的v0.1.1 可以换为你实际看到的最新版本
wget https://github.com/sipeed/picoclaw/releases/download/v0.1.1/picoclaw-linux-arm64
chmod +x picoclaw-linux-arm64
pkg install proot
termux-chroot ./picoclaw-linux-arm64 onboard
```
然后跟随下面的“快速开始”章节继续配置picoclaw即可使用
<img src="assets/termux.jpg" alt="PicoClaw" width="512">
### 🐜 创新的低占用部署
PicoClaw 几乎可以部署在任何 Linux 设备上!
* $9.9 [LicheeRV-Nano](https://www.aliexpress.com/item/1005006519668532.html) E(网口) 或 W(WiFi6) 版本,用于极简家庭助手。
* $30~50 [NanoKVM](https://www.aliexpress.com/item/1005007369816019.html),或 $100 [NanoKVM-Pro](https://www.aliexpress.com/item/1005010048471263.html),用于自动化服务器运维。
* $50 [MaixCAM](https://www.aliexpress.com/item/1005008053333693.html) 或 $100 [MaixCAM2](https://www.kickstarter.com/projects/zepan/maixcam2-build-your-next-gen-4k-ai-camera),用于智能监控。
[https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4](https://private-user-images.githubusercontent.com/83055338/547056448-e7b031ff-d6f5-4468-bcca-5726b6fecb5c.mp4)
🌟 更多部署案例敬请期待!
## 📦 安装
### 使用预编译二进制文件安装
从 [Release 页面](https://github.com/sipeed/picoclaw/releases) 下载适用于您平台的固件。
### 从源码安装(获取最新特性,开发推荐)
```bash
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
make deps
# 构建(无需安装)
make build
# 为多平台构建
make build-all
# 构建并安装
make install
```
## 🐳 Docker Compose
您也可以使用 Docker Compose 运行 PicoClaw无需在本地安装任何环境。
```bash
# 1. 克隆仓库
git clone https://github.com/sipeed/picoclaw.git
cd picoclaw
# 2. 设置 API Key
cp config/config.example.json config/config.json
vim config/config.json # 设置 DISCORD_BOT_TOKEN, API keys 等
# 3. 构建并启动
docker compose --profile gateway up -d
# 4. 查看日志
docker compose logs -f picoclaw-gateway
# 5. 停止
docker compose --profile gateway down
```
### Agent 模式 (一次性运行)
```bash
# 提问
docker compose run --rm picoclaw-agent -m "2+2 等于几?"
# 交互模式
docker compose run --rm picoclaw-agent
```
### 重新构建
```bash
docker compose --profile gateway build --no-cache
docker compose --profile gateway up -d
```
### 🚀 快速开始
> [!TIP]
> 在 `~/.picoclaw/config.json` 中设置您的 API Key。
> 获取 API Key: [OpenRouter](https://openrouter.ai/keys) (LLM) · [Zhipu (智谱)](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) (LLM)
> 网络搜索是 **可选的** - 获取免费的 [Brave Search API](https://brave.com/search/api) (每月 2000 次免费查询)
**1. 初始化 (Initialize)**
```bash
picoclaw onboard
```
**2. 配置 (Configure)** (`~/.picoclaw/config.json`)
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"openrouter": {
"api_key": "xxx",
"api_base": "https://openrouter.ai/api/v1"
}
},
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
},
"cron": {
"exec_timeout_minutes": 5
}
}
}
```
**3. 获取 API Key**
* **LLM 提供商**: [OpenRouter](https://openrouter.ai/keys) · [Zhipu](https://open.bigmodel.cn/usercenter/proj-mgmt/apikeys) · [Anthropic](https://console.anthropic.com) · [OpenAI](https://platform.openai.com) · [Gemini](https://aistudio.google.com/api-keys)
* **网络搜索** (可选): [Brave Search](https://brave.com/search/api) - 提供免费层级 (2000 请求/月)
> **注意**: 完整的配置模板请参考 `config.example.json`
**4. 对话 (Chat)**
```bash
picoclaw agent -m "2+2 等于几?"
```
就是这样!您在 2 分钟内就拥有了一个可工作的 AI 助手。
---
## 💬 聊天应用集成 (Chat Apps)
通过 Telegram, Discord 或钉钉与您的 PicoClaw 对话。
| 渠道 | 设置难度 |
| --- | --- |
| **Telegram** | 简单 (仅需 token) |
| **Discord** | 简单 (bot token + intents) |
| **QQ** | 简单 (AppID + AppSecret) |
| **钉钉 (DingTalk)** | 中等 (app credentials) |
<details>
<summary><b>Telegram</b> (推荐)</summary>
**1. 创建机器人**
* 打开 Telegram搜索 `@BotFather`
* 发送 `/newbot`,按照提示操作
* 复制 token
**2. 配置**
```json
{
"channels": {
"telegram": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
> 从 Telegram 上的 `@userinfobot` 获取您的用户 ID。
**3. 运行**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>Discord</b></summary>
**1. 创建机器人**
* 前往 [https://discord.com/developers/applications](https://discord.com/developers/applications)
* Create an application → Bot → Add Bot
* 复制 bot token
**2. 开启 Intents**
* 在 Bot 设置中,开启 **MESSAGE CONTENT INTENT**
* (可选) 如果计划基于成员数据使用白名单,开启 **SERVER MEMBERS INTENT**
**3. 获取您的 User ID**
* Discord 设置 → Advanced → 开启 **Developer Mode**
* 右键点击您的头像 → **Copy User ID**
**4. 配置**
```json
{
"channels": {
"discord": {
"enabled": true,
"token": "YOUR_BOT_TOKEN",
"allowFrom": ["YOUR_USER_ID"]
}
}
}
```
**5. 邀请机器人**
* OAuth2 → URL Generator
* Scopes: `bot`
* Bot Permissions: `Send Messages`, `Read Message History`
* 打开生成的邀请 URL将机器人添加到您的服务器
**6. 运行**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>QQ</b></summary>
**1. 创建机器人**
* 前往 [QQ 开放平台](https://q.qq.com/#)
* 创建应用 → 获取 **AppID** 和 **AppSecret**
**2. 配置**
```json
{
"channels": {
"qq": {
"enabled": true,
"app_id": "YOUR_APP_ID",
"app_secret": "YOUR_APP_SECRET",
"allow_from": []
}
}
}
```
> 将 `allow_from` 设为空以允许所有用户,或指定 QQ 号以限制访问。
**3. 运行**
```bash
picoclaw gateway
```
</details>
<details>
<summary><b>钉钉 (DingTalk)</b></summary>
**1. 创建机器人**
* 前往 [开放平台](https://open.dingtalk.com/)
* 创建内部应用
* 复制 Client ID 和 Client Secret
**2. 配置**
```json
{
"channels": {
"dingtalk": {
"enabled": true,
"client_id": "YOUR_CLIENT_ID",
"client_secret": "YOUR_CLIENT_SECRET",
"allow_from": []
}
}
}
```
> 将 `allow_from` 设为空以允许所有用户,或指定 ID 以限制访问。
**3. 运行**
```bash
picoclaw gateway
```
</details>
## <img src="assets/clawdchat-icon.png" width="24" height="24" alt="ClawdChat"> 加入 Agent 社交网络
只需通过 CLI 或任何集成的聊天应用发送一条消息,即可将 PicoClaw 连接到 Agent 社交网络。
**阅读 `https://clawdchat.ai/skill.md` 并按照说明加入 [ClawdChat.ai**](https://clawdchat.ai)
## ⚙️ 配置详解
配置文件路径: `~/.picoclaw/config.json`
### 工作区布局 (Workspace Layout)
PicoClaw 将数据存储在您配置的工作区中(默认:`~/.picoclaw/workspace`
```
~/.picoclaw/workspace/
├── sessions/ # 对话会话和历史
├── memory/ # 长期记忆 (MEMORY.md)
├── state/ # 持久化状态 (最后一次频道等)
├── cron/ # 定时任务数据库
├── skills/ # 自定义技能
├── AGENTS.md # Agent 行为指南
├── HEARTBEAT.md # 周期性任务提示词 (每 30 分钟检查一次)
├── IDENTITY.md # Agent 身份设定
├── SOUL.md # Agent 灵魂/性格
├── TOOLS.md # 工具描述
└── USER.md # 用户偏好
```
### 心跳 / 周期性任务 (Heartbeat)
PicoClaw 可以自动执行周期性任务。在工作区创建 `HEARTBEAT.md` 文件:
```markdown
# Periodic Tasks
- Check my email for important messages
- Review my calendar for upcoming events
- Check the weather forecast
```
Agent 将每隔 30 分钟(可配置)读取此文件,并使用可用工具执行任务。
#### 使用 Spawn 的异步任务
对于耗时较长的任务网络搜索、API 调用),使用 `spawn` 工具创建一个 **子 Agent (subagent)**
```markdown
# Periodic Tasks
## Quick Tasks (respond directly)
- Report current time
## Long Tasks (use spawn for async)
- Search the web for AI news and summarize
- Check email and report important messages
```
**关键行为:**
| 特性 | 描述 |
| --- | --- |
| **spawn** | 创建异步子 Agent不阻塞主心跳进程 |
| **独立上下文** | 子 Agent 拥有独立上下文,无会话历史 |
| **message tool** | 子 Agent 通过 message 工具直接与用户通信 |
| **非阻塞** | spawn 后,心跳继续处理下一个任务 |
#### 子 Agent 通信原理
```
心跳触发 (Heartbeat triggers)
Agent 读取 HEARTBEAT.md
对于长任务: spawn 子 Agent
↓ ↓
继续下一个任务 子 Agent 独立工作
↓ ↓
所有任务完成 子 Agent 使用 "message" 工具
↓ ↓
响应 HEARTBEAT_OK 用户直接收到结果
```
子 Agent 可以访问工具message, web_search 等),并且无需通过主 Agent 即可独立与用户通信。
**配置:**
```json
{
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
| 选项 | 默认值 | 描述 |
| --- | --- | --- |
| `enabled` | `true` | 启用/禁用心跳 |
| `interval` | `30` | 检查间隔,单位分钟 (最小: 5) |
**环境变量:**
* `PICOCLAW_HEARTBEAT_ENABLED=false` 禁用
* `PICOCLAW_HEARTBEAT_INTERVAL=60` 更改间隔
### 提供商 (Providers)
> [!NOTE]
> Groq 通过 Whisper 提供免费的语音转录。如果配置了 GroqTelegram 语音消息将被自动转录为文字。
| 提供商 | 用途 | 获取 API Key |
| --- | --- | --- |
| `gemini` | LLM (Gemini 直连) | [aistudio.google.com](https://aistudio.google.com) |
| `zhipu` | LLM (智谱直连) | [bigmodel.cn](bigmodel.cn) |
| `openrouter(待测试)` | LLM (推荐,可访问所有模型) | [openrouter.ai](https://openrouter.ai) |
| `anthropic(待测试)` | LLM (Claude 直连) | [console.anthropic.com](https://console.anthropic.com) |
| `openai(待测试)` | LLM (GPT 直连) | [platform.openai.com](https://platform.openai.com) |
| `deepseek(待测试)` | LLM (DeepSeek 直连) | [platform.deepseek.com](https://platform.deepseek.com) |
| `groq` | LLM + **语音转录** (Whisper) | [console.groq.com](https://console.groq.com) |
<details>
<summary><b>智谱 (Zhipu) 配置示例</b></summary>
**1. 获取 API key 和 base URL**
* 获取 [API key](https://bigmodel.cn/usercenter/proj-mgmt/apikeys)
**2. 配置**
```json
{
"agents": {
"defaults": {
"workspace": "~/.picoclaw/workspace",
"model": "glm-4.7",
"max_tokens": 8192,
"temperature": 0.7,
"max_tool_iterations": 20
}
},
"providers": {
"zhipu": {
"api_key": "Your API Key",
"api_base": "https://open.bigmodel.cn/api/paas/v4"
},
},
}
```
**3. 运行**
```bash
picoclaw agent -m "你好"
```
</details>
<details>
<summary><b>完整配置示例</b></summary>
```json
{
"agents": {
"defaults": {
"model": "anthropic/claude-opus-4-5"
}
},
"providers": {
"openrouter": {
"api_key": "sk-or-v1-xxx"
},
"groq": {
"api_key": "gsk_xxx"
}
},
"channels": {
"telegram": {
"enabled": true,
"token": "123456:ABC...",
"allow_from": ["123456789"]
},
"discord": {
"enabled": true,
"token": "",
"allow_from": [""]
},
"whatsapp": {
"enabled": false
},
"feishu": {
"enabled": false,
"app_id": "cli_xxx",
"app_secret": "xxx",
"encrypt_key": "",
"verification_token": "",
"allow_from": []
},
"qq": {
"enabled": false,
"app_id": "",
"app_secret": "",
"allow_from": []
}
},
"tools": {
"web": {
"search": {
"api_key": "BSA..."
}
},
"cron": {
"exec_timeout_minutes": 5
}
},
"heartbeat": {
"enabled": true,
"interval": 30
}
}
```
</details>
## CLI 命令行参考
| 命令 | 描述 |
| --- | --- |
| `picoclaw onboard` | 初始化配置和工作区 |
| `picoclaw agent -m "..."` | 与 Agent 对话 |
| `picoclaw agent` | 交互式聊天模式 |
| `picoclaw gateway` | 启动网关 (Gateway) |
| `picoclaw status` | 显示状态 |
| `picoclaw cron list` | 列出所有定时任务 |
| `picoclaw cron add ...` | 添加定时任务 |
### 定时任务 / 提醒 (Scheduled Tasks)
PicoClaw 通过 `cron` 工具支持定时提醒和重复任务:
* **一次性提醒**: "Remind me in 10 minutes" (10分钟后提醒我) → 10分钟后触发一次
* **重复任务**: "Remind me every 2 hours" (每2小时提醒我) → 每2小时触发
* **Cron 表达式**: "Remind me at 9am daily" (每天上午9点提醒我) → 使用 cron 表达式
任务存储在 `~/.picoclaw/workspace/cron/` 中并自动处理。
## 🤝 贡献与路线图 (Roadmap)
欢迎提交 PR代码库刻意保持小巧和可读。🤗
路线图即将发布...
开发者群组正在组建中,入群门槛:至少合并过 1 个 PR。
用户群组:
Discord: [https://discord.gg/V4sAZ9XWpN](https://discord.gg/V4sAZ9XWpN)
<img src="assets/wechat.png" alt="PicoClaw" width="512">
## 🐛 疑难解答 (Troubleshooting)
### 网络搜索提示 "API 配置问题"
如果您尚未配置搜索 API Key这是正常的。PicoClaw 会提供手动搜索的帮助链接。
启用网络搜索:
1. 在 [https://brave.com/search/api](https://brave.com/search/api) 获取免费 API Key (每月 2000 次免费查询)
2. 添加到 `~/.picoclaw/config.json`:
```json
{
"tools": {
"web": {
"search": {
"api_key": "YOUR_BRAVE_API_KEY",
"max_results": 5
}
}
}
}
```
### 遇到内容过滤错误 (Content Filtering Errors)
某些提供商(如智谱)有严格的内容过滤。尝试改写您的问题或使用其他模型。
### Telegram bot 提示 "Conflict: terminated by other getUpdates"
这表示有另一个机器人实例正在运行。请确保同一时间只有一个 `picoclaw gateway` 进程在运行。
---
## 📝 API Key 对比
| 服务 | 免费层级 | 适用场景 |
| --- | --- | --- |
| **OpenRouter** | 200K tokens/月 | 多模型聚合 (Claude, GPT-4 等) |
| **智谱 (Zhipu)** | 200K tokens/月 | 最适合中国用户 |
| **Brave Search** | 2000 次查询/月 | 网络搜索功能 |
| **Groq** | 提供免费层级 | 极速推理 (Llama, Mixtral) |

View file

@ -1,8 +1,8 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot // Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package main package main
@ -21,28 +21,28 @@ import (
"strings" "strings"
"time" "time"
"github.com/ZanzyTHEbar/dragonscale/pkg/agent"
"github.com/ZanzyTHEbar/dragonscale/pkg/auth"
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/ZanzyTHEbar/dragonscale/pkg/channels"
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/ZanzyTHEbar/dragonscale/pkg/cron"
"github.com/ZanzyTHEbar/dragonscale/pkg/devices"
"github.com/ZanzyTHEbar/dragonscale/pkg/health"
"github.com/ZanzyTHEbar/dragonscale/pkg/heartbeat"
"github.com/ZanzyTHEbar/dragonscale/pkg/itr"
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
picomemory "github.com/ZanzyTHEbar/dragonscale/pkg/memory"
"github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate"
"github.com/ZanzyTHEbar/dragonscale/pkg/migrate"
picoruntime "github.com/ZanzyTHEbar/dragonscale/pkg/runtime"
"github.com/ZanzyTHEbar/dragonscale/pkg/security"
"github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
"github.com/ZanzyTHEbar/dragonscale/pkg/skills"
"github.com/ZanzyTHEbar/dragonscale/pkg/state"
"github.com/ZanzyTHEbar/dragonscale/pkg/tools"
"github.com/ZanzyTHEbar/dragonscale/pkg/voice"
"github.com/chzyer/readline" "github.com/chzyer/readline"
"github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/auth"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/cron"
"github.com/sipeed/picoclaw/pkg/devices"
"github.com/sipeed/picoclaw/pkg/health"
"github.com/sipeed/picoclaw/pkg/heartbeat"
"github.com/sipeed/picoclaw/pkg/itr"
"github.com/sipeed/picoclaw/pkg/logger"
picomemory "github.com/sipeed/picoclaw/pkg/memory"
"github.com/sipeed/picoclaw/pkg/memory/delegate"
"github.com/sipeed/picoclaw/pkg/migrate"
picoruntime "github.com/sipeed/picoclaw/pkg/runtime"
"github.com/sipeed/picoclaw/pkg/security"
"github.com/sipeed/picoclaw/pkg/security/securebus"
"github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/voice"
) )
//go:generate cp -r ../../workspace . //go:generate cp -r ../../workspace .
@ -80,7 +80,7 @@ func formatBuildInfo() (build string, goVer string) {
} }
func printVersion() { func printVersion() {
fmt.Printf("%s picoclaw %s\n", logo, formatVersion()) fmt.Printf("%s dragonscale %s\n", logo, formatVersion())
build, goVer := formatBuildInfo() build, goVer := formatBuildInfo()
if build != "" { if build != "" {
fmt.Printf(" Build: %s\n", build) fmt.Printf(" Build: %s\n", build)
@ -159,7 +159,7 @@ func main() {
installer := skills.NewSkillInstaller(skillsDir) installer := skills.NewSkillInstaller(skillsDir)
cfgDir, _ := config.ConfigDir() cfgDir, _ := config.ConfigDir()
globalSkillsDir := filepath.Join(cfgDir, "skills") globalSkillsDir := filepath.Join(cfgDir, "skills")
builtinSkillsDir := filepath.Join(cfgDir, "picoclaw", "skills") builtinSkillsDir := filepath.Join(cfgDir, "dragonscale", "skills")
skillsLoader := skills.NewSkillsLoader(skillsDir, globalSkillsDir, builtinSkillsDir) skillsLoader := skills.NewSkillsLoader(skillsDir, globalSkillsDir, builtinSkillsDir)
switch subcommand { switch subcommand {
@ -169,7 +169,7 @@ func main() {
skillsInstallCmd(installer) skillsInstallCmd(installer)
case "remove", "uninstall": case "remove", "uninstall":
if len(os.Args) < 4 { if len(os.Args) < 4 {
fmt.Println("Usage: picoclaw skills remove <skill-name>") fmt.Println("Usage: dragonscale skills remove <skill-name>")
return return
} }
skillsRemoveCmd(installer, os.Args[3]) skillsRemoveCmd(installer, os.Args[3])
@ -181,7 +181,7 @@ func main() {
skillsSearchCmd(installer) skillsSearchCmd(installer)
case "show": case "show":
if len(os.Args) < 4 { if len(os.Args) < 4 {
fmt.Println("Usage: picoclaw skills show <skill-name>") fmt.Println("Usage: dragonscale skills show <skill-name>")
return return
} }
skillsShowCmd(skillsLoader, os.Args[3]) skillsShowCmd(skillsLoader, os.Args[3])
@ -205,20 +205,20 @@ func main() {
} }
func printHelp() { func printHelp() {
fmt.Printf("%s picoclaw - Personal AI Assistant v%s\n\n", logo, version) fmt.Printf("%s dragonscale - Personal AI Assistant v%s\n\n", logo, version)
fmt.Println("Usage: picoclaw <command>") fmt.Println("Usage: dragonscale <command>")
fmt.Println() fmt.Println()
fmt.Println("Commands:") fmt.Println("Commands:")
fmt.Println(" onboard Initialize picoclaw configuration and workspace") fmt.Println(" onboard Initialize dragonscale configuration and workspace")
fmt.Println(" agent Interact with the agent directly") fmt.Println(" agent Interact with the agent directly")
fmt.Println(" auth Manage authentication (login, logout, status)") fmt.Println(" auth Manage authentication (login, logout, status)")
fmt.Println(" gateway Start picoclaw gateway") fmt.Println(" gateway Start dragonscale gateway")
fmt.Println(" status Show picoclaw status") fmt.Println(" status Show dragonscale status")
fmt.Println(" cron Manage scheduled tasks") fmt.Println(" cron Manage scheduled tasks")
fmt.Println(" migrate Migrate from OpenClaw to PicoClaw") fmt.Println(" migrate Migrate from OpenClaw to DragonScale")
fmt.Println(" memory Memory system management (db status, session migration)") fmt.Println(" memory Memory system management (db status, session migration)")
fmt.Println(" secret Manage secrets (add, list, delete)") fmt.Println(" secret Manage secrets (add, list, delete)")
fmt.Println(" daemon Manage the picoclaw daemon (start, stop, status)") fmt.Println(" daemon Manage the dragonscale daemon (start, stop, status)")
fmt.Println(" skills Manage skills (install, list, remove)") fmt.Println(" skills Manage skills (install, list, remove)")
fmt.Println(" version Show version information") fmt.Println(" version Show version information")
} }
@ -249,7 +249,7 @@ func onboard() {
createWorkspaceTemplates(cfg) createWorkspaceTemplates(cfg)
fmt.Printf("%s picoclaw is ready!\n", logo) fmt.Printf("%s dragonscale is ready!\n", logo)
fmt.Print("\nSet up encrypted secret storage? (y/n): ") fmt.Print("\nSet up encrypted secret storage? (y/n): ")
var secretResponse string var secretResponse string
@ -264,20 +264,20 @@ func onboard() {
fmt.Println(" " + encoded) fmt.Println(" " + encoded)
fmt.Println() fmt.Println()
fmt.Println("Add to your shell profile:") fmt.Println("Add to your shell profile:")
fmt.Println(" export PICOCLAW_MASTER_KEY=" + encoded) fmt.Println(" export DRAGONSCALE_MASTER_KEY=" + encoded)
fmt.Println() fmt.Println()
fmt.Println("Then store secrets with: picoclaw secret add <name>") fmt.Println("Then store secrets with: dragonscale secret add <name>")
} }
} }
fmt.Println("\nNext steps:") fmt.Println("\nNext steps:")
fmt.Println(" 1. Add your API key to", configPath) fmt.Println(" 1. Add your API key to", configPath)
fmt.Println(" Get one at: https://openrouter.ai/keys") fmt.Println(" Get one at: https://openrouter.ai/keys")
fmt.Println(" 2. Chat: picoclaw agent -m \"Hello!\"") fmt.Println(" 2. Chat: dragonscale agent -m \"Hello!\"")
} }
// seedEmbeddedIdentity copies identity template files from the embedded FS // seedEmbeddedIdentity copies identity template files from the embedded FS
// into the XDG identity directory ($XDG_CONFIG_HOME/picoclaw/identity/). // into the XDG identity directory ($XDG_CONFIG_HOME/dragonscale/identity/).
func seedEmbeddedIdentity(identityDir string) error { func seedEmbeddedIdentity(identityDir string) error {
identityFiles := []string{"AGENT.md", "IDENTITY.md", "SOUL.md", "USER.md"} identityFiles := []string{"AGENT.md", "IDENTITY.md", "SOUL.md", "USER.md"}
for _, name := range identityFiles { for _, name := range identityFiles {
@ -297,7 +297,7 @@ func seedEmbeddedIdentity(identityDir string) error {
} }
// seedEmbeddedSkills copies skill templates from the embedded FS into the // seedEmbeddedSkills copies skill templates from the embedded FS into the
// XDG skills directory ($XDG_DATA_HOME/picoclaw/skills/). // XDG skills directory ($XDG_DATA_HOME/dragonscale/skills/).
func seedEmbeddedSkills(skillsDir string) error { func seedEmbeddedSkills(skillsDir string) error {
return fs.WalkDir(embeddedFiles, "workspace/skills", func(path string, d fs.DirEntry, err error) error { return fs.WalkDir(embeddedFiles, "workspace/skills", func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() { if err != nil || d.IsDir() {
@ -366,7 +366,7 @@ func migrateCmd() {
opts.OpenClawHome = args[i+1] opts.OpenClawHome = args[i+1]
i++ i++
} }
case "--picoclaw-home": case "--dragonscale-home":
if i+1 < len(args) { if i+1 < len(args) {
opts.PicoClawHome = args[i+1] opts.PicoClawHome = args[i+1]
i++ i++
@ -390,9 +390,9 @@ func migrateCmd() {
} }
func migrateHelp() { func migrateHelp() {
fmt.Println("\nMigrate from OpenClaw to PicoClaw") fmt.Println("\nMigrate from OpenClaw to DragonScale")
fmt.Println() fmt.Println()
fmt.Println("Usage: picoclaw migrate [options]") fmt.Println("Usage: dragonscale migrate [options]")
fmt.Println() fmt.Println()
fmt.Println("Options:") fmt.Println("Options:")
fmt.Println(" --dry-run Show what would be migrated without making changes") fmt.Println(" --dry-run Show what would be migrated without making changes")
@ -401,13 +401,13 @@ func migrateHelp() {
fmt.Println(" --workspace-only Only migrate workspace files, skip config") fmt.Println(" --workspace-only Only migrate workspace files, skip config")
fmt.Println(" --force Skip confirmation prompts") fmt.Println(" --force Skip confirmation prompts")
fmt.Println(" --openclaw-home Override OpenClaw home directory (default: ~/.openclaw)") fmt.Println(" --openclaw-home Override OpenClaw home directory (default: ~/.openclaw)")
fmt.Println(" --picoclaw-home Override PicoClaw home directory (default: ~/.picoclaw)") fmt.Println(" --dragonscale-home Override DragonScale home directory (default: ~/.dragonscale)")
fmt.Println() fmt.Println()
fmt.Println("Examples:") fmt.Println("Examples:")
fmt.Println(" picoclaw migrate Detect and migrate from OpenClaw") fmt.Println(" dragonscale migrate Detect and migrate from OpenClaw")
fmt.Println(" picoclaw migrate --dry-run Show what would be migrated") fmt.Println(" dragonscale migrate --dry-run Show what would be migrated")
fmt.Println(" picoclaw migrate --refresh Re-sync workspace files") fmt.Println(" dragonscale migrate --refresh Re-sync workspace files")
fmt.Println(" picoclaw migrate --force Migrate without confirmation") fmt.Println(" dragonscale migrate --force Migrate without confirmation")
} }
func agentCmd() { func agentCmd() {
@ -479,7 +479,7 @@ func interactiveMode(ctx context.Context, agentLoop *agent.AgentLoop, sessionKey
rl, err := readline.NewEx(&readline.Config{ rl, err := readline.NewEx(&readline.Config{
Prompt: prompt, Prompt: prompt,
HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"), HistoryFile: filepath.Join(os.TempDir(), ".dragonscale_history"),
HistoryLimit: 100, HistoryLimit: 100,
InterruptPrompt: "^C", InterruptPrompt: "^C",
EOFPrompt: "exit", EOFPrompt: "exit",
@ -611,14 +611,19 @@ func gatewayCmd() {
execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute
var cronOpts []cron.CronOption var cronOpts []cron.CronOption
if del := agentLoop.MemoryDelegate(); del != nil { if del := agentLoop.MemoryDelegate(); del != nil {
cronOpts = append(cronOpts, cron.WithCronDelegate(del, "picoclaw")) cronOpts = append(cronOpts, cron.WithCronDelegate(del, "dragonscale"))
} }
cronService := setupCronTool(appCtx, agentLoop, msgBus, cfg.SandboxPath(), cfg.RestrictToSandbox(), execTimeout, cronOpts...) cronService := setupCronTool(appCtx, agentLoop, msgBus, cfg.SandboxPath(), cfg.RestrictToSandbox(), execTimeout, cronOpts...)
var heartbeatStateOpts []state.Option
if del := agentLoop.MemoryDelegate(); del != nil {
heartbeatStateOpts = append(heartbeatStateOpts, state.WithDelegate(del))
}
heartbeatService := heartbeat.NewHeartbeatService( heartbeatService := heartbeat.NewHeartbeatService(
cfg.SandboxPath(), cfg.SandboxPath(),
cfg.Heartbeat.Interval, cfg.Heartbeat.Interval,
cfg.Heartbeat.Enabled, cfg.Heartbeat.Enabled,
heartbeatStateOpts...,
) )
heartbeatService.SetBus(msgBus) heartbeatService.SetBus(msgBus)
heartbeatService.SetHandler(func(prompt, channel, chatID string) *tools.ToolResult { heartbeatService.SetHandler(func(prompt, channel, chatID string) *tools.ToolResult {
@ -638,6 +643,40 @@ func gatewayCmd() {
// sent to user via processSystemMessage when the async task completes // sent to user via processSystemMessage when the async task completes
return tools.SilentResult(response) return tools.SilentResult(response)
}) })
if del := agentLoop.MemoryDelegate(); del != nil {
obligations := tools.NewObligationTool(del, "dragonscale")
heartbeatService.SetDueContextProvider(func(now time.Time) (string, error) {
ctx, cancel := context.WithTimeout(appCtx, 10*time.Second)
defer cancel()
due, err := obligations.CollectDueObligations(ctx, now, "heartbeat")
if err != nil {
return "", err
}
if len(due) == 0 {
return "", nil
}
var b strings.Builder
b.WriteString("The following obligations are currently due and require action:\n")
for _, rec := range due {
dueAt := rec.DueAt
if dueAt.IsZero() {
dueAt = rec.ScheduledAt
}
dueLabel := "unspecified"
if !dueAt.IsZero() {
dueLabel = dueAt.Format(time.RFC3339)
}
b.WriteString(fmt.Sprintf("- [%s] %s (state=%s, due_at=%s)\n", rec.ID, rec.Title, rec.State, dueLabel))
if strings.TrimSpace(rec.Details) != "" {
b.WriteString(fmt.Sprintf(" details: %s\n", strings.TrimSpace(rec.Details)))
}
}
b.WriteString("For each due obligation, complete the action, then call obligation update_state with executed and obligation add_evidence describing what was done.\n")
return b.String(), nil
})
}
channelManager, err := channels.NewManager(cfg, msgBus) channelManager, err := channels.NewManager(cfg, msgBus)
if err != nil { if err != nil {
@ -762,7 +801,7 @@ func memoryCmd() {
func memoryHelp() { func memoryHelp() {
fmt.Println("\nMemory system management") fmt.Println("\nMemory system management")
fmt.Println() fmt.Println()
fmt.Println("Usage: picoclaw memory <subcommand>") fmt.Println("Usage: dragonscale memory <subcommand>")
fmt.Println() fmt.Println()
fmt.Println("Subcommands:") fmt.Println("Subcommands:")
fmt.Println(" migrate-sessions Import file-based sessions into recall memory") fmt.Println(" migrate-sessions Import file-based sessions into recall memory")
@ -793,7 +832,7 @@ func memoryMigrateSessions() {
} }
sessionsDir := filepath.Join(cfg.SandboxPath(), "sessions") sessionsDir := filepath.Join(cfg.SandboxPath(), "sessions")
stats, err := picomemory.MigrateFileSessions(ctx, del, "picoclaw", sessionsDir) stats, err := picomemory.MigrateFileSessions(ctx, del, "dragonscale", sessionsDir)
if err != nil { if err != nil {
fmt.Printf("Migration error: %v\n", err) fmt.Printf("Migration error: %v\n", err)
os.Exit(1) os.Exit(1)
@ -841,7 +880,7 @@ func statusCmd() {
configPath := getConfigPath() configPath := getConfigPath()
fmt.Printf("%s picoclaw Status\n", logo) fmt.Printf("%s dragonscale Status\n", logo)
fmt.Printf("Version: %s\n", formatVersion()) fmt.Printf("Version: %s\n", formatVersion())
build, _ := formatBuildInfo() build, _ := formatBuildInfo()
if build != "" { if build != "" {
@ -924,7 +963,7 @@ func statusCmd() {
} }
memDBPath := cfg.Memory.DBPath memDBPath := cfg.Memory.DBPath
if memDBPath == "" { if memDBPath == "" {
memDBPath = filepath.Join(cfg.WorkspacePath(), "memory", "picoclaw.db") memDBPath = filepath.Join(cfg.WorkspacePath(), "memory", "dragonscale.db")
} }
if fi, err := os.Stat(memDBPath); err == nil { if fi, err := os.Stat(memDBPath); err == nil {
fmt.Printf(" DB size: %.1f KB\n", float64(fi.Size())/1024) fmt.Printf(" DB size: %.1f KB\n", float64(fi.Size())/1024)
@ -962,11 +1001,11 @@ func authHelp() {
fmt.Println(" --device-code Use device code flow (for headless environments)") fmt.Println(" --device-code Use device code flow (for headless environments)")
fmt.Println() fmt.Println()
fmt.Println("Examples:") fmt.Println("Examples:")
fmt.Println(" picoclaw auth login --provider openai") fmt.Println(" dragonscale auth login --provider openai")
fmt.Println(" picoclaw auth login --provider openai --device-code") fmt.Println(" dragonscale auth login --provider openai --device-code")
fmt.Println(" picoclaw auth login --provider anthropic") fmt.Println(" dragonscale auth login --provider anthropic")
fmt.Println(" picoclaw auth logout --provider openai") fmt.Println(" dragonscale auth logout --provider openai")
fmt.Println(" picoclaw auth status") fmt.Println(" dragonscale auth status")
} }
func authLoginCmd() { func authLoginCmd() {
@ -1125,7 +1164,7 @@ func authStatusCmd() {
if len(store.Credentials) == 0 { if len(store.Credentials) == 0 {
fmt.Println("No authenticated providers.") fmt.Println("No authenticated providers.")
fmt.Println("Run: picoclaw auth login --provider <name>") fmt.Println("Run: dragonscale auth login --provider <name>")
return return
} }
@ -1178,7 +1217,7 @@ func secretCmd() {
func secretHelp() { func secretHelp() {
fmt.Println("\nSecret management (encrypted at rest)") fmt.Println("\nSecret management (encrypted at rest)")
fmt.Println() fmt.Println()
fmt.Println("Usage: picoclaw secret <subcommand>") fmt.Println("Usage: dragonscale secret <subcommand>")
fmt.Println() fmt.Println()
fmt.Println("Subcommands:") fmt.Println("Subcommands:")
fmt.Println(" init Generate a master key (stored in keyring or env)") fmt.Println(" init Generate a master key (stored in keyring or env)")
@ -1187,24 +1226,24 @@ func secretHelp() {
fmt.Println(" delete <name> Remove a secret") fmt.Println(" delete <name> Remove a secret")
fmt.Println() fmt.Println()
fmt.Println("Environment:") fmt.Println("Environment:")
fmt.Println(" PICOCLAW_MASTER_KEY 32-byte key (hex or base64) for encryption") fmt.Println(" DRAGONSCALE_MASTER_KEY 32-byte key (hex or base64) for encryption")
fmt.Println() fmt.Println()
fmt.Println("Examples:") fmt.Println("Examples:")
fmt.Println(" picoclaw secret init") fmt.Println(" dragonscale secret init")
fmt.Println(" picoclaw secret add github_token") fmt.Println(" dragonscale secret add github_token")
fmt.Println(" picoclaw secret list") fmt.Println(" dragonscale secret list")
fmt.Println(" picoclaw secret delete github_token") fmt.Println(" dragonscale secret delete github_token")
} }
func secretStorePath() string { func secretStorePath() string {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "secrets.json") return filepath.Join(home, ".dragonscale", "secrets.json")
} }
func loadSecretStore() (*security.SecretStore, error) { func loadSecretStore() (*security.SecretStore, error) {
var keyring security.KeyringProvider var keyring security.KeyringProvider
if mk := os.Getenv("PICOCLAW_MASTER_KEY"); mk != "" { if mk := os.Getenv("DRAGONSCALE_MASTER_KEY"); mk != "" {
keyring = security.NewEnvKeyring("PICOCLAW_MASTER_KEY") keyring = security.NewEnvKeyring("DRAGONSCALE_MASTER_KEY")
} else { } else {
keyring = security.NewNoopKeyring(nil) keyring = security.NewNoopKeyring(nil)
} }
@ -1224,14 +1263,14 @@ func secretInit() {
fmt.Println(" " + encoded) fmt.Println(" " + encoded)
fmt.Println() fmt.Println()
fmt.Println("Set it as an environment variable:") fmt.Println("Set it as an environment variable:")
fmt.Println(" export PICOCLAW_MASTER_KEY=" + encoded) fmt.Println(" export DRAGONSCALE_MASTER_KEY=" + encoded)
fmt.Println() fmt.Println()
fmt.Println("Or add to your shell profile (~/.bashrc, ~/.zshrc).") fmt.Println("Or add to your shell profile (~/.bashrc, ~/.zshrc).")
} }
func secretAdd() { func secretAdd() {
if len(os.Args) < 4 { if len(os.Args) < 4 {
fmt.Println("Usage: picoclaw secret add <name>") fmt.Println("Usage: dragonscale secret add <name>")
return return
} }
name := os.Args[3] name := os.Args[3]
@ -1274,7 +1313,7 @@ func secretList() {
names := ss.List() names := ss.List()
if len(names) == 0 { if len(names) == 0 {
fmt.Println("No secrets stored.") fmt.Println("No secrets stored.")
fmt.Println("Add one with: picoclaw secret add <name>") fmt.Println("Add one with: dragonscale secret add <name>")
return return
} }
@ -1286,7 +1325,7 @@ func secretList() {
func secretDelete() { func secretDelete() {
if len(os.Args) < 4 { if len(os.Args) < 4 {
fmt.Println("Usage: picoclaw secret delete <name>") fmt.Println("Usage: dragonscale secret delete <name>")
return return
} }
name := os.Args[3] name := os.Args[3]
@ -1312,12 +1351,12 @@ func secretDelete() {
func daemonSocketPath() string { func daemonSocketPath() string {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "daemon.sock") return filepath.Join(home, ".dragonscale", "daemon.sock")
} }
func daemonPIDPath() string { func daemonPIDPath() string {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "daemon.pid") return filepath.Join(home, ".dragonscale", "daemon.pid")
} }
func daemonCmd() { func daemonCmd() {
@ -1345,14 +1384,14 @@ func daemonCmd() {
func daemonHelp() { func daemonHelp() {
fmt.Println("\nDaemon mode (Unix socket transport)") fmt.Println("\nDaemon mode (Unix socket transport)")
fmt.Println() fmt.Println()
fmt.Println("Usage: picoclaw daemon <subcommand>") fmt.Println("Usage: dragonscale daemon <subcommand>")
fmt.Println() fmt.Println()
fmt.Println("Subcommands:") fmt.Println("Subcommands:")
fmt.Println(" start Start the daemon (foreground)") fmt.Println(" start Start the daemon (foreground)")
fmt.Println(" stop Stop a running daemon") fmt.Println(" stop Stop a running daemon")
fmt.Println(" status Check daemon status") fmt.Println(" status Check daemon status")
fmt.Println() fmt.Println()
fmt.Println("The daemon listens on ~/.picoclaw/daemon.sock and provides") fmt.Println("The daemon listens on ~/.dragonscale/daemon.sock and provides")
fmt.Println("tool execution services via the SecureBus.") fmt.Println("tool execution services via the SecureBus.")
} }
@ -1376,7 +1415,7 @@ func daemonStart() {
pid := os.Getpid() pid := os.Getpid()
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
_ = os.MkdirAll(filepath.Join(home, ".picoclaw"), 0700) _ = os.MkdirAll(filepath.Join(home, ".dragonscale"), 0700)
_ = os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", pid)), 0600) _ = os.WriteFile(pidPath, []byte(fmt.Sprintf("%d", pid)), 0600)
defer os.Remove(pidPath) defer os.Remove(pidPath)
@ -1416,7 +1455,7 @@ func daemonStart() {
secureBus := securebus.New(busCfg, ss, capLookup, executor) secureBus := securebus.New(busCfg, ss, capLookup, executor)
defer secureBus.Close() defer secureBus.Close()
fmt.Printf("picoclaw daemon started (pid=%d, socket=%s)\n", pid, sockPath) fmt.Printf("dragonscale daemon started (pid=%d, socket=%s)\n", pid, sockPath)
fmt.Printf(" sandbox: %s\n", sandbox) fmt.Printf(" sandbox: %s\n", sandbox)
fmt.Printf(" tools: %d registered\n", len(registry.List())) fmt.Printf(" tools: %d registered\n", len(registry.List()))
fmt.Println("Press Ctrl+C to stop.") fmt.Println("Press Ctrl+C to stop.")
@ -1510,9 +1549,9 @@ func getConfigPath() string {
if p, err := config.DefaultConfigPath(); err == nil { if p, err := config.DefaultConfigPath(); err == nil {
return p return p
} }
// Fallback: legacy ~/.picoclaw/config.json for systems where XDG resolution fails. // Fallback: legacy ~/.dragonscale/config.json for systems where XDG resolution fails.
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "config.json") return filepath.Join(home, ".dragonscale", "config.json")
} }
func setupCronTool(appCtx context.Context, agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, cronOpts ...cron.CronOption) *cron.CronService { func setupCronTool(appCtx context.Context, agentLoop *agent.AgentLoop, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, cronOpts ...cron.CronOption) *cron.CronService {
@ -1546,7 +1585,7 @@ func bootstrapAgentRuntime(appCtx context.Context, cfg *config.Config) (*picorun
} }
// setupSecureBus wires the Isolated Tool Runtime into an AgentLoop. // setupSecureBus wires the Isolated Tool Runtime into an AgentLoop.
// It loads (or lazily creates) the SecretStore from the picoclaw home directory // It loads (or lazily creates) the SecretStore from the dragonscale home directory
// and calls agentLoop.SetupSecureBus so all tool calls are routed through // and calls agentLoop.SetupSecureBus so all tool calls are routed through
// capability enforcement, secret injection, leak scanning, and audit logging. // capability enforcement, secret injection, leak scanning, and audit logging.
// //
@ -1556,24 +1595,20 @@ func bootstrapAgentRuntime(appCtx context.Context, cfg *config.Config) (*picorun
func setupSecureBus(agentLoop *agent.AgentLoop) (closer func()) { func setupSecureBus(agentLoop *agent.AgentLoop) (closer func()) {
cfgDir, err := config.ConfigDir() cfgDir, err := config.ConfigDir()
if err != nil { if err != nil {
// Fallback to ~/.picoclaw if XDG resolution fails. // Fallback to ~/.dragonscale if XDG resolution fails.
home, herr := os.UserHomeDir() home, herr := os.UserHomeDir()
if herr != nil { if herr != nil {
logger.WarnC("itr", "SecureBus: cannot determine config dir — running without ITR") logger.WarnC("itr", "SecureBus: cannot determine config dir — running without ITR")
return func() {} return func() {}
} }
cfgDir = filepath.Join(home, ".picoclaw") cfgDir = filepath.Join(home, ".dragonscale")
} }
secretsPath := filepath.Join(cfgDir, "secrets.json") secretsPath := filepath.Join(cfgDir, "secrets.json")
// Use NoopKeyring by default; EnvKeyring when PICOCLAW_MASTER_KEY is set. // Always use EnvKeyring in runtime wiring. We intentionally avoid
var keyring security.KeyringProvider // in-memory fallback keyrings in production execution paths.
if mk := os.Getenv("PICOCLAW_MASTER_KEY"); mk != "" { keyring := security.NewEnvKeyring(security.MasterKeyEnvVar)
keyring = security.NewEnvKeyring("PICOCLAW_MASTER_KEY")
} else {
keyring = security.NewNoopKeyring(nil)
}
ss, err := security.NewSecretStore(secretsPath, keyring) ss, err := security.NewSecretStore(secretsPath, keyring)
if err != nil { if err != nil {
@ -1581,6 +1616,10 @@ func setupSecureBus(agentLoop *agent.AgentLoop) (closer func()) {
map[string]interface{}{"error": err.Error()}) map[string]interface{}{"error": err.Error()})
ss = nil ss = nil
} }
if os.Getenv(security.MasterKeyEnvVar) == "" {
logger.WarnCF("itr", "SecureBus: master key env var is not set; secret injection requiring stored secrets will fail",
map[string]interface{}{"env_var": security.MasterKeyEnvVar})
}
bus := agentLoop.SetupSecureBus(ss, securebus.DefaultBusConfig()) bus := agentLoop.SetupSecureBus(ss, securebus.DefaultBusConfig())
logger.InfoC("itr", "SecureBus enabled — tool calls routed through ITR") logger.InfoC("itr", "SecureBus enabled — tool calls routed through ITR")
@ -1611,7 +1650,7 @@ func cronCmd() {
cronAddCmd(cronStorePath) cronAddCmd(cronStorePath)
case "remove": case "remove":
if len(os.Args) < 4 { if len(os.Args) < 4 {
fmt.Println("Usage: picoclaw cron remove <job_id>") fmt.Println("Usage: dragonscale cron remove <job_id>")
return return
} }
cronRemoveCmd(cronStorePath, os.Args[3]) cronRemoveCmd(cronStorePath, os.Args[3])
@ -1781,7 +1820,7 @@ func cronRemoveCmd(storePath, jobID string) {
func cronEnableCmd(storePath string, disable bool) { func cronEnableCmd(storePath string, disable bool) {
if len(os.Args) < 4 { if len(os.Args) < 4 {
fmt.Println("Usage: picoclaw cron enable/disable <job_id>") fmt.Println("Usage: dragonscale cron enable/disable <job_id>")
return return
} }
@ -1812,11 +1851,11 @@ func skillsHelp() {
fmt.Println(" show <name> Show skill details") fmt.Println(" show <name> Show skill details")
fmt.Println() fmt.Println()
fmt.Println("Examples:") fmt.Println("Examples:")
fmt.Println(" picoclaw skills list") fmt.Println(" dragonscale skills list")
fmt.Println(" picoclaw skills install sipeed/picoclaw-skills/weather") fmt.Println(" dragonscale skills install sipeed/dragonscale-skills/weather")
fmt.Println(" picoclaw skills install-builtin") fmt.Println(" dragonscale skills install-builtin")
fmt.Println(" picoclaw skills list-builtin") fmt.Println(" dragonscale skills list-builtin")
fmt.Println(" picoclaw skills remove weather") fmt.Println(" dragonscale skills remove weather")
} }
func skillsListCmd(loader *skills.SkillsLoader) { func skillsListCmd(loader *skills.SkillsLoader) {
@ -1839,8 +1878,8 @@ func skillsListCmd(loader *skills.SkillsLoader) {
func skillsInstallCmd(installer *skills.SkillInstaller) { func skillsInstallCmd(installer *skills.SkillInstaller) {
if len(os.Args) < 4 { if len(os.Args) < 4 {
fmt.Println("Usage: picoclaw skills install <github-repo>") fmt.Println("Usage: dragonscale skills install <github-repo>")
fmt.Println("Example: picoclaw skills install sipeed/picoclaw-skills/weather") fmt.Println("Example: dragonscale skills install sipeed/dragonscale-skills/weather")
return return
} }
@ -1870,7 +1909,7 @@ func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) {
} }
func skillsInstallBuiltinCmd(workspace string) { func skillsInstallBuiltinCmd(workspace string) {
builtinSkillsDir := "./picoclaw/skills" builtinSkillsDir := "./dragonscale/skills"
workspaceSkillsDir := filepath.Join(workspace, "skills") workspaceSkillsDir := filepath.Join(workspace, "skills")
fmt.Printf("Copying builtin skills to workspace...\n") fmt.Printf("Copying builtin skills to workspace...\n")

View file

@ -1,7 +1,7 @@
{ {
"agents": { "agents": {
"defaults": { "defaults": {
"workspace": "~/.picoclaw/workspace", "workspace": "~/.dragonscale/workspace",
"restrict_to_workspace": true, "restrict_to_workspace": true,
"model": "glm-4.7", "model": "glm-4.7",
"max_tokens": 8192, "max_tokens": 8192,

View file

@ -1,40 +1,40 @@
services: services:
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# PicoClaw Agent (one-shot query) # DragonScale Agent (one-shot query)
# docker compose run --rm picoclaw-agent -m "Hello" # docker compose run --rm dragonscale-agent -m "Hello"
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
picoclaw-agent: dragonscale-agent:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: picoclaw-agent container_name: dragonscale-agent
profiles: profiles:
- agent - agent
volumes: volumes:
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro - ./config/config.json:/home/dragonscale/.dragonscale/config.json:ro
- picoclaw-workspace:/home/picoclaw/.picoclaw/workspace - dragonscale-workspace:/home/dragonscale/.dragonscale/workspace
entrypoint: ["picoclaw", "agent"] entrypoint: ["dragonscale", "agent"]
stdin_open: true stdin_open: true
tty: true tty: true
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# PicoClaw Gateway (Long-running Bot) # DragonScale Gateway (Long-running Bot)
# docker compose up picoclaw-gateway # docker compose up dragonscale-gateway
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
picoclaw-gateway: dragonscale-gateway:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
container_name: picoclaw-gateway container_name: dragonscale-gateway
restart: unless-stopped restart: unless-stopped
profiles: profiles:
- gateway - gateway
volumes: volumes:
# Configuration file # Configuration file
- ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro - ./config/config.json:/home/dragonscale/.dragonscale/config.json:ro
# Persistent workspace (sessions, memory, logs) # Persistent workspace (sessions, memory, logs)
- picoclaw-workspace:/home/picoclaw/.picoclaw/workspace - dragonscale-workspace:/home/dragonscale/.dragonscale/workspace
command: ["gateway"] command: ["gateway"]
volumes: volumes:
picoclaw-workspace: dragonscale-workspace:

View file

@ -8,7 +8,7 @@
## Context ## Context
PicoClaw tools execute in-process with the agent loop. The `Vault` (XChaCha20-Poly1305) exists for encrypting secrets at rest, but there is no pipeline for injecting those secrets into tool execution. The `Redactor` scans for sensitive patterns, but only in log paths — not on tool output before it reaches the LLM. There is no privilege boundary DragonScale tools execute in-process with the agent loop. The `Vault` (XChaCha20-Poly1305) exists for encrypting secrets at rest, but there is no pipeline for injecting those secrets into tool execution. The `Redactor` scans for sensitive patterns, but only in log paths — not on tool output before it reaches the LLM. There is no privilege boundary
between the LLM-facing agent code and the tool execution path. between the LLM-facing agent code and the tool execution path.
A compromised tool — via prompt injection, malicious skill, or supply chain attack — has the same memory-space access as the agent itself. This is the same class of vulnerability that led to the OpenClaw token exfiltration incident (Feb 2026), where malicious skills on ClawHub could read API keys from the host environment and exfiltrate them through tool output. A compromised tool — via prompt injection, malicious skill, or supply chain attack — has the same memory-space access as the agent itself. This is the same class of vulnerability that led to the OpenClaw token exfiltration incident (Feb 2026), where malicious skills on ClawHub could read API keys from the host environment and exfiltrate them through tool output.
@ -18,7 +18,7 @@ Competing frameworks have responded:
- **IronClaw** (NEAR AI, Rust): WASM container isolation, capability-based permissions, encrypted credential vault with runtime injection, network interception for leak detection. - **IronClaw** (NEAR AI, Rust): WASM container isolation, capability-based permissions, encrypted credential vault with runtime injection, network interception for leak detection.
- **WyrmLock** (ZanzyTHEbar, Rust): ZKP-based application locking via proc connector, cryptographic authentication before process execution, system keyring integration. - **WyrmLock** (ZanzyTHEbar, Rust): ZKP-based application locking via proc connector, cryptographic authentication before process execution, system keyring integration.
PicoClaw's constraint is unique: the binary must remain under 20MB and run on 64MB RAM embedded boards. A full WASM runtime or separate daemon process is not viable as a mandatory dependency. The solution must be **progressive** — lightweight in-process enforcement by default, with optional heavier isolation for richer platforms. DragonScale's constraint is unique: the binary must remain under 20MB and run on 64MB RAM embedded boards. A full WASM runtime or separate daemon process is not viable as a mandatory dependency. The solution must be **progressive** — lightweight in-process enforcement by default, with optional heavier isolation for richer platforms.
**RLM convergence**: Independently, Recursive Language Models (Zhang & Khattab, MIT CSAIL, Oct 2025; arXiv:2512.24601 v2) demonstrate that long-context systems suffer from "context rot" — performance degrades as context length grows, not from technical limits but from information overload and distributional mismatch. RLM's solution is **context-as-variable** + **recursive decomposition**: the LM never sees the full context directly. Instead, it interacts with context through structured operations (peek, grep, partition, recurse) in a REPL-like environment, spawning recursive sub-calls over partitioned subsets. **RLM convergence**: Independently, Recursive Language Models (Zhang & Khattab, MIT CSAIL, Oct 2025; arXiv:2512.24601 v2) demonstrate that long-context systems suffer from "context rot" — performance degrades as context length grows, not from technical limits but from information overload and distributional mismatch. RLM's solution is **context-as-variable** + **recursive decomposition**: the LM never sees the full context directly. Instead, it interacts with context through structured operations (peek, grep, partition, recurse) in a REPL-like environment, spawning recursive sub-calls over partitioned subsets.
@ -37,7 +37,7 @@ The ITR's SecureBus is therefore not just a security layer — it is the RLM exe
3. **RLM** (already described above): Unbounded context via recursive decomposition. 3. **RLM** (already described above): Unbounded context via recursive decomposition.
These three systems compose naturally: **the LLMCompiler DAG planner produces the execution plan, the SecureBus DAG executor dispatches nodes in parallel with PTC-style context isolation, and the RLM engine activates when any node's context exceeds the LM's window**. PicoClaw's current agent loop (`RunToolLoop`) uses Fantasy's ReAct pattern — each tool call requires a full inference pass, and all intermediate results accumulate in context. The DAG executor replaces this sequential loop for complex multi-tool workflows while preserving ReAct for simple cases. These three systems compose naturally: **the LLMCompiler DAG planner produces the execution plan, the SecureBus DAG executor dispatches nodes in parallel with PTC-style context isolation, and the RLM engine activates when any node's context exceeds the LM's window**. DragonScale's current agent loop (`RunToolLoop`) uses Fantasy's ReAct pattern — each tool call requires a full inference pass, and all intermediate results accumulate in context. The DAG executor replaces this sequential loop for complex multi-tool workflows while preserving ReAct for simple cases.
**Anthropic Tool Search** (Nov 2025): Separately, Anthropic's Tool Search Tool demonstrates that loading all tool definitions upfront (55K+ tokens for a modest 5-server setup) is itself a form of context pollution. On-demand tool discovery reduces token consumption by 85% while improving accuracy (Opus 4: 49% → 74%). This maps directly to a `ToolSearch` command variant in our FlatBuffers schema, enabling the DAG planner to discover tools lazily rather than seeing all definitions. **Anthropic Tool Search** (Nov 2025): Separately, Anthropic's Tool Search Tool demonstrates that loading all tool definitions upfront (55K+ tokens for a modest 5-server setup) is itself a form of context pollution. On-demand tool discovery reduces token consumption by 85% while improving accuracy (Opus 4: 49% → 74%). This maps directly to a `ToolSearch` command variant in our FlatBuffers schema, enabling the DAG planner to discover tools lazily rather than seeing all definitions.
@ -142,7 +142,7 @@ type WasmTransport struct { /* wazero isolate, untrusted tools */ }
**Command protocol**: Tool requests use a structured, schema-enforced format rather than free-form JSON. FlatBuffers (google/flatbuffers, zero-copy serialization) defines the canonical `ToolRequest` schema, covering both traditional tool calls and RLM recursive operations: **Command protocol**: Tool requests use a structured, schema-enforced format rather than free-form JSON. FlatBuffers (google/flatbuffers, zero-copy serialization) defines the canonical `ToolRequest` schema, covering both traditional tool calls and RLM recursive operations:
```flatbuffers ```flatbuffers
namespace picoclaw.itr; namespace dragonscale.itr;
// --- Individual command types --- // --- Individual command types ---
table Peek { start: uint64; length: uint32; } table Peek { start: uint64; length: uint32; }
@ -199,7 +199,7 @@ This schema serves four purposes: (1) zero-copy reads eliminate serialization ov
### Layer 3: Secret Store + Keyring Integration ### Layer 3: Secret Store + Keyring Integration
The `SecretStore` maps logical secret names to encrypted ciphertext, persisted to a local file (`~/.picoclaw/secrets.enc`). The `Vault` handles encryption/decryption. The `SecretStore` maps logical secret names to encrypted ciphertext, persisted to a local file (`~/.dragonscale/secrets.enc`). The `Vault` handles encryption/decryption.
The master key for the `Vault` is sourced from one of three backends, selected at The master key for the `Vault` is sourced from one of three backends, selected at
onboarding: onboarding:
@ -215,11 +215,11 @@ Keyring support is gated behind a build tag (`!embedded`) to avoid pulling in CG
**CLI surface**: **CLI surface**:
``` ```
picoclaw secret add <name> # interactive prompt for value dragonscale secret add <name> # interactive prompt for value
picoclaw secret list # names only, no values dragonscale secret list # names only, no values
picoclaw secret delete <name> dragonscale secret delete <name>
picoclaw secret export # encrypted backup dragonscale secret export # encrypted backup
picoclaw secret import <file> # restore from backup dragonscale secret import <file> # restore from backup
``` ```
The `onboard` command is extended to include master key setup as part of the interactive wizard. The `onboard` command is extended to include master key setup as part of the interactive wizard.
@ -241,7 +241,7 @@ For non-embedded deployments (desktop, server), the SecureBus can optionally run
**Session establishment** uses a Schnorr-based zero-knowledge proof: **Session establishment** uses a Schnorr-based zero-knowledge proof:
1. Client connects to `~/.picoclaw/daemon.sock` 1. Client connects to `~/.dragonscale/daemon.sock`
2. Daemon sends a random 32-byte challenge `c` 2. Daemon sends a random 32-byte challenge `c`
3. Client computes commitment and response from passphrase-derived secret 3. Client computes commitment and response from passphrase-derived secret
4. Daemon verifies against stored verifier (passphrase never crosses the socket) 4. Daemon verifies against stored verifier (passphrase never crosses the socket)
@ -256,9 +256,9 @@ For non-embedded deployments (desktop, server), the SecureBus can optionally run
**Daemon lifecycle**: **Daemon lifecycle**:
``` ```
picoclaw daemon start # background, creates pidfile + socket dragonscale daemon start # background, creates pidfile + socket
picoclaw daemon stop # graceful shutdown, zeroes key material dragonscale daemon stop # graceful shutdown, zeroes key material
picoclaw daemon status # running/stopped, uptime, client count dragonscale daemon status # running/stopped, uptime, client count
``` ```
Systemd and launchd service templates are provided in `deploy/`. Systemd and launchd service templates are provided in `deploy/`.
@ -303,7 +303,7 @@ TinyGo produces smaller binaries (~100KB-1MB) suitable for embedded constraints.
### RLM Integration: Recursive Context Decomposition Engine ### RLM Integration: Recursive Context Decomposition Engine
The Isolated Tool Runtime subsumes and secures the RLM execution model. Rather than The Isolated Tool Runtime subsumes and secures the RLM execution model. Rather than
implementing RLM as a standalone system, PicoClaw treats RLM operations as first-class implementing RLM as a standalone system, DragonScale treats RLM operations as first-class
tool calls flowing through the SecureBus. This section describes the concrete tool calls flowing through the SecureBus. This section describes the concrete
integration. integration.
@ -392,7 +392,7 @@ When the budget is exhausted, remaining sub-calls are canceled via Go context
propagation and the best partial result is synthesized. propagation and the best partial result is synthesized.
**Integration with existing memory tiers**: RLM operates on the archival tier of **Integration with existing memory tiers**: RLM operates on the archival tier of
PicoClaw's 3-tier memory system. When the agent issues a `memory search` that returns DragonScale's 3-tier memory system. When the agent issues a `memory search` that returns
results exceeding the context window, the RLM engine activates automatically — treating results exceeding the context window, the RLM engine activates automatically — treating
the search results as the context variable and recursively decomposing them. This is the search results as the context variable and recursively decomposing them. This is
transparent to the agent: it issued a search, it gets a synthesized answer. transparent to the agent: it issued a search, it gets a synthesized answer.
@ -450,7 +450,7 @@ every DAG node is a `ToolRequest` that flows through capability checking, secret
injection, and leak scanning. The DAG is a batch of `ToolRequest`s with dependency injection, and leak scanning. The DAG is a batch of `ToolRequest`s with dependency
metadata. metadata.
**Why this replaces ReAct for complex workflows**: PicoClaw's current `RunToolLoop` **Why this replaces ReAct for complex workflows**: DragonScale's current `RunToolLoop`
uses Fantasy's ReAct pattern. Each tool call requires a full LLM inference pass uses Fantasy's ReAct pattern. Each tool call requires a full LLM inference pass
(hundreds of ms to seconds), and all intermediate results accumulate in the LLM's (hundreds of ms to seconds), and all intermediate results accumulate in the LLM's
context window. For a 20-tool workflow, that's 20 inference passes and potentially context window. For a 20-tool workflow, that's 20 inference passes and potentially
@ -643,7 +643,7 @@ A lightweight classifier (heuristic or cheap LM call) routes queries to the
appropriate executor. The `ToolLoopConfig` gains a `Mode` field: appropriate executor. The `ToolLoopConfig` gains a `Mode` field:
`ModeReAct | ModeDAG | ModeAuto`. `ModeReAct | ModeDAG | ModeAuto`.
**Relationship to PTC**: PicoClaw implements PTC's core principle — intermediate **Relationship to PTC**: DragonScale implements PTC's core principle — intermediate
results never pollute the LM's context — without depending on Anthropic's specific results never pollute the LM's context — without depending on Anthropic's specific
code execution sandbox. The DAG executor IS the sandbox: it holds all intermediate code execution sandbox. The DAG executor IS the sandbox: it holds all intermediate
node outputs in a `sync.Map`, resolves references internally, and only the Joiner's node outputs in a `sync.Map`, resolves references internally, and only the Joiner's
@ -717,10 +717,10 @@ Use Anthropic's Programmatic Tool Calling directly, where Claude writes Python
orchestration code that runs in Anthropic's sandboxed environment. orchestration code that runs in Anthropic's sandboxed environment.
**Rejected because**: PTC is provider-specific (requires Anthropic's **Rejected because**: PTC is provider-specific (requires Anthropic's
`code_execution_20250825` server tool). PicoClaw is LLM-agnostic — it works with `code_execution_20250825` server tool). DragonScale is LLM-agnostic — it works with
OpenAI, Anthropic, Ollama, and any Fantasy-compatible provider. The DAG executor OpenAI, Anthropic, Ollama, and any Fantasy-compatible provider. The DAG executor
implements PTC's core principle (intermediate results isolated from context) without implements PTC's core principle (intermediate results isolated from context) without
provider lock-in. Additionally, PTC's Python sandbox cannot enforce PicoClaw's provider lock-in. Additionally, PTC's Python sandbox cannot enforce DragonScale's
capability manifests or leak scanning — our SecureBus provides stronger guarantees. capability manifests or leak scanning — our SecureBus provides stronger guarantees.
### G. Pure LLMCompiler without RLM integration ### G. Pure LLMCompiler without RLM integration
@ -772,7 +772,7 @@ complementary, not alternatives.
context window. The DAG executor's `sync.Map` holds all node outputs internally, context window. The DAG executor's `sync.Map` holds all node outputs internally,
resolving `#nodeN` references without LLM involvement. Only the Joiner's compact resolving `#nodeN` references without LLM involvement. Only the Joiner's compact
output crosses back to the agent loop. output crosses back to the agent loop.
- **LLM-agnostic PTC**: PicoClaw achieves Anthropic PTC's benefits (37% token - **LLM-agnostic PTC**: DragonScale achieves Anthropic PTC's benefits (37% token
reduction, parallel execution, context isolation) without provider lock-in. Any reduction, parallel execution, context isolation) without provider lock-in. Any
Fantasy-compatible LLM can produce DAG plans. Fantasy-compatible LLM can produce DAG plans.
- **Recursive DAG expansion**: When RLM detects a node with oversized context, it - **Recursive DAG expansion**: When RLM detects a node with oversized context, it
@ -799,6 +799,17 @@ complementary, not alternatives.
- **FlatBuffers tooling**: Requires `flatc` compiler in the build pipeline for codegen. - **FlatBuffers tooling**: Requires `flatc` compiler in the build pipeline for codegen.
Generated Go code is committed to the repo, so downstream consumers do not need Generated Go code is committed to the repo, so downstream consumers do not need
`flatc`. Schema changes require regeneration. `flatc`. Schema changes require regeneration.
- Schema files:
- `pkg/itr/commands.fbs``pkg/itr/itrfb/*`
- `pkg/tools/map_payloads.fbs``pkg/tools/mapopsfb/*`
- `go:generate` hooks:
- `pkg/itr/generate_flatbuffers.go`
- `pkg/tools/generate_flatbuffers.go`
- Verification:
- Local: `make flatc-check`
- CI: `.github/workflows/pr.yml` (`flatc-check` job)
- Devcontainer: `make devcontainer-generate`, `make devcontainer-verify`
- Check semantics: generation checks compare pre/post fingerprints (tracked diffs + untracked hashes) on generated directories, which keeps validation deterministic in both clean and dirty worktrees.
- **DAG planning quality**: The LLM must produce valid DAGs with correct dependency - **DAG planning quality**: The LLM must produce valid DAGs with correct dependency
edges. Malformed DAGs (cycles, missing dependencies) are caught at validation time edges. Malformed DAGs (cycles, missing dependencies) are caught at validation time
but waste an inference pass. Mitigated by structured output constraints (FlatBuffers but waste an inference pass. Mitigated by structured output constraints (FlatBuffers
@ -846,7 +857,7 @@ complementary, not alternatives.
| 2 | Migrate | Built-in tools (`shell`, `filesystem`, `web`) implement `CapableTool` | P0 | | 2 | Migrate | Built-in tools (`shell`, `filesystem`, `web`) implement `CapableTool` | P0 |
| 3 | DAG | `DAGExecutor`: topological dispatch, dependency resolution, parallel wave execution via errgroup, Joiner synthesis, replanning loop. ReAct/DAG routing in agent loop. `ToolSearch` command variant. | P0 | | 3 | DAG | `DAGExecutor`: topological dispatch, dependency resolution, parallel wave execution via errgroup, Joiner synthesis, replanning loop. ReAct/DAG routing in agent loop. `ToolSearch` command variant. | P0 |
| 4 | RLM | `RLMEngine` with rope context, strategy planning via cheap sub-LM, parallel fan-out through SecureBus, cost tracking, integration with archival memory tier, recursive DAG expansion | P0 | | 4 | RLM | `RLMEngine` with rope context, strategy planning via cheap sub-LM, parallel fan-out through SecureBus, cost tracking, integration with archival memory tier, recursive DAG expansion | P0 |
| 5 | 3 | Keyring integration, `picoclaw secret` CLI, onboard wizard extension | P1 | | 5 | 3 | Keyring integration, `dragonscale secret` CLI, onboard wizard extension | P1 |
| 6 | 4 | `SocketTransport`, daemon mode, Schnorr ZKP handshake, systemd/launchd templates | P2 | | 6 | 4 | `SocketTransport`, daemon mode, Schnorr ZKP handshake, systemd/launchd templates | P2 |
| 7 | 5 | `WasmTransport` via wazero, WASM tool isolation for untrusted tools, `CodeExec` command variant, RLM sub-call isolation | P2 | | 7 | 5 | `WasmTransport` via wazero, WASM tool isolation for untrusted tools, `CodeExec` command variant, RLM sub-call isolation | P2 |
@ -857,7 +868,9 @@ complementary, not alternatives.
| Path | Purpose | | Path | Purpose |
|------|---------| |------|---------|
| `pkg/itr/commands.fbs` | FlatBuffers schema defining the command protocol (Peek, Grep, Partition, Recurse, ToolExec, ExecWasm, Final, ToolSearch, CodeExec, DAGNode, DAGPlan) | | `pkg/itr/commands.fbs` | FlatBuffers schema defining the command protocol (Peek, Grep, Partition, Recurse, ToolExec, ExecWasm, Final, ToolSearch, CodeExec, DAGNode, DAGPlan) |
| `pkg/itr/commands_generated.go` | Generated Go code from `flatc --go` (committed, no build-time flatc dependency) | | `pkg/itr/itrfb/*` | Generated Go code from `flatc --go` for ITR command protocol (committed) |
| `pkg/tools/map_payloads.fbs` | FlatBuffers schema for map operator run/item payload persistence |
| `pkg/tools/mapopsfb/*` | Generated Go code from `flatc --go` for map payload tables (committed) |
| `pkg/itr/dag/executor.go` | `DAGExecutor`: topological dispatch, parallel wave execution, dependency resolution, Joiner synthesis | | `pkg/itr/dag/executor.go` | `DAGExecutor`: topological dispatch, parallel wave execution, dependency resolution, Joiner synthesis |
| `pkg/itr/dag/planner.go` | `DAGPlanner`: LLM-driven DAG generation with structured output, few-shot examples, validation | | `pkg/itr/dag/planner.go` | `DAGPlanner`: LLM-driven DAG generation with structured output, few-shot examples, validation |
| `pkg/itr/dag/resolver.go` | Dependency reference resolver: `#nodeN` substitution, type coercion, error propagation | | `pkg/itr/dag/resolver.go` | Dependency reference resolver: `#nodeN` substitution, type coercion, error propagation |
@ -889,7 +902,7 @@ complementary, not alternatives.
| `pkg/tools/toolloop.go` | Add `ToolLoopMode` enum, DAG executor integration, routing logic | | `pkg/tools/toolloop.go` | Add `ToolLoopMode` enum, DAG executor integration, routing logic |
| `pkg/agent/loop.go` | Route tool execution through SecureBus; integrate DAGExecutor + RLMEngine; ReAct/DAG mode selection | | `pkg/agent/loop.go` | Route tool execution through SecureBus; integrate DAGExecutor + RLMEngine; ReAct/DAG mode selection |
| `pkg/memory/store/memory_store.go` | Add RLM activation when search results exceed context window | | `pkg/memory/store/memory_store.go` | Add RLM activation when search results exceed context window |
| `cmd/picoclaw/main.go` | Wire SecureBus + DAGExecutor + RLMEngine, add `secret` and `daemon` subcommands | | `cmd/dragonscale/main.go` | Wire SecureBus + DAGExecutor + RLMEngine, add `secret` and `daemon` subcommands |
| `go.mod` | Add `tetratelabs/wazero`, `zyedidia/rope`, `google/flatbuffers` | | `go.mod` | Add `tetratelabs/wazero`, `zyedidia/rope`, `google/flatbuffers` |
| `ROADMAP.md` | Update to reference DAG executor convergence and this ADR | | `ROADMAP.md` | Update to reference DAG executor convergence and this ADR |

View file

@ -1,6 +1,6 @@
# Architecture Decision Records # Architecture Decision Records
This directory contains Architecture Decision Records (ADRs) for PicoClaw. This directory contains Architecture Decision Records (ADRs) for DragonScale.
ADRs document significant technical decisions — the context, the options considered, the chosen approach, and the trade-offs accepted. They create a historical record of how the system evolved and why. ADRs document significant technical decisions — the context, the options considered, the chosen approach, and the trade-offs accepted. They create a historical record of how the system evolved and why.
@ -18,3 +18,15 @@ Each ADR follows [Michael Nygard's template](https://cognitect.com/blog/2011/11/
| ADR | Title | Status | | ADR | Title | Status |
|-----|-------|--------| |-----|-------|--------|
| [001](001-isolated-tool-runtime.md) | Isolated Tool Runtime (ITR) + DAG Task Executor + RLM Engine | Proposed | | [001](001-isolated-tool-runtime.md) | Isolated Tool Runtime (ITR) + DAG Task Executor + RLM Engine | Proposed |
| [002](002-unified-kernel-runtime.md) | Unified Kernel Runtime | Accepted |
## Development Requirements
- FlatBuffers (`flatc`) is required for schema-driven code generation workflows documented in ADR-001.
- sqlc is required for SQL query code generation in memory/runtime persistence layers.
- Canonical local checks:
- `make flatc-check`
- `make sqlc-check`
- Canonical containerized checks:
- `make devcontainer-generate`
- `make devcontainer-verify`

View file

@ -1,112 +0,0 @@
## 🚀 Join the PicoClaw Journey: Call for Community Volunteers & Roadmap Reveal
**Hello, PicoClaw Community!**
First, a massive thank you to everyone for your enthusiasm and PR contributions. It is because of you that PicoClaw continues to iterate and evolve so rapidly. Thanks to the simplicity and accessibility of the **Go language**, weve seen a non-stop stream of high-quality PRs!
PicoClaw is growing much faster than we anticipated. As we are currently in the midst of the **Chinese New Year holiday**, we are looking to recruit community volunteers to help us maintain this incredible momentum.
This document outlines the specific volunteer roles we need right now and provides a look at our upcoming **Roadmap**.
### 🎁 Community Perks
To show our appreciation, developers who officially join our community operations will receive:
* **Exclusive AI Hardware:** Our upcoming, unreleased AI device.
* **Token Discounts:** Potential discounts on LLM tokens (currently in negotiations with major providers).
### 🎥 Calling All Content Creators!
Not a developer? You can still help! We welcome users to post **PicoClaw reviews or tutorials**.
* **Twitter:** Use the tag **#picoclaw** and mention **@SipeedIO**.
* **Bilibili:** Mention **@Sipeed矽速科技** or send us a DM.
We will be rewarding high-quality content creators with the same perks as our community developers!
---
## 🛠️ Urgent Volunteer Roles
We are looking for experts in the following areas:
1. **Issue/PR Reviewers**
* **The Mission:** With PRs and Issues exploding in volume, we need help with initial triage, evaluation, and merging.
* **Focus:** Preliminary merging and community health. Efficiency optimization and security audits will be handled by specialized roles.
2. **Resource Optimization Experts**
* **The Mission:** Rapid growth has introduced dependencies that are making PicoClaw a bit "heavy." We want to keep it lean.
* **Focus:** Analyzing resource growth between releases and trimming redundancy.
* **Priority:** **RAM usage optimization** > Binary size reduction.
3. **Security Audit & Bug Fixes**
* **The Mission:** Due to the "vibe coding" nature of our early stages, we need a thorough review of network security and AI permission management.
* **Focus:** Auditing the codebase for vulnerabilities and implementing robust fixes.
4. **Documentation & DX (Developer Experience)**
* **The Mission:** Our current README is a bit outdated. We need "step-by-step" guides that even beginners can follow.
* **Focus:** Creating clear, user-friendly documentation for both setup and development.
5. **AI-Powered CI/CD Optimization**
* **The Mission:** PicoClaw started as a "vibe coding" experiment; now we want to use AI to manage it.
* **Focus:** Automating builds with AI and exploring AI-driven issue resolution.
**How to Apply:** > If you are interested in any of the roles above, please send an email to support@sipeed.com with the subject line: [Apply: PicoClaw Expert Volunteer] + Your Desired Role.
Please include a brief introduction and any relevant experience or portfolio links. We will review all applications and grant project permissions to selected contributors!
---
## 📍 The Roadmap
Interested in a specific feature? You can "claim" these tasks and start building:
###
* **Provider:**
* **Provider Refactor:** Currently being handled by **@Daming** (ETA: 5 days)
* You can still submit code; Daming will merge it into the new implementation.
* **Channels:**
* Support for OneBot, additional platforms
* attachments (images, audio, video, files).
* **Skills:**
* Implementing `find_skill` to discover tools via [openclaw/skills](https://github.com/openclaw/skills) and other platforms.
* **Operations:** * MCP Support.
* Android operations (e.g., botdrop).
* Browser automation via CDP or ActionBook.
* **Multi-Agent Ecosystem:**
* **Basic Model-Agnet** S
* **Model Routing:** Small models for easy tasks, large models for hard ones (to save tokens).
* **Swarm Mode.**
* **AIEOS Integration.**
* **Branding:**
* **Logo**: We need a cute logo! Were leaning toward a **Mantis Shrimp**—small, but packs a legendary punch!
We have officially created these tasks as GitHub Issues, all marked with the roadmap tag.
This list will be updated continuously as we progress.
If you would like to claim a task, please feel free to start a conversation by commenting directly on the corresponding issue!
---
## 🤝 How to Join
**Everything is open to your creativity!** If you have a wild idea, just PR it.
1. **The Fast Track:** Once you have at least **one merged PR**, you are eligible to join our **Developer Discord** to help plan the future of PicoClaw.
2. **The Application Track:** If you havent submitted a PR yet but want to dive in, email **support@sipeed.com** with the subject:
> `[Apply Join PicoClaw Dev Group] + Your GitHub Account`
> Include the role you're interested in and any evidence of your development experience.
### Looking Ahead
Powered by PicoClaw, we are crafting a Swarm AI Assistant to transform your environment into a seamless network of personal stewards. By automating the friction of daily life, we empower you to transcend the ordinary and freely explore your creative potential.
**Finally, Happy Chinese New Year to everyone!** May PicoClaw gallop forward in this **Year of the Horse!** 🐎

View file

@ -1,6 +1,6 @@
# PicoClaw Eval Harness # DragonScale Eval Harness
End-to-end evaluation system for the PicoClaw agent runtime. End-to-end evaluation system for the DragonScale agent runtime.
## Quick Start ## Quick Start
@ -33,7 +33,7 @@ The current agent architecture has these always-on behaviors:
``` ```
eval/ eval/
├── cmd/eval-runner/ # Go binary that wraps picoclaw for promptfoo ├── cmd/eval-runner/ # Go binary that wraps dragonscale for promptfoo
├── cases/ # Golden dataset (YAML test cases) ├── cases/ # Golden dataset (YAML test cases)
│ ├── tool_calling.yaml │ ├── tool_calling.yaml
│ ├── token_efficiency.yaml │ ├── token_efficiency.yaml
@ -44,6 +44,9 @@ eval/
│ ├── skills.yaml │ ├── skills.yaml
│ ├── subagent.yaml │ ├── subagent.yaml
│ ├── reasoning.yaml │ ├── reasoning.yaml
│ ├── assistant_proactive.yaml
│ ├── assistant_first_metrics.yaml
│ ├── procedural_long_context.yaml
│ └── error_recovery.yaml │ └── error_recovery.yaml
├── go_evals/ # Go-native component tests (memory, tools) ├── go_evals/ # Go-native component tests (memory, tools)
├── scripts/ # CI/comparison scripts ├── scripts/ # CI/comparison scripts
@ -55,7 +58,7 @@ eval/
## How It Works ## How It Works
1. **eval-runner** wraps picoclaw with an instrumented language model that captures every LLM call, tool invocation, token count, and timing. 1. **eval-runner** wraps dragonscale with an instrumented language model that captures every LLM call, tool invocation, token count, and timing.
2. **promptfoo** invokes `eval-runner` via `exec:` provider, sending prompts as JSON on stdin and parsing the structured trace JSON from stdout. 2. **promptfoo** invokes `eval-runner` via `exec:` provider, sending prompts as JSON on stdin and parsing the structured trace JSON from stdout.
@ -99,7 +102,7 @@ Create a new YAML file in `eval/cases/` following this pattern:
```yaml ```yaml
- description: "what this tests" - description: "what this tests"
vars: vars:
prompt: "the prompt to send to picoclaw" prompt: "the prompt to send to dragonscale"
assert: assert:
- type: javascript - type: javascript
value: | value: |
@ -108,11 +111,17 @@ Create a new YAML file in `eval/cases/` following this pattern:
return { pass: true, score: 1.0, reason: 'explanation' }; return { pass: true, score: 1.0, reason: 'explanation' };
``` ```
For generated long-context suites:
```bash
python eval/scripts/generate_long_context_cases.py --count 12 --seed 20260221
```
## A/B Comparison ## A/B Comparison
`make eval-compare` builds both your current branch and main, then runs the identical test suite against both. Results show a side-by-side comparison matrix with per-test scores. `make eval-compare` builds both your current branch and main, then runs the identical test suite against both. Results show a side-by-side comparison matrix with per-test scores.
## Environment Variables ## Environment Variables
- `PICOCLAW_EVAL_CONFIG` - Optional overlay config path applied on top of user base config. - `DRAGONSCALE_EVAL_CONFIG` - Optional overlay config path applied on top of user base config.
- Base config discovery uses XDG first (`~/.config/picoclaw/config.json`), then legacy (`~/.picoclaw/config.json`), then XDG fallback if neither exists. - Base config discovery uses XDG first (`~/.config/dragonscale/config.json`), then legacy (`~/.dragonscale/config.json`), then XDG fallback if neither exists.

View file

@ -54,7 +54,7 @@
- description: "unicode content: handles non-ASCII text" - description: "unicode content: handles non-ASCII text"
vars: vars:
prompt: "Write the text '你好世界 🌍 picoclaw' to a file called unicode_test.txt, then read it back." prompt: "Write the text '你好世界 🌍 dragonscale' to a file called unicode_test.txt, then read it back."
assert: assert:
- type: javascript - type: javascript
value: | value: |

View file

@ -4,7 +4,7 @@
- description: "memory write and search: store a fact then retrieve it" - description: "memory write and search: store a fact then retrieve it"
vars: vars:
prompt: "Remember this important fact: 'The picoclaw eval harness was built in February 2026.' Then search your memory for 'eval harness' to confirm you stored it." prompt: "Remember this important fact: 'The dragonscale eval harness was built in February 2026.' Then search your memory for 'eval harness' to confirm you stored it."
assert: assert:
- type: javascript - type: javascript
value: | value: |

View file

@ -41,7 +41,7 @@
value: | value: |
const trace = JSON.parse(output); const trace = JSON.parse(output);
const out = (trace.output || ''); const out = (trace.output || '');
const hasMarker = out.includes('picoclaw-fixture-marker-abc123') || out.includes('fixture'); const hasMarker = out.includes('dragonscale-fixture-marker-abc123') || out.includes('fixture');
return { pass: hasMarker, score: hasMarker ? 1.0 : 0.0, reason: hasMarker ? 'returned fixture content' : 'did not return expected file content' }; return { pass: hasMarker, score: hasMarker ? 1.0 : 0.0, reason: hasMarker ? 'returned fixture content' : 'did not return expected file content' };
- description: "meta tools: tool_call dispatches exec correctly" - description: "meta tools: tool_call dispatches exec correctly"

View file

@ -3,7 +3,7 @@
- description: "create and verify: write a file then read it back" - description: "create and verify: write a file then read it back"
vars: vars:
prompt: "Write the text 'picoclaw eval checkpoint' to a file called eval_checkpoint.txt, then read it back and confirm the contents match." prompt: "Write the text 'dragonscale eval checkpoint' to a file called eval_checkpoint.txt, then read it back and confirm the contents match."
assert: assert:
- type: javascript - type: javascript
value: | value: |

View file

@ -1,6 +1,6 @@
# Tool Calling Evaluation Cases # Tool Calling Evaluation Cases
# Tests that the agent correctly selects and invokes the right tools. # Tests that the agent correctly selects and invokes the right tools.
# PicoClaw uses progressive disclosure: the LLM may invoke tools directly # DragonScale uses progressive disclosure: the LLM may invoke tools directly
# (e.g. "read_file") or via the meta-tool "tool_call" with tool_name in args. # (e.g. "read_file") or via the meta-tool "tool_call" with tool_name in args.
- description: "file read: agent reads a known fixture file and returns its content" - description: "file read: agent reads a known fixture file and returns its content"
@ -24,7 +24,7 @@
value: | value: |
const trace = JSON.parse(output); const trace = JSON.parse(output);
const out = (trace.output || '').toLowerCase(); const out = (trace.output || '').toLowerCase();
const hasContent = out.includes('picoclaw eval fixture') || out.includes('hello from the eval harness'); const hasContent = out.includes('dragonscale eval fixture') || out.includes('hello from the eval harness');
return { pass: hasContent, score: hasContent ? 1.0 : 0.3, reason: hasContent ? 'returned fixture content' : 'output did not contain expected fixture text' }; return { pass: hasContent, score: hasContent ? 1.0 : 0.3, reason: hasContent ? 'returned fixture content' : 'output did not contain expected fixture text' };
- description: "file write: agent creates a new file when asked" - description: "file write: agent creates a new file when asked"
@ -47,7 +47,7 @@
- description: "shell exec: agent runs a shell command" - description: "shell exec: agent runs a shell command"
vars: vars:
prompt: "Run the command 'echo picoclaw-eval-test' and tell me the output." prompt: "Run the command 'echo dragonscale-eval-test' and tell me the output."
assert: assert:
- type: javascript - type: javascript
value: | value: |
@ -61,7 +61,7 @@
} }
return false; return false;
}); });
const outputContains = trace.output.includes('picoclaw-eval-test'); const outputContains = trace.output.includes('dragonscale-eval-test');
return { pass: hasExec && outputContains, score: (hasExec ? 0.5 : 0) + (outputContains ? 0.5 : 0), reason: `exec=${hasExec}, output_correct=${outputContains} (${toolCalls.length} tool calls)` }; return { pass: hasExec && outputContains, score: (hasExec ? 0.5 : 0) + (outputContains ? 0.5 : 0), reason: `exec=${hasExec}, output_correct=${outputContains} (${toolCalls.length} tool calls)` };
- description: "list directory: agent lists workspace contents" - description: "list directory: agent lists workspace contents"
@ -84,7 +84,7 @@
- description: "edit file: agent edits an existing file" - description: "edit file: agent edits an existing file"
vars: vars:
prompt: "First write a file called edit_target.txt with 'hello world'. Then edit it to replace 'world' with 'picoclaw'. Read it back and confirm." prompt: "First write a file called edit_target.txt with 'hello world'. Then edit it to replace 'world' with 'dragonscale'. Read it back and confirm."
assert: assert:
- type: javascript - type: javascript
value: | value: |
@ -104,8 +104,8 @@
value: | value: |
const trace = JSON.parse(output); const trace = JSON.parse(output);
const out = (trace.output || '').toLowerCase(); const out = (trace.output || '').toLowerCase();
const hasPicoclaw = out.includes('picoclaw'); const hasPicoclaw = out.includes('dragonscale');
return { pass: hasPicoclaw, score: hasPicoclaw ? 1.0 : 0.0, reason: hasPicoclaw ? 'confirmed edit result' : 'did not confirm picoclaw in output' }; return { pass: hasPicoclaw, score: hasPicoclaw ? 1.0 : 0.0, reason: hasPicoclaw ? 'confirmed edit result' : 'did not confirm dragonscale in output' };
- description: "append file: agent appends to an existing file" - description: "append file: agent appends to an existing file"
vars: vars:
@ -128,7 +128,7 @@
- description: "web search: agent searches the web" - description: "web search: agent searches the web"
vars: vars:
prompt: "Search the web for 'picoclaw AI agent' and summarize what you find." prompt: "Search the web for 'dragonscale AI agent' and summarize what you find."
assert: assert:
- type: javascript - type: javascript
value: | value: |

View file

@ -11,9 +11,9 @@ import (
"time" "time"
fantasy "charm.land/fantasy" fantasy "charm.land/fantasy"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
picoruntime "github.com/sipeed/picoclaw/pkg/runtime" picoruntime "github.com/ZanzyTHEbar/dragonscale/pkg/runtime"
) )
// Trace is the structured output emitted by the eval runner. // Trace is the structured output emitted by the eval runner.
@ -247,7 +247,7 @@ func truncate(s string, maxLen int) string {
func evalRunnerTimeout() time.Duration { func evalRunnerTimeout() time.Duration {
const defaultTimeout = 180 * time.Second const defaultTimeout = 180 * time.Second
raw := strings.TrimSpace(os.Getenv("PICOCLAW_EVAL_TIMEOUT_MS")) raw := strings.TrimSpace(os.Getenv("DRAGONSCALE_EVAL_TIMEOUT_MS"))
if raw == "" { if raw == "" {
return defaultTimeout return defaultTimeout
} }

View file

@ -1,5 +1,5 @@
PicoClaw Eval Fixture Data DragonScale Eval Fixture Data
Line 2: This file is used by the evaluation harness for deterministic read tests. Line 2: This file is used by the evaluation harness for deterministic read tests.
Line 3: It contains known content that assertions can verify against. Line 3: It contains known content that assertions can verify against.
Line 4: The quick brown fox jumps over the lazy dog. Line 4: The quick brown fox jumps over the lazy dog.
Line 5: picoclaw-fixture-marker-abc123 Line 5: dragonscale-fixture-marker-abc123

View file

@ -19,5 +19,5 @@ When asked to greet someone, use one of the following templates:
## Notes ## Notes
This skill is a fixture for the picoclaw evaluation harness. This skill is a fixture for the dragonscale evaluation harness.
The marker `eval-skill-loaded` confirms this skill was successfully read. The marker `eval-skill-loaded` confirms this skill was successfully read.

View file

@ -7,8 +7,8 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -127,12 +127,12 @@ func TestToolExecution_ExecBlocking(t *testing.T) {
execTool := tools.NewExecTool(workspace, false) execTool := tools.NewExecTool(workspace, false)
result := execTool.Execute(context.Background(), map[string]interface{}{ result := execTool.Execute(context.Background(), map[string]interface{}{
"command": "echo picoclaw-eval-test", "command": "echo dragonscale-eval-test",
}) })
require.NotNil(t, result) require.NotNil(t, result)
assert.False(t, result.IsError, "echo should succeed") assert.False(t, result.IsError, "echo should succeed")
assert.Contains(t, result.ForLLM, "picoclaw-eval-test") assert.Contains(t, result.ForLLM, "dragonscale-eval-test")
} }
func TestToolExecution_ListDir(t *testing.T) { func TestToolExecution_ListDir(t *testing.T) {
@ -165,7 +165,7 @@ func TestToolExecution_EditFile(t *testing.T) {
result := editTool.Execute(context.Background(), map[string]interface{}{ result := editTool.Execute(context.Background(), map[string]interface{}{
"path": "edit_target.txt", "path": "edit_target.txt",
"old_text": "world", "old_text": "world",
"new_text": "picoclaw", "new_text": "dragonscale",
}) })
require.NotNil(t, result) require.NotNil(t, result)
assert.False(t, result.IsError, "edit should succeed: %s", result.ForLLM) assert.False(t, result.IsError, "edit should succeed: %s", result.ForLLM)
@ -174,7 +174,7 @@ func TestToolExecution_EditFile(t *testing.T) {
readResult := readTool.Execute(context.Background(), map[string]interface{}{ readResult := readTool.Execute(context.Background(), map[string]interface{}{
"path": "edit_target.txt", "path": "edit_target.txt",
}) })
assert.Contains(t, readResult.ForLLM, "picoclaw") assert.Contains(t, readResult.ForLLM, "dragonscale")
assert.NotContains(t, readResult.ForLLM, "world") assert.NotContains(t, readResult.ForLLM, "world")
} }
@ -244,7 +244,7 @@ func TestToolExecution_ReadFile_PathTraversal(t *testing.T) {
func TestToolExecution_Unrestricted(t *testing.T) { func TestToolExecution_Unrestricted(t *testing.T) {
workspace := testWorkspace(t) workspace := testWorkspace(t)
tmpFile := filepath.Join(os.TempDir(), "picoclaw_unrestricted_test.txt") tmpFile := filepath.Join(os.TempDir(), "dragonscale_unrestricted_test.txt")
os.WriteFile(tmpFile, []byte("unrestricted content"), 0644) os.WriteFile(tmpFile, []byte("unrestricted content"), 0644)
defer os.Remove(tmpFile) defer os.Remove(tmpFile)

View file

@ -1,18 +1,18 @@
# PicoClaw Eval Harness - promptfoo configuration # DragonScale Eval Harness - promptfoo configuration
# Run: make eval # Run: make eval
# View: cd eval && npx promptfoo view # View: cd eval && npx promptfoo view
description: "PicoClaw agent end-to-end evaluation" description: "DragonScale agent end-to-end evaluation"
maxConcurrency: 1 maxConcurrency: 1
providers: providers:
- id: "exec:./bin/eval-runner" - id: "exec:./bin/eval-runner"
label: "picoclaw" label: "dragonscale"
config: config:
timeout: 180000 timeout: 180000
env: env:
PICOCLAW_EVAL_CONFIG: "./configs/default.json" DRAGONSCALE_EVAL_CONFIG: "./configs/default.json"
# Default assertions applied to every test case # Default assertions applied to every test case
defaultTest: defaultTest:

View file

@ -13,7 +13,7 @@ if [[ "${1:-}" == "--repeat" ]]; then
REPEAT="${2:-3}" REPEAT="${2:-3}"
fi fi
echo "=== PicoClaw Eval Comparison ===" echo "=== DragonScale Eval Comparison ==="
echo "Repeat: ${REPEAT}x per test case" echo "Repeat: ${REPEAT}x per test case"
echo "" echo ""
@ -47,7 +47,7 @@ cd "$EVAL_DIR"
# Create a temp config with both providers # Create a temp config with both providers
cat > promptfooconfig-compare.yaml <<'YAML' cat > promptfooconfig-compare.yaml <<'YAML'
description: "PicoClaw A/B comparison (branch vs main)" description: "DragonScale A/B comparison (branch vs main)"
providers: providers:
- id: "exec:./bin/eval-runner" - id: "exec:./bin/eval-runner"

2
go.mod
View file

@ -1,4 +1,4 @@
module github.com/sipeed/picoclaw module github.com/ZanzyTHEbar/dragonscale
go 1.26 go 1.26

View file

@ -3,16 +3,17 @@
## What This Is ## What This Is
This directory contains a vendored copy of `charm.land/fantasy`, the Charmbracelet This directory contains a vendored copy of `charm.land/fantasy`, the Charmbracelet
Fantasy LLM agent framework. We vendor it to enable direct modifications for PicoClaw-specific Fantasy LLM agent framework. We vendor it to enable direct modifications for
features (progressive disclosure, custom streaming hooks, tool call repair, etc.). features (progressive disclosure, custom streaming hooks, tool call repair, etc.).
PicoClaw's `go.mod` contains a `replace` directive: The project `go.mod` contains a `replace` directive:
``` ```
replace charm.land/fantasy v0.8.1 => ./internal/fantasy replace charm.land/fantasy v0.8.1 => ./internal/fantasy
``` ```
This redirects all `charm.land/fantasy` imports to this local copy. No import paths need to change in either PicoClaw code or the fantasy source itself. This redirects all `charm.land/fantasy` imports to this local copy. No import paths
need to change in either project code or the fantasy source itself.
## Automated Sync System ## Automated Sync System

View file

@ -9,9 +9,9 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors"
sqlc "github.com/sipeed/picoclaw/pkg/memory/sqlc" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/pcerrors" sqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
) )
// Store wraps the SQLC queries for conversation operations. // Store wraps the SQLC queries for conversation operations.
@ -79,16 +79,16 @@ type EditMessageParams struct {
func (s *Store) EditMessage(ctx context.Context, p EditMessageParams) (sqlc.AgentMessage, error) { func (s *Store) EditMessage(ctx context.Context, p EditMessageParams) (sqlc.AgentMessage, error) {
msgIDStr := strings.TrimSpace(p.MessageID) msgIDStr := strings.TrimSpace(p.MessageID)
if msgIDStr == "" { if msgIDStr == "" {
return sqlc.AgentMessage{}, pcerrors.New(pcerrors.CodeInvalidArgument, "message_id is required") return sqlc.AgentMessage{}, dserrors.New(dserrors.CodeInvalidArgument, "message_id is required")
} }
msgID, err := ids.Parse(msgIDStr) msgID, err := ids.Parse(msgIDStr)
if err != nil { if err != nil {
return sqlc.AgentMessage{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse message_id %q", msgIDStr) return sqlc.AgentMessage{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse message_id %q", msgIDStr)
} }
newText := strings.TrimSpace(p.NewText) newText := strings.TrimSpace(p.NewText)
if newText == "" { if newText == "" {
return sqlc.AgentMessage{}, pcerrors.New(pcerrors.CodeInvalidArgument, "new content is empty") return sqlc.AgentMessage{}, dserrors.New(dserrors.CodeInvalidArgument, "new content is empty")
} }
editor := strings.TrimSpace(p.Editor) editor := strings.TrimSpace(p.Editor)
@ -141,16 +141,16 @@ type ForkFromCheckpointParams struct {
func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointParams) (sqlc.AgentConversation, error) { func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointParams) (sqlc.AgentConversation, error) {
fromIDStr := strings.TrimSpace(p.FromConversationID) fromIDStr := strings.TrimSpace(p.FromConversationID)
if fromIDStr == "" { if fromIDStr == "" {
return sqlc.AgentConversation{}, pcerrors.New(pcerrors.CodeInvalidArgument, "from_conversation_id is required") return sqlc.AgentConversation{}, dserrors.New(dserrors.CodeInvalidArgument, "from_conversation_id is required")
} }
fromID, err := ids.Parse(fromIDStr) fromID, err := ids.Parse(fromIDStr)
if err != nil { if err != nil {
return sqlc.AgentConversation{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse from_conversation_id %q", fromIDStr) return sqlc.AgentConversation{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse from_conversation_id %q", fromIDStr)
} }
cpName := strings.TrimSpace(p.CheckpointName) cpName := strings.TrimSpace(p.CheckpointName)
if cpName == "" { if cpName == "" {
return sqlc.AgentConversation{}, pcerrors.New(pcerrors.CodeInvalidArgument, "checkpoint_name is required") return sqlc.AgentConversation{}, dserrors.New(dserrors.CodeInvalidArgument, "checkpoint_name is required")
} }
cp, err := s.q.GetAgentCheckpointByConversationIDAndName(ctx, cp, err := s.q.GetAgentCheckpointByConversationIDAndName(ctx,
@ -177,7 +177,7 @@ func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointPara
var snap snapshot var snap snapshot
if len(runState.SnapshotJson) > 0 { if len(runState.SnapshotJson) > 0 {
if err := jsonv2.Unmarshal(runState.SnapshotJson, &snap); err != nil { if err := jsonv2.Unmarshal(runState.SnapshotJson, &snap); err != nil {
return sqlc.AgentConversation{}, pcerrors.Wrapf(pcerrors.CodeInternal, err, "parse snapshot for run state %s", cp.RunStateID) return sqlc.AgentConversation{}, dserrors.Wrapf(dserrors.CodeInternal, err, "parse snapshot for run state %s", cp.RunStateID)
} }
} }
@ -253,24 +253,24 @@ type MergeAsLinkedContextParams struct {
func (s *Store) MergeAsLinkedContext(ctx context.Context, p MergeAsLinkedContextParams) (sqlc.AgentConversation, error) { func (s *Store) MergeAsLinkedContext(ctx context.Context, p MergeAsLinkedContextParams) (sqlc.AgentConversation, error) {
baseIDStr := strings.TrimSpace(p.BaseConversationID) baseIDStr := strings.TrimSpace(p.BaseConversationID)
if baseIDStr == "" { if baseIDStr == "" {
return sqlc.AgentConversation{}, pcerrors.New(pcerrors.CodeInvalidArgument, "base_conversation_id is required") return sqlc.AgentConversation{}, dserrors.New(dserrors.CodeInvalidArgument, "base_conversation_id is required")
} }
baseID, err := ids.Parse(baseIDStr) baseID, err := ids.Parse(baseIDStr)
if err != nil { if err != nil {
return sqlc.AgentConversation{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse base_conversation_id %q", baseIDStr) return sqlc.AgentConversation{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse base_conversation_id %q", baseIDStr)
} }
otherIDStr := strings.TrimSpace(p.OtherConversationID) otherIDStr := strings.TrimSpace(p.OtherConversationID)
if otherIDStr == "" { if otherIDStr == "" {
return sqlc.AgentConversation{}, pcerrors.New(pcerrors.CodeInvalidArgument, "other_conversation_id is required") return sqlc.AgentConversation{}, dserrors.New(dserrors.CodeInvalidArgument, "other_conversation_id is required")
} }
otherID, err := ids.Parse(otherIDStr) otherID, err := ids.Parse(otherIDStr)
if err != nil { if err != nil {
return sqlc.AgentConversation{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse other_conversation_id %q", otherIDStr) return sqlc.AgentConversation{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse other_conversation_id %q", otherIDStr)
} }
if baseID == otherID { if baseID == otherID {
return sqlc.AgentConversation{}, pcerrors.New(pcerrors.CodeInvalidArgument, "base and other conversations must differ") return sqlc.AgentConversation{}, dserrors.New(dserrors.CodeInvalidArgument, "base and other conversations must differ")
} }
conv, err := s.q.CreateAgentConversation(ctx, sqlc.CreateAgentConversationParams{ conv, err := s.q.CreateAgentConversation(ctx, sqlc.CreateAgentConversationParams{
@ -335,11 +335,11 @@ type AncestryResult struct {
func (s *Store) Ancestry(ctx context.Context, p AncestryParams) (AncestryResult, error) { func (s *Store) Ancestry(ctx context.Context, p AncestryParams) (AncestryResult, error) {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return AncestryResult{}, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return AncestryResult{}, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
convID, err := ids.Parse(convIDStr) convID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return AncestryResult{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return AncestryResult{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
conv, err := s.q.GetAgentConversation(ctx, sqlc.GetAgentConversationParams{ID: convID}) conv, err := s.q.GetAgentConversation(ctx, sqlc.GetAgentConversationParams{ID: convID})
@ -377,11 +377,11 @@ type LinksListParams struct {
func (s *Store) LinksList(ctx context.Context, p LinksListParams) ([]sqlc.AgentConversationLink, error) { func (s *Store) LinksList(ctx context.Context, p LinksListParams) ([]sqlc.AgentConversationLink, error) {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return nil, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return nil, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
convID, err := ids.Parse(convIDStr) convID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return nil, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return nil, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
return s.q.ListAgentConversationLinksByConversationID(ctx, return s.q.ListAgentConversationLinksByConversationID(ctx,
sqlc.ListAgentConversationLinksByConversationIDParams{ConversationID: convID}) sqlc.ListAgentConversationLinksByConversationIDParams{ConversationID: convID})
@ -399,20 +399,20 @@ type LinksRemoveParams struct {
func (s *Store) LinksRemove(ctx context.Context, p LinksRemoveParams) error { func (s *Store) LinksRemove(ctx context.Context, p LinksRemoveParams) error {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
convID, err := ids.Parse(convIDStr) convID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
linkedIDStr := strings.TrimSpace(p.LinkedConversationID) linkedIDStr := strings.TrimSpace(p.LinkedConversationID)
if linkedIDStr == "" { if linkedIDStr == "" {
return pcerrors.New(pcerrors.CodeInvalidArgument, "linked_conversation_id is required") return dserrors.New(dserrors.CodeInvalidArgument, "linked_conversation_id is required")
} }
linkedID, err := ids.Parse(linkedIDStr) linkedID, err := ids.Parse(linkedIDStr)
if err != nil { if err != nil {
return pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse linked_conversation_id %q", linkedIDStr) return dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse linked_conversation_id %q", linkedIDStr)
} }
kind := strings.TrimSpace(p.Kind) kind := strings.TrimSpace(p.Kind)
@ -467,11 +467,11 @@ type GraphResult struct {
func (s *Store) Graph(ctx context.Context, p GraphParams) (GraphResult, error) { func (s *Store) Graph(ctx context.Context, p GraphParams) (GraphResult, error) {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return GraphResult{}, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return GraphResult{}, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
rootID, err := ids.Parse(convIDStr) rootID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return GraphResult{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return GraphResult{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
depth := p.Depth depth := p.Depth

View file

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/sipeed/picoclaw/pkg/agent" "github.com/ZanzyTHEbar/dragonscale/pkg/agent"
) )
func newDelegateKV(t *testing.T, agentID string) *agent.DelegateKV { func newDelegateKV(t *testing.T, agentID string) *agent.DelegateKV {

View file

@ -1,7 +1,7 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package agent package agent
@ -9,11 +9,11 @@ import (
"context" "context"
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
memstore "github.com/sipeed/picoclaw/pkg/memory/store" memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
// MemGPTTool wraps store.MemoryTool as a PicoClaw tools.Tool so it can be // MemGPTTool wraps store.MemoryTool as a DragonScale tools.Tool so it can be
// registered in the ToolRegistry and executed by the Fantasy agent loop. // registered in the ToolRegistry and executed by the Fantasy agent loop.
type MemGPTTool struct { type MemGPTTool struct {
inner *memstore.MemoryTool inner *memstore.MemoryTool
@ -21,7 +21,7 @@ type MemGPTTool struct {
var _ tools.Tool = (*MemGPTTool)(nil) var _ tools.Tool = (*MemGPTTool)(nil)
// NewMemGPTTool creates a PicoClaw tool wrapper around a MemoryTool. // NewMemGPTTool creates a DragonScale tool wrapper around a MemoryTool.
func NewMemGPTTool(store *memstore.MemoryStore, agentID, session string) *MemGPTTool { func NewMemGPTTool(store *memstore.MemoryStore, agentID, session string) *MemGPTTool {
return &MemGPTTool{ return &MemGPTTool{
inner: memstore.NewMemoryTool(store, agentID, session), inner: memstore.NewMemoryTool(store, agentID, session),

View file

@ -8,9 +8,9 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"strings" "strings"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors"
sqlc "github.com/sipeed/picoclaw/pkg/memory/sqlc" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/pcerrors" sqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
) )
// Store wraps the SQLC queries for mention operations. // Store wraps the SQLC queries for mention operations.
@ -46,34 +46,34 @@ type AddParams struct {
func (s *Store) Add(ctx context.Context, p AddParams) (sqlc.AgentMention, error) { func (s *Store) Add(ctx context.Context, p AddParams) (sqlc.AgentMention, error) {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return sqlc.AgentMention{}, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return sqlc.AgentMention{}, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
convID, err := ids.Parse(convIDStr) convID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return sqlc.AgentMention{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return sqlc.AgentMention{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
msgIDStr := strings.TrimSpace(p.MessageID) msgIDStr := strings.TrimSpace(p.MessageID)
if msgIDStr == "" { if msgIDStr == "" {
return sqlc.AgentMention{}, pcerrors.New(pcerrors.CodeInvalidArgument, "message_id is required") return sqlc.AgentMention{}, dserrors.New(dserrors.CodeInvalidArgument, "message_id is required")
} }
msgID, err := ids.Parse(msgIDStr) msgID, err := ids.Parse(msgIDStr)
if err != nil { if err != nil {
return sqlc.AgentMention{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse message_id %q", msgIDStr) return sqlc.AgentMention{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse message_id %q", msgIDStr)
} }
kind := strings.TrimSpace(p.Kind) kind := strings.TrimSpace(p.Kind)
if kind == "" { if kind == "" {
return sqlc.AgentMention{}, pcerrors.New(pcerrors.CodeInvalidArgument, "kind is required") return sqlc.AgentMention{}, dserrors.New(dserrors.CodeInvalidArgument, "kind is required")
} }
targetIDStr := strings.TrimSpace(p.TargetID) targetIDStr := strings.TrimSpace(p.TargetID)
if targetIDStr == "" { if targetIDStr == "" {
return sqlc.AgentMention{}, pcerrors.New(pcerrors.CodeInvalidArgument, "target_id is required") return sqlc.AgentMention{}, dserrors.New(dserrors.CodeInvalidArgument, "target_id is required")
} }
targetID, err := ids.Parse(targetIDStr) targetID, err := ids.Parse(targetIDStr)
if err != nil { if err != nil {
return sqlc.AgentMention{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse target_id %q", targetIDStr) return sqlc.AgentMention{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse target_id %q", targetIDStr)
} }
metaJSON, _ := jsonv2.Marshal(p.Metadata) metaJSON, _ := jsonv2.Marshal(p.Metadata)
@ -100,11 +100,11 @@ type ListByConversationParams struct {
func (s *Store) ListByConversation(ctx context.Context, p ListByConversationParams) ([]sqlc.AgentMention, error) { func (s *Store) ListByConversation(ctx context.Context, p ListByConversationParams) ([]sqlc.AgentMention, error) {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return nil, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return nil, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
convID, err := ids.Parse(convIDStr) convID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return nil, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return nil, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
return s.q.ListAgentMentionsByConversationID(ctx, return s.q.ListAgentMentionsByConversationID(ctx,
sqlc.ListAgentMentionsByConversationIDParams{ConversationID: convID}) sqlc.ListAgentMentionsByConversationIDParams{ConversationID: convID})

View file

@ -9,8 +9,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/sipeed/picoclaw/pkg/agent" "github.com/ZanzyTHEbar/dragonscale/pkg/agent"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
) )
// staticToolRuntime is a test ToolRuntime that returns pre-defined results. // staticToolRuntime is a test ToolRuntime that returns pre-defined results.

View file

@ -8,9 +8,9 @@ import (
"strings" "strings"
"charm.land/fantasy" "charm.land/fantasy"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors"
"github.com/sipeed/picoclaw/pkg/memory/sqlc" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/pcerrors" "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
) )
const defaultToolMaxConcurrency = 4 const defaultToolMaxConcurrency = 4
@ -59,13 +59,13 @@ func (r OffloadingToolRuntime) Execute(ctx context.Context, tools []fantasy.Agen
r.Base = fantasy.DAGToolRuntime{MaxConcurrency: defaultToolMaxConcurrency} r.Base = fantasy.DAGToolRuntime{MaxConcurrency: defaultToolMaxConcurrency}
} }
if r.KV == nil { if r.KV == nil {
return nil, pcerrors.New(pcerrors.CodeFailedPrecondition, "KV delegate is nil") return nil, dserrors.New(dserrors.CodeFailedPrecondition, "KV delegate is nil")
} }
if r.Queries == nil { if r.Queries == nil {
return nil, pcerrors.New(pcerrors.CodeFailedPrecondition, "db queries is nil") return nil, dserrors.New(dserrors.CodeFailedPrecondition, "db queries is nil")
} }
if r.ConversationID.IsZero() || r.RunID.IsZero() { if r.ConversationID.IsZero() || r.RunID.IsZero() {
return nil, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id/run_id is required") return nil, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id/run_id is required")
} }
threshold := r.ThresholdChars threshold := r.ThresholdChars

View file

@ -3,11 +3,12 @@ package agent
import ( import (
"context" "context"
"errors" "errors"
"fmt"
fantasy "charm.land/fantasy" fantasy "charm.land/fantasy"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/itr" "github.com/ZanzyTHEbar/dragonscale/pkg/itr"
"github.com/sipeed/picoclaw/pkg/security/securebus" "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
) )
// SecureBusToolRuntime is a fantasy.ToolRuntime that routes every tool call // SecureBusToolRuntime is a fantasy.ToolRuntime that routes every tool call
@ -20,7 +21,7 @@ import (
// 4. Otherwise delegate to Base runtime for actual execution // 4. Otherwise delegate to Base runtime for actual execution
// 5. If bus detected a leak, replace Base output with the redacted version // 5. If bus detected a leak, replace Base output with the redacted version
type SecureBusToolRuntime struct { type SecureBusToolRuntime struct {
// Base is the underlying runtime. If nil, the bus result is used directly. // Base is the underlying runtime and is required.
Base fantasy.ToolRuntime Base fantasy.ToolRuntime
// Bus is required. // Bus is required.
@ -28,6 +29,11 @@ type SecureBusToolRuntime struct {
// SessionKey is forwarded to bus requests for audit tracing. // SessionKey is forwarded to bus requests for audit tracing.
SessionKey string SessionKey string
// Optional state persistence for runtime execution.
StateStore *StateStore
RunID ids.UUID
StepIndex int
} }
// Execute implements fantasy.ToolRuntime. // Execute implements fantasy.ToolRuntime.
@ -37,16 +43,24 @@ func (r SecureBusToolRuntime) Execute(
toolCalls []fantasy.ToolCallContent, toolCalls []fantasy.ToolCallContent,
onResult func(fantasy.ToolResultContent) error, onResult func(fantasy.ToolResultContent) error,
) ([]fantasy.ToolResultContent, error) { ) ([]fantasy.ToolResultContent, error) {
if r.Bus == nil || len(toolCalls) == 0 { if len(toolCalls) == 0 {
if r.Base != nil {
return r.Base.Execute(ctx, tools, toolCalls, onResult)
}
return nil, nil return nil, nil
} }
if r.Bus == nil {
return nil, fmt.Errorf("secure bus runtime requires bus")
}
if r.Base == nil {
return nil, fmt.Errorf("secure bus runtime requires base runtime")
}
results := make([]fantasy.ToolResultContent, 0, len(toolCalls)) results := make([]fantasy.ToolResultContent, 0, len(toolCalls))
for _, tc := range toolCalls { for i, tc := range toolCalls {
step := r.StepIndex + i
r.recordRunState(ctx, step, "tool_call", map[string]any{
"tool_name": tc.ToolName,
})
reqID := ids.New().String() reqID := ids.New().String()
req := itr.NewToolExecRequest(reqID, r.SessionKey, tc.ToolCallID, tc.ToolName, tc.Input) req := itr.NewToolExecRequest(reqID, r.SessionKey, tc.ToolCallID, tc.ToolName, tc.Input)
busResp := r.Bus.Execute(ctx, req) busResp := r.Bus.Execute(ctx, req)
@ -58,22 +72,10 @@ func (r SecureBusToolRuntime) Execute(
ToolName: tc.ToolName, ToolName: tc.ToolName,
Result: fantasy.ToolResultOutputContentError{Error: errors.New(busResp.Result)}, Result: fantasy.ToolResultOutputContentError{Error: errors.New(busResp.Result)},
} }
results = append(results, tr) r.recordRunState(ctx, step, "tool_call_error", map[string]any{
if onResult != nil { "tool_name": tc.ToolName,
if err := onResult(tr); err != nil { "error": busResp.Result,
return results, err })
}
}
continue
}
// Bus accepted — delegate to Base for actual execution.
if r.Base == nil {
tr := fantasy.ToolResultContent{
ToolCallID: tc.ToolCallID,
ToolName: tc.ToolName,
Result: fantasy.ToolResultOutputContentText{Text: busResp.Result},
}
results = append(results, tr) results = append(results, tr)
if onResult != nil { if onResult != nil {
if err := onResult(tr); err != nil { if err := onResult(tr); err != nil {
@ -94,6 +96,9 @@ func (r SecureBusToolRuntime) Execute(
br = overrideResultText(br, busResp.Result) br = overrideResultText(br, busResp.Result)
} }
results = append(results, br) results = append(results, br)
r.recordRunState(ctx, step, "tool_result", map[string]any{
"tool_name": tc.ToolName,
})
if onResult != nil { if onResult != nil {
if err := onResult(br); err != nil { if err := onResult(br); err != nil {
return results, err return results, err
@ -105,6 +110,13 @@ func (r SecureBusToolRuntime) Execute(
return results, nil return results, nil
} }
func (r SecureBusToolRuntime) recordRunState(ctx context.Context, stepIndex int, state string, snapshot map[string]any) {
if r.StateStore == nil || r.RunID.IsZero() {
return
}
_, _ = r.StateStore.AddRunState(ctx, r.RunID, stepIndex, fantasy.ReActState(state), snapshot)
}
// overrideResultText replaces the text output of a ToolResultContent with // overrideResultText replaces the text output of a ToolResultContent with
// the redacted version produced by the SecureBus. // the redacted version produced by the SecureBus.
func overrideResultText(tr fantasy.ToolResultContent, text string) fantasy.ToolResultContent { func overrideResultText(tr fantasy.ToolResultContent, text string) fantasy.ToolResultContent {

View file

@ -9,9 +9,9 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"charm.land/fantasy" "charm.land/fantasy"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors"
"github.com/sipeed/picoclaw/pkg/memory/sqlc" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/pcerrors" "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
) )
// StateStore persists agent run state snapshots and transition logs. // StateStore persists agent run state snapshots and transition logs.
@ -25,10 +25,10 @@ func NewStateStore(q *sqlc.Queries) *StateStore {
func (s *StateStore) CreateRun(ctx context.Context, conversationID ids.UUID) (sqlc.AgentRun, error) { func (s *StateStore) CreateRun(ctx context.Context, conversationID ids.UUID) (sqlc.AgentRun, error) {
if s == nil || s.q == nil { if s == nil || s.q == nil {
return sqlc.AgentRun{}, pcerrors.New(pcerrors.CodeUnknown, "state store is not configured") return sqlc.AgentRun{}, dserrors.New(dserrors.CodeUnknown, "state store is not configured")
} }
if conversationID.IsZero() { if conversationID.IsZero() {
return sqlc.AgentRun{}, pcerrors.New(pcerrors.CodeUnknown, "conversation id is empty") return sqlc.AgentRun{}, dserrors.New(dserrors.CodeUnknown, "conversation id is empty")
} }
return s.q.CreateAgentRun(ctx, sqlc.CreateAgentRunParams{ return s.q.CreateAgentRun(ctx, sqlc.CreateAgentRunParams{
@ -41,13 +41,13 @@ func (s *StateStore) CreateRun(ctx context.Context, conversationID ids.UUID) (sq
func (s *StateStore) UpdateRunStatus(ctx context.Context, runID ids.UUID, status string, meta map[string]any) (sqlc.AgentRun, error) { func (s *StateStore) UpdateRunStatus(ctx context.Context, runID ids.UUID, status string, meta map[string]any) (sqlc.AgentRun, error) {
if s == nil || s.q == nil { if s == nil || s.q == nil {
return sqlc.AgentRun{}, pcerrors.New(pcerrors.CodeUnknown, "state store is not configured") return sqlc.AgentRun{}, dserrors.New(dserrors.CodeUnknown, "state store is not configured")
} }
if runID.IsZero() { if runID.IsZero() {
return sqlc.AgentRun{}, pcerrors.New(pcerrors.CodeUnknown, "run id is empty") return sqlc.AgentRun{}, dserrors.New(dserrors.CodeUnknown, "run id is empty")
} }
if status == "" { if status == "" {
return sqlc.AgentRun{}, pcerrors.New(pcerrors.CodeUnknown, "status is empty") return sqlc.AgentRun{}, dserrors.New(dserrors.CodeUnknown, "status is empty")
} }
metaJSON := json.RawMessage(`{}`) metaJSON := json.RawMessage(`{}`)
@ -66,13 +66,13 @@ func (s *StateStore) UpdateRunStatus(ctx context.Context, runID ids.UUID, status
func (s *StateStore) AddRunState(ctx context.Context, runID ids.UUID, stepIndex int, state fantasy.ReActState, snapshot any) (sqlc.AgentRunState, error) { func (s *StateStore) AddRunState(ctx context.Context, runID ids.UUID, stepIndex int, state fantasy.ReActState, snapshot any) (sqlc.AgentRunState, error) {
if s == nil || s.q == nil { if s == nil || s.q == nil {
return sqlc.AgentRunState{}, pcerrors.New(pcerrors.CodeUnknown, "state store is not configured") return sqlc.AgentRunState{}, dserrors.New(dserrors.CodeUnknown, "state store is not configured")
} }
if runID.IsZero() { if runID.IsZero() {
return sqlc.AgentRunState{}, pcerrors.New(pcerrors.CodeUnknown, "run id is empty") return sqlc.AgentRunState{}, dserrors.New(dserrors.CodeUnknown, "run id is empty")
} }
if stepIndex < 0 { if stepIndex < 0 {
return sqlc.AgentRunState{}, pcerrors.New(pcerrors.CodeUnknown, "step index is negative") return sqlc.AgentRunState{}, dserrors.New(dserrors.CodeUnknown, "step index is negative")
} }
snapJSON := json.RawMessage(`{}`) snapJSON := json.RawMessage(`{}`)
@ -93,10 +93,10 @@ func (s *StateStore) AddRunState(ctx context.Context, runID ids.UUID, stepIndex
func (s *StateStore) AddTransition(ctx context.Context, runID ids.UUID, t fantasy.ReActTransition) (sqlc.AgentStateTransition, error) { func (s *StateStore) AddTransition(ctx context.Context, runID ids.UUID, t fantasy.ReActTransition) (sqlc.AgentStateTransition, error) {
if s == nil || s.q == nil { if s == nil || s.q == nil {
return sqlc.AgentStateTransition{}, pcerrors.New(pcerrors.CodeUnknown, "state store is not configured") return sqlc.AgentStateTransition{}, dserrors.New(dserrors.CodeUnknown, "state store is not configured")
} }
if runID.IsZero() { if runID.IsZero() {
return sqlc.AgentStateTransition{}, pcerrors.New(pcerrors.CodeUnknown, "run id is empty") return sqlc.AgentStateTransition{}, dserrors.New(dserrors.CodeUnknown, "run id is empty")
} }
metaJSON := json.RawMessage(`{}`) metaJSON := json.RawMessage(`{}`)
@ -140,16 +140,16 @@ func NewCheckpointStore(q *sqlc.Queries) *CheckpointStore {
func (s *CheckpointStore) CreateCheckpoint(ctx context.Context, conversationID ids.UUID, name string, runStateID ids.UUID, meta map[string]any) (sqlc.AgentCheckpoint, error) { func (s *CheckpointStore) CreateCheckpoint(ctx context.Context, conversationID ids.UUID, name string, runStateID ids.UUID, meta map[string]any) (sqlc.AgentCheckpoint, error) {
if s == nil || s.q == nil { if s == nil || s.q == nil {
return sqlc.AgentCheckpoint{}, pcerrors.New(pcerrors.CodeUnknown, "checkpoint store is not configured") return sqlc.AgentCheckpoint{}, dserrors.New(dserrors.CodeUnknown, "checkpoint store is not configured")
} }
if conversationID.IsZero() { if conversationID.IsZero() {
return sqlc.AgentCheckpoint{}, pcerrors.New(pcerrors.CodeUnknown, "conversation id is empty") return sqlc.AgentCheckpoint{}, dserrors.New(dserrors.CodeUnknown, "conversation id is empty")
} }
if strings.TrimSpace(name) == "" { if strings.TrimSpace(name) == "" {
return sqlc.AgentCheckpoint{}, pcerrors.New(pcerrors.CodeUnknown, "checkpoint name is empty") return sqlc.AgentCheckpoint{}, dserrors.New(dserrors.CodeUnknown, "checkpoint name is empty")
} }
if runStateID.IsZero() { if runStateID.IsZero() {
return sqlc.AgentCheckpoint{}, pcerrors.New(pcerrors.CodeUnknown, "run state id is empty") return sqlc.AgentCheckpoint{}, dserrors.New(dserrors.CodeUnknown, "run state id is empty")
} }
metaJSON := json.RawMessage(`{}`) metaJSON := json.RawMessage(`{}`)
@ -170,10 +170,10 @@ func (s *CheckpointStore) CreateCheckpoint(ctx context.Context, conversationID i
func (s *CheckpointStore) ListCheckpoints(ctx context.Context, conversationID ids.UUID) ([]sqlc.AgentCheckpoint, error) { func (s *CheckpointStore) ListCheckpoints(ctx context.Context, conversationID ids.UUID) ([]sqlc.AgentCheckpoint, error) {
if s == nil || s.q == nil { if s == nil || s.q == nil {
return nil, pcerrors.New(pcerrors.CodeUnknown, "checkpoint store is not configured") return nil, dserrors.New(dserrors.CodeUnknown, "checkpoint store is not configured")
} }
if conversationID.IsZero() { if conversationID.IsZero() {
return nil, pcerrors.New(pcerrors.CodeUnknown, "conversation id is empty") return nil, dserrors.New(dserrors.CodeUnknown, "conversation id is empty")
} }
return s.q.ListAgentCheckpointsByConversationID(ctx, sqlc.ListAgentCheckpointsByConversationIDParams{ return s.q.ListAgentCheckpointsByConversationID(ctx, sqlc.ListAgentCheckpointsByConversationIDParams{
ConversationID: conversationID, ConversationID: conversationID,
@ -183,13 +183,13 @@ func (s *CheckpointStore) ListCheckpoints(ctx context.Context, conversationID id
func (s *CheckpointStore) GetCheckpoint(ctx context.Context, conversationID ids.UUID, name string) (sqlc.AgentCheckpoint, error) { func (s *CheckpointStore) GetCheckpoint(ctx context.Context, conversationID ids.UUID, name string) (sqlc.AgentCheckpoint, error) {
if s == nil || s.q == nil { if s == nil || s.q == nil {
return sqlc.AgentCheckpoint{}, pcerrors.New(pcerrors.CodeUnknown, "checkpoint store is not configured") return sqlc.AgentCheckpoint{}, dserrors.New(dserrors.CodeUnknown, "checkpoint store is not configured")
} }
if conversationID.IsZero() { if conversationID.IsZero() {
return sqlc.AgentCheckpoint{}, pcerrors.New(pcerrors.CodeUnknown, "conversation id is empty") return sqlc.AgentCheckpoint{}, dserrors.New(dserrors.CodeUnknown, "conversation id is empty")
} }
if strings.TrimSpace(name) == "" { if strings.TrimSpace(name) == "" {
return sqlc.AgentCheckpoint{}, pcerrors.New(pcerrors.CodeUnknown, "checkpoint name is empty") return sqlc.AgentCheckpoint{}, dserrors.New(dserrors.CodeUnknown, "checkpoint name is empty")
} }
return s.q.GetAgentCheckpointByConversationIDAndName(ctx, sqlc.GetAgentCheckpointByConversationIDAndNameParams{ return s.q.GetAgentCheckpointByConversationIDAndName(ctx, sqlc.GetAgentCheckpointByConversationIDAndNameParams{
ConversationID: conversationID, ConversationID: conversationID,

View file

@ -9,10 +9,10 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/sipeed/picoclaw/pkg/agent" "github.com/ZanzyTHEbar/dragonscale/pkg/agent"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/memory/delegate" "github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate"
"github.com/sipeed/picoclaw/pkg/memory/sqlc" "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
) )
func newTestQueries(t *testing.T) *testDB { func newTestQueries(t *testing.T) *testDB {

View file

@ -9,9 +9,9 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"strings" "strings"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors"
sqlc "github.com/sipeed/picoclaw/pkg/memory/sqlc" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/pcerrors" sqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
) )
// Store wraps the SQLC queries for thread operations. // Store wraps the SQLC queries for thread operations.
@ -38,11 +38,11 @@ type CreateParams struct {
func (s *Store) Create(ctx context.Context, p CreateParams) (sqlc.AgentThread, error) { func (s *Store) Create(ctx context.Context, p CreateParams) (sqlc.AgentThread, error) {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return sqlc.AgentThread{}, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return sqlc.AgentThread{}, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
convID, err := ids.Parse(convIDStr) convID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return sqlc.AgentThread{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return sqlc.AgentThread{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
metaJSON, _ := jsonv2.Marshal(p.Metadata) metaJSON, _ := jsonv2.Marshal(p.Metadata)
@ -66,11 +66,11 @@ type ListParams struct {
func (s *Store) List(ctx context.Context, p ListParams) ([]sqlc.AgentThread, error) { func (s *Store) List(ctx context.Context, p ListParams) ([]sqlc.AgentThread, error) {
convIDStr := strings.TrimSpace(p.ConversationID) convIDStr := strings.TrimSpace(p.ConversationID)
if convIDStr == "" { if convIDStr == "" {
return nil, pcerrors.New(pcerrors.CodeInvalidArgument, "conversation_id is required") return nil, dserrors.New(dserrors.CodeInvalidArgument, "conversation_id is required")
} }
convID, err := ids.Parse(convIDStr) convID, err := ids.Parse(convIDStr)
if err != nil { if err != nil {
return nil, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr) return nil, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse conversation_id %q", convIDStr)
} }
return s.q.ListAgentThreadsByConversationID(ctx, return s.q.ListAgentThreadsByConversationID(ctx,
sqlc.ListAgentThreadsByConversationIDParams{ConversationID: convID}) sqlc.ListAgentThreadsByConversationIDParams{ConversationID: convID})
@ -91,11 +91,11 @@ type AddMessageParams struct {
func (s *Store) AddMessage(ctx context.Context, p AddMessageParams) (sqlc.AgentThreadMessage, error) { func (s *Store) AddMessage(ctx context.Context, p AddMessageParams) (sqlc.AgentThreadMessage, error) {
threadIDStr := strings.TrimSpace(p.ThreadID) threadIDStr := strings.TrimSpace(p.ThreadID)
if threadIDStr == "" { if threadIDStr == "" {
return sqlc.AgentThreadMessage{}, pcerrors.New(pcerrors.CodeInvalidArgument, "thread_id is required") return sqlc.AgentThreadMessage{}, dserrors.New(dserrors.CodeInvalidArgument, "thread_id is required")
} }
threadID, err := ids.Parse(threadIDStr) threadID, err := ids.Parse(threadIDStr)
if err != nil { if err != nil {
return sqlc.AgentThreadMessage{}, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse thread_id %q", threadIDStr) return sqlc.AgentThreadMessage{}, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse thread_id %q", threadIDStr)
} }
role := strings.TrimSpace(p.Role) role := strings.TrimSpace(p.Role)
@ -105,7 +105,7 @@ func (s *Store) AddMessage(ctx context.Context, p AddMessageParams) (sqlc.AgentT
content := strings.TrimSpace(p.Content) content := strings.TrimSpace(p.Content)
if content == "" { if content == "" {
return sqlc.AgentThreadMessage{}, pcerrors.New(pcerrors.CodeInvalidArgument, "content is empty") return sqlc.AgentThreadMessage{}, dserrors.New(dserrors.CodeInvalidArgument, "content is empty")
} }
metaJSON, _ := jsonv2.Marshal(p.Metadata) metaJSON, _ := jsonv2.Marshal(p.Metadata)
@ -134,11 +134,11 @@ type ListMessagesParams struct {
func (s *Store) ListMessages(ctx context.Context, p ListMessagesParams) ([]sqlc.AgentThreadMessage, error) { func (s *Store) ListMessages(ctx context.Context, p ListMessagesParams) ([]sqlc.AgentThreadMessage, error) {
threadIDStr := strings.TrimSpace(p.ThreadID) threadIDStr := strings.TrimSpace(p.ThreadID)
if threadIDStr == "" { if threadIDStr == "" {
return nil, pcerrors.New(pcerrors.CodeInvalidArgument, "thread_id is required") return nil, dserrors.New(dserrors.CodeInvalidArgument, "thread_id is required")
} }
threadID, err := ids.Parse(threadIDStr) threadID, err := ids.Parse(threadIDStr)
if err != nil { if err != nil {
return nil, pcerrors.Wrapf(pcerrors.CodeInvalidArgument, err, "parse thread_id %q", threadIDStr) return nil, dserrors.Wrapf(dserrors.CodeInvalidArgument, err, "parse thread_id %q", threadIDStr)
} }
limit := int64(p.Limit) limit := int64(p.Limit)

View file

@ -9,9 +9,9 @@ import (
"github.com/go-json-experiment/json/jsontext" "github.com/go-json-experiment/json/jsontext"
"charm.land/fantasy" "charm.land/fantasy"
"github.com/sipeed/picoclaw/pkg/ids" "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors"
"github.com/sipeed/picoclaw/pkg/memory/sqlc" "github.com/ZanzyTHEbar/dragonscale/pkg/ids"
"github.com/sipeed/picoclaw/pkg/pcerrors" "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc"
) )
type ToolResultSearchView struct { type ToolResultSearchView struct {
@ -145,7 +145,7 @@ func NewToolResultSearchTool(q *sqlc.Queries, kv KVDelegate) fantasy.AgentTool {
func loadToolResultRows(ctx context.Context, q *sqlc.Queries, input ToolResultSearchInput) ([]sqlc.AgentToolResult, error) { func loadToolResultRows(ctx context.Context, q *sqlc.Queries, input ToolResultSearchInput) ([]sqlc.AgentToolResult, error) {
if q == nil { if q == nil {
return nil, pcerrors.New(pcerrors.CodeUnknown, "db is not configured") return nil, dserrors.New(dserrors.CodeUnknown, "db is not configured")
} }
if strings.TrimSpace(input.RunID) != "" && strings.TrimSpace(input.ToolCallID) != "" { if strings.TrimSpace(input.RunID) != "" && strings.TrimSpace(input.ToolCallID) != "" {
@ -184,7 +184,7 @@ func loadToolResultRows(ctx context.Context, q *sqlc.Queries, input ToolResultSe
}) })
} }
return nil, pcerrors.New(pcerrors.CodeUnknown, "conversation_id or run_id is required (and tool_call_id requires run_id)") return nil, dserrors.New(dserrors.CodeUnknown, "conversation_id or run_id is required (and tool_call_id requires run_id)")
} }
func normalizeLineView(v *ToolResultSearchView) (startLine int, endLine int) { func normalizeLineView(v *ToolResultSearchView) (startLine int, endLine int) {
@ -243,7 +243,7 @@ func normalizeChunkView(v *ToolResultSearchView) (startChunk int, endChunk int)
func loadView(ctx context.Context, kv KVDelegate, row sqlc.AgentToolResult, startLine, endLine, startChunk, endChunk int) (string, map[string]int, error) { func loadView(ctx context.Context, kv KVDelegate, row sqlc.AgentToolResult, startLine, endLine, startChunk, endChunk int) (string, map[string]int, error) {
if kv == nil { if kv == nil {
return "", nil, pcerrors.New(pcerrors.CodeUnknown, "KV delegate is nil") return "", nil, dserrors.New(dserrors.CodeUnknown, "KV delegate is nil")
} }
if row.ChunkCount > 0 { if row.ChunkCount > 0 {

View file

@ -10,7 +10,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/sipeed/picoclaw/pkg/agent" "github.com/ZanzyTHEbar/dragonscale/pkg/agent"
) )
// setupSearchFixture runs a set of tool calls through the OffloadingToolRuntime // setupSearchFixture runs a set of tool calls through the OffloadingToolRuntime

View file

@ -1,48 +1,135 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package agent package agent
import ( import (
"context" "context"
"fmt" "fmt"
"strings"
fantasy "charm.land/fantasy" fantasy "charm.land/fantasy"
picofantasy "github.com/sipeed/picoclaw/pkg/fantasy" picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
memstore "github.com/sipeed/picoclaw/pkg/memory/store" memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
// RunToolLoop executes an agent tool loop using Fantasy with the canonical // MakeUnifiedRunLoopFunc wires subagent execution through the same unified
// PicoToolAdapter (schema unwrapping + offloading). This is the single // runtime stack used by the main loop: SecureBus + offloading + run state.
// implementation used by both main agent and subagents. func MakeUnifiedRunLoopFunc(al *AgentLoop) tools.RunLoopFunc {
func RunToolLoop(ctx context.Context, config tools.ToolLoopConfig, systemPrompt, userPrompt, channel, chatID string) (*tools.ToolLoopResult, error) {
return runToolLoopWithMem(ctx, config, systemPrompt, userPrompt, channel, chatID, nil)
}
// MakeRunLoopFunc returns a RunLoopFunc that uses the given MemoryStore for
// tool result offloading. This is wired into SubagentManager so subagent tool
// results get offloaded to archival memory.
func MakeRunLoopFunc(ms *memstore.MemoryStore) tools.RunLoopFunc {
return func(ctx context.Context, config tools.ToolLoopConfig, systemPrompt, userPrompt, channel, chatID string) (*tools.ToolLoopResult, error) { return func(ctx context.Context, config tools.ToolLoopConfig, systemPrompt, userPrompt, channel, chatID string) (*tools.ToolLoopResult, error) {
return runToolLoopWithMem(ctx, config, systemPrompt, userPrompt, channel, chatID, ms) if al == nil {
return nil, fmt.Errorf("agent loop is nil")
}
if al.secureBus == nil || al.queries == nil || al.kvDelegate == nil || al.stateStore == nil {
return nil, fmt.Errorf("unified runtime dependencies are not initialized")
}
baseSession := fmt.Sprintf("%s:%s", channel, chatID)
if v := al.activeSessionKey.Load(); v != nil {
if active, ok := v.(string); ok && strings.TrimSpace(active) != "" {
baseSession = active
}
}
if strings.TrimSpace(baseSession) == "" {
baseSession = "subagent:default"
}
sessionKey := baseSession + "::subagent"
conversationID, runID, err := al.prepareRuntimeState(ctx, sessionKey)
if err != nil {
return nil, err
}
baseRuntime := OffloadingToolRuntime{
Base: fantasy.DAGToolRuntime{MaxConcurrency: defaultToolMaxConcurrency},
KV: al.kvDelegate,
Queries: al.queries,
ConversationID: conversationID,
RunID: runID,
}
toolRuntime := SecureBusToolRuntime{
Base: baseRuntime,
Bus: al.secureBus,
SessionKey: sessionKey,
StateStore: al.stateStore,
RunID: runID,
}
extraTools := make([]fantasy.AgentTool, 0, 1)
if al.toolResultSearch != nil {
extraTools = append(extraTools, al.toolResultSearch)
}
al.sessions.AddMessage(sessionKey, "user", userPrompt)
result, err := runToolLoopWithRuntime(ctx, config, systemPrompt, userPrompt, channel, chatID, al.memoryStore, sessionKey, toolRuntime, extraTools)
if err != nil {
return nil, err
}
al.sessions.AddMessage(sessionKey, "assistant", result.Content)
al.sessions.Save(sessionKey)
al.maybeSummarize(ctx, sessionKey, channel, chatID)
return result, nil
} }
} }
func runToolLoopWithMem(ctx context.Context, config tools.ToolLoopConfig, systemPrompt, userPrompt, channel, chatID string, ms *memstore.MemoryStore) (*tools.ToolLoopResult, error) { func runToolLoopWithRuntime(
ctx context.Context,
config tools.ToolLoopConfig,
systemPrompt, userPrompt, channel, chatID string,
ms *memstore.MemoryStore,
sessionKey string,
toolRuntime fantasy.ToolRuntime,
extraTools []fantasy.AgentTool,
) (*tools.ToolLoopResult, error) {
if toolRuntime == nil {
return nil, fmt.Errorf("tool runtime is required")
}
adaptCfg := picofantasy.AdaptedToolsConfig{ adaptCfg := picofantasy.AdaptedToolsConfig{
MemStore: ms, MemStore: ms,
AgentID: "picoclaw", AgentID: "dragonscale",
SessionKey: "", SessionKey: sessionKey,
} }
adaptedTools := picofantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg) adaptedTools := picofantasy.BuildAdaptedTools(config.Tools, config.Bus, channel, chatID, adaptCfg)
if len(extraTools) > 0 {
adaptedTools = append(adaptedTools, extraTools...)
}
promotedSet := make(map[string]bool, len(adaptedTools))
for _, at := range adaptedTools {
promotedSet[at.Info().Name] = true
}
agentOpts := []fantasy.AgentOption{ agentOpts := []fantasy.AgentOption{
fantasy.WithTools(adaptedTools...), fantasy.WithTools(adaptedTools...),
fantasy.WithStopConditions(fantasy.StepCountIs(config.MaxIterations)), fantasy.WithStopConditions(fantasy.StepCountIs(config.MaxIterations)),
fantasy.WithToolRuntime(toolRuntime),
}
if config.Tools != nil {
prepareStep := func(ctx context.Context, _ fantasy.PrepareStepFunctionOptions) (context.Context, fantasy.PrepareStepResult, error) {
discovered := config.Tools.DrainDiscovered()
if len(discovered) == 0 {
return ctx, fantasy.PrepareStepResult{}, nil
}
var newTools []tools.Tool
for _, t := range discovered {
if promotedSet[t.Name()] {
continue
}
newTools = append(newTools, t)
promotedSet[t.Name()] = true
}
if len(newTools) == 0 {
return ctx, fantasy.PrepareStepResult{}, nil
}
newAdapted := picofantasy.AdaptTools(newTools, config.Bus, channel, chatID, adaptCfg)
adaptedTools = append(adaptedTools, newAdapted...)
return ctx, fantasy.PrepareStepResult{Tools: adaptedTools}, nil
}
agentOpts = append(agentOpts, fantasy.WithPrepareStep(prepareStep))
} }
if systemPrompt != "" { if systemPrompt != "" {
agentOpts = append(agentOpts, fantasy.WithSystemPrompt(systemPrompt)) agentOpts = append(agentOpts, fantasy.WithSystemPrompt(systemPrompt))

View file

@ -103,7 +103,7 @@ func LoginBrowser(cfg OAuthProviderConfig) (*AuthCredential, error) {
fmt.Printf("Could not open browser automatically.\nPlease open this URL manually:\n\n%s\n\n", authURL) fmt.Printf("Could not open browser automatically.\nPlease open this URL manually:\n\n%s\n\n", authURL)
} }
fmt.Println("If you're running in a headless environment, use: picoclaw auth login --provider openai --device-code") fmt.Println("If you're running in a headless environment, use: dragonscale auth login --provider openai --device-code")
fmt.Println("Waiting for authentication in browser...") fmt.Println("Waiting for authentication in browser...")
select { select {
@ -313,7 +313,7 @@ func buildAuthorizeURL(cfg OAuthProviderConfig, pkce PKCECodes, state, redirectU
"state": {state}, "state": {state},
} }
if strings.Contains(strings.ToLower(cfg.Issuer), "auth.openai.com") { if strings.Contains(strings.ToLower(cfg.Issuer), "auth.openai.com") {
params.Set("originator", "picoclaw") params.Set("originator", "dragonscale")
} }
if cfg.Originator != "" { if cfg.Originator != "" {
params.Set("originator", cfg.Originator) params.Set("originator", cfg.Originator)

View file

@ -38,7 +38,7 @@ func (c *AuthCredential) NeedsRefresh() bool {
func authFilePath() string { func authFilePath() string {
home, _ := os.UserHomeDir() home, _ := os.UserHomeDir()
return filepath.Join(home, ".picoclaw", "auth.json") return filepath.Join(home, ".dragonscale", "auth.json")
} }
func LoadStore() (*AuthStore, error) { func LoadStore() (*AuthStore, error) {

View file

@ -102,7 +102,7 @@ func TestStoreFilePermissions(t *testing.T) {
t.Fatalf("SetCredential() error: %v", err) t.Fatalf("SetCredential() error: %v", err)
} }
path := filepath.Join(tmpDir, ".picoclaw", "auth.json") path := filepath.Join(tmpDir, ".dragonscale", "auth.json")
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil { if err != nil {
t.Fatalf("Stat() error: %v", err) t.Fatalf("Stat() error: %v", err)

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"sync" "sync"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
) )
type MessageBus struct { type MessageBus struct {

View file

@ -6,8 +6,8 @@ import (
"strings" "strings"
"sync/atomic" "sync/atomic"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
) )
type Channel interface { type Channel interface {

View file

@ -1,4 +1,4 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// DingTalk channel implementation using Stream Mode // DingTalk channel implementation using Stream Mode
package channels package channels
@ -8,12 +8,12 @@ import (
"fmt" "fmt"
"sync" "sync"
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/ZanzyTHEbar/dragonscale/pkg/utils"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot" "github.com/open-dingtalk/dingtalk-stream-sdk-go/chatbot"
"github.com/open-dingtalk/dingtalk-stream-sdk-go/client" "github.com/open-dingtalk/dingtalk-stream-sdk-go/client"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/utils"
) )
// DingTalkChannel implements the Channel interface for DingTalk (钉钉) // DingTalkChannel implements the Channel interface for DingTalk (钉钉)
@ -175,7 +175,7 @@ func (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, c
// Convert string content to []byte for the API // Convert string content to []byte for the API
contentBytes := []byte(content) contentBytes := []byte(content)
titleBytes := []byte("PicoClaw") titleBytes := []byte("DragonScale")
// Send markdown formatted reply // Send markdown formatted reply
err := replier.SimpleReplyMarkdown( err := replier.SimpleReplyMarkdown(

View file

@ -7,12 +7,12 @@ import (
"strings" "strings"
"time" "time"
"github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/ZanzyTHEbar/dragonscale/pkg/utils"
"github.com/ZanzyTHEbar/dragonscale/pkg/voice"
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/utils"
"github.com/sipeed/picoclaw/pkg/voice"
) )
const ( const (
@ -50,7 +50,7 @@ func (c *DiscordChannel) SetTranscriber(transcriber *voice.GroqTranscriber) {
func (c *DiscordChannel) getContext() context.Context { func (c *DiscordChannel) getContext() context.Context {
if c.ctx == nil { if c.ctx == nil {
return context.TODO() return context.Background()
} }
return c.ctx return c.ctx
} }

View file

@ -6,8 +6,8 @@ import (
"context" "context"
"errors" "errors"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
) )
// FeishuChannel is a stub implementation for 32-bit architectures // FeishuChannel is a stub implementation for 32-bit architectures

View file

@ -15,10 +15,10 @@ import (
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1" larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
larkws "github.com/larksuite/oapi-sdk-go/v3/ws" larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/ZanzyTHEbar/dragonscale/pkg/utils"
) )
type FeishuChannel struct { type FeishuChannel struct {
@ -109,7 +109,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
ReceiveId(msg.ChatID). ReceiveId(msg.ChatID).
MsgType(larkim.MsgTypeText). MsgType(larkim.MsgTypeText).
Content(string(payload)). Content(string(payload)).
Uuid(fmt.Sprintf("picoclaw-%d", time.Now().UnixNano())). Uuid(fmt.Sprintf("dragonscale-%d", time.Now().UnixNano())).
Build()). Build()).
Build() Build()

View file

@ -17,10 +17,10 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext" "github.com/go-json-experiment/json/jsontext"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/ZanzyTHEbar/dragonscale/pkg/utils"
) )
const ( const (

View file

@ -8,9 +8,9 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
) )
type MaixCamChannel struct { type MaixCamChannel struct {

View file

@ -1,8 +1,8 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot // Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package channels package channels
@ -11,10 +11,10 @@ import (
"fmt" "fmt"
"sync" "sync"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/constants" "github.com/ZanzyTHEbar/dragonscale/pkg/constants"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
) )
type Manager struct { type Manager struct {

View file

@ -12,9 +12,9 @@ import (
"github.com/go-json-experiment/json/jsontext" "github.com/go-json-experiment/json/jsontext"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
) )
type OneBotChannel struct { type OneBotChannel struct {

View file

@ -13,9 +13,9 @@ import (
"github.com/tencent-connect/botgo/token" "github.com/tencent-connect/botgo/token"
"golang.org/x/oauth2" "golang.org/x/oauth2"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
) )
type QQChannel struct { type QQChannel struct {

View file

@ -12,11 +12,11 @@ import (
"github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/slackevents"
"github.com/slack-go/slack/socketmode" "github.com/slack-go/slack/socketmode"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/ZanzyTHEbar/dragonscale/pkg/utils"
"github.com/sipeed/picoclaw/pkg/voice" "github.com/ZanzyTHEbar/dragonscale/pkg/voice"
) )
type SlackChannel struct { type SlackChannel struct {

View file

@ -3,8 +3,8 @@ package channels
import ( import (
"testing" "testing"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
) )
func TestParseSlackChatID(t *testing.T) { func TestParseSlackChatID(t *testing.T) {

View file

@ -17,11 +17,11 @@ import (
"github.com/mymmrac/telego/telegohandler" "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil" tu "github.com/mymmrac/telego/telegoutil"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/ZanzyTHEbar/dragonscale/pkg/utils"
"github.com/sipeed/picoclaw/pkg/voice" "github.com/ZanzyTHEbar/dragonscale/pkg/voice"
) )
type TelegramChannel struct { type TelegramChannel struct {

View file

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
"github.com/sipeed/picoclaw/pkg/config"
) )
type TelegramCommander interface { type TelegramCommander interface {
@ -54,7 +54,7 @@ func (c *cmd) Help(ctx context.Context, message telego.Message) error {
func (c *cmd) Start(ctx context.Context, message telego.Message) error { func (c *cmd) Start(ctx context.Context, message telego.Message) error {
_, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{
ChatID: telego.ChatID{ID: message.Chat.ID}, ChatID: telego.ChatID{ID: message.Chat.ID},
Text: "Hello! I am PicoClaw 🦞", Text: "Hello! I am DragonScale 🦞",
ReplyParameters: &telego.ReplyParameters{ ReplyParameters: &telego.ReplyParameters{
MessageID: message.MessageID, MessageID: message.MessageID,
}, },

View file

@ -10,9 +10,9 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/config" "github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/ZanzyTHEbar/dragonscale/pkg/utils"
) )
type WhatsAppChannel struct { type WhatsAppChannel struct {

View file

@ -15,7 +15,7 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/go-json-experiment/json/jsontext" "github.com/go-json-experiment/json/jsontext"
"github.com/sipeed/picoclaw/pkg/memory" "github.com/ZanzyTHEbar/dragonscale/pkg/memory"
) )
type CronSchedule struct { type CronSchedule struct {

View file

@ -5,12 +5,12 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/constants" "github.com/ZanzyTHEbar/dragonscale/pkg/constants"
"github.com/sipeed/picoclaw/pkg/devices/events" "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events"
"github.com/sipeed/picoclaw/pkg/devices/sources" "github.com/ZanzyTHEbar/dragonscale/pkg/devices/sources"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/sipeed/picoclaw/pkg/state" "github.com/ZanzyTHEbar/dragonscale/pkg/state"
) )
type Service struct { type Service struct {

View file

@ -1,5 +1,5 @@
package devices package devices
import "github.com/sipeed/picoclaw/pkg/devices/events" import "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events"
type EventSource = events.EventSource type EventSource = events.EventSource

View file

@ -10,8 +10,8 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/sipeed/picoclaw/pkg/devices/events" "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
) )
var usbClassToCapability = map[string]string{ var usbClassToCapability = map[string]string{

View file

@ -5,7 +5,7 @@ package sources
import ( import (
"context" "context"
"github.com/sipeed/picoclaw/pkg/devices/events" "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events"
) )
type USBMonitor struct{} type USBMonitor struct{}

View file

@ -1,4 +1,4 @@
package pcerrors package dserrors
import ( import (
"fmt" "fmt"
@ -10,7 +10,7 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
) )
// CLIHandler is the PicoClaw error lifecycle boundary for the CLI. // CLIHandler is the DragonScale error lifecycle boundary for the CLI.
// //
// It renders a user-facing message to Writer and returns an appropriate exit // It renders a user-facing message to Writer and returns an appropriate exit
// code. Optional Verbose and Redact flags layer on debug traces and sensitive- // code. Optional Verbose and Redact flags layer on debug traces and sensitive-

View file

@ -1,4 +1,4 @@
package pcerrors package dserrors
import ( import (
"context" "context"
@ -9,7 +9,7 @@ import (
errbuilder "github.com/ZanzyTHEbar/errbuilder-go" errbuilder "github.com/ZanzyTHEbar/errbuilder-go"
) )
// Code is the canonical PicoClaw error code type. // Code is the canonical DragonScale error code type.
// //
// We intentionally re-export errbuilder's gRPC-inspired code set so callers can // We intentionally re-export errbuilder's gRPC-inspired code set so callers can
// classify errors without inventing ad-hoc sentinels. // classify errors without inventing ad-hoc sentinels.
@ -78,7 +78,7 @@ func WithDetails(m ErrMap) Option {
} }
} }
// New constructs a structured PicoClaw error. // New constructs a structured DragonScale error.
// //
// This returns an *errbuilder.ErrBuilder which: // This returns an *errbuilder.ErrBuilder which:
// - implements error // - implements error

View file

@ -1,7 +1,7 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package fantasy package fantasy
@ -12,14 +12,14 @@ import (
"charm.land/fantasy" "charm.land/fantasy"
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
memstore "github.com/sipeed/picoclaw/pkg/memory/store" memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
// PicoToolAdapter wraps a PicoClaw tool as a Fantasy AgentTool. // PicoToolAdapter wraps a DragonScale tool as a Fantasy AgentTool.
// It bridges PicoClaw's dual-channel ToolResult semantics with Fantasy's // It bridges DragonScale's dual-channel ToolResult semantics with Fantasy's
// simple ToolResponse by publishing ForUser content to the bus as a side effect // simple ToolResponse by publishing ForUser content to the bus as a side effect
// and returning only ForLLM content to Fantasy. // and returning only ForLLM content to Fantasy.
type PicoToolAdapter struct { type PicoToolAdapter struct {
@ -35,9 +35,9 @@ type PicoToolAdapter struct {
// Compile-time check that PicoToolAdapter implements fantasy.AgentTool. // Compile-time check that PicoToolAdapter implements fantasy.AgentTool.
var _ fantasy.AgentTool = (*PicoToolAdapter)(nil) var _ fantasy.AgentTool = (*PicoToolAdapter)(nil)
// Info returns Fantasy-compatible tool metadata from the PicoClaw tool. // Info returns Fantasy-compatible tool metadata from the DragonScale tool.
// Fantasy's ToolInfo expects Parameters to be just the properties map and // Fantasy's ToolInfo expects Parameters to be just the properties map and
// Required to be a separate []string. PicoClaw tools return a full JSON // Required to be a separate []string. DragonScale tools return a full JSON
// Schema object from Parameters() (with "type", "properties", "required" // Schema object from Parameters() (with "type", "properties", "required"
// keys), so we must unwrap it here to avoid double-wrapping in // keys), so we must unwrap it here to avoid double-wrapping in
// agent.prepareTools() and agent.validateToolCall(). // agent.prepareTools() and agent.validateToolCall().
@ -77,14 +77,14 @@ func unwrapSchema(params map[string]interface{}) (map[string]interface{}, []stri
return props, required return props, required
} }
// Run executes the PicoClaw tool and bridges the result to Fantasy. // Run executes the DragonScale tool and bridges the result to Fantasy.
// //
// Side effects: // Side effects:
// - If the tool result has ForUser content and is not Silent, publishes to the bus. // - If the tool result has ForUser content and is not Silent, publishes to the bus.
// - If the tool is a ContextualTool, sets channel/chatID context before execution. // - If the tool is a ContextualTool, sets channel/chatID context before execution.
// - If the tool is an AsyncTool, wires a callback that publishes results to the bus. // - If the tool is an AsyncTool, wires a callback that publishes results to the bus.
func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) { func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fantasy.ToolResponse, error) {
// 1. Deserialize Fantasy's JSON string input into PicoClaw's map format. // 1. Deserialize Fantasy's JSON string input into DragonScale's map format.
args, err := parseToolArgs(call.Input) args, err := parseToolArgs(call.Input)
if err != nil { if err != nil {
return fantasy.NewTextErrorResponse(fmt.Sprintf("invalid arguments: %v", err)), nil return fantasy.NewTextErrorResponse(fmt.Sprintf("invalid arguments: %v", err)), nil
@ -108,7 +108,7 @@ func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fanta
}) })
} }
// 4. Execute the PicoClaw tool. // 4. Execute the DragonScale tool.
result := a.inner.Execute(ctx, args) result := a.inner.Execute(ctx, args)
if result == nil { if result == nil {
return fantasy.NewTextErrorResponse("tool returned nil result"), nil return fantasy.NewTextErrorResponse("tool returned nil result"), nil
@ -146,12 +146,12 @@ func (a *PicoToolAdapter) Run(ctx context.Context, call fantasy.ToolCall) (fanta
return fantasy.NewTextResponse(result.ForLLM), nil return fantasy.NewTextResponse(result.ForLLM), nil
} }
// ProviderOptions returns nil — PicoClaw tools have no provider-specific options. // ProviderOptions returns nil — DragonScale tools have no provider-specific options.
func (a *PicoToolAdapter) ProviderOptions() fantasy.ProviderOptions { func (a *PicoToolAdapter) ProviderOptions() fantasy.ProviderOptions {
return fantasy.ProviderOptions{} return fantasy.ProviderOptions{}
} }
// SetProviderOptions is a no-op for PicoClaw tools. // SetProviderOptions is a no-op for DragonScale tools.
func (a *PicoToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {} func (a *PicoToolAdapter) SetProviderOptions(_ fantasy.ProviderOptions) {}
// AdaptedToolsConfig configures how tools are adapted for the Fantasy agent. // AdaptedToolsConfig configures how tools are adapted for the Fantasy agent.

View file

@ -5,8 +5,8 @@ import (
"testing" "testing"
"charm.land/fantasy" "charm.land/fantasy"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
// --- Mock tool implementations --- // --- Mock tool implementations ---

View file

@ -1,7 +1,7 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package fantasy package fantasy

View file

@ -1,7 +1,7 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package fantasy package fantasy
@ -9,10 +9,10 @@ import (
"charm.land/fantasy" "charm.land/fantasy"
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/sipeed/picoclaw/pkg/messages" "github.com/ZanzyTHEbar/dragonscale/pkg/messages"
) )
// MessagesToFantasy converts PicoClaw session messages to Fantasy's multipart format. // MessagesToFantasy converts DragonScale session messages to Fantasy's multipart format.
func MessagesToFantasy(msgs []messages.Message) []fantasy.Message { func MessagesToFantasy(msgs []messages.Message) []fantasy.Message {
out := make([]fantasy.Message, 0, len(msgs)) out := make([]fantasy.Message, 0, len(msgs))
@ -23,7 +23,7 @@ func MessagesToFantasy(msgs []messages.Message) []fantasy.Message {
return out return out
} }
// MessageToFantasy converts a single PicoClaw message to Fantasy format. // MessageToFantasy converts a single DragonScale message to Fantasy format.
func MessageToFantasy(msg messages.Message) fantasy.Message { func MessageToFantasy(msg messages.Message) fantasy.Message {
var parts []fantasy.MessagePart var parts []fantasy.MessagePart
@ -72,7 +72,7 @@ func MessageToFantasy(msg messages.Message) fantasy.Message {
} }
} }
// StepToMessages converts a Fantasy StepResult back to PicoClaw message format // StepToMessages converts a Fantasy StepResult back to DragonScale message format
// for session storage. Each step may produce an assistant message with tool calls // for session storage. Each step may produce an assistant message with tool calls
// and zero or more tool result messages. // and zero or more tool result messages.
func StepToMessages(step fantasy.StepResult) []messages.Message { func StepToMessages(step fantasy.StepResult) []messages.Message {
@ -129,7 +129,7 @@ func StepToMessages(step fantasy.StepResult) []messages.Message {
return out return out
} }
// AgentResultToMessages converts a complete AgentResult to PicoClaw messages. // AgentResultToMessages converts a complete AgentResult to DragonScale messages.
// This flattens all steps into a single message sequence. // This flattens all steps into a single message sequence.
func AgentResultToMessages(result *fantasy.AgentResult) []messages.Message { func AgentResultToMessages(result *fantasy.AgentResult) []messages.Message {
var out []messages.Message var out []messages.Message

View file

@ -5,7 +5,7 @@ import (
"testing" "testing"
"charm.land/fantasy" "charm.land/fantasy"
"github.com/sipeed/picoclaw/pkg/messages" "github.com/ZanzyTHEbar/dragonscale/pkg/messages"
) )
// --- MessagesToFantasy Tests --- // --- MessagesToFantasy Tests ---
@ -417,7 +417,7 @@ func TestAgentResultToMessages_MultipleSteps(t *testing.T) {
// --- Round-trip fidelity test --- // --- Round-trip fidelity test ---
func TestRoundTrip_PicoClawToFantasyAndBack(t *testing.T) { func TestRoundTrip_PicoClawToFantasyAndBack(t *testing.T) {
// Start with PicoClaw messages representing a typical conversation // Start with DragonScale messages representing a typical conversation
original := []messages.Message{ original := []messages.Message{
{Role: "user", Content: "Read the file"}, {Role: "user", Content: "Read the file"},
{ {

View file

@ -1,7 +1,7 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package fantasy package fantasy
@ -15,8 +15,8 @@ import (
"charm.land/fantasy" "charm.land/fantasy"
"charm.land/fantasy/providers/openaicompat" "charm.land/fantasy/providers/openaicompat"
"github.com/ZanzyTHEbar/dragonscale/pkg/config"
"github.com/openai/openai-go/v2/option" "github.com/openai/openai-go/v2/option"
"github.com/sipeed/picoclaw/pkg/config"
) )
// providerEntry describes a single LLM provider: how to identify it, how to // providerEntry describes a single LLM provider: how to identify it, how to
@ -191,7 +191,7 @@ func (e *providerEntry) resolveBase(cfg *config.Config) string {
return e.defaultBase return e.defaultBase
} }
// CreateProvider builds a Fantasy provider from PicoClaw config. // CreateProvider builds a Fantasy provider from DragonScale config.
func CreateProvider(cfg *config.Config) (fantasy.Provider, error) { func CreateProvider(cfg *config.Config) (fantasy.Provider, error) {
providerName := strings.ToLower(cfg.Agents.Defaults.Provider) providerName := strings.ToLower(cfg.Agents.Defaults.Provider)
model := cfg.Agents.Defaults.Model model := cfg.Agents.Defaults.Model
@ -333,7 +333,7 @@ func defaultIfEmpty(val, fallback string) string {
func providerNameOrDefault(name string) string { func providerNameOrDefault(name string) string {
if name == "" { if name == "" {
return "picoclaw" return "dragonscale"
} }
return name return name
} }

View file

@ -1,8 +1,8 @@
// PicoClaw - Ultra-lightweight personal AI agent // DragonScale - Ultra-lightweight personal AI agent
// Inspired by and based on nanobot: https://github.com/HKUDS/nanobot // Inspired by and based on nanobot: https://github.com/HKUDS/nanobot
// License: MIT // License: MIT
// //
// Copyright (c) 2026 PicoClaw contributors // Copyright (c) 2026 DragonScale contributors
package heartbeat package heartbeat
@ -14,11 +14,11 @@ import (
"sync" "sync"
"time" "time"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/ZanzyTHEbar/dragonscale/pkg/bus"
"github.com/sipeed/picoclaw/pkg/constants" "github.com/ZanzyTHEbar/dragonscale/pkg/constants"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/ZanzyTHEbar/dragonscale/pkg/logger"
"github.com/sipeed/picoclaw/pkg/state" "github.com/ZanzyTHEbar/dragonscale/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
const ( const (
@ -31,12 +31,17 @@ const (
// channel and chatID are derived from the last active user channel. // channel and chatID are derived from the last active user channel.
type HeartbeatHandler func(prompt, channel, chatID string) *tools.ToolResult type HeartbeatHandler func(prompt, channel, chatID string) *tools.ToolResult
// DueContextProvider returns additional heartbeat prompt context for due
// obligations/reminders discovered by scheduler-integrated checks.
type DueContextProvider func(now time.Time) (string, error)
// HeartbeatService manages periodic heartbeat checks // HeartbeatService manages periodic heartbeat checks
type HeartbeatService struct { type HeartbeatService struct {
workspace string workspace string
bus *bus.MessageBus bus *bus.MessageBus
state *state.Manager state *state.Manager
handler HeartbeatHandler handler HeartbeatHandler
dueCtx DueContextProvider
interval time.Duration interval time.Duration
enabled bool enabled bool
mu sync.RWMutex mu sync.RWMutex
@ -76,6 +81,14 @@ func (hs *HeartbeatService) SetHandler(handler HeartbeatHandler) {
hs.handler = handler hs.handler = handler
} }
// SetDueContextProvider registers a callback that contributes due-obligation
// context to heartbeat prompts.
func (hs *HeartbeatService) SetDueContextProvider(provider DueContextProvider) {
hs.mu.Lock()
defer hs.mu.Unlock()
hs.dueCtx = provider
}
// Start begins the heartbeat service // Start begins the heartbeat service
func (hs *HeartbeatService) Start() error { func (hs *HeartbeatService) Start() error {
hs.mu.Lock() hs.mu.Lock()
@ -148,6 +161,7 @@ func (hs *HeartbeatService) executeHeartbeat() {
enabled := hs.enabled enabled := hs.enabled
stopped := hs.stopChan == nil stopped := hs.stopChan == nil
handler := hs.handler handler := hs.handler
dueCtxProvider := hs.dueCtx
hs.mu.RUnlock() hs.mu.RUnlock()
if !enabled || stopped { if !enabled || stopped {
@ -156,9 +170,19 @@ func (hs *HeartbeatService) executeHeartbeat() {
logger.DebugC("heartbeat", "Executing heartbeat") logger.DebugC("heartbeat", "Executing heartbeat")
prompt := hs.buildPrompt() dueContext := ""
if dueCtxProvider != nil {
ctx, err := dueCtxProvider(time.Now().UTC())
if err != nil {
hs.logError("Due obligation check failed: %v", err)
} else {
dueContext = strings.TrimSpace(ctx)
}
}
prompt := hs.buildPrompt(dueContext)
if prompt == "" { if prompt == "" {
logger.InfoC("heartbeat", "No heartbeat prompt (HEARTBEAT.md empty or missing)") logger.InfoC("heartbeat", "No heartbeat prompt or due obligations")
return return
} }
@ -213,23 +237,28 @@ func (hs *HeartbeatService) executeHeartbeat() {
} }
// buildPrompt builds the heartbeat prompt from HEARTBEAT.md // buildPrompt builds the heartbeat prompt from HEARTBEAT.md
func (hs *HeartbeatService) buildPrompt() string { func (hs *HeartbeatService) buildPrompt(dueContext string) string {
heartbeatPath := filepath.Join(hs.workspace, "HEARTBEAT.md") heartbeatPath := filepath.Join(hs.workspace, "HEARTBEAT.md")
data, err := os.ReadFile(heartbeatPath) data, err := os.ReadFile(heartbeatPath)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
hs.createDefaultHeartbeatTemplate() hs.createDefaultHeartbeatTemplate()
return "" data = []byte{}
} } else {
hs.logError("Error reading HEARTBEAT.md: %v", err) hs.logError("Error reading HEARTBEAT.md: %v", err)
return "" return ""
} }
}
content := string(data) content := strings.TrimSpace(string(data))
if len(content) == 0 { dueContext = strings.TrimSpace(dueContext)
if content == "" && dueContext == "" {
return "" return ""
} }
if dueContext == "" {
dueContext = "None."
}
now := time.Now().Format("2006-01-02 15:04:05") now := time.Now().Format("2006-01-02 15:04:05")
return fmt.Sprintf(`# Heartbeat Check return fmt.Sprintf(`# Heartbeat Check
@ -240,8 +269,11 @@ You are a proactive AI assistant. This is a scheduled heartbeat check.
Review the following tasks and execute any necessary actions using available skills. Review the following tasks and execute any necessary actions using available skills.
If there is nothing that requires attention, respond ONLY with: HEARTBEAT_OK If there is nothing that requires attention, respond ONLY with: HEARTBEAT_OK
## Due Obligations
%s %s
`, now, content)
%s
`, now, dueContext, content)
} }
// createDefaultHeartbeatTemplate creates the default HEARTBEAT.md file // createDefaultHeartbeatTemplate creates the default HEARTBEAT.md file

View file

@ -3,10 +3,11 @@ package heartbeat
import ( import (
"os" "os"
"path/filepath" "path/filepath"
"strings"
"testing" "testing"
"time" "time"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
func TestExecuteHeartbeat_Async(t *testing.T) { func TestExecuteHeartbeat_Async(t *testing.T) {
@ -211,7 +212,7 @@ func TestHeartbeatFilePath(t *testing.T) {
hs := NewHeartbeatService(tmpDir, 30, true) hs := NewHeartbeatService(tmpDir, 30, true)
// Trigger default template creation // Trigger default template creation
hs.buildPrompt() hs.buildPrompt("")
// Verify HEARTBEAT.md exists at workspace root // Verify HEARTBEAT.md exists at workspace root
expectedPath := filepath.Join(tmpDir, "HEARTBEAT.md") expectedPath := filepath.Join(tmpDir, "HEARTBEAT.md")
@ -219,3 +220,40 @@ func TestHeartbeatFilePath(t *testing.T) {
t.Errorf("Expected HEARTBEAT.md at %s, but it doesn't exist", expectedPath) t.Errorf("Expected HEARTBEAT.md at %s, but it doesn't exist", expectedPath)
} }
} }
func TestExecuteHeartbeat_UsesDueContextWithoutHeartbeatFile(t *testing.T) {
tmpDir, err := os.MkdirTemp("", "heartbeat-test-*")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
defer os.RemoveAll(tmpDir)
hs := NewHeartbeatService(tmpDir, 30, true)
hs.stopChan = make(chan struct{}) // Enable for testing
dueCalled := false
handlerCalled := false
hs.SetDueContextProvider(func(now time.Time) (string, error) {
dueCalled = true
return "- [obl-1] send follow-up message", nil
})
hs.SetHandler(func(prompt, channel, chatID string) *tools.ToolResult {
handlerCalled = true
if prompt == "" {
t.Fatal("expected heartbeat prompt with due context")
}
if !strings.Contains(prompt, "obl-1") {
t.Fatalf("expected due context in prompt, got: %s", prompt)
}
return tools.SilentResult("HEARTBEAT_OK")
})
hs.executeHeartbeat()
if !dueCalled {
t.Fatal("expected due context provider to be called")
}
if !handlerCalled {
t.Fatal("expected heartbeat handler to be called")
}
}

View file

@ -1,4 +1,4 @@
// PicoClaw Isolated Tool Runtime — Command Protocol // DragonScale Isolated Tool Runtime — Command Protocol
// Namespace: itrfb (FlatBuffers generated types) // Namespace: itrfb (FlatBuffers generated types)
// //
// This schema defines the binary command protocol between the agent loop // This schema defines the binary command protocol between the agent loop

View file

@ -1,4 +1,4 @@
// Package itr defines the binary command protocol for the PicoClaw Isolated // Package itr defines the binary command protocol for the DragonScale Isolated
// Tool Runtime (ITR). All tool invocations — traditional tools and RLM // Tool Runtime (ITR). All tool invocations — traditional tools and RLM
// recursive decomposition operations — are serialized as ToolRequest / // recursive decomposition operations — are serialized as ToolRequest /
// ToolResponse pairs. // ToolResponse pairs.

View file

@ -1,4 +1,4 @@
// Package dag implements the DAG Task Executor for PicoClaw. It receives a // Package dag implements the DAG Task Executor for DragonScale. It receives a
// DAGPlan (a set of nodes with dependency edges), dispatches them in // DAGPlan (a set of nodes with dependency edges), dispatches them in
// topological wave order via the SecureBus, and synthesises a final result // topological wave order via the SecureBus, and synthesises a final result
// using the Joiner pattern. // using the Joiner pattern.
@ -18,8 +18,8 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/sipeed/picoclaw/pkg/itr" "github.com/ZanzyTHEbar/dragonscale/pkg/itr"
"github.com/sipeed/picoclaw/pkg/security/securebus" "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
) )
// JoinerFunc synthesises a final answer from all node results. // JoinerFunc synthesises a final answer from all node results.

View file

@ -6,10 +6,10 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"testing" "testing"
"github.com/sipeed/picoclaw/pkg/itr" "github.com/ZanzyTHEbar/dragonscale/pkg/itr"
"github.com/sipeed/picoclaw/pkg/itr/dag" "github.com/ZanzyTHEbar/dragonscale/pkg/itr/dag"
"github.com/sipeed/picoclaw/pkg/security/securebus" "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View file

@ -5,8 +5,8 @@ import (
"fmt" "fmt"
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"github.com/sipeed/picoclaw/pkg/itr" "github.com/ZanzyTHEbar/dragonscale/pkg/itr"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/ZanzyTHEbar/dragonscale/pkg/tools"
) )
// PlannerFunc calls the LLM with a system prompt and user query, returning // PlannerFunc calls the LLM with a system prompt and user query, returning

View file

@ -5,7 +5,7 @@ import (
jsonv2 "github.com/go-json-experiment/json" jsonv2 "github.com/go-json-experiment/json"
"testing" "testing"
"github.com/sipeed/picoclaw/pkg/itr" "github.com/ZanzyTHEbar/dragonscale/pkg/itr"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/sipeed/picoclaw/pkg/itr" "github.com/ZanzyTHEbar/dragonscale/pkg/itr"
) )
// ReplanConfig configures the iterative replanning loop. // ReplanConfig configures the iterative replanning loop.

View file

@ -3,8 +3,8 @@ package itr
import ( import (
"fmt" "fmt"
"github.com/ZanzyTHEbar/dragonscale/pkg/itr/itrfb"
flatbuffers "github.com/google/flatbuffers/go" flatbuffers "github.com/google/flatbuffers/go"
"github.com/sipeed/picoclaw/pkg/itr/itrfb"
) )
// ── Domain ↔ FlatBuffers enum mapping ─────────────────────────────────────── // ── Domain ↔ FlatBuffers enum mapping ───────────────────────────────────────
@ -67,7 +67,7 @@ func MarshalRequestFB(r ToolRequest) ([]byte, error) {
itrfb.ToolRequestAddSessionKey(b, skOff) itrfb.ToolRequestAddSessionKey(b, skOff)
itrfb.ToolRequestAddToolCallId(b, tcOff) itrfb.ToolRequestAddToolCallId(b, tcOff)
reqOff := itrfb.ToolRequestEnd(b) reqOff := itrfb.ToolRequestEnd(b)
itrfb.FinishToolRequestBuffer(b, reqOff) b.Finish(reqOff)
return b.FinishedBytes(), nil return b.FinishedBytes(), nil
} }
@ -86,7 +86,7 @@ func MarshalRequestFB(r ToolRequest) ([]byte, error) {
itrfb.ToolRequestAddSessionKey(b, skOff) itrfb.ToolRequestAddSessionKey(b, skOff)
itrfb.ToolRequestAddToolCallId(b, tcOff) itrfb.ToolRequestAddToolCallId(b, tcOff)
reqOff := itrfb.ToolRequestEnd(b) reqOff := itrfb.ToolRequestEnd(b)
itrfb.FinishToolRequestBuffer(b, reqOff) b.Finish(reqOff)
return b.FinishedBytes(), nil return b.FinishedBytes(), nil
} }
@ -161,7 +161,7 @@ func MarshalResponseFB(r ToolResponse) ([]byte, error) {
itrfb.ToolResponseAddRedactedKeys(b, keysOff) itrfb.ToolResponseAddRedactedKeys(b, keysOff)
} }
respOff := itrfb.ToolResponseEnd(b) respOff := itrfb.ToolResponseEnd(b)
itrfb.FinishToolResponseBuffer(b, respOff) b.Finish(respOff)
return b.FinishedBytes(), nil return b.FinishedBytes(), nil
} }

View file

@ -4,7 +4,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/sipeed/picoclaw/pkg/itr" "github.com/ZanzyTHEbar/dragonscale/pkg/itr"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )

View file

@ -17,10 +17,6 @@ func GetRootAsCodeExec(buf []byte, offset flatbuffers.UOffsetT) *CodeExec {
return x return x
} }
func FinishCodeExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsCodeExec(buf []byte, offset flatbuffers.UOffsetT) *CodeExec { func GetSizePrefixedRootAsCodeExec(buf []byte, offset flatbuffers.UOffsetT) *CodeExec {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &CodeExec{} x := &CodeExec{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsCodeExec(buf []byte, offset flatbuffers.UOffsetT) *Cod
return x return x
} }
func FinishSizePrefixedCodeExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *CodeExec) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *CodeExec) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsDAGNode(buf []byte, offset flatbuffers.UOffsetT) *DAGNode {
return x return x
} }
func FinishDAGNodeBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsDAGNode(buf []byte, offset flatbuffers.UOffsetT) *DAGNode { func GetSizePrefixedRootAsDAGNode(buf []byte, offset flatbuffers.UOffsetT) *DAGNode {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &DAGNode{} x := &DAGNode{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsDAGNode(buf []byte, offset flatbuffers.UOffsetT) *DAGN
return x return x
} }
func FinishSizePrefixedDAGNodeBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *DAGNode) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *DAGNode) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsDAGPlan(buf []byte, offset flatbuffers.UOffsetT) *DAGPlan {
return x return x
} }
func FinishDAGPlanBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsDAGPlan(buf []byte, offset flatbuffers.UOffsetT) *DAGPlan { func GetSizePrefixedRootAsDAGPlan(buf []byte, offset flatbuffers.UOffsetT) *DAGPlan {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &DAGPlan{} x := &DAGPlan{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsDAGPlan(buf []byte, offset flatbuffers.UOffsetT) *DAGP
return x return x
} }
func FinishSizePrefixedDAGPlanBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *DAGPlan) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *DAGPlan) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsExecWasm(buf []byte, offset flatbuffers.UOffsetT) *ExecWasm {
return x return x
} }
func FinishExecWasmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsExecWasm(buf []byte, offset flatbuffers.UOffsetT) *ExecWasm { func GetSizePrefixedRootAsExecWasm(buf []byte, offset flatbuffers.UOffsetT) *ExecWasm {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ExecWasm{} x := &ExecWasm{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsExecWasm(buf []byte, offset flatbuffers.UOffsetT) *Exe
return x return x
} }
func FinishSizePrefixedExecWasmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ExecWasm) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *ExecWasm) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsFinal(buf []byte, offset flatbuffers.UOffsetT) *Final {
return x return x
} }
func FinishFinalBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsFinal(buf []byte, offset flatbuffers.UOffsetT) *Final { func GetSizePrefixedRootAsFinal(buf []byte, offset flatbuffers.UOffsetT) *Final {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &Final{} x := &Final{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsFinal(buf []byte, offset flatbuffers.UOffsetT) *Final
return x return x
} }
func FinishSizePrefixedFinalBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *Final) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *Final) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsGrep(buf []byte, offset flatbuffers.UOffsetT) *Grep {
return x return x
} }
func FinishGrepBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsGrep(buf []byte, offset flatbuffers.UOffsetT) *Grep { func GetSizePrefixedRootAsGrep(buf []byte, offset flatbuffers.UOffsetT) *Grep {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &Grep{} x := &Grep{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsGrep(buf []byte, offset flatbuffers.UOffsetT) *Grep {
return x return x
} }
func FinishSizePrefixedGrepBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *Grep) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *Grep) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsPartition(buf []byte, offset flatbuffers.UOffsetT) *Partition {
return x return x
} }
func FinishPartitionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsPartition(buf []byte, offset flatbuffers.UOffsetT) *Partition { func GetSizePrefixedRootAsPartition(buf []byte, offset flatbuffers.UOffsetT) *Partition {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &Partition{} x := &Partition{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsPartition(buf []byte, offset flatbuffers.UOffsetT) *Pa
return x return x
} }
func FinishSizePrefixedPartitionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *Partition) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *Partition) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsPeek(buf []byte, offset flatbuffers.UOffsetT) *Peek {
return x return x
} }
func FinishPeekBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsPeek(buf []byte, offset flatbuffers.UOffsetT) *Peek { func GetSizePrefixedRootAsPeek(buf []byte, offset flatbuffers.UOffsetT) *Peek {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &Peek{} x := &Peek{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsPeek(buf []byte, offset flatbuffers.UOffsetT) *Peek {
return x return x
} }
func FinishSizePrefixedPeekBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *Peek) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *Peek) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsRecurse(buf []byte, offset flatbuffers.UOffsetT) *Recurse {
return x return x
} }
func FinishRecurseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsRecurse(buf []byte, offset flatbuffers.UOffsetT) *Recurse { func GetSizePrefixedRootAsRecurse(buf []byte, offset flatbuffers.UOffsetT) *Recurse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &Recurse{} x := &Recurse{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsRecurse(buf []byte, offset flatbuffers.UOffsetT) *Recu
return x return x
} }
func FinishSizePrefixedRecurseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *Recurse) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *Recurse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsToolExec(buf []byte, offset flatbuffers.UOffsetT) *ToolExec {
return x return x
} }
func FinishToolExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsToolExec(buf []byte, offset flatbuffers.UOffsetT) *ToolExec { func GetSizePrefixedRootAsToolExec(buf []byte, offset flatbuffers.UOffsetT) *ToolExec {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ToolExec{} x := &ToolExec{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsToolExec(buf []byte, offset flatbuffers.UOffsetT) *Too
return x return x
} }
func FinishSizePrefixedToolExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ToolExec) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *ToolExec) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsToolRequest(buf []byte, offset flatbuffers.UOffsetT) *ToolRequest
return x return x
} }
func FinishToolRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsToolRequest(buf []byte, offset flatbuffers.UOffsetT) *ToolRequest { func GetSizePrefixedRootAsToolRequest(buf []byte, offset flatbuffers.UOffsetT) *ToolRequest {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ToolRequest{} x := &ToolRequest{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsToolRequest(buf []byte, offset flatbuffers.UOffsetT) *
return x return x
} }
func FinishSizePrefixedToolRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ToolRequest) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *ToolRequest) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

View file

@ -17,10 +17,6 @@ func GetRootAsToolResponse(buf []byte, offset flatbuffers.UOffsetT) *ToolRespons
return x return x
} }
func FinishToolResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.Finish(offset)
}
func GetSizePrefixedRootAsToolResponse(buf []byte, offset flatbuffers.UOffsetT) *ToolResponse { func GetSizePrefixedRootAsToolResponse(buf []byte, offset flatbuffers.UOffsetT) *ToolResponse {
n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:])
x := &ToolResponse{} x := &ToolResponse{}
@ -28,10 +24,6 @@ func GetSizePrefixedRootAsToolResponse(buf []byte, offset flatbuffers.UOffsetT)
return x return x
} }
func FinishSizePrefixedToolResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) {
builder.FinishSizePrefixed(offset)
}
func (rcv *ToolResponse) Init(buf []byte, i flatbuffers.UOffsetT) { func (rcv *ToolResponse) Init(buf []byte, i flatbuffers.UOffsetT) {
rcv._tab.Bytes = buf rcv._tab.Bytes = buf
rcv._tab.Pos = i rcv._tab.Pos = i

Some files were not shown because too many files have changed in this diff Show more