@ai-sdk/harness-cline 0.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -0
- package/LICENSE +13 -0
- package/README.md +56 -3
- package/dist/index.d.ts +127 -0
- package/dist/index.js +1655 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -11
- package/src/cline-auth.ts +40 -0
- package/src/cline-harness.ts +197 -0
- package/src/cline-mcp.ts +153 -0
- package/src/cline-remote-ops.ts +243 -0
- package/src/cline-resume-state.ts +139 -0
- package/src/cline-session.ts +915 -0
- package/src/cline-skills.ts +59 -0
- package/src/cline-tools.ts +381 -0
- package/src/cline-translate.ts +261 -0
- package/src/cline-utils.ts +151 -0
- package/src/index.ts +13 -0
- package/src/version.ts +6 -0
- package/index.js +0 -1
|
@@ -0,0 +1,915 @@
|
|
|
1
|
+
import {
|
|
2
|
+
HarnessCapabilityUnsupportedError,
|
|
3
|
+
type HarnessV1BuiltinToolFiltering,
|
|
4
|
+
type HarnessV1ContinueTurnOptions,
|
|
5
|
+
type HarnessV1ContinueTurnState,
|
|
6
|
+
type HarnessV1NetworkSandboxSession,
|
|
7
|
+
type HarnessV1PermissionMode,
|
|
8
|
+
type HarnessV1PromptControl,
|
|
9
|
+
type HarnessV1PromptTurnOptions,
|
|
10
|
+
type HarnessV1ResumeSessionState,
|
|
11
|
+
type HarnessV1Session,
|
|
12
|
+
type HarnessV1Skill,
|
|
13
|
+
type HarnessV1StreamPart,
|
|
14
|
+
type HarnessV1ToolSpec,
|
|
15
|
+
} from '@ai-sdk/harness';
|
|
16
|
+
import { resolveSandboxHomeDir } from '@ai-sdk/harness/utils';
|
|
17
|
+
import {
|
|
18
|
+
Agent,
|
|
19
|
+
createTool,
|
|
20
|
+
type AgentMessage,
|
|
21
|
+
type AgentModel,
|
|
22
|
+
type AgentRunResult,
|
|
23
|
+
} from '@cline/agents';
|
|
24
|
+
import { Llms } from '@cline/core';
|
|
25
|
+
import { createClineMcpRuntime } from './cline-mcp';
|
|
26
|
+
import { createClineRemoteOps } from './cline-remote-ops';
|
|
27
|
+
import {
|
|
28
|
+
CLINE_DEFAULT_HISTORY_FILE_NAME,
|
|
29
|
+
persistHistoryToSandbox,
|
|
30
|
+
pullHistoryFromSandbox,
|
|
31
|
+
resolveClinePrivateSessionDirectory,
|
|
32
|
+
safeClineHistoryFileName,
|
|
33
|
+
} from './cline-resume-state';
|
|
34
|
+
import { renderSkillsPromptSection, writeClineSkills } from './cline-skills';
|
|
35
|
+
import {
|
|
36
|
+
buildBuiltinAgentTools,
|
|
37
|
+
buildUserAgentTools,
|
|
38
|
+
CLINE_NATIVE_TOOL_KINDS,
|
|
39
|
+
createClineToolResult,
|
|
40
|
+
isClineBuiltinToolName,
|
|
41
|
+
resolveActiveClineBuiltinNames,
|
|
42
|
+
type PendingToolResult,
|
|
43
|
+
unwrapClineToolResult,
|
|
44
|
+
} from './cline-tools';
|
|
45
|
+
import {
|
|
46
|
+
createClineTranslatorState,
|
|
47
|
+
finishClineTranslation,
|
|
48
|
+
toolApprovalParts,
|
|
49
|
+
translateClineEvent,
|
|
50
|
+
type ClineTranslatorState,
|
|
51
|
+
} from './cline-translate';
|
|
52
|
+
import {
|
|
53
|
+
extractUserText,
|
|
54
|
+
mapRunFinishReason,
|
|
55
|
+
usageFromAgentUsage,
|
|
56
|
+
} from './cline-utils';
|
|
57
|
+
|
|
58
|
+
const HARNESS_ID = 'cline';
|
|
59
|
+
|
|
60
|
+
export type ClineReasoningEffort =
|
|
61
|
+
| 'none'
|
|
62
|
+
| 'minimal'
|
|
63
|
+
| 'low'
|
|
64
|
+
| 'medium'
|
|
65
|
+
| 'high'
|
|
66
|
+
| 'xhigh'
|
|
67
|
+
| 'max';
|
|
68
|
+
|
|
69
|
+
/*
|
|
70
|
+
* The Cline runtime lives in this Node process, not behind an attachable
|
|
71
|
+
* in-sandbox bridge. During a tool approval pause the turn is still alive and
|
|
72
|
+
* blocked on a parked promise, so detach must keep that live session for the
|
|
73
|
+
* next same-process resume instead of stopping it and failing the promise.
|
|
74
|
+
* Cross-process resume falls back to the history file persisted in the
|
|
75
|
+
* sandbox.
|
|
76
|
+
*/
|
|
77
|
+
const parkedClineSessions = new Map<string, HarnessV1Session>();
|
|
78
|
+
|
|
79
|
+
export interface ClineSessionSettings {
|
|
80
|
+
readonly authEnv: Record<string, string>;
|
|
81
|
+
readonly mcpServers?: Record<string, unknown>;
|
|
82
|
+
readonly providerId?: string;
|
|
83
|
+
readonly modelId?: string;
|
|
84
|
+
readonly apiKey?: string;
|
|
85
|
+
readonly baseUrl?: string;
|
|
86
|
+
readonly headers?: Record<string, string>;
|
|
87
|
+
readonly reasoningEffort?: ClineReasoningEffort;
|
|
88
|
+
readonly maxIterations?: number;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface CreateClineSessionInput {
|
|
92
|
+
readonly sessionId: string;
|
|
93
|
+
readonly sandboxSession: HarnessV1NetworkSandboxSession;
|
|
94
|
+
readonly sessionWorkDir: string;
|
|
95
|
+
readonly skills: ReadonlyArray<HarnessV1Skill>;
|
|
96
|
+
readonly settings: ClineSessionSettings;
|
|
97
|
+
readonly clientApp: string;
|
|
98
|
+
readonly isResume: boolean;
|
|
99
|
+
readonly permissionMode?: HarnessV1PermissionMode;
|
|
100
|
+
readonly builtinToolFiltering?: HarnessV1BuiltinToolFiltering;
|
|
101
|
+
readonly resumeHistoryFileName?: string;
|
|
102
|
+
readonly abortSignal?: AbortSignal;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
interface PendingToolApproval {
|
|
106
|
+
resolve: (value: { approved: boolean; reason?: string }) => void;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
interface ActiveClineTurn {
|
|
110
|
+
readonly token: object;
|
|
111
|
+
readonly done: Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Whether a thrown error is an abort — the expected result of
|
|
116
|
+
* `doSuspendTurn` aborting the in-flight turn. Only these are safe to swallow
|
|
117
|
+
* while `suspending`; any other error must surface as an `error` chunk.
|
|
118
|
+
*/
|
|
119
|
+
function isAbortError(value: unknown): boolean {
|
|
120
|
+
if (value == null) return false;
|
|
121
|
+
const name = (value as { name?: unknown }).name;
|
|
122
|
+
if (name === 'AbortError' || name === 'AgentRuntimeAbortError') {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
const text =
|
|
126
|
+
typeof value === 'string'
|
|
127
|
+
? value
|
|
128
|
+
: value instanceof Error
|
|
129
|
+
? value.message
|
|
130
|
+
: String(value);
|
|
131
|
+
return /\baborted\b|AbortError/i.test(text);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function clineBuiltinToolRequiresApproval(input: {
|
|
135
|
+
permissionMode: HarnessV1PermissionMode;
|
|
136
|
+
kind: 'readonly' | 'edit' | 'bash';
|
|
137
|
+
}): boolean {
|
|
138
|
+
if (input.permissionMode === 'allow-all') return false;
|
|
139
|
+
if (input.permissionMode === 'allow-edits') return input.kind === 'bash';
|
|
140
|
+
return input.kind === 'edit' || input.kind === 'bash';
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildSystemPrompt(input: {
|
|
144
|
+
sessionWorkDir: string;
|
|
145
|
+
sandboxDescription: string;
|
|
146
|
+
skillsSection?: string;
|
|
147
|
+
}): string {
|
|
148
|
+
const sections = [
|
|
149
|
+
'You are Cline, an autonomous coding agent operating inside a sandboxed workspace.',
|
|
150
|
+
`## Workspace\n\nThe workspace root is \`${input.sessionWorkDir}\`. All relative file paths and shell commands resolve against it. Use the provided tools (read, write, edit, bash, grep, glob, ls) to inspect and modify the workspace.`,
|
|
151
|
+
`## Sandbox\n\n${input.sandboxDescription}`,
|
|
152
|
+
];
|
|
153
|
+
if (input.skillsSection) {
|
|
154
|
+
sections.push(input.skillsSection);
|
|
155
|
+
}
|
|
156
|
+
return sections.join('\n\n');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function toClineBackendProviderBaseUrl(baseUrl: string): string {
|
|
160
|
+
return `${baseUrl.replace(/\/+$/, '')}/api/v1`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function toAiGatewayProviderBaseUrl(baseUrl: string): string {
|
|
164
|
+
const trimmed = baseUrl.replace(/\/+$/, '');
|
|
165
|
+
return trimmed.endsWith('/v1') ? trimmed : `${trimmed}/v1`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function createClineAgentModel({
|
|
169
|
+
settings,
|
|
170
|
+
clientApp,
|
|
171
|
+
}: {
|
|
172
|
+
settings: ClineSessionSettings;
|
|
173
|
+
clientApp: string;
|
|
174
|
+
}): { model: AgentModel; providerId: string } {
|
|
175
|
+
const gatewayBaseUrl = settings.authEnv.AI_GATEWAY_BASE_URL;
|
|
176
|
+
const isAiGateway = gatewayBaseUrl != null;
|
|
177
|
+
const providerId = isAiGateway ? 'cline' : (settings.providerId ?? 'cline');
|
|
178
|
+
const apiKey = isAiGateway
|
|
179
|
+
? settings.authEnv.AI_GATEWAY_API_KEY
|
|
180
|
+
: (settings.apiKey ??
|
|
181
|
+
(providerId === 'cline' ? settings.authEnv.CLINE_API_KEY : undefined));
|
|
182
|
+
const baseUrl = isAiGateway
|
|
183
|
+
? toAiGatewayProviderBaseUrl(gatewayBaseUrl)
|
|
184
|
+
: (settings.baseUrl ??
|
|
185
|
+
(providerId === 'cline' && settings.authEnv.CLINE_API_BASE_URL
|
|
186
|
+
? toClineBackendProviderBaseUrl(settings.authEnv.CLINE_API_BASE_URL)
|
|
187
|
+
: undefined));
|
|
188
|
+
const headers = isAiGateway
|
|
189
|
+
? {
|
|
190
|
+
...settings.headers,
|
|
191
|
+
'User-Agent': clientApp,
|
|
192
|
+
'x-client-app': clientApp,
|
|
193
|
+
}
|
|
194
|
+
: settings.headers;
|
|
195
|
+
const gateway = Llms.createGateway({
|
|
196
|
+
providerConfigs: [
|
|
197
|
+
{
|
|
198
|
+
providerId,
|
|
199
|
+
...(apiKey ? { apiKey } : {}),
|
|
200
|
+
...(baseUrl ? { baseUrl } : {}),
|
|
201
|
+
...(headers ? { headers } : {}),
|
|
202
|
+
...(isAiGateway ? { apiKeyEnv: [] } : {}),
|
|
203
|
+
},
|
|
204
|
+
],
|
|
205
|
+
});
|
|
206
|
+
const modelSelection = {
|
|
207
|
+
providerId,
|
|
208
|
+
...(settings.modelId ? { modelId: settings.modelId } : {}),
|
|
209
|
+
};
|
|
210
|
+
const modelOptions =
|
|
211
|
+
settings.reasoningEffort === 'none'
|
|
212
|
+
? { reasoning: { enabled: false } }
|
|
213
|
+
: {
|
|
214
|
+
reasoning: {
|
|
215
|
+
enabled: true,
|
|
216
|
+
effort: settings.reasoningEffort,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
const model =
|
|
220
|
+
settings.reasoningEffort === undefined
|
|
221
|
+
? gateway.createAgentModel(modelSelection)
|
|
222
|
+
: gateway.createAgentModel(modelSelection, modelOptions);
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
model,
|
|
226
|
+
providerId,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export async function createClineSession(
|
|
231
|
+
input: CreateClineSessionInput,
|
|
232
|
+
): Promise<HarnessV1Session> {
|
|
233
|
+
if (input.isResume) {
|
|
234
|
+
const parkedSession = parkedClineSessions.get(input.sessionId);
|
|
235
|
+
if (parkedSession) {
|
|
236
|
+
parkedClineSessions.delete(input.sessionId);
|
|
237
|
+
return {
|
|
238
|
+
...parkedSession,
|
|
239
|
+
isResume: true,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const sandbox = input.sandboxSession.restricted();
|
|
245
|
+
const sandboxHomeDir = await resolveSandboxHomeDir({
|
|
246
|
+
sandbox,
|
|
247
|
+
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
248
|
+
});
|
|
249
|
+
const privateSessionDir = resolveClinePrivateSessionDirectory({
|
|
250
|
+
sandboxHomeDir,
|
|
251
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
252
|
+
sessionId: input.sessionId,
|
|
253
|
+
});
|
|
254
|
+
const permissionMode = input.permissionMode ?? 'allow-all';
|
|
255
|
+
const activeBuiltinNames = resolveActiveClineBuiltinNames(
|
|
256
|
+
input.builtinToolFiltering,
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
const ops = createClineRemoteOps({
|
|
260
|
+
sandbox,
|
|
261
|
+
workDir: input.sessionWorkDir,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
// Materialize harness-provided skills into sandbox HOME (not the workspace)
|
|
265
|
+
// and advertise them via a system prompt section.
|
|
266
|
+
let skillsSection: string | undefined;
|
|
267
|
+
if (input.skills.length > 0) {
|
|
268
|
+
const skillRootDir = await writeClineSkills({
|
|
269
|
+
sandbox,
|
|
270
|
+
sandboxHomeDir,
|
|
271
|
+
skills: input.skills,
|
|
272
|
+
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
273
|
+
});
|
|
274
|
+
if (skillRootDir) {
|
|
275
|
+
skillsSection = renderSkillsPromptSection(input.skills, skillRootDir);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const baseSystemPrompt = buildSystemPrompt({
|
|
280
|
+
sessionWorkDir: input.sessionWorkDir,
|
|
281
|
+
sandboxDescription: sandbox.description,
|
|
282
|
+
...(skillsSection ? { skillsSection } : {}),
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// On resume: pull the persisted conversation history from the sandbox so
|
|
286
|
+
// the fresh runtime instance starts from the prior transcript.
|
|
287
|
+
let currentMessages: readonly AgentMessage[] = [];
|
|
288
|
+
if (input.isResume && input.resumeHistoryFileName) {
|
|
289
|
+
const restored = await pullHistoryFromSandbox({
|
|
290
|
+
sandbox,
|
|
291
|
+
privateSessionDir,
|
|
292
|
+
historyFileName: safeClineHistoryFileName(input.resumeHistoryFileName),
|
|
293
|
+
...(input.abortSignal ? { abortSignal: input.abortSignal } : {}),
|
|
294
|
+
});
|
|
295
|
+
if (restored) {
|
|
296
|
+
currentMessages = restored;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
const mcpRuntime = await createClineMcpRuntime({
|
|
300
|
+
mcpServers: input.settings.mcpServers,
|
|
301
|
+
});
|
|
302
|
+
const { model, providerId } = createClineAgentModel({
|
|
303
|
+
settings: input.settings,
|
|
304
|
+
clientApp: input.clientApp,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Per-session mutable state we hold across prompts.
|
|
308
|
+
let agent: Agent | undefined;
|
|
309
|
+
let unsubscribe: (() => void) | undefined;
|
|
310
|
+
let lastToolsSignature: string | undefined;
|
|
311
|
+
let agentHasRun = false;
|
|
312
|
+
let stopped = false;
|
|
313
|
+
/*
|
|
314
|
+
* Set by `doSuspendTurn` before it aborts the in-flight turn at a slice
|
|
315
|
+
* boundary. The turn settles silently when this is set, so the stream
|
|
316
|
+
* closes cleanly (no spurious `error` chunk) — the next slice
|
|
317
|
+
* rerun-continues from the persisted history.
|
|
318
|
+
*/
|
|
319
|
+
let suspending = false;
|
|
320
|
+
const pendingToolResults = new Map<string, PendingToolResult>();
|
|
321
|
+
const pendingToolApprovals = new Map<string, PendingToolApproval>();
|
|
322
|
+
const pendingUserMessages: string[] = [];
|
|
323
|
+
|
|
324
|
+
// Emit channel set at the start of every turn and cleared on end.
|
|
325
|
+
let currentEmit: ((part: HarnessV1StreamPart) => void) | undefined;
|
|
326
|
+
let translatorState: ClineTranslatorState | undefined;
|
|
327
|
+
let activeTurn: ActiveClineTurn | undefined;
|
|
328
|
+
|
|
329
|
+
function settlePendingToolResults(reason: string): void {
|
|
330
|
+
for (const pending of pendingToolResults.values()) {
|
|
331
|
+
pending.resolve(
|
|
332
|
+
createClineToolResult({
|
|
333
|
+
output: { error: reason },
|
|
334
|
+
isError: true,
|
|
335
|
+
}),
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
pendingToolResults.clear();
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function settlePendingToolApprovals(reason: string): void {
|
|
342
|
+
for (const pending of pendingToolApprovals.values()) {
|
|
343
|
+
pending.resolve({ approved: false, reason });
|
|
344
|
+
}
|
|
345
|
+
pendingToolApprovals.clear();
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function persistHistory(): Promise<void> {
|
|
349
|
+
const messages = agent?.snapshot().messages ?? currentMessages;
|
|
350
|
+
await persistHistoryToSandbox({
|
|
351
|
+
sandbox,
|
|
352
|
+
privateSessionDir,
|
|
353
|
+
historyFileName: CLINE_DEFAULT_HISTORY_FILE_NAME,
|
|
354
|
+
messages,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function requestBuiltinToolApproval(toolCall: {
|
|
359
|
+
toolCallId: string;
|
|
360
|
+
toolName: string;
|
|
361
|
+
input: unknown;
|
|
362
|
+
}): Promise<{ approved: boolean; reason?: string }> {
|
|
363
|
+
if (translatorState && currentEmit) {
|
|
364
|
+
for (const part of toolApprovalParts(translatorState, toolCall)) {
|
|
365
|
+
currentEmit(part);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return new Promise(resolve => {
|
|
369
|
+
pendingToolApprovals.set(toolCall.toolCallId, { resolve });
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function rebuildAgent(rebuildInput: {
|
|
374
|
+
userTools: ReadonlyArray<HarnessV1ToolSpec>;
|
|
375
|
+
instructions?: string;
|
|
376
|
+
responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
|
|
377
|
+
}): void {
|
|
378
|
+
unsubscribe?.();
|
|
379
|
+
unsubscribe = undefined;
|
|
380
|
+
agent = new Agent({
|
|
381
|
+
model,
|
|
382
|
+
...(input.settings.modelId
|
|
383
|
+
? {
|
|
384
|
+
messageModelInfo: {
|
|
385
|
+
id: input.settings.modelId,
|
|
386
|
+
provider: providerId,
|
|
387
|
+
},
|
|
388
|
+
}
|
|
389
|
+
: {}),
|
|
390
|
+
sessionId: input.sessionId,
|
|
391
|
+
systemPrompt: rebuildInput.instructions
|
|
392
|
+
? `${baseSystemPrompt}\n\n${rebuildInput.instructions}`
|
|
393
|
+
: baseSystemPrompt,
|
|
394
|
+
initialMessages: currentMessages,
|
|
395
|
+
toolExecution: 'parallel',
|
|
396
|
+
...(input.settings.maxIterations !== undefined
|
|
397
|
+
? { maxIterations: input.settings.maxIterations }
|
|
398
|
+
: {}),
|
|
399
|
+
tools: [
|
|
400
|
+
...buildBuiltinAgentTools({ ops, activeNames: activeBuiltinNames }),
|
|
401
|
+
...mcpRuntime.tools,
|
|
402
|
+
...buildUserAgentTools({
|
|
403
|
+
specs: rebuildInput.userTools,
|
|
404
|
+
pendingToolResults,
|
|
405
|
+
}),
|
|
406
|
+
...(rebuildInput.responseFormat?.type === 'json' &&
|
|
407
|
+
rebuildInput.responseFormat.schema != null
|
|
408
|
+
? [
|
|
409
|
+
createTool({
|
|
410
|
+
name: 'structured_output',
|
|
411
|
+
description:
|
|
412
|
+
rebuildInput.responseFormat.description ??
|
|
413
|
+
'Return the final structured output.',
|
|
414
|
+
inputSchema: {
|
|
415
|
+
type: 'object',
|
|
416
|
+
properties: {
|
|
417
|
+
output: rebuildInput.responseFormat.schema,
|
|
418
|
+
},
|
|
419
|
+
required: ['output'],
|
|
420
|
+
additionalProperties: false,
|
|
421
|
+
},
|
|
422
|
+
lifecycle: { completesRun: true },
|
|
423
|
+
execute: async ({ output }: { output: unknown }) =>
|
|
424
|
+
JSON.stringify(output),
|
|
425
|
+
}),
|
|
426
|
+
]
|
|
427
|
+
: []),
|
|
428
|
+
],
|
|
429
|
+
...(rebuildInput.responseFormat?.type === 'json'
|
|
430
|
+
? { completionPolicy: { requireCompletionTool: true } }
|
|
431
|
+
: {}),
|
|
432
|
+
hooks: {
|
|
433
|
+
afterTool: ({ result }) => {
|
|
434
|
+
const toolResult = unwrapClineToolResult(result.output);
|
|
435
|
+
return toolResult == null ? undefined : { result: toolResult };
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
// Built-in tools whose kind requires approval under the session's
|
|
439
|
+
// permission mode are marked `autoApprove: false`; the runtime then
|
|
440
|
+
// routes them through `requestToolApproval` below. User tools are
|
|
441
|
+
// approved by the harness framework, never by the adapter.
|
|
442
|
+
toolPolicies: Object.fromEntries(
|
|
443
|
+
activeBuiltinNames
|
|
444
|
+
.filter(name =>
|
|
445
|
+
clineBuiltinToolRequiresApproval({
|
|
446
|
+
permissionMode,
|
|
447
|
+
kind: CLINE_NATIVE_TOOL_KINDS[name],
|
|
448
|
+
}),
|
|
449
|
+
)
|
|
450
|
+
.map(name => [name, { autoApprove: false }]),
|
|
451
|
+
),
|
|
452
|
+
requestToolApproval: request => {
|
|
453
|
+
if (!isClineBuiltinToolName(request.toolName)) {
|
|
454
|
+
// Custom host-executed tool approvals are handled by the framework
|
|
455
|
+
// before results are submitted back; the adapter never gates them.
|
|
456
|
+
return { approved: true };
|
|
457
|
+
}
|
|
458
|
+
return requestBuiltinToolApproval({
|
|
459
|
+
toolCallId: request.toolCallId,
|
|
460
|
+
toolName: request.toolName,
|
|
461
|
+
input: request.input,
|
|
462
|
+
});
|
|
463
|
+
},
|
|
464
|
+
// Mid-turn user messages injected via `submitUserMessage` are consumed
|
|
465
|
+
// by the runtime between loop iterations, before the next model call.
|
|
466
|
+
consumePendingUserMessage: () => pendingUserMessages.shift(),
|
|
467
|
+
});
|
|
468
|
+
agentHasRun = false;
|
|
469
|
+
|
|
470
|
+
unsubscribe = agent.subscribe(event => {
|
|
471
|
+
if (!translatorState || !currentEmit) return;
|
|
472
|
+
for (const part of translateClineEvent(event, translatorState)) {
|
|
473
|
+
currentEmit(part);
|
|
474
|
+
}
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function createPromptControl(controlInput: {
|
|
479
|
+
done: Promise<void>;
|
|
480
|
+
abortSignal?: AbortSignal;
|
|
481
|
+
}): HarnessV1PromptControl {
|
|
482
|
+
const abortHandler = () => {
|
|
483
|
+
// Settle parked host-tool promises first so the runtime's tool
|
|
484
|
+
// executions do not dangle, then abort the loop itself.
|
|
485
|
+
settlePendingToolResults('Turn aborted');
|
|
486
|
+
settlePendingToolApprovals('Turn aborted');
|
|
487
|
+
agent?.abort('Turn aborted by caller');
|
|
488
|
+
};
|
|
489
|
+
if (controlInput.abortSignal) {
|
|
490
|
+
controlInput.abortSignal.addEventListener('abort', abortHandler, {
|
|
491
|
+
once: true,
|
|
492
|
+
});
|
|
493
|
+
void controlInput.done.then(
|
|
494
|
+
() => {
|
|
495
|
+
controlInput.abortSignal?.removeEventListener('abort', abortHandler);
|
|
496
|
+
},
|
|
497
|
+
() => {
|
|
498
|
+
controlInput.abortSignal?.removeEventListener('abort', abortHandler);
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return {
|
|
504
|
+
async submitToolResult(args) {
|
|
505
|
+
const pending = pendingToolResults.get(args.toolCallId);
|
|
506
|
+
if (!pending) return;
|
|
507
|
+
pendingToolResults.delete(args.toolCallId);
|
|
508
|
+
pending.resolve(
|
|
509
|
+
createClineToolResult({
|
|
510
|
+
output: args.output,
|
|
511
|
+
isError: args.isError,
|
|
512
|
+
}),
|
|
513
|
+
);
|
|
514
|
+
},
|
|
515
|
+
async submitToolApproval(args) {
|
|
516
|
+
const pending = pendingToolApprovals.get(args.approvalId);
|
|
517
|
+
if (!pending) return;
|
|
518
|
+
pendingToolApprovals.delete(args.approvalId);
|
|
519
|
+
pending.resolve({
|
|
520
|
+
approved: args.approved,
|
|
521
|
+
...(args.reason !== undefined ? { reason: args.reason } : {}),
|
|
522
|
+
});
|
|
523
|
+
},
|
|
524
|
+
async submitUserMessage(text) {
|
|
525
|
+
pendingUserMessages.push(text);
|
|
526
|
+
},
|
|
527
|
+
done: controlInput.done,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/*
|
|
532
|
+
* Drive one turn against the Cline runtime and return the control surface.
|
|
533
|
+
* Shared by `doPromptTurn` (a fresh user prompt) and `doContinueTurn`
|
|
534
|
+
* (no prompt — the runtime continues its own thread after a rerun resume).
|
|
535
|
+
*/
|
|
536
|
+
function runTurn(turnOpts: {
|
|
537
|
+
text: string | undefined;
|
|
538
|
+
tools: ReadonlyArray<HarnessV1ToolSpec>;
|
|
539
|
+
instructions?: string;
|
|
540
|
+
emit: (part: HarnessV1StreamPart) => void;
|
|
541
|
+
abortSignal?: AbortSignal;
|
|
542
|
+
responseFormat?: HarnessV1PromptTurnOptions['responseFormat'];
|
|
543
|
+
}): HarnessV1PromptControl {
|
|
544
|
+
if (stopped) {
|
|
545
|
+
throw new Error('Cline session has been stopped.');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const userTools = turnOpts.tools;
|
|
549
|
+
if (
|
|
550
|
+
turnOpts.responseFormat?.type === 'json' &&
|
|
551
|
+
turnOpts.responseFormat.schema == null
|
|
552
|
+
) {
|
|
553
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
554
|
+
message:
|
|
555
|
+
"Harness 'cline' requires a JSON schema for structured output.",
|
|
556
|
+
harnessId: HARNESS_ID,
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
if (
|
|
560
|
+
turnOpts.responseFormat?.type === 'json' &&
|
|
561
|
+
providerId === 'openai-codex-cli'
|
|
562
|
+
) {
|
|
563
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
564
|
+
message:
|
|
565
|
+
"Harness 'cline' cannot require structured output with the openai-codex-cli provider because that provider does not expose external tools.",
|
|
566
|
+
harnessId: HARNESS_ID,
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const signature = JSON.stringify({
|
|
571
|
+
tools: userTools.map(t => t.name).sort(),
|
|
572
|
+
responseFormat: turnOpts.responseFormat,
|
|
573
|
+
});
|
|
574
|
+
if (agent == null || signature !== lastToolsSignature) {
|
|
575
|
+
currentMessages = agent?.snapshot().messages ?? currentMessages;
|
|
576
|
+
rebuildAgent({
|
|
577
|
+
userTools,
|
|
578
|
+
responseFormat: turnOpts.responseFormat,
|
|
579
|
+
...(turnOpts.instructions
|
|
580
|
+
? { instructions: turnOpts.instructions }
|
|
581
|
+
: {}),
|
|
582
|
+
});
|
|
583
|
+
lastToolsSignature = signature;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
currentEmit = turnOpts.emit;
|
|
587
|
+
translatorState = createClineTranslatorState({
|
|
588
|
+
builtinToolNames: activeBuiltinNames,
|
|
589
|
+
hostToolNames: userTools.map(tool => tool.name),
|
|
590
|
+
ignoredToolNames:
|
|
591
|
+
turnOpts.responseFormat?.type === 'json' ? ['structured_output'] : [],
|
|
592
|
+
mcpToolNames: mcpRuntime.toolNames,
|
|
593
|
+
});
|
|
594
|
+
|
|
595
|
+
turnOpts.emit({
|
|
596
|
+
type: 'stream-start',
|
|
597
|
+
...(input.settings.modelId ? { modelId: input.settings.modelId } : {}),
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
const turnPromise = (async () => {
|
|
601
|
+
const runtime = agent!;
|
|
602
|
+
let result: AgentRunResult;
|
|
603
|
+
try {
|
|
604
|
+
// `text` is undefined only on rerun-continue: `continue()` without
|
|
605
|
+
// input re-drives the loop from the restored transcript instead of
|
|
606
|
+
// pushing an empty user message.
|
|
607
|
+
result =
|
|
608
|
+
turnOpts.text === undefined
|
|
609
|
+
? await runtime.continue()
|
|
610
|
+
: agentHasRun
|
|
611
|
+
? await runtime.continue(turnOpts.text)
|
|
612
|
+
: await runtime.run(turnOpts.text);
|
|
613
|
+
agentHasRun = true;
|
|
614
|
+
} catch (error) {
|
|
615
|
+
// `run` resolves for aborted/failed statuses; a throw here is
|
|
616
|
+
// unanticipated (e.g. config error). Swallow only the abort our own
|
|
617
|
+
// suspend caused; surface anything else.
|
|
618
|
+
if (suspending && isAbortError(error)) return;
|
|
619
|
+
currentEmit?.({ type: 'error', error });
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
currentMessages = result.messages;
|
|
624
|
+
|
|
625
|
+
if (translatorState) {
|
|
626
|
+
for (const part of finishClineTranslation(translatorState)) {
|
|
627
|
+
currentEmit?.(part);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (
|
|
632
|
+
turnOpts.responseFormat?.type === 'json' &&
|
|
633
|
+
result.status === 'completed'
|
|
634
|
+
) {
|
|
635
|
+
const id = `structured-output-${input.sessionId}`;
|
|
636
|
+
currentEmit?.({ type: 'text-start', id });
|
|
637
|
+
currentEmit?.({
|
|
638
|
+
type: 'text-delta',
|
|
639
|
+
id,
|
|
640
|
+
delta: result.outputText,
|
|
641
|
+
});
|
|
642
|
+
currentEmit?.({ type: 'text-end', id });
|
|
643
|
+
currentEmit?.({
|
|
644
|
+
type: 'finish-step',
|
|
645
|
+
finishReason: { unified: 'stop', raw: 'structured-output' },
|
|
646
|
+
usage: {
|
|
647
|
+
inputTokens: {
|
|
648
|
+
total: 0,
|
|
649
|
+
noCache: undefined,
|
|
650
|
+
cacheRead: undefined,
|
|
651
|
+
cacheWrite: undefined,
|
|
652
|
+
},
|
|
653
|
+
outputTokens: {
|
|
654
|
+
total: 0,
|
|
655
|
+
text: undefined,
|
|
656
|
+
reasoning: undefined,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (result.status === 'aborted') {
|
|
663
|
+
/*
|
|
664
|
+
* A `doSuspendTurn` aborts the in-flight turn on purpose — settle
|
|
665
|
+
* silently so the stream closes cleanly; the next slice
|
|
666
|
+
* rerun-continues from the persisted history. Caller-driven aborts
|
|
667
|
+
* finish the turn with an `other`/aborted reason.
|
|
668
|
+
*/
|
|
669
|
+
if (suspending) return;
|
|
670
|
+
currentEmit?.({
|
|
671
|
+
type: 'finish',
|
|
672
|
+
finishReason: mapRunFinishReason(result.status),
|
|
673
|
+
totalUsage: usageFromAgentUsage(result.usage),
|
|
674
|
+
});
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
if (result.status === 'failed') {
|
|
679
|
+
currentEmit?.({
|
|
680
|
+
type: 'error',
|
|
681
|
+
error: result.error ?? new Error('Cline agent run failed'),
|
|
682
|
+
});
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
currentEmit?.({
|
|
687
|
+
type: 'finish',
|
|
688
|
+
finishReason: mapRunFinishReason(result.status),
|
|
689
|
+
totalUsage: usageFromAgentUsage(result.usage),
|
|
690
|
+
});
|
|
691
|
+
})();
|
|
692
|
+
|
|
693
|
+
const activeTurnToken = {};
|
|
694
|
+
const done = turnPromise.finally(() => {
|
|
695
|
+
if (activeTurn?.token === activeTurnToken) {
|
|
696
|
+
activeTurn = undefined;
|
|
697
|
+
}
|
|
698
|
+
currentEmit = undefined;
|
|
699
|
+
});
|
|
700
|
+
activeTurn = {
|
|
701
|
+
token: activeTurnToken,
|
|
702
|
+
done,
|
|
703
|
+
};
|
|
704
|
+
|
|
705
|
+
return createPromptControl({
|
|
706
|
+
done,
|
|
707
|
+
...(turnOpts.abortSignal ? { abortSignal: turnOpts.abortSignal } : {}),
|
|
708
|
+
});
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
async function teardown(): Promise<void> {
|
|
712
|
+
unsubscribe?.();
|
|
713
|
+
unsubscribe = undefined;
|
|
714
|
+
agent = undefined;
|
|
715
|
+
await mcpRuntime.dispose();
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const doStop = async (): Promise<HarnessV1ResumeSessionState> => {
|
|
719
|
+
if (stopped) {
|
|
720
|
+
throw new Error('Cline session has been stopped.');
|
|
721
|
+
}
|
|
722
|
+
stopped = true;
|
|
723
|
+
parkedClineSessions.delete(input.sessionId);
|
|
724
|
+
settlePendingToolResults('Cline session stopped');
|
|
725
|
+
settlePendingToolApprovals('Cline session stopped');
|
|
726
|
+
|
|
727
|
+
// Persist the conversation into the sandbox so a future process can pick
|
|
728
|
+
// it up after the sandbox provider reattaches.
|
|
729
|
+
try {
|
|
730
|
+
await persistHistory();
|
|
731
|
+
} catch {
|
|
732
|
+
// Best-effort: a missing history file means resume returns to a fresh
|
|
733
|
+
// conversation rather than failing stop.
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
agent?.abort('Cline session stopped');
|
|
737
|
+
await teardown();
|
|
738
|
+
|
|
739
|
+
return {
|
|
740
|
+
type: 'resume-session',
|
|
741
|
+
harnessId: HARNESS_ID,
|
|
742
|
+
specificationVersion: 'harness-v1',
|
|
743
|
+
data: { historyFileName: CLINE_DEFAULT_HISTORY_FILE_NAME },
|
|
744
|
+
};
|
|
745
|
+
};
|
|
746
|
+
|
|
747
|
+
const sessionImpl: HarnessV1Session = {
|
|
748
|
+
sessionId: input.sessionId,
|
|
749
|
+
isResume: input.isResume,
|
|
750
|
+
...(input.settings.modelId ? { modelId: input.settings.modelId } : {}),
|
|
751
|
+
|
|
752
|
+
// The Cline runtime has no bridge to attach to and no in-sandbox event
|
|
753
|
+
// log to replay; its only cross-process resume path is restoring the
|
|
754
|
+
// persisted history on a fresh runtime, i.e. `rerun`.
|
|
755
|
+
|
|
756
|
+
doPromptTurn: async (
|
|
757
|
+
promptOpts: HarnessV1PromptTurnOptions,
|
|
758
|
+
): Promise<HarnessV1PromptControl> => {
|
|
759
|
+
return runTurn({
|
|
760
|
+
text: extractUserText(promptOpts.prompt),
|
|
761
|
+
tools: promptOpts.tools ?? [],
|
|
762
|
+
...(promptOpts.instructions
|
|
763
|
+
? { instructions: promptOpts.instructions }
|
|
764
|
+
: {}),
|
|
765
|
+
emit: promptOpts.emit,
|
|
766
|
+
...(promptOpts.abortSignal
|
|
767
|
+
? { abortSignal: promptOpts.abortSignal }
|
|
768
|
+
: {}),
|
|
769
|
+
responseFormat: promptOpts.responseFormat,
|
|
770
|
+
});
|
|
771
|
+
},
|
|
772
|
+
|
|
773
|
+
doContinueTurn: async (
|
|
774
|
+
continueOpts: HarnessV1ContinueTurnOptions,
|
|
775
|
+
): Promise<HarnessV1PromptControl> => {
|
|
776
|
+
if (activeTurn != null) {
|
|
777
|
+
currentEmit = continueOpts.emit;
|
|
778
|
+
return createPromptControl({
|
|
779
|
+
done: activeTurn.done,
|
|
780
|
+
...(continueOpts.abortSignal
|
|
781
|
+
? { abortSignal: continueOpts.abortSignal }
|
|
782
|
+
: {}),
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/*
|
|
787
|
+
* The model runs on the host, so there is no live turn to attach to —
|
|
788
|
+
* the previous slice's turn died with its process. Rerun-continue:
|
|
789
|
+
* re-drive the runtime from the history restored on resume. Lossy —
|
|
790
|
+
* work in flight at the slice boundary is recomputed, because a
|
|
791
|
+
* host-resident runtime cannot do a lossless attach.
|
|
792
|
+
*/
|
|
793
|
+
return runTurn({
|
|
794
|
+
text: undefined,
|
|
795
|
+
tools: continueOpts.tools ?? [],
|
|
796
|
+
...(continueOpts.instructions
|
|
797
|
+
? { instructions: continueOpts.instructions }
|
|
798
|
+
: {}),
|
|
799
|
+
emit: continueOpts.emit,
|
|
800
|
+
...(continueOpts.abortSignal
|
|
801
|
+
? { abortSignal: continueOpts.abortSignal }
|
|
802
|
+
: {}),
|
|
803
|
+
responseFormat: continueOpts.responseFormat,
|
|
804
|
+
});
|
|
805
|
+
},
|
|
806
|
+
|
|
807
|
+
doCompact: async () => {
|
|
808
|
+
throw new HarnessCapabilityUnsupportedError({
|
|
809
|
+
message:
|
|
810
|
+
'cline: manual compaction is not supported by the standalone Cline runtime.',
|
|
811
|
+
harnessId: HARNESS_ID,
|
|
812
|
+
});
|
|
813
|
+
},
|
|
814
|
+
|
|
815
|
+
doDestroy: async () => {
|
|
816
|
+
if (stopped) return;
|
|
817
|
+
stopped = true;
|
|
818
|
+
parkedClineSessions.delete(input.sessionId);
|
|
819
|
+
settlePendingToolResults('Cline session stopped');
|
|
820
|
+
settlePendingToolApprovals('Cline session stopped');
|
|
821
|
+
agent?.abort('Cline session destroyed');
|
|
822
|
+
await teardown();
|
|
823
|
+
},
|
|
824
|
+
|
|
825
|
+
doStop,
|
|
826
|
+
|
|
827
|
+
doDetach: async (): Promise<HarnessV1ResumeSessionState> => {
|
|
828
|
+
if (activeTurn != null || pendingToolResults.size > 0) {
|
|
829
|
+
parkedClineSessions.set(input.sessionId, sessionImpl);
|
|
830
|
+
try {
|
|
831
|
+
await persistHistory();
|
|
832
|
+
} catch {
|
|
833
|
+
/*
|
|
834
|
+
* The parked in-process session is the authoritative continuation
|
|
835
|
+
* path while the live turn is waiting on host input. Persistence
|
|
836
|
+
* is only a fallback for later non-live resumes.
|
|
837
|
+
*/
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
type: 'resume-session',
|
|
841
|
+
harnessId: HARNESS_ID,
|
|
842
|
+
specificationVersion: 'harness-v1',
|
|
843
|
+
data: { historyFileName: CLINE_DEFAULT_HISTORY_FILE_NAME },
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
return doStop();
|
|
847
|
+
},
|
|
848
|
+
|
|
849
|
+
doSuspendTurn: async (): Promise<HarnessV1ContinueTurnState> => {
|
|
850
|
+
if (stopped) {
|
|
851
|
+
throw new Error('Cline session has been stopped.');
|
|
852
|
+
}
|
|
853
|
+
if (
|
|
854
|
+
activeTurn != null &&
|
|
855
|
+
(pendingToolResults.size > 0 || pendingToolApprovals.size > 0)
|
|
856
|
+
) {
|
|
857
|
+
parkedClineSessions.set(input.sessionId, sessionImpl);
|
|
858
|
+
try {
|
|
859
|
+
await persistHistory();
|
|
860
|
+
} catch {
|
|
861
|
+
/*
|
|
862
|
+
* While waiting on host input, the live parked session is the
|
|
863
|
+
* authoritative same-process continuation path. The sandbox copy
|
|
864
|
+
* remains a best-effort fallback for a later cold resume.
|
|
865
|
+
*/
|
|
866
|
+
}
|
|
867
|
+
return {
|
|
868
|
+
type: 'continue-turn',
|
|
869
|
+
harnessId: HARNESS_ID,
|
|
870
|
+
specificationVersion: 'harness-v1',
|
|
871
|
+
data: { historyFileName: CLINE_DEFAULT_HISTORY_FILE_NAME },
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
/*
|
|
875
|
+
* The model runs in this host process, which is about to be suspended
|
|
876
|
+
* at the slice boundary — the in-flight turn cannot survive it. Abort
|
|
877
|
+
* it (the turn settles silently via the `suspending` guard so the
|
|
878
|
+
* stream closes cleanly), persist the history into the sandbox, and
|
|
879
|
+
* tear down host-side resources. The sandbox itself is left running;
|
|
880
|
+
* the next slice pulls the history after the sandbox provider
|
|
881
|
+
* reattaches and rerun-continues. The tail in flight at the boundary
|
|
882
|
+
* is recomputed — a host-resident runtime cannot freeze a live turn
|
|
883
|
+
* the way a bridge adapter can.
|
|
884
|
+
*/
|
|
885
|
+
suspending = true;
|
|
886
|
+
agent?.abort('Cline session suspended');
|
|
887
|
+
if (activeTurn) {
|
|
888
|
+
await activeTurn.done.catch(() => {});
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
try {
|
|
892
|
+
await persistHistory();
|
|
893
|
+
} catch {
|
|
894
|
+
// Best-effort: a missing/failed copy leaves the previously persisted
|
|
895
|
+
// history in place, so the next slice resumes from a slightly older
|
|
896
|
+
// (still valid) state.
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
stopped = true;
|
|
900
|
+
parkedClineSessions.delete(input.sessionId);
|
|
901
|
+
settlePendingToolResults('Cline session suspended');
|
|
902
|
+
settlePendingToolApprovals('Cline session suspended');
|
|
903
|
+
await teardown();
|
|
904
|
+
|
|
905
|
+
return {
|
|
906
|
+
type: 'continue-turn',
|
|
907
|
+
harnessId: HARNESS_ID,
|
|
908
|
+
specificationVersion: 'harness-v1',
|
|
909
|
+
data: { historyFileName: CLINE_DEFAULT_HISTORY_FILE_NAME },
|
|
910
|
+
};
|
|
911
|
+
},
|
|
912
|
+
};
|
|
913
|
+
|
|
914
|
+
return sessionImpl;
|
|
915
|
+
}
|