Refine AI implementation spec and data flow guidance

This commit is contained in:
hy committed 2026-03-23 12:54:06 +08:00
1 parent 683fb8d385
commit 403fc0d706
3 files changed
+481 -434

No files matched your search

+1 -1
View File
@@ -4,7 +4,7 @@
- 当涉及技术栈、整体架构、进程边界、本地优先约束或混合检索链路时,请阅读 `doc/architecture.md`
- 当涉及 AI 总体架构、Main/Human 分工、复杂度路由、主流程或整体协作关系时,请阅读 `doc/ai-design/architecture.md`
- 当涉及 `main`/`human` 的实现规则、节点行为、关键伪代码、`deliveryMode`、`watchHint`、`artifacts` 责任边界、最近决策历史、`task_handoff` 或 `context_note` 的运行语义时,请阅读 `doc/ai-design/implementation-spec.md`
- 当涉及 `main`/`human` 的实现规则、节点行为、状态转移、`deliveryMode`、`watchHint`、`artifacts` 责任边界、最近决策历史、`task_handoff` / `context_note` 运行语义,或 `LangGraph`、`chatStructured` 等 AI 技术实现约束时,请阅读 `doc/ai-design/implementation-spec.md`
- 当涉及 `main`/`human` 状态字段、数据契约、`deliveryMarkdown` 交接结构、`contextEvidence` 提取规则、`fact/evidence` 引用约束、`artifacts` 契约、Main/Human 数据流转或流转图时,请阅读 `doc/ai-design/data-definition-and-flow.md`
- 当涉及 AI 未来重构规划、迭代方向、未决方案、下一次准备怎么改时,请阅读 `doc/ai-design/refactor-plan.md`
- 当涉及 `human` 的 skill 集合、skill 命名、skill 拆分边界、`skillCatalog` / `skillRegistry` 或 skill 与 tools 实现边界时,请阅读 `doc/ai-design/human-skill-design.md`
@@ -6,6 +6,7 @@
- `main` 负责 run 级编排、计划调度和最终汇总。
- `human` 负责单任务闭环执行。
- 节点行为、技术实现约束与跨层规则见 `implementation-spec.md`。
- 本文只定义状态、契约与数据流转,不展开架构动机与阅读导引。
## 2. `main` 数据定义
+479 -433
View File
@@ -1,502 +1,548 @@
# NCurator-Desktop AI 实现规范
本文定义 `main + human` 的实现规则。代码实现应优先对齐本文档。
本文定义 `main + human` 的当前实现契约。代码实现应优先对齐本文档;本文中的状态转移只描述 graph 行为
## 1. `main` 实现主线
## 1. `main`
### 1.1 `main` 主循环
### 1.1 运行模型
```ts
function runMain(input) {
const route = runMainIntent(input)
- `main` 以 `@langchain/langgraph` 的 `StateGraph` 为运行骨架,状态 schema 由 `main/state/schema.ts` 提供。
- `main` graph 的节点返回 `Partial<AiRunState>` 形式的 state patch,由 graph 负责状态推进。
- `main` 的条件路由由 `MainState.mainStage` 和 `MainState.status` 决定,不在 graph 外额外手写控制循环。
if (route === 'direct_human') {
const output = dispatchHuman(
{ taskId: 'direct', userInput: input },
{ deliveryMode: 'user_answer' }
)
return completeRun(output)
}
状态转移:
const contextOutput = dispatchHuman(
{ taskId: 'context', userInput: buildContextObjective(input) },
{ deliveryMode: 'context_note' }
)
const contextEvidence = extractContextEvidence(contextOutput)
const plan = buildPlan(input, contextEvidence)
const taskOutputs = []
for (const task of plan) {
const output = dispatchHuman(task, { deliveryMode: 'task_handoff' })
taskOutputs.push(output)
updatePlanState(task, output)
if (output.status === 'blocked') {
break
}
}
const finalOutput = dispatchHuman(
{ taskId: 'synthesis', userInput: buildSynthesisObjective(input, taskOutputs) },
{ deliveryMode: 'user_answer' }
)
return completeRun(finalOutput)
}
```text
START -> main_intent
main_intent -> main_direct_human_exec | main_context_human | main_plan | main_plan_exec | main_synthesis_human | main_complete
main_context_human -> main_intent
main_direct_human_exec -> main_complete
main_plan -> main_plan_exec | main_complete
main_plan_exec -> main_plan_exec | main_synthesis_human
main_synthesis_human -> main_complete
main_complete -> END
```
对应数据结构:
- `MainState`
- `MainPlanTask`
- `AiArtifactRef`
- `HumanTaskInput`
- `HumanRunDispatchOptions`
- `HumanTaskOutput`
对应流转图:
- `main` 直接路径:`direct human`
- `main` 阶段一:输入进入 `context / plan`
- `main` 阶段二:计划派发、结果回写、最终收尾
### 1.2 `main_intent`
```ts
function runMainIntent(input) {
if (canBeHandledBySingleHuman(input)) {
return 'direct_human'
}
return 'complex_pipeline'
}
```
实现要求:
- 默认优先选择 `direct_human`。
- 只有当当前单 `human` 直接闭环明显不稳时,才进入复杂链路。
- `main_intent` 只做 orchestration 复杂度判断,不负责展开求解。
- 运行模型:LangGraph 节点
- 触发条件:
- graph 起点
- `main_context_human` 返回后重新判断
- 读取:
- `MainState.input`
- `MainState.contextRounds`
- `MainState.contextEvidence`
- 会话历史摘要与最近对话
- `ctx.selectedScopeSummary`
- 回写:
- `MainState.mainStage`
- 必要时回写 `MainState.contextObjective`
- 下一跳:
- `main_direct_human_exec`
- `main_context_human`
- `main_plan`
- 实现约束:
- 节点入口先调用 `ctx.checkCancelled()`
- 超过上下文补充轮次上限时,直接进入 `main_plan`
- 模型输出必须通过 `ctx.chatStructured`
- prompt 必须通过 `buildIntentPrompt` 组织
- 节点返回值应保持为 `Partial<AiRunState>`
- 对应数据结构:
- `MainState.input`
- `MainState.contextRounds`
- `MainState.contextObjective`
- `MainState.mainStage`
- 对应流转图:
- `main` 直接路径:`direct human`
- `main` 阶段一:输入进入 `context / plan`
### 1.3 `main_context_human`
```ts
function buildContextObjective(input) {
return describeWhatContextStageNeedsToClarify(input)
}
- 运行模型:LangGraph 节点
- 触发条件:`MainState.mainStage = main_context_human`
- 读取:
- `MainState.input`
- `MainState.contextObjective`
- `MainState.contextEvidence`
- `MainState.contextRounds`
- 调用:
- 派发一个 `human` 任务
- `deliveryMode = context_note`
- `internalDelivery = true`
- 回写:
- `MainState.contextRounds += 1`
- `MainState.contextEvidence`
- `MainState.contextObjective = undefined`
- `MainState.mainStage = main_intent`
- 下一跳:
- `main_intent`
- 实现约束:
- `taskId` 使用 `context-${contextRounds + 1}` 形式
- `userInput` 通过 `buildContextHumanObjective` 生成,不直接把原始输入原样派发
- `contextEvidence` 优先从 `context_note` 的可复用证据区提取
- 提取结果通过去重/追加逻辑并入现有 `contextEvidence`
- 对应数据结构:
- `MainState.contextObjective`
- `MainState.contextRounds`
- `MainState.contextEvidence`
- `HumanTaskInput`
- `HumanTaskOutput`
- 对应流转图:
- `main` 阶段一:输入进入 `context / plan`
function runContextHuman(input) {
return dispatchHuman(
{ taskId: 'context', userInput: buildContextObjective(input) },
{ deliveryMode: 'context_note' }
)
}
```
### 1.4 `main_direct_human_exec`
实现要求:
- 运行模型:LangGraph 节点
- 触发条件:`MainState.mainStage = main_direct_human_exec`
- 读取:
- `MainState.input`
- `MainState.contextEvidence`
- 调用:
- 派发一个 `human` 任务
- `deliveryMode = user_answer`
- `streamAnswer = true`
- 回写:
- `MainState.finalDeliveryMarkdown`
- `MainState.finalArtifacts`
- `MainState.mainStage = main_complete`
- 下一跳:
- `main_complete`
- 实现约束:
- `taskId` 固定为 direct human 专用任务 id
- `userInput` 通过 `buildDirectHumanObjective` 生成
- `main` 只消费 `HumanTaskOutput`,不直接读取 `human` 内部状态
- 对应数据结构:
- `MainState.finalDeliveryMarkdown`
- `MainState.finalArtifacts`
- `HumanTaskOutput`
- 对应流转图:
- `main` 直接路径:`direct human`
- `contextObjective` 是本轮上下文补充阶段的临时目标。
- `main_context_human` 的输出主要用于提取 `contextEvidence`,不是直接代替最终交付。
### 1.5 `main_plan`
### 1.4 `main_plan`
```ts
function buildPlan(input, contextEvidence) {
return splitGoalIntoHumanExecutableTasks(input, contextEvidence)
}
```
实现要求:
- 计划任务必须能交给单个 `human` 闭环执行。
- 计划任务的正式交付载体是 `task_handoff`。
### 1.5 `main_plan_exec`
```ts
function executePlan(plan) {
const outputs = []
for (const task of plan) {
const output = dispatchHuman(task, { deliveryMode: 'task_handoff' })
outputs.push(output)
updatePlanState(task, output)
if (output.status === 'blocked') {
return outputs
}
}
return outputs
}
```
实现要求:
- 当前链路按顺序串行派发任务。
- 每个任务的 `status / deliveryMarkdown / artifacts` 都要回写到计划状态。
- `blocked` 表示任务已经派发且最终阻塞,不与未执行状态混用。
### 1.5.1 计划任务回写
```ts
function updatePlanState(plan, taskId, output) {
const nextPlan = plan.map((task) => {
if (task.id !== taskId) {
return task
}
return {
...task,
status: output.status,
deliveryMarkdown: output.deliveryMarkdown,
artifacts: output.artifacts
}
})
const dispatchCursor = findNextReadyTaskIndex(nextPlan)
return { nextPlan, dispatchCursor }
}
```
实现要求:
- 回写时只更新当前已派发任务。
- `dispatchCursor` 始终指向下一项 `ready` 任务;不存在时表示计划阶段已完成。
- `task_handoff` 是计划阶段唯一正式交接载体。
### 1.6 `main_synthesis_human`
```ts
function runSynthesisHuman(input, taskOutputs) {
return dispatchHuman(
{ taskId: 'synthesis', userInput: buildSynthesisObjective(input, taskOutputs) },
{ deliveryMode: 'user_answer' }
)
}
```
实现要求:
- `main_synthesis_human` 负责汇总多个 `task_handoff`。
- 该阶段面向最终用户生成 `user_answer`。
## 2. `human` 实现主线
### 2.1 `human` 主循环
```ts
function runHuman(task, options) {
const state = initHumanState(task)
while (state.runtime.status === 'running') {
const decision = runBrain(state, options)
applyMemoryOps(state, decision.memoryOps)
state.lastBrainDecision = decision
appendBrainDecisionHistory(state, decision)
if (decision.nextAction.action === 'learning') {
runLearning(state, decision.nextAction.wantLearnSkillIds)
continue
}
if (decision.nextAction.action === 'hand') {
runHand(state, decision.nextAction.parallelCallToolParam)
continue
}
if (decision.nextAction.action === 'mouth') {
const mouthOutput = runMouth(state, options.deliveryMode)
return finalizeHumanResult(state, mouthOutput, decision.nextAction.status)
}
}
}
```
实现要求:
- `brain` 是唯一控制中心。
- `learning / hand / mouth` 都是执行节点,不扩张为额外推理节点。
- `human` 的执行风格是逐步推进直到收敛或稳定阻塞。
### 2.2 决策历史沉淀
```ts
function appendBrainDecisionHistory(state, decision) {
const record = {
...decision,
createdAt: Date.now()
}
state.brainMemory.outside.brainDecisions = [
...state.brainMemory.outside.brainDecisions,
record
].slice(-10)
}
function buildRecentDecisionDigest(records) {
return records.map((record) => ({
decision: record.decision,
nextAction: record.nextAction,
createdAt: record.createdAt
}))
}
```
实现要求:
- `brainDecisions` 只保留最近 10 条。
- `brain` 回看历史时只消费 `decision / nextAction / createdAt` 摘要。
- `lastBrainDecision` 只服务当前分发,不进入下一轮 `brain` 的直接输入。
### 2.3 `human` result 收口
```ts
function finalizeHumanResult(state, mouthOutput, status) {
state.runtime.status = status
state.runtime.stage = 'mouth'
state.result = {
status,
summary: mouthOutput.summary,
deliveryMarkdown: mouthOutput.deliveryMarkdown,
artifacts: state.runtimeArtifacts
}
return {
taskId: state.task.taskId,
...state.result
}
}
```
实现要求:
- `HumanResult.status` 由运行时按 `nextAction.status` 回写。
- `runtimeArtifacts` 由运行时透传到 `result`,不是由 `mouth` 模型生成。
- `HumanTaskOutput` 是 `main` 与 `human` 之间的稳定交接结构。
## 3. `brain` 实现规则
### 3.1 判断顺序
```ts
function runBrain(state) {
if (canFinishNow(state)) {
return nextActionMouth('completed')
}
if (shouldStopAsBlocked(state)) {
return nextActionMouth('blocked')
}
if (needSkillExplanation(state)) {
return nextActionLearning(selectRequiredSkills(state))
}
if (knowExactToolCalls(state)) {
return nextActionHand(buildToolCalls(state))
}
return nextActionMouth('blocked')
}
```
实现要求:
- 优先判断是否已经可以结束。
- 再判断是否需要补 skill。
- 最后判断是否需要调用工具。
- 当没有足够依据继续推进、也没有明确下一步动作时,应优先收敛为 `blocked`,而不是继续漂移。
### 3.2 `brain` 输入输出
- 输入:
- `task`
- `brainMemory`
- `lastParallelCallToolResults`
- 最近决策历史摘要
- `watchHint`
- 运行模型:LangGraph 节点
- 触发条件:`MainState.mainStage = main_plan`
- 读取:
- `MainState.input`
- `MainState.contextEvidence`
- 会话历史摘要与最近对话
- `ctx.selectedScopeSummary`
- 输出:
- `memoryOps`
- `decision`
- `nextAction`
- 计划任务列表
- 回写:
- `MainState.plan`
- `MainState.dispatchCursor = 0`
- `MainState.mainStage = main_plan_exec`
- 失败时回写 `MainState.status = failed`、`MainState.error`
- 下一跳:
- `main_plan_exec`
- `main_complete`
- 实现约束:
- 模型输出必须通过 `ctx.chatStructured`
- prompt 必须通过 `buildPlanPrompt` 组织
- 计划结果必须先经过 `validatePlannerTasks` 校验,再转换为 `MainPlanTask`
- 每个计划任务都必须能交给单个 `human` 闭环执行
- 对应数据结构:
- `MainState.plan`
- `MainState.dispatchCursor`
- `MainPlanTask`
- 对应流转图:
- `main` 阶段一:输入进入 `context / plan`
实现要求:
### 1.6 `main_plan_exec`
- `brain` 只通过 `memoryOps` 更新 `brainMemory.inside`。
- `brain` 自身不直接读 `lastBrainDecision` 作为回看输入。
- 回看材料只使用最近决策历史摘要。
- 运行模型:LangGraph 节点
- 触发条件:`MainState.mainStage = main_plan_exec`
- 读取:
- `MainState.plan`
- `MainState.dispatchCursor`
- `MainState.input`
- `MainState.contextEvidence`
- 之前任务的 `deliveryMarkdown`
- `ctx.selectedScopeSummary`
- 调用:
- 按 `dispatchCursor` 串行派发 `human`
- `deliveryMode = task_handoff`
- 回写:
- 当前任务 `status`
- 当前任务 `deliveryMarkdown`
- 当前任务 `artifacts`
- `MainState.dispatchCursor`
- `MainState.mainStage`
- 下一跳:
- `main_plan_exec`
- `main_synthesis_human`
- 实现约束:
- 当前策略是串行 `stop-dispatch`
- 没有待执行任务时直接进入 `main_synthesis_human`
- 任务派发前先把当前任务标记为 `running`
- `blocked` 任务会停止后续业务任务,但仍进入 synthesis 收口
- 回写时只更新当前已派发任务,不重写其他任务结果
- 对应数据结构:
- `MainState.plan`
- `MainState.dispatchCursor`
- `MainPlanTask.status`
- `MainPlanTask.deliveryMarkdown`
- `MainPlanTask.artifacts`
- `HumanTaskOutput`
- 对应流转图:
- `main` 阶段二:计划派发、结果回写、最终收尾
## 4. `memoryReducer` 实现规则
### 1.7 `main_synthesis_human`
```ts
function applyMemoryOps(state, memoryOps) {
if (!validateIndexes(memoryOps)) {
return writeWatchHint(state, 'memory edit index invalid')
}
- 运行模型:LangGraph 节点
- 触发条件:`MainState.mainStage = main_synthesis_human`
- 读取:
- `MainState.input`
- `MainState.contextEvidence`
- `MainState.plan`
- `ctx.selectedScopeSummary`
- 调用:
- 派发一个 `human` 任务
- `deliveryMode = user_answer`
- `streamAnswer = true`
- 回写:
- `MainState.finalDeliveryMarkdown`
- `MainState.finalArtifacts`
- `MainState.mainStage = main_complete`
- 下一跳:
- `main_complete`
- 实现约束:
- synthesis 只面向最终用户输出,不再生成新的计划任务
- `finalArtifacts` 需要合并计划阶段已有 artifacts 与 synthesis 输出 artifacts
- `userInput` 通过 `buildSynthesisHumanObjective` 组织
- 对应数据结构:
- `MainState.plan`
- `MainState.finalDeliveryMarkdown`
- `MainState.finalArtifacts`
- `HumanTaskOutput`
- 对应流转图:
- `main` 阶段二:计划派发、结果回写、最终收尾
const evidenceIdsAfterPatch = simulateEvidenceIdsAfterPatch(state, memoryOps)
### 1.8 `main_complete`
if (!allFactEvidenceRefsValid(state, memoryOps, evidenceIdsAfterPatch)) {
return writeWatchHint(state, 'fact/evidence references invalid')
}
- 运行模型:LangGraph 节点
- 触发条件:`MainState.mainStage = main_complete`
- 读取:
- `MainState.status`
- `MainState.finalDeliveryMarkdown`
- `MainState.finalArtifacts`
- 回写:
- 正常路径下回写 `MainState.status = completed`
- 保持 `MainState.mainStage = main_complete`
- 下一跳:
- `END`
- 实现约束:
- `main_complete` 只负责 run 级收尾,不再派发模型节点
- 已处于 `failed / cancelled / completed` 的 run 不应被二次覆盖
- 对应数据结构:
- `MainState.status`
- `MainState.finalDeliveryMarkdown`
- `MainState.finalArtifacts`
- 对应流转图:
- `main` 直接路径:`direct human`
- `main` 阶段二:计划派发、结果回写、最终收尾
commitInsideMemoryPatch(state, memoryOps)
}
## 2. `human`
### 2.1 运行模型
- `human` 以 `@langchain/langgraph` 的 `StateGraph` 为运行骨架,状态 schema 由 `human/state/schema.ts` 提供。
- `human` graph 的节点返回下一版 `HumanState`;调度仍由 graph 决定,不在 runner 外手写循环。
- `createHumanGraphRunner` 负责创建 graph、注入 `deliveryMode` 与 runtime cache,并把最终 `HumanState` 转换为 `HumanTaskOutput`。
- 当前 runner 使用固定 recursion limit,防止 graph 漂移。
状态转移:
```text
START -> brain
brain -> learning | hand | mouth | brain
learning -> brain
hand -> brain
mouth -> brain | END
```
实现要求:
对应数据结构:
- `facts[].evidenceIds` 只能引用当前仍存在或本轮新增的 evidence id。
- 引用校验不得依赖 `memoryOps` 顺序。
- 任何会导致悬空 `evidenceIds` 的编辑都视为整组非法编辑。
- 非法编辑只写入 `watchHint`,不自动级联修正事实。
- `HumanState`
- `BrainEvidence`
- `BrainFact`
- `LastBrainDecision`
- `BrainDecisionRecord`
- `ToolCallResult`
- `HumanResult`
- `HumanTaskOutput`
- `HumanTraceStep`
## 5. `learning` 实现规则
对应流转图:
```ts
function runLearning(state, wantLearnSkillIds) {
for (const skillId of wantLearnSkillIds) {
if (alreadyLearned(state, skillId)) {
writeWatchHint(state, 'skill already learned')
return
}
- `human` 阶段一:`brain` 读取输入并产出决策
- `human` 阶段二:`learning` 路径的数据流转
- `human` 阶段三:`hand` 路径的数据流转
- `human` 阶段四:`mouth` 路径的数据流转
appendLearnedSkillId(state, skillId)
}
}
```
### 2.2 `brain`
实现要求:
- 运行模型:LangGraph 节点
- 触发条件:`runtime.stage = brain`
- 读取:
- `HumanState.task`
- `HumanState.brainMemory`
- `HumanState.lastParallelCallToolResults`
- `HumanState.watchHint`
- 最近决策历史摘要
- 输出:
- `LastBrainDecision`
- 回写:
- `HumanState.brainMemory.inside`
- `HumanState.brainMemory.outside.brainDecisions`
- `HumanState.lastBrainDecision`
- `HumanState.runtime.stage`
- `HumanState.watchHint`
- `HumanState.trace.steps`
- 下一跳:
- `learning`
- `hand`
- `mouth`
- 当 memory reducer 产出 `watchHint` 时留在 `brain`
- 实现约束:
- 模型输出必须通过 `ctx.chatStructured`
- prompt 必须通过 `buildBrainPromptContext`
- 输出必须先经过 `parseBrainDecisionOutput`
- `brainMemory.inside` 的修改必须经过 `reduceBrainMemory`
- `brainDecisions` 只保留最近 10 条
- `brain` 回看历史时只消费摘要,不直接把 `lastBrainDecision` 重新喂回
- `brain` 是唯一推理节点
- 对应数据结构:
- `HumanState.brainMemory.inside`
- `HumanState.brainMemory.outside.brainDecisions`
- `LastBrainDecision`
- `BrainDecisionRecord`
- `BrainMemoryOp`
- 对应流转图:
- `human` 阶段一:`brain` 读取输入并产出决策
- `learning` 只负责更新 `learnedSkillIds`。
- `skillCatalog` 是目录摘要,不在 `learning` 阶段重新计算。
- 重复学习只写 `watchHint`,不做重复动作。
### 2.3 `learning`
## 6. `hand` 实现规则
- 运行模型:LangGraph 节点
- 触发条件:`lastBrainDecision.nextAction.action = learning`
- 读取:
- `lastBrainDecision.nextAction.wantLearnSkillIds`
- `brainMemory.outside.learnedSkillIds`
- skill registry / loader
- 回写:
- `brainMemory.outside.learnedSkillIds`
- `runtime.stage = brain`
- 必要时回写 `watchHint`
- `trace.steps`
- 下一跳:
- `brain`
- 实现约束:
- `learning` 不调用模型
- 未知 skill 或重复学习必须写 `watchHint` 并返回 `brain`
- `ensureSkillTools` 负责把 skill 对应工具装入 runtime cache
- `learning` 只更新已学习 skill 集合,不扩张为新推理节点
- 对应数据结构:
- `SkillCatalogItem`
- `HumanState.brainMemory.outside.learnedSkillIds`
- `HumanTraceStep`
- 对应流转图:
- `human` 阶段二:`learning` 路径的数据流转
### 6.1 工具执行
### 2.4 `hand`
```ts
function runHand(state, toolCalls) {
const results = executeToolCalls(toolCalls)
state.lastParallelCallToolResults = results
state.runtimeArtifacts = collectRuntimeArtifacts(state.runtimeArtifacts, results)
- 运行模型:LangGraph 节点
- 触发条件:`lastBrainDecision.nextAction.action = hand`
- 读取:
- `lastBrainDecision.nextAction.parallelCallToolParam`
- `brainMemory.outside.learnedSkillIds`
- runtime cache 中已装载的工具
- 回写:
- `lastParallelCallToolResults`
- `runtimeArtifacts`
- `runtime.stage = brain`
- 必要时回写 `watchHint`
- `trace.steps`
- 下一跳:
- `brain`
- 实现约束:
- `hand` 不调用模型
- 工具批次先经过 `validateParallelToolCalls`
- 工具执行通过 `executeToolCall` 并行完成
- tool call count 超限时只写 `watchHint`,由下一轮 `brain` 决定是否收敛
- `runtimeArtifacts` 只从工具结构化结果提取并合并
- 对应数据结构:
- `ToolCallParam`
- `ToolCallResult`
- `HumanState.runtimeArtifacts`
- `AiArtifactRef`
- `HumanTraceStep`
- 对应流转图:
- `human` 阶段三:`hand` 路径的数据流转
if (exceedsToolCallLimit(state)) {
writeWatchHint(state, 'tool call limit exceeded')
}
}
```
### 2.5 `mouth`
### 6.2 `artifacts` 收集
- 运行模型:LangGraph 节点
- 触发条件:`runtime.stage = mouth`
- 读取:
- `HumanState.task`
- `HumanState.brainMemory.inside`
- `HumanState.lastBrainDecision`
- `HumanState.runtimeArtifacts`
- `deliveryMode`
- 输出:
- `MouthOutput`
- 回写:
- `HumanState.result`
- `HumanState.runtime.status`
- `HumanState.runtime.stage = mouth`
- `HumanState.trace.steps`
- 下一跳:
- 结束当前 `human` run
- 少数需要继续运行的特殊场景才允许回到 `brain`
- 实现约束:
- 最终状态由 `lastBrainDecision.nextAction.status` 推导,不由 `mouth` 自行决定
- 模型输出必须通过 `ctx.chatStructured`
- prompt 必须通过 `buildMouthPromptContext`
- `deliveryMarkdown` 为空时,必须使用 fallback builder 补足
- `artifacts` 由 runtime 透传到 `result`,不是由 `mouth` 生成
- `mouth` 不补编新事实,不扩张 artifacts
- 对应数据结构:
- `MouthOutput`
- `HumanResult`
- `HumanTaskOutput`
- `AiArtifactRef`
- 对应流转图:
- `human` 阶段四:`mouth` 路径的数据流转
```ts
function collectRuntimeArtifacts(existingArtifacts, toolResults) {
const newArtifacts = extractArtifactsFromStructuredToolResults(toolResults)
return [...existingArtifacts, ...newArtifacts]
}
```
### 2.6 `human` 共享状态规则
实现要求:
- `lastBrainDecision` 是当前分发快照,服务 `learning / hand / mouth` 读取当前动作。
- `brainDecisions` 是最近决策历史,只保留最近 10 条,用于回看和审计。
- `watchHint` 是轻量纠偏信号,不单独形成一个新节点。
- `runtimeArtifacts` 只在 `hand` 阶段增长,在 `mouth` 阶段透传。
- `trace.steps` 由各节点持续追加,供事件、审计与回放使用。
- `hand` 只执行 `brain` 已明确决定的工具调用。
- `hand` 只写最近一轮工具结果、`runtimeArtifacts` 与必要的 `watchHint`。
- `hand` 不直接生成最终结论。
对应数据结构:
## 7. `mouth` 实现规则
- `LastBrainDecision`
- `BrainDecisionRecord`
- `HumanState.watchHint`
- `HumanState.runtimeArtifacts`
- `HumanTraceStep`
```ts
function runMouth(state, deliveryMode) {
const input = {
task: state.task,
status: state.lastBrainDecision.nextAction.status,
decision: state.lastBrainDecision.decision,
facts: state.brainMemory.inside.facts,
evidences: state.brainMemory.inside.evidences,
openQuestions: state.brainMemory.inside.openQuestions,
artifacts: state.runtimeArtifacts,
deliveryMode
}
## 3. 跨层实现规则
return renderMouthOutput(input)
}
```
### 3.1 `main` 与 `human` 的派发契约
实现要求:
- `main` 与 `human` 的稳定边界是:
- 输入:`HumanTaskInput + HumanRunDispatchOptions`
- 输出:`HumanTaskOutput`
- `main` 只调用 `HumanTaskRunner`,不直接读写 `HumanState`。
- `human` runner 负责把最终 `HumanState` 收口为稳定的 `HumanTaskOutput`。
- `mouth` 只生成 `summary + deliveryMarkdown`。
- `mouth` 不重新判断最终状态。
- `mouth` 不补编新事实,不扩张 artifacts。
- `result.status` 由运行时按 `nextAction.status` 回写。
对应数据结构:
## 8. 共享实现规则
- `HumanTaskInput`
- `HumanRunDispatchOptions`
- `HumanTaskOutput`
### 8.1 `deliveryMode`
### 3.2 `deliveryMode`
```ts
function resolveDeliveryMode(mainStage) {
if (mainStage === 'main_direct_human_exec') return 'user_answer'
if (mainStage === 'main_synthesis_human') return 'user_answer'
if (mainStage === 'main_plan_exec') return 'task_handoff'
return 'context_note'
}
```
- `main_direct_human_exec` 与 `main_synthesis_human` 使用 `user_answer`
- `main_plan_exec` 使用 `task_handoff`
- `main_context_human` 使用 `context_note`
- `deliveryMode` 只影响 `mouth` 的交付组织方式,不改变前置节点职责。
### 8.2 `contextEvidence` 提取
对应数据结构:
```ts
function extractContextEvidence(output) {
const lines = extractBulletsFromSection(output.deliveryMarkdown, '可复用证据')
- `HumanRunDispatchOptions.deliveryMode`
- `HumanResult.deliveryMarkdown`
if (lines.length > 0) {
return lines
}
### 3.3 `contextEvidence`
return output.summary ? [output.summary] : []
}
```
- `contextEvidence` 是 `main` 侧消费的证据摘要,不直接保存整段 `context_note`。
- 提取来源优先是 `context_note` 中的“可复用证据”区块。
- 提取失败时,才 fallback 到 `HumanTaskOutput.summary`。
- intent / routing 的判定理由不写入 `contextEvidence`。
实现要求:
对应数据结构:
- 只从 `context_note` 的 `## 可复用证据` 区块提取 bullet / numbered line。
- 提取失败时才 fallback 到 `summary`。
- 路由理由不写入 `contextEvidence`。
- `MainState.contextEvidence`
- `HumanTaskOutput.summary`
- `HumanTaskOutput.deliveryMarkdown`
### 8.3 `main` 派发 `human`
### 3.4 `artifacts`
```ts
function dispatchHuman(task, options) {
const deliveryMode = options.deliveryMode ?? 'user_answer'
const humanOutput = runHuman(task, { deliveryMode })
- `artifacts` 的产生源头是工具结构化输出。
- `hand` 负责把工具结果提取为 `runtimeArtifacts`。
- `mouth` 只透传 `runtimeArtifacts` 到 `HumanResult`。
- `main_plan_exec` 把子任务 artifacts 回写到 `MainPlanTask.artifacts`。
- `main_synthesis_human` / `main_complete` 负责合并并收口到 `MainState.finalArtifacts`。
return {
taskId: humanOutput.taskId,
status: humanOutput.status,
summary: humanOutput.summary,
deliveryMarkdown: humanOutput.deliveryMarkdown,
artifacts: humanOutput.artifacts ?? []
}
}
```
对应数据结构:
实现要求:
- `AiArtifactRef`
- `HumanState.runtimeArtifacts`
- `HumanResult.artifacts`
- `MainPlanTask.artifacts`
- `MainState.finalArtifacts`
- `main` 与 `human` 的稳定交接边界是 `HumanTaskInput + HumanRunDispatchOptions -> HumanTaskOutput`。
- `deliveryMode` 只作为运行时派发参数传入,不要求持久化到 `MainState`。
- `main` 不直接改写 `human` 的内部状态。
### 3.5 技术实现约束
### 8.4 run finalization
- `main` 与 `human` 的运行编排必须基于 `@langchain/langgraph` 的 `StateGraph`。
- graph 的状态 schema 必须通过各自目录下的 `state/schema.ts` 定义和约束。
- graph 路由必须通过 `addConditionalEdges` 或等价 graph 路由表达,不要在 graph 外另写一套手工循环替代。
- 模型结构化输出必须统一通过 `ctx.chatStructured`,由 runtime 封装的 `invokeStructuredChat` 承接。
- 如果后续需要引入非结构化模型调用,也应先补 runtime 级封装,不在 node 内直接拼接底层模型调用。
- 节点内部不要直接调用 `model.withStructuredOutput`,也不要散落手写 schema parse 逻辑。
- prompt 必须通过 `build*Prompt` 或 `build*PromptContext` 组织,不在 node 内联堆大段 prompt 文本。
- `learning`、`hand`、`complete` 这类确定性节点默认不引入额外模型调用。
- 节点入口先执行取消检查,再进行模型调用、工具调用或状态写入。
- 文档中的状态转移用于约束 graph 结构,不应用来反向实现一套脱离 LangGraph 的伪循环。
```ts
function completeRun(finalOutput) {
return {
status: 'completed',
finalDeliveryMarkdown: finalOutput.deliveryMarkdown,
finalArtifacts: finalizeRunArtifacts(finalOutput.artifacts)
}
}
```
### 3.6 与数据文档的联动
实现要求:
- `main` 侧实现时,先对照 `data-definition-and-flow.md` 中的:
- `MainState`
- `MainPlanTask`
- `main` 三张流转图
- `human` 侧实现时,先对照 `data-definition-and-flow.md` 中的:
- `HumanState`
- `brainMemory.inside`
- `brainMemory.outside`
- `lastBrainDecision` 与 `brainDecisions`
- `lastParallelCallToolResults` 与 `result`
- `human` 四张流转图
- 跨层派发与交付时,重点对照:
- `HumanTaskInput`
- `HumanRunDispatchOptions`
- `HumanTaskOutput`
- `AiArtifactRef`
- `main_complete` 负责 run 级收尾。
- run finalization 可以追加 run 级 artifacts。
## 9. 不变量
## 4. 不变量
- `main` 只负责 run 级 orchestration。
- `human` 只负责 task 级 autonomous execution。
- `brain` 是唯一推理节点。
- `deliveryMode` 只影响 `mouth` 的交付组织方式。
- `brainMemory.inside` 只由 `brain` 通过 `memoryOps` 修改。
- `lastBrainDecision` 是当前分发快照,`brainDecisions` 是最近决策历史。
- `mouth` 不读取最近决策历史。
- `brainMemory.inside` 只允许通过 `brain` 的 `memoryOps` 修改。
- `lastBrainDecision` 只表示当前动作快照,`brainDecisions` 只表示最近历史。
- `deliveryMode` 只影响交付组织,不改变求解职责。
- `mouth` 不新增事实,不扩张 artifacts。
- `main` 与 `human` 之间只通过稳定交接契约通信,不直接穿透内部状态。