@tyvm/knowhow 0.0.101 → 0.0.102

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.101",
3
+ "version": "0.0.102",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -64,7 +64,13 @@ export abstract class BaseAgent implements IAgent {
64
64
  protected compressMinMessages = 30;
65
65
 
66
66
  protected threads = [] as Message[][];
67
+
68
+ // Message from users
67
69
  protected pendingUserMessages = [] as Message[];
70
+
71
+ // Internal messages
72
+ protected pendingMessages = [] as Message[];
73
+
68
74
  protected taskBreakdown = "";
69
75
  protected summaries = [] as string[];
70
76
  protected currentTaskId: string | null = null;
@@ -538,10 +544,14 @@ export abstract class BaseAgent implements IAgent {
538
544
 
539
545
  async kill() {
540
546
  this.log("Killing agent");
547
+ if (this.status === this.eventTypes.kill || this.status === this.eventTypes.done) {
548
+ this.log("Agent is already being killed or done, ignoring duplicate kill()", "warn");
549
+ return;
550
+ }
541
551
  this.agentEvents.emit(this.eventTypes.kill, this);
542
552
  this.status = this.eventTypes.kill;
543
553
 
544
- this.addPendingUserMessage({
554
+ this.addPendingMessage({
545
555
  role: "user",
546
556
  content: `<Workflow>The user has requested the task to end, please call ${this.requiredToolNames} with a report of your ending state</Workflow>`,
547
557
  } as Message);
@@ -599,6 +609,11 @@ export abstract class BaseAgent implements IAgent {
599
609
  this.pendingUserMessages = [];
600
610
  }
601
611
 
612
+ if (this.pendingMessages.length) {
613
+ messages.push(...this.pendingMessages);
614
+ this.pendingMessages = [];
615
+ }
616
+
602
617
  messages = this.formatInputMessages(messages);
603
618
  this.updateCurrentThread(messages);
604
619
  const isMissingTool = this.isRequiredToolMissing();
@@ -673,7 +688,7 @@ export abstract class BaseAgent implements IAgent {
673
688
  this.updateCurrentThread(messages);
674
689
 
675
690
  for (const toolCall of toolCalls) {
676
- if(this.status === this.eventTypes.pause) {
691
+ if (this.status === this.eventTypes.pause) {
677
692
  this.log(
678
693
  "Agent was paused before tool call, waiting before processing tool calls"
679
694
  );
@@ -716,7 +731,6 @@ export abstract class BaseAgent implements IAgent {
716
731
  });
717
732
  const doneMsg = finalMessage.content || "Done";
718
733
 
719
-
720
734
  this.agentEvents.emit(this.eventTypes.done, doneMsg);
721
735
  this.status = this.eventTypes.done;
722
736
  return doneMsg;
@@ -908,21 +922,32 @@ export abstract class BaseAgent implements IAgent {
908
922
  });
909
923
  }
910
924
 
925
+ // A new message from system, non blocking
911
926
  addPendingMessage(message: Message) {
912
927
  if (this.status === this.eventTypes.done) {
913
928
  this.log("Agent is done, cannot take more messages", "warn");
914
929
  } else {
915
- const pendingMessages = this.pendingUserMessages.map((m) => m.content);
930
+ const pendingMessages = this.pendingMessages.map((m) => m.content);
916
931
  if (pendingMessages.includes(message.content)) {
917
932
  // Ignore messages we already have queue'd up
918
933
  return;
919
934
  }
920
- this.pendingUserMessages.push(message);
935
+ this.pendingMessages.push(message);
921
936
  }
922
937
  }
923
938
 
939
+ // A new message from users, blocks completion
924
940
  addPendingUserMessage(message: Message) {
925
- this.addPendingMessage(message);
941
+ if (this.status === this.eventTypes.done) {
942
+ this.log("Agent is done, cannot take more messages", "warn");
943
+ } else {
944
+ const pendingMessages = this.pendingUserMessages.map((m) => m.content);
945
+ if (pendingMessages.includes(message.content)) {
946
+ // Ignore messages we already have queue'd up
947
+ return;
948
+ }
949
+ this.pendingUserMessages.push(message);
950
+ }
926
951
  this.events.emit(this.eventTypes.userSay, message.content);
927
952
  }
928
953
 
@@ -1,6 +1,5 @@
1
1
  import * as fs from "fs";
2
2
  import * as path from "path";
3
- import { glob } from "glob";
4
3
  import { KnowhowSimpleClient, KNOWHOW_API_URL } from "./services/KnowhowClient";
5
4
  import { loadJwt } from "./login";
6
5
  import { getConfig, updateConfig, getLanguageConfig } from "./config";
@@ -23,6 +22,26 @@ interface FileToSync {
23
22
  localPath: string;
24
23
  remotePath: string;
25
24
  downloadLocalPath?: string; // override localPath used when worker downloads the file
25
+ isDirectory?: boolean; // true if this represents a whole directory
26
+ }
27
+
28
+ /**
29
+ * Recursively list all files in a local directory, returning relative paths
30
+ */
31
+ function listFilesRecursively(dir: string): string[] {
32
+ const results: string[] = [];
33
+ if (!fs.existsSync(dir)) return results;
34
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
35
+ for (const entry of entries) {
36
+ if (entry.isDirectory()) {
37
+ listFilesRecursively(path.join(dir, entry.name)).forEach((f) =>
38
+ results.push(entry.name + "/" + f)
39
+ );
40
+ } else {
41
+ results.push(entry.name);
42
+ }
43
+ }
44
+ return results;
26
45
  }
27
46
 
28
47
  /**
@@ -48,6 +67,8 @@ function buildWorkerConfigJson(config: Config, files: { remotePath: string; loca
48
67
 
49
68
  /**
50
69
  * Collect all files from the .knowhow directory that should be synced
70
+ * Uses directory-level entries where possible so the worker config stays compact
71
+ * and the folder upload/download feature handles individual files automatically.
51
72
  */
52
73
  async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
53
74
  const filesToSync: FileToSync[] = [];
@@ -59,39 +80,24 @@ async function collectFilesToSync(projectName: string): Promise<FileToSync[]> {
59
80
  }
60
81
  };
61
82
 
83
+ // Helper to add a directory entry if it exists (trailing slash = directory mode)
84
+ const addDirIfExists = (localPath: string, remotePath: string) => {
85
+ if (fs.existsSync(localPath)) {
86
+ filesToSync.push({ localPath: localPath + "/", remotePath: remotePath + "/", isDirectory: true });
87
+ }
88
+ };
89
+
62
90
  // .knowhow/language.json
63
91
  addIfExists(".knowhow/language.json", `${projectName}/.knowhow/language.json`);
64
92
 
65
93
  // .knowhow/hashes.json
66
94
  addIfExists(".knowhow/hashes.json", `${projectName}/.knowhow/hashes.json`);
67
95
 
68
- // .knowhow/prompts/**/*
69
- const promptFiles = await glob(".knowhow/prompts/**/*", { nodir: true });
70
- for (const filePath of promptFiles) {
71
- const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
72
- const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
73
- filesToSync.push({ localPath: filePath, remotePath });
74
- }
75
-
76
- // .knowhow/scripts/**/* (if exists)
77
- if (fs.existsSync(".knowhow/scripts")) {
78
- const scriptFiles = await glob(".knowhow/scripts/**/*", { nodir: true });
79
- for (const filePath of scriptFiles) {
80
- const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
81
- const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
82
- filesToSync.push({ localPath: filePath, remotePath });
83
- }
84
- }
85
-
86
- // .knowhow/skills/**/* (if exists)
87
- if (fs.existsSync(".knowhow/skills")) {
88
- const skillFiles = await glob(".knowhow/skills/**/*", { nodir: true });
89
- for (const filePath of skillFiles) {
90
- const relativeToDotKnowhow = filePath.replace(/^\.knowhow\//, "");
91
- const remotePath = `${projectName}/.knowhow/${relativeToDotKnowhow}`;
92
- filesToSync.push({ localPath: filePath, remotePath });
93
- }
94
- }
96
+ // Directories — use trailing-slash entries so folder upload/download handles them
97
+ addDirIfExists(".knowhow/prompts", `${projectName}/.knowhow/prompts`);
98
+ addDirIfExists(".knowhow/scripts", `${projectName}/.knowhow/scripts`);
99
+ addDirIfExists(".knowhow/skills", `${projectName}/.knowhow/skills`);
100
+ addDirIfExists(".knowhow/tasks", `${projectName}/.knowhow/tasks`);
95
101
 
96
102
  return filesToSync;
97
103
  }
@@ -264,8 +270,20 @@ export async function cloudWorker(options: CloudWorkerOptions) {
264
270
 
265
271
  for (const file of allFiles) {
266
272
  try {
267
- await uploadSingleFile(client, AwsS3, file.localPath, file.remotePath, dryRun);
268
- successCount++;
273
+ if (file.isDirectory) {
274
+ // Upload all files recursively in the local directory
275
+ const localDir = file.localPath.endsWith("/") ? file.localPath : file.localPath + "/";
276
+ const remoteDir = file.remotePath.endsWith("/") ? file.remotePath : file.remotePath + "/";
277
+ const relFiles = listFilesRecursively(localDir);
278
+ console.log(` 📁 Uploading directory ${localDir} → ${remoteDir} (${relFiles.length} files)`);
279
+ for (const relFile of relFiles) {
280
+ await uploadSingleFile(client, AwsS3, localDir + relFile, remoteDir + relFile, dryRun);
281
+ successCount++;
282
+ }
283
+ } else {
284
+ await uploadSingleFile(client, AwsS3, file.localPath, file.remotePath, dryRun);
285
+ successCount++;
286
+ }
269
287
  } catch (error) {
270
288
  console.error(` ❌ Failed to upload ${file.localPath}: ${error.message}`);
271
289
  failCount++;
package/src/fileSync.ts CHANGED
@@ -5,6 +5,7 @@ import { loadJwt } from "./login";
5
5
  import { getConfig } from "./config";
6
6
  import { services } from "./services";
7
7
  import { S3Service } from "./services/S3";
8
+ import { getHashes, hasFileChangedSinceUpload, saveUploadHash, isLocalFileMatchingRemote } from "./hashes";
8
9
 
9
10
  export interface FileSyncOptions {
10
11
  upload?: boolean;
@@ -14,6 +15,33 @@ export interface FileSyncOptions {
14
15
  dryRun?: boolean;
15
16
  }
16
17
 
18
+ /**
19
+ * Returns true if the path looks like a directory (ends with /)
20
+ */
21
+ function isDirectoryPath(p: string): boolean {
22
+ return p.endsWith("/");
23
+ }
24
+
25
+ /**
26
+ * Recursively list all files in a local directory, returning relative paths
27
+ */
28
+ function listFilesRecursively(dir: string): string[] {
29
+ const results: string[] = [];
30
+ if (!fs.existsSync(dir)) return results;
31
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
32
+ for (const entry of entries) {
33
+ if (entry.isDirectory()) {
34
+ const subFiles = listFilesRecursively(path.join(dir, entry.name));
35
+ for (const f of subFiles) {
36
+ results.push(entry.name + "/" + f);
37
+ }
38
+ } else {
39
+ results.push(entry.name);
40
+ }
41
+ }
42
+ return results;
43
+ }
44
+
17
45
  /**
18
46
  * Sync files between local filesystem and Knowhow FS
19
47
  */
@@ -67,11 +95,21 @@ export async function fileSync(options: FileSyncOptions = {}) {
67
95
 
68
96
  try {
69
97
  if (actualDirection === "download") {
70
- await downloadFile(client, AwsS3, remotePath, localPath, dryRun);
71
- successCount++;
98
+ if (isDirectoryPath(remotePath) || isDirectoryPath(localPath)) {
99
+ const count = await downloadDirectory(client, AwsS3, remotePath, localPath, dryRun);
100
+ successCount += count;
101
+ } else {
102
+ await downloadFile(client, AwsS3, remotePath, localPath, dryRun);
103
+ successCount++;
104
+ }
72
105
  } else if (actualDirection === "upload") {
73
- await uploadFile(client, AwsS3, remotePath, localPath, dryRun);
74
- successCount++;
106
+ if (isDirectoryPath(remotePath) || isDirectoryPath(localPath)) {
107
+ const count = await uploadDirectory(client, AwsS3, remotePath, localPath, dryRun);
108
+ successCount += count;
109
+ } else {
110
+ await uploadFile(client, AwsS3, remotePath, localPath, dryRun);
111
+ successCount++;
112
+ }
75
113
  }
76
114
  } catch (error) {
77
115
  console.error(`❌ Failed to sync ${remotePath}: ${error.message}`);
@@ -88,6 +126,7 @@ export async function fileSync(options: FileSyncOptions = {}) {
88
126
  }
89
127
  }
90
128
 
129
+
91
130
  /**
92
131
  * Download a file from Knowhow FS to local filesystem
93
132
  */
@@ -106,10 +145,14 @@ async function downloadFile(
106
145
  }
107
146
 
108
147
  try {
109
- // Get presigned download URL
110
- const presignedUrl = await client.getOrgFilePresignedDownloadUrl(
111
- remotePath
112
- );
148
+ // Get presigned download URL + remote checksum
149
+ const { downloadUrl, checksumSHA256 } = await client.getOrgFilePresignedDownloadUrl(remotePath);
150
+
151
+ // Skip if local file matches remote checksum
152
+ if (isLocalFileMatchingRemote(localPath, checksumSHA256)) {
153
+ console.log(` ✓ Skipping ${localPath} (matches remote checksum)`);
154
+ return;
155
+ }
113
156
 
114
157
  // Ensure parent directory exists
115
158
  const dir = path.dirname(localPath);
@@ -118,7 +161,7 @@ async function downloadFile(
118
161
  }
119
162
 
120
163
  // Download file using presigned URL
121
- await s3Service.downloadFromPresignedUrl(presignedUrl, localPath);
164
+ await s3Service.downloadFromPresignedUrl(downloadUrl, localPath);
122
165
 
123
166
  // Get file size for logging
124
167
  const stats = fs.statSync(localPath);
@@ -151,6 +194,14 @@ async function uploadFile(
151
194
  return;
152
195
  }
153
196
 
197
+ // Skip upload if file hasn't changed since last upload
198
+ const hashes = await getHashes();
199
+ const changed = await hasFileChangedSinceUpload(localPath, hashes);
200
+ if (!changed) {
201
+ console.log(` ✓ Skipping ${localPath} (unchanged since last upload)`);
202
+ return;
203
+ }
204
+
154
205
  // Get presigned upload URL
155
206
  const presignedUrl = await client.getOrgFilePresignedUploadUrl(remotePath);
156
207
 
@@ -160,6 +211,99 @@ async function uploadFile(
160
211
  // Notify backend that upload is complete to update the updatedAt timestamp
161
212
  await client.markOrgFileUploadComplete(remotePath);
162
213
 
214
+ // Save upload hash so we can skip unchanged files next time
215
+ await saveUploadHash(localPath);
216
+
163
217
  const stats = fs.statSync(localPath);
164
218
  console.log(` ✓ Uploaded ${stats.size} bytes`);
165
219
  }
220
+
221
+ /**
222
+ * Upload all files from a local directory to a remote directory path
223
+ */
224
+ export async function uploadDirectory(
225
+ client: KnowhowSimpleClient,
226
+ s3Service: S3Service,
227
+ remotePath: string,
228
+ localPath: string,
229
+ dryRun: boolean
230
+ ): Promise<number> {
231
+ // Normalize paths to end with /
232
+ const remoteDir = remotePath.endsWith("/") ? remotePath : remotePath + "/";
233
+ const localDir = localPath.endsWith("/") ? localPath : localPath + "/";
234
+
235
+ console.log(`⬆️ Uploading directory ${localDir} → ${remoteDir}`);
236
+
237
+ if (!fs.existsSync(localDir)) {
238
+ console.warn(` ⚠️ Local directory not found: ${localDir}`);
239
+ return 0;
240
+ }
241
+
242
+ // Find all files recursively in the local directory
243
+ const localFiles = listFilesRecursively(localDir);
244
+
245
+ if (localFiles.length === 0) {
246
+ console.log(` ⚠️ No local files found under ${localDir}`);
247
+ return 0;
248
+ }
249
+
250
+ console.log(` Found ${localFiles.length} local file(s)`);
251
+
252
+ let count = 0;
253
+ for (const relFile of localFiles) {
254
+ const localFilePath = localDir + relFile;
255
+ const remoteFilePath = remoteDir + relFile;
256
+ await uploadFile(client, s3Service, remoteFilePath, localFilePath, dryRun);
257
+ count++;
258
+ }
259
+ return count;
260
+ }
261
+
262
+ /**
263
+ * Download all files from a remote directory path to a local directory
264
+ */
265
+ async function downloadDirectory(
266
+ client: KnowhowSimpleClient,
267
+ s3Service: S3Service,
268
+ remotePath: string,
269
+ localPath: string,
270
+ dryRun: boolean
271
+ ): Promise<number> {
272
+ // Normalize paths to end with /
273
+ const remoteDir = remotePath.endsWith("/") ? remotePath : remotePath + "/";
274
+ const localDir = localPath.endsWith("/") ? localPath : localPath + "/";
275
+
276
+ console.log(`⬇️ Downloading directory ${remoteDir} → ${localDir}`);
277
+
278
+ // List all org files and find those in the remote directory
279
+ const response = await client.listOrgFiles();
280
+ const allFiles = response.data;
281
+
282
+ // Find files where the full path starts with remoteDir
283
+ const matchingFiles = allFiles.filter((f) => {
284
+ const fullPath = f.folderPath.endsWith("/")
285
+ ? f.folderPath + f.fileName
286
+ : f.folderPath + "/" + f.fileName;
287
+ return fullPath.startsWith(remoteDir);
288
+ });
289
+
290
+ if (matchingFiles.length === 0) {
291
+ console.log(` ⚠️ No remote files found under ${remoteDir}`);
292
+ return 0;
293
+ }
294
+
295
+ console.log(` Found ${matchingFiles.length} remote file(s)`);
296
+
297
+ let count = 0;
298
+ for (const f of matchingFiles) {
299
+ const fullRemotePath = f.folderPath.endsWith("/")
300
+ ? f.folderPath + f.fileName
301
+ : f.folderPath + "/" + f.fileName;
302
+ // Strip the base remote dir prefix to get relative path
303
+ const relativePath = fullRemotePath.slice(remoteDir.length);
304
+ const localFilePath = localDir + relativePath;
305
+ await downloadFile(client, s3Service, fullRemotePath, localFilePath, dryRun);
306
+ count++;
307
+ }
308
+ return count;
309
+ }
package/src/hashes.ts CHANGED
@@ -70,3 +70,55 @@ export async function saveAllFileHashes(files: string[], promptHash: string) {
70
70
 
71
71
  await saveHashes(hashes);
72
72
  }
73
+
74
+ const UPLOAD_KEY = "upload";
75
+
76
+ /**
77
+ * Returns true if the file has changed since the last successful upload
78
+ * (or if it has never been uploaded before)
79
+ */
80
+ export async function hasFileChangedSinceUpload(
81
+ localPath: string,
82
+ hashes: any
83
+ ): Promise<boolean> {
84
+ if (!fs.existsSync(localPath)) return true;
85
+ const content = fs.readFileSync(localPath);
86
+ const currentHash = crypto.createHash("md5").update(content).digest("hex");
87
+ return hashes[localPath]?.[UPLOAD_KEY] !== currentHash;
88
+ }
89
+
90
+ /**
91
+ * Saves the hash of the file at the time of a successful upload
92
+ */
93
+ export async function saveUploadHash(localPath: string) {
94
+ const hashes = await getHashes();
95
+ const content = fs.readFileSync(localPath);
96
+ const currentHash = crypto.createHash("md5").update(content).digest("hex");
97
+ if (!hashes[localPath]) {
98
+ hashes[localPath] = { fileHash: currentHash, promptHash: "" };
99
+ }
100
+ hashes[localPath][UPLOAD_KEY] = currentHash;
101
+ await saveHashes(hashes);
102
+ }
103
+
104
+ /**
105
+ * Compute SHA-256 of a local file, returned as base64 (matches S3 encoding)
106
+ */
107
+ export function computeSHA256Base64(filePath: string): string {
108
+ const content = fs.readFileSync(filePath);
109
+ return crypto.createHash("sha256").update(content).digest("base64");
110
+ }
111
+
112
+ /**
113
+ * Returns true if the local file's SHA-256 matches the remote checksum,
114
+ * meaning the file is up-to-date and download can be skipped.
115
+ */
116
+ export function isLocalFileMatchingRemote(
117
+ localPath: string,
118
+ remoteChecksumSHA256: string | null
119
+ ): boolean {
120
+ if (!remoteChecksumSHA256) return false;
121
+ if (!fs.existsSync(localPath)) return false;
122
+ const localChecksum = computeSHA256Base64(localPath);
123
+ return localChecksum === remoteChecksumSHA256;
124
+ }
@@ -575,7 +575,9 @@ export class KnowhowSimpleClient {
575
575
  * Get presigned S3 URL for downloading a file from Knowhow FS.
576
576
  * First finds or creates the file by path, then gets its download URL.
577
577
  */
578
- async getOrgFilePresignedDownloadUrl(filePath: string): Promise<string> {
578
+ async getOrgFilePresignedDownloadUrl(
579
+ filePath: string
580
+ ): Promise<{ downloadUrl: string; checksumSHA256: string | null }> {
579
581
  await this.checkJwt();
580
582
 
581
583
  // Find the file by path
@@ -585,12 +587,15 @@ export class KnowhowSimpleClient {
585
587
  }
586
588
 
587
589
  // Get download URL using the file ID
588
- const response = await http.post<{ downloadUrl: string }>(
590
+ const response = await http.post<{ downloadUrl: string; checksumSHA256: string | null }>(
589
591
  `${this.baseUrl}/api/org-files/download/${file.id}`,
590
592
  {},
591
593
  { headers: this.headers }
592
594
  );
593
- return response.data.downloadUrl;
595
+ return {
596
+ downloadUrl: response.data.downloadUrl,
597
+ checksumSHA256: response.data.checksumSHA256 ?? null,
598
+ };
594
599
  }
595
600
 
596
601
  /**
@@ -1,4 +1,5 @@
1
1
  import * as fs from "fs";
2
+ import * as crypto from "crypto";
2
3
  import { createWriteStream, createReadStream } from "fs";
3
4
  import { pipeline, Readable } from "stream";
4
5
  import * as util from "util";
@@ -11,19 +12,28 @@ export class S3Service {
11
12
  filePath: string
12
13
  ): Promise<void> {
13
14
  try {
14
- const fileStream = createReadStream(filePath);
15
+ const fileContent = fs.readFileSync(filePath);
15
16
  const fileStats = await fs.promises.stat(filePath);
17
+ const sha256Base64 = crypto
18
+ .createHash("sha256")
19
+ .update(fileContent)
20
+ .digest("base64");
16
21
 
17
22
  const response = await fetch(presignedUrl, {
18
23
  method: "PUT",
19
- headers: { "Content-Length": String(fileStats.size) },
20
- body: fileStream as any,
21
- // @ts-ignore - Node 18+ supports ReadableStream body with duplex
24
+ headers: {
25
+ "Content-Length": String(fileStats.size),
26
+ "x-amz-checksum-sha256": sha256Base64,
27
+ "x-amz-sdk-checksum-algorithm": "SHA256",
28
+ },
29
+ body: fileContent,
30
+ // @ts-ignore
22
31
  duplex: "half",
23
32
  });
24
33
 
25
34
  if (!response.ok) {
26
- throw new Error(`Upload failed with status code: ${response.status}`);
35
+ const text = await response.text();
36
+ throw new Error(`Upload failed with status code: ${response.status} - ${text}`);
27
37
  }
28
38
 
29
39
  console.log("File uploaded successfully to pre-signed URL");
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tyvm/knowhow",
3
- "version": "0.0.101",
3
+ "version": "0.0.102",
4
4
  "description": "ai cli with plugins and agents",
5
5
  "main": "ts_build/src/index.js",
6
6
  "bin": {
@@ -42,6 +42,7 @@ export declare abstract class BaseAgent implements IAgent {
42
42
  protected compressMinMessages: number;
43
43
  protected threads: Message[][];
44
44
  protected pendingUserMessages: Message[];
45
+ protected pendingMessages: Message[];
45
46
  protected taskBreakdown: string;
46
47
  protected summaries: string[];
47
48
  protected currentTaskId: string | null;
@@ -28,6 +28,7 @@ class BaseAgent {
28
28
  compressMinMessages = 30;
29
29
  threads = [];
30
30
  pendingUserMessages = [];
31
+ pendingMessages = [];
31
32
  taskBreakdown = "";
32
33
  summaries = [];
33
34
  currentTaskId = null;
@@ -372,9 +373,13 @@ class BaseAgent {
372
373
  }
373
374
  async kill() {
374
375
  this.log("Killing agent");
376
+ if (this.status === this.eventTypes.kill || this.status === this.eventTypes.done) {
377
+ this.log("Agent is already being killed or done, ignoring duplicate kill()", "warn");
378
+ return;
379
+ }
375
380
  this.agentEvents.emit(this.eventTypes.kill, this);
376
381
  this.status = this.eventTypes.kill;
377
- this.addPendingUserMessage({
382
+ this.addPendingMessage({
378
383
  role: "user",
379
384
  content: `<Workflow>The user has requested the task to end, please call ${this.requiredToolNames} with a report of your ending state</Workflow>`,
380
385
  });
@@ -407,6 +412,10 @@ class BaseAgent {
407
412
  messages.push(...this.pendingUserMessages);
408
413
  this.pendingUserMessages = [];
409
414
  }
415
+ if (this.pendingMessages.length) {
416
+ messages.push(...this.pendingMessages);
417
+ this.pendingMessages = [];
418
+ }
410
419
  messages = this.formatInputMessages(messages);
411
420
  this.updateCurrentThread(messages);
412
421
  const isMissingTool = this.isRequiredToolMissing();
@@ -601,15 +610,24 @@ class BaseAgent {
601
610
  this.log("Agent is done, cannot take more messages", "warn");
602
611
  }
603
612
  else {
604
- const pendingMessages = this.pendingUserMessages.map((m) => m.content);
613
+ const pendingMessages = this.pendingMessages.map((m) => m.content);
605
614
  if (pendingMessages.includes(message.content)) {
606
615
  return;
607
616
  }
608
- this.pendingUserMessages.push(message);
617
+ this.pendingMessages.push(message);
609
618
  }
610
619
  }
611
620
  addPendingUserMessage(message) {
612
- this.addPendingMessage(message);
621
+ if (this.status === this.eventTypes.done) {
622
+ this.log("Agent is done, cannot take more messages", "warn");
623
+ }
624
+ else {
625
+ const pendingMessages = this.pendingUserMessages.map((m) => m.content);
626
+ if (pendingMessages.includes(message.content)) {
627
+ return;
628
+ }
629
+ this.pendingUserMessages.push(message);
630
+ }
613
631
  this.events.emit(this.eventTypes.userSay, message.content);
614
632
  }
615
633
  getMessagesLength(messages) {