Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,24 @@ LLM_API_KEY=
# OpenAI 兼容 Base URL,例如 https://api.openai.com/v1(可空)
LLM_BASE_URL=

# 思考开关:enabled | disabled。空则沿用模型默认(DeepSeek 当前默认开启)
# LLM_THINKING=

# 单次回复输出 token 上限,思考和正文共用(默认 65536)
# LLM_MAX_OUTPUT_TOKENS=65536

# 单次 Run 输入 token 上限,含上下文(默认 256000)
# LLM_MAX_INPUT_TOKENS=256000

# 单次 Run 最多模型轮数(默认 32)
# LLM_MAX_TURNS=32

# 单次 Run 最多工具调用次数(默认 64)
# LLM_MAX_TOOL_CALLS=64

# 单次 Run 墙钟上限(默认 20m)
# LLM_MAX_WALL_TIME=20m

# 本地 Git 仓库根。未设则用进程 cwd(从 server/ 启动时 cwd 不是仓根)。
# 撤回会改磁盘。本地试用先跑 scripts/git-sandbox.sh,再把这里指到 tmp/git-sandbox。
# 要操作本仓时显式写成仓根。pnpm dev:api 在沙箱存在时默认用沙箱。
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,7 +371,7 @@ Worker

## 配置

`LLM_PROVIDER`(`openai` | `fake`,默认 `fake`)、`LLM_MODEL`、`LLM_API_KEY`、`LLM_BASE_URL`。工具出站审批用 `EVALUATOR_PROVIDER` / `EVALUATOR_MODEL`(及可选 Key/BaseURL);explore 用 `SUBAGENT_PROVIDER` / `SUBAGENT_MODEL`。空则回落主模型。`GIT_REPO` 指向本地仓库根,未设则用进程 cwd(不向上找 `.git`)。未设 `DB_DSN` 时 SQLite 写仓根 `data/codedock.db`,不写 `server/`。`PLUGIN_DIR` 指向插件根目录,未设则不拉插件进程;`PLUGIN_RPC_TIMEOUT` 默认 `10s`。`CODEX_BIN` 为本机 Codex CLI(默认 `codex`)。Handler 创建 Run 时写入 `RunConfigSnapshot`(含 `EvaluatorModel` / `SubagentModel`),后续 Turn 只读快照。收尾验证跑工作区 `.cursor/verify.yaml`,以及本会话绑定计划里非 `manual` 的 `verify_cmd`。取消已标 `cancel_requested` 后不得再落 `completed`;`verifying` 上的取消立即终态。
`LLM_PROVIDER`(`openai` | `fake`,默认 `fake`)、`LLM_MODEL`、`LLM_API_KEY`、`LLM_BASE_URL`、`LLM_THINKING`(`enabled` | `disabled`,空则沿用模型默认)。Run 预算:`LLM_MAX_INPUT_TOKENS`(默认 256000)、`LLM_MAX_OUTPUT_TOKENS`(默认 65536,思考和正文共用)、`LLM_MAX_TURNS`(默认 32)、`LLM_MAX_TOOL_CALLS`(默认 64)、`LLM_MAX_WALL_TIME`(默认 20m)。工具出站审批用 `EVALUATOR_PROVIDER` / `EVALUATOR_MODEL`(及可选 Key/BaseURL);explore 用 `SUBAGENT_PROVIDER` / `SUBAGENT_MODEL`。空则回落主模型。`GIT_REPO` 指向本地仓库根,未设则用进程 cwd(不向上找 `.git`)。未设 `DB_DSN` 时 SQLite 写仓根 `data/codedock.db`,不写 `server/`。`PLUGIN_DIR` 指向插件根目录,未设则不拉插件进程;`PLUGIN_RPC_TIMEOUT` 默认 `10s`。`CODEX_BIN` 为本机 Codex CLI(默认 `codex`)。Handler 创建 Run 时写入 `RunConfigSnapshot`(含 `EvaluatorModel` / `SubagentModel`),后续 Turn 只读快照。收尾验证跑工作区 `.cursor/verify.yaml`,以及本会话绑定计划里非 `manual` 的 `verify_cmd`。取消已标 `cancel_requested` 后不得再落 `completed`;`verifying` 上的取消立即终态。

HTTP 出站领域对象使用 snake_case JSON。Router 只对本地回环 Origin 放行 CORS,便于本机 Web 直连 `:8080`。Web 用 `NEXT_PUBLIC_API_BASE`(默认 `http://localhost:8080`)和 `NEXT_PUBLIC_USER_ID`(默认 `local`)。

Expand Down
35 changes: 24 additions & 11 deletions server/cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,14 @@ func main() {
runtime.Start(ctx)

defaults := pkgagent.DefaultRunConfig(pkgagent.WorkAgent, model)
defaults.EvaluatorModel = sideModel(cfg.EvaluatorProvider, cfg.EvaluatorModel, cfg.EvaluatorAPIKey, cfg.EvaluatorBaseURL, model)
defaults.SubagentModel = sideModel(cfg.SubagentProvider, cfg.SubagentModel, cfg.SubagentAPIKey, cfg.SubagentBaseURL, defaults.EvaluatorModel)
defaults.Limits.MaxInputTokens = cfg.LLMMaxInputTokens
defaults.Limits.MaxOutputTokens = cfg.LLMMaxOutputTokens
defaults.Limits.MaxTurns = cfg.LLMMaxTurns
defaults.Limits.MaxToolCalls = cfg.LLMMaxToolCalls
defaults.Limits.MaxWallTime = cfg.LLMMaxWallTime
log.Info("llm limits", "max_input_tokens", cfg.LLMMaxInputTokens, "max_output_tokens", cfg.LLMMaxOutputTokens, "max_turns", cfg.LLMMaxTurns, "max_tool_calls", cfg.LLMMaxToolCalls, "max_wall_time", cfg.LLMMaxWallTime.String(), "thinking", cfg.LLMThinking)
defaults.EvaluatorModel = sideModel(cfg.EvaluatorProvider, cfg.EvaluatorModel, cfg.EvaluatorAPIKey, cfg.EvaluatorBaseURL, cfg.LLMThinking, model)
defaults.SubagentModel = sideModel(cfg.SubagentProvider, cfg.SubagentModel, cfg.SubagentAPIKey, cfg.SubagentBaseURL, cfg.LLMThinking, defaults.EvaluatorModel)
api := handler.New(client, queries, runtime, bus, defaults, cfg, logger.NewLogger("handler"))
codexRT := intcodex.New(intcodex.Options{Bin: cfg.CodexBin})
defer func() { _ = codexRT.Close() }()
Expand Down Expand Up @@ -144,21 +150,24 @@ func main() {
}
}

// modelOptions 把 LLM API Key 与 BaseURL 编进 ModelConfig.Options。
// modelOptions 把 LLM API Key、BaseURL 和思考开关编进 ModelConfig.Options。
func modelOptions(cfg config.Config) json.RawMessage {
return encodeModelOptions(cfg.LLMAPIKey, cfg.LLMBaseURL)
return encodeModelOptions(cfg.LLMAPIKey, cfg.LLMBaseURL, cfg.LLMThinking)
}

// sideModel 组装复审或子代理模型;未配置时回落 fallback。
func sideModel(provider, name, apiKey, baseURL string, fallback pkgagent.ModelConfig) pkgagent.ModelConfig {
func sideModel(provider, name, apiKey, baseURL, thinking string, fallback pkgagent.ModelConfig) pkgagent.ModelConfig {
out := fallback
if strings.TrimSpace(provider) != "" {
out.Provider = provider
}
if strings.TrimSpace(name) != "" {
out.Model = name
}
if strings.TrimSpace(apiKey) != "" || strings.TrimSpace(baseURL) != "" {
if strings.TrimSpace(thinking) == "" {
thinking = optionString(fallback.Options, "thinking")
}
if strings.TrimSpace(apiKey) != "" || strings.TrimSpace(baseURL) != "" || strings.TrimSpace(thinking) != "" {
key := apiKey
if key == "" {
key = optionString(fallback.Options, "api_key")
Expand All @@ -167,17 +176,21 @@ func sideModel(provider, name, apiKey, baseURL string, fallback pkgagent.ModelCo
if url == "" {
url = optionString(fallback.Options, "base_url")
}
out.Options = encodeModelOptions(key, url)
out.Options = encodeModelOptions(key, url, thinking)
}
return out
}

// encodeModelOptions 把 Key 与 BaseURL 编成模型 Options。
func encodeModelOptions(apiKey, baseURL string) json.RawMessage {
body, err := json.Marshal(map[string]string{
// encodeModelOptions 把 Key、BaseURL 和思考开关编成模型 Options。
func encodeModelOptions(apiKey, baseURL, thinking string) json.RawMessage {
fields := map[string]string{
"api_key": apiKey,
"base_url": baseURL,
})
}
if strings.TrimSpace(thinking) != "" {
fields["thinking"] = thinking
}
body, err := json.Marshal(fields)
if err != nil {
return json.RawMessage("{}")
}
Expand Down
1 change: 1 addition & 0 deletions server/internal/agent/explore.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func (r *Runtime) explore(ctx context.Context, input pkgagent.ExploreInput, work
scope := pkgagent.ResolvePlanScope(state.ActivePlan, hist.Messages)
req.ActivePlan = scope.ActivePlan
req.MentionedPlans = scope.Mentioned
req.MaxOutputTokens = state.Config.Limits.MaxOutputTokens
}
}
return pkgagent.Explore(ctx, req)
Expand Down
113 changes: 69 additions & 44 deletions server/internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,55 +10,67 @@ import (

// Config 是进程启动时一次性读取的环境配置。
type Config struct {
HTTPAddr string // HTTP 监听地址
LogLevel string
DBEngine string
DBDSN string
LLMProvider string
LLMModel string
LLMAPIKey string
LLMBaseURL string
GitRepo string // 默认仓库根;会话未指定工作目录时回落到这里,再否则 cwd
LLMConcurrency int // 进程内同时进行的模型调用上限;0 表示不限制
ToolConcurrency int // 进程内同时执行的工具调用上限;0 表示不限制
PluginDir string // 插件目录;空则不拉进程
PluginRPCTimeout time.Duration // 单次插件 RPC 超时
CodexBin string // 本机 Codex CLI;未安装时主服务仍可启动
EvaluatorProvider string // 独立复审模型供应商;空则回落主模型
EvaluatorModel string // 独立复审模型名
EvaluatorAPIKey string // 复审模型 Key;空则回落 LLM_API_KEY
EvaluatorBaseURL string // 复审模型地址;空则回落 LLM_BASE_URL
SubagentProvider string // explore 子代理供应商;空则回落复审模型
SubagentModel string // explore 子代理模型名
SubagentAPIKey string // 子代理 Key;空则回落复审/主模型
SubagentBaseURL string // 子代理地址;空则回落复审/主模型
HTTPAddr string // HTTP 监听地址
LogLevel string
DBEngine string
DBDSN string
LLMProvider string
LLMModel string
LLMAPIKey string
LLMBaseURL string
LLMThinking string // 思考开关:enabled | disabled;空则沿用模型默认
LLMMaxInputTokens int64 // 单次 Run 的输入 token 上限,含上下文
LLMMaxOutputTokens int64 // 单次模型回复的输出 token 上限,思考和正文共用
LLMMaxTurns int // 单次 Run 最多调用模型的轮数
LLMMaxToolCalls int // 单次 Run 最多执行的工具次数
LLMMaxWallTime time.Duration // 单次 Run 的墙钟上限
GitRepo string // 默认仓库根;会话未指定工作目录时回落到这里,再否则 cwd
LLMConcurrency int // 进程内同时进行的模型调用上限;0 表示不限制
ToolConcurrency int // 进程内同时执行的工具调用上限;0 表示不限制
PluginDir string // 插件目录;空则不拉进程
PluginRPCTimeout time.Duration // 单次插件 RPC 超时
CodexBin string // 本机 Codex CLI;未安装时主服务仍可启动
EvaluatorProvider string // 独立复审模型供应商;空则回落主模型
EvaluatorModel string // 独立复审模型名
EvaluatorAPIKey string // 复审模型 Key;空则回落 LLM_API_KEY
EvaluatorBaseURL string // 复审模型地址;空则回落 LLM_BASE_URL
SubagentProvider string // explore 子代理供应商;空则回落复审模型
SubagentModel string // explore 子代理模型名
SubagentAPIKey string // 子代理 Key;空则回落复审/主模型
SubagentBaseURL string // 子代理地址;空则回落复审/主模型
}

// Load 从环境变量读取配置,未设置时使用默认值。
func Load() Config {
return Config{
HTTPAddr: env("HTTP_ADDR", ":8080"),
LogLevel: env("LOG_LEVEL", "debug"),
DBEngine: env("DB_ENGINE", "sqlite"),
DBDSN: env("DB_DSN", defaultSQLiteDSN()),
LLMProvider: env("LLM_PROVIDER", "fake"),
LLMModel: env("LLM_MODEL", "fake"),
LLMAPIKey: env("LLM_API_KEY", ""),
LLMBaseURL: env("LLM_BASE_URL", ""),
GitRepo: env("GIT_REPO", ""),
LLMConcurrency: envInt("LLM_CONCURRENCY", 4),
ToolConcurrency: envInt("TOOL_CONCURRENCY", 8),
PluginDir: env("PLUGIN_DIR", ""),
PluginRPCTimeout: envDuration("PLUGIN_RPC_TIMEOUT", 10*time.Second),
CodexBin: env("CODEX_BIN", "codex"),
EvaluatorProvider: env("EVALUATOR_PROVIDER", ""),
EvaluatorModel: env("EVALUATOR_MODEL", ""),
EvaluatorAPIKey: env("EVALUATOR_API_KEY", ""),
EvaluatorBaseURL: env("EVALUATOR_BASE_URL", ""),
SubagentProvider: env("SUBAGENT_PROVIDER", ""),
SubagentModel: env("SUBAGENT_MODEL", ""),
SubagentAPIKey: env("SUBAGENT_API_KEY", ""),
SubagentBaseURL: env("SUBAGENT_BASE_URL", ""),
HTTPAddr: env("HTTP_ADDR", ":8080"),
LogLevel: env("LOG_LEVEL", "debug"),
DBEngine: env("DB_ENGINE", "sqlite"),
DBDSN: env("DB_DSN", defaultSQLiteDSN()),
LLMProvider: env("LLM_PROVIDER", "fake"),
LLMModel: env("LLM_MODEL", "fake"),
LLMAPIKey: env("LLM_API_KEY", ""),
LLMBaseURL: env("LLM_BASE_URL", ""),
LLMThinking: env("LLM_THINKING", ""),
LLMMaxInputTokens: envInt64("LLM_MAX_INPUT_TOKENS", 256000),
LLMMaxOutputTokens: envInt64("LLM_MAX_OUTPUT_TOKENS", 65536),
LLMMaxTurns: envInt("LLM_MAX_TURNS", 32),
LLMMaxToolCalls: envInt("LLM_MAX_TOOL_CALLS", 64),
LLMMaxWallTime: envDuration("LLM_MAX_WALL_TIME", 20*time.Minute),
GitRepo: env("GIT_REPO", ""),
LLMConcurrency: envInt("LLM_CONCURRENCY", 4),
ToolConcurrency: envInt("TOOL_CONCURRENCY", 8),
PluginDir: env("PLUGIN_DIR", ""),
PluginRPCTimeout: envDuration("PLUGIN_RPC_TIMEOUT", 10*time.Second),
CodexBin: env("CODEX_BIN", "codex"),
EvaluatorProvider: env("EVALUATOR_PROVIDER", ""),
EvaluatorModel: env("EVALUATOR_MODEL", ""),
EvaluatorAPIKey: env("EVALUATOR_API_KEY", ""),
EvaluatorBaseURL: env("EVALUATOR_BASE_URL", ""),
SubagentProvider: env("SUBAGENT_PROVIDER", ""),
SubagentModel: env("SUBAGENT_MODEL", ""),
SubagentAPIKey: env("SUBAGENT_API_KEY", ""),
SubagentBaseURL: env("SUBAGENT_BASE_URL", ""),
}
}

Expand Down Expand Up @@ -129,6 +141,19 @@ func env(key, fallback string) string {
return value
}

// envInt64 读 int64 环境变量,未设或解析失败时用 fallback。
func envInt64(key string, fallback int64) int64 {
value := os.Getenv(key)
if value == "" {
return fallback
}
n, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return fallback
}
return n
}

// envInt 读整数环境变量,未设或解析失败时用 fallback。
func envInt(key string, fallback int) int {
value := os.Getenv(key)
Expand Down
18 changes: 18 additions & 0 deletions server/internal/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ func TestLoadDefaults(t *testing.T) {
t.Setenv("LLM_MODEL", "")
t.Setenv("LLM_API_KEY", "")
t.Setenv("LLM_BASE_URL", "")
t.Setenv("LLM_THINKING", "")
t.Setenv("LLM_MAX_INPUT_TOKENS", "")
t.Setenv("LLM_MAX_OUTPUT_TOKENS", "")
t.Setenv("LLM_MAX_TURNS", "")
t.Setenv("LLM_MAX_TOOL_CALLS", "")
t.Setenv("LLM_MAX_WALL_TIME", "")
t.Setenv("GIT_REPO", "")
t.Setenv("PLUGIN_DIR", "")
t.Setenv("PLUGIN_RPC_TIMEOUT", "")
Expand All @@ -43,6 +49,9 @@ func TestLoadDefaults(t *testing.T) {
if cfg.LLMModel != "fake" {
t.Fatalf("LLMModel = %q, want fake", cfg.LLMModel)
}
if cfg.LLMThinking != "" || cfg.LLMMaxInputTokens != 256000 || cfg.LLMMaxOutputTokens != 65536 || cfg.LLMMaxTurns != 32 || cfg.LLMMaxToolCalls != 64 || cfg.LLMMaxWallTime != 20*time.Minute {
t.Fatalf("llm limits = %+v", cfg)
}
if cfg.GitRepo != "" {
t.Fatalf("GitRepo = %q, want empty", cfg.GitRepo)
}
Expand Down Expand Up @@ -73,6 +82,12 @@ func TestLoadFromEnv(t *testing.T) {
t.Setenv("LLM_MODEL", "gpt-4o")
t.Setenv("LLM_API_KEY", "sk-test")
t.Setenv("LLM_BASE_URL", "https://api.example.com/v1")
t.Setenv("LLM_THINKING", "disabled")
t.Setenv("LLM_MAX_INPUT_TOKENS", "1000")
t.Setenv("LLM_MAX_OUTPUT_TOKENS", "2000")
t.Setenv("LLM_MAX_TURNS", "3")
t.Setenv("LLM_MAX_TOOL_CALLS", "4")
t.Setenv("LLM_MAX_WALL_TIME", "1m")
t.Setenv("GIT_REPO", "/tmp/repo")
t.Setenv("PLUGIN_DIR", "/tmp/plugins")
t.Setenv("PLUGIN_RPC_TIMEOUT", "2s")
Expand All @@ -85,6 +100,9 @@ func TestLoadFromEnv(t *testing.T) {
if cfg.LLMProvider != "openai" || cfg.LLMModel != "gpt-4o" || cfg.LLMAPIKey != "sk-test" || cfg.LLMBaseURL != "https://api.example.com/v1" {
t.Fatalf("Load() LLM = %+v", cfg)
}
if cfg.LLMThinking != "disabled" || cfg.LLMMaxInputTokens != 1000 || cfg.LLMMaxOutputTokens != 2000 || cfg.LLMMaxTurns != 3 || cfg.LLMMaxToolCalls != 4 || cfg.LLMMaxWallTime != time.Minute {
t.Fatalf("llm limits = %+v", cfg)
}
if cfg.GitRepo != "/tmp/repo" {
t.Fatalf("GitRepo = %q, want /tmp/repo", cfg.GitRepo)
}
Expand Down
7 changes: 7 additions & 0 deletions server/pkg/agent/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package agent

import (
"encoding/json"
"strings"

"codedock/pkg/agent/tool"
)
Expand Down Expand Up @@ -42,6 +43,12 @@ func DecodeText(content json.RawMessage) string {
return string(content)
}

// assistantBlank 判断助手消息是否既无正文也无工具调用。
// 空 content 且没有 tool_calls 的助手消息会被兼容网关拒绝。
func assistantBlank(content json.RawMessage, calls []tool.Call) bool {
return strings.TrimSpace(DecodeText(content)) == "" && len(calls) == 0
}

// EncodeToolResult 将工具结果编码为消息 Content。
func EncodeToolResult(callID string, output json.RawMessage) json.RawMessage {
if len(output) == 0 {
Expand Down
10 changes: 5 additions & 5 deletions server/pkg/agent/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,11 @@ func DefaultRunConfig(mode WorkMode, model ModelConfig) RunConfigSnapshot {
Tool: retry,
},
Limits: RunLimits{
MaxWallTime: 5 * time.Minute,
MaxTurns: 8,
MaxToolCalls: 16,
MaxInputTokens: 128000,
MaxOutputTokens: 8192,
MaxWallTime: 20 * time.Minute,
MaxTurns: 32,
MaxToolCalls: 64,
MaxInputTokens: 256000,
MaxOutputTokens: 65536,
MaxParallelTools: 4,
MaxVerifyRounds: 3,
MaxEvaluateRounds: 2,
Expand Down
31 changes: 19 additions & 12 deletions server/pkg/agent/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func (e *Engine) Step(ctx context.Context, in StepInput) (StepResult, error) {

// callLLM 占槽后压缩上下文、调模型,把流式增量写成 Fact,再产出 assistant 消息与下一步。
// 逻辑:校验取消 → 超回合则有副作用先验证否则收束 → CompactIfNeeded + Stream → 收齐文本/工具调用 → 有 Tool 则下一步 llm_result。
// wrap-up 回合清空工具表;模型空正文时用改动和验证事实拼装。
// wrap-up 回合清空工具表;模型空正文时用改动和验证事实拼装。正文和工具调用都空时不落助手消息。
func (e *Engine) callLLM(ctx context.Context, in StepInput, _ Instruction) (StepResult, error) {
if err := ctx.Err(); err != nil {
return e.finish(ctx, in, finishInstructions(RunCancelled, StopCancelled)[0])
Expand Down Expand Up @@ -241,18 +241,21 @@ func (e *Engine) callLLM(ctx context.Context, in StepInput, _ Instruction) (Step
assistant.Content = EncodeText(composeWrapUpText(e, state, hist.Messages))
}
}
if len(assistant.Content) == 0 {
blank := assistantBlank(assistant.Content, assistant.ToolCalls)
if !blank && len(assistant.Content) == 0 {
assistant.Content = EncodeText("")
}
_ = e.appendFact(ctx, state.RunID, Fact{
Type: EventAssistantCompleted,
TurnID: state.TurnID,
Payload: MarshalPayload(AssistantCompletedPayload{
MessageID: msgID,
Text: DecodeText(assistant.Content),
ToolCalls: result.ToolCalls,
}),
})
if !blank {
_ = e.appendFact(ctx, state.RunID, Fact{
Type: EventAssistantCompleted,
TurnID: state.TurnID,
Payload: MarshalPayload(AssistantCompletedPayload{
MessageID: msgID,
Text: DecodeText(assistant.Content),
ToolCalls: result.ToolCalls,
}),
})
}

now := time.Now().UTC()
if state.StartedAt == nil {
Expand All @@ -269,9 +272,13 @@ func (e *Engine) callLLM(ctx context.Context, in StepInput, _ Instruction) (Step
state.Checkpoint.Results = nil
state.Checkpoint.Completed = nil
}
var messages []Message
if !blank {
messages = []Message{assistant}
}
return StepResult{
State: state,
Messages: []Message{assistant},
Messages: messages,
Next: &StepJob{
RunID: state.RunID,
StepIndex: state.StepIndex + 1,
Expand Down
Loading
Loading