返回 DeepSeek-Reasonix
startup.go
根目录 / internal / plugin / startup.go
1 package plugin
2
3 import (
4 "errors"
5 "fmt"
6 "strings"
7 "time"
8
9 "reasonix/internal/secrets"
10 )
11
12 // defaultStartupTimeout bounds the real initialize + tools/list handshake.
13 // The model-facing wait stays shorter so a slow but healthy server can finish
14 // in the background without holding an interactive turn open.
15 const defaultStartupTimeout = 30 * time.Second
16
17 // DefaultStartupTimeout is the default background handshake safety cap used by
18 // config and host adapters when no explicit server override is present.
19 func DefaultStartupTimeout() time.Duration { return defaultStartupTimeout }
20
21 // DefaultStartupWaitBudget is the maximum time one interactive tool call waits
22 // for a shared background handshake before asking the model to retry later.
23 func DefaultStartupWaitBudget() time.Duration { return defaultStartTimeout }
24
25 func (s Spec) startupTimeout() time.Duration {
26 if s.StartupTimeout > 0 {
27 return s.StartupTimeout
28 }
29 if s.DefaultStartupTimeout > 0 {
30 return s.DefaultStartupTimeout
31 }
32 return defaultStartupTimeout
33 }
34
35 // ResolvedStartupTimeout returns the server override, global default, or the
36 // built-in background handshake cap, in that order.
37 func (s Spec) ResolvedStartupTimeout() time.Duration { return s.startupTimeout() }
38
39 type startupFailure struct {
40 Stage string
41 Elapsed time.Duration
42 Stderr string
43 Err error
44 }
45
46 func (e *startupFailure) Error() string {
47 if e == nil {
48 return "MCP startup failed"
49 }
50 stage := strings.TrimSpace(e.Stage)
51 if stage == "" {
52 stage = "unknown"
53 }
54 msg := fmt.Sprintf("MCP startup %s failed after %s: %v", stage, formatElapsed(e.Elapsed), e.Err)
55 if stderr := strings.TrimSpace(e.Stderr); stderr != "" {
56 msg += "; stderr: " + stderr
57 }
58 return msg
59 }
60
61 func (e *startupFailure) Unwrap() error {
62 if e == nil {
63 return nil
64 }
65 return e.Err
66 }
67
68 func newStartupFailure(stage string, started time.Time, stderr string, err error) error {
69 if err == nil {
70 return nil
71 }
72 var existing *startupFailure
73 if errors.As(err, &existing) {
74 return err
75 }
76 elapsed := time.Since(started)
77 if elapsed < 0 {
78 elapsed = 0
79 }
80 return &startupFailure{
81 Stage: strings.TrimSpace(stage),
82 Elapsed: elapsed,
83 Stderr: secrets.RedactCredentials(strings.TrimSpace(stderr)),
84 Err: err,
85 }
86 }
87
88 func startupFailureDetails(err error) (stage string, elapsed time.Duration, stderr string) {
89 var startupErr *startupFailure
90 if !errors.As(err, &startupErr) || startupErr == nil {
91 return "", 0, ""
92 }
93 return startupErr.Stage, startupErr.Elapsed, startupErr.Stderr
94 }
95
96 func formatElapsed(elapsed time.Duration) string {
97 if elapsed < time.Millisecond {
98 return elapsed.String()
99 }
100 return elapsed.Round(time.Millisecond).String()
101 }
102
103 type startupDiagnosticTransport interface {
104 startupStderr() string
105 }
106
107 func (c *Client) startupStderr() string {
108 if c == nil || c.t == nil {
109 return ""
110 }
111 if diagnostic, ok := c.t.(startupDiagnosticTransport); ok {
112 return diagnostic.startupStderr()
113 }
114 return ""
115 }
116
116 lines GO