@istarry/dsh-assistant-profile 0.2.1 → 0.3.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/lib/client.js +575 -7
- package/lib/client.js.map +1 -1
- package/lib/index.js +476 -31
- package/package.json +7 -2
package/lib/index.js
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
import z from "@deepseek-ai/schemastery";
|
|
2
|
+
import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
|
|
3
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
4
|
+
import { BlockAssembler, createUserMessage } from "@deepseek-ai/dsh-llm";
|
|
5
|
+
import { SessionId } from "@deepseek-ai/dsh-session";
|
|
6
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { dirname, join } from "node:path";
|
|
8
|
+
import { dshHomePath } from "@deepseek-ai/dsh-home-paths";
|
|
2
9
|
//#region ../tooling/node_modules/.pnpm/js-yaml@4.2.0/node_modules/js-yaml/dist/js-yaml.mjs
|
|
3
10
|
/*! js-yaml 4.2.0 https://github.com/nodeca/js-yaml @license MIT */
|
|
4
11
|
var __create = Object.create;
|
|
@@ -2407,13 +2414,126 @@ const DEFAULT_PERSONA = {
|
|
|
2407
2414
|
].join("\n")
|
|
2408
2415
|
};
|
|
2409
2416
|
//#endregion
|
|
2417
|
+
//#region ../.build/tsc/memory.js
|
|
2418
|
+
/**
|
|
2419
|
+
* 助理记忆的 Markdown 存储。
|
|
2420
|
+
*
|
|
2421
|
+
* 只有一个档案,所以只有一份记忆:`<dshHome>/assistant-profile/memory.md`。
|
|
2422
|
+
* 路径经 `dshHomePath` 解析,`$DSH_HOME` 覆盖与跨平台 home 都能落对位置。
|
|
2423
|
+
*
|
|
2424
|
+
* 文档形状:`# 标题` 之类的自由文本由用户自己写,每条记忆是
|
|
2425
|
+
* `- [YYYY-MM-DD HH:mm] 内容` 一行。时间戳是条目的主元数据——重存同一件事会刷新
|
|
2426
|
+
* 时间戳而不是复制一行。
|
|
2427
|
+
*
|
|
2428
|
+
* 读写都是同步的:提示词组装调用 section thunk 时没有 await 点,而文件很小。
|
|
2429
|
+
*
|
|
2430
|
+
* @module @istarry/dsh-assistant-profile/memory
|
|
2431
|
+
*/
|
|
2432
|
+
/** 本包在 harness home 下占用的目录名。 */
|
|
2433
|
+
const MEMORY_DIR = "assistant-profile";
|
|
2434
|
+
/** 记忆文档的文件名。 */
|
|
2435
|
+
const MEMORY_FILE = "memory.md";
|
|
2436
|
+
const ENTRY_RE = /^-?\s*\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2})\]\s*(.*)$/;
|
|
2437
|
+
/** 把本地时间格式化成条目的时间戳。 */
|
|
2438
|
+
function stampNow(date = /* @__PURE__ */ new Date()) {
|
|
2439
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
2440
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
|
|
2441
|
+
}
|
|
2442
|
+
/** 记忆文档的绝对路径。 */
|
|
2443
|
+
function memoryPath() {
|
|
2444
|
+
return join(dshHomePath(MEMORY_DIR), MEMORY_FILE);
|
|
2445
|
+
}
|
|
2446
|
+
/** 读取整篇记忆文档;文件不存在时返回空串。 */
|
|
2447
|
+
function readMemoryText() {
|
|
2448
|
+
const path = memoryPath();
|
|
2449
|
+
return existsSync(path) ? readFileSync(path, "utf8") : "";
|
|
2450
|
+
}
|
|
2451
|
+
/** 整体替换记忆文档(用户编辑框里的保存)。 */
|
|
2452
|
+
function writeMemory(text) {
|
|
2453
|
+
const path = memoryPath();
|
|
2454
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
2455
|
+
writeFileSync(path, text, "utf8");
|
|
2456
|
+
}
|
|
2457
|
+
/** 从文档正文里解析出所有记忆条目。 */
|
|
2458
|
+
function parseEntries(text) {
|
|
2459
|
+
const entries = [];
|
|
2460
|
+
for (const line of text.split(/\r?\n/)) {
|
|
2461
|
+
const match = ENTRY_RE.exec(line);
|
|
2462
|
+
if (match === null) continue;
|
|
2463
|
+
entries.push({
|
|
2464
|
+
stamp: match[1] ?? "",
|
|
2465
|
+
content: (match[2] ?? "").trim()
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2468
|
+
return entries;
|
|
2469
|
+
}
|
|
2470
|
+
/**
|
|
2471
|
+
* 去重比较键:内容大小写折叠、连续空白压成一个空格。
|
|
2472
|
+
* 「记得 DSH 路径」和「记得DSH路径」因此算同一条。
|
|
2473
|
+
* @param content - 原始条目内容。
|
|
2474
|
+
* @returns 用于比较的规范键。
|
|
2475
|
+
*/
|
|
2476
|
+
function dedupeKey(content) {
|
|
2477
|
+
return content.trim().toLowerCase().replace(/\s+/g, " ");
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* 追加一条记忆,对整篇文档去重:已存在同义条目就刷新它的时间戳,否则追加新行。
|
|
2481
|
+
* 非条目行(标题、空行)原样保留。
|
|
2482
|
+
* @param content - 要记住的事实(单行文本)。
|
|
2483
|
+
* @param at - 时间戳来源,测试可注入。
|
|
2484
|
+
* @returns 这次写入做了什么。
|
|
2485
|
+
*/
|
|
2486
|
+
function appendMemory(content, at = /* @__PURE__ */ new Date()) {
|
|
2487
|
+
const text = readMemoryText();
|
|
2488
|
+
const stamp = stampNow(at);
|
|
2489
|
+
const trimmed = content.trim();
|
|
2490
|
+
if (trimmed === "") return {
|
|
2491
|
+
appended: false,
|
|
2492
|
+
refreshed: false,
|
|
2493
|
+
content: trimmed
|
|
2494
|
+
};
|
|
2495
|
+
const key = dedupeKey(trimmed);
|
|
2496
|
+
const headerLines = [];
|
|
2497
|
+
const entryLines = [];
|
|
2498
|
+
for (const line of text.split(/\r?\n/)) if (ENTRY_RE.test(line)) entryLines.push(line);
|
|
2499
|
+
else headerLines.push(line);
|
|
2500
|
+
for (let index = 0; index < entryLines.length; index += 1) {
|
|
2501
|
+
const match = ENTRY_RE.exec(entryLines[index]);
|
|
2502
|
+
if (match === null) continue;
|
|
2503
|
+
if (dedupeKey((match[2] ?? "").trim()) === key) {
|
|
2504
|
+
entryLines[index] = `- [${stamp}] ${trimmed}`;
|
|
2505
|
+
writeChunked(headerLines, entryLines);
|
|
2506
|
+
return {
|
|
2507
|
+
appended: false,
|
|
2508
|
+
refreshed: true,
|
|
2509
|
+
content: trimmed
|
|
2510
|
+
};
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
entryLines.push(`- [${stamp}] ${trimmed}`);
|
|
2514
|
+
writeChunked(headerLines, entryLines);
|
|
2515
|
+
return {
|
|
2516
|
+
appended: true,
|
|
2517
|
+
refreshed: false,
|
|
2518
|
+
content: trimmed
|
|
2519
|
+
};
|
|
2520
|
+
}
|
|
2521
|
+
/** 把标题行与条目行拼回一篇文档并写入(顺手收掉首尾空行)。 */
|
|
2522
|
+
function writeChunked(headerLines, entryLines) {
|
|
2523
|
+
const meaningful = [...headerLines];
|
|
2524
|
+
while (meaningful.length > 0 && /^\s*$/.test(meaningful[0])) meaningful.shift();
|
|
2525
|
+
while (meaningful.length > 0 && /^\s*$/.test(meaningful[meaningful.length - 1])) meaningful.pop();
|
|
2526
|
+
const body = [...meaningful, ...entryLines].join("\n");
|
|
2527
|
+
writeMemory(body.endsWith("\n") ? body : `${body}\n`);
|
|
2528
|
+
}
|
|
2529
|
+
//#endregion
|
|
2410
2530
|
//#region ../.build/tsc/index.js
|
|
2411
2531
|
/**
|
|
2412
2532
|
* 助理档案 · 宿主半端。
|
|
2413
2533
|
*
|
|
2414
|
-
*
|
|
2415
|
-
* agent **自己的上下文**里注册一段 `deployment:persona-prefix
|
|
2416
|
-
*
|
|
2534
|
+
* 一个档案、一份人格、四个模式通用。人格的做法只有一件事:每个对话 agent 创建时,
|
|
2535
|
+
* 在这个 agent **自己的上下文**里注册一段 `deployment:persona-prefix`——作用域链上最近
|
|
2536
|
+
* 的同名段胜出,于是预设自带的身份句被档案替换,而工具、流程、模式语义一律不动。
|
|
2417
2537
|
* 官方给子代理发独立人格走的正是这条路径(`dsh-subagent` 的 applyChildComposition),
|
|
2418
2538
|
* 所以它对标准 / PTC / 极简 / 创造四种预设都成立,不用复制预设组合,也不用改模式
|
|
2419
2539
|
* 选择器与会话绑定逻辑。
|
|
@@ -2421,30 +2541,82 @@ const DEFAULT_PERSONA = {
|
|
|
2421
2541
|
* 极简模式的预设人格是「整段系统提示只有这一句」(complete: true)。遮蔽段必须镜像
|
|
2422
2542
|
* 这个标记,否则会把它变成「全套工具说明 + 人格」,等于改掉了那个模式的定义。
|
|
2423
2543
|
*
|
|
2424
|
-
*
|
|
2425
|
-
*
|
|
2426
|
-
*
|
|
2544
|
+
* 文本是**每次组装提示词时求值**的,所以保存后已打开的会话下一句话就用新人格。
|
|
2545
|
+
*
|
|
2546
|
+
* ## 助理记忆
|
|
2547
|
+
*
|
|
2548
|
+
* 单档案所以单记忆:一份 markdown(见 `memory.ts`),以 `memory:assistant` 段注入,
|
|
2549
|
+
* order 500——排在人格(0)之后、工具目录(1000)之前:记忆描述的是「用户和我是什么
|
|
2550
|
+
* 关系」,工具说明会引用这些前提。段文本每次组装重读文件,所以编辑器保存后下一句话
|
|
2551
|
+
* 就带着新记忆。两个开关都是 volatile:设置页写、加载器就地提交、无需重挂。
|
|
2552
|
+
*
|
|
2553
|
+
* 模型主动记忆走 `memory_add` 工具,仅在「记忆开 + 自动记忆开」时注册;设置页重建
|
|
2554
|
+
* 注册靠监听 `loader/volatile-update`。按钮提取走 `rememberFromSession`:用该会话自己的
|
|
2555
|
+
* 模型把选中的对话文本提炼成条目后追加。
|
|
2427
2556
|
*
|
|
2428
2557
|
* @module @istarry/dsh-assistant-profile
|
|
2429
2558
|
*/
|
|
2559
|
+
var __runInitializers = function(thisArg, initializers, value) {
|
|
2560
|
+
var useValue = arguments.length > 2;
|
|
2561
|
+
for (var i = 0; i < initializers.length; i++) value = useValue ? initializers[i].call(thisArg, value) : initializers[i].call(thisArg);
|
|
2562
|
+
return useValue ? value : void 0;
|
|
2563
|
+
};
|
|
2564
|
+
var __esDecorate = function(ctor, descriptorIn, decorators, contextIn, initializers, extraInitializers) {
|
|
2565
|
+
function accept(f) {
|
|
2566
|
+
if (f !== void 0 && typeof f !== "function") throw new TypeError("Function expected");
|
|
2567
|
+
return f;
|
|
2568
|
+
}
|
|
2569
|
+
var kind = contextIn.kind, key = kind === "getter" ? "get" : kind === "setter" ? "set" : "value";
|
|
2570
|
+
var target = !descriptorIn && ctor ? contextIn["static"] ? ctor : ctor.prototype : null;
|
|
2571
|
+
var descriptor = descriptorIn || (target ? Object.getOwnPropertyDescriptor(target, contextIn.name) : {});
|
|
2572
|
+
var _, done = false;
|
|
2573
|
+
for (var i = decorators.length - 1; i >= 0; i--) {
|
|
2574
|
+
var context = {};
|
|
2575
|
+
for (var p in contextIn) context[p] = p === "access" ? {} : contextIn[p];
|
|
2576
|
+
for (var p in contextIn.access) context.access[p] = contextIn.access[p];
|
|
2577
|
+
context.addInitializer = function(f) {
|
|
2578
|
+
if (done) throw new TypeError("Cannot add initializers after decoration has completed");
|
|
2579
|
+
extraInitializers.push(accept(f || null));
|
|
2580
|
+
};
|
|
2581
|
+
var result = (0, decorators[i])(kind === "accessor" ? {
|
|
2582
|
+
get: descriptor.get,
|
|
2583
|
+
set: descriptor.set
|
|
2584
|
+
} : descriptor[key], context);
|
|
2585
|
+
if (kind === "accessor") {
|
|
2586
|
+
if (result === void 0) continue;
|
|
2587
|
+
if (result === null || typeof result !== "object") throw new TypeError("Object expected");
|
|
2588
|
+
if (_ = accept(result.get)) descriptor.get = _;
|
|
2589
|
+
if (_ = accept(result.set)) descriptor.set = _;
|
|
2590
|
+
if (_ = accept(result.init)) initializers.unshift(_);
|
|
2591
|
+
} else if (_ = accept(result)) if (kind === "field") initializers.unshift(_);
|
|
2592
|
+
else descriptor[key] = _;
|
|
2593
|
+
}
|
|
2594
|
+
if (target) Object.defineProperty(target, contextIn.name, descriptor);
|
|
2595
|
+
done = true;
|
|
2596
|
+
};
|
|
2430
2597
|
/** Cordis 插件名。 */
|
|
2431
2598
|
const name = "assistant-profile";
|
|
2432
2599
|
/** 人格所在的那一段的规范名——预设与档案共用同一个名字,遮蔽才成立。 */
|
|
2433
2600
|
const PERSONA_PREFIX_SECTION = "deployment:persona-prefix";
|
|
2434
2601
|
/** 该段在提示词里的位置(宿主 SECTION_ORDERS.DEPLOYMENT_PERSONA_PREFIX)。 */
|
|
2435
2602
|
const PERSONA_PREFIX_ORDER = 0;
|
|
2603
|
+
/** 记忆段的名字与位置:人格之后、工具目录(1000)之前。 */
|
|
2604
|
+
const MEMORY_SECTION = "memory:assistant";
|
|
2605
|
+
const MEMORY_ORDER = 500;
|
|
2436
2606
|
/** 人格插件的包名,兼容没有 `id` 的旧写法。 */
|
|
2437
2607
|
const PERSONA_PLUGIN_NAME = "@deepseek-ai/dsh-persona";
|
|
2438
2608
|
/**
|
|
2439
|
-
* 运行时 schema。字段都是 volatile
|
|
2440
|
-
*
|
|
2441
|
-
*
|
|
2609
|
+
* 运行时 schema。字段都是 volatile:它们由「助理档案」页与「Agent记忆」设置页
|
|
2610
|
+
* 而不是插件配置页编辑,改动经设置通道写回 profile 的 patch,重启后仍在。默认值取
|
|
2611
|
+
* 内置「默认三」,所以装好就是一条带人设的鱼;三栏全部清空再保存,则什么都不注入。
|
|
2442
2612
|
*/
|
|
2443
2613
|
const Config = z.object({
|
|
2444
2614
|
name: z.string().default(DEFAULT_PERSONA.name).volatile(),
|
|
2445
2615
|
language: z.string().default(DEFAULT_PERSONA.language).volatile(),
|
|
2446
2616
|
persona: z.string().default(DEFAULT_PERSONA.persona).volatile(),
|
|
2447
|
-
customsJson: z.string().default("[]").volatile()
|
|
2617
|
+
customsJson: z.string().default("[]").volatile(),
|
|
2618
|
+
memoryEnabled: z.boolean().default(true).description("Inject the assistant memory into the system prompt").volatile(),
|
|
2619
|
+
autoMemory: z.boolean().default(false).description("Let the model write memories on its own judgment (requires memoryEnabled)").volatile()
|
|
2448
2620
|
});
|
|
2449
2621
|
/** 跨插件重载共享的当前配置引用:旧闭包也能读到新值。 */
|
|
2450
2622
|
const LIVE = Symbol.for("dsh.assistant-profile.config");
|
|
@@ -2463,7 +2635,7 @@ function text(value) {
|
|
|
2463
2635
|
return trimmed === void 0 || trimmed === "" ? void 0 : trimmed;
|
|
2464
2636
|
}
|
|
2465
2637
|
/**
|
|
2466
|
-
*
|
|
2638
|
+
* 把档案渲染成系统提示的首段。三段空行分隔、各自可缺省;全空时返回空串,
|
|
2467
2639
|
* 由调用方回落到预设自带的身份。
|
|
2468
2640
|
* @returns 人格文本。
|
|
2469
2641
|
*/
|
|
@@ -2532,7 +2704,7 @@ async function basePersonaOf(ctx, agentPreset) {
|
|
|
2532
2704
|
* @param ctx - 插件上下文。
|
|
2533
2705
|
* @param agent - 新建(或恢复)的 agent。
|
|
2534
2706
|
*/
|
|
2535
|
-
async function
|
|
2707
|
+
async function installPersona(ctx, agent) {
|
|
2536
2708
|
if (agent.session.header.origin === "subagent") return;
|
|
2537
2709
|
const agentCtx = agent.ctx;
|
|
2538
2710
|
const presetId = ctx.get("agentPresets")?.composedPreset(agentCtx);
|
|
@@ -2551,24 +2723,297 @@ async function install(ctx, agent) {
|
|
|
2551
2723
|
...base?.complete === true ? { complete: true } : {}
|
|
2552
2724
|
});
|
|
2553
2725
|
}
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
}
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2726
|
+
const MEMORY_ADD_DESCRIPTION = "Add one fact to the assistant's long-term memory when the conversation reveals something worth remembering across future sessions: stable preferences, work habits, durable decisions, or facts the human stated as lasting. Remember prose conclusions, not the raw exchange; merge related facts into one entry. Do not store credentials, one-off task details, or anything the human said is transient.";
|
|
2727
|
+
/** `memory_add` 的输出结构。 */
|
|
2728
|
+
const MEMORY_ADD_VALUE_SCHEMA = {
|
|
2729
|
+
type: "object",
|
|
2730
|
+
additionalProperties: false,
|
|
2731
|
+
properties: {
|
|
2732
|
+
saved: {
|
|
2733
|
+
type: "boolean",
|
|
2734
|
+
required: true
|
|
2735
|
+
},
|
|
2736
|
+
outcome: { type: "string" },
|
|
2737
|
+
content: { type: "string" }
|
|
2738
|
+
}
|
|
2739
|
+
};
|
|
2740
|
+
/** 把选中的对话文本提炼成记忆条目的指令(与 agent-suite 一致的规则)。 */
|
|
2741
|
+
const REMEMBER_INSTRUCTION = "你是一个长期记忆提炼引擎。用户从对话中圈选了下面这段内容,明确希望把它记入助手的长期记忆。请把这些内容提炼成简短、稳定、可跨会话复用的记忆条目。\n规则:\n- 每行输出一条记忆内容本身(不带时间戳、不带 \"- \" 前缀、不带编号)。\n- 用户既然圈选了它,请默认提取出其中明确、稳定的信息:称呼、偏好、习惯、约定、身份事实、项目背景。\n- 把相关事实合并成一条;不要逐字复述,只提炼结论,但保留关键名称、数字、关系。\n- 只有当整段内容完全是临时任务细节或含凭据、密钥时,才不输出条目(此时只输出一个真正的空行)。\n- 用与对话片段一致的语言书写(对话是中文就用中文,是英文就用英文)。\n- 严禁输出\"空行\"\"(空行)\"\"无\"\"没有\"\"none\"之类的占位文字。";
|
|
2742
|
+
let AssistantProfile = (() => {
|
|
2743
|
+
let _classSuper = TypertRemoteService;
|
|
2744
|
+
let _instanceExtraInitializers = [];
|
|
2745
|
+
let _readMemory_decorators;
|
|
2746
|
+
let _writeMemory_decorators;
|
|
2747
|
+
let _rememberFromSession_decorators;
|
|
2748
|
+
return class AssistantProfile extends _classSuper {
|
|
2749
|
+
static {
|
|
2750
|
+
const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
|
|
2751
|
+
_readMemory_decorators = [Remote("readMemory")];
|
|
2752
|
+
_writeMemory_decorators = [Remote("writeMemory")];
|
|
2753
|
+
_rememberFromSession_decorators = [Remote("rememberFromSession")];
|
|
2754
|
+
__esDecorate(this, null, _readMemory_decorators, {
|
|
2755
|
+
kind: "method",
|
|
2756
|
+
name: "readMemory",
|
|
2757
|
+
static: false,
|
|
2758
|
+
private: false,
|
|
2759
|
+
access: {
|
|
2760
|
+
has: (obj) => "readMemory" in obj,
|
|
2761
|
+
get: (obj) => obj.readMemory
|
|
2762
|
+
},
|
|
2763
|
+
metadata: _metadata
|
|
2764
|
+
}, null, _instanceExtraInitializers);
|
|
2765
|
+
__esDecorate(this, null, _writeMemory_decorators, {
|
|
2766
|
+
kind: "method",
|
|
2767
|
+
name: "writeMemory",
|
|
2768
|
+
static: false,
|
|
2769
|
+
private: false,
|
|
2770
|
+
access: {
|
|
2771
|
+
has: (obj) => "writeMemory" in obj,
|
|
2772
|
+
get: (obj) => obj.writeMemory
|
|
2773
|
+
},
|
|
2774
|
+
metadata: _metadata
|
|
2775
|
+
}, null, _instanceExtraInitializers);
|
|
2776
|
+
__esDecorate(this, null, _rememberFromSession_decorators, {
|
|
2777
|
+
kind: "method",
|
|
2778
|
+
name: "rememberFromSession",
|
|
2779
|
+
static: false,
|
|
2780
|
+
private: false,
|
|
2781
|
+
access: {
|
|
2782
|
+
has: (obj) => "rememberFromSession" in obj,
|
|
2783
|
+
get: (obj) => obj.rememberFromSession
|
|
2784
|
+
},
|
|
2785
|
+
metadata: _metadata
|
|
2786
|
+
}, null, _instanceExtraInitializers);
|
|
2787
|
+
if (_metadata) Object.defineProperty(this, Symbol.metadata, {
|
|
2788
|
+
enumerable: true,
|
|
2789
|
+
configurable: true,
|
|
2790
|
+
writable: true,
|
|
2791
|
+
value: _metadata
|
|
2792
|
+
});
|
|
2793
|
+
}
|
|
2794
|
+
config = __runInitializers(this, _instanceExtraInitializers);
|
|
2795
|
+
static inject = [
|
|
2796
|
+
"systemPrompt",
|
|
2797
|
+
"tools",
|
|
2798
|
+
"llm"
|
|
2799
|
+
];
|
|
2800
|
+
static Config = Config;
|
|
2801
|
+
/** `memory_add` 工具的注册 disposer,只在开关允许时存在。 */
|
|
2802
|
+
toolDisposer;
|
|
2803
|
+
constructor(ctx, config) {
|
|
2804
|
+
super(ctx, "assistantProfile");
|
|
2805
|
+
this.config = config;
|
|
2806
|
+
live.config = config;
|
|
2807
|
+
ctx.inject(["settings"], (child) => {
|
|
2808
|
+
child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
|
|
2809
|
+
});
|
|
2810
|
+
ctx.effect(() => ctx.systemPrompt.section({
|
|
2811
|
+
name: MEMORY_SECTION,
|
|
2812
|
+
order: MEMORY_ORDER,
|
|
2813
|
+
text: () => {
|
|
2814
|
+
if (config.memoryEnabled.get() !== true) return "";
|
|
2815
|
+
return readMemoryText().trim();
|
|
2816
|
+
}
|
|
2817
|
+
}), "assistant-profile: memory section");
|
|
2818
|
+
const reinstall = (agent) => installPersona(ctx, agent).catch((error) => {
|
|
2819
|
+
ctx.logger.warn(`assistant-profile: persona install failed: ${String(error)}`);
|
|
2820
|
+
});
|
|
2821
|
+
ctx.effect(() => ctx.on("agent/created", ({ agent }) => {
|
|
2822
|
+
reinstall(agent);
|
|
2823
|
+
}), "assistant-profile: persona");
|
|
2824
|
+
ctx.effect(() => ctx.on("agent-preset/selected", (sessionId) => {
|
|
2825
|
+
const agent = ctx.get("agents")?.get(String(sessionId));
|
|
2826
|
+
if (agent !== void 0) reinstall(agent);
|
|
2827
|
+
}), "assistant-profile: preset switch");
|
|
2828
|
+
this.syncTool();
|
|
2829
|
+
ctx.on("loader/volatile-update", () => {
|
|
2830
|
+
this.syncTool();
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2833
|
+
/** 按当前开关注册或撤下 `memory_add` 工具。 */
|
|
2834
|
+
syncTool() {
|
|
2835
|
+
const want = this.config.memoryEnabled.get() === true && this.config.autoMemory.get() === true;
|
|
2836
|
+
if (want && this.toolDisposer === void 0) this.toolDisposer = this.registerTool();
|
|
2837
|
+
else if (!want && this.toolDisposer !== void 0) {
|
|
2838
|
+
this.toolDisposer();
|
|
2839
|
+
this.toolDisposer = void 0;
|
|
2840
|
+
}
|
|
2841
|
+
}
|
|
2842
|
+
/** 注册模型侧的记忆工具,返回撤下函数。 */
|
|
2843
|
+
registerTool() {
|
|
2844
|
+
const ctx = this.ctx;
|
|
2845
|
+
const disposer = ctx.tools.register(defineTool({
|
|
2846
|
+
name: "memory_add",
|
|
2847
|
+
description: MEMORY_ADD_DESCRIPTION,
|
|
2848
|
+
parameters: { text: {
|
|
2849
|
+
type: "string",
|
|
2850
|
+
required: true,
|
|
2851
|
+
description: "The stable fact to remember, in its final prose form."
|
|
2852
|
+
} },
|
|
2853
|
+
output: {
|
|
2854
|
+
schema: MEMORY_ADD_VALUE_SCHEMA,
|
|
2855
|
+
render: (_args, value) => {
|
|
2856
|
+
const v = value;
|
|
2857
|
+
if (v.saved) return [{
|
|
2858
|
+
type: "text",
|
|
2859
|
+
text: `记住:${v.content ?? ""}`
|
|
2860
|
+
}];
|
|
2861
|
+
return [{
|
|
2862
|
+
type: "text",
|
|
2863
|
+
text: `未记住:${v.outcome ?? "未启用"}`
|
|
2864
|
+
}];
|
|
2865
|
+
}
|
|
2866
|
+
},
|
|
2867
|
+
execute: async (args, _exec) => {
|
|
2868
|
+
const trimmed = args.text.trim();
|
|
2869
|
+
if (trimmed === "") return {
|
|
2870
|
+
saved: false,
|
|
2871
|
+
outcome: "empty fact"
|
|
2872
|
+
};
|
|
2873
|
+
const result = appendMemory(trimmed);
|
|
2874
|
+
return {
|
|
2875
|
+
saved: true,
|
|
2876
|
+
content: result.content,
|
|
2877
|
+
outcome: result.appended ? "appended" : result.refreshed ? "refreshed" : "unchanged"
|
|
2878
|
+
};
|
|
2879
|
+
}
|
|
2880
|
+
}));
|
|
2881
|
+
const teardown = () => {
|
|
2882
|
+
disposer();
|
|
2883
|
+
this.toolDisposer = void 0;
|
|
2884
|
+
};
|
|
2885
|
+
ctx.effect(() => teardown, "assistant-profile: memory tool");
|
|
2886
|
+
return () => {
|
|
2887
|
+
disposer();
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
/** 读取记忆全文(设置页编辑框)。 */
|
|
2891
|
+
readMemory() {
|
|
2892
|
+
return readMemoryText();
|
|
2893
|
+
}
|
|
2894
|
+
/** 整体保存记忆(设置页编辑框的保存按钮)。 */
|
|
2895
|
+
writeMemory(text) {
|
|
2896
|
+
writeMemory(text);
|
|
2897
|
+
}
|
|
2898
|
+
/** 追加一条记忆(内部使用/将来的其它入口)。 */
|
|
2899
|
+
appendMemory(content) {
|
|
2900
|
+
return appendMemory(content);
|
|
2901
|
+
}
|
|
2902
|
+
/**
|
|
2903
|
+
* 用某个会话自己的模型,把选中的对话文本提炼成记忆条目后追加。
|
|
2904
|
+
*
|
|
2905
|
+
* 这是 UI 按钮的路径:用户在弹窗里选一段聊天记录,模型把其中稳定的事实抽出来。
|
|
2906
|
+
* 会话必须是活的(它的 agent 提供模型配置),否则没有可用的 provider/model。
|
|
2907
|
+
* @param sessionId - 执行提炼的会话。
|
|
2908
|
+
* @param text - 选中的对话文本。
|
|
2909
|
+
* @returns 是否写入、写入了哪些条目,以及失败原因。
|
|
2910
|
+
*/
|
|
2911
|
+
async rememberFromSession(sessionId, text) {
|
|
2912
|
+
const agent = this.ctx.get("agents")?.get(SessionId(sessionId));
|
|
2913
|
+
if (agent === void 0) return {
|
|
2914
|
+
saved: false,
|
|
2915
|
+
outcome: "session not open"
|
|
2916
|
+
};
|
|
2917
|
+
const callConfig = (agent.session.requestHeader?.())?.config;
|
|
2918
|
+
if (callConfig === void 0) return {
|
|
2919
|
+
saved: false,
|
|
2920
|
+
outcome: "no provider/model available"
|
|
2921
|
+
};
|
|
2922
|
+
const trimmed = text.trim();
|
|
2923
|
+
if (trimmed === "") return {
|
|
2924
|
+
saved: false,
|
|
2925
|
+
outcome: "empty selection"
|
|
2926
|
+
};
|
|
2927
|
+
const assembler = new BlockAssembler();
|
|
2928
|
+
const messages = [createUserMessage({
|
|
2929
|
+
content: [{
|
|
2930
|
+
type: "text",
|
|
2931
|
+
text: trimmed
|
|
2932
|
+
}],
|
|
2933
|
+
source: { kind: "user" }
|
|
2934
|
+
}), createUserMessage({
|
|
2935
|
+
content: [{
|
|
2936
|
+
type: "text",
|
|
2937
|
+
text: REMEMBER_INSTRUCTION
|
|
2938
|
+
}],
|
|
2939
|
+
source: { kind: "user" }
|
|
2940
|
+
})];
|
|
2941
|
+
try {
|
|
2942
|
+
for await (const chunk of this.ctx.llm.stream({
|
|
2943
|
+
...callConfig,
|
|
2944
|
+
messages,
|
|
2945
|
+
sessionId: SessionId(sessionId)
|
|
2946
|
+
})) assembler.push(chunk);
|
|
2947
|
+
} catch (error) {
|
|
2948
|
+
return {
|
|
2949
|
+
saved: false,
|
|
2950
|
+
outcome: `analysis failed: ${errorChain(error)}`
|
|
2951
|
+
};
|
|
2952
|
+
}
|
|
2953
|
+
const failure = rememberFinishError(assembler.finish);
|
|
2954
|
+
if (failure !== void 0) return {
|
|
2955
|
+
saved: false,
|
|
2956
|
+
outcome: failure
|
|
2957
|
+
};
|
|
2958
|
+
const entries = splitEntries(assembler.blocks());
|
|
2959
|
+
if (entries.length === 0) return {
|
|
2960
|
+
saved: false,
|
|
2961
|
+
outcome: "nothing worth remembering"
|
|
2962
|
+
};
|
|
2963
|
+
for (const entry of entries) appendMemory(entry);
|
|
2964
|
+
return {
|
|
2965
|
+
saved: true,
|
|
2966
|
+
entries,
|
|
2967
|
+
outcome: `remembered ${entries.length} entries`
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
};
|
|
2971
|
+
})();
|
|
2972
|
+
/** 把错误及其 cause 链压成一行。 */
|
|
2973
|
+
function errorChain(error) {
|
|
2974
|
+
const parts = [];
|
|
2975
|
+
let current = error;
|
|
2976
|
+
while (typeof current === "object" && current !== null && parts.length < 4) {
|
|
2977
|
+
const message = current.message;
|
|
2978
|
+
if (typeof message !== "string" || message === "") break;
|
|
2979
|
+
parts.push(message);
|
|
2980
|
+
current = current.cause;
|
|
2981
|
+
}
|
|
2982
|
+
return parts.join(" | ");
|
|
2983
|
+
}
|
|
2984
|
+
/** 把终止原因映射成失败描述(fail-closed)。 */
|
|
2985
|
+
function rememberFinishError(finish) {
|
|
2986
|
+
switch (finish.kind) {
|
|
2987
|
+
case "error":
|
|
2988
|
+
case "aborted": return `analysis ${finish.kind}: ${errorChain(finish.failure)}`;
|
|
2989
|
+
case "max-tokens": return "analysis truncated at the token cap";
|
|
2990
|
+
default: return;
|
|
2991
|
+
}
|
|
2992
|
+
}
|
|
2993
|
+
/** 把模型输出的文本拆成一行一条记忆(去前缀、丢空行、丢占位词)。 */
|
|
2994
|
+
function splitEntries(blocks) {
|
|
2995
|
+
const entries = [];
|
|
2996
|
+
for (const block of blocks) {
|
|
2997
|
+
if (block.type !== "text" || typeof block.text !== "string") continue;
|
|
2998
|
+
for (const raw of block.text.split(/\r?\n/)) {
|
|
2999
|
+
const line = stripInvisible(raw.trim().replace(/^[-*]\s+/, "").trim());
|
|
3000
|
+
if (line === "") continue;
|
|
3001
|
+
if (isVacuousEntry(line)) continue;
|
|
3002
|
+
entries.push(line);
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return entries;
|
|
3006
|
+
}
|
|
3007
|
+
/** 去掉模型可能在正文周围吐出的不可见字符。 */
|
|
3008
|
+
function stripInvisible(value) {
|
|
3009
|
+
return value.replace(/[\u0000-\u001F\u007F-\u009F\u00A0\u00AD\u1680\u2000-\u200B\u2028-\u202F\u205F-\u206F\u3000\uFEFF\uFFFD]/g, "").trim();
|
|
3010
|
+
}
|
|
3011
|
+
/** 这一行是不是「没有内容」的占位词。 */
|
|
3012
|
+
function isVacuousEntry(line) {
|
|
3013
|
+
const cleaned = stripInvisible(line);
|
|
3014
|
+
if (cleaned === "") return true;
|
|
3015
|
+
const normalized = cleaned.toLowerCase().replace(/[\s()()「」"' \-*—–]+/g, "");
|
|
3016
|
+
return normalized === "" || normalized === "空行" || normalized === "无" || normalized === "没有" || normalized === "无内容" || normalized === "无值得记住的内容" || normalized === "暂无" || normalized === "none" || normalized === "no" || normalized === "nothing" || normalized === "na" || normalized === "n/a";
|
|
2572
3017
|
}
|
|
2573
3018
|
//#endregion
|
|
2574
|
-
export { Config,
|
|
3019
|
+
export { Config, MEMORY_DIR, MEMORY_FILE, MEMORY_SECTION, appendMemory, dedupeKey, AssistantProfile as default, isVacuousEntry, memoryPath, name, parseEntries, readMemoryText, renderPersona, stampNow, stripInvisible, writeMemory };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@istarry/dsh-assistant-profile",
|
|
3
3
|
"description": "助理档案:给 DSH 一份全局人格(名称 / 风格 / 语言 / 人格提示词),每个新会话的系统提示最前面都带着它;侧栏「新会话」上方显示助理名",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.1",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
|
@@ -52,6 +52,11 @@
|
|
|
52
52
|
"@deepseek-ai/dsh-agent": "^0.2.1-rc.2",
|
|
53
53
|
"@deepseek-ai/dsh-agent-preset-registry": "^0.2.1-rc.2",
|
|
54
54
|
"@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
|
|
55
|
-
"@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2"
|
|
55
|
+
"@deepseek-ai/dsh-system-prompt": "^0.2.1-rc.2",
|
|
56
|
+
"@deepseek-ai/dsh-typert-protocol": "*",
|
|
57
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
58
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
59
|
+
"@deepseek-ai/dsh-session": "*",
|
|
60
|
+
"@deepseek-ai/dsh-home-paths": "*"
|
|
56
61
|
}
|
|
57
62
|
}
|