@aman_asmuei/aman-agent 0.4.0 → 0.5.1
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/README.md +232 -88
- package/dist/index.js +510 -117
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -13,7 +13,8 @@ var DEFAULT_HOOKS = {
|
|
|
13
13
|
rulesCheck: true,
|
|
14
14
|
workflowSuggest: true,
|
|
15
15
|
evalPrompt: true,
|
|
16
|
-
autoSessionSave: true
|
|
16
|
+
autoSessionSave: true,
|
|
17
|
+
extractMemories: true
|
|
17
18
|
};
|
|
18
19
|
var CONFIG_DIR = path.join(os.homedir(), ".aman-agent");
|
|
19
20
|
var CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
@@ -462,6 +463,74 @@ function createOllamaClient(model, baseURL) {
|
|
|
462
463
|
// src/mcp/client.ts
|
|
463
464
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
464
465
|
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
466
|
+
|
|
467
|
+
// src/logger.ts
|
|
468
|
+
import fs3 from "fs";
|
|
469
|
+
import path3 from "path";
|
|
470
|
+
import os3 from "os";
|
|
471
|
+
var LOG_DIR = path3.join(os3.homedir(), ".aman-agent");
|
|
472
|
+
var LOG_PATH = path3.join(LOG_DIR, "debug.log");
|
|
473
|
+
var MAX_LOG_SIZE = 1048576;
|
|
474
|
+
function ensureDir() {
|
|
475
|
+
if (!fs3.existsSync(LOG_DIR)) {
|
|
476
|
+
fs3.mkdirSync(LOG_DIR, { recursive: true });
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
function maybeRotate() {
|
|
480
|
+
try {
|
|
481
|
+
if (!fs3.existsSync(LOG_PATH)) return;
|
|
482
|
+
const stat = fs3.statSync(LOG_PATH);
|
|
483
|
+
if (stat.size >= MAX_LOG_SIZE) {
|
|
484
|
+
const backupPath = LOG_PATH + ".1";
|
|
485
|
+
if (fs3.existsSync(backupPath)) fs3.unlinkSync(backupPath);
|
|
486
|
+
fs3.renameSync(LOG_PATH, backupPath);
|
|
487
|
+
}
|
|
488
|
+
} catch {
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
function write(level, module, message, data) {
|
|
492
|
+
try {
|
|
493
|
+
ensureDir();
|
|
494
|
+
maybeRotate();
|
|
495
|
+
const entry = {
|
|
496
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
497
|
+
level,
|
|
498
|
+
module,
|
|
499
|
+
message
|
|
500
|
+
};
|
|
501
|
+
if (data !== void 0) {
|
|
502
|
+
entry.data = data instanceof Error ? data.message : String(data);
|
|
503
|
+
}
|
|
504
|
+
fs3.appendFileSync(LOG_PATH, JSON.stringify(entry) + "\n");
|
|
505
|
+
} catch {
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
var log = {
|
|
509
|
+
debug: (module, message, data) => write("debug", module, message, data),
|
|
510
|
+
warn: (module, message, data) => write("warn", module, message, data),
|
|
511
|
+
error: (module, message, data) => write("error", module, message, data)
|
|
512
|
+
};
|
|
513
|
+
|
|
514
|
+
// src/retry.ts
|
|
515
|
+
async function withRetry(fn, options) {
|
|
516
|
+
const { maxAttempts, baseDelay, retryable } = options;
|
|
517
|
+
let lastError;
|
|
518
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
519
|
+
try {
|
|
520
|
+
return await fn();
|
|
521
|
+
} catch (err) {
|
|
522
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
523
|
+
if (!retryable(lastError) || attempt === maxAttempts) {
|
|
524
|
+
throw lastError;
|
|
525
|
+
}
|
|
526
|
+
const delay = baseDelay * Math.pow(2, attempt - 1) * (0.5 + Math.random() * 0.5);
|
|
527
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
throw lastError;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// src/mcp/client.ts
|
|
465
534
|
var McpManager = class {
|
|
466
535
|
connections = [];
|
|
467
536
|
tools = [];
|
|
@@ -483,7 +552,8 @@ var McpManager = class {
|
|
|
483
552
|
serverName: name
|
|
484
553
|
});
|
|
485
554
|
}
|
|
486
|
-
} catch {
|
|
555
|
+
} catch (err) {
|
|
556
|
+
log.error("mcp", "Failed to connect to " + name + " MCP server", err);
|
|
487
557
|
console.error(` Warning: Could not connect to ${name} MCP server`);
|
|
488
558
|
}
|
|
489
559
|
}
|
|
@@ -496,10 +566,10 @@ var McpManager = class {
|
|
|
496
566
|
const conn = this.connections.find((c) => c.name === tool.serverName);
|
|
497
567
|
if (!conn) return `Error: server ${tool.serverName} not connected`;
|
|
498
568
|
try {
|
|
499
|
-
const result = await
|
|
500
|
-
name: toolName,
|
|
501
|
-
|
|
502
|
-
|
|
569
|
+
const result = await withRetry(
|
|
570
|
+
() => conn.client.callTool({ name: toolName, arguments: args }),
|
|
571
|
+
{ maxAttempts: 2, baseDelay: 500, retryable: (err) => err.message.includes("ETIMEDOUT") || err.message.includes("timeout") }
|
|
572
|
+
);
|
|
503
573
|
if (result.content && Array.isArray(result.content)) {
|
|
504
574
|
return result.content.filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n");
|
|
505
575
|
}
|
|
@@ -512,7 +582,8 @@ var McpManager = class {
|
|
|
512
582
|
for (const conn of this.connections) {
|
|
513
583
|
try {
|
|
514
584
|
await conn.client.close();
|
|
515
|
-
} catch {
|
|
585
|
+
} catch (err) {
|
|
586
|
+
log.debug("mcp", "Cleanup error disconnecting " + conn.name, err);
|
|
516
587
|
}
|
|
517
588
|
}
|
|
518
589
|
this.connections = [];
|
|
@@ -522,20 +593,23 @@ var McpManager = class {
|
|
|
522
593
|
|
|
523
594
|
// src/agent.ts
|
|
524
595
|
import * as readline from "readline";
|
|
596
|
+
import fs6 from "fs";
|
|
597
|
+
import path6 from "path";
|
|
598
|
+
import os6 from "os";
|
|
525
599
|
import pc3 from "picocolors";
|
|
526
600
|
|
|
527
601
|
// src/commands.ts
|
|
528
|
-
import
|
|
529
|
-
import
|
|
530
|
-
import
|
|
602
|
+
import fs5 from "fs";
|
|
603
|
+
import path5 from "path";
|
|
604
|
+
import os5 from "os";
|
|
531
605
|
import { execFileSync } from "child_process";
|
|
532
606
|
import pc from "picocolors";
|
|
533
607
|
|
|
534
608
|
// src/layers/parsers.ts
|
|
535
|
-
import
|
|
536
|
-
import
|
|
537
|
-
import
|
|
538
|
-
var home =
|
|
609
|
+
import fs4 from "fs";
|
|
610
|
+
import path4 from "path";
|
|
611
|
+
import os4 from "os";
|
|
612
|
+
var home = os4.homedir();
|
|
539
613
|
var LAYER_FILES = [
|
|
540
614
|
{ name: "identity", dir: ".acore", file: "core.md" },
|
|
541
615
|
{ name: "rules", dir: ".arules", file: "rules.md" },
|
|
@@ -571,11 +645,11 @@ function getLayerSummary(name, content) {
|
|
|
571
645
|
}
|
|
572
646
|
function getEcosystemStatus(mcpToolCount, amemConnected) {
|
|
573
647
|
const layers = LAYER_FILES.map((entry) => {
|
|
574
|
-
const filePath =
|
|
575
|
-
const exists =
|
|
648
|
+
const filePath = path4.join(home, entry.dir, entry.file);
|
|
649
|
+
const exists = fs4.existsSync(filePath);
|
|
576
650
|
let summary = "not configured";
|
|
577
651
|
if (exists) {
|
|
578
|
-
const content =
|
|
652
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
579
653
|
summary = getLayerSummary(entry.name, content);
|
|
580
654
|
}
|
|
581
655
|
return { name: entry.name, exists, path: filePath, summary };
|
|
@@ -590,10 +664,10 @@ function getEcosystemStatus(mcpToolCount, amemConnected) {
|
|
|
590
664
|
|
|
591
665
|
// src/commands.ts
|
|
592
666
|
function readEcosystemFile(filePath, label) {
|
|
593
|
-
if (!
|
|
667
|
+
if (!fs5.existsSync(filePath)) {
|
|
594
668
|
return pc.dim(`No ${label} file found at ${filePath}`);
|
|
595
669
|
}
|
|
596
|
-
return
|
|
670
|
+
return fs5.readFileSync(filePath, "utf-8").trim();
|
|
597
671
|
}
|
|
598
672
|
function parseCommand(input) {
|
|
599
673
|
const trimmed = input.trim();
|
|
@@ -614,9 +688,9 @@ async function mcpWrite(ctx, layer, tool, args) {
|
|
|
614
688
|
return pc.green(result);
|
|
615
689
|
}
|
|
616
690
|
async function handleIdentityCommand(action, args, ctx) {
|
|
617
|
-
const home2 =
|
|
691
|
+
const home2 = os5.homedir();
|
|
618
692
|
if (!action) {
|
|
619
|
-
const content = readEcosystemFile(
|
|
693
|
+
const content = readEcosystemFile(path5.join(home2, ".acore", "core.md"), "identity (acore)");
|
|
620
694
|
return { handled: true, output: content };
|
|
621
695
|
}
|
|
622
696
|
if (action === "update") {
|
|
@@ -640,9 +714,9 @@ async function handleIdentityCommand(action, args, ctx) {
|
|
|
640
714
|
return { handled: true, output: pc.yellow(`Unknown action: /identity ${action}. Use /identity or /identity update <section>.`) };
|
|
641
715
|
}
|
|
642
716
|
async function handleRulesCommand(action, args, ctx) {
|
|
643
|
-
const home2 =
|
|
717
|
+
const home2 = os5.homedir();
|
|
644
718
|
if (!action) {
|
|
645
|
-
const content = readEcosystemFile(
|
|
719
|
+
const content = readEcosystemFile(path5.join(home2, ".arules", "rules.md"), "guardrails (arules)");
|
|
646
720
|
return { handled: true, output: content };
|
|
647
721
|
}
|
|
648
722
|
if (action === "add") {
|
|
@@ -671,9 +745,9 @@ async function handleRulesCommand(action, args, ctx) {
|
|
|
671
745
|
return { handled: true, output: pc.yellow(`Unknown action: /rules ${action}. Use /rules [add|remove|toggle].`) };
|
|
672
746
|
}
|
|
673
747
|
async function handleWorkflowsCommand(action, args, ctx) {
|
|
674
|
-
const home2 =
|
|
748
|
+
const home2 = os5.homedir();
|
|
675
749
|
if (!action) {
|
|
676
|
-
const content = readEcosystemFile(
|
|
750
|
+
const content = readEcosystemFile(path5.join(home2, ".aflow", "flow.md"), "workflows (aflow)");
|
|
677
751
|
return { handled: true, output: content };
|
|
678
752
|
}
|
|
679
753
|
if (action === "add") {
|
|
@@ -693,9 +767,9 @@ async function handleWorkflowsCommand(action, args, ctx) {
|
|
|
693
767
|
return { handled: true, output: pc.yellow(`Unknown action: /workflows ${action}. Use /workflows [add|remove].`) };
|
|
694
768
|
}
|
|
695
769
|
async function handleToolsCommand(action, args, ctx) {
|
|
696
|
-
const home2 =
|
|
770
|
+
const home2 = os5.homedir();
|
|
697
771
|
if (!action) {
|
|
698
|
-
const content = readEcosystemFile(
|
|
772
|
+
const content = readEcosystemFile(path5.join(home2, ".akit", "kit.md"), "tools (akit)");
|
|
699
773
|
return { handled: true, output: content };
|
|
700
774
|
}
|
|
701
775
|
if (action === "add") {
|
|
@@ -718,9 +792,9 @@ async function handleToolsCommand(action, args, ctx) {
|
|
|
718
792
|
return { handled: true, output: pc.yellow(`Unknown action: /tools ${action}. Use /tools [add|remove].`) };
|
|
719
793
|
}
|
|
720
794
|
async function handleSkillsCommand(action, args, ctx) {
|
|
721
|
-
const home2 =
|
|
795
|
+
const home2 = os5.homedir();
|
|
722
796
|
if (!action) {
|
|
723
|
-
const content = readEcosystemFile(
|
|
797
|
+
const content = readEcosystemFile(path5.join(home2, ".askill", "skills.md"), "skills (askill)");
|
|
724
798
|
return { handled: true, output: content };
|
|
725
799
|
}
|
|
726
800
|
if (action === "install") {
|
|
@@ -740,9 +814,9 @@ async function handleSkillsCommand(action, args, ctx) {
|
|
|
740
814
|
return { handled: true, output: pc.yellow(`Unknown action: /skills ${action}. Use /skills [install|uninstall].`) };
|
|
741
815
|
}
|
|
742
816
|
async function handleEvalCommand(action, args, ctx) {
|
|
743
|
-
const home2 =
|
|
817
|
+
const home2 = os5.homedir();
|
|
744
818
|
if (!action) {
|
|
745
|
-
const content = readEcosystemFile(
|
|
819
|
+
const content = readEcosystemFile(path5.join(home2, ".aeval", "eval.md"), "evaluation (aeval)");
|
|
746
820
|
return { handled: true, output: content };
|
|
747
821
|
}
|
|
748
822
|
if (action === "milestone") {
|
|
@@ -833,6 +907,9 @@ function handleHelp() {
|
|
|
833
907
|
` ${pc.cyan("/memory")} View recent memories [search|clear ...]`,
|
|
834
908
|
` ${pc.cyan("/status")} Ecosystem dashboard`,
|
|
835
909
|
` ${pc.cyan("/doctor")} Health check all layers`,
|
|
910
|
+
` ${pc.cyan("/decisions")} View decision log [<project>]`,
|
|
911
|
+
` ${pc.cyan("/export")} Export conversation to markdown`,
|
|
912
|
+
` ${pc.cyan("/debug")} Show debug log`,
|
|
836
913
|
` ${pc.cyan("/save")} Save conversation to memory`,
|
|
837
914
|
` ${pc.cyan("/model")} Show current LLM model`,
|
|
838
915
|
` ${pc.cyan("/update")} Check for updates`,
|
|
@@ -846,9 +923,9 @@ function handleSave() {
|
|
|
846
923
|
return { handled: true, saveConversation: true };
|
|
847
924
|
}
|
|
848
925
|
function handleReconfig() {
|
|
849
|
-
const configPath =
|
|
850
|
-
if (
|
|
851
|
-
|
|
926
|
+
const configPath = path5.join(os5.homedir(), ".aman-agent", "config.json");
|
|
927
|
+
if (fs5.existsSync(configPath)) {
|
|
928
|
+
fs5.unlinkSync(configPath);
|
|
852
929
|
}
|
|
853
930
|
return {
|
|
854
931
|
handled: true,
|
|
@@ -862,7 +939,7 @@ function handleReconfig() {
|
|
|
862
939
|
function handleUpdate() {
|
|
863
940
|
try {
|
|
864
941
|
const current = execFileSync("npm", ["view", "@aman_asmuei/aman-agent", "version"], { encoding: "utf-8" }).trim();
|
|
865
|
-
const local = JSON.parse(
|
|
942
|
+
const local = JSON.parse(fs5.readFileSync(path5.join(__dirname, "..", "package.json"), "utf-8")).version;
|
|
866
943
|
if (current === local) {
|
|
867
944
|
return { handled: true, output: `${pc.green("Up to date")} \u2014 v${local}` };
|
|
868
945
|
}
|
|
@@ -891,6 +968,35 @@ function handleUpdate() {
|
|
|
891
968
|
};
|
|
892
969
|
}
|
|
893
970
|
}
|
|
971
|
+
async function handleDecisionsCommand(action, _args, ctx) {
|
|
972
|
+
if (!ctx.mcpManager) {
|
|
973
|
+
return { handled: true, output: pc.red("Decisions not available: MCP not connected.") };
|
|
974
|
+
}
|
|
975
|
+
const scope = action || void 0;
|
|
976
|
+
const result = await ctx.mcpManager.callTool("memory_recall", {
|
|
977
|
+
query: "decision",
|
|
978
|
+
type: "decision",
|
|
979
|
+
limit: 20,
|
|
980
|
+
...scope ? { scope } : {}
|
|
981
|
+
});
|
|
982
|
+
if (result.startsWith("Error")) {
|
|
983
|
+
return { handled: true, output: pc.red(result) };
|
|
984
|
+
}
|
|
985
|
+
return { handled: true, output: pc.bold("Decision Log:\n") + result };
|
|
986
|
+
}
|
|
987
|
+
function handleExportCommand() {
|
|
988
|
+
return { handled: true, exportConversation: true };
|
|
989
|
+
}
|
|
990
|
+
function handleDebugCommand() {
|
|
991
|
+
const logPath = path5.join(os5.homedir(), ".aman-agent", "debug.log");
|
|
992
|
+
if (!fs5.existsSync(logPath)) {
|
|
993
|
+
return { handled: true, output: pc.dim("No debug log found.") };
|
|
994
|
+
}
|
|
995
|
+
const content = fs5.readFileSync(logPath, "utf-8");
|
|
996
|
+
const lines = content.trim().split("\n");
|
|
997
|
+
const last20 = lines.slice(-20).join("\n");
|
|
998
|
+
return { handled: true, output: pc.bold("Debug Log (last 20 entries):\n") + pc.dim(last20) };
|
|
999
|
+
}
|
|
894
1000
|
async function handleCommand(input, ctx) {
|
|
895
1001
|
const trimmed = input.trim();
|
|
896
1002
|
if (!trimmed.startsWith("/")) return { handled: false };
|
|
@@ -926,6 +1032,12 @@ async function handleCommand(input, ctx) {
|
|
|
926
1032
|
return handleDoctorCommand(ctx);
|
|
927
1033
|
case "save":
|
|
928
1034
|
return handleSave();
|
|
1035
|
+
case "decisions":
|
|
1036
|
+
return handleDecisionsCommand(action, args, ctx);
|
|
1037
|
+
case "export":
|
|
1038
|
+
return handleExportCommand();
|
|
1039
|
+
case "debug":
|
|
1040
|
+
return handleDebugCommand();
|
|
929
1041
|
case "update-config":
|
|
930
1042
|
case "reconfig":
|
|
931
1043
|
return handleReconfig();
|
|
@@ -940,6 +1052,24 @@ async function handleCommand(input, ctx) {
|
|
|
940
1052
|
// src/hooks.ts
|
|
941
1053
|
import pc2 from "picocolors";
|
|
942
1054
|
import * as p from "@clack/prompts";
|
|
1055
|
+
function getTimeContext() {
|
|
1056
|
+
const now = /* @__PURE__ */ new Date();
|
|
1057
|
+
const hour = now.getHours();
|
|
1058
|
+
const days = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
1059
|
+
const day = days[now.getDay()];
|
|
1060
|
+
let period;
|
|
1061
|
+
if (hour < 6) period = "late-night";
|
|
1062
|
+
else if (hour < 12) period = "morning";
|
|
1063
|
+
else if (hour < 17) period = "afternoon";
|
|
1064
|
+
else if (hour < 21) period = "evening";
|
|
1065
|
+
else period = "night";
|
|
1066
|
+
const timeStr = now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
|
|
1067
|
+
const dateStr = now.toLocaleDateString();
|
|
1068
|
+
return `<time-context>
|
|
1069
|
+
Current time: ${dateStr} ${timeStr} (${period}, ${day})
|
|
1070
|
+
Adapt your tone naturally \u2014 don't announce the time, just be contextually appropriate.
|
|
1071
|
+
</time-context>`;
|
|
1072
|
+
}
|
|
943
1073
|
var isHookCall = false;
|
|
944
1074
|
async function onSessionStart(ctx) {
|
|
945
1075
|
let greeting = "";
|
|
@@ -951,7 +1081,8 @@ async function onSessionStart(ctx) {
|
|
|
951
1081
|
if (result && !result.startsWith("Error")) {
|
|
952
1082
|
greeting += result;
|
|
953
1083
|
}
|
|
954
|
-
} catch {
|
|
1084
|
+
} catch (err) {
|
|
1085
|
+
log.warn("hooks", "memory_context recall failed", err);
|
|
955
1086
|
} finally {
|
|
956
1087
|
isHookCall = false;
|
|
957
1088
|
}
|
|
@@ -964,11 +1095,26 @@ async function onSessionStart(ctx) {
|
|
|
964
1095
|
if (greeting) greeting += "\n";
|
|
965
1096
|
greeting += result;
|
|
966
1097
|
}
|
|
967
|
-
} catch {
|
|
1098
|
+
} catch (err) {
|
|
1099
|
+
log.warn("hooks", "identity_summary failed", err);
|
|
968
1100
|
} finally {
|
|
969
1101
|
isHookCall = false;
|
|
970
1102
|
}
|
|
971
1103
|
}
|
|
1104
|
+
const timeContext = getTimeContext();
|
|
1105
|
+
if (greeting) greeting += "\n" + timeContext;
|
|
1106
|
+
else greeting = timeContext;
|
|
1107
|
+
try {
|
|
1108
|
+
isHookCall = true;
|
|
1109
|
+
const reminderResult = await ctx.mcpManager.callTool("reminder_check", {});
|
|
1110
|
+
if (reminderResult && !reminderResult.startsWith("Error") && !reminderResult.includes("No pending")) {
|
|
1111
|
+
greeting += "\n\n<pending-reminders>\n" + reminderResult + "\n</pending-reminders>";
|
|
1112
|
+
}
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
log.debug("hooks", "reminder_check failed", err);
|
|
1115
|
+
} finally {
|
|
1116
|
+
isHookCall = false;
|
|
1117
|
+
}
|
|
972
1118
|
if (greeting) {
|
|
973
1119
|
contextInjection = `<session-context>
|
|
974
1120
|
${greeting}
|
|
@@ -1000,10 +1146,12 @@ async function onBeforeToolExec(toolName, toolArgs, ctx) {
|
|
|
1000
1146
|
reason: parsed.violations.join("; ")
|
|
1001
1147
|
};
|
|
1002
1148
|
}
|
|
1003
|
-
} catch {
|
|
1149
|
+
} catch (err) {
|
|
1150
|
+
log.debug("hooks", "rules_check parse failed", err);
|
|
1004
1151
|
}
|
|
1005
1152
|
return { allow: true };
|
|
1006
|
-
} catch {
|
|
1153
|
+
} catch (err) {
|
|
1154
|
+
log.warn("hooks", "rules_check call failed", err);
|
|
1007
1155
|
return { allow: true };
|
|
1008
1156
|
} finally {
|
|
1009
1157
|
isHookCall = false;
|
|
@@ -1035,7 +1183,8 @@ async function onWorkflowMatch(userInput, ctx) {
|
|
|
1035
1183
|
}
|
|
1036
1184
|
}
|
|
1037
1185
|
return null;
|
|
1038
|
-
} catch {
|
|
1186
|
+
} catch (err) {
|
|
1187
|
+
log.debug("hooks", "workflow_list failed", err);
|
|
1039
1188
|
return null;
|
|
1040
1189
|
} finally {
|
|
1041
1190
|
isHookCall = false;
|
|
@@ -1054,7 +1203,8 @@ async function onSessionEnd(ctx, messages, sessionId) {
|
|
|
1054
1203
|
role: msg.role,
|
|
1055
1204
|
content: msg.content.slice(0, 5e3)
|
|
1056
1205
|
});
|
|
1057
|
-
} catch {
|
|
1206
|
+
} catch (err) {
|
|
1207
|
+
log.debug("hooks", "memory_log write failed for " + sessionId, err);
|
|
1058
1208
|
} finally {
|
|
1059
1209
|
isHookCall = false;
|
|
1060
1210
|
}
|
|
@@ -1104,7 +1254,8 @@ async function onSessionEnd(ctx, messages, sessionId) {
|
|
|
1104
1254
|
}
|
|
1105
1255
|
}
|
|
1106
1256
|
}
|
|
1107
|
-
} catch {
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
log.warn("hooks", "session end hook failed", err);
|
|
1108
1259
|
}
|
|
1109
1260
|
}
|
|
1110
1261
|
|
|
@@ -1131,7 +1282,7 @@ function estimateTotalTokens(messages) {
|
|
|
1131
1282
|
var MAX_CONVERSATION_TOKENS = 8e4;
|
|
1132
1283
|
var KEEP_RECENT = 10;
|
|
1133
1284
|
var KEEP_INITIAL = 2;
|
|
1134
|
-
function trimConversation(messages,
|
|
1285
|
+
async function trimConversation(messages, client) {
|
|
1135
1286
|
const totalTokens = estimateTotalTokens(messages);
|
|
1136
1287
|
if (totalTokens < MAX_CONVERSATION_TOKENS || messages.length <= KEEP_INITIAL + KEEP_RECENT) {
|
|
1137
1288
|
return;
|
|
@@ -1139,20 +1290,39 @@ function trimConversation(messages, _client) {
|
|
|
1139
1290
|
const initial = messages.slice(0, KEEP_INITIAL);
|
|
1140
1291
|
const recent = messages.slice(-KEEP_RECENT);
|
|
1141
1292
|
const middle = messages.slice(KEEP_INITIAL, messages.length - KEEP_RECENT);
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1293
|
+
const middleText = middle.filter((m) => typeof m.content === "string" && m.content.length > 0).map((m) => `[${m.role}]: ${m.content.slice(0, 500)}`).slice(0, 30).join("\n");
|
|
1294
|
+
let summaryText;
|
|
1295
|
+
try {
|
|
1296
|
+
const summaryPrompt = "Summarize the following conversation messages in 3-5 bullet points. Preserve: decisions made, user preferences expressed, action items, and key facts discussed. Be concise.\n\n" + middleText;
|
|
1297
|
+
let fullText = "";
|
|
1298
|
+
await client.chat(
|
|
1299
|
+
"You are a concise summarizer. Return only bullet points, no preamble.",
|
|
1300
|
+
[{ role: "user", content: summaryPrompt }],
|
|
1301
|
+
(chunk) => {
|
|
1302
|
+
if (chunk.type === "text" && chunk.text) fullText += chunk.text;
|
|
1303
|
+
}
|
|
1304
|
+
);
|
|
1305
|
+
summaryText = `<conversation-summary>
|
|
1306
|
+
Summary of ${middle.length} earlier messages:
|
|
1307
|
+
|
|
1308
|
+
${fullText}
|
|
1309
|
+
</conversation-summary>`;
|
|
1310
|
+
log.debug("context", `Summarized ${middle.length} messages via LLM`);
|
|
1311
|
+
} catch (err) {
|
|
1312
|
+
log.warn("context", "LLM summarization failed, using fallback", err);
|
|
1313
|
+
const summaryParts = [];
|
|
1314
|
+
for (const msg of middle) {
|
|
1315
|
+
if (typeof msg.content === "string" && msg.content.length > 0) {
|
|
1316
|
+
const preview = msg.content.slice(0, 150);
|
|
1317
|
+
summaryParts.push(`[${msg.role}]: ${preview}${msg.content.length > 150 ? "..." : ""}`);
|
|
1318
|
+
}
|
|
1147
1319
|
}
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
The following is a summary of ${middle.length} earlier messages that were compressed to save context:
|
|
1320
|
+
summaryText = `<conversation-summary>
|
|
1321
|
+
Summary of ${middle.length} earlier messages:
|
|
1151
1322
|
|
|
1152
1323
|
${summaryParts.slice(0, 20).join("\n")}
|
|
1153
|
-
${summaryParts.length > 20 ? `
|
|
1154
|
-
... and ${summaryParts.length - 20} more messages` : ""}
|
|
1155
1324
|
</conversation-summary>`;
|
|
1325
|
+
}
|
|
1156
1326
|
messages.length = 0;
|
|
1157
1327
|
messages.push(...initial);
|
|
1158
1328
|
messages.push({ role: "user", content: summaryText });
|
|
@@ -1160,7 +1330,151 @@ ${summaryParts.length > 20 ? `
|
|
|
1160
1330
|
messages.push(...recent);
|
|
1161
1331
|
}
|
|
1162
1332
|
|
|
1333
|
+
// src/memory-extractor.ts
|
|
1334
|
+
var AUTO_STORE_TYPES = /* @__PURE__ */ new Set(["preference", "fact", "pattern", "topology"]);
|
|
1335
|
+
var CONFIRM_TYPES = /* @__PURE__ */ new Set(["decision", "correction"]);
|
|
1336
|
+
var VALID_TYPES = /* @__PURE__ */ new Set([...AUTO_STORE_TYPES, ...CONFIRM_TYPES]);
|
|
1337
|
+
var MIN_RESPONSE_LENGTH = 50;
|
|
1338
|
+
var MIN_TURNS_BETWEEN_EMPTY = 3;
|
|
1339
|
+
var EXTRACTION_PROMPT = `Analyze this conversation turn. Extract any information worth remembering long-term.
|
|
1340
|
+
|
|
1341
|
+
Return a JSON array (empty [] if nothing worth storing):
|
|
1342
|
+
[{
|
|
1343
|
+
"content": "what to remember \u2014 be specific and self-contained",
|
|
1344
|
+
"type": "preference|fact|pattern|decision|correction|topology",
|
|
1345
|
+
"tags": ["relevant", "tags"],
|
|
1346
|
+
"confidence": 0.0-1.0,
|
|
1347
|
+
"scope": "global"
|
|
1348
|
+
}]
|
|
1349
|
+
|
|
1350
|
+
Type guide:
|
|
1351
|
+
- "preference" = user likes/dislikes/preferences
|
|
1352
|
+
- "fact" = objective information about systems, people, projects
|
|
1353
|
+
- "pattern" = recurring behavior, coding style, approach
|
|
1354
|
+
- "topology" = how systems/components connect to each other
|
|
1355
|
+
- "decision" = explicit choice between alternatives (requires confirmation)
|
|
1356
|
+
- "correction" = user correcting a prior wrong assumption (requires confirmation)
|
|
1357
|
+
|
|
1358
|
+
Rules:
|
|
1359
|
+
- Only extract genuinely useful LONG-TERM information
|
|
1360
|
+
- Skip ephemeral things ("user asked about X" is NOT useful)
|
|
1361
|
+
- Be conservative \u2014 90% of turns produce nothing worth storing
|
|
1362
|
+
- Return ONLY the JSON array, no other text`;
|
|
1363
|
+
function shouldExtract(assistantResponse, turnsSinceLastExtraction, lastExtractionCount) {
|
|
1364
|
+
if (lastExtractionCount > 0) return true;
|
|
1365
|
+
if (assistantResponse.length < MIN_RESPONSE_LENGTH) return false;
|
|
1366
|
+
if (turnsSinceLastExtraction < MIN_TURNS_BETWEEN_EMPTY) return false;
|
|
1367
|
+
return true;
|
|
1368
|
+
}
|
|
1369
|
+
function parseExtractionResult(raw) {
|
|
1370
|
+
try {
|
|
1371
|
+
let cleaned = raw.trim();
|
|
1372
|
+
const codeBlockMatch = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
1373
|
+
if (codeBlockMatch) {
|
|
1374
|
+
cleaned = codeBlockMatch[1].trim();
|
|
1375
|
+
}
|
|
1376
|
+
const parsed = JSON.parse(cleaned);
|
|
1377
|
+
if (!Array.isArray(parsed)) return [];
|
|
1378
|
+
return parsed.filter(
|
|
1379
|
+
(item) => typeof item.content === "string" && item.content.length > 0 && typeof item.type === "string" && VALID_TYPES.has(item.type)
|
|
1380
|
+
);
|
|
1381
|
+
} catch {
|
|
1382
|
+
return [];
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function extractMemories(userMessage, assistantResponse, client, mcpManager, state, confirmFn) {
|
|
1386
|
+
if (!shouldExtract(assistantResponse, state.turnsSinceLastExtraction, state.lastExtractionCount)) {
|
|
1387
|
+
state.turnsSinceLastExtraction++;
|
|
1388
|
+
return 0;
|
|
1389
|
+
}
|
|
1390
|
+
try {
|
|
1391
|
+
const conversationTurn = `User: ${userMessage.slice(0, 2e3)}
|
|
1392
|
+
|
|
1393
|
+
Assistant: ${assistantResponse.slice(0, 2e3)}`;
|
|
1394
|
+
let fullText = "";
|
|
1395
|
+
await client.chat(
|
|
1396
|
+
EXTRACTION_PROMPT,
|
|
1397
|
+
[{ role: "user", content: conversationTurn }],
|
|
1398
|
+
(chunk) => {
|
|
1399
|
+
if (chunk.type === "text" && chunk.text) fullText += chunk.text;
|
|
1400
|
+
}
|
|
1401
|
+
);
|
|
1402
|
+
const candidates = parseExtractionResult(fullText);
|
|
1403
|
+
state.turnsSinceLastExtraction = 0;
|
|
1404
|
+
state.lastExtractionCount = candidates.length;
|
|
1405
|
+
if (candidates.length === 0) return 0;
|
|
1406
|
+
let stored = 0;
|
|
1407
|
+
for (const candidate of candidates) {
|
|
1408
|
+
try {
|
|
1409
|
+
const existing = await mcpManager.callTool("memory_recall", {
|
|
1410
|
+
query: candidate.content,
|
|
1411
|
+
limit: 1
|
|
1412
|
+
});
|
|
1413
|
+
if (existing && !existing.startsWith("Error")) {
|
|
1414
|
+
try {
|
|
1415
|
+
const parsed = JSON.parse(existing);
|
|
1416
|
+
if (Array.isArray(parsed) && parsed.length > 0 && parsed[0].score > 0.85) {
|
|
1417
|
+
log.debug("extractor", "Skipping duplicate: " + candidate.content);
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
} catch {
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
} catch {
|
|
1424
|
+
}
|
|
1425
|
+
if (CONFIRM_TYPES.has(candidate.type)) {
|
|
1426
|
+
const confirmed = await confirmFn(candidate.content);
|
|
1427
|
+
if (!confirmed) continue;
|
|
1428
|
+
}
|
|
1429
|
+
try {
|
|
1430
|
+
await mcpManager.callTool("memory_store", {
|
|
1431
|
+
content: candidate.content,
|
|
1432
|
+
type: candidate.type,
|
|
1433
|
+
tags: candidate.tags,
|
|
1434
|
+
confidence: candidate.confidence,
|
|
1435
|
+
source: "auto-extraction",
|
|
1436
|
+
scope: candidate.scope
|
|
1437
|
+
});
|
|
1438
|
+
stored++;
|
|
1439
|
+
log.debug("extractor", "Stored " + candidate.type + ": " + candidate.content);
|
|
1440
|
+
} catch (err) {
|
|
1441
|
+
log.warn("extractor", "Failed to store: " + candidate.content, err);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
return stored;
|
|
1445
|
+
} catch (err) {
|
|
1446
|
+
log.debug("extractor", "extraction failed", err);
|
|
1447
|
+
state.turnsSinceLastExtraction = 0;
|
|
1448
|
+
state.lastExtractionCount = 0;
|
|
1449
|
+
return 0;
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1163
1453
|
// src/agent.ts
|
|
1454
|
+
async function recallForMessage(input, mcpManager) {
|
|
1455
|
+
try {
|
|
1456
|
+
const result = await mcpManager.callTool("memory_recall", {
|
|
1457
|
+
query: input,
|
|
1458
|
+
limit: 5,
|
|
1459
|
+
compact: true
|
|
1460
|
+
});
|
|
1461
|
+
if (!result || result.startsWith("Error") || result.includes("No memories found")) {
|
|
1462
|
+
return null;
|
|
1463
|
+
}
|
|
1464
|
+
const tokenEstimate = Math.round(result.split(/\s+/).filter(Boolean).length * 1.3);
|
|
1465
|
+
return {
|
|
1466
|
+
text: `
|
|
1467
|
+
|
|
1468
|
+
<relevant-memories>
|
|
1469
|
+
${result}
|
|
1470
|
+
</relevant-memories>`,
|
|
1471
|
+
tokenEstimate
|
|
1472
|
+
};
|
|
1473
|
+
} catch (err) {
|
|
1474
|
+
log.debug("agent", "memory recall failed", err);
|
|
1475
|
+
return null;
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1164
1478
|
function generateSessionId() {
|
|
1165
1479
|
const now = /* @__PURE__ */ new Date();
|
|
1166
1480
|
const pad = (n) => n.toString().padStart(2, "0");
|
|
@@ -1169,6 +1483,16 @@ function generateSessionId() {
|
|
|
1169
1483
|
async function runAgent(client, systemPrompt, aiName, model, tools, mcpManager, hooksConfig) {
|
|
1170
1484
|
const messages = [];
|
|
1171
1485
|
const sessionId = generateSessionId();
|
|
1486
|
+
const extractorState = { turnsSinceLastExtraction: 0, lastExtractionCount: 0 };
|
|
1487
|
+
const isRetryable = (err) => err.message.includes("Rate limit") || err.message.includes("rate limit") || err.message.includes("ECONNRESET") || err.message.includes("ETIMEDOUT") || err.message.includes("fetch failed");
|
|
1488
|
+
const onChunkHandler = (chunk) => {
|
|
1489
|
+
if (chunk.type === "text" && chunk.text) {
|
|
1490
|
+
process.stdout.write(chunk.text);
|
|
1491
|
+
}
|
|
1492
|
+
if (chunk.type === "done") {
|
|
1493
|
+
process.stdout.write("\n");
|
|
1494
|
+
}
|
|
1495
|
+
};
|
|
1172
1496
|
const rl = readline.createInterface({
|
|
1173
1497
|
input: process.stdin,
|
|
1174
1498
|
output: process.stdout
|
|
@@ -1178,7 +1502,8 @@ async function runAgent(client, systemPrompt, aiName, model, tools, mcpManager,
|
|
|
1178
1502
|
try {
|
|
1179
1503
|
const hookCtx = { mcpManager, config: hooksConfig };
|
|
1180
1504
|
await onSessionEnd(hookCtx, messages, sessionId);
|
|
1181
|
-
} catch {
|
|
1505
|
+
} catch (err) {
|
|
1506
|
+
log.debug("agent", "session end hook failed on SIGINT", err);
|
|
1182
1507
|
}
|
|
1183
1508
|
}
|
|
1184
1509
|
console.log(pc3.dim("\nGoodbye.\n"));
|
|
@@ -1206,7 +1531,8 @@ Type a message, ${pc3.dim("/help")} for commands, or ${pc3.dim("/quit")} to exit
|
|
|
1206
1531
|
messages.push({ role: "user", content: session.contextInjection });
|
|
1207
1532
|
messages.push({ role: "assistant", content: "I have context from our previous sessions. How can I help?" });
|
|
1208
1533
|
}
|
|
1209
|
-
} catch {
|
|
1534
|
+
} catch (err) {
|
|
1535
|
+
log.warn("agent", "session start hook failed", err);
|
|
1210
1536
|
}
|
|
1211
1537
|
}
|
|
1212
1538
|
while (true) {
|
|
@@ -1219,13 +1545,39 @@ Type a message, ${pc3.dim("/help")} for commands, or ${pc3.dim("/quit")} to exit
|
|
|
1219
1545
|
try {
|
|
1220
1546
|
const hookCtx = { mcpManager, config: hooksConfig };
|
|
1221
1547
|
await onSessionEnd(hookCtx, messages, sessionId);
|
|
1222
|
-
} catch {
|
|
1548
|
+
} catch (err) {
|
|
1549
|
+
log.debug("agent", "session end hook failed on quit", err);
|
|
1223
1550
|
}
|
|
1224
1551
|
}
|
|
1225
1552
|
console.log(pc3.dim("\nGoodbye.\n"));
|
|
1226
1553
|
rl.close();
|
|
1227
1554
|
return;
|
|
1228
1555
|
}
|
|
1556
|
+
if (cmdResult.exportConversation) {
|
|
1557
|
+
try {
|
|
1558
|
+
const exportDir = path6.join(os6.homedir(), ".aman-agent", "exports");
|
|
1559
|
+
fs6.mkdirSync(exportDir, { recursive: true });
|
|
1560
|
+
const exportPath = path6.join(exportDir, `${sessionId}.md`);
|
|
1561
|
+
const lines = [
|
|
1562
|
+
`# Conversation \u2014 ${(/* @__PURE__ */ new Date()).toLocaleString()}`,
|
|
1563
|
+
`**Model:** ${model}`,
|
|
1564
|
+
"",
|
|
1565
|
+
"---",
|
|
1566
|
+
""
|
|
1567
|
+
];
|
|
1568
|
+
for (const msg of messages) {
|
|
1569
|
+
if (typeof msg.content === "string") {
|
|
1570
|
+
const label = msg.role === "user" ? "**You:**" : `**${aiName}:**`;
|
|
1571
|
+
lines.push(`${label} ${msg.content}`, "");
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
fs6.writeFileSync(exportPath, lines.join("\n"), "utf-8");
|
|
1575
|
+
console.log(pc3.green(`Exported to ${exportPath}`));
|
|
1576
|
+
} catch {
|
|
1577
|
+
console.log(pc3.red("Failed to export conversation."));
|
|
1578
|
+
}
|
|
1579
|
+
continue;
|
|
1580
|
+
}
|
|
1229
1581
|
if (cmdResult.saveConversation && mcpManager) {
|
|
1230
1582
|
try {
|
|
1231
1583
|
await saveConversationToMemory(mcpManager, messages, sessionId);
|
|
@@ -1261,79 +1613,102 @@ ${wfMatch.steps}
|
|
|
1261
1613
|
console.log(pc3.dim(` Using "${wfMatch.name}" workflow.`));
|
|
1262
1614
|
}
|
|
1263
1615
|
}
|
|
1264
|
-
} catch {
|
|
1616
|
+
} catch (err) {
|
|
1617
|
+
log.debug("agent", "workflow match failed", err);
|
|
1265
1618
|
}
|
|
1266
1619
|
}
|
|
1267
|
-
trimConversation(messages, client);
|
|
1620
|
+
await trimConversation(messages, client);
|
|
1268
1621
|
messages.push({ role: "user", content: input });
|
|
1622
|
+
let augmentedSystemPrompt = activeSystemPrompt;
|
|
1623
|
+
if (mcpManager) {
|
|
1624
|
+
const recall = await recallForMessage(input, mcpManager);
|
|
1625
|
+
if (recall) {
|
|
1626
|
+
augmentedSystemPrompt = activeSystemPrompt + recall.text;
|
|
1627
|
+
process.stdout.write(pc3.dim(` [memories: ~${recall.tokenEstimate} tokens]
|
|
1628
|
+
`));
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1269
1631
|
process.stdout.write(pc3.cyan(`
|
|
1270
1632
|
${aiName} > `));
|
|
1271
1633
|
try {
|
|
1272
|
-
let response = await
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
(chunk) => {
|
|
1276
|
-
if (chunk.type === "text" && chunk.text) {
|
|
1277
|
-
process.stdout.write(chunk.text);
|
|
1278
|
-
}
|
|
1279
|
-
if (chunk.type === "done") {
|
|
1280
|
-
process.stdout.write("\n");
|
|
1281
|
-
}
|
|
1282
|
-
},
|
|
1283
|
-
tools
|
|
1634
|
+
let response = await withRetry(
|
|
1635
|
+
() => client.chat(augmentedSystemPrompt, messages, onChunkHandler, tools),
|
|
1636
|
+
{ maxAttempts: 3, baseDelay: 1e3, retryable: isRetryable }
|
|
1284
1637
|
);
|
|
1285
1638
|
messages.push(response.message);
|
|
1286
1639
|
while (response.toolUses.length > 0 && mcpManager) {
|
|
1287
|
-
const toolResults =
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1640
|
+
const toolResults = await Promise.all(
|
|
1641
|
+
response.toolUses.map(async (toolUse) => {
|
|
1642
|
+
if (hooksConfig) {
|
|
1643
|
+
const hookCtx = { mcpManager, config: hooksConfig };
|
|
1644
|
+
const check = await onBeforeToolExec(toolUse.name, toolUse.input, hookCtx);
|
|
1645
|
+
if (!check.allow) {
|
|
1646
|
+
process.stdout.write(pc3.red(` [BLOCKED: ${check.reason}]
|
|
1647
|
+
`));
|
|
1648
|
+
return {
|
|
1649
|
+
type: "tool_result",
|
|
1650
|
+
tool_use_id: toolUse.id,
|
|
1651
|
+
content: `BLOCKED by guardrail: ${check.reason}`,
|
|
1652
|
+
is_error: true
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
process.stdout.write(pc3.dim(` [using ${toolUse.name}...]
|
|
1294
1657
|
`));
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1658
|
+
const result = await mcpManager.callTool(toolUse.name, toolUse.input);
|
|
1659
|
+
const skipLogging = ["memory_log", "memory_recall", "memory_context", "memory_detail", "reminder_check"].includes(toolUse.name);
|
|
1660
|
+
if (!skipLogging) {
|
|
1661
|
+
mcpManager.callTool("memory_log", {
|
|
1662
|
+
session_id: sessionId,
|
|
1663
|
+
role: "system",
|
|
1664
|
+
content: `[tool:${toolUse.name}] input=${JSON.stringify(toolUse.input).slice(0, 500)} result=${result.slice(0, 500)}`
|
|
1665
|
+
}).catch(() => {
|
|
1300
1666
|
});
|
|
1301
|
-
continue;
|
|
1302
1667
|
}
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
toolUse.input
|
|
1311
|
-
);
|
|
1312
|
-
toolResults.push({
|
|
1313
|
-
type: "tool_result",
|
|
1314
|
-
tool_use_id: toolUse.id,
|
|
1315
|
-
content: result
|
|
1316
|
-
});
|
|
1317
|
-
}
|
|
1668
|
+
return {
|
|
1669
|
+
type: "tool_result",
|
|
1670
|
+
tool_use_id: toolUse.id,
|
|
1671
|
+
content: result
|
|
1672
|
+
};
|
|
1673
|
+
})
|
|
1674
|
+
);
|
|
1318
1675
|
messages.push({
|
|
1319
1676
|
role: "user",
|
|
1320
1677
|
content: toolResults
|
|
1321
1678
|
});
|
|
1322
|
-
response = await
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
(chunk) => {
|
|
1326
|
-
if (chunk.type === "text" && chunk.text) {
|
|
1327
|
-
process.stdout.write(chunk.text);
|
|
1328
|
-
}
|
|
1329
|
-
if (chunk.type === "done") {
|
|
1330
|
-
process.stdout.write("\n");
|
|
1331
|
-
}
|
|
1332
|
-
},
|
|
1333
|
-
tools
|
|
1679
|
+
response = await withRetry(
|
|
1680
|
+
() => client.chat(augmentedSystemPrompt, messages, onChunkHandler, tools),
|
|
1681
|
+
{ maxAttempts: 3, baseDelay: 1e3, retryable: isRetryable }
|
|
1334
1682
|
);
|
|
1335
1683
|
messages.push(response.message);
|
|
1336
1684
|
}
|
|
1685
|
+
if (mcpManager && hooksConfig?.extractMemories) {
|
|
1686
|
+
const assistantText = typeof response.message.content === "string" ? response.message.content : response.message.content.filter((b) => b.type === "text").map((b) => "text" in b ? b.text : "").join("");
|
|
1687
|
+
if (assistantText) {
|
|
1688
|
+
const confirmFn = async (content) => {
|
|
1689
|
+
return new Promise((resolve) => {
|
|
1690
|
+
rl.question(
|
|
1691
|
+
pc3.dim(` Remember: "${content}"? (y/N) `),
|
|
1692
|
+
(answer) => resolve(answer.toLowerCase() === "y")
|
|
1693
|
+
);
|
|
1694
|
+
});
|
|
1695
|
+
};
|
|
1696
|
+
const count = await extractMemories(
|
|
1697
|
+
input,
|
|
1698
|
+
assistantText,
|
|
1699
|
+
client,
|
|
1700
|
+
mcpManager,
|
|
1701
|
+
extractorState,
|
|
1702
|
+
confirmFn
|
|
1703
|
+
);
|
|
1704
|
+
if (count > 0) {
|
|
1705
|
+
process.stdout.write(pc3.dim(` [${count} memory${count > 1 ? "ies" : ""} stored]
|
|
1706
|
+
`));
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
} else {
|
|
1710
|
+
extractorState.turnsSinceLastExtraction++;
|
|
1711
|
+
}
|
|
1337
1712
|
} catch (error) {
|
|
1338
1713
|
const message = error instanceof Error ? error.message : "Unknown error occurred";
|
|
1339
1714
|
console.error(pc3.red(`
|
|
@@ -1352,15 +1727,16 @@ async function saveConversationToMemory(mcpManager, messages, sessionId) {
|
|
|
1352
1727
|
role: msg.role,
|
|
1353
1728
|
content: msg.content.slice(0, 5e3)
|
|
1354
1729
|
});
|
|
1355
|
-
} catch {
|
|
1730
|
+
} catch (err) {
|
|
1731
|
+
log.debug("agent", "memory_log write failed", err);
|
|
1356
1732
|
}
|
|
1357
1733
|
}
|
|
1358
1734
|
}
|
|
1359
1735
|
|
|
1360
1736
|
// src/index.ts
|
|
1361
|
-
import
|
|
1362
|
-
import
|
|
1363
|
-
import
|
|
1737
|
+
import fs7 from "fs";
|
|
1738
|
+
import path7 from "path";
|
|
1739
|
+
import os7 from "os";
|
|
1364
1740
|
var program = new Command();
|
|
1365
1741
|
program.name("aman-agent").description("Your AI companion, running locally").version("0.1.0").option("--model <model>", "Override LLM model").option("--budget <tokens>", "Token budget for system prompt (default: 8000)", parseInt).action(async (options) => {
|
|
1366
1742
|
p2.intro(pc4.bold("aman agent") + pc4.dim(" \u2014 starting your AI companion"));
|
|
@@ -1472,10 +1848,10 @@ program.name("aman-agent").description("Your AI companion, running locally").ver
|
|
|
1472
1848
|
}
|
|
1473
1849
|
}
|
|
1474
1850
|
p2.log.info(`Model: ${pc4.dim(model)}`);
|
|
1475
|
-
const corePath =
|
|
1851
|
+
const corePath = path7.join(os7.homedir(), ".acore", "core.md");
|
|
1476
1852
|
let aiName = "Assistant";
|
|
1477
|
-
if (
|
|
1478
|
-
const content =
|
|
1853
|
+
if (fs7.existsSync(corePath)) {
|
|
1854
|
+
const content = fs7.readFileSync(corePath, "utf-8");
|
|
1479
1855
|
const match = content.match(/^# (.+)$/m);
|
|
1480
1856
|
if (match) aiName = match[1];
|
|
1481
1857
|
}
|
|
@@ -1487,6 +1863,23 @@ program.name("aman-agent").description("Your AI companion, running locally").ver
|
|
|
1487
1863
|
const mcpTools = mcpManager.getTools();
|
|
1488
1864
|
if (mcpTools.length > 0) {
|
|
1489
1865
|
p2.log.success(`${mcpTools.length} MCP tools available`);
|
|
1866
|
+
if (mcpTools.some((t) => t.name === "memory_consolidate")) {
|
|
1867
|
+
try {
|
|
1868
|
+
const consolidateResult = await mcpManager.callTool("memory_consolidate", { dry_run: false });
|
|
1869
|
+
if (consolidateResult && !consolidateResult.startsWith("Error")) {
|
|
1870
|
+
try {
|
|
1871
|
+
const report = JSON.parse(consolidateResult);
|
|
1872
|
+
if (report.merged > 0 || report.pruned > 0 || report.promoted > 0) {
|
|
1873
|
+
p2.log.info(
|
|
1874
|
+
`Memory health: ${report.healthScore ?? "?"}% ` + pc4.dim(`(merged ${report.merged}, pruned ${report.pruned}, promoted ${report.promoted})`)
|
|
1875
|
+
);
|
|
1876
|
+
}
|
|
1877
|
+
} catch {
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
} catch {
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1490
1883
|
} else {
|
|
1491
1884
|
p2.log.info(
|
|
1492
1885
|
"No MCP tools connected (install aman-mcp or amem for tool support)"
|