hyacinth-ai 0.9.14 → 0.9.16
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/dist/cli/doctor.js +6 -16
- package/dist/gateway/cli.js +2 -9
- package/dist/gateway/factory.d.ts +1 -4
- package/dist/gateway/factory.js +4 -10
- package/dist/gateway/tui.d.ts +1 -2
- package/dist/gateway/tui.js +2 -22
- package/dist/machine/index.d.ts +0 -1
- package/dist/machine/index.js +0 -1
- package/dist/machine/runner.js +19 -20
- package/dist/orchestrator/loop.d.ts +0 -2
- package/dist/orchestrator/loop.js +0 -4
- package/dist/setup/persona-bootstrap.d.ts +4 -10
- package/dist/setup/persona-bootstrap.js +16 -126
- package/dist/setup/wizard.js +1 -1
- package/package.json +1 -1
package/dist/cli/doctor.js
CHANGED
|
@@ -54,7 +54,7 @@ function checkEncoding() {
|
|
|
54
54
|
}
|
|
55
55
|
function checkPersona() {
|
|
56
56
|
const personaDir = path.join(os.homedir(), '.agent', 'prompts', 'persona');
|
|
57
|
-
const files = ['SOUL.md', 'IDENTITY.md', 'USER.md'
|
|
57
|
+
const files = ['SOUL.md', 'IDENTITY.md', 'USER.md'];
|
|
58
58
|
const existing = [];
|
|
59
59
|
const missing = [];
|
|
60
60
|
for (const f of files) {
|
|
@@ -64,20 +64,15 @@ function checkPersona() {
|
|
|
64
64
|
else
|
|
65
65
|
missing.push(f);
|
|
66
66
|
}
|
|
67
|
-
const hasBootstrap = existing.includes('BOOTSTRAP.md');
|
|
68
67
|
return {
|
|
69
68
|
label: 'Persona 文件',
|
|
70
|
-
ok: existing.length >= 3
|
|
71
|
-
detail:
|
|
72
|
-
?
|
|
73
|
-
:
|
|
74
|
-
? `缺失: ${missing.join(', ')}`
|
|
75
|
-
: `完整 (${existing.length}/4 文件,bootstrap 已完成)`,
|
|
69
|
+
ok: existing.length >= 3,
|
|
70
|
+
detail: missing.length > 0
|
|
71
|
+
? `缺失: ${missing.join(', ')}`
|
|
72
|
+
: `完整 (${existing.length}/3 文件)`,
|
|
76
73
|
fix: missing.length > 0
|
|
77
74
|
? '运行 hyacinth setup 完成初始化'
|
|
78
|
-
:
|
|
79
|
-
? '启动 TUI 并完成身份对话,或运行 hyacinth setup'
|
|
80
|
-
: undefined,
|
|
75
|
+
: undefined,
|
|
81
76
|
};
|
|
82
77
|
}
|
|
83
78
|
function checkDeps() {
|
|
@@ -267,7 +262,6 @@ export async function runDoctor(opts = {}) {
|
|
|
267
262
|
console.log('\n── 自动修复 ──');
|
|
268
263
|
// 修复 persona 文件
|
|
269
264
|
const personaDir = path.join(os.homedir(), '.agent', 'prompts', 'persona');
|
|
270
|
-
const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
|
|
271
265
|
if (!fs.existsSync(personaDir)) {
|
|
272
266
|
fs.mkdirSync(personaDir, { recursive: true });
|
|
273
267
|
}
|
|
@@ -279,10 +273,6 @@ export async function runDoctor(opts = {}) {
|
|
|
279
273
|
personaCreated++;
|
|
280
274
|
}
|
|
281
275
|
}
|
|
282
|
-
// 确保 BOOTSTRAP.md 存在(触发 bootstrap 流程)
|
|
283
|
-
if (!fs.existsSync(bootstrapPath) && personaCreated > 0) {
|
|
284
|
-
fs.writeFileSync(bootstrapPath, '# Bootstrap\n请与用户对话,了解其偏好后填写 IDENTITY.md 和 USER.md。', 'utf-8');
|
|
285
|
-
}
|
|
286
276
|
if (personaCreated > 0) {
|
|
287
277
|
console.log(` ✅ 创建了 ${personaCreated} 个 Persona 文件`);
|
|
288
278
|
}
|
package/dist/gateway/cli.js
CHANGED
|
@@ -20,7 +20,7 @@ import { ConfigManager, API_KEY_MAP, DEFAULT_MAX_CONTEXT_TOKENS } from '../setup
|
|
|
20
20
|
import { getModelContextWindow } from '../setup/model-defaults.js';
|
|
21
21
|
import { SetupWizard } from '../setup/wizard.js';
|
|
22
22
|
import { PROVIDER_MODELS } from '../setup/model-defaults.js';
|
|
23
|
-
import { DEFAULT_PERSONA_DIR, ensurePersonaFiles
|
|
23
|
+
import { DEFAULT_PERSONA_DIR, ensurePersonaFiles } from '../setup/persona-bootstrap.js';
|
|
24
24
|
import { createLogger } from '../logging/logger.js';
|
|
25
25
|
import { getDefaultConfig } from '../runtime/defaults.js';
|
|
26
26
|
const logger = createLogger('cli');
|
|
@@ -184,7 +184,6 @@ export async function runCli() {
|
|
|
184
184
|
// 初始化 persona 文件
|
|
185
185
|
const personaDir = DEFAULT_PERSONA_DIR;
|
|
186
186
|
await ensurePersonaFiles(personaDir);
|
|
187
|
-
const bootstrapStatus = await getBootstrapStatus(personaDir);
|
|
188
187
|
// 如果用户选择进入 TUI,自动启动
|
|
189
188
|
if (result.enterTui && result.config.provider) {
|
|
190
189
|
await configManager.loadEnvKeys();
|
|
@@ -195,7 +194,6 @@ export async function runCli() {
|
|
|
195
194
|
maxContext: String(result.config.maxContext),
|
|
196
195
|
tui: true,
|
|
197
196
|
skipSetup: true,
|
|
198
|
-
bootstrapStatus,
|
|
199
197
|
});
|
|
200
198
|
}
|
|
201
199
|
});
|
|
@@ -686,7 +684,6 @@ async function executeAction(prompt, options) {
|
|
|
686
684
|
const startModel = options.startModel;
|
|
687
685
|
const skipSetup = options.skipSetup;
|
|
688
686
|
const localModelName = options.localModel;
|
|
689
|
-
const bootstrapStatus = options.bootstrapStatus;
|
|
690
687
|
// 加载 .env 中的 API Key
|
|
691
688
|
const configManager = new ConfigManager(process.cwd());
|
|
692
689
|
await configManager.loadEnvKeys();
|
|
@@ -794,7 +791,7 @@ async function executeAction(prompt, options) {
|
|
|
794
791
|
continuationMessage = fsSync.readFileSync(continuationFile, 'utf-8').trim() || undefined;
|
|
795
792
|
fsSync.unlinkSync(continuationFile);
|
|
796
793
|
}
|
|
797
|
-
await runTui(provider, sessionId, shouldContinue ?? false, maxTurns, maxContext, maxMessages, skipSetup, DEFAULT_PERSONA_DIR,
|
|
794
|
+
await runTui(provider, sessionId, shouldContinue ?? false, maxTurns, maxContext, maxMessages, skipSetup, DEFAULT_PERSONA_DIR, localModelProvider, continuationMessage);
|
|
798
795
|
return;
|
|
799
796
|
}
|
|
800
797
|
// Session + 模块初始化(由工厂统一处理)
|
|
@@ -813,10 +810,6 @@ async function executeAction(prompt, options) {
|
|
|
813
810
|
sessionDir = agentResult.sessionDir;
|
|
814
811
|
// 注入 LifecycleSupervisor,实现运行时 provider 切换时自动管理本地模型进程
|
|
815
812
|
loop.setLifecycleSupervisor(supervisor);
|
|
816
|
-
// Bootstrap Flow 已在 factory 中自动激活,发送空消息启动引导对话
|
|
817
|
-
if (loop.bootstrapActive) {
|
|
818
|
-
await loop.run('');
|
|
819
|
-
}
|
|
820
813
|
// 检测重启续工指令
|
|
821
814
|
if (!prompt) {
|
|
822
815
|
const continuationFile = path.join(os.homedir(), '.agent', '.restart-continuation');
|
|
@@ -12,7 +12,6 @@ import { AgentLoop } from '../orchestrator/loop.js';
|
|
|
12
12
|
import type { Provider } from '../provider/interface.js';
|
|
13
13
|
import type { DependencyAnalyzer } from '../dependency/analyzer.js';
|
|
14
14
|
import type { OutputHandler } from '../orchestrator/loop.js';
|
|
15
|
-
import type { BootstrapStatus } from '../setup/persona-bootstrap.js';
|
|
16
15
|
import { ModelRouter } from '../provider/model-router.js';
|
|
17
16
|
import { HeartbeatScheduler } from '../schedule/scheduler.js';
|
|
18
17
|
import { HotReloadManager } from '../hot-reload/index.js';
|
|
@@ -31,10 +30,8 @@ export interface CreateAgentOptions {
|
|
|
31
30
|
shouldContinue?: boolean;
|
|
32
31
|
/** 每个 session 的最大消息数 */
|
|
33
32
|
maxMessages?: number;
|
|
34
|
-
/**
|
|
33
|
+
/** Persona 文件目录 */
|
|
35
34
|
personaDir?: string;
|
|
36
|
-
/** Bootstrap 状态 */
|
|
37
|
-
bootstrapStatus?: BootstrapStatus;
|
|
38
35
|
/** 本地模型 Provider(用于压缩通道,不影响主对话) */
|
|
39
36
|
localModelProvider?: Provider;
|
|
40
37
|
/** 渠道信息(注入到 System Prompt 的 environment section) */
|
package/dist/gateway/factory.js
CHANGED
|
@@ -27,7 +27,7 @@ import { MemoryStore } from '../memory/memory-store.js';
|
|
|
27
27
|
import { AgentLoop } from '../orchestrator/loop.js';
|
|
28
28
|
import { PluginManager } from '../plugins/index.js';
|
|
29
29
|
import { initDependencyAnalyzer } from '../dependency/index.js';
|
|
30
|
-
import { ensureGlobalPersonaFiles, ensureGlobalPromptDir
|
|
30
|
+
import { ensureGlobalPersonaFiles, ensureGlobalPromptDir } from '../setup/persona-bootstrap.js';
|
|
31
31
|
import { ConfigManager } from '../setup/config.js';
|
|
32
32
|
import { RuntimeConfigCenter } from '../runtime/config-center.js';
|
|
33
33
|
import { getDefaultConfig } from '../runtime/defaults.js';
|
|
@@ -45,7 +45,7 @@ import { injectConfigCenter } from '../provider/local-config.js';
|
|
|
45
45
|
import { getModelContextWindow } from '../setup/model-defaults.js';
|
|
46
46
|
import { modelCatalog } from '../provider/catalog.js';
|
|
47
47
|
import { TurnRecorder, TurnStore, createRollbackStatusTool, createRollbackTool } from '../rollback/index.js';
|
|
48
|
-
import { MachineRegistry,
|
|
48
|
+
import { MachineRegistry, TodoFlow, SpecFlow } from '../machine/index.js';
|
|
49
49
|
import { createAskUserTool } from '../tools/ask-user.js';
|
|
50
50
|
import { createFlowStartTool, createFlowAddTool, createFlowCompleteTool } from '../tools/flow.js';
|
|
51
51
|
import { createTriggerCompressionTool } from '../tools/compression.js';
|
|
@@ -73,7 +73,7 @@ const logger = createLogger('factory');
|
|
|
73
73
|
//
|
|
74
74
|
// ─────────────────────────────────────────────────────────────────────
|
|
75
75
|
export async function createAgent(options, supervisor) {
|
|
76
|
-
const { cwd, provider, maxTurns, maxContext, outputHandler, sessionId, shouldContinue, maxMessages = 10000, personaDir,
|
|
76
|
+
const { cwd, provider, maxTurns, maxContext, outputHandler, sessionId, shouldContinue, maxMessages = 10000, personaDir, localModelProvider, channelsInfo } = options;
|
|
77
77
|
// ── 配置预加载(需在 session 创建前读取 startup.defaultMode)──────
|
|
78
78
|
const configManager = new ConfigManager(cwd);
|
|
79
79
|
const config = await configManager.load();
|
|
@@ -118,7 +118,6 @@ export async function createAgent(options, supervisor) {
|
|
|
118
118
|
// ── Global Persona Bootstrap ──────────────────────────────────────
|
|
119
119
|
const personaSetup = await ensureGlobalPersonaFiles();
|
|
120
120
|
const effectivePersonaDir = personaDir ?? personaSetup.personaDir;
|
|
121
|
-
const effectiveBootstrapStatus = bootstrapStatus ?? (await getBootstrapStatus(effectivePersonaDir));
|
|
122
121
|
// ── 内置 Prompt 同步 ─────────────────────────────────────────────
|
|
123
122
|
// 将所有内置 prompt 同步到 ~/.agent/prompts/ 下,
|
|
124
123
|
// 使得 loadPrompt() 的查找优先级正确:外部覆盖 > 内置兜底。
|
|
@@ -130,8 +129,7 @@ export async function createAgent(options, supervisor) {
|
|
|
130
129
|
await ensureGlobalPromptDir('precise');
|
|
131
130
|
await ensureGlobalPromptDir('environment');
|
|
132
131
|
// root 级文件 summary.md 由 loadPrompt 递归搜索找到,暂不单独同步
|
|
133
|
-
// ── 注册 Flow(
|
|
134
|
-
flowRegistry.register(new BootstrapFlow(effectivePersonaDir));
|
|
132
|
+
// ── 注册 Flow(TODO + Spec)────────────────────────────────
|
|
135
133
|
flowRegistry.register(new TodoFlow());
|
|
136
134
|
flowRegistry.register(new SpecFlow());
|
|
137
135
|
// ── Provider Config Loader(必须在 getDefaultConfig 之前,确保 providerDefault 读到 JSON) ──
|
|
@@ -523,10 +521,6 @@ export async function createAgent(options, supervisor) {
|
|
|
523
521
|
// 注入旁路 Provider 引用(供模式切换时 setBypassUserId 使用)
|
|
524
522
|
if (bypassProvider)
|
|
525
523
|
loop.setBypassProvider(bypassProvider);
|
|
526
|
-
// ── Bootstrap Flow 自动激活(首次安装后自动运行) ──
|
|
527
|
-
if (effectiveBootstrapStatus === 'pending') {
|
|
528
|
-
flowRegistry.activate('bootstrap');
|
|
529
|
-
}
|
|
530
524
|
// 注册后台进程注册表到 LifecycleSupervisor(优雅关闭时自动清理)
|
|
531
525
|
if (supervisor) {
|
|
532
526
|
supervisor.registerBackgroundRegistry(backgroundRegistry);
|
package/dist/gateway/tui.d.ts
CHANGED
|
@@ -11,6 +11,5 @@
|
|
|
11
11
|
* editor — user input area
|
|
12
12
|
*/
|
|
13
13
|
import type { Provider } from '../provider/interface.js';
|
|
14
|
-
|
|
15
|
-
export declare function runTui(provider: Provider, sessionId: string | undefined, shouldContinue: boolean, maxTurns: number, maxContext: number, maxMessages?: number, skipSetup?: boolean, personaDir?: string, bootstrapStatus?: BootstrapStatus, localModelProvider?: Provider, continuationMessage?: string): Promise<void>;
|
|
14
|
+
export declare function runTui(provider: Provider, sessionId: string | undefined, shouldContinue: boolean, maxTurns: number, maxContext: number, maxMessages?: number, skipSetup?: boolean, personaDir?: string, localModelProvider?: Provider, continuationMessage?: string): Promise<void>;
|
|
16
15
|
//# sourceMappingURL=tui.d.ts.map
|
package/dist/gateway/tui.js
CHANGED
|
@@ -23,7 +23,7 @@ import { RuntimeConfigCenter } from '../runtime/config-center.js';
|
|
|
23
23
|
import { ProviderManager } from '../provider/manager.js';
|
|
24
24
|
import { LifecycleSupervisor } from '../lifecycle/index.js';
|
|
25
25
|
import { createAgent } from './factory.js';
|
|
26
|
-
import {
|
|
26
|
+
import { DEFAULT_PERSONA_DIR } from '../setup/persona-bootstrap.js';
|
|
27
27
|
import { createLogger } from '../logging/logger.js';
|
|
28
28
|
import { TuiChannel } from '../channels/builtin/tui-channel.js';
|
|
29
29
|
import { ChannelManager } from '../channels/manager.js';
|
|
@@ -197,7 +197,7 @@ function replayEvents(chatLog, sessionDir) {
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
// ─── Main TUI ─────────────────────────────────────────────────────────────
|
|
200
|
-
export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxContext, maxMessages = 10000, skipSetup, personaDir = DEFAULT_PERSONA_DIR,
|
|
200
|
+
export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxContext, maxMessages = 10000, skipSetup, personaDir = DEFAULT_PERSONA_DIR, localModelProvider, continuationMessage) {
|
|
201
201
|
// ── Force UTF-8 console encoding on Windows ──
|
|
202
202
|
if (process.platform === 'win32') {
|
|
203
203
|
try {
|
|
@@ -716,8 +716,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
716
716
|
});
|
|
717
717
|
},
|
|
718
718
|
};
|
|
719
|
-
// ── Bootstrap detection ──
|
|
720
|
-
const resolvedBootstrapStatus = bootstrapStatus ?? (await getBootstrapStatus(personaDir));
|
|
721
719
|
// ── Local Model (llama.cpp) ──
|
|
722
720
|
try {
|
|
723
721
|
const loadedModels = await supervisor.loadAndStartModels(process.cwd());
|
|
@@ -793,7 +791,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
793
791
|
shouldContinue,
|
|
794
792
|
maxMessages,
|
|
795
793
|
personaDir,
|
|
796
|
-
bootstrapStatus: resolvedBootstrapStatus,
|
|
797
794
|
localModelProvider,
|
|
798
795
|
channel: 'tui',
|
|
799
796
|
});
|
|
@@ -821,7 +818,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
821
818
|
setScheduler: () => { },
|
|
822
819
|
notifyTaskFired: async () => { },
|
|
823
820
|
subscribeConfig: () => { },
|
|
824
|
-
isBootstrapPending: () => false,
|
|
825
821
|
shutdown: async () => { },
|
|
826
822
|
};
|
|
827
823
|
// 远程模式下 agent 为 undefined,这些变量仅用于本地 TUI 功能(斜杠命令等),用 any 避免 null 检查
|
|
@@ -1049,22 +1045,6 @@ export async function runTui(provider, sessionId, shouldContinue, maxTurns, maxC
|
|
|
1049
1045
|
chatLog.addSystem('');
|
|
1050
1046
|
chatLog.addSystem(theme.dim('Persona: ') + theme.accent(personaDir));
|
|
1051
1047
|
chatLog.addSystem('');
|
|
1052
|
-
// ── Bootstrap ──
|
|
1053
|
-
if (resolvedBootstrapStatus === 'pending') {
|
|
1054
|
-
chatLog.addSystem(theme.warning('\u256d' + BOX_H.repeat(56) + '\u256e'));
|
|
1055
|
-
chatLog.addSystem(theme.warning('\u2502 \uD83D\uDD27 Bootstrap Init'));
|
|
1056
|
-
chatLog.addSystem(theme.warning('\u2502 AI will ask you setup questions to configure the persona.'));
|
|
1057
|
-
chatLog.addSystem(theme.warning('\u2570' + BOX_H.repeat(56) + '\u256f'));
|
|
1058
|
-
chatLog.addSystem('');
|
|
1059
|
-
try {
|
|
1060
|
-
// Flow 已在 factory.ts 中自动激活,发送空消息让模型根据 flow_injection 引导对话
|
|
1061
|
-
await loop.run('');
|
|
1062
|
-
}
|
|
1063
|
-
catch (error) {
|
|
1064
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1065
|
-
chatLog.addSystem(theme.errorBright('[Bootstrap Error] ') + theme.error(message));
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
1048
|
chatLog.addSystem(theme.dim('Type ') +
|
|
1069
1049
|
theme.success('exit') +
|
|
1070
1050
|
theme.dim(' to quit. ') +
|
package/dist/machine/index.d.ts
CHANGED
|
@@ -3,6 +3,5 @@ export { MachineRunner } from './runner.js';
|
|
|
3
3
|
export { MachineRegistry } from './registry.js';
|
|
4
4
|
export type { FlowController } from './flows/types.js';
|
|
5
5
|
export { TodoFlow } from './flows/todo.js';
|
|
6
|
-
export { BootstrapFlow } from './flows/bootstrap.js';
|
|
7
6
|
export { SpecFlow } from './flows/spec.js';
|
|
8
7
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/machine/index.js
CHANGED
|
@@ -4,6 +4,5 @@
|
|
|
4
4
|
export { MachineRunner } from './runner.js';
|
|
5
5
|
export { MachineRegistry } from './registry.js';
|
|
6
6
|
export { TodoFlow } from './flows/todo.js';
|
|
7
|
-
export { BootstrapFlow } from './flows/bootstrap.js';
|
|
8
7
|
export { SpecFlow } from './flows/spec.js';
|
|
9
8
|
//# sourceMappingURL=index.js.map
|
package/dist/machine/runner.js
CHANGED
|
@@ -77,46 +77,45 @@ export class MachineRunner {
|
|
|
77
77
|
if (this.status !== 'active') {
|
|
78
78
|
return { ok: false, reason: `Machine "${this.definition.id}" is not active (status: ${this.status}).` };
|
|
79
79
|
}
|
|
80
|
-
// 1.
|
|
81
|
-
|
|
80
|
+
// 1. 找匹配的转移(按优先级——跳过 guard 不通过的)
|
|
81
|
+
let transition;
|
|
82
|
+
for (const t of this.definition.transitions) {
|
|
82
83
|
const fromMatch = Array.isArray(t.from)
|
|
83
84
|
? t.from.includes(this.currentState)
|
|
84
85
|
: t.from === this.currentState;
|
|
85
|
-
|
|
86
|
-
|
|
86
|
+
if (fromMatch && t.event === event) {
|
|
87
|
+
if (t.guard) {
|
|
88
|
+
const guardResult = t.guard(this.context);
|
|
89
|
+
if (!guardResult.ok)
|
|
90
|
+
continue; // guard 不通过,试下一个
|
|
91
|
+
}
|
|
92
|
+
transition = t;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
87
96
|
if (!transition) {
|
|
88
97
|
return {
|
|
89
98
|
ok: false,
|
|
90
99
|
reason: `Event "${event}" is not valid in state "${this.currentState}" of machine "${this.definition.id}". Available events: [${this.getAvailableEvents().join(', ')}]`,
|
|
91
100
|
};
|
|
92
101
|
}
|
|
93
|
-
// 2.
|
|
94
|
-
if (transition.guard) {
|
|
95
|
-
const guardResult = transition.guard(this.context);
|
|
96
|
-
if (!guardResult.ok) {
|
|
97
|
-
return {
|
|
98
|
-
ok: false,
|
|
99
|
-
reason: guardResult.reason ?? `Guard rejected transition from "${this.currentState}" to "${transition.to}" via "${event}".`,
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// 3. onExit 副作用(fire-and-forget)
|
|
102
|
+
// 2. onExit 副作用(fire-and-forget)
|
|
104
103
|
const oldState = this.currentState;
|
|
105
104
|
const oldDef = this.definition.states[oldState];
|
|
106
105
|
safeFire(oldDef?.onExit, this.context);
|
|
107
|
-
//
|
|
106
|
+
// 3. onTransition 副作用(同步执行——可阻塞)
|
|
108
107
|
transition.onTransition?.(this.context);
|
|
109
|
-
//
|
|
108
|
+
// 4. 更新状态
|
|
110
109
|
this.currentState = transition.to;
|
|
111
|
-
//
|
|
110
|
+
// 5. onEnter 副作用(fire-and-forget)
|
|
112
111
|
const newDef = this.definition.states[this.currentState];
|
|
113
112
|
safeFire(newDef?.onEnter, this.context);
|
|
114
|
-
//
|
|
113
|
+
// 6. 记录历史
|
|
115
114
|
this.transitionHistory.push({ from: oldState, to: transition.to, event });
|
|
116
115
|
if (this.transitionHistory.length > MAX_HISTORY) {
|
|
117
116
|
this.transitionHistory = this.transitionHistory.slice(-MAX_HISTORY);
|
|
118
117
|
}
|
|
119
|
-
//
|
|
118
|
+
// 7. 终端状态检查
|
|
120
119
|
const isTerminal = this.definition.terminalStates?.includes(this.currentState) ?? false;
|
|
121
120
|
if (isTerminal) {
|
|
122
121
|
this.status = 'completed';
|
|
@@ -213,8 +213,6 @@ export declare class AgentLoop {
|
|
|
213
213
|
/** Get current turn/mode info for status display */
|
|
214
214
|
getTurnInfo(turnCount: number, tokensUsed: number): TurnInfo;
|
|
215
215
|
private getRecentToolCallNames;
|
|
216
|
-
/** Bootstrap Flow 是否活跃(供 CLI/TUI 判断是否需要发送空消息启动引导) */
|
|
217
|
-
get bootstrapActive(): boolean;
|
|
218
216
|
/** 就地切换到指定 session,无需重启进程 */
|
|
219
217
|
switchSession(newSessionDir: string): Promise<void>;
|
|
220
218
|
/** 设置调度器 */
|
|
@@ -411,10 +411,6 @@ export class AgentLoop {
|
|
|
411
411
|
getRecentToolCallNames() {
|
|
412
412
|
return this.recentToolNames;
|
|
413
413
|
}
|
|
414
|
-
/** Bootstrap Flow 是否活跃(供 CLI/TUI 判断是否需要发送空消息启动引导) */
|
|
415
|
-
get bootstrapActive() {
|
|
416
|
-
return this.flowRegistry.getActive()?.id === 'bootstrap';
|
|
417
|
-
}
|
|
418
414
|
/** 就地切换到指定 session,无需重启进程 */
|
|
419
415
|
async switchSession(newSessionDir) {
|
|
420
416
|
this.sessionDir = newSessionDir;
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
declare const PERSONA_FILE_NAMES: readonly ["
|
|
1
|
+
declare const PERSONA_FILE_NAMES: readonly ["SOUL.md", "IDENTITY.md", "USER.md", "PartnerSoul.md", "PartnerMemory.md"];
|
|
2
2
|
export type PersonaFileName = (typeof PERSONA_FILE_NAMES)[number];
|
|
3
|
-
export type BootstrapStatus = 'pending' | 'complete';
|
|
4
3
|
export interface PersonaState {
|
|
5
4
|
version: number;
|
|
6
|
-
bootstrapSeededAt?: string;
|
|
7
5
|
setupCompletedAt?: string;
|
|
8
6
|
}
|
|
9
7
|
export interface PersonaFile {
|
|
@@ -22,29 +20,25 @@ export declare const DEFAULT_PERSONA_DIR: string;
|
|
|
22
20
|
/**
|
|
23
21
|
* 确保内置 prompt 目录同步到 ~/.agent/prompts/{name}/。
|
|
24
22
|
*
|
|
25
|
-
*
|
|
23
|
+
* 这只是一个简单的文件复制:
|
|
26
24
|
* 首次安装时复制,已存在则跳过。
|
|
27
25
|
*
|
|
28
26
|
* 用于 attention.md 等非 persona 的内置 prompt 文件。
|
|
29
27
|
*/
|
|
30
28
|
export declare function ensureGlobalPromptDir(name: string, configHome?: string): Promise<string[]>;
|
|
31
29
|
export declare function ensurePersonaFiles(personaDir: string): Promise<{
|
|
32
|
-
|
|
30
|
+
needsSetup: boolean;
|
|
33
31
|
filesCreated: string[];
|
|
34
32
|
state: PersonaState;
|
|
35
33
|
}>;
|
|
36
34
|
export declare function ensureGlobalPersonaFiles(configHome?: string): Promise<{
|
|
37
|
-
|
|
35
|
+
needsSetup: boolean;
|
|
38
36
|
filesCreated: string[];
|
|
39
37
|
state: PersonaState;
|
|
40
38
|
personaDir: string;
|
|
41
39
|
}>;
|
|
42
40
|
export declare function validatePersonaFiles(personaDir: string): Promise<PersonaValidationResult>;
|
|
43
41
|
export declare function validatePersonaFilesSync(personaDir: string): PersonaValidationResult;
|
|
44
|
-
export declare function isBootstrapComplete(personaDir: string): Promise<boolean>;
|
|
45
|
-
export declare function getBootstrapStatus(personaDir: string): Promise<BootstrapStatus>;
|
|
46
|
-
export declare function markBootstrapComplete(personaDir: string): Promise<void>;
|
|
47
|
-
export declare function markBootstrapCompleteSync(personaDir: string): void;
|
|
48
42
|
export declare function loadPersonaFiles(personaDir: string, cwdFallback?: string): Promise<PersonaFile[]>;
|
|
49
43
|
export declare function getPersonaState(personaDir: string): Promise<PersonaState>;
|
|
50
44
|
export {};
|
|
@@ -11,7 +11,6 @@ const PERSONA_STATE_DIR = '.state';
|
|
|
11
11
|
const PERSONA_STATE_FILE = 'persona-state.json';
|
|
12
12
|
const PERSONA_STATE_VERSION = 1;
|
|
13
13
|
const PERSONA_FILE_NAMES = [
|
|
14
|
-
'BOOTSTRAP.md',
|
|
15
14
|
'SOUL.md',
|
|
16
15
|
'IDENTITY.md',
|
|
17
16
|
'USER.md',
|
|
@@ -97,7 +96,7 @@ async function fileContentDiffersFromTemplate(filePath, template) {
|
|
|
97
96
|
/**
|
|
98
97
|
* 确保内置 prompt 目录同步到 ~/.agent/prompts/{name}/。
|
|
99
98
|
*
|
|
100
|
-
*
|
|
99
|
+
* 这只是一个简单的文件复制:
|
|
101
100
|
* 首次安装时复制,已存在则跳过。
|
|
102
101
|
*
|
|
103
102
|
* 用于 attention.md 等非 persona 的内置 prompt 文件。
|
|
@@ -150,9 +149,7 @@ export async function ensurePersonaFiles(personaDir) {
|
|
|
150
149
|
await fsp.mkdir(personaDir, { recursive: true });
|
|
151
150
|
let state = await readState(personaDir);
|
|
152
151
|
const now = new Date().toISOString();
|
|
153
|
-
//
|
|
154
|
-
//(例如 markBootstrapComplete 因格式验证过严而失败),
|
|
155
|
-
// 在重建 BOOTSTRAP.md 之前标记为完成。这是修复"每次启动都重新引导"的 bug。
|
|
152
|
+
// 自动检测:如果 persona 文件已被填写(内容 ≠ 模板)但 state 文件未反映,自动标记完成
|
|
156
153
|
if (!state.setupCompletedAt) {
|
|
157
154
|
try {
|
|
158
155
|
const identityPath = path.join(personaDir, 'IDENTITY.md');
|
|
@@ -173,8 +170,6 @@ export async function ensurePersonaFiles(personaDir) {
|
|
|
173
170
|
const alreadyComplete = !!state.setupCompletedAt;
|
|
174
171
|
const filesCreated = [];
|
|
175
172
|
for (const fileName of PERSONA_FILE_NAMES) {
|
|
176
|
-
if (alreadyComplete && fileName === 'BOOTSTRAP.md')
|
|
177
|
-
continue;
|
|
178
173
|
const filePath = path.join(personaDir, fileName);
|
|
179
174
|
const template = loadTemplateContent(fileName);
|
|
180
175
|
const created = await writeFileIfMissing(filePath, template);
|
|
@@ -183,38 +178,27 @@ export async function ensurePersonaFiles(personaDir) {
|
|
|
183
178
|
logger.info('Created persona file', { file: fileName, dir: personaDir });
|
|
184
179
|
}
|
|
185
180
|
}
|
|
186
|
-
|
|
187
|
-
let bootstrapExists = true;
|
|
188
|
-
try {
|
|
189
|
-
await fsp.access(bootstrapPath);
|
|
190
|
-
}
|
|
191
|
-
catch {
|
|
192
|
-
bootstrapExists = false;
|
|
193
|
-
}
|
|
194
|
-
if (bootstrapExists && !state.bootstrapSeededAt) {
|
|
195
|
-
state = { ...state, bootstrapSeededAt: now };
|
|
196
|
-
}
|
|
181
|
+
// 二次检查:如果文件已被修改但 state 尚未记录
|
|
197
182
|
if (!state.setupCompletedAt) {
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
state = { ...state, setupCompletedAt: now };
|
|
206
|
-
logger.info('Persona setup detected as complete (user-modified files)');
|
|
207
|
-
}
|
|
208
|
-
else if (!bootstrapExists && state.bootstrapSeededAt && !state.setupCompletedAt) {
|
|
183
|
+
try {
|
|
184
|
+
const identityPath = path.join(personaDir, 'IDENTITY.md');
|
|
185
|
+
const userPath = path.join(personaDir, 'USER.md');
|
|
186
|
+
const identityTemplate = loadTemplateContent('IDENTITY.md');
|
|
187
|
+
const userTemplate = loadTemplateContent('USER.md');
|
|
188
|
+
const identityChanged = await fileContentDiffersFromTemplate(identityPath, identityTemplate);
|
|
189
|
+
const userChanged = await fileContentDiffersFromTemplate(userPath, userTemplate);
|
|
209
190
|
if (identityChanged || userChanged) {
|
|
210
191
|
state = { ...state, setupCompletedAt: now };
|
|
211
|
-
logger.info('Persona setup
|
|
192
|
+
logger.info('Persona setup detected as complete (user-modified files)');
|
|
212
193
|
}
|
|
213
194
|
}
|
|
195
|
+
catch {
|
|
196
|
+
// 文件可能尚未创建
|
|
197
|
+
}
|
|
214
198
|
}
|
|
215
199
|
await writeState(personaDir, state);
|
|
216
|
-
const
|
|
217
|
-
return {
|
|
200
|
+
const needsSetup = !state.setupCompletedAt;
|
|
201
|
+
return { needsSetup, filesCreated, state };
|
|
218
202
|
}
|
|
219
203
|
export async function ensureGlobalPersonaFiles(configHome) {
|
|
220
204
|
const personaDir = getGlobalPersonaDir(configHome);
|
|
@@ -281,98 +265,6 @@ export function validatePersonaFilesSync(personaDir) {
|
|
|
281
265
|
templateFiles,
|
|
282
266
|
};
|
|
283
267
|
}
|
|
284
|
-
export async function isBootstrapComplete(personaDir) {
|
|
285
|
-
const state = await readState(personaDir);
|
|
286
|
-
if (!state.setupCompletedAt)
|
|
287
|
-
return false;
|
|
288
|
-
const validation = await validatePersonaFiles(personaDir);
|
|
289
|
-
return validation.complete;
|
|
290
|
-
}
|
|
291
|
-
export async function getBootstrapStatus(personaDir) {
|
|
292
|
-
const state = await readState(personaDir);
|
|
293
|
-
if (state.setupCompletedAt) {
|
|
294
|
-
const validation = await validatePersonaFiles(personaDir);
|
|
295
|
-
return validation.complete ? 'complete' : 'pending';
|
|
296
|
-
}
|
|
297
|
-
const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
|
|
298
|
-
try {
|
|
299
|
-
await fsp.access(bootstrapPath);
|
|
300
|
-
return 'pending';
|
|
301
|
-
}
|
|
302
|
-
catch {
|
|
303
|
-
return 'complete';
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
export async function markBootstrapComplete(personaDir) {
|
|
307
|
-
const validation = await validatePersonaFiles(personaDir);
|
|
308
|
-
if (!validation.complete) {
|
|
309
|
-
const details = [
|
|
310
|
-
validation.missing.length > 0 ? `missing: ${validation.missing.join(', ')}` : '',
|
|
311
|
-
validation.templateFiles.length > 0 ? `templates: ${validation.templateFiles.join(', ')}` : '',
|
|
312
|
-
].filter(Boolean).join('; ');
|
|
313
|
-
throw new Error(`Persona bootstrap is incomplete${details ? ` (${details})` : ''}.`);
|
|
314
|
-
}
|
|
315
|
-
const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
|
|
316
|
-
try {
|
|
317
|
-
await fsp.unlink(bootstrapPath);
|
|
318
|
-
logger.info('Deleted BOOTSTRAP.md');
|
|
319
|
-
}
|
|
320
|
-
catch (err) {
|
|
321
|
-
const code = err.code;
|
|
322
|
-
if (code !== 'ENOENT')
|
|
323
|
-
throw err;
|
|
324
|
-
}
|
|
325
|
-
const state = await readState(personaDir);
|
|
326
|
-
state.setupCompletedAt = new Date().toISOString();
|
|
327
|
-
await writeState(personaDir, state);
|
|
328
|
-
logger.info('Bootstrap marked as complete');
|
|
329
|
-
}
|
|
330
|
-
export function markBootstrapCompleteSync(personaDir) {
|
|
331
|
-
const validation = validatePersonaFilesSync(personaDir);
|
|
332
|
-
if (!validation.complete) {
|
|
333
|
-
const details = [
|
|
334
|
-
validation.missing.length > 0 ? `missing: ${validation.missing.join(', ')}` : '',
|
|
335
|
-
validation.templateFiles.length > 0 ? `templates: ${validation.templateFiles.join(', ')}` : '',
|
|
336
|
-
].filter(Boolean).join('; ');
|
|
337
|
-
throw new Error(`Persona bootstrap is incomplete${details ? ` (${details})` : ''}.`);
|
|
338
|
-
}
|
|
339
|
-
const bootstrapPath = path.join(personaDir, 'BOOTSTRAP.md');
|
|
340
|
-
try {
|
|
341
|
-
fs.unlinkSync(bootstrapPath);
|
|
342
|
-
logger.info('Deleted BOOTSTRAP.md');
|
|
343
|
-
}
|
|
344
|
-
catch (err) {
|
|
345
|
-
const code = err.code;
|
|
346
|
-
if (code !== 'ENOENT')
|
|
347
|
-
throw err;
|
|
348
|
-
}
|
|
349
|
-
const state = readStateSync(personaDir);
|
|
350
|
-
state.setupCompletedAt = new Date().toISOString();
|
|
351
|
-
writeStateSync(personaDir, state);
|
|
352
|
-
logger.info('Bootstrap marked as complete');
|
|
353
|
-
}
|
|
354
|
-
function readStateSync(personaDir) {
|
|
355
|
-
const statePath = resolveStatePath(personaDir);
|
|
356
|
-
try {
|
|
357
|
-
const raw = fs.readFileSync(statePath, 'utf-8');
|
|
358
|
-
const parsed = JSON.parse(raw);
|
|
359
|
-
if (parsed && typeof parsed === 'object' && parsed.version === PERSONA_STATE_VERSION) {
|
|
360
|
-
return parsed;
|
|
361
|
-
}
|
|
362
|
-
return { version: PERSONA_STATE_VERSION };
|
|
363
|
-
}
|
|
364
|
-
catch (err) {
|
|
365
|
-
const code = err.code;
|
|
366
|
-
if (code === 'ENOENT')
|
|
367
|
-
return { version: PERSONA_STATE_VERSION };
|
|
368
|
-
throw err;
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
function writeStateSync(personaDir, state) {
|
|
372
|
-
const stateDir = path.join(personaDir, PERSONA_STATE_DIR);
|
|
373
|
-
fs.mkdirSync(stateDir, { recursive: true });
|
|
374
|
-
fs.writeFileSync(resolveStatePath(personaDir), JSON.stringify(state, null, 2) + '\n', 'utf-8');
|
|
375
|
-
}
|
|
376
268
|
export async function loadPersonaFiles(personaDir, cwdFallback) {
|
|
377
269
|
const files = [];
|
|
378
270
|
for (const fileName of PERSONA_FILE_NAMES) {
|
|
@@ -406,8 +298,6 @@ export async function loadPersonaFiles(personaDir, cwdFallback) {
|
|
|
406
298
|
if (cwdFallback) {
|
|
407
299
|
for (let i = 0; i < files.length; i++) {
|
|
408
300
|
const f = files[i];
|
|
409
|
-
if (f.name === 'BOOTSTRAP.md')
|
|
410
|
-
continue;
|
|
411
301
|
const template = loadTemplateContent(f.name);
|
|
412
302
|
if (f.content === template || !f.content) {
|
|
413
303
|
const cwdPath = path.join(cwdFallback, f.name);
|
package/dist/setup/wizard.js
CHANGED
|
@@ -121,7 +121,7 @@ export class SetupWizard {
|
|
|
121
121
|
if (ws.filesCreated.length > 0) {
|
|
122
122
|
p.log.info(`已在 ${personaDir} 创建 ${ws.filesCreated.length} 个模板文件`);
|
|
123
123
|
}
|
|
124
|
-
if (ws.
|
|
124
|
+
if (ws.needsSetup) {
|
|
125
125
|
p.log.info(pc.cyan('首次启动时将进行个性化引导对话'));
|
|
126
126
|
}
|
|
127
127
|
}
|