From db4d98a1d47335ebe0288f4e88ef12367a1c44c9 Mon Sep 17 00:00:00 2001 From: ZanzyTHEbar Date: Sat, 21 Feb 2026 18:56:35 +0000 Subject: [PATCH] chore!: rename module from github.com/sipeed/picoclaw to github.com/ZanzyTHEbar/dragonscale MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .dockerignore | 2 +- .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/workflows/docker-build.yml | 2 +- .github/workflows/pr.yml | 35 +- .gitignore | 10 +- .goreleaser.yaml | 12 +- Dockerfile | 14 +- Dockerfile.goreleaser | 4 +- Makefile | 118 ++- README.ja.md | 775 --------------- README.pt-br.md | 881 ------------------ README.zh.md | 744 --------------- cmd/{picoclaw => dragonscale}/main.go | 241 +++-- config/config.example.json | 2 +- docker-compose.yml | 28 +- docs/adr/001-isolated-tool-runtime.md | 61 +- docs/adr/README.md | 14 +- docs/picoclaw_community_roadmap_260216.md | 112 --- eval/README.md | 23 +- eval/cases/edge_cases.yaml | 2 +- eval/cases/memory_ops.yaml | 2 +- eval/cases/meta_tools.yaml | 2 +- eval/cases/multi_step.yaml | 2 +- eval/cases/tool_calling.yaml | 16 +- eval/cmd/eval-runner/main.go | 8 +- eval/fixtures/sample_data.txt | 4 +- eval/fixtures/skills/eval-test-skill/SKILL.md | 2 +- eval/go_evals/eval_test.go | 14 +- eval/promptfooconfig.yaml | 8 +- eval/scripts/compare.sh | 4 +- go.mod | 2 +- internal/fantasy/VENDORING.md | 7 +- pkg/agent/conversations/store.go | 50 +- pkg/agent/kv_delegate_test.go | 2 +- pkg/agent/memgpt_tool.go | 12 +- pkg/agent/mentions/store.go | 24 +- pkg/agent/offloading_runtime_test.go | 4 +- pkg/agent/offloading_tool_runtime.go | 12 +- pkg/agent/securebus_runtime.go | 62 +- pkg/agent/state_store.go | 44 +- pkg/agent/state_store_test.go | 8 +- pkg/agent/threads/store.go | 24 +- pkg/agent/tool_result_search.go | 12 +- pkg/agent/tool_result_search_test.go | 2 +- pkg/agent/toolloop.go | 129 ++- pkg/auth/oauth.go | 4 +- pkg/auth/store.go | 2 +- pkg/auth/store_test.go | 2 +- pkg/bus/bus.go | 2 +- pkg/channels/base.go | 4 +- pkg/channels/dingtalk.go | 12 +- pkg/channels/discord.go | 12 +- pkg/channels/feishu_32.go | 4 +- pkg/channels/feishu_64.go | 10 +- pkg/channels/line.go | 8 +- pkg/channels/maixcam.go | 6 +- pkg/channels/manager.go | 12 +- pkg/channels/onebot.go | 6 +- pkg/channels/qq.go | 6 +- pkg/channels/slack.go | 10 +- pkg/channels/slack_test.go | 4 +- pkg/channels/telegram.go | 10 +- pkg/channels/telegram_commands.go | 4 +- pkg/channels/whatsapp.go | 6 +- pkg/cron/service.go | 2 +- pkg/devices/service.go | 12 +- pkg/devices/source.go | 2 +- pkg/devices/sources/usb_linux.go | 4 +- pkg/devices/sources/usb_stub.go | 2 +- pkg/{pcerrors => dserrors}/cli.go | 4 +- pkg/{pcerrors => dserrors}/pcerrors.go | 6 +- pkg/fantasy/adapter.go | 30 +- pkg/fantasy/adapter_test.go | 4 +- pkg/fantasy/claude_cli.go | 4 +- pkg/fantasy/convert.go | 14 +- pkg/fantasy/convert_test.go | 4 +- pkg/fantasy/factory.go | 10 +- pkg/heartbeat/service.go | 62 +- pkg/heartbeat/service_test.go | 42 +- pkg/itr/commands.fbs | 2 +- pkg/itr/commands.go | 2 +- pkg/itr/dag/executor.go | 6 +- pkg/itr/dag/executor_test.go | 8 +- pkg/itr/dag/planner.go | 4 +- pkg/itr/dag/planner_test.go | 2 +- pkg/itr/dag/replan.go | 2 +- pkg/itr/fb_codec.go | 8 +- pkg/itr/fb_codec_test.go | 2 +- pkg/itr/itrfb/CodeExec.go | 8 - pkg/itr/itrfb/DAGNode.go | 8 - pkg/itr/itrfb/DAGPlan.go | 8 - pkg/itr/itrfb/ExecWasm.go | 8 - pkg/itr/itrfb/Final.go | 8 - pkg/itr/itrfb/Grep.go | 8 - pkg/itr/itrfb/Partition.go | 8 - pkg/itr/itrfb/Peek.go | 8 - pkg/itr/itrfb/Recurse.go | 8 - pkg/itr/itrfb/ToolExec.go | 8 - pkg/itr/itrfb/ToolRequest.go | 8 - pkg/itr/itrfb/ToolResponse.go | 8 - pkg/itr/itrfb/ToolSearch.go | 8 - pkg/itr/wasm/transport.go | 2 +- pkg/itr/wasm/transport_test.go | 2 +- pkg/memory/dag/compress.go | 6 +- pkg/memory/delegate/factory.go | 4 +- pkg/memory/delegate/factory_test.go | 6 +- pkg/memory/delegate/search_fts.go | 2 +- pkg/memory/delegate/search_vector.go | 4 +- pkg/memory/delegate/sqlite.go | 14 +- pkg/memory/delegate/sqlite_audit_test.go | 4 +- pkg/memory/delegate/sqlite_batch.go | 2 +- pkg/memory/delegate/sqlite_bench_test.go | 4 +- pkg/memory/delegate/sqlite_doc_test.go | 4 +- .../delegate/sqlite_integration_test.go | 14 +- .../delegate/sqlite_session_msg_test.go | 4 +- pkg/memory/delegate/sqlite_test.go | 4 +- pkg/memory/integration_test.go | 8 +- pkg/memory/memory.go | 4 +- pkg/memory/migrate_dailynotes.go | 2 +- pkg/memory/migrate_documents.go | 2 +- pkg/memory/migrate_sessions.go | 4 +- pkg/memory/migrate_sessions_test.go | 20 +- pkg/memory/observation/manager.go | 2 +- pkg/memory/observation/store.go | 2 +- pkg/memory/sqlc/agent_audit_log.sql.go | 2 +- .../sqlc/agent_conversation_forks.sql.go | 2 +- .../sqlc/agent_conversation_links.sql.go | 2 +- pkg/memory/sqlc/agent_conversations.sql.go | 2 +- pkg/memory/sqlc/agent_documents.sql.go | 2 +- pkg/memory/sqlc/agent_mentions.sql.go | 2 +- .../sqlc/agent_message_revisions.sql.go | 2 +- pkg/memory/sqlc/agent_messages.sql.go | 2 +- pkg/memory/sqlc/agent_state.sql.go | 2 +- pkg/memory/sqlc/agent_threads.sql.go | 2 +- pkg/memory/sqlc/agent_tool_results.sql.go | 2 +- pkg/memory/sqlc/archival.sql.go | 4 +- pkg/memory/sqlc/jobs.sql.go | 2 +- pkg/memory/sqlc/models.go | 80 +- pkg/memory/sqlc/querier.go | 411 +++++++- pkg/memory/sqlc/queries/recall.sql | 49 +- pkg/memory/sqlc/recall.sql.go | 155 ++- pkg/memory/sqlc/schema.sql | 107 ++- pkg/memory/sqlc/sqlc.yaml | 124 ++- pkg/memory/sqlc/summaries.sql.go | 2 +- pkg/memory/store/cached_embedder.go | 4 +- pkg/memory/store/cached_embedder_test.go | 2 +- pkg/memory/store/chunker.go | 2 +- pkg/memory/store/embed_factory.go | 6 +- pkg/memory/store/embed_factory_test.go | 2 +- pkg/memory/store/embed_ollama.go | 2 +- pkg/memory/store/embed_openai.go | 2 +- pkg/memory/store/memory_store.go | 237 ++++- pkg/memory/store/memory_store_test.go | 357 ++++++- pkg/memory/store/memory_tool.go | 4 +- pkg/memory/store/queue.go | 2 +- pkg/memory/store/queue_test.go | 16 +- pkg/memory/store/retrieval.go | 4 +- pkg/memory/store/scorer.go | 4 +- pkg/memory/store/scorer_test.go | 2 +- pkg/messages/types.go | 4 +- pkg/migrate/config.go | 8 +- pkg/migrate/migrate.go | 12 +- pkg/migrate/migrate_test.go | 14 +- pkg/migrate/xdg.go | 24 +- pkg/providers/types.go | 2 +- pkg/rlm/engine.go | 4 +- pkg/rlm/engine_test.go | 2 +- pkg/rlm/rope.go | 2 +- pkg/rlm/rope_test.go | 2 +- pkg/runtime/bootstrap.go | 33 +- pkg/runtime/config.go | 8 +- pkg/runtime/runtime_test.go | 46 +- pkg/security/keyring.go | 7 +- pkg/security/securebus/bus.go | 6 +- pkg/security/securebus/bus_test.go | 8 +- pkg/security/securebus/policy.go | 4 +- pkg/security/securebus/policy_test.go | 4 +- pkg/security/securebus/socket_transport.go | 2 +- .../securebus/socket_transport_test.go | 2 +- pkg/security/securebus/transport.go | 2 +- pkg/session/manager.go | 182 +++- pkg/session/manager_test.go | 170 +++- pkg/skills/installer.go | 2 +- pkg/skills/loader.go | 8 +- pkg/state/state.go | 4 +- pkg/sync/identity.go | 8 +- pkg/sync/identity_test.go | 2 +- pkg/tools/call.go | 2 +- pkg/tools/cron.go | 6 +- pkg/tools/focus.go | 8 +- pkg/tools/focus_test.go | 4 +- pkg/tools/registry.go | 2 +- pkg/tools/retrieval.go | 4 +- pkg/tools/search.go | 2 +- pkg/tools/search_test.go | 2 +- pkg/tools/shell.go | 4 +- pkg/tools/skills.go | 2 +- pkg/tools/skills_test.go | 2 +- pkg/tools/spawn.go | 12 +- pkg/tools/toolloop.go | 19 +- pkg/utils/media.go | 4 +- pkg/voice/transcriber.go | 4 +- pkg/worker/worker.go | 6 +- pkg/worker/worker_test.go | 8 +- 204 files changed, 2896 insertions(+), 3493 deletions(-) delete mode 100644 README.ja.md delete mode 100644 README.pt-br.md delete mode 100644 README.zh.md rename cmd/{picoclaw => dragonscale}/main.go (86%) delete mode 100644 docs/picoclaw_community_roadmap_260216.md rename pkg/{pcerrors => dserrors}/cli.go (97%) rename pkg/{pcerrors => dserrors}/pcerrors.go (97%) diff --git a/.dockerignore b/.dockerignore index d632da5ea..9cbc8def5 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,7 @@ .git .gitignore build/ -.picoclaw/ +.dragonscale/ config/ .env .env.example diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 4be385b22..f86664953 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -10,7 +10,7 @@ assignees: '' ## Quick Summary ## 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) - **AI Model & Provider:** (e.g., GPT-4o via OpenAI / DeepSeek via SiliconFlow) - **Operating System:** (e.g., Ubuntu 22.04 / macOS / Android Termux) diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2d1aa9ffc..3f822c2b8 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -10,7 +10,7 @@ on: env: GHCR_REGISTRY: ghcr.io - GHCR_IMAGE_NAME: ${{ github.repository_owner }}/picoclaw + GHCR_IMAGE_NAME: ${{ github.repository_owner }}/dragonscale DOCKERHUB_REGISTRY: docker.io DOCKERHUB_IMAGE_NAME: ${{ vars.DOCKERHUB_REPOSITORY }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d8f35c24d..fa75ed0d5 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -41,7 +41,8 @@ jobs: - name: Run sqlc vet rules run: make sqlc-vet - vet: + # Verify FlatBuffers generation is idempotent for current tree state. + flatc-check: runs-on: ubuntu-latest needs: fmt-check steps: @@ -53,6 +54,31 @@ jobs: with: 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 run: go generate ./... @@ -61,7 +87,7 @@ jobs: test: runs-on: ubuntu-latest - needs: fmt-check + needs: [fmt-check, flatc-check] steps: - name: Checkout uses: actions/checkout@v4 @@ -71,6 +97,11 @@ jobs: 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 run: go generate ./... diff --git a/.gitignore b/.gitignore index 702341b7e..8942355bf 100644 --- a/.gitignore +++ b/.gitignore @@ -7,14 +7,14 @@ bin/ *.dylib *.test *.out -/picoclaw -/picoclaw-test -cmd/picoclaw/workspace +/dragonscale +/dragonscale-test +cmd/dragonscale/workspace # Picoclaw specific -# PicoClaw -.picoclaw/ +# DragonScale +.dragonscale/ config.json sessions/ diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 2c47f7d86..96a96c66a 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -5,10 +5,10 @@ version: 2 before: hooks: - go mod tidy - - go generate ./cmd/picoclaw + - go generate ./cmd/dragonscale builds: - - id: picoclaw + - id: dragonscale env: - CGO_ENABLED=0 tags: @@ -31,18 +31,18 @@ builds: - s390x - mips64 - arm - main: ./cmd/picoclaw + main: ./cmd/dragonscale ignore: - goos: windows goarch: arm dockers_v2: - - id: picoclaw + - id: dragonscale dockerfile: Dockerfile.goreleaser ids: - - picoclaw + - dragonscale images: - - "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/picoclaw" + - "ghcr.io/{{ .Env.GITHUB_REPOSITORY_OWNER }}/dragonscale" - "docker.io/{{ .Env.DOCKERHUB_IMAGE_NAME }}" tags: - "{{ .Tag }}" diff --git a/Dockerfile b/Dockerfile index cef87e196..87e38ef11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ============================================================ -# Stage 1: Build the picoclaw binary +# Stage 1: Build the dragonscale binary # ============================================================ 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 # 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 -RUN addgroup -g 1000 picoclaw && \ - adduser -D -u 1000 -G picoclaw picoclaw +RUN addgroup -g 1000 dragonscale && \ + adduser -D -u 1000 -G dragonscale dragonscale # Switch to non-root user -USER picoclaw +USER dragonscale # 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"] diff --git a/Dockerfile.goreleaser b/Dockerfile.goreleaser index 0cdc8c6bd..01a6d579f 100644 --- a/Dockerfile.goreleaser +++ b/Dockerfile.goreleaser @@ -4,7 +4,7 @@ ARG TARGETPLATFORM 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"] diff --git a/Makefile b/Makefile index 1d87c6578..14386807f 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,9 @@ .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 -BINARY_NAME=picoclaw +BINARY_NAME=dragonscale BUILD_DIR=bin CMD_DIR=cmd/$(BINARY_NAME) MAIN_GO=$(CMD_DIR)/main.go @@ -24,8 +25,8 @@ INSTALL_BIN_DIR=$(INSTALL_PREFIX)/bin INSTALL_MAN_DIR=$(INSTALL_PREFIX)/share/man/man1 # Workspace and Skills -PICOCLAW_HOME?=$(HOME)/.picoclaw -WORKSPACE_DIR?=$(PICOCLAW_HOME)/workspace +DRAGONSCALE_HOME?=$(HOME)/.dragonscale +WORKSPACE_DIR?=$(DRAGONSCALE_HOME)/workspace WORKSPACE_SKILLS_DIR=$(WORKSPACE_DIR)/skills BUILTIN_SKILLS_DIR=$(CURDIR)/skills @@ -73,7 +74,7 @@ generate: @$(GO) generate ./... @echo "Run generate complete" -## build: Build the picoclaw binary for current platform +## build: Build the dragonscale binary for current platform build: generate @echo "Building $(BINARY_NAME) for $(PLATFORM)/$(ARCH)..." @mkdir -p $(BUILD_DIR) @@ -81,7 +82,7 @@ build: generate @echo "Build complete: $(BINARY_PATH)" @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 @echo "Building for multiple platforms..." @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) @echo "All builds complete" -## install: Install picoclaw to system and copy builtin skills +## install: Install dragonscale to system and copy builtin skills install: build @echo "Installing $(BINARY_NAME)..." @mkdir -p $(INSTALL_BIN_DIR) @@ -102,7 +103,7 @@ install: build @echo "Installed binary to $(INSTALL_BIN_DIR)/$(BINARY_NAME)" @echo "Installation complete!" -## uninstall: Remove picoclaw from system +## uninstall: Remove dragonscale from system uninstall: @echo "Uninstalling $(BINARY_NAME)..." @rm -f $(INSTALL_BIN_DIR)/$(BINARY_NAME) @@ -110,11 +111,11 @@ uninstall: @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'" -## uninstall-all: Remove picoclaw and all data +## uninstall-all: Remove dragonscale and all data uninstall-all: @echo "Removing workspace and skills..." - @rm -rf $(PICOCLAW_HOME) - @echo "Removed workspace: $(PICOCLAW_HOME)" + @rm -rf $(DRAGONSCALE_HOME) + @echo "Removed workspace: $(DRAGONSCALE_HOME)" @echo "Complete uninstallation done!" ## clean: Remove build artifacts @@ -160,13 +161,48 @@ update-deps: @$(GO) get -u ./... @$(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: @echo "Checking sqlc generation..." - @sqlc generate -f pkg/memory/sqlc/sqlc.yaml - @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) + @set -e; repo="$$(pwd)"; before="$$(mktemp)"; after="$$(mktemp)"; \ + 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" +## 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: @echo "Running sqlc vet..." @@ -205,10 +241,26 @@ test-integration: ## check: Run vet, fmt, sqlc vet, and verify dependencies check: deps fmt vet sqlc-vet test -## run: Build and run picoclaw +## run: Build and run dragonscale run: build @$(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 # Usage: make eval-build && make eval @@ -225,27 +277,27 @@ eval-build: generate ## eval: Run the eval suite against the current build eval: eval-build eval-fixtures @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 "View: cd eval && npx promptfoo view" ## 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: - @mkdir -p $(HOME)/.local/share/picoclaw/sandbox - @rm -f $(HOME)/.local/share/picoclaw/sandbox/eval_test_output.txt \ - $(HOME)/.local/share/picoclaw/sandbox/test_steps.txt \ - $(HOME)/.local/share/picoclaw/sandbox/eval_checkpoint.txt \ - $(HOME)/.local/share/picoclaw/sandbox/chain_test.txt \ - $(HOME)/.local/share/picoclaw/sandbox/current_year.txt \ - $(HOME)/.local/share/picoclaw/sandbox/result.txt \ - $(HOME)/.local/share/picoclaw/sandbox/progressive_test.txt \ - $(HOME)/.local/share/picoclaw/sandbox/os_name.txt - @rm -rf $(HOME)/.local/share/picoclaw/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 - @cp -f eval/fixtures/sample_data.txt $(HOME)/.local/share/picoclaw/sandbox/sample_data.txt - @mkdir -p $(HOME)/.local/share/picoclaw/skills - @cp -rf eval/fixtures/skills/* $(HOME)/.local/share/picoclaw/skills/ 2>/dev/null || true + @mkdir -p $(HOME)/.local/share/dragonscale/sandbox + @rm -f $(HOME)/.local/share/dragonscale/sandbox/eval_test_output.txt \ + $(HOME)/.local/share/dragonscale/sandbox/test_steps.txt \ + $(HOME)/.local/share/dragonscale/sandbox/eval_checkpoint.txt \ + $(HOME)/.local/share/dragonscale/sandbox/chain_test.txt \ + $(HOME)/.local/share/dragonscale/sandbox/current_year.txt \ + $(HOME)/.local/share/dragonscale/sandbox/result.txt \ + $(HOME)/.local/share/dragonscale/sandbox/progressive_test.txt \ + $(HOME)/.local/share/dragonscale/sandbox/os_name.txt + @rm -rf $(HOME)/.local/share/dragonscale/sandbox/project + @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/dragonscale/sandbox/sample_data.txt + @mkdir -p $(HOME)/.local/share/dragonscale/skills + @cp -rf eval/fixtures/skills/* $(HOME)/.local/share/dragonscale/skills/ 2>/dev/null || true ## eval-view: Open the promptfoo results viewer eval-view: @@ -266,7 +318,7 @@ eval-test: ## help: Show this help message help: - @echo "picoclaw Makefile" + @echo "dragonscale Makefile" @echo "" @echo "Usage:" @echo " make [target]" @@ -282,7 +334,7 @@ help: @echo "" @echo "Environment Variables:" @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 "" @echo "Current Configuration:" diff --git a/README.ja.md b/README.ja.md deleted file mode 100644 index e7556b5b3..000000000 --- a/README.ja.md +++ /dev/null @@ -1,775 +0,0 @@ -
-PicoClaw - -

PicoClaw: Go で書かれた超効率 AI アシスタント

- -

$10 ハードウェア · 10MB RAM · 1秒起動 · 行くぜ、シャコ!

-

- -

-Go -Hardware -License -

- -[中文](README.zh.md) | **日本語** | [Português](README.pt-br.md) | [English](README.md) - -
- - ---- - -🦐 PicoClaw は [nanobot](https://github.com/HKUDS/nanobot) にインスパイアされた超軽量パーソナル AI アシスタントです。Go でゼロからリファクタリングされ、AI エージェント自身がアーキテクチャの移行とコード最適化を推進するセルフブートストラッピングプロセスで構築されました。 - -⚡️ $10 のハードウェアで 10MB 未満の RAM で動作:OpenClaw より 99% 少ないメモリ、Mac mini より 98% 安い! - - - - - - -
-

- -

-
-

- -

-
- -## 📢 ニュース -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** | -| **起動時間**
(0.8GHz コア) | >500秒 | >30秒 | **<1秒** | -| **コスト** | Mac Mini 599$ | 大半の Linux SBC
~50$ |**あらゆる Linux ボード**
**最安 10$** | -PicoClaw - - -## 🦾 デモンストレーション -### 🛠️ スタンダードアシスタントワークフロー - - - - - - - - - - - - - - - - -

🧩 フルスタックエンジニア

🗂️ ログ&計画管理

🔎 Web 検索&学習

開発 · デプロイ · スケールスケジュール · 自動化 · メモリ発見 · インサイト · トレンド
- -### 🐜 革新的な省フットプリントデプロイ -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) | - -
-Telegram(推奨) - -**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 -``` -
- - -
-Discord - -**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 -``` - -
- -
-QQ - -**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 -``` - -
- -
-DingTalk - -**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 -``` - -
- -
-LINE - -**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 ポートを公開してください。 - -
- -## ⚙️ 設定 - -設定ファイル: `~/.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" - ``` - - -
-完全な設定例 - -```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 - } -} -``` - -
- -## CLI リファレンス - -| コマンド | 説明 | -|---------|------| -| `picoclaw onboard` | 設定&ワークスペースの初期化 | -| `picoclaw agent -m "..."` | エージェントとチャット | -| `picoclaw agent` | インタラクティブチャットモード | -| `picoclaw gateway` | ゲートウェイを起動 | -| `picoclaw status` | ステータスを表示 | - -## 🤝 コントリビュート&ロードマップ - -PR 歓迎!コードベースは意図的に小さく読みやすくしています。🤗 - -Discord: https://discord.gg/V4sAZ9XWpN - -PicoClaw - - -## 🐛 トラブルシューティング - -### 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) | diff --git a/README.pt-br.md b/README.pt-br.md deleted file mode 100644 index d250cc956..000000000 --- a/README.pt-br.md +++ /dev/null @@ -1,881 +0,0 @@ -
-PicoClaw - -

PicoClaw: Assistente de IA Ultra-Eficiente em Go

- -

Hardware de $10 · 10MB de RAM · Boot em 1s · 皮皮虾,我们走!

- -

- Go - Hardware - License -
- Website - Twitter -

- - [中文](README.zh.md) | [日本語](README.ja.md) | [English](README.md) | **Português** -
- ---- - -🦐 **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! - - - - - - -
-

- -

-
-

- -

-
- -> [!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**
(CPU 0.8GHz) | >500s | >30s | **<1s** | -| **Custo** | Mac Mini $599 | Maioria dos SBC Linux
~$50 | **Qualquer placa Linux**
**A partir de $10** | - -PicoClaw - -## 🦾 Demonstracao - -### 🛠️ Fluxos de Trabalho Padrao do Assistente - - - - - - - - - - - - - - - - - -

🧩 Engenharia Full-Stack

🗂️ Gerenciamento de Logs & Planejamento

🔎 Busca Web & Aprendizado

Desenvolver • Implantar • EscalarAgendar • Automatizar • MemorizarDescobrir • Analisar • Tendencias
- -### 📱 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! - -PicoClaw - -### 🐜 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) | - -
-Telegram (Recomendado) - -**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 -``` - -
- -
-Discord - -**1. Criar o bot** - -* Acesse -* 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 -``` - -
- -
-QQ - -**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 -``` - -
- -
-DingTalk - -**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 -``` - -
- -
-LINE - -**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. - -
- -## 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) | - -
-Configuracao Zhipu - -**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?" -``` - -
- -
-Exemplo de configuracao completa - -```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 - } -} -``` - -
- -## 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: - -PicoClaw - -## 🐛 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) | diff --git a/README.zh.md b/README.zh.md deleted file mode 100644 index 6c87ba785..000000000 --- a/README.zh.md +++ /dev/null @@ -1,744 +0,0 @@ -
-PicoClaw - -

PicoClaw: 基于Go语言的超高效 AI 助手

- -

10$硬件 · 10MB内存 · 1秒启动 · 皮皮虾,我们走!

- -

- Go - Hardware - License -
- Website - Twitter -

- - **中文** | [日本語](README.ja.md) | [Português](README.pt-br.md) | [English](README.md) -
- ---- - -🦐 **PicoClaw** 是一个受 [nanobot](https://github.com/HKUDS/nanobot) 启发的超轻量级个人 AI 助手。它采用 **Go 语言** 从零重构,经历了一个“自举”过程——即由 AI Agent 自身驱动了整个架构迁移和代码优化。 - -⚡️ **极致轻量**:可在 **10 美元** 的硬件上运行,内存占用 **<10MB**。这意味着比 OpenClaw 节省 99% 的内存,比 Mac mini 便宜 98%! - - - - - - -
-

- -

-
-

- -

-
- -注意:人手有限,中文文档可能略有滞后,请优先查看英文文档。 - -> [!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** | -| **启动时间**
(0.8GHz core) | >500s | >30s | **<1s** | -| **成本** | Mac Mini $599 | 大多数 Linux 开发板 ~$50 | **任意 Linux 开发板**
**低至 $10** | - -PicoClaw - -## 🦾 演示 - -### 🛠️ 标准助手工作流 - - - - - - - - - - - - - - - - - -

🧩 全栈工程师模式

🗂️ 日志与规划管理

🔎 网络搜索与学习

开发 • 部署 • 扩展日程 • 自动化 • 记忆发现 • 洞察 • 趋势
- -### 📱 在手机上轻松运行 -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即可使用! -PicoClaw - - - - -### 🐜 创新的低占用部署 - -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) | - -
-Telegram (推荐) - -**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 - -``` - -
- -
-Discord - -**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 - -``` - -
- -
-QQ - -**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 - -``` - -
- -
-钉钉 (DingTalk) - -**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 - -``` - -
- -## 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 提供免费的语音转录。如果配置了 Groq,Telegram 语音消息将被自动转录为文字。 - -| 提供商 | 用途 | 获取 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) | - -
-智谱 (Zhipu) 配置示例 - -**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 "你好" - -``` - -
- -
-完整配置示例 - -```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 - } -} - -``` - -
- -## 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) - -PicoClaw - -## 🐛 疑难解答 (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) | \ No newline at end of file diff --git a/cmd/picoclaw/main.go b/cmd/dragonscale/main.go similarity index 86% rename from cmd/picoclaw/main.go rename to cmd/dragonscale/main.go index c9004aef4..76c8e8a23 100644 --- a/cmd/picoclaw/main.go +++ b/cmd/dragonscale/main.go @@ -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 // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package main @@ -21,28 +21,28 @@ import ( "strings" "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/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 . @@ -80,7 +80,7 @@ func formatBuildInfo() (build string, goVer string) { } func printVersion() { - fmt.Printf("%s picoclaw %s\n", logo, formatVersion()) + fmt.Printf("%s dragonscale %s\n", logo, formatVersion()) build, goVer := formatBuildInfo() if build != "" { fmt.Printf(" Build: %s\n", build) @@ -159,7 +159,7 @@ func main() { installer := skills.NewSkillInstaller(skillsDir) cfgDir, _ := config.ConfigDir() globalSkillsDir := filepath.Join(cfgDir, "skills") - builtinSkillsDir := filepath.Join(cfgDir, "picoclaw", "skills") + builtinSkillsDir := filepath.Join(cfgDir, "dragonscale", "skills") skillsLoader := skills.NewSkillsLoader(skillsDir, globalSkillsDir, builtinSkillsDir) switch subcommand { @@ -169,7 +169,7 @@ func main() { skillsInstallCmd(installer) case "remove", "uninstall": if len(os.Args) < 4 { - fmt.Println("Usage: picoclaw skills remove ") + fmt.Println("Usage: dragonscale skills remove ") return } skillsRemoveCmd(installer, os.Args[3]) @@ -181,7 +181,7 @@ func main() { skillsSearchCmd(installer) case "show": if len(os.Args) < 4 { - fmt.Println("Usage: picoclaw skills show ") + fmt.Println("Usage: dragonscale skills show ") return } skillsShowCmd(skillsLoader, os.Args[3]) @@ -205,20 +205,20 @@ func main() { } func printHelp() { - fmt.Printf("%s picoclaw - Personal AI Assistant v%s\n\n", logo, version) - fmt.Println("Usage: picoclaw ") + fmt.Printf("%s dragonscale - Personal AI Assistant v%s\n\n", logo, version) + fmt.Println("Usage: dragonscale ") fmt.Println() 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(" auth Manage authentication (login, logout, status)") - fmt.Println(" gateway Start picoclaw gateway") - fmt.Println(" status Show picoclaw status") + fmt.Println(" gateway Start dragonscale gateway") + fmt.Println(" status Show dragonscale status") 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(" 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(" version Show version information") } @@ -249,7 +249,7 @@ func onboard() { 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): ") var secretResponse string @@ -264,20 +264,20 @@ func onboard() { fmt.Println(" " + encoded) fmt.Println() 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("Then store secrets with: picoclaw secret add ") + fmt.Println("Then store secrets with: dragonscale secret add ") } } fmt.Println("\nNext steps:") fmt.Println(" 1. Add your API key to", configPath) 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 -// 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 { identityFiles := []string{"AGENT.md", "IDENTITY.md", "SOUL.md", "USER.md"} for _, name := range identityFiles { @@ -297,7 +297,7 @@ func seedEmbeddedIdentity(identityDir string) error { } // 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 { return fs.WalkDir(embeddedFiles, "workspace/skills", func(path string, d fs.DirEntry, err error) error { if err != nil || d.IsDir() { @@ -366,7 +366,7 @@ func migrateCmd() { opts.OpenClawHome = args[i+1] i++ } - case "--picoclaw-home": + case "--dragonscale-home": if i+1 < len(args) { opts.PicoClawHome = args[i+1] i++ @@ -390,9 +390,9 @@ func migrateCmd() { } func migrateHelp() { - fmt.Println("\nMigrate from OpenClaw to PicoClaw") + fmt.Println("\nMigrate from OpenClaw to DragonScale") fmt.Println() - fmt.Println("Usage: picoclaw migrate [options]") + fmt.Println("Usage: dragonscale migrate [options]") fmt.Println() fmt.Println("Options:") 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(" --force Skip confirmation prompts") 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("Examples:") - fmt.Println(" picoclaw migrate Detect and migrate from OpenClaw") - fmt.Println(" picoclaw migrate --dry-run Show what would be migrated") - fmt.Println(" picoclaw migrate --refresh Re-sync workspace files") - fmt.Println(" picoclaw migrate --force Migrate without confirmation") + fmt.Println(" dragonscale migrate Detect and migrate from OpenClaw") + fmt.Println(" dragonscale migrate --dry-run Show what would be migrated") + fmt.Println(" dragonscale migrate --refresh Re-sync workspace files") + fmt.Println(" dragonscale migrate --force Migrate without confirmation") } func agentCmd() { @@ -479,7 +479,7 @@ func interactiveMode(ctx context.Context, agentLoop *agent.AgentLoop, sessionKey rl, err := readline.NewEx(&readline.Config{ Prompt: prompt, - HistoryFile: filepath.Join(os.TempDir(), ".picoclaw_history"), + HistoryFile: filepath.Join(os.TempDir(), ".dragonscale_history"), HistoryLimit: 100, InterruptPrompt: "^C", EOFPrompt: "exit", @@ -611,14 +611,19 @@ func gatewayCmd() { execTimeout := time.Duration(cfg.Tools.Cron.ExecTimeoutMinutes) * time.Minute var cronOpts []cron.CronOption 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...) + var heartbeatStateOpts []state.Option + if del := agentLoop.MemoryDelegate(); del != nil { + heartbeatStateOpts = append(heartbeatStateOpts, state.WithDelegate(del)) + } heartbeatService := heartbeat.NewHeartbeatService( cfg.SandboxPath(), cfg.Heartbeat.Interval, cfg.Heartbeat.Enabled, + heartbeatStateOpts..., ) heartbeatService.SetBus(msgBus) 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 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) if err != nil { @@ -762,7 +801,7 @@ func memoryCmd() { func memoryHelp() { fmt.Println("\nMemory system management") fmt.Println() - fmt.Println("Usage: picoclaw memory ") + fmt.Println("Usage: dragonscale memory ") fmt.Println() fmt.Println("Subcommands:") fmt.Println(" migrate-sessions Import file-based sessions into recall memory") @@ -793,7 +832,7 @@ func memoryMigrateSessions() { } 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 { fmt.Printf("Migration error: %v\n", err) os.Exit(1) @@ -841,7 +880,7 @@ func statusCmd() { configPath := getConfigPath() - fmt.Printf("%s picoclaw Status\n", logo) + fmt.Printf("%s dragonscale Status\n", logo) fmt.Printf("Version: %s\n", formatVersion()) build, _ := formatBuildInfo() if build != "" { @@ -924,7 +963,7 @@ func statusCmd() { } memDBPath := cfg.Memory.DBPath 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 { 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() fmt.Println("Examples:") - fmt.Println(" picoclaw auth login --provider openai") - fmt.Println(" picoclaw auth login --provider openai --device-code") - fmt.Println(" picoclaw auth login --provider anthropic") - fmt.Println(" picoclaw auth logout --provider openai") - fmt.Println(" picoclaw auth status") + fmt.Println(" dragonscale auth login --provider openai") + fmt.Println(" dragonscale auth login --provider openai --device-code") + fmt.Println(" dragonscale auth login --provider anthropic") + fmt.Println(" dragonscale auth logout --provider openai") + fmt.Println(" dragonscale auth status") } func authLoginCmd() { @@ -1125,7 +1164,7 @@ func authStatusCmd() { if len(store.Credentials) == 0 { fmt.Println("No authenticated providers.") - fmt.Println("Run: picoclaw auth login --provider ") + fmt.Println("Run: dragonscale auth login --provider ") return } @@ -1178,7 +1217,7 @@ func secretCmd() { func secretHelp() { fmt.Println("\nSecret management (encrypted at rest)") fmt.Println() - fmt.Println("Usage: picoclaw secret ") + fmt.Println("Usage: dragonscale secret ") fmt.Println() fmt.Println("Subcommands:") fmt.Println(" init Generate a master key (stored in keyring or env)") @@ -1187,24 +1226,24 @@ func secretHelp() { fmt.Println(" delete Remove a secret") fmt.Println() 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("Examples:") - fmt.Println(" picoclaw secret init") - fmt.Println(" picoclaw secret add github_token") - fmt.Println(" picoclaw secret list") - fmt.Println(" picoclaw secret delete github_token") + fmt.Println(" dragonscale secret init") + fmt.Println(" dragonscale secret add github_token") + fmt.Println(" dragonscale secret list") + fmt.Println(" dragonscale secret delete github_token") } func secretStorePath() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".picoclaw", "secrets.json") + return filepath.Join(home, ".dragonscale", "secrets.json") } func loadSecretStore() (*security.SecretStore, error) { var keyring security.KeyringProvider - if mk := os.Getenv("PICOCLAW_MASTER_KEY"); mk != "" { - keyring = security.NewEnvKeyring("PICOCLAW_MASTER_KEY") + if mk := os.Getenv("DRAGONSCALE_MASTER_KEY"); mk != "" { + keyring = security.NewEnvKeyring("DRAGONSCALE_MASTER_KEY") } else { keyring = security.NewNoopKeyring(nil) } @@ -1224,14 +1263,14 @@ func secretInit() { fmt.Println(" " + encoded) fmt.Println() 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("Or add to your shell profile (~/.bashrc, ~/.zshrc).") } func secretAdd() { if len(os.Args) < 4 { - fmt.Println("Usage: picoclaw secret add ") + fmt.Println("Usage: dragonscale secret add ") return } name := os.Args[3] @@ -1274,7 +1313,7 @@ func secretList() { names := ss.List() if len(names) == 0 { fmt.Println("No secrets stored.") - fmt.Println("Add one with: picoclaw secret add ") + fmt.Println("Add one with: dragonscale secret add ") return } @@ -1286,7 +1325,7 @@ func secretList() { func secretDelete() { if len(os.Args) < 4 { - fmt.Println("Usage: picoclaw secret delete ") + fmt.Println("Usage: dragonscale secret delete ") return } name := os.Args[3] @@ -1312,12 +1351,12 @@ func secretDelete() { func daemonSocketPath() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".picoclaw", "daemon.sock") + return filepath.Join(home, ".dragonscale", "daemon.sock") } func daemonPIDPath() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".picoclaw", "daemon.pid") + return filepath.Join(home, ".dragonscale", "daemon.pid") } func daemonCmd() { @@ -1345,14 +1384,14 @@ func daemonCmd() { func daemonHelp() { fmt.Println("\nDaemon mode (Unix socket transport)") fmt.Println() - fmt.Println("Usage: picoclaw daemon ") + fmt.Println("Usage: dragonscale daemon ") fmt.Println() fmt.Println("Subcommands:") fmt.Println(" start Start the daemon (foreground)") fmt.Println(" stop Stop a running daemon") fmt.Println(" status Check daemon status") 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.") } @@ -1376,7 +1415,7 @@ func daemonStart() { pid := os.Getpid() 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) defer os.Remove(pidPath) @@ -1416,7 +1455,7 @@ func daemonStart() { secureBus := securebus.New(busCfg, ss, capLookup, executor) 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(" tools: %d registered\n", len(registry.List())) fmt.Println("Press Ctrl+C to stop.") @@ -1510,9 +1549,9 @@ func getConfigPath() string { if p, err := config.DefaultConfigPath(); err == nil { 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() - 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 { @@ -1546,7 +1585,7 @@ func bootstrapAgentRuntime(appCtx context.Context, cfg *config.Config) (*picorun } // 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 // 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()) { cfgDir, err := config.ConfigDir() if err != nil { - // Fallback to ~/.picoclaw if XDG resolution fails. + // Fallback to ~/.dragonscale if XDG resolution fails. home, herr := os.UserHomeDir() if herr != nil { logger.WarnC("itr", "SecureBus: cannot determine config dir — running without ITR") return func() {} } - cfgDir = filepath.Join(home, ".picoclaw") + cfgDir = filepath.Join(home, ".dragonscale") } secretsPath := filepath.Join(cfgDir, "secrets.json") - // Use NoopKeyring by default; EnvKeyring when PICOCLAW_MASTER_KEY is set. - var keyring security.KeyringProvider - if mk := os.Getenv("PICOCLAW_MASTER_KEY"); mk != "" { - keyring = security.NewEnvKeyring("PICOCLAW_MASTER_KEY") - } else { - keyring = security.NewNoopKeyring(nil) - } + // Always use EnvKeyring in runtime wiring. We intentionally avoid + // in-memory fallback keyrings in production execution paths. + keyring := security.NewEnvKeyring(security.MasterKeyEnvVar) ss, err := security.NewSecretStore(secretsPath, keyring) if err != nil { @@ -1581,6 +1616,10 @@ func setupSecureBus(agentLoop *agent.AgentLoop) (closer func()) { map[string]interface{}{"error": err.Error()}) 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()) logger.InfoC("itr", "SecureBus enabled — tool calls routed through ITR") @@ -1611,7 +1650,7 @@ func cronCmd() { cronAddCmd(cronStorePath) case "remove": if len(os.Args) < 4 { - fmt.Println("Usage: picoclaw cron remove ") + fmt.Println("Usage: dragonscale cron remove ") return } cronRemoveCmd(cronStorePath, os.Args[3]) @@ -1781,7 +1820,7 @@ func cronRemoveCmd(storePath, jobID string) { func cronEnableCmd(storePath string, disable bool) { if len(os.Args) < 4 { - fmt.Println("Usage: picoclaw cron enable/disable ") + fmt.Println("Usage: dragonscale cron enable/disable ") return } @@ -1812,11 +1851,11 @@ func skillsHelp() { fmt.Println(" show Show skill details") fmt.Println() fmt.Println("Examples:") - fmt.Println(" picoclaw skills list") - fmt.Println(" picoclaw skills install sipeed/picoclaw-skills/weather") - fmt.Println(" picoclaw skills install-builtin") - fmt.Println(" picoclaw skills list-builtin") - fmt.Println(" picoclaw skills remove weather") + fmt.Println(" dragonscale skills list") + fmt.Println(" dragonscale skills install sipeed/dragonscale-skills/weather") + fmt.Println(" dragonscale skills install-builtin") + fmt.Println(" dragonscale skills list-builtin") + fmt.Println(" dragonscale skills remove weather") } func skillsListCmd(loader *skills.SkillsLoader) { @@ -1839,8 +1878,8 @@ func skillsListCmd(loader *skills.SkillsLoader) { func skillsInstallCmd(installer *skills.SkillInstaller) { if len(os.Args) < 4 { - fmt.Println("Usage: picoclaw skills install ") - fmt.Println("Example: picoclaw skills install sipeed/picoclaw-skills/weather") + fmt.Println("Usage: dragonscale skills install ") + fmt.Println("Example: dragonscale skills install sipeed/dragonscale-skills/weather") return } @@ -1870,7 +1909,7 @@ func skillsRemoveCmd(installer *skills.SkillInstaller, skillName string) { } func skillsInstallBuiltinCmd(workspace string) { - builtinSkillsDir := "./picoclaw/skills" + builtinSkillsDir := "./dragonscale/skills" workspaceSkillsDir := filepath.Join(workspace, "skills") fmt.Printf("Copying builtin skills to workspace...\n") diff --git a/config/config.example.json b/config/config.example.json index 37c2bcd81..b3e3b2a74 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -1,7 +1,7 @@ { "agents": { "defaults": { - "workspace": "~/.picoclaw/workspace", + "workspace": "~/.dragonscale/workspace", "restrict_to_workspace": true, "model": "glm-4.7", "max_tokens": 8192, diff --git a/docker-compose.yml b/docker-compose.yml index 32e8ee339..6c8e1fc00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,40 +1,40 @@ services: # ───────────────────────────────────────────── - # PicoClaw Agent (one-shot query) - # docker compose run --rm picoclaw-agent -m "Hello" + # DragonScale Agent (one-shot query) + # docker compose run --rm dragonscale-agent -m "Hello" # ───────────────────────────────────────────── - picoclaw-agent: + dragonscale-agent: build: context: . dockerfile: Dockerfile - container_name: picoclaw-agent + container_name: dragonscale-agent profiles: - agent volumes: - - ./config/config.json:/home/picoclaw/.picoclaw/config.json:ro - - picoclaw-workspace:/home/picoclaw/.picoclaw/workspace - entrypoint: ["picoclaw", "agent"] + - ./config/config.json:/home/dragonscale/.dragonscale/config.json:ro + - dragonscale-workspace:/home/dragonscale/.dragonscale/workspace + entrypoint: ["dragonscale", "agent"] stdin_open: true tty: true # ───────────────────────────────────────────── - # PicoClaw Gateway (Long-running Bot) - # docker compose up picoclaw-gateway + # DragonScale Gateway (Long-running Bot) + # docker compose up dragonscale-gateway # ───────────────────────────────────────────── - picoclaw-gateway: + dragonscale-gateway: build: context: . dockerfile: Dockerfile - container_name: picoclaw-gateway + container_name: dragonscale-gateway restart: unless-stopped profiles: - gateway volumes: # 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) - - picoclaw-workspace:/home/picoclaw/.picoclaw/workspace + - dragonscale-workspace:/home/dragonscale/.dragonscale/workspace command: ["gateway"] volumes: - picoclaw-workspace: + dragonscale-workspace: diff --git a/docs/adr/001-isolated-tool-runtime.md b/docs/adr/001-isolated-tool-runtime.md index 5ffbb0b18..ac59f7c56 100644 --- a/docs/adr/001-isolated-tool-runtime.md +++ b/docs/adr/001-isolated-tool-runtime.md @@ -8,7 +8,7 @@ ## 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. 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. - **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. @@ -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. -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. @@ -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: ```flatbuffers -namespace picoclaw.itr; +namespace dragonscale.itr; // --- Individual command types --- 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 -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 onboarding: @@ -215,11 +215,11 @@ Keyring support is gated behind a build tag (`!embedded`) to avoid pulling in CG **CLI surface**: ``` -picoclaw secret add # interactive prompt for value -picoclaw secret list # names only, no values -picoclaw secret delete -picoclaw secret export # encrypted backup -picoclaw secret import # restore from backup +dragonscale secret add # interactive prompt for value +dragonscale secret list # names only, no values +dragonscale secret delete +dragonscale secret export # encrypted backup +dragonscale secret import # restore from backup ``` 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: -1. Client connects to `~/.picoclaw/daemon.sock` +1. Client connects to `~/.dragonscale/daemon.sock` 2. Daemon sends a random 32-byte challenge `c` 3. Client computes commitment and response from passphrase-derived secret 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**: ``` -picoclaw daemon start # background, creates pidfile + socket -picoclaw daemon stop # graceful shutdown, zeroes key material -picoclaw daemon status # running/stopped, uptime, client count +dragonscale daemon start # background, creates pidfile + socket +dragonscale daemon stop # graceful shutdown, zeroes key material +dragonscale daemon status # running/stopped, uptime, client count ``` 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 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 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. **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 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. @@ -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 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 (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 @@ -643,7 +643,7 @@ A lightweight classifier (heuristic or cheap LM call) routes queries to the appropriate executor. The `ToolLoopConfig` gains a `Mode` field: `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 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 @@ -717,10 +717,10 @@ Use Anthropic's Programmatic Tool Calling directly, where Claude writes Python orchestration code that runs in Anthropic's sandboxed environment. **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 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. ### 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, resolving `#nodeN` references without LLM involvement. Only the Joiner's compact 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 Fantasy-compatible LLM can produce DAG plans. - **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. Generated Go code is committed to the repo, so downstream consumers do not need `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 edges. Malformed DAGs (cycles, missing dependencies) are caught at validation time 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 | | 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 | -| 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 | | 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 | |------|---------| | `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/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 | @@ -889,7 +902,7 @@ complementary, not alternatives. | `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/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` | | `ROADMAP.md` | Update to reference DAG executor convergence and this ADR | diff --git a/docs/adr/README.md b/docs/adr/README.md index 2e5eb5ea8..de6a2facf 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -1,6 +1,6 @@ # 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. @@ -18,3 +18,15 @@ Each ADR follows [Michael Nygard's template](https://cognitect.com/blog/2011/11/ | ADR | Title | Status | |-----|-------|--------| | [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` diff --git a/docs/picoclaw_community_roadmap_260216.md b/docs/picoclaw_community_roadmap_260216.md deleted file mode 100644 index cfcc30f17..000000000 --- a/docs/picoclaw_community_roadmap_260216.md +++ /dev/null @@ -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**, we’ve 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! We’re 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 haven’t 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!** 🐎 diff --git a/eval/README.md b/eval/README.md index 6dd5b119d..db5c11eee 100644 --- a/eval/README.md +++ b/eval/README.md @@ -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 @@ -33,7 +33,7 @@ The current agent architecture has these always-on behaviors: ``` 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) │ ├── tool_calling.yaml │ ├── token_efficiency.yaml @@ -44,6 +44,9 @@ eval/ │ ├── skills.yaml │ ├── subagent.yaml │ ├── reasoning.yaml +│ ├── assistant_proactive.yaml +│ ├── assistant_first_metrics.yaml +│ ├── procedural_long_context.yaml │ └── error_recovery.yaml ├── go_evals/ # Go-native component tests (memory, tools) ├── scripts/ # CI/comparison scripts @@ -55,7 +58,7 @@ eval/ ## 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. @@ -99,7 +102,7 @@ Create a new YAML file in `eval/cases/` following this pattern: ```yaml - description: "what this tests" vars: - prompt: "the prompt to send to picoclaw" + prompt: "the prompt to send to dragonscale" assert: - type: javascript value: | @@ -108,11 +111,17 @@ Create a new YAML file in `eval/cases/` following this pattern: 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 `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 -- `PICOCLAW_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. +- `DRAGONSCALE_EVAL_CONFIG` - Optional overlay config path applied on top of user base config. +- Base config discovery uses XDG first (`~/.config/dragonscale/config.json`), then legacy (`~/.dragonscale/config.json`), then XDG fallback if neither exists. diff --git a/eval/cases/edge_cases.yaml b/eval/cases/edge_cases.yaml index d6cdd6e0d..c8888cb98 100644 --- a/eval/cases/edge_cases.yaml +++ b/eval/cases/edge_cases.yaml @@ -54,7 +54,7 @@ - description: "unicode content: handles non-ASCII text" 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: - type: javascript value: | diff --git a/eval/cases/memory_ops.yaml b/eval/cases/memory_ops.yaml index 3c5339da2..3589b1a4c 100644 --- a/eval/cases/memory_ops.yaml +++ b/eval/cases/memory_ops.yaml @@ -4,7 +4,7 @@ - description: "memory write and search: store a fact then retrieve it" 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: - type: javascript value: | diff --git a/eval/cases/meta_tools.yaml b/eval/cases/meta_tools.yaml index b0a447122..cbda867fe 100644 --- a/eval/cases/meta_tools.yaml +++ b/eval/cases/meta_tools.yaml @@ -41,7 +41,7 @@ value: | const trace = JSON.parse(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' }; - description: "meta tools: tool_call dispatches exec correctly" diff --git a/eval/cases/multi_step.yaml b/eval/cases/multi_step.yaml index d8b72da92..089d60839 100644 --- a/eval/cases/multi_step.yaml +++ b/eval/cases/multi_step.yaml @@ -3,7 +3,7 @@ - description: "create and verify: write a file then read it back" 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: - type: javascript value: | diff --git a/eval/cases/tool_calling.yaml b/eval/cases/tool_calling.yaml index 3ed526805..c6c44dde9 100644 --- a/eval/cases/tool_calling.yaml +++ b/eval/cases/tool_calling.yaml @@ -1,6 +1,6 @@ # Tool Calling Evaluation Cases # 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. - description: "file read: agent reads a known fixture file and returns its content" @@ -24,7 +24,7 @@ value: | const trace = JSON.parse(output); 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' }; - description: "file write: agent creates a new file when asked" @@ -47,7 +47,7 @@ - description: "shell exec: agent runs a shell command" 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: - type: javascript value: | @@ -61,7 +61,7 @@ } 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)` }; - description: "list directory: agent lists workspace contents" @@ -84,7 +84,7 @@ - description: "edit file: agent edits an existing file" 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: - type: javascript value: | @@ -104,8 +104,8 @@ value: | const trace = JSON.parse(output); const out = (trace.output || '').toLowerCase(); - const hasPicoclaw = out.includes('picoclaw'); - return { pass: hasPicoclaw, score: hasPicoclaw ? 1.0 : 0.0, reason: hasPicoclaw ? 'confirmed edit result' : 'did not confirm picoclaw in output' }; + const hasPicoclaw = out.includes('dragonscale'); + 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" vars: @@ -128,7 +128,7 @@ - description: "web search: agent searches the web" 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: - type: javascript value: | diff --git a/eval/cmd/eval-runner/main.go b/eval/cmd/eval-runner/main.go index 5e75a6534..9ab9338ca 100644 --- a/eval/cmd/eval-runner/main.go +++ b/eval/cmd/eval-runner/main.go @@ -11,9 +11,9 @@ import ( "time" fantasy "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/logger" - picoruntime "github.com/sipeed/picoclaw/pkg/runtime" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + picoruntime "github.com/ZanzyTHEbar/dragonscale/pkg/runtime" ) // 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 { 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 == "" { return defaultTimeout } diff --git a/eval/fixtures/sample_data.txt b/eval/fixtures/sample_data.txt index f9297efce..013a01153 100644 --- a/eval/fixtures/sample_data.txt +++ b/eval/fixtures/sample_data.txt @@ -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 3: It contains known content that assertions can verify against. Line 4: The quick brown fox jumps over the lazy dog. -Line 5: picoclaw-fixture-marker-abc123 +Line 5: dragonscale-fixture-marker-abc123 diff --git a/eval/fixtures/skills/eval-test-skill/SKILL.md b/eval/fixtures/skills/eval-test-skill/SKILL.md index 1434c9043..7187f6a5b 100644 --- a/eval/fixtures/skills/eval-test-skill/SKILL.md +++ b/eval/fixtures/skills/eval-test-skill/SKILL.md @@ -19,5 +19,5 @@ When asked to greet someone, use one of the following templates: ## 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. diff --git a/eval/go_evals/eval_test.go b/eval/go_evals/eval_test.go index ae1c63dad..a378e4cab 100644 --- a/eval/go_evals/eval_test.go +++ b/eval/go_evals/eval_test.go @@ -7,8 +7,8 @@ import ( "path/filepath" "testing" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -127,12 +127,12 @@ func TestToolExecution_ExecBlocking(t *testing.T) { execTool := tools.NewExecTool(workspace, false) result := execTool.Execute(context.Background(), map[string]interface{}{ - "command": "echo picoclaw-eval-test", + "command": "echo dragonscale-eval-test", }) require.NotNil(t, result) 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) { @@ -165,7 +165,7 @@ func TestToolExecution_EditFile(t *testing.T) { result := editTool.Execute(context.Background(), map[string]interface{}{ "path": "edit_target.txt", "old_text": "world", - "new_text": "picoclaw", + "new_text": "dragonscale", }) require.NotNil(t, result) 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{}{ "path": "edit_target.txt", }) - assert.Contains(t, readResult.ForLLM, "picoclaw") + assert.Contains(t, readResult.ForLLM, "dragonscale") assert.NotContains(t, readResult.ForLLM, "world") } @@ -244,7 +244,7 @@ func TestToolExecution_ReadFile_PathTraversal(t *testing.T) { func TestToolExecution_Unrestricted(t *testing.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) defer os.Remove(tmpFile) diff --git a/eval/promptfooconfig.yaml b/eval/promptfooconfig.yaml index d28403d5f..d84db9c85 100644 --- a/eval/promptfooconfig.yaml +++ b/eval/promptfooconfig.yaml @@ -1,18 +1,18 @@ -# PicoClaw Eval Harness - promptfoo configuration +# DragonScale Eval Harness - promptfoo configuration # Run: make eval # View: cd eval && npx promptfoo view -description: "PicoClaw agent end-to-end evaluation" +description: "DragonScale agent end-to-end evaluation" maxConcurrency: 1 providers: - id: "exec:./bin/eval-runner" - label: "picoclaw" + label: "dragonscale" config: timeout: 180000 env: - PICOCLAW_EVAL_CONFIG: "./configs/default.json" + DRAGONSCALE_EVAL_CONFIG: "./configs/default.json" # Default assertions applied to every test case defaultTest: diff --git a/eval/scripts/compare.sh b/eval/scripts/compare.sh index 37c4dad94..a02b5c29e 100755 --- a/eval/scripts/compare.sh +++ b/eval/scripts/compare.sh @@ -13,7 +13,7 @@ if [[ "${1:-}" == "--repeat" ]]; then REPEAT="${2:-3}" fi -echo "=== PicoClaw Eval Comparison ===" +echo "=== DragonScale Eval Comparison ===" echo "Repeat: ${REPEAT}x per test case" echo "" @@ -47,7 +47,7 @@ cd "$EVAL_DIR" # Create a temp config with both providers cat > promptfooconfig-compare.yaml <<'YAML' -description: "PicoClaw A/B comparison (branch vs main)" +description: "DragonScale A/B comparison (branch vs main)" providers: - id: "exec:./bin/eval-runner" diff --git a/go.mod b/go.mod index a112c71da..2f1bf8be9 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/sipeed/picoclaw +module github.com/ZanzyTHEbar/dragonscale go 1.26 diff --git a/internal/fantasy/VENDORING.md b/internal/fantasy/VENDORING.md index a5907206c..3dd43338d 100644 --- a/internal/fantasy/VENDORING.md +++ b/internal/fantasy/VENDORING.md @@ -3,16 +3,17 @@ ## What This Is 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.). -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 ``` -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 diff --git a/pkg/agent/conversations/store.go b/pkg/agent/conversations/store.go index 75a5d7cd1..04527c176 100644 --- a/pkg/agent/conversations/store.go +++ b/pkg/agent/conversations/store.go @@ -9,9 +9,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/ids" - sqlc "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/pcerrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + sqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) // 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) { msgIDStr := strings.TrimSpace(p.MessageID) 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) 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) 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) @@ -141,16 +141,16 @@ type ForkFromCheckpointParams struct { func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointParams) (sqlc.AgentConversation, error) { fromIDStr := strings.TrimSpace(p.FromConversationID) 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) 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) 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, @@ -177,7 +177,7 @@ func (s *Store) ForkFromCheckpoint(ctx context.Context, p ForkFromCheckpointPara var snap snapshot if len(runState.SnapshotJson) > 0 { 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) { baseIDStr := strings.TrimSpace(p.BaseConversationID) 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) 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) 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) 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 { - 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{ @@ -335,11 +335,11 @@ type AncestryResult struct { func (s *Store) Ancestry(ctx context.Context, p AncestryParams) (AncestryResult, error) { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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}) @@ -377,11 +377,11 @@ type LinksListParams struct { func (s *Store) LinksList(ctx context.Context, p LinksListParams) ([]sqlc.AgentConversationLink, error) { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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, sqlc.ListAgentConversationLinksByConversationIDParams{ConversationID: convID}) @@ -399,20 +399,20 @@ type LinksRemoveParams struct { func (s *Store) LinksRemove(ctx context.Context, p LinksRemoveParams) error { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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) 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) 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) @@ -467,11 +467,11 @@ type GraphResult struct { func (s *Store) Graph(ctx context.Context, p GraphParams) (GraphResult, error) { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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 diff --git a/pkg/agent/kv_delegate_test.go b/pkg/agent/kv_delegate_test.go index 5fbaf0e7c..b25bdd6f8 100644 --- a/pkg/agent/kv_delegate_test.go +++ b/pkg/agent/kv_delegate_test.go @@ -7,7 +7,7 @@ import ( "github.com/stretchr/testify/assert" "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 { diff --git a/pkg/agent/memgpt_tool.go b/pkg/agent/memgpt_tool.go index 89178f222..d7ec885e4 100644 --- a/pkg/agent/memgpt_tool.go +++ b/pkg/agent/memgpt_tool.go @@ -1,7 +1,7 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package agent @@ -9,11 +9,11 @@ import ( "context" jsonv2 "github.com/go-json-experiment/json" - memstore "github.com/sipeed/picoclaw/pkg/memory/store" - "github.com/sipeed/picoclaw/pkg/tools" + memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store" + "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. type MemGPTTool struct { inner *memstore.MemoryTool @@ -21,7 +21,7 @@ type MemGPTTool struct { 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 { return &MemGPTTool{ inner: memstore.NewMemoryTool(store, agentID, session), diff --git a/pkg/agent/mentions/store.go b/pkg/agent/mentions/store.go index c6dee2ddb..f25dfe572 100644 --- a/pkg/agent/mentions/store.go +++ b/pkg/agent/mentions/store.go @@ -8,9 +8,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" "strings" - "github.com/sipeed/picoclaw/pkg/ids" - sqlc "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/pcerrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + sqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) // 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) { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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) 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) 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) 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) 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) 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) @@ -100,11 +100,11 @@ type ListByConversationParams struct { func (s *Store) ListByConversation(ctx context.Context, p ListByConversationParams) ([]sqlc.AgentMention, error) { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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, sqlc.ListAgentMentionsByConversationIDParams{ConversationID: convID}) diff --git a/pkg/agent/offloading_runtime_test.go b/pkg/agent/offloading_runtime_test.go index 65adfcb26..fe9d42d66 100644 --- a/pkg/agent/offloading_runtime_test.go +++ b/pkg/agent/offloading_runtime_test.go @@ -9,8 +9,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/sipeed/picoclaw/pkg/agent" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/agent" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) // staticToolRuntime is a test ToolRuntime that returns pre-defined results. diff --git a/pkg/agent/offloading_tool_runtime.go b/pkg/agent/offloading_tool_runtime.go index 59f23215d..e890bfe80 100644 --- a/pkg/agent/offloading_tool_runtime.go +++ b/pkg/agent/offloading_tool_runtime.go @@ -8,9 +8,9 @@ import ( "strings" "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/pcerrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) const defaultToolMaxConcurrency = 4 @@ -59,13 +59,13 @@ func (r OffloadingToolRuntime) Execute(ctx context.Context, tools []fantasy.Agen r.Base = fantasy.DAGToolRuntime{MaxConcurrency: defaultToolMaxConcurrency} } 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 { - 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() { - 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 diff --git a/pkg/agent/securebus_runtime.go b/pkg/agent/securebus_runtime.go index 444289a9f..a3d3a00f2 100644 --- a/pkg/agent/securebus_runtime.go +++ b/pkg/agent/securebus_runtime.go @@ -3,11 +3,12 @@ package agent import ( "context" "errors" + "fmt" fantasy "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/security/securebus" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus" ) // SecureBusToolRuntime is a fantasy.ToolRuntime that routes every tool call @@ -20,7 +21,7 @@ import ( // 4. Otherwise delegate to Base runtime for actual execution // 5. If bus detected a leak, replace Base output with the redacted version 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 // Bus is required. @@ -28,6 +29,11 @@ type SecureBusToolRuntime struct { // SessionKey is forwarded to bus requests for audit tracing. SessionKey string + + // Optional state persistence for runtime execution. + StateStore *StateStore + RunID ids.UUID + StepIndex int } // Execute implements fantasy.ToolRuntime. @@ -37,16 +43,24 @@ func (r SecureBusToolRuntime) Execute( toolCalls []fantasy.ToolCallContent, onResult func(fantasy.ToolResultContent) error, ) ([]fantasy.ToolResultContent, error) { - if r.Bus == nil || len(toolCalls) == 0 { - if r.Base != nil { - return r.Base.Execute(ctx, tools, toolCalls, onResult) - } + if len(toolCalls) == 0 { 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)) - 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() req := itr.NewToolExecRequest(reqID, r.SessionKey, tc.ToolCallID, tc.ToolName, tc.Input) busResp := r.Bus.Execute(ctx, req) @@ -58,22 +72,10 @@ func (r SecureBusToolRuntime) Execute( ToolName: tc.ToolName, Result: fantasy.ToolResultOutputContentError{Error: errors.New(busResp.Result)}, } - results = append(results, tr) - if onResult != nil { - if err := onResult(tr); err != nil { - 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}, - } + r.recordRunState(ctx, step, "tool_call_error", map[string]any{ + "tool_name": tc.ToolName, + "error": busResp.Result, + }) results = append(results, tr) if onResult != nil { if err := onResult(tr); err != nil { @@ -94,6 +96,9 @@ func (r SecureBusToolRuntime) Execute( br = overrideResultText(br, busResp.Result) } results = append(results, br) + r.recordRunState(ctx, step, "tool_result", map[string]any{ + "tool_name": tc.ToolName, + }) if onResult != nil { if err := onResult(br); err != nil { return results, err @@ -105,6 +110,13 @@ func (r SecureBusToolRuntime) Execute( 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 // the redacted version produced by the SecureBus. func overrideResultText(tr fantasy.ToolResultContent, text string) fantasy.ToolResultContent { diff --git a/pkg/agent/state_store.go b/pkg/agent/state_store.go index e407e0171..2bc7bd766 100644 --- a/pkg/agent/state_store.go +++ b/pkg/agent/state_store.go @@ -9,9 +9,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/pcerrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) // 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) { 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() { - 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{ @@ -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) { 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() { - return sqlc.AgentRun{}, pcerrors.New(pcerrors.CodeUnknown, "run id is empty") + return sqlc.AgentRun{}, dserrors.New(dserrors.CodeUnknown, "run id is empty") } 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(`{}`) @@ -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) { 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() { - 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 { - 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(`{}`) @@ -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) { 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() { - 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(`{}`) @@ -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) { 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() { - 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) == "" { - 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() { - 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(`{}`) @@ -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) { 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() { - 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{ 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) { 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() { - 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) == "" { - 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{ ConversationID: conversationID, diff --git a/pkg/agent/state_store_test.go b/pkg/agent/state_store_test.go index d09d0125b..a1a8a8f95 100644 --- a/pkg/agent/state_store_test.go +++ b/pkg/agent/state_store_test.go @@ -9,10 +9,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/sipeed/picoclaw/pkg/agent" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory/delegate" - "github.com/sipeed/picoclaw/pkg/memory/sqlc" + "github.com/ZanzyTHEbar/dragonscale/pkg/agent" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) func newTestQueries(t *testing.T) *testDB { diff --git a/pkg/agent/threads/store.go b/pkg/agent/threads/store.go index 646d319cb..ef0379279 100644 --- a/pkg/agent/threads/store.go +++ b/pkg/agent/threads/store.go @@ -9,9 +9,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" "strings" - "github.com/sipeed/picoclaw/pkg/ids" - sqlc "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/pcerrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + sqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) // 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) { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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) @@ -66,11 +66,11 @@ type ListParams struct { func (s *Store) List(ctx context.Context, p ListParams) ([]sqlc.AgentThread, error) { convIDStr := strings.TrimSpace(p.ConversationID) 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) 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, sqlc.ListAgentThreadsByConversationIDParams{ConversationID: convID}) @@ -91,11 +91,11 @@ type AddMessageParams struct { func (s *Store) AddMessage(ctx context.Context, p AddMessageParams) (sqlc.AgentThreadMessage, error) { threadIDStr := strings.TrimSpace(p.ThreadID) 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) 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) @@ -105,7 +105,7 @@ func (s *Store) AddMessage(ctx context.Context, p AddMessageParams) (sqlc.AgentT content := strings.TrimSpace(p.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) @@ -134,11 +134,11 @@ type ListMessagesParams struct { func (s *Store) ListMessages(ctx context.Context, p ListMessagesParams) ([]sqlc.AgentThreadMessage, error) { threadIDStr := strings.TrimSpace(p.ThreadID) 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) 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) diff --git a/pkg/agent/tool_result_search.go b/pkg/agent/tool_result_search.go index 1ee1b2556..0fde5cc02 100644 --- a/pkg/agent/tool_result_search.go +++ b/pkg/agent/tool_result_search.go @@ -9,9 +9,9 @@ import ( "github.com/go-json-experiment/json/jsontext" "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/pcerrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) 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) { 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) != "" { @@ -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) { @@ -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) { 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 { diff --git a/pkg/agent/tool_result_search_test.go b/pkg/agent/tool_result_search_test.go index bfd9f7fae..b07d31e83 100644 --- a/pkg/agent/tool_result_search_test.go +++ b/pkg/agent/tool_result_search_test.go @@ -10,7 +10,7 @@ import ( "github.com/stretchr/testify/assert" "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 diff --git a/pkg/agent/toolloop.go b/pkg/agent/toolloop.go index 9b9a58f1e..d22203a52 100644 --- a/pkg/agent/toolloop.go +++ b/pkg/agent/toolloop.go @@ -1,48 +1,135 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package agent import ( "context" "fmt" + "strings" fantasy "charm.land/fantasy" - picofantasy "github.com/sipeed/picoclaw/pkg/fantasy" - "github.com/sipeed/picoclaw/pkg/logger" - memstore "github.com/sipeed/picoclaw/pkg/memory/store" - "github.com/sipeed/picoclaw/pkg/tools" + picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) -// RunToolLoop executes an agent tool loop using Fantasy with the canonical -// PicoToolAdapter (schema unwrapping + offloading). This is the single -// implementation used by both main agent and subagents. -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 { +// MakeUnifiedRunLoopFunc wires subagent execution through the same unified +// runtime stack used by the main loop: SecureBus + offloading + run state. +func MakeUnifiedRunLoopFunc(al *AgentLoop) tools.RunLoopFunc { 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{ MemStore: ms, - AgentID: "picoclaw", - SessionKey: "", + AgentID: "dragonscale", + SessionKey: sessionKey, } 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{ fantasy.WithTools(adaptedTools...), 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 != "" { agentOpts = append(agentOpts, fantasy.WithSystemPrompt(systemPrompt)) diff --git a/pkg/auth/oauth.go b/pkg/auth/oauth.go index 00f185521..04827d97f 100644 --- a/pkg/auth/oauth.go +++ b/pkg/auth/oauth.go @@ -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.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...") select { @@ -313,7 +313,7 @@ func buildAuthorizeURL(cfg OAuthProviderConfig, pkce PKCECodes, state, redirectU "state": {state}, } if strings.Contains(strings.ToLower(cfg.Issuer), "auth.openai.com") { - params.Set("originator", "picoclaw") + params.Set("originator", "dragonscale") } if cfg.Originator != "" { params.Set("originator", cfg.Originator) diff --git a/pkg/auth/store.go b/pkg/auth/store.go index d0aa56154..7685d8eef 100644 --- a/pkg/auth/store.go +++ b/pkg/auth/store.go @@ -38,7 +38,7 @@ func (c *AuthCredential) NeedsRefresh() bool { func authFilePath() string { home, _ := os.UserHomeDir() - return filepath.Join(home, ".picoclaw", "auth.json") + return filepath.Join(home, ".dragonscale", "auth.json") } func LoadStore() (*AuthStore, error) { diff --git a/pkg/auth/store_test.go b/pkg/auth/store_test.go index d96b460a1..e914bbee0 100644 --- a/pkg/auth/store_test.go +++ b/pkg/auth/store_test.go @@ -102,7 +102,7 @@ func TestStoreFilePermissions(t *testing.T) { 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) if err != nil { t.Fatalf("Stat() error: %v", err) diff --git a/pkg/bus/bus.go b/pkg/bus/bus.go index 04f94a30e..6bf088992 100644 --- a/pkg/bus/bus.go +++ b/pkg/bus/bus.go @@ -4,7 +4,7 @@ import ( "context" "sync" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type MessageBus struct { diff --git a/pkg/channels/base.go b/pkg/channels/base.go index 427550f63..215662056 100644 --- a/pkg/channels/base.go +++ b/pkg/channels/base.go @@ -6,8 +6,8 @@ import ( "strings" "sync/atomic" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type Channel interface { diff --git a/pkg/channels/dingtalk.go b/pkg/channels/dingtalk.go index 263785c0c..9aae253e7 100644 --- a/pkg/channels/dingtalk.go +++ b/pkg/channels/dingtalk.go @@ -1,4 +1,4 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // DingTalk channel implementation using Stream Mode package channels @@ -8,12 +8,12 @@ import ( "fmt" "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/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 (钉钉) @@ -175,7 +175,7 @@ func (c *DingTalkChannel) SendDirectReply(ctx context.Context, sessionWebhook, c // Convert string content to []byte for the API contentBytes := []byte(content) - titleBytes := []byte("PicoClaw") + titleBytes := []byte("DragonScale") // Send markdown formatted reply err := replier.SimpleReplyMarkdown( diff --git a/pkg/channels/discord.go b/pkg/channels/discord.go index 11c254a47..c20461ef4 100644 --- a/pkg/channels/discord.go +++ b/pkg/channels/discord.go @@ -7,12 +7,12 @@ import ( "strings" "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/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 ( @@ -50,7 +50,7 @@ func (c *DiscordChannel) SetTranscriber(transcriber *voice.GroqTranscriber) { func (c *DiscordChannel) getContext() context.Context { if c.ctx == nil { - return context.TODO() + return context.Background() } return c.ctx } diff --git a/pkg/channels/feishu_32.go b/pkg/channels/feishu_32.go index 4e60fbc11..b3194523b 100644 --- a/pkg/channels/feishu_32.go +++ b/pkg/channels/feishu_32.go @@ -6,8 +6,8 @@ import ( "context" "errors" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) // FeishuChannel is a stub implementation for 32-bit architectures diff --git a/pkg/channels/feishu_64.go b/pkg/channels/feishu_64.go index 52f310996..6718ca4a4 100644 --- a/pkg/channels/feishu_64.go +++ b/pkg/channels/feishu_64.go @@ -15,10 +15,10 @@ import ( larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1" larkws "github.com/larksuite/oapi-sdk-go/v3/ws" - "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/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/utils" ) type FeishuChannel struct { @@ -109,7 +109,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error ReceiveId(msg.ChatID). MsgType(larkim.MsgTypeText). Content(string(payload)). - Uuid(fmt.Sprintf("picoclaw-%d", time.Now().UnixNano())). + Uuid(fmt.Sprintf("dragonscale-%d", time.Now().UnixNano())). Build()). Build() diff --git a/pkg/channels/line.go b/pkg/channels/line.go index 2703aeea8..3dae89b9b 100644 --- a/pkg/channels/line.go +++ b/pkg/channels/line.go @@ -17,10 +17,10 @@ import ( jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" - "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/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/utils" ) const ( diff --git a/pkg/channels/maixcam.go b/pkg/channels/maixcam.go index f1b786b95..cc7e45f34 100644 --- a/pkg/channels/maixcam.go +++ b/pkg/channels/maixcam.go @@ -8,9 +8,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type MaixCamChannel struct { diff --git a/pkg/channels/manager.go b/pkg/channels/manager.go index 7f6abc4cb..678912250 100644 --- a/pkg/channels/manager.go +++ b/pkg/channels/manager.go @@ -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 // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package channels @@ -11,10 +11,10 @@ import ( "fmt" "sync" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/constants" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/constants" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type Manager struct { diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go index 9e0f14e8a..e004f310a 100644 --- a/pkg/channels/onebot.go +++ b/pkg/channels/onebot.go @@ -12,9 +12,9 @@ import ( "github.com/go-json-experiment/json/jsontext" "github.com/gorilla/websocket" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type OneBotChannel struct { diff --git a/pkg/channels/qq.go b/pkg/channels/qq.go index 18b4ca0e0..559299c87 100644 --- a/pkg/channels/qq.go +++ b/pkg/channels/qq.go @@ -13,9 +13,9 @@ import ( "github.com/tencent-connect/botgo/token" "golang.org/x/oauth2" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type QQChannel struct { diff --git a/pkg/channels/slack.go b/pkg/channels/slack.go index 5387e9213..442426040 100644 --- a/pkg/channels/slack.go +++ b/pkg/channels/slack.go @@ -12,11 +12,11 @@ import ( "github.com/slack-go/slack/slackevents" "github.com/slack-go/slack/socketmode" - "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" + "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" ) type SlackChannel struct { diff --git a/pkg/channels/slack_test.go b/pkg/channels/slack_test.go index 3707c2703..3b51c2ca0 100644 --- a/pkg/channels/slack_test.go +++ b/pkg/channels/slack_test.go @@ -3,8 +3,8 @@ package channels import ( "testing" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) func TestParseSlackChatID(t *testing.T) { diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 5601d508c..4e599f639 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -17,11 +17,11 @@ import ( "github.com/mymmrac/telego/telegohandler" tu "github.com/mymmrac/telego/telegoutil" - "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" + "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" ) type TelegramChannel struct { diff --git a/pkg/channels/telegram_commands.go b/pkg/channels/telegram_commands.go index df245e156..89ffe58ec 100644 --- a/pkg/channels/telegram_commands.go +++ b/pkg/channels/telegram_commands.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" "github.com/mymmrac/telego" - "github.com/sipeed/picoclaw/pkg/config" ) 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 { _, err := c.bot.SendMessage(ctx, &telego.SendMessageParams{ ChatID: telego.ChatID{ID: message.Chat.ID}, - Text: "Hello! I am PicoClaw 🦞", + Text: "Hello! I am DragonScale 🦞", ReplyParameters: &telego.ReplyParameters{ MessageID: message.MessageID, }, diff --git a/pkg/channels/whatsapp.go b/pkg/channels/whatsapp.go index e30861117..9aaa188af 100644 --- a/pkg/channels/whatsapp.go +++ b/pkg/channels/whatsapp.go @@ -10,9 +10,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" "github.com/gorilla/websocket" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/utils" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/utils" ) type WhatsAppChannel struct { diff --git a/pkg/cron/service.go b/pkg/cron/service.go index 2026bea14..762974b50 100644 --- a/pkg/cron/service.go +++ b/pkg/cron/service.go @@ -15,7 +15,7 @@ import ( jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) type CronSchedule struct { diff --git a/pkg/devices/service.go b/pkg/devices/service.go index 05a254729..c4bdd1c2b 100644 --- a/pkg/devices/service.go +++ b/pkg/devices/service.go @@ -5,12 +5,12 @@ import ( "strings" "sync" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/constants" - "github.com/sipeed/picoclaw/pkg/devices/events" - "github.com/sipeed/picoclaw/pkg/devices/sources" - "github.com/sipeed/picoclaw/pkg/logger" - "github.com/sipeed/picoclaw/pkg/state" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/constants" + "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events" + "github.com/ZanzyTHEbar/dragonscale/pkg/devices/sources" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/state" ) type Service struct { diff --git a/pkg/devices/source.go b/pkg/devices/source.go index cbf0a7d88..e1d6516dd 100644 --- a/pkg/devices/source.go +++ b/pkg/devices/source.go @@ -1,5 +1,5 @@ package devices -import "github.com/sipeed/picoclaw/pkg/devices/events" +import "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events" type EventSource = events.EventSource diff --git a/pkg/devices/sources/usb_linux.go b/pkg/devices/sources/usb_linux.go index 1f6c068b3..ea17b9278 100644 --- a/pkg/devices/sources/usb_linux.go +++ b/pkg/devices/sources/usb_linux.go @@ -10,8 +10,8 @@ import ( "strings" "sync" - "github.com/sipeed/picoclaw/pkg/devices/events" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) var usbClassToCapability = map[string]string{ diff --git a/pkg/devices/sources/usb_stub.go b/pkg/devices/sources/usb_stub.go index f08c2d406..b41edbf6f 100644 --- a/pkg/devices/sources/usb_stub.go +++ b/pkg/devices/sources/usb_stub.go @@ -5,7 +5,7 @@ package sources import ( "context" - "github.com/sipeed/picoclaw/pkg/devices/events" + "github.com/ZanzyTHEbar/dragonscale/pkg/devices/events" ) type USBMonitor struct{} diff --git a/pkg/pcerrors/cli.go b/pkg/dserrors/cli.go similarity index 97% rename from pkg/pcerrors/cli.go rename to pkg/dserrors/cli.go index d95232479..819858486 100644 --- a/pkg/pcerrors/cli.go +++ b/pkg/dserrors/cli.go @@ -1,4 +1,4 @@ -package pcerrors +package dserrors import ( "fmt" @@ -10,7 +10,7 @@ import ( 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 // code. Optional Verbose and Redact flags layer on debug traces and sensitive- diff --git a/pkg/pcerrors/pcerrors.go b/pkg/dserrors/pcerrors.go similarity index 97% rename from pkg/pcerrors/pcerrors.go rename to pkg/dserrors/pcerrors.go index b1def407a..11b203453 100644 --- a/pkg/pcerrors/pcerrors.go +++ b/pkg/dserrors/pcerrors.go @@ -1,4 +1,4 @@ -package pcerrors +package dserrors import ( "context" @@ -9,7 +9,7 @@ import ( 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 // 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: // - implements error diff --git a/pkg/fantasy/adapter.go b/pkg/fantasy/adapter.go index 2997cffaa..9b531b1e6 100644 --- a/pkg/fantasy/adapter.go +++ b/pkg/fantasy/adapter.go @@ -1,7 +1,7 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package fantasy @@ -12,14 +12,14 @@ import ( "charm.land/fantasy" jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/logger" - memstore "github.com/sipeed/picoclaw/pkg/memory/store" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) -// PicoToolAdapter wraps a PicoClaw tool as a Fantasy AgentTool. -// It bridges PicoClaw's dual-channel ToolResult semantics with Fantasy's +// PicoToolAdapter wraps a DragonScale tool as a Fantasy AgentTool. +// It bridges DragonScale's dual-channel ToolResult semantics with Fantasy's // simple ToolResponse by publishing ForUser content to the bus as a side effect // and returning only ForLLM content to Fantasy. type PicoToolAdapter struct { @@ -35,9 +35,9 @@ type PicoToolAdapter struct { // Compile-time check that PicoToolAdapter implements fantasy.AgentTool. 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 -// 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" // keys), so we must unwrap it here to avoid double-wrapping in // agent.prepareTools() and agent.validateToolCall(). @@ -77,14 +77,14 @@ func unwrapSchema(params map[string]interface{}) (map[string]interface{}, []stri 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: // - 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 an AsyncTool, wires a callback that publishes results to the bus. 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) if 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) if 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 } -// 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 { 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) {} // AdaptedToolsConfig configures how tools are adapted for the Fantasy agent. diff --git a/pkg/fantasy/adapter_test.go b/pkg/fantasy/adapter_test.go index 7d42ea933..eb78c569c 100644 --- a/pkg/fantasy/adapter_test.go +++ b/pkg/fantasy/adapter_test.go @@ -5,8 +5,8 @@ import ( "testing" "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) // --- Mock tool implementations --- diff --git a/pkg/fantasy/claude_cli.go b/pkg/fantasy/claude_cli.go index 9f71ebbfa..e42db8469 100644 --- a/pkg/fantasy/claude_cli.go +++ b/pkg/fantasy/claude_cli.go @@ -1,7 +1,7 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package fantasy diff --git a/pkg/fantasy/convert.go b/pkg/fantasy/convert.go index 02c09d3c0..6f7178faf 100644 --- a/pkg/fantasy/convert.go +++ b/pkg/fantasy/convert.go @@ -1,7 +1,7 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package fantasy @@ -9,10 +9,10 @@ import ( "charm.land/fantasy" 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 { out := make([]fantasy.Message, 0, len(msgs)) @@ -23,7 +23,7 @@ func MessagesToFantasy(msgs []messages.Message) []fantasy.Message { 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 { 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 // and zero or more tool result messages. func StepToMessages(step fantasy.StepResult) []messages.Message { @@ -129,7 +129,7 @@ func StepToMessages(step fantasy.StepResult) []messages.Message { 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. func AgentResultToMessages(result *fantasy.AgentResult) []messages.Message { var out []messages.Message diff --git a/pkg/fantasy/convert_test.go b/pkg/fantasy/convert_test.go index a4762830e..ad2346f93 100644 --- a/pkg/fantasy/convert_test.go +++ b/pkg/fantasy/convert_test.go @@ -5,7 +5,7 @@ import ( "testing" "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/messages" + "github.com/ZanzyTHEbar/dragonscale/pkg/messages" ) // --- MessagesToFantasy Tests --- @@ -417,7 +417,7 @@ func TestAgentResultToMessages_MultipleSteps(t *testing.T) { // --- Round-trip fidelity test --- 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{ {Role: "user", Content: "Read the file"}, { diff --git a/pkg/fantasy/factory.go b/pkg/fantasy/factory.go index 021dab545..88833c12b 100644 --- a/pkg/fantasy/factory.go +++ b/pkg/fantasy/factory.go @@ -1,7 +1,7 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package fantasy @@ -15,8 +15,8 @@ import ( "charm.land/fantasy" "charm.land/fantasy/providers/openaicompat" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" "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 @@ -191,7 +191,7 @@ func (e *providerEntry) resolveBase(cfg *config.Config) string { 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) { providerName := strings.ToLower(cfg.Agents.Defaults.Provider) model := cfg.Agents.Defaults.Model @@ -333,7 +333,7 @@ func defaultIfEmpty(val, fallback string) string { func providerNameOrDefault(name string) string { if name == "" { - return "picoclaw" + return "dragonscale" } return name } diff --git a/pkg/heartbeat/service.go b/pkg/heartbeat/service.go index b5e8a132f..cce1448aa 100644 --- a/pkg/heartbeat/service.go +++ b/pkg/heartbeat/service.go @@ -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 // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package heartbeat @@ -14,11 +14,11 @@ import ( "sync" "time" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/constants" - "github.com/sipeed/picoclaw/pkg/logger" - "github.com/sipeed/picoclaw/pkg/state" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/constants" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/state" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) const ( @@ -31,12 +31,17 @@ const ( // channel and chatID are derived from the last active user channel. 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 type HeartbeatService struct { workspace string bus *bus.MessageBus state *state.Manager handler HeartbeatHandler + dueCtx DueContextProvider interval time.Duration enabled bool mu sync.RWMutex @@ -76,6 +81,14 @@ func (hs *HeartbeatService) SetHandler(handler HeartbeatHandler) { 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 func (hs *HeartbeatService) Start() error { hs.mu.Lock() @@ -148,6 +161,7 @@ func (hs *HeartbeatService) executeHeartbeat() { enabled := hs.enabled stopped := hs.stopChan == nil handler := hs.handler + dueCtxProvider := hs.dueCtx hs.mu.RUnlock() if !enabled || stopped { @@ -156,9 +170,19 @@ func (hs *HeartbeatService) executeHeartbeat() { 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 == "" { - logger.InfoC("heartbeat", "No heartbeat prompt (HEARTBEAT.md empty or missing)") + logger.InfoC("heartbeat", "No heartbeat prompt or due obligations") return } @@ -213,23 +237,28 @@ func (hs *HeartbeatService) executeHeartbeat() { } // 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") data, err := os.ReadFile(heartbeatPath) if err != nil { if os.IsNotExist(err) { hs.createDefaultHeartbeatTemplate() + data = []byte{} + } else { + hs.logError("Error reading HEARTBEAT.md: %v", err) return "" } - hs.logError("Error reading HEARTBEAT.md: %v", err) - return "" } - content := string(data) - if len(content) == 0 { + content := strings.TrimSpace(string(data)) + dueContext = strings.TrimSpace(dueContext) + if content == "" && dueContext == "" { return "" } + if dueContext == "" { + dueContext = "None." + } now := time.Now().Format("2006-01-02 15:04:05") 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. If there is nothing that requires attention, respond ONLY with: HEARTBEAT_OK +## Due Obligations %s -`, now, content) + +%s +`, now, dueContext, content) } // createDefaultHeartbeatTemplate creates the default HEARTBEAT.md file diff --git a/pkg/heartbeat/service_test.go b/pkg/heartbeat/service_test.go index a2b59e350..a6e062b22 100644 --- a/pkg/heartbeat/service_test.go +++ b/pkg/heartbeat/service_test.go @@ -3,10 +3,11 @@ package heartbeat import ( "os" "path/filepath" + "strings" "testing" "time" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) func TestExecuteHeartbeat_Async(t *testing.T) { @@ -211,7 +212,7 @@ func TestHeartbeatFilePath(t *testing.T) { hs := NewHeartbeatService(tmpDir, 30, true) // Trigger default template creation - hs.buildPrompt() + hs.buildPrompt("") // Verify HEARTBEAT.md exists at workspace root 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) } } + +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") + } +} diff --git a/pkg/itr/commands.fbs b/pkg/itr/commands.fbs index 553064a12..348fafaae 100644 --- a/pkg/itr/commands.fbs +++ b/pkg/itr/commands.fbs @@ -1,4 +1,4 @@ -// PicoClaw Isolated Tool Runtime — Command Protocol +// DragonScale Isolated Tool Runtime — Command Protocol // Namespace: itrfb (FlatBuffers generated types) // // This schema defines the binary command protocol between the agent loop diff --git a/pkg/itr/commands.go b/pkg/itr/commands.go index 6cb16792e..acf67be04 100644 --- a/pkg/itr/commands.go +++ b/pkg/itr/commands.go @@ -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 // recursive decomposition operations — are serialized as ToolRequest / // ToolResponse pairs. diff --git a/pkg/itr/dag/executor.go b/pkg/itr/dag/executor.go index f2726bf70..907d8de92 100644 --- a/pkg/itr/dag/executor.go +++ b/pkg/itr/dag/executor.go @@ -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 // topological wave order via the SecureBus, and synthesises a final result // using the Joiner pattern. @@ -18,8 +18,8 @@ import ( "strings" "sync" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/security/securebus" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus" ) // JoinerFunc synthesises a final answer from all node results. diff --git a/pkg/itr/dag/executor_test.go b/pkg/itr/dag/executor_test.go index efb6fdc0a..cca541b2d 100644 --- a/pkg/itr/dag/executor_test.go +++ b/pkg/itr/dag/executor_test.go @@ -6,10 +6,10 @@ import ( jsonv2 "github.com/go-json-experiment/json" "testing" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/itr/dag" - "github.com/sipeed/picoclaw/pkg/security/securebus" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr/dag" + "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/itr/dag/planner.go b/pkg/itr/dag/planner.go index 1b82a56b5..4a4c2914f 100644 --- a/pkg/itr/dag/planner.go +++ b/pkg/itr/dag/planner.go @@ -5,8 +5,8 @@ import ( "fmt" jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) // PlannerFunc calls the LLM with a system prompt and user query, returning diff --git a/pkg/itr/dag/planner_test.go b/pkg/itr/dag/planner_test.go index 0ff511978..fae836ad1 100644 --- a/pkg/itr/dag/planner_test.go +++ b/pkg/itr/dag/planner_test.go @@ -5,7 +5,7 @@ import ( jsonv2 "github.com/go-json-experiment/json" "testing" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/itr/dag/replan.go b/pkg/itr/dag/replan.go index 5f4dbcf41..f755e5a24 100644 --- a/pkg/itr/dag/replan.go +++ b/pkg/itr/dag/replan.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" ) // ReplanConfig configures the iterative replanning loop. diff --git a/pkg/itr/fb_codec.go b/pkg/itr/fb_codec.go index 8c9174843..fe43801d1 100644 --- a/pkg/itr/fb_codec.go +++ b/pkg/itr/fb_codec.go @@ -3,8 +3,8 @@ package itr import ( "fmt" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr/itrfb" flatbuffers "github.com/google/flatbuffers/go" - "github.com/sipeed/picoclaw/pkg/itr/itrfb" ) // ── Domain ↔ FlatBuffers enum mapping ─────────────────────────────────────── @@ -67,7 +67,7 @@ func MarshalRequestFB(r ToolRequest) ([]byte, error) { itrfb.ToolRequestAddSessionKey(b, skOff) itrfb.ToolRequestAddToolCallId(b, tcOff) reqOff := itrfb.ToolRequestEnd(b) - itrfb.FinishToolRequestBuffer(b, reqOff) + b.Finish(reqOff) return b.FinishedBytes(), nil } @@ -86,7 +86,7 @@ func MarshalRequestFB(r ToolRequest) ([]byte, error) { itrfb.ToolRequestAddSessionKey(b, skOff) itrfb.ToolRequestAddToolCallId(b, tcOff) reqOff := itrfb.ToolRequestEnd(b) - itrfb.FinishToolRequestBuffer(b, reqOff) + b.Finish(reqOff) return b.FinishedBytes(), nil } @@ -161,7 +161,7 @@ func MarshalResponseFB(r ToolResponse) ([]byte, error) { itrfb.ToolResponseAddRedactedKeys(b, keysOff) } respOff := itrfb.ToolResponseEnd(b) - itrfb.FinishToolResponseBuffer(b, respOff) + b.Finish(respOff) return b.FinishedBytes(), nil } diff --git a/pkg/itr/fb_codec_test.go b/pkg/itr/fb_codec_test.go index 8a817c014..78e86d2b0 100644 --- a/pkg/itr/fb_codec_test.go +++ b/pkg/itr/fb_codec_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/itr/itrfb/CodeExec.go b/pkg/itr/itrfb/CodeExec.go index 2b844c817..43fcd2251 100644 --- a/pkg/itr/itrfb/CodeExec.go +++ b/pkg/itr/itrfb/CodeExec.go @@ -17,10 +17,6 @@ func GetRootAsCodeExec(buf []byte, offset flatbuffers.UOffsetT) *CodeExec { return x } -func FinishCodeExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsCodeExec(buf []byte, offset flatbuffers.UOffsetT) *CodeExec { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &CodeExec{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsCodeExec(buf []byte, offset flatbuffers.UOffsetT) *Cod return x } -func FinishSizePrefixedCodeExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *CodeExec) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/DAGNode.go b/pkg/itr/itrfb/DAGNode.go index ab195b60e..d1efd239e 100644 --- a/pkg/itr/itrfb/DAGNode.go +++ b/pkg/itr/itrfb/DAGNode.go @@ -17,10 +17,6 @@ func GetRootAsDAGNode(buf []byte, offset flatbuffers.UOffsetT) *DAGNode { return x } -func FinishDAGNodeBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsDAGNode(buf []byte, offset flatbuffers.UOffsetT) *DAGNode { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &DAGNode{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsDAGNode(buf []byte, offset flatbuffers.UOffsetT) *DAGN return x } -func FinishSizePrefixedDAGNodeBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *DAGNode) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/DAGPlan.go b/pkg/itr/itrfb/DAGPlan.go index 5f10fee0d..76494ea55 100644 --- a/pkg/itr/itrfb/DAGPlan.go +++ b/pkg/itr/itrfb/DAGPlan.go @@ -17,10 +17,6 @@ func GetRootAsDAGPlan(buf []byte, offset flatbuffers.UOffsetT) *DAGPlan { return x } -func FinishDAGPlanBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsDAGPlan(buf []byte, offset flatbuffers.UOffsetT) *DAGPlan { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &DAGPlan{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsDAGPlan(buf []byte, offset flatbuffers.UOffsetT) *DAGP return x } -func FinishSizePrefixedDAGPlanBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *DAGPlan) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/ExecWasm.go b/pkg/itr/itrfb/ExecWasm.go index 863a5bd1f..979019bb0 100644 --- a/pkg/itr/itrfb/ExecWasm.go +++ b/pkg/itr/itrfb/ExecWasm.go @@ -17,10 +17,6 @@ func GetRootAsExecWasm(buf []byte, offset flatbuffers.UOffsetT) *ExecWasm { return x } -func FinishExecWasmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsExecWasm(buf []byte, offset flatbuffers.UOffsetT) *ExecWasm { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &ExecWasm{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsExecWasm(buf []byte, offset flatbuffers.UOffsetT) *Exe return x } -func FinishSizePrefixedExecWasmBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *ExecWasm) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/Final.go b/pkg/itr/itrfb/Final.go index 7c19dd41d..cb467835c 100644 --- a/pkg/itr/itrfb/Final.go +++ b/pkg/itr/itrfb/Final.go @@ -17,10 +17,6 @@ func GetRootAsFinal(buf []byte, offset flatbuffers.UOffsetT) *Final { return x } -func FinishFinalBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsFinal(buf []byte, offset flatbuffers.UOffsetT) *Final { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &Final{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsFinal(buf []byte, offset flatbuffers.UOffsetT) *Final return x } -func FinishSizePrefixedFinalBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *Final) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/Grep.go b/pkg/itr/itrfb/Grep.go index 4ede94037..958f5f439 100644 --- a/pkg/itr/itrfb/Grep.go +++ b/pkg/itr/itrfb/Grep.go @@ -17,10 +17,6 @@ func GetRootAsGrep(buf []byte, offset flatbuffers.UOffsetT) *Grep { return x } -func FinishGrepBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsGrep(buf []byte, offset flatbuffers.UOffsetT) *Grep { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &Grep{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsGrep(buf []byte, offset flatbuffers.UOffsetT) *Grep { return x } -func FinishSizePrefixedGrepBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *Grep) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/Partition.go b/pkg/itr/itrfb/Partition.go index 80cfca6c6..1d92ecd9a 100644 --- a/pkg/itr/itrfb/Partition.go +++ b/pkg/itr/itrfb/Partition.go @@ -17,10 +17,6 @@ func GetRootAsPartition(buf []byte, offset flatbuffers.UOffsetT) *Partition { return x } -func FinishPartitionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsPartition(buf []byte, offset flatbuffers.UOffsetT) *Partition { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &Partition{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsPartition(buf []byte, offset flatbuffers.UOffsetT) *Pa return x } -func FinishSizePrefixedPartitionBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *Partition) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/Peek.go b/pkg/itr/itrfb/Peek.go index ce1daef0c..4e64b567d 100644 --- a/pkg/itr/itrfb/Peek.go +++ b/pkg/itr/itrfb/Peek.go @@ -17,10 +17,6 @@ func GetRootAsPeek(buf []byte, offset flatbuffers.UOffsetT) *Peek { return x } -func FinishPeekBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsPeek(buf []byte, offset flatbuffers.UOffsetT) *Peek { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &Peek{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsPeek(buf []byte, offset flatbuffers.UOffsetT) *Peek { return x } -func FinishSizePrefixedPeekBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *Peek) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/Recurse.go b/pkg/itr/itrfb/Recurse.go index 666f2fdc8..2302fd801 100644 --- a/pkg/itr/itrfb/Recurse.go +++ b/pkg/itr/itrfb/Recurse.go @@ -17,10 +17,6 @@ func GetRootAsRecurse(buf []byte, offset flatbuffers.UOffsetT) *Recurse { return x } -func FinishRecurseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsRecurse(buf []byte, offset flatbuffers.UOffsetT) *Recurse { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &Recurse{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsRecurse(buf []byte, offset flatbuffers.UOffsetT) *Recu return x } -func FinishSizePrefixedRecurseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *Recurse) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/ToolExec.go b/pkg/itr/itrfb/ToolExec.go index 2651fb200..e937707e7 100644 --- a/pkg/itr/itrfb/ToolExec.go +++ b/pkg/itr/itrfb/ToolExec.go @@ -17,10 +17,6 @@ func GetRootAsToolExec(buf []byte, offset flatbuffers.UOffsetT) *ToolExec { return x } -func FinishToolExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsToolExec(buf []byte, offset flatbuffers.UOffsetT) *ToolExec { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &ToolExec{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsToolExec(buf []byte, offset flatbuffers.UOffsetT) *Too return x } -func FinishSizePrefixedToolExecBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *ToolExec) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/ToolRequest.go b/pkg/itr/itrfb/ToolRequest.go index d235e7b39..4404d45b3 100644 --- a/pkg/itr/itrfb/ToolRequest.go +++ b/pkg/itr/itrfb/ToolRequest.go @@ -17,10 +17,6 @@ func GetRootAsToolRequest(buf []byte, offset flatbuffers.UOffsetT) *ToolRequest return x } -func FinishToolRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsToolRequest(buf []byte, offset flatbuffers.UOffsetT) *ToolRequest { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &ToolRequest{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsToolRequest(buf []byte, offset flatbuffers.UOffsetT) * return x } -func FinishSizePrefixedToolRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *ToolRequest) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/ToolResponse.go b/pkg/itr/itrfb/ToolResponse.go index 64b59fbb2..0486e18be 100644 --- a/pkg/itr/itrfb/ToolResponse.go +++ b/pkg/itr/itrfb/ToolResponse.go @@ -17,10 +17,6 @@ func GetRootAsToolResponse(buf []byte, offset flatbuffers.UOffsetT) *ToolRespons return x } -func FinishToolResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsToolResponse(buf []byte, offset flatbuffers.UOffsetT) *ToolResponse { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &ToolResponse{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsToolResponse(buf []byte, offset flatbuffers.UOffsetT) return x } -func FinishSizePrefixedToolResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *ToolResponse) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/itrfb/ToolSearch.go b/pkg/itr/itrfb/ToolSearch.go index f0f17f407..1bffca0cb 100644 --- a/pkg/itr/itrfb/ToolSearch.go +++ b/pkg/itr/itrfb/ToolSearch.go @@ -17,10 +17,6 @@ func GetRootAsToolSearch(buf []byte, offset flatbuffers.UOffsetT) *ToolSearch { return x } -func FinishToolSearchBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.Finish(offset) -} - func GetSizePrefixedRootAsToolSearch(buf []byte, offset flatbuffers.UOffsetT) *ToolSearch { n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) x := &ToolSearch{} @@ -28,10 +24,6 @@ func GetSizePrefixedRootAsToolSearch(buf []byte, offset flatbuffers.UOffsetT) *T return x } -func FinishSizePrefixedToolSearchBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { - builder.FinishSizePrefixed(offset) -} - func (rcv *ToolSearch) Init(buf []byte, i flatbuffers.UOffsetT) { rcv._tab.Bytes = buf rcv._tab.Pos = i diff --git a/pkg/itr/wasm/transport.go b/pkg/itr/wasm/transport.go index 1a45c9e49..e36d1bc36 100644 --- a/pkg/itr/wasm/transport.go +++ b/pkg/itr/wasm/transport.go @@ -5,7 +5,7 @@ import ( "fmt" "time" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" ) // Transport implements the SecureBus transport interface for WASM-isolated diff --git a/pkg/itr/wasm/transport_test.go b/pkg/itr/wasm/transport_test.go index f06ee9412..86bd4c9fe 100644 --- a/pkg/itr/wasm/transport_test.go +++ b/pkg/itr/wasm/transport_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/memory/dag/compress.go b/pkg/memory/dag/compress.go index fc2baf31b..d23bcb627 100644 --- a/pkg/memory/dag/compress.go +++ b/pkg/memory/dag/compress.go @@ -4,6 +4,8 @@ import ( "fmt" "strings" "unicode/utf8" + + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/observation" ) // Message is a minimal message representation for DAG compression. @@ -226,7 +228,7 @@ func findSentenceEnd(s string) int { return -1 } -// estimateTokens provides a rough token count (chars * 2/5 heuristic). +// estimateTokens uses the shared runtime estimator for consistent budgeting. func estimateTokens(s string) int { - return utf8.RuneCountInString(s) * 2 / 5 + return observation.EstimateTokens(s) } diff --git a/pkg/memory/delegate/factory.go b/pkg/memory/delegate/factory.go index 9902df056..751b7c5ff 100644 --- a/pkg/memory/delegate/factory.go +++ b/pkg/memory/delegate/factory.go @@ -4,8 +4,8 @@ import ( "fmt" "time" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) // NewFromConfig creates a LibSQLDelegate from the application config. diff --git a/pkg/memory/delegate/factory_test.go b/pkg/memory/delegate/factory_test.go index 17c12808a..a147dd726 100644 --- a/pkg/memory/delegate/factory_test.go +++ b/pkg/memory/delegate/factory_test.go @@ -6,9 +6,9 @@ import ( "path/filepath" "testing" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) func TestNewFromConfig_LocalOnly_DefaultPath(t *testing.T) { diff --git a/pkg/memory/delegate/search_fts.go b/pkg/memory/delegate/search_fts.go index c7e275bfa..533446e9d 100644 --- a/pkg/memory/delegate/search_fts.go +++ b/pkg/memory/delegate/search_fts.go @@ -6,7 +6,7 @@ import ( "strings" "unicode" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) const ftsSearchQuery = ` diff --git a/pkg/memory/delegate/search_vector.go b/pkg/memory/delegate/search_vector.go index b73a12c1d..3273cd43e 100644 --- a/pkg/memory/delegate/search_vector.go +++ b/pkg/memory/delegate/search_vector.go @@ -7,8 +7,8 @@ import ( "math" "strings" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) const vectorSearchANN = ` diff --git a/pkg/memory/delegate/sqlite.go b/pkg/memory/delegate/sqlite.go index 790742a45..f7f694de0 100644 --- a/pkg/memory/delegate/sqlite.go +++ b/pkg/memory/delegate/sqlite.go @@ -7,11 +7,12 @@ import ( "fmt" "time" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/dag" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/migrations" + memsqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" "github.com/pressly/goose/v3" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" - "github.com/sipeed/picoclaw/pkg/memory/migrations" - memsqlc "github.com/sipeed/picoclaw/pkg/memory/sqlc" libsql "github.com/tursodatabase/go-libsql" ) @@ -689,6 +690,11 @@ func (d *LibSQLDelegate) CountAuditEntriesByAction(ctx context.Context, agentID, return int(count), err } +// PersistDAG implements dag.DAGPersister. Persists DAG snapshot to storage. +func (d *LibSQLDelegate) PersistDAG(ctx context.Context, agentID, sessionKey string, snap *dag.PersistSnapshot) error { + return dag.PersistDAG(ctx, d.db, d.queries, agentID, sessionKey, snap) +} + // --- Conversion helpers --- func sqlcRecallToMemory(row memsqlc.RecallItem) *memory.RecallItem { diff --git a/pkg/memory/delegate/sqlite_audit_test.go b/pkg/memory/delegate/sqlite_audit_test.go index 125c1b115..294f7c21a 100644 --- a/pkg/memory/delegate/sqlite_audit_test.go +++ b/pkg/memory/delegate/sqlite_audit_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/memory/delegate/sqlite_batch.go b/pkg/memory/delegate/sqlite_batch.go index f3708eb05..625882651 100644 --- a/pkg/memory/delegate/sqlite_batch.go +++ b/pkg/memory/delegate/sqlite_batch.go @@ -5,7 +5,7 @@ import ( "database/sql" "fmt" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // InsertRecallItemsBatch inserts a slice of RecallItems within a single diff --git a/pkg/memory/delegate/sqlite_bench_test.go b/pkg/memory/delegate/sqlite_bench_test.go index e2294dff3..7307ae51a 100644 --- a/pkg/memory/delegate/sqlite_bench_test.go +++ b/pkg/memory/delegate/sqlite_bench_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) func BenchmarkListRecallItems(b *testing.B) { diff --git a/pkg/memory/delegate/sqlite_doc_test.go b/pkg/memory/delegate/sqlite_doc_test.go index 46811dbd5..83db598f2 100644 --- a/pkg/memory/delegate/sqlite_doc_test.go +++ b/pkg/memory/delegate/sqlite_doc_test.go @@ -4,8 +4,8 @@ import ( "context" "testing" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/memory/delegate/sqlite_integration_test.go b/pkg/memory/delegate/sqlite_integration_test.go index 1917bedcd..a835f7523 100644 --- a/pkg/memory/delegate/sqlite_integration_test.go +++ b/pkg/memory/delegate/sqlite_integration_test.go @@ -5,8 +5,8 @@ import ( jsonv2 "github.com/go-json-experiment/json" "testing" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -14,7 +14,7 @@ import ( func TestCronKVBackend_Roundtrip(t *testing.T) { d := newTestDelegate(t) ctx := context.Background() - agentID := "picoclaw" + agentID := "dragonscale" kvKey := "cron:store" type cronStore struct { @@ -61,7 +61,7 @@ func TestCronKVBackend_Roundtrip(t *testing.T) { func TestCronKVBackend_UpdatePreservesShape(t *testing.T) { d := newTestDelegate(t) ctx := context.Background() - agentID := "picoclaw" + agentID := "dragonscale" kvKey := "cron:store" v1 := `{"version":1,"jobs":[{"id":"j1","name":"test","enabled":true}]}` @@ -78,7 +78,7 @@ func TestCronKVBackend_UpdatePreservesShape(t *testing.T) { func TestCronKVBackend_PrefixScan(t *testing.T) { d := newTestDelegate(t) ctx := context.Background() - agentID := "picoclaw" + agentID := "dragonscale" require.NoError(t, d.UpsertKV(ctx, agentID, "cron:store", "{}")) require.NoError(t, d.UpsertKV(ctx, agentID, "cron:lock", "held")) @@ -149,14 +149,14 @@ func TestEndToEnd_SessionAndAuditFlow(t *testing.T) { AgentID: agentID, Name: "AGENT.md", Category: "core", - Content: "# Agent Identity\nI am picoclaw.", + Content: "# Agent Identity\nI am dragonscale.", } require.NoError(t, d.UpsertDocument(ctx, doc)) loadedDoc, err := d.GetDocument(ctx, agentID, "AGENT.md") require.NoError(t, err) require.NotNil(t, loadedDoc) - assert.Equal(t, "# Agent Identity\nI am picoclaw.", loadedDoc.Content) + assert.Equal(t, "# Agent Identity\nI am dragonscale.", loadedDoc.Content) // 8. Verify cross-table isolation: different session sees nothing otherMsgs, err := d.ListSessionMessages(ctx, agentID, "sess-other", "", 50) diff --git a/pkg/memory/delegate/sqlite_session_msg_test.go b/pkg/memory/delegate/sqlite_session_msg_test.go index 6d5bc4be0..276146960 100644 --- a/pkg/memory/delegate/sqlite_session_msg_test.go +++ b/pkg/memory/delegate/sqlite_session_msg_test.go @@ -5,8 +5,8 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/memory/delegate/sqlite_test.go b/pkg/memory/delegate/sqlite_test.go index d3131743d..bce5372c0 100644 --- a/pkg/memory/delegate/sqlite_test.go +++ b/pkg/memory/delegate/sqlite_test.go @@ -5,8 +5,8 @@ import ( "fmt" "testing" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) func newTestDelegate(t *testing.T) *LibSQLDelegate { diff --git a/pkg/memory/integration_test.go b/pkg/memory/integration_test.go index d08a7c6a3..1b9d9803f 100644 --- a/pkg/memory/integration_test.go +++ b/pkg/memory/integration_test.go @@ -6,10 +6,10 @@ import ( "context" "testing" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" - "github.com/sipeed/picoclaw/pkg/memory/delegate" - memstore "github.com/sipeed/picoclaw/pkg/memory/store" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate" + memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/memory/memory.go b/pkg/memory/memory.go index 64df0d2b0..ab639ca58 100644 --- a/pkg/memory/memory.go +++ b/pkg/memory/memory.go @@ -1,4 +1,4 @@ -// Package memory provides a MemGPT-style 3-tier memory system for the PicoClaw agent. +// Package memory provides a MemGPT-style 3-tier memory system for the DragonScale agent. // // Architecture follows the Memory (logic) + MemoryDelegate (backend) pattern: // - Memory: orchestrates tiers, scoring, context pressure, retrieval pipeline @@ -18,7 +18,7 @@ import ( "math" "time" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) // Sector classifies the type of memory for retrieval and scoring. diff --git a/pkg/memory/migrate_dailynotes.go b/pkg/memory/migrate_dailynotes.go index 782605485..37d5412de 100644 --- a/pkg/memory/migrate_dailynotes.go +++ b/pkg/memory/migrate_dailynotes.go @@ -8,7 +8,7 @@ import ( "strings" "time" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) // MigrateDailyNotes performs a one-time migration of daily note files diff --git a/pkg/memory/migrate_documents.go b/pkg/memory/migrate_documents.go index 0d74fe304..a54ecd2e1 100644 --- a/pkg/memory/migrate_documents.go +++ b/pkg/memory/migrate_documents.go @@ -7,7 +7,7 @@ import ( "path/filepath" "time" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) var bootstrapDocNames = []string{ diff --git a/pkg/memory/migrate_sessions.go b/pkg/memory/migrate_sessions.go index 3664863eb..b844bdddf 100644 --- a/pkg/memory/migrate_sessions.go +++ b/pkg/memory/migrate_sessions.go @@ -12,8 +12,8 @@ import ( "strings" "time" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) const migrationMarkerFile = ".sessions_migrated" diff --git a/pkg/memory/migrate_sessions_test.go b/pkg/memory/migrate_sessions_test.go index 93e7fd86a..5d5cf62be 100644 --- a/pkg/memory/migrate_sessions_test.go +++ b/pkg/memory/migrate_sessions_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) // mockDelegate captures all calls for testing migration without a real DB. @@ -134,7 +134,7 @@ func TestMigrateFileSessions_Basic(t *testing.T) { }, }) - result, err := MigrateFileSessions(context.Background(), del, "picoclaw", sessDir) + result, err := MigrateFileSessions(context.Background(), del, "dragonscale", sessDir) if err != nil { t.Fatalf("MigrateFileSessions: %v", err) } @@ -170,7 +170,7 @@ func TestMigrateFileSessions_Basic(t *testing.T) { } // Check summary was stored as working context - wc, err := del.GetWorkingContext(context.Background(), "picoclaw", "session-2") + wc, err := del.GetWorkingContext(context.Background(), "dragonscale", "session-2") if err != nil { t.Fatalf("GetWorkingContext: %v", err) } @@ -191,7 +191,7 @@ func TestMigrateFileSessions_Idempotent(t *testing.T) { }) // First run - result1, err := MigrateFileSessions(context.Background(), del, "picoclaw", sessDir) + result1, err := MigrateFileSessions(context.Background(), del, "dragonscale", sessDir) if err != nil { t.Fatalf("first migration: %v", err) } @@ -200,7 +200,7 @@ func TestMigrateFileSessions_Idempotent(t *testing.T) { } // Second run should be a no-op (marker file exists) - result2, err := MigrateFileSessions(context.Background(), del, "picoclaw", sessDir) + result2, err := MigrateFileSessions(context.Background(), del, "dragonscale", sessDir) if err != nil { t.Fatalf("second migration: %v", err) } @@ -218,7 +218,7 @@ func TestMigrateFileSessions_EmptyDir(t *testing.T) { sessDir := t.TempDir() del := newMockDelegate() - result, err := MigrateFileSessions(context.Background(), del, "picoclaw", sessDir) + result, err := MigrateFileSessions(context.Background(), del, "dragonscale", sessDir) if err != nil { t.Fatalf("MigrateFileSessions: %v", err) } @@ -230,7 +230,7 @@ func TestMigrateFileSessions_EmptyDir(t *testing.T) { func TestMigrateFileSessions_NonexistentDir(t *testing.T) { del := newMockDelegate() - result, err := MigrateFileSessions(context.Background(), del, "picoclaw", "/nonexistent/path") + result, err := MigrateFileSessions(context.Background(), del, "dragonscale", "/nonexistent/path") if err != nil { t.Fatalf("MigrateFileSessions: %v", err) } @@ -252,7 +252,7 @@ func TestMigrateFileSessions_SkipsEmptyMessages(t *testing.T) { }, }) - result, err := MigrateFileSessions(context.Background(), del, "picoclaw", sessDir) + result, err := MigrateFileSessions(context.Background(), del, "dragonscale", sessDir) if err != nil { t.Fatalf("MigrateFileSessions: %v", err) } @@ -273,7 +273,7 @@ func TestMigrateFileSessions_FallbackKey(t *testing.T) { }, }) - result, err := MigrateFileSessions(context.Background(), del, "picoclaw", sessDir) + result, err := MigrateFileSessions(context.Background(), del, "dragonscale", sessDir) if err != nil { t.Fatalf("MigrateFileSessions: %v", err) } @@ -300,7 +300,7 @@ func TestMigrateFileSessions_MalformedJSON(t *testing.T) { }, }) - result, err := MigrateFileSessions(context.Background(), del, "picoclaw", sessDir) + result, err := MigrateFileSessions(context.Background(), del, "dragonscale", sessDir) if err != nil { t.Fatalf("MigrateFileSessions: %v", err) } diff --git a/pkg/memory/observation/manager.go b/pkg/memory/observation/manager.go index bc47c7e52..9239a57e7 100644 --- a/pkg/memory/observation/manager.go +++ b/pkg/memory/observation/manager.go @@ -5,7 +5,7 @@ import ( "log/slog" "sync" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // Manager orchestrates the observation lifecycle: threshold detection, diff --git a/pkg/memory/observation/store.go b/pkg/memory/observation/store.go index 48da27c87..2f5f7440d 100644 --- a/pkg/memory/observation/store.go +++ b/pkg/memory/observation/store.go @@ -4,7 +4,7 @@ import ( "context" "fmt" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) const kvPrefix = "obs:" diff --git a/pkg/memory/sqlc/agent_audit_log.sql.go b/pkg/memory/sqlc/agent_audit_log.sql.go index bd9e3399e..cecdb46e1 100644 --- a/pkg/memory/sqlc/agent_audit_log.sql.go +++ b/pkg/memory/sqlc/agent_audit_log.sql.go @@ -9,7 +9,7 @@ import ( "context" "time" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const CountAuditEntries = `-- name: CountAuditEntries :one diff --git a/pkg/memory/sqlc/agent_conversation_forks.sql.go b/pkg/memory/sqlc/agent_conversation_forks.sql.go index 7032af104..4778297f3 100644 --- a/pkg/memory/sqlc/agent_conversation_forks.sql.go +++ b/pkg/memory/sqlc/agent_conversation_forks.sql.go @@ -9,7 +9,7 @@ import ( "context" "encoding/json" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const CreateAgentConversationFork = `-- name: CreateAgentConversationFork :one diff --git a/pkg/memory/sqlc/agent_conversation_links.sql.go b/pkg/memory/sqlc/agent_conversation_links.sql.go index 1344882bf..dbf0a8611 100644 --- a/pkg/memory/sqlc/agent_conversation_links.sql.go +++ b/pkg/memory/sqlc/agent_conversation_links.sql.go @@ -9,7 +9,7 @@ import ( "context" "encoding/json" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const CreateAgentConversationLink = `-- name: CreateAgentConversationLink :one diff --git a/pkg/memory/sqlc/agent_conversations.sql.go b/pkg/memory/sqlc/agent_conversations.sql.go index 946d18ad2..889e9acb9 100644 --- a/pkg/memory/sqlc/agent_conversations.sql.go +++ b/pkg/memory/sqlc/agent_conversations.sql.go @@ -8,7 +8,7 @@ package sqlc import ( "context" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const CreateAgentConversation = `-- name: CreateAgentConversation :one diff --git a/pkg/memory/sqlc/agent_documents.sql.go b/pkg/memory/sqlc/agent_documents.sql.go index b97df2ac5..eb477d1a9 100644 --- a/pkg/memory/sqlc/agent_documents.sql.go +++ b/pkg/memory/sqlc/agent_documents.sql.go @@ -8,7 +8,7 @@ package sqlc import ( "context" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const DeleteDocument = `-- name: DeleteDocument :exec diff --git a/pkg/memory/sqlc/agent_mentions.sql.go b/pkg/memory/sqlc/agent_mentions.sql.go index a5273a803..71462c30c 100644 --- a/pkg/memory/sqlc/agent_mentions.sql.go +++ b/pkg/memory/sqlc/agent_mentions.sql.go @@ -9,7 +9,7 @@ import ( "context" "encoding/json" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const AddAgentMention = `-- name: AddAgentMention :one diff --git a/pkg/memory/sqlc/agent_message_revisions.sql.go b/pkg/memory/sqlc/agent_message_revisions.sql.go index d8e6f55dc..903f1ad3b 100644 --- a/pkg/memory/sqlc/agent_message_revisions.sql.go +++ b/pkg/memory/sqlc/agent_message_revisions.sql.go @@ -9,7 +9,7 @@ import ( "context" "encoding/json" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const AddAgentMessageRevision = `-- name: AddAgentMessageRevision :one diff --git a/pkg/memory/sqlc/agent_messages.sql.go b/pkg/memory/sqlc/agent_messages.sql.go index 5127c14ff..8026482ab 100644 --- a/pkg/memory/sqlc/agent_messages.sql.go +++ b/pkg/memory/sqlc/agent_messages.sql.go @@ -9,7 +9,7 @@ import ( "context" "encoding/json" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const AddAgentMessage = `-- name: AddAgentMessage :one diff --git a/pkg/memory/sqlc/agent_state.sql.go b/pkg/memory/sqlc/agent_state.sql.go index fae586253..663dec394 100644 --- a/pkg/memory/sqlc/agent_state.sql.go +++ b/pkg/memory/sqlc/agent_state.sql.go @@ -10,7 +10,7 @@ import ( "encoding/json" "time" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const AddAgentRunState = `-- name: AddAgentRunState :one diff --git a/pkg/memory/sqlc/agent_threads.sql.go b/pkg/memory/sqlc/agent_threads.sql.go index 854dd0eee..29ed11e29 100644 --- a/pkg/memory/sqlc/agent_threads.sql.go +++ b/pkg/memory/sqlc/agent_threads.sql.go @@ -9,7 +9,7 @@ import ( "context" "encoding/json" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const AddAgentThreadMessage = `-- name: AddAgentThreadMessage :one diff --git a/pkg/memory/sqlc/agent_tool_results.sql.go b/pkg/memory/sqlc/agent_tool_results.sql.go index 9f267408d..9fe7ecd55 100644 --- a/pkg/memory/sqlc/agent_tool_results.sql.go +++ b/pkg/memory/sqlc/agent_tool_results.sql.go @@ -9,7 +9,7 @@ import ( "context" "encoding/json" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const AddAgentToolResult = `-- name: AddAgentToolResult :one diff --git a/pkg/memory/sqlc/archival.sql.go b/pkg/memory/sqlc/archival.sql.go index 0d1e80007..6dccfc53d 100644 --- a/pkg/memory/sqlc/archival.sql.go +++ b/pkg/memory/sqlc/archival.sql.go @@ -9,8 +9,8 @@ import ( "context" "strings" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) const CountArchivalChunks = `-- name: CountArchivalChunks :one diff --git a/pkg/memory/sqlc/jobs.sql.go b/pkg/memory/sqlc/jobs.sql.go index 54420fcaa..17a33fa80 100644 --- a/pkg/memory/sqlc/jobs.sql.go +++ b/pkg/memory/sqlc/jobs.sql.go @@ -9,7 +9,7 @@ import ( "context" "time" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const ClaimJobByID = `-- name: ClaimJobByID :one diff --git a/pkg/memory/sqlc/models.go b/pkg/memory/sqlc/models.go index bbee18e52..2cae18c4f 100644 --- a/pkg/memory/sqlc/models.go +++ b/pkg/memory/sqlc/models.go @@ -8,8 +8,8 @@ import ( "encoding/json" "time" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) type AgentAuditLog struct { @@ -191,6 +191,47 @@ type ArchivalChunk struct { CreatedAt time.Time `db:"created_at" json:"created_at"` } +type DagEdge struct { + ID ids.UUID `db:"id" json:"id"` + SnapshotID ids.UUID `db:"snapshot_id" json:"snapshot_id"` + ParentNodeID ids.UUID `db:"parent_node_id" json:"parent_node_id"` + ChildNodeID ids.UUID `db:"child_node_id" json:"child_node_id"` + EdgeIndex int64 `db:"edge_index" json:"edge_index"` + MetadataJson json.RawMessage `db:"metadata_json" json:"metadata_json"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type DagNode struct { + ID ids.UUID `db:"id" json:"id"` + SnapshotID ids.UUID `db:"snapshot_id" json:"snapshot_id"` + NodeID string `db:"node_id" json:"node_id"` + Level int64 `db:"level" json:"level"` + Summary string `db:"summary" json:"summary"` + Tokens int64 `db:"tokens" json:"tokens"` + StartIdx int64 `db:"start_idx" json:"start_idx"` + EndIdx int64 `db:"end_idx" json:"end_idx"` + Span int64 `db:"span" json:"span"` + ContentHash string `db:"content_hash" json:"content_hash"` + MetricsJson json.RawMessage `db:"metrics_json" json:"metrics_json"` + MetadataJson json.RawMessage `db:"metadata_json" json:"metadata_json"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + +type DagSnapshot struct { + ID ids.UUID `db:"id" json:"id"` + AgentID string `db:"agent_id" json:"agent_id"` + SessionKey string `db:"session_key" json:"session_key"` + FromMsgIdx int64 `db:"from_msg_idx" json:"from_msg_idx"` + ToMsgIdx int64 `db:"to_msg_idx" json:"to_msg_idx"` + MsgCount int64 `db:"msg_count" json:"msg_count"` + RootsJson string `db:"roots_json" json:"roots_json"` + ContentHash string `db:"content_hash" json:"content_hash"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` +} + type Job struct { ID ids.UUID `db:"id" json:"id"` Kind string `db:"kind" json:"kind"` @@ -208,6 +249,41 @@ type Job struct { CompletedAt *time.Time `db:"completed_at" json:"completed_at"` } +type MapItem struct { + ID ids.UUID `db:"id" json:"id"` + RunID ids.UUID `db:"run_id" json:"run_id"` + ItemIndex int64 `db:"item_index" json:"item_index"` + Status string `db:"status" json:"status"` + Attempts int64 `db:"attempts" json:"attempts"` + LastError *string `db:"last_error" json:"last_error"` + InputFb []byte `db:"input_fb" json:"input_fb"` + OutputFb []byte `db:"output_fb" json:"output_fb"` + InputHash *string `db:"input_hash" json:"input_hash"` + OutputHash *string `db:"output_hash" json:"output_hash"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + CompletedAt *time.Time `db:"completed_at" json:"completed_at"` +} + +type MapRun struct { + ID ids.UUID `db:"id" json:"id"` + AgentID string `db:"agent_id" json:"agent_id"` + SessionKey string `db:"session_key" json:"session_key"` + OperatorKind string `db:"operator_kind" json:"operator_kind"` + IdempotencyKey *string `db:"idempotency_key" json:"idempotency_key"` + Status string `db:"status" json:"status"` + TotalItems int64 `db:"total_items" json:"total_items"` + QueuedItems int64 `db:"queued_items" json:"queued_items"` + RunningItems int64 `db:"running_items" json:"running_items"` + SucceededItems int64 `db:"succeeded_items" json:"succeeded_items"` + FailedItems int64 `db:"failed_items" json:"failed_items"` + SpecFb []byte `db:"spec_fb" json:"spec_fb"` + LastError *string `db:"last_error" json:"last_error"` + CreatedAt time.Time `db:"created_at" json:"created_at"` + UpdatedAt time.Time `db:"updated_at" json:"updated_at"` + CompletedAt *time.Time `db:"completed_at" json:"completed_at"` +} + type MemorySummary struct { ID ids.UUID `db:"id" json:"id"` AgentID string `db:"agent_id" json:"agent_id"` diff --git a/pkg/memory/sqlc/querier.go b/pkg/memory/sqlc/querier.go index df577cf4e..9e886c4e2 100644 --- a/pkg/memory/sqlc/querier.go +++ b/pkg/memory/sqlc/querier.go @@ -7,7 +7,7 @@ package sqlc import ( "context" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) type Querier interface { @@ -135,6 +135,20 @@ type Querier interface { // FROM jobs // GROUP BY status CountJobsByStatus(ctx context.Context) ([]CountJobsByStatusRow, error) + //CountMapItemsByRun + // + // SELECT count(*) AS count + // FROM map_items + // WHERE run_id = ?1 + CountMapItemsByRun(ctx context.Context, arg CountMapItemsByRunParams) (int64, error) + //CountMapItemsByRunAndStatus + // + // SELECT status, + // count(*) AS count + // FROM map_items + // WHERE run_id = ?1 + // GROUP BY status + CountMapItemsByRunAndStatus(ctx context.Context, arg CountMapItemsByRunAndStatusParams) ([]CountMapItemsByRunAndStatusRow, error) //CountRecallItems // // SELECT COUNT(*) @@ -356,6 +370,43 @@ type Querier interface { // WHERE ac.id IN (/*SLICE:ids*/?) // AND ri.agent_id = ?2 GetArchivalChunksByIDs(ctx context.Context, arg GetArchivalChunksByIDsParams) ([]ArchivalChunk, error) + //GetDAGNodeBySnapshotAndNodeID + // + // SELECT id, + // snapshot_id, + // node_id, + // level, + // summary, + // tokens, + // start_idx, + // end_idx, + // span, + // content_hash, + // metrics_json, + // metadata_json, + // created_at, + // updated_at + // FROM dag_nodes + // WHERE snapshot_id = ?1 + // AND node_id = ?2 + // LIMIT 1 + GetDAGNodeBySnapshotAndNodeID(ctx context.Context, arg GetDAGNodeBySnapshotAndNodeIDParams) (DagNode, error) + //GetDAGSnapshotByID + // + // SELECT id, + // agent_id, + // session_key, + // from_msg_idx, + // to_msg_idx, + // msg_count, + // roots_json, + // content_hash, + // created_at, + // updated_at + // FROM dag_snapshots + // WHERE id = ?1 + // LIMIT 1 + GetDAGSnapshotByID(ctx context.Context, arg GetDAGSnapshotByIDParams) (DagSnapshot, error) // Agent Documents queries // // SELECT id, @@ -406,6 +457,56 @@ type Querier interface { // ORDER BY step_index DESC // LIMIT 1 GetLatestAgentRunStateByRunID(ctx context.Context, arg GetLatestAgentRunStateByRunIDParams) (AgentRunState, error) + //GetLatestDAGSnapshotBySession + // + // SELECT id, + // agent_id, + // session_key, + // from_msg_idx, + // to_msg_idx, + // msg_count, + // roots_json, + // content_hash, + // created_at, + // updated_at + // FROM dag_snapshots + // WHERE agent_id = ?1 + // AND session_key = ?2 + // ORDER BY created_at DESC + // LIMIT 1 + GetLatestDAGSnapshotBySession(ctx context.Context, arg GetLatestDAGSnapshotBySessionParams) (DagSnapshot, error) + //GetMapItemByID + // + // SELECT id, run_id, item_index, status, attempts, last_error, input_fb, output_fb, input_hash, output_hash, created_at, updated_at, completed_at + // FROM map_items + // WHERE id = ?1 + // LIMIT 1 + GetMapItemByID(ctx context.Context, arg GetMapItemByIDParams) (MapItem, error) + //GetMapItemByRunAndIndex + // + // SELECT id, run_id, item_index, status, attempts, last_error, input_fb, output_fb, input_hash, output_hash, created_at, updated_at, completed_at + // FROM map_items + // WHERE run_id = ?1 + // AND item_index = ?2 + // LIMIT 1 + GetMapItemByRunAndIndex(ctx context.Context, arg GetMapItemByRunAndIndexParams) (MapItem, error) + //GetMapRunByID + // + // SELECT id, agent_id, session_key, operator_kind, idempotency_key, status, total_items, queued_items, running_items, succeeded_items, failed_items, spec_fb, last_error, created_at, updated_at, completed_at + // FROM map_runs + // WHERE id = ?1 + // LIMIT 1 + GetMapRunByID(ctx context.Context, arg GetMapRunByIDParams) (MapRun, error) + //GetMapRunByIdempotencyKey + // + // SELECT id, agent_id, session_key, operator_kind, idempotency_key, status, total_items, queued_items, running_items, succeeded_items, failed_items, spec_fb, last_error, created_at, updated_at, completed_at + // FROM map_runs + // WHERE agent_id = ?1 + // AND session_key = ?2 + // AND operator_kind = ?3 + // AND idempotency_key = ?4 + // LIMIT 1 + GetMapRunByIdempotencyKey(ctx context.Context, arg GetMapRunByIdempotencyKeyParams) (MapRun, error) //GetRecallItem // // SELECT id, @@ -504,6 +605,165 @@ type Querier interface { // ) // RETURNING id, agent_id, session_key, action, target, input, output, duration_ms, created_at InsertAuditEntry(ctx context.Context, arg InsertAuditEntryParams) (AgentAuditLog, error) + //InsertDAGEdge + // + // INSERT INTO dag_edges ( + // id, + // snapshot_id, + // parent_node_id, + // child_node_id, + // edge_index, + // metadata_json + // ) + // VALUES ( + // ?1, + // ?2, + // ?3, + // ?4, + // ?5, + // ?6 + // ) + InsertDAGEdge(ctx context.Context, arg InsertDAGEdgeParams) error + //InsertDAGNode + // + // INSERT INTO dag_nodes ( + // id, + // snapshot_id, + // node_id, + // level, + // summary, + // tokens, + // start_idx, + // end_idx, + // span, + // content_hash, + // metrics_json, + // metadata_json + // ) + // VALUES ( + // ?1, + // ?2, + // ?3, + // ?4, + // ?5, + // ?6, + // ?7, + // ?8, + // ?9, + // ?10, + // ?11, + // ?12 + // ) + // RETURNING id, + // snapshot_id, + // node_id, + // level, + // summary, + // tokens, + // start_idx, + // end_idx, + // span, + // content_hash, + // metrics_json, + // metadata_json, + // created_at, + // updated_at + InsertDAGNode(ctx context.Context, arg InsertDAGNodeParams) (DagNode, error) + // DAG persistence queries + // + // INSERT INTO dag_snapshots ( + // id, + // agent_id, + // session_key, + // from_msg_idx, + // to_msg_idx, + // msg_count, + // roots_json, + // content_hash + // ) + // VALUES ( + // ?1, + // ?2, + // ?3, + // ?4, + // ?5, + // ?6, + // ?7, + // ?8 + // ) + // RETURNING id, + // agent_id, + // session_key, + // from_msg_idx, + // to_msg_idx, + // msg_count, + // roots_json, + // content_hash, + // created_at, + // updated_at + InsertDAGSnapshot(ctx context.Context, arg InsertDAGSnapshotParams) (DagSnapshot, error) + //InsertMapItem + // + // INSERT INTO map_items ( + // id, + // run_id, + // item_index, + // status, + // attempts, + // last_error, + // input_fb, + // output_fb, + // input_hash, + // output_hash + // ) + // VALUES ( + // ?1, + // ?2, + // ?3, + // ?4, + // ?5, + // ?6, + // ?7, + // ?8, + // ?9, + // ?10 + // ) + // RETURNING id, run_id, item_index, status, attempts, last_error, input_fb, output_fb, input_hash, output_hash, created_at, updated_at, completed_at + InsertMapItem(ctx context.Context, arg InsertMapItemParams) (MapItem, error) + //InsertMapRun + // + // INSERT INTO map_runs ( + // id, + // agent_id, + // session_key, + // operator_kind, + // idempotency_key, + // status, + // total_items, + // queued_items, + // running_items, + // succeeded_items, + // failed_items, + // spec_fb, + // last_error + // ) + // VALUES ( + // ?1, + // ?2, + // ?3, + // ?4, + // ?5, + // ?6, + // ?7, + // ?8, + // ?9, + // ?10, + // ?11, + // ?12, + // ?13 + // ) + // RETURNING id, agent_id, session_key, operator_kind, idempotency_key, status, total_items, queued_items, running_items, succeeded_items, failed_items, spec_fb, last_error, created_at, updated_at, completed_at + InsertMapRun(ctx context.Context, arg InsertMapRunParams) (MapRun, error) // Recall Item queries // // INSERT INTO recall_items ( @@ -534,7 +794,18 @@ type Querier interface { // datetime('now'), // datetime('now') // ) - // RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at + // RETURNING id, + // agent_id, + // session_key, + // role, + // sector, + // importance, + // salience, + // decay_rate, + // content, + // tags, + // created_at, + // updated_at InsertRecallItem(ctx context.Context, arg InsertRecallItemParams) (RecallItem, error) //InsertSessionMessage // @@ -566,7 +837,18 @@ type Querier interface { // datetime('now'), // datetime('now') // ) - // RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at + // RETURNING id, + // agent_id, + // session_key, + // role, + // sector, + // importance, + // salience, + // decay_rate, + // content, + // tags, + // created_at, + // updated_at InsertSessionMessage(ctx context.Context, arg InsertSessionMessageParams) (RecallItem, error) // Memory Summary queries // @@ -816,6 +1098,41 @@ type Querier interface { // ORDER BY created_at DESC // LIMIT ?3 ListAuditEntriesBySession(ctx context.Context, arg ListAuditEntriesBySessionParams) ([]AgentAuditLog, error) + //ListDAGEdgesBySnapshotID + // + // SELECT id, + // snapshot_id, + // parent_node_id, + // child_node_id, + // edge_index, + // metadata_json, + // created_at, + // updated_at + // FROM dag_edges + // WHERE snapshot_id = ?1 + // ORDER BY edge_index ASC, + // created_at ASC + ListDAGEdgesBySnapshotID(ctx context.Context, arg ListDAGEdgesBySnapshotIDParams) ([]DagEdge, error) + //ListDAGNodesBySnapshotID + // + // SELECT id, + // snapshot_id, + // node_id, + // level, + // summary, + // tokens, + // start_idx, + // end_idx, + // span, + // content_hash, + // metrics_json, + // metadata_json, + // created_at, + // updated_at + // FROM dag_nodes + // WHERE snapshot_id = ?1 + // ORDER BY start_idx ASC + ListDAGNodesBySnapshotID(ctx context.Context, arg ListDAGNodesBySnapshotIDParams) ([]DagNode, error) //ListDocumentsByCategory // // SELECT id, @@ -853,6 +1170,23 @@ type Querier interface { // ORDER BY key // LIMIT ?3 ListKVByPrefix(ctx context.Context, arg ListKVByPrefixParams) ([]AgentKv, error) + //ListMapItemsByRunPaged + // + // SELECT id, run_id, item_index, status, attempts, last_error, input_fb, output_fb, input_hash, output_hash, created_at, updated_at, completed_at + // FROM map_items + // WHERE run_id = ?1 + // ORDER BY item_index ASC + // LIMIT ?3 OFFSET ?2 + ListMapItemsByRunPaged(ctx context.Context, arg ListMapItemsByRunPagedParams) ([]MapItem, error) + //ListMapRunsBySession + // + // SELECT id, agent_id, session_key, operator_kind, idempotency_key, status, total_items, queued_items, running_items, succeeded_items, failed_items, spec_fb, last_error, created_at, updated_at, completed_at + // FROM map_runs + // WHERE agent_id = ?1 + // AND session_key = ?2 + // ORDER BY created_at DESC + // LIMIT ?4 OFFSET ?3 + ListMapRunsBySession(ctx context.Context, arg ListMapRunsBySessionParams) ([]MapRun, error) //ListRecallItems // // SELECT id, @@ -901,6 +1235,31 @@ type Querier interface { // ORDER BY created_at ASC // LIMIT ?4 ListSessionMessages(ctx context.Context, arg ListSessionMessagesParams) ([]RecallItem, error) + //ListSessionMessagesPaged + // + // SELECT id, + // agent_id, + // session_key, + // role, + // sector, + // importance, + // salience, + // decay_rate, + // content, + // tags, + // created_at, + // updated_at + // FROM recall_items + // WHERE agent_id = ?1 + // AND session_key = ?2 + // AND tags = 'session-message' + // AND ( + // role = ?3 + // OR ?3 = '' + // ) + // ORDER BY created_at ASC + // LIMIT ?5 OFFSET ?4 + ListSessionMessagesPaged(ctx context.Context, arg ListSessionMessagesPagedParams) ([]RecallItem, error) //ListSummaries // // SELECT id, @@ -939,6 +1298,38 @@ type Querier interface { // WHERE id = ?1 // RETURNING id, kind, status, run_at, attempts, max_attempts, locked_at, locked_by, payload_json, dedupe_key, last_error, created_at, updated_at, completed_at MarkJobSucceeded(ctx context.Context, arg MarkJobSucceededParams) (Job, error) + //MarkMapItemFailed + // + // UPDATE map_items + // SET status = 'failed', + // last_error = ?1, + // updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + // completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') + // WHERE id = ?2 + // RETURNING id, run_id, item_index, status, attempts, last_error, input_fb, output_fb, input_hash, output_hash, created_at, updated_at, completed_at + MarkMapItemFailed(ctx context.Context, arg MarkMapItemFailedParams) (MapItem, error) + //MarkMapItemRunning + // + // UPDATE map_items + // SET status = 'running', + // attempts = attempts + 1, + // last_error = NULL, + // updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') + // WHERE id = ?1 + // RETURNING id, run_id, item_index, status, attempts, last_error, input_fb, output_fb, input_hash, output_hash, created_at, updated_at, completed_at + MarkMapItemRunning(ctx context.Context, arg MarkMapItemRunningParams) (MapItem, error) + //MarkMapItemSucceeded + // + // UPDATE map_items + // SET status = 'succeeded', + // output_fb = ?1, + // output_hash = ?2, + // last_error = NULL, + // updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + // completed_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now') + // WHERE id = ?3 + // RETURNING id, run_id, item_index, status, attempts, last_error, input_fb, output_fb, input_hash, output_hash, created_at, updated_at, completed_at + MarkMapItemSucceeded(ctx context.Context, arg MarkMapItemSucceededParams) (MapItem, error) //PruneOldAuditEntries // // DELETE FROM agent_audit_log @@ -1005,6 +1396,20 @@ type Querier interface { // WHERE id = ? // RETURNING id, conversation_id, status, metadata_json, created_at, updated_at UpdateAgentRunStatus(ctx context.Context, arg UpdateAgentRunStatusParams) (AgentRun, error) + //UpdateMapRunProgress + // + // UPDATE map_runs + // SET status = ?1, + // queued_items = ?2, + // running_items = ?3, + // succeeded_items = ?4, + // failed_items = ?5, + // last_error = ?6, + // updated_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now'), + // completed_at = ?7 + // WHERE id = ?8 + // RETURNING id, agent_id, session_key, operator_kind, idempotency_key, status, total_items, queued_items, running_items, succeeded_items, failed_items, spec_fb, last_error, created_at, updated_at, completed_at + UpdateMapRunProgress(ctx context.Context, arg UpdateMapRunProgressParams) (MapRun, error) //UpdateRecallItem // // UPDATE recall_items diff --git a/pkg/memory/sqlc/queries/recall.sql b/pkg/memory/sqlc/queries/recall.sql index 85858640e..8ef82039e 100644 --- a/pkg/memory/sqlc/queries/recall.sql +++ b/pkg/memory/sqlc/queries/recall.sql @@ -28,7 +28,18 @@ VALUES ( datetime('now'), datetime('now') ) -RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at; +RETURNING id, + agent_id, + session_key, + role, + sector, + importance, + salience, + decay_rate, + content, + tags, + created_at, + updated_at; -- name: GetRecallItem :one SELECT id, agent_id, @@ -154,7 +165,18 @@ VALUES ( datetime('now'), datetime('now') ) -RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at; +RETURNING id, + agent_id, + session_key, + role, + sector, + importance, + salience, + decay_rate, + content, + tags, + created_at, + updated_at; -- name: ListSessionMessages :many SELECT id, agent_id, @@ -178,6 +200,29 @@ WHERE agent_id = sqlc.arg(agent_id) ) ORDER BY created_at ASC LIMIT sqlc.arg(lim); +-- name: ListSessionMessagesPaged :many +SELECT id, + agent_id, + session_key, + role, + sector, + importance, + salience, + decay_rate, + content, + tags, + created_at, + updated_at +FROM recall_items +WHERE agent_id = sqlc.arg(agent_id) + AND session_key = sqlc.arg(session_key) + AND tags = 'session-message' + AND ( + role = sqlc.arg(role) + OR sqlc.arg(role) = '' + ) +ORDER BY created_at ASC +LIMIT sqlc.arg(lim) OFFSET sqlc.arg(off); -- name: CountSessionMessages :one SELECT COUNT(*) FROM recall_items diff --git a/pkg/memory/sqlc/recall.sql.go b/pkg/memory/sqlc/recall.sql.go index 9ee04ba39..946f9968b 100644 --- a/pkg/memory/sqlc/recall.sql.go +++ b/pkg/memory/sqlc/recall.sql.go @@ -9,8 +9,8 @@ import ( "context" "strings" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) const CountRecallItems = `-- name: CountRecallItems :one @@ -270,7 +270,18 @@ VALUES ( datetime('now'), datetime('now') ) -RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at +RETURNING id, + agent_id, + session_key, + role, + sector, + importance, + salience, + decay_rate, + content, + tags, + created_at, + updated_at ` type InsertRecallItemParams struct { @@ -316,7 +327,18 @@ type InsertRecallItemParams struct { // datetime('now'), // datetime('now') // ) -// RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at +// RETURNING id, +// agent_id, +// session_key, +// role, +// sector, +// importance, +// salience, +// decay_rate, +// content, +// tags, +// created_at, +// updated_at func (q *Queries) InsertRecallItem(ctx context.Context, arg InsertRecallItemParams) (RecallItem, error) { row := q.db.QueryRowContext(ctx, InsertRecallItem, arg.ID, @@ -377,7 +399,18 @@ VALUES ( datetime('now'), datetime('now') ) -RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at +RETURNING id, + agent_id, + session_key, + role, + sector, + importance, + salience, + decay_rate, + content, + tags, + created_at, + updated_at ` type InsertSessionMessageParams struct { @@ -418,7 +451,18 @@ type InsertSessionMessageParams struct { // datetime('now'), // datetime('now') // ) -// RETURNING id, agent_id, session_key, role, sector, importance, salience, decay_rate, content, tags, created_at, updated_at +// RETURNING id, +// agent_id, +// session_key, +// role, +// sector, +// importance, +// salience, +// decay_rate, +// content, +// tags, +// created_at, +// updated_at func (q *Queries) InsertSessionMessage(ctx context.Context, arg InsertSessionMessageParams) (RecallItem, error) { row := q.db.QueryRowContext(ctx, InsertSessionMessage, arg.ID, @@ -635,6 +679,105 @@ func (q *Queries) ListSessionMessages(ctx context.Context, arg ListSessionMessag return items, nil } +const ListSessionMessagesPaged = `-- name: ListSessionMessagesPaged :many +SELECT id, + agent_id, + session_key, + role, + sector, + importance, + salience, + decay_rate, + content, + tags, + created_at, + updated_at +FROM recall_items +WHERE agent_id = ?1 + AND session_key = ?2 + AND tags = 'session-message' + AND ( + role = ?3 + OR ?3 = '' + ) +ORDER BY created_at ASC +LIMIT ?5 OFFSET ?4 +` + +type ListSessionMessagesPagedParams struct { + AgentID string `db:"agent_id" json:"agent_id"` + SessionKey string `db:"session_key" json:"session_key"` + Role string `db:"role" json:"role"` + Off int64 `db:"off" json:"off"` + Lim int64 `db:"lim" json:"lim"` +} + +// ListSessionMessagesPaged +// +// SELECT id, +// agent_id, +// session_key, +// role, +// sector, +// importance, +// salience, +// decay_rate, +// content, +// tags, +// created_at, +// updated_at +// FROM recall_items +// WHERE agent_id = ?1 +// AND session_key = ?2 +// AND tags = 'session-message' +// AND ( +// role = ?3 +// OR ?3 = '' +// ) +// ORDER BY created_at ASC +// LIMIT ?5 OFFSET ?4 +func (q *Queries) ListSessionMessagesPaged(ctx context.Context, arg ListSessionMessagesPagedParams) ([]RecallItem, error) { + rows, err := q.db.QueryContext(ctx, ListSessionMessagesPaged, + arg.AgentID, + arg.SessionKey, + arg.Role, + arg.Off, + arg.Lim, + ) + if err != nil { + return nil, err + } + defer rows.Close() + items := []RecallItem{} + for rows.Next() { + var i RecallItem + if err := rows.Scan( + &i.ID, + &i.AgentID, + &i.SessionKey, + &i.Role, + &i.Sector, + &i.Importance, + &i.Salience, + &i.DecayRate, + &i.Content, + &i.Tags, + &i.CreatedAt, + &i.UpdatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const SearchRecallByKeyword = `-- name: SearchRecallByKeyword :many SELECT ri.id, ri.agent_id, diff --git a/pkg/memory/sqlc/schema.sql b/pkg/memory/sqlc/schema.sql index fb3a6a583..ee259f9dc 100644 --- a/pkg/memory/sqlc/schema.sql +++ b/pkg/memory/sqlc/schema.sql @@ -1,4 +1,4 @@ --- PicoClaw Memory System Schema (SQLite / libSQL) +-- DragonScale Memory System Schema (SQLite / libSQL) -- 3-tier MemGPT-style: working_context (hot), recall_items (warm), archival_chunks (cold) -- NOTE: This schema is parsed by sqlc. The actual runtime DDL (with F32_BLOB, etc.) -- is in delegate/schemaDDL(). Keep column names and types in sync. @@ -208,6 +208,57 @@ CREATE INDEX IF NOT EXISTS idx_jobs_status_run_at ON jobs(status, run_at); CREATE UNIQUE INDEX IF NOT EXISTS idx_jobs_kind_dedupe ON jobs(kind, dedupe_key) WHERE dedupe_key IS NOT NULL; -- ============================================================================ +-- Map Operator Runs / Items (FlatBuffers payload persistence) +-- ============================================================================ +CREATE TABLE IF NOT EXISTS map_runs ( + id BLOB PRIMARY KEY, + agent_id TEXT NOT NULL, + session_key TEXT NOT NULL, + operator_kind TEXT NOT NULL, + -- llm_map | agentic_map + idempotency_key TEXT, + status TEXT NOT NULL DEFAULT 'queued', + -- queued | running | succeeded | failed | cancelled + total_items INTEGER NOT NULL DEFAULT 0, + queued_items INTEGER NOT NULL DEFAULT 0, + running_items INTEGER NOT NULL DEFAULT 0, + succeeded_items INTEGER NOT NULL DEFAULT 0, + failed_items INTEGER NOT NULL DEFAULT 0, + spec_fb BLOB NOT NULL, + last_error TEXT, + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + completed_at DATETIME +); +CREATE INDEX IF NOT EXISTS idx_map_runs_agent_session_created_at ON map_runs(agent_id, session_key, created_at DESC); +CREATE INDEX IF NOT EXISTS idx_map_runs_status_updated_at ON map_runs(status, updated_at DESC); +CREATE UNIQUE INDEX IF NOT EXISTS idx_map_runs_dedupe ON map_runs( + agent_id, + session_key, + operator_kind, + idempotency_key +) +WHERE idempotency_key IS NOT NULL; +CREATE TABLE IF NOT EXISTS map_items ( + id BLOB PRIMARY KEY, + run_id BLOB NOT NULL REFERENCES map_runs(id) ON DELETE CASCADE, + item_index INTEGER NOT NULL, + status TEXT NOT NULL DEFAULT 'queued', + -- queued | running | succeeded | failed + attempts INTEGER NOT NULL DEFAULT 0, + last_error TEXT, + input_fb BLOB NOT NULL, + output_fb BLOB, + input_hash TEXT, + output_hash TEXT, + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + completed_at DATETIME, + UNIQUE(run_id, item_index) +); +CREATE INDEX IF NOT EXISTS idx_map_items_run_id_status_item_index ON map_items(run_id, status, item_index); +CREATE INDEX IF NOT EXISTS idx_map_items_run_id_item_index ON map_items(run_id, item_index); +-- ============================================================================ -- Conversation Graph (forks, links, threads, mentions, edits) -- ============================================================================ -- Forks: parent→child conversation via checkpoint. @@ -281,4 +332,56 @@ CREATE TABLE IF NOT EXISTS agent_message_revisions ( created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) ); -CREATE INDEX IF NOT EXISTS idx_agent_message_revisions_message_id_created_at ON agent_message_revisions(message_id, created_at DESC); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_agent_message_revisions_message_id_created_at ON agent_message_revisions(message_id, created_at DESC); +-- ============================================================================ +-- DAG Compression (persistent node/edge schema for lossless expand, describe, grep) +-- ============================================================================ +-- Snapshots: one per compression run, keyed by agent/session. +CREATE TABLE IF NOT EXISTS dag_snapshots ( + id BLOB PRIMARY KEY, + agent_id TEXT NOT NULL, + session_key TEXT NOT NULL DEFAULT '', + from_msg_idx INTEGER NOT NULL DEFAULT 0, + to_msg_idx INTEGER NOT NULL DEFAULT 0, + msg_count INTEGER NOT NULL DEFAULT 0, + roots_json TEXT NOT NULL DEFAULT '[]', + content_hash TEXT NOT NULL DEFAULT '', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); +CREATE INDEX IF NOT EXISTS idx_dag_snapshots_agent_session ON dag_snapshots(agent_id, session_key); +CREATE INDEX IF NOT EXISTS idx_dag_snapshots_created_at ON dag_snapshots(created_at DESC); +-- Nodes: DAG nodes with summary, spans, metrics, hashes. +CREATE TABLE IF NOT EXISTS dag_nodes ( + id BLOB PRIMARY KEY, + snapshot_id BLOB NOT NULL REFERENCES dag_snapshots(id) ON DELETE CASCADE, + node_id TEXT NOT NULL, + level INTEGER NOT NULL DEFAULT 1, + summary TEXT NOT NULL DEFAULT '', + tokens INTEGER NOT NULL DEFAULT 0, + start_idx INTEGER NOT NULL DEFAULT 0, + end_idx INTEGER NOT NULL DEFAULT 0, + span INTEGER NOT NULL DEFAULT 0, + content_hash TEXT NOT NULL DEFAULT '', + metrics_json JSON NOT NULL DEFAULT '{}', + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); +CREATE INDEX IF NOT EXISTS idx_dag_nodes_snapshot ON dag_nodes(snapshot_id); +CREATE INDEX IF NOT EXISTS idx_dag_nodes_node_id ON dag_nodes(snapshot_id, node_id); +CREATE INDEX IF NOT EXISTS idx_dag_nodes_level_start ON dag_nodes(snapshot_id, level, start_idx); +-- Edges: parent->child relationships. +CREATE TABLE IF NOT EXISTS dag_edges ( + id BLOB PRIMARY KEY, + snapshot_id BLOB NOT NULL REFERENCES dag_snapshots(id) ON DELETE CASCADE, + parent_node_id BLOB NOT NULL REFERENCES dag_nodes(id) ON DELETE CASCADE, + child_node_id BLOB NOT NULL REFERENCES dag_nodes(id) ON DELETE CASCADE, + edge_index INTEGER NOT NULL DEFAULT 0, + metadata_json JSON NOT NULL DEFAULT '{}', + created_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + updated_at DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')) +); +CREATE INDEX IF NOT EXISTS idx_dag_edges_snapshot ON dag_edges(snapshot_id); +CREATE INDEX IF NOT EXISTS idx_dag_edges_parent ON dag_edges(parent_node_id); +CREATE INDEX IF NOT EXISTS idx_dag_edges_child ON dag_edges(child_node_id); \ No newline at end of file diff --git a/pkg/memory/sqlc/sqlc.yaml b/pkg/memory/sqlc/sqlc.yaml index 8fb52adff..c92ffdbc9 100644 --- a/pkg/memory/sqlc/sqlc.yaml +++ b/pkg/memory/sqlc/sqlc.yaml @@ -27,185 +27,227 @@ sql: # Only entity-owned PKs and their FKs — NOT agent_id/session_key (external identifiers) - column: "recall_items.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "archival_chunks.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "archival_chunks.recall_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "memory_summaries.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_documents.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_audit_log.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Agent conversations - column: "agent_conversations.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Agent messages - column: "agent_messages.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_messages.conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Agent runs - column: "agent_runs.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_runs.conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Agent run states - column: "agent_run_states.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_run_states.run_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Agent state transitions - column: "agent_state_transitions.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_state_transitions.run_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Agent checkpoints - column: "agent_checkpoints.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_checkpoints.conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_checkpoints.run_state_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Agent tool results - column: "agent_tool_results.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_tool_results.conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_tool_results.run_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Jobs - column: "jobs.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + # Map operator runs/items + - column: "map_runs.id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "map_items.id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "map_items.run_id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Conversation forks - column: "agent_conversation_forks.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_conversation_forks.parent_conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_conversation_forks.child_conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_conversation_forks.checkpoint_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Conversation links - column: "agent_conversation_links.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_conversation_links.conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_conversation_links.linked_conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Threads - column: "agent_threads.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_threads.conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Thread messages - column: "agent_thread_messages.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_thread_messages.thread_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Mentions - column: "agent_mentions.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_mentions.conversation_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_mentions.message_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_mentions.target_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Message revisions - column: "agent_message_revisions.id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" - column: "agent_message_revisions.message_id" go_type: - import: "github.com/sipeed/picoclaw/pkg/ids" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + # DAG + - column: "dag_snapshots.id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "dag_nodes.id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "dag_nodes.snapshot_id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "dag_edges.id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "dag_edges.snapshot_id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "dag_edges.parent_node_id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + type: "UUID" + - column: "dag_edges.child_node_id" + go_type: + import: "github.com/ZanzyTHEbar/dragonscale/pkg/ids" type: "UUID" # Domain type: recall_items.sector → memory.Sector - column: "recall_items.sector" go_type: - import: "github.com/sipeed/picoclaw/pkg/memory" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/memory" type: "Sector" # F32_BLOB: archival_chunks.embedding → memory.Embedding # Implements driver.Valuer/sql.Scanner for transparent blob↔float32 conversion. # Empty embeddings serialize as SQL NULL (not 0-byte blob). - column: "archival_chunks.embedding" go_type: - import: "github.com/sipeed/picoclaw/pkg/memory" + import: "github.com/ZanzyTHEbar/dragonscale/pkg/memory" type: "Embedding" # Timestamp types → time.Time (consistent with go-libsql DATETIME handling) - db_type: "DATETIME" diff --git a/pkg/memory/sqlc/summaries.sql.go b/pkg/memory/sqlc/summaries.sql.go index f8ff66c13..45be8dbc7 100644 --- a/pkg/memory/sqlc/summaries.sql.go +++ b/pkg/memory/sqlc/summaries.sql.go @@ -8,7 +8,7 @@ package sqlc import ( "context" - "github.com/sipeed/picoclaw/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" ) const InsertSummary = `-- name: InsertSummary :one diff --git a/pkg/memory/store/cached_embedder.go b/pkg/memory/store/cached_embedder.go index 0492885d3..d9790e984 100644 --- a/pkg/memory/store/cached_embedder.go +++ b/pkg/memory/store/cached_embedder.go @@ -6,8 +6,8 @@ import ( "encoding/hex" "time" - "github.com/sipeed/picoclaw/pkg/cache" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/cache" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // CachedEmbedder wraps an EmbeddingProvider with an LRU cache keyed by content hash. diff --git a/pkg/memory/store/cached_embedder_test.go b/pkg/memory/store/cached_embedder_test.go index 2d8b28be2..0ab1f7d6d 100644 --- a/pkg/memory/store/cached_embedder_test.go +++ b/pkg/memory/store/cached_embedder_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) type countingEmbedder struct { diff --git a/pkg/memory/store/chunker.go b/pkg/memory/store/chunker.go index a4366dcd7..95e260ddd 100644 --- a/pkg/memory/store/chunker.go +++ b/pkg/memory/store/chunker.go @@ -5,7 +5,7 @@ import ( "strings" "unicode/utf8" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // MarkdownChunker splits markdown text at structural boundaries (headings, diff --git a/pkg/memory/store/embed_factory.go b/pkg/memory/store/embed_factory.go index b77057fa3..d9a92b3d6 100644 --- a/pkg/memory/store/embed_factory.go +++ b/pkg/memory/store/embed_factory.go @@ -4,9 +4,9 @@ import ( "fmt" "strings" - "github.com/sipeed/picoclaw/pkg/config" - "github.com/sipeed/picoclaw/pkg/logger" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // NewEmbedderFromConfig creates an EmbeddingProvider from config, wrapped diff --git a/pkg/memory/store/embed_factory_test.go b/pkg/memory/store/embed_factory_test.go index 0f75e6559..882dfb501 100644 --- a/pkg/memory/store/embed_factory_test.go +++ b/pkg/memory/store/embed_factory_test.go @@ -6,7 +6,7 @@ import ( "net/http/httptest" "testing" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) func TestNewEmbedderFromConfig_EmptyProvider(t *testing.T) { diff --git a/pkg/memory/store/embed_ollama.go b/pkg/memory/store/embed_ollama.go index 02d67c5e6..db9550af8 100644 --- a/pkg/memory/store/embed_ollama.go +++ b/pkg/memory/store/embed_ollama.go @@ -9,7 +9,7 @@ import ( "net/http" "time" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) const ( diff --git a/pkg/memory/store/embed_openai.go b/pkg/memory/store/embed_openai.go index f1328eec0..dec473814 100644 --- a/pkg/memory/store/embed_openai.go +++ b/pkg/memory/store/embed_openai.go @@ -9,7 +9,7 @@ import ( "net/http" "time" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) const ( diff --git a/pkg/memory/store/memory_store.go b/pkg/memory/store/memory_store.go index d6e2cc8f8..a778c29e8 100644 --- a/pkg/memory/store/memory_store.go +++ b/pkg/memory/store/memory_store.go @@ -4,10 +4,15 @@ import ( "context" "crypto/sha256" "fmt" + "sort" + "strings" + "sync" "time" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/observation" + memsqlc "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" ) // Config controls the MemoryStore behavior. @@ -33,11 +38,12 @@ func DefaultConfig() Config { // MemoryStore implements memory.Memory by composing a MemoryDelegate, EmbeddingProvider, and Chunker. type MemoryStore struct { - delegate memory.MemoryDelegate - embedder memory.EmbeddingProvider // may be nil if embeddings disabled - chunker memory.Chunker - cfg Config - agentID string + delegate memory.MemoryDelegate + embedder memory.EmbeddingProvider // may be nil if embeddings disabled + chunker memory.Chunker + cfg Config + agentID string + retrievalPolicyMu sync.Mutex } // New creates a MemoryStore. @@ -226,8 +232,8 @@ func (m *MemoryStore) Search(ctx context.Context, query string, opts memory.Sear limit = 10 } - var resultSets [][]memory.SearchResult - var weights []float64 + var baselineSets [][]memory.SearchResult + var baselineWeights []float64 // 1. Keyword search (via delegate) kwWeight := opts.KeywordWeight @@ -236,8 +242,8 @@ func (m *MemoryStore) Search(ctx context.Context, query string, opts memory.Sear } kwResults, err := m.keywordSearch(ctx, query, opts, limit*2) // fetch extra for fusion if err == nil && len(kwResults) > 0 { - resultSets = append(resultSets, kwResults) - weights = append(weights, kwWeight) + baselineSets = append(baselineSets, kwResults) + baselineWeights = append(baselineWeights, kwWeight) } // 2. Vector search (if embedder available) @@ -248,23 +254,77 @@ func (m *MemoryStore) Search(ctx context.Context, query string, opts memory.Sear if m.embedder != nil { vecResults, err := m.vectorSearch(ctx, query, opts, limit*2) if err == nil && len(vecResults) > 0 { - resultSets = append(resultSets, vecResults) - weights = append(weights, vecWeight) + baselineSets = append(baselineSets, vecResults) + baselineWeights = append(baselineWeights, vecWeight) } } - if len(resultSets) == 0 { + var baselineFinal []memory.SearchResult + if len(baselineSets) > 0 { + baselineMerged := ReciprocalRankFusion(baselineSets, baselineWeights, 60) + baselineFinal = m.postProcessMergedResults(ctx, baselineMerged, opts, limit) + } + + // 3. Projection-aware retrieval (working-context + DAG summaries) + hybridWeight := opts.RecencyWeight + if hybridWeight <= 0 { + hybridWeight = 0.6 + } + + hybridResults, err := m.hybridProjectionSearch(ctx, query, opts, limit*2) + augmentedFinal := baselineFinal + augmentedUsed := false + if err == nil && len(hybridResults) > 0 { + augmentedSets := append([][]memory.SearchResult{}, baselineSets...) + augmentedWeights := append([]float64{}, baselineWeights...) + augmentedSets = append(augmentedSets, hybridResults) + augmentedWeights = append(augmentedWeights, hybridWeight) + + augmentedMerged := ReciprocalRankFusion(augmentedSets, augmentedWeights, 60) + augmentedFinal = m.postProcessMergedResults(ctx, augmentedMerged, opts, limit) + if len(augmentedFinal) > 0 { + augmentedUsed = true + } + } + + if len(baselineFinal) == 0 && len(augmentedFinal) == 0 { return nil, nil } - // 3. RRF fusion - merged := ReciprocalRankFusion(resultSets, weights, 60) + m.retrievalPolicyMu.Lock() + state := m.loadRetrievalPolicyState(ctx) + gates := m.loadRetrievalPromotionGates(ctx) + metrics := m.loadRetrievalShadowMetrics(ctx) + parity := computeRetrievalParity(baselineFinal, augmentedFinal, limit) + state = m.updateRetrievalPolicy(ctx, state, gates, metrics, parity, augmentedUsed, baselineFinal) + m.retrievalPolicyMu.Unlock() - // 4. Recency decay + switch state.Mode { + case retrievalModePromoted: + return augmentedFinal, nil + case retrievalModeRollback, retrievalModeShadow: + return baselineFinal, nil + default: + return baselineFinal, nil + } +} + +func (m *MemoryStore) postProcessMergedResults( + ctx context.Context, + merged []memory.SearchResult, + opts memory.SearchOptions, + limit int, +) []memory.SearchResult { + if len(merged) == 0 { + return nil + } + + // 1. Recency decay halfLife := opts.HalfLifeHours if halfLife <= 0 { halfLife = m.cfg.DefaultHalfLifeHours } + // Build a createdAt lookup from recall items createdAtMap := make(map[ids.UUID]time.Time) for _, r := range merged { @@ -277,10 +337,10 @@ func (m *MemoryStore) Search(ctx context.Context, query string, opts memory.Sear return createdAtMap[id] }) - // 5. Metadata pre-filtering (sectors, session_key, date range) + // 2. Metadata pre-filtering (sectors, session_key, date range) merged = m.applyMetadataFilters(ctx, merged, opts) - // 6. Filter by min score + // 3. Filter by min score if opts.MinScore > 0 { filtered := merged[:0] for _, r := range merged { @@ -291,12 +351,86 @@ func (m *MemoryStore) Search(ctx context.Context, query string, opts memory.Sear merged = filtered } - // 7. Limit + // 4. Limit if len(merged) > limit { merged = merged[:limit] } + return merged +} - return merged, nil +func (m *MemoryStore) hybridProjectionSearch(ctx context.Context, query string, opts memory.SearchOptions, limit int) ([]memory.SearchResult, error) { + if opts.SessionKey == "" || limit <= 0 { + return nil, nil + } + + queryLower := strings.ToLower(strings.TrimSpace(query)) + results := make([]memory.SearchResult, 0, limit) + + // Working-context view + wc, err := m.delegate.GetWorkingContext(ctx, m.agentID, opts.SessionKey) + if err == nil && wc != nil && strings.TrimSpace(wc.Content) != "" { + score := 0.4 + if queryLower != "" && strings.Contains(strings.ToLower(wc.Content), queryLower) { + score = 0.95 + } + content := wc.Content + if len(content) > 1200 { + content = content[:1200] + "..." + } + results = append(results, memory.SearchResult{ + ID: ids.New(), + Content: content, + Source: "working-context:" + opts.SessionKey, + Score: score, + Sector: memory.SectorReflective, + }) + } + + // DAG summary view + type dagQueryProvider interface { + Queries() *memsqlc.Queries + } + if provider, ok := m.delegate.(dagQueryProvider); ok && provider.Queries() != nil { + snap, err := provider.Queries().GetLatestDAGSnapshotBySession(ctx, memsqlc.GetLatestDAGSnapshotBySessionParams{ + AgentID: m.agentID, + SessionKey: opts.SessionKey, + }) + if err == nil { + nodes, err := provider.Queries().ListDAGNodesBySnapshotID(ctx, memsqlc.ListDAGNodesBySnapshotIDParams{ + SnapshotID: snap.ID, + }) + if err == nil { + for _, node := range nodes { + if queryLower != "" && !strings.Contains(strings.ToLower(node.Summary), queryLower) { + continue + } + score := 0.55 + (0.1 * float64(node.Level)) + if score > 0.95 { + score = 0.95 + } + results = append(results, memory.SearchResult{ + ID: ids.New(), + Content: node.Summary, + Source: fmt.Sprintf("dag:%s:%s", opts.SessionKey, node.NodeID), + Score: score, + Sector: memory.SectorReflective, + }) + } + } + } + } + + if len(results) == 0 { + return nil, nil + } + + sort.Slice(results, func(i, j int) bool { + return results[i].Score > results[j].Score + }) + if len(results) > limit { + results = results[:limit] + } + return results, nil } // applyMetadataFilters removes results that don't match the requested sector, @@ -319,6 +453,24 @@ func (m *MemoryStore) applyMetadataFilters(ctx context.Context, results []memory filtered := results[:0] for _, r := range results { + // Synthetic projection entries (working-context + DAG summaries) are + // already scoped by session and do not map to recall item IDs. + if strings.HasPrefix(r.Source, "working-context:") || strings.HasPrefix(r.Source, "dag:") { + if needSessionFilter && !strings.Contains(r.Source, opts.SessionKey) { + continue + } + if needSectorFilter && !sectorSet[r.Sector] { + continue + } + // Synthetic entries do not carry durable timestamps, so honor + // explicit date filters conservatively by excluding them. + if needDateFilter { + continue + } + filtered = append(filtered, r) + continue + } + item, err := m.delegate.GetRecallItem(ctx, m.agentID, r.ID) if err != nil || item == nil { continue @@ -455,8 +607,12 @@ func (m *MemoryStore) ContextUsage(ctx context.Context, agentID, sessionKey stri return nil, err } - // Rough estimate: recall items avg ~100 tokens each - estimatedTotal := wcTokens + (recallCount * 100) + recallTokens, err := m.estimateRecallTokens(ctx, agentID, sessionKey, recallCount) + if err != nil { + return nil, err + } + + estimatedTotal := wcTokens + recallTokens ratio := float64(estimatedTotal) / float64(m.cfg.ContextWindowTokens) if ratio > 1.0 { ratio = 1.0 @@ -530,11 +686,44 @@ func (m *MemoryStore) Close() error { // --- helpers --- +func (m *MemoryStore) estimateRecallTokens(ctx context.Context, agentID, sessionKey string, recallCount int) (int, error) { + if recallCount <= 0 { + return 0, nil + } + + // Sample and scale for very large recall sets to avoid expensive full scans. + const maxSampleSize = 200 + sampleSize := recallCount + if sampleSize > maxSampleSize { + sampleSize = maxSampleSize + } + + items, err := m.delegate.ListRecallItems(ctx, agentID, sessionKey, sampleSize, 0) + if err != nil { + return 0, fmt.Errorf("list recall items for token estimate: %w", err) + } + if len(items) == 0 { + return 0, nil + } + + sampleTokens := 0 + for _, item := range items { + sampleTokens += estimateTokens(item.Content) + } + + if recallCount <= len(items) { + return sampleTokens, nil + } + + avgTokens := sampleTokens / len(items) + return sampleTokens + avgTokens*(recallCount-len(items)), nil +} + func estimateTokens(s string) int { if len(s) == 0 { return 0 } - return (len(s) + 3) / 4 + return observation.EstimateTokens(s) } func truncate(s string, maxChars int) string { diff --git a/pkg/memory/store/memory_store_test.go b/pkg/memory/store/memory_store_test.go index d82a40693..1e4ae1967 100644 --- a/pkg/memory/store/memory_store_test.go +++ b/pkg/memory/store/memory_store_test.go @@ -2,13 +2,17 @@ package store import ( "context" + "encoding/json" + "errors" "strings" + "sync" "testing" "time" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" - "github.com/sipeed/picoclaw/pkg/memory/delegate" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + memdag "github.com/ZanzyTHEbar/dragonscale/pkg/memory/dag" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -18,6 +22,20 @@ type mockEmbedder struct { dim int } +type failingKVDelegate struct { + memory.MemoryDelegate + failKeys map[string]error +} + +func (d *failingKVDelegate) UpsertKV(ctx context.Context, agentID, key, value string) error { + if d.failKeys != nil { + if err, ok := d.failKeys[key]; ok { + return err + } + } + return d.MemoryDelegate.UpsertKV(ctx, agentID, key, value) +} + func (m *mockEmbedder) Embed(_ context.Context, text string) (memory.Embedding, error) { return deterministicVec(text, m.dim), nil } @@ -264,6 +282,299 @@ func TestSearch_HybridWithEmbeddings(t *testing.T) { assert.NotEmpty(t, results) } +func TestSearch_ShadowModeUsesBaselineAndTracksParity(t *testing.T) { + ctx := context.Background() + store := newTestStore(t, false) + + // Baseline recall hit. + require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{ + AgentID: "agent-1", + SessionKey: "s1", + Role: "user", + Sector: memory.SectorSemantic, + Importance: 0.8, + Content: "Grocery checklist includes milk and eggs.", + })) + + // Augmented projection hit (would win if promoted). + require.NoError(t, store.SetWorkingContext(ctx, "agent-1", "s1", "Weekly planning notes include grocery tasks and laundry reminders.")) + + results, err := store.Search(ctx, "grocery", memory.SearchOptions{ + AgentID: "agent-1", + SessionKey: "s1", + Limit: 5, + }) + require.NoError(t, err) + require.NotEmpty(t, results) + // Shadow mode should keep baseline result ordering for production output. + assert.NotContains(t, results[0].Source, "working-context:s1") + + metricsRaw, err := store.delegate.GetKV(ctx, "agent-1", retrievalPolicyMetricsKey) + require.NoError(t, err) + require.NotEmpty(t, metricsRaw) + + var metrics retrievalShadowMetrics + require.NoError(t, json.Unmarshal([]byte(metricsRaw), &metrics)) + assert.Equal(t, 1, metrics.TotalQueries) +} + +func TestSearch_DoesNotPromoteWithoutAugmentedSignals(t *testing.T) { + ctx := context.Background() + store := newTestStore(t, false) + + gates := retrievalPromotionGates{ + MinSamples: 1, + MinAugmentedSamples: 1, + MinTop1Parity: 0.0001, + MinOverlapAtK: 0.0001, + MinPromotedSamples: 1, + RollbackTop1Parity: 0.0001, + RollbackOverlapAtK: 0.0001, + MaxNoResultRate: 1.0, + PromotedNoResultRate: 1.0, + } + gatesPayload, err := json.Marshal(gates) + require.NoError(t, err) + require.NoError(t, store.delegate.UpsertKV(ctx, "agent-1", retrievalPolicyGatesKey, string(gatesPayload))) + + require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{ + AgentID: "agent-1", + SessionKey: "s1", + Role: "user", + Sector: memory.SectorSemantic, + Importance: 0.8, + Content: "Baseline-only query signal.", + })) + + _, err = store.Search(ctx, "baseline-only", memory.SearchOptions{ + AgentID: "agent-1", + SessionKey: "s1", + Limit: 5, + }) + require.NoError(t, err) + + stateRaw, err := store.delegate.GetKV(ctx, "agent-1", retrievalPolicyStateKey) + require.NoError(t, err) + var state retrievalPolicyState + require.NoError(t, json.Unmarshal([]byte(stateRaw), &state)) + assert.Equal(t, retrievalModeShadow, state.Mode) +} + +func TestSearch_PromoteOnlyOnGateWin(t *testing.T) { + ctx := context.Background() + store := newTestStore(t, false) + + gates := retrievalPromotionGates{ + MinSamples: 1, + MinAugmentedSamples: 1, + MinTop1Parity: 0.0001, + MinOverlapAtK: 0.0001, + MinPromotedSamples: 1, + RollbackTop1Parity: 0.0001, + RollbackOverlapAtK: 0.0001, + MaxNoResultRate: 1.0, + PromotedNoResultRate: 1.0, + } + gatesPayload, err := json.Marshal(gates) + require.NoError(t, err) + require.NoError(t, store.delegate.UpsertKV(ctx, "agent-1", retrievalPolicyGatesKey, string(gatesPayload))) + + require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{ + AgentID: "agent-1", + SessionKey: "s1", + Role: "user", + Sector: memory.SectorSemantic, + Importance: 0.8, + Content: "Baseline grocery memory entry.", + })) + require.NoError(t, store.SetWorkingContext(ctx, "agent-1", "s1", "Augmented grocery projection content.")) + + _, err = store.Search(ctx, "grocery", memory.SearchOptions{ + AgentID: "agent-1", + SessionKey: "s1", + Limit: 5, + }) + require.NoError(t, err) + + stateRaw, err := store.delegate.GetKV(ctx, "agent-1", retrievalPolicyStateKey) + require.NoError(t, err) + require.NotEmpty(t, stateRaw) + + var state retrievalPolicyState + require.NoError(t, json.Unmarshal([]byte(stateRaw), &state)) + assert.Equal(t, retrievalModePromoted, state.Mode) +} + +func TestSearch_FastRollbackPreservesBaselinePath(t *testing.T) { + ctx := context.Background() + store := newTestStore(t, false) + + del, ok := store.delegate.(*delegate.LibSQLDelegate) + require.True(t, ok) + + gates := retrievalPromotionGates{ + MinSamples: 1, + MinAugmentedSamples: 1, + MinTop1Parity: 0.0001, + MinOverlapAtK: 0.0001, + MinPromotedSamples: 1, + RollbackTop1Parity: 1.0, // Force rollback if top-1 diverges. + RollbackOverlapAtK: 1.0, // Force rollback if overlap is not perfect. + MaxNoResultRate: 1.0, + PromotedNoResultRate: 1.0, + } + gatesPayload, err := json.Marshal(gates) + require.NoError(t, err) + require.NoError(t, store.delegate.UpsertKV(ctx, "agent-1", retrievalPolicyGatesKey, string(gatesPayload))) + + statePayload, err := json.Marshal(retrievalPolicyState{ + Mode: retrievalModePromoted, + Reason: "test_bootstrap", + UpdatedAt: time.Now().UTC(), + }) + require.NoError(t, err) + require.NoError(t, store.delegate.UpsertKV(ctx, "agent-1", retrievalPolicyStateKey, string(statePayload))) + + require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{ + AgentID: "agent-1", + SessionKey: "s1", + Role: "user", + Sector: memory.SectorSemantic, + Importance: 0.8, + Content: "Baseline grocery memory entry.", + })) + + snap := &memdag.PersistSnapshot{ + FromMsgIdx: 0, + ToMsgIdx: 4, + MsgCount: 4, + DAG: &memdag.DAG{ + Nodes: map[string]*memdag.Node{ + "n-session": { + ID: "n-session", + Level: memdag.LevelSession, + Summary: "Household grocery commitments with Friday reminder schedule.", + Tokens: 12, + StartIdx: 0, + EndIdx: 4, + }, + }, + Roots: []string{"n-session"}, + }, + } + require.NoError(t, del.PersistDAG(ctx, "agent-1", "s1", snap)) + require.NoError(t, store.SetWorkingContext(ctx, "agent-1", "s1", "Working context grocery signal.")) + + // First call in promoted mode should evaluate and trigger rollback. + _, err = store.Search(ctx, "grocery", memory.SearchOptions{ + AgentID: "agent-1", + SessionKey: "s1", + Limit: 10, + }) + require.NoError(t, err) + + stateRaw, err := store.delegate.GetKV(ctx, "agent-1", retrievalPolicyStateKey) + require.NoError(t, err) + var state retrievalPolicyState + require.NoError(t, json.Unmarshal([]byte(stateRaw), &state)) + assert.Equal(t, retrievalModeRollback, state.Mode) + + // Subsequent calls should return baseline-only path again. + results, err := store.Search(ctx, "grocery", memory.SearchOptions{ + AgentID: "agent-1", + SessionKey: "s1", + Limit: 10, + }) + require.NoError(t, err) + require.NotEmpty(t, results) + assert.NotContains(t, results[0].Source, "working-context:") + assert.NotContains(t, results[0].Source, "dag:") +} + +func TestUpdateRetrievalPolicy_PersistFailuresDoNotBlockTransitions(t *testing.T) { + ctx := context.Background() + store := newTestStore(t, true) + + baseDelegate := store.delegate + store.delegate = &failingKVDelegate{ + MemoryDelegate: baseDelegate, + failKeys: map[string]error{ + retrievalPolicyStateKey: errors.New("state kv unavailable"), + retrievalPolicyMetricsKey: errors.New("metrics kv unavailable"), + }, + } + + state := defaultRetrievalPolicyState() + gates := retrievalPromotionGates{ + MinSamples: 1, + MinAugmentedSamples: 1, + MinTop1Parity: 0.5, + MinOverlapAtK: 0.5, + MinPromotedSamples: 1, + RollbackTop1Parity: 0.1, + RollbackOverlapAtK: 0.1, + MaxNoResultRate: 1.0, + PromotedNoResultRate: 1.0, + } + metrics := defaultRetrievalShadowMetrics() + parity := retrievalParity{ + Top1Match: true, + OverlapAtK: 1.0, + } + baseline := []memory.SearchResult{ + {ID: ids.New(), Content: "baseline"}, + } + + next := store.updateRetrievalPolicy(ctx, state, gates, metrics, parity, true, baseline) + assert.Equal(t, retrievalModePromoted, next.Mode) +} + +func TestSearch_ConcurrentRetrievalPolicyUpdates(t *testing.T) { + ctx := context.Background() + store := newTestStore(t, true) + + require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{ + AgentID: "agent-1", + SessionKey: "s1", + Role: "user", + Sector: memory.SectorSemantic, + Importance: 0.9, + Content: "Grocery planning memory for retrieval policy concurrency tests.", + Tags: "grocery", + })) + + const workers = 16 + var wg sync.WaitGroup + errCh := make(chan error, workers) + + for i := 0; i < workers; i++ { + wg.Add(1) + go func() { + defer wg.Done() + _, err := store.Search(ctx, "grocery", memory.SearchOptions{ + AgentID: "agent-1", + SessionKey: "s1", + Limit: 10, + }) + if err != nil { + errCh <- err + } + }() + } + wg.Wait() + close(errCh) + + for err := range errCh { + require.NoError(t, err) + } + + metricsRaw, err := store.delegate.GetKV(ctx, "agent-1", retrievalPolicyMetricsKey) + require.NoError(t, err) + var metrics retrievalShadowMetrics + require.NoError(t, json.Unmarshal([]byte(metricsRaw), &metrics)) + assert.GreaterOrEqual(t, metrics.TotalQueries, workers) +} + func TestContextUsage(t *testing.T) { ctx := context.Background() store := newTestStore(t, false) @@ -285,6 +596,40 @@ func TestContextUsage(t *testing.T) { assert.Greater(t, pressure.EstimatedTotalTokens, 0) } +func TestContextUsage_RecallTokenEstimateUsesContent(t *testing.T) { + ctx := context.Background() + store := newTestStore(t, false) + + contentA := strings.Repeat("schedule follow-up reminder ", 80) + contentB := strings.Repeat("prepare project update summary ", 80) + + require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{ + AgentID: "agent-1", + SessionKey: "session-1", + Role: "user", + Sector: memory.SectorEpisodic, + Importance: 0.7, + Content: contentA, + Tags: "reminder", + })) + require.NoError(t, store.StoreRecall(ctx, &memory.RecallItem{ + AgentID: "agent-1", + SessionKey: "session-1", + Role: "assistant", + Sector: memory.SectorEpisodic, + Importance: 0.7, + Content: contentB, + Tags: "summary", + })) + + pressure, err := store.ContextUsage(ctx, "agent-1", "session-1") + require.NoError(t, err) + + expectedRecallTokens := estimateTokens(contentA) + estimateTokens(contentB) + assert.GreaterOrEqual(t, pressure.EstimatedTotalTokens, expectedRecallTokens) + assert.Equal(t, 2, pressure.RecallItemCount) +} + func TestContextUsage_PressureLevels(t *testing.T) { ctx := context.Background() @@ -301,8 +646,8 @@ func TestContextUsage_PressureLevels(t *testing.T) { OffloadThresholdTokens: 50, }) - // Set working context to ~80 tokens (320 chars / 4) - err = store.SetWorkingContext(ctx, "agent-1", "s1", strings.Repeat("a", 320)) + // Set working context to ~80 tokens. + err = store.SetWorkingContext(ctx, "agent-1", "s1", contentAtLeastTokens(80)) require.NoError(t, err) pressure, err := store.ContextUsage(ctx, "agent-1", "s1") @@ -314,7 +659,7 @@ func TestShouldOffload(t *testing.T) { store := &MemoryStore{cfg: Config{OffloadThresholdTokens: 100}} assert.False(t, store.ShouldOffload("short")) - assert.True(t, store.ShouldOffload(strings.Repeat("x", 500))) // 500 chars ≈ 125 tokens + assert.True(t, store.ShouldOffload(contentAtLeastTokens(120))) } func TestOffloadToolResult(t *testing.T) { diff --git a/pkg/memory/store/memory_tool.go b/pkg/memory/store/memory_tool.go index c67e6bb1e..561a19d1c 100644 --- a/pkg/memory/store/memory_tool.go +++ b/pkg/memory/store/memory_tool.go @@ -6,8 +6,8 @@ import ( jsonv2 "github.com/go-json-experiment/json" "strings" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // MemoryToolAction is the action to perform on the memory system. diff --git a/pkg/memory/store/queue.go b/pkg/memory/store/queue.go index 38fe020f1..cb5cc997c 100644 --- a/pkg/memory/store/queue.go +++ b/pkg/memory/store/queue.go @@ -5,7 +5,7 @@ import ( "fmt" "strings" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // QueueManagerConfig configures the context pressure management policy. diff --git a/pkg/memory/store/queue_test.go b/pkg/memory/store/queue_test.go index d8daddf5b..f55525140 100644 --- a/pkg/memory/store/queue_test.go +++ b/pkg/memory/store/queue_test.go @@ -5,8 +5,8 @@ import ( "strings" "testing" - "github.com/sipeed/picoclaw/pkg/memory" - "github.com/sipeed/picoclaw/pkg/memory/delegate" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -45,8 +45,8 @@ func TestQueueManager_WarnPressure(t *testing.T) { ctx := context.Background() qm, store := newTestQueueManager(t, 100) // tiny window - // Fill working context to ~75% (300 chars ≈ 75 tokens, 75% of 100) - err := store.SetWorkingContext(ctx, "agent-1", "s1", strings.Repeat("x", 300)) + // Fill working context to ~75% of context window. + err := store.SetWorkingContext(ctx, "agent-1", "s1", contentAtLeastTokens(75)) require.NoError(t, err) decision, err := qm.Evaluate(ctx, "agent-1", "s1") @@ -59,8 +59,8 @@ func TestQueueManager_OffloadPressure(t *testing.T) { ctx := context.Background() qm, store := newTestQueueManager(t, 100) // tiny window - // Fill to ~82% (328 chars ≈ 82 tokens) - err := store.SetWorkingContext(ctx, "agent-1", "s1", strings.Repeat("x", 328)) + // Fill to ~82% of context window. + err := store.SetWorkingContext(ctx, "agent-1", "s1", contentAtLeastTokens(82)) require.NoError(t, err) decision, err := qm.Evaluate(ctx, "agent-1", "s1") @@ -73,8 +73,8 @@ func TestQueueManager_FlushPressure(t *testing.T) { ctx := context.Background() qm, store := newTestQueueManager(t, 100) // tiny window - // Fill to ~88% (352 chars ≈ 88 tokens) - err := store.SetWorkingContext(ctx, "agent-1", "s1", strings.Repeat("x", 352)) + // Fill to ~88% of context window. + err := store.SetWorkingContext(ctx, "agent-1", "s1", contentAtLeastTokens(88)) require.NoError(t, err) decision, err := qm.Evaluate(ctx, "agent-1", "s1") diff --git a/pkg/memory/store/retrieval.go b/pkg/memory/store/retrieval.go index 148229e22..620f43037 100644 --- a/pkg/memory/store/retrieval.go +++ b/pkg/memory/store/retrieval.go @@ -5,8 +5,8 @@ import ( "sort" "time" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) // CosineSimilarity computes the cosine similarity between two embedding vectors. diff --git a/pkg/memory/store/scorer.go b/pkg/memory/store/scorer.go index e0fa71666..1e4987012 100644 --- a/pkg/memory/store/scorer.go +++ b/pkg/memory/store/scorer.go @@ -7,8 +7,8 @@ import ( "strings" "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/memory" - "github.com/sipeed/picoclaw/pkg/security" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/security" ) // ScoreResult is the output of scoring a piece of content. diff --git a/pkg/memory/store/scorer_test.go b/pkg/memory/store/scorer_test.go index f9d7d1d8e..80f408980 100644 --- a/pkg/memory/store/scorer_test.go +++ b/pkg/memory/store/scorer_test.go @@ -4,7 +4,7 @@ import ( "context" "testing" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/messages/types.go b/pkg/messages/types.go index 2450e8bf0..0b8025460 100644 --- a/pkg/messages/types.go +++ b/pkg/messages/types.go @@ -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 // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package messages diff --git a/pkg/migrate/config.go b/pkg/migrate/config.go index 70a3b3373..d446b006f 100644 --- a/pkg/migrate/config.go +++ b/pkg/migrate/config.go @@ -9,7 +9,7 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) var supportedProviders = map[string]bool{ @@ -99,7 +99,7 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error if !supportedProviders[name] { if apiKey != "" || apiBase != "" { - warnings = append(warnings, fmt.Sprintf("Provider '%s' not supported in PicoClaw, skipping", name)) + warnings = append(warnings, fmt.Sprintf("Provider '%s' not supported in DragonScale, skipping", name)) } continue } @@ -134,7 +134,7 @@ func ConvertConfig(data map[string]interface{}) (*config.Config, []string, error continue } if !supportedChannels[name] { - warnings = append(warnings, fmt.Sprintf("Channel '%s' not supported in PicoClaw, skipping", name)) + warnings = append(warnings, fmt.Sprintf("Channel '%s' not supported in DragonScale, skipping", name)) continue } enabled, _ := getBool(cMap, "enabled") @@ -327,7 +327,7 @@ func convertKeysToSnake(data interface{}) interface{} { } func rewriteWorkspacePath(path string) string { - path = strings.Replace(path, ".openclaw", ".picoclaw", 1) + path = strings.Replace(path, ".openclaw", ".dragonscale", 1) return path } diff --git a/pkg/migrate/migrate.go b/pkg/migrate/migrate.go index 921f821cb..96cc85ecd 100644 --- a/pkg/migrate/migrate.go +++ b/pkg/migrate/migrate.go @@ -7,7 +7,7 @@ import ( "path/filepath" "strings" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) type ActionType int @@ -76,7 +76,7 @@ func Run(opts Options) (*Result, error) { return nil, err } - fmt.Println("Migrating from OpenClaw to PicoClaw") + fmt.Println("Migrating from OpenClaw to DragonScale") fmt.Printf(" Source: %s\n", openclawHome) fmt.Printf(" Destination: %s\n", picoClawHome) fmt.Println() @@ -118,7 +118,7 @@ func Plan(opts Options, openclawHome, picoClawHome string) ([]Action, []string, Type: ActionConvertConfig, Source: configPath, Destination: filepath.Join(picoClawHome, "config.json"), - Description: "convert OpenClaw config to PicoClaw format", + Description: "convert OpenClaw config to DragonScale format", }) data, err := LoadOpenClawConfig(configPath) @@ -221,7 +221,7 @@ func executeConfigMigration(srcConfigPath, dstConfigPath, picoClawHome string) e if _, err := os.Stat(dstConfigPath); err == nil { existing, err := config.LoadConfig(dstConfigPath) if err != nil { - return fmt.Errorf("loading existing PicoClaw config: %w", err) + return fmt.Errorf("loading existing DragonScale config: %w", err) } incoming = MergeConfig(existing, incoming) } @@ -330,14 +330,14 @@ func resolvePicoClawHome(override string) (string, error) { if override != "" { return expandHome(override), nil } - if envHome := os.Getenv("PICOCLAW_HOME"); envHome != "" { + if envHome := os.Getenv("DRAGONSCALE_HOME"); envHome != "" { return expandHome(envHome), nil } home, err := os.UserHomeDir() if err != nil { return "", fmt.Errorf("resolving home directory: %w", err) } - return filepath.Join(home, ".picoclaw"), nil + return filepath.Join(home, ".dragonscale"), nil } func resolveWorkspace(homeDir string) string { diff --git a/pkg/migrate/migrate_test.go b/pkg/migrate/migrate_test.go index 346bfed19..dc4bfe368 100644 --- a/pkg/migrate/migrate_test.go +++ b/pkg/migrate/migrate_test.go @@ -7,7 +7,7 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) func TestCamelToSnake(t *testing.T) { @@ -194,7 +194,7 @@ func TestConvertConfig(t *testing.T) { if len(warnings) != 1 { t.Fatalf("expected 1 warning, got %d", len(warnings)) } - if warnings[0] != "Provider 'deepseek' not supported in PicoClaw, skipping" { + if warnings[0] != "Provider 'deepseek' not supported in DragonScale, skipping" { t.Errorf("unexpected warning: %s", warnings[0]) } }) @@ -248,7 +248,7 @@ func TestConvertConfig(t *testing.T) { if len(warnings) != 1 { t.Fatalf("expected 1 warning, got %d", len(warnings)) } - if warnings[0] != "Channel 'email' not supported in PicoClaw, skipping" { + if warnings[0] != "Channel 'email' not supported in DragonScale, skipping" { t.Errorf("unexpected warning: %s", warnings[0]) } }) @@ -279,8 +279,8 @@ func TestConvertConfig(t *testing.T) { if cfg.Agents.Defaults.Temperature != 0.5 { t.Errorf("Temperature = %f, want %f", cfg.Agents.Defaults.Temperature, 0.5) } - if cfg.Agents.Defaults.Workspace != "~/.picoclaw/workspace" { - t.Errorf("Workspace = %q, want %q", cfg.Agents.Defaults.Workspace, "~/.picoclaw/workspace") + if cfg.Agents.Defaults.Workspace != "~/.dragonscale/workspace" { + t.Errorf("Workspace = %q, want %q", cfg.Agents.Defaults.Workspace, "~/.dragonscale/workspace") } }) @@ -554,7 +554,7 @@ func TestRewriteWorkspacePath(t *testing.T) { input string want string }{ - {"default path", "~/.openclaw/workspace", "~/.picoclaw/workspace"}, + {"default path", "~/.openclaw/workspace", "~/.dragonscale/workspace"}, {"custom path", "/custom/path", "/custom/path"}, {"empty", "", ""}, } @@ -681,7 +681,7 @@ func TestRunFullMigration(t *testing.T) { picoConfig, err := config.LoadConfig(filepath.Join(picoClawHome, "config.json")) if err != nil { - t.Fatalf("loading PicoClaw config: %v", err) + t.Fatalf("loading DragonScale config: %v", err) } if picoConfig.Providers.Anthropic.APIKey != "sk-ant-migrate-test" { t.Errorf("Anthropic.APIKey = %q, want %q", picoConfig.Providers.Anthropic.APIKey, "sk-ant-migrate-test") diff --git a/pkg/migrate/xdg.go b/pkg/migrate/xdg.go index b25edf3d8..9ed2b96f1 100644 --- a/pkg/migrate/xdg.go +++ b/pkg/migrate/xdg.go @@ -7,29 +7,29 @@ import ( "path/filepath" "strings" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) -// MigrateToXDG moves files from the legacy ~/.picoclaw/workspace/ layout to +// MigrateToXDG moves files from the legacy ~/.dragonscale/workspace/ layout to // XDG-compliant directories. It is idempotent: files are only copied if they // don't already exist at the destination, and it never deletes the source. // // Layout mapping: // -// ~/.picoclaw/workspace/AGENT.md → $XDG_CONFIG_HOME/picoclaw/identity/AGENT.md -// ~/.picoclaw/workspace/IDENTITY.md → $XDG_CONFIG_HOME/picoclaw/identity/IDENTITY.md -// ~/.picoclaw/workspace/SOUL.md → $XDG_CONFIG_HOME/picoclaw/identity/SOUL.md -// ~/.picoclaw/workspace/USER.md → $XDG_CONFIG_HOME/picoclaw/identity/USER.md -// ~/.picoclaw/workspace/skills/* → $XDG_DATA_HOME/picoclaw/skills/* -// ~/.picoclaw/workspace/memory/*.db → $XDG_DATA_HOME/picoclaw/picoclaw.db -// ~/.picoclaw/workspace/* → $XDG_DATA_HOME/picoclaw/sandbox/* (remaining files) +// ~/.dragonscale/workspace/AGENT.md → $XDG_CONFIG_HOME/dragonscale/identity/AGENT.md +// ~/.dragonscale/workspace/IDENTITY.md → $XDG_CONFIG_HOME/dragonscale/identity/IDENTITY.md +// ~/.dragonscale/workspace/SOUL.md → $XDG_CONFIG_HOME/dragonscale/identity/SOUL.md +// ~/.dragonscale/workspace/USER.md → $XDG_CONFIG_HOME/dragonscale/identity/USER.md +// ~/.dragonscale/workspace/skills/* → $XDG_DATA_HOME/dragonscale/skills/* +// ~/.dragonscale/workspace/memory/*.db → $XDG_DATA_HOME/dragonscale/dragonscale.db +// ~/.dragonscale/workspace/* → $XDG_DATA_HOME/dragonscale/sandbox/* (remaining files) func MigrateToXDG(legacyWorkspace string) error { if legacyWorkspace == "" { home, err := os.UserHomeDir() if err != nil { return fmt.Errorf("resolve home: %w", err) } - legacyWorkspace = filepath.Join(home, ".picoclaw", "workspace") + legacyWorkspace = filepath.Join(home, ".dragonscale", "workspace") } if _, err := os.Stat(legacyWorkspace); os.IsNotExist(err) { @@ -73,9 +73,9 @@ func MigrateToXDG(legacyWorkspace string) error { } } - legacyDB := filepath.Join(legacyWorkspace, "memory", "picoclaw.db") + legacyDB := filepath.Join(legacyWorkspace, "memory", "dragonscale.db") if _, err := os.Stat(legacyDB); err == nil { - newDB := filepath.Join(dataDir, "picoclaw.db") + newDB := filepath.Join(dataDir, "dragonscale.db") if err := copyIfMissing(legacyDB, newDB); err != nil { return fmt.Errorf("migrate database: %w", err) } diff --git a/pkg/providers/types.go b/pkg/providers/types.go index 56f42290c..5433a6607 100644 --- a/pkg/providers/types.go +++ b/pkg/providers/types.go @@ -3,7 +3,7 @@ package providers import ( "context" - "github.com/sipeed/picoclaw/pkg/messages" + "github.com/ZanzyTHEbar/dragonscale/pkg/messages" ) // Type aliases: canonical types now live in pkg/messages. diff --git a/pkg/rlm/engine.go b/pkg/rlm/engine.go index c246e5591..58d4925d0 100644 --- a/pkg/rlm/engine.go +++ b/pkg/rlm/engine.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/security/securebus" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus" ) // CallModelFunc is the signature for making a direct LLM call. diff --git a/pkg/rlm/engine_test.go b/pkg/rlm/engine_test.go index edfe99405..4610569d4 100644 --- a/pkg/rlm/engine_test.go +++ b/pkg/rlm/engine_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/sipeed/picoclaw/pkg/rlm" + "github.com/ZanzyTHEbar/dragonscale/pkg/rlm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/rlm/rope.go b/pkg/rlm/rope.go index 052a6bd86..794e31ec0 100644 --- a/pkg/rlm/rope.go +++ b/pkg/rlm/rope.go @@ -1,5 +1,5 @@ // Package rlm implements the Recursive Language Model (RLM) engine for -// PicoClaw. RLM enables unbounded context processing by recursively +// DragonScale. RLM enables unbounded context processing by recursively // decomposing long contexts into manageable partitions, processing each // through the SecureBus, and synthesizing the results. // diff --git a/pkg/rlm/rope_test.go b/pkg/rlm/rope_test.go index df3f355a4..7f92de5ee 100644 --- a/pkg/rlm/rope_test.go +++ b/pkg/rlm/rope_test.go @@ -4,7 +4,7 @@ import ( "strings" "testing" - "github.com/sipeed/picoclaw/pkg/rlm" + "github.com/ZanzyTHEbar/dragonscale/pkg/rlm" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/runtime/bootstrap.go b/pkg/runtime/bootstrap.go index f42e4b98b..732e42fb8 100644 --- a/pkg/runtime/bootstrap.go +++ b/pkg/runtime/bootstrap.go @@ -6,10 +6,10 @@ import ( "time" fantasy "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/agent" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/config" - picofantasy "github.com/sipeed/picoclaw/pkg/fantasy" + "github.com/ZanzyTHEbar/dragonscale/pkg/agent" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" + picofantasy "github.com/ZanzyTHEbar/dragonscale/pkg/fantasy" ) type OutboundMode string @@ -36,6 +36,11 @@ type RuntimeHandle struct { outDone chan struct{} } +type kernelInvariantChecker interface { + HasSecureBus() bool + HasUnifiedRuntimeDeps() bool +} + func (h *RuntimeHandle) Context() context.Context { return h.ctx } @@ -94,6 +99,13 @@ func Bootstrap(parent context.Context, cfg *config.Config, opts BootstrapOptions } return nil, fmt.Errorf("agent loop init error: %w", err) } + if err := validateKernelInvariants(agentLoop); err != nil { + cancel() + if outDone != nil { + <-outDone + } + return nil, fmt.Errorf("agent loop init error: %w", err) + } return &RuntimeHandle{ ctx: ctx, @@ -104,6 +116,19 @@ func Bootstrap(parent context.Context, cfg *config.Config, opts BootstrapOptions }, nil } +func validateKernelInvariants(loop kernelInvariantChecker) error { + if loop == nil { + return fmt.Errorf("agent loop is nil") + } + if !loop.HasSecureBus() { + return fmt.Errorf("secure bus is not configured") + } + if !loop.HasUnifiedRuntimeDeps() { + return fmt.Errorf("unified runtime dependencies are missing") + } + return nil +} + func withExecutionContext(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) { if timeout > 0 { return context.WithTimeout(parent, timeout) diff --git a/pkg/runtime/config.go b/pkg/runtime/config.go index b4db2fd8a..52c78886b 100644 --- a/pkg/runtime/config.go +++ b/pkg/runtime/config.go @@ -6,10 +6,10 @@ import ( "path/filepath" "time" - "github.com/sipeed/picoclaw/pkg/config" + "github.com/ZanzyTHEbar/dragonscale/pkg/config" ) -const EvalConfigEnvVar = "PICOCLAW_EVAL_CONFIG" +const EvalConfigEnvVar = "DRAGONSCALE_EVAL_CONFIG" type LoadConfigOptions struct { BaseConfigPath string @@ -18,7 +18,7 @@ type LoadConfigOptions struct { } func ResolveBaseConfigPath() string { - // Prefer XDG standard path (~/.config/picoclaw/config.json) when present. + // Prefer XDG standard path (~/.config/dragonscale/config.json) when present. if xdgPath, err := config.DefaultConfigPath(); err == nil { if _, statErr := os.Stat(xdgPath); statErr == nil { return xdgPath @@ -26,7 +26,7 @@ func ResolveBaseConfigPath() string { } home, _ := os.UserHomeDir() - legacy := filepath.Join(home, ".picoclaw", "config.json") + legacy := filepath.Join(home, ".dragonscale", "config.json") if _, err := os.Stat(legacy); err == nil { return legacy } diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index ff8b98bde..cad5a82c4 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -7,19 +7,32 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +type stubKernelChecker struct { + secure bool + deps bool +} + +func (s stubKernelChecker) HasSecureBus() bool { + return s.secure +} + +func (s stubKernelChecker) HasUnifiedRuntimeDeps() bool { + return s.deps +} + func TestResolveBaseConfigPath_PrefersXDGOverLegacy(t *testing.T) { home := t.TempDir() xdg := t.TempDir() t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", xdg) - xdgPath := filepath.Join(xdg, "picoclaw", "config.json") - legacyPath := filepath.Join(home, ".picoclaw", "config.json") + xdgPath := filepath.Join(xdg, "dragonscale", "config.json") + legacyPath := filepath.Join(home, ".dragonscale", "config.json") require.NoError(t, os.MkdirAll(filepath.Dir(xdgPath), 0o755)) require.NoError(t, os.MkdirAll(filepath.Dir(legacyPath), 0o755)) require.NoError(t, os.WriteFile(xdgPath, []byte(`{}`), 0o644)) @@ -35,7 +48,7 @@ func TestResolveBaseConfigPath_FallsBackToLegacyWhenXDGMissing(t *testing.T) { t.Setenv("HOME", home) t.Setenv("XDG_CONFIG_HOME", xdg) - legacyPath := filepath.Join(home, ".picoclaw", "config.json") + legacyPath := filepath.Join(home, ".dragonscale", "config.json") require.NoError(t, os.MkdirAll(filepath.Dir(legacyPath), 0o755)) require.NoError(t, os.WriteFile(legacyPath, []byte(`{}`), 0o644)) @@ -142,3 +155,28 @@ func TestStartOutbound_CallbackReceivesMessages(t *testing.T) { t.Fatal("did not receive callback outbound message") } } + +func TestValidateKernelInvariants(t *testing.T) { + t.Run("nil loop", func(t *testing.T) { + err := validateKernelInvariants(nil) + require.Error(t, err) + assert.ErrorContains(t, err, "agent loop is nil") + }) + + t.Run("secure bus missing", func(t *testing.T) { + err := validateKernelInvariants(stubKernelChecker{secure: false, deps: true}) + require.Error(t, err) + assert.ErrorContains(t, err, "secure bus is not configured") + }) + + t.Run("unified deps missing", func(t *testing.T) { + err := validateKernelInvariants(stubKernelChecker{secure: true, deps: false}) + require.Error(t, err) + assert.ErrorContains(t, err, "unified runtime dependencies are missing") + }) + + t.Run("all invariants satisfied", func(t *testing.T) { + err := validateKernelInvariants(stubKernelChecker{secure: true, deps: true}) + require.NoError(t, err) + }) +} diff --git a/pkg/security/keyring.go b/pkg/security/keyring.go index 496bbbace..51c8de9dd 100644 --- a/pkg/security/keyring.go +++ b/pkg/security/keyring.go @@ -1,10 +1,13 @@ -// Package security provides credential management for PicoClaw. +// Package security provides credential management for DragonScale. // This file defines the KeyringProvider interface and the master key // sourcing strategy used by SecretStore. package security import "fmt" +// MasterKeyEnvVar is the canonical environment variable used by EnvKeyring. +const MasterKeyEnvVar = "DRAGONSCALE_MASTER_KEY" + // KeyringProvider abstracts OS keyring backends. The implementation is // selected at runtime (or via build tags for platform-specific backends). // @@ -23,7 +26,7 @@ type KeyringProvider interface { } // ErrNoMasterKey is returned by KeyringProvider when no master key is available. -var ErrNoMasterKey = fmt.Errorf("no master key configured: run `picoclaw secret init` to set one") +var ErrNoMasterKey = fmt.Errorf("no master key configured: run `dragonscale secret init` to set one") // ErrKeyringReadOnly is returned when SetMasterKey is called on a read-only provider. var ErrKeyringReadOnly = fmt.Errorf("keyring is read-only") diff --git a/pkg/security/securebus/bus.go b/pkg/security/securebus/bus.go index c9b357085..3c1baf440 100644 --- a/pkg/security/securebus/bus.go +++ b/pkg/security/securebus/bus.go @@ -9,9 +9,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/security" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/security" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) // ToolExecutor is the function signature the SecureBus calls to run a tool. diff --git a/pkg/security/securebus/bus_test.go b/pkg/security/securebus/bus_test.go index 78a726b44..0f51e3232 100644 --- a/pkg/security/securebus/bus_test.go +++ b/pkg/security/securebus/bus_test.go @@ -5,10 +5,10 @@ import ( jsonv2 "github.com/go-json-experiment/json" "testing" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/security" - "github.com/sipeed/picoclaw/pkg/security/securebus" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/security" + "github.com/ZanzyTHEbar/dragonscale/pkg/security/securebus" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/security/securebus/policy.go b/pkg/security/securebus/policy.go index 805b175b5..f156b6083 100644 --- a/pkg/security/securebus/policy.go +++ b/pkg/security/securebus/policy.go @@ -5,8 +5,8 @@ import ( "path/filepath" "strings" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" ) // PolicyConfig controls global enforcement settings for the SecureBus. diff --git a/pkg/security/securebus/policy_test.go b/pkg/security/securebus/policy_test.go index 3f5d57f88..fc38c3a9d 100644 --- a/pkg/security/securebus/policy_test.go +++ b/pkg/security/securebus/policy_test.go @@ -3,8 +3,8 @@ package securebus import ( "testing" - "github.com/sipeed/picoclaw/pkg/itr" - "github.com/sipeed/picoclaw/pkg/tools" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/tools" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/security/securebus/socket_transport.go b/pkg/security/securebus/socket_transport.go index e38c1cfb3..3115f0c22 100644 --- a/pkg/security/securebus/socket_transport.go +++ b/pkg/security/securebus/socket_transport.go @@ -10,7 +10,7 @@ import ( "sync" "time" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" ) const ( diff --git a/pkg/security/securebus/socket_transport_test.go b/pkg/security/securebus/socket_transport_test.go index 621c94482..cfc18d1da 100644 --- a/pkg/security/securebus/socket_transport_test.go +++ b/pkg/security/securebus/socket_transport_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/security/securebus/transport.go b/pkg/security/securebus/transport.go index 33c47c577..175a083bb 100644 --- a/pkg/security/securebus/transport.go +++ b/pkg/security/securebus/transport.go @@ -14,7 +14,7 @@ import ( "context" "fmt" - "github.com/sipeed/picoclaw/pkg/itr" + "github.com/ZanzyTHEbar/dragonscale/pkg/itr" ) // Transport abstracts the communication channel between the agent loop (caller) diff --git a/pkg/session/manager.go b/pkg/session/manager.go index 57ab418f2..5fc16ebdc 100644 --- a/pkg/session/manager.go +++ b/pkg/session/manager.go @@ -11,11 +11,11 @@ import ( jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" - "github.com/sipeed/picoclaw/pkg/cache" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/logger" - "github.com/sipeed/picoclaw/pkg/memory" - "github.com/sipeed/picoclaw/pkg/messages" + "github.com/ZanzyTHEbar/dragonscale/pkg/cache" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/messages" ) type Session struct { @@ -100,34 +100,142 @@ func (sm *SessionManager) loadSessionsFromDelegate() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - items, err := sm.delegate.ListRecallItems(ctx, sm.agentID, "", 500, 0) - if err != nil { - logger.WarnCF("session", "Failed to load sessions from delegate", - map[string]interface{}{"error": err.Error()}) - return + const pageSize = 500 + offset := 0 + sessionKeys := make(map[string]struct{}) + for { + items, err := sm.delegate.ListRecallItems(ctx, sm.agentID, "", pageSize, offset) + if err != nil { + logger.WarnCF("session", "Failed to load sessions from delegate", + map[string]interface{}{"error": err.Error()}) + return + } + if len(items) == 0 { + break + } + for _, item := range items { + if !strings.Contains(item.Tags, "session-message") { + continue + } + sessionKeys[item.SessionKey] = struct{}{} + } + if len(items) < pageSize { + break + } + offset += len(items) } - for _, item := range items { - if !strings.Contains(item.Tags, "session-message") { - continue + type sessionLister interface { + ListSessionMessages(ctx context.Context, agentID, sessionKey, role string, limit int) ([]*memory.RecallItem, error) + } + lister, hasSessionLister := sm.delegate.(sessionLister) + sessionsScanned := 0 + pointersUpdated := 0 + + for sessionKey := range sessionKeys { + sessionsScanned++ + session := &Session{ + Key: sessionKey, + Messages: []messages.Message{}, + Created: time.Now(), + Updated: time.Time{}, } - session := sm.sessions[item.SessionKey] - if session == nil { - session = &Session{ - Key: item.SessionKey, - Messages: []messages.Message{}, - Created: item.CreatedAt, - Updated: item.CreatedAt, + sm.sessions[sessionKey] = session + + var chronItems []*memory.RecallItem + + if hasSessionLister { + count, _ := sm.delegate.CountRecallItems(ctx, sm.agentID, sessionKey) + limit := count + 32 + if limit < 32 { + limit = 32 + } + rows, err := lister.ListSessionMessages(ctx, sm.agentID, sessionKey, "", limit) + if err == nil { + chronItems = rows + for _, item := range rows { + session.Messages = append(session.Messages, messages.Message{ + Role: item.Role, + Content: item.Content, + }) + if item.CreatedAt.Before(session.Created) { + session.Created = item.CreatedAt + } + if item.CreatedAt.After(session.Updated) { + session.Updated = item.CreatedAt + } + } } - sm.sessions[item.SessionKey] = session } - session.Messages = append(session.Messages, messages.Message{ - Role: item.Role, - Content: item.Content, - }) - if item.CreatedAt.After(session.Updated) { - session.Updated = item.CreatedAt + + if len(session.Messages) == 0 { + // Fallback when delegate doesn't expose ListSessionMessages. + offset := 0 + descItems := make([]*memory.RecallItem, 0, pageSize) + for { + batch, err := sm.delegate.ListRecallItems(ctx, sm.agentID, sessionKey, pageSize, offset) + if err != nil { + logger.WarnCF("session", "Failed fallback session restore page", + map[string]interface{}{"session": sessionKey, "offset": offset, "error": err.Error()}) + break + } + if len(batch) == 0 { + break + } + for _, item := range batch { + if !strings.Contains(item.Tags, "session-message") { + continue + } + descItems = append(descItems, item) + } + if len(batch) < pageSize { + break + } + offset += len(batch) + } + for i := len(descItems) - 1; i >= 0; i-- { + item := descItems[i] + if !strings.Contains(item.Tags, "session-message") { + continue + } + chronItems = append(chronItems, item) + session.Messages = append(session.Messages, messages.Message{ + Role: item.Role, + Content: item.Content, + }) + if item.CreatedAt.Before(session.Created) { + session.Created = item.CreatedAt + } + if item.CreatedAt.After(session.Updated) { + session.Updated = item.CreatedAt + } + } } + + // Integrity validation and projection pointer persistence for deterministic resume. + restoredPtr := projectFromItems(chronItems) + if restoredPtr != nil { + priorPtr, _ := sm.loadProjectionPointer(ctx, sessionKey) + sm.validateAndLogRestore(sessionKey, priorPtr, restoredPtr) + if !projectionPointerEqual(priorPtr, restoredPtr) { + if err := sm.persistProjectionPointer(ctx, sessionKey, restoredPtr); err != nil { + logger.WarnCF("session", "Failed to persist projection pointer", + map[string]interface{}{"session": sessionKey, "error": err.Error()}) + } else { + pointersUpdated++ + } + } + } + } + + if err := sm.persistProjectionBackfillStatus(ctx, ProjectionBackfillStatus{ + Version: 1, + SessionsScanned: sessionsScanned, + PointersUpdated: pointersUpdated, + CompletedAt: time.Now().UTC(), + }); err != nil { + logger.WarnCF("session", "Failed to persist projection backfill status", + map[string]interface{}{"error": err.Error()}) } } @@ -245,19 +353,6 @@ func (sm *SessionManager) AddFullMessage(sessionKey string, msg messages.Message sm.persistMessageToDelegate(sessionKey, msg) } - // Hard cap: prevent unbounded growth if summarization keeps failing. - // Keep last 50 messages when we exceed 200. - const hardCap = 200 - const keepOnOverflow = 50 - if len(session.Messages) > hardCap { - logger.InfoCF("session", "Session exceeded hard cap, force-truncating", - map[string]interface{}{ - "session": sessionKey, - "messages": len(session.Messages), - "keep": keepOnOverflow, - }) - session.Messages = session.Messages[len(session.Messages)-keepOnOverflow:] - } } func (sm *SessionManager) persistMessageToDelegate(sessionKey string, msg messages.Message) { @@ -282,6 +377,13 @@ func (sm *SessionManager) persistMessageToDelegate(sessionKey string, msg messag if err := sm.delegate.InsertRecallItem(ctx, item); err != nil { logger.WarnCF("session", "Failed to persist message to delegate", map[string]interface{}{"session": sessionKey, "error": err.Error()}) + return + } + priorPtr, _ := sm.loadProjectionPointer(ctx, sessionKey) + newPtr := advancePointer(priorPtr, item.ID, now) + if err := sm.persistProjectionPointer(ctx, sessionKey, newPtr); err != nil { + logger.WarnCF("session", "Failed to persist projection pointer after append", + map[string]interface{}{"session": sessionKey, "error": err.Error()}) } } diff --git a/pkg/session/manager_test.go b/pkg/session/manager_test.go index d5c3e5c09..b65cd3c56 100644 --- a/pkg/session/manager_test.go +++ b/pkg/session/manager_test.go @@ -2,13 +2,18 @@ package session import ( "context" + "fmt" "os" "path/filepath" "testing" "time" - "github.com/sipeed/picoclaw/pkg/memory/delegate" - "github.com/sipeed/picoclaw/pkg/messages" + jsonv2 "github.com/go-json-experiment/json" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate" + "github.com/ZanzyTHEbar/dragonscale/pkg/messages" ) func TestSanitizeFilename(t *testing.T) { @@ -145,22 +150,19 @@ func TestTruncateHistory_NoToolCalls(t *testing.T) { } } -func TestAddFullMessage_HardCap(t *testing.T) { +func TestAddFullMessage_NoHardCapTruncation(t *testing.T) { sm := NewSessionManager("") key := "test-hard-cap" - // Add 201 messages to trigger the hard cap + // Add many messages and verify startup continuity is not lossy. for i := 0; i < 201; i++ { sm.AddFullMessage(key, messages.Message{Role: "user", Content: "msg"}) } history := sm.GetHistory(key) - if len(history) > 200 { - t.Errorf("expected <= 200 messages after hard cap, got %d", len(history)) - } - if len(history) != 50 { - t.Errorf("expected 50 messages after hard cap truncation, got %d", len(history)) + if len(history) != 201 { + t.Errorf("expected 201 messages without hard-cap truncation, got %d", len(history)) } } @@ -252,3 +254,153 @@ func TestSessionManager_DelegateSaveIsNoop(t *testing.T) { } } } + +func TestSessionManager_DelegateBootstrapPaginationAndOrder(t *testing.T) { + del, err := delegate.NewLibSQLInMemory() + if err != nil { + t.Fatalf("NewLibSQLInMemory: %v", err) + } + if err := del.Init(context.Background()); err != nil { + t.Fatalf("Init: %v", err) + } + defer del.Close() + + const ( + sessionKey = "bootstrap-order" + totalMsgs = 1200 + ) + + writer := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + for i := 0; i < totalMsgs; i++ { + writer.AddMessage(sessionKey, "user", fmt.Sprintf("m-%04d", i)) + } + + // New manager instance exercises delegate bootstrap restore path. + reader := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + history := reader.GetHistory(sessionKey) + + if len(history) != totalMsgs { + t.Fatalf("expected %d restored messages, got %d", totalMsgs, len(history)) + } + if history[0].Content != "m-0000" { + t.Fatalf("expected oldest message first, got %q", history[0].Content) + } + if history[len(history)-1].Content != "m-1199" { + t.Fatalf("expected newest message last, got %q", history[len(history)-1].Content) + } +} + +func TestSessionManager_ProjectionPointerPersistedAndRestored(t *testing.T) { + del, err := delegate.NewLibSQLInMemory() + require.NoError(t, err) + require.NoError(t, del.Init(context.Background())) + defer del.Close() + + sessionKey := "ptr-persist" + sm := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + sm.AddMessage(sessionKey, "user", "first") + sm.AddMessage(sessionKey, "assistant", "second") + sm.AddMessage(sessionKey, "user", "third") + + // Read persisted pointer from KV + raw, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey)) + require.NoError(t, err) + require.NotEmpty(t, raw) + var ptr ProjectionPointer + require.NoError(t, jsonv2.Unmarshal([]byte(raw), &ptr)) + assert.Equal(t, 3, ptr.Count) + assert.False(t, ptr.FirstMessageID.IsZero()) + assert.False(t, ptr.LastMessageID.IsZero()) + assert.False(t, ptr.FirstCreatedAt.IsZero()) + assert.False(t, ptr.LastCreatedAt.IsZero()) + + // New manager restores; pointer is re-persisted (same values) + sm2 := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + history := sm2.GetHistory(sessionKey) + require.Len(t, history, 3) + raw2, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey)) + require.NoError(t, err) + require.NotEmpty(t, raw2) + var ptr2 ProjectionPointer + require.NoError(t, jsonv2.Unmarshal([]byte(raw2), &ptr2)) + assert.Equal(t, ptr.Count, ptr2.Count) + assert.Equal(t, ptr.FirstMessageID, ptr2.FirstMessageID) + assert.Equal(t, ptr.LastMessageID, ptr2.LastMessageID) +} + +func TestSessionManager_ProjectionPointerUpdatedOnAppend(t *testing.T) { + del, err := delegate.NewLibSQLInMemory() + require.NoError(t, err) + require.NoError(t, del.Init(context.Background())) + defer del.Close() + + sessionKey := "ptr-append" + sm := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + + for i := 0; i < 4; i++ { + sm.AddMessage(sessionKey, "user", fmt.Sprintf("msg-%d", i)) + raw, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey)) + require.NoError(t, err) + require.NotEmpty(t, raw) + var ptr ProjectionPointer + require.NoError(t, jsonv2.Unmarshal([]byte(raw), &ptr)) + assert.Equal(t, i+1, ptr.Count) + } +} + +func TestSessionManager_IntegrityMismatchRestoreStillSucceeds(t *testing.T) { + del, err := delegate.NewLibSQLInMemory() + require.NoError(t, err) + require.NoError(t, del.Init(context.Background())) + defer del.Close() + + sessionKey := "integrity-mismatch" + sm := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + sm.AddMessage(sessionKey, "user", "a") + sm.AddMessage(sessionKey, "assistant", "b") + + // Corrupt the stored pointer to simulate prior state mismatch + corrupt := ProjectionPointer{Count: 0} + data, _ := jsonv2.Marshal(corrupt) + require.NoError(t, del.UpsertKV(context.Background(), "test-agent", projectionPointerKey(sessionKey), string(data))) + + // New manager restores; should succeed (lossless) despite mismatch + sm2 := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + history := sm2.GetHistory(sessionKey) + require.Len(t, history, 2) + assert.Equal(t, "a", history[0].Content) + assert.Equal(t, "b", history[1].Content) + + // Pointer should now reflect restored state + raw, err := del.GetKV(context.Background(), "test-agent", projectionPointerKey(sessionKey)) + require.NoError(t, err) + require.NotEmpty(t, raw) + var ptr ProjectionPointer + require.NoError(t, jsonv2.Unmarshal([]byte(raw), &ptr)) + assert.Equal(t, 2, ptr.Count) +} + +func TestSessionManager_ProjectionBackfillStatusPersistedOnBootstrap(t *testing.T) { + del, err := delegate.NewLibSQLInMemory() + require.NoError(t, err) + require.NoError(t, del.Init(context.Background())) + defer del.Close() + + writer := NewSessionManager("", WithSessionDelegate(del, "test-agent")) + writer.AddMessage("session-a", "user", "hello") + writer.AddMessage("session-a", "assistant", "hi") + writer.AddMessage("session-b", "user", "task") + + _ = NewSessionManager("", WithSessionDelegate(del, "test-agent")) + + raw, err := del.GetKV(context.Background(), "test-agent", projectionBackfillStatusKey()) + require.NoError(t, err) + require.NotEmpty(t, raw) + + var status ProjectionBackfillStatus + require.NoError(t, jsonv2.Unmarshal([]byte(raw), &status)) + assert.Equal(t, 1, status.Version) + assert.GreaterOrEqual(t, status.SessionsScanned, 2) + assert.GreaterOrEqual(t, status.PointersUpdated, 0) + assert.WithinDuration(t, time.Now().UTC(), status.CompletedAt, 5*time.Second) +} diff --git a/pkg/skills/installer.go b/pkg/skills/installer.go index e442c5971..1c55054eb 100644 --- a/pkg/skills/installer.go +++ b/pkg/skills/installer.go @@ -96,7 +96,7 @@ func (si *SkillInstaller) Uninstall(skillName string) error { } func (si *SkillInstaller) ListAvailableSkills(ctx context.Context) ([]AvailableSkill, error) { - url := "https://raw.githubusercontent.com/sipeed/picoclaw-skills/main/skills.json" + url := "https://raw.githubusercontent.com/sipeed/dragonscale-skills/main/skills.json" client := &http.Client{Timeout: 15 * time.Second} req, err := http.NewRequestWithContext(ctx, "GET", url, nil) diff --git a/pkg/skills/loader.go b/pkg/skills/loader.go index 05335feb7..82add1982 100644 --- a/pkg/skills/loader.go +++ b/pkg/skills/loader.go @@ -60,13 +60,13 @@ func (info SkillInfo) validate() error { type SkillsLoader struct { primarySkills string // primary skills directory (XDG data dir or workspace/skills) - globalSkills string // user-level override skills (~/.config/picoclaw/skills) + globalSkills string // user-level override skills (~/.config/dragonscale/skills) builtinSkills string // built-in skills (bundled with binary) } // NewSkillsLoader creates a loader that searches for skills in three directories // with priority: primary > global > builtin. The primary directory is typically -// $XDG_DATA_HOME/picoclaw/skills; the global directory allows user overrides; +// $XDG_DATA_HOME/dragonscale/skills; the global directory allows user overrides; // and the builtin directory ships with the binary. func NewSkillsLoader(primarySkillsDir string, globalSkills string, builtinSkills string) *SkillsLoader { return &SkillsLoader{ @@ -108,7 +108,7 @@ func (sl *SkillsLoader) ListSkills() []SkillInfo { } } - // 全局 skills (~/.picoclaw/skills) - 被 workspace skills 覆盖 + // 全局 skills (~/.dragonscale/skills) - 被 workspace skills 覆盖 if sl.globalSkills != "" { if dirs, err := os.ReadDir(sl.globalSkills); err == nil { for _, dir := range dirs { @@ -202,7 +202,7 @@ func (sl *SkillsLoader) LoadSkill(name string) (string, bool) { } } - // 2. 其次从全局 skills 加载 (~/.picoclaw/skills) + // 2. 其次从全局 skills 加载 (~/.dragonscale/skills) if sl.globalSkills != "" { skillFile := filepath.Join(sl.globalSkills, name, "SKILL.md") if content, err := os.ReadFile(skillFile); err == nil { diff --git a/pkg/state/state.go b/pkg/state/state.go index f2db056d5..8e0733a48 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -12,10 +12,10 @@ import ( jsonv2 "github.com/go-json-experiment/json" "github.com/go-json-experiment/json/jsontext" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" ) -const kvAgentID = "picoclaw" +const kvAgentID = "dragonscale" // State represents the persistent state for a workspace. // It includes information about the last active channel/chat. diff --git a/pkg/sync/identity.go b/pkg/sync/identity.go index e4d711152..7d89ab230 100644 --- a/pkg/sync/identity.go +++ b/pkg/sync/identity.go @@ -12,9 +12,9 @@ import ( "sync/atomic" "time" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/fsnotify/fsnotify" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory" ) // IdentityFiles are the canonical set of bootstrap identity documents. @@ -56,7 +56,7 @@ type IdentitySync struct { } // New creates a new IdentitySync. identityDir is the path to the directory -// containing the identity markdown files ($XDG_CONFIG_HOME/picoclaw/identity). +// containing the identity markdown files ($XDG_CONFIG_HOME/dragonscale/identity). func New(identityDir, agentID string, store DocumentStore) *IdentitySync { s := &IdentitySync{ identityDir: identityDir, @@ -70,7 +70,7 @@ func New(identityDir, agentID string, store DocumentStore) *IdentitySync { // SyncAll performs a full reconciliation: reads every identity file from disk, // compares its content hash against the stored hash in agent_kv, and upserts // any that differ. This is the startup path and catches all changes made while -// picoclaw was not running. +// dragonscale was not running. func (s *IdentitySync) SyncAll(ctx context.Context) error { for _, name := range IdentityFiles { if err := s.syncFile(ctx, name); err != nil { diff --git a/pkg/sync/identity_test.go b/pkg/sync/identity_test.go index 790a4f84f..fef25297e 100644 --- a/pkg/sync/identity_test.go +++ b/pkg/sync/identity_test.go @@ -8,7 +8,7 @@ import ( "testing" "time" - "github.com/sipeed/picoclaw/pkg/memory" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/tools/call.go b/pkg/tools/call.go index a8caf4cda..830d0a3ce 100644 --- a/pkg/tools/call.go +++ b/pkg/tools/call.go @@ -8,7 +8,7 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type ctxKeyResources struct{} diff --git a/pkg/tools/cron.go b/pkg/tools/cron.go index 21bee42ef..ba1cff970 100644 --- a/pkg/tools/cron.go +++ b/pkg/tools/cron.go @@ -6,9 +6,9 @@ import ( "sync" "time" - "github.com/sipeed/picoclaw/pkg/bus" - "github.com/sipeed/picoclaw/pkg/cron" - "github.com/sipeed/picoclaw/pkg/utils" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/cron" + "github.com/ZanzyTHEbar/dragonscale/pkg/utils" ) // JobExecutor is the interface for executing cron jobs through the agent diff --git a/pkg/tools/focus.go b/pkg/tools/focus.go index c50200200..4835e772f 100644 --- a/pkg/tools/focus.go +++ b/pkg/tools/focus.go @@ -8,9 +8,9 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/logger" - "github.com/sipeed/picoclaw/pkg/messages" - "github.com/sipeed/picoclaw/pkg/session" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/messages" + "github.com/ZanzyTHEbar/dragonscale/pkg/session" ) // KVStore is the minimal interface needed for focus persistence. @@ -23,7 +23,7 @@ type KVStore interface { const ( focusKVPrefix = "focus:" knowledgeKVPrefix = "knowledge:" - focusAgentID = "picoclaw" + focusAgentID = "dragonscale" ) // FocusState tracks an active focus investigation. diff --git a/pkg/tools/focus_test.go b/pkg/tools/focus_test.go index 1cd7f9f93..7ed83cfec 100644 --- a/pkg/tools/focus_test.go +++ b/pkg/tools/focus_test.go @@ -6,8 +6,8 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/messages" - "github.com/sipeed/picoclaw/pkg/session" + "github.com/ZanzyTHEbar/dragonscale/pkg/messages" + "github.com/ZanzyTHEbar/dragonscale/pkg/session" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/tools/registry.go b/pkg/tools/registry.go index a19eda8d7..e08408c77 100644 --- a/pkg/tools/registry.go +++ b/pkg/tools/registry.go @@ -6,7 +6,7 @@ import ( "sync" "time" - "github.com/sipeed/picoclaw/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" ) type ToolRegistry struct { diff --git a/pkg/tools/retrieval.go b/pkg/tools/retrieval.go index 6ae057ef6..c37ff9db1 100644 --- a/pkg/tools/retrieval.go +++ b/pkg/tools/retrieval.go @@ -5,8 +5,8 @@ import ( "fmt" "strings" - "github.com/sipeed/picoclaw/pkg/memory" - memstore "github.com/sipeed/picoclaw/pkg/memory/store" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory" + memstore "github.com/ZanzyTHEbar/dragonscale/pkg/memory/store" ) // KeywordSearchTool performs FTS5+BM25 keyword search across recall and archival memory. diff --git a/pkg/tools/search.go b/pkg/tools/search.go index 7cac93a60..6a69f3d9c 100644 --- a/pkg/tools/search.go +++ b/pkg/tools/search.go @@ -6,8 +6,8 @@ import ( "sort" "strings" + "github.com/ZanzyTHEbar/dragonscale/pkg/skills" jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/skills" ) // ToolSearchTool implements unified discovery via fuzzy search over the registry diff --git a/pkg/tools/search_test.go b/pkg/tools/search_test.go index 61b750dc3..50b8f7eab 100644 --- a/pkg/tools/search_test.go +++ b/pkg/tools/search_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" + "github.com/ZanzyTHEbar/dragonscale/pkg/skills" jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/skills" ) func TestToolSearchTool_Name(t *testing.T) { diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 34eb51196..ef5ff16b1 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -14,8 +14,8 @@ import ( "syscall" "time" - "github.com/sipeed/picoclaw/pkg/logger" - "github.com/sipeed/picoclaw/pkg/security" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/security" ) var shellRedactor = security.NewRedactor() diff --git a/pkg/tools/skills.go b/pkg/tools/skills.go index 45c868b72..53f46a7e9 100644 --- a/pkg/tools/skills.go +++ b/pkg/tools/skills.go @@ -6,7 +6,7 @@ import ( "strings" "sync" - "github.com/sipeed/picoclaw/pkg/skills" + "github.com/ZanzyTHEbar/dragonscale/pkg/skills" ) // SkillSearchTool performs fuzzy search across skill names, descriptions, tags, and domains. diff --git a/pkg/tools/skills_test.go b/pkg/tools/skills_test.go index f76589b24..a36eb11de 100644 --- a/pkg/tools/skills_test.go +++ b/pkg/tools/skills_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/sipeed/picoclaw/pkg/skills" + "github.com/ZanzyTHEbar/dragonscale/pkg/skills" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) diff --git a/pkg/tools/spawn.go b/pkg/tools/spawn.go index 42dd36a33..68bdd139b 100644 --- a/pkg/tools/spawn.go +++ b/pkg/tools/spawn.go @@ -45,6 +45,14 @@ func (t *SpawnTool) Parameters() map[string]interface{} { "type": "string", "description": "Optional short label for the task (for display)", }, + "delegated_scope": map[string]interface{}{ + "type": "string", + "description": "What part of parent work is delegated. Required for nested delegation.", + }, + "kept_work": map[string]interface{}{ + "type": "string", + "description": "What work remains with delegator. Required for nested delegation.", + }, }, "required": []string{"task"}, } @@ -62,13 +70,15 @@ func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *T } label, _ := args["label"].(string) + delegatedScope, _ := args["delegated_scope"].(string) + keptWork, _ := args["kept_work"].(string) if t.manager == nil { return ErrorResult("Subagent manager not configured") } // Pass callback to manager for async completion notification - result, err := t.manager.Spawn(ctx, task, label, t.originChannel, t.originChatID, t.callback) + result, err := t.manager.Spawn(ctx, task, label, delegatedScope, keptWork, t.originChannel, t.originChatID, t.callback) if err != nil { return ErrorResult(fmt.Sprintf("failed to spawn subagent: %v", err)) } diff --git a/pkg/tools/toolloop.go b/pkg/tools/toolloop.go index 83104ff4f..0376d8c39 100644 --- a/pkg/tools/toolloop.go +++ b/pkg/tools/toolloop.go @@ -1,15 +1,17 @@ -// PicoClaw - Ultra-lightweight personal AI agent +// DragonScale - Ultra-lightweight personal AI agent // License: MIT // -// Copyright (c) 2026 PicoClaw contributors +// Copyright (c) 2026 DragonScale contributors package tools import ( "context" + "errors" + "fmt" fantasy "charm.land/fantasy" - "github.com/sipeed/picoclaw/pkg/bus" + "github.com/ZanzyTHEbar/dragonscale/pkg/bus" ) // ToolLoopConfig configures the tool execution loop. @@ -27,9 +29,14 @@ type ToolLoopResult struct { Iterations int } +// ErrRunLoopNotConfigured indicates that the unified loop runner was not +// injected into tools runtime wiring. +var ErrRunLoopNotConfigured = errors.New("subagent run loop is not configured") + // RunToolLoop is the package-level fallback used when no injected loop runner -// is available. It panics because the concrete implementation lives in -// pkg/agent and must be injected via SubagentManager.SetRunLoop. +// is available. It returns an explicit contract error because the concrete +// implementation lives in pkg/agent and must be injected via +// SubagentManager.SetRunLoop. func RunToolLoop(_ context.Context, _ ToolLoopConfig, _, _, _, _ string) (*ToolLoopResult, error) { - panic("tools.RunToolLoop called without injected loop runner; wire agent.MakeRunLoopFunc via SubagentManager.SetRunLoop") + return nil, fmt.Errorf("%w: wire agent.MakeUnifiedRunLoopFunc via SubagentManager.SetRunLoop", ErrRunLoopNotConfigured) } diff --git a/pkg/utils/media.go b/pkg/utils/media.go index 2b184f2ec..84816513a 100644 --- a/pkg/utils/media.go +++ b/pkg/utils/media.go @@ -8,8 +8,8 @@ import ( "strings" "time" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" "github.com/google/uuid" - "github.com/sipeed/picoclaw/pkg/logger" ) // IsAudioFile checks if a file is an audio file based on its filename extension and content type. @@ -64,7 +64,7 @@ func DownloadFile(url, filename string, opts DownloadOptions) string { opts.LoggerPrefix = "utils" } - mediaDir := filepath.Join(os.TempDir(), "picoclaw_media") + mediaDir := filepath.Join(os.TempDir(), "dragonscale_media") if err := os.MkdirAll(mediaDir, 0700); err != nil { logger.ErrorCF(opts.LoggerPrefix, "Failed to create media directory", map[string]interface{}{ "error": err.Error(), diff --git a/pkg/voice/transcriber.go b/pkg/voice/transcriber.go index 4963654c0..c8e6e1f2d 100644 --- a/pkg/voice/transcriber.go +++ b/pkg/voice/transcriber.go @@ -13,8 +13,8 @@ import ( jsonv2 "github.com/go-json-experiment/json" - "github.com/sipeed/picoclaw/pkg/logger" - "github.com/sipeed/picoclaw/pkg/utils" + "github.com/ZanzyTHEbar/dragonscale/pkg/logger" + "github.com/ZanzyTHEbar/dragonscale/pkg/utils" ) type GroqTranscriber struct { diff --git a/pkg/worker/worker.go b/pkg/worker/worker.go index bf6156265..94b2cbd72 100644 --- a/pkg/worker/worker.go +++ b/pkg/worker/worker.go @@ -7,9 +7,9 @@ import ( "os" "time" + "github.com/ZanzyTHEbar/dragonscale/pkg/dserrors" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" "github.com/rs/zerolog" - "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/pcerrors" ) // HandlerFunc processes a single claimed job. Returning nil marks it succeeded. @@ -71,7 +71,7 @@ func (o *Options) logger() zerolog.Logger { // RunOnce claims and executes a single job if available. func RunOnce(ctx context.Context, q *sqlc.Queries, opts *Options) error { if q == nil { - return pcerrors.New(pcerrors.CodeFailedPrecondition, "queries is nil") + return dserrors.New(dserrors.CodeFailedPrecondition, "queries is nil") } l := opts.logger() diff --git a/pkg/worker/worker_test.go b/pkg/worker/worker_test.go index 22d6d4f3b..96c66a240 100644 --- a/pkg/worker/worker_test.go +++ b/pkg/worker/worker_test.go @@ -10,10 +10,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/sipeed/picoclaw/pkg/ids" - "github.com/sipeed/picoclaw/pkg/memory/delegate" - "github.com/sipeed/picoclaw/pkg/memory/sqlc" - "github.com/sipeed/picoclaw/pkg/worker" + "github.com/ZanzyTHEbar/dragonscale/pkg/ids" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/delegate" + "github.com/ZanzyTHEbar/dragonscale/pkg/memory/sqlc" + "github.com/ZanzyTHEbar/dragonscale/pkg/worker" ) func newTestDB(t *testing.T) *sqlc.Queries {