mcp-codex-worker 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/app.js +23 -11
- package/dist/src/app.js.map +1 -1
- package/dist/src/config/question-guidance.d.ts +15 -0
- package/dist/src/config/question-guidance.js +75 -0
- package/dist/src/config/question-guidance.js.map +1 -0
- package/dist/src/execution/base-adapter.d.ts +1 -0
- package/dist/src/execution/base-adapter.js.map +1 -1
- package/dist/src/execution/codex-adapter.js +1 -0
- package/dist/src/execution/codex-adapter.js.map +1 -1
- package/dist/src/index.js +16 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/mcp/next-action-guidance.d.ts +26 -3
- package/dist/src/mcp/next-action-guidance.js +115 -16
- package/dist/src/mcp/next-action-guidance.js.map +1 -1
- package/dist/src/mcp/resource-renderers.js +83 -5
- package/dist/src/mcp/resource-renderers.js.map +1 -1
- package/dist/src/mcp/tool-definitions.d.ts +9 -8
- package/dist/src/mcp/tool-definitions.js +8 -4
- package/dist/src/mcp/tool-definitions.js.map +1 -1
- package/dist/src/services/fleet-mode.d.ts +5 -0
- package/dist/src/services/fleet-mode.js +16 -0
- package/dist/src/services/fleet-mode.js.map +1 -1
- package/dist/src/services/tool-description-banner.d.ts +23 -0
- package/dist/src/services/tool-description-banner.js +106 -0
- package/dist/src/services/tool-description-banner.js.map +1 -0
- package/package.json +1 -1
- package/src/app.ts +29 -11
- package/src/config/question-guidance.ts +77 -0
- package/src/execution/base-adapter.ts +1 -0
- package/src/execution/codex-adapter.ts +1 -0
- package/src/index.ts +15 -0
- package/src/mcp/next-action-guidance.ts +163 -25
- package/src/mcp/resource-renderers.ts +85 -5
- package/src/mcp/tool-definitions.ts +9 -4
- package/src/services/fleet-mode.ts +22 -0
- package/src/services/tool-description-banner.ts +116 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider-specific question guidance appended to sub-agent prompts at spawn time.
|
|
3
|
+
*
|
|
4
|
+
* Codex uses `request_user_input` for gathering user preferences. Command and
|
|
5
|
+
* file approvals are auto-approved by our pause-flow, so the guidance focuses
|
|
6
|
+
* on the user_input question type only.
|
|
7
|
+
*
|
|
8
|
+
* Policy: ASK when the brief is ambiguous about user-facing choices. The
|
|
9
|
+
* orchestrator answers promptly. Decide yourself for implementation details.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const CODEX_GUIDANCE = `
|
|
13
|
+
--- QUESTION POLICY (IMPORTANT — READ BEFORE STARTING) ---
|
|
14
|
+
|
|
15
|
+
You have access to \`request_user_input\` for gathering user preferences.
|
|
16
|
+
|
|
17
|
+
**WHEN TO ASK:** The brief is ambiguous about design preferences, visual style, branding,
|
|
18
|
+
colors, naming, scope, content tone, or any user-facing choice where guessing wrong wastes
|
|
19
|
+
significant rework. Ask early — before you start building — so you build the right thing.
|
|
20
|
+
|
|
21
|
+
**WHEN TO DECIDE YOURSELF:** Implementation details, internal architecture, code patterns,
|
|
22
|
+
folder structure, library choices, refactoring approach, or anything the brief and codebase
|
|
23
|
+
already specify. For these, decide and document your assumption.
|
|
24
|
+
|
|
25
|
+
**IF YOU DECIDE WITHOUT ASKING:** Document your choice clearly in output
|
|
26
|
+
(e.g. "Assumed modern blue theme since brief didn't specify").
|
|
27
|
+
|
|
28
|
+
Use \`request_user_input\` with this exact schema:
|
|
29
|
+
|
|
30
|
+
\`\`\`
|
|
31
|
+
request_user_input({
|
|
32
|
+
questions: [
|
|
33
|
+
{
|
|
34
|
+
header: "Brand",
|
|
35
|
+
id: "brand_direction",
|
|
36
|
+
question: "What branding direction should I use?",
|
|
37
|
+
options: [
|
|
38
|
+
{
|
|
39
|
+
label: "Modern Care (Recommended)",
|
|
40
|
+
description: "Clean, trustworthy clinic branding."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
label: "Luxury Smile",
|
|
44
|
+
description: "Upscale positioning with premium feel."
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
})
|
|
50
|
+
\`\`\`
|
|
51
|
+
|
|
52
|
+
**Rules:**
|
|
53
|
+
- Recommended option goes FIRST with "(Recommended)" in label
|
|
54
|
+
- Do NOT add an "Other" option — the client adds freeform input automatically
|
|
55
|
+
- Max 3 options per question. Keep labels 1-5 words. Keep descriptions one sentence.
|
|
56
|
+
- Ask ALL ambiguous design questions in one call (batch them), then build.
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
const FALLBACK_GUIDANCE = `
|
|
60
|
+
--- QUESTION POLICY ---
|
|
61
|
+
When the brief is ambiguous about design preferences, branding, colors, naming, or scope —
|
|
62
|
+
ask using the question tool if available. The orchestrator answers promptly.
|
|
63
|
+
For implementation details — decide yourself and document your assumption.
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Returns provider-specific question guidance to append to the sub-agent prompt.
|
|
68
|
+
* Includes full tool schema details so the agent knows the exact format.
|
|
69
|
+
*/
|
|
70
|
+
export function getQuestionGuidance(provider: string): string {
|
|
71
|
+
switch (provider) {
|
|
72
|
+
case 'codex':
|
|
73
|
+
return CODEX_GUIDANCE;
|
|
74
|
+
default:
|
|
75
|
+
return FALLBACK_GUIDANCE;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -94,6 +94,7 @@ export class CodexAdapter extends BaseProviderAdapter {
|
|
|
94
94
|
model: options.model,
|
|
95
95
|
effort: options.effort,
|
|
96
96
|
cwd: options.cwd,
|
|
97
|
+
developerInstructions: options.developerInstructions,
|
|
97
98
|
});
|
|
98
99
|
const threadResult = await runtime.request('thread/start', threadParams) as {
|
|
99
100
|
thread?: { id?: string };
|
package/src/index.ts
CHANGED
|
@@ -95,11 +95,25 @@ async function main(): Promise<void> {
|
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
// --- Status changes: task completed, failed, etc. ---
|
|
98
|
+
// Debounced tool list refresh — dynamic banners in tool descriptions
|
|
99
|
+
// need to update when tasks enter/leave running/waiting_answer/terminal states.
|
|
100
|
+
let toolListTimer: ReturnType<typeof setTimeout> | null = null;
|
|
101
|
+
const scheduleToolListChanged = (): void => {
|
|
102
|
+
if (toolListTimer) return;
|
|
103
|
+
toolListTimer = setTimeout(() => {
|
|
104
|
+
toolListTimer = null;
|
|
105
|
+
server.sendToolListChanged().catch(() => {});
|
|
106
|
+
}, 1000);
|
|
107
|
+
toolListTimer.unref?.();
|
|
108
|
+
};
|
|
109
|
+
|
|
98
110
|
taskManager.onStatusChange((task) => {
|
|
99
111
|
const uris = subscriptions.getMatchingSubscriptions(task.id);
|
|
100
112
|
for (const uri of uris) {
|
|
101
113
|
server.sendResourceUpdated({ uri }).catch(() => {});
|
|
102
114
|
}
|
|
115
|
+
// Refresh tool descriptions so dynamic banners reflect the new state
|
|
116
|
+
scheduleToolListChanged();
|
|
103
117
|
});
|
|
104
118
|
|
|
105
119
|
// --- Output changes: new agent messages, command output, diffs ---
|
|
@@ -124,6 +138,7 @@ async function main(): Promise<void> {
|
|
|
124
138
|
process.stdin.resume();
|
|
125
139
|
|
|
126
140
|
const shutdown = async () => {
|
|
141
|
+
if (toolListTimer) clearTimeout(toolListTimer);
|
|
127
142
|
await app.shutdown().catch(() => {});
|
|
128
143
|
process.exit(0);
|
|
129
144
|
};
|
|
@@ -1,11 +1,35 @@
|
|
|
1
1
|
import { TaskStatus, isTerminalStatus } from '../task/task-state.js';
|
|
2
2
|
import type { TaskState, PendingQuestion } from '../task/task-state.js';
|
|
3
3
|
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Helpers
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
function outputPath(task: TaskState): string {
|
|
9
|
+
return task.outputFilePath ?? `~/.mcp-codex-worker/tasks/${task.id}/summary.log`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function sleepEscalation(): string {
|
|
13
|
+
return 'If the agent is still running after your first check, escalate wait times: `sleep 60`, then `sleep 90`, `sleep 120`, `sleep 150`, up to `sleep 180` max.';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function wcHint(task: TaskState): string {
|
|
17
|
+
return `Quick progress check: \`wc -l ${outputPath(task)}\` — a growing line count means the agent is still working.`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function catHint(task: TaskState): string {
|
|
21
|
+
return `Read the full output: \`cat -n ${outputPath(task)}\` (use \`tail -n +<N>\` on subsequent reads to skip already-read lines).`;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Per-tool guidance builders
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
4
28
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
29
|
+
* Guidance appended to spawn-task responses.
|
|
30
|
+
* Goal: tell the orchestrator to launch more tasks, then wait, use wc -l.
|
|
7
31
|
*/
|
|
8
|
-
export function
|
|
32
|
+
export function buildSpawnGuidance(task: TaskState): string[] {
|
|
9
33
|
const lines: string[] = ['', '---', '**What to do next:**'];
|
|
10
34
|
|
|
11
35
|
switch (task.status) {
|
|
@@ -13,18 +37,19 @@ export function buildNextActionGuidance(task: TaskState): string[] {
|
|
|
13
37
|
case TaskStatus.RUNNING:
|
|
14
38
|
case TaskStatus.RATE_LIMITED:
|
|
15
39
|
lines.push(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
`-
|
|
40
|
+
'- If you still have more agents to launch, launch them now — all agents run in parallel.',
|
|
41
|
+
`- Once all agents are launched, call \`wait-task\` with \`task_id: "${task.id}"\` (or run \`sleep 30\` then read \`task:///${task.id}\`).`,
|
|
42
|
+
`- ${wcHint(task)}`,
|
|
19
43
|
'- Read `task:///all` for a scoreboard of all tasks.',
|
|
44
|
+
`- \`waiting_answer\` → agent needs input — answer via \`respond-task\`.`,
|
|
45
|
+
`- ${sleepEscalation()}`,
|
|
20
46
|
);
|
|
21
47
|
break;
|
|
22
48
|
|
|
23
49
|
case TaskStatus.WAITING_ANSWER:
|
|
24
|
-
lines.push('- **ACTION REQUIRED** — the agent
|
|
50
|
+
lines.push('- **ACTION REQUIRED** — the agent paused immediately and is waiting for your input.');
|
|
25
51
|
if (task.pendingQuestions.length > 0) {
|
|
26
|
-
|
|
27
|
-
lines.push(...formatPendingQuestionGuidance(task.id, pq));
|
|
52
|
+
lines.push(...formatPendingQuestionGuidance(task.id, task.pendingQuestions[0]!));
|
|
28
53
|
}
|
|
29
54
|
lines.push(`- After responding, call \`wait-task\` with \`task_id: "${task.id}"\` to resume monitoring.`);
|
|
30
55
|
break;
|
|
@@ -32,8 +57,8 @@ export function buildNextActionGuidance(task: TaskState): string[] {
|
|
|
32
57
|
case TaskStatus.COMPLETED:
|
|
33
58
|
lines.push(
|
|
34
59
|
'- The agent finished successfully.',
|
|
60
|
+
`- ${catHint(task)}`,
|
|
35
61
|
`- Read \`task:///${task.id}\` for the full result detail.`,
|
|
36
|
-
`- Read \`task:///${task.id}/log\` for the execution summary.`,
|
|
37
62
|
`- Use \`message-task\` with \`task_id: "${task.id}"\` to send follow-up instructions on the same session.`,
|
|
38
63
|
);
|
|
39
64
|
break;
|
|
@@ -42,8 +67,8 @@ export function buildNextActionGuidance(task: TaskState): string[] {
|
|
|
42
67
|
case TaskStatus.TIMED_OUT:
|
|
43
68
|
lines.push(
|
|
44
69
|
`- The agent failed: ${task.error ?? 'unknown error'}`,
|
|
70
|
+
`- ${catHint(task)}`,
|
|
45
71
|
`- Read \`task:///${task.id}/events\` for the raw event trace to diagnose the failure.`,
|
|
46
|
-
`- Read \`task:///${task.id}/log\` for the execution summary up to the failure point.`,
|
|
47
72
|
'- To retry: spawn a new task with the same prompt. The failed task\'s logs are preserved on disk.',
|
|
48
73
|
);
|
|
49
74
|
if (task.error?.includes('AUTH_TOKEN_EXPIRED')) {
|
|
@@ -51,28 +76,141 @@ export function buildNextActionGuidance(task: TaskState): string[] {
|
|
|
51
76
|
}
|
|
52
77
|
break;
|
|
53
78
|
|
|
54
|
-
|
|
55
|
-
lines.push(
|
|
56
|
-
'- The task was cancelled.',
|
|
57
|
-
`- Read \`task:///${task.id}/log\` to see what was accomplished before cancellation.`,
|
|
58
|
-
);
|
|
79
|
+
default:
|
|
59
80
|
break;
|
|
81
|
+
}
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
'- This task was recovered after a server restart. The original session is lost.',
|
|
64
|
-
`- Read \`task:///${task.id}/log\` to see what was accomplished before the restart.`,
|
|
65
|
-
'- To continue the work: spawn a new task with the same prompt.',
|
|
66
|
-
);
|
|
67
|
-
break;
|
|
83
|
+
return lines;
|
|
84
|
+
}
|
|
68
85
|
|
|
69
|
-
|
|
70
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Guidance appended to wait-task responses.
|
|
88
|
+
* Three branches: terminal, waiting_answer, still working.
|
|
89
|
+
*/
|
|
90
|
+
export function buildWaitGuidance(task: TaskState): string[] {
|
|
91
|
+
const lines: string[] = ['', '---'];
|
|
92
|
+
|
|
93
|
+
if (isTerminalStatus(task.status)) {
|
|
94
|
+
// Terminal state — task is done
|
|
95
|
+
const statusLabel = task.status === TaskStatus.COMPLETED ? 'completed successfully'
|
|
96
|
+
: task.status === TaskStatus.CANCELLED ? 'was cancelled'
|
|
97
|
+
: `failed: ${task.error ?? 'unknown error'}`;
|
|
98
|
+
lines.push(`**Task ${statusLabel}.**`);
|
|
99
|
+
lines.push(
|
|
100
|
+
`- ${catHint(task)}`,
|
|
101
|
+
`- Read \`task:///${task.id}\` for the full result detail.`,
|
|
102
|
+
`- Read \`task:///${task.id}/log\` for the execution summary.`,
|
|
103
|
+
);
|
|
104
|
+
if (task.status === TaskStatus.COMPLETED) {
|
|
105
|
+
lines.push(`- Use \`message-task\` with \`task_id: "${task.id}"\` to send follow-up instructions on the same session.`);
|
|
106
|
+
} else {
|
|
107
|
+
lines.push('- To retry: spawn a new task with the same prompt.');
|
|
108
|
+
if (task.status === TaskStatus.FAILED || task.status === TaskStatus.TIMED_OUT) {
|
|
109
|
+
lines.push(`- Read \`task:///${task.id}/events\` for the raw event trace to diagnose the failure.`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (task.error?.includes('AUTH_TOKEN_EXPIRED')) {
|
|
113
|
+
lines.push('- **Auth fix required:** run `codex auth login` to refresh your token, then retry.');
|
|
114
|
+
}
|
|
115
|
+
return lines;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (task.status === TaskStatus.WAITING_ANSWER) {
|
|
119
|
+
lines.push('**ACTION REQUIRED** — the agent is paused and waiting for your input.');
|
|
120
|
+
if (task.pendingQuestions.length > 0) {
|
|
121
|
+
lines.push(...formatPendingQuestionGuidance(task.id, task.pendingQuestions[0]!));
|
|
122
|
+
}
|
|
123
|
+
lines.push(`- After responding, call \`wait-task\` with \`task_id: "${task.id}"\` to resume monitoring.`);
|
|
124
|
+
return lines;
|
|
71
125
|
}
|
|
72
126
|
|
|
127
|
+
// Still working — wait timed out
|
|
128
|
+
lines.push('**Task is still working.**');
|
|
129
|
+
lines.push(
|
|
130
|
+
`- Call \`wait-task\` again with \`task_id: "${task.id}"\` and a longer \`timeout_ms\`.`,
|
|
131
|
+
`- ${wcHint(task)}`,
|
|
132
|
+
'- If you have other tasks to check, check them now and come back to this one.',
|
|
133
|
+
'- Read `task:///all` for the full scoreboard.',
|
|
134
|
+
`- ${sleepEscalation()}`,
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
return lines;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Guidance appended to respond-task responses.
|
|
142
|
+
* Two branches: task resumed (working), task already terminal.
|
|
143
|
+
*/
|
|
144
|
+
export function buildRespondGuidance(task: TaskState): string[] {
|
|
145
|
+
const lines: string[] = ['', '---'];
|
|
146
|
+
|
|
147
|
+
if (isTerminalStatus(task.status)) {
|
|
148
|
+
lines.push(`**Task is no longer running** (status: ${task.status}).`);
|
|
149
|
+
lines.push(
|
|
150
|
+
'- The task reached a terminal state. The response could not be delivered.',
|
|
151
|
+
`- ${catHint(task)}`,
|
|
152
|
+
`- Read \`task:///${task.id}/log\` to see what was accomplished.`,
|
|
153
|
+
'- To retry: spawn a new task with the same prompt.',
|
|
154
|
+
);
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
lines.push('**Answer submitted — the agent is resuming work.**');
|
|
159
|
+
lines.push(
|
|
160
|
+
'- Run `sleep 30` and then check status.',
|
|
161
|
+
`- To check: call \`wait-task\` with \`task_id: "${task.id}"\`, or read \`task:///${task.id}\`.`,
|
|
162
|
+
`- ${wcHint(task)}`,
|
|
163
|
+
`- ${sleepEscalation()}`,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Guidance appended to message-task responses.
|
|
171
|
+
* The task just received a follow-up turn and is back to RUNNING.
|
|
172
|
+
*/
|
|
173
|
+
export function buildMessageGuidance(task: TaskState): string[] {
|
|
174
|
+
return [
|
|
175
|
+
'', '---',
|
|
176
|
+
'**Message sent — the agent is resuming work.**',
|
|
177
|
+
`- Call \`wait-task\` with \`task_id: "${task.id}"\` to block until the agent finishes or needs input.`,
|
|
178
|
+
`- ${wcHint(task)}`,
|
|
179
|
+
`- ${sleepEscalation()}`,
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Guidance appended to cancel-task responses.
|
|
185
|
+
*/
|
|
186
|
+
export function buildCancelGuidance(summary: {
|
|
187
|
+
cancelled: string[];
|
|
188
|
+
alreadyTerminal: string[];
|
|
189
|
+
notFound: string[];
|
|
190
|
+
}): string[] {
|
|
191
|
+
const lines: string[] = ['', '---', '**What to do next:**'];
|
|
192
|
+
|
|
193
|
+
if (summary.cancelled.length > 0) {
|
|
194
|
+
lines.push(`- Cancelled ${summary.cancelled.length} task(s). Read \`task:///{id}/log\` for partial output of each.`);
|
|
195
|
+
}
|
|
196
|
+
if (summary.alreadyTerminal.length > 0) {
|
|
197
|
+
lines.push(`- ${summary.alreadyTerminal.length} task(s) were already in a terminal state.`);
|
|
198
|
+
}
|
|
199
|
+
if (summary.notFound.length > 0) {
|
|
200
|
+
lines.push(`- ${summary.notFound.length} task ID(s) not found: ${summary.notFound.join(', ')}`);
|
|
201
|
+
}
|
|
202
|
+
lines.push(
|
|
203
|
+
'- Read `task:///all` for the updated scoreboard.',
|
|
204
|
+
'- To resume cancelled work, spawn new tasks with the same prompts.',
|
|
205
|
+
);
|
|
206
|
+
|
|
73
207
|
return lines;
|
|
74
208
|
}
|
|
75
209
|
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
// Pending question formatting (shared helper)
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
|
|
76
214
|
function formatPendingQuestionGuidance(taskId: string, pq: PendingQuestion): string[] {
|
|
77
215
|
const lines: string[] = [];
|
|
78
216
|
|
|
@@ -98,6 +98,42 @@ export function renderScoreboard(tasks: TaskState[]): string {
|
|
|
98
98
|
lines.push(`${badge} ${task.id} -- "${prompt}" (${elapsed})`);
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
// Footer with quick-reference instructions
|
|
102
|
+
lines.push('');
|
|
103
|
+
lines.push('> Details: `task:///{id}` · Logs: `cat -n <output_file>` · Poll: read `task:///all` every ~30s');
|
|
104
|
+
|
|
105
|
+
// Pending Questions section
|
|
106
|
+
const tasksWithQuestions = tasks.filter(t => t.pendingQuestions.length > 0);
|
|
107
|
+
if (tasksWithQuestions.length > 0) {
|
|
108
|
+
lines.push('');
|
|
109
|
+
lines.push(`## Pending Questions (${tasksWithQuestions.length})`);
|
|
110
|
+
|
|
111
|
+
for (const task of tasksWithQuestions) {
|
|
112
|
+
const pq = task.pendingQuestions[0]!;
|
|
113
|
+
lines.push('');
|
|
114
|
+
lines.push(`### ${task.id}`);
|
|
115
|
+
lines.push('');
|
|
116
|
+
|
|
117
|
+
if (pq.type === 'user_input') {
|
|
118
|
+
for (const q of pq.questions) {
|
|
119
|
+
lines.push(`**Q:** ${q.text}`);
|
|
120
|
+
if (q.options && q.options.length > 0) {
|
|
121
|
+
for (let i = 0; i < q.options.length; i++) {
|
|
122
|
+
lines.push(` ${i + 1}. ${q.options[i]}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
const firstId = pq.questions[0]?.id ?? 'q';
|
|
127
|
+
lines.push('');
|
|
128
|
+
lines.push(`Answer: \`respond-task { "task_id": "${task.id}", "type": "user_input", "answers": {"${firstId}": "1"} }\``);
|
|
129
|
+
} else {
|
|
130
|
+
lines.push(formatPendingQuestion(pq));
|
|
131
|
+
lines.push('');
|
|
132
|
+
lines.push(`Answer: \`respond-task { "task_id": "${task.id}", "type": "${pq.type}", ... }\``);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
101
137
|
return lines.join('\n');
|
|
102
138
|
}
|
|
103
139
|
|
|
@@ -147,13 +183,57 @@ export function renderTaskDetail(task: TaskState): string {
|
|
|
147
183
|
lines.push(`| **Updated** | ${task.updatedAt} |`);
|
|
148
184
|
lines.push('');
|
|
149
185
|
|
|
150
|
-
// Pending questions
|
|
186
|
+
// Pending questions — enhanced ACTION REQUIRED block with exact JSON examples
|
|
151
187
|
if (task.pendingQuestions.length > 0) {
|
|
152
|
-
lines.push('##
|
|
153
|
-
|
|
154
|
-
|
|
188
|
+
lines.push('## ACTION REQUIRED — Agent is paused', '');
|
|
189
|
+
|
|
190
|
+
for (const pq of task.pendingQuestions) {
|
|
191
|
+
if (pq.type === 'user_input') {
|
|
192
|
+
for (let i = 0; i < pq.questions.length; i++) {
|
|
193
|
+
const q = pq.questions[i]!;
|
|
194
|
+
lines.push(`### Q${i + 1} [${q.id}] — ${q.text}`);
|
|
195
|
+
if (q.options && q.options.length > 0) {
|
|
196
|
+
for (let j = 0; j < q.options.length; j++) {
|
|
197
|
+
lines.push(` ${j + 1}. **${q.options[j]}**`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
lines.push('');
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Build concrete call example
|
|
204
|
+
const answersExample: Record<string, string> = {};
|
|
205
|
+
for (const q of pq.questions) {
|
|
206
|
+
answersExample[q.id] = q.options?.length ? '1' : 'YOUR_ANSWER';
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
lines.push('### How to answer', '');
|
|
210
|
+
lines.push('You **MUST** call the `respond-task` tool now:', '');
|
|
211
|
+
lines.push('```json');
|
|
212
|
+
lines.push(JSON.stringify({ task_id: task.id, type: 'user_input', answers: answersExample }, null, 2));
|
|
213
|
+
lines.push('```', '');
|
|
214
|
+
lines.push('Answer formats: `"N"` select by number · `"N: detail"` select + context · `"OTHER: text"` freeform', '');
|
|
215
|
+
} else if (pq.type === 'command_approval') {
|
|
216
|
+
lines.push(`**Command approval:** \`${pq.command}\``, '');
|
|
217
|
+
lines.push('```json');
|
|
218
|
+
lines.push(JSON.stringify({ task_id: task.id, type: 'command_approval', decision: 'accept' }, null, 2));
|
|
219
|
+
lines.push('```', '');
|
|
220
|
+
} else if (pq.type === 'file_approval') {
|
|
221
|
+
lines.push(`**File approval:** ${pq.fileChanges.map(f => f.path).join(', ')}`, '');
|
|
222
|
+
lines.push('```json');
|
|
223
|
+
lines.push(JSON.stringify({ task_id: task.id, type: 'file_approval', decision: 'accept' }, null, 2));
|
|
224
|
+
lines.push('```', '');
|
|
225
|
+
} else if (pq.type === 'elicitation') {
|
|
226
|
+
lines.push(`**Elicitation from "${pq.serverName ?? 'unknown'}":** ${pq.message}`, '');
|
|
227
|
+
lines.push('```json');
|
|
228
|
+
lines.push(JSON.stringify({ task_id: task.id, type: 'elicitation', action: 'accept' }, null, 2));
|
|
229
|
+
lines.push('```', '');
|
|
230
|
+
} else if (pq.type === 'dynamic_tool') {
|
|
231
|
+
lines.push(`**Dynamic tool:** \`${pq.toolName}\``, '');
|
|
232
|
+
lines.push('```json');
|
|
233
|
+
lines.push(JSON.stringify({ task_id: task.id, type: 'dynamic_tool', result: 'your result' }, null, 2));
|
|
234
|
+
lines.push('```', '');
|
|
235
|
+
}
|
|
155
236
|
}
|
|
156
|
-
lines.push('');
|
|
157
237
|
}
|
|
158
238
|
|
|
159
239
|
// Error
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
3
|
import { REASONING_OPTIONS } from '../services/reasoning-options.js';
|
|
4
|
+
import type { TaskManager } from '../task/task-manager.js';
|
|
5
|
+
import { buildStatusBanner, buildRespondBanner } from '../services/tool-description-banner.js';
|
|
4
6
|
|
|
5
7
|
// ---------------------------------------------------------------------------
|
|
6
8
|
// Unified task tool schemas (provider-agnostic)
|
|
@@ -105,8 +107,11 @@ const REASONING_DESCRIPTION = [
|
|
|
105
107
|
'Omit to use the server default from config.',
|
|
106
108
|
].join('\n');
|
|
107
109
|
|
|
108
|
-
export function createToolDefinitions(serverVersion?: string): ToolDefinition[] {
|
|
110
|
+
export function createToolDefinitions(serverVersion?: string, taskManager?: TaskManager): ToolDefinition[] {
|
|
109
111
|
const versionTag = serverVersion ? ` (v${serverVersion})` : '';
|
|
112
|
+
// Compute banners once per tool-list request
|
|
113
|
+
const statusBanner = taskManager ? buildStatusBanner(taskManager) : '';
|
|
114
|
+
const respondBanner = taskManager ? buildRespondBanner(taskManager) : '';
|
|
110
115
|
return [
|
|
111
116
|
{
|
|
112
117
|
name: 'spawn-task',
|
|
@@ -227,7 +232,7 @@ export function createToolDefinitions(serverVersion?: string): ToolDefinition[]
|
|
|
227
232
|
'- `dynamic_tool` — return a tool call result via `result`, or an `error` string on failure.',
|
|
228
233
|
'',
|
|
229
234
|
'After responding the task resumes automatically. Follow up with `wait-task` to track the next step.',
|
|
230
|
-
].join('\n'),
|
|
235
|
+
].join('\n') + (respondBanner ? `\n\n${respondBanner}` : ''),
|
|
231
236
|
inputSchema: objectSchema({
|
|
232
237
|
task_id: { type: 'string', minLength: 1, description: 'ID of the paused task. Must currently be in `waiting_answer`.' },
|
|
233
238
|
type: {
|
|
@@ -272,7 +277,7 @@ export function createToolDefinitions(serverVersion?: string): ToolDefinition[]
|
|
|
272
277
|
'Use this to add instructions to a still-running task, ask a completed task to refine or extend its work, or steer the agent after reviewing partial results. If the task is idle, the session is resumed first; if it is actively running, the message is queued as the next turn.',
|
|
273
278
|
'',
|
|
274
279
|
'After calling, follow up with `wait-task` exactly like after `spawn-task`.',
|
|
275
|
-
].join('\n'),
|
|
280
|
+
].join('\n') + (statusBanner ? `\n\n${statusBanner}` : ''),
|
|
276
281
|
inputSchema: objectSchema({
|
|
277
282
|
task_id: { type: 'string', minLength: 1, description: 'ID of the task whose session should receive the follow-up.' },
|
|
278
283
|
message: { type: 'string', minLength: 1, description: 'The follow-up instruction or question. Be as specific as the original prompt — reference files and expected behavior.' },
|
|
@@ -290,7 +295,7 @@ export function createToolDefinitions(serverVersion?: string): ToolDefinition[]
|
|
|
290
295
|
'Cancel one or more running tasks.',
|
|
291
296
|
'',
|
|
292
297
|
'Accepts a single task_id or an array. For each running task, asks the provider to abort execution and marks the task `cancelled`. Tasks already in a terminal state are returned under `already_terminal`; unknown ids are returned under `not_found`. Safe to call on a batch — each id is handled independently.',
|
|
293
|
-
].join('\n'),
|
|
298
|
+
].join('\n') + (statusBanner ? `\n\n${statusBanner}` : ''),
|
|
294
299
|
inputSchema: objectSchema({
|
|
295
300
|
task_id: {
|
|
296
301
|
oneOf: [
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CODEX_ENABLE_FLEET_ENV } from '../config/defaults.js';
|
|
2
|
+
import { getQuestionGuidance } from '../config/question-guidance.js';
|
|
2
3
|
|
|
3
4
|
const TRUTHY_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
|
4
5
|
|
|
@@ -27,3 +28,24 @@ export function appendFleetDeveloperInstructions(
|
|
|
27
28
|
return `${base}\n${FLEET_DEVELOPER_INSTRUCTIONS_SUFFIX}`;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Build the full developerInstructions string for a thread/start call.
|
|
33
|
+
* Composes: user instructions → question guidance → fleet sentinel.
|
|
34
|
+
*/
|
|
35
|
+
export function buildDeveloperInstructions(
|
|
36
|
+
userInstructions: string | undefined,
|
|
37
|
+
provider: string,
|
|
38
|
+
): string | undefined {
|
|
39
|
+
let base = userInstructions?.trim() ?? '';
|
|
40
|
+
|
|
41
|
+
// Append provider-specific question guidance
|
|
42
|
+
const questionGuidance = getQuestionGuidance(provider);
|
|
43
|
+
base = base ? `${base}\n${questionGuidance}` : questionGuidance.trim();
|
|
44
|
+
|
|
45
|
+
// Append fleet sentinel if enabled
|
|
46
|
+
if (isFleetModeEnabled()) {
|
|
47
|
+
base = `${base}\n${FLEET_DEVELOPER_INSTRUCTIONS_SUFFIX}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return base || undefined;
|
|
51
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Description Banner — embeds live task status into tool descriptions.
|
|
3
|
+
*
|
|
4
|
+
* When the orchestrating LLM re-fetches the tool list (via
|
|
5
|
+
* notifications/tools/list_changed), the description for respond-task,
|
|
6
|
+
* message-task, and cancel-task includes a compact status footer showing
|
|
7
|
+
* running/completed/question-pending tasks.
|
|
8
|
+
*
|
|
9
|
+
* Hard cap: 500 chars max for the banner to stay within tool description limits.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TaskManager } from '../task/task-manager.js';
|
|
13
|
+
import { TaskStatus, isTerminalStatus } from '../task/task-state.js';
|
|
14
|
+
|
|
15
|
+
const BANNER_MAX_CHARS = 500;
|
|
16
|
+
const RECENTLY_TERMINAL_WINDOW_MS = 5 * 60 * 1000; // 5 minutes
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Build a compact status banner for message-task and cancel-task descriptions.
|
|
20
|
+
* Shows: running count, recently completed tasks, tasks needing answers.
|
|
21
|
+
* Returns '' when nothing to report.
|
|
22
|
+
*/
|
|
23
|
+
export function buildStatusBanner(taskManager: TaskManager): string {
|
|
24
|
+
const allTasks = taskManager.getAllTasks();
|
|
25
|
+
if (allTasks.length === 0) return '';
|
|
26
|
+
|
|
27
|
+
const now = Date.now();
|
|
28
|
+
const running: string[] = [];
|
|
29
|
+
const needsAnswer: string[] = [];
|
|
30
|
+
const recentlyDone: { id: string; status: string; agoMs: number }[] = [];
|
|
31
|
+
|
|
32
|
+
for (const task of allTasks) {
|
|
33
|
+
if (task.status === TaskStatus.WAITING_ANSWER) {
|
|
34
|
+
needsAnswer.push(task.id);
|
|
35
|
+
} else if (task.status === TaskStatus.RUNNING || task.status === TaskStatus.PENDING) {
|
|
36
|
+
running.push(task.id);
|
|
37
|
+
} else if (isTerminalStatus(task.status)) {
|
|
38
|
+
const updatedMs = new Date(task.updatedAt).getTime();
|
|
39
|
+
const agoMs = now - updatedMs;
|
|
40
|
+
if (agoMs <= RECENTLY_TERMINAL_WINDOW_MS) {
|
|
41
|
+
recentlyDone.push({ id: task.id, status: task.status, agoMs });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (running.length === 0 && needsAnswer.length === 0 && recentlyDone.length === 0) {
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const parts: string[] = ['---'];
|
|
51
|
+
|
|
52
|
+
// Summary line
|
|
53
|
+
const summaryParts: string[] = [];
|
|
54
|
+
if (running.length > 0) summaryParts.push(`${running.length} running`);
|
|
55
|
+
if (needsAnswer.length > 0) summaryParts.push(`${needsAnswer.length} needs answer`);
|
|
56
|
+
if (recentlyDone.length > 0) summaryParts.push(`${recentlyDone.length} recently finished`);
|
|
57
|
+
parts.push(`AGENT STATUS: ${summaryParts.join(' | ')}`);
|
|
58
|
+
|
|
59
|
+
// Tasks needing answers
|
|
60
|
+
for (const id of needsAnswer) {
|
|
61
|
+
parts.push(`- ${id} [waiting_answer] — use respond-task`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Recently terminal tasks (most recent first, limit 3)
|
|
65
|
+
const sorted = recentlyDone.sort((a, b) => a.agoMs - b.agoMs).slice(0, 3);
|
|
66
|
+
for (const t of sorted) {
|
|
67
|
+
const ago = t.agoMs < 60_000 ? `${Math.round(t.agoMs / 1000)}s ago` : `${Math.round(t.agoMs / 60_000)}min ago`;
|
|
68
|
+
parts.push(`- ${t.id} [${t.status}] (${ago})`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
parts.push('Read task:///all for full details.');
|
|
72
|
+
|
|
73
|
+
let banner = parts.join('\n');
|
|
74
|
+
if (banner.length > BANNER_MAX_CHARS) {
|
|
75
|
+
banner = banner.slice(0, BANNER_MAX_CHARS - 3) + '...';
|
|
76
|
+
}
|
|
77
|
+
return banner;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Build a status banner specifically for respond-task.
|
|
82
|
+
* Focuses on tasks with pending questions, showing question text and answer format.
|
|
83
|
+
* Returns '' when no questions are pending.
|
|
84
|
+
*/
|
|
85
|
+
export function buildRespondBanner(taskManager: TaskManager): string {
|
|
86
|
+
const tasksWithQuestions = taskManager.getAllTasks()
|
|
87
|
+
.filter(t => t.status === TaskStatus.WAITING_ANSWER && t.pendingQuestions.length > 0);
|
|
88
|
+
|
|
89
|
+
if (tasksWithQuestions.length === 0) return '';
|
|
90
|
+
|
|
91
|
+
const parts: string[] = ['---'];
|
|
92
|
+
parts.push(`ACTION REQUIRED — ${tasksWithQuestions.length} task(s) waiting for your answer:`);
|
|
93
|
+
|
|
94
|
+
for (const task of tasksWithQuestions) {
|
|
95
|
+
const pq = task.pendingQuestions[0]!;
|
|
96
|
+
if (pq.type === 'user_input' && pq.questions.length > 0) {
|
|
97
|
+
const firstQ = pq.questions[0]!;
|
|
98
|
+
let line = `- ${task.id}: "${firstQ.text}"`;
|
|
99
|
+
if (firstQ.options && firstQ.options.length > 0) {
|
|
100
|
+
const choiceStr = firstQ.options.map((c, i) => `${i + 1}) ${c}`).join(' ');
|
|
101
|
+
line += ` Options: ${choiceStr}`;
|
|
102
|
+
}
|
|
103
|
+
parts.push(line);
|
|
104
|
+
} else {
|
|
105
|
+
parts.push(`- ${task.id}: [${pq.type}]`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
parts.push('Use respond-task { "task_id": "<id>", ... }');
|
|
110
|
+
|
|
111
|
+
let banner = parts.join('\n');
|
|
112
|
+
if (banner.length > BANNER_MAX_CHARS) {
|
|
113
|
+
banner = banner.slice(0, BANNER_MAX_CHARS - 3) + '...';
|
|
114
|
+
}
|
|
115
|
+
return banner;
|
|
116
|
+
}
|