mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 02:36:01 -10:00
docs: add a packaged read-only subagent exercise
This commit is contained in:
1 parent
88ca5ae82b
commit
e309ee2200
8 files changed
+449
No files matched your search
@@ -16,6 +16,8 @@ The [multi-file skill example](../examples/release-note-kit/README.md) shows
|
||||
how a plugin keeps a required reference file with its skill.
|
||||
For an issue-to-PR-draft exercise with a deliberately failing local fixture,
|
||||
see the [repository workflow example](../examples/issue-fix-kit/README.md).
|
||||
For a packaged read-only subagent, local inputs and lifecycle verification,
|
||||
see the [repository orientation example](../examples/repo-map-kit/README.md).
|
||||
For a frontend task with a brief, sample data, and browser acceptance steps,
|
||||
see the [local interface example](../examples/frontend-page-kit/README.md).
|
||||
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
owner: "@esengine"
|
||||
backup: "@SivanCola"
|
||||
status: active
|
||||
reviewed: 2026-10-02
|
||||
---
|
||||
|
||||
# Packaged repository orientation profile
|
||||
|
||||
This native v2 package contributes one manually invoked subagent profile,
|
||||
`map`. It reads selected local files in an isolated child conversation and
|
||||
returns an answer to the caller. It adds no hook, MCP server, extension runtime,
|
||||
install script or production dependency.
|
||||
|
||||
1. The flat `agents/map.md` declares a description, a read-only tool ceiling
|
||||
and the method. Reasonix loads declared agent roots as manual subagent
|
||||
profiles; the qualified invocation is `/repo-map-kit:agent:map`.
|
||||
2. The file omits a model override, so the normal host model selection applies.
|
||||
Use a configured model that supports tool calls. Live invocation uses that
|
||||
provider and its normal credentials and usage charges.
|
||||
|
||||
## Install and discover
|
||||
|
||||
From the Reasonix repository root:
|
||||
|
||||
```sh
|
||||
reasonix plugin install ./examples/repo-map-kit --dry-run
|
||||
reasonix plugin install ./examples/repo-map-kit --yes
|
||||
reasonix plugin doctor repo-map-kit
|
||||
reasonix plugin show repo-map-kit
|
||||
reasonix subagent list
|
||||
```
|
||||
|
||||
1. Preview must leave the package uninstalled. Installation copies the whole
|
||||
package into the configured Reasonix home.
|
||||
2. `subagent list` must include `repo-map-kit:agent:map` as manual and read-only.
|
||||
`doctor` checks package paths; it does not verify an answer's quality.
|
||||
3. Start a new session after changing package installation or enablement.
|
||||
Use the qualified invocation below in an interactive session. Packaged
|
||||
profiles are edited in their source package, rather than rewritten by the
|
||||
project/global profile editor.
|
||||
|
||||
## Run the bounded exercise
|
||||
|
||||
The fixture is source to inspect, not a command to execute. It needs no compiler
|
||||
for this exercise. From the repository root, invoke the read-only runner:
|
||||
|
||||
```sh
|
||||
reasonix subagent try repo-map-kit:agent:map --dir ./examples/repo-map-kit/fixture \
|
||||
"Explain LoadLines in loader.go, compare it with NOTES.md, and report what you did not verify."
|
||||
```
|
||||
|
||||
`run` also honors this profile's `read-only: true`:
|
||||
|
||||
```sh
|
||||
reasonix subagent run repo-map-kit:agent:map --dir ./examples/repo-map-kit/fixture \
|
||||
"Explain LoadLines in loader.go, compare it with NOTES.md, and report what you did not verify."
|
||||
```
|
||||
|
||||
In an interactive session rooted at the fixture, use:
|
||||
|
||||
```text
|
||||
/repo-map-kit:agent:map Explain LoadLines in loader.go, compare it with NOTES.md, and report what you did not verify.
|
||||
```
|
||||
|
||||
1. Check that the answer explains newline splitting, whitespace trimming,
|
||||
blank-line removal and order preservation, citing `loader.go`.
|
||||
2. `NOTES.md` deliberately claims blank lines and whitespace are preserved.
|
||||
A successful answer cites that note and the contradictory source.
|
||||
3. The answer must list inspected files, say tests were not run and leave both
|
||||
files unchanged. This is repository orientation, not a review verdict.
|
||||
4. Run a failure case asking for `missing.go`, which is absent. The answer must
|
||||
identify the missing input and avoid inventing its implementation.
|
||||
5. The profile's tool ceiling excludes shell, writer and recursive delegation
|
||||
tools. The host also supplies its `complete_subtask` reporting protocol.
|
||||
Normal host access rules still govern readable files; naming a task directory
|
||||
is guidance, not a new per-profile filesystem access boundary.
|
||||
|
||||
## Edit, disable and remove
|
||||
|
||||
Edit the source profile, preview replacement, then apply it and start a new
|
||||
session. Editing the source does not change an installed copy.
|
||||
|
||||
```sh
|
||||
reasonix plugin install ./examples/repo-map-kit --replace --dry-run
|
||||
reasonix plugin install ./examples/repo-map-kit --replace --yes
|
||||
reasonix plugin disable repo-map-kit
|
||||
reasonix plugin enable repo-map-kit
|
||||
reasonix plugin remove repo-map-kit --yes
|
||||
```
|
||||
|
||||
1. Disable preserves the copy but excludes the profile from newly built
|
||||
sessions. Enable restores it. Removal deletes the managed copy and registration.
|
||||
2. For link-based authoring, remove the copy, preview with `--link --dry-run`,
|
||||
then apply with `--link --yes`. Keep the linked source available; removing
|
||||
the link registration keeps its external source directory.
|
||||
|
||||
## Verify the host path
|
||||
|
||||
```sh
|
||||
go test ./internal/assembly/boot/ -run '^TestEffectRepoMapExampleLifecycle$' -count=1
|
||||
```
|
||||
|
||||
1. The test copies this package through the real approved-plan installer,
|
||||
removes the temporary source and builds real controllers. It checks
|
||||
discovery, child prompt/task isolation, the read-only tool schema and actual
|
||||
fixture reads reaching the provider across install, disable, enable and removal.
|
||||
2. The recording provider is scripted; this proves the host path, not that a
|
||||
live model correctly explains the exercise. The manual acceptance steps
|
||||
above are still needed for the selected model. No public market submission
|
||||
is claimed.
|
||||
|
||||
| Observation | Check |
|
||||
| --- | --- |
|
||||
| No profile in a fresh session | Confirm package enablement and `contributes.agents`; run `subagent list` in the target workspace. |
|
||||
| A profile cannot be selected | Use the exact qualified name from `subagent list`, `/repo-map-kit:agent:map`. |
|
||||
| An edit has no effect | Replace the copy or use a link, then build a new session. |
|
||||
| The answer claims a build or edit | Check tool results; this profile has only reader tools. |
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
owner: "@esengine"
|
||||
backup: "@SivanCola"
|
||||
status: active
|
||||
reviewed: 2026-10-02
|
||||
name: map
|
||||
description: Trace a bounded repository question to local files and report gaps
|
||||
read-only: true
|
||||
tools: [Read, Grep, Glob, LS]
|
||||
---
|
||||
|
||||
Trace the task's repository question through the selected local files.
|
||||
|
||||
1. Establish the question and the files or directory named in the task. If an
|
||||
essential input is missing or unreadable, identify it and limit the answer
|
||||
to the evidence available. Do not invent a replacement input.
|
||||
2. Read the selected definitions and callers, using targeted searches as needed.
|
||||
For the fixture, inspect `loader.go` and `NOTES.md` directly. Do not survey
|
||||
unrelated directories.
|
||||
3. Explain the path from input to output. Cite file paths and line numbers for
|
||||
each behavior claim. If documentation disagrees with source, cite both and
|
||||
describe the disagreement without changing either file.
|
||||
4. Return three short sections: Findings, Sources inspected, and Not verified.
|
||||
Name the inspected scope. Report missing files, untraced paths and checks
|
||||
not run; an empty search is not proof that a behavior is absent everywhere.
|
||||
5. Keep the answer in the caller's requested language. Read-only tools cannot
|
||||
apply a fix or run a build in this profile. Do not claim test results,
|
||||
permission decisions, a completed review or an implemented change.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
owner: "@esengine"
|
||||
backup: "@SivanCola"
|
||||
status: active
|
||||
reviewed: 2026-10-02
|
||||
---
|
||||
|
||||
# Loader notes
|
||||
|
||||
LoadLines preserves blank lines and leading or trailing whitespace.
|
||||
|
||||
This sentence is deliberately stale exercise material. Compare it with
|
||||
`loader.go`; report the disagreement and cite the relevant source lines.
|
||||
The fixture has no tests and provides no runtime verification evidence.
|
||||
@@ -0,0 +1,3 @@
|
||||
module example.invalid/repo-map-fixture
|
||||
|
||||
go 1.22
|
||||
@@ -0,0 +1,13 @@
|
||||
package fixture
|
||||
|
||||
import "strings"
|
||||
|
||||
func LoadLines(text string) []string {
|
||||
var lines []string
|
||||
for _, line := range strings.Split(text, "\n") {
|
||||
if line = strings.TrimSpace(line); line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"apiVersion": "reasonix.io/plugin/v2",
|
||||
"name": "repo-map-kit",
|
||||
"version": "1.0.0",
|
||||
"description": "A read-only repository orientation profile with a local exercise",
|
||||
"contributes": {
|
||||
"agents": ["agents"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
package boot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/frontmatter"
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/ext/installsource"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
"reasonix/internal/ext/skill"
|
||||
)
|
||||
|
||||
const repoMapPromptStart = "Trace the task's repository question through the selected local files."
|
||||
|
||||
type repoMapExampleProvider struct {
|
||||
effectRecordingProvider
|
||||
files []string
|
||||
}
|
||||
|
||||
func (*repoMapExampleProvider) Name() string { return "boot-repo-map-example" }
|
||||
|
||||
func (p *repoMapExampleProvider) Stream(_ context.Context, req provider.Request) (<-chan provider.Chunk, error) {
|
||||
p.mu.Lock()
|
||||
p.reqs = append(p.reqs, req)
|
||||
p.mu.Unlock()
|
||||
ch := make(chan provider.Chunk, len(p.files)+1)
|
||||
if !strings.Contains(systemMessage(req.Messages), repoMapPromptStart) {
|
||||
ch <- provider.Chunk{Type: provider.ChunkText, Text: "parent acknowledged"}
|
||||
} else {
|
||||
results := 0
|
||||
for _, message := range req.Messages {
|
||||
if message.Role == provider.RoleTool && strings.HasPrefix(message.ToolCallID, "fixture-") {
|
||||
results++
|
||||
}
|
||||
}
|
||||
if results == len(p.files) {
|
||||
ch <- provider.Chunk{Type: provider.ChunkText, Text: "fixture inspected"}
|
||||
} else {
|
||||
for i, file := range p.files {
|
||||
args, err := json.Marshal(map[string]string{"path": file})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ch <- provider.Chunk{Type: provider.ChunkToolCall, ToolCall: &provider.ToolCall{ID: fmt.Sprintf("fixture-%d", i), Name: "read_file", Arguments: string(args)}}
|
||||
}
|
||||
}
|
||||
}
|
||||
ch <- provider.Chunk{Type: provider.ChunkDone}
|
||||
close(ch)
|
||||
return ch, nil
|
||||
}
|
||||
|
||||
func TestEffectRepoMapExampleLifecycle(t *testing.T) {
|
||||
example, err := filepath.Abs(filepath.Join("..", "..", "..", "examples", "repo-map-kit"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := robustTempDir(t)
|
||||
if err := os.CopyFS(source, os.DirFS(example)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rawProfile, err := os.ReadFile(filepath.Join(source, "agents", "map.md"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, profileBody := frontmatter.SplitLegacy(string(rawProfile))
|
||||
profileBody = strings.TrimSpace(profileBody)
|
||||
home := isolateConfigHome(t)
|
||||
reasonixHome := filepath.Join(home, ".reasonix")
|
||||
t.Setenv("REASONIX_HOME", reasonixHome)
|
||||
workspace := robustTempDir(t)
|
||||
if err := os.CopyFS(workspace, os.DirFS(filepath.Join(source, "fixture"))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files := []string{"loader.go", "NOTES.md"}
|
||||
contents := make([]string, len(files))
|
||||
for i, file := range files {
|
||||
body, err := os.ReadFile(filepath.Join(workspace, file))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents[i] = string(body)
|
||||
}
|
||||
t.Chdir(workspace)
|
||||
writeFile(t, workspace, "reasonix.toml", `
|
||||
default_model = "test-model"
|
||||
[agent]
|
||||
system_prompt = "REPO MAP PARENT BASE"
|
||||
[environment]
|
||||
enabled = false
|
||||
[codegraph]
|
||||
enabled = false
|
||||
[[providers]]
|
||||
name = "test-model"
|
||||
kind = "boot-repo-map-example"
|
||||
model = "x"
|
||||
`)
|
||||
approveWorkspace(t, workspace)
|
||||
var rec *repoMapExampleProvider
|
||||
provider.Register("boot-repo-map-example", func(provider.Config) (provider.Provider, error) { return rec, nil })
|
||||
installer := installsource.NewTool(installsource.Options{ProjectRoot: workspace, HomeDir: home, RequireApprovedPlan: true})
|
||||
type result struct {
|
||||
OK bool `json:"ok"`
|
||||
Status string `json:"status"`
|
||||
Applied bool `json:"applied"`
|
||||
PlanID string `json:"planId"`
|
||||
}
|
||||
install := func(args map[string]any) result {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out, err := installer.Execute(t.Context(), raw)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got result
|
||||
if err := json.Unmarshal([]byte(out), &got); err != nil || !got.OK {
|
||||
t.Fatalf("install_source = %s, err=%v", out, err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
var prefix, schema string
|
||||
check := func(t *testing.T, present bool) {
|
||||
t.Helper()
|
||||
rec = &repoMapExampleProvider{files: files}
|
||||
ctrl, err := Build(t.Context(), Options{Sink: event.Discard, SessionDir: filepath.Join(robustTempDir(t), "sessions")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer ctrl.Close()
|
||||
const name = "repo-map-kit:agent:map"
|
||||
var profile skill.Skill
|
||||
found := false
|
||||
for _, sk := range ctrl.SlashSkills() {
|
||||
if sk.SlashName() == name {
|
||||
profile, found = sk, true
|
||||
}
|
||||
}
|
||||
if found != present {
|
||||
t.Fatalf("qualified profile found=%t, want %t", found, present)
|
||||
}
|
||||
if present && (profile.Plugin != "repo-map-kit" || profile.RunAs != skill.RunSubagent || profile.Invocation != "manual" || !profile.ReadOnly || profile.Model != "" || profile.Body != profileBody || !slices.Equal(profile.AllowedTools, []string{"read_file", "grep", "glob", "ls"})) {
|
||||
t.Fatalf("installed profile = %+v", profile)
|
||||
}
|
||||
if present && profile.Path != filepath.Join(pluginpkg.InstallRoot(reasonixHome, "repo-map-kit"), "agents", "map.md") {
|
||||
t.Fatalf("profile is not the managed copy: %s", profile.Path)
|
||||
}
|
||||
ctrl.EnsureSessionPath()
|
||||
const parentOnly = "PARENT-ONLY-CONTEXT: examine an unrelated question"
|
||||
if err := ctrl.Run(t.Context(), parentOnly); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
parent := rec.requests()[0]
|
||||
encoded, err := json.Marshal(parent.Tools)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if prefix == "" {
|
||||
prefix, schema = systemMessage(parent.Messages), string(encoded)
|
||||
} else if prefix != systemMessage(parent.Messages) || schema != string(encoded) {
|
||||
t.Fatal("package lifecycle changed the parent prefix or tool schema")
|
||||
}
|
||||
if strings.Contains(systemMessage(parent.Messages), profileBody) {
|
||||
t.Fatal("manual child profile entered the parent prefix")
|
||||
}
|
||||
const task = "Explain loader.go and compare NOTES.md; report what was not verified."
|
||||
answer, err := ctrl.RunSubagentProfile(t.Context(), name, task, false)
|
||||
if !present {
|
||||
if err == nil || len(rec.requests()) != 1 {
|
||||
t.Fatalf("absent profile invoked a provider: answer=%q, err=%v", answer, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil || !strings.HasSuffix(answer, "fixture inspected") {
|
||||
t.Fatalf("child answer = %q, err=%v", answer, err)
|
||||
}
|
||||
requests := rec.requests()
|
||||
if len(requests) != 3 {
|
||||
t.Fatalf("provider requests=%d, want parent + child read + child result", len(requests))
|
||||
}
|
||||
for _, req := range requests[1:] {
|
||||
if !strings.HasPrefix(systemMessage(req.Messages), profileBody) {
|
||||
t.Fatal("installed profile body did not reach the child")
|
||||
}
|
||||
if !slices.Equal(toolSchemaNames(req.Tools), []string{"complete_subtask", "glob", "grep", "ls", "read_file"}) {
|
||||
t.Fatalf("child tool ceiling = %v", toolSchemaNames(req.Tools))
|
||||
}
|
||||
var user string
|
||||
for _, message := range req.Messages {
|
||||
if strings.Contains(message.Content, parentOnly) {
|
||||
t.Fatal("child received unrelated parent conversation")
|
||||
}
|
||||
if message.Role == provider.RoleUser {
|
||||
user = message.Content
|
||||
}
|
||||
}
|
||||
if !strings.Contains(user, task) {
|
||||
t.Fatalf("child did not receive the explicit task: %s", user)
|
||||
}
|
||||
}
|
||||
for i, file := range files {
|
||||
var toolResult string
|
||||
for _, message := range requests[2].Messages {
|
||||
if message.Role == provider.RoleTool && message.ToolCallID == fmt.Sprintf("fixture-%d", i) {
|
||||
toolResult = message.Content
|
||||
}
|
||||
}
|
||||
for line := range strings.SplitSeq(contents[i], "\n") {
|
||||
if line = strings.TrimSpace(line); line != "" && !strings.Contains(toolResult, line) {
|
||||
t.Errorf("fixture %s line missing from provider tool result: %q", file, line)
|
||||
}
|
||||
}
|
||||
if got, err := os.ReadFile(filepath.Join(workspace, file)); err != nil || string(got) != contents[i] {
|
||||
t.Fatalf("fixture %s changed: %q, err=%v", file, got, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("absent", func(t *testing.T) { check(t, false) })
|
||||
args := map[string]any{"source": source, "kind": "plugin", "mode": "copy", "scope": "global"}
|
||||
preview := install(args)
|
||||
if preview.Applied || preview.Status != "planned" || preview.PlanID == "" {
|
||||
t.Fatalf("preview = %+v", preview)
|
||||
}
|
||||
t.Run("preview", func(t *testing.T) { check(t, false) })
|
||||
args["apply"], args["planId"] = true, preview.PlanID
|
||||
if applied := install(args); !applied.Applied || applied.Status != "done" {
|
||||
t.Fatalf("applied = %+v", applied)
|
||||
}
|
||||
if err := os.RemoveAll(source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("copied", func(t *testing.T) { check(t, true) })
|
||||
if err := pluginpkg.SetEnabled(reasonixHome, "repo-map-kit", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got, err := os.ReadFile(filepath.Join(pluginpkg.InstallRoot(reasonixHome, "repo-map-kit"), "agents", "map.md")); err != nil || string(got) != string(rawProfile) {
|
||||
t.Fatalf("disable changed the copied profile: %q, err=%v", got, err)
|
||||
}
|
||||
t.Run("disabled", func(t *testing.T) { check(t, false) })
|
||||
if err := pluginpkg.SetEnabled(reasonixHome, "repo-map-kit", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("reenabled", func(t *testing.T) { check(t, true) })
|
||||
if removed := install(map[string]any{"op": "uninstall", "name": "repo-map-kit", "scope": "global"}); !removed.Applied || removed.Status != "done" {
|
||||
t.Fatalf("removed = %+v", removed)
|
||||
}
|
||||
if _, err := os.Stat(pluginpkg.InstallRoot(reasonixHome, "repo-map-kit")); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("managed profile survives removal: %v", err)
|
||||
}
|
||||
t.Run("removed", func(t *testing.T) { check(t, false) })
|
||||
}
|
||||
Reference in new issue
Block a user