Files
Junjie Zhou 4a97acf12a refactor(agent): separate Aside provider connection from model settings (#827)
* refactor(agent): connect Aside as a native provider with verified settings

* fix(agent): offer explicit offline Aside model and disconnect actions

Offer Save for next start after an initial runtime read failure, using a
structured API error instead of saved runtime state. Keep refusals and
failed readbacks on the live error path.

Add a separate offline disconnect confirmation that restores the checked
file plan, clears restore records and removes the provider last. Bind the
confirmation to its preview revision and roll back ordinary write failures.

Cover offline API and browser flows, cancellation, stale plans, missing
CLI, restore metadata and native provider spelling. Keep state file-only.
2026-10-05 14:37:44 +08:00

115 lines
2.9 KiB
Go

package agent
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"os"
)
// NativeConnection registers a provider independently of model selection.
// Its plan describes file and runtime changes without performing either.
type NativeConnection struct {
Read func() NativeState
Connect func() error
Apply func(key, value string) error
Stage func(key, value string) error
Disconnect func() (*DisconnectPlan, error)
Execute func(*DisconnectPlan) error
ExecuteOffline func(*DisconnectPlan) error
}
type OfflineAction string
const (
OfflineStage OfflineAction = "stage"
OfflineDisconnect OfflineAction = "disconnect"
)
// RuntimeUnavailableError is created only for an initial runtime read,
// before the operation has written either runtime settings or local files.
type RuntimeUnavailableError struct {
Agent string
Operation string
Offline OfflineAction
Cause error
}
func (e *RuntimeUnavailableError) Error() string {
return fmt.Sprintf("%s runtime settings could not be read for %s: %v", e.Agent, e.Operation, e.Cause)
}
func (e *RuntimeUnavailableError) Unwrap() error { return e.Cause }
type NativeState struct {
Provider string `json:"provider"`
Detail string `json:"detail,omitempty"`
Runtime string `json:"runtime"`
Fields map[string]NativeField `json:"fields"`
}
type NativeField struct {
Value string `json:"value"`
Status string `json:"status"`
Detail string `json:"detail,omitempty"`
}
type PlannedFile struct {
Path string
Before []byte
After []byte
}
type PlannedSetting struct {
Key string
Before json.RawMessage
After json.RawMessage
}
type DisconnectPlan struct {
Files []PlannedFile
Settings []PlannedSetting
}
// Revision binds confirmation to file preconditions and restoration values,
// without accepting a serialized plan from the client.
func (p *DisconnectPlan) Revision(agent string) string {
b, _ := json.Marshal(struct {
Agent string
Plan *DisconnectPlan
}{agent, p})
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:])
}
func (p *DisconnectPlan) Preview() []FileChange {
var changes []FileChange
for _, f := range p.Files {
lines := diffLines(string(f.Before), string(f.After))
if len(lines) != 0 {
path := f.Path
if home, err := os.UserHomeDir(); err == nil && len(path) > len(home) && path[:len(home)+1] == home+string(os.PathSeparator) {
path = "~" + path[len(home):]
}
changes = append(changes, FileChange{Path: path, Lines: lines})
}
}
return changes
}
func (p *DisconnectPlan) CheckFiles() error {
for _, f := range p.Files {
b, err := os.ReadFile(f.Path)
if os.IsNotExist(err) {
b, err = nil, nil
}
if err != nil {
return err
}
if string(b) != string(f.Before) {
return fmt.Errorf("%s changed since the disconnect plan was read; try again", f.Path)
}
}
return nil
}