@clinebot/core 0.0.7 → 0.0.10
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/auth/cline.d.ts +2 -0
- package/dist/auth/codex.d.ts +5 -1
- package/dist/auth/oca.d.ts +7 -1
- package/dist/auth/types.d.ts +2 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.node.d.ts +1 -0
- package/dist/index.node.js +164 -162
- package/dist/input/mention-enricher.d.ts +1 -0
- package/dist/providers/local-provider-service.d.ts +1 -1
- package/dist/runtime/session-runtime.d.ts +1 -1
- package/dist/session/default-session-manager.d.ts +13 -17
- package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
- package/dist/session/session-agent-events.d.ts +15 -0
- package/dist/session/session-config-builder.d.ts +13 -0
- package/dist/session/session-manager.d.ts +2 -2
- package/dist/session/session-team-coordination.d.ts +12 -0
- package/dist/session/session-telemetry.d.ts +9 -0
- package/dist/session/unified-session-persistence-service.d.ts +12 -16
- package/dist/session/utils/helpers.d.ts +1 -1
- package/dist/session/utils/types.d.ts +1 -1
- package/dist/telemetry/core-events.d.ts +122 -0
- package/dist/tools/definitions.d.ts +1 -1
- package/dist/tools/executors/file-read.d.ts +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/presets.d.ts +1 -1
- package/dist/tools/schemas.d.ts +46 -3
- package/dist/tools/types.d.ts +3 -3
- package/dist/types/config.d.ts +1 -1
- package/dist/types/provider-settings.d.ts +4 -4
- package/dist/types.d.ts +1 -1
- package/package.json +4 -3
- package/src/auth/cline.ts +35 -1
- package/src/auth/codex.ts +27 -2
- package/src/auth/oca.ts +31 -4
- package/src/auth/types.ts +3 -0
- package/src/index.ts +27 -0
- package/src/input/mention-enricher.test.ts +3 -0
- package/src/input/mention-enricher.ts +3 -0
- package/src/providers/local-provider-service.ts +6 -7
- package/src/runtime/hook-file-hooks.ts +11 -10
- package/src/runtime/session-runtime.ts +1 -1
- package/src/session/default-session-manager.e2e.test.ts +2 -1
- package/src/session/default-session-manager.ts +367 -601
- package/src/session/runtime-oauth-token-manager.ts +21 -14
- package/src/session/session-agent-events.ts +159 -0
- package/src/session/session-config-builder.ts +111 -0
- package/src/session/session-host.ts +13 -0
- package/src/session/session-manager.ts +2 -2
- package/src/session/session-team-coordination.ts +198 -0
- package/src/session/session-telemetry.ts +95 -0
- package/src/session/unified-session-persistence-service.test.ts +81 -0
- package/src/session/unified-session-persistence-service.ts +470 -469
- package/src/session/utils/helpers.ts +1 -1
- package/src/session/utils/types.ts +1 -1
- package/src/storage/provider-settings-legacy-migration.ts +3 -3
- package/src/telemetry/core-events.ts +344 -0
- package/src/tools/definitions.test.ts +121 -7
- package/src/tools/definitions.ts +60 -24
- package/src/tools/executors/file-read.test.ts +29 -5
- package/src/tools/executors/file-read.ts +17 -6
- package/src/tools/index.ts +2 -0
- package/src/tools/presets.ts +1 -1
- package/src/tools/schemas.ts +65 -5
- package/src/tools/types.ts +7 -3
- package/src/types/config.ts +1 -1
- package/src/types/provider-settings.ts +6 -6
- package/src/types.ts +1 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { LlmsProviders } from "@clinebot/llms";
|
|
2
|
+
import {
|
|
3
|
+
type ITelemetryService,
|
|
4
|
+
isOAuthProviderId,
|
|
5
|
+
type OAuthProviderId,
|
|
6
|
+
} from "@clinebot/shared";
|
|
2
7
|
import {
|
|
3
8
|
type ClineOAuthCredentials,
|
|
4
9
|
getValidClineCredentials,
|
|
@@ -11,14 +16,7 @@ import { ProviderSettingsManager } from "../storage/provider-settings-manager";
|
|
|
11
16
|
const DEFAULT_CLINE_API_BASE_URL = "https://api.cline.bot";
|
|
12
17
|
const WORKOS_TOKEN_PREFIX = "workos:";
|
|
13
18
|
|
|
14
|
-
|
|
15
|
-
type ManagedOAuthProviderId = (typeof MANAGED_OAUTH_PROVIDERS)[number];
|
|
16
|
-
|
|
17
|
-
function isManagedOAuthProviderId(
|
|
18
|
-
providerId: string,
|
|
19
|
-
): providerId is ManagedOAuthProviderId {
|
|
20
|
-
return (MANAGED_OAUTH_PROVIDERS as readonly string[]).includes(providerId);
|
|
21
|
-
}
|
|
19
|
+
type ManagedOAuthProviderId = OAuthProviderId;
|
|
22
20
|
|
|
23
21
|
function toStoredAccessToken(
|
|
24
22
|
providerId: ManagedOAuthProviderId,
|
|
@@ -143,21 +141,26 @@ export type RuntimeOAuthResolution = {
|
|
|
143
141
|
|
|
144
142
|
export class RuntimeOAuthTokenManager {
|
|
145
143
|
private readonly providerSettingsManager: ProviderSettingsManager;
|
|
144
|
+
private readonly telemetry?: ITelemetryService;
|
|
146
145
|
private readonly refreshInFlight = new Map<
|
|
147
146
|
ManagedOAuthProviderId,
|
|
148
147
|
Promise<RuntimeOAuthResolution | null>
|
|
149
148
|
>();
|
|
150
149
|
|
|
151
|
-
constructor(options?: {
|
|
150
|
+
constructor(options?: {
|
|
151
|
+
providerSettingsManager?: ProviderSettingsManager;
|
|
152
|
+
telemetry?: ITelemetryService;
|
|
153
|
+
}) {
|
|
152
154
|
this.providerSettingsManager =
|
|
153
155
|
options?.providerSettingsManager ?? new ProviderSettingsManager();
|
|
156
|
+
this.telemetry = options?.telemetry;
|
|
154
157
|
}
|
|
155
158
|
|
|
156
159
|
public async resolveProviderApiKey(input: {
|
|
157
160
|
providerId: string;
|
|
158
161
|
forceRefresh?: boolean;
|
|
159
162
|
}): Promise<RuntimeOAuthResolution | null> {
|
|
160
|
-
if (!
|
|
163
|
+
if (!isOAuthProviderId(input.providerId)) {
|
|
161
164
|
return null;
|
|
162
165
|
}
|
|
163
166
|
return this.resolveWithSingleFlight(input.providerId, input.forceRefresh);
|
|
@@ -249,6 +252,7 @@ export class RuntimeOAuthTokenManager {
|
|
|
249
252
|
currentCredentials,
|
|
250
253
|
{
|
|
251
254
|
apiBaseUrl: settings.baseUrl?.trim() || DEFAULT_CLINE_API_BASE_URL,
|
|
255
|
+
telemetry: this.telemetry,
|
|
252
256
|
},
|
|
253
257
|
{ forceRefresh },
|
|
254
258
|
);
|
|
@@ -256,10 +260,13 @@ export class RuntimeOAuthTokenManager {
|
|
|
256
260
|
if (providerId === "oca") {
|
|
257
261
|
return getValidOcaCredentials(
|
|
258
262
|
currentCredentials,
|
|
259
|
-
{ forceRefresh },
|
|
260
|
-
{ mode: settings.oca?.mode },
|
|
263
|
+
{ forceRefresh, telemetry: this.telemetry },
|
|
264
|
+
{ mode: settings.oca?.mode, telemetry: this.telemetry },
|
|
261
265
|
);
|
|
262
266
|
}
|
|
263
|
-
return getValidOpenAICodexCredentials(currentCredentials, {
|
|
267
|
+
return getValidOpenAICodexCredentials(currentCredentials, {
|
|
268
|
+
forceRefresh,
|
|
269
|
+
telemetry: this.telemetry,
|
|
270
|
+
});
|
|
264
271
|
}
|
|
265
272
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import type { AgentEvent } from "@clinebot/agents";
|
|
2
|
+
import {
|
|
3
|
+
captureConversationTurnEvent,
|
|
4
|
+
captureDiffEditFailure,
|
|
5
|
+
captureProviderApiError,
|
|
6
|
+
captureSkillUsed,
|
|
7
|
+
captureTokenUsage,
|
|
8
|
+
captureToolUsage,
|
|
9
|
+
} from "../telemetry/core-events";
|
|
10
|
+
import type { CoreSessionConfig } from "../types/config";
|
|
11
|
+
import type { CoreSessionEvent } from "../types/events";
|
|
12
|
+
import type { SessionAccumulatedUsage } from "./session-manager";
|
|
13
|
+
import { serializeAgentEvent } from "./utils/helpers";
|
|
14
|
+
import type { ActiveSession } from "./utils/types";
|
|
15
|
+
import { accumulateUsageTotals } from "./utils/usage";
|
|
16
|
+
|
|
17
|
+
export function extractSkillNameFromToolInput(
|
|
18
|
+
input: unknown,
|
|
19
|
+
): string | undefined {
|
|
20
|
+
if (!input || typeof input !== "object") return undefined;
|
|
21
|
+
const record = input as Record<string, unknown>;
|
|
22
|
+
const skillName = record.skill ?? record.skill_name ?? record.skillName;
|
|
23
|
+
if (typeof skillName !== "string") return undefined;
|
|
24
|
+
const trimmed = skillName.trim();
|
|
25
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AgentEventContext {
|
|
29
|
+
sessionId: string;
|
|
30
|
+
config: CoreSessionConfig;
|
|
31
|
+
liveSession: ActiveSession | undefined;
|
|
32
|
+
usageBySession: Map<string, SessionAccumulatedUsage>;
|
|
33
|
+
persistMessages: (
|
|
34
|
+
sessionId: string,
|
|
35
|
+
messages: unknown[],
|
|
36
|
+
systemPrompt?: string,
|
|
37
|
+
) => void;
|
|
38
|
+
emit: (event: CoreSessionEvent) => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function handleAgentEvent(
|
|
42
|
+
ctx: AgentEventContext,
|
|
43
|
+
event: AgentEvent,
|
|
44
|
+
): void {
|
|
45
|
+
const { sessionId, config, liveSession, emit } = ctx;
|
|
46
|
+
const telemetry = config.telemetry;
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
event.type === "content_start" &&
|
|
50
|
+
event.contentType === "tool" &&
|
|
51
|
+
event.toolName === "skills"
|
|
52
|
+
) {
|
|
53
|
+
const skillName = extractSkillNameFromToolInput(event.input);
|
|
54
|
+
if (skillName) {
|
|
55
|
+
captureSkillUsed(telemetry, {
|
|
56
|
+
ulid: sessionId,
|
|
57
|
+
skillName,
|
|
58
|
+
skillSource: "project",
|
|
59
|
+
skillsAvailableGlobal: 0,
|
|
60
|
+
skillsAvailableProject: 0,
|
|
61
|
+
provider: config.providerId,
|
|
62
|
+
modelId: config.modelId,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (event.type === "content_end" && event.contentType === "tool") {
|
|
68
|
+
const toolName = event.toolName ?? "unknown";
|
|
69
|
+
const success = !event.error;
|
|
70
|
+
captureToolUsage(telemetry, {
|
|
71
|
+
ulid: sessionId,
|
|
72
|
+
tool: toolName,
|
|
73
|
+
autoApproved: undefined,
|
|
74
|
+
success,
|
|
75
|
+
modelId: config.modelId,
|
|
76
|
+
provider: config.providerId,
|
|
77
|
+
isNativeToolCall: false,
|
|
78
|
+
});
|
|
79
|
+
if (!success && (toolName === "editor" || toolName === "apply_patch")) {
|
|
80
|
+
captureDiffEditFailure(telemetry, {
|
|
81
|
+
ulid: sessionId,
|
|
82
|
+
modelId: config.modelId,
|
|
83
|
+
provider: config.providerId,
|
|
84
|
+
errorType: event.error,
|
|
85
|
+
isNativeToolCall: false,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (event.type === "notice" && event.reason === "api_error") {
|
|
91
|
+
captureProviderApiError(telemetry, {
|
|
92
|
+
ulid: sessionId,
|
|
93
|
+
model: config.modelId,
|
|
94
|
+
provider: config.providerId,
|
|
95
|
+
errorMessage: event.message,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (event.type === "error") {
|
|
100
|
+
captureProviderApiError(telemetry, {
|
|
101
|
+
ulid: sessionId,
|
|
102
|
+
model: config.modelId,
|
|
103
|
+
provider: config.providerId,
|
|
104
|
+
errorMessage: event.error?.message ?? "unknown error",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (event.type === "usage" && liveSession?.turnUsageBaseline) {
|
|
109
|
+
ctx.usageBySession.set(
|
|
110
|
+
sessionId,
|
|
111
|
+
accumulateUsageTotals(liveSession.turnUsageBaseline, {
|
|
112
|
+
inputTokens: event.totalInputTokens,
|
|
113
|
+
outputTokens: event.totalOutputTokens,
|
|
114
|
+
totalCost: event.totalCost,
|
|
115
|
+
}),
|
|
116
|
+
);
|
|
117
|
+
captureConversationTurnEvent(telemetry, {
|
|
118
|
+
ulid: sessionId,
|
|
119
|
+
provider: config.providerId,
|
|
120
|
+
model: config.modelId,
|
|
121
|
+
source: "assistant",
|
|
122
|
+
mode: config.mode,
|
|
123
|
+
tokensIn: event.inputTokens,
|
|
124
|
+
tokensOut: event.outputTokens,
|
|
125
|
+
cacheWriteTokens: event.cacheWriteTokens,
|
|
126
|
+
cacheReadTokens: event.cacheReadTokens,
|
|
127
|
+
totalCost: event.cost,
|
|
128
|
+
isNativeToolCall: false,
|
|
129
|
+
});
|
|
130
|
+
captureTokenUsage(telemetry, {
|
|
131
|
+
ulid: sessionId,
|
|
132
|
+
tokensIn: event.inputTokens,
|
|
133
|
+
tokensOut: event.outputTokens,
|
|
134
|
+
model: config.modelId,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (event.type === "iteration_end") {
|
|
139
|
+
ctx.persistMessages(
|
|
140
|
+
sessionId,
|
|
141
|
+
liveSession?.agent.getMessages() ?? [],
|
|
142
|
+
liveSession?.config.systemPrompt,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
emit({
|
|
147
|
+
type: "agent_event",
|
|
148
|
+
payload: { sessionId, event },
|
|
149
|
+
});
|
|
150
|
+
emit({
|
|
151
|
+
type: "chunk",
|
|
152
|
+
payload: {
|
|
153
|
+
sessionId,
|
|
154
|
+
stream: "agent",
|
|
155
|
+
chunk: serializeAgentEvent(event),
|
|
156
|
+
ts: Date.now(),
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { LlmsProviders } from "@clinebot/llms";
|
|
2
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
3
|
+
import { resolveAndLoadAgentPlugins } from "../agents/plugin-config-loader";
|
|
4
|
+
import {
|
|
5
|
+
createHookAuditHooks,
|
|
6
|
+
createHookConfigFileHooks,
|
|
7
|
+
mergeAgentHooks,
|
|
8
|
+
} from "../runtime/hook-file-hooks";
|
|
9
|
+
import type { ProviderSettingsManager } from "../storage/provider-settings-manager";
|
|
10
|
+
import type { CoreSessionConfig } from "../types/config";
|
|
11
|
+
import {
|
|
12
|
+
type ProviderSettings,
|
|
13
|
+
toProviderConfig,
|
|
14
|
+
} from "../types/provider-settings";
|
|
15
|
+
import type { StartSessionInput } from "./session-manager";
|
|
16
|
+
import { hasRuntimeHooks, mergeAgentExtensions } from "./utils/helpers";
|
|
17
|
+
|
|
18
|
+
export function resolveWorkspacePath(config: CoreSessionConfig): string {
|
|
19
|
+
return config.workspaceRoot ?? config.cwd;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function buildEffectiveConfig(
|
|
23
|
+
input: StartSessionInput,
|
|
24
|
+
hookPath: string,
|
|
25
|
+
sessionId: string,
|
|
26
|
+
defaultTelemetry: ITelemetryService | undefined,
|
|
27
|
+
): Promise<{
|
|
28
|
+
config: CoreSessionConfig;
|
|
29
|
+
pluginSandboxShutdown?: () => Promise<void>;
|
|
30
|
+
}> {
|
|
31
|
+
const workspacePath = resolveWorkspacePath(input.config);
|
|
32
|
+
|
|
33
|
+
const fileHooks = createHookConfigFileHooks({
|
|
34
|
+
cwd: input.config.cwd,
|
|
35
|
+
workspacePath,
|
|
36
|
+
rootSessionId: sessionId,
|
|
37
|
+
hookLogPath: hookPath,
|
|
38
|
+
logger: input.config.logger,
|
|
39
|
+
});
|
|
40
|
+
const auditHooks = hasRuntimeHooks(input.config.hooks)
|
|
41
|
+
? undefined
|
|
42
|
+
: createHookAuditHooks({
|
|
43
|
+
hookLogPath: hookPath,
|
|
44
|
+
rootSessionId: sessionId,
|
|
45
|
+
workspacePath,
|
|
46
|
+
});
|
|
47
|
+
const effectiveHooks = mergeAgentHooks([
|
|
48
|
+
input.config.hooks,
|
|
49
|
+
fileHooks,
|
|
50
|
+
auditHooks,
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const loadedPlugins = await resolveAndLoadAgentPlugins({
|
|
54
|
+
pluginPaths: input.config.pluginPaths,
|
|
55
|
+
workspacePath,
|
|
56
|
+
cwd: input.config.cwd,
|
|
57
|
+
});
|
|
58
|
+
const effectiveExtensions = mergeAgentExtensions(
|
|
59
|
+
input.config.extensions,
|
|
60
|
+
loadedPlugins.extensions,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
config: {
|
|
65
|
+
...input.config,
|
|
66
|
+
hooks: effectiveHooks,
|
|
67
|
+
extensions: effectiveExtensions,
|
|
68
|
+
telemetry: input.config.telemetry ?? defaultTelemetry,
|
|
69
|
+
},
|
|
70
|
+
pluginSandboxShutdown: loadedPlugins.shutdown,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function buildResolvedProviderConfig(
|
|
75
|
+
config: CoreSessionConfig,
|
|
76
|
+
providerSettingsManager: ProviderSettingsManager,
|
|
77
|
+
resolveReasoningFn: (
|
|
78
|
+
config: CoreSessionConfig,
|
|
79
|
+
storedReasoning: ProviderSettings["reasoning"],
|
|
80
|
+
) => ProviderSettings["reasoning"],
|
|
81
|
+
): LlmsProviders.ProviderConfig {
|
|
82
|
+
const stored = providerSettingsManager.getProviderSettings(config.providerId);
|
|
83
|
+
const settings: ProviderSettings = {
|
|
84
|
+
...(stored ?? {}),
|
|
85
|
+
provider: config.providerId,
|
|
86
|
+
model: config.modelId,
|
|
87
|
+
apiKey: config.apiKey ?? stored?.apiKey,
|
|
88
|
+
baseUrl: config.baseUrl ?? stored?.baseUrl,
|
|
89
|
+
headers: config.headers ?? stored?.headers,
|
|
90
|
+
reasoning: resolveReasoningFn(config, stored?.reasoning),
|
|
91
|
+
};
|
|
92
|
+
const providerConfig = toProviderConfig(settings);
|
|
93
|
+
if (config.knownModels) {
|
|
94
|
+
providerConfig.knownModels = config.knownModels;
|
|
95
|
+
}
|
|
96
|
+
return providerConfig;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function resolveReasoningSettings(
|
|
100
|
+
config: CoreSessionConfig,
|
|
101
|
+
storedReasoning: ProviderSettings["reasoning"],
|
|
102
|
+
): ProviderSettings["reasoning"] {
|
|
103
|
+
const hasThinking = typeof config.thinking === "boolean";
|
|
104
|
+
const hasEffort = typeof config.reasoningEffort === "string";
|
|
105
|
+
if (!hasThinking && !hasEffort) return storedReasoning;
|
|
106
|
+
return {
|
|
107
|
+
...(storedReasoning ?? {}),
|
|
108
|
+
...(hasThinking ? { enabled: config.thinking } : {}),
|
|
109
|
+
...(hasEffort ? { effort: config.reasoningEffort } : {}),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
@@ -44,6 +44,15 @@ export interface CreateSessionHostOptions {
|
|
|
44
44
|
|
|
45
45
|
export type SessionHost = SessionManager;
|
|
46
46
|
|
|
47
|
+
async function reconcileDeadSessionsIfSupported(
|
|
48
|
+
backend: SessionBackend,
|
|
49
|
+
): Promise<void> {
|
|
50
|
+
const service = backend as SessionBackend & {
|
|
51
|
+
reconcileDeadSessions?: (limit?: number) => Promise<number>;
|
|
52
|
+
};
|
|
53
|
+
await service.reconcileDeadSessions?.().catch(() => {});
|
|
54
|
+
}
|
|
55
|
+
|
|
47
56
|
function startRpcServerInBackground(address: string): void {
|
|
48
57
|
const lease = tryAcquireRpcSpawnLease(address);
|
|
49
58
|
if (!lease) {
|
|
@@ -156,12 +165,14 @@ export async function resolveSessionBackend(
|
|
|
156
165
|
backendInitPromise = (async () => {
|
|
157
166
|
if (mode === "local") {
|
|
158
167
|
cachedBackend = createLocalBackend();
|
|
168
|
+
await reconcileDeadSessionsIfSupported(cachedBackend);
|
|
159
169
|
return cachedBackend;
|
|
160
170
|
}
|
|
161
171
|
|
|
162
172
|
const existingRpcBackend = await tryConnectRpcBackend(address);
|
|
163
173
|
if (existingRpcBackend) {
|
|
164
174
|
cachedBackend = existingRpcBackend;
|
|
175
|
+
await reconcileDeadSessionsIfSupported(cachedBackend);
|
|
165
176
|
return cachedBackend;
|
|
166
177
|
}
|
|
167
178
|
|
|
@@ -180,6 +191,7 @@ export async function resolveSessionBackend(
|
|
|
180
191
|
const rpcBackend = await tryConnectRpcBackend(address);
|
|
181
192
|
if (rpcBackend) {
|
|
182
193
|
cachedBackend = rpcBackend;
|
|
194
|
+
await reconcileDeadSessionsIfSupported(cachedBackend);
|
|
183
195
|
return cachedBackend;
|
|
184
196
|
}
|
|
185
197
|
if (delayMs > 0) {
|
|
@@ -189,6 +201,7 @@ export async function resolveSessionBackend(
|
|
|
189
201
|
}
|
|
190
202
|
|
|
191
203
|
cachedBackend = createLocalBackend();
|
|
204
|
+
await reconcileDeadSessionsIfSupported(cachedBackend);
|
|
192
205
|
return cachedBackend;
|
|
193
206
|
})().finally(() => {
|
|
194
207
|
backendInitPromise = undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentResult } from "@clinebot/agents";
|
|
2
|
-
import type {
|
|
2
|
+
import type { LlmsProviders } from "@clinebot/llms";
|
|
3
3
|
import type { SessionSource } from "../types/common";
|
|
4
4
|
import type { CoreSessionConfig } from "../types/config";
|
|
5
5
|
import type { CoreSessionEvent } from "../types/events";
|
|
@@ -54,7 +54,7 @@ export interface SessionManager {
|
|
|
54
54
|
getAccumulatedUsage(
|
|
55
55
|
sessionId: string,
|
|
56
56
|
): Promise<SessionAccumulatedUsage | undefined>;
|
|
57
|
-
abort(sessionId: string): Promise<void>;
|
|
57
|
+
abort(sessionId: string, reason?: unknown): Promise<void>;
|
|
58
58
|
stop(sessionId: string): Promise<void>;
|
|
59
59
|
dispose(reason?: string): Promise<void>;
|
|
60
60
|
get(sessionId: string): Promise<SessionRecord | undefined>;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import type { AgentResult, TeamEvent } from "@clinebot/agents";
|
|
2
|
+
import { formatUserInputBlock } from "@clinebot/shared";
|
|
3
|
+
import {
|
|
4
|
+
buildTeamProgressSummary,
|
|
5
|
+
toTeamProgressLifecycleEvent,
|
|
6
|
+
} from "../team";
|
|
7
|
+
import type { CoreSessionEvent } from "../types/events";
|
|
8
|
+
import type { ActiveSession, TeamRunUpdate } from "./utils/types";
|
|
9
|
+
|
|
10
|
+
export function trackTeamRunState(
|
|
11
|
+
session: ActiveSession,
|
|
12
|
+
event: TeamEvent,
|
|
13
|
+
): void {
|
|
14
|
+
switch (event.type) {
|
|
15
|
+
case "run_queued":
|
|
16
|
+
case "run_started":
|
|
17
|
+
session.activeTeamRunIds.add(event.run.id);
|
|
18
|
+
break;
|
|
19
|
+
case "run_completed":
|
|
20
|
+
case "run_failed":
|
|
21
|
+
case "run_cancelled":
|
|
22
|
+
case "run_interrupted": {
|
|
23
|
+
let runError: string | undefined;
|
|
24
|
+
if (event.type === "run_failed") {
|
|
25
|
+
runError = event.run.error;
|
|
26
|
+
} else if (
|
|
27
|
+
event.type === "run_cancelled" ||
|
|
28
|
+
event.type === "run_interrupted"
|
|
29
|
+
) {
|
|
30
|
+
runError = event.run.error ?? event.reason;
|
|
31
|
+
}
|
|
32
|
+
session.activeTeamRunIds.delete(event.run.id);
|
|
33
|
+
session.pendingTeamRunUpdates.push({
|
|
34
|
+
runId: event.run.id,
|
|
35
|
+
agentId: event.run.agentId,
|
|
36
|
+
taskId: event.run.taskId,
|
|
37
|
+
status: event.type.replace("run_", "") as TeamRunUpdate["status"],
|
|
38
|
+
error: runError,
|
|
39
|
+
iterations: event.run.result?.iterations,
|
|
40
|
+
});
|
|
41
|
+
notifyTeamRunWaiters(session);
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
default:
|
|
45
|
+
break;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function dispatchTeamEventToBackend(
|
|
50
|
+
rootSessionId: string,
|
|
51
|
+
event: TeamEvent,
|
|
52
|
+
invokeOptional: (method: string, ...args: unknown[]) => Promise<void>,
|
|
53
|
+
): Promise<void> {
|
|
54
|
+
switch (event.type) {
|
|
55
|
+
case "task_start":
|
|
56
|
+
await invokeOptional(
|
|
57
|
+
"onTeamTaskStart",
|
|
58
|
+
rootSessionId,
|
|
59
|
+
event.agentId,
|
|
60
|
+
event.message,
|
|
61
|
+
);
|
|
62
|
+
break;
|
|
63
|
+
case "task_end": {
|
|
64
|
+
if (event.error) {
|
|
65
|
+
await invokeOptional(
|
|
66
|
+
"onTeamTaskEnd",
|
|
67
|
+
rootSessionId,
|
|
68
|
+
event.agentId,
|
|
69
|
+
"failed",
|
|
70
|
+
`[error] ${event.error.message}`,
|
|
71
|
+
event.messages,
|
|
72
|
+
);
|
|
73
|
+
} else if (event.result?.finishReason === "aborted") {
|
|
74
|
+
await invokeOptional(
|
|
75
|
+
"onTeamTaskEnd",
|
|
76
|
+
rootSessionId,
|
|
77
|
+
event.agentId,
|
|
78
|
+
"cancelled",
|
|
79
|
+
"[done] aborted",
|
|
80
|
+
event.result.messages,
|
|
81
|
+
);
|
|
82
|
+
} else {
|
|
83
|
+
await invokeOptional(
|
|
84
|
+
"onTeamTaskEnd",
|
|
85
|
+
rootSessionId,
|
|
86
|
+
event.agentId,
|
|
87
|
+
"completed",
|
|
88
|
+
`[done] ${event.result?.finishReason ?? "completed"}`,
|
|
89
|
+
event.result?.messages,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
default:
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function emitTeamProgress(
|
|
100
|
+
session: ActiveSession,
|
|
101
|
+
rootSessionId: string,
|
|
102
|
+
event: TeamEvent,
|
|
103
|
+
emit: (event: CoreSessionEvent) => void,
|
|
104
|
+
): void {
|
|
105
|
+
if (!session.runtime.teamRuntime) return;
|
|
106
|
+
const teamName = session.config.teamName?.trim() || "team";
|
|
107
|
+
emit({
|
|
108
|
+
type: "team_progress",
|
|
109
|
+
payload: {
|
|
110
|
+
sessionId: rootSessionId,
|
|
111
|
+
teamName,
|
|
112
|
+
lifecycle: toTeamProgressLifecycleEvent({
|
|
113
|
+
teamName,
|
|
114
|
+
sessionId: rootSessionId,
|
|
115
|
+
event,
|
|
116
|
+
}),
|
|
117
|
+
summary: buildTeamProgressSummary(
|
|
118
|
+
teamName,
|
|
119
|
+
session.runtime.teamRuntime.exportState(),
|
|
120
|
+
),
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function hasPendingTeamRunWork(session: ActiveSession): boolean {
|
|
126
|
+
return (
|
|
127
|
+
session.activeTeamRunIds.size > 0 ||
|
|
128
|
+
session.pendingTeamRunUpdates.length > 0
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function shouldAutoContinueTeamRuns(
|
|
133
|
+
session: ActiveSession,
|
|
134
|
+
finishReason: AgentResult["finishReason"],
|
|
135
|
+
): boolean {
|
|
136
|
+
if (
|
|
137
|
+
session.aborting ||
|
|
138
|
+
finishReason === "aborted" ||
|
|
139
|
+
finishReason === "error"
|
|
140
|
+
) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
session.config.enableAgentTeams === true && hasPendingTeamRunWork(session)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function notifyTeamRunWaiters(session: ActiveSession): void {
|
|
149
|
+
const waiters = session.teamRunWaiters.splice(0);
|
|
150
|
+
for (const resolve of waiters) resolve();
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function waitForTeamRunUpdates(
|
|
154
|
+
session: ActiveSession,
|
|
155
|
+
): Promise<TeamRunUpdate[]> {
|
|
156
|
+
while (true) {
|
|
157
|
+
if (session.aborting) return [];
|
|
158
|
+
if (session.pendingTeamRunUpdates.length > 0) {
|
|
159
|
+
const updates = [...session.pendingTeamRunUpdates];
|
|
160
|
+
session.pendingTeamRunUpdates.length = 0;
|
|
161
|
+
return updates;
|
|
162
|
+
}
|
|
163
|
+
if (session.activeTeamRunIds.size === 0) return [];
|
|
164
|
+
await new Promise<void>((resolve) => {
|
|
165
|
+
session.teamRunWaiters.push(resolve);
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function buildTeamRunContinuationPrompt(
|
|
171
|
+
session: ActiveSession,
|
|
172
|
+
updates: TeamRunUpdate[],
|
|
173
|
+
): string {
|
|
174
|
+
const lines = updates.map((u) => {
|
|
175
|
+
const parts = [`- ${u.runId} (${u.agentId}) -> ${u.status}`];
|
|
176
|
+
if (u.taskId) parts.push(` task=${u.taskId}`);
|
|
177
|
+
if (typeof u.iterations === "number")
|
|
178
|
+
parts.push(` iterations=${u.iterations}`);
|
|
179
|
+
if (u.error) parts.push(` error=${u.error}`);
|
|
180
|
+
return parts.join("");
|
|
181
|
+
});
|
|
182
|
+
const remaining = session.activeTeamRunIds.size;
|
|
183
|
+
const instruction =
|
|
184
|
+
remaining > 0
|
|
185
|
+
? `There are still ${remaining} teammate run(s) in progress. Continue coordination and decide whether to wait for more updates.`
|
|
186
|
+
: "No teammate runs are currently in progress. Continue coordination using these updates.";
|
|
187
|
+
return formatModePrompt(
|
|
188
|
+
`System-delivered teammate async run updates:\n${lines.join("\n")}\n\n${instruction}`,
|
|
189
|
+
session.config.mode,
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function formatModePrompt(
|
|
194
|
+
prompt: string,
|
|
195
|
+
mode: "act" | "plan" | undefined,
|
|
196
|
+
): string {
|
|
197
|
+
return formatUserInputBlock(prompt, mode === "plan" ? "plan" : "act");
|
|
198
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { ITelemetryService } from "@clinebot/shared";
|
|
2
|
+
import {
|
|
3
|
+
listHookConfigFiles,
|
|
4
|
+
resolveDocumentsHooksDirectoryPath,
|
|
5
|
+
} from "../agents/hooks-config-loader";
|
|
6
|
+
import type { enrichPromptWithMentions } from "../input";
|
|
7
|
+
import {
|
|
8
|
+
captureHookDiscovery,
|
|
9
|
+
captureMentionFailed,
|
|
10
|
+
captureMentionSearchResults,
|
|
11
|
+
captureMentionUsed,
|
|
12
|
+
captureTaskCreated,
|
|
13
|
+
captureTaskRestarted,
|
|
14
|
+
} from "../telemetry/core-events";
|
|
15
|
+
import type { SessionSource } from "../types/common";
|
|
16
|
+
import type { CoreSessionConfig } from "../types/config";
|
|
17
|
+
|
|
18
|
+
export function emitSessionCreationTelemetry(
|
|
19
|
+
config: CoreSessionConfig,
|
|
20
|
+
sessionId: string,
|
|
21
|
+
source: SessionSource,
|
|
22
|
+
isRestart: boolean,
|
|
23
|
+
workspacePath: string,
|
|
24
|
+
): void {
|
|
25
|
+
if (isRestart) {
|
|
26
|
+
captureTaskRestarted(config.telemetry, {
|
|
27
|
+
ulid: sessionId,
|
|
28
|
+
apiProvider: config.providerId,
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
captureTaskCreated(config.telemetry, {
|
|
32
|
+
ulid: sessionId,
|
|
33
|
+
apiProvider: config.providerId,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
captureHookDiscoveryTelemetry(config.telemetry, { workspacePath });
|
|
37
|
+
config.telemetry?.capture({
|
|
38
|
+
event: "session.started",
|
|
39
|
+
properties: {
|
|
40
|
+
sessionId,
|
|
41
|
+
source,
|
|
42
|
+
providerId: config.providerId,
|
|
43
|
+
modelId: config.modelId,
|
|
44
|
+
enableTools: config.enableTools,
|
|
45
|
+
enableSpawnAgent: config.enableSpawnAgent,
|
|
46
|
+
enableAgentTeams: config.enableAgentTeams,
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function captureHookDiscoveryTelemetry(
|
|
52
|
+
telemetry: ITelemetryService | undefined,
|
|
53
|
+
options: { workspacePath: string },
|
|
54
|
+
): void {
|
|
55
|
+
const globalHooksDir = resolveDocumentsHooksDirectoryPath();
|
|
56
|
+
const entries = listHookConfigFiles(options.workspacePath);
|
|
57
|
+
const counts = new Map<string, { global: number; workspace: number }>();
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const hookName = entry.hookEventName ?? "unknown";
|
|
60
|
+
const current = counts.get(hookName) ?? { global: 0, workspace: 0 };
|
|
61
|
+
if (
|
|
62
|
+
entry.path === globalHooksDir ||
|
|
63
|
+
entry.path.startsWith(`${globalHooksDir}/`)
|
|
64
|
+
) {
|
|
65
|
+
current.global += 1;
|
|
66
|
+
} else {
|
|
67
|
+
current.workspace += 1;
|
|
68
|
+
}
|
|
69
|
+
counts.set(hookName, current);
|
|
70
|
+
}
|
|
71
|
+
for (const [hookName, count] of counts.entries()) {
|
|
72
|
+
captureHookDiscovery(telemetry, hookName, count.global, count.workspace);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function emitMentionTelemetry(
|
|
77
|
+
telemetry: ITelemetryService | undefined,
|
|
78
|
+
enriched: Awaited<ReturnType<typeof enrichPromptWithMentions>>,
|
|
79
|
+
): void {
|
|
80
|
+
for (const mention of enriched.mentions) {
|
|
81
|
+
captureMentionSearchResults(
|
|
82
|
+
telemetry,
|
|
83
|
+
mention,
|
|
84
|
+
enriched.matchedFiles.includes(mention) ? 1 : 0,
|
|
85
|
+
"file",
|
|
86
|
+
!enriched.matchedFiles.includes(mention),
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
for (const matched of enriched.matchedFiles) {
|
|
90
|
+
captureMentionUsed(telemetry, "file", matched.length);
|
|
91
|
+
}
|
|
92
|
+
for (const ignored of enriched.ignoredMentions) {
|
|
93
|
+
captureMentionFailed(telemetry, "file", "not_found", ignored);
|
|
94
|
+
}
|
|
95
|
+
}
|