feat(dispatcher,gateway): Eino 采纳 Phase D —— 任务生命周期状态机
submitted → running → done / failed / timeout 显式状态机,根治"卡运行中 看不出来"(此前 Task.Status 只写死 submitted、从不流转)。 - contract:新增 SubjectTaskStatus 主题 + TaskStatusEvent + 状态常量 - shared/bus:PublishTaskStatus / SubscribeTaskStatus(core NATS pub-sub) - dispatcher:StatusSink 接口 + Orchestrator.Handle 状态钩子——进入执行 →running、收尾 finishStatus→done/failed、整体超时上限 taskExecTimeout =3min→timeout;经 sub 回写 - gateway:SubscribeTaskStatus 落 PG(Task 增 Detail 字段,AutoMigrate); 新增 GET /api/v1/tasks/:id 供 UI 轮询状态 验收:实测 submitted→running→done 流转 + PG 持久化 + 端点查询闭环; make test-go 全绿。HITL 中断恢复 / 多智能体仍按场景待做。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ package eino
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
@@ -29,6 +30,11 @@ type ToolCaller interface {
|
||||
CallTool(ctx context.Context, subject string, call *contract.ToolCall) (*contract.ToolResult, error)
|
||||
}
|
||||
|
||||
// StatusSink 回写任务生命周期状态(由 NATS bus 实现;可为 nil → 不回写)。
|
||||
type StatusSink interface {
|
||||
PublishTaskStatus(taskID, status, detail string) error
|
||||
}
|
||||
|
||||
// LLM 是编排所需的语言模型能力(生产由 *llm.Pool 实现)。抽成接口便于测试注入假模型。
|
||||
type LLM interface {
|
||||
Ready() bool
|
||||
@@ -42,6 +48,9 @@ type LLM interface {
|
||||
// 工具调用超时;超时即降级(不带工具上下文继续推理)。
|
||||
const toolCallTimeout = 3 * time.Second
|
||||
|
||||
// taskExecTimeout 是单个任务整体执行上限;超时即判 timeout(状态机),避免无限期"运行中"。
|
||||
const taskExecTimeout = 3 * time.Minute
|
||||
|
||||
// Orchestrator 把每个 DSL 任务动态编译为 Eino 图并执行(记忆召回 → 工具节点 → 注入 → 流式)。
|
||||
type Orchestrator struct {
|
||||
pool LLM
|
||||
@@ -50,15 +59,39 @@ type Orchestrator struct {
|
||||
sink TokenSink
|
||||
tools ToolCaller
|
||||
exec ExecSink
|
||||
status StatusSink // 任务生命周期状态回写(可为 nil)
|
||||
|
||||
turnMu sync.Mutex // 保护 turns(攒批计数,多任务 goroutine 共享)
|
||||
turns map[string]int // sessionID → 累计轮次,用于每 N 轮触发 consolidate
|
||||
}
|
||||
|
||||
// NewOrchestrator 持有依赖;图按任务的 DSL 在 Handle 内动态编译。
|
||||
// exec 为执行可视化事件出口(可为 nil,则不发轨迹事件);eval 为自动化评测(可为 nil)。
|
||||
func NewOrchestrator(pool LLM, breaker *harness.CircuitBreaker, eval *harness.Evaluator, sink TokenSink, tools ToolCaller, exec ExecSink) (*Orchestrator, error) {
|
||||
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec}, nil
|
||||
// exec 为执行可视化事件出口(可为 nil,则不发轨迹事件);eval 为自动化评测(可为 nil);
|
||||
// status 为任务生命周期状态回写出口(可为 nil)。
|
||||
func NewOrchestrator(pool LLM, breaker *harness.CircuitBreaker, eval *harness.Evaluator, sink TokenSink, tools ToolCaller, exec ExecSink, status StatusSink) (*Orchestrator, error) {
|
||||
return &Orchestrator{pool: pool, breaker: breaker, eval: eval, sink: sink, tools: tools, exec: exec, status: status}, nil
|
||||
}
|
||||
|
||||
// setStatus 回写一次任务状态流转(status 为 nil 时静默跳过)。
|
||||
func (o *Orchestrator) setStatus(taskID, status, detail string) {
|
||||
if o.status == nil {
|
||||
return
|
||||
}
|
||||
if err := o.status.PublishTaskStatus(taskID, status, detail); err != nil {
|
||||
log.Printf("[eino] 回写任务状态 %s=%s 失败: %v", taskID, status, err)
|
||||
}
|
||||
}
|
||||
|
||||
// finishStatus 据收尾错误把任务置为 done / timeout / failed。
|
||||
func (o *Orchestrator) finishStatus(taskID string, err error) {
|
||||
switch {
|
||||
case err == nil:
|
||||
o.setStatus(taskID, contract.TaskDone, "")
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
o.setStatus(taskID, contract.TaskTimeout, "执行超时")
|
||||
default:
|
||||
o.setStatus(taskID, contract.TaskFailed, truncate(err.Error(), 200))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle 消费一个任务:按 DSL 编译 Eino 图并执行,把 Token 流回流到 sundynix.streams.<id>。
|
||||
@@ -72,22 +105,31 @@ func (o *Orchestrator) Handle(ctx context.Context, t *contract.Task) error {
|
||||
tr.info("task", "system", "服务熔断", "后端连续失败,暂时拒绝新任务,请稍后重试")
|
||||
_ = o.sink.PublishToken(t.ID, []byte("⚠️ 服务繁忙(已触发熔断保护),请稍后重试。"))
|
||||
_ = o.sink.CompleteStream(t.ID)
|
||||
o.setStatus(t.ID, contract.TaskFailed, "服务熔断")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 任务状态机:进入执行 → running;整体加超时上限,超时判 timeout(杜绝无限期"运行中")。
|
||||
o.setStatus(t.ID, contract.TaskRunning, "")
|
||||
tctx, cancel := context.WithTimeout(ctx, taskExecTimeout)
|
||||
defer cancel()
|
||||
|
||||
// 报告生成走专用多步编排(规划→分章并行检索撰写→汇聚→渲染 Word),而非通用对话图。
|
||||
if intent, _ := t.Meta[contract.MetaIntent].(string); intent == contract.IntentReport {
|
||||
return o.handleReport(ctx, t, tr)
|
||||
err := o.handleReport(tctx, t, tr)
|
||||
o.finishStatus(t.ID, err)
|
||||
return err
|
||||
}
|
||||
log.Printf("[eino] task %s received (graph=%d bytes), 按图执行(拓扑+连线+分支)...", t.ID, len(t.Graph))
|
||||
tr.info("task", "system", "任务受理", fmt.Sprintf("DSL %d 字节,按图执行", len(t.Graph)))
|
||||
|
||||
// 按 DSL 图的真实拓扑/连线/分支执行(graph.go 解释器),agent 节点流式回流 token。
|
||||
answer, err := o.runGraph(ctx, t, tr)
|
||||
answer, err := o.runGraph(tctx, t, tr)
|
||||
if err != nil {
|
||||
log.Printf("[eino] task %s graph error: %v", t.ID, err)
|
||||
_ = o.sink.CompleteStream(t.ID)
|
||||
o.breaker.Report(false)
|
||||
o.finishStatus(t.ID, err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -96,6 +138,7 @@ func (o *Orchestrator) Handle(ctx context.Context, t *contract.Task) error {
|
||||
}
|
||||
log.Printf("[eino] task %s done (%d 字答复)", t.ID, len([]rune(answer)))
|
||||
o.breaker.Report(true)
|
||||
o.finishStatus(t.ID, nil)
|
||||
|
||||
// 写回阶段:离开热路径、异步落历史 + (TODO)抽取记忆。
|
||||
go o.memorize(t, answer)
|
||||
|
||||
Reference in New Issue
Block a user