Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/lib/agent/runner/harness/pi/orchestrator-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
applyComplete,
applyEnqueue,
applyReadHandoffs,
COMPLETE_TASK_DESCRIPTION,
ENQUEUE_MODEL_DESCRIPTION,
HANDOFF_FIELDS,
NOT_NEEDED_REASON_ASK,
Expand Down Expand Up @@ -138,10 +139,9 @@ export function createPiOrchestratorTools(
const completeTask = defineTool({
name: 'complete_task',
label: 'Complete task',
description:
"Report the outcome of your task. Always call this exactly once when you finish, with a structured handoff for the next agent. Use status 'not needed' when the task does not apply to this project and you cannot do it (say why in the handoff) — not 'done'.",
description: COMPLETE_TASK_DESCRIPTION,
promptSnippet:
'complete_task(status, handoff) — report your outcome exactly once when done',
'complete_task(status, handoff) — report your outcome exactly once when done, with goals/did/forNextAgent nested inside handoff',
parameters: COMPLETE_PARAMS,
execute(_id, args) {
const res = applyComplete(ctx, args as CompleteArgs);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,16 @@
* asked for a report section it could not submit.
*/
import { describe, it, expect } from 'vitest';
import { HANDOFF_FIELDS, HANDOFF_SHAPE_KEYS } from '../queue-tools';
import { PI_HANDOFF_PARAM_KEYS } from '../../../harness/pi/orchestrator-tools';
import {
COMPLETE_TASK_DESCRIPTION,
HANDOFF_FIELDS,
HANDOFF_SHAPE_KEYS,
type OrchestratorToolsContext,
} from '../queue-tools';
import {
createPiOrchestratorTools,
PI_HANDOFF_PARAM_KEYS,
} from '../../../harness/pi/orchestrator-tools';

describe('complete_task handoff schema', () => {
it('exposes the same fields on both harnesses', () => {
Expand All @@ -33,3 +41,29 @@ describe('complete_task handoff schema', () => {
},
);
});

describe('complete_task description', () => {
const piCompleteTask = () => {
const ctx = { validTypes: [] } as unknown as OrchestratorToolsContext;
const tool = createPiOrchestratorTools(ctx).find(
(t) => (t as unknown as { name: string }).name === 'complete_task',
);
return tool as unknown as { description: string };
};

it('shares one tool description with the MCP server', () => {
expect(piCompleteTask().description).toBe(COMPLETE_TASK_DESCRIPTION);
});

// An agent that cannot see the nesting spends a turn on a rejected flat call.
it.each(['goals', 'did', 'forNextAgent'])(
'names %s as a field that goes inside the nested handoff',
(field) => {
expect(COMPLETE_TASK_DESCRIPTION).toContain(field);
},
);

it('says the handoff is nested', () => {
expect(COMPLETE_TASK_DESCRIPTION).toMatch(/nested object/);
});
});
18 changes: 17 additions & 1 deletion src/lib/agent/runner/sequence/orchestrator/queue-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,22 @@ export const ENQUEUE_MODEL_DESCRIPTION = `Optional model override for this task.
...VALID_MODELS,
].join(', ')}.`;

/**
* The `complete_task` tool description, shared by both harnesses' schemas so
* the outcome contract cannot drift — the same discipline {@link HANDOFF_FIELDS}
* applies to the fields inside the handoff.
*
* The closing sentence names the nesting because agents kept learning it from a
* rejection instead: a first call with `goals`/`did`/`forNextAgent` at the top
* level fails schema validation, and reading that error back was the only place
* the shape was stated. Naming it where the agent fills the call in costs
* nothing, the same reasoning as {@link ENQUEUE_MODEL_DESCRIPTION}.
*/
export const COMPLETE_TASK_DESCRIPTION =
'Report the outcome of your task. Always call this exactly once when you finish, with a structured handoff for the next agent. ' +
"Use status 'not needed' when the task does not apply to this project and you cannot do it (say why in the handoff) — not 'done'. " +
'The handoff is one nested object: put `goals`, `did` and `forNextAgent` — all three required — inside `handoff`, along with any optional fields, never at the top level.';

/** The per-task remark ask, shared by both harnesses' complete_task schemas. */
export const REMARK_ASK =
'What information or guidance would have been useful to have in the integration prompt or documentation for this task — specifically anything that would have prevented tool failures, erroneous edits, or other wasted turns.';
Expand Down Expand Up @@ -512,7 +528,7 @@ export function buildOrchestratorTools(

const completeTask = tool(
'complete_task',
"Report the outcome of your task. Always call this exactly once when you finish, with a structured handoff for the next agent. Use status 'not needed' when the task does not apply to this project and you cannot do it (say why in the handoff) — not 'done'.",
COMPLETE_TASK_DESCRIPTION,
COMPLETE_SHAPE,
((args: CompleteArgs) => {
const res = applyComplete(ctx, args);
Expand Down
Loading