返回 DeepSeek-Reasonix
runtime_plan.go
根目录 / internal / boot / runtime_plan.go
1 package boot
2
3 import (
4 "strings"
5
6 "reasonix/internal/config"
7 "reasonix/internal/extension"
8 "reasonix/internal/extension/dispatch"
9 "reasonix/internal/extension/sidecar"
10 "reasonix/internal/extensioncontract"
11 )
12
13 // RuntimeReload is previous-generation state for incremental sidecar adoption
14 // and subgraph-classified rebuild.
15 type RuntimeReload struct {
16 // ForceFullRebuild bypasses extension subgraph reuse and refreshes existing
17 // native sidecars, including linked binaries whose graph metadata is unchanged.
18 ForceFullRebuild bool
19 Extensions *sidecar.Manager
20 Graph *extension.DependencyGraph
21 Generation uint64
22 // Owner is reused across one controller/session rebuild lineage. Cold builds
23 // leave it nil and receive a fresh isolated owner.
24 Owner *extension.RuntimeOwner
25 // PreviousSnapshot/Dispatcher enable CacheHash-stable no-op rebuilds and
26 // interceptor-only rewire without rediscovering the whole assembly.
27 PreviousSnapshot *extension.RuntimeSnapshot
28 PreviousDispatcher *dispatch.Dispatcher
29 PreviousPlan *extension.RuntimePlan
30 // ReuseAssembly, when set with a compatible plan, skips skill/command/hook
31 // rediscovery inside BuildRuntime.
32 ReuseAssembly *ReusedAssembly
33 }
34
35 // buildRuntimeGraph constructs the dependency graph for installed native
36 // runtime packages. Host-provided capabilities (if any) are included as a
37 // synthetic "host" component so plugin requirements can resolve.
38 func buildRuntimeGraph(home string, hostProvides []extensioncontract.Capability) (*extension.DependencyGraph, error) {
39 packages, _ := sidecar.LoadRuntimePackages(home)
40 comps := make([]extension.ComponentDescriptor, 0, len(packages)+1)
41 if len(hostProvides) > 0 {
42 comps = append(comps, extension.ComponentDescriptor{
43 ID: "host",
44 Source: extension.ContributionSource{Scope: extension.ScopeBuiltin, Origin: "host"},
45 Provides: hostProvides,
46 })
47 }
48 for _, item := range packages {
49 pkg := item.Package
50 id := extension.ComponentID("plugin/" + pkg.Manifest.Name)
51 var intercepts []extension.InterceptorPoint
52 var replaces []extension.Slot
53 priority := 0
54 optional := true
55 if rt := pkg.Manifest.Runtime; rt != nil {
56 priority = rt.Priority
57 optional = !rt.Required
58 for _, p := range rt.Intercepts {
59 intercepts = append(intercepts, extension.InterceptorPoint(p))
60 }
61 for _, s := range rt.Replaces {
62 if slot, err := extension.ParseSlot(s); err == nil {
63 replaces = append(replaces, slot)
64 }
65 }
66 }
67 comps = append(comps, extension.ComponentDescriptor{
68 ID: id,
69 Source: extension.ContributionSource{Scope: extension.ScopePlugin, PluginID: pkg.Manifest.Name, Origin: "plugin", Version: pkg.Manifest.Version},
70 Requires: pkg.Requires(),
71 Provides: pkg.ProvidesCapabilities(),
72 Intercepts: intercepts,
73 Replaces: replaces,
74 Priority: priority,
75 Optional: optional,
76 })
77 }
78 if len(comps) == 0 {
79 return &extension.DependencyGraph{
80 Components: map[extension.ComponentID]extension.ComponentDescriptor{},
81 Edges: map[extension.ComponentID][]extension.ComponentID{},
82 Providers: map[string][]extension.ComponentID{},
83 }, nil
84 }
85 return extension.BuildDependencyGraph(comps)
86 }
87
88 // attachPlanAndStatus fills BuildResult.Plan and Status from graphs using the
89 // lifecycle registry so doctor can explain Inactive/Failed components.
90 func attachPlanAndStatus(res *BuildResult, from *extension.DependencyGraph, to *extension.DependencyGraph, fromGen uint64, previousSnapshot *extension.RuntimeSnapshot) {
91 if res == nil {
92 return
93 }
94 toGen := uint64(0)
95 if res.Snapshot != nil {
96 toGen = res.Snapshot.Generation()
97 }
98 plan := extension.DiffRuntimePlan(from, to, fromGen, toGen)
99 if previousSnapshot != nil && res.Snapshot != nil {
100 plan.PrefixChanged = previousSnapshot.CacheHash() != res.Snapshot.CacheHash()
101 }
102 res.Plan = plan
103 life := extension.NewLifecycleRegistry(toGen)
104 status := &extension.RuntimeStatus{
105 PublishedGeneration: toGen,
106 Plan: extension.PlanView(plan),
107 }
108 if res.Runtime != nil {
109 status.Receipts = res.Runtime.Receipts()
110 }
111 if to != nil {
112 for _, id := range to.ActivateOrder() {
113 life.Ensure(id)
114 _ = life.Transition(id, extension.ComponentPreparing, "")
115 // Structured inactive reasons: missing requirements + graph diagnostics.
116 inactive := false
117 var diags []string
118 for _, d := range to.Diagnostics {
119 if strings.Contains(d, string(id)) {
120 inactive = true
121 diags = append(diags, d)
122 }
123 }
124 if desc, ok := to.Components[id]; ok {
125 for _, req := range desc.Requires {
126 if req.Optional {
127 continue
128 }
129 if _, found := to.EpochFor(id, req); !found {
130 inactive = true
131 diags = append(diags, "missing required capability "+req.Key.String()+" (versionRange="+req.VersionRange+")")
132 }
133 }
134 // Declared provides with no live client → Unavailable diagnostic.
135 if res.Extensions != nil && strings.HasPrefix(string(id), "plugin/") {
136 name := sidecar.PluginNameFromComponentID(id)
137 if name != "" && res.Extensions.Client(name) == nil && len(desc.Provides) > 0 {
138 inactive = true
139 for _, cap := range desc.Provides {
140 diags = append(diags, "capability Unavailable: "+cap.Key.String()+" (runtime client not started)")
141 }
142 }
143 }
144 }
145 if inactive {
146 _ = life.Transition(id, extension.ComponentInactive, strings.Join(diags, "; "))
147 } else if res.Extensions != nil && strings.HasPrefix(string(id), "plugin/") {
148 name := sidecar.PluginNameFromComponentID(id)
149 if name != "" && res.Extensions.Client(name) == nil {
150 _ = life.Transition(id, extension.ComponentInactive, "runtime client not started")
151 } else {
152 _ = life.Transition(id, extension.ComponentActive, "")
153 }
154 } else {
155 _ = life.Transition(id, extension.ComponentActive, "")
156 }
157 }
158 status.Components = life.All()
159 }
160 res.Status = status
161 res.Lifecycle = life
162 }
163
164 // planForPreflight builds the RuntimePlan used to adopt Unchanged sidecars.
165 func planForPreflight(opts Options, toGen uint64) *extension.RuntimePlan {
166 if opts.Extensions == nil && opts.Graph == nil {
167 return nil
168 }
169 toGraph, err := buildRuntimeGraph(config.ReasonixHomeDir(), nil)
170 if err != nil {
171 return nil
172 }
173 plan := extension.DiffRuntimePlan(opts.Graph, toGraph, opts.Generation, toGen)
174 if opts.ForceFullRebuild && opts.Extensions != nil {
175 // Explicit reloads refresh linked binaries even when graph metadata is unchanged.
176 // Preflight replaces them beside the live manager; publishing the new
177 // controller retires the old processes.
178 plan.RestartUnchangedSidecars = true
179 }
180 return plan
181 }
182
183 // finalizeBuildResult attaches the cold-start RuntimePlan and status, then
184 // publishes the generation so stale traffic from prior runtimes is dropped.
185 func finalizeBuildResult(res *BuildResult, publish bool) *BuildResult {
186 if res == nil {
187 return nil
188 }
189 if graph, err := buildRuntimeGraph(config.ReasonixHomeDir(), nil); err == nil {
190 attachPlanAndStatus(res, nil, graph, 0, nil)
191 }
192 if publish {
193 publishBuildResult(res)
194 }
195 return res
196 }
197
198 func publishBuildResult(res *BuildResult) {
199 if res == nil || res.Snapshot == nil {
200 return
201 }
202 gen := res.Snapshot.Generation()
203 if gen == 0 {
204 return
205 }
206 owner := res.Owner
207 if owner == nil {
208 owner = extension.RuntimeOwnerOrDefault(nil)
209 res.Owner = owner
210 }
211 gate := owner.Gate
212 // Expire any previous drain TTLs before publishing the new generation.
213 _ = gate.SweepAndForceExpire()
214 gate.Publish(gen)
215 // Product path: force-expire old generations after drainTTL so in-flight
216 // work cannot linger forever after rebuild.
217 gate.ScheduleDrainWatch()
218 if res.Controller != nil {
219 res.Controller.SetRuntimeGeneration(gen)
220 }
221 if res.Runtime != nil {
222 owner.Receipts.IngestScope(res.Runtime.Scope())
223 }
224 if res.Status != nil {
225 res.Status.PublishedGeneration = gen
226 }
227 }
228
228 lines GO