@ai-sdk/harness-cline 1.0.27 → 1.0.29
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 +19 -0
- package/dist/index.d.ts +682 -0
- package/dist/index.js +143 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cline-harness.ts +7 -0
- package/src/cline-session.ts +59 -4
- 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.29",
|
|
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.102",
|
|
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',
|
|
@@ -187,6 +193,7 @@ export function createCline(
|
|
|
187
193
|
...(settings.apiKey ? { apiKey: settings.apiKey } : {}),
|
|
188
194
|
...(settings.baseUrl ? { baseUrl: settings.baseUrl } : {}),
|
|
189
195
|
...(settings.headers ? { headers: settings.headers } : {}),
|
|
196
|
+
...(startOpts.headers ? { agentHeaders: startOpts.headers } : {}),
|
|
190
197
|
...(settings.reasoningEffort !== undefined
|
|
191
198
|
? { reasoningEffort: settings.reasoningEffort }
|
|
192
199
|
: {}),
|
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 {
|
|
@@ -92,6 +97,7 @@ export interface ClineSessionSettings {
|
|
|
92
97
|
readonly apiKey?: string;
|
|
93
98
|
readonly baseUrl?: string;
|
|
94
99
|
readonly headers?: Record<string, string>;
|
|
100
|
+
readonly agentHeaders?: Readonly<Record<string, string>>;
|
|
95
101
|
readonly reasoningEffort?: ClineReasoningEffort;
|
|
96
102
|
readonly maxIterations?: number;
|
|
97
103
|
}
|
|
@@ -198,10 +204,16 @@ function createClineAgentModel({
|
|
|
198
204
|
const headers = isAiGateway
|
|
199
205
|
? {
|
|
200
206
|
...settings.headers,
|
|
207
|
+
...settings.agentHeaders,
|
|
201
208
|
'User-Agent': clientApp,
|
|
202
209
|
'x-client-app': clientApp,
|
|
203
210
|
}
|
|
204
|
-
: settings.headers
|
|
211
|
+
: settings.headers != null || settings.agentHeaders != null
|
|
212
|
+
? {
|
|
213
|
+
...settings.headers,
|
|
214
|
+
...settings.agentHeaders,
|
|
215
|
+
}
|
|
216
|
+
: undefined;
|
|
205
217
|
const gateway = Llms.createGateway({
|
|
206
218
|
providerConfigs: [
|
|
207
219
|
{
|
|
@@ -320,6 +332,8 @@ export async function createClineSession(
|
|
|
320
332
|
// Emit channel set at the start of every turn and cleared on end.
|
|
321
333
|
let currentEmit: ((part: HarnessV1StreamPart) => void) | undefined;
|
|
322
334
|
let translatorState: ClineTranslatorState | undefined;
|
|
335
|
+
const pendingQuestions = new Map<string, PendingClineQuestion>();
|
|
336
|
+
const pendingQuestionResults = new Map<string, PendingClineQuestionResult>();
|
|
323
337
|
let activeTurn: ActiveClineTurn | undefined;
|
|
324
338
|
|
|
325
339
|
function settlePendingToolResults(reason: string): void {
|
|
@@ -355,6 +369,14 @@ export async function createClineSession(
|
|
|
355
369
|
pendingUserMessages.length = 0;
|
|
356
370
|
}
|
|
357
371
|
|
|
372
|
+
function settlePendingQuestions(reason: string): void {
|
|
373
|
+
for (const pending of pendingQuestions.values()) {
|
|
374
|
+
pending.resolve(reason);
|
|
375
|
+
}
|
|
376
|
+
pendingQuestions.clear();
|
|
377
|
+
pendingQuestionResults.clear();
|
|
378
|
+
}
|
|
379
|
+
|
|
358
380
|
async function persistHistory(): Promise<void> {
|
|
359
381
|
const messages = agent?.snapshot().messages ?? currentMessages;
|
|
360
382
|
await persistHistoryToSandbox({
|
|
@@ -414,7 +436,12 @@ export async function createClineSession(
|
|
|
414
436
|
? { maxIterations: input.settings.maxIterations }
|
|
415
437
|
: {}),
|
|
416
438
|
tools: [
|
|
417
|
-
...buildBuiltinAgentTools({
|
|
439
|
+
...buildBuiltinAgentTools({
|
|
440
|
+
ops,
|
|
441
|
+
activeNames: activeBuiltinNames,
|
|
442
|
+
pendingQuestions,
|
|
443
|
+
pendingQuestionResults,
|
|
444
|
+
}),
|
|
418
445
|
...(rebuildInput.skillsRuntime.tool &&
|
|
419
446
|
activeBuiltinNames.includes('skills')
|
|
420
447
|
? [rebuildInput.skillsRuntime.tool]
|
|
@@ -505,6 +532,7 @@ export async function createClineSession(
|
|
|
505
532
|
// executions do not dangle, then abort the loop itself.
|
|
506
533
|
settlePendingToolResults('Turn aborted');
|
|
507
534
|
settlePendingToolApprovals('Turn aborted');
|
|
535
|
+
settlePendingQuestions('The question was cancelled.');
|
|
508
536
|
agent?.abort('Turn aborted by caller');
|
|
509
537
|
};
|
|
510
538
|
if (controlInput.abortSignal) {
|
|
@@ -523,6 +551,26 @@ export async function createClineSession(
|
|
|
523
551
|
|
|
524
552
|
return {
|
|
525
553
|
async submitToolResult(args) {
|
|
554
|
+
const pendingQuestion = pendingQuestions.get(args.toolCallId);
|
|
555
|
+
if (pendingQuestion != null) {
|
|
556
|
+
pendingQuestions.delete(args.toolCallId);
|
|
557
|
+
pendingQuestion.resolve(
|
|
558
|
+
toClineQuestionResult({
|
|
559
|
+
nativeInput: pendingQuestion.input,
|
|
560
|
+
output: args.output as HarnessV1QuestionsToolOutput,
|
|
561
|
+
}),
|
|
562
|
+
);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
if (args.toolResult?.toolName === 'askUserQuestions') {
|
|
566
|
+
const nativeRequest = args.toolResult.providerOptions?.cline
|
|
567
|
+
?.nativeRequest as PendingClineQuestion['input'] | undefined;
|
|
568
|
+
if (nativeRequest == null) return;
|
|
569
|
+
pendingQuestionResults.set(clineQuestionKey(nativeRequest), {
|
|
570
|
+
output: args.output as HarnessV1QuestionsToolOutput,
|
|
571
|
+
});
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
526
574
|
const pending = pendingToolResults.get(args.toolCallId);
|
|
527
575
|
if (!pending) return;
|
|
528
576
|
pendingToolResults.delete(args.toolCallId);
|
|
@@ -784,6 +832,7 @@ export async function createClineSession(
|
|
|
784
832
|
parkedClineSessions.delete(input.sessionId);
|
|
785
833
|
settlePendingToolResults('Cline session stopped');
|
|
786
834
|
settlePendingToolApprovals('Cline session stopped');
|
|
835
|
+
settlePendingQuestions('The question was cancelled.');
|
|
787
836
|
|
|
788
837
|
// Persist the conversation into the sandbox so a future process can pick
|
|
789
838
|
// it up after the sandbox provider reattaches.
|
|
@@ -889,7 +938,11 @@ export async function createClineSession(
|
|
|
889
938
|
doStop,
|
|
890
939
|
|
|
891
940
|
doDetach: async (): Promise<HarnessV1ResumeSessionState> => {
|
|
892
|
-
if (
|
|
941
|
+
if (
|
|
942
|
+
activeTurn != null ||
|
|
943
|
+
pendingToolResults.size > 0 ||
|
|
944
|
+
pendingQuestions.size > 0
|
|
945
|
+
) {
|
|
893
946
|
parkedClineSessions.set(input.sessionId, sessionImpl);
|
|
894
947
|
try {
|
|
895
948
|
await persistHistory();
|
|
@@ -916,7 +969,9 @@ export async function createClineSession(
|
|
|
916
969
|
}
|
|
917
970
|
if (
|
|
918
971
|
activeTurn != null &&
|
|
919
|
-
(pendingToolResults.size > 0 ||
|
|
972
|
+
(pendingToolResults.size > 0 ||
|
|
973
|
+
pendingToolApprovals.size > 0 ||
|
|
974
|
+
pendingQuestions.size > 0)
|
|
920
975
|
) {
|
|
921
976
|
parkedClineSessions.set(input.sessionId, sessionImpl);
|
|
922
977
|
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 } : {}),
|