@ai-sdk/harness-cline 1.0.27 → 1.0.28
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/CHANGELOG.md +8 -0
- package/dist/index.d.ts +682 -0
- package/dist/index.js +137 -9
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cline-harness.ts +6 -0
- package/src/cline-session.ts +51 -3
- package/src/cline-tools.ts +89 -2
- package/src/cline-translate.ts +36 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-sdk/harness-cline",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/harness": "1.0.
|
|
29
|
+
"@ai-sdk/harness": "1.0.101",
|
|
30
30
|
"@ai-sdk/provider": "4.0.10",
|
|
31
31
|
"@ai-sdk/provider-utils": "5.0.36",
|
|
32
32
|
"@cline/agents": "^0.0.79",
|
package/src/cline-harness.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
commonTool,
|
|
3
|
+
HARNESS_V1_BUILTIN_TOOLS,
|
|
3
4
|
type HarnessV1,
|
|
4
5
|
type HarnessV1BuiltinTool,
|
|
5
6
|
} from '@ai-sdk/harness';
|
|
@@ -70,6 +71,11 @@ export type ClineHarnessSettings = {
|
|
|
70
71
|
* `cline-tools.ts` — keep the two in sync.
|
|
71
72
|
*/
|
|
72
73
|
const CLINE_BUILTIN_TOOLS = {
|
|
74
|
+
askUserQuestions: {
|
|
75
|
+
...HARNESS_V1_BUILTIN_TOOLS.askUserQuestions,
|
|
76
|
+
nativeName: 'ask_question',
|
|
77
|
+
toolUseKind: 'readonly',
|
|
78
|
+
},
|
|
73
79
|
read: commonTool('read', {
|
|
74
80
|
nativeName: 'read',
|
|
75
81
|
toolUseKind: 'readonly',
|
package/src/cline-session.ts
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
type HarnessV1PermissionMode,
|
|
8
8
|
type HarnessV1PromptControl,
|
|
9
9
|
type HarnessV1PromptTurnOptions,
|
|
10
|
+
type HarnessV1QuestionsToolOutput,
|
|
10
11
|
type HarnessV1ResumeSessionState,
|
|
11
12
|
type HarnessV1Session,
|
|
12
13
|
type HarnessV1Skill,
|
|
@@ -45,8 +46,12 @@ import {
|
|
|
45
46
|
CLINE_NATIVE_TOOL_KINDS,
|
|
46
47
|
createClineToolResult,
|
|
47
48
|
isClineBuiltinToolName,
|
|
49
|
+
clineQuestionKey,
|
|
48
50
|
resolveActiveClineBuiltinNames,
|
|
49
51
|
type PendingToolResult,
|
|
52
|
+
type PendingClineQuestion,
|
|
53
|
+
type PendingClineQuestionResult,
|
|
54
|
+
toClineQuestionResult,
|
|
50
55
|
unwrapClineToolResult,
|
|
51
56
|
} from './cline-tools';
|
|
52
57
|
import {
|
|
@@ -320,6 +325,8 @@ export async function createClineSession(
|
|
|
320
325
|
// Emit channel set at the start of every turn and cleared on end.
|
|
321
326
|
let currentEmit: ((part: HarnessV1StreamPart) => void) | undefined;
|
|
322
327
|
let translatorState: ClineTranslatorState | undefined;
|
|
328
|
+
const pendingQuestions = new Map<string, PendingClineQuestion>();
|
|
329
|
+
const pendingQuestionResults = new Map<string, PendingClineQuestionResult>();
|
|
323
330
|
let activeTurn: ActiveClineTurn | undefined;
|
|
324
331
|
|
|
325
332
|
function settlePendingToolResults(reason: string): void {
|
|
@@ -355,6 +362,14 @@ export async function createClineSession(
|
|
|
355
362
|
pendingUserMessages.length = 0;
|
|
356
363
|
}
|
|
357
364
|
|
|
365
|
+
function settlePendingQuestions(reason: string): void {
|
|
366
|
+
for (const pending of pendingQuestions.values()) {
|
|
367
|
+
pending.resolve(reason);
|
|
368
|
+
}
|
|
369
|
+
pendingQuestions.clear();
|
|
370
|
+
pendingQuestionResults.clear();
|
|
371
|
+
}
|
|
372
|
+
|
|
358
373
|
async function persistHistory(): Promise<void> {
|
|
359
374
|
const messages = agent?.snapshot().messages ?? currentMessages;
|
|
360
375
|
await persistHistoryToSandbox({
|
|
@@ -414,7 +429,12 @@ export async function createClineSession(
|
|
|
414
429
|
? { maxIterations: input.settings.maxIterations }
|
|
415
430
|
: {}),
|
|
416
431
|
tools: [
|
|
417
|
-
...buildBuiltinAgentTools({
|
|
432
|
+
...buildBuiltinAgentTools({
|
|
433
|
+
ops,
|
|
434
|
+
activeNames: activeBuiltinNames,
|
|
435
|
+
pendingQuestions,
|
|
436
|
+
pendingQuestionResults,
|
|
437
|
+
}),
|
|
418
438
|
...(rebuildInput.skillsRuntime.tool &&
|
|
419
439
|
activeBuiltinNames.includes('skills')
|
|
420
440
|
? [rebuildInput.skillsRuntime.tool]
|
|
@@ -505,6 +525,7 @@ export async function createClineSession(
|
|
|
505
525
|
// executions do not dangle, then abort the loop itself.
|
|
506
526
|
settlePendingToolResults('Turn aborted');
|
|
507
527
|
settlePendingToolApprovals('Turn aborted');
|
|
528
|
+
settlePendingQuestions('The question was cancelled.');
|
|
508
529
|
agent?.abort('Turn aborted by caller');
|
|
509
530
|
};
|
|
510
531
|
if (controlInput.abortSignal) {
|
|
@@ -523,6 +544,26 @@ export async function createClineSession(
|
|
|
523
544
|
|
|
524
545
|
return {
|
|
525
546
|
async submitToolResult(args) {
|
|
547
|
+
const pendingQuestion = pendingQuestions.get(args.toolCallId);
|
|
548
|
+
if (pendingQuestion != null) {
|
|
549
|
+
pendingQuestions.delete(args.toolCallId);
|
|
550
|
+
pendingQuestion.resolve(
|
|
551
|
+
toClineQuestionResult({
|
|
552
|
+
nativeInput: pendingQuestion.input,
|
|
553
|
+
output: args.output as HarnessV1QuestionsToolOutput,
|
|
554
|
+
}),
|
|
555
|
+
);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
if (args.toolResult?.toolName === 'askUserQuestions') {
|
|
559
|
+
const nativeRequest = args.toolResult.providerOptions?.cline
|
|
560
|
+
?.nativeRequest as PendingClineQuestion['input'] | undefined;
|
|
561
|
+
if (nativeRequest == null) return;
|
|
562
|
+
pendingQuestionResults.set(clineQuestionKey(nativeRequest), {
|
|
563
|
+
output: args.output as HarnessV1QuestionsToolOutput,
|
|
564
|
+
});
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
526
567
|
const pending = pendingToolResults.get(args.toolCallId);
|
|
527
568
|
if (!pending) return;
|
|
528
569
|
pendingToolResults.delete(args.toolCallId);
|
|
@@ -784,6 +825,7 @@ export async function createClineSession(
|
|
|
784
825
|
parkedClineSessions.delete(input.sessionId);
|
|
785
826
|
settlePendingToolResults('Cline session stopped');
|
|
786
827
|
settlePendingToolApprovals('Cline session stopped');
|
|
828
|
+
settlePendingQuestions('The question was cancelled.');
|
|
787
829
|
|
|
788
830
|
// Persist the conversation into the sandbox so a future process can pick
|
|
789
831
|
// it up after the sandbox provider reattaches.
|
|
@@ -889,7 +931,11 @@ export async function createClineSession(
|
|
|
889
931
|
doStop,
|
|
890
932
|
|
|
891
933
|
doDetach: async (): Promise<HarnessV1ResumeSessionState> => {
|
|
892
|
-
if (
|
|
934
|
+
if (
|
|
935
|
+
activeTurn != null ||
|
|
936
|
+
pendingToolResults.size > 0 ||
|
|
937
|
+
pendingQuestions.size > 0
|
|
938
|
+
) {
|
|
893
939
|
parkedClineSessions.set(input.sessionId, sessionImpl);
|
|
894
940
|
try {
|
|
895
941
|
await persistHistory();
|
|
@@ -916,7 +962,9 @@ export async function createClineSession(
|
|
|
916
962
|
}
|
|
917
963
|
if (
|
|
918
964
|
activeTurn != null &&
|
|
919
|
-
(pendingToolResults.size > 0 ||
|
|
965
|
+
(pendingToolResults.size > 0 ||
|
|
966
|
+
pendingToolApprovals.size > 0 ||
|
|
967
|
+
pendingQuestions.size > 0)
|
|
920
968
|
) {
|
|
921
969
|
parkedClineSessions.set(input.sessionId, sessionImpl);
|
|
922
970
|
try {
|
package/src/cline-tools.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
HarnessV1BuiltinToolFiltering,
|
|
3
|
+
HarnessV1QuestionsToolOutput,
|
|
3
4
|
HarnessV1ToolSpec,
|
|
4
5
|
} from '@ai-sdk/harness';
|
|
5
6
|
import {
|
|
@@ -7,6 +8,7 @@ import {
|
|
|
7
8
|
type AgentTool,
|
|
8
9
|
type AgentToolResult,
|
|
9
10
|
} from '@cline/agents';
|
|
11
|
+
import { createDefaultTools } from '@cline/core';
|
|
10
12
|
import type { ClineRemoteOps } from './cline-remote-ops';
|
|
11
13
|
import { getErrorText } from './cline-utils';
|
|
12
14
|
|
|
@@ -17,6 +19,7 @@ import { getErrorText } from './cline-utils';
|
|
|
17
19
|
* identical.
|
|
18
20
|
*/
|
|
19
21
|
export const CLINE_NATIVE_BUILTIN_NAMES = [
|
|
22
|
+
'ask_question',
|
|
20
23
|
'read',
|
|
21
24
|
'write',
|
|
22
25
|
'edit',
|
|
@@ -33,6 +36,7 @@ export type ClineNativeBuiltinName =
|
|
|
33
36
|
export const CLINE_NATIVE_TOOL_KINDS: Readonly<
|
|
34
37
|
Record<ClineNativeBuiltinName, 'readonly' | 'edit' | 'bash'>
|
|
35
38
|
> = {
|
|
39
|
+
ask_question: 'readonly',
|
|
36
40
|
read: 'readonly',
|
|
37
41
|
write: 'edit',
|
|
38
42
|
edit: 'edit',
|
|
@@ -58,13 +62,16 @@ export function resolveActiveClineBuiltinNames(
|
|
|
58
62
|
toolFiltering: HarnessV1BuiltinToolFiltering | undefined,
|
|
59
63
|
): ReadonlyArray<ClineNativeBuiltinName> {
|
|
60
64
|
if (toolFiltering == null) return CLINE_NATIVE_BUILTIN_NAMES;
|
|
65
|
+
const filteredNames = toolFiltering.toolNames.map(name =>
|
|
66
|
+
name === 'askUserQuestions' ? 'ask_question' : name,
|
|
67
|
+
);
|
|
61
68
|
if (toolFiltering.mode === 'allow') {
|
|
62
69
|
return CLINE_NATIVE_BUILTIN_NAMES.filter(name =>
|
|
63
|
-
|
|
70
|
+
filteredNames.includes(name),
|
|
64
71
|
);
|
|
65
72
|
}
|
|
66
73
|
return CLINE_NATIVE_BUILTIN_NAMES.filter(
|
|
67
|
-
name => !
|
|
74
|
+
name => !filteredNames.includes(name),
|
|
68
75
|
);
|
|
69
76
|
}
|
|
70
77
|
|
|
@@ -72,6 +79,25 @@ export interface PendingToolResult {
|
|
|
72
79
|
resolve: (value: unknown) => void;
|
|
73
80
|
}
|
|
74
81
|
|
|
82
|
+
export interface PendingClineQuestion {
|
|
83
|
+
readonly input: {
|
|
84
|
+
readonly question: string;
|
|
85
|
+
readonly options: readonly string[];
|
|
86
|
+
};
|
|
87
|
+
resolve: (value: string) => void;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface PendingClineQuestionResult {
|
|
91
|
+
readonly output: HarnessV1QuestionsToolOutput;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function clineQuestionKey(input: {
|
|
95
|
+
readonly question: string;
|
|
96
|
+
readonly options: readonly string[];
|
|
97
|
+
}): string {
|
|
98
|
+
return JSON.stringify([input.question, input.options]);
|
|
99
|
+
}
|
|
100
|
+
|
|
75
101
|
const clineToolResultMarker = Symbol('cline-tool-result');
|
|
76
102
|
|
|
77
103
|
interface ClineToolResult {
|
|
@@ -136,12 +162,50 @@ async function guarded<T>(run: () => Promise<T>): Promise<ClineToolResult> {
|
|
|
136
162
|
export function buildBuiltinAgentTools({
|
|
137
163
|
ops,
|
|
138
164
|
activeNames,
|
|
165
|
+
pendingQuestions = new Map(),
|
|
166
|
+
pendingQuestionResults = new Map(),
|
|
139
167
|
}: {
|
|
140
168
|
ops: ClineRemoteOps;
|
|
141
169
|
activeNames: ReadonlyArray<ClineNativeBuiltinName>;
|
|
170
|
+
pendingQuestions?: Map<string, PendingClineQuestion>;
|
|
171
|
+
pendingQuestionResults?: Map<string, PendingClineQuestionResult>;
|
|
142
172
|
}): AgentTool<any, any>[] {
|
|
143
173
|
// biome/eslint: `any` matches the runtime's own `tools?: readonly AgentTool<any, any>[]`.
|
|
144
174
|
const tools: Partial<Record<ClineNativeBuiltinName, AgentTool<any, any>>> = {
|
|
175
|
+
ask_question: createDefaultTools({
|
|
176
|
+
executors: {
|
|
177
|
+
askQuestion: async (question, options, context) => {
|
|
178
|
+
if (context.toolCallId == null) {
|
|
179
|
+
throw new Error(
|
|
180
|
+
'Cline ask_question did not provide a tool call id.',
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
const questionKey = clineQuestionKey({ question, options });
|
|
184
|
+
const pendingResult = pendingQuestionResults.get(questionKey);
|
|
185
|
+
if (pendingResult != null) {
|
|
186
|
+
pendingQuestionResults.delete(questionKey);
|
|
187
|
+
return toClineQuestionResult({
|
|
188
|
+
nativeInput: { question, options },
|
|
189
|
+
output: pendingResult.output,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
return new Promise<string>(resolve => {
|
|
193
|
+
pendingQuestions.set(context.toolCallId!, {
|
|
194
|
+
input: { question, options },
|
|
195
|
+
resolve,
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
enableReadFiles: false,
|
|
201
|
+
enableSearch: false,
|
|
202
|
+
enableBash: false,
|
|
203
|
+
enableWebFetch: false,
|
|
204
|
+
enableApplyPatch: false,
|
|
205
|
+
enableEditor: false,
|
|
206
|
+
enableSkills: false,
|
|
207
|
+
enableAskQuestion: true,
|
|
208
|
+
}).find(tool => tool.name === 'ask_question'),
|
|
145
209
|
read: createTool({
|
|
146
210
|
name: 'read',
|
|
147
211
|
description: 'Read file contents.',
|
|
@@ -325,6 +389,29 @@ export function buildBuiltinAgentTools({
|
|
|
325
389
|
.filter((tool): tool is AgentTool<any, any> => tool != null);
|
|
326
390
|
}
|
|
327
391
|
|
|
392
|
+
export function toClineQuestionResult(input: {
|
|
393
|
+
nativeInput: PendingClineQuestion['input'];
|
|
394
|
+
output: HarnessV1QuestionsToolOutput;
|
|
395
|
+
}): string {
|
|
396
|
+
if (
|
|
397
|
+
input.output.action === 'declined' ||
|
|
398
|
+
input.output.action === 'cancelled'
|
|
399
|
+
) {
|
|
400
|
+
return `The user ${input.output.action} the question.`;
|
|
401
|
+
}
|
|
402
|
+
const answer = input.output.answers['question-1'];
|
|
403
|
+
if (answer == null) return 'The user did not answer the question.';
|
|
404
|
+
if (answer.freeform != null) return answer.freeform;
|
|
405
|
+
const result = answer.optionIds
|
|
406
|
+
.map(id => {
|
|
407
|
+
const index = Number(id.replace(/^option-/, '')) - 1;
|
|
408
|
+
return input.nativeInput.options[index];
|
|
409
|
+
})
|
|
410
|
+
.filter((value): value is string => value != null)
|
|
411
|
+
.join(', ');
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
|
|
328
415
|
/**
|
|
329
416
|
* Build host-executed user tools. When the model calls one, the tool parks a
|
|
330
417
|
* promise keyed by the runtime's `toolCallId`; the harness caller supplies the
|
package/src/cline-translate.ts
CHANGED
|
@@ -78,6 +78,38 @@ function toolCallPart(
|
|
|
78
78
|
if (isMcpTool) {
|
|
79
79
|
state.dynamicToolCallIds.add(toolCall.toolCallId);
|
|
80
80
|
}
|
|
81
|
+
if (toolCall.toolName === 'ask_question') {
|
|
82
|
+
const nativeInput = toolCall.input as {
|
|
83
|
+
question: string;
|
|
84
|
+
options: string[];
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
type: 'tool-call',
|
|
88
|
+
toolCallId: toolCall.toolCallId,
|
|
89
|
+
toolName: 'askUserQuestions',
|
|
90
|
+
input: JSON.stringify({
|
|
91
|
+
allowPartialAnswers: false,
|
|
92
|
+
questions: [
|
|
93
|
+
{
|
|
94
|
+
id: 'question-1',
|
|
95
|
+
question: nativeInput.question,
|
|
96
|
+
options: nativeInput.options.map((label, index) => ({
|
|
97
|
+
id: `option-${index + 1}`,
|
|
98
|
+
label,
|
|
99
|
+
})),
|
|
100
|
+
allowFreeForm: true,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
}),
|
|
104
|
+
providerExecuted: false,
|
|
105
|
+
providerMetadata: {
|
|
106
|
+
cline: {
|
|
107
|
+
nativeRequest: nativeInput,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
nativeName: 'ask_question',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
81
113
|
return {
|
|
82
114
|
type: 'tool-call',
|
|
83
115
|
toolCallId: toolCall.toolCallId,
|
|
@@ -209,7 +241,10 @@ export function translateClineEvent(
|
|
|
209
241
|
{
|
|
210
242
|
type: 'tool-result',
|
|
211
243
|
toolCallId: event.toolCall.toolCallId,
|
|
212
|
-
toolName:
|
|
244
|
+
toolName:
|
|
245
|
+
event.toolCall.toolName === 'ask_question'
|
|
246
|
+
? 'askUserQuestions'
|
|
247
|
+
: event.toolCall.toolName,
|
|
213
248
|
result: toToolResultValue(output),
|
|
214
249
|
...(resultPart?.isError ? { isError: true } : {}),
|
|
215
250
|
...(dynamic ? { dynamic: true } : {}),
|