Compare commits

...

1 Commits

Author SHA1 Message Date
abeatrix 1e45137c24 example: use cline sdk 2026-03-09 21:40:23 -07:00
26 changed files with 1901 additions and 2818 deletions
+10
View File
@@ -51,3 +51,13 @@ test-results
# Smoke test results (generated)
evals/smoke-tests/results/
# output
out
dist
*.tg
.next
target
packages-wip
+3
View File
@@ -1,3 +1,6 @@
[submodule "evals/cline-bench"]
path = evals/cline-bench
url = https://github.com/cline/cline-bench.git
[submodule "sdk-wip"]
path = sdk-wip
url = https://github.com/cline/sdk-wip.git
+2 -3
View File
@@ -12,7 +12,6 @@ import type { TelemetrySetting } from "@shared/TelemetrySetting"
import { Box, Text, useInput } from "ink"
import Spinner from "ink-spinner"
import React, { useCallback, useEffect, useMemo, useState } from "react"
import { buildApiHandler } from "@/core/api"
import type { Controller } from "@/core/controller"
import { refreshOcaModels } from "@/core/controller/models/refreshOcaModels"
import { StateManager } from "@/core/storage/StateManager"
@@ -745,7 +744,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
}
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
controller.task.updateApiHandler(apiConfig, currentMode)
}, [controller, stateManager])
const setReasoningEffortForMode = useCallback(
@@ -1046,7 +1045,7 @@ export const SettingsPanelContent: React.FC<SettingsPanelContentProps> = ({
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const freshApiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...freshApiConfig, ulid: controller.task.ulid }, currentMode)
controller.task.updateApiHandler(freshApiConfig, currentMode)
}
refreshModelIds()
+2 -3
View File
@@ -5,7 +5,6 @@
import type { ApiProvider } from "@shared/api"
import { getProviderModelIdKey, ProviderToApiKeyMap } from "@shared/storage"
import { buildApiHandler } from "@/core/api"
import type { Controller } from "@/core/controller"
import { StateManager } from "@/core/storage/StateManager"
import type { BedrockConfig } from "../components/BedrockSetup"
@@ -73,7 +72,7 @@ export async function applyProviderConfig(options: ApplyProviderConfigOptions):
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
controller.task.updateApiHandler(apiConfig, currentMode)
}
}
@@ -137,6 +136,6 @@ export async function applyBedrockConfig(options: ApplyBedrockConfigOptions): Pr
if (controller?.task) {
const currentMode = stateManager.getGlobalSettingsKey("mode")
const apiConfig = stateManager.getApiConfiguration()
controller.task.api = buildApiHandler({ ...apiConfig, ulid: controller.task.ulid }, currentMode)
controller.task.updateApiHandler(apiConfig, currentMode)
}
}
+12
View File
@@ -24,6 +24,18 @@
"useDefineForClassFields": true,
"useUnknownInCatchVariables": false,
"paths": {
"@cline/agents": [
"../packages/agents/dist/index.d.ts"
],
"@cline/agents/*": [
"../packages/agents/dist/*"
],
"@cline/llms": [
"../packages/llms/dist/index.d.ts"
],
"@cline/llms/*": [
"../packages/llms/dist/*"
],
"@/*": [
"../src/*"
],
+5
View File
@@ -19,6 +19,11 @@ const aliasResolverPlugin = {
name: "alias-resolver",
setup(build) {
const aliases = {
"@cline/agents/extensions": path.resolve(__dirname, "sdk-wip/packages/agents/dist/index.js"),
"@cline/agents/hooks": path.resolve(__dirname, "sdk-wip/packages/agents/dist/index.js"),
"@cline/agents": path.resolve(__dirname, "sdk-wip/packages/agents/dist"),
"@cline/core": path.resolve(__dirname, "sdk-wip/packages/core/dist"),
"@cline/llms": path.resolve(__dirname, "sdk-wip/packages/llms/dist"),
"@": path.resolve(__dirname, "src"),
"@core": path.resolve(__dirname, "src/core"),
"@integrations": path.resolve(__dirname, "src/integrations"),
+44 -79
View File
@@ -162,7 +162,7 @@
},
"cli": {
"name": "cline",
"version": "2.4.1",
"version": "2.4.2",
"cpu": [
"x64",
"arm64"
@@ -1576,6 +1576,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -3129,6 +3130,7 @@
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz",
"integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@grpc/proto-loader": "^0.8.0",
"@js-sdsl/ordered-map": "^4.4.2"
@@ -4106,6 +4108,7 @@
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
"integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==",
"license": "Apache-2.0",
"peer": true,
"engines": {
"node": ">=8.0.0"
}
@@ -5796,8 +5799,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1",
@@ -5810,8 +5812,7 @@
"optional": true,
"os": [
"android"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1",
@@ -5824,8 +5825,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1",
@@ -5838,8 +5838,7 @@
"optional": true,
"os": [
"darwin"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1",
@@ -5852,8 +5851,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1",
@@ -5866,8 +5864,7 @@
"optional": true,
"os": [
"freebsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1",
@@ -5880,8 +5877,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1",
@@ -5894,8 +5890,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1",
@@ -5908,8 +5903,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1",
@@ -5922,8 +5916,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1",
@@ -5936,8 +5929,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1",
@@ -5950,8 +5942,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1",
@@ -5964,8 +5955,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1",
@@ -5978,8 +5968,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1",
@@ -5992,8 +5981,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1",
@@ -6006,8 +5994,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1",
@@ -6020,8 +6007,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
@@ -6034,8 +6020,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1",
@@ -6048,8 +6033,7 @@
"optional": true,
"os": [
"linux"
],
"peer": true
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1",
@@ -6062,8 +6046,7 @@
"optional": true,
"os": [
"openbsd"
],
"peer": true
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1",
@@ -6076,8 +6059,7 @@
"optional": true,
"os": [
"openharmony"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1",
@@ -6090,8 +6072,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1",
@@ -6104,8 +6085,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1",
@@ -6118,8 +6098,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1",
@@ -6132,8 +6111,7 @@
"optional": true,
"os": [
"win32"
],
"peer": true
]
},
"node_modules/@sap-ai-sdk/ai-api": {
"version": "2.7.0",
@@ -7805,6 +7783,7 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz",
"integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -7867,6 +7846,7 @@
"integrity": "sha512-KkiJeU6VbYbUOp5ITMIc7kBfqlYkKA5KhEHVrGMmUUMt7NeaZg65ojdPk+FtNrBAOXNVM5QM72jnADjM+XVRAQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -8801,6 +8781,7 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -9765,6 +9746,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -11167,7 +11149,8 @@
"version": "0.0.1367902",
"resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz",
"integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==",
"license": "BSD-3-Clause"
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/diff": {
"version": "5.2.2",
@@ -12184,6 +12167,7 @@
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
"license": "MIT",
"peer": true,
"dependencies": {
"accepts": "^2.0.0",
"body-parser": "^2.2.1",
@@ -13491,6 +13475,7 @@
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=16.9.0"
}
@@ -13769,6 +13754,7 @@
"resolved": "https://registry.npmjs.org/@jrichman/ink/-/ink-6.4.7.tgz",
"integrity": "sha512-QHyxhNF5VonF5cRmdAJD/UPucB9nRx3FozWMjQrDGfBxfAL9lpyu72/MlFPgloS1TMTGsOt7YN6dTPPA6mh0Aw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@alcalzone/ansi-tokenize": "^0.2.1",
"ansi-escapes": "^7.0.0",
@@ -15022,6 +15008,7 @@
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
"integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
"license": "MIT",
"peer": true,
"bin": {
"jiti": "lib/jiti-cli.mjs"
}
@@ -15371,6 +15358,7 @@
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz",
"integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==",
"license": "MPL-2.0",
"peer": true,
"dependencies": {
"detect-libc": "^2.0.3"
},
@@ -19107,6 +19095,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -21814,6 +21803,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -22135,6 +22125,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -22216,7 +22207,6 @@
"os": [
"aix"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22233,7 +22223,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22250,7 +22239,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22267,7 +22255,6 @@
"os": [
"android"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22284,7 +22271,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22301,7 +22287,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22318,7 +22303,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22335,7 +22319,6 @@
"os": [
"freebsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22352,7 +22335,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22369,7 +22351,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22386,7 +22367,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22403,7 +22383,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22420,7 +22399,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22437,7 +22415,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22454,7 +22431,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22471,7 +22447,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22488,7 +22463,6 @@
"os": [
"linux"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22505,7 +22479,6 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22522,7 +22495,6 @@
"os": [
"netbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22539,7 +22511,6 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22556,7 +22527,6 @@
"os": [
"openbsd"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22573,7 +22543,6 @@
"os": [
"openharmony"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22590,7 +22559,6 @@
"os": [
"sunos"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22607,7 +22575,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22624,7 +22591,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22641,7 +22607,6 @@
"os": [
"win32"
],
"peer": true,
"engines": {
"node": ">=18"
}
@@ -22697,7 +22662,6 @@
"os": [
"darwin"
],
"peer": true,
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
@@ -23632,6 +23596,7 @@
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
Submodule
+1
Submodule sdk-wip added at 814a2e42b7
+226
View File
@@ -0,0 +1,226 @@
import { type ApiHandler, buildApiHandler } from "@core/api"
import type {
ApiStream,
ApiStreamChunk,
ApiStreamTextChunk,
ApiStreamThinkingChunk,
ApiStreamToolCallsChunk,
ApiStreamUsageChunk,
} from "@core/api/transform/stream"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { isNextGenModelFamily } from "@utils/model-utils"
import { ClineError, ClineErrorType } from "@/services/error/ClineError"
import type { ApiConfiguration } from "@/shared/api"
import type { Mode } from "@/shared/storage/types"
export interface InitialStreamRetryDecision {
isAuthError: boolean
isBalanceError: boolean
shouldRetry: boolean
}
export type CoreLoopStatus = "continue" | "complete" | "failed"
export interface CoreLoopTurnResult<TInput, TOutput> {
status: CoreLoopStatus
nextInput?: TInput
output?: TOutput
error?: string
}
export interface CoreLoopParams<TInput, TOutput> {
initialInput: TInput
runTurn: (input: TInput, iteration: number) => Promise<CoreLoopTurnResult<TInput, TOutput>>
shouldAbort?: () => boolean
}
export type CoreLoopResult<TOutput> =
| { status: "complete"; output: TOutput }
| { status: "failed"; error?: string }
| { status: "aborted" }
export interface CoreStreamUsage {
inputTokens: number
outputTokens: number
cacheWriteTokens: number
cacheReadTokens: number
totalCost?: number
}
export interface CoreStreamResult {
requestId?: string
assistantText: string
assistantTextSignature?: string
usage: CoreStreamUsage
aborted: boolean
interrupted: boolean
}
export interface CoreStreamParams {
stream: ApiStream
onUsageChunk?: (chunk: ApiStreamUsageChunk, state: CoreStreamResult) => Promise<void> | void
onTextChunk?: (chunk: ApiStreamTextChunk, state: CoreStreamResult) => Promise<void> | void
onToolCallChunk?: (chunk: ApiStreamToolCallsChunk, state: CoreStreamResult) => Promise<void> | void
onReasoningChunk?: (chunk: ApiStreamThinkingChunk, state: CoreStreamResult) => Promise<void> | void
onChunkProcessed?: (chunk: ApiStreamChunk, state: CoreStreamResult) => Promise<"break" | void> | "break" | void
shouldAbort?: () => boolean
onAbort?: (state: CoreStreamResult) => Promise<void> | void
}
export class CoreAgent {
static readonly AUTO_CONDENSE_THRESHOLD = 0.75
private apiHandler?: ApiHandler
constructor(params?: { apiConfiguration: ApiConfiguration; mode: Mode }) {
if (params) {
this.apiHandler = buildApiHandler(params.apiConfiguration, params.mode)
}
}
initializeApiHandler(apiConfiguration: ApiConfiguration, mode: Mode): ApiHandler {
this.apiHandler = buildApiHandler(apiConfiguration, mode)
return this.apiHandler
}
getApiHandler(): ApiHandler {
if (!this.apiHandler) {
throw new Error("CoreAgent API handler has not been initialized")
}
return this.apiHandler
}
getModel(): ReturnType<ApiHandler["getModel"]> {
return this.getApiHandler().getModel()
}
createMessage(...args: Parameters<ApiHandler["createMessage"]>): ReturnType<ApiHandler["createMessage"]> {
return this.getApiHandler().createMessage(...args)
}
abortCurrentRequest(): void {
this.getApiHandler().abort?.()
}
getApiStreamUsage(): ReturnType<NonNullable<ApiHandler["getApiStreamUsage"]>> | undefined {
return this.getApiHandler().getApiStreamUsage?.()
}
getLastRequestIdSafe(): string | undefined {
const apiLike = this.getApiHandler() as Partial<{
getLastRequestId: () => string | undefined
lastGenerationId?: string
}>
return apiLike.getLastRequestId?.() ?? apiLike.lastGenerationId
}
async runLoop<TInput, TOutput>(params: CoreLoopParams<TInput, TOutput>): Promise<CoreLoopResult<TOutput>> {
let iteration = 0
let currentInput = params.initialInput
while (!params.shouldAbort?.()) {
iteration += 1
const turn = await params.runTurn(currentInput, iteration)
if (turn.status === "complete") {
return { status: "complete", output: turn.output as TOutput }
}
if (turn.status === "failed") {
return { status: "failed", error: turn.error }
}
currentInput = turn.nextInput as TInput
}
return { status: "aborted" }
}
async consumeStream(params: CoreStreamParams): Promise<CoreStreamResult> {
const state: CoreStreamResult = {
requestId: undefined,
assistantText: "",
assistantTextSignature: undefined,
usage: {
inputTokens: 0,
outputTokens: 0,
cacheWriteTokens: 0,
cacheReadTokens: 0,
totalCost: undefined,
},
aborted: false,
interrupted: false,
}
for await (const chunk of params.stream) {
state.requestId = state.requestId ?? chunk.id
switch (chunk.type) {
case "usage":
state.usage.inputTokens += chunk.inputTokens
state.usage.outputTokens += chunk.outputTokens
state.usage.cacheWriteTokens += chunk.cacheWriteTokens ?? 0
state.usage.cacheReadTokens += chunk.cacheReadTokens ?? 0
state.usage.totalCost = chunk.totalCost ?? state.usage.totalCost
await params.onUsageChunk?.(chunk, state)
break
case "text":
state.assistantText += chunk.text || ""
state.assistantTextSignature = chunk.signature || state.assistantTextSignature
await params.onTextChunk?.(chunk, state)
break
case "tool_calls":
await params.onToolCallChunk?.(chunk, state)
break
case "reasoning":
await params.onReasoningChunk?.(chunk, state)
break
}
const shouldBreak = await params.onChunkProcessed?.(chunk, state)
if (shouldBreak === "break") {
state.interrupted = true
break
}
if (params.shouldAbort?.()) {
state.aborted = true
await params.onAbort?.(state)
break
}
}
return state
}
isAutoCondenseEnabledForModel(useAutoCondense: boolean, modelId: string): boolean {
return useAutoCondense && isNextGenModelFamily(modelId)
}
getAutoCondenseThresholdTokens(contextWindow: number, maxAllowedSize: number): number {
const roundedThreshold = Math.floor(contextWindow * CoreAgent.AUTO_CONDENSE_THRESHOLD)
return Math.min(roundedThreshold, maxAllowedSize)
}
shouldCompactBeforeNextRequest(
previousRequestTotalTokens: number,
api: ApiHandler,
modelId: string,
useAutoCondense: boolean,
): boolean {
const { contextWindow, maxAllowedSize } = getContextWindowInfo(api)
if (this.isAutoCondenseEnabledForModel(useAutoCondense, modelId)) {
const thresholdTokens = this.getAutoCondenseThresholdTokens(contextWindow, maxAllowedSize)
return previousRequestTotalTokens >= thresholdTokens
}
return previousRequestTotalTokens >= maxAllowedSize
}
classifyInitialStreamRetry(error: unknown, providerId: string, modelId: string): InitialStreamRetryDecision {
const parsedError = ClineError.transform(error, modelId, providerId)
const isAuthError = parsedError.isErrorType(ClineErrorType.Auth)
const isBalanceError = parsedError.isErrorType(ClineErrorType.Balance)
return {
isAuthError,
isBalanceError,
shouldRetry: !isAuthError && !isBalanceError,
}
}
}
+271
View File
@@ -0,0 +1,271 @@
import { providers } from "@cline/llms"
import type { ClineContent, ClineImageContentBlock, ClineStorageMessage, ClineTextContentBlock } from "@/shared/messages/content"
import { Logger } from "@/shared/services/Logger"
import { SELECTOR_SEPARATOR } from "@/shared/vsCodeSelectorUtils"
import { VsCodeLmHandler } from "./vscode-lm"
export const VSCODE_LM_SELECTOR_HEADER = "x-cline-vscode-lm-selector"
let createHandlersPatched = false
let vscodeLmProviderRegistered = false
let originalCreateHandler: ((config: providers.ProviderConfig) => providers.ApiHandler) | undefined
let originalCreateHandlerAsync: ((config: providers.ProviderConfig) => Promise<providers.ApiHandler>) | undefined
const extensionProviderFactories = new Map<string, (config: providers.ProviderConfig) => providers.ApiHandler>()
function ensureProvidersCreateHandlerPatched() {
if (createHandlersPatched) {
return
}
const mutableProviders = providers as typeof providers & {
createHandler: (config: providers.ProviderConfig) => providers.ApiHandler
createHandlerAsync: (config: providers.ProviderConfig) => Promise<providers.ApiHandler>
}
originalCreateHandler = mutableProviders.createHandler.bind(providers)
originalCreateHandlerAsync = mutableProviders.createHandlerAsync.bind(providers)
mutableProviders.createHandler = (config: providers.ProviderConfig): providers.ApiHandler => {
const extensionFactory = extensionProviderFactories.get(config.providerId)
if (extensionFactory) {
return extensionFactory(config)
}
if (!originalCreateHandler) {
throw new Error("LLMS createHandler has not been initialized")
}
return originalCreateHandler(config)
}
mutableProviders.createHandlerAsync = async (config: providers.ProviderConfig): Promise<providers.ApiHandler> => {
const extensionFactory = extensionProviderFactories.get(config.providerId)
if (extensionFactory) {
return extensionFactory(config)
}
if (!originalCreateHandlerAsync) {
throw new Error("LLMS createHandlerAsync has not been initialized")
}
return originalCreateHandlerAsync(config)
}
createHandlersPatched = true
}
function normalizeImageMediaType(mediaType: string): "image/jpeg" | "image/png" | "image/gif" | "image/webp" {
switch (mediaType) {
case "image/jpeg":
case "image/png":
case "image/gif":
case "image/webp":
return mediaType
default:
return "image/png"
}
}
function toClineContentBlock(block: providers.ContentBlock): ClineContent {
switch (block.type) {
case "text":
return { type: "text", text: block.text }
case "image":
return {
type: "image",
source: {
type: "base64",
media_type: normalizeImageMediaType(block.mediaType),
data: block.data,
},
}
case "file":
return {
type: "text",
text: `File: ${block.path}\n${block.content}`,
}
case "tool_use":
return {
type: "tool_use",
id: block.id,
name: block.name,
input: block.input,
}
case "tool_result":
return {
type: "tool_result",
tool_use_id: block.tool_use_id,
content:
typeof block.content === "string"
? block.content
: block.content.map((contentBlock): ClineTextContentBlock | ClineImageContentBlock => {
switch (contentBlock.type) {
case "text":
return { type: "text", text: contentBlock.text }
case "image":
return {
type: "image",
source: {
type: "base64",
media_type: normalizeImageMediaType(contentBlock.mediaType),
data: contentBlock.data,
},
}
case "file":
return { type: "text", text: `File: ${contentBlock.path}\n${contentBlock.content}` }
default: {
const exhaustiveCheck: never = contentBlock
throw new Error(
`Unsupported provider tool_result content block type: ${JSON.stringify(exhaustiveCheck)}`,
)
}
}
}),
is_error: block.is_error,
}
case "thinking":
return {
type: "thinking",
thinking: block.thinking,
signature: block.signature ?? "",
}
case "redacted_thinking":
return {
type: "redacted_thinking",
data: block.data,
}
default: {
const exhaustiveCheck: never = block
throw new Error(`Unsupported provider content block type: ${JSON.stringify(exhaustiveCheck)}`)
}
}
}
function toClineMessages(messages: providers.Message[]): ClineStorageMessage[] {
return messages.map((message) => {
if (typeof message.content === "string") {
return {
role: message.role,
content: message.content,
}
}
return {
role: message.role,
content: message.content.map((block) => toClineContentBlock(block)),
}
})
}
function parseVsCodeLmSelector(modelId?: string): Record<string, string> | undefined {
if (!modelId) {
return undefined
}
const parts = modelId.split(SELECTOR_SEPARATOR).filter(Boolean)
if (parts.length === 0) {
return undefined
}
const [vendor, family, version, id] = parts
const selector: Record<string, string> = {}
if (vendor) {
selector.vendor = vendor
}
if (family) {
selector.family = family
}
if (version) {
selector.version = version
}
if (id) {
selector.id = id
}
return Object.keys(selector).length > 0 ? selector : undefined
}
function toRegisteredModelInfo(modelId: string, modelInfo: ReturnType<VsCodeLmHandler["getModel"]>["info"]): providers.ModelInfo {
const capabilities: providers.ModelInfo["capabilities"] = []
if (modelInfo.supportsImages) {
capabilities.push("images")
}
if (modelInfo.supportsPromptCache) {
capabilities.push("prompt-cache")
}
if (modelInfo.supportsReasoning) {
capabilities.push("reasoning")
}
if (modelInfo.supportsGlobalEndpoint) {
capabilities.push("global-endpoint")
}
return {
id: modelId,
name: modelInfo.name,
description: modelInfo.description,
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
temperature: modelInfo.temperature,
capabilities: capabilities.length > 0 ? capabilities : undefined,
pricing:
modelInfo.inputPrice !== undefined ||
modelInfo.outputPrice !== undefined ||
modelInfo.cacheWritesPrice !== undefined ||
modelInfo.cacheReadsPrice !== undefined
? {
input: modelInfo.inputPrice,
output: modelInfo.outputPrice,
cacheWrite: modelInfo.cacheWritesPrice,
cacheRead: modelInfo.cacheReadsPrice,
}
: undefined,
thinkingConfig: modelInfo.thinkingConfig
? {
maxBudget: modelInfo.thinkingConfig.maxBudget,
outputPrice: modelInfo.thinkingConfig.outputPrice,
thinkingLevel: modelInfo.thinkingConfig.geminiThinkingLevel,
}
: undefined,
}
}
export function ensureVsCodeLmProviderRegistered() {
if (vscodeLmProviderRegistered) {
return
}
ensureProvidersCreateHandlerPatched()
extensionProviderFactories.set("vscode-lm", (config: providers.ProviderConfig): providers.ApiHandler => {
const selectorFromHeader = config?.headers?.[VSCODE_LM_SELECTOR_HEADER]
let selector: Record<string, string> | undefined
if (selectorFromHeader) {
try {
selector = JSON.parse(selectorFromHeader)
} catch {
selector = undefined
}
}
selector = selector ?? parseVsCodeLmSelector(config?.modelId)
const handler = new VsCodeLmHandler({
vsCodeLmModelSelector: selector,
})
return {
getMessages: (systemPrompt: string, messages: providers.Message[]) => ({
systemPrompt,
messages: toClineMessages(messages),
}),
createMessage: (
systemPrompt: string,
messages: providers.Message[],
_tools?: providers.ToolDefinition[],
): providers.ApiStream =>
handler.createMessage(systemPrompt, toClineMessages(messages)) as unknown as providers.ApiStream,
getModel: () => {
const model = handler.getModel()
return {
id: model.id,
info: toRegisteredModelInfo(model.id, model.info),
}
},
}
})
vscodeLmProviderRegistered = true
Logger.debug("Registered extension provider handler for vscode-lm")
}
+7 -7
View File
@@ -392,7 +392,7 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
const totalInputTokens: number = await this.calculateTotalInputTokens(vsCodeLmMessages)
// Accumulate the text and count at the end of the stream to reduce token counting overhead.
let accumulatedText: string = ""
let accumulatedText = ""
try {
// Create the response stream with minimal required options
@@ -498,17 +498,17 @@ export class VsCodeLmHandler implements ApiHandler, SingleCompletionHandler {
// Return original error if it's already an Error instance
throw error
} else if (typeof error === "object" && error !== null) {
}
if (typeof error === "object" && error !== null) {
// Handle error-like objects
const errorDetails = JSON.stringify(error, null, 2)
Logger.error("Cline <Language Model API>: Stream error object:", errorDetails)
throw new Error(`Cline <Language Model API>: Response stream error: ${errorDetails}`)
} else {
// Fallback for unknown error types
const errorMessage = String(error)
Logger.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
}
// Fallback for unknown error types
const errorMessage = String(error)
Logger.error("Cline <Language Model API>: Unknown stream error:", errorMessage)
throw new Error(`Cline <Language Model API>: Response stream error: ${errorMessage}`)
}
}
+9 -10
View File
@@ -1,5 +1,4 @@
import type { Anthropic } from "@anthropic-ai/sdk"
import { buildApiHandler } from "@core/api"
import { getHooksEnabledSafe } from "@core/hooks/hooks-utils"
import { tryAcquireTaskLockWithRetry } from "@core/task/TaskLockUtils"
import { detectWorkspaceRoots } from "@core/workspace/detection"
@@ -370,10 +369,10 @@ export class Controller {
// Switch to act mode
this.stateManager.setGlobalState("mode", modeToSwitchTo)
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
// Update API handler with new mode
if (this.task) {
const apiConfiguration = this.stateManager.getApiConfiguration()
this.task.api = buildApiHandler({ ...apiConfiguration, ulid: this.task.ulid }, modeToSwitchTo)
this.task.updateApiHandler(apiConfiguration, modeToSwitchTo)
}
await this.postStateToWebview()
@@ -394,10 +393,10 @@ export class Controller {
// Capture mode switch telemetry | Capture regardless of if we know the taskId
telemetryService.captureModeSwitch(this.task?.ulid ?? "0", modeToSwitchTo)
// Update API handler with new mode (buildApiHandler now selects provider based on mode)
// Update API handler with new mode
if (this.task) {
const apiConfiguration = this.stateManager.getApiConfiguration()
this.task.api = buildApiHandler({ ...apiConfiguration, ulid: this.task.ulid }, modeToSwitchTo)
this.task.updateApiHandler(apiConfiguration, modeToSwitchTo)
}
await this.postStateToWebview()
@@ -547,7 +546,7 @@ export class Controller {
await fetchRemoteConfig(this)
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
this.task.updateApiHandler(updatedConfig, currentMode)
}
await this.postStateToWebview()
@@ -598,7 +597,7 @@ export class Controller {
this.stateManager.setGlobalState("welcomeViewCompleted", true)
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
this.task.updateApiHandler(updatedConfig, currentMode)
}
await this.postStateToWebview()
@@ -716,7 +715,7 @@ export class Controller {
await this.postStateToWebview()
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
this.task.updateApiHandler(updatedConfig, currentMode)
}
// Dont send settingsButtonClicked because its bad ux if user is on welcome
}
@@ -736,7 +735,7 @@ export class Controller {
this.stateManager.setApiConfiguration(updatedConfig)
await this.postStateToWebview()
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
this.task.updateApiHandler(updatedConfig, currentMode)
}
}
@@ -776,7 +775,7 @@ export class Controller {
await this.postStateToWebview()
this.accountService
if (this.task) {
this.task.api = buildApiHandler({ ...updatedConfig, ulid: this.task.ulid }, currentMode)
this.task.updateApiHandler(updatedConfig, currentMode)
}
}
@@ -1,6 +1,5 @@
import { Empty } from "@shared/proto/cline/common"
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
import { buildApiHandler } from "@/core/api"
import { ApiHandlerOptions, ApiProvider } from "@/shared/api"
import { UpdateApiConfigurationRequestNew } from "@/shared/proto/index.cline"
import { Logger } from "@/shared/services/Logger"
@@ -41,7 +40,8 @@ function parseFieldMask(updateMask: string[]): {
function getAlternateModeField(fieldName: string): string | null {
if (fieldName.startsWith("planMode")) {
return fieldName.replace("planMode", "actMode")
} else if (fieldName.startsWith("actMode")) {
}
if (fieldName.startsWith("actMode")) {
return fieldName.replace("actMode", "planMode")
}
return null
@@ -141,14 +141,7 @@ export async function updateApiConfiguration(controller: Controller, request: Up
// Update the task's API handler if there's an active task
if (controller.task) {
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
// Build updated config
controller.task.api = buildApiHandler(
{
...controller.stateManager.getApiConfiguration(),
ulid: controller.task.ulid,
},
currentMode,
)
controller.task.updateApiHandler(controller.stateManager.getApiConfiguration(), currentMode)
}
// Post updated state to webview
@@ -1,4 +1,3 @@
import { buildApiHandler } from "@core/api"
import { Empty } from "@shared/proto/cline/common"
import { UpdateApiConfigurationPartialRequest } from "@shared/proto/cline/models"
import { convertProtoToApiConfiguration } from "@shared/proto-conversions/models/api-configuration-conversion"
@@ -44,7 +43,7 @@ export async function updateApiConfigurationPartial(
controller.stateManager.setApiConfiguration(updatedConfig)
if (controller.task) {
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
controller.task.api = buildApiHandler({ ...updatedConfig, ulid: controller.task.ulid }, currentMode)
controller.task.updateApiHandler(updatedConfig, currentMode)
}
// Notify webview
@@ -8,7 +8,6 @@ import {
fromProtobufOpenAiCompatibleModelInfo,
} from "@shared/proto-conversions/models/typeConversion"
import { OpenaiReasoningEffort } from "@shared/storage/types"
import { buildApiHandler } from "@/core/api"
import { Logger } from "@/shared/services/Logger"
import type { Controller } from "../index"
@@ -124,10 +123,7 @@ export async function updateApiConfigurationProto(
// Update the task's API handler if there's an active task
if (controller.task) {
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
controller.task.api = buildApiHandler(
{ ...convertedApiConfigurationFromProto, ulid: controller.task.ulid },
currentMode,
)
controller.task.updateApiHandler(convertedApiConfigurationFromProto, currentMode)
}
// Post updated state to webview
+3 -8
View File
@@ -1,4 +1,3 @@
import { buildApiHandler } from "@core/api"
import { Empty } from "@shared/proto/cline/common"
import { PlanActMode, McpDisplayMode as ProtoMcpDisplayMode, UpdateSettingsRequest } from "@shared/proto/cline/state"
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
@@ -49,11 +48,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
if (controller.task) {
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
const apiConfigForHandler = {
...convertedApiConfigurationFromProto,
ulid: controller.task.ulid,
}
controller.task.api = buildApiHandler(apiConfigForHandler, currentMode)
controller.task.updateApiHandler(convertedApiConfigurationFromProto, currentMode)
}
}
@@ -185,7 +180,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
telemetryService.captureAutoCondenseToggle(
controller.task.ulid,
request.useAutoCondense,
controller.task.api.getModel().id,
controller.task.getModel().id,
)
}
controller.stateManager.setGlobalState("useAutoCondense", request.useAutoCondense)
@@ -312,7 +307,7 @@ export async function updateSettings(controller: Controller, request: UpdateSett
controller.task.ulid,
"native-tool-call",
request.nativeToolCallEnabled,
controller.task.api.getModel().id,
controller.task.getModel().id,
)
}
}
@@ -1,5 +1,3 @@
import { buildApiHandler } from "@core/api"
import { Empty } from "@shared/proto/cline/common"
import { PlanActMode, UpdateSettingsRequestCli } from "@shared/proto/cline/state"
import { convertProtoToApiProvider } from "@shared/proto-conversions/models/api-configuration-conversion"
@@ -118,11 +116,7 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
if (controller.task) {
const currentMode = controller.stateManager.getGlobalSettingsKey("mode")
const apiConfigForHandler = {
...controller.stateManager.getApiConfiguration(),
ulid: controller.task.ulid,
}
controller.task.api = buildApiHandler(apiConfigForHandler, currentMode)
controller.task.updateApiHandler(controller.stateManager.getApiConfiguration(), currentMode)
}
// Update telemetry setting
@@ -144,7 +138,7 @@ export async function updateSettingsCli(controller: Controller, request: UpdateS
telemetryService.captureAutoCondenseToggle(
controller.task.ulid,
useAutoCondense,
controller.task.api.getModel().id,
controller.task.getModel().id,
)
}
controller.stateManager.setGlobalState("useAutoCondense", useAutoCondense)
@@ -4,6 +4,8 @@ import * as fs from "fs/promises"
import type { McpHub } from "@/services/mcp/McpHub"
import { Logger } from "@/shared/services/Logger"
const REMOTE_CONFIG_WATCHER_GRACE_MS = 300
/**
* Synchronizes remote MCP servers from remote config to the local MCP settings file
* This allows admins to centrally configure MCP servers that are automatically deployed to users
@@ -33,6 +35,7 @@ export async function syncRemoteMcpServersToSettings(
// Read current settings
const content = await fs.readFile(settingsPath, "utf-8")
const config = JSON.parse(content)
const initialSerializedConfig = JSON.stringify(config)
// Ensure mcpServers object exists
if (!config.mcpServers || typeof config.mcpServers !== "object") {
@@ -81,9 +84,18 @@ export async function syncRemoteMcpServersToSettings(
}
try {
const nextSerializedConfig = JSON.stringify(config)
if (initialSerializedConfig === nextSerializedConfig) {
return
}
// Write back to file
await fs.writeFile(settingsPath, JSON.stringify(config, null, 2))
} finally {
// Keep the flag set briefly to outlive chokidar's awaitWriteFinish delay.
// Otherwise the watcher may process our own remote-config write and reconnect servers.
await new Promise((resolve) => setTimeout(resolve, REMOTE_CONFIG_WATCHER_GRACE_MS))
// Always clear flag, even if write fails
if (mcpHub) {
mcpHub.setIsUpdatingFromRemoteConfig(false)
+546
View File
@@ -0,0 +1,546 @@
import { Agent, type AgentEvent, type AgentHooks, getClineDefaultSystemPrompt, type Tool } from "@cline/agents"
import { createBuiltinTools } from "@cline/core"
import { providers } from "@cline/llms"
import type { Hooks as HookInputs } from "@core/hooks/hook-factory"
import { getHooksEnabledSafe } from "@core/hooks/hooks-utils"
import { formatContentBlockToMarkdown } from "@integrations/misc/export-markdown"
import { telemetryService } from "@services/telemetry"
import type { ApiConfiguration } from "@shared/api"
import type { ClineSay } from "@shared/ExtensionMessage"
import { buildClineExtraHeaders } from "@/services/EnvUtils"
import { type ClineContent, ClineMessageModelInfo } from "@/shared/messages"
import { Logger } from "@/shared/services/Logger"
import type { ApiProviderInfo } from "../api"
import { ensureVsCodeLmProviderRegistered, VSCODE_LM_SELECTOR_HEADER } from "../api/providers/registry"
import type { MessageStateHandler } from "./message-state"
import type { CreateTaskAgentHooksOptions } from "./TaskHookExtensionAdapter"
import type { HookExecution } from "./types/HookExecution"
type ProviderConnectionConfig = {
apiKey?: string
baseUrl?: string
headers?: Record<string, string>
knownModels?: Record<string, providers.ModelInfo>
}
type RegisteredModelInfo = providers.ModelInfo
function toRegisteredModelInfo(modelId: string, modelInfo: ApiProviderInfo["model"]["info"]): RegisteredModelInfo {
const capabilities: RegisteredModelInfo["capabilities"] = []
if (modelInfo.supportsImages) {
capabilities.push("images")
}
if (modelInfo.supportsPromptCache) {
capabilities.push("prompt-cache")
}
if (modelInfo.supportsReasoning) {
capabilities.push("reasoning")
}
if (modelInfo.supportsGlobalEndpoint) {
capabilities.push("global-endpoint")
}
return {
id: modelId,
name: modelInfo.name,
description: modelInfo.description,
maxTokens: modelInfo.maxTokens,
contextWindow: modelInfo.contextWindow,
temperature: modelInfo.temperature,
capabilities: capabilities.length > 0 ? capabilities : undefined,
pricing:
modelInfo.inputPrice !== undefined ||
modelInfo.outputPrice !== undefined ||
modelInfo.cacheWritesPrice !== undefined ||
modelInfo.cacheReadsPrice !== undefined
? {
input: modelInfo.inputPrice,
output: modelInfo.outputPrice,
cacheWrite: modelInfo.cacheWritesPrice,
cacheRead: modelInfo.cacheReadsPrice,
}
: undefined,
thinkingConfig: modelInfo.thinkingConfig
? {
maxBudget: modelInfo.thinkingConfig.maxBudget,
outputPrice: modelInfo.thinkingConfig.outputPrice,
thinkingLevel: modelInfo.thinkingConfig.geminiThinkingLevel,
}
: undefined,
}
}
type AgentRuntimeHost = {
taskId: string
ulid: string
cwd: string
taskState: {
apiRequestCount: number
apiRequestsSinceLastTodoUpdate: number
abort: boolean
}
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
postStateToWebview: () => Promise<void>
getCurrentProviderInfo: () => ApiProviderInfo
getApiConfiguration: () => ApiConfiguration
loadContext: (
userContent: ClineContent[],
includeFileDetails: boolean,
useCompactPrompt: boolean,
) => Promise<[ClineContent[], string, boolean]>
messageStateHandler: MessageStateHandler
setActiveHookExecution: (hookExecution: HookExecution) => Promise<void>
clearActiveHookExecution: () => Promise<void>
runTaskLifecycleHook: <K extends "TaskStart" | "TaskResume">(params: {
hookName: K
hookInput: HookInputs[K]
}) => Promise<{ cancel?: boolean; contextModification?: string; errorMessage?: string; wasCancelled: boolean }>
runUserPromptSubmitHook: (
userContent: ClineContent[],
) => Promise<{ cancel?: boolean; contextModification?: string; errorMessage?: string; wasCancelled: boolean }>
handleHookCancellation: (hookName: string, wasCancelled: boolean) => Promise<void>
createAgentHooks: (options: CreateTaskAgentHooksOptions) => AgentHooks
cancelTask: () => Promise<void>
}
export type TaskAgentRunContext = {
phase: "initial_task" | "resume" | "continue"
initialTask?: string
resumePreviousState?: {
lastMessageTs: string
messageCount: string
conversationHistoryDeleted: string
}
userPromptHookContent?: ClineContent[]
}
export class TaskAgentRuntime {
private agent: InstanceType<typeof Agent> | undefined
private agentSignature: string | undefined
private readonly builtInTools: Tool[]
private readonly host: AgentRuntimeHost
private pendingRunContext: TaskAgentRunContext | undefined
private streamedReasoning = ""
constructor(host: AgentRuntimeHost) {
this.host = host
this.builtInTools = createBuiltinTools({ cwd: host.cwd })
}
async run(userContent: ClineContent[], runContext: TaskAgentRunContext): Promise<void> {
this.host.taskState.apiRequestCount++
this.host.taskState.apiRequestsSinceLastTodoUpdate++
this.streamedReasoning = ""
// const [parsedUserContent, environmentDetails] = await this.host.loadContext(userContent, includeFileDetails, false)
// const normalizedUserContent = [...parsedUserContent]
// if (environmentDetails) {
// normalizedUserContent.push({ type: "text", text: environmentDetails })
// }
const normalizedUserContent = [...userContent]
const userMessage = normalizedUserContent.map((block) => formatContentBlockToMarkdown(block)).join("\n\n")
await this.host.say("api_req_started", JSON.stringify({ request: userMessage }))
const providerInfo = this.host.getCurrentProviderInfo()
const modelInfo: ClineMessageModelInfo = {
modelId: providerInfo.model.id,
providerId: providerInfo.providerId,
mode: providerInfo.mode,
}
await this.host.messageStateHandler.addToApiConversationHistory({
role: "user",
content: normalizedUserContent,
ts: Date.now(),
})
const customHeader = await buildClineExtraHeaders()
this.pendingRunContext = runContext
const { agent, reusedExistingAgent } = this.getOrCreateAgent(providerInfo, customHeader)
const result = await (async () => {
try {
return reusedExistingAgent ? await agent.continue(userMessage) : await agent.run(userMessage)
} finally {
this.pendingRunContext = undefined
}
})()
this.agent = agent
if (result.text) {
await this.host.say("text", result.text, undefined, undefined, false)
}
await this.host.messageStateHandler.addToApiConversationHistory({
role: "assistant",
content: [{ type: "text", text: result.text || "" }],
modelInfo,
metrics: {
tokens: {
prompt: result.usage.inputTokens || 0,
completion: result.usage.outputTokens || 0,
cached: (result.usage.cacheWriteTokens || 0) + (result.usage.cacheReadTokens || 0),
},
cost: result.usage.totalCost,
},
ts: Date.now(),
})
telemetryService.captureConversationTurnEvent(
this.host.ulid,
modelInfo.providerId,
modelInfo.modelId,
"assistant",
modelInfo.mode,
{
tokensIn: result.usage.inputTokens || 0,
tokensOut: result.usage.outputTokens || 0,
cacheWriteTokens: result.usage.cacheWriteTokens || 0,
cacheReadTokens: result.usage.cacheReadTokens || 0,
totalCost: result.usage.totalCost,
},
)
await this.host.postStateToWebview()
}
public abort(): void {
this.agent?.abort()
}
private getOrCreateAgent(
providerInfo: ReturnType<AgentRuntimeHost["getCurrentProviderInfo"]>,
customHeader: Record<string, string>,
): {
agent: InstanceType<typeof Agent>
reusedExistingAgent: boolean
} {
const apiConfig = this.host.getApiConfiguration()
if (providerInfo.providerId === "vscode-lm") {
ensureVsCodeLmProviderRegistered()
}
const providerConfig = this.resolveProviderConfig(providerInfo, apiConfig)
const nextSignature = JSON.stringify({
providerId: providerInfo.providerId,
modelId: providerInfo.model.id,
apiKey: providerConfig.apiKey ?? "",
baseUrl: providerConfig.baseUrl ?? "",
headers: providerConfig.headers ?? {},
knownModels: providerConfig.knownModels ?? {},
})
if (this.agent && this.agentSignature === nextSignature) {
return { agent: this.agent, reusedExistingAgent: true }
}
const defaultHeaders: Record<string, string> = {
"HTTP-Referer": "https://cline.bot",
"X-Title": "Cline",
"X-Task-ID": this.host.ulid || "",
}
Object.assign(defaultHeaders, customHeader)
const hooks = this.createAgentHooks()
const agent = new Agent({
providerId: providerInfo.providerId,
modelId: providerInfo.model.id,
apiKey: providerConfig.apiKey,
baseUrl: providerConfig.baseUrl,
headers: providerConfig.headers,
knownModels: providerConfig.knownModels,
systemPrompt: getClineDefaultSystemPrompt("VS Code", this.host.cwd),
tools: this.builtInTools,
hooks,
maxIterations: 50,
onEvent: async (event: AgentEvent) => {
switch (event.type) {
case "content_start":
if (event.contentType === "text") {
const nextText = event.accumulated ?? event.text
if (nextText !== undefined) {
await this.host.say("text", nextText, undefined, undefined, true)
}
}
if (event.contentType === "reasoning" && event.reasoning !== undefined) {
this.streamedReasoning += event.reasoning
await this.host.say("reasoning", this.streamedReasoning, undefined, undefined, true)
}
if (event.contentType === "tool" && event.toolName) {
Logger.debug(`[Task ${this.host.taskId}] Agent tool call start: ${event.toolName}`)
}
break
case "content_end":
if (event.contentType === "reasoning") {
const finalReasoning = event.reasoning ?? this.streamedReasoning
if (finalReasoning) {
await this.host.say("reasoning", finalReasoning, undefined, undefined, false)
}
}
break
case "error":
Logger.error(`[Task ${this.host.taskId}] Agent runtime error`, event.error)
break
}
},
})
this.agent = agent
this.agentSignature = nextSignature
return { agent, reusedExistingAgent: false }
}
private createAgentHooks(): AgentHooks {
return this.host.createAgentHooks({
onRunStart: () => this.handleRunStartHook(),
buildPendingToolInfo: (toolName, toolInput) => this.buildPendingToolInfo(toolName, toolInput),
toHookStringParameters: (input) => this.toHookStringParameters(input),
createHookContextBlock: (hookName, contextModification) => this.createHookContextBlock(hookName, contextModification),
safeJsonStringify: (value) => this.safeJsonStringify(value),
})
}
private async handleRunStartHook(): Promise<{ cancel?: boolean; context?: string } | undefined> {
const hooksEnabled = getHooksEnabledSafe()
if (!hooksEnabled) {
return undefined
}
const runContext = this.pendingRunContext
if (!runContext) {
return undefined
}
const contextBlocks: string[] = []
if (runContext.phase === "initial_task") {
const taskStartResult = await this.host.runTaskLifecycleHook({
hookName: "TaskStart",
hookInput: {
taskStart: {
taskMetadata: {
taskId: this.host.taskId,
ulid: this.host.ulid,
initialTask: runContext.initialTask || "",
},
},
},
})
if (taskStartResult.cancel) {
await this.host.handleHookCancellation("TaskStart", taskStartResult.wasCancelled)
await this.host.cancelTask()
return { cancel: true }
}
const taskStartContext = this.createHookContextBlock("TaskStart", taskStartResult.contextModification)
if (taskStartContext) {
contextBlocks.push(taskStartContext)
}
}
if (runContext.phase === "resume") {
const taskResumeResult = await this.host.runTaskLifecycleHook({
hookName: "TaskResume",
hookInput: {
taskResume: {
taskMetadata: {
taskId: this.host.taskId,
ulid: this.host.ulid,
},
previousState: runContext.resumePreviousState || {
lastMessageTs: "",
messageCount: "0",
conversationHistoryDeleted: "false",
},
},
},
})
if (taskResumeResult.cancel) {
await this.host.handleHookCancellation("TaskResume", taskResumeResult.wasCancelled)
await this.host.cancelTask()
return { cancel: true }
}
const taskResumeContext = this.createHookContextBlock("TaskResume", taskResumeResult.contextModification)
if (taskResumeContext) {
contextBlocks.push(taskResumeContext)
}
}
const userPromptHookContent = runContext.userPromptHookContent ?? []
const userPromptResult = await this.host.runUserPromptSubmitHook(userPromptHookContent)
if (userPromptResult.cancel) {
await this.host.handleHookCancellation("UserPromptSubmit", userPromptResult.wasCancelled)
await this.host.cancelTask()
return { cancel: true }
}
const userPromptContext = this.createHookContextBlock("UserPromptSubmit", userPromptResult.contextModification)
if (userPromptContext) {
contextBlocks.push(userPromptContext)
}
if (contextBlocks.length > 0) {
return { context: contextBlocks.join("\n") }
}
return undefined
}
private createHookContextBlock(source: string, contextModification?: string): string | undefined {
if (!contextModification) {
return
}
const contextText = contextModification.trim()
if (!contextText) {
return
}
const lines = contextText.split("\n")
const firstLine = lines[0]
let contextType = "general"
let content = contextText
const typeMatch = /^([A-Z_]+):\s*(.*)/.exec(firstLine)
if (typeMatch) {
contextType = typeMatch[1].toLowerCase()
const remainingLines = lines.slice(1).filter((line) => line.trim())
content = typeMatch[2] ? [typeMatch[2], ...remainingLines].join("\n") : remainingLines.join("\n")
}
if (contextType === "general") {
return `<hook_context source="${source}">\n${content}\n</hook_context>`
}
return `<hook_context source="${source}" type="${contextType}">\n${content}\n</hook_context>`
}
private toHookParameters(input: unknown): Record<string, unknown> {
if (!input || typeof input !== "object" || Array.isArray(input)) {
return {}
}
return { ...(input as Record<string, unknown>) }
}
private toHookStringParameters(input: unknown): Record<string, string> {
const params = this.toHookParameters(input)
const entries = Object.entries(params).map(([key, value]) => {
if (typeof value === "string") {
return [key, value] as const
}
return [key, this.safeJsonStringify(value)] as const
})
return Object.fromEntries(entries)
}
private buildPendingToolInfo(toolName: string, input: unknown): Record<string, unknown> {
const pendingToolInfo: Record<string, unknown> = { tool: toolName }
const params = this.toHookParameters(input)
for (const key of ["path", "command", "regex", "url", "tool_name", "server_name", "uri"]) {
if (params[key] !== undefined) {
pendingToolInfo[key] = params[key]
}
}
if (typeof params.content === "string") {
pendingToolInfo.content = params.content.slice(0, 200)
}
if (typeof params.diff === "string") {
pendingToolInfo.diff = params.diff.slice(0, 200)
}
return pendingToolInfo
}
private safeJsonStringify(value: unknown): string {
try {
return JSON.stringify(value)
} catch {
return String(value)
}
}
private resolveProviderConfig(providerInfo: ApiProviderInfo, apiConfig: ApiConfiguration): ProviderConnectionConfig {
const { providerId, mode, model } = providerInfo
switch (providerId) {
case "anthropic":
return { apiKey: apiConfig.apiKey, baseUrl: apiConfig.anthropicBaseUrl }
case "openrouter":
return { apiKey: apiConfig.openRouterApiKey }
case "cline":
return {
apiKey: apiConfig.clineAccountId,
knownModels:
model?.id && model?.info ? { [model.id]: toRegisteredModelInfo(model.id, model.info) } : undefined,
}
case "openai":
return {
apiKey: apiConfig.openAiApiKey,
baseUrl: apiConfig.openAiBaseUrl,
headers: apiConfig.openAiHeaders,
}
case "gemini":
return { apiKey: apiConfig.geminiApiKey, baseUrl: apiConfig.geminiBaseUrl }
case "vertex":
return { apiKey: apiConfig.geminiApiKey, baseUrl: apiConfig.geminiBaseUrl }
case "ollama":
return { apiKey: apiConfig.ollamaApiKey, baseUrl: apiConfig.ollamaBaseUrl }
case "lmstudio":
return { baseUrl: apiConfig.lmStudioBaseUrl }
case "vscode-lm": {
const selector =
mode === "plan" ? apiConfig.planModeVsCodeLmModelSelector : apiConfig.actModeVsCodeLmModelSelector
return {
headers: selector ? { [VSCODE_LM_SELECTOR_HEADER]: JSON.stringify(selector) } : undefined,
}
}
case "deepseek":
return { apiKey: apiConfig.deepSeekApiKey }
case "together":
return { apiKey: apiConfig.togetherApiKey }
case "litellm":
return { apiKey: apiConfig.liteLlmApiKey, baseUrl: apiConfig.liteLlmBaseUrl }
case "nebius":
return { apiKey: apiConfig.nebiusApiKey }
case "sambanova":
return { apiKey: apiConfig.sambanovaApiKey }
case "cerebras":
return { apiKey: apiConfig.cerebrasApiKey }
case "baseten":
return { apiKey: apiConfig.basetenApiKey }
case "huggingface":
return { apiKey: apiConfig.huggingFaceApiKey }
case "huawei-cloud-maas":
return { apiKey: apiConfig.huaweiCloudMaasApiKey }
case "vercel-ai-gateway":
return { apiKey: apiConfig.vercelAiGatewayApiKey }
case "aihubmix":
return {
apiKey: apiConfig.aihubmixApiKey,
baseUrl: apiConfig.aihubmixBaseUrl,
headers: apiConfig.aihubmixAppCode ? { "APP-Code": apiConfig.aihubmixAppCode } : undefined,
}
case "hicap":
return {
apiKey: apiConfig.hicapApiKey,
headers: apiConfig.hicapApiKey ? { "api-key": apiConfig.hicapApiKey } : undefined,
}
case "nousResearch":
return { apiKey: apiConfig.nousResearchApiKey }
case "requesty":
return { apiKey: apiConfig.requestyApiKey, baseUrl: apiConfig.requestyBaseUrl }
case "xai":
return { apiKey: apiConfig.xaiApiKey }
case "groq":
return { apiKey: apiConfig.groqApiKey }
case "fireworks":
return { apiKey: apiConfig.fireworksApiKey }
default:
Logger.warn(`[Task ${this.host.taskId}] No explicit provider config mapping for provider "${providerId}"`)
return {}
}
}
}
+342
View File
@@ -0,0 +1,342 @@
import type { AgentHooks } from "@cline/agents"
import { executeHook } from "@core/hooks/hook-executor"
import type { Hooks as HookInputs } from "@core/hooks/hook-factory"
import { getHooksEnabledSafe } from "@core/hooks/hooks-utils"
import { executePreCompactHookWithCleanup, type HookExecution } from "@core/hooks/precompact-executor"
import type { ClineMessage, ClineSay } from "@shared/ExtensionMessage"
import type { ClineContent, ClineStorageMessage } from "@shared/messages/content"
import type { ContextManager } from "@/core/context/context-management/ContextManager"
import type { MessageStateHandler } from "@/core/task/message-state"
import { Logger } from "@/shared/services/Logger"
type WithStateLock = <T>(fn: () => T | Promise<T>) => Promise<T>
export interface TaskHookResult {
cancel?: boolean
contextModification?: string
errorMessage?: string
wasCancelled: boolean
}
export interface TaskHookAdapterContext {
taskId: string
ulid: string
taskState: {
abort: boolean
didFinishAbortingStream: boolean
activeHookExecution?: HookExecution
}
messageStateHandler: MessageStateHandler
say: (type: ClineSay, text?: string, images?: string[], files?: string[], partial?: boolean) => Promise<number | undefined>
postStateToWebview: () => Promise<void>
cancelTask: () => Promise<void>
withStateLock?: WithStateLock
}
export interface CreateTaskAgentHooksOptions {
onRunStart: () => Promise<{ cancel?: boolean; context?: string } | undefined>
buildPendingToolInfo: (toolName: string, toolInput: unknown) => Record<string, unknown> | undefined
toHookStringParameters: (input: unknown) => Record<string, string>
createHookContextBlock: (hookName: string, contextModification?: string) => string | undefined
safeJsonStringify: (value: unknown) => string
}
/**
* Adapter that ports task-coupled hook behavior to the agents-extension boundary.
* This keeps hook semantics in one place while task/runtime migration completes.
*/
export class TaskHookExtensionAdapter {
constructor(private readonly ctx: TaskHookAdapterContext) {}
async setActiveHookExecution(hookExecution: HookExecution): Promise<void> {
if (this.ctx.withStateLock) {
await this.ctx.withStateLock(() => {
this.ctx.taskState.activeHookExecution = hookExecution
})
return
}
this.ctx.taskState.activeHookExecution = hookExecution
}
async clearActiveHookExecution(): Promise<void> {
if (this.ctx.withStateLock) {
await this.ctx.withStateLock(() => {
this.ctx.taskState.activeHookExecution = undefined
})
return
}
this.ctx.taskState.activeHookExecution = undefined
}
async getActiveHookExecution(): Promise<HookExecution | undefined> {
if (this.ctx.withStateLock) {
return await this.ctx.withStateLock(() => this.ctx.taskState.activeHookExecution)
}
return this.ctx.taskState.activeHookExecution
}
async runTaskLifecycleHook<K extends "TaskStart" | "TaskResume">(params: {
hookName: K
hookInput: HookInputs[K]
}): Promise<TaskHookResult> {
const hooksEnabled = getHooksEnabledSafe()
if (!hooksEnabled) {
return { wasCancelled: false }
}
const result = await executeHook({
hookName: params.hookName,
hookInput: params.hookInput,
isCancellable: true,
say: this.ctx.say,
setActiveHookExecution: async (execution) => this.setActiveHookExecution(execution as HookExecution),
clearActiveHookExecution: async () => this.clearActiveHookExecution(),
messageStateHandler: this.ctx.messageStateHandler,
taskId: this.ctx.taskId,
hooksEnabled,
})
return {
cancel: result.cancel,
contextModification: result.contextModification,
errorMessage: result.errorMessage,
wasCancelled: result.wasCancelled,
}
}
async runUserPromptSubmitHook(userContent: ClineContent[]): Promise<TaskHookResult> {
const hooksEnabled = getHooksEnabledSafe()
if (!hooksEnabled) {
return { wasCancelled: false }
}
const { extractUserPromptFromContent } = await import("@/core/task/utils/extractUserPromptFromContent")
const promptText = extractUserPromptFromContent(userContent)
const result = await executeHook({
hookName: "UserPromptSubmit",
hookInput: {
userPromptSubmit: {
prompt: promptText,
attachments: [],
},
},
isCancellable: true,
say: this.ctx.say,
setActiveHookExecution: async (execution) => this.setActiveHookExecution(execution as HookExecution),
clearActiveHookExecution: async () => this.clearActiveHookExecution(),
messageStateHandler: this.ctx.messageStateHandler,
taskId: this.ctx.taskId,
hooksEnabled,
})
if (result.cancel === true && result.wasCancelled) {
this.ctx.taskState.didFinishAbortingStream = true
await this.ctx.messageStateHandler.saveClineMessagesAndUpdateHistory()
await this.ctx.messageStateHandler.overwriteApiConversationHistory(
this.ctx.messageStateHandler.getApiConversationHistory(),
)
await this.ctx.postStateToWebview()
}
return {
cancel: result.cancel,
contextModification: result.contextModification,
errorMessage: result.errorMessage,
wasCancelled: result.wasCancelled,
}
}
async runTaskCancelHook(taskMetadata: { taskId: string; ulid: string; completionStatus: string }): Promise<void> {
const hooksEnabled = getHooksEnabledSafe()
if (!hooksEnabled) {
return
}
await executeHook({
hookName: "TaskCancel",
hookInput: {
taskCancel: {
taskMetadata,
},
},
isCancellable: false,
say: this.ctx.say,
messageStateHandler: this.ctx.messageStateHandler,
taskId: this.ctx.taskId,
hooksEnabled,
})
}
async runPreCompactHookWithCleanup(params: {
apiConversationHistory: ClineStorageMessage[]
conversationHistoryDeletedRange: [number, number] | undefined
contextManager: ContextManager
clineMessages: ClineMessage[]
deletedRange?: [number, number]
}): Promise<{ contextModification?: string }> {
const hooksEnabled = getHooksEnabledSafe()
if (!hooksEnabled) {
return {}
}
return await executePreCompactHookWithCleanup({
taskId: this.ctx.taskId,
ulid: this.ctx.ulid,
apiConversationHistory: params.apiConversationHistory,
conversationHistoryDeletedRange: params.conversationHistoryDeletedRange,
contextManager: params.contextManager,
clineMessages: params.clineMessages,
messageStateHandler: this.ctx.messageStateHandler,
compactionStrategy: "standard-truncation-lastquarter",
deletedRange: params.deletedRange,
say: this.ctx.say,
setActiveHookExecution: async (hookExecution: HookExecution | undefined) => {
if (hookExecution) {
await this.setActiveHookExecution(hookExecution)
}
},
clearActiveHookExecution: async () => this.clearActiveHookExecution(),
postStateToWebview: this.ctx.postStateToWebview,
taskState: this.ctx.taskState,
cancelTask: this.ctx.cancelTask,
hooksEnabled,
})
}
async cancelHookExecution(logError: (message: string, error: unknown) => void): Promise<boolean> {
const activeHook = await this.getActiveHookExecution()
if (!activeHook) {
return false
}
const { hookName, toolName, messageTs, abortController } = activeHook
try {
abortController.abort()
const clineMessages = this.ctx.messageStateHandler.getClineMessages()
const hookMessageIndex = clineMessages.findIndex((m) => m.ts === messageTs)
if (hookMessageIndex !== -1) {
await this.ctx.messageStateHandler.updateClineMessage(hookMessageIndex, {
text: JSON.stringify({
hookName,
toolName,
status: "cancelled",
exitCode: 130,
}),
})
}
await this.ctx.say("hook_output_stream", "\nHook execution cancelled by user")
return true
} catch (error) {
logError("Failed to cancel hook execution", error)
return false
}
}
async handleHookCancellation(hookName: string, wasCancelled: boolean): Promise<void> {
this.ctx.taskState.didFinishAbortingStream = true
await this.ctx.messageStateHandler.saveClineMessagesAndUpdateHistory()
await this.ctx.messageStateHandler.overwriteApiConversationHistory(
this.ctx.messageStateHandler.getApiConversationHistory(),
)
await this.ctx.postStateToWebview()
Logger.log(`[Task ${this.ctx.taskId}] ${hookName} hook cancelled (userInitiated: ${wasCancelled})`)
}
createAgentHooks(options: CreateTaskAgentHooksOptions): AgentHooks {
return {
onRunStart: async () => options.onRunStart(),
onToolCallStart: async (ctx) => {
if (this.ctx.taskState.abort) {
return { cancel: true }
}
const hooksEnabled = getHooksEnabledSafe()
if (!hooksEnabled) {
return
}
const pendingToolInfo = options.buildPendingToolInfo(ctx.call.name, ctx.call.input)
const result = await executeHook({
hookName: "PreToolUse",
hookInput: {
preToolUse: {
toolName: ctx.call.name,
parameters: options.toHookStringParameters(ctx.call.input),
},
},
isCancellable: true,
say: this.ctx.say,
setActiveHookExecution: async (execution) => this.setActiveHookExecution(execution as HookExecution),
clearActiveHookExecution: async () => this.clearActiveHookExecution(),
messageStateHandler: this.ctx.messageStateHandler,
taskId: this.ctx.taskId,
hooksEnabled,
toolName: ctx.call.name,
pendingToolInfo,
})
if (result.cancel) {
await this.clearActiveHookExecution()
if (result.errorMessage) {
await this.ctx.say("error", result.errorMessage)
}
await this.ctx.cancelTask()
return { cancel: true }
}
const context = options.createHookContextBlock("PreToolUse", result.contextModification)
return context ? { context } : undefined
},
onToolCallEnd: async (ctx) => {
if (this.ctx.taskState.abort) {
return { cancel: true }
}
const hooksEnabled = getHooksEnabledSafe()
if (!hooksEnabled) {
return
}
const toolResult = ctx.record.error
? { error: ctx.record.error }
: typeof ctx.record.output === "string"
? ctx.record.output
: options.safeJsonStringify(ctx.record.output)
const result = await executeHook({
hookName: "PostToolUse",
hookInput: {
postToolUse: {
toolName: ctx.record.name,
parameters: options.toHookStringParameters(ctx.record.input),
result: typeof toolResult === "string" ? toolResult : options.safeJsonStringify(toolResult),
success: !ctx.record.error,
executionTimeMs: ctx.record.durationMs,
},
},
isCancellable: true,
say: this.ctx.say,
setActiveHookExecution: async (execution) => this.setActiveHookExecution(execution as HookExecution),
clearActiveHookExecution: async () => this.clearActiveHookExecution(),
messageStateHandler: this.ctx.messageStateHandler,
taskId: this.ctx.taskId,
hooksEnabled,
toolName: ctx.record.name,
})
if (result.cancel) {
if (result.errorMessage) {
await this.ctx.say("error", result.errorMessage)
}
await this.ctx.cancelTask()
return { cancel: true }
}
const context = options.createHookContextBlock("PostToolUse", result.contextModification)
return context ? { context } : undefined
},
}
}
}
+4
View File
@@ -66,6 +66,10 @@ export class ToolExecutor {
private autoApprover: AutoApprove
private coordinator: ToolExecutorCoordinator
public setApiHandler(api: ApiHandler): void {
this.api = api
}
// Auto-approval methods using the AutoApprove class
private shouldAutoApproveTool(toolName: ClineDefaultTool): boolean | [boolean, boolean] {
return this.autoApprover.shouldAutoApproveTool(toolName)
+98 -2363
View File
File diff suppressed because it is too large Load Diff
@@ -1,10 +1,8 @@
import type { ToolUse } from "@core/assistant-message"
import { getHooksEnabledSafe } from "@core/hooks/hooks-utils"
import { executePreCompactHookWithCleanup, HookCancellationError } from "@core/hooks/precompact-executor"
import { continuationPrompt } from "@core/prompts/contextManagement"
import { formatResponse } from "@core/prompts/responses"
import { ensureTaskDirectoryExists } from "@core/storage/disk"
import { StateManager } from "@core/storage/StateManager"
import { TaskHookExtensionAdapter } from "@core/task/TaskHookExtensionAdapter"
import { resolveWorkspacePath } from "@core/workspace"
import { extractFileContent } from "@integrations/misc/extract-file-content"
import { ClineSayTool } from "@shared/ExtensionMessage"
@@ -41,63 +39,43 @@ export class SummarizeTaskHandler implements IToolHandler, IPartialBlockHandler
// Variable to store context modification from PreCompact hook
let hookContextModification: string | undefined
// Run PreCompact hook right before showing the condensing message
const hooksEnabled = getHooksEnabledSafe()
if (hooksEnabled) {
try {
// Determine compaction strategy
const useAutoCondense = StateManager.get().getGlobalSettingsKey("useAutoCondense")
const strategy = useAutoCondense ? "auto-condense" : "standard-truncation-firstpair"
const apiHistory = config.messageState.getApiConversationHistory()
const result = await executePreCompactHookWithCleanup({
taskId: config.taskId,
ulid: config.ulid,
apiConversationHistory: apiHistory,
conversationHistoryDeletedRange: config.taskState.conversationHistoryDeletedRange,
contextManager: config.services.contextManager,
clineMessages: config.messageState.getClineMessages(),
messageStateHandler: config.messageState,
compactionStrategy: strategy,
say: config.callbacks.say,
setActiveHookExecution: async (hookExecution) => {
if (hookExecution) {
await config.callbacks.setActiveHookExecution(hookExecution)
}
},
clearActiveHookExecution: config.callbacks.clearActiveHookExecution,
postStateToWebview: config.callbacks.postStateToWebview,
taskState: config.taskState,
cancelTask: config.callbacks.cancelTask,
hooksEnabled,
})
// Hook completed successfully - capture context modification if provided
if (result.contextModification) {
hookContextModification = result.contextModification
Logger.log(`[PreCompact] Hook provided context modification for task ${config.taskId}`)
}
} catch (error) {
// Check if this is a hook cancellation error
if (error instanceof HookCancellationError) {
// Hook was cancelled - show message and return early without executing summarization
// (State already saved and task already cancelled by executePreCompactHookWithCleanup)
await config.callbacks.say(
"error",
"Context compaction was cancelled by PreCompact hook. Task has been aborted.",
)
return "Context compaction was cancelled. Task has been aborted."
}
// Graceful degradation: Show warning but continue with compaction
// Hook UI already shows "Failed" status with error details
// Run PreCompact hook right before showing the condensing message.
try {
const adapter = new TaskHookExtensionAdapter({
taskId: config.taskId,
ulid: config.ulid,
taskState: config.taskState,
messageStateHandler: config.messageState,
say: config.callbacks.say,
postStateToWebview: config.callbacks.postStateToWebview,
cancelTask: config.callbacks.cancelTask,
})
const preCompactResult = await adapter.runPreCompactHookWithCleanup({
apiConversationHistory: config.messageState.getApiConversationHistory(),
conversationHistoryDeletedRange: config.taskState.conversationHistoryDeletedRange,
contextManager: config.services.contextManager,
clineMessages: config.messageState.getClineMessages(),
})
if (preCompactResult.contextModification) {
hookContextModification = preCompactResult.contextModification
Logger.log(`[PreCompact] Hook provided context modification for task ${config.taskId}`)
}
} catch (error) {
// If cancellation happened, cancelTask has already run inside the adapter.
if (config.taskState.abort) {
await config.callbacks.say(
"error",
`PreCompact hook failed, continuing with compaction: ${error instanceof Error ? error.message : String(error)}`,
"Context compaction was cancelled by PreCompact hook. Task has been aborted.",
)
Logger.error("[PreCompact] Hook execution failed, continuing with compaction:", error)
return "Context compaction was cancelled. Task has been aborted."
}
// Graceful degradation: Show warning but continue with compaction.
await config.callbacks.say(
"error",
`PreCompact hook failed, continuing with compaction: ${error instanceof Error ? error.message : String(error)}`,
)
Logger.error("[PreCompact] Hook execution failed, continuing with compaction:", error)
}
// Show completed summary in tool UI
+210 -252
View File
@@ -1,9 +1,8 @@
import { setTimeout as delay } from "node:timers/promises"
import { buildApiHandler } from "@core/api"
import type { ApiHandler } from "@core/api"
import { parseAssistantMessageV2, ToolUse } from "@core/assistant-message"
import { ContextManager } from "@core/context/context-management/ContextManager"
import { checkContextWindowExceededError } from "@core/context/context-management/context-error-handling"
import { getContextWindowInfo } from "@core/context/context-management/context-window-utils"
import { discoverSkills, getAvailableSkills } from "@core/context/instructions/user-instructions/skills"
import { formatResponse } from "@core/prompts/responses"
import { PromptRegistry } from "@core/prompts/system-prompt"
@@ -14,10 +13,9 @@ import { ClineAssistantToolUseBlock, ClineStorageMessage, ClineTextContentBlock
import { Logger } from "@shared/services/Logger"
import type { ClineTool } from "@shared/tools"
import { ClineDefaultTool } from "@shared/tools"
import { isNextGenModelFamily } from "@utils/model-utils"
import * as path from "path"
import { CoreAgent } from "@/core/agents/CoreAgent"
import { HostProvider } from "@/hosts/host-provider"
import { ClineError, ClineErrorType } from "@/services/error/ClineError"
import { ApiFormat } from "@/shared/proto/cline/models"
import { calculateApiCostAnthropic } from "@/utils/cost"
import { TaskState } from "../../TaskState"
@@ -225,6 +223,7 @@ export class SubagentRunner {
private abortRequested = false
private activeCommandExecutions = 0
private abortingCommands = false
private coreAgent = new CoreAgent()
constructor(private baseConfig: TaskConfig) {}
@@ -302,15 +301,15 @@ export class SubagentRunner {
...apiConfiguration,
ulid: this.baseConfig.ulid,
}
const api = buildApiHandler(effectiveApiConfiguration, mode)
this.activeApiAbort = api.abort?.bind(api)
const api = this.coreAgent.initializeApiHandler(effectiveApiConfiguration, mode)
this.activeApiAbort = () => this.coreAgent.abortCurrentRequest()
const providerId = (
mode === "plan" ? apiConfiguration.planModeApiProvider : apiConfiguration.actModeApiProvider
) as string
const providerInfo = {
providerId,
model: api.getModel(),
model: this.coreAgent.getModel(),
mode,
customPrompt: this.baseConfig.services.stateManager.getGlobalSettingsKey("customPrompt"),
}
@@ -377,66 +376,59 @@ export class SubagentRunner {
},
]
while (true) {
if (
previousRequestTotalTokens !== undefined &&
this.shouldCompactBeforeNextRequest(previousRequestTotalTokens, api, providerInfo.model.id)
) {
const didCompact = this.compactConversationForContextWindow(conversation)
if (didCompact) {
Logger.warn("[SubagentRunner] Proactively compacted context before next subagent request.")
const loopResult = await this.coreAgent.runLoop<void, SubagentRunResult>({
initialInput: undefined,
shouldAbort: () => this.shouldAbort(),
runTurn: async () => {
if (
previousRequestTotalTokens !== undefined &&
this.coreAgent.shouldCompactBeforeNextRequest(
previousRequestTotalTokens,
api,
providerInfo.model.id,
this.baseConfig.services.stateManager.getGlobalSettingsKey("useAutoCondense"),
)
) {
const didCompact = this.compactConversationForContextWindow(conversation)
if (didCompact) {
Logger.warn("[SubagentRunner] Proactively compacted context before next subagent request.")
}
previousRequestTotalTokens = undefined
}
// Prevent repeated compaction attempts off the same token sample.
previousRequestTotalTokens = undefined
}
const streamHandler = new StreamResponseHandler()
const { toolUseHandler } = streamHandler.getHandlers()
let requestInputTokens = 0
let requestOutputTokens = 0
let requestCacheWriteTokens = 0
let requestCacheReadTokens = 0
let requestTotalCost: number | undefined
const streamHandler = new StreamResponseHandler()
const { toolUseHandler } = streamHandler.getHandlers()
let assistantText = ""
let assistantTextSignature: string | undefined
let requestId: string | undefined
const stream = this.createMessageWithInitialChunkRetry(
api,
systemPrompt,
conversation,
nativeTools,
providerInfo.providerId,
providerInfo.model.id,
)
const stream = this.createMessageWithInitialChunkRetry(
api,
systemPrompt,
conversation,
nativeTools,
providerInfo.providerId,
providerInfo.model.id,
)
for await (const chunk of stream) {
switch (chunk.type) {
case "usage":
requestId = requestId ?? chunk.id
const streamResult = await this.coreAgent.consumeStream({
stream,
shouldAbort: () => this.shouldAbort(),
onAbort: async () => {
await this.abort()
},
onUsageChunk: (chunk, state) => {
stats.inputTokens += chunk.inputTokens || 0
stats.outputTokens += chunk.outputTokens || 0
stats.cacheWriteTokens += chunk.cacheWriteTokens || 0
stats.cacheReadTokens += chunk.cacheReadTokens || 0
requestInputTokens += chunk.inputTokens || 0
requestOutputTokens += chunk.outputTokens || 0
requestCacheWriteTokens += chunk.cacheWriteTokens || 0
requestCacheReadTokens += chunk.cacheReadTokens || 0
requestTotalCost = chunk.totalCost ?? requestTotalCost
stats.contextTokens =
requestInputTokens + requestOutputTokens + requestCacheWriteTokens + requestCacheReadTokens
state.usage.inputTokens +
state.usage.outputTokens +
state.usage.cacheWriteTokens +
state.usage.cacheReadTokens
stats.contextUsagePercentage =
stats.contextWindow > 0 ? (stats.contextTokens / stats.contextWindow) * 100 : 0
onProgress({ stats: { ...stats } })
break
case "text":
requestId = requestId ?? chunk.id
assistantText += chunk.text || ""
assistantTextSignature = chunk.signature || assistantTextSignature
break
case "tool_calls":
requestId = requestId ?? chunk.id
},
onToolCallChunk: (chunk) => {
toolUseHandler.processToolUseDelta(
{
id: chunk.tool_call.function?.id,
@@ -447,188 +439,184 @@ export class SubagentRunner {
},
chunk.tool_call.call_id,
)
break
case "reasoning":
requestId = requestId ?? chunk.id
break
}
if (this.shouldAbort()) {
await this.abort()
const error = "Subagent run cancelled."
onProgress({ status: "failed", error, stats: { ...stats } })
return { status: "failed", error, stats }
}
}
const calculatedRequestCost =
requestTotalCost ??
calculateApiCostAnthropic(
providerInfo.model.info,
requestInputTokens,
requestOutputTokens,
requestCacheWriteTokens,
requestCacheReadTokens,
)
stats.totalCost += calculatedRequestCost || 0
previousRequestTotalTokens =
requestInputTokens + requestOutputTokens + requestCacheWriteTokens + requestCacheReadTokens
const nativeFinalizedToolCalls = toolUseHandler.getAllFinalizedToolUses().map((toolCall, index) => ({
toolUseId: resolveToolUseId(toolCall, index),
id: toolCall.id,
call_id: toolCall.call_id,
signature: toolCall.signature,
name: toolCall.name,
input: toolCall.input,
isNativeToolCall: true,
}))
const parsedNonNativeToolCalls = parseNonNativeToolCalls(assistantText)
const fallbackNonNativeToolCalls = nativeFinalizedToolCalls.map((toolCall) => ({
...toolCall,
isNativeToolCall: false,
}))
let finalizedToolCalls: SubagentToolCall[] = []
if (useNativeToolCalls) {
finalizedToolCalls = nativeFinalizedToolCalls
} else if (parsedNonNativeToolCalls.length > 0) {
finalizedToolCalls = parsedNonNativeToolCalls
} else if (fallbackNonNativeToolCalls.length > 0) {
// Defensive fallback: if non-native mode receives structured tool call chunks,
// execute them but serialize results as plain text to avoid tool_result pairing mismatches.
Logger.warn(
"[SubagentRunner] Received structured tool_calls while native tool calling is disabled; falling back to non-native result serialization.",
)
finalizedToolCalls = fallbackNonNativeToolCalls
}
const assistantContent = [] as any[]
if (assistantText.trim().length > 0) {
assistantContent.push({
type: "text",
text: assistantText,
signature: assistantTextSignature,
},
})
}
if (useNativeToolCalls) {
assistantContent.push(...finalizedToolCalls.map(toAssistantToolUseBlock))
}
if (assistantContent.length > 0) {
conversation.push({
role: "assistant",
content: assistantContent,
id: requestId,
})
}
if (finalizedToolCalls.length === 0) {
emptyAssistantResponseRetries += 1
if (emptyAssistantResponseRetries > MAX_EMPTY_ASSISTANT_RETRIES) {
const error = "Subagent did not call attempt_completion."
onProgress({ status: "failed", error, stats: { ...stats } })
return { status: "failed", error, stats }
if (streamResult.aborted) {
return { status: "failed", error: "Subagent run cancelled." }
}
// Mirror the main loop's no-tools-used nudge so empty/blank model turns
// can recover without surfacing an immediate hard failure in subagent UI.
if (assistantContent.length === 0) {
conversation.push({
role: "assistant",
content: [
{
type: "text",
text: "Failure: I did not provide a response.",
},
],
id: requestId,
const calculatedRequestCost =
streamResult.usage.totalCost ??
calculateApiCostAnthropic(
providerInfo.model.info,
streamResult.usage.inputTokens,
streamResult.usage.outputTokens,
streamResult.usage.cacheWriteTokens,
streamResult.usage.cacheReadTokens,
)
stats.totalCost += calculatedRequestCost || 0
previousRequestTotalTokens =
streamResult.usage.inputTokens +
streamResult.usage.outputTokens +
streamResult.usage.cacheWriteTokens +
streamResult.usage.cacheReadTokens
const nativeFinalizedToolCalls = toolUseHandler.getAllFinalizedToolUses().map((toolCall, index) => ({
toolUseId: resolveToolUseId(toolCall, index),
id: toolCall.id,
call_id: toolCall.call_id,
signature: toolCall.signature,
name: toolCall.name,
input: toolCall.input,
isNativeToolCall: true,
}))
const parsedNonNativeToolCalls = parseNonNativeToolCalls(streamResult.assistantText)
const fallbackNonNativeToolCalls = nativeFinalizedToolCalls.map((toolCall) => ({
...toolCall,
isNativeToolCall: false,
}))
let finalizedToolCalls: SubagentToolCall[] = []
if (useNativeToolCalls) {
finalizedToolCalls = nativeFinalizedToolCalls
} else if (parsedNonNativeToolCalls.length > 0) {
finalizedToolCalls = parsedNonNativeToolCalls
} else if (fallbackNonNativeToolCalls.length > 0) {
Logger.warn(
"[SubagentRunner] Received structured tool_calls while native tool calling is disabled; falling back to non-native result serialization.",
)
finalizedToolCalls = fallbackNonNativeToolCalls
}
const assistantContent = [] as any[]
if (streamResult.assistantText.trim().length > 0) {
assistantContent.push({
type: "text",
text: streamResult.assistantText,
signature: streamResult.assistantTextSignature,
})
}
conversation.push({
role: "user",
content: [
{
type: "text",
text: formatResponse.noToolsUsed(useNativeToolCalls),
},
],
})
await delay(0)
continue
}
emptyAssistantResponseRetries = 0
if (useNativeToolCalls) {
assistantContent.push(...finalizedToolCalls.map(toAssistantToolUseBlock))
}
const toolResultBlocks = [] as any[]
for (const call of finalizedToolCalls) {
const toolName = call.name as ClineDefaultTool
const toolCallParams = toToolUseParams(call.input)
if (assistantContent.length > 0) {
conversation.push({
role: "assistant",
content: assistantContent,
id: streamResult.requestId,
})
}
if (toolName === ClineDefaultTool.ATTEMPT) {
const completionResult = toolCallParams.result?.trim()
if (!completionResult) {
const missingResultError = formatResponse.missingToolParameterError("result")
pushSubagentToolResultBlock(toolResultBlocks, call, toolName, missingResultError)
if (finalizedToolCalls.length === 0) {
emptyAssistantResponseRetries += 1
if (emptyAssistantResponseRetries > MAX_EMPTY_ASSISTANT_RETRIES) {
return { status: "failed", error: "Subagent did not call attempt_completion." }
}
if (assistantContent.length === 0) {
conversation.push({
role: "assistant",
content: [{ type: "text", text: "Failure: I did not provide a response." }],
id: streamResult.requestId,
})
}
conversation.push({
role: "user",
content: [{ type: "text", text: formatResponse.noToolsUsed(useNativeToolCalls) }],
})
await delay(0)
return { status: "continue", nextInput: undefined }
}
emptyAssistantResponseRetries = 0
const toolResultBlocks = [] as any[]
for (const call of finalizedToolCalls) {
const toolName = call.name as ClineDefaultTool
const toolCallParams = toToolUseParams(call.input)
if (toolName === ClineDefaultTool.ATTEMPT) {
const completionResult = toolCallParams.result?.trim()
if (!completionResult) {
const missingResultError = formatResponse.missingToolParameterError("result")
pushSubagentToolResultBlock(toolResultBlocks, call, toolName, missingResultError)
continue
}
stats.toolCalls += 1
onProgress({ stats: { ...stats } })
const completed: SubagentRunResult = { status: "completed", result: completionResult, stats }
return { status: "complete", output: completed }
}
if (!SUBAGENT_ALLOWED_TOOLS.includes(toolName)) {
const deniedResult = formatResponse.toolError(
`Tool '${toolName}' is not available inside subagent runs.`,
)
pushSubagentToolResultBlock(toolResultBlocks, call, toolName, deniedResult)
continue
}
const toolCallBlock: ToolUse = {
type: "tool_use",
name: toolName,
params: toolCallParams,
partial: false,
isNativeToolCall: call.isNativeToolCall,
call_id: call.call_id || call.toolUseId,
signature: call.signature,
}
if (call.call_id) {
state.toolUseIdMap.set(call.call_id, call.toolUseId)
}
onProgress({ latestToolCall: formatToolCallPreview(toolName, toolCallParams) })
const subagentConfig = this.createSubagentTaskConfig(state)
const handler = this.baseConfig.coordinator.getHandler(toolName)
let toolResult: unknown
if (!handler) {
toolResult = formatResponse.toolError(`No handler registered for tool '${toolName}'.`)
} else {
try {
toolResult = await this.baseConfig.coordinator.execute(subagentConfig, toolCallBlock)
} catch (error) {
toolResult = formatResponse.toolError((error as Error).message)
}
}
stats.toolCalls += 1
onProgress({ stats: { ...stats } })
onProgress({ status: "completed", result: completionResult, stats: { ...stats } })
return { status: "completed", result: completionResult, stats }
const serializedToolResult = serializeToolResult(toolResult)
const toolDescription = handler?.getDescription(toolCallBlock) || `[${toolName}]`
pushSubagentToolResultBlock(toolResultBlocks, call, toolDescription, serializedToolResult)
}
if (!SUBAGENT_ALLOWED_TOOLS.includes(toolName)) {
const deniedResult = formatResponse.toolError(`Tool '${toolName}' is not available inside subagent runs.`)
pushSubagentToolResultBlock(toolResultBlocks, call, toolName, deniedResult)
continue
}
conversation.push({
role: "user",
content: toolResultBlocks,
})
const toolCallBlock: ToolUse = {
type: "tool_use",
name: toolName,
params: toolCallParams,
partial: false,
isNativeToolCall: call.isNativeToolCall,
call_id: call.call_id || call.toolUseId,
signature: call.signature,
}
await delay(0)
return { status: "continue", nextInput: undefined }
},
})
if (call.call_id) {
state.toolUseIdMap.set(call.call_id, call.toolUseId)
}
const latestToolCall = formatToolCallPreview(toolName, toolCallParams)
onProgress({ latestToolCall })
const subagentConfig = this.createSubagentTaskConfig(state)
const handler = this.baseConfig.coordinator.getHandler(toolName)
let toolResult: unknown
if (!handler) {
toolResult = formatResponse.toolError(`No handler registered for tool '${toolName}'.`)
} else {
try {
toolResult = await handler.execute(subagentConfig, toolCallBlock)
} catch (error) {
toolResult = formatResponse.toolError((error as Error).message)
}
}
stats.toolCalls += 1
onProgress({ stats: { ...stats } })
const serializedToolResult = serializeToolResult(toolResult)
const toolDescription = handler?.getDescription(toolCallBlock) || `[${toolName}]`
pushSubagentToolResultBlock(toolResultBlocks, call, toolDescription, serializedToolResult)
}
conversation.push({
role: "user",
content: toolResultBlocks,
})
await delay(0)
if (loopResult.status === "complete") {
onProgress({ status: "completed", result: loopResult.output.result, stats: { ...stats } })
return loopResult.output
}
if (loopResult.status === "failed") {
const error = loopResult.error || "Subagent execution failed."
onProgress({ status: "failed", error, stats: { ...stats } })
return { status: "failed", error, stats }
}
const cancelledError = "Subagent run cancelled."
onProgress({ status: "failed", error: cancelledError, stats: { ...stats } })
return { status: "failed", error: cancelledError, stats }
} catch (error) {
if (this.shouldAbort()) {
const cancelledError = "Subagent run cancelled."
@@ -673,19 +661,6 @@ export class SubagentRunner {
}
}
private shouldRetryInitialStreamError(error: unknown, providerId: string, modelId: string): boolean {
// Mirror main loop behavior: do not auto-retry auth/balance failures.
const parsedError = ClineError.transform(error, modelId, providerId)
const isAuthError = parsedError.isErrorType(ClineErrorType.Auth)
const isBalanceError = parsedError.isErrorType(ClineErrorType.Balance)
if (isAuthError || isBalanceError) {
return false
}
return true
}
private compactConversationForContextWindow(conversation: ClineStorageMessage[]): boolean {
const contextManager = new ContextManager()
const optimizationResult = this.optimizeConversationForContextWindow(contextManager, conversation)
@@ -729,25 +704,8 @@ export class SubagentRunner {
return { didOptimize: true, needToTruncate: optimizationResult.needToTruncate }
}
private shouldCompactBeforeNextRequest(
previousRequestTotalTokens: number,
api: ReturnType<typeof buildApiHandler>,
modelId: string,
): boolean {
const { contextWindow, maxAllowedSize } = getContextWindowInfo(api)
const useAutoCondense = this.baseConfig.services.stateManager.getGlobalSettingsKey("useAutoCondense")
if (useAutoCondense && isNextGenModelFamily(modelId)) {
const autoCondenseThreshold = 0.75
const roundedThreshold = autoCondenseThreshold ? Math.floor(contextWindow * autoCondenseThreshold) : maxAllowedSize
const thresholdTokens = Math.min(roundedThreshold, maxAllowedSize)
return previousRequestTotalTokens >= thresholdTokens
}
return previousRequestTotalTokens >= maxAllowedSize
}
private async *createMessageWithInitialChunkRetry(
api: ReturnType<typeof buildApiHandler>,
api: ApiHandler,
systemPrompt: string,
conversation: ClineStorageMessage[],
nativeTools: ClineTool[] | undefined,
@@ -755,7 +713,7 @@ export class SubagentRunner {
modelId: string,
) {
for (let attempt = 1; attempt <= MAX_INITIAL_STREAM_ATTEMPTS; attempt += 1) {
const stream = api.createMessage(systemPrompt, conversation, nativeTools)
const stream = this.coreAgent.createMessage(systemPrompt, conversation, nativeTools)
const iterator = stream[Symbol.asyncIterator]()
try {
@@ -781,7 +739,7 @@ export class SubagentRunner {
const shouldRetry =
!this.shouldAbort() &&
attempt < MAX_INITIAL_STREAM_ATTEMPTS &&
this.shouldRetryInitialStreamError(error, providerId, modelId)
this.coreAgent.classifyInitialStreamRetry(error, providerId, modelId).shouldRetry
if (!shouldRetry) {
throw error
}
+18
View File
@@ -25,6 +25,24 @@
"useUnknownInCatchVariables": false,
"baseUrl": ".",
"paths": {
"@cline/agents": [
"sdk-wip/packages/agents/dist/index.d.ts"
],
"@cline/agents/*": [
"sdk-wip/packages/agents/dist/*"
],
"@cline/llms": [
"sdk-wip/packages/llms/dist/index.d.ts"
],
"@cline/llms/*": [
"sdk-wip/packages/llms/dist/*"
],
"@cline/core": [
"sdk-wip/packages/core/dist/index.d.ts"
],
"@cline/core/*": [
"sdk-wip/packages/core/dist/*"
],
"@/*": [
"src/*"
],
+35 -12
View File
@@ -160,6 +160,7 @@
"version": "7.28.4",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
@@ -500,6 +501,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
},
@@ -521,6 +523,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18"
}
@@ -528,6 +531,7 @@
"node_modules/@emotion/is-prop-valid": {
"version": "1.2.2",
"license": "MIT",
"peer": true,
"dependencies": {
"@emotion/memoize": "^0.8.1"
}
@@ -1033,6 +1037,7 @@
"node_modules/@firebase/app": {
"version": "0.13.2",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@firebase/component": "0.6.18",
"@firebase/logger": "0.4.4",
@@ -1089,6 +1094,7 @@
"node_modules/@firebase/app-compat": {
"version": "0.4.2",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@firebase/app": "0.13.2",
"@firebase/component": "0.6.18",
@@ -1102,7 +1108,8 @@
},
"node_modules/@firebase/app-types": {
"version": "0.9.3",
"license": "Apache-2.0"
"license": "Apache-2.0",
"peer": true
},
"node_modules/@firebase/auth-compat": {
"version": "0.5.28",
@@ -1489,6 +1496,7 @@
"version": "1.12.1",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
},
@@ -2585,6 +2593,7 @@
"node_modules/@heroui/system": {
"version": "2.4.22",
"license": "MIT",
"peer": true,
"dependencies": {
"@heroui/react-utils": "2.1.13",
"@heroui/system-rsc": "2.3.19",
@@ -2669,6 +2678,7 @@
"node_modules/@heroui/theme": {
"version": "2.4.22",
"license": "MIT",
"peer": true,
"dependencies": {
"@heroui/shared-utils": "2.1.11",
"clsx": "^1.2.1",
@@ -6351,8 +6361,7 @@
"node_modules/@types/aria-query": {
"version": "5.0.4",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -6729,6 +6738,7 @@
"node_modules/@types/react": {
"version": "18.3.18",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -6738,6 +6748,7 @@
"version": "18.3.7",
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^18.0.0"
}
@@ -7129,6 +7140,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.3",
"caniuse-lite": "^1.0.30001741",
@@ -7258,6 +7270,7 @@
"node_modules/chevrotain": {
"version": "11.0.3",
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@chevrotain/cst-dts-gen": "11.0.3",
"@chevrotain/gast": "11.0.3",
@@ -7528,6 +7541,7 @@
"node_modules/cytoscape": {
"version": "3.33.1",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10"
}
@@ -7860,6 +7874,7 @@
"node_modules/d3-selection": {
"version": "3.0.0",
"license": "ISC",
"peer": true,
"engines": {
"node": ">=12"
}
@@ -8086,8 +8101,7 @@
"node_modules/dom-accessibility-api": {
"version": "0.5.16",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dompurify": {
"version": "3.2.7",
@@ -8150,6 +8164,7 @@
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"bin": {
"esbuild": "bin/esbuild"
},
@@ -8430,6 +8445,7 @@
"node_modules/framer-motion": {
"version": "12.23.18",
"license": "MIT",
"peer": true,
"dependencies": {
"motion-dom": "^12.23.18",
"motion-utils": "^12.23.6",
@@ -9372,6 +9388,7 @@
"version": "26.1.0",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
@@ -9842,7 +9859,6 @@
"version": "1.5.0",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -11745,7 +11761,6 @@
"version": "27.5.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -11759,7 +11774,6 @@
"version": "5.2.0",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -11822,6 +11836,7 @@
"node_modules/react": {
"version": "18.3.1",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -11860,6 +11875,7 @@
"node_modules/react-dom": {
"version": "18.3.1",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -11871,8 +11887,7 @@
"node_modules/react-is": {
"version": "17.0.2",
"dev": true,
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-markdown": {
"version": "10.1.0",
@@ -12536,6 +12551,7 @@
"version": "4.52.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -12859,6 +12875,7 @@
"version": "9.1.17",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@storybook/global": "^5.0.0",
"@testing-library/jest-dom": "^6.6.3",
@@ -13124,6 +13141,7 @@
"node_modules/tailwind-merge": {
"version": "3.3.1",
"license": "MIT",
"peer": true,
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -13148,7 +13166,8 @@
},
"node_modules/tailwindcss": {
"version": "4.1.13",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -13360,12 +13379,14 @@
},
"node_modules/tslib": {
"version": "2.8.1",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/typescript": {
"version": "5.9.2",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -13718,6 +13739,7 @@
"version": "7.2.2",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@@ -13839,6 +13861,7 @@
"version": "3.2.4",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/chai": "^5.2.2",
"@vitest/expect": "3.2.4",