@istuen/pt 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/dist/agent/api-bridge.d.ts +8 -0
- package/dist/agent/api-bridge.js +83 -0
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +3 -0
- package/dist/agent/pi-adapter.d.ts +35 -0
- package/dist/agent/pi-adapter.js +251 -0
- package/dist/agent/registry.d.ts +4 -0
- package/dist/agent/registry.js +40 -0
- package/dist/asset-health.d.ts +50 -0
- package/dist/asset-health.js +267 -0
- package/dist/asset-pack/loader.d.ts +47 -0
- package/dist/asset-pack/loader.js +121 -0
- package/dist/asset-pack/manifest.d.ts +23 -0
- package/dist/asset-pack/manifest.js +96 -0
- package/dist/asset-pack/md-file-pack.d.ts +41 -0
- package/dist/asset-pack/md-file-pack.js +153 -0
- package/dist/asset-pack/validate.d.ts +55 -0
- package/dist/asset-pack/validate.js +131 -0
- package/dist/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
- package/dist/builtin/assets/domains/agent-info.md +15 -0
- package/dist/builtin/assets/domains/authoring.md +201 -0
- package/dist/builtin/assets/domains/pack-repair.md +47 -0
- package/dist/builtin/assets/domains/project-analysis.md +52 -0
- package/dist/builtin/assets/domains/usage.md +81 -0
- package/dist/builtin/assets/domains/user-info.md +15 -0
- package/dist/builtin/assets/profiles/guide.profile.md +28 -0
- package/dist/commands.d.ts +69 -0
- package/dist/commands.js +344 -0
- package/dist/compile/agent-context.d.ts +48 -0
- package/dist/compile/agent-context.js +423 -0
- package/dist/compile/index.d.ts +1 -0
- package/dist/compile/index.js +5 -0
- package/dist/compile/resolve-use.d.ts +36 -0
- package/dist/compile/resolve-use.js +171 -0
- package/dist/compile/type-guards.d.ts +29 -0
- package/dist/compile/type-guards.js +107 -0
- package/dist/config.d.ts +31 -0
- package/dist/config.js +133 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.js +81 -0
- package/dist/diagnostics.d.ts +14 -0
- package/dist/diagnostics.js +29 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +838 -0
- package/dist/injection-status.d.ts +45 -0
- package/dist/injection-status.js +134 -0
- package/dist/log.d.ts +64 -0
- package/dist/log.js +139 -0
- package/dist/manual-session.d.ts +24 -0
- package/dist/manual-session.js +152 -0
- package/dist/manual-track.d.ts +35 -0
- package/dist/manual-track.js +107 -0
- package/dist/parse/blueprint.d.ts +5 -0
- package/dist/parse/blueprint.js +59 -0
- package/dist/parse/domain-renderers.d.ts +8 -0
- package/dist/parse/domain-renderers.js +115 -0
- package/dist/parse/domain.d.ts +7 -0
- package/dist/parse/domain.js +69 -0
- package/dist/parse/index.d.ts +9 -0
- package/dist/parse/index.js +223 -0
- package/dist/parse/profile.d.ts +18 -0
- package/dist/parse/profile.js +117 -0
- package/dist/parse/ref-resolver.d.ts +63 -0
- package/dist/parse/ref-resolver.js +174 -0
- package/dist/parse/shared.d.ts +76 -0
- package/dist/parse/shared.js +273 -0
- package/dist/profile-persist.d.ts +22 -0
- package/dist/profile-persist.js +53 -0
- package/dist/render/cache.d.ts +15 -0
- package/dist/render/cache.js +135 -0
- package/dist/render/index.d.ts +3 -0
- package/dist/render/index.js +14 -0
- package/dist/render/session-inject.d.ts +5 -0
- package/dist/render/session-inject.js +24 -0
- package/dist/render/turn-inject.d.ts +36 -0
- package/dist/render/turn-inject.js +248 -0
- package/dist/schema.d.ts +448 -0
- package/dist/schema.js +45 -0
- package/dist/session.d.ts +82 -0
- package/dist/session.js +102 -0
- package/dist/slog.d.ts +3 -0
- package/dist/slog.js +27 -0
- package/dist/transpile.d.ts +34 -0
- package/dist/transpile.js +220 -0
- package/dist/verify/file-hash.d.ts +2 -0
- package/dist/verify/file-hash.js +30 -0
- package/dist/verify/fs-content-match.d.ts +2 -0
- package/dist/verify/fs-content-match.js +26 -0
- package/dist/verify/fs-exists.d.ts +2 -0
- package/dist/verify/fs-exists.js +13 -0
- package/dist/verify/fs-not-exists.d.ts +2 -0
- package/dist/verify/fs-not-exists.js +13 -0
- package/dist/verify/git-status-clean.d.ts +2 -0
- package/dist/verify/git-status-clean.js +19 -0
- package/dist/verify/index.d.ts +7 -0
- package/dist/verify/index.js +46 -0
- package/dist/verify/lint-check.d.ts +2 -0
- package/dist/verify/lint-check.js +17 -0
- package/dist/verify/read-stderr.d.ts +5 -0
- package/dist/verify/read-stderr.js +20 -0
- package/dist/verify/ref-check.d.ts +13 -0
- package/dist/verify/ref-check.js +88 -0
- package/dist/verify/test-pass.d.ts +2 -0
- package/dist/verify/test-pass.js +17 -0
- package/dist/verify/ts-compiles.d.ts +2 -0
- package/dist/verify/ts-compiles.js +17 -0
- package/package.json +62 -0
- package/src/agent/api-bridge.ts +105 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/pi-adapter.ts +326 -0
- package/src/agent/registry.ts +44 -0
- package/src/asset-health.ts +327 -0
- package/src/asset-pack/loader.ts +141 -0
- package/src/asset-pack/manifest.ts +118 -0
- package/src/asset-pack/md-file-pack.ts +202 -0
- package/src/asset-pack/validate.ts +186 -0
- package/src/builtin/assets/blueprints/dev-knowledge.blueprint.yaml +13 -0
- package/src/builtin/assets/domains/agent-info.md +15 -0
- package/src/builtin/assets/domains/authoring.md +201 -0
- package/src/builtin/assets/domains/pack-repair.md +47 -0
- package/src/builtin/assets/domains/project-analysis.md +52 -0
- package/src/builtin/assets/domains/usage.md +81 -0
- package/src/builtin/assets/domains/user-info.md +15 -0
- package/src/builtin/assets/profiles/guide.profile.md +28 -0
- package/src/commands.ts +405 -0
- package/src/compile/agent-context.ts +487 -0
- package/src/compile/index.ts +5 -0
- package/src/compile/resolve-use.ts +208 -0
- package/src/compile/type-guards.ts +132 -0
- package/src/config.ts +154 -0
- package/src/constants.ts +104 -0
- package/src/diagnostics.ts +36 -0
- package/src/index.ts +1028 -0
- package/src/injection-status.ts +155 -0
- package/src/log.ts +173 -0
- package/src/manual-session.ts +190 -0
- package/src/manual-track.ts +127 -0
- package/src/parse/blueprint.ts +82 -0
- package/src/parse/domain-renderers.ts +120 -0
- package/src/parse/domain.ts +78 -0
- package/src/parse/index.ts +266 -0
- package/src/parse/profile.ts +139 -0
- package/src/parse/ref-resolver.ts +198 -0
- package/src/parse/shared.ts +331 -0
- package/src/profile-persist.ts +60 -0
- package/src/render/cache.ts +150 -0
- package/src/render/index.ts +14 -0
- package/src/render/session-inject.ts +26 -0
- package/src/render/turn-inject.ts +278 -0
- package/src/schema.ts +542 -0
- package/src/session.ts +190 -0
- package/src/slog.ts +32 -0
- package/src/transpile.ts +302 -0
- package/src/verify/file-hash.ts +29 -0
- package/src/verify/fs-content-match.ts +28 -0
- package/src/verify/fs-exists.ts +14 -0
- package/src/verify/fs-not-exists.ts +17 -0
- package/src/verify/git-status-clean.ts +23 -0
- package/src/verify/index.ts +57 -0
- package/src/verify/lint-check.ts +21 -0
- package/src/verify/read-stderr.ts +24 -0
- package/src/verify/ref-check.ts +118 -0
- package/src/verify/test-pass.ts +18 -0
- package/src/verify/ts-compiles.ts +21 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// src/profile-persist.ts — Session JSONL profile 持久化(P1.4 抽出)
|
|
2
|
+
//
|
|
3
|
+
// v10.x:用 pi.appendEntry() 把 activeProfile 写到 session JSONL。
|
|
4
|
+
// - 读:反向遍历 entries,取最后一个 pt:active-profile(最新覆盖前一次)。
|
|
5
|
+
// - 写:appendEntry,失败静默(ephemeral session / 旧版 pi 无此 API)。
|
|
6
|
+
//
|
|
7
|
+
// MinimalSessionManager 是 session JSONL 通用接口的最小子集(profile + manual 持久化共用)。
|
|
8
|
+
// P1 范围内放这里;未来 contract/ 迁移(P2+)时可考虑抽到 src/contract/。
|
|
9
|
+
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { errMsg } from "./diagnostics.js";
|
|
12
|
+
|
|
13
|
+
/** session JSONL 通用接口的最小子集(结构类型,不用 ReadonlySessionManager——不在 pi 包顶层 export)。 */
|
|
14
|
+
export interface MinimalSessionManager {
|
|
15
|
+
getEntries(): Array<{ type: string; customType?: string; data?: unknown }>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** session JSONL 中持久化 activeProfile 的 custom entry customType。
|
|
19
|
+
* 用 `pt:` 命名空间避免污染 pi 通用命名空间。 */
|
|
20
|
+
export const PT_PROFILE_ENTRY = "pt:active-profile";
|
|
21
|
+
|
|
22
|
+
/** 从 session JSONL 读上次保存的 profile。
|
|
23
|
+
* - 反向遍历 entries,取最后一个 `pt:active-profile`(最新一次切换覆盖前一次)。
|
|
24
|
+
* - 静默 fallback:SessionManager 不可用 / ephemeral session / entry 损坏 → 返 undefined。
|
|
25
|
+
* - 不校验 profile 是否仍存在于 assets——校验留给 transpileActive(transpile 失败会被 session_start catch)。
|
|
26
|
+
* - 用结构类型而非 `ReadonlySessionManager`(该类型不在 pi 包顶层 export.d.ts 里)。 */
|
|
27
|
+
export function readProfileFromSession(sessionManager: MinimalSessionManager): string | undefined {
|
|
28
|
+
try {
|
|
29
|
+
const entries = sessionManager.getEntries();
|
|
30
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
31
|
+
const e = entries[i];
|
|
32
|
+
if (e && e.type === "custom" && e.customType === PT_PROFILE_ENTRY) {
|
|
33
|
+
const data = (e as { data?: unknown }).data;
|
|
34
|
+
if (data && typeof data === "object") {
|
|
35
|
+
const profile = (data as { profile?: unknown }).profile;
|
|
36
|
+
if (typeof profile === "string" && profile.trim()) {
|
|
37
|
+
return profile.trim();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
// SessionManager 异常(如不存在 / 旧版 pi)→ 静默
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 把当前 activeProfile 写入 session JSONL(Pi 自带持久化)。
|
|
49
|
+
* - 用 `pi.appendEntry()`(dist/core/extensions/types.d.ts:78 官方 API)。
|
|
50
|
+
* - 失败静默(ephemeral session / 旧版 pi 无此 API)——内存中 activeProfile 仍可用本进程。 */
|
|
51
|
+
export function persistProfileToSession(pi: ExtensionAPI, name: string): void {
|
|
52
|
+
try {
|
|
53
|
+
if (typeof pi.appendEntry !== "function") return;
|
|
54
|
+
pi.appendEntry(PT_PROFILE_ENTRY, { profile: name });
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// P1.4 边界:slog 是 index.ts 本地函数,profile-persist 不反向依赖 index。
|
|
57
|
+
// 直接 console.warn 兜底(与 slog 行为一致)。未来抽 log 工具时统一接入。
|
|
58
|
+
console.warn(`[pt] persistProfileToSession failed:`, { profileName: name, err: errMsg(e) });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// src/render/cache.ts — AgentContext 文件读写 + hash 校验
|
|
2
|
+
//
|
|
3
|
+
// v9 失效策略:sourceHash = hash(Profile + Blueprint + Domains) 组合。
|
|
4
|
+
// 三者任一变化即失效重编译。
|
|
5
|
+
//
|
|
6
|
+
// v15.x PR2(§8.1):sourceHash 加 packs 维度——pack 内容变化触发失效。
|
|
7
|
+
// v15.x PR2(§8.3):cache 文件名 <pack>__<profile>.agent-context.md(sanitize 特殊字符)。
|
|
8
|
+
// v15.x PR2(§9.4.1):旧缓存清理——loadAgentContext 检测旧名 → 删除 → 触发重编译。
|
|
9
|
+
//
|
|
10
|
+
// Phase term-P1:Context IR → AgentContext 改名同步——
|
|
11
|
+
// - 文件后缀 .context.md → .agent-context.md
|
|
12
|
+
// - saveContext → saveAgentContext / loadContext → loadAgentContext
|
|
13
|
+
// - serializeContext → serializeAgentContext / deserializeContext → deserializeAgentContext
|
|
14
|
+
//
|
|
15
|
+
// Phase term-P4.2:cacheDir 改用 constants.CACHE_DIR 常量(替代 Blueprint.compilation.cacheDir)。
|
|
16
|
+
// split 硬编码 single-file(唯一选项,by-injection-point 已 YAGNI 移除)。
|
|
17
|
+
// 签名删 compilation 参数。
|
|
18
|
+
|
|
19
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { CACHE_DIR } from "../constants.js";
|
|
22
|
+
import type { AgentContext } from "../schema.js";
|
|
23
|
+
|
|
24
|
+
/** v15.x PR2(§8.3):cache 文件名 <pack>__<profile>.agent-context.md,
|
|
25
|
+
* sanitize 非法字符为 _(pack 名已是 kebab-case 通常不需要;profile 名可能含特殊字符)。 */
|
|
26
|
+
function cacheFileName(packName: string, profileName: string): string {
|
|
27
|
+
const sanitize = (s: string): string => s.replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
28
|
+
return `${sanitize(packName)}__${sanitize(profileName)}.agent-context.md`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 把 AgentContext IR 序列化并写入 <CACHE_DIR>/<pack>__<name>.agent-context.md。
|
|
32
|
+
* 文件头:source-hash: <hash>(缓存失效依据)+ pack: <packName>。 */
|
|
33
|
+
export async function saveAgentContext(cwd: string, ctx: AgentContext): Promise<string> {
|
|
34
|
+
const dir = join(cwd, CACHE_DIR);
|
|
35
|
+
await mkdir(dir, { recursive: true });
|
|
36
|
+
|
|
37
|
+
// v15.x PR2(§8.3):cache 文件名 <pack>__<profile>.agent-context.md
|
|
38
|
+
const file = join(dir, cacheFileName(ctx.packName, ctx.name));
|
|
39
|
+
const body = serializeAgentContext(ctx);
|
|
40
|
+
await writeFile(file, body, "utf8");
|
|
41
|
+
return file;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 读 <CACHE_DIR>/<pack>__<name>.agent-context.md 并校验 sourceHash。
|
|
45
|
+
* - 文件不存在 → 触发 §9.4.1 旧缓存清理:检测旧名 <name>.agent-context.md → 删除 → 返 null(重编译)
|
|
46
|
+
* - 文件存在但 hash 不一致 → 返 null(需重编译覆盖)
|
|
47
|
+
* - 命中 → 返 AgentContext IR
|
|
48
|
+
* v15.x PR2(§8.3):签名加 packName 参数;旧名 <profile>.agent-context.md 自动清理迁移。 */
|
|
49
|
+
export async function loadAgentContext(
|
|
50
|
+
cwd: string,
|
|
51
|
+
packName: string,
|
|
52
|
+
name: string,
|
|
53
|
+
expectedHash: string
|
|
54
|
+
): Promise<AgentContext | null> {
|
|
55
|
+
const file = join(cwd, CACHE_DIR, cacheFileName(packName, name));
|
|
56
|
+
let raw: string;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readFile(file, "utf8");
|
|
59
|
+
} catch {
|
|
60
|
+
// §9.4.1:旧缓存清理——新名不存在时尝试旧名
|
|
61
|
+
return await tryLoadOldCacheAndCleanup(cwd, name, expectedHash);
|
|
62
|
+
}
|
|
63
|
+
const ctx = deserializeAgentContext(name, raw);
|
|
64
|
+
if (!ctx) return null;
|
|
65
|
+
if (ctx.sourceHash !== expectedHash) return null; // 失效,需重编译
|
|
66
|
+
return ctx;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** v15.x PR2(§9.4.1):旧缓存清理——升级后旧名 <profile>.agent-context.md 检测 + 删除。
|
|
70
|
+
* 旧文件存在时删除(无论 hash 是否匹配),返 null 触发 saveAgentContext 以新名重写。
|
|
71
|
+
* 未传 packName 也能工作(旧名无 pack 概念,默认归 "prj"——但会被清理掉,不会真用到)。 */
|
|
72
|
+
async function tryLoadOldCacheAndCleanup(
|
|
73
|
+
cwd: string,
|
|
74
|
+
name: string,
|
|
75
|
+
_expectedHash: string
|
|
76
|
+
): Promise<AgentContext | null> {
|
|
77
|
+
const oldFile = join(cwd, CACHE_DIR, `${name}.agent-context.md`);
|
|
78
|
+
try {
|
|
79
|
+
await readFile(oldFile, "utf8");
|
|
80
|
+
} catch {
|
|
81
|
+
return null; // 旧名也不存在——首次加载
|
|
82
|
+
}
|
|
83
|
+
// 旧文件存在——无论 hash 是否匹配都删除(新名格式不同,重编译)
|
|
84
|
+
await rm(oldFile, { force: true });
|
|
85
|
+
return null; // 返 null 触发 saveAgentContext 以新名重写
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ==================== 序列化/反序列化 ====================
|
|
89
|
+
|
|
90
|
+
/** AgentContext IR → 文件内容(含 sourceHash 头 + pack 字段)。 */
|
|
91
|
+
function serializeAgentContext(ctx: AgentContext): string {
|
|
92
|
+
const lines: string[] = [];
|
|
93
|
+
lines.push("---");
|
|
94
|
+
lines.push(`source-hash: ${ctx.sourceHash}`);
|
|
95
|
+
lines.push(`pack: ${ctx.packName}`); // v15.x PR2 新增
|
|
96
|
+
lines.push(`profile: ${ctx.name}`);
|
|
97
|
+
lines.push(`blueprint: ${ctx.blueprint}`);
|
|
98
|
+
lines.push("---");
|
|
99
|
+
lines.push("");
|
|
100
|
+
for (const [h2Name, content] of Object.entries(ctx.modules)) {
|
|
101
|
+
lines.push(`## ${h2Name}`);
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(content);
|
|
104
|
+
lines.push("");
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n").trimEnd();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 文件内容 → AgentContext IR。返 null 表示格式损坏。 */
|
|
110
|
+
function deserializeAgentContext(name: string, raw: string): AgentContext | null {
|
|
111
|
+
const fmMatch = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
|
112
|
+
if (!fmMatch) return null;
|
|
113
|
+
const fm: Record<string, string> = {};
|
|
114
|
+
for (const line of fmMatch[1].split(/\r?\n/)) {
|
|
115
|
+
const kv = line.match(/^([a-zA-Z_-]+)\s*:\s*(.+)$/);
|
|
116
|
+
if (kv) fm[kv[1]] = kv[2].trim();
|
|
117
|
+
}
|
|
118
|
+
const hash = fm["source-hash"];
|
|
119
|
+
const fmName = fm.profile ?? fm.name ?? name;
|
|
120
|
+
const fmBlueprint = fm.blueprint ?? "";
|
|
121
|
+
// v15.x PR2(§9.4.1):旧 cache 无 pack 字段时兜底 "prj"——但 §9.4.1 tryLoadOldCacheAndCleanup
|
|
122
|
+
// 会先删除旧文件,不会真用到兜底。这里兜底仅为 deserialize 容错。
|
|
123
|
+
const fmPack = fm.pack ?? "prj";
|
|
124
|
+
if (!hash) return null;
|
|
125
|
+
|
|
126
|
+
const body = fmMatch[2];
|
|
127
|
+
const modules: Record<string, string> = {};
|
|
128
|
+
// 按 ## H2 切分(保留段内换行)
|
|
129
|
+
const sectionRe = /^## (.+)$/gm;
|
|
130
|
+
const matches: Array<{ name: string; start: number; end: number }> = [];
|
|
131
|
+
let m: RegExpExecArray | null;
|
|
132
|
+
// biome-ignore lint/suspicious/noAssignInExpressions: standard regex exec loop pattern
|
|
133
|
+
while ((m = sectionRe.exec(body)) !== null) {
|
|
134
|
+
matches.push({ name: m[1].trim(), start: m.index + m[0].length + 1, end: body.length });
|
|
135
|
+
}
|
|
136
|
+
// 排序每个段的结束位置
|
|
137
|
+
for (let i = 0; i < matches.length; i++) {
|
|
138
|
+
const cur = matches[i];
|
|
139
|
+
const next = matches[i + 1];
|
|
140
|
+
cur.end = next ? next.start - `## ${next.name}`.length - 2 : body.length;
|
|
141
|
+
}
|
|
142
|
+
for (const sec of matches) {
|
|
143
|
+
modules[sec.name] = body.slice(sec.start, sec.end).trim();
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { name: fmName, blueprint: fmBlueprint, sourceHash: hash, modules, packName: fmPack };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** v15.x PR2(§8.3):导出 cacheFileName 给测试用。 */
|
|
150
|
+
export { cacheFileName };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/render/index.ts — Render 后端入口
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.5:v9 后端入口。
|
|
4
|
+
// - session-inject.ts : AgentContext 聚合组 → Session Inject 字符串(按 Blueprint inject=session 聚合)
|
|
5
|
+
// - turn-inject.ts : FlowTemplate + 参数 → Turn Inject(/manual:xxx + /<flow-name>)
|
|
6
|
+
// AgentAdapter 内部映射到 Pi 的 system_prompt/context_message
|
|
7
|
+
// - cache.ts : AgentContext 文件读写 + hash 校验(用 constants.CACHE_DIR 常量,P4.2)
|
|
8
|
+
//
|
|
9
|
+
// Phase term-P1:Context IR → AgentContext 改名同步——
|
|
10
|
+
// - saveContext/loadContext → saveAgentContext/loadAgentContext
|
|
11
|
+
// Phase term-P4.3:render 函数名 + 文件名对齐 session/turn。
|
|
12
|
+
export { renderSessionInject } from "./session-inject.js";
|
|
13
|
+
export { bindFlowTemplate, findFlowInBlueprint, renderTurnInject } from "./turn-inject.js";
|
|
14
|
+
export { saveAgentContext, loadAgentContext } from "./cache.js";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// src/render/session-inject.ts — AgentContext 聚合组 → Session Inject 字符串
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.5:v9 后端通用化——遍历 Blueprint.groups(不再 Channel.groups),
|
|
4
|
+
// 聚合所有 inject=session 的聚合组内容。
|
|
5
|
+
//
|
|
6
|
+
// renderSessionInject(ctx, blueprint) → string
|
|
7
|
+
// 设计上保留函数包装,便于将来在渲染阶段做最终修整(如日志、注入标记)。
|
|
8
|
+
//
|
|
9
|
+
// Phase term-P1:参数类型 Context → AgentContext(IR 改名同步)。
|
|
10
|
+
// Phase term-P4.3:函数名 renderSystemPrompt → renderSessionInject;inject 语义值 system_prompt → session。
|
|
11
|
+
|
|
12
|
+
import type { AgentContext, Blueprint } from "../schema.js";
|
|
13
|
+
|
|
14
|
+
/** AgentContext 聚合组聚合 → Session Inject 字符串。
|
|
15
|
+
* v9:遍历 Blueprint.groups,聚合所有 inject=session 的聚合组内容。
|
|
16
|
+
* AgentAdapter 内部把 Session Inject 注入到 Agent 的 session 级(Pi: system_prompt 事件)。 */
|
|
17
|
+
export function renderSessionInject(ctx: AgentContext, blueprint: Blueprint): string {
|
|
18
|
+
const parts: string[] = [];
|
|
19
|
+
for (const group of blueprint.groups) {
|
|
20
|
+
if (group.inject === "session") {
|
|
21
|
+
const content = ctx.modules[group.name];
|
|
22
|
+
if (content) parts.push(content);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return parts.join("\n\n");
|
|
26
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
// src/render/turn-inject.ts — FlowTemplate + 参数 → Turn Inject
|
|
2
|
+
//
|
|
3
|
+
// Phase 9.5:v9 后端通用化。
|
|
4
|
+
// - renderTurnInject(ctx, blueprint, domains, args) 修复死代码——实现 /manual:xxx 触发
|
|
5
|
+
// - findFlowInBlueprint(blueprint, domains, tplName) — 替代 v8 findFlowInBundle
|
|
6
|
+
//
|
|
7
|
+
// /manual:<domain-name> 触发:从 Blueprint 的 turn 聚合组引用的 Domain 里查 Manual 段
|
|
8
|
+
// /<flow-name> <args> 触发:展开 Domain 的 FlowTemplate(v8 逻辑保留)
|
|
9
|
+
//
|
|
10
|
+
// Phase term-P4.3:renderContextMessage → renderTurnInject;inject 语义值 context_message → turn(Agent-agnostic 语义值)。
|
|
11
|
+
// bindFlowTemplate / findFlowInBlueprint 函数名不改——它们是 FlowTemplate 操作,非注入位置概念。
|
|
12
|
+
//
|
|
13
|
+
// Tech Debt T6: 全用 type guard 收窄,不用 as 断言(pt-quality #1)
|
|
14
|
+
// Tech Debt T2: 用 constants 模块名常量(pt-quality #5)
|
|
15
|
+
|
|
16
|
+
import { MOD_CHECKLISTS, MOD_FLOWS, MOD_RULES } from "../constants.js";
|
|
17
|
+
import { isChecklistArray, isFlowTemplateArray, isRuleArray } from "../compile/type-guards.js";
|
|
18
|
+
import type {
|
|
19
|
+
AgentContext,
|
|
20
|
+
Blueprint,
|
|
21
|
+
Domain,
|
|
22
|
+
FlowStep,
|
|
23
|
+
FlowTemplate,
|
|
24
|
+
ModName,
|
|
25
|
+
Profile,
|
|
26
|
+
} from "../schema.js";
|
|
27
|
+
|
|
28
|
+
/** FlowTemplate + 元数据(adapter 附加的 _vars)。_vars 优先于 argument-hint fallback。 */
|
|
29
|
+
export type BoundableTemplate = FlowTemplate & { _vars?: string[] };
|
|
30
|
+
|
|
31
|
+
interface VarSpec {
|
|
32
|
+
name: string;
|
|
33
|
+
default?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* 给定 AgentContext + Blueprint + Domains + args(形如 "/manual:pt-quality" 或 "/risk-check 客户A 5000"),
|
|
38
|
+
* 展开目标 Domain 的 Manual 段内容或 FlowTemplate。
|
|
39
|
+
*
|
|
40
|
+
* v9 触发:
|
|
41
|
+
* - /manual:<domain-name>:注入该 Domain 的 Manual 段内容(term→Rule checklist / workflow→FlowTemplate 列表)
|
|
42
|
+
* - /<flow-name> <args>:展开 workflow-Domain 的 FlowTemplate(v8 逻辑保留)
|
|
43
|
+
*
|
|
44
|
+
* Phase term-P4.3:AgentAdapter 内部把 Turn Inject 注入到 Agent 的 turn 级(Pi: input 事件 transform)。
|
|
45
|
+
*
|
|
46
|
+
* v13.x(issue pt-turn-inject-not-profile-scoped):加 profile 参数——
|
|
47
|
+
* - domain 维度:调用方(adapter)应已用 filterDomainsByProfile 过滤 domains,renderTurnInject
|
|
48
|
+
* 不再二次过滤(信任传入的 domains scope)
|
|
49
|
+
* - modules 维度(阶段 2):从 blueprint 的 inject=turn 聚合组 + profile 同名 ProfileGroup.modules 算
|
|
50
|
+
* 白名单,/manual:<domain> 只渲染白名单内的段,/<flow-name> 只在白名单含 Flows 段时查找。
|
|
51
|
+
* profile 为 null 或无 turn 聚合组 → 白名单 null = 不限制(向后兼容)。
|
|
52
|
+
*/
|
|
53
|
+
export function renderTurnInject(
|
|
54
|
+
_ctx: AgentContext,
|
|
55
|
+
blueprint: Blueprint,
|
|
56
|
+
domains: Domain[],
|
|
57
|
+
profile: Profile | null,
|
|
58
|
+
args: string
|
|
59
|
+
): string | null {
|
|
60
|
+
const m = args.trim().match(/^\/(\S+)\s*(.*)$/);
|
|
61
|
+
if (!m) return null;
|
|
62
|
+
const [, name, rest] = m;
|
|
63
|
+
|
|
64
|
+
// 阶段 2:算 turn 聚合组的 modules 白名单(Profile "参考手册"聚合组的 ### Modules)
|
|
65
|
+
const mods = turnGroupModules(blueprint, profile);
|
|
66
|
+
|
|
67
|
+
// /manual:<domain-name> 触发(v9 新增)—— name 可能是 "manual:pt-quality"
|
|
68
|
+
// Phase term-P9.2:Domain 不再有单一 Manual 段,而是 Rules/Flows/Checklists 三选一。
|
|
69
|
+
// 顺序遍历找第一个非空段,返回对应渲染。
|
|
70
|
+
// v13.x:受 mods 白名单限制——只渲染白名单内的段。
|
|
71
|
+
if (name.startsWith("manual:")) {
|
|
72
|
+
const domainName = name.slice("manual:".length).trim();
|
|
73
|
+
const d = domains.find((x) => x.name === domainName);
|
|
74
|
+
if (!d) return null;
|
|
75
|
+
return renderDomainManual(d, mods);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// /manual <domain-name> 触发——空格分隔形式
|
|
79
|
+
if (name === "manual") {
|
|
80
|
+
const domainName = rest.trim();
|
|
81
|
+
const d = domains.find((x) => x.name === domainName);
|
|
82
|
+
if (!d) return null;
|
|
83
|
+
return renderDomainManual(d, mods);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// /<flow-name> <args> 触发(v8 逻辑保留)
|
|
87
|
+
// v13.x:findFlowInBlueprint 内部按 mods 白名单判断是否允许查找 Flows 段。
|
|
88
|
+
const tpl = findFlowInBlueprint(blueprint, domains, name, profile);
|
|
89
|
+
if (!tpl) return null;
|
|
90
|
+
return bindFlowTemplate(tpl, rest);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ==================== turn 聚合组 modules 白名单辅助(v13.x issue pt-turn-inject-not-profile-scoped) ====================
|
|
94
|
+
|
|
95
|
+
/** 找 turn 聚合组的 modules 白名单(Profile 同名 ProfileGroup 的 ### Modules)。
|
|
96
|
+
* 返 null = 不限制(profile 为 null / 无 turn 聚合组 / ProfileGroup 未填 modules——向后兼容)。
|
|
97
|
+
* 这是阶段 2 的核心:让 Profile "参考手册"### Modules 的段选择真正影响 turn 触发渲染范围。 */
|
|
98
|
+
function turnGroupModules(blueprint: Blueprint, profile: Profile | null): ModName[] | null {
|
|
99
|
+
if (!profile) return null;
|
|
100
|
+
const turnGroupName = blueprint.groups.find((g) => g.inject === "turn")?.name;
|
|
101
|
+
if (!turnGroupName) return null;
|
|
102
|
+
const pg = profile.groups.find((g) => g.name === turnGroupName);
|
|
103
|
+
if (!pg || pg.modules.length === 0) return null;
|
|
104
|
+
return pg.modules;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** 检查 mods 白名单是否包含某段名。null = 不限制(含)。 */
|
|
108
|
+
function modsAllowsSection(mods: ModName[] | null, section: string): boolean {
|
|
109
|
+
if (!mods) return true;
|
|
110
|
+
return mods.some((m) => m.section === section);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 渲染 Domain 的"手册"段内容(Phase term-P9.2:从 Manual 拆三段)。
|
|
115
|
+
* - Rules → Rule checklist("## 规范清单")
|
|
116
|
+
* - Flows → FlowTemplate 列表("## 可用手册")
|
|
117
|
+
* - Checklists → Checklist 列表("## 验收清单")
|
|
118
|
+
* 优先级:Flows > Rules > Checklists(workhorse 最常被查)
|
|
119
|
+
* v13.x:受 mods 白名单限制——只渲染 modsAllowsSection 通过的段。
|
|
120
|
+
* 返回 null 表示该 Domain 没有任何手册段(或所有段都被白名单排除)。 */
|
|
121
|
+
function renderDomainManual(d: Domain, mods: ModName[] | null): string | null {
|
|
122
|
+
if (modsAllowsSection(mods, MOD_FLOWS)) {
|
|
123
|
+
const flows = d.modules[MOD_FLOWS];
|
|
124
|
+
if (isFlowTemplateArray(flows)) {
|
|
125
|
+
const lines: string[] = [];
|
|
126
|
+
for (const t of flows) {
|
|
127
|
+
const hint = t.argumentHint ? ` ${t.argumentHint}` : "";
|
|
128
|
+
lines.push(`- ${t.name}${hint}: ${t.intent}`);
|
|
129
|
+
}
|
|
130
|
+
if (lines.length > 0) {
|
|
131
|
+
return `# /manual:${d.name}\n\n## 可用手册\n\n${lines.join("\n")}`.trimEnd();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (modsAllowsSection(mods, MOD_RULES)) {
|
|
137
|
+
const rules = d.modules[MOD_RULES];
|
|
138
|
+
if (isRuleArray(rules)) {
|
|
139
|
+
const lines: string[] = [];
|
|
140
|
+
for (const r of rules) {
|
|
141
|
+
if (r.type === "invariant") {
|
|
142
|
+
if (r.check) lines.push(`- ${r.name}: ${r.check}`);
|
|
143
|
+
else lines.push(`- ${r.name}`);
|
|
144
|
+
} else if (r.type === "ban" && r.items && r.items.length > 0) {
|
|
145
|
+
if (r.check) lines.push(`- ${r.name}: ${r.check} (${r.items.join(" / ")})`);
|
|
146
|
+
else lines.push(`- ${r.name}: ${r.items.join(" / ")}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
if (lines.length > 0) {
|
|
150
|
+
return `# /manual:${d.name}\n\n## 规范清单\n\n${lines.join("\n")}`.trimEnd();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (modsAllowsSection(mods, MOD_CHECKLISTS)) {
|
|
156
|
+
const checklists = d.modules[MOD_CHECKLISTS];
|
|
157
|
+
if (isChecklistArray(checklists)) {
|
|
158
|
+
const sections: string[] = [];
|
|
159
|
+
for (const cl of checklists) {
|
|
160
|
+
sections.push(`### ${cl.name}\n${cl.items.map((i) => `- ${i}`).join("\n")}`);
|
|
161
|
+
}
|
|
162
|
+
if (sections.length > 0) {
|
|
163
|
+
return `# /manual:${d.name}\n\n## 验收清单\n\n${sections.join("\n\n")}`.trimEnd();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 把 FlowTemplate 模板 + 用户参数 展开为完整手册 markdown。
|
|
173
|
+
* 错误 / 缺失变量 → 留为字面量(不抛异常)。
|
|
174
|
+
*/
|
|
175
|
+
export function bindFlowTemplate(tpl: BoundableTemplate, args: string): string {
|
|
176
|
+
const tokens = args.trim() ? args.trim().split(/\s+/) : [];
|
|
177
|
+
const varSpecs = parseVarSpecs(tpl._vars, tpl.argumentHint);
|
|
178
|
+
|
|
179
|
+
const bound = new Map<string, string>();
|
|
180
|
+
for (let i = 0; i < varSpecs.length; i++) {
|
|
181
|
+
const spec = varSpecs[i];
|
|
182
|
+
const token = tokens[i];
|
|
183
|
+
if (token !== undefined) {
|
|
184
|
+
bound.set(spec.name, token);
|
|
185
|
+
} else if (spec.default !== undefined) {
|
|
186
|
+
bound.set(spec.name, spec.default);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const lines: string[] = [];
|
|
191
|
+
lines.push(`# ${tpl.name}`);
|
|
192
|
+
lines.push("");
|
|
193
|
+
if (tpl.argumentHint) {
|
|
194
|
+
lines.push(`_参数:${tpl.argumentHint}_`);
|
|
195
|
+
lines.push("");
|
|
196
|
+
}
|
|
197
|
+
lines.push(`## 前提(Intent)`);
|
|
198
|
+
lines.push(replaceVars(tpl.intent, bound));
|
|
199
|
+
lines.push("");
|
|
200
|
+
lines.push(`## 步骤`);
|
|
201
|
+
tpl.steps.forEach((s: FlowStep, i: number) => {
|
|
202
|
+
lines.push(`${i + 1}. ${replaceVars(s.desc, bound)}`);
|
|
203
|
+
if (s.observe && s.observe.length > 0) {
|
|
204
|
+
lines.push(` - 验证参照:${s.observe.join(", ")}`);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return lines.join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// ==================== 内部辅助 ====================
|
|
212
|
+
|
|
213
|
+
function parseVarSpecs(_vars: string[] | undefined, hint: string | undefined): VarSpec[] {
|
|
214
|
+
if (_vars && _vars.length > 0) {
|
|
215
|
+
return _vars.map(parseVarSpecName);
|
|
216
|
+
}
|
|
217
|
+
if (hint) {
|
|
218
|
+
const matches = [...hint.matchAll(/<([^>]+)>/g)];
|
|
219
|
+
return matches.map((m) => parseVarSpecName(m[1]));
|
|
220
|
+
}
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function parseVarSpecName(raw: string): VarSpec {
|
|
225
|
+
const trimmed = raw.trim();
|
|
226
|
+
const idx = trimmed.indexOf("|");
|
|
227
|
+
if (idx < 0) return { name: trimmed };
|
|
228
|
+
const name = trimmed.slice(0, idx).trim();
|
|
229
|
+
const defaultPart = trimmed.slice(idx + 1).trim();
|
|
230
|
+
const defaultMatch = defaultPart.match(/^default\s*:\s*(.*)$/);
|
|
231
|
+
if (defaultMatch) return { name, default: defaultMatch[1].trim() };
|
|
232
|
+
return { name, default: defaultPart };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function replaceVars(text: string, bound: Map<string, string>): string {
|
|
236
|
+
return text.replace(/\{\{([^}]+)\}\}/g, (match, inner: string) => {
|
|
237
|
+
const spec = parseVarSpecName(inner);
|
|
238
|
+
const v = bound.get(spec.name);
|
|
239
|
+
if (v !== undefined) return v;
|
|
240
|
+
if (spec.default !== undefined) return spec.default;
|
|
241
|
+
return match;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** 在 Blueprint 聚合组(inject=turn)的引用域中按名查找 FlowTemplate(跨 Domain)。
|
|
246
|
+
* v13.x(issue pt-turn-inject-not-profile-scoped):加 profile 参数——内部算 mods 白名单,
|
|
247
|
+
* 白名单不含 Flows 段时返 undefined(Profile "参考手册"### Modules 未声明 Flows 则 /<flow-name> 触发失效)。
|
|
248
|
+
* Phase term-P4.3 + term-final:inject 语义值 turn(原 context_message → turn → 现聚合组 inject=turn)。 */
|
|
249
|
+
export function findFlowInBlueprint(
|
|
250
|
+
blueprint: Blueprint,
|
|
251
|
+
// Phase term-P9.3:type 字段删除——Domain 不再有 type 维度,按 H2 段名(这里是 ## Flows)识别 FlowTemplate
|
|
252
|
+
domains: Array<{ name: string; modules: Record<string, unknown> }>,
|
|
253
|
+
tplName: string,
|
|
254
|
+
profile: Profile | null = null
|
|
255
|
+
): BoundableTemplate | undefined {
|
|
256
|
+
// 验证 Blueprint 里有 inject=turn 的聚合组(间接确认 input 事件该由本实例覆盖接管)
|
|
257
|
+
const hasTurnGroup = blueprint.groups.some((g) => g.inject === "turn");
|
|
258
|
+
if (!hasTurnGroup) return undefined;
|
|
259
|
+
|
|
260
|
+
// v13.x:mods 白名单不含 Flows 段则不查找——配置即行为
|
|
261
|
+
const mods = turnGroupModules(blueprint, profile);
|
|
262
|
+
if (!modsAllowsSection(mods, MOD_FLOWS)) return undefined;
|
|
263
|
+
|
|
264
|
+
for (const d of domains) {
|
|
265
|
+
// Phase term-P9.2:FlowTemplate 在 ## Flows 段(不分 type)。
|
|
266
|
+
const flows = d.modules[MOD_FLOWS];
|
|
267
|
+
if (!isFlowTemplateArray(flows)) continue;
|
|
268
|
+
const hit = flows.find((t) => t.name === tplName);
|
|
269
|
+
if (hit) {
|
|
270
|
+
const bt: BoundableTemplate = { ...hit };
|
|
271
|
+
// P2.1:v9 BoundableTemplate = FlowTemplate & { _vars? },vars 字段 spread 不会产生
|
|
272
|
+
// 原双重 cast (bt as unknown as { vars?: unknown }).vars 永远 undefined(dead code)。
|
|
273
|
+
// 若未来需从 args 传 vars 进来,应在调用方 bindFlowTemplate 处理,不在此处 cast 补救。
|
|
274
|
+
return bt;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|