Repository navigation
Expand file tree
/
Copy pathlimits.go
More file actions
256 lines (239 loc) · 9.83 KB
/
Copy pathlimits.go
File metadata and controls
256 lines (239 loc) · 9.83 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
package spec
import (
"fmt"
"strings"
)
// Default byte limits for execution bounds (issue #117). Values use binary KiB/MiB.
const (
DefaultMaxToolInputBytes = 256 << 10 // 256 KiB
DefaultMaxToolOutputBytes = 256 << 10 // 256 KiB
DefaultMaxCheckpointBytes = 1 << 20 // 1 MiB
// DefaultMaxWorkflowNesting is the maximum workflow: call depth (issue #194).
// Top-level run is depth 0; the first subworkflow is depth 1.
DefaultMaxWorkflowNesting = 8
// DefaultMaxLoopIterations caps how many elements a single `.agent` loop or
// dynamic fan-out may iterate (issue #199). The surface has no unbounded
// (`while`) loop, so every loop is bounded by its collection; this cap bounds
// that collection so a runtime list cannot make termination unbounded. A loop
// whose collection exceeds it fails loudly rather than fanning out without
// limit.
DefaultMaxLoopIterations = 1000
)
// LimitExceedPolicy controls behavior when a byte limit is exceeded.
type LimitExceedPolicy string
const (
// LimitExceedTruncate shortens payload fields in-place (engine truncateMapInPlace) and records a limit-hit trace event.
LimitExceedTruncate LimitExceedPolicy = "truncate"
// LimitExceedFail aborts the step or run with a clear error.
LimitExceedFail LimitExceedPolicy = "fail"
)
// LimitKind identifies which execution limit was evaluated (trace events, issue #117).
type LimitKind string
const (
LimitKindToolInput LimitKind = "tool_input"
LimitKindToolOutput LimitKind = "tool_output"
LimitKindCheckpoint LimitKind = "checkpoint"
)
// ExecutionLimits bounds tool I/O and checkpoint/state size. Project YAML may set defaults;
// Workflow and Tool resources may override individual fields (issue #117).
type ExecutionLimits struct {
MaxToolInputBytes int `yaml:"maxToolInputBytes,omitempty" json:"maxToolInputBytes,omitempty"`
MaxToolOutputBytes int `yaml:"maxToolOutputBytes,omitempty" json:"maxToolOutputBytes,omitempty"`
MaxCheckpointBytes int `yaml:"maxCheckpointBytes,omitempty" json:"maxCheckpointBytes,omitempty"`
MaxStateBytes int `yaml:"maxStateBytes,omitempty" json:"maxStateBytes,omitempty"`
MaxWorkflowNesting int `yaml:"maxWorkflowNesting,omitempty" json:"maxWorkflowNesting,omitempty"`
MaxLoopIterations int `yaml:"maxLoopIterations,omitempty" json:"maxLoopIterations,omitempty"`
ToolInputExceedPolicy LimitExceedPolicy `yaml:"toolInputExceedPolicy,omitempty" json:"toolInputExceedPolicy,omitempty"`
ToolOutputExceedPolicy LimitExceedPolicy `yaml:"toolOutputExceedPolicy,omitempty" json:"toolOutputExceedPolicy,omitempty"`
CheckpointExceedPolicy LimitExceedPolicy `yaml:"checkpointExceedPolicy,omitempty" json:"checkpointExceedPolicy,omitempty"`
}
// ResolvedExecutionLimits holds fully merged limits after precedence resolution.
type ResolvedExecutionLimits struct {
MaxToolInputBytes int
MaxToolOutputBytes int
MaxCheckpointBytes int
MaxWorkflowNesting int
MaxLoopIterations int
ToolInputExceedPolicy LimitExceedPolicy
ToolOutputExceedPolicy LimitExceedPolicy
CheckpointExceedPolicy LimitExceedPolicy
}
// DefaultExecutionLimits returns built-in limits when project config omits a limits block.
func DefaultExecutionLimits() ResolvedExecutionLimits {
return ResolvedExecutionLimits{
MaxToolInputBytes: DefaultMaxToolInputBytes,
MaxToolOutputBytes: DefaultMaxToolOutputBytes,
MaxCheckpointBytes: DefaultMaxCheckpointBytes,
MaxWorkflowNesting: DefaultMaxWorkflowNesting,
MaxLoopIterations: DefaultMaxLoopIterations,
ToolInputExceedPolicy: LimitExceedTruncate,
ToolOutputExceedPolicy: LimitExceedTruncate,
CheckpointExceedPolicy: LimitExceedFail,
}
}
// NormalizeExecutionLimits merges maxStateBytes into maxCheckpointBytes when the latter is unset.
func NormalizeExecutionLimits(l *ExecutionLimits) {
if l == nil {
return
}
if l.MaxCheckpointBytes <= 0 && l.MaxStateBytes > 0 {
l.MaxCheckpointBytes = l.MaxStateBytes
}
}
// MergeExecutionLimits overlays override onto base; non-zero override fields win.
//
// MaxStateBytes is intentionally not merged as an independent field: it is a deprecated alias whose
// only effect is to seed MaxCheckpointBytes, applied by NormalizeExecutionLimits on both override and
// out below. If it ever gains independent semantics, add an explicit branch here so this routine
// stays the single, complete source of field-merge truth.
func MergeExecutionLimits(base ExecutionLimits, override *ExecutionLimits) ExecutionLimits {
if override == nil {
return base
}
NormalizeExecutionLimits(override)
out := base
if override.MaxToolInputBytes > 0 {
out.MaxToolInputBytes = override.MaxToolInputBytes
}
if override.MaxToolOutputBytes > 0 {
out.MaxToolOutputBytes = override.MaxToolOutputBytes
}
if override.MaxCheckpointBytes > 0 {
out.MaxCheckpointBytes = override.MaxCheckpointBytes
}
if override.MaxWorkflowNesting > 0 {
out.MaxWorkflowNesting = override.MaxWorkflowNesting
}
if override.MaxLoopIterations > 0 {
out.MaxLoopIterations = override.MaxLoopIterations
}
if p := strings.TrimSpace(string(override.ToolInputExceedPolicy)); p != "" {
out.ToolInputExceedPolicy = LimitExceedPolicy(p)
}
if p := strings.TrimSpace(string(override.ToolOutputExceedPolicy)); p != "" {
out.ToolOutputExceedPolicy = LimitExceedPolicy(p)
}
if p := strings.TrimSpace(string(override.CheckpointExceedPolicy)); p != "" {
out.CheckpointExceedPolicy = LimitExceedPolicy(p)
}
NormalizeExecutionLimits(&out)
return out
}
// ResolveExecutionLimits merges project, workflow, and tool limits with built-in defaults.
// Precedence (highest wins): tool > workflow > project > defaults.
func ResolveExecutionLimits(project *ProjectSpec, workflow *WorkflowSpec, tool *ToolSpec) ResolvedExecutionLimits {
def := DefaultExecutionLimits()
merged := ExecutionLimits{
MaxToolInputBytes: def.MaxToolInputBytes,
MaxToolOutputBytes: def.MaxToolOutputBytes,
MaxCheckpointBytes: def.MaxCheckpointBytes,
MaxWorkflowNesting: def.MaxWorkflowNesting,
MaxLoopIterations: def.MaxLoopIterations,
ToolInputExceedPolicy: def.ToolInputExceedPolicy,
ToolOutputExceedPolicy: def.ToolOutputExceedPolicy,
CheckpointExceedPolicy: def.CheckpointExceedPolicy,
}
if project != nil && project.Limits != nil {
merged = MergeExecutionLimits(merged, project.Limits)
}
if workflow != nil && workflow.Limits != nil {
merged = MergeExecutionLimits(merged, workflow.Limits)
}
if tool != nil && tool.Limits != nil {
merged = MergeExecutionLimits(merged, tool.Limits)
}
return ResolvedExecutionLimits{
MaxToolInputBytes: merged.MaxToolInputBytes,
MaxToolOutputBytes: merged.MaxToolOutputBytes,
MaxCheckpointBytes: merged.MaxCheckpointBytes,
MaxWorkflowNesting: merged.MaxWorkflowNesting,
MaxLoopIterations: merged.MaxLoopIterations,
ToolInputExceedPolicy: merged.ToolInputExceedPolicy,
ToolOutputExceedPolicy: merged.ToolOutputExceedPolicy,
CheckpointExceedPolicy: merged.CheckpointExceedPolicy,
}
}
// ResolveMaxWorkflowNesting is the effective workflow: nesting cap (issue #194).
// Zero/omitted YAML uses DefaultMaxWorkflowNesting.
func ResolveMaxWorkflowNesting(project *ProjectSpec, workflow *WorkflowSpec) int {
return ResolveExecutionLimits(project, workflow, nil).MaxWorkflowNesting
}
// ValidateExecutionLimits returns an error when limits or policies are invalid.
func ValidateExecutionLimits(l *ExecutionLimits) error {
if l == nil {
return nil
}
NormalizeExecutionLimits(l)
if err := validateLimitBytes("maxToolInputBytes", l.MaxToolInputBytes); err != nil {
return err
}
if err := validateLimitBytes("maxToolOutputBytes", l.MaxToolOutputBytes); err != nil {
return err
}
if err := validateLimitBytes("maxCheckpointBytes", l.MaxCheckpointBytes); err != nil {
return err
}
if err := validateLimitBytes("maxWorkflowNesting", l.MaxWorkflowNesting); err != nil {
return err
}
if err := validateLimitBytes("maxLoopIterations", l.MaxLoopIterations); err != nil {
return err
}
if err := validateLimitBytes("maxStateBytes", l.MaxStateBytes); err != nil {
return err
}
if err := validateExceedPolicy("toolInputExceedPolicy", l.ToolInputExceedPolicy); err != nil {
return err
}
if err := validateExceedPolicy("toolOutputExceedPolicy", l.ToolOutputExceedPolicy); err != nil {
return err
}
if err := validateExceedPolicy("checkpointExceedPolicy", l.CheckpointExceedPolicy); err != nil {
return err
}
if l.CheckpointExceedPolicy == LimitExceedTruncate {
return fmt.Errorf("spec: checkpointExceedPolicy %q is unsafe; use %q", LimitExceedTruncate, LimitExceedFail)
}
return nil
}
func validateLimitBytes(field string, n int) error {
if n < 0 {
return fmt.Errorf("spec: %s must be non-negative, got %d", field, n)
}
return nil
}
func validateExceedPolicy(field string, p LimitExceedPolicy) error {
switch LimitExceedPolicy(strings.TrimSpace(string(p))) {
case "", LimitExceedTruncate, LimitExceedFail:
return nil
default:
return fmt.Errorf("spec: %s must be %q or %q, got %q", field, LimitExceedTruncate, LimitExceedFail, p)
}
}
// validateExecutionLimitsGraph validates limits blocks on project, workflows, and tools.
func validateExecutionLimitsGraph(g *ProjectGraph) []error {
if g == nil {
return nil
}
var errs []error
if err := ValidateExecutionLimits(g.Spec.Limits); err != nil {
errs = append(errs, g.Pos.Errorf("Project: spec.limits: %w", err))
}
for name, wr := range g.Workflows {
if wr == nil || wr.Spec.Limits == nil {
continue
}
if err := ValidateExecutionLimits(wr.Spec.Limits); err != nil {
errs = append(errs, wr.Pos.Errorf("Workflow/%s: spec.limits: %w", name, err))
}
}
for name, tr := range g.Tools {
if tr == nil || tr.Spec.Limits == nil {
continue
}
if err := ValidateExecutionLimits(tr.Spec.Limits); err != nil {
errs = append(errs, tr.Pos.Errorf("Tool/%s: spec.limits: %w", name, err))
}
}
return errs
}