minimal-agent 0.11.0 → 0.12.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/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { SessionState } from '../../session/sessionState.js';
|
|
4
|
+
import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
|
|
5
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
readRuntimeJson,
|
|
9
|
+
sessionRuntimeDir,
|
|
10
|
+
writeRuntimeJson,
|
|
11
|
+
} from '../../session/runtimeStore.js';
|
|
12
|
+
import {
|
|
13
|
+
applyAgentWorktree,
|
|
14
|
+
commitWorktreeChanges,
|
|
15
|
+
createAgentWorktree,
|
|
16
|
+
hasWorktreeChanges,
|
|
17
|
+
removeAgentWorktree,
|
|
18
|
+
} from '../agent/worktree.js';
|
|
19
|
+
|
|
20
|
+
const enterSchema = z.object({
|
|
21
|
+
name: z
|
|
22
|
+
.string()
|
|
23
|
+
.regex(/^[a-zA-Z0-9._-]{1,48}$/)
|
|
24
|
+
.optional(),
|
|
25
|
+
});
|
|
26
|
+
const exitSchema = z.object({ action: z.enum(['apply', 'keep', 'discard']) });
|
|
27
|
+
async function persist(
|
|
28
|
+
ctx: ToolCallContext,
|
|
29
|
+
state: SessionState['activeWorktree'],
|
|
30
|
+
) {
|
|
31
|
+
await writeRuntimeJson(join(sessionRuntimeDir(ctx), 'worktree.json'), {
|
|
32
|
+
active: state ?? null,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
export async function hydrateSessionWorktree(
|
|
36
|
+
ctx: ToolCallContext,
|
|
37
|
+
): Promise<void> {
|
|
38
|
+
if (!ctx.sessionState || ctx.sessionState.activeWorktree) return;
|
|
39
|
+
const saved = await readRuntimeJson<{
|
|
40
|
+
active?: NonNullable<SessionState['activeWorktree']> | null;
|
|
41
|
+
}>(join(sessionRuntimeDir(ctx), 'worktree.json'), {});
|
|
42
|
+
if (saved.active) ctx.sessionState.activeWorktree = saved.active;
|
|
43
|
+
}
|
|
44
|
+
async function enter(
|
|
45
|
+
input: z.infer<typeof enterSchema>,
|
|
46
|
+
ctx: ToolCallContext,
|
|
47
|
+
): Promise<ToolResult> {
|
|
48
|
+
const state = ctx.sessionState;
|
|
49
|
+
if (!state) return { ok: false, error: 'EnterWorktree 需要会话状态' };
|
|
50
|
+
if (state.activeWorktree)
|
|
51
|
+
return {
|
|
52
|
+
ok: true,
|
|
53
|
+
content: `当前已在 worktree ${state.activeWorktree.path}`,
|
|
54
|
+
};
|
|
55
|
+
const slug = `session-${input.name ?? randomUUID().slice(0, 8)}`;
|
|
56
|
+
const wt = await createAgentWorktree(slug, ctx.cwd);
|
|
57
|
+
if (!wt.ok) return wt;
|
|
58
|
+
state.activeWorktree = {
|
|
59
|
+
id: slug,
|
|
60
|
+
path: wt.path,
|
|
61
|
+
branch: wt.branch,
|
|
62
|
+
baseSha: wt.baseSha,
|
|
63
|
+
gitRoot: wt.gitRoot,
|
|
64
|
+
};
|
|
65
|
+
await persist(ctx, state.activeWorktree);
|
|
66
|
+
return {
|
|
67
|
+
ok: true,
|
|
68
|
+
content: `已进入主 Agent 隔离 worktree:${wt.path}\n分支:${wt.branch}\n后续工具调用立即使用该目录。`,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
async function exit(
|
|
72
|
+
input: z.infer<typeof exitSchema>,
|
|
73
|
+
ctx: ToolCallContext,
|
|
74
|
+
): Promise<ToolResult> {
|
|
75
|
+
const state = ctx.sessionState,
|
|
76
|
+
wt = state?.activeWorktree;
|
|
77
|
+
if (!state || !wt) return { ok: false, error: '当前会话没有活动 worktree' };
|
|
78
|
+
if (input.action === 'keep') {
|
|
79
|
+
state.activeWorktree = undefined;
|
|
80
|
+
await persist(ctx, undefined);
|
|
81
|
+
return {
|
|
82
|
+
ok: true,
|
|
83
|
+
content: `已退出有效 cwd,但保留 worktree:${wt.path}\n分支:${wt.branch}`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const changed = await hasWorktreeChanges(wt.path, wt.baseSha);
|
|
87
|
+
if (input.action === 'discard' && changed) {
|
|
88
|
+
const committed = await commitWorktreeChanges(
|
|
89
|
+
wt.path,
|
|
90
|
+
`archive: preserve discarded session ${wt.id}`,
|
|
91
|
+
);
|
|
92
|
+
if (committed.ok) {
|
|
93
|
+
state.activeWorktree = undefined;
|
|
94
|
+
await persist(ctx, undefined);
|
|
95
|
+
}
|
|
96
|
+
return committed.ok
|
|
97
|
+
? {
|
|
98
|
+
ok: true,
|
|
99
|
+
content: `检测到成果,未永久删除。已归档到分支 ${wt.branch},worktree 保留在 ${wt.path}。`,
|
|
100
|
+
}
|
|
101
|
+
: {
|
|
102
|
+
ok: false,
|
|
103
|
+
error: `无法安全归档,worktree 保持活动:${committed.error}`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (input.action === 'discard') {
|
|
107
|
+
await removeAgentWorktree(wt.path, wt.branch, wt.gitRoot);
|
|
108
|
+
state.activeWorktree = undefined;
|
|
109
|
+
await persist(ctx, undefined);
|
|
110
|
+
return {
|
|
111
|
+
ok: true,
|
|
112
|
+
content: 'worktree 无改动,已通过 Git worktree 管理安全清理。',
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const committed = await commitWorktreeChanges(
|
|
116
|
+
wt.path,
|
|
117
|
+
`minimal-agent: apply session ${wt.id}`,
|
|
118
|
+
);
|
|
119
|
+
if (!committed.ok)
|
|
120
|
+
return {
|
|
121
|
+
ok: false,
|
|
122
|
+
error: `提交隔离改动失败,worktree 保持活动:${committed.error}`,
|
|
123
|
+
};
|
|
124
|
+
const applied = await applyAgentWorktree(wt.gitRoot, wt.id);
|
|
125
|
+
if (!applied.ok)
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: `应用检查失败,worktree 保持活动:${applied.error}`,
|
|
129
|
+
};
|
|
130
|
+
state.activeWorktree = undefined;
|
|
131
|
+
await persist(ctx, undefined);
|
|
132
|
+
return {
|
|
133
|
+
ok: true,
|
|
134
|
+
content: `已通过 git apply --check 并将变更应用到原工作区;未自动提交。`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export const enterWorktreeTool: Tool<z.infer<typeof enterSchema>> = {
|
|
138
|
+
name: 'EnterWorktree',
|
|
139
|
+
description:
|
|
140
|
+
'Creates and enters a session-scoped Git worktree for the main Agent. Later tool calls use it immediately while project memory keeps the original workspace identity.',
|
|
141
|
+
inputSchema: enterSchema,
|
|
142
|
+
parameters: toToolParameters(enterSchema),
|
|
143
|
+
isReadOnly: false,
|
|
144
|
+
isConcurrencySafe: false,
|
|
145
|
+
call: async () => ({ ok: false, error: 'requires session context' }),
|
|
146
|
+
callWithContext: (i, _s, c) => enter(i, c),
|
|
147
|
+
};
|
|
148
|
+
export const exitWorktreeTool: Tool<z.infer<typeof exitSchema>> = {
|
|
149
|
+
name: 'ExitWorktree',
|
|
150
|
+
description:
|
|
151
|
+
'Leaves the current session worktree. apply checks and applies its patch to the original workspace; keep preserves it; discard never destroys uncommitted results and archives changed work.',
|
|
152
|
+
inputSchema: exitSchema,
|
|
153
|
+
parameters: toToolParameters(exitSchema),
|
|
154
|
+
isReadOnly: false,
|
|
155
|
+
isConcurrencySafe: false,
|
|
156
|
+
call: async () => ({ ok: false, error: 'requires session context' }),
|
|
157
|
+
callWithContext: (i, _s, c) => exit(i, c),
|
|
158
|
+
};
|
package/src/tools/write/write.js
CHANGED
|
@@ -22,6 +22,7 @@ import { filePathField } from '../shared/schemas.js';
|
|
|
22
22
|
import { assertFresh, hashFileBytes, recordRead } from '../shared/fileState.js';
|
|
23
23
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
24
24
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
25
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
25
26
|
/** 写入内容上限:1GB。防止模型试图写入超大内容撑爆内存/磁盘。 */
|
|
26
27
|
const MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
27
28
|
// ---------------- 1. Zod 输入 schema ----------------
|
|
@@ -69,9 +70,11 @@ async function call(input, state, cwd) {
|
|
|
69
70
|
catch { }
|
|
70
71
|
}
|
|
71
72
|
let contentToWrite = input.content;
|
|
73
|
+
let originalContent = '';
|
|
72
74
|
let bomEncoding = 'utf8';
|
|
73
75
|
if (fileExisted) {
|
|
74
76
|
const snapshot = await readTextFile(filePath);
|
|
77
|
+
originalContent = snapshot.content;
|
|
75
78
|
bomEncoding = snapshot.encoding;
|
|
76
79
|
contentToWrite = applyLineEnding(input.content, snapshot.lineEnding);
|
|
77
80
|
}
|
|
@@ -88,10 +91,11 @@ async function call(input, state, cwd) {
|
|
|
88
91
|
const bomInfo = bomEncoding === 'utf8-bom'
|
|
89
92
|
? ',已保留 UTF-8 BOM'
|
|
90
93
|
: bomEncoding === 'utf16le' ? ',已保留 UTF-16 LE' : '';
|
|
94
|
+
const patch = buildPatchSummary(originalContent, contentToWrite, input.file_path);
|
|
91
95
|
return {
|
|
92
96
|
ok: true,
|
|
93
|
-
content: `${action} ${filePath}${sizeInfo}${bomInfo}`,
|
|
94
|
-
metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd),
|
|
97
|
+
content: `${action} ${filePath}${sizeInfo}${bomInfo}\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
98
|
+
metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd, patch),
|
|
95
99
|
};
|
|
96
100
|
}
|
|
97
101
|
catch (e) {
|
package/src/tools/write/write.ts
CHANGED
|
@@ -31,6 +31,7 @@ import { filePathField } from '../shared/schemas.js';
|
|
|
31
31
|
import { assertFresh, hashFileBytes, recordRead, type FileReadState } from '../shared/fileState.js';
|
|
32
32
|
import { fileActivityMetadata } from '../shared/fileActivity.js';
|
|
33
33
|
import { emitFileMutation } from '../shared/fileMutationBus.js';
|
|
34
|
+
import { buildPatchSummary } from '../shared/diffSummary.js';
|
|
34
35
|
|
|
35
36
|
/** 写入内容上限:1GB。防止模型试图写入超大内容撑爆内存/磁盘。 */
|
|
36
37
|
const MAX_WRITE_SIZE_BYTES = 1024 * 1024 * 1024;
|
|
@@ -86,9 +87,11 @@ async function call(input: WriteInput, state?: FileReadState, cwd?: string): Pro
|
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
let contentToWrite = input.content;
|
|
90
|
+
let originalContent = '';
|
|
89
91
|
let bomEncoding: Awaited<ReturnType<typeof readTextFile>>['encoding'] = 'utf8';
|
|
90
92
|
if (fileExisted) {
|
|
91
93
|
const snapshot = await readTextFile(filePath);
|
|
94
|
+
originalContent = snapshot.content;
|
|
92
95
|
bomEncoding = snapshot.encoding;
|
|
93
96
|
contentToWrite = applyLineEnding(input.content, snapshot.lineEnding);
|
|
94
97
|
}
|
|
@@ -106,11 +109,12 @@ async function call(input: WriteInput, state?: FileReadState, cwd?: string): Pro
|
|
|
106
109
|
const bomInfo = bomEncoding === 'utf8-bom'
|
|
107
110
|
? ',已保留 UTF-8 BOM'
|
|
108
111
|
: bomEncoding === 'utf16le' ? ',已保留 UTF-16 LE' : '';
|
|
112
|
+
const patch = buildPatchSummary(originalContent, contentToWrite, input.file_path);
|
|
109
113
|
|
|
110
114
|
return {
|
|
111
115
|
ok: true,
|
|
112
|
-
content: `${action} ${filePath}${sizeInfo}${bomInfo}`,
|
|
113
|
-
metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd),
|
|
116
|
+
content: `${action} ${filePath}${sizeInfo}${bomInfo}\n${patch.unifiedDiff ?? '[diff omitted: too large]'}`,
|
|
117
|
+
metadata: await fileActivityMetadata(filePath, fileExisted ? 'modified' : 'created', 'not_run', 'support', cwd, patch),
|
|
114
118
|
};
|
|
115
119
|
} catch (e) {
|
|
116
120
|
return { ok: false, error: `写入失败:${(e as Error).message}` };
|
package/src/types.ts
CHANGED
|
@@ -191,6 +191,8 @@ export interface AgentResultMetadata {
|
|
|
191
191
|
kind: 'agent';
|
|
192
192
|
agentType: 'explore' | 'general';
|
|
193
193
|
description: string;
|
|
194
|
+
agentId?: string;
|
|
195
|
+
status?: 'running' | 'completed' | 'failed' | 'stopped' | 'interrupted';
|
|
194
196
|
isolation: 'none' | 'worktree';
|
|
195
197
|
turns: number;
|
|
196
198
|
toolCalls: number;
|
|
@@ -214,6 +216,15 @@ export interface AgentResultMetadata {
|
|
|
214
216
|
export type FileActivityKind = 'read' | 'created' | 'modified' | 'generated' | 'deleted';
|
|
215
217
|
export type FileVerification = 'passed' | 'failed' | 'not_run';
|
|
216
218
|
export type ConversationFilePurpose = 'source' | 'deliverable' | 'support';
|
|
219
|
+
export interface FilePatchSummary {
|
|
220
|
+
unifiedDiff?: string;
|
|
221
|
+
beforeHash?: string;
|
|
222
|
+
afterHash: string;
|
|
223
|
+
additions: number;
|
|
224
|
+
deletions: number;
|
|
225
|
+
truncated: boolean;
|
|
226
|
+
omittedReason?: 'too_large';
|
|
227
|
+
}
|
|
217
228
|
|
|
218
229
|
/** Structured local file output. Paths are always workspace-relative. */
|
|
219
230
|
export interface FileActivityMetadata {
|
|
@@ -225,6 +236,7 @@ export interface FileActivityMetadata {
|
|
|
225
236
|
mime?: string;
|
|
226
237
|
verification: FileVerification;
|
|
227
238
|
purpose: ConversationFilePurpose;
|
|
239
|
+
patch?: FilePatchSummary;
|
|
228
240
|
}>;
|
|
229
241
|
}
|
|
230
242
|
|
package/src/ui/hooks/useChat.js
CHANGED
|
@@ -27,6 +27,7 @@ import { resolveSessionFile, sessionFileFor, } from '../../context/sessionPath.j
|
|
|
27
27
|
import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../../prompts/system.js';
|
|
28
28
|
import { createPromptCacheContext } from '../../llm/promptCache.js';
|
|
29
29
|
import { createAgentRuntimeContext, filterToolsForRuntime, memoryContextFromRuntime, } from '../../runtime/agentRuntimeContext.js';
|
|
30
|
+
import { startTurnJournal } from '../../memory/turnJournal.js';
|
|
30
31
|
import { ALL_TOOLS, getToolByName } from '../../tools/index.js';
|
|
31
32
|
import { createInputQueue } from '../../messageQueue.js';
|
|
32
33
|
import { createAskCoordinator, setAskHandler, } from '../../askBridge.js';
|
|
@@ -34,6 +35,7 @@ import { createAskSlot } from './askSlot.js';
|
|
|
34
35
|
import { figures } from '../figures.js';
|
|
35
36
|
import { clearFileState } from '../../tools/shared/fileState.js';
|
|
36
37
|
import { getOrCreateSessionState, sessionStateKey } from '../../session/sessionState.js';
|
|
38
|
+
import { hydrateSessionWorktree } from '../../tools/worktree/sessionWorktree.js';
|
|
37
39
|
import { runAdvisor } from '../../tools/advisor/advisor.js';
|
|
38
40
|
import { runWithPlugins } from '../../plugins/pluginRunner.js';
|
|
39
41
|
import { dlog } from '../../utils/debugLog.js';
|
|
@@ -394,6 +396,8 @@ export function useChat(args) {
|
|
|
394
396
|
const ac = new AbortController();
|
|
395
397
|
abortRef.current = ac;
|
|
396
398
|
let releaseContextLock = null;
|
|
399
|
+
let turnJournal = null;
|
|
400
|
+
let turnJournalStart = 0;
|
|
397
401
|
dlog('ui', 'turn START', { input: trimmed.slice(0, 80) });
|
|
398
402
|
// 长时间运行时(如工作流 agent 步骤)的定时 token 刷新:
|
|
399
403
|
// runQuery 的 text/reasoning 事件不触发 bump,导致 StatusLine token 数
|
|
@@ -441,10 +445,20 @@ export function useChat(args) {
|
|
|
441
445
|
})
|
|
442
446
|
: undefined;
|
|
443
447
|
const toolGate = computerRun ? createComputerUseHostGate(computerRun) : undefined;
|
|
448
|
+
const turnSessionState = getOrCreateSessionState(sessionStateKey({
|
|
449
|
+
surface: 'tui', workspaceId: workspace?.id,
|
|
450
|
+
cwd: workspace?.id ? undefined : getWorkingDir(),
|
|
451
|
+
sessionId: activeSessionIdRef.current,
|
|
452
|
+
}));
|
|
453
|
+
await hydrateSessionWorktree({
|
|
454
|
+
sessionState: turnSessionState, workspaceId: workspace?.id,
|
|
455
|
+
sessionId: activeSessionIdRef.current, cwd: getWorkingDir(),
|
|
456
|
+
});
|
|
457
|
+
const effectiveCwd = turnSessionState.activeWorktree?.path ?? getWorkingDir();
|
|
444
458
|
const runtimeContext = createAgentRuntimeContext({
|
|
445
459
|
surface: 'tui',
|
|
446
460
|
profile: 'main',
|
|
447
|
-
cwd:
|
|
461
|
+
cwd: effectiveCwd,
|
|
448
462
|
workspaceId: workspace?.id,
|
|
449
463
|
sessionId: activeSessionIdRef.current,
|
|
450
464
|
owner: 'local-owner',
|
|
@@ -455,9 +469,9 @@ export function useChat(args) {
|
|
|
455
469
|
toolGate,
|
|
456
470
|
});
|
|
457
471
|
const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
|
|
458
|
-
const toolSignature = turnTools.map((tool) => tool.name).join(',')
|
|
472
|
+
const toolSignature = `${effectiveCwd}\0${turnTools.map((tool) => tool.name).join(',')}`;
|
|
459
473
|
if (promptToolSignatureRef.current !== toolSignature) {
|
|
460
|
-
const stablePrompt = await buildBaseSystemPrompt(
|
|
474
|
+
const stablePrompt = await buildBaseSystemPrompt(effectiveCwd, turnTools);
|
|
461
475
|
const rest = historyRef.current[0]?.role === 'system'
|
|
462
476
|
? historyRef.current.slice(1)
|
|
463
477
|
: [...historyRef.current];
|
|
@@ -467,30 +481,33 @@ export function useChat(args) {
|
|
|
467
481
|
}
|
|
468
482
|
// 每回合只刷新易变层;稳定层仅在实际能力目录变化时重建。
|
|
469
483
|
const turnTransient = await buildTransientPromptBundle({
|
|
470
|
-
cwd:
|
|
484
|
+
cwd: effectiveCwd,
|
|
471
485
|
tools: turnTools,
|
|
472
486
|
runtime: {
|
|
473
487
|
surface: 'tui',
|
|
474
|
-
cwd:
|
|
488
|
+
cwd: effectiveCwd,
|
|
475
489
|
currentUserInput: trimmed,
|
|
476
490
|
workspaceId: workspace?.id,
|
|
491
|
+
sessionId: activeSessionIdRef.current,
|
|
477
492
|
interactiveAsk: true,
|
|
478
493
|
supervisedBrowser: false,
|
|
479
494
|
memoryAccess: 'read-write',
|
|
480
495
|
},
|
|
481
496
|
}).catch(() => undefined);
|
|
497
|
+
// 会话原文记账(与 session/runTurn.ts 共用同一份实现)。
|
|
498
|
+
// TUI 不经过 runTurn,此前整条路径都不入库,SearchConversations 对 TUI 用户静默失效。
|
|
499
|
+
turnJournalStart = historyRef.current.length;
|
|
500
|
+
turnJournal = await startTurnJournal({
|
|
501
|
+
memoryContext: memoryContextFromRuntime(runtimeContext),
|
|
502
|
+
userInput: trimmed,
|
|
503
|
+
}).catch(() => null);
|
|
482
504
|
for await (const ev of runWithPlugins(trimmed, {
|
|
483
505
|
provider: providerRef.current,
|
|
484
506
|
history: historyRef.current,
|
|
485
507
|
signal: ac.signal,
|
|
486
508
|
runtimeContext,
|
|
487
509
|
allowedTools: turnTools.map((tool) => tool.name),
|
|
488
|
-
sessionState:
|
|
489
|
-
surface: 'tui',
|
|
490
|
-
workspaceId: workspace?.id,
|
|
491
|
-
cwd: workspace?.id ? undefined : getWorkingDir(),
|
|
492
|
-
sessionId: activeSessionIdRef.current,
|
|
493
|
-
})),
|
|
510
|
+
sessionState: turnSessionState,
|
|
494
511
|
parseInlineImages: true,
|
|
495
512
|
...(turnTransient ? { transientBundle: turnTransient } : {}),
|
|
496
513
|
promptCache: createPromptCacheContext(`workspace:${workspace?.id ?? getWorkingDir()}:session:${activeSessionIdRef.current}`),
|
|
@@ -558,6 +575,9 @@ export function useChat(args) {
|
|
|
558
575
|
}
|
|
559
576
|
activeComputerRunRef.current = null;
|
|
560
577
|
runningToolsRef.current.clear();
|
|
578
|
+
// 记账失败不影响落盘与解锁(它是附加能力,不是功能)。
|
|
579
|
+
await turnJournal?.finish(historyRef.current, turnJournalStart).catch(() => undefined);
|
|
580
|
+
turnJournal = null;
|
|
561
581
|
// v2 multi-session:持久化到当前 active session 文件。
|
|
562
582
|
try {
|
|
563
583
|
await saveContext(historyRef.current, sessionFileFor(getWorkingDir(), activeSessionIdRef.current));
|
package/src/ui/hooks/useChat.ts
CHANGED
|
@@ -45,6 +45,7 @@ import {
|
|
|
45
45
|
filterToolsForRuntime,
|
|
46
46
|
memoryContextFromRuntime,
|
|
47
47
|
} from '../../runtime/agentRuntimeContext.js';
|
|
48
|
+
import { startTurnJournal, type TurnJournal } from '../../memory/turnJournal.js';
|
|
48
49
|
import { ALL_TOOLS, getToolByName } from '../../tools/index.js';
|
|
49
50
|
import { createInputQueue } from '../../messageQueue.js';
|
|
50
51
|
import {
|
|
@@ -58,6 +59,7 @@ import { createAskSlot } from './askSlot.js';
|
|
|
58
59
|
import { figures } from '../figures.js';
|
|
59
60
|
import { clearFileState } from '../../tools/shared/fileState.js';
|
|
60
61
|
import { getOrCreateSessionState, sessionStateKey } from '../../session/sessionState.js';
|
|
62
|
+
import { hydrateSessionWorktree } from '../../tools/worktree/sessionWorktree.js';
|
|
61
63
|
import type {
|
|
62
64
|
LoopEvent,
|
|
63
65
|
Message,
|
|
@@ -627,6 +629,8 @@ export function useChat(args: {
|
|
|
627
629
|
const ac = new AbortController();
|
|
628
630
|
abortRef.current = ac;
|
|
629
631
|
let releaseContextLock: (() => Promise<void>) | null = null;
|
|
632
|
+
let turnJournal: TurnJournal | null = null;
|
|
633
|
+
let turnJournalStart = 0;
|
|
630
634
|
dlog('ui', 'turn START', { input: trimmed.slice(0, 80) });
|
|
631
635
|
|
|
632
636
|
// 长时间运行时(如工作流 agent 步骤)的定时 token 刷新:
|
|
@@ -681,10 +685,20 @@ export function useChat(args: {
|
|
|
681
685
|
})
|
|
682
686
|
: undefined;
|
|
683
687
|
const toolGate = computerRun ? createComputerUseHostGate(computerRun) : undefined;
|
|
688
|
+
const turnSessionState = getOrCreateSessionState(sessionStateKey({
|
|
689
|
+
surface: 'tui', workspaceId: workspace?.id,
|
|
690
|
+
cwd: workspace?.id ? undefined : getWorkingDir(),
|
|
691
|
+
sessionId: activeSessionIdRef.current,
|
|
692
|
+
}));
|
|
693
|
+
await hydrateSessionWorktree({
|
|
694
|
+
sessionState: turnSessionState, workspaceId: workspace?.id,
|
|
695
|
+
sessionId: activeSessionIdRef.current, cwd: getWorkingDir(),
|
|
696
|
+
});
|
|
697
|
+
const effectiveCwd = turnSessionState.activeWorktree?.path ?? getWorkingDir();
|
|
684
698
|
const runtimeContext = createAgentRuntimeContext({
|
|
685
699
|
surface: 'tui',
|
|
686
700
|
profile: 'main',
|
|
687
|
-
cwd:
|
|
701
|
+
cwd: effectiveCwd,
|
|
688
702
|
workspaceId: workspace?.id,
|
|
689
703
|
sessionId: activeSessionIdRef.current,
|
|
690
704
|
owner: 'local-owner',
|
|
@@ -695,9 +709,9 @@ export function useChat(args: {
|
|
|
695
709
|
toolGate,
|
|
696
710
|
});
|
|
697
711
|
const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
|
|
698
|
-
const toolSignature = turnTools.map((tool) => tool.name).join(',')
|
|
712
|
+
const toolSignature = `${effectiveCwd}\0${turnTools.map((tool) => tool.name).join(',')}`;
|
|
699
713
|
if (promptToolSignatureRef.current !== toolSignature) {
|
|
700
|
-
const stablePrompt = await buildBaseSystemPrompt(
|
|
714
|
+
const stablePrompt = await buildBaseSystemPrompt(effectiveCwd, turnTools);
|
|
701
715
|
const rest = historyRef.current[0]?.role === 'system'
|
|
702
716
|
? historyRef.current.slice(1)
|
|
703
717
|
: [...historyRef.current];
|
|
@@ -707,32 +721,33 @@ export function useChat(args: {
|
|
|
707
721
|
}
|
|
708
722
|
// 每回合只刷新易变层;稳定层仅在实际能力目录变化时重建。
|
|
709
723
|
const turnTransient = await buildTransientPromptBundle({
|
|
710
|
-
cwd:
|
|
724
|
+
cwd: effectiveCwd,
|
|
711
725
|
tools: turnTools,
|
|
712
726
|
runtime: {
|
|
713
727
|
surface: 'tui',
|
|
714
|
-
cwd:
|
|
728
|
+
cwd: effectiveCwd,
|
|
715
729
|
currentUserInput: trimmed,
|
|
716
730
|
workspaceId: workspace?.id,
|
|
731
|
+
sessionId: activeSessionIdRef.current,
|
|
717
732
|
interactiveAsk: true,
|
|
718
733
|
supervisedBrowser: false,
|
|
719
734
|
memoryAccess: 'read-write',
|
|
720
735
|
},
|
|
721
736
|
}).catch(() => undefined);
|
|
737
|
+
// 会话原文记账(与 session/runTurn.ts 共用同一份实现)。
|
|
738
|
+
// TUI 不经过 runTurn,此前整条路径都不入库,SearchConversations 对 TUI 用户静默失效。
|
|
739
|
+
turnJournalStart = historyRef.current.length;
|
|
740
|
+
turnJournal = await startTurnJournal({
|
|
741
|
+
memoryContext: memoryContextFromRuntime(runtimeContext),
|
|
742
|
+
userInput: trimmed,
|
|
743
|
+
}).catch(() => null);
|
|
722
744
|
for await (const ev of runWithPlugins(trimmed, {
|
|
723
745
|
provider: providerRef.current,
|
|
724
746
|
history: historyRef.current,
|
|
725
747
|
signal: ac.signal,
|
|
726
748
|
runtimeContext,
|
|
727
749
|
allowedTools: turnTools.map((tool) => tool.name),
|
|
728
|
-
sessionState:
|
|
729
|
-
sessionStateKey({
|
|
730
|
-
surface: 'tui',
|
|
731
|
-
workspaceId: workspace?.id,
|
|
732
|
-
cwd: workspace?.id ? undefined : getWorkingDir(),
|
|
733
|
-
sessionId: activeSessionIdRef.current,
|
|
734
|
-
}),
|
|
735
|
-
),
|
|
750
|
+
sessionState: turnSessionState,
|
|
736
751
|
parseInlineImages: true,
|
|
737
752
|
...(turnTransient ? { transientBundle: turnTransient } : {}),
|
|
738
753
|
promptCache: createPromptCacheContext(
|
|
@@ -800,6 +815,9 @@ export function useChat(args: {
|
|
|
800
815
|
}
|
|
801
816
|
activeComputerRunRef.current = null;
|
|
802
817
|
runningToolsRef.current.clear();
|
|
818
|
+
// 记账失败不影响落盘与解锁(它是附加能力,不是功能)。
|
|
819
|
+
await turnJournal?.finish(historyRef.current, turnJournalStart).catch(() => undefined);
|
|
820
|
+
turnJournal = null;
|
|
803
821
|
// v2 multi-session:持久化到当前 active session 文件。
|
|
804
822
|
try {
|
|
805
823
|
await saveContext(historyRef.current, sessionFileFor(getWorkingDir(), activeSessionIdRef.current));
|
package/src/web/protocol.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import type { AskAnswer, AskOption, AskQuestionSpec, AskSource } from '../askBridge.js';
|
|
21
21
|
import type { PluginEvent } from '../plugins/pluginApi.js';
|
|
22
|
-
import type { AgentResultMetadata, ConversationFilePurpose, FileActivityMetadata, LoopEvent, Message, MessageAttachmentRef, MessageBrowserReference, ProgressStage, SubagentProgress, ToolCall, ToolProgressDetail, ToolResultMetadata } from '../types.js';
|
|
22
|
+
import type { AgentResultMetadata, ConversationFilePurpose, DeliverableManifest, FileActivityMetadata, LoopEvent, Message, MessageAttachmentRef, MessageBrowserReference, ProgressStage, SubagentProgress, ToolCall, ToolProgressDetail, ToolResultMetadata } from '../types.js';
|
|
23
23
|
import type { ConversationSearchResult, MemoryCategory, MemoryDiagnostic, MemoryEditablePatch, MemoryRecord, MemorySettings, MemoryUpdate, PersonaDocument } from '../memory/memoryService.js';
|
|
24
24
|
import type { HumanInterventionReason } from '../humanIntervention.js';
|
|
25
25
|
|
|
@@ -1067,7 +1067,7 @@ export interface GroupMessageAttachment {
|
|
|
1067
1067
|
size: number;
|
|
1068
1068
|
}
|
|
1069
1069
|
|
|
1070
|
-
export type ConversationFileKind = 'workspace-file' | 'group-attachment' | 'agent-file';
|
|
1070
|
+
export type ConversationFileKind = 'workspace-file' | 'user-attachment' | 'group-attachment' | 'agent-file';
|
|
1071
1071
|
export type ConversationFileActivity = 'referenced' | 'read' | 'created' | 'modified' | 'generated' | 'deleted';
|
|
1072
1072
|
|
|
1073
1073
|
/** Safe metadata an Agent Node may publish to a group. Absolute paths and file bytes are forbidden. */
|
|
@@ -1105,6 +1105,65 @@ export interface ConversationFileRef {
|
|
|
1105
1105
|
sha256?: string;
|
|
1106
1106
|
modifiedAt?: number;
|
|
1107
1107
|
contentChanged?: boolean;
|
|
1108
|
+
originalAvailable?: boolean;
|
|
1109
|
+
archivedAvailable?: boolean;
|
|
1110
|
+
previewSource?: 'original' | 'archive' | 'upload';
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/** A durable, turn-scoped formal delivery. File refs are resolved at read time. */
|
|
1114
|
+
export interface ConversationDelivery {
|
|
1115
|
+
id: string;
|
|
1116
|
+
turnId: string;
|
|
1117
|
+
toolCallId?: string;
|
|
1118
|
+
createdAt: number;
|
|
1119
|
+
manifest: DeliverableManifest;
|
|
1120
|
+
resolvedFiles: ConversationFileRef[];
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
/** Durable user-supplied and formally delivered artifacts for one user turn. */
|
|
1124
|
+
export interface ConversationArtifactGroup {
|
|
1125
|
+
turnId: string;
|
|
1126
|
+
createdAt: number;
|
|
1127
|
+
compacted: boolean;
|
|
1128
|
+
userAttachments: MessageAttachmentRef[];
|
|
1129
|
+
deliveries: ConversationDelivery[];
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
export type ConversationAssetCategory = 'document' | 'code' | 'image' | 'link' | 'other';
|
|
1133
|
+
export type ConversationAssetOrigin = 'user-upload' | 'user-reference' | 'ai-delivery';
|
|
1134
|
+
export type ConversationAssetKind = 'workspace-file' | 'user-attachment' | 'web-link';
|
|
1135
|
+
|
|
1136
|
+
/** A durable, session-scoped item explicitly supplied by the user or formally delivered by the Agent. */
|
|
1137
|
+
export interface ConversationAsset {
|
|
1138
|
+
id: string;
|
|
1139
|
+
conversationKey: string;
|
|
1140
|
+
kind: ConversationAssetKind;
|
|
1141
|
+
category: ConversationAssetCategory;
|
|
1142
|
+
name: string;
|
|
1143
|
+
origins: ConversationAssetOrigin[];
|
|
1144
|
+
firstTurnId: string;
|
|
1145
|
+
lastTurnId: string;
|
|
1146
|
+
firstSeenAt: number;
|
|
1147
|
+
lastSeenAt: number;
|
|
1148
|
+
pinned: boolean;
|
|
1149
|
+
available: boolean;
|
|
1150
|
+
previewable: boolean;
|
|
1151
|
+
relativePath?: string;
|
|
1152
|
+
url?: string;
|
|
1153
|
+
domain?: string;
|
|
1154
|
+
mime?: string;
|
|
1155
|
+
size?: number;
|
|
1156
|
+
previewUrl?: string;
|
|
1157
|
+
verification?: import('../types.js').FileVerification;
|
|
1158
|
+
sha256?: string;
|
|
1159
|
+
modifiedAt?: number;
|
|
1160
|
+
contentChanged?: boolean;
|
|
1161
|
+
/** The original workspace file still exists at its submitted path. */
|
|
1162
|
+
originalAvailable?: boolean;
|
|
1163
|
+
/** An immutable, content-addressed copy of the submitted file is available. */
|
|
1164
|
+
archivedAvailable?: boolean;
|
|
1165
|
+
/** Which bytes are opened by the default preview action. */
|
|
1166
|
+
previewSource?: 'original' | 'archive' | 'upload';
|
|
1108
1167
|
}
|
|
1109
1168
|
|
|
1110
1169
|
export interface GroupChatMessage {
|
|
@@ -1244,8 +1303,13 @@ export type ClientFrame =
|
|
|
1244
1303
|
| { t: 'plain/rename'; sessionId: string; title: string }
|
|
1245
1304
|
| { t: 'plain/delete'; sessionId: string }
|
|
1246
1305
|
| { t: 'resource/files/list'; dir: string; sessionId: string }
|
|
1306
|
+
| { t: 'resource/assets/list'; requestId: string; target: ConversationTarget & { sessionId: string } }
|
|
1307
|
+
| { t: 'resource/assets/pin'; requestId: string; target: ConversationTarget & { sessionId: string }; assetId: string; pinned: boolean }
|
|
1308
|
+
| { t: 'resource/assets/open'; requestId: string; target: ConversationTarget & { sessionId: string }; assetId: string; source?: 'default' | 'original' | 'archive' }
|
|
1309
|
+
| { t: 'resource/assets/reveal'; requestId: string; target: ConversationTarget & { sessionId: string }; assetId: string; source?: 'original' | 'archive' }
|
|
1310
|
+
| { t: 'resource/files/open'; requestId: string; target: ConversationTarget & { sessionId: string }; relativePath: string }
|
|
1247
1311
|
| { t: 'resource/files/pin'; dir: string; sessionId: string; relativePath: string; pinned: boolean }
|
|
1248
|
-
| { t: 'resource/files/reveal'; dir: string; relativePath: string }
|
|
1312
|
+
| { t: 'resource/files/reveal'; requestId?: string; dir?: string; target?: ConversationTarget & { sessionId: string }; relativePath: string }
|
|
1249
1313
|
| { t: 'resource/memories/list'; workspaceId?: string; query?: string }
|
|
1250
1314
|
| { t: 'resource/memories/settings/update'; autoRemember: boolean; workspaceId?: string }
|
|
1251
1315
|
| { t: 'resource/memories/update'; id: string; scope: 'personal' | 'workspace'; expectedVersion: string; patch: MemoryUpdate; workspaceId?: string }
|
|
@@ -1519,10 +1583,21 @@ export type ServerFrame =
|
|
|
1519
1583
|
conversationKey: string;
|
|
1520
1584
|
historyRevision: string;
|
|
1521
1585
|
messages: Message[];
|
|
1586
|
+
deliveries?: ConversationDelivery[];
|
|
1587
|
+
assets?: ConversationAsset[];
|
|
1588
|
+
artifactGroups?: ConversationArtifactGroup[];
|
|
1522
1589
|
runSnapshot?: ConversationRunSnapshot;
|
|
1523
1590
|
contextUsage?: Extract<LoopEvent, { type: 'context_usage' }>;
|
|
1524
1591
|
}
|
|
1592
|
+
| { t: 'conversation/delivery/changed'; conversationKey: string; delivery: ConversationDelivery }
|
|
1593
|
+
| { t: 'conversation/assets/changed'; conversationKey: string; assets: ConversationAsset[] }
|
|
1525
1594
|
| { t: 'resource/files/list'; dir: string; sessionId: string; files: ConversationFileRef[] }
|
|
1595
|
+
| { t: 'resource/assets/list'; requestId: string; conversationKey: string; assets?: ConversationAsset[]; error?: string }
|
|
1596
|
+
| { t: 'resource/assets/pinned'; requestId: string; ok: boolean; error?: string }
|
|
1597
|
+
| { t: 'resource/assets/opened'; requestId: string; ref?: ConversationFileRef; error?: string }
|
|
1598
|
+
| { t: 'resource/assets/revealed'; requestId: string; ok: boolean; error?: string }
|
|
1599
|
+
| { t: 'resource/files/opened'; requestId: string; ref?: ConversationFileRef; error?: string }
|
|
1600
|
+
| { t: 'resource/files/revealed'; requestId: string; ok: boolean; error?: string }
|
|
1526
1601
|
| { t: 'resource/memories'; records: MemoryRecord[]; settings: MemorySettings; workspaceId?: string; reviewRecords?: MemoryRecord[]; persona?: PersonaDocument | null }
|
|
1527
1602
|
| { t: 'resource/memories/conversations'; requestId: string; results: ConversationSearchResult[]; error?: string }
|
|
1528
1603
|
| { t: 'resource/memories/manage/state'; requestId: string; state: MemoryManagementState }
|