Commit 93e56553 by YeYuheng Committed by GitHub

fix: constrain update_plan schema by action (#7204)

parent cc2d8e37
import type { ChatCompletionTool } from '@fastgpt/global/core/ai/llm/type'; import type { ChatCompletionTool } from '@fastgpt/global/core/ai/llm/type';
const planEvidenceSchema = {
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['tool_result', 'model_output', 'user_input', 'manual']
},
ref: {
type: 'string'
},
summary: {
type: 'string'
}
},
required: ['kind', 'summary']
} as const;
const planSchema = {
type: 'object',
description:
'Complete active plan. Shape: { planId?, task, description, background?, steps: [{ id, title, description, acceptanceCriteria, status, evidence?, outputSummary?, blocker?, needsReplan? }] }.',
properties: {
planId: {
type: 'string'
},
task: {
type: 'string'
},
description: {
type: 'string'
},
background: {
type: 'string'
},
steps: {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
id: {
type: 'string'
},
title: {
type: 'string'
},
description: {
type: 'string'
},
acceptanceCriteria: {
type: 'array',
items: {
type: 'string'
}
},
status: {
type: 'string',
enum: ['pending', 'in_progress', 'done', 'blocked', 'skipped']
},
evidence: {
type: 'array',
items: planEvidenceSchema
},
outputSummary: {
type: 'string'
},
blocker: {
type: 'string'
},
needsReplan: {
type: 'boolean'
}
},
required: ['id', 'title', 'description', 'acceptanceCriteria', 'status']
}
}
},
required: ['task', 'description', 'steps']
} as const;
const setPlanOperationSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['set_plan']
},
plan: planSchema,
reason: {
type: 'string'
}
},
required: ['action', 'plan'],
additionalProperties: false
} as const;
const replacePlanOperationSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['replace_plan']
},
plan: planSchema,
reason: {
type: 'string'
}
},
required: ['action', 'plan'],
additionalProperties: false
} as const;
const updateStepOperationSchema = {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['update_step']
},
stepId: {
type: 'string',
description:
'Required when action is update_step. update_step changes exactly one step; use multiple update_step operations for multiple steps.'
},
status: {
type: 'string',
enum: ['pending', 'in_progress', 'done', 'blocked', 'skipped']
},
evidence: {
type: 'array',
items: planEvidenceSchema
},
outputSummary: {
type: 'string'
},
blocker: {
type: 'string'
},
needsReplan: {
type: 'boolean'
},
reason: {
type: 'string'
}
},
required: ['action', 'stepId', 'status'],
additionalProperties: false
} as const;
/** /**
* 创建单主 loop 使用的计划维护工具。 * 创建单主 loop 使用的计划维护工具。
* Main Agent 通过它创建、更新或替换 active plan;工具调用由 loop 内部消费,不进入业务工具执行器。 * Main Agent 通过它创建、更新或替换 active plan;工具调用由 loop 内部消费,不进入业务工具执行器。
...@@ -9,7 +158,7 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool = ...@@ -9,7 +158,7 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool =
function: { function: {
name, name,
description: description:
'Create, update, or replace the active plan. Send one or more operations in updates; batch related step changes in a single call. For set_plan and replace_plan, always provide a complete plan object.', 'Create, update, or replace the active plan. Send one or more operations in updates; batch related step changes in a single call. set_plan and replace_plan require a complete plan object. update_step must only include stepId/status/evidence/outputSummary/blocker/needsReplan/reason; never include plan in update_step.',
parameters: { parameters: {
type: 'object', type: 'object',
properties: { properties: {
...@@ -19,130 +168,7 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool = ...@@ -19,130 +168,7 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool =
'Ordered plan operations. Use multiple update_step operations in one call when several steps changed together.', 'Ordered plan operations. Use multiple update_step operations in one call when several steps changed together.',
minItems: 1, minItems: 1,
items: { items: {
type: 'object', oneOf: [setPlanOperationSchema, updateStepOperationSchema, replacePlanOperationSchema]
properties: {
action: {
type: 'string',
enum: ['set_plan', 'update_step', 'replace_plan']
},
plan: {
type: 'object',
description:
'Required for set_plan or replace_plan. Shape: { planId?, task, description, background?, steps: [{ id, title, description, acceptanceCriteria, status, evidence?, outputSummary?, blocker?, needsReplan? }] }.',
properties: {
planId: {
type: 'string'
},
task: {
type: 'string'
},
description: {
type: 'string'
},
background: {
type: 'string'
},
steps: {
type: 'array',
minItems: 1,
items: {
type: 'object',
properties: {
id: {
type: 'string'
},
title: {
type: 'string'
},
description: {
type: 'string'
},
acceptanceCriteria: {
type: 'array',
items: {
type: 'string'
}
},
status: {
type: 'string',
enum: ['pending', 'in_progress', 'done', 'blocked', 'skipped']
},
evidence: {
type: 'array',
items: {
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['tool_result', 'model_output', 'user_input', 'manual']
},
ref: {
type: 'string'
},
summary: {
type: 'string'
}
},
required: ['kind', 'summary']
}
},
outputSummary: {
type: 'string'
},
blocker: {
type: 'string'
},
needsReplan: {
type: 'boolean'
}
},
required: ['id', 'title', 'description', 'acceptanceCriteria', 'status']
}
}
},
required: ['task', 'description', 'steps']
},
stepId: {
type: 'string',
description: 'Step id to update when action is update_step.'
},
status: {
type: 'string',
enum: ['pending', 'in_progress', 'done', 'blocked', 'skipped']
},
evidence: {
type: 'array',
items: {
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['tool_result', 'model_output', 'user_input', 'manual']
},
ref: {
type: 'string'
},
summary: {
type: 'string'
}
},
required: ['kind', 'summary']
}
},
outputSummary: {
type: 'string'
},
blocker: {
type: 'string'
},
needsReplan: {
type: 'boolean'
},
reason: {
type: 'string'
}
},
required: ['action']
} }
}, },
reason: { reason: {
......
...@@ -71,9 +71,14 @@ ${ ...@@ -71,9 +71,14 @@ ${
调用 update_plan 时保持计划可执行、可验证、简洁。 调用 update_plan 时保持计划可执行、可验证、简洁。
update_plan 使用 updates 数组;如果多个 step 在同一轮工具结果或推理中同时变化,把这些 update_step 合并到一次调用里。 update_plan 使用 updates 数组;如果多个 step 在同一轮工具结果或推理中同时变化,把这些 update_step 合并到一次调用里。
创建计划时,set_plan 必须传完整 plan 对象,不要把 status/reason/evidence 直接放在 set_plan operation 上。 创建计划时,set_plan 必须传完整 plan 对象,不要把 status/reason/evidence 直接放在 set_plan operation 上。
更新步骤时,update_step 只允许传 stepId、status、evidence、outputSummary、blocker、needsReplan、reason;不要传 plan。
如果要更新多个步骤,必须在 updates 数组里写多个 update_step;不要把完整 plan.steps 放进单个 update_step。
正确格式: 正确格式:
{"updates":[{"action":"set_plan","plan":{"task":"...","description":"...","steps":[{"id":"1","title":"...","description":"...","acceptanceCriteria":["..."],"status":"pending","evidence":[]}]}}]} {"updates":[{"action":"set_plan","plan":{"task":"...","description":"...","steps":[{"id":"1","title":"...","description":"...","acceptanceCriteria":["..."],"status":"pending","evidence":[]}]}}]}
批量更新步骤的正确格式:
{"updates":[{"action":"update_step","stepId":"1","status":"done","outputSummary":"..."},{"action":"update_step","stepId":"2","status":"done","outputSummary":"..."}]}
不要使用这种格式:{"updates":[{"action":"set_plan","status":"in_progress","reason":"..."},{"action":"update_step","stepId":"1","status":"pending"}]} 不要使用这种格式:{"updates":[{"action":"set_plan","status":"in_progress","reason":"..."},{"action":"update_step","stepId":"1","status":"pending"}]}
也不要使用这种格式:{"updates":[{"action":"update_step","stepId":"1","status":"done","plan":{"steps":[...]}}]}
每个 step 都要有明确 title、description、acceptanceCriteria,新 step 初始 status 通常为 pending。 每个 step 都要有明确 title、description、acceptanceCriteria,新 step 初始 status 通常为 pending。
更新步骤时,完成步骤要写 outputSummary 并尽量附 evidence;阻塞步骤必须写 blocker;如果原计划不适用,调用 replace_plan 或标记 needsReplan。 更新步骤时,完成步骤要写 outputSummary 并尽量附 evidence;阻塞步骤必须写 blocker;如果原计划不适用,调用 replace_plan 或标记 needsReplan。
</plan_update_rules> </plan_update_rules>
......
...@@ -62,4 +62,30 @@ describe('agent loop plan parser', () => { ...@@ -62,4 +62,30 @@ describe('agent loop plan parser', () => {
expect(createUpdatePlanTool().function.name).toBe('update_plan'); expect(createUpdatePlanTool().function.name).toBe('update_plan');
expect(createUpdatePlanTool().function.parameters.required).toEqual(['updates']); expect(createUpdatePlanTool().function.parameters.required).toEqual(['updates']);
}); });
it('separates update_plan schema fields by action', () => {
const parameters = createUpdatePlanTool().function.parameters as {
properties: {
updates: {
items: {
oneOf: Array<{
properties: Record<string, unknown>;
required: string[];
}>;
};
};
};
};
const operationSchemas = parameters.properties.updates.items.oneOf;
const planSchemas = operationSchemas.filter((schema) => 'plan' in schema.properties);
const updateStepSchema = operationSchemas.find((schema) => 'stepId' in schema.properties);
expect(planSchemas).toHaveLength(2);
planSchemas.forEach((schema) => {
expect(schema.required).toContain('plan');
});
expect(updateStepSchema?.required).toEqual(['action', 'stepId', 'status']);
expect(updateStepSchema?.properties).not.toHaveProperty('plan');
});
}); });
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment