@miphamai/cli 0.85.4 → 0.85.6
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/bin/mipham.ts +48 -7
- package/package.json +2 -2
- package/src/agent/agent-context.ts +8 -1
- package/src/agent/effectiveness-tracker.ts +16 -2
- package/src/agent/sub-agent.ts +25 -17
- package/src/agent-view/agents-standalone.tsx +42 -0
- package/src/core/autocomplete.ts +30 -2
- package/src/core/context.ts +61 -6
- package/src/core/dream-engine.ts +17 -2
- package/src/core/engine.ts +95 -34
- package/src/core/error-signature-db.ts +7 -2
- package/src/core/hooks-executor.ts +88 -11
- package/src/core/hooks.ts +26 -2
- package/src/core/instructions.ts +105 -17
- package/src/core/memory/memory-manager.ts +14 -6
- package/src/core/permission-classifier.ts +17 -5
- package/src/core/permission-rules.ts +1 -1
- package/src/core/permission.ts +50 -1
- package/src/core/rule-engine.ts +27 -2
- package/src/core/self-critique.ts +15 -3
- package/src/core/session-log.ts +60 -0
- package/src/daemon/index.ts +2 -5
- package/src/daemon/launch.ts +69 -2
- package/src/daemon/server.ts +19 -14
- package/src/i18n-core/locales/en-US.json +86 -143
- package/src/i18n-core/locales/zh-CN.json +86 -143
- package/src/index.tsx +17 -7
- package/src/mcp/client.ts +61 -0
- package/src/mcp/instructions.ts +49 -0
- package/src/mcp/types.ts +7 -0
- package/src/plugin/claude-plugin.ts +12 -2
- package/src/plugin/plugin-loader.ts +32 -14
- package/src/plugin/plugin-manager.ts +16 -2
- package/src/plugin/plugin-validator.ts +183 -1
- package/src/providers/anthropic.ts +159 -124
- package/src/providers/fetch-utils.ts +65 -34
- package/src/providers/openai-compat.ts +121 -106
- package/src/security/dangerous-rm.ts +192 -0
- package/src/shared/arg-validation.ts +11 -1
- package/src/shared/constants.ts +18 -0
- package/src/shared/deleted-cwd.ts +46 -1
- package/src/shared/package-info.ts +36 -1
- package/src/shared/types.ts +8 -0
- package/src/shared/update.ts +290 -146
- package/src/ui/command-picker.tsx +18 -10
- package/src/ui/commands.ts +20 -6
- package/src/ui/config-wizard.tsx +22 -19
- package/src/ui/graft-status.tsx +35 -6
- package/src/ui/input.tsx +7 -2
- package/src/ui/picker.tsx +38 -27
- package/src/ui/use-key-state.ts +55 -0
- package/src/daemon/message-bus.ts +0 -84
package/bin/mipham.ts
CHANGED
|
@@ -293,13 +293,26 @@ async function runUpdate(): Promise<boolean> {
|
|
|
293
293
|
if (!result.ok) {
|
|
294
294
|
console.log()
|
|
295
295
|
console.log(`✗ Update failed: ${result.reason ?? 'unknown error'}`)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
296
|
+
// 这句必须只说**我们知道的**:staging 之后最常见的失败(装在旁边那步挂了)压根没碰过
|
|
297
|
+
// 旧树,此时沿用「未能恢复」就是在讲一件没发生的事。
|
|
298
|
+
switch (result.installState) {
|
|
299
|
+
case 'untouched':
|
|
300
|
+
console.log(
|
|
301
|
+
` Your previous install (v${currentVersion}) was not touched — mipham still works.`,
|
|
302
|
+
)
|
|
303
|
+
break
|
|
304
|
+
case 'restored':
|
|
305
|
+
console.log(
|
|
306
|
+
` Your previous install (v${currentVersion}) has been restored — mipham still works.`,
|
|
307
|
+
)
|
|
308
|
+
break
|
|
309
|
+
case 'unknown':
|
|
310
|
+
console.log(' ⚠ Could not locate the global install path — unable to tell.')
|
|
311
|
+
console.log(` Check with: mipham --version`)
|
|
312
|
+
break
|
|
313
|
+
default:
|
|
314
|
+
console.log(' ⚠ The previous install could not be restored.')
|
|
315
|
+
console.log(` Reinstall with: npm install -g ${PACKAGE}@${currentVersion}`)
|
|
303
316
|
}
|
|
304
317
|
if (backupPath && existsSync(backupPath)) {
|
|
305
318
|
console.log(` Your config backup is at: ${backupPath}`)
|
|
@@ -1249,6 +1262,8 @@ Flags:
|
|
|
1249
1262
|
--resume <name> Open a saved session (see /resume for names)
|
|
1250
1263
|
--permission <mode> Start in this mode: ${ALL_MODES.join('|')}
|
|
1251
1264
|
(also accepted by 'mipham attach'; the daemon may clamp it)
|
|
1265
|
+
--provider <id> Start on this provider (overrides config.yml)
|
|
1266
|
+
--model <id> Start on this model (overrides config.yml)
|
|
1252
1267
|
--version, -v, -V Print version
|
|
1253
1268
|
|
|
1254
1269
|
Docs: https://mipham.ai/code
|
|
@@ -1384,12 +1399,38 @@ npm: https://www.npmjs.com/package/@miphamai/cli`)
|
|
|
1384
1399
|
process.exit(1)
|
|
1385
1400
|
}
|
|
1386
1401
|
|
|
1402
|
+
// Parse --provider <id> / --model <id>: the provider and model the session starts
|
|
1403
|
+
// on. Same shape as `--resume`/`--permission` — `RunOptions` declared both fields all
|
|
1404
|
+
// along, `index.tsx` already reads them *ahead of* the merged config, and **nothing
|
|
1405
|
+
// ever passed them**. That third instance cost more than the first two: both shipped
|
|
1406
|
+
// IDE integrations build this exact command from their settings
|
|
1407
|
+
// (`infrastructure/vscode/extension.js` `buildFlags()`, `MiphamAction.kt`
|
|
1408
|
+
// `buildCommand()`), so a user who set a provider there got
|
|
1409
|
+
// `Unknown command: mipham deepseek` and no CLI at all. Their values are open — a
|
|
1410
|
+
// provider may be user-defined — so the value is forwarded as typed and an unknown id
|
|
1411
|
+
// is refused by the registry (`ProviderRegistry.getActive()` throws with the id named),
|
|
1412
|
+
// exactly as the same value already was when it came from `config.yml`.
|
|
1413
|
+
const flagValue = (name: string): string | undefined => {
|
|
1414
|
+
const at = process.argv.indexOf(name)
|
|
1415
|
+
if (at === -1) return undefined
|
|
1416
|
+
const value = process.argv[at + 1]
|
|
1417
|
+
if (!value || value.startsWith('-')) {
|
|
1418
|
+
console.error(`Usage: mipham ${name} <id>`)
|
|
1419
|
+
process.exit(1)
|
|
1420
|
+
}
|
|
1421
|
+
return value
|
|
1422
|
+
}
|
|
1423
|
+
const providerFlag = flagValue('--provider')
|
|
1424
|
+
const modelFlag = flagValue('--model')
|
|
1425
|
+
|
|
1387
1426
|
try {
|
|
1388
1427
|
const { runApp } = await import('../src/index')
|
|
1389
1428
|
await runApp({
|
|
1390
1429
|
version: APP_VERSION,
|
|
1391
1430
|
resume: resumeName,
|
|
1392
1431
|
permission: permissionFlag.kind === 'ok' ? permissionFlag.mode : undefined,
|
|
1432
|
+
provider: providerFlag,
|
|
1433
|
+
model: modelFlag,
|
|
1393
1434
|
})
|
|
1394
1435
|
} catch (err: unknown) {
|
|
1395
1436
|
const msg = err instanceof Error ? err.message : String(err)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miphamai/cli",
|
|
3
|
-
"version": "0.85.
|
|
3
|
+
"version": "0.85.6",
|
|
4
4
|
"description": "Mipham Code — Multi-model open-core intelligent coding terminal by MiphamAI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"typecheck": "tsc --noEmit",
|
|
41
41
|
"test": "vitest run",
|
|
42
42
|
"knip": "knip --production --no-progress --no-exit-code",
|
|
43
|
-
"coverage": "vitest run --coverage",
|
|
43
|
+
"coverage": "vitest run --coverage --reporter=default --reporter=json --outputFile.json=coverage/vitest-report.json",
|
|
44
44
|
"mutate": "stryker run"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
@@ -13,6 +13,13 @@ import { MIPHAM_DIR } from '../shared/constants.ts'
|
|
|
13
13
|
export interface AgentContextResult {
|
|
14
14
|
context: ContextManager
|
|
15
15
|
allowedTools: ToolDefinition[]
|
|
16
|
+
/**
|
|
17
|
+
* 组装好的系统提示(**含** agent memory)。
|
|
18
|
+
*
|
|
19
|
+
* 必须由这里交出去、而不是让调用方自己再拼一份:memory 的拼接规则只此一处,调用方
|
|
20
|
+
* 拿不到它就等于重新推导一遍(漏掉的那一遍正是缺陷本身 —— 请求里从来没有记忆)。
|
|
21
|
+
*/
|
|
22
|
+
systemPrompt: string
|
|
16
23
|
}
|
|
17
24
|
|
|
18
25
|
/**
|
|
@@ -145,5 +152,5 @@ export function createAgentContext(
|
|
|
145
152
|
allowedTools = allowedTools.filter((t) => !denySet.has(t.name))
|
|
146
153
|
}
|
|
147
154
|
|
|
148
|
-
return { context, allowedTools }
|
|
155
|
+
return { context, allowedTools, systemPrompt }
|
|
149
156
|
}
|
|
@@ -181,8 +181,22 @@ export class EffectivenessTracker {
|
|
|
181
181
|
load(): void {
|
|
182
182
|
if (!existsSync(this.storePath)) return
|
|
183
183
|
try {
|
|
184
|
-
const
|
|
185
|
-
|
|
184
|
+
const parsed: unknown = JSON.parse(readFileSync(this.storePath, 'utf-8'))
|
|
185
|
+
// 形状门:要的是「**以规则 id 为键的对象**」。`Object.entries` 对数组会拿**下标**
|
|
186
|
+
// 当键(`{"0":"x"}` ⇒ 一条 ruleId 为 undefined 的记录),而 `new Map(...)` 照收,
|
|
187
|
+
// 于是 `allRules` 把非规则对象当规则报出去。
|
|
188
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
189
|
+
this.data = new Map()
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
const data = new Map<string, RuleEffectiveness>()
|
|
193
|
+
for (const [k, v] of Object.entries(parsed)) {
|
|
194
|
+
if (!v || typeof v !== 'object' || Array.isArray(v)) continue
|
|
195
|
+
const rec = v as { ruleId?: unknown }
|
|
196
|
+
if (typeof rec.ruleId !== 'string') continue
|
|
197
|
+
data.set(k, v as RuleEffectiveness)
|
|
198
|
+
}
|
|
199
|
+
this.data = data
|
|
186
200
|
} catch {
|
|
187
201
|
// Corrupt file — start fresh
|
|
188
202
|
this.data = new Map()
|
package/src/agent/sub-agent.ts
CHANGED
|
@@ -339,23 +339,31 @@ export class SubAgent {
|
|
|
339
339
|
}
|
|
340
340
|
|
|
341
341
|
// Create isolated context with tool scoping, sized to the resolved model.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
342
|
+
// `systemPrompt` 是运行时**解析出来的那一份**(定义 > 调用方传入 > 内建类型),
|
|
343
|
+
// 定义自己没写时就落到解析结果上 —— 否则 `createAgentContext()` 拿到的是空串,
|
|
344
|
+
// 记忆会拼在一个空的基座上。
|
|
345
|
+
const resolvedDef: AgentDefinition = agentDef
|
|
346
|
+
? { ...agentDef, systemPrompt: agentDef.systemPrompt || systemPrompt }
|
|
347
|
+
: {
|
|
348
|
+
name: agentType,
|
|
349
|
+
description: '',
|
|
350
|
+
systemPrompt,
|
|
351
|
+
model: options.modelOverride || 'inherit',
|
|
352
|
+
permissionMode: 'inherit',
|
|
353
|
+
background: false,
|
|
354
|
+
source: 'builtin',
|
|
355
|
+
}
|
|
351
356
|
const contextWindow = this.registry.findModel(finalModel)?.contextWindow
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
+
// 这一份**已含 agent memory**,是发出去的那个提示的唯一来源。
|
|
358
|
+
const {
|
|
359
|
+
context,
|
|
360
|
+
allowedTools,
|
|
361
|
+
systemPrompt: composedSystemPrompt,
|
|
362
|
+
} = createAgentContext(resolvedDef, this.toolRegistry, contextWindow)
|
|
357
363
|
|
|
358
|
-
context.setSystemPrompt(systemPrompt)
|
|
364
|
+
// 别再 `context.setSystemPrompt(systemPrompt)`:`createAgentContext` 已经拿**含记忆**的
|
|
365
|
+
// 那一份设过上下文,这里用裸提示再设一遍只会把记忆从上下文里抹掉,而请求读的又不是
|
|
366
|
+
// 上下文 —— 两处各设一次的结果是「上下文里没有、请求里也没有」。
|
|
359
367
|
|
|
360
368
|
// Seed inherited parent conversation (fork inheritance) as a byte-identical
|
|
361
369
|
// prefix so the provider prompt cache is reused.
|
|
@@ -405,8 +413,8 @@ export class SubAgent {
|
|
|
405
413
|
// 权限段随**唯一**带提示的那一轮走,而不是每轮派生。
|
|
406
414
|
const permissionBlock = buildPermissionBlock(gate.getMode())
|
|
407
415
|
let currentSystemPrompt = permissionBlock
|
|
408
|
-
? `${
|
|
409
|
-
:
|
|
416
|
+
? `${composedSystemPrompt}\n\n---\n\n${permissionBlock}`
|
|
417
|
+
: composedSystemPrompt
|
|
410
418
|
let totalTokens = 0
|
|
411
419
|
|
|
412
420
|
// Context for the sub-agent's own tool calls. Built once per run: the caller's
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mipham agents` 独立面板 —— 从 `index.tsx` 里抽出来的那段 JSX,好让它可测。
|
|
3
|
+
*
|
|
4
|
+
* 抽取的理由是**可测性**:`index.tsx` 是进程入口(`render` + `waitUntilExit`),
|
|
5
|
+
* 它里面传下去的东西在测试里够不着;而这条路上的 Enter 落点曾经整个缺席
|
|
6
|
+
* —— 页脚广告 attach,落点是 `() => {}`。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import React, { useState } from 'react'
|
|
10
|
+
|
|
11
|
+
import { AgentSessionView } from './session-view'
|
|
12
|
+
import { AgentViewDashboard } from './dashboard'
|
|
13
|
+
import type { AgentViewManager } from './agent-view-manager'
|
|
14
|
+
|
|
15
|
+
interface AgentsStandaloneProps {
|
|
16
|
+
manager: AgentViewManager
|
|
17
|
+
onExit: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AgentsStandalone({ manager, onExit }: AgentsStandaloneProps) {
|
|
21
|
+
// 与 `app.tsx` 同形:attach 把会话交给只读视图,视图里的 Esc 再交还列表。
|
|
22
|
+
// 页脚无条件印着「Enter attach」—— 它必须有个落点,否则按键有反应而世界不变。
|
|
23
|
+
const [attachedSessionId, setAttachedSessionId] = useState<string | null>(null)
|
|
24
|
+
|
|
25
|
+
if (attachedSessionId) {
|
|
26
|
+
return (
|
|
27
|
+
<AgentSessionView
|
|
28
|
+
manager={manager}
|
|
29
|
+
sessionId={attachedSessionId}
|
|
30
|
+
onDetach={() => setAttachedSessionId(null)}
|
|
31
|
+
/>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<AgentViewDashboard
|
|
37
|
+
manager={manager}
|
|
38
|
+
onAttach={(session) => setAttachedSessionId(session.id)}
|
|
39
|
+
onExit={onExit}
|
|
40
|
+
/>
|
|
41
|
+
)
|
|
42
|
+
}
|
package/src/core/autocomplete.ts
CHANGED
|
@@ -7,16 +7,39 @@ export const AUTOCOMPLETE_SYSTEM_PROMPT =
|
|
|
7
7
|
/** 带上最近几条对话(含待续写输入),供续写贴合上下文。 */
|
|
8
8
|
export const AUTOCOMPLETE_MAX_CONTEXT = 6
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* 每条上下文消息最多带这么多**字符**(保留尾部)。
|
|
12
|
+
*
|
|
13
|
+
* 上面那个常数限的是**条数**,而一条 `content` 可以任意长 —— 贴进来一个文件、
|
|
14
|
+
* 或一条长回复,6 条就是上万 token,而用户每次 >400ms 的停顿都要买一次。
|
|
15
|
+
* 续写要看的是「刚说到哪儿」,所以砍头留尾;加 `…` 是免得把片段读成消息开头。
|
|
16
|
+
* 每条封顶 + 条数封顶,总量就是封死的,不需要再维护第二个预算常数。
|
|
17
|
+
*/
|
|
18
|
+
export const AUTOCOMPLETE_MAX_CHARS_PER_MESSAGE = 2000
|
|
19
|
+
|
|
10
20
|
export interface RecentMessage {
|
|
11
21
|
role: 'user' | 'assistant'
|
|
12
22
|
content: string
|
|
13
23
|
}
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
function tailOf(content: string): string {
|
|
26
|
+
return content.length <= AUTOCOMPLETE_MAX_CHARS_PER_MESSAGE
|
|
27
|
+
? content
|
|
28
|
+
: '…' + content.slice(-AUTOCOMPLETE_MAX_CHARS_PER_MESSAGE)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** 拼续写请求:systemPrompt + 最近 N 条(每条限长)+ 当前输入作为待续写消息。 */
|
|
16
32
|
export function buildAutocompleteRequest(recent: RecentMessage[], input: string): ChatRequest {
|
|
17
33
|
return {
|
|
18
34
|
model: '', // falsy → registry 回退 active model
|
|
19
|
-
|
|
35
|
+
// 待续写的当前输入**不截断**:它是被续写的那条本身,且 extractCompletion 的
|
|
36
|
+
// 判据依赖它的完整值。上限落在历史消息上。
|
|
37
|
+
messages: [
|
|
38
|
+
...recent
|
|
39
|
+
.slice(-AUTOCOMPLETE_MAX_CONTEXT)
|
|
40
|
+
.map((m) => ({ role: m.role, content: tailOf(m.content) })),
|
|
41
|
+
{ role: 'user', content: input },
|
|
42
|
+
],
|
|
20
43
|
systemPrompt: AUTOCOMPLETE_SYSTEM_PROMPT,
|
|
21
44
|
temperature: 0,
|
|
22
45
|
maxTokens: 64,
|
|
@@ -57,6 +80,11 @@ export async function requestSuggestion(
|
|
|
57
80
|
const req = buildAutocompleteRequest(recent, input)
|
|
58
81
|
let text = ''
|
|
59
82
|
for await (const chunk of llm.chat(req)) {
|
|
83
|
+
// 用户又敲了一下 ⇒ 这条请求已经过期,当场走人。`break` 不只是「不再读」:
|
|
84
|
+
// 它触发生成器的 `.return()` ⇒ provider 的 `finally` ⇒ `reader.cancel()`,
|
|
85
|
+
// 连接当场释放。若把这一判挪到循环外,就等于**先把整条流读完**再丢掉结果 ——
|
|
86
|
+
// 那正是「取消不掉」:每次 >400ms 的停顿都买一个完整 completion。
|
|
87
|
+
if (isStale()) break
|
|
60
88
|
if (chunk.type === 'text' && chunk.content) text += chunk.content
|
|
61
89
|
}
|
|
62
90
|
if (isStale()) return null
|
package/src/core/context.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
deriveMessages,
|
|
9
9
|
assertModelVisible,
|
|
10
10
|
isAssertModelVisibleDebug,
|
|
11
|
+
closeInterruptedToolCalls,
|
|
11
12
|
} from './session-log'
|
|
12
13
|
|
|
13
14
|
export type Summarizer = (messages: Message[], heading: string) => Promise<string>
|
|
@@ -45,6 +46,12 @@ export class ContextManager {
|
|
|
45
46
|
* `index.tsx` 把它接到 live `PermissionSystem.getMode()` 上,于是切一次档,下一次请求就变。
|
|
46
47
|
*/
|
|
47
48
|
private permissionContextSource: (() => string) | null = null
|
|
49
|
+
/**
|
|
50
|
+
* 系统提示里的 **MCP instructions 段**同样是读时派生的,理由比权限段更硬:
|
|
51
|
+
* MCP server 是**启动后异步连上**的,而提示在 `setSystemPrompt()` 那一刻就建好了。
|
|
52
|
+
* 组装时烘进去的话,本次会话里后连上的 server 永远进不了提示 —— 用户只能重启。
|
|
53
|
+
*/
|
|
54
|
+
private mcpInstructionsSource: (() => string) | null = null
|
|
48
55
|
private estimatedTokens = 0
|
|
49
56
|
private checkpoints: Checkpoint[] = []
|
|
50
57
|
private checkpointCounter = 0
|
|
@@ -79,6 +86,12 @@ export class ContextManager {
|
|
|
79
86
|
/** 从已持久化的日志恢复:设 log 为源,messages 为投影(不重复写通)。 */
|
|
80
87
|
restoreLog(log: SessionLog): void {
|
|
81
88
|
this.log = log
|
|
89
|
+
// 盘上那份历史可能以一条**没有结果**的调用收尾(写盘写到一半被打断,末尾那行半截
|
|
90
|
+
// 结果被 `open()` 静默丢掉 —— 引擎自己的日志顺序到不了这个形状,见下面那个函数的
|
|
91
|
+
// 注释)。挂着 `tool_calls` 却没有结果回应的请求会被 provider 整条拒收,用户看到的
|
|
92
|
+
// 是「恢复之后第一句话就报协议错」。恢复的这一刻把它补**进日志**(不是补进投影,
|
|
93
|
+
// 否则「模型看得见的必须已记录」这条不变量当场破),见 `closeInterruptedToolCalls`。
|
|
94
|
+
closeInterruptedToolCalls(log)
|
|
82
95
|
this.messages = deriveMessages(log.events())
|
|
83
96
|
this.reEstimateTokens()
|
|
84
97
|
}
|
|
@@ -124,7 +137,11 @@ export class ContextManager {
|
|
|
124
137
|
|
|
125
138
|
setSystemPrompt(prompt: string): void {
|
|
126
139
|
this.systemPrompt = prompt
|
|
127
|
-
|
|
140
|
+
// 估值必须**含 messages** —— 走 `reEstimateTokens()` 这一条唯一推导,别在这儿手写
|
|
141
|
+
// 第二份。`--resume` 路径(`index.tsx:584-585`)先 `restoreLog()` 算出含消息的完整
|
|
142
|
+
// 估值,紧接着调这里;只按系统提示重算会把它**覆盖成偏低值** ⇒ `needsCompaction()`
|
|
143
|
+
// 长期偏 false ⇒ 压缩迟触发(上下文越滚越大才动手)。
|
|
144
|
+
this.reEstimateTokens()
|
|
128
145
|
}
|
|
129
146
|
|
|
130
147
|
/**
|
|
@@ -138,14 +155,28 @@ export class ContextManager {
|
|
|
138
155
|
}
|
|
139
156
|
|
|
140
157
|
/**
|
|
141
|
-
*
|
|
158
|
+
* 接线点(`index.tsx`):把已连 MCP server 自带的 `instructions` 接到提示上。
|
|
159
|
+
*
|
|
160
|
+
* 传 `null` 撤销。空串(无 server / 都没写 instructions)不产生空段。
|
|
161
|
+
*/
|
|
162
|
+
setMcpInstructionsSource(fn: (() => string) | null): void {
|
|
163
|
+
this.mcpInstructionsSource = fn
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 存储的提示 + 读时派生的段(权限 / MCP instructions)。
|
|
142
168
|
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
169
|
+
* 段尾追加(而非插回原来的中段位置)是刻意的:只切档、只连一个新 server 时
|
|
170
|
+
* **前缀保持不变**,提供方的 prefix cache 仍能命中到这些段之前的部分。
|
|
145
171
|
*/
|
|
146
172
|
private composedSystemPrompt(): string {
|
|
147
|
-
const
|
|
148
|
-
|
|
173
|
+
const blocks = [
|
|
174
|
+
this.permissionContextSource?.() ?? '',
|
|
175
|
+
this.mcpInstructionsSource?.() ?? '',
|
|
176
|
+
].filter((b) => b !== '')
|
|
177
|
+
return blocks.length > 0
|
|
178
|
+
? [this.systemPrompt, ...blocks].join('\n\n---\n\n')
|
|
179
|
+
: this.systemPrompt
|
|
149
180
|
}
|
|
150
181
|
|
|
151
182
|
getSystemPrompt(): string {
|
|
@@ -170,6 +201,30 @@ export class ContextManager {
|
|
|
170
201
|
this.checkCompression()
|
|
171
202
|
}
|
|
172
203
|
|
|
204
|
+
/**
|
|
205
|
+
* 注入一段**模型可见的上下文**(规则块 / compact 前后的 hook 提示 / stop hook 交回的话 /
|
|
206
|
+
* 后台 agent 的来件)。
|
|
207
|
+
*
|
|
208
|
+
* 为什么不复用 addMessage:那会把注入记成 `user/message`,在日志里与**用户真说过的话**
|
|
209
|
+
* 完全同形 —— 事后翻 session log 分不出哪一句是用户敲的、哪一句是我们塞进去的。记成
|
|
210
|
+
* `context/inject`(带 `source`)就带得出来源;而 `deriveMessages` 仍把它还原成同一个
|
|
211
|
+
* user 消息,所以**投影逐字节不变**,「model-visible means logged」照样成立。
|
|
212
|
+
*/
|
|
213
|
+
injectContext(source: string, text: string): void {
|
|
214
|
+
this.messages.push({ role: 'user', content: text })
|
|
215
|
+
this.estimatedTokens += this.estimateTokens(text)
|
|
216
|
+
|
|
217
|
+
if (this.log) {
|
|
218
|
+
this.log.append({ type: 'context/inject', at: Date.now(), source, text })
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (this.log && isAssertModelVisibleDebug()) {
|
|
222
|
+
assertModelVisible(this.log.events(), this.messages)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.checkCompression()
|
|
226
|
+
}
|
|
227
|
+
|
|
173
228
|
/** 记录工具执行结果(全量 ToolResult 含 success/error)到日志,并写投影消息(不重复走 messageToEvents 拆分)。 */
|
|
174
229
|
addToolResult(toolUseId: string, result: ToolResult): void {
|
|
175
230
|
const content = result.success ? result.content : result.error || result.content
|
package/src/core/dream-engine.ts
CHANGED
|
@@ -521,7 +521,11 @@ export class DreamEngine {
|
|
|
521
521
|
|
|
522
522
|
let log: Array<{ timestamp: string; actions: DreamAction[] }> = []
|
|
523
523
|
if (existsSync(this.dreamLog)) {
|
|
524
|
-
|
|
524
|
+
// 形状门:`{"a":1}` 是**合法 JSON**,不抛 —— 而 `log.unshift` 会抛,被下面的
|
|
525
|
+
// `catch` 吞掉 ⇒ 这一轮梦白做、且用户看不到任何提示。退成空表继续,
|
|
526
|
+
// 让这一轮活下来,别让它给一个坏文件陪葬。
|
|
527
|
+
const parsed: unknown = JSON.parse(readFileSync(this.dreamLog, 'utf-8'))
|
|
528
|
+
if (Array.isArray(parsed)) log = parsed
|
|
525
529
|
}
|
|
526
530
|
log.unshift({ timestamp, actions })
|
|
527
531
|
// Keep last 10 dream cycles
|
|
@@ -536,7 +540,18 @@ export class DreamEngine {
|
|
|
536
540
|
getDreamHistory(): Array<{ timestamp: string; actions: DreamAction[] }> {
|
|
537
541
|
try {
|
|
538
542
|
if (!existsSync(this.dreamLog)) return []
|
|
539
|
-
|
|
543
|
+
const parsed: unknown = JSON.parse(readFileSync(this.dreamLog, 'utf-8'))
|
|
544
|
+
// 声明返回数组,就必须真的是数组:`{"a":1}` 合法且不抛,而调用方
|
|
545
|
+
// (`ui/commands.ts` 的 `/dream --status`)紧接着 `.length` / `.slice` /
|
|
546
|
+
// `entry.actions.length` ⇒ `try` 护不住调用点,TypeError 直接冒到 UI。
|
|
547
|
+
if (!Array.isArray(parsed)) return []
|
|
548
|
+
return parsed.filter(
|
|
549
|
+
(e): e is { timestamp: string; actions: DreamAction[] } =>
|
|
550
|
+
!!e &&
|
|
551
|
+
typeof e === 'object' &&
|
|
552
|
+
typeof e.timestamp === 'string' &&
|
|
553
|
+
Array.isArray(e.actions),
|
|
554
|
+
)
|
|
540
555
|
} catch {
|
|
541
556
|
return []
|
|
542
557
|
}
|