@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.
Files changed (67) hide show
  1. package/dist/auth/cline.d.ts +2 -0
  2. package/dist/auth/codex.d.ts +5 -1
  3. package/dist/auth/oca.d.ts +7 -1
  4. package/dist/auth/types.d.ts +2 -0
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.node.d.ts +1 -0
  7. package/dist/index.node.js +164 -162
  8. package/dist/input/mention-enricher.d.ts +1 -0
  9. package/dist/providers/local-provider-service.d.ts +1 -1
  10. package/dist/runtime/session-runtime.d.ts +1 -1
  11. package/dist/session/default-session-manager.d.ts +13 -17
  12. package/dist/session/runtime-oauth-token-manager.d.ts +4 -2
  13. package/dist/session/session-agent-events.d.ts +15 -0
  14. package/dist/session/session-config-builder.d.ts +13 -0
  15. package/dist/session/session-manager.d.ts +2 -2
  16. package/dist/session/session-team-coordination.d.ts +12 -0
  17. package/dist/session/session-telemetry.d.ts +9 -0
  18. package/dist/session/unified-session-persistence-service.d.ts +12 -16
  19. package/dist/session/utils/helpers.d.ts +1 -1
  20. package/dist/session/utils/types.d.ts +1 -1
  21. package/dist/telemetry/core-events.d.ts +122 -0
  22. package/dist/tools/definitions.d.ts +1 -1
  23. package/dist/tools/executors/file-read.d.ts +1 -1
  24. package/dist/tools/index.d.ts +1 -1
  25. package/dist/tools/presets.d.ts +1 -1
  26. package/dist/tools/schemas.d.ts +46 -3
  27. package/dist/tools/types.d.ts +3 -3
  28. package/dist/types/config.d.ts +1 -1
  29. package/dist/types/provider-settings.d.ts +4 -4
  30. package/dist/types.d.ts +1 -1
  31. package/package.json +4 -3
  32. package/src/auth/cline.ts +35 -1
  33. package/src/auth/codex.ts +27 -2
  34. package/src/auth/oca.ts +31 -4
  35. package/src/auth/types.ts +3 -0
  36. package/src/index.ts +27 -0
  37. package/src/input/mention-enricher.test.ts +3 -0
  38. package/src/input/mention-enricher.ts +3 -0
  39. package/src/providers/local-provider-service.ts +6 -7
  40. package/src/runtime/hook-file-hooks.ts +11 -10
  41. package/src/runtime/session-runtime.ts +1 -1
  42. package/src/session/default-session-manager.e2e.test.ts +2 -1
  43. package/src/session/default-session-manager.ts +367 -601
  44. package/src/session/runtime-oauth-token-manager.ts +21 -14
  45. package/src/session/session-agent-events.ts +159 -0
  46. package/src/session/session-config-builder.ts +111 -0
  47. package/src/session/session-host.ts +13 -0
  48. package/src/session/session-manager.ts +2 -2
  49. package/src/session/session-team-coordination.ts +198 -0
  50. package/src/session/session-telemetry.ts +95 -0
  51. package/src/session/unified-session-persistence-service.test.ts +81 -0
  52. package/src/session/unified-session-persistence-service.ts +470 -469
  53. package/src/session/utils/helpers.ts +1 -1
  54. package/src/session/utils/types.ts +1 -1
  55. package/src/storage/provider-settings-legacy-migration.ts +3 -3
  56. package/src/telemetry/core-events.ts +344 -0
  57. package/src/tools/definitions.test.ts +121 -7
  58. package/src/tools/definitions.ts +60 -24
  59. package/src/tools/executors/file-read.test.ts +29 -5
  60. package/src/tools/executors/file-read.ts +17 -6
  61. package/src/tools/index.ts +2 -0
  62. package/src/tools/presets.ts +1 -1
  63. package/src/tools/schemas.ts +65 -5
  64. package/src/tools/types.ts +7 -3
  65. package/src/types/config.ts +1 -1
  66. package/src/types/provider-settings.ts +6 -6
  67. package/src/types.ts +1 -1
@@ -1,4 +1,9 @@
1
- import type { providers as LlmsProviders } from "@clinebot/llms";
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
- const MANAGED_OAUTH_PROVIDERS = ["cline", "oca", "openai-codex"] as const;
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?: { providerSettingsManager?: ProviderSettingsManager }) {
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 (!isManagedOAuthProviderId(input.providerId)) {
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, { forceRefresh });
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 { providers as LlmsProviders } from "@clinebot/llms";
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
+ }