@clinebot/core 0.0.5 → 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/index.d.ts +4 -1
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +134 -107
- package/dist/runtime/session-runtime.d.ts +3 -1
- package/dist/session/default-session-manager.d.ts +4 -0
- package/dist/session/session-host.d.ts +2 -0
- package/dist/session/session-manager.d.ts +1 -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/tools/schemas.d.ts +6 -0
- package/dist/types/config.d.ts +2 -1
- package/package.json +16 -3
- package/src/agents/hooks-config-loader.ts +19 -1
- package/src/index.node.ts +3 -0
- package/src/index.ts +16 -0
- 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 +1 -0
- package/src/runtime/session-runtime.ts +3 -1
- package/src/session/default-session-manager.test.ts +72 -0
- package/src/session/default-session-manager.ts +59 -1
- package/src/session/session-host.ts +6 -1
- package/src/session/session-manager.ts +1 -0
- 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/tools/definitions.ts +35 -28
- package/src/tools/schemas.ts +9 -0
- package/src/types/config.ts +2 -0
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ export declare const ReadFilesInputUnionSchema: z.ZodUnion<readonly [z.ZodObject
|
|
|
23
23
|
export declare const SearchCodebaseInputSchema: z.ZodObject<{
|
|
24
24
|
queries: z.ZodArray<z.ZodString>;
|
|
25
25
|
}, z.core.$strip>;
|
|
26
|
+
/**
|
|
27
|
+
* Union schema for search_codebase tool input, allowing either a single string, an array of strings, or the full object schema
|
|
28
|
+
*/
|
|
29
|
+
export declare const SearchCodebaseUnionInputSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
30
|
+
queries: z.ZodArray<z.ZodString>;
|
|
31
|
+
}, z.core.$strip>, z.ZodArray<z.ZodString>, z.ZodString]>;
|
|
26
32
|
/**
|
|
27
33
|
* Schema for run_commands tool input
|
|
28
34
|
*/
|
package/dist/types/config.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AgentConfig, AgentHooks, ConsecutiveMistakeLimitContext, ConsecutiveMistakeLimitDecision, HookErrorMode, TeamEvent, Tool } from "@clinebot/agents";
|
|
2
2
|
import type { providers as LlmsProviders } from "@clinebot/llms";
|
|
3
|
-
import type { AgentMode, BasicLogger, SessionExecutionConfig, SessionPromptConfig, SessionWorkspaceConfig } from "@clinebot/shared";
|
|
3
|
+
import type { AgentMode, BasicLogger, ITelemetryService, SessionExecutionConfig, SessionPromptConfig, SessionWorkspaceConfig } from "@clinebot/shared";
|
|
4
4
|
import type { ToolRoutingRule } from "../tools/model-tool-routing.js";
|
|
5
5
|
export type CoreAgentMode = AgentMode;
|
|
6
6
|
export interface CoreModelConfig {
|
|
@@ -35,6 +35,7 @@ export interface CoreSessionConfig extends CoreModelConfig, CoreRuntimeFeatures,
|
|
|
35
35
|
hooks?: AgentHooks;
|
|
36
36
|
hookErrorMode?: HookErrorMode;
|
|
37
37
|
logger?: BasicLogger;
|
|
38
|
+
telemetry?: ITelemetryService;
|
|
38
39
|
extraTools?: Tool[];
|
|
39
40
|
pluginPaths?: string[];
|
|
40
41
|
extensions?: AgentConfig["extensions"];
|
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clinebot/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.6",
|
|
4
4
|
"main": "./dist/index.node.js",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@clinebot/agents": "0.0.
|
|
7
|
-
"@clinebot/llms": "0.0.
|
|
6
|
+
"@clinebot/agents": "0.0.6",
|
|
7
|
+
"@clinebot/llms": "0.0.6",
|
|
8
|
+
"@opentelemetry/api": "^1.9.0",
|
|
9
|
+
"@opentelemetry/api-logs": "^0.56.0",
|
|
10
|
+
"@opentelemetry/exporter-logs-otlp-http": "^0.56.0",
|
|
11
|
+
"@opentelemetry/exporter-metrics-otlp-http": "^0.56.0",
|
|
12
|
+
"@opentelemetry/resources": "^1.30.1",
|
|
13
|
+
"@opentelemetry/sdk-logs": "^0.56.0",
|
|
14
|
+
"@opentelemetry/sdk-metrics": "^1.30.1",
|
|
15
|
+
"@opentelemetry/semantic-conventions": "^1.37.0",
|
|
8
16
|
"better-sqlite3": "^11.10.0",
|
|
9
17
|
"nanoid": "^5.1.7",
|
|
10
18
|
"simple-git": "^3.32.3",
|
|
@@ -21,6 +29,11 @@
|
|
|
21
29
|
"development": "./src/index.node.ts",
|
|
22
30
|
"types": "./dist/index.node.d.ts",
|
|
23
31
|
"import": "./dist/index.node.js"
|
|
32
|
+
},
|
|
33
|
+
"./telemetry/opentelemetry": {
|
|
34
|
+
"development": "./src/telemetry/opentelemetry.ts",
|
|
35
|
+
"types": "./dist/telemetry/opentelemetry.d.ts",
|
|
36
|
+
"import": "./dist/telemetry/opentelemetry.js"
|
|
24
37
|
}
|
|
25
38
|
},
|
|
26
39
|
"description": "State-aware orchestration for Cline Agent runtimes",
|
|
@@ -45,10 +45,28 @@ const HOOK_CONFIG_FILE_LOOKUP = new Map<string, HookConfigFileName>(
|
|
|
45
45
|
Object.values(HookConfigFileName).map((name) => [name.toLowerCase(), name]),
|
|
46
46
|
);
|
|
47
47
|
|
|
48
|
+
const SUPPORTED_HOOK_FILE_EXTENSIONS = new Set([
|
|
49
|
+
"",
|
|
50
|
+
".sh",
|
|
51
|
+
".bash",
|
|
52
|
+
".zsh",
|
|
53
|
+
".js",
|
|
54
|
+
".mjs",
|
|
55
|
+
".cjs",
|
|
56
|
+
".ts",
|
|
57
|
+
".mts",
|
|
58
|
+
".cts",
|
|
59
|
+
".py",
|
|
60
|
+
]);
|
|
61
|
+
|
|
48
62
|
export function toHookConfigFileName(
|
|
49
63
|
fileName: string,
|
|
50
64
|
): HookConfigFileName | undefined {
|
|
51
|
-
const
|
|
65
|
+
const extension = extname(fileName).toLowerCase();
|
|
66
|
+
if (!SUPPORTED_HOOK_FILE_EXTENSIONS.has(extension)) {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
const key = basename(fileName, extension).trim().toLowerCase();
|
|
52
70
|
return HOOK_CONFIG_FILE_LOOKUP.get(key);
|
|
53
71
|
}
|
|
54
72
|
|
package/src/index.node.ts
CHANGED
|
@@ -100,6 +100,9 @@ export {
|
|
|
100
100
|
OCI_HEADER_OPC_REQUEST_ID,
|
|
101
101
|
refreshOcaToken,
|
|
102
102
|
} from "./auth/oca";
|
|
103
|
+
export async function loadOpenTelemetryAdapter() {
|
|
104
|
+
return import("./telemetry/opentelemetry.js");
|
|
105
|
+
}
|
|
103
106
|
export { startLocalOAuthServer } from "./auth/server";
|
|
104
107
|
export type {
|
|
105
108
|
OAuthCredentials,
|
package/src/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type {
|
|
|
19
19
|
BasicLogger,
|
|
20
20
|
ConnectorHookEvent,
|
|
21
21
|
HookSessionContext,
|
|
22
|
+
ITelemetryService,
|
|
22
23
|
RpcAddProviderActionRequest,
|
|
23
24
|
RpcChatMessage,
|
|
24
25
|
RpcChatRunTurnRequest,
|
|
@@ -38,6 +39,12 @@ export type {
|
|
|
38
39
|
RpcSaveProviderSettingsActionRequest,
|
|
39
40
|
SessionLineage,
|
|
40
41
|
TeamProgressProjectionEvent,
|
|
42
|
+
TelemetryArray,
|
|
43
|
+
TelemetryMetadata,
|
|
44
|
+
TelemetryObject,
|
|
45
|
+
TelemetryPrimitive,
|
|
46
|
+
TelemetryProperties,
|
|
47
|
+
TelemetryValue,
|
|
41
48
|
ToolPolicy,
|
|
42
49
|
} from "@clinebot/shared";
|
|
43
50
|
export {
|
|
@@ -116,6 +123,15 @@ export {
|
|
|
116
123
|
buildTeamProgressSummary,
|
|
117
124
|
toTeamProgressLifecycleEvent,
|
|
118
125
|
} from "./team";
|
|
126
|
+
export type { ITelemetryAdapter } from "./telemetry/ITelemetryAdapter";
|
|
127
|
+
export {
|
|
128
|
+
LoggerTelemetryAdapter,
|
|
129
|
+
type LoggerTelemetryAdapterOptions,
|
|
130
|
+
} from "./telemetry/LoggerTelemetryAdapter";
|
|
131
|
+
export {
|
|
132
|
+
TelemetryService,
|
|
133
|
+
type TelemetryServiceOptions,
|
|
134
|
+
} from "./telemetry/TelemetryService";
|
|
119
135
|
export {
|
|
120
136
|
ALL_DEFAULT_TOOL_NAMES,
|
|
121
137
|
type AskQuestionExecutor,
|
|
@@ -17,6 +17,22 @@ async function createWorkspaceWithHook(
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
describe("createHookConfigFileHooks", () => {
|
|
20
|
+
it("ignores example hook files", async () => {
|
|
21
|
+
const { workspace } = await createWorkspaceWithHook(
|
|
22
|
+
"PreToolUse.example",
|
|
23
|
+
'echo \'HOOK_CONTROL\t{"cancel":true,"context":"should-not-run"}\'\n',
|
|
24
|
+
);
|
|
25
|
+
try {
|
|
26
|
+
const hooks = createHookConfigFileHooks({
|
|
27
|
+
cwd: workspace,
|
|
28
|
+
workspacePath: workspace,
|
|
29
|
+
});
|
|
30
|
+
expect(hooks).toBeUndefined();
|
|
31
|
+
} finally {
|
|
32
|
+
await rm(workspace, { recursive: true, force: true });
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
20
36
|
it("executes extensionless legacy hook files via bash fallback", async () => {
|
|
21
37
|
const { workspace } = await createWorkspaceWithHook(
|
|
22
38
|
"PreToolUse",
|
|
@@ -103,4 +119,35 @@ describe("createHookConfigFileHooks", () => {
|
|
|
103
119
|
await rm(workspace, { recursive: true, force: true });
|
|
104
120
|
}
|
|
105
121
|
});
|
|
122
|
+
|
|
123
|
+
it("executes python hook files", async () => {
|
|
124
|
+
const { workspace } = await createWorkspaceWithHook(
|
|
125
|
+
"PreToolUse.py",
|
|
126
|
+
'print(\'HOOK_CONTROL\\t{"cancel": false, "context": "python-ok"}\')\n',
|
|
127
|
+
);
|
|
128
|
+
try {
|
|
129
|
+
const hooks = createHookConfigFileHooks({
|
|
130
|
+
cwd: workspace,
|
|
131
|
+
workspacePath: workspace,
|
|
132
|
+
});
|
|
133
|
+
expect(hooks?.onToolCallStart).toBeTypeOf("function");
|
|
134
|
+
const control = await hooks?.onToolCallStart?.({
|
|
135
|
+
agentId: "agent_1",
|
|
136
|
+
conversationId: "conv_1",
|
|
137
|
+
parentAgentId: null,
|
|
138
|
+
iteration: 1,
|
|
139
|
+
call: {
|
|
140
|
+
id: "call_1",
|
|
141
|
+
name: "read_file",
|
|
142
|
+
input: { path: "README.md" },
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
expect(control).toMatchObject({
|
|
146
|
+
cancel: false,
|
|
147
|
+
context: "python-ok",
|
|
148
|
+
});
|
|
149
|
+
} finally {
|
|
150
|
+
await rm(workspace, { recursive: true, force: true });
|
|
151
|
+
}
|
|
152
|
+
});
|
|
106
153
|
});
|
|
@@ -323,6 +323,9 @@ function inferHookCommand(path: string): string[] {
|
|
|
323
323
|
) {
|
|
324
324
|
return ["bun", "run", path];
|
|
325
325
|
}
|
|
326
|
+
if (lowered.endsWith(".py")) {
|
|
327
|
+
return ["python3", path];
|
|
328
|
+
}
|
|
326
329
|
// Default to bash for legacy hook files with no extension/shebang.
|
|
327
330
|
return ["/bin/bash", path];
|
|
328
331
|
}
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { Tool } from "@clinebot/agents";
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
|
+
import { TelemetryService } from "../telemetry/TelemetryService";
|
|
6
7
|
import { DefaultRuntimeBuilder } from "./runtime-builder";
|
|
7
8
|
|
|
8
9
|
function makeSpawnTool(): Tool {
|
|
@@ -55,6 +56,25 @@ describe("DefaultRuntimeBuilder", () => {
|
|
|
55
56
|
expect(runtime.logger).toBe(logger);
|
|
56
57
|
});
|
|
57
58
|
|
|
59
|
+
it("forwards telemetry for downstream runtime consumers", () => {
|
|
60
|
+
const telemetry = new TelemetryService();
|
|
61
|
+
const runtime = new DefaultRuntimeBuilder().build({
|
|
62
|
+
config: {
|
|
63
|
+
providerId: "anthropic",
|
|
64
|
+
modelId: "claude-sonnet-4-6",
|
|
65
|
+
apiKey: "key",
|
|
66
|
+
systemPrompt: "test",
|
|
67
|
+
cwd: process.cwd(),
|
|
68
|
+
enableTools: false,
|
|
69
|
+
enableSpawnAgent: false,
|
|
70
|
+
enableAgentTeams: false,
|
|
71
|
+
telemetry,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
expect(runtime.telemetry).toBe(telemetry);
|
|
76
|
+
});
|
|
77
|
+
|
|
58
78
|
it("uses readonly preset in plan mode", () => {
|
|
59
79
|
const runtime = new DefaultRuntimeBuilder().build({
|
|
60
80
|
config: {
|
|
@@ -5,7 +5,7 @@ import type {
|
|
|
5
5
|
AgentTeamsRuntime,
|
|
6
6
|
Tool,
|
|
7
7
|
} from "@clinebot/agents";
|
|
8
|
-
import type { BasicLogger } from "@clinebot/shared";
|
|
8
|
+
import type { BasicLogger, ITelemetryService } from "@clinebot/shared";
|
|
9
9
|
import type { UserInstructionConfigWatcher } from "../agents";
|
|
10
10
|
import type { ToolExecutors } from "../tools";
|
|
11
11
|
import type { CoreSessionConfig } from "../types/config";
|
|
@@ -14,6 +14,7 @@ export interface BuiltRuntime {
|
|
|
14
14
|
tools: Tool[];
|
|
15
15
|
hooks?: AgentHooks;
|
|
16
16
|
logger?: BasicLogger;
|
|
17
|
+
telemetry?: ITelemetryService;
|
|
17
18
|
teamRuntime?: AgentTeamsRuntime;
|
|
18
19
|
completionGuard?: () => string | undefined;
|
|
19
20
|
shutdown: (reason: string) => Promise<void> | void;
|
|
@@ -29,6 +30,7 @@ export interface RuntimeBuilderInput {
|
|
|
29
30
|
userInstructionWatcher?: UserInstructionConfigWatcher;
|
|
30
31
|
defaultToolExecutors?: Partial<ToolExecutors>;
|
|
31
32
|
logger?: BasicLogger;
|
|
33
|
+
telemetry?: ITelemetryService;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
export interface RuntimeBuilder {
|
|
@@ -3,6 +3,7 @@ import { tmpdir } from "node:os";
|
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { AgentResult } from "@clinebot/agents";
|
|
5
5
|
import { describe, expect, it, vi } from "vitest";
|
|
6
|
+
import { TelemetryService } from "../telemetry/TelemetryService";
|
|
6
7
|
import { SessionSource } from "../types/common";
|
|
7
8
|
import type { CoreSessionConfig } from "../types/config";
|
|
8
9
|
import { DefaultSessionManager } from "./default-session-manager";
|
|
@@ -70,6 +71,77 @@ function createConfig(
|
|
|
70
71
|
}
|
|
71
72
|
|
|
72
73
|
describe("DefaultSessionManager", () => {
|
|
74
|
+
it("emits session lifecycle telemetry when configured", async () => {
|
|
75
|
+
const sessionId = "sess-telemetry";
|
|
76
|
+
const manifest = createManifest(sessionId);
|
|
77
|
+
const adapter = {
|
|
78
|
+
name: "test",
|
|
79
|
+
emit: vi.fn(),
|
|
80
|
+
emitRequired: vi.fn(),
|
|
81
|
+
recordCounter: vi.fn(),
|
|
82
|
+
recordHistogram: vi.fn(),
|
|
83
|
+
recordGauge: vi.fn(),
|
|
84
|
+
isEnabled: vi.fn(() => true),
|
|
85
|
+
flush: vi.fn().mockResolvedValue(undefined),
|
|
86
|
+
dispose: vi.fn().mockResolvedValue(undefined),
|
|
87
|
+
};
|
|
88
|
+
const telemetry = new TelemetryService({
|
|
89
|
+
adapters: [adapter],
|
|
90
|
+
distinctId: distinctId,
|
|
91
|
+
});
|
|
92
|
+
const sessionService = {
|
|
93
|
+
ensureSessionsDir: vi.fn().mockReturnValue("/tmp/sessions"),
|
|
94
|
+
createRootSessionWithArtifacts: vi.fn().mockResolvedValue({
|
|
95
|
+
manifestPath: "/tmp/manifest.json",
|
|
96
|
+
transcriptPath: "/tmp/transcript.log",
|
|
97
|
+
hookPath: "/tmp/hook.log",
|
|
98
|
+
messagesPath: "/tmp/messages.json",
|
|
99
|
+
manifest,
|
|
100
|
+
}),
|
|
101
|
+
persistSessionMessages: vi.fn(),
|
|
102
|
+
updateSessionStatus: vi.fn().mockResolvedValue({
|
|
103
|
+
updated: true,
|
|
104
|
+
endedAt: "2026-01-01T00:00:05.000Z",
|
|
105
|
+
}),
|
|
106
|
+
writeSessionManifest: vi.fn(),
|
|
107
|
+
listSessions: vi.fn().mockResolvedValue([]),
|
|
108
|
+
deleteSession: vi.fn().mockResolvedValue({ deleted: true }),
|
|
109
|
+
};
|
|
110
|
+
const runtimeBuilder = {
|
|
111
|
+
build: vi.fn().mockReturnValue({
|
|
112
|
+
tools: [],
|
|
113
|
+
shutdown: vi.fn(),
|
|
114
|
+
}),
|
|
115
|
+
};
|
|
116
|
+
const agent = {
|
|
117
|
+
run: vi.fn().mockResolvedValue(createResult()),
|
|
118
|
+
continue: vi.fn().mockResolvedValue(createResult()),
|
|
119
|
+
getMessages: vi.fn().mockReturnValue([]),
|
|
120
|
+
abort: vi.fn(),
|
|
121
|
+
shutdown: vi.fn().mockResolvedValue(undefined),
|
|
122
|
+
};
|
|
123
|
+
const manager = new DefaultSessionManager({
|
|
124
|
+
distinctId,
|
|
125
|
+
sessionService: sessionService as never,
|
|
126
|
+
runtimeBuilder: runtimeBuilder as never,
|
|
127
|
+
createAgent: () => agent as never,
|
|
128
|
+
telemetry,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await manager.start({
|
|
132
|
+
config: createConfig({ telemetry, sessionId }),
|
|
133
|
+
prompt: "hello",
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(adapter.emit).toHaveBeenCalledWith(
|
|
137
|
+
"session.started",
|
|
138
|
+
expect.objectContaining({
|
|
139
|
+
sessionId,
|
|
140
|
+
distinct_id: distinctId,
|
|
141
|
+
}),
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
73
145
|
it("runs a non-interactive prompt and persists messages/status", async () => {
|
|
74
146
|
const sessionId = "sess-1";
|
|
75
147
|
const manifest = createManifest(sessionId);
|
|
@@ -14,7 +14,11 @@ 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";
|
|
@@ -89,6 +93,7 @@ export interface DefaultSessionManagerOptions {
|
|
|
89
93
|
toolPolicies?: AgentConfig["toolPolicies"];
|
|
90
94
|
providerSettingsManager?: ProviderSettingsManager;
|
|
91
95
|
oauthTokenManager?: RuntimeOAuthTokenManager;
|
|
96
|
+
telemetry?: ITelemetryService;
|
|
92
97
|
requestToolApproval?: (
|
|
93
98
|
request: ToolApprovalRequest,
|
|
94
99
|
) => Promise<ToolApprovalResult>;
|
|
@@ -120,6 +125,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
120
125
|
private readonly defaultToolPolicies?: AgentConfig["toolPolicies"];
|
|
121
126
|
private readonly providerSettingsManager: ProviderSettingsManager;
|
|
122
127
|
private readonly oauthTokenManager: RuntimeOAuthTokenManager;
|
|
128
|
+
private readonly defaultTelemetry?: ITelemetryService;
|
|
123
129
|
private readonly defaultRequestToolApproval?: (
|
|
124
130
|
request: ToolApprovalRequest,
|
|
125
131
|
) => Promise<ToolApprovalResult>;
|
|
@@ -143,6 +149,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
143
149
|
new RuntimeOAuthTokenManager({
|
|
144
150
|
providerSettingsManager: this.providerSettingsManager,
|
|
145
151
|
});
|
|
152
|
+
this.defaultTelemetry = options.telemetry;
|
|
146
153
|
this.defaultRequestToolApproval = options.requestToolApproval;
|
|
147
154
|
}
|
|
148
155
|
|
|
@@ -263,6 +270,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
263
270
|
...input.config,
|
|
264
271
|
hooks: effectiveHooks,
|
|
265
272
|
extensions: effectiveExtensions,
|
|
273
|
+
telemetry: input.config.telemetry ?? this.defaultTelemetry,
|
|
266
274
|
};
|
|
267
275
|
const providerConfig =
|
|
268
276
|
this.buildResolvedProviderConfig(effectiveConfigBase);
|
|
@@ -276,6 +284,7 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
276
284
|
hooks: effectiveHooks,
|
|
277
285
|
extensions: effectiveExtensions,
|
|
278
286
|
logger: effectiveConfig.logger,
|
|
287
|
+
telemetry: effectiveConfig.telemetry,
|
|
279
288
|
onTeamEvent: (event: TeamEvent) => {
|
|
280
289
|
void this.handleTeamEvent(sessionId, event);
|
|
281
290
|
effectiveConfig.onTeamEvent?.(event);
|
|
@@ -287,6 +296,18 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
287
296
|
input.defaultToolExecutors ?? this.defaultToolExecutors,
|
|
288
297
|
});
|
|
289
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
|
+
});
|
|
290
311
|
const agent = this.createAgentInstance({
|
|
291
312
|
providerId: providerConfig.providerId,
|
|
292
313
|
modelId: providerConfig.modelId,
|
|
@@ -326,6 +347,14 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
326
347
|
}),
|
|
327
348
|
);
|
|
328
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
|
+
}
|
|
329
358
|
this.emit({
|
|
330
359
|
type: "agent_event",
|
|
331
360
|
payload: {
|
|
@@ -397,6 +426,15 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
397
426
|
if (!session) {
|
|
398
427
|
throw new Error(`session not found: ${input.sessionId}`);
|
|
399
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
|
+
});
|
|
400
438
|
try {
|
|
401
439
|
const result = await this.runTurn(session, {
|
|
402
440
|
prompt: input.prompt,
|
|
@@ -428,6 +466,10 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
428
466
|
if (!session) {
|
|
429
467
|
return;
|
|
430
468
|
}
|
|
469
|
+
session.config.telemetry?.capture({
|
|
470
|
+
event: "session.aborted",
|
|
471
|
+
properties: { sessionId },
|
|
472
|
+
});
|
|
431
473
|
session.aborting = true;
|
|
432
474
|
session.agent.abort();
|
|
433
475
|
}
|
|
@@ -437,6 +479,10 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
437
479
|
if (!session) {
|
|
438
480
|
return;
|
|
439
481
|
}
|
|
482
|
+
session.config.telemetry?.capture({
|
|
483
|
+
event: "session.stopped",
|
|
484
|
+
properties: { sessionId },
|
|
485
|
+
});
|
|
440
486
|
await this.shutdownSession(session, {
|
|
441
487
|
status: "cancelled",
|
|
442
488
|
exitCode: null,
|
|
@@ -1200,4 +1246,16 @@ export class DefaultSessionManager implements SessionManager {
|
|
|
1200
1246
|
apiKey: resolved.apiKey,
|
|
1201
1247
|
});
|
|
1202
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
|
+
}
|
|
1203
1261
|
}
|
|
@@ -7,6 +7,7 @@ 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
13
|
import { SqliteSessionStore } from "../storage/sqlite-session-store";
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -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
|
+
});
|