@clinebot/agents 0.0.0

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 (90) hide show
  1. package/README.md +145 -0
  2. package/dist/agent-input.d.ts +2 -0
  3. package/dist/agent.d.ts +56 -0
  4. package/dist/extensions.d.ts +21 -0
  5. package/dist/hooks/engine.d.ts +42 -0
  6. package/dist/hooks/index.d.ts +2 -0
  7. package/dist/hooks/lifecycle.d.ts +5 -0
  8. package/dist/hooks/node.d.ts +2 -0
  9. package/dist/hooks/subprocess-runner.d.ts +16 -0
  10. package/dist/hooks/subprocess.d.ts +268 -0
  11. package/dist/index.browser.d.ts +1 -0
  12. package/dist/index.browser.js +49 -0
  13. package/dist/index.d.ts +15 -0
  14. package/dist/index.js +49 -0
  15. package/dist/index.node.d.ts +5 -0
  16. package/dist/index.node.js +49 -0
  17. package/dist/mcp/index.d.ts +4 -0
  18. package/dist/mcp/policies.d.ts +14 -0
  19. package/dist/mcp/tools.d.ts +9 -0
  20. package/dist/mcp/types.d.ts +35 -0
  21. package/dist/message-builder.d.ts +31 -0
  22. package/dist/prompts/cline.d.ts +1 -0
  23. package/dist/prompts/index.d.ts +1 -0
  24. package/dist/runtime/agent-runtime-bus.d.ts +13 -0
  25. package/dist/runtime/conversation-store.d.ts +16 -0
  26. package/dist/runtime/lifecycle-orchestrator.d.ts +28 -0
  27. package/dist/runtime/tool-orchestrator.d.ts +39 -0
  28. package/dist/runtime/turn-processor.d.ts +21 -0
  29. package/dist/teams/index.d.ts +3 -0
  30. package/dist/teams/multi-agent.d.ts +566 -0
  31. package/dist/teams/spawn-agent-tool.d.ts +85 -0
  32. package/dist/teams/team-tools.d.ts +51 -0
  33. package/dist/tools/ask-question.d.ts +12 -0
  34. package/dist/tools/create.d.ts +59 -0
  35. package/dist/tools/execution.d.ts +61 -0
  36. package/dist/tools/formatting.d.ts +20 -0
  37. package/dist/tools/index.d.ts +11 -0
  38. package/dist/tools/registry.d.ts +26 -0
  39. package/dist/tools/validation.d.ts +27 -0
  40. package/dist/types.d.ts +826 -0
  41. package/package.json +54 -0
  42. package/src/agent-input.ts +116 -0
  43. package/src/agent.test.ts +931 -0
  44. package/src/agent.ts +1050 -0
  45. package/src/example.test.ts +564 -0
  46. package/src/extensions.ts +337 -0
  47. package/src/hooks/engine.test.ts +163 -0
  48. package/src/hooks/engine.ts +537 -0
  49. package/src/hooks/index.ts +6 -0
  50. package/src/hooks/lifecycle.ts +239 -0
  51. package/src/hooks/node.ts +18 -0
  52. package/src/hooks/subprocess-runner.ts +140 -0
  53. package/src/hooks/subprocess.test.ts +180 -0
  54. package/src/hooks/subprocess.ts +620 -0
  55. package/src/index.browser.ts +1 -0
  56. package/src/index.node.ts +21 -0
  57. package/src/index.ts +133 -0
  58. package/src/mcp/index.ts +17 -0
  59. package/src/mcp/policies.test.ts +51 -0
  60. package/src/mcp/policies.ts +53 -0
  61. package/src/mcp/tools.test.ts +76 -0
  62. package/src/mcp/tools.ts +60 -0
  63. package/src/mcp/types.ts +41 -0
  64. package/src/message-builder.test.ts +175 -0
  65. package/src/message-builder.ts +429 -0
  66. package/src/prompts/cline.ts +49 -0
  67. package/src/prompts/index.ts +1 -0
  68. package/src/runtime/agent-runtime-bus.ts +53 -0
  69. package/src/runtime/conversation-store.ts +61 -0
  70. package/src/runtime/lifecycle-orchestrator.ts +90 -0
  71. package/src/runtime/tool-orchestrator.ts +177 -0
  72. package/src/runtime/turn-processor.ts +250 -0
  73. package/src/streaming.test.ts +197 -0
  74. package/src/streaming.ts +307 -0
  75. package/src/teams/index.ts +63 -0
  76. package/src/teams/multi-agent.lifecycle.test.ts +48 -0
  77. package/src/teams/multi-agent.ts +1866 -0
  78. package/src/teams/spawn-agent-tool.test.ts +172 -0
  79. package/src/teams/spawn-agent-tool.ts +223 -0
  80. package/src/teams/team-tools.test.ts +448 -0
  81. package/src/teams/team-tools.ts +929 -0
  82. package/src/tools/ask-question.ts +78 -0
  83. package/src/tools/create.ts +104 -0
  84. package/src/tools/execution.ts +311 -0
  85. package/src/tools/formatting.ts +73 -0
  86. package/src/tools/index.ts +45 -0
  87. package/src/tools/registry.ts +52 -0
  88. package/src/tools/tools.test.ts +292 -0
  89. package/src/tools/validation.ts +73 -0
  90. package/src/types.ts +966 -0
@@ -0,0 +1,429 @@
1
+ import type { providers as LlmsProviders } from "@clinebot/llms";
2
+
3
+ const DEFAULT_MAX_TOOL_RESULT_CHARS = 50_000;
4
+ const TARGET_TOOL_NAMES = new Set([
5
+ "read",
6
+ "read_files",
7
+ "bash",
8
+ "run_commands",
9
+ ]);
10
+ const READ_TOOL_NAMES = new Set(["read", "read_files"]);
11
+ const KEEP_CHARS_PER_SIDE = 50_000;
12
+ const OUTDATED_FILE_CONTENT = "[outdated - see the latest file content]";
13
+
14
+ interface ReadResultRecord {
15
+ toolUseId: string;
16
+ paths: string[];
17
+ }
18
+
19
+ /**
20
+ * Builds an API-safe message copy without mutating original conversation history.
21
+ */
22
+ export class MessageBuilder {
23
+ private indexedMessageCount = 0;
24
+ private indexedTailRef: LlmsProviders.Message | undefined;
25
+ private readonly toolNameByIdCache = new Map<string, string>();
26
+ private readonly readPathsByToolUseIdCache = new Map<string, string[]>();
27
+ private readonly latestReadToolUseByPathCache = new Map<string, string>();
28
+ private readResultPathCache = new WeakMap<object, string[]>();
29
+
30
+ constructor(
31
+ private readonly maxToolResultChars = DEFAULT_MAX_TOOL_RESULT_CHARS,
32
+ private readonly targetToolNames = TARGET_TOOL_NAMES,
33
+ ) {}
34
+
35
+ buildForApi(messages: LlmsProviders.Message[]): LlmsProviders.Message[] {
36
+ this.reindex(messages);
37
+ const toolNameById = this.toolNameByIdCache;
38
+ const readPathsByToolUseId = this.readPathsByToolUseIdCache;
39
+ const latestReadToolUseByPath = this.latestReadToolUseByPathCache;
40
+
41
+ return messages.map((message) => {
42
+ if (!Array.isArray(message.content)) {
43
+ return message;
44
+ }
45
+
46
+ const content = message.content.map((block) => {
47
+ if (block.type === "file") {
48
+ const truncated = this.truncateMiddle(block.content);
49
+ if (truncated === block.content) {
50
+ return block;
51
+ }
52
+ return {
53
+ ...block,
54
+ content: truncated,
55
+ };
56
+ }
57
+
58
+ if (block.type !== "tool_result") {
59
+ return block;
60
+ }
61
+
62
+ const toolName = toolNameById.get(block.tool_use_id);
63
+ let nextContent = block.content;
64
+
65
+ if (this.isReadTool(toolName)) {
66
+ const readRecord = this.getReadResultRecord(
67
+ block,
68
+ readPathsByToolUseId.get(block.tool_use_id),
69
+ );
70
+ if (readRecord) {
71
+ const outdatedPaths = readRecord.paths.filter(
72
+ (path) => latestReadToolUseByPath.get(path) !== block.tool_use_id,
73
+ );
74
+ if (outdatedPaths.length > 0) {
75
+ nextContent = this.replaceOutdatedReadContent(
76
+ nextContent,
77
+ outdatedPaths,
78
+ );
79
+ }
80
+ }
81
+ }
82
+
83
+ if (this.shouldTruncateTool(toolName)) {
84
+ nextContent = this.truncateToolResultContent(nextContent);
85
+ }
86
+
87
+ if (nextContent === block.content) {
88
+ return block;
89
+ }
90
+
91
+ return {
92
+ ...block,
93
+ content: nextContent,
94
+ };
95
+ });
96
+
97
+ return {
98
+ ...message,
99
+ content,
100
+ };
101
+ });
102
+ }
103
+
104
+ private reindex(messages: LlmsProviders.Message[]): void {
105
+ if (messages.length < this.indexedMessageCount) {
106
+ this.resetIndexes();
107
+ }
108
+ if (
109
+ this.indexedMessageCount > 0 &&
110
+ messages.length >= this.indexedMessageCount &&
111
+ messages[this.indexedMessageCount - 1] !== this.indexedTailRef
112
+ ) {
113
+ this.resetIndexes();
114
+ }
115
+
116
+ for (let i = this.indexedMessageCount; i < messages.length; i++) {
117
+ const message = messages[i];
118
+ if (!Array.isArray(message.content)) {
119
+ continue;
120
+ }
121
+
122
+ for (const block of message.content) {
123
+ if (block.type === "tool_use") {
124
+ const normalizedName = block.name.toLowerCase();
125
+ this.toolNameByIdCache.set(block.id, normalizedName);
126
+ if (this.isReadTool(normalizedName)) {
127
+ const paths = this.extractPathsFromReadToolInput(block.input);
128
+ if (paths.length > 0) {
129
+ this.readPathsByToolUseIdCache.set(block.id, paths);
130
+ }
131
+ }
132
+ continue;
133
+ }
134
+ if (block.type === "tool_result") {
135
+ const toolName = this.toolNameByIdCache.get(block.tool_use_id);
136
+ if (!this.isReadTool(toolName)) {
137
+ continue;
138
+ }
139
+ const readRecord = this.getReadResultRecord(
140
+ block,
141
+ this.readPathsByToolUseIdCache.get(block.tool_use_id),
142
+ );
143
+ if (!readRecord) {
144
+ continue;
145
+ }
146
+ for (const path of readRecord.paths) {
147
+ this.latestReadToolUseByPathCache.set(path, readRecord.toolUseId);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ this.indexedMessageCount = messages.length;
153
+ this.indexedTailRef =
154
+ messages.length > 0 ? messages[messages.length - 1] : undefined;
155
+ }
156
+
157
+ private resetIndexes(): void {
158
+ this.indexedMessageCount = 0;
159
+ this.indexedTailRef = undefined;
160
+ this.toolNameByIdCache.clear();
161
+ this.readPathsByToolUseIdCache.clear();
162
+ this.latestReadToolUseByPathCache.clear();
163
+ this.readResultPathCache = new WeakMap<object, string[]>();
164
+ }
165
+
166
+ private getReadResultRecord(
167
+ block: LlmsProviders.ToolResultContent,
168
+ fallbackPaths: string[] | undefined,
169
+ ): ReadResultRecord | undefined {
170
+ const blockRef = block as unknown as object;
171
+ const cachedParsedPaths = this.readResultPathCache.get(blockRef);
172
+ const parsedPaths =
173
+ cachedParsedPaths ??
174
+ this.extractReadPathsFromToolResultContent(block.content);
175
+ if (!cachedParsedPaths) {
176
+ this.readResultPathCache.set(blockRef, parsedPaths);
177
+ }
178
+ const paths = parsedPaths.length > 0 ? parsedPaths : (fallbackPaths ?? []);
179
+ if (paths.length === 0) {
180
+ return undefined;
181
+ }
182
+
183
+ return {
184
+ toolUseId: block.tool_use_id,
185
+ paths,
186
+ };
187
+ }
188
+
189
+ private extractPathsFromReadToolInput(
190
+ input: Record<string, unknown>,
191
+ ): string[] {
192
+ const paths: string[] = [];
193
+ const maybePath = input.path;
194
+ const maybeFilePath = input.file_path;
195
+ const maybeFilePaths = input.file_paths;
196
+
197
+ if (typeof maybePath === "string" && maybePath.length > 0) {
198
+ paths.push(maybePath);
199
+ }
200
+ if (typeof maybeFilePath === "string" && maybeFilePath.length > 0) {
201
+ paths.push(maybeFilePath);
202
+ }
203
+ if (Array.isArray(maybeFilePaths)) {
204
+ for (const value of maybeFilePaths) {
205
+ if (typeof value === "string" && value.length > 0) {
206
+ paths.push(value);
207
+ }
208
+ }
209
+ }
210
+
211
+ return Array.from(new Set(paths));
212
+ }
213
+
214
+ private extractReadPathsFromToolResultContent(
215
+ content: LlmsProviders.ToolResultContent["content"],
216
+ ): string[] {
217
+ if (typeof content !== "string") {
218
+ return [];
219
+ }
220
+
221
+ try {
222
+ const parsed = JSON.parse(content);
223
+ return this.extractPathsFromParsedReadResult(parsed);
224
+ } catch {
225
+ return [];
226
+ }
227
+ }
228
+
229
+ private extractPathsFromParsedReadResult(value: unknown): string[] {
230
+ if (Array.isArray(value)) {
231
+ const paths = value
232
+ .map((item) => this.extractPathFromResultEntry(item))
233
+ .filter(
234
+ (path): path is string => typeof path === "string" && path.length > 0,
235
+ );
236
+ return Array.from(new Set(paths));
237
+ }
238
+
239
+ const path = this.extractPathFromResultEntry(value);
240
+ return path ? [path] : [];
241
+ }
242
+
243
+ private extractPathFromResultEntry(value: unknown): string | undefined {
244
+ if (!value || typeof value !== "object") {
245
+ return undefined;
246
+ }
247
+
248
+ const record = value as Record<string, unknown>;
249
+ const candidates = [
250
+ record.path,
251
+ record.file_path,
252
+ record.filePath,
253
+ record.query,
254
+ ];
255
+ for (const candidate of candidates) {
256
+ if (typeof candidate === "string" && candidate.length > 0) {
257
+ return candidate;
258
+ }
259
+ }
260
+
261
+ return undefined;
262
+ }
263
+
264
+ private replaceOutdatedReadContent(
265
+ content: LlmsProviders.ToolResultContent["content"],
266
+ outdatedPaths: string[],
267
+ ): LlmsProviders.ToolResultContent["content"] {
268
+ const outdatedPathSet = new Set(outdatedPaths);
269
+
270
+ if (typeof content === "string") {
271
+ const replaced = this.replaceOutdatedReadContentInString(
272
+ content,
273
+ outdatedPathSet,
274
+ );
275
+ return replaced ?? OUTDATED_FILE_CONTENT;
276
+ }
277
+
278
+ return content.map((entry) => {
279
+ if (entry.type === "file") {
280
+ if (!outdatedPathSet.has(entry.path)) {
281
+ return entry;
282
+ }
283
+ return {
284
+ ...(entry as LlmsProviders.FileContent),
285
+ content: OUTDATED_FILE_CONTENT,
286
+ };
287
+ }
288
+
289
+ if (entry.type !== "text") {
290
+ return entry;
291
+ }
292
+ const replaced = this.replaceOutdatedReadContentInString(
293
+ entry.text,
294
+ outdatedPathSet,
295
+ );
296
+ if (replaced === null) {
297
+ return {
298
+ ...(entry as LlmsProviders.TextContent),
299
+ text: OUTDATED_FILE_CONTENT,
300
+ };
301
+ }
302
+ if (replaced === entry.text) {
303
+ return entry;
304
+ }
305
+ return {
306
+ ...(entry as LlmsProviders.TextContent),
307
+ text: replaced,
308
+ };
309
+ });
310
+ }
311
+
312
+ private replaceOutdatedReadContentInString(
313
+ text: string,
314
+ outdatedPathSet: Set<string>,
315
+ ): string | null {
316
+ try {
317
+ const parsed = JSON.parse(text);
318
+ const replaced = this.replaceOutdatedReadContentInParsed(
319
+ parsed,
320
+ outdatedPathSet,
321
+ );
322
+ return JSON.stringify(replaced);
323
+ } catch {
324
+ return null;
325
+ }
326
+ }
327
+
328
+ private replaceOutdatedReadContentInParsed(
329
+ value: unknown,
330
+ outdatedPathSet: Set<string>,
331
+ ): unknown {
332
+ if (Array.isArray(value)) {
333
+ return value.map((entry) =>
334
+ this.replaceOutdatedReadEntry(entry, outdatedPathSet),
335
+ );
336
+ }
337
+
338
+ return this.replaceOutdatedReadEntry(value, outdatedPathSet);
339
+ }
340
+
341
+ private replaceOutdatedReadEntry(
342
+ entry: unknown,
343
+ outdatedPathSet: Set<string>,
344
+ ): unknown {
345
+ if (!entry || typeof entry !== "object") {
346
+ return entry;
347
+ }
348
+
349
+ const record = { ...(entry as Record<string, unknown>) };
350
+ const path = this.extractPathFromResultEntry(record);
351
+ if (!path || !outdatedPathSet.has(path)) {
352
+ return entry;
353
+ }
354
+
355
+ if (typeof record.result === "string") {
356
+ record.result = OUTDATED_FILE_CONTENT;
357
+ } else if (typeof record.content === "string") {
358
+ record.content = OUTDATED_FILE_CONTENT;
359
+ } else {
360
+ record.result = OUTDATED_FILE_CONTENT;
361
+ }
362
+
363
+ return record;
364
+ }
365
+
366
+ private isReadTool(toolName: string | undefined): boolean {
367
+ if (!toolName) {
368
+ return false;
369
+ }
370
+ return READ_TOOL_NAMES.has(toolName.toLowerCase());
371
+ }
372
+
373
+ private shouldTruncateTool(toolName: string | undefined): boolean {
374
+ if (!toolName) {
375
+ return false;
376
+ }
377
+ return this.targetToolNames.has(toolName.toLowerCase());
378
+ }
379
+
380
+ private truncateToolResultContent(
381
+ content: LlmsProviders.ToolResultContent["content"],
382
+ ): LlmsProviders.ToolResultContent["content"] {
383
+ if (typeof content === "string") {
384
+ return this.truncateMiddle(content);
385
+ }
386
+
387
+ return content.map((entry) => {
388
+ if (entry.type === "file") {
389
+ const fileContent = this.truncateMiddle(entry.content);
390
+ if (fileContent === entry.content) {
391
+ return entry;
392
+ }
393
+ return {
394
+ ...(entry as LlmsProviders.FileContent),
395
+ content: fileContent,
396
+ };
397
+ }
398
+
399
+ if (entry.type !== "text") {
400
+ return entry;
401
+ }
402
+
403
+ const text = this.truncateMiddle(entry.text);
404
+ if (text === entry.text) {
405
+ return entry;
406
+ }
407
+
408
+ return {
409
+ ...(entry as LlmsProviders.TextContent),
410
+ text,
411
+ };
412
+ });
413
+ }
414
+
415
+ private truncateMiddle(text: string): string {
416
+ if (text.length <= this.maxToolResultChars) {
417
+ return text;
418
+ }
419
+
420
+ const retainedChars = KEEP_CHARS_PER_SIDE * 2;
421
+ const removedChars = Math.max(0, text.length - retainedChars);
422
+ const marker = `\n\n...[truncated ${removedChars} chars]...\n\n`;
423
+
424
+ const start = text.slice(0, KEEP_CHARS_PER_SIDE);
425
+ const end = text.slice(-KEEP_CHARS_PER_SIDE);
426
+
427
+ return `${start}${marker}${end}`;
428
+ }
429
+ }
@@ -0,0 +1,49 @@
1
+ const DEFAULT_CLINE_SYSTEM_PROMPT = `You are Cline, an AI coding agent. Your primary goal is to assist users with various coding tasks by leveraging your knowledge and the tools at your disposal. Given the user's prompt, you should use the tools available to you to answer user's question.
2
+
3
+ Always gather all the necessary context before starting to work on a task. For example, if you are generating a unit test or new code, make sure you understand the requirement, the naming conventions, frameworks and libraries used and aligned in the current codebase, and the environment and commands used to run and test the code etc. Always validate the new unit test at the end including running the code if possible for live feedback.
4
+ Review each question carefully and answer it with detailed, accurate information.
5
+ If you need more information, use one of the available tools or ask for clarification instead of making assumptions or lies.
6
+
7
+ Environment you are running in:
8
+ <env>
9
+ 1. Platform: {{PLATFORM_NAME}}
10
+ 2. Date: {{CURRENT_DATE}}
11
+ 3. IDE: {{IDE_NAME}}
12
+ 4. Working Directory: {{CWD}}
13
+ </env>
14
+
15
+ Remember:
16
+ - Always adhere to existing code conventions and patterns.
17
+ - Use only libraries and frameworks that are confirmed to be in use in the current codebase.
18
+ - Provide complete and functional code without omissions or placeholders.
19
+ - Be explicit about any assumptions or limitations in your solution.
20
+ - Always show your planning process before executing any task. This will help ensure that you have a clear understanding of the requirements and that your approach aligns with the user's needs.
21
+
22
+ Begin by analyzing the user's input and gathering any necessary additional context. Then, present your plan at the start of your response before proceeding with the task. It's OK for this section to be quite long.
23
+
24
+ REMEMBER, always be helpful and proactive! Don't ask for permission to do something when you can do it! Do not indicates you will be using a tool unless you are actually going to use it.
25
+
26
+ When you have completed the task, please provide a summary of what you did and any relevant information that the user should know. This will help ensure that the user understands the changes made and can easily follow up if they have any questions or need further assistance. Do not indicate that you will perform an action without actually doing it. Always provide the final result in your response.
27
+
28
+ If user asked a simple question without any coding context, answer it directly without using any tools.
29
+ {{CLINE_RULES}}
30
+ {{CLINE_METADATA}}`;
31
+
32
+ export function getClineDefaultSystemPrompt(
33
+ ide: string,
34
+ cwd: string,
35
+ metadata = "",
36
+ rules = "",
37
+ ) {
38
+ const platform =
39
+ typeof process !== "undefined" && process?.platform
40
+ ? process.platform
41
+ : "unknown";
42
+ return DEFAULT_CLINE_SYSTEM_PROMPT.replace("{{PLATFORM_NAME}}", platform)
43
+ .replace("{{CWD}}", cwd)
44
+ .replace("{{CURRENT_DATE}}", new Date().toLocaleDateString())
45
+ .replace("{{IDE_NAME}}", ide)
46
+ .replace("{{CLINE_METADATA}}", metadata)
47
+ .replace("{{CLINE_RULES}}", rules)
48
+ .trim();
49
+ }
@@ -0,0 +1 @@
1
+ export { getClineDefaultSystemPrompt } from "./cline.js";
@@ -0,0 +1,53 @@
1
+ import type { AgentEvent, HookStage } from "../types.js";
2
+
3
+ export interface LifecycleBusEvent<TPayload = unknown> {
4
+ stage: HookStage;
5
+ iteration?: number;
6
+ payload: TPayload;
7
+ }
8
+
9
+ export interface AgentRuntimeBus {
10
+ subscribeRuntimeEvent: (listener: (event: AgentEvent) => void) => () => void;
11
+ subscribeLifecycleEvent: (
12
+ listener: (event: LifecycleBusEvent) => void,
13
+ ) => () => void;
14
+ emitRuntimeEvent: (event: AgentEvent) => void;
15
+ emitLifecycleEvent: <TPayload>(event: LifecycleBusEvent<TPayload>) => void;
16
+ }
17
+
18
+ export function createAgentRuntimeBus(): AgentRuntimeBus {
19
+ let runtimeSeq = 0;
20
+ let lifecycleSeq = 0;
21
+ const runtimeListeners = new Map<number, (event: AgentEvent) => void>();
22
+ const lifecycleListeners = new Map<
23
+ number,
24
+ (event: LifecycleBusEvent) => void
25
+ >();
26
+
27
+ return {
28
+ subscribeRuntimeEvent: (listener) => {
29
+ const id = ++runtimeSeq;
30
+ runtimeListeners.set(id, listener);
31
+ return () => {
32
+ runtimeListeners.delete(id);
33
+ };
34
+ },
35
+ subscribeLifecycleEvent: (listener) => {
36
+ const id = ++lifecycleSeq;
37
+ lifecycleListeners.set(id, listener);
38
+ return () => {
39
+ lifecycleListeners.delete(id);
40
+ };
41
+ },
42
+ emitRuntimeEvent: (event) => {
43
+ for (const listener of runtimeListeners.values()) {
44
+ listener(event);
45
+ }
46
+ },
47
+ emitLifecycleEvent: (event) => {
48
+ for (const listener of lifecycleListeners.values()) {
49
+ listener(event);
50
+ }
51
+ },
52
+ };
53
+ }
@@ -0,0 +1,61 @@
1
+ import type { providers } from "@clinebot/llms";
2
+
3
+ function createConversationId(): string {
4
+ return `conv_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
5
+ }
6
+
7
+ export class ConversationStore {
8
+ private messages: providers.Message[] = [];
9
+ private conversationId = createConversationId();
10
+ private sessionStarted = false;
11
+
12
+ constructor(initialMessages?: providers.Message[]) {
13
+ if ((initialMessages?.length ?? 0) > 0) {
14
+ this.restore(initialMessages ?? []);
15
+ }
16
+ }
17
+
18
+ getConversationId(): string {
19
+ return this.conversationId;
20
+ }
21
+
22
+ getMessages(): providers.Message[] {
23
+ return [...this.messages];
24
+ }
25
+
26
+ appendMessage(message: providers.Message): void {
27
+ this.messages.push(message);
28
+ }
29
+
30
+ appendMessages(messages: providers.Message[]): void {
31
+ if (messages.length === 0) {
32
+ return;
33
+ }
34
+ this.messages.push(...messages);
35
+ }
36
+
37
+ resetForRun(): void {
38
+ this.messages = [];
39
+ this.conversationId = createConversationId();
40
+ this.sessionStarted = false;
41
+ }
42
+
43
+ clearHistory(): void {
44
+ this.messages = [];
45
+ this.conversationId = createConversationId();
46
+ this.sessionStarted = false;
47
+ }
48
+
49
+ restore(messages: providers.Message[]): void {
50
+ this.messages = [...messages];
51
+ this.sessionStarted = false;
52
+ }
53
+
54
+ isSessionStarted(): boolean {
55
+ return this.sessionStarted;
56
+ }
57
+
58
+ markSessionStarted(): void {
59
+ this.sessionStarted = true;
60
+ }
61
+ }
@@ -0,0 +1,90 @@
1
+ import type { HookDispatchInput, HookEngine } from "../hooks/index.js";
2
+ import type { AgentEvent, AgentHookControl, HookStage } from "../types.js";
3
+ import type { AgentRuntimeBus } from "./agent-runtime-bus.js";
4
+
5
+ export interface LifecycleOrchestratorOptions {
6
+ hookEngine: HookEngine;
7
+ runtimeBus: AgentRuntimeBus;
8
+ getRunId: () => string;
9
+ getAgentId: () => string;
10
+ getConversationId: () => string;
11
+ getParentAgentId: () => string | null;
12
+ onHookContext?: (source: string, context: string) => void;
13
+ onDispatchError?: (error: unknown) => void;
14
+ }
15
+
16
+ export class LifecycleOrchestrator {
17
+ private readonly hookEngine: HookEngine;
18
+ private readonly runtimeBus: AgentRuntimeBus;
19
+ private readonly getRunId: () => string;
20
+ private readonly getAgentId: () => string;
21
+ private readonly getConversationId: () => string;
22
+ private readonly getParentAgentId: () => string | null;
23
+ private readonly onHookContext?: (source: string, context: string) => void;
24
+ private readonly onDispatchError?: (error: unknown) => void;
25
+
26
+ constructor(options: LifecycleOrchestratorOptions) {
27
+ this.hookEngine = options.hookEngine;
28
+ this.runtimeBus = options.runtimeBus;
29
+ this.getRunId = options.getRunId;
30
+ this.getAgentId = options.getAgentId;
31
+ this.getConversationId = options.getConversationId;
32
+ this.getParentAgentId = options.getParentAgentId;
33
+ this.onHookContext = options.onHookContext;
34
+ this.onDispatchError = options.onDispatchError;
35
+ }
36
+
37
+ async dispatch(
38
+ source: string,
39
+ input: Pick<
40
+ HookDispatchInput,
41
+ "stage" | "payload" | "iteration" | "parentEventId"
42
+ >,
43
+ ): Promise<AgentHookControl | undefined> {
44
+ this.runtimeBus.emitLifecycleEvent({
45
+ stage: input.stage,
46
+ iteration: input.iteration,
47
+ payload: input.payload,
48
+ });
49
+
50
+ const dispatchResult = await this.hookEngine.dispatch({
51
+ ...input,
52
+ runId: this.getRunId(),
53
+ agentId: this.getAgentId(),
54
+ conversationId: this.getConversationId(),
55
+ parentAgentId: this.getParentAgentId(),
56
+ });
57
+ if (dispatchResult.control?.context) {
58
+ this.onHookContext?.(source, dispatchResult.control.context);
59
+ }
60
+ return dispatchResult.control;
61
+ }
62
+
63
+ dispatchRuntimeEvent(event: AgentEvent): void {
64
+ void this.hookEngine
65
+ .dispatch({
66
+ stage: "runtime_event",
67
+ runId: this.getRunId(),
68
+ agentId: this.getAgentId(),
69
+ conversationId: this.getConversationId(),
70
+ parentAgentId: this.getParentAgentId(),
71
+ payload: {
72
+ agentId: this.getAgentId(),
73
+ conversationId: this.getConversationId(),
74
+ parentAgentId: this.getParentAgentId(),
75
+ event,
76
+ },
77
+ })
78
+ .catch((error) => {
79
+ this.onDispatchError?.(error);
80
+ });
81
+ }
82
+
83
+ async shutdown(timeoutMs?: number): Promise<void> {
84
+ await this.hookEngine.shutdown(timeoutMs);
85
+ }
86
+ }
87
+
88
+ export function isBlockingLifecycleStage(stage: HookStage): boolean {
89
+ return stage !== "runtime_event";
90
+ }