人手改的那一手真正落得下去;界面不撒谎那几处补齐

手改是**兜 AI 的错**的那一手 —— 这一轮把它从「摆在那儿但落不了地」做成真的:

件上就地改
· 保存**按地址收**,不看当下那张卡:载荷从前借道工单的 `blocks[i]` 取列名,
  于是熔断卡(没有 blocks)在屏上时,人补的那一格一个字都送不出去 ——
  而他要补的恰恰是把这一单卡住的那个必填格。授权面改由活动清单的台账声明把关。
· **能增删行**:错不止「这一格填错了」,还有「这一行不该在」与「漏了一行」。
  删挂记号不当场抹掉(看得见、可后悔),且**删要说出来**(`drops`)——
  「没回传就是删了」会把一次不完整的回传变成一次删表。
· **必填没填按不下提交**:与放行门 L1 同一个数同一套算法;闸上摆出差在哪一行哪一格。
· 只写真改了的那几行 —— 改一格却留「修订 4 行」的痕,那句话是假的。

判据与门
· 裁量判据带谓词时**谓词说了算**:「选填缺口已补齐」基座数得出来,
  人填好的目标价它照样判「客户未提供」的毛病由此消掉。
· 验收卡判据**读时现算** —— 人在成果件上补一格,判据跟着变。
· 放行门认「**办完了没有**」不认「有人写过」:从前写个 `false` 就把门推开,
  于是一单没打标就进了放行门。三个数全由基座现数。
· 拒回把「活没干完」说成「数或行对不上」的毛病:判据点名的表一行都没写时直说那一步没办。
  **指向两扇打不开的门,比不指还糟。**

界面不撒谎
· 日志只有三色:负责人绿 · 智能体蓝 · 系统灰(归侧五种是度量的分法,不是给人看的)。
· 验收收口答两件事:落了什么(按**交付物**点名,一份成果的几张表并成一行)、
  往哪儿去(转出哪一口、带过去哪些字段);一个口都没发就直说到此为止。
· 签完到收尾之间那一段有自己的话「字已签下,正在落地」—— 中间态本来就该说出来,只是要说对。
· 消息不再被内部事件刷屏:只有从通道进来的件才谈得上「无人认领」。
· 建客户公司与联系人**一次确认落两行**;联系人编码自增不问人;卡面摆表的人读名。

A1 三案例端到端全 released:全 A 类直通 12 拍 / 客户点名跳一条 10 拍 / 新客户缺必填 14 拍。
五道门禁全绿(971 passed)。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JsTDjoHovERXyy6Wmw1ALp
This commit is contained in:
2026-08-02 13:41:44 +08:00
co-authored by Claude Opus 5
parent 94f1c7a4ef
commit 259192f736
228 changed files with 27850 additions and 2847 deletions
+58 -9
View File
@@ -21,9 +21,11 @@ import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { Agent } from '@mastra/core/agent';
import { z } from 'zod';
import type { TaskId } from '@contracts';
import type { AgentAction, EnabledSet } from '@modules/loop-engine';
import type { AgentAction, AgentTurn, EnabledSet } from '@modules/loop-engine';
import type { ModelGateway } from '@modules/llm';
import type { BusTool } from '@modules/toolbox';
import { ledgerDictionary } from './agent-runner.ts';
import type { WritableLedger } from './agent-runner.ts';
import { buildMastraTools } from './tool-bus.ts';
import type { ToolBusContext, ToolBusPorts } from './tool-bus.ts';
@@ -35,6 +37,10 @@ const TURN_RESULT = z.object({
criterion: z.string().optional().describe('judge:评估的是哪条语义性前提'),
satisfied: z.boolean().optional().describe('judge:满足与否'),
rationale: z.string().optional().describe('结论理由 —— 留痕要看,写实'),
/** 对负责人说的一句人话 —— 每一拍都要带(他正看着这一单) */
say: z.string().optional().describe('对负责人说的一句人话:这一拍干了什么、拿到什么结论'),
tag: z.enum(['guidance', 'criterion', 'conclusion']).optional(),
view: z.string().optional().describe('可选:换成哪张局面模版'),
});
export type MastraAgentRunnerDeps = {
@@ -54,10 +60,18 @@ export type MastraAgentRunnerDeps = {
};
export function createMastraAgentRunner(deps: MastraAgentRunnerDeps): {
decide(input: { taskId: TaskId; narrative: string; enabled: EnabledSet; scope: Record<string, unknown> }): Promise<AgentAction>;
decide(input: {
taskId: TaskId;
narrative: string;
enabled: EnabledSet;
scope: Record<string, unknown>;
writable: WritableLedger;
/** 开环路径的工具清单来自 `busOf`(它自己会取),这里只为满足同一个端口 */
tools?: unknown;
}): Promise<AgentTurn>;
} {
return {
async decide({ taskId, narrative, enabled, scope }) {
async decide({ taskId, narrative, enabled, scope, writable }) {
const modelName = await deps.modelOf(taskId);
const base = await deps.contextOf(taskId);
const bus = await deps.busOf(taskId);
@@ -75,8 +89,16 @@ export function createMastraAgentRunner(deps: MastraAgentRunnerDeps): {
'③ **终局唯放行**:成果齐了就 submit(提交放行),由人验收 —— 你不能自己宣布完成。',
'④ 干完活必须给出这一拍的结论(见输出格式);拿不准就 idle 并说清卡在哪。',
'',
'⑥ **每一拍都要带一句 say**:对负责人说人话(他正看着这一单;只干活不说话,他的对话区是空的)。',
'⑤ **成果就是台账里的行**:干出来的结论用 ledger.write 逐行写进台账(草稿层),',
' 不写就等于没做 —— 验收判据读的是台账,出口载荷也从台账推。只准写字典里的地址与列名。',
'',
`【案例说明书与记忆】\n${narrative}`,
].join('\n');
'',
ledgerDictionary(writable),
]
.filter((line) => line !== '')
.join('\n');
const endpoint = await deps.gateway.resolve(modelName);
const provider = createOpenAICompatible({
@@ -99,11 +121,16 @@ export function createMastraAgentRunner(deps: MastraAgentRunnerDeps): {
.map((e) => `- ${e.id} ${e.name}(${e.type}${e.requiresManualStart ? ' · 须你启动' : ''}${e.discretionary ? ' · 裁量项' : ''})`)
.join('\n') || '(空)'
}`,
enabled.inProgress.length === 0
? ''
: `【进行中(已开工,未交成果)】\n${enabled.inProgress.map((e) => `- ${e.id} ${e.name}`).join('\n')}`,
`【目标缺口】\n${enabled.gaps.join('、') || '(无)'}`,
enabled.judgements.length === 0
? ''
: `【待你评估的语义性前提】\n${enabled.judgements.map((j) => `- ${j.criterion}:${j.statement}`).join('\n')}`,
enabled.entries.length === 0 ? '【注意】当下可为 ∩ 目标所需 = 空 —— 这是依赖断点,调人或说清卡在哪。' : '',
enabled.entries.length === 0 && enabled.inProgress.length === 0
? '【注意】当下可为与进行中都空 —— 这是依赖断点,调人或说清卡在哪。'
: '',
`【任务数据】\n${JSON.stringify(scope).slice(0, 4000)}`,
'',
'干这一拍该干的活(可以连续调工具),最后给出结论。',
@@ -122,11 +149,16 @@ export function createMastraAgentRunner(deps: MastraAgentRunnerDeps): {
if (tokens > 0) await deps.onUsage(taskId, tokens);
// 调过人就必须收在 asked:卡已发、监听已立,引擎只收尾挂起(再发一张就是重复调人)
const object = (result as { object?: unknown }).object;
const said = sayOf(object);
if (trace.askedCardId !== undefined) {
return { kind: 'asked', cardId: trace.askedCardId, rationale: '本回合已调人,等回传' };
return {
actions: [{ kind: 'asked', cardId: trace.askedCardId, rationale: '本回合已调人,等回传' }],
...(said === undefined ? {} : { say: said }),
};
}
return normalizeTurn((result as { object?: unknown }).object, enabled);
// 开环一拍 = Mastra 在拍内自主连环调工具,拍末只交一个收束行动 —— 包成单元素串
return { actions: [normalizeTurn(object, enabled)], ...(said === undefined ? {} : { say: said }) };
} catch (error) {
/*
* 这一拍问不出结果(端点不可达、鉴权失败、超时、工具连环炸)——
@@ -137,10 +169,12 @@ export function createMastraAgentRunner(deps: MastraAgentRunnerDeps): {
* 不要再叠一张「模型出错」的卡上去。
*/
if (trace.askedCardId !== undefined) {
return { kind: 'asked', cardId: trace.askedCardId, rationale: '本回合已调人(其后模型出错)' };
return { actions: [{ kind: 'asked', cardId: trace.askedCardId, rationale: '本回合已调人(其后模型出错)' }] };
}
const detail = error instanceof Error ? error.message : String(error);
return {
say: { text: `这一轮我没能决断:${detail}`, tag: 'conclusion' as const },
actions: [{
kind: 'human',
itemId: enabled.entries[0]?.id ?? 'breakpoint',
workOrder: 'execution',
@@ -154,12 +188,27 @@ export function createMastraAgentRunner(deps: MastraAgentRunnerDeps): {
ask: '请判断下一步:接着走哪一项,或补齐缺口后让环继续。',
fields: ['decision'],
},
}],
};
}
},
};
}
/** 拍末那句交代 —— 每一拍都该有;没给就没有,不替它编 */
function sayOf(object: unknown): { text: string; tag?: 'guidance' | 'criterion' | 'conclusion'; view?: string } | undefined {
const raw = (object ?? {}) as Record<string, unknown>;
const text = String(raw['say'] ?? '').trim();
if (text === '') return undefined;
const tag = raw['tag'];
const view = String(raw['view'] ?? '').trim();
return {
text,
...(tag === 'guidance' || tag === 'criterion' || tag === 'conclusion' ? { tag } : {}),
...(view === '' ? {} : { view }),
};
}
/** 拍末结论归一为 AgentAction;给不出结论就当空拍(idle → judge 留痕),绝不乱动世界 */
function normalizeTurn(object: unknown, enabled: EnabledSet): AgentAction {
const raw = (object ?? {}) as Record<string, unknown>;