@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-cline",
3
- "version": "1.0.27",
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.100",
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",
@@ -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
  : {}),
@@ -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({ ops, activeNames: activeBuiltinNames }),
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 (activeTurn != null || pendingToolResults.size > 0) {
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 || pendingToolApprovals.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 {
@@ -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
- toolFiltering.toolNames.includes(name),
70
+ filteredNames.includes(name),
64
71
  );
65
72
  }
66
73
  return CLINE_NATIVE_BUILTIN_NAMES.filter(
67
- name => !toolFiltering.toolNames.includes(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
@@ -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: event.toolCall.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 } : {}),