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 = {
* 创建单主 loop 使用的计划维护工具。
* Main Agent 通过它创建、更新或替换 active plan;工具调用由 loop 内部消费,不进入业务工具执行器。
*/
export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool => ({
type: 'function',
function: {
name,
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.',
parameters: {
type: 'object', type: 'object',
properties: { properties: {
updates: { kind: {
type: 'array',
description:
'Ordered plan operations. Use multiple update_step operations in one call when several steps changed together.',
minItems: 1,
items: {
type: 'object',
properties: {
action: {
type: 'string', type: 'string',
enum: ['set_plan', 'update_step', 'replace_plan'] enum: ['tool_result', 'model_output', 'user_input', 'manual']
},
ref: {
type: 'string'
},
summary: {
type: 'string'
}
}, },
plan: { required: ['kind', 'summary']
} as const;
const planSchema = {
type: 'object', type: 'object',
description: description:
'Required for set_plan or replace_plan. Shape: { planId?, task, description, background?, steps: [{ id, title, description, acceptanceCriteria, status, evidence?, outputSummary?, blocker?, needsReplan? }] }.', 'Complete active plan. Shape: { planId?, task, description, background?, steps: [{ id, title, description, acceptanceCriteria, status, evidence?, outputSummary?, blocker?, needsReplan? }] }.',
properties: { properties: {
planId: { planId: {
type: 'string' type: 'string'
...@@ -69,22 +61,7 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool = ...@@ -69,22 +61,7 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool =
}, },
evidence: { evidence: {
type: 'array', type: 'array',
items: { items: planEvidenceSchema
type: 'object',
properties: {
kind: {
type: 'string',
enum: ['tool_result', 'model_output', 'user_input', 'manual']
},
ref: {
type: 'string'
},
summary: {
type: 'string'
}
},
required: ['kind', 'summary']
}
}, },
outputSummary: { outputSummary: {
type: 'string' type: 'string'
...@@ -101,33 +78,59 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool = ...@@ -101,33 +78,59 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool =
} }
}, },
required: ['task', 'description', 'steps'] required: ['task', 'description', 'steps']
}, } as const;
stepId: {
const setPlanOperationSchema = {
type: 'object',
properties: {
action: {
type: 'string', type: 'string',
description: 'Step id to update when action is update_step.' enum: ['set_plan']
}, },
status: { plan: planSchema,
type: 'string', reason: {
enum: ['pending', 'in_progress', 'done', 'blocked', 'skipped'] type: 'string'
}
}, },
evidence: { required: ['action', 'plan'],
type: 'array', additionalProperties: false
items: { } as const;
const replacePlanOperationSchema = {
type: 'object', type: 'object',
properties: { properties: {
kind: { action: {
type: 'string', type: 'string',
enum: ['tool_result', 'model_output', 'user_input', 'manual'] enum: ['replace_plan']
},
ref: {
type: 'string'
}, },
summary: { plan: planSchema,
reason: {
type: 'string' type: 'string'
} }
}, },
required: ['kind', 'summary'] 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: { outputSummary: {
type: 'string' type: 'string'
...@@ -142,7 +145,30 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool = ...@@ -142,7 +145,30 @@ export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool =
type: 'string' type: 'string'
} }
}, },
required: ['action'] required: ['action', 'stepId', 'status'],
additionalProperties: false
} as const;
/**
* 创建单主 loop 使用的计划维护工具。
* Main Agent 通过它创建、更新或替换 active plan;工具调用由 loop 内部消费,不进入业务工具执行器。
*/
export const createUpdatePlanTool = (name = 'update_plan'): ChatCompletionTool => ({
type: 'function',
function: {
name,
description:
'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: {
type: 'object',
properties: {
updates: {
type: 'array',
description:
'Ordered plan operations. Use multiple update_step operations in one call when several steps changed together.',
minItems: 1,
items: {
oneOf: [setPlanOperationSchema, updateStepOperationSchema, replacePlanOperationSchema]
} }
}, },
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