-
Notifications
You must be signed in to change notification settings - Fork 2.2k
Expand file tree
/
Copy pathmain.go
More file actions
352 lines (331 loc) · 12.6 KB
/
Copy pathmain.go
File metadata and controls
352 lines (331 loc) · 12.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
// Command fullsidecar is the reference Reasonix extension sidecar: one small
// program that exercises every Extension Protocol v2 contribution kind —
// input rewriting, tool interception, system-prompt strategy replacement, an
// extension-hosted streaming provider, structured UI surfaces and prompts,
// and a clean bounded shutdown. It is the example third parties copy.
//
// Behavior map:
//
// input "/fs <text>" → input.receive replaces the input with
// "<text> [rewritten by fullsidecar]"
// tool "dangerous_exec" → tool.before blocks it with a policy reason
// tool "read" → tool.before rewrites the arguments (sandbox)
// system_prompt.build → the strategy slot owner wraps the prompt
// session.start → publishes a status line and a card
// action "demo" → asks a form prompt, greets via notification
// provider plugin/<id>/fake/echo → streams a fixed completion: two text
// chunks, one tool call, usage, done
//
// Environment:
//
// REASONIX_PLUGIN_NAME plugin ID, set by the host at launch (provider
// refs must live in the plugin/<id>/ namespace);
// defaults to "fullsidecar" when run standalone
// FULLSIDECAR_STREAM_INTERVAL_MS
// pacing between provider chunks (default 15)
//
// The two hooks below exist for the host↔SDK conformance suite
// (internal/extension/conformance); they are inert unless set:
//
// FULLSIDECAR_CRASH_ON_INPUT exit(3) without answering when an
// input.receive text matches exactly
// FULLSIDECAR_STALL_ON_INPUT hold an input.receive answer until the
// intercept context ends when the text matches
package main
import (
"context"
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
extension "github.com/esengine/DeepSeek-Reasonix/sdk/go"
)
const (
rewritePrefix = "/fs "
rewriteSuffix = " [rewritten by fullsidecar]"
deniedTool = "dangerous_exec"
rewrittenTool = "read"
fakeModel = "echo"
defaultPluginID = "fullsidecar"
)
// plugin is the extension handler. The session context arrives with the
// handshake and is read by later callbacks, so it travels through an atomic.
type plugin struct {
id string
log *log.Logger
ui extension.HostUI
session atomic.Pointer[extension.SessionContext]
}
func main() {
logger := log.New(os.Stderr, "fullsidecar: ", log.LstdFlags)
id := strings.TrimSpace(os.Getenv("REASONIX_PLUGIN_NAME"))
if id == "" {
id = defaultPluginID
}
p := &plugin{id: id, log: logger}
provider := &fakeProvider{id: id, interval: streamInterval(), log: logger}
err := extension.Serve(context.Background(), p, extension.Options{
Name: id,
Version: "1.0.0",
Interceptors: map[string]extension.InterceptorFunc{
"input.receive": func(ctx context.Context, _ string, payload json.RawMessage) (*extension.InterceptResult, error) {
return p.interceptInput(ctx, payload)
},
"tool.before": func(ctx context.Context, _ string, payload json.RawMessage) (*extension.InterceptResult, error) {
return p.interceptTool(ctx, payload)
},
"system_prompt.build": func(ctx context.Context, _ string, payload json.RawMessage) (*extension.InterceptResult, error) {
return p.interceptSystemPrompt(ctx, payload)
},
},
Observer: p.observe,
Provider: provider,
UI: extension.UIHandler{
Action: p.action,
Submit: p.submit,
},
Shutdown: func(context.Context) { logger.Print("shutdown requested; exiting") },
Logger: logger,
})
if err != nil {
logger.Printf("serve: %v", err)
os.Exit(1)
}
// Serve returned nil: the host asked for shutdown. Exit 0 so the host
// reaps the process as an orderly stop.
}
// streamInterval reads FULLSIDECAR_STREAM_INTERVAL_MS with a 15ms default.
func streamInterval() time.Duration {
if raw := strings.TrimSpace(os.Getenv("FULLSIDECAR_STREAM_INTERVAL_MS")); raw != "" {
if ms, err := strconv.Atoi(raw); err == nil && ms > 0 {
return time.Duration(ms) * time.Millisecond
}
}
return 15 * time.Millisecond
}
// Initialize declares everything this extension contributes. The host rejects
// anything the installed manifest did not declare first.
func (p *plugin) Initialize(_ context.Context, params extension.InitializeParams) (*extension.InitializeResult, error) {
session := params.Session
p.session.Store(&session)
p.log.Printf("initialized for session %s (workspace %s)", session.SessionID, session.WorkspaceRoot)
return &extension.InitializeResult{
Subscriptions: []string{"input.receive", "tool.before", "system_prompt.build", "session.start"},
Replaces: []string{"system_prompt"},
Providers: []extension.ProviderDescriptor{fakeDescriptor(p.id)},
UIActions: []extension.UIActionDecl{{ActionID: "demo", Label: "Run the fullsidecar demo"}},
Provides: append([]extension.CapabilityWire(nil), params.Manifest.Provides...),
}, nil
}
// Interceptors
// interceptInput rewrites any input that starts with the "/fs " trigger.
func (p *plugin) interceptInput(ctx context.Context, payload json.RawMessage) (*extension.InterceptResult, error) {
var in struct {
Text string `json:"text"`
}
if err := json.Unmarshal(payload, &in); err != nil {
return extension.Continue(), nil
}
// Conformance hooks (see the package comment); inert when unset.
if crash := os.Getenv("FULLSIDECAR_CRASH_ON_INPUT"); crash != "" && in.Text == crash {
p.log.Printf("crash hook triggered by input %q", in.Text)
os.Exit(3)
}
if stall := os.Getenv("FULLSIDECAR_STALL_ON_INPUT"); stall != "" && in.Text == stall {
<-ctx.Done()
return nil, ctx.Err()
}
if !strings.HasPrefix(in.Text, rewritePrefix) {
return extension.Continue(), nil
}
rewritten := strings.TrimPrefix(in.Text, rewritePrefix) + rewriteSuffix
p.log.Printf("input.receive: rewrote %q → %q", in.Text, rewritten)
return extension.Replace(map[string]string{"text": rewritten})
}
// interceptTool blocks the denied tool outright and rewrites the arguments of
// the rewritten tool; every other tool continues untouched.
func (p *plugin) interceptTool(_ context.Context, payload json.RawMessage) (*extension.InterceptResult, error) {
var call struct {
Name string `json:"name"`
Arguments string `json:"arguments"`
}
if err := json.Unmarshal(payload, &call); err != nil {
return extension.Continue(), nil
}
switch call.Name {
case deniedTool:
return extension.Block("fullsidecar: tool " + deniedTool + " is denied by the demo policy"), nil
case rewrittenTool:
args := map[string]any{}
if strings.TrimSpace(call.Arguments) != "" {
if err := json.Unmarshal([]byte(call.Arguments), &args); err != nil {
return extension.Continue(), nil
}
}
args["sandbox"] = true
encoded, err := json.Marshal(args)
if err != nil {
return extension.Continue(), nil
}
return extension.Replace(map[string]string{"name": call.Name, "arguments": string(encoded)})
default:
return extension.Continue(), nil
}
}
// interceptSystemPrompt owns the system_prompt strategy slot: it wraps the
// base prompt instead of letting the default assembler render it.
func (p *plugin) interceptSystemPrompt(_ context.Context, payload json.RawMessage) (*extension.InterceptResult, error) {
var in struct {
Prompt string `json:"prompt"`
WorkspaceRoot string `json:"workspaceRoot"`
}
if err := json.Unmarshal(payload, &in); err != nil {
return nil, err
}
owned := "You are Reasonix running under the fullsidecar demo strategy.\n\n" +
"Workspace: " + in.WorkspaceRoot + "\n\nBase prompt:\n" + in.Prompt
return extension.Replace(map[string]string{"prompt": owned, "workspaceRoot": in.WorkspaceRoot})
}
// Observation and UI
// observe publishes the extension's status line and demo card when the
// session starts.
func (p *plugin) observe(ctx context.Context, event string, _ json.RawMessage) {
if event != "session.start" {
return
}
session := p.session.Load()
if session == nil {
return
}
if err := p.ui.PublishStatus(ctx, session.SessionID, session.Generation, "fullsidecar-status", extension.UIStatusPayload{
Label: "fullsidecar online",
Detail: "intercepts, provider, and UI are live",
Severity: extension.UISeverityInfo,
}); err != nil {
p.log.Printf("publish status: %v", err)
}
if err := p.ui.PublishCard(ctx, session.SessionID, session.Generation, "fullsidecar-card", extension.UICardPayload{
Title: "fullsidecar",
Markdown: "Reference extension: try the **demo** action or the `/fs ` input trigger.",
Fields: []extension.UIKeyValue{{Key: "plugin", Value: p.id}, {Key: "provider", Value: fakeRef(p.id)}},
Actions: []extension.UIActionRef{{ActionID: "demo", Label: "Run demo"}},
}); err != nil {
p.log.Printf("publish card: %v", err)
}
}
// action runs the declared "demo" action: a blocking form prompt, then a
// notification built from the answers. A dismissed prompt is not a failure.
func (p *plugin) action(ctx context.Context, actionID string, _ map[string]string) error {
if actionID != "demo" {
return fmt.Errorf("fullsidecar: unknown action %q", actionID)
}
session := p.session.Load()
if session == nil {
return errors.New("fullsidecar: no session yet")
}
values, err := p.ui.RequestForm(ctx, session.SessionID, session.Generation, "fullsidecar-demo-form", extension.UIFormPayload{
Title: "fullsidecar demo",
Message: "Whom should the demo greet?",
Fields: []extension.UIFormField{
{Key: "name", Label: "Your name", Kind: extension.UIFieldInput, Required: true},
{Key: "loud", Label: "Shout the greeting", Kind: extension.UIFieldConfirm},
},
})
if errors.Is(err, extension.ErrUICancelled) {
return nil
}
if err != nil {
return err
}
name, _ := values["name"].(string)
if strings.TrimSpace(name) == "" {
name = "world"
}
greeting := "Hello, " + name + "!"
if loud, _ := values["loud"].(bool); loud {
greeting = strings.ToUpper(greeting)
}
return p.ui.PublishNotification(ctx, session.SessionID, session.Generation, "fullsidecar-greeting", extension.UINotificationPayload{
Title: greeting,
Severity: extension.UISeverityInfo,
})
}
// submit acknowledges published-form submissions with a status update.
func (p *plugin) submit(ctx context.Context, surfaceID string, values map[string]any) error {
p.log.Printf("form %q submitted: %v", surfaceID, values)
session := p.session.Load()
if session == nil {
return nil
}
return p.ui.PublishStatus(ctx, session.SessionID, session.Generation, "fullsidecar-status", extension.UIStatusPayload{
Label: "fullsidecar: form " + surfaceID + " submitted",
Severity: extension.UISeverityInfo,
})
}
// Fake streaming provider
func fakeRef(pluginID string) string { return "plugin/" + pluginID + "/fake/" + fakeModel }
func fakeDescriptor(pluginID string) extension.ProviderDescriptor {
return extension.ProviderDescriptor{
Ref: fakeRef(pluginID),
DisplayName: "fullsidecar fake",
Model: fakeModel,
ContextWindow: 64000,
Tools: true,
Reasoning: true,
Efforts: []string{"low", "high"},
DefaultEffort: "low",
}
}
// fakeProvider streams a fixed scripted completion: two text chunks, one tool
// call, final usage, done. Chunks are paced so hosts can exercise mid-stream
// cancel; a cancelled context stops production immediately, and the SDK ends
// the stream interrupted.
type fakeProvider struct {
id string
interval time.Duration
log *log.Logger
}
func (p *fakeProvider) Catalog(context.Context) ([]extension.ProviderDescriptor, error) {
return []extension.ProviderDescriptor{fakeDescriptor(p.id)}, nil
}
func (p *fakeProvider) Stream(ctx context.Context, req extension.StreamRequest) (<-chan extension.StreamChunk, error) {
if req.ProviderRef != fakeRef(p.id) {
return nil, fmt.Errorf("fullsidecar: unknown provider ref %q", req.ProviderRef)
}
p.log.Printf("stream %s opened for %s (model %s)", req.StreamID, req.ProviderRef, req.Model)
chunks := make(chan extension.StreamChunk)
go func() {
defer close(chunks)
script := []extension.StreamChunk{
extension.TextChunk("fake-hello "),
extension.TextChunk("fake-world"),
{Type: extension.ChunkToolCall, ToolCall: &extension.ProviderToolCall{
ID: "call-1", Name: "lookup", Arguments: `{"query":"reasonix"}`,
}},
extension.UsageChunk(extension.ProviderUsage{
PromptTokens: 5, CompletionTokens: 7, TotalTokens: 12,
CacheHitTokens: 2, CacheMissTokens: 3, ReasoningTokens: 4,
FinishReason: "stop",
}),
extension.DoneChunk(),
}
for _, chunk := range script {
select {
case <-ctx.Done():
return
case <-time.After(p.interval):
}
select {
case <-ctx.Done():
return
case chunks <- chunk:
}
}
}()
return chunks, nil
}