@ai-sdk/harness-cline 0.0.0 → 1.0.0

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.
@@ -0,0 +1,59 @@
1
+ import path from 'node:path';
2
+ import type { HarnessV1Skill } from '@ai-sdk/harness';
3
+ import { writeSkills } from '@ai-sdk/harness/utils';
4
+ import type { Experimental_SandboxSession } from '@ai-sdk/provider-utils';
5
+
6
+ /**
7
+ * Materialize harness-provided skills into the sandbox HOME
8
+ * (`~/.agents/skills/<name>/SKILL.md` plus any auxiliary files) and return
9
+ * the root directory they were written to. The Cline runtime has no native
10
+ * skill loader, so the session surfaces them to the model through a system
11
+ * prompt section (see `renderSkillsPromptSection`) that points at these files.
12
+ */
13
+ export async function writeClineSkills({
14
+ sandbox,
15
+ sandboxHomeDir,
16
+ skills,
17
+ abortSignal,
18
+ }: {
19
+ sandbox: Experimental_SandboxSession;
20
+ sandboxHomeDir: string;
21
+ skills: ReadonlyArray<HarnessV1Skill>;
22
+ abortSignal?: AbortSignal;
23
+ }): Promise<string | undefined> {
24
+ if (skills.length === 0) return undefined;
25
+ const skillRootDir = path.posix.join(sandboxHomeDir, '.agents', 'skills');
26
+ await writeSkills({
27
+ sandbox,
28
+ rootDir: skillRootDir,
29
+ skills,
30
+ ...(abortSignal ? { abortSignal } : {}),
31
+ });
32
+ return skillRootDir;
33
+ }
34
+
35
+ /**
36
+ * Render the system-prompt section that advertises available skills. Only the
37
+ * name + description sit in context; the model loads the full skill via the
38
+ * `read` tool when the task calls for it (progressive disclosure).
39
+ */
40
+ export function renderSkillsPromptSection(
41
+ skills: ReadonlyArray<HarnessV1Skill>,
42
+ skillRootDir: string,
43
+ ): string {
44
+ const entries = skills
45
+ .map(
46
+ skill =>
47
+ `- ${skill.name}: ${skill.description}\n SKILL.md: ${path.posix.join(
48
+ skillRootDir,
49
+ skill.name,
50
+ 'SKILL.md',
51
+ )}`,
52
+ )
53
+ .join('\n');
54
+ return (
55
+ '## Skills\n\n' +
56
+ 'The following skills are available. When a task matches a skill, read its SKILL.md with the `read` tool and follow its instructions.\n\n' +
57
+ entries
58
+ );
59
+ }
@@ -0,0 +1,381 @@
1
+ import type {
2
+ HarnessV1BuiltinToolFiltering,
3
+ HarnessV1ToolSpec,
4
+ } from '@ai-sdk/harness';
5
+ import {
6
+ createTool,
7
+ type AgentTool,
8
+ type AgentToolResult,
9
+ } from '@cline/agents';
10
+ import type { ClineRemoteOps } from './cline-remote-ops';
11
+ import { getErrorText } from './cline-utils';
12
+
13
+ /**
14
+ * Native names of the Cline harness's built-in tools. The Cline runtime
15
+ * requires snake_case tool names, and every one of these already matches its
16
+ * cross-harness common name, so the public (wire) names and native names are
17
+ * identical.
18
+ */
19
+ export const CLINE_NATIVE_BUILTIN_NAMES = [
20
+ 'read',
21
+ 'write',
22
+ 'edit',
23
+ 'bash',
24
+ 'grep',
25
+ 'glob',
26
+ 'ls',
27
+ ] as const;
28
+
29
+ export type ClineNativeBuiltinName =
30
+ (typeof CLINE_NATIVE_BUILTIN_NAMES)[number];
31
+
32
+ export const CLINE_NATIVE_TOOL_KINDS: Readonly<
33
+ Record<ClineNativeBuiltinName, 'readonly' | 'edit' | 'bash'>
34
+ > = {
35
+ read: 'readonly',
36
+ write: 'edit',
37
+ edit: 'edit',
38
+ bash: 'bash',
39
+ grep: 'readonly',
40
+ glob: 'readonly',
41
+ ls: 'readonly',
42
+ };
43
+
44
+ export function isClineBuiltinToolName(
45
+ name: string,
46
+ ): name is ClineNativeBuiltinName {
47
+ return (CLINE_NATIVE_BUILTIN_NAMES as readonly string[]).includes(name);
48
+ }
49
+
50
+ /**
51
+ * Resolve which built-in tools stay active for a session under the framework's
52
+ * `builtinToolFiltering`. Names map 1:1 (native === common), so filtering is a
53
+ * plain include/exclude.
54
+ */
55
+ export function resolveActiveClineBuiltinNames(
56
+ toolFiltering: HarnessV1BuiltinToolFiltering | undefined,
57
+ ): ReadonlyArray<ClineNativeBuiltinName> {
58
+ if (toolFiltering == null) return CLINE_NATIVE_BUILTIN_NAMES;
59
+ if (toolFiltering.mode === 'allow') {
60
+ return CLINE_NATIVE_BUILTIN_NAMES.filter(name =>
61
+ toolFiltering.toolNames.includes(name),
62
+ );
63
+ }
64
+ return CLINE_NATIVE_BUILTIN_NAMES.filter(
65
+ name => !toolFiltering.toolNames.includes(name),
66
+ );
67
+ }
68
+
69
+ export interface PendingToolResult {
70
+ resolve: (value: unknown) => void;
71
+ }
72
+
73
+ const clineToolResultMarker = Symbol('cline-tool-result');
74
+
75
+ interface ClineToolResult {
76
+ readonly [clineToolResultMarker]: true;
77
+ readonly output: unknown;
78
+ readonly isError?: boolean;
79
+ }
80
+
81
+ export function createClineToolResult({
82
+ output,
83
+ isError,
84
+ }: {
85
+ output: unknown;
86
+ isError?: boolean;
87
+ }): ClineToolResult {
88
+ return {
89
+ [clineToolResultMarker]: true,
90
+ output,
91
+ ...(isError ? { isError: true } : {}),
92
+ };
93
+ }
94
+
95
+ export function unwrapClineToolResult(
96
+ value: unknown,
97
+ ): AgentToolResult | undefined {
98
+ if (
99
+ value == null ||
100
+ typeof value !== 'object' ||
101
+ (value as ClineToolResult)[clineToolResultMarker] !== true
102
+ ) {
103
+ return undefined;
104
+ }
105
+
106
+ const result = value as ClineToolResult;
107
+ return {
108
+ output: result.output,
109
+ ...(result.isError ? { isError: true } : {}),
110
+ };
111
+ }
112
+
113
+ /*
114
+ * The Cline runtime treats a thrown tool error as a "mistake" against the
115
+ * agent's mistake limit, so built-ins catch and return errors as structured
116
+ * data — the model reads the error and adjusts.
117
+ */
118
+ async function guarded<T>(run: () => Promise<T>): Promise<ClineToolResult> {
119
+ try {
120
+ return createClineToolResult({ output: await run() });
121
+ } catch (error) {
122
+ return createClineToolResult({
123
+ output: { error: getErrorText(error) },
124
+ isError: true,
125
+ });
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Build the sandbox-backed built-in `AgentTool`s handed to the Cline runtime.
131
+ * These schemas mirror the zod declarations in `CLINE_BUILTIN_TOOLS`
132
+ * (cline-harness.ts) — keep the two in sync.
133
+ */
134
+ export function buildBuiltinAgentTools({
135
+ ops,
136
+ activeNames,
137
+ }: {
138
+ ops: ClineRemoteOps;
139
+ activeNames: ReadonlyArray<ClineNativeBuiltinName>;
140
+ }): AgentTool<any, any>[] {
141
+ // biome/eslint: `any` matches the runtime's own `tools?: readonly AgentTool<any, any>[]`.
142
+ const tools: Partial<Record<ClineNativeBuiltinName, AgentTool<any, any>>> = {
143
+ read: createTool({
144
+ name: 'read',
145
+ description: 'Read file contents.',
146
+ inputSchema: {
147
+ type: 'object',
148
+ properties: {
149
+ file_path: {
150
+ type: 'string',
151
+ description: 'Path of the file to read.',
152
+ },
153
+ },
154
+ required: ['file_path'],
155
+ },
156
+ execute: async (input: { file_path: string }) =>
157
+ guarded(() => ops.readFile(input.file_path)),
158
+ }),
159
+ write: createTool({
160
+ name: 'write',
161
+ description: 'Overwrite or create a file.',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ file_path: {
166
+ type: 'string',
167
+ description: 'Path of the file to write.',
168
+ },
169
+ content: {
170
+ type: 'string',
171
+ description: 'Full file content to write.',
172
+ },
173
+ },
174
+ required: ['file_path', 'content'],
175
+ },
176
+ execute: async (input: { file_path: string; content: string }) =>
177
+ guarded(async () => {
178
+ await ops.writeFile(input.file_path, input.content);
179
+ return `Wrote ${input.file_path}`;
180
+ }),
181
+ }),
182
+ edit: createTool({
183
+ name: 'edit',
184
+ description: 'Edit a file by exact string replacement.',
185
+ inputSchema: {
186
+ type: 'object',
187
+ properties: {
188
+ file_path: {
189
+ type: 'string',
190
+ description: 'Path of the file to edit.',
191
+ },
192
+ old_string: { type: 'string', description: 'Exact text to replace.' },
193
+ new_string: { type: 'string', description: 'Replacement text.' },
194
+ },
195
+ required: ['file_path', 'old_string', 'new_string'],
196
+ },
197
+ execute: async (input: {
198
+ file_path: string;
199
+ old_string: string;
200
+ new_string: string;
201
+ }) =>
202
+ guarded(async () => {
203
+ await ops.editFile(
204
+ input.file_path,
205
+ input.old_string,
206
+ input.new_string,
207
+ );
208
+ return `Edited ${input.file_path}`;
209
+ }),
210
+ }),
211
+ bash: createTool({
212
+ name: 'bash',
213
+ description:
214
+ 'Execute a shell command in the sandbox workspace. Returns combined stdout/stderr and the exit code.',
215
+ inputSchema: {
216
+ type: 'object',
217
+ properties: {
218
+ command: { type: 'string', description: 'Shell command to execute.' },
219
+ timeout: {
220
+ type: 'number',
221
+ description: 'Optional timeout in seconds.',
222
+ },
223
+ },
224
+ required: ['command'],
225
+ },
226
+ execute: async (input: { command: string; timeout?: number }, context) =>
227
+ guarded(async () => {
228
+ const result = await ops.bash(input.command, {
229
+ ...(typeof input.timeout === 'number'
230
+ ? { timeout: input.timeout }
231
+ : {}),
232
+ ...(context.signal ? { signal: context.signal } : {}),
233
+ });
234
+ return `${result.output}${
235
+ result.exitCode != null ? `\n\n(exit ${result.exitCode})` : ''
236
+ }`.trim();
237
+ }),
238
+ }),
239
+ grep: createTool({
240
+ name: 'grep',
241
+ description: 'Search file contents with regex.',
242
+ inputSchema: {
243
+ type: 'object',
244
+ properties: {
245
+ pattern: {
246
+ type: 'string',
247
+ description: 'Regex pattern to search for.',
248
+ },
249
+ path: { type: 'string', description: 'File or directory to search.' },
250
+ glob: { type: 'string', description: 'Glob filter for file names.' },
251
+ ignoreCase: { type: 'boolean' },
252
+ literal: {
253
+ type: 'boolean',
254
+ description: 'Treat pattern as a literal string.',
255
+ },
256
+ context: {
257
+ type: 'number',
258
+ description: 'Lines of context around matches.',
259
+ },
260
+ limit: {
261
+ type: 'number',
262
+ description: 'Maximum matching lines to return.',
263
+ },
264
+ },
265
+ required: ['pattern'],
266
+ },
267
+ execute: async (input: {
268
+ pattern: string;
269
+ path?: string;
270
+ glob?: string;
271
+ ignoreCase?: boolean;
272
+ literal?: boolean;
273
+ context?: number;
274
+ limit?: number;
275
+ }) => guarded(() => ops.grep(input.pattern, input)),
276
+ }),
277
+ glob: createTool({
278
+ name: 'glob',
279
+ description: 'Find files matching a glob pattern.',
280
+ inputSchema: {
281
+ type: 'object',
282
+ properties: {
283
+ pattern: { type: 'string', description: 'Glob pattern to match.' },
284
+ path: { type: 'string', description: 'Directory to search from.' },
285
+ limit: { type: 'number', description: 'Maximum paths to return.' },
286
+ },
287
+ required: ['pattern'],
288
+ },
289
+ execute: async (input: {
290
+ pattern: string;
291
+ path?: string;
292
+ limit?: number;
293
+ }) =>
294
+ guarded(async () => {
295
+ const matches = await ops.glob(
296
+ input.pattern,
297
+ input.path ?? '.',
298
+ input.limit ?? 1_000,
299
+ );
300
+ return matches.length > 0 ? matches.join('\n') : 'No files found';
301
+ }),
302
+ }),
303
+ ls: createTool({
304
+ name: 'ls',
305
+ description: 'List directory entries.',
306
+ inputSchema: {
307
+ type: 'object',
308
+ properties: {
309
+ path: { type: 'string', description: 'Directory to list.' },
310
+ limit: { type: 'number', description: 'Maximum entries to return.' },
311
+ },
312
+ },
313
+ execute: async (input: { path?: string; limit?: number }) =>
314
+ guarded(async () => {
315
+ const entries = await ops.ls(input.path ?? '.', input.limit ?? 500);
316
+ return entries.join('\n');
317
+ }),
318
+ }),
319
+ };
320
+
321
+ return activeNames
322
+ .map(name => tools[name])
323
+ .filter((tool): tool is AgentTool<any, any> => tool != null);
324
+ }
325
+
326
+ /**
327
+ * Build host-executed user tools. When the model calls one, the tool parks a
328
+ * promise keyed by the runtime's `toolCallId`; the harness caller supplies the
329
+ * output via `submitToolResult`, which resolves the promise and lets the loop
330
+ * continue. The adapter never executes these itself.
331
+ */
332
+ export function buildUserAgentTools({
333
+ specs,
334
+ pendingToolResults,
335
+ }: {
336
+ specs: ReadonlyArray<HarnessV1ToolSpec>;
337
+ pendingToolResults: Map<string, PendingToolResult>;
338
+ }): AgentTool<any, any>[] {
339
+ return specs.map(spec =>
340
+ createTool({
341
+ name: spec.name,
342
+ description: spec.description ?? `User-registered tool ${spec.name}`,
343
+ inputSchema: (spec.inputSchema as Record<string, unknown>) ?? {
344
+ type: 'object',
345
+ properties: {},
346
+ additionalProperties: true,
347
+ },
348
+ execute: async (_input: unknown, context) => {
349
+ const toolCallId = context.toolCallId;
350
+ if (!toolCallId) {
351
+ return createClineToolResult({
352
+ output: { error: 'Tool call is missing a toolCallId.' },
353
+ isError: true,
354
+ });
355
+ }
356
+ return new Promise<unknown>(resolve => {
357
+ pendingToolResults.set(toolCallId, { resolve });
358
+ // If the turn is aborted while waiting for the host, settle so the
359
+ // runtime's tool execution does not dangle forever.
360
+ context.signal?.addEventListener(
361
+ 'abort',
362
+ () => {
363
+ if (pendingToolResults.delete(toolCallId)) {
364
+ resolve(
365
+ createClineToolResult({
366
+ output: {
367
+ error:
368
+ 'Turn was aborted before a tool result was submitted.',
369
+ },
370
+ isError: true,
371
+ }),
372
+ );
373
+ }
374
+ },
375
+ { once: true },
376
+ );
377
+ });
378
+ },
379
+ }),
380
+ );
381
+ }
@@ -0,0 +1,255 @@
1
+ import type { HarnessV1StreamPart } from '@ai-sdk/harness';
2
+ import type { AgentRuntimeEvent, AgentToolResult } from '@cline/agents';
3
+ import {
4
+ mapModelFinishReason,
5
+ toToolResultValue,
6
+ usageFromMessageMetrics,
7
+ } from './cline-utils';
8
+
9
+ type FinishStepPart = Extract<
10
+ HarnessV1StreamPart,
11
+ { readonly type: 'finish-step' }
12
+ >;
13
+
14
+ /**
15
+ * Per-turn translator state. Tracks open text/reasoning blocks (the Cline
16
+ * runtime streams flat deltas; the harness contract wants explicit
17
+ * start/delta/end block framing) and which tool calls have already been
18
+ * surfaced — an approval request can emit a `tool-call` part before the
19
+ * runtime's own `tool-started` event fires for it. Cline emits the assistant
20
+ * message before executing that iteration's tools, so the step boundary is
21
+ * buffered until `turn-finished` confirms that those tools have settled.
22
+ */
23
+ export interface ClineTranslatorState {
24
+ readonly builtinToolNames: ReadonlySet<string>;
25
+ readonly dynamicToolCallIds: Set<string>;
26
+ readonly hostToolNames: ReadonlySet<string>;
27
+ readonly mcpToolNames: ReadonlySet<string>;
28
+ openTextBlockId?: string;
29
+ openReasoningBlockId?: string;
30
+ emittedToolCalls: Set<string>;
31
+ pendingFinishStep?: FinishStepPart;
32
+ blockCounter: number;
33
+ }
34
+
35
+ export function createClineTranslatorState({
36
+ builtinToolNames,
37
+ hostToolNames = [],
38
+ mcpToolNames = [],
39
+ }: {
40
+ builtinToolNames: Iterable<string>;
41
+ hostToolNames?: Iterable<string>;
42
+ mcpToolNames?: Iterable<string>;
43
+ }): ClineTranslatorState {
44
+ return {
45
+ builtinToolNames: new Set(builtinToolNames),
46
+ dynamicToolCallIds: new Set(),
47
+ hostToolNames: new Set(hostToolNames),
48
+ mcpToolNames: new Set(mcpToolNames),
49
+ emittedToolCalls: new Set(),
50
+ blockCounter: 0,
51
+ };
52
+ }
53
+
54
+ function closeOpenBlocks(state: ClineTranslatorState): HarnessV1StreamPart[] {
55
+ const parts: HarnessV1StreamPart[] = [];
56
+ if (state.openReasoningBlockId) {
57
+ parts.push({ type: 'reasoning-end', id: state.openReasoningBlockId });
58
+ state.openReasoningBlockId = undefined;
59
+ }
60
+ if (state.openTextBlockId) {
61
+ parts.push({ type: 'text-end', id: state.openTextBlockId });
62
+ state.openTextBlockId = undefined;
63
+ }
64
+ return parts;
65
+ }
66
+
67
+ function toolCallPart(
68
+ state: ClineTranslatorState,
69
+ toolCall: { toolCallId: string; toolName: string; input: unknown },
70
+ ): HarnessV1StreamPart {
71
+ const isMcpTool =
72
+ state.mcpToolNames.has(toolCall.toolName) &&
73
+ !state.hostToolNames.has(toolCall.toolName);
74
+ if (isMcpTool) {
75
+ state.dynamicToolCallIds.add(toolCall.toolCallId);
76
+ }
77
+ return {
78
+ type: 'tool-call',
79
+ toolCallId: toolCall.toolCallId,
80
+ toolName: toolCall.toolName,
81
+ input: JSON.stringify(toolCall.input ?? {}),
82
+ /*
83
+ * Built-ins and external MCP tools are executed by the Cline runtime.
84
+ * Typed host tools wait for dispatch through `submitToolResult`, including
85
+ * when a host tool has the same name as a configured MCP tool.
86
+ */
87
+ ...(state.builtinToolNames.has(toolCall.toolName) || isMcpTool
88
+ ? { providerExecuted: true }
89
+ : {}),
90
+ ...(isMcpTool ? { dynamic: true } : {}),
91
+ };
92
+ }
93
+
94
+ /**
95
+ * Emit the `tool-call` + `tool-approval-request` pair for a built-in tool
96
+ * that requires approval. The Cline runtime asks for approval *before* its
97
+ * `tool-started` event fires, so the `tool-call` part is emitted here and
98
+ * deduplicated when `tool-started` arrives.
99
+ */
100
+ export function toolApprovalParts(
101
+ state: ClineTranslatorState,
102
+ toolCall: { toolCallId: string; toolName: string; input: unknown },
103
+ ): HarnessV1StreamPart[] {
104
+ const parts: HarnessV1StreamPart[] = [];
105
+ if (!state.emittedToolCalls.has(toolCall.toolCallId)) {
106
+ state.emittedToolCalls.add(toolCall.toolCallId);
107
+ parts.push(toolCallPart(state, toolCall));
108
+ }
109
+ parts.push({
110
+ type: 'tool-approval-request',
111
+ approvalId: toolCall.toolCallId,
112
+ toolCallId: toolCall.toolCallId,
113
+ });
114
+ return parts;
115
+ }
116
+
117
+ /**
118
+ * Translate one Cline runtime event into zero or more harness stream parts.
119
+ * Terminal events (`run-finished`, `run-failed`) are intentionally not
120
+ * translated here — the session driver emits `finish`/`error` after the
121
+ * runtime's promise settles, so a suspend can close the stream cleanly.
122
+ */
123
+ export function translateClineEvent(
124
+ event: AgentRuntimeEvent,
125
+ state: ClineTranslatorState,
126
+ ): HarnessV1StreamPart[] {
127
+ switch (event.type) {
128
+ case 'assistant-reasoning-delta': {
129
+ const parts: HarnessV1StreamPart[] = [];
130
+ if (state.openTextBlockId) {
131
+ parts.push({ type: 'text-end', id: state.openTextBlockId });
132
+ state.openTextBlockId = undefined;
133
+ }
134
+ if (!state.openReasoningBlockId) {
135
+ state.openReasoningBlockId = `reasoning-${++state.blockCounter}`;
136
+ parts.push({ type: 'reasoning-start', id: state.openReasoningBlockId });
137
+ }
138
+ parts.push({
139
+ type: 'reasoning-delta',
140
+ id: state.openReasoningBlockId,
141
+ delta: event.text,
142
+ });
143
+ return parts;
144
+ }
145
+
146
+ case 'assistant-text-delta': {
147
+ const parts: HarnessV1StreamPart[] = [];
148
+ if (state.openReasoningBlockId) {
149
+ parts.push({ type: 'reasoning-end', id: state.openReasoningBlockId });
150
+ state.openReasoningBlockId = undefined;
151
+ }
152
+ if (!state.openTextBlockId) {
153
+ state.openTextBlockId = `text-${++state.blockCounter}`;
154
+ parts.push({ type: 'text-start', id: state.openTextBlockId });
155
+ }
156
+ parts.push({
157
+ type: 'text-delta',
158
+ id: state.openTextBlockId,
159
+ delta: event.text,
160
+ });
161
+ return parts;
162
+ }
163
+
164
+ case 'assistant-message': {
165
+ const parts = closeOpenBlocks(state);
166
+ state.pendingFinishStep = {
167
+ type: 'finish-step',
168
+ finishReason: mapModelFinishReason(event.finishReason),
169
+ usage: usageFromMessageMetrics(event.message.metrics),
170
+ };
171
+ return parts;
172
+ }
173
+
174
+ case 'turn-finished': {
175
+ const pendingFinishStep = state.pendingFinishStep;
176
+ if (!pendingFinishStep) {
177
+ return [];
178
+ }
179
+ state.pendingFinishStep = undefined;
180
+ return [pendingFinishStep];
181
+ }
182
+
183
+ case 'tool-started': {
184
+ if (state.emittedToolCalls.has(event.toolCall.toolCallId)) {
185
+ return [];
186
+ }
187
+ state.emittedToolCalls.add(event.toolCall.toolCallId);
188
+ return [toolCallPart(state, event.toolCall)];
189
+ }
190
+
191
+ case 'tool-finished': {
192
+ // The tool message carries the runtime's recorded result for this call.
193
+ const resultPart = event.message.content.find(
194
+ (part): part is Extract<typeof part, { type: 'tool-result' }> =>
195
+ part.type === 'tool-result' &&
196
+ part.toolCallId === event.toolCall.toolCallId,
197
+ );
198
+ const output = resultPart?.output as AgentToolResult['output'] | unknown;
199
+ const dynamic = state.dynamicToolCallIds.delete(
200
+ event.toolCall.toolCallId,
201
+ );
202
+ return [
203
+ {
204
+ type: 'tool-result',
205
+ toolCallId: event.toolCall.toolCallId,
206
+ toolName: event.toolCall.toolName,
207
+ result: toToolResultValue(output),
208
+ ...(resultPart?.isError ? { isError: true } : {}),
209
+ ...(dynamic ? { dynamic: true } : {}),
210
+ },
211
+ ];
212
+ }
213
+
214
+ case 'tool-updated':
215
+ return [
216
+ {
217
+ type: 'raw',
218
+ rawValue: {
219
+ type: 'tool-updated',
220
+ toolCallId: event.toolCall.toolCallId,
221
+ toolName: event.toolCall.toolName,
222
+ update: event.update,
223
+ },
224
+ },
225
+ ];
226
+
227
+ case 'status-notice':
228
+ return [
229
+ {
230
+ type: 'raw',
231
+ rawValue: {
232
+ type: 'status-notice',
233
+ message: event.message,
234
+ ...(event.metadata ? { metadata: event.metadata } : {}),
235
+ },
236
+ },
237
+ ];
238
+
239
+ default:
240
+ return [];
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Close any blocks still open when the turn ends (e.g. an aborted turn whose
246
+ * `assistant-message` never arrived). A buffered step that never reached
247
+ * Cline's `turn-finished` boundary is incomplete and must not be emitted.
248
+ */
249
+ export function finishClineTranslation(
250
+ state: ClineTranslatorState,
251
+ ): HarnessV1StreamPart[] {
252
+ const parts = closeOpenBlocks(state);
253
+ state.pendingFinishStep = undefined;
254
+ return parts;
255
+ }