mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
feat(sdk): add installable offline provider example
This commit is contained in:
1 parent
6e1a0cccaa
commit
54c8e7ab68
6 files changed
+494
No files matched your search
@@ -0,0 +1,155 @@
|
||||
package boot
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"reasonix/internal/contract/event"
|
||||
"reasonix/internal/contract/provider"
|
||||
"reasonix/internal/ext/installsource"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
)
|
||||
|
||||
func buildSDKProviderPackage(t *testing.T) string {
|
||||
t.Helper()
|
||||
example, err := filepath.Abs(filepath.Join("..", "..", "..", "sdk", "go", "examples", "providerextension"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
source := robustTempDir(t)
|
||||
binary := filepath.Join(source, "bin", "echo-provider.exe")
|
||||
if err := os.MkdirAll(filepath.Dir(binary), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(t.Context(), time.Minute)
|
||||
defer cancel()
|
||||
cmd := exec.CommandContext(ctx, "go", "build", "-o", binary, ".")
|
||||
cmd.Dir = example
|
||||
cmd.Env = append(os.Environ(), "GOWORK=off", "GOTOOLCHAIN=local", "GOPROXY=off")
|
||||
if out, err := cmd.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build SDK provider example: %v\n%s", err, out)
|
||||
}
|
||||
manifest, err := os.ReadFile(filepath.Join(example, pluginpkg.NativeManifest))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(source, pluginpkg.NativeManifest), manifest, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return source
|
||||
}
|
||||
|
||||
func TestEffectInstalledSDKProviderCompletesFirstTurn(t *testing.T) {
|
||||
source := buildSDKProviderPackage(t)
|
||||
home := isolateConfigHome(t)
|
||||
reasonixHome := filepath.Join(home, ".reasonix")
|
||||
t.Setenv("REASONIX_HOME", reasonixHome)
|
||||
workspace := robustTempDir(t)
|
||||
t.Chdir(workspace)
|
||||
const ref = "plugin/echo-provider/offline/echo"
|
||||
writePluginDefaultFixture(t, workspace, ref)
|
||||
installer := installsource.NewTool(installsource.Options{ProjectRoot: workspace, HomeDir: home, RequireApprovedPlan: true})
|
||||
install := func(args map[string]any) string {
|
||||
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 result struct {
|
||||
OK bool `json:"ok"`
|
||||
Applied bool `json:"applied"`
|
||||
Status string `json:"status"`
|
||||
PlanID string `json:"planId"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &result); err != nil || !result.OK {
|
||||
t.Fatalf("install_source = %s, err=%v", out, err)
|
||||
}
|
||||
if args["apply"] == true || args["op"] == "uninstall" {
|
||||
if !result.Applied || result.Status != "done" {
|
||||
t.Fatalf("apply = %s", out)
|
||||
}
|
||||
} else if result.Applied || result.Status != "planned" || result.PlanID == "" {
|
||||
t.Fatalf("preview = %s", out)
|
||||
}
|
||||
return result.PlanID
|
||||
}
|
||||
runPhase := func(t *testing.T, enabled bool) {
|
||||
t.Helper()
|
||||
res, err := BuildRuntime(t.Context(), Options{Sink: event.Discard})
|
||||
if !enabled {
|
||||
if res != nil {
|
||||
res.Controller.Close()
|
||||
}
|
||||
if !errors.Is(err, ErrUnknownModel) {
|
||||
t.Fatalf("inactive plugin default_model: got %v, want ErrUnknownModel", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Controller.Close()
|
||||
if res.Controller.ModelRef() != ref || res.Extensions == nil || res.Extensions.Client("echo-provider") == nil {
|
||||
t.Fatalf("first boot did not select the installed SDK provider: %q", res.Controller.ModelRef())
|
||||
}
|
||||
client := res.Extensions.Client("echo-provider")
|
||||
defer func() {
|
||||
res.Controller.Close()
|
||||
waitForCond(t, "SDK provider process exit", 10*time.Second, client.Exited)
|
||||
if !res.Runtime.Closed() {
|
||||
t.Fatal("SDK provider runtime did not close")
|
||||
}
|
||||
}()
|
||||
for _, input := range []string{"SDK-FIRST-TURN 世界", "SDK-SECOND-TURN again"} {
|
||||
before := len(res.Controller.History())
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 30*time.Second)
|
||||
err := res.Controller.RunTurn(ctx, input)
|
||||
cancel()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var text strings.Builder
|
||||
for _, message := range res.Controller.History()[before:] {
|
||||
if len(message.ToolCalls) != 0 || message.Role == provider.RoleTool {
|
||||
t.Fatalf("offline provider produced a tool call or result: %+v", message)
|
||||
}
|
||||
if message.Role == provider.RoleAssistant {
|
||||
text.WriteString(message.Content)
|
||||
}
|
||||
}
|
||||
if !strings.HasPrefix(text.String(), "Offline echo: ") || !strings.Contains(text.String(), input) {
|
||||
t.Fatalf("assistant turn = %q, want the SDK echo of %q", text.String(), input)
|
||||
}
|
||||
}
|
||||
}
|
||||
t.Run("absent", func(t *testing.T) { runPhase(t, false) })
|
||||
args := map[string]any{"source": source, "kind": "plugin", "scope": "global", "mode": "copy"}
|
||||
args["planId"] = install(args)
|
||||
args["apply"] = true
|
||||
install(args)
|
||||
if err := os.RemoveAll(source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("installed", func(t *testing.T) { runPhase(t, true) })
|
||||
if err := pluginpkg.SetEnabled(reasonixHome, "echo-provider", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("disabled", func(t *testing.T) { runPhase(t, false) })
|
||||
if err := pluginpkg.SetEnabled(reasonixHome, "echo-provider", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Run("re-enabled", func(t *testing.T) { runPhase(t, true) })
|
||||
install(map[string]any{"op": "uninstall", "name": "echo-provider", "scope": "global"})
|
||||
t.Run("removed", func(t *testing.T) { runPhase(t, false) })
|
||||
}
|
||||
@@ -93,6 +93,11 @@ input-rewrite check.
|
||||
tool-serving package: manifest declarations, `Options.Tools`, catalog
|
||||
discovery, and an observable word-count result through the real host.
|
||||
|
||||
[`examples/providerextension`](examples/providerextension/README.md) is a
|
||||
text-only offline provider: manifest identity, `Options.Provider`, catalog,
|
||||
cancellable streaming, and a first controller turn with no API key or tool
|
||||
calls. It echoes the last provider-visible user message.
|
||||
|
||||
[`examples/fullsidecar`](examples/fullsidecar/README.md) is the reference
|
||||
extension: protocol-level input rewriting, tool interception
|
||||
(block + argument rewrite), system-prompt strategy replacement, a fake
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
---
|
||||
owner: @SivanCola
|
||||
backup: @esengine
|
||||
status: active
|
||||
reviewed: 2026-10-03
|
||||
---
|
||||
|
||||
# Offline Provider Extension
|
||||
|
||||
This installable Go SDK example contributes one text-only provider through
|
||||
`Options.Provider`. It returns `Offline echo: ` followed by the last user
|
||||
message's content, then a done chunk. It needs no API key or network service
|
||||
and emits no tool calls. It is an integration example, not a language model.
|
||||
|
||||
The host may compose workspace context and reminders into that message. The
|
||||
example echoes the provider-visible content unchanged; it does not extract
|
||||
the original typed input. It does not implement vision, reasoning, token
|
||||
accounting, or structured response formats.
|
||||
|
||||
## Build and install
|
||||
|
||||
From this directory in a Reasonix source checkout, with Go 1.23+ and the
|
||||
`reasonix` CLI available, on macOS or Linux:
|
||||
|
||||
```sh
|
||||
go test .
|
||||
go build -o bin/echo-provider.exe .
|
||||
plugin_root="$(pwd -P)"
|
||||
reasonix plugin install "$plugin_root" --dry-run
|
||||
reasonix plugin install "$plugin_root" --replace --yes
|
||||
reasonix plugin doctor echo-provider
|
||||
```
|
||||
|
||||
From PowerShell on Windows:
|
||||
|
||||
```powershell
|
||||
go test .
|
||||
go build -o bin/echo-provider.exe .
|
||||
$pluginRoot = (Resolve-Path .).Path
|
||||
reasonix plugin install $pluginRoot --dry-run
|
||||
reasonix plugin install $pluginRoot --replace --yes
|
||||
reasonix plugin doctor echo-provider
|
||||
```
|
||||
|
||||
The fixed `.exe` path works on Unix and gives Windows its required suffix.
|
||||
Review the dry-run's `FULL TRUST` block before installation. The CLI copies
|
||||
the package by default; rebuilding the source does not change that copy.
|
||||
For development, add `--link` to authorize changes in the source directory.
|
||||
|
||||
## Run a turn
|
||||
|
||||
From your workspace, use the installed provider directly:
|
||||
|
||||
```sh
|
||||
reasonix --model plugin/echo-provider/offline/echo -p "SDK-PROVIDER-CHECK hello"
|
||||
```
|
||||
|
||||
The assistant text MUST begin with `Offline echo: ` and include
|
||||
`SDK-PROVIDER-CHECK hello`. Additional host-composed context may also appear.
|
||||
The provider does not call another model or execute tools.
|
||||
|
||||
In an existing session, run `/reload` while idle, then use `/model` to choose
|
||||
`plugin/echo-provider/offline/echo`. The same reference works as
|
||||
`default_model` in `reasonix.toml`, including on the first runtime build.
|
||||
|
||||
`internal/assembly/boot/effect_sdk_provider_test.go` builds this actual
|
||||
example, installs a copy through preview and approved apply, deletes the
|
||||
source, and checks the first controller turn and a second turn on the same
|
||||
sidecar.
|
||||
|
||||
It repeats after re-enabling the package and verifies the typed
|
||||
unknown-model error when the configured plugin default is absent, disabled,
|
||||
or removed. This is offline host evidence, not model quality evidence.
|
||||
|
||||
## Author contract
|
||||
|
||||
| Part | Rule |
|
||||
| --- | --- |
|
||||
| Manifest | Declare the `providers` runtime capability and `plugin/echo-provider` provider capability with ID `offline/echo`. |
|
||||
| Identity | The host sets `REASONIX_PLUGIN_NAME`; both initialize and catalog MUST return the matching `plugin/<id>/offline/echo` reference. |
|
||||
| Fingerprint | This example's `schemaHash` is SHA-256 of the compact JSON encoding of its default `ProviderDescriptor`, as checked by `go test .`; it is an author capability fingerprint, not the full protocol schema hash. |
|
||||
| Streaming | Return promptly, produce chunks on the channel, and close it. `DoneChunk` ends the assistant turn; channel closure lets the SDK emit the stream end. |
|
||||
| Cancellation | The producer MUST stop when the callback's context is cancelled, including while waiting to send. |
|
||||
| Concurrency | This provider is stateless. Added shared mutable state MUST follow the [SDK concurrency contract](../../README.md#concurrency-contract). |
|
||||
| Output | stdout belongs to the SDK protocol. Diagnostics MUST go to stderr. |
|
||||
|
||||
## Iterate and remove
|
||||
|
||||
Run `go test .`, rebuild, and replace the installed copy after editing. For a
|
||||
linked development install, rebuild and run `/reload` while idle. Start a new
|
||||
session or reload after changing activation:
|
||||
|
||||
```sh
|
||||
reasonix plugin disable echo-provider
|
||||
reasonix plugin enable echo-provider
|
||||
reasonix plugin uninstall echo-provider --yes
|
||||
```
|
||||
|
||||
Select another model before disabling or removing the active provider. A
|
||||
`default_model` pointing to an inactive plugin cannot resolve at boot; change
|
||||
that default before starting a new session.
|
||||
|
||||
See [Plugin Packages](../../../../docs/PLUGIN_PACKAGES.md#manifest-v2-extensions)
|
||||
for manifest and trust rules, and [Extensions](../../../../docs/EXTENSIONS.md#runtime-reload)
|
||||
for reload behavior.
|
||||
@@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
extension "github.com/esengine/DeepSeek-Reasonix/sdk/go"
|
||||
)
|
||||
|
||||
type echoProvider struct{ id string }
|
||||
|
||||
func (p echoProvider) descriptor() extension.ProviderDescriptor {
|
||||
return extension.ProviderDescriptor{
|
||||
Ref: "plugin/" + p.id + "/offline/echo",
|
||||
DisplayName: "Offline echo (SDK example)",
|
||||
Model: "echo",
|
||||
ContextWindow: 64000,
|
||||
}
|
||||
}
|
||||
|
||||
func (p echoProvider) Initialize(_ context.Context, params extension.InitializeParams) (*extension.InitializeResult, error) {
|
||||
return &extension.InitializeResult{
|
||||
Providers: []extension.ProviderDescriptor{p.descriptor()},
|
||||
Provides: append([]extension.CapabilityWire(nil), params.Manifest.Provides...),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p echoProvider) Catalog(context.Context) ([]extension.ProviderDescriptor, error) {
|
||||
return []extension.ProviderDescriptor{p.descriptor()}, nil
|
||||
}
|
||||
|
||||
func (p echoProvider) Stream(ctx context.Context, req extension.StreamRequest) (<-chan extension.StreamChunk, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.ProviderRef != p.descriptor().Ref {
|
||||
return nil, fmt.Errorf("unknown offline provider ref %q", req.ProviderRef)
|
||||
}
|
||||
var text string
|
||||
for i := len(req.Request.Messages) - 1; i >= 0; i-- {
|
||||
if req.Request.Messages[i].Role == extension.ProviderRoleUser {
|
||||
text = req.Request.Messages[i].Content
|
||||
break
|
||||
}
|
||||
}
|
||||
chunks := make(chan extension.StreamChunk)
|
||||
go func() {
|
||||
defer close(chunks)
|
||||
for _, chunk := range []extension.StreamChunk{extension.TextChunk("Offline echo: "), extension.TextChunk(text), extension.DoneChunk()} {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case chunks <- chunk:
|
||||
}
|
||||
}
|
||||
}()
|
||||
return chunks, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
id := strings.TrimSpace(os.Getenv("REASONIX_PLUGIN_NAME"))
|
||||
if id == "" {
|
||||
id = "echo-provider"
|
||||
}
|
||||
p := echoProvider{id: id}
|
||||
if err := extension.Serve(context.Background(), p, extension.Options{
|
||||
Name: id, Version: "0.1.0", Provider: p,
|
||||
}); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
extension "github.com/esengine/DeepSeek-Reasonix/sdk/go"
|
||||
)
|
||||
|
||||
func TestEchoProviderManifestAndCatalog(t *testing.T) {
|
||||
raw, err := os.ReadFile("reasonix-plugin.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var manifest struct {
|
||||
Name string `json:"name"`
|
||||
Provides []extension.CapabilityWire `json:"provides"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &manifest); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := echoProvider{id: manifest.Name}
|
||||
result, err := p.Initialize(context.Background(), extension.InitializeParams{
|
||||
Manifest: extension.ManifestExpectation{Provides: manifest.Provides},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
catalog, err := p.Catalog(context.Background())
|
||||
if err != nil || len(catalog) != 1 || len(result.Providers) != 1 || len(result.Provides) != 1 {
|
||||
t.Fatalf("initialize=%+v catalog=%+v err=%v", result, catalog, err)
|
||||
}
|
||||
capability := result.Provides[0]
|
||||
if capability.Namespace+"/"+capability.ID != catalog[0].Ref || capability.Kind != "provider" || catalog[0].Tools || catalog[0].Reasoning || catalog[0].Vision {
|
||||
t.Fatalf("manifest capability=%+v catalog=%+v", capability, catalog)
|
||||
}
|
||||
encoded, err := json.Marshal(catalog[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := fmt.Sprintf("sha256:%x", sha256.Sum256(encoded)); capability.SchemaHash != want {
|
||||
t.Fatalf("descriptor fingerprint=%s, want %s", capability.SchemaHash, want)
|
||||
}
|
||||
if result.Providers[0].Ref != catalog[0].Ref {
|
||||
t.Fatalf("initialize ref=%q catalog ref=%q", result.Providers[0].Ref, catalog[0].Ref)
|
||||
}
|
||||
if got := (echoProvider{id: "renamed"}).descriptor().Ref; got != "plugin/renamed/offline/echo" {
|
||||
t.Fatalf("renamed provider ref=%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEchoProviderStreamsLastUserContent(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
messages []extension.ProviderMessage
|
||||
want string
|
||||
}{
|
||||
{"no user", []extension.ProviderMessage{{Role: extension.ProviderRoleSystem, Content: "system"}}, ""},
|
||||
{"unicode", []extension.ProviderMessage{{Role: extension.ProviderRoleUser, Content: "第一行\nsecond line"}}, "第一行\nsecond line"},
|
||||
{"last user", []extension.ProviderMessage{
|
||||
{Role: extension.ProviderRoleUser, Content: "old"},
|
||||
{Role: extension.ProviderRoleAssistant, Content: "answer"},
|
||||
{Role: extension.ProviderRoleUser, Content: "<context>host context</context>\nnew"},
|
||||
{Role: extension.ProviderRoleTool, Content: "tool result"},
|
||||
}, "<context>host context</context>\nnew"},
|
||||
{"empty last user", []extension.ProviderMessage{{Role: extension.ProviderRoleUser, Content: "old"}, {Role: extension.ProviderRoleUser}}, ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
p := echoProvider{id: "echo-provider"}
|
||||
stream, err := p.Stream(ctx, extension.StreamRequest{
|
||||
ProviderRef: p.descriptor().Ref, Request: extension.ProviderRequest{Messages: tc.messages},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var text string
|
||||
var types []extension.ProviderChunkType
|
||||
for chunk := range stream {
|
||||
types = append(types, chunk.Type)
|
||||
text += chunk.Text
|
||||
}
|
||||
if text != "Offline echo: "+tc.want || len(types) != 3 || types[0] != extension.ChunkText || types[1] != extension.ChunkText || types[2] != extension.ChunkDone {
|
||||
t.Fatalf("text=%q types=%v", text, types)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEchoProviderCancellationAndUnknownRef(t *testing.T) {
|
||||
p := echoProvider{id: "echo-provider"}
|
||||
if _, err := p.Stream(context.Background(), extension.StreamRequest{ProviderRef: "plugin/other/offline/echo"}); err == nil {
|
||||
t.Fatal("accepted another provider's ref")
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if _, err := p.Stream(ctx, extension.StreamRequest{ProviderRef: p.descriptor().Ref}); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("cancelled Stream=%v", err)
|
||||
}
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
stream, err := p.Stream(ctx, extension.StreamRequest{ProviderRef: p.descriptor().Ref})
|
||||
if err != nil {
|
||||
cancel()
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case chunk := <-stream:
|
||||
if chunk.Type != extension.ChunkText || chunk.Text != "Offline echo: " {
|
||||
t.Fatalf("first chunk=%+v", chunk)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("producer did not send its first chunk")
|
||||
}
|
||||
cancel()
|
||||
deadline := time.After(time.Second)
|
||||
for {
|
||||
select {
|
||||
case _, ok := <-stream:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
case <-deadline:
|
||||
t.Fatal("producer did not stop after cancellation")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"apiVersion": "reasonix.io/plugin/v2",
|
||||
"name": "echo-provider",
|
||||
"version": "0.1.0",
|
||||
"description": "Offline Go SDK provider example: echo the last user message",
|
||||
"provides": [
|
||||
{
|
||||
"namespace": "plugin/echo-provider",
|
||||
"kind": "provider",
|
||||
"id": "offline/echo",
|
||||
"version": "1.0.0",
|
||||
"schemaHash": "sha256:6387b7d7ff7acb18360dec8d514dea146e65bde0f7dafcf0b5d6eeac64a13dea"
|
||||
}
|
||||
],
|
||||
"contributes": {},
|
||||
"runtime": {
|
||||
"command": "${REASONIX_PLUGIN_ROOT}/bin/echo-provider.exe",
|
||||
"capabilities": [
|
||||
"providers"
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user