@mars.liu/dsh-cybernetics 0.7.8 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +210 -192
- package/README.zh-CN.md +213 -0
- package/cordis.patch.yml +8 -0
- package/docs/CHANGELOG.md +139 -0
- package/lib/client.js +332 -36
- package/package.json +2 -1
- package/src/controller.ts +49 -6
- package/src/index.ts +77 -11
- package/workflows/round.js +96 -19
package/src/index.ts
CHANGED
|
@@ -73,6 +73,8 @@ export interface Config extends ControllerConfig {
|
|
|
73
73
|
taskAgentModel?: string
|
|
74
74
|
/** Flat verify-agent model override (same format); empty = inherit session agent. */
|
|
75
75
|
verifyAgentModel?: string
|
|
76
|
+
/** Flat triage-agent model override (same format); empty = inherit session agent. */
|
|
77
|
+
triageAgentModel?: string
|
|
76
78
|
/**
|
|
77
79
|
* Hard orchestration gate (default false): hide execution tools from
|
|
78
80
|
* TOP-LEVEL session agents at prompt-assembly time, so the session agent
|
|
@@ -101,6 +103,12 @@ export interface Config extends ControllerConfig {
|
|
|
101
103
|
* Off (default) keeps the v0.6 implicit-trigger behavior exactly unchanged.
|
|
102
104
|
*/
|
|
103
105
|
allGoalsMode?: boolean
|
|
106
|
+
/**
|
|
107
|
+
* Max concurrent rounds across ALL sessions (default 0 = unlimited): a new
|
|
108
|
+
* round (blocking, 'start' or 'revise') is refused while this many rounds
|
|
109
|
+
* are in flight, protecting the host from parallel-agent storms.
|
|
110
|
+
*/
|
|
111
|
+
maxConcurrentRounds?: number
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
// Under the host's 0.1.7 settings model, an exported schemastery `Config`
|
|
@@ -110,20 +118,35 @@ export interface Config extends ControllerConfig {
|
|
|
110
118
|
// pattern). At least one field must be `.volatile()` for the page to be
|
|
111
119
|
// served at all; a volatile edit is committed into the running fiber's
|
|
112
120
|
// reference without a remount and announced via `loader/volatile-update`.
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
// the
|
|
121
|
+
// The user-facing knobs (taskAgentModel / verifyAgentModel / triageAgentModel /
|
|
122
|
+
// roundsPerSubgoal / maxConcurrentRounds / allGoalsMode) are all
|
|
123
|
+
// volatile so they are visible AND writable in the settings panel and take
|
|
124
|
+
// effect on the next goal round. roles stays in the schema but is NOT
|
|
125
|
+
// volatile (v0.8.4): the panel serves only volatile fields, so it no longer
|
|
126
|
+
// shows in the GUI — advanced users (agent-guided) configure it directly in
|
|
127
|
+
// the profile config yaml; a host reload applies it. hardGate and the gate fields stay non-volatile: the panel
|
|
128
|
+
// cannot write them (they change structural wiring — tool stripping listener —
|
|
129
|
+
// so they belong in the profile config document instead).
|
|
117
130
|
export const Config = z.object({
|
|
118
131
|
/** Model-facing tool name. */
|
|
119
132
|
toolName: z.string().default('cybernetics').description('模型侧工具名(缺省 cybernetics)。'),
|
|
120
133
|
/** Inject the implicit goal-loop guidance section. */
|
|
121
134
|
implicitTrigger: z.boolean().default(true).description('注入隐式 goal-loop 系统提示段(缺省开启)。'),
|
|
122
|
-
/**
|
|
135
|
+
/**
|
|
136
|
+
* Per-role sub-agent model/provider overrides: role -> {provider, model}.
|
|
137
|
+
* v0.8.4: NOT volatile — the settings card no longer exposes the roles JSON
|
|
138
|
+
* textarea (hand-editing JSON in the GUI is unfriendly), and the host panel
|
|
139
|
+
* serves only volatile fields, so a non-volatile roles simply disappears
|
|
140
|
+
* from the GUI while remaining configurable in the profile config yaml
|
|
141
|
+
* (save triggers a plugin-row remount / host reload, which is fine for an
|
|
142
|
+
* advanced setting an agent guides the user through). Runtime semantics are
|
|
143
|
+
* unchanged: readConfigField accepts a plain object (non-volatile) exactly
|
|
144
|
+
* like a live reference (volatile), and named task roles keep working.
|
|
145
|
+
*/
|
|
123
146
|
roles: z.dict(z.object({
|
|
124
147
|
provider: z.string(),
|
|
125
148
|
model: z.string(),
|
|
126
|
-
})).description('按角色的子 agent provider/model 覆盖:role -> {provider, model}(如 default/verify/triage/frontend
|
|
149
|
+
})).description('按角色的子 agent provider/model 覆盖:role -> {provider, model}(如 default/verify/triage/frontend)。高级配置:直接编辑 profile 配置 yaml 后需宿主重载生效(GUI 不再提供此字段的编辑控件)。'),
|
|
127
150
|
/**
|
|
128
151
|
* Flat task-agent model override ("provider/model" or a bare model name).
|
|
129
152
|
* Empty = inherit the session agent. Merged over roles.default at wiring
|
|
@@ -140,6 +163,25 @@ export const Config = z.object({
|
|
|
140
163
|
taskAgentModel: z.string().default('').volatile().description('task agent 模型覆盖(「provider/model」或裸模型名);留空 = 与 session agent 一致。保存后下一轮 goal 即生效,无需重载。'),
|
|
141
164
|
/** Flat verify-agent model override; same format, fallback and volatile semantics as taskAgentModel. */
|
|
142
165
|
verifyAgentModel: z.string().default('').volatile().description('verify agent 模型覆盖(「provider/model」或裸模型名);留空 = 与 session agent 一致。保存后下一轮 goal 即生效,无需重载。'),
|
|
166
|
+
/**
|
|
167
|
+
* Flat triage-agent model override (v0.8.2); identical shape, fallback and
|
|
168
|
+
* volatile semantics as taskAgentModel/verifyAgentModel. The triage role
|
|
169
|
+
* reads the task/verify results and produces the round's convergence
|
|
170
|
+
* decision (met / continue / human-input / ...); quick-depth rounds skip
|
|
171
|
+
* verify+triage entirely, so this override only ever fires on full rounds.
|
|
172
|
+
*/
|
|
173
|
+
triageAgentModel: z.string().default('').volatile().description('triage agent 模型覆盖(「provider/model」或裸模型名);留空 = 与 session agent 一致。triage 角色读 task/verify 结果输出本轮收敛决策(met/continue/human-input 等);quick 轮不启用。保存后下一轮 goal 即生效,无需重载。'),
|
|
174
|
+
/**
|
|
175
|
+
* Max consecutive rounds per subgoal before the loop pauses for the user
|
|
176
|
+
* (controller round-cap, default 3). Volatile: read per round start, so the
|
|
177
|
+
* panel edit governs the very next goal round without a remount.
|
|
178
|
+
*/
|
|
179
|
+
roundsPerSubgoal: z.number().default(3).volatile().description('同一子目标的最大连续轮数(递归深度上限,缺省 3):超过即暂停并升级给用户。保存后下一轮 goal 即生效,无需重载。'),
|
|
180
|
+
/**
|
|
181
|
+
* Max concurrent rounds across all sessions (default 0 = unlimited).
|
|
182
|
+
* Volatile: enforced at every launch, live on the next tool call.
|
|
183
|
+
*/
|
|
184
|
+
maxConcurrentRounds: z.number().default(0).volatile().description('全局并行轮数上限(缺省 0 = 不限):在跑轮数达到上限时拒绝启动新一轮,防止并行风暴。保存后即时生效,无需重载。'),
|
|
143
185
|
/** Hard orchestration gate (default false). */
|
|
144
186
|
hardGate: z.boolean().default(false).description('硬编排门:装配期从顶层 session agent 摘除执行工具,所有改动必须经 cybernetics goal + task agent。修改会触发插件重载。'),
|
|
145
187
|
/** Tool names hidden in denylist mode (default DEFAULT_GATED_TOOLS). */
|
|
@@ -859,19 +901,39 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
859
901
|
|
|
860
902
|
// Flat settings-panel model fields fold into the canonical roles shape:
|
|
861
903
|
// an unset field leaves its role untouched = inherit the session agent.
|
|
862
|
-
//
|
|
863
|
-
// references — unwrap through readConfigField
|
|
864
|
-
// like the allGoalsMode live ref above.
|
|
865
|
-
|
|
904
|
+
// All three fields are volatile (task/verify v0.7.7, triage v0.8.2), so
|
|
905
|
+
// the loader hands back live references — unwrap through readConfigField
|
|
906
|
+
// ('' = inherit) exactly like the allGoalsMode live ref above.
|
|
907
|
+
// roles may be volatile (a live reference, pre-0.8.3 configs) or a plain
|
|
908
|
+
// object (non-volatile since v0.8.4, configured via profile yaml) —
|
|
909
|
+
// readConfigField handles both shapes: a live ref is unwrapped, a plain
|
|
910
|
+
// object is returned as-is (hardGate and friends take the same path).
|
|
911
|
+
const roleOverrides: RolesConfig = { ...readConfigField(config.roles, {} as RolesConfig) }
|
|
866
912
|
const taskSpec = parseAgentModelSpec(readConfigField(config.taskAgentModel, ''))
|
|
867
913
|
if (taskSpec) roleOverrides.default = { ...roleOverrides.default, ...taskSpec }
|
|
868
914
|
const verifySpec = parseAgentModelSpec(readConfigField(config.verifyAgentModel, ''))
|
|
869
915
|
if (verifySpec) roleOverrides.verify = { ...roleOverrides.verify, ...verifySpec }
|
|
916
|
+
const triageSpec = parseAgentModelSpec(readConfigField(config.triageAgentModel, ''))
|
|
917
|
+
if (triageSpec) roleOverrides.triage = { ...roleOverrides.triage, ...triageSpec }
|
|
870
918
|
const controllerConfig: ControllerConfig = {
|
|
871
919
|
roundScriptPath: config.roundScriptPath ?? fileURLToPath(new URL('../workflows/round.js', import.meta.url)),
|
|
872
|
-
|
|
920
|
+
// Volatile schema fields surface as live references — unwrap per round
|
|
921
|
+
// start so a settings-panel edit governs the next round (0/undefined
|
|
922
|
+
// falls back to the controller's built-in default).
|
|
923
|
+
roundsPerSubgoal: Math.max(1, Math.floor(readConfigField(config.roundsPerSubgoal, 3))),
|
|
873
924
|
roles: roleOverrides,
|
|
874
925
|
}
|
|
926
|
+
// Concurrent-round cap (0 = unlimited), read live at every launch.
|
|
927
|
+
const maxConcurrentRounds = () => {
|
|
928
|
+
const raw = Math.floor(readConfigField(config.maxConcurrentRounds, 0))
|
|
929
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 0
|
|
930
|
+
}
|
|
931
|
+
const concurrentCapError = () => {
|
|
932
|
+
const cap = maxConcurrentRounds()
|
|
933
|
+
return cap > 0 && running.size >= cap
|
|
934
|
+
? `cybernetics: 已有 ${running.size} 个运行中的轮,达到全局并行上限 ${cap}(可在设置面板调高 maxConcurrentRounds 或等待完成)`
|
|
935
|
+
: undefined
|
|
936
|
+
}
|
|
875
937
|
const objective = loop.objective ?? goalView?.objective ?? loop.title ?? ''
|
|
876
938
|
|
|
877
939
|
/**
|
|
@@ -904,6 +966,8 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
904
966
|
* goal's status is already 'running' when this is called.
|
|
905
967
|
*/
|
|
906
968
|
const launchBackground = async (target: LoopState): Promise<{ round: number }> => {
|
|
969
|
+
const blocked = concurrentCapError()
|
|
970
|
+
if (blocked) throw new Error(blocked)
|
|
907
971
|
const controller = new AbortController()
|
|
908
972
|
if (exec.signal) {
|
|
909
973
|
if (exec.signal.aborted) controller.abort()
|
|
@@ -1046,6 +1110,8 @@ export function apply(ctx: any, config: Partial<Config> = {}) {
|
|
|
1046
1110
|
if (running.has(loop.goalId!)) {
|
|
1047
1111
|
throw new Error(`cybernetics: goal "${loop.goalId}" already has a round in flight — collect or cancel it first`)
|
|
1048
1112
|
}
|
|
1113
|
+
const blocked = concurrentCapError()
|
|
1114
|
+
if (blocked) throw new Error(blocked)
|
|
1049
1115
|
loop.status = 'running'
|
|
1050
1116
|
// In-flight visibility: the blocking 'round' path can run for minutes, so
|
|
1051
1117
|
// publish before awaiting it (the async path needs no second publish —
|
package/workflows/round.js
CHANGED
|
@@ -36,13 +36,90 @@ const specFor = (key) => {
|
|
|
36
36
|
: {}
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// --- v0.8 调用级模型回退(call-level model fallback) --------------------------
|
|
40
|
+
// 判定信号来自 workflow runtime(@deepseek-ai/dsh-workflow-ptc)的 agent() hook:
|
|
41
|
+
// - 抛出 WorkflowError:code AGENT_START(消息 `agent() could not start a child: …`,
|
|
42
|
+
// provider 解析 / 子 agent spawn / 首调失败——恒为可用性失败)或 AGENT_RESULT
|
|
43
|
+
// (`child agent run failed: …`,消息内嵌失败原因,需命中可用性模式才回退);
|
|
44
|
+
// 其余抛出(INVALID_ARGUMENT / UNSUPPORTED_* / AGENT_CAP 等脚本或守卫错误)
|
|
45
|
+
// 原样上抛,绝不换模型。离线 harness 的 mock 错误没有 code 时按消息模式判定。
|
|
46
|
+
// - 返回 null:runtime 把子 agent 的失败压成 null 且不透传原因(子 run
|
|
47
|
+
// stopReason ≠ 'completed' —— LLM 调用中途失败(限额/网络/provider 报错经
|
|
48
|
+
// dsh-subagent 的 agent/error → stopReason 'error')或无结构化产出)。仅当该
|
|
49
|
+
// 角色配置了模型覆盖时,把 null 视为「覆盖模型疑似不可调用」,撤销覆盖以
|
|
50
|
+
// session agent 模型重试一次(无覆盖时 null 维持原失败语义——本来就跑在
|
|
51
|
+
// session 模型上,无处可退)。
|
|
52
|
+
// 逻辑失败是返回值层面的判定(task agent 正常交付 done=false 的报告、verify
|
|
53
|
+
// 给出未达成 verdict),从不触发回退。每次回退记录进 fallbacks(轮结果携带,
|
|
54
|
+
// 随审计日志落盘),并经 log() 出现在 workflow/log 事件流。
|
|
55
|
+
const fallbacks = []
|
|
56
|
+
|
|
57
|
+
const errorTextOf = (err) => {
|
|
58
|
+
try { return String((err && (err.stack || err.message)) || err) } catch { return '' }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 模型可用性失败的消息模式:限流/quota/鉴权/服务端不可用/网络/provider 解析。
|
|
62
|
+
const AVAILABILITY_PATTERNS = [
|
|
63
|
+
/could not start a child/i, // AGENT_START 包装
|
|
64
|
+
/no subagent provider registered/i, // 覆盖的 provider 未注册
|
|
65
|
+
// v0.8.3:配置的模型在模型设置里删除订阅(provider 仍在但该 model 不可路由)时
|
|
66
|
+
// 的典型报错形态(AGENT_RESULT 内嵌或无 code 的直抛),与既有模式互补。
|
|
67
|
+
/model not found|no such model|unknown model|invalid model|not subscribed|model .*not (?:available|enabled|found)/i,
|
|
68
|
+
/rate.?limit|\b429\b|too many requests/i, // 限流
|
|
69
|
+
/quota|insufficient|额度|余额|配额/i, // 限额 / quota 耗尽
|
|
70
|
+
/\b401\b|\b403\b|unauthorized|forbidden|invalid api key|authentication/i, // 鉴权
|
|
71
|
+
/overloaded|service unavailable|temporarily unavailable|\b502\b|\b503\b|\b529\b/i, // 服务端不可用
|
|
72
|
+
/ECONNREFUSED|ECONNRESET|ECONNABORTED|ETIMEDOUT|ENOTFOUND|EAI_AGAIN|ENETUNREACH|EHOSTUNREACH|socket hang up|fetch failed|network/i, // 网络不可达
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
const isAvailabilityFailure = (err) => {
|
|
76
|
+
const code = err && typeof err === 'object' ? err.code : undefined
|
|
77
|
+
if (code === 'AGENT_START') return true // 子 agent 启动即失败恒为可用性失败
|
|
78
|
+
const text = errorTextOf(err)
|
|
79
|
+
return AVAILABILITY_PATTERNS.some((re) => re.test(text))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const specLabel = (spec) => [spec.provider, spec.model].filter(Boolean).join('/')
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 以角色模型覆盖调用 agent();覆盖模型不可用(可用性错误或 null 无产出)时
|
|
86
|
+
* 回退到 session agent 的模型配置重试一次(不传 provider/model = 继承调用方)。
|
|
87
|
+
* 无覆盖 = 直接调用(无回退必要);非可用性错误原样上抛;重试再失败降级为
|
|
88
|
+
* null(沿用既有「本轮无结果」路径,回退记录仍可见)。
|
|
89
|
+
*/
|
|
90
|
+
const callRoleAgent = async (roleKey, prompt, opts) => {
|
|
91
|
+
const spec = specFor(roleKey)
|
|
92
|
+
const overridden = !!(spec.provider || spec.model)
|
|
93
|
+
try {
|
|
94
|
+
const result = await agent(prompt, { ...opts, ...spec })
|
|
95
|
+
if (result === null && overridden) {
|
|
96
|
+
const reason = 'agent() 返回 null(runtime 未透传原因:子 agent 未完成或无结构化产出),疑似覆盖模型不可调用'
|
|
97
|
+
fallbacks.push({ role: roleKey, from: spec, trigger: 'null-result', reason })
|
|
98
|
+
log(`[cybernetics fallback] ${roleKey} 角色覆盖模型 ${specLabel(spec)} 执行无产出,回退到 session agent 模型重试`)
|
|
99
|
+
try { return await agent(prompt, opts) } catch { return null }
|
|
100
|
+
}
|
|
101
|
+
return result
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (overridden && isAvailabilityFailure(err)) {
|
|
104
|
+
const reason = errorTextOf(err).split('\n')[0].slice(0, 300)
|
|
105
|
+
fallbacks.push({ role: roleKey, from: spec, trigger: 'availability-error', reason })
|
|
106
|
+
log(`[cybernetics fallback] ${roleKey} 角色覆盖模型 ${specLabel(spec)} 调用失败(${reason}),回退到 session agent 模型重试`)
|
|
107
|
+
try { return await agent(prompt, opts) } catch { return null }
|
|
108
|
+
}
|
|
109
|
+
throw err
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 轮结果统一携带回退记录(空数组 = 未发生回退)。 */
|
|
114
|
+
const withFallbacks = (out) => ({ ...out, fallbacks })
|
|
115
|
+
|
|
39
116
|
// v0.6 TeamExecutor: the persistent teammate already did the task work; the
|
|
40
117
|
// controller hands its report in as `taskReport`. Skip the task agent entirely
|
|
41
118
|
// and go straight to verify/triage (quick depth returns the derived verdict).
|
|
42
119
|
if (taskReport && typeof taskReport === 'object') {
|
|
43
120
|
const report = taskReport
|
|
44
121
|
if (depth === 'quick') {
|
|
45
|
-
return {
|
|
122
|
+
return withFallbacks({
|
|
46
123
|
round: state.round,
|
|
47
124
|
report,
|
|
48
125
|
verdict: {
|
|
@@ -53,10 +130,10 @@ if (taskReport && typeof taskReport === 'object') {
|
|
|
53
130
|
goalItselfQuestionable: false,
|
|
54
131
|
},
|
|
55
132
|
triage: { auto: [], defer: [], reject: [], human: [] },
|
|
56
|
-
}
|
|
133
|
+
})
|
|
57
134
|
}
|
|
58
135
|
await phase('verify')
|
|
59
|
-
const verdict = await
|
|
136
|
+
const verdict = await callRoleAgent('verify',
|
|
60
137
|
[
|
|
61
138
|
`你是独立的 verify agent:你没有、也不应获得 task agent 的叙述过程。`,
|
|
62
139
|
`下面是目标、工作委托书和 task agent 的报告,请基于证据判定本轮是否达成目标。`,
|
|
@@ -72,7 +149,7 @@ if (taskReport && typeof taskReport === 'object') {
|
|
|
72
149
|
``,
|
|
73
150
|
`判定标准:证据是否支撑报告的每一项结论;是否覆盖了本轮子目标;是否存在未验证的断言。`,
|
|
74
151
|
].join('\n'),
|
|
75
|
-
{ label: 'verify',
|
|
152
|
+
{ label: 'verify', schema: {
|
|
76
153
|
type: 'object', additionalProperties: false,
|
|
77
154
|
required: ['met', 'criteria', 'gaps', 'goalItselfQuestionable'],
|
|
78
155
|
properties: {
|
|
@@ -84,7 +161,7 @@ if (taskReport && typeof taskReport === 'object') {
|
|
|
84
161
|
} },
|
|
85
162
|
)
|
|
86
163
|
await phase('triage')
|
|
87
|
-
const triage = await
|
|
164
|
+
const triage = await callRoleAgent('triage',
|
|
88
165
|
[
|
|
89
166
|
`你是 triage agent:把 verify 的缺口拆成四类处置建议。`,
|
|
90
167
|
``,
|
|
@@ -94,7 +171,7 @@ if (taskReport && typeof taskReport === 'object') {
|
|
|
94
171
|
`【本轮子目标】`,
|
|
95
172
|
subgoal || '',
|
|
96
173
|
].join('\n'),
|
|
97
|
-
{ label: 'triage',
|
|
174
|
+
{ label: 'triage', schema: {
|
|
98
175
|
type: 'object', additionalProperties: false,
|
|
99
176
|
required: ['auto', 'defer', 'reject', 'human'],
|
|
100
177
|
properties: {
|
|
@@ -106,9 +183,9 @@ if (taskReport && typeof taskReport === 'object') {
|
|
|
106
183
|
} },
|
|
107
184
|
)
|
|
108
185
|
if (verdict === null || triage === null) {
|
|
109
|
-
return { round: state.round, report, failed: 'verify/triage agent 失败,本轮无结论' }
|
|
186
|
+
return withFallbacks({ round: state.round, report, failed: 'verify/triage agent 失败,本轮无结论' })
|
|
110
187
|
}
|
|
111
|
-
return { round: state.round, report, verdict, triage }
|
|
188
|
+
return withFallbacks({ round: state.round, report, verdict, triage })
|
|
112
189
|
}
|
|
113
190
|
|
|
114
191
|
await phase('task')
|
|
@@ -119,7 +196,7 @@ await phase('task')
|
|
|
119
196
|
// enough to fully drive execution) falling back to the durable objective.
|
|
120
197
|
const workOrder = (brief && brief.trim()) || goal.objective
|
|
121
198
|
|
|
122
|
-
const report = await
|
|
199
|
+
const report = await callRoleAgent(role ?? 'default',
|
|
123
200
|
[
|
|
124
201
|
`你是一个独立的 task agent:不继承任何对话历史,从零开始工作。`,
|
|
125
202
|
`下面这份工作委托书是你可以获得的全部任务信息,它被要求详细到足以让你独立完成当前工作;`,
|
|
@@ -142,7 +219,7 @@ const report = await agent(
|
|
|
142
219
|
`完成后交付:summary、done、artifacts、evidence、nextStepThoughts。`,
|
|
143
220
|
`nextStepThoughts 只记录"值得做但超出本轮子目标"的后续工作线索,不要执行它们。`,
|
|
144
221
|
].join('\n'),
|
|
145
|
-
{ label: `task:${role ?? 'default'}`,
|
|
222
|
+
{ label: `task:${role ?? 'default'}`, schema: {
|
|
146
223
|
type: 'object', additionalProperties: false,
|
|
147
224
|
required: ['summary', 'done', 'artifacts', 'evidence', 'nextStepThoughts'],
|
|
148
225
|
properties: {
|
|
@@ -155,14 +232,14 @@ const report = await agent(
|
|
|
155
232
|
)
|
|
156
233
|
|
|
157
234
|
if (report === null) {
|
|
158
|
-
return { round: state.round, failed: 'task agent 失败,本轮无结果' }
|
|
235
|
+
return withFallbacks({ round: state.round, failed: 'task agent 失败,本轮无结果' })
|
|
159
236
|
}
|
|
160
237
|
|
|
161
238
|
// quick depth: lightweight analysis — one agent, one round, direct feedback.
|
|
162
239
|
// Skip verify/triage entirely; derive a verdict-shaped outcome from the
|
|
163
240
|
// task report so the controller's decision logic works unchanged.
|
|
164
241
|
if (depth === 'quick') {
|
|
165
|
-
return {
|
|
242
|
+
return withFallbacks({
|
|
166
243
|
round: state.round,
|
|
167
244
|
report,
|
|
168
245
|
verdict: {
|
|
@@ -173,12 +250,12 @@ if (depth === 'quick') {
|
|
|
173
250
|
goalItselfQuestionable: false,
|
|
174
251
|
},
|
|
175
252
|
triage: { auto: [], defer: [], reject: [], human: [] },
|
|
176
|
-
}
|
|
253
|
+
})
|
|
177
254
|
}
|
|
178
255
|
|
|
179
256
|
await phase('verify')
|
|
180
257
|
|
|
181
|
-
const verdict = await
|
|
258
|
+
const verdict = await callRoleAgent('verify',
|
|
182
259
|
[
|
|
183
260
|
`你是 verify agent,独立验收 task agent 的交付。你只对照目标判定达成度,把目标拆成可判定的方面逐条列出并判定,每个判断必须给出证据;`,
|
|
184
261
|
`不重做工作,不接受"大概完成了"。`,
|
|
@@ -192,7 +269,7 @@ const verdict = await agent(
|
|
|
192
269
|
``,
|
|
193
270
|
`若你认为是目标本身有问题(而非执行缺口),置 goalItselfQuestionable=true。`,
|
|
194
271
|
].join('\n'),
|
|
195
|
-
{ label: 'verify',
|
|
272
|
+
{ label: 'verify', schema: {
|
|
196
273
|
type: 'object', additionalProperties: false,
|
|
197
274
|
required: ['met', 'criteria', 'gaps', 'goalItselfQuestionable'],
|
|
198
275
|
properties: {
|
|
@@ -209,12 +286,12 @@ const verdict = await agent(
|
|
|
209
286
|
)
|
|
210
287
|
|
|
211
288
|
if (verdict === null) {
|
|
212
|
-
return { round: state.round, failed: 'verify agent 失败,本轮无结果', report }
|
|
289
|
+
return withFallbacks({ round: state.round, failed: 'verify agent 失败,本轮无结果', report })
|
|
213
290
|
}
|
|
214
291
|
|
|
215
292
|
await phase('triage')
|
|
216
293
|
|
|
217
|
-
const triage = await
|
|
294
|
+
const triage = await callRoleAgent('triage',
|
|
218
295
|
[
|
|
219
296
|
`你是 goal agent。对 task agent 提出的后续工作线索做三分拣:`,
|
|
220
297
|
`- auto:目标内、低风险、可自动执行的下一步工作`,
|
|
@@ -229,7 +306,7 @@ const triage = await agent(
|
|
|
229
306
|
`待分拣线索:`,
|
|
230
307
|
...report.nextStepThoughts.map((s, i) => `${i + 1}. ${s}`),
|
|
231
308
|
].join('\n'),
|
|
232
|
-
{ label: 'triage',
|
|
309
|
+
{ label: 'triage', schema: {
|
|
233
310
|
type: 'object', additionalProperties: false,
|
|
234
311
|
required: ['auto', 'defer', 'reject', 'human'],
|
|
235
312
|
properties: {
|
|
@@ -242,4 +319,4 @@ const triage = await agent(
|
|
|
242
319
|
} },
|
|
243
320
|
)
|
|
244
321
|
|
|
245
|
-
return { round: state.round, report, verdict, triage: triage ?? { auto: [], defer: [], reject: [], human: [] } }
|
|
322
|
+
return withFallbacks({ round: state.round, report, verdict, triage: triage ?? { auto: [], defer: [], reject: [], human: [] } })
|