@clinebot/core 0.0.4 → 0.0.6
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/dist/agents/agent-config-parser.d.ts +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +134 -107
- package/dist/runtime/session-runtime.d.ts +4 -2
- package/dist/session/default-session-manager.d.ts +5 -1
- package/dist/session/session-host.d.ts +3 -1
- package/dist/session/session-manager.d.ts +2 -1
- package/dist/session/unified-session-persistence-service.d.ts +4 -0
- package/dist/telemetry/ITelemetryAdapter.d.ts +54 -0
- package/dist/telemetry/LoggerTelemetryAdapter.d.ts +21 -0
- package/dist/telemetry/OpenTelemetryAdapter.d.ts +43 -0
- package/dist/telemetry/OpenTelemetryProvider.d.ts +41 -0
- package/dist/telemetry/TelemetryService.d.ts +34 -0
- package/dist/telemetry/opentelemetry.d.ts +3 -0
- package/dist/telemetry/opentelemetry.js +27 -0
- package/dist/{default-tools → tools}/schemas.d.ts +6 -0
- package/dist/types/config.d.ts +3 -2
- package/package.json +17 -3
- package/src/agents/agent-config-parser.ts +1 -1
- package/src/agents/hooks-config-loader.ts +19 -1
- package/src/index.node.ts +3 -0
- package/src/index.ts +35 -19
- package/src/providers/local-provider-service.ts +25 -7
- package/src/runtime/hook-file-hooks.test.ts +47 -0
- package/src/runtime/hook-file-hooks.ts +3 -0
- package/src/runtime/runtime-builder.test.ts +20 -0
- package/src/runtime/runtime-builder.ts +3 -2
- package/src/runtime/runtime-parity.test.ts +1 -1
- package/src/runtime/session-runtime.ts +4 -2
- package/src/session/default-session-manager.test.ts +72 -0
- package/src/session/default-session-manager.ts +63 -6
- package/src/session/session-host.ts +7 -2
- package/src/session/session-manager.ts +2 -1
- package/src/session/unified-session-persistence-service.ts +213 -23
- package/src/telemetry/ITelemetryAdapter.ts +94 -0
- package/src/telemetry/LoggerTelemetryAdapter.test.ts +42 -0
- package/src/telemetry/LoggerTelemetryAdapter.ts +114 -0
- package/src/telemetry/OpenTelemetryAdapter.test.ts +157 -0
- package/src/telemetry/OpenTelemetryAdapter.ts +348 -0
- package/src/telemetry/OpenTelemetryProvider.test.ts +113 -0
- package/src/telemetry/OpenTelemetryProvider.ts +322 -0
- package/src/telemetry/TelemetryService.test.ts +134 -0
- package/src/telemetry/TelemetryService.ts +141 -0
- package/src/telemetry/opentelemetry.ts +20 -0
- package/src/{default-tools → tools}/definitions.ts +35 -28
- package/src/{default-tools → tools}/schemas.ts +9 -0
- package/src/types/config.ts +3 -1
- /package/dist/{default-tools → tools}/constants.d.ts +0 -0
- /package/dist/{default-tools → tools}/definitions.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/apply-patch-parser.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/apply-patch.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/bash.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/editor.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/file-read.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/index.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/search.d.ts +0 -0
- /package/dist/{default-tools → tools}/executors/web-fetch.d.ts +0 -0
- /package/dist/{default-tools → tools}/index.d.ts +0 -0
- /package/dist/{default-tools → tools}/model-tool-routing.d.ts +0 -0
- /package/dist/{default-tools → tools}/presets.d.ts +0 -0
- /package/dist/{default-tools → tools}/types.d.ts +0 -0
- /package/src/{default-tools → tools}/constants.ts +0 -0
- /package/src/{default-tools → tools}/definitions.test.ts +0 -0
- /package/src/{default-tools → tools}/executors/apply-patch-parser.ts +0 -0
- /package/src/{default-tools → tools}/executors/apply-patch.ts +0 -0
- /package/src/{default-tools → tools}/executors/bash.ts +0 -0
- /package/src/{default-tools → tools}/executors/editor.ts +0 -0
- /package/src/{default-tools → tools}/executors/file-read.test.ts +0 -0
- /package/src/{default-tools → tools}/executors/file-read.ts +0 -0
- /package/src/{default-tools → tools}/executors/index.ts +0 -0
- /package/src/{default-tools → tools}/executors/search.ts +0 -0
- /package/src/{default-tools → tools}/executors/web-fetch.ts +0 -0
- /package/src/{default-tools → tools}/index.ts +0 -0
- /package/src/{default-tools → tools}/model-tool-routing.test.ts +0 -0
- /package/src/{default-tools → tools}/model-tool-routing.ts +0 -0
- /package/src/{default-tools → tools}/presets.test.ts +0 -0
- /package/src/{default-tools → tools}/presets.ts +0 -0
- /package/src/{default-tools → tools}/types.ts +0 -0
|
@@ -14,15 +14,14 @@ import {
|
|
|
14
14
|
type ToolApprovalResult,
|
|
15
15
|
} from "@clinebot/agents";
|
|
16
16
|
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
formatUserInputBlock,
|
|
19
|
+
type ITelemetryService,
|
|
20
|
+
normalizeUserInput,
|
|
21
|
+
} from "@clinebot/shared";
|
|
18
22
|
import { setHomeDirIfUnset } from "@clinebot/shared/storage";
|
|
19
23
|
import { nanoid } from "nanoid";
|
|
20
24
|
import { resolveAndLoadAgentPlugins } from "../agents/plugin-config-loader";
|
|
21
|
-
import {
|
|
22
|
-
createBuiltinTools,
|
|
23
|
-
type ToolExecutors,
|
|
24
|
-
ToolPresets,
|
|
25
|
-
} from "../default-tools";
|
|
26
25
|
import { enrichPromptWithMentions } from "../input";
|
|
27
26
|
import {
|
|
28
27
|
createHookAuditHooks,
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
buildTeamProgressSummary,
|
|
37
36
|
toTeamProgressLifecycleEvent,
|
|
38
37
|
} from "../team";
|
|
38
|
+
import { createBuiltinTools, type ToolExecutors, ToolPresets } from "../tools";
|
|
39
39
|
import { SessionSource, type SessionStatus } from "../types/common";
|
|
40
40
|
import type { CoreSessionConfig } from "../types/config";
|
|
41
41
|
import type { CoreSessionEvent } from "../types/events";
|
|
@@ -93,6 +93,7 @@ export interface DefaultSessionManagerOptions {
|
|
|
93
93
|
toolPolicies?: AgentConfig["toolPolicies"];
|
|
94
94
|
providerSettingsManager?: ProviderSettingsManager;
|
|
95
95
|
oauthTokenManager?: RuntimeOAuthTokenManager;
|
|
96
|
+
telemetry?: ITelemetryService;
|
|
96
97
|
requestToolApproval?: (
|
|
97
98
|
request: ToolApprovalRequest,
|
|
98
99
|
) => Promise<ToolApprovalResult>;
|
|
@@ -124,6 +125,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
124
125
|
private readonly defaultToolPolicies?: AgentConfig["toolPolicies"];
|
|
125
126
|
private readonly providerSettingsManager: ProviderSettingsManager;
|
|
126
127
|
private readonly oauthTokenManager: RuntimeOAuthTokenManager;
|
|
128
|
+
private readonly defaultTelemetry?: ITelemetryService;
|
|
127
129
|
private readonly defaultRequestToolApproval?: (
|
|
128
130
|
request: ToolApprovalRequest,
|
|
129
131
|
) => Promise<ToolApprovalResult>;
|
|
@@ -147,6 +149,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
147
149
|
new RuntimeOAuthTokenManager({
|
|
148
150
|
providerSettingsManager: this.providerSettingsManager,
|
|
149
151
|
});
|
|
152
|
+
this.defaultTelemetry = options.telemetry;
|
|
150
153
|
this.defaultRequestToolApproval = options.requestToolApproval;
|
|
151
154
|
}
|
|
152
155
|
|
|
@@ -267,6 +270,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
267
270
|
...input.config,
|
|
268
271
|
hooks: effectiveHooks,
|
|
269
272
|
extensions: effectiveExtensions,
|
|
273
|
+
telemetry: input.config.telemetry ?? this.defaultTelemetry,
|
|
270
274
|
};
|
|
271
275
|
const providerConfig =
|
|
272
276
|
this.buildResolvedProviderConfig(effectiveConfigBase);
|
|
@@ -280,6 +284,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
280
284
|
hooks: effectiveHooks,
|
|
281
285
|
extensions: effectiveExtensions,
|
|
282
286
|
logger: effectiveConfig.logger,
|
|
287
|
+
telemetry: effectiveConfig.telemetry,
|
|
283
288
|
onTeamEvent: (event: TeamEvent) => {
|
|
284
289
|
void this.handleTeamEvent(sessionId, event);
|
|
285
290
|
effectiveConfig.onTeamEvent?.(event);
|
|
@@ -291,6 +296,18 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
291
296
|
input.defaultToolExecutors ?? this.defaultToolExecutors,
|
|
292
297
|
});
|
|
293
298
|
const tools = [...runtime.tools, ...(effectiveConfig.extraTools ?? [])];
|
|
299
|
+
effectiveConfig.telemetry?.capture({
|
|
300
|
+
event: "session.started",
|
|
301
|
+
properties: {
|
|
302
|
+
sessionId,
|
|
303
|
+
source,
|
|
304
|
+
providerId: effectiveConfig.providerId,
|
|
305
|
+
modelId: effectiveConfig.modelId,
|
|
306
|
+
enableTools: effectiveConfig.enableTools,
|
|
307
|
+
enableSpawnAgent: effectiveConfig.enableSpawnAgent,
|
|
308
|
+
enableAgentTeams: effectiveConfig.enableAgentTeams,
|
|
309
|
+
},
|
|
310
|
+
});
|
|
294
311
|
const agent = this.createAgentInstance({
|
|
295
312
|
providerId: providerConfig.providerId,
|
|
296
313
|
modelId: providerConfig.modelId,
|
|
@@ -330,6 +347,14 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
330
347
|
}),
|
|
331
348
|
);
|
|
332
349
|
}
|
|
350
|
+
if (event.type === "iteration_end") {
|
|
351
|
+
void this.invoke<void>(
|
|
352
|
+
"persistSessionMessages",
|
|
353
|
+
sessionId,
|
|
354
|
+
liveSession?.agent.getMessages() ?? [],
|
|
355
|
+
liveSession?.config.systemPrompt,
|
|
356
|
+
);
|
|
357
|
+
}
|
|
333
358
|
this.emit({
|
|
334
359
|
type: "agent_event",
|
|
335
360
|
payload: {
|
|
@@ -401,6 +426,15 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
401
426
|
if (!session) {
|
|
402
427
|
throw new Error(`session not found: ${input.sessionId}`);
|
|
403
428
|
}
|
|
429
|
+
session.config.telemetry?.capture({
|
|
430
|
+
event: "session.input_sent",
|
|
431
|
+
properties: {
|
|
432
|
+
sessionId: input.sessionId,
|
|
433
|
+
promptLength: input.prompt.length,
|
|
434
|
+
userImageCount: input.userImages?.length ?? 0,
|
|
435
|
+
userFileCount: input.userFiles?.length ?? 0,
|
|
436
|
+
},
|
|
437
|
+
});
|
|
404
438
|
try {
|
|
405
439
|
const result = await this.runTurn(session, {
|
|
406
440
|
prompt: input.prompt,
|
|
@@ -432,6 +466,10 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
432
466
|
if (!session) {
|
|
433
467
|
return;
|
|
434
468
|
}
|
|
469
|
+
session.config.telemetry?.capture({
|
|
470
|
+
event: "session.aborted",
|
|
471
|
+
properties: { sessionId },
|
|
472
|
+
});
|
|
435
473
|
session.aborting = true;
|
|
436
474
|
session.agent.abort();
|
|
437
475
|
}
|
|
@@ -441,6 +479,10 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
441
479
|
if (!session) {
|
|
442
480
|
return;
|
|
443
481
|
}
|
|
482
|
+
session.config.telemetry?.capture({
|
|
483
|
+
event: "session.stopped",
|
|
484
|
+
properties: { sessionId },
|
|
485
|
+
});
|
|
444
486
|
await this.shutdownSession(session, {
|
|
445
487
|
status: "cancelled",
|
|
446
488
|
exitCode: null,
|
|
@@ -565,6 +607,9 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
565
607
|
if (!prompt) {
|
|
566
608
|
throw new Error("prompt cannot be empty");
|
|
567
609
|
}
|
|
610
|
+
if (!session.artifacts && !session.pendingPrompt) {
|
|
611
|
+
session.pendingPrompt = prompt;
|
|
612
|
+
}
|
|
568
613
|
await this.ensureSessionPersisted(session);
|
|
569
614
|
await this.syncOAuthCredentials(session);
|
|
570
615
|
|
|
@@ -1201,4 +1246,16 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
1201
1246
|
apiKey: resolved.apiKey,
|
|
1202
1247
|
});
|
|
1203
1248
|
}
|
|
1249
|
+
|
|
1250
|
+
async updateSessionModel(sessionId: string, modelId: string): Promise<void> {
|
|
1251
|
+
const session = this.sessions.get(sessionId);
|
|
1252
|
+
if (!session) {
|
|
1253
|
+
throw new Error(`session not found: ${sessionId}`);
|
|
1254
|
+
}
|
|
1255
|
+
session.config.modelId = modelId;
|
|
1256
|
+
const agentWithConnection = session.agent as Agent & {
|
|
1257
|
+
updateConnection?: (overrides: { modelId?: string }) => void;
|
|
1258
|
+
};
|
|
1259
|
+
agentWithConnection.updateConnection?.({ modelId });
|
|
1260
|
+
}
|
|
1204
1261
|
}
|
|
@@ -7,10 +7,11 @@ import type {
|
|
|
7
7
|
ToolApprovalResult,
|
|
8
8
|
} from "@clinebot/agents";
|
|
9
9
|
import { getRpcServerDefaultAddress, getRpcServerHealth } from "@clinebot/rpc";
|
|
10
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
10
11
|
import { resolveSessionDataDir } from "@clinebot/shared/storage";
|
|
11
12
|
import { nanoid } from "nanoid";
|
|
12
|
-
import type { ToolExecutors } from "../default-tools";
|
|
13
13
|
import { SqliteSessionStore } from "../storage/sqlite-session-store";
|
|
14
|
+
import type { ToolExecutors } from "../tools";
|
|
14
15
|
import { DefaultSessionManager } from "./default-session-manager";
|
|
15
16
|
import { RpcCoreSessionService } from "./rpc-session-service";
|
|
16
17
|
import type { SessionManager } from "./session-manager";
|
|
@@ -33,6 +34,7 @@ export interface CreateSessionHostOptions {
|
|
|
33
34
|
rpcConnectAttempts?: number;
|
|
34
35
|
rpcConnectDelayMs?: number;
|
|
35
36
|
defaultToolExecutors?: Partial<ToolExecutors>;
|
|
37
|
+
telemetry?: ITelemetryService;
|
|
36
38
|
toolPolicies?: AgentConfig["toolPolicies"];
|
|
37
39
|
requestToolApproval?: (
|
|
38
40
|
request: ToolApprovalRequest,
|
|
@@ -190,13 +192,16 @@ export async function resolveSessionBackend(
|
|
|
190
192
|
export async function createSessionHost(
|
|
191
193
|
options: CreateSessionHostOptions,
|
|
192
194
|
): Promise<SessionHost> {
|
|
195
|
+
const distinctId = resolveHostDistinctId(options.distinctId);
|
|
196
|
+
options.telemetry?.setDistinctId(distinctId);
|
|
193
197
|
const backend =
|
|
194
198
|
options.sessionService ?? (await resolveSessionBackend(options));
|
|
195
199
|
return new DefaultSessionManager({
|
|
196
200
|
sessionService: backend,
|
|
197
201
|
defaultToolExecutors: options.defaultToolExecutors,
|
|
202
|
+
telemetry: options.telemetry,
|
|
198
203
|
toolPolicies: options.toolPolicies,
|
|
199
204
|
requestToolApproval: options.requestToolApproval,
|
|
200
|
-
distinctId
|
|
205
|
+
distinctId,
|
|
201
206
|
});
|
|
202
207
|
}
|
|
@@ -16,7 +16,7 @@ export interface StartSessionInput {
|
|
|
16
16
|
userFiles?: string[];
|
|
17
17
|
userInstructionWatcher?: import("../agents").UserInstructionConfigWatcher;
|
|
18
18
|
onTeamRestored?: () => void;
|
|
19
|
-
defaultToolExecutors?: Partial<import("../
|
|
19
|
+
defaultToolExecutors?: Partial<import("../tools").ToolExecutors>;
|
|
20
20
|
toolPolicies?: import("@clinebot/agents").AgentConfig["toolPolicies"];
|
|
21
21
|
requestToolApproval?: (
|
|
22
22
|
request: import("@clinebot/agents").ToolApprovalRequest,
|
|
@@ -64,4 +64,5 @@ export interface SessionManager {
|
|
|
64
64
|
readTranscript(sessionId: string, maxChars?: number): Promise<string>;
|
|
65
65
|
readHooks(sessionId: string, limit?: number): Promise<unknown[]>;
|
|
66
66
|
subscribe(listener: (event: CoreSessionEvent) => void): () => void;
|
|
67
|
+
updateSessionModel?(sessionId: string, modelId: string): Promise<void>;
|
|
67
68
|
}
|
|
@@ -10,7 +10,7 @@ import type {
|
|
|
10
10
|
SubAgentStartContext,
|
|
11
11
|
} from "@clinebot/agents";
|
|
12
12
|
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
13
|
-
import { resolveRootSessionId } from "@clinebot/shared";
|
|
13
|
+
import { normalizeUserInput, resolveRootSessionId } from "@clinebot/shared";
|
|
14
14
|
import { nanoid } from "nanoid";
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import type { SessionStatus } from "../types/common";
|
|
@@ -48,6 +48,58 @@ function stringifyMetadataJson(
|
|
|
48
48
|
return JSON.stringify(metadata);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
function normalizeSessionTitle(title?: string | null): string | undefined {
|
|
52
|
+
const trimmed = title?.trim();
|
|
53
|
+
return trimmed ? trimmed.slice(0, 120) : undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function deriveSessionTitleFromPrompt(
|
|
57
|
+
prompt?: string | null,
|
|
58
|
+
): string | undefined {
|
|
59
|
+
const normalizedPrompt = normalizeUserInput(prompt ?? "").trim();
|
|
60
|
+
if (!normalizedPrompt) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const firstLine = normalizedPrompt.split("\n")[0]?.trim();
|
|
64
|
+
return normalizeSessionTitle(firstLine);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeMetadataForStorage(
|
|
68
|
+
metadata: Record<string, unknown> | null | undefined,
|
|
69
|
+
): Record<string, unknown> | undefined {
|
|
70
|
+
if (!metadata) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
const next = { ...metadata };
|
|
74
|
+
if (typeof next.title === "string") {
|
|
75
|
+
const normalizedTitle = normalizeSessionTitle(next.title);
|
|
76
|
+
if (normalizedTitle) {
|
|
77
|
+
next.title = normalizedTitle;
|
|
78
|
+
} else {
|
|
79
|
+
delete next.title;
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
delete next.title;
|
|
83
|
+
}
|
|
84
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function metadataWithResolvedTitle(input: {
|
|
88
|
+
metadata?: Record<string, unknown> | null;
|
|
89
|
+
title?: string | null;
|
|
90
|
+
prompt?: string | null;
|
|
91
|
+
}): Record<string, unknown> | undefined {
|
|
92
|
+
const next = { ...(normalizeMetadataForStorage(input.metadata) ?? {}) };
|
|
93
|
+
const resolvedTitle =
|
|
94
|
+
input.title !== undefined
|
|
95
|
+
? normalizeSessionTitle(input.title)
|
|
96
|
+
: deriveSessionTitleFromPrompt(input.prompt);
|
|
97
|
+
if (resolvedTitle) {
|
|
98
|
+
next.title = resolvedTitle;
|
|
99
|
+
}
|
|
100
|
+
return Object.keys(next).length > 0 ? next : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
51
103
|
export interface PersistedSessionUpdateInput {
|
|
52
104
|
sessionId: string;
|
|
53
105
|
expectedStatusLock?: number;
|
|
@@ -56,6 +108,7 @@ export interface PersistedSessionUpdateInput {
|
|
|
56
108
|
exitCode?: number | null;
|
|
57
109
|
prompt?: string | null;
|
|
58
110
|
metadataJson?: string | null;
|
|
111
|
+
title?: string | null;
|
|
59
112
|
parentSessionId?: string | null;
|
|
60
113
|
parentAgentId?: string | null;
|
|
61
114
|
agentId?: string | null;
|
|
@@ -173,6 +226,63 @@ export class UnifiedSessionPersistenceService {
|
|
|
173
226
|
);
|
|
174
227
|
}
|
|
175
228
|
|
|
229
|
+
private readSessionManifestFile(sessionId: string): {
|
|
230
|
+
path: string;
|
|
231
|
+
manifest?: SessionManifest;
|
|
232
|
+
} {
|
|
233
|
+
const manifestPath = this.sessionManifestPath(sessionId, false);
|
|
234
|
+
if (!existsSync(manifestPath)) {
|
|
235
|
+
return { path: manifestPath };
|
|
236
|
+
}
|
|
237
|
+
try {
|
|
238
|
+
const manifest = SessionManifestSchema.parse(
|
|
239
|
+
JSON.parse(readFileSync(manifestPath, "utf8")) as SessionManifest,
|
|
240
|
+
);
|
|
241
|
+
return { path: manifestPath, manifest };
|
|
242
|
+
} catch {
|
|
243
|
+
return { path: manifestPath };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private applyResolvedTitleToRow(row: SessionRowShape): SessionRowShape {
|
|
248
|
+
const existingMetadata =
|
|
249
|
+
typeof row.metadata_json === "string" &&
|
|
250
|
+
row.metadata_json.trim().length > 0
|
|
251
|
+
? (() => {
|
|
252
|
+
try {
|
|
253
|
+
const parsed = JSON.parse(row.metadata_json) as unknown;
|
|
254
|
+
if (
|
|
255
|
+
parsed &&
|
|
256
|
+
typeof parsed === "object" &&
|
|
257
|
+
!Array.isArray(parsed)
|
|
258
|
+
) {
|
|
259
|
+
return parsed as Record<string, unknown>;
|
|
260
|
+
}
|
|
261
|
+
} catch {
|
|
262
|
+
// Ignore malformed metadata payloads.
|
|
263
|
+
}
|
|
264
|
+
return undefined;
|
|
265
|
+
})()
|
|
266
|
+
: undefined;
|
|
267
|
+
const sanitizedMetadata = normalizeMetadataForStorage(existingMetadata);
|
|
268
|
+
const { manifest } = this.readSessionManifestFile(row.session_id);
|
|
269
|
+
const manifestTitle = normalizeSessionTitle(
|
|
270
|
+
typeof manifest?.metadata?.title === "string"
|
|
271
|
+
? (manifest.metadata.title as string)
|
|
272
|
+
: undefined,
|
|
273
|
+
);
|
|
274
|
+
const resolvedMetadata = manifestTitle
|
|
275
|
+
? {
|
|
276
|
+
...(sanitizedMetadata ?? {}),
|
|
277
|
+
title: manifestTitle,
|
|
278
|
+
}
|
|
279
|
+
: sanitizedMetadata;
|
|
280
|
+
return {
|
|
281
|
+
...row,
|
|
282
|
+
metadata_json: stringifyMetadataJson(resolvedMetadata),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
176
286
|
private createRootSessionId(): string {
|
|
177
287
|
return `${Date.now()}_${nanoid(5)}`;
|
|
178
288
|
}
|
|
@@ -207,9 +317,13 @@ export class UnifiedSessionPersistenceService {
|
|
|
207
317
|
enable_spawn: input.enableSpawn,
|
|
208
318
|
enable_teams: input.enableTeams,
|
|
209
319
|
prompt: input.prompt?.trim() || undefined,
|
|
210
|
-
metadata:
|
|
320
|
+
metadata: metadataWithResolvedTitle({
|
|
321
|
+
metadata: input.metadata,
|
|
322
|
+
prompt: input.prompt,
|
|
323
|
+
}),
|
|
211
324
|
messages_path: messagesPath,
|
|
212
325
|
});
|
|
326
|
+
const storedMetadata = normalizeMetadataForStorage(manifest.metadata);
|
|
213
327
|
|
|
214
328
|
await this.adapter.upsertSession({
|
|
215
329
|
session_id: sessionId,
|
|
@@ -235,7 +349,7 @@ export class UnifiedSessionPersistenceService {
|
|
|
235
349
|
conversation_id: null,
|
|
236
350
|
is_subagent: 0,
|
|
237
351
|
prompt: manifest.prompt ?? null,
|
|
238
|
-
metadata_json: stringifyMetadataJson(
|
|
352
|
+
metadata_json: stringifyMetadataJson(storedMetadata),
|
|
239
353
|
transcript_path: transcriptPath,
|
|
240
354
|
hook_path: hookPath,
|
|
241
355
|
messages_path: messagesPath,
|
|
@@ -293,40 +407,86 @@ export class UnifiedSessionPersistenceService {
|
|
|
293
407
|
sessionId: string;
|
|
294
408
|
prompt?: string | null;
|
|
295
409
|
metadata?: Record<string, unknown> | null;
|
|
410
|
+
title?: string | null;
|
|
296
411
|
}): Promise<{ updated: boolean }> {
|
|
297
412
|
for (let attempt = 0; attempt < 4; attempt++) {
|
|
298
413
|
const row = await this.adapter.getSession(input.sessionId);
|
|
299
414
|
if (!row || typeof row.status_lock !== "number") {
|
|
300
415
|
return { updated: false };
|
|
301
416
|
}
|
|
417
|
+
const sanitizedMetadata =
|
|
418
|
+
input.metadata === undefined
|
|
419
|
+
? undefined
|
|
420
|
+
: normalizeMetadataForStorage(input.metadata);
|
|
421
|
+
const existingMetadata = (() => {
|
|
422
|
+
const raw = row.metadata_json?.trim();
|
|
423
|
+
if (!raw) {
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
try {
|
|
427
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
428
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
429
|
+
return normalizeMetadataForStorage(
|
|
430
|
+
parsed as Record<string, unknown>,
|
|
431
|
+
);
|
|
432
|
+
}
|
|
433
|
+
} catch {
|
|
434
|
+
// Ignore malformed metadata payloads.
|
|
435
|
+
}
|
|
436
|
+
return undefined;
|
|
437
|
+
})();
|
|
438
|
+
const existingTitle = normalizeSessionTitle(
|
|
439
|
+
typeof existingMetadata?.title === "string"
|
|
440
|
+
? (existingMetadata.title as string)
|
|
441
|
+
: undefined,
|
|
442
|
+
);
|
|
443
|
+
const nextTitle =
|
|
444
|
+
input.title !== undefined
|
|
445
|
+
? normalizeSessionTitle(input.title)
|
|
446
|
+
: input.prompt !== undefined
|
|
447
|
+
? deriveSessionTitleFromPrompt(input.prompt)
|
|
448
|
+
: existingTitle;
|
|
449
|
+
const nextMetadata =
|
|
450
|
+
input.metadata !== undefined
|
|
451
|
+
? { ...(sanitizedMetadata ?? {}) }
|
|
452
|
+
: { ...(existingMetadata ?? {}) };
|
|
453
|
+
if (nextTitle) {
|
|
454
|
+
nextMetadata.title = nextTitle;
|
|
455
|
+
} else {
|
|
456
|
+
delete nextMetadata.title;
|
|
457
|
+
}
|
|
302
458
|
const changed = await this.adapter.updateSession({
|
|
303
459
|
sessionId: input.sessionId,
|
|
304
460
|
prompt: input.prompt,
|
|
305
461
|
metadataJson:
|
|
306
|
-
input.metadata === undefined
|
|
462
|
+
input.metadata === undefined &&
|
|
463
|
+
input.prompt === undefined &&
|
|
464
|
+
input.title === undefined
|
|
307
465
|
? undefined
|
|
308
|
-
: stringifyMetadataJson(
|
|
466
|
+
: stringifyMetadataJson(nextMetadata),
|
|
467
|
+
title: nextTitle,
|
|
309
468
|
expectedStatusLock: row.status_lock,
|
|
310
469
|
});
|
|
311
470
|
if (!changed.updated) {
|
|
312
471
|
continue;
|
|
313
472
|
}
|
|
314
|
-
const manifestPath = this.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
} catch {
|
|
328
|
-
// Ignore malformed manifests and keep backend session state as source of truth.
|
|
473
|
+
const { path: manifestPath, manifest } = this.readSessionManifestFile(
|
|
474
|
+
input.sessionId,
|
|
475
|
+
);
|
|
476
|
+
if (manifest) {
|
|
477
|
+
if (input.prompt !== undefined) {
|
|
478
|
+
manifest.prompt = input.prompt ?? undefined;
|
|
479
|
+
}
|
|
480
|
+
const nextMetadata =
|
|
481
|
+
input.metadata !== undefined
|
|
482
|
+
? { ...(normalizeMetadataForStorage(input.metadata) ?? {}) }
|
|
483
|
+
: { ...(normalizeMetadataForStorage(manifest.metadata) ?? {}) };
|
|
484
|
+
if (nextTitle) {
|
|
485
|
+
nextMetadata.title = nextTitle;
|
|
329
486
|
}
|
|
487
|
+
manifest.metadata =
|
|
488
|
+
Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined;
|
|
489
|
+
this.writeSessionManifestFile(manifestPath, manifest);
|
|
330
490
|
}
|
|
331
491
|
return { updated: true };
|
|
332
492
|
}
|
|
@@ -422,7 +582,9 @@ export class UnifiedSessionPersistenceService {
|
|
|
422
582
|
conversation_id: input.conversationId,
|
|
423
583
|
is_subagent: 1,
|
|
424
584
|
prompt,
|
|
425
|
-
metadata_json:
|
|
585
|
+
metadata_json: stringifyMetadataJson(
|
|
586
|
+
metadataWithResolvedTitle({ prompt }),
|
|
587
|
+
),
|
|
426
588
|
transcript_path: artifactPaths.transcriptPath,
|
|
427
589
|
hook_path: artifactPaths.hookPath,
|
|
428
590
|
messages_path: artifactPaths.messagesPath,
|
|
@@ -444,6 +606,30 @@ export class UnifiedSessionPersistenceService {
|
|
|
444
606
|
agentId: input.agentId,
|
|
445
607
|
conversationId: input.conversationId,
|
|
446
608
|
prompt: existing.prompt ?? prompt ?? null,
|
|
609
|
+
metadataJson: stringifyMetadataJson(
|
|
610
|
+
metadataWithResolvedTitle({
|
|
611
|
+
metadata: (() => {
|
|
612
|
+
const raw = existing.metadata_json?.trim();
|
|
613
|
+
if (!raw) {
|
|
614
|
+
return undefined;
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
618
|
+
if (
|
|
619
|
+
parsed &&
|
|
620
|
+
typeof parsed === "object" &&
|
|
621
|
+
!Array.isArray(parsed)
|
|
622
|
+
) {
|
|
623
|
+
return parsed as Record<string, unknown>;
|
|
624
|
+
}
|
|
625
|
+
} catch {
|
|
626
|
+
// Ignore malformed metadata payloads.
|
|
627
|
+
}
|
|
628
|
+
return undefined;
|
|
629
|
+
})(),
|
|
630
|
+
prompt: existing.prompt ?? prompt ?? null,
|
|
631
|
+
}),
|
|
632
|
+
),
|
|
447
633
|
expectedStatusLock: existing.status_lock,
|
|
448
634
|
});
|
|
449
635
|
return sessionId;
|
|
@@ -601,7 +787,9 @@ export class UnifiedSessionPersistenceService {
|
|
|
601
787
|
conversation_id: null,
|
|
602
788
|
is_subagent: 1,
|
|
603
789
|
prompt: message || `Team task for ${agentId}`,
|
|
604
|
-
metadata_json:
|
|
790
|
+
metadata_json: stringifyMetadataJson(
|
|
791
|
+
metadataWithResolvedTitle({ prompt: message }),
|
|
792
|
+
),
|
|
605
793
|
transcript_path: transcriptPath,
|
|
606
794
|
hook_path: hookPath,
|
|
607
795
|
messages_path: messagesPath,
|
|
@@ -750,7 +938,9 @@ export class UnifiedSessionPersistenceService {
|
|
|
750
938
|
}
|
|
751
939
|
rows = await this.adapter.listSessions({ limit: scanLimit });
|
|
752
940
|
}
|
|
753
|
-
return rows
|
|
941
|
+
return rows
|
|
942
|
+
.slice(0, requestedLimit)
|
|
943
|
+
.map((row) => this.applyResolvedTitleToRow(row));
|
|
754
944
|
}
|
|
755
945
|
|
|
756
946
|
async deleteSession(sessionId: string): Promise<{ deleted: boolean }> {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telemetry adapter interface for the @clinebot/core SDK.
|
|
3
|
+
*
|
|
4
|
+
* This is the SDK-side counterpart to the extension's ITelemetryProvider.
|
|
5
|
+
* It is intentionally free of VS Code / host-provider dependencies so that
|
|
6
|
+
* any consumer (CLI, tests, third-party integrations) can plug in their own
|
|
7
|
+
* backend without pulling in the full extension runtime.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { TelemetryProperties } from "@clinebot/shared";
|
|
11
|
+
|
|
12
|
+
export type {
|
|
13
|
+
TelemetryArray,
|
|
14
|
+
TelemetryMetadata,
|
|
15
|
+
TelemetryObject,
|
|
16
|
+
TelemetryPrimitive,
|
|
17
|
+
TelemetryProperties,
|
|
18
|
+
TelemetryValue,
|
|
19
|
+
} from "@clinebot/shared";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// Adapter interface
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Telemetry adapter that an SDK consumer implements (or uses via the
|
|
27
|
+
* provided {@link OpenTelemetryAdapter}) to receive Cline telemetry events.
|
|
28
|
+
*
|
|
29
|
+
* The interface intentionally mirrors ITelemetryProvider from the extension
|
|
30
|
+
* so that shared logic can be re-used or compared easily.
|
|
31
|
+
*/
|
|
32
|
+
export interface ITelemetryAdapter {
|
|
33
|
+
/** Human-readable adapter name used for logging / diagnostics. */
|
|
34
|
+
readonly name: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Emit a standard telemetry event.
|
|
38
|
+
* Implementations may silently drop events when telemetry is disabled.
|
|
39
|
+
*/
|
|
40
|
+
emit(event: string, properties?: TelemetryProperties): void;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Emit a *required* telemetry event that must not be suppressed by
|
|
44
|
+
* user opt-out settings (e.g. final opt-out confirmation events).
|
|
45
|
+
*/
|
|
46
|
+
emitRequired(event: string, properties?: TelemetryProperties): void;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Record a monotonically-increasing counter metric.
|
|
50
|
+
* Implementations that do not support metrics may treat this as a no-op.
|
|
51
|
+
*/
|
|
52
|
+
recordCounter(
|
|
53
|
+
name: string,
|
|
54
|
+
value: number,
|
|
55
|
+
attributes?: TelemetryProperties,
|
|
56
|
+
description?: string,
|
|
57
|
+
required?: boolean,
|
|
58
|
+
): void;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Record a histogram (distribution) metric.
|
|
62
|
+
* Implementations that do not support metrics may treat this as a no-op.
|
|
63
|
+
*/
|
|
64
|
+
recordHistogram(
|
|
65
|
+
name: string,
|
|
66
|
+
value: number,
|
|
67
|
+
attributes?: TelemetryProperties,
|
|
68
|
+
description?: string,
|
|
69
|
+
required?: boolean,
|
|
70
|
+
): void;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Record a gauge (point-in-time) metric.
|
|
74
|
+
* Pass `null` as `value` to retire the series identified by
|
|
75
|
+
* `name + attributes` and prevent stale gauge entries.
|
|
76
|
+
* Implementations that do not support metrics may treat this as a no-op.
|
|
77
|
+
*/
|
|
78
|
+
recordGauge(
|
|
79
|
+
name: string,
|
|
80
|
+
value: number | null,
|
|
81
|
+
attributes?: TelemetryProperties,
|
|
82
|
+
description?: string,
|
|
83
|
+
required?: boolean,
|
|
84
|
+
): void;
|
|
85
|
+
|
|
86
|
+
/** Returns whether the adapter is currently accepting events. */
|
|
87
|
+
isEnabled(): boolean;
|
|
88
|
+
|
|
89
|
+
/** Flush any buffered events/metrics to the backend. */
|
|
90
|
+
flush(): Promise<void>;
|
|
91
|
+
|
|
92
|
+
/** Release all resources held by the adapter. */
|
|
93
|
+
dispose(): Promise<void>;
|
|
94
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { LoggerTelemetryAdapter } from "./LoggerTelemetryAdapter";
|
|
3
|
+
|
|
4
|
+
describe("LoggerTelemetryAdapter", () => {
|
|
5
|
+
it("logs events and metrics through the provided logger", async () => {
|
|
6
|
+
const logger = {
|
|
7
|
+
debug: vi.fn(),
|
|
8
|
+
info: vi.fn(),
|
|
9
|
+
warn: vi.fn(),
|
|
10
|
+
};
|
|
11
|
+
const adapter = new LoggerTelemetryAdapter({ logger });
|
|
12
|
+
|
|
13
|
+
adapter.emit("session.started", { sessionId: "s1" });
|
|
14
|
+
adapter.emitRequired("user.opt_out", { reason: "manual" });
|
|
15
|
+
adapter.recordCounter("cline.session.starts.total", 1, {
|
|
16
|
+
sessionId: "s1",
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(logger.info).toHaveBeenCalledWith("telemetry.event", {
|
|
20
|
+
adapter: "LoggerTelemetryAdapter",
|
|
21
|
+
event: "session.started",
|
|
22
|
+
properties: { sessionId: "s1" },
|
|
23
|
+
});
|
|
24
|
+
expect(logger.warn).toHaveBeenCalledWith("telemetry.required_event", {
|
|
25
|
+
adapter: "LoggerTelemetryAdapter",
|
|
26
|
+
event: "user.opt_out",
|
|
27
|
+
properties: { reason: "manual" },
|
|
28
|
+
});
|
|
29
|
+
expect(logger.debug).toHaveBeenCalledWith("telemetry.metric", {
|
|
30
|
+
adapter: "LoggerTelemetryAdapter",
|
|
31
|
+
instrument: "counter",
|
|
32
|
+
name: "cline.session.starts.total",
|
|
33
|
+
value: 1,
|
|
34
|
+
attributes: { sessionId: "s1" },
|
|
35
|
+
description: undefined,
|
|
36
|
+
required: false,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
await adapter.flush();
|
|
40
|
+
await adapter.dispose();
|
|
41
|
+
});
|
|
42
|
+
});
|