minimal-agent 0.11.0 → 0.13.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 +5 -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/expressions.js +29 -8
- package/plugins/workflow-runner/src/expressions.ts +28 -10
- package/plugins/workflow-runner/src/index.js +50 -20
- package/plugins/workflow-runner/src/index.ts +75 -31
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/recovery.js +9 -2
- package/plugins/workflow-runner/src/recovery.ts +9 -2
- package/plugins/workflow-runner/src/runner.js +31 -5
- package/plugins/workflow-runner/src/runner.ts +33 -5
- package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
- package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -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 +91 -61
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
- package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
- package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
- package/plugins/workflow-runner/src/taskIdentity.js +197 -21
- package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.js +113 -7
- package/plugins/workflow-runner/src/types.ts +137 -14
- package/plugins/workflow-runner/src/workflowState.js +455 -54
- package/plugins/workflow-runner/src/workflowState.ts +508 -63
- package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
- package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
- package/plugins/workflow-runner/test/expressions.test.ts +42 -0
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
- package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
- package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
- package/plugins/workflow-runner/test/research.load.test.ts +328 -0
- package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
- package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
- package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
- package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
- package/plugins/workflow-runner/test/word.load.test.ts +9 -9
- package/skills/skill-creator/scripts/run_eval.py +3 -2
- package/src/cli/print.js +36 -4
- package/src/cli/print.ts +42 -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/config/configService.js +24 -0
- package/src/config/configService.ts +23 -0
- 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/infra/verification.js +25 -18
- package/src/infra/verification.ts +50 -18
- 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 +23 -8
- package/src/plugins/pluginRunner.ts +32 -13
- package/src/prompts/policyRegistry.js +55 -1
- package/src/prompts/policyRegistry.ts +55 -1
- 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/advisor/advisor.js +76 -9
- package/src/tools/advisor/advisor.ts +79 -9
- 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 +80 -15
- package/src/tools/bash/bash.ts +83 -15
- 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 +134 -27
- package/src/tools/powershell/powershell.ts +171 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +249 -29
- package/src/tools/read/read.ts +396 -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 +57 -0
- package/src/tools/shared/permanentDeleteGuard.ts +71 -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/webbrowser/browserControl.js +5 -1
- package/src/tools/webbrowser/browserControl.ts +7 -1
- package/src/tools/webbrowser/webbrowser.js +8 -4
- package/src/tools/webbrowser/webbrowser.ts +8 -4
- 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 +22 -4
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
- package/workflows/adaptive-research.yaml +5 -5
- package/workflows/book-review-short.yaml +5 -5
- package/workflows/e2e-write-greet.yaml +2 -2
- package/workflows/illustrate.yaml +12 -12
- package/workflows/plan.yaml +18 -18
- package/workflows/ppt.yaml +201 -201
- package/workflows/ralph-loop.yaml +3 -1
- package/workflows/research.md +449 -0
- package/workflows/research.yaml +873 -0
- package/workflows/webppt.yaml +47 -47
- package/workflows/word.yaml +80 -80
- package/workflows/youtube-shorts.yaml +25 -25
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { toToolParameters } from '../../utils/zodToJson.js';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { readRuntimeJson, sessionRuntimeDir, writeRuntimeJson, } from '../../session/runtimeStore.js';
|
|
6
|
+
import { applyAgentWorktree, commitWorktreeChanges, createAgentWorktree, hasWorktreeChanges, removeAgentWorktree, } from '../agent/worktree.js';
|
|
7
|
+
const enterSchema = z.object({
|
|
8
|
+
name: z
|
|
9
|
+
.string()
|
|
10
|
+
.regex(/^[a-zA-Z0-9._-]{1,48}$/)
|
|
11
|
+
.optional(),
|
|
12
|
+
});
|
|
13
|
+
const exitSchema = z.object({ action: z.enum(['apply', 'keep', 'discard']) });
|
|
14
|
+
async function persist(ctx, state) {
|
|
15
|
+
await writeRuntimeJson(join(sessionRuntimeDir(ctx), 'worktree.json'), {
|
|
16
|
+
active: state ?? null,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export async function hydrateSessionWorktree(ctx) {
|
|
20
|
+
if (!ctx.sessionState || ctx.sessionState.activeWorktree)
|
|
21
|
+
return;
|
|
22
|
+
const saved = await readRuntimeJson(join(sessionRuntimeDir(ctx), 'worktree.json'), {});
|
|
23
|
+
if (saved.active)
|
|
24
|
+
ctx.sessionState.activeWorktree = saved.active;
|
|
25
|
+
}
|
|
26
|
+
async function enter(input, ctx) {
|
|
27
|
+
const state = ctx.sessionState;
|
|
28
|
+
if (!state)
|
|
29
|
+
return { ok: false, error: 'EnterWorktree 需要会话状态' };
|
|
30
|
+
if (state.activeWorktree)
|
|
31
|
+
return {
|
|
32
|
+
ok: true,
|
|
33
|
+
content: `当前已在 worktree ${state.activeWorktree.path}`,
|
|
34
|
+
};
|
|
35
|
+
const slug = `session-${input.name ?? randomUUID().slice(0, 8)}`;
|
|
36
|
+
const wt = await createAgentWorktree(slug, ctx.cwd);
|
|
37
|
+
if (!wt.ok)
|
|
38
|
+
return wt;
|
|
39
|
+
state.activeWorktree = {
|
|
40
|
+
id: slug,
|
|
41
|
+
path: wt.path,
|
|
42
|
+
branch: wt.branch,
|
|
43
|
+
baseSha: wt.baseSha,
|
|
44
|
+
gitRoot: wt.gitRoot,
|
|
45
|
+
};
|
|
46
|
+
await persist(ctx, state.activeWorktree);
|
|
47
|
+
return {
|
|
48
|
+
ok: true,
|
|
49
|
+
content: `已进入主 Agent 隔离 worktree:${wt.path}\n分支:${wt.branch}\n后续工具调用立即使用该目录。`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
async function exit(input, ctx) {
|
|
53
|
+
const state = ctx.sessionState, wt = state?.activeWorktree;
|
|
54
|
+
if (!state || !wt)
|
|
55
|
+
return { ok: false, error: '当前会话没有活动 worktree' };
|
|
56
|
+
if (input.action === 'keep') {
|
|
57
|
+
state.activeWorktree = undefined;
|
|
58
|
+
await persist(ctx, undefined);
|
|
59
|
+
return {
|
|
60
|
+
ok: true,
|
|
61
|
+
content: `已退出有效 cwd,但保留 worktree:${wt.path}\n分支:${wt.branch}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const changed = await hasWorktreeChanges(wt.path, wt.baseSha);
|
|
65
|
+
if (input.action === 'discard' && changed) {
|
|
66
|
+
const committed = await commitWorktreeChanges(wt.path, `archive: preserve discarded session ${wt.id}`);
|
|
67
|
+
if (committed.ok) {
|
|
68
|
+
state.activeWorktree = undefined;
|
|
69
|
+
await persist(ctx, undefined);
|
|
70
|
+
}
|
|
71
|
+
return committed.ok
|
|
72
|
+
? {
|
|
73
|
+
ok: true,
|
|
74
|
+
content: `检测到成果,未永久删除。已归档到分支 ${wt.branch},worktree 保留在 ${wt.path}。`,
|
|
75
|
+
}
|
|
76
|
+
: {
|
|
77
|
+
ok: false,
|
|
78
|
+
error: `无法安全归档,worktree 保持活动:${committed.error}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
if (input.action === 'discard') {
|
|
82
|
+
await removeAgentWorktree(wt.path, wt.branch, wt.gitRoot);
|
|
83
|
+
state.activeWorktree = undefined;
|
|
84
|
+
await persist(ctx, undefined);
|
|
85
|
+
return {
|
|
86
|
+
ok: true,
|
|
87
|
+
content: 'worktree 无改动,已通过 Git worktree 管理安全清理。',
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
const committed = await commitWorktreeChanges(wt.path, `minimal-agent: apply session ${wt.id}`);
|
|
91
|
+
if (!committed.ok)
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: `提交隔离改动失败,worktree 保持活动:${committed.error}`,
|
|
95
|
+
};
|
|
96
|
+
const applied = await applyAgentWorktree(wt.gitRoot, wt.id);
|
|
97
|
+
if (!applied.ok)
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
error: `应用检查失败,worktree 保持活动:${applied.error}`,
|
|
101
|
+
};
|
|
102
|
+
state.activeWorktree = undefined;
|
|
103
|
+
await persist(ctx, undefined);
|
|
104
|
+
return {
|
|
105
|
+
ok: true,
|
|
106
|
+
content: `已通过 git apply --check 并将变更应用到原工作区;未自动提交。`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
export const enterWorktreeTool = {
|
|
110
|
+
name: 'EnterWorktree',
|
|
111
|
+
description: '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.',
|
|
112
|
+
inputSchema: enterSchema,
|
|
113
|
+
parameters: toToolParameters(enterSchema),
|
|
114
|
+
isReadOnly: false,
|
|
115
|
+
isConcurrencySafe: false,
|
|
116
|
+
call: async () => ({ ok: false, error: 'requires session context' }),
|
|
117
|
+
callWithContext: (i, _s, c) => enter(i, c),
|
|
118
|
+
};
|
|
119
|
+
export const exitWorktreeTool = {
|
|
120
|
+
name: 'ExitWorktree',
|
|
121
|
+
description: '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.',
|
|
122
|
+
inputSchema: exitSchema,
|
|
123
|
+
parameters: toToolParameters(exitSchema),
|
|
124
|
+
isReadOnly: false,
|
|
125
|
+
isConcurrencySafe: false,
|
|
126
|
+
call: async () => ({ ok: false, error: 'requires session context' }),
|
|
127
|
+
callWithContext: (i, _s, c) => exit(i, c),
|
|
128
|
+
};
|
|
@@ -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
|
|
|
@@ -873,11 +885,17 @@ export type LoopEvent =
|
|
|
873
885
|
*
|
|
874
886
|
* code(v2.x 新增,可选):让 `-p --output-format json` 把终结原因映射成稳定的
|
|
875
887
|
* stop_reason,也让 print.ts 区分"致命错误(退出)"与"非致命警告(继续)":
|
|
876
|
-
* - 'llm_error'
|
|
877
|
-
* - 'max_turns'
|
|
878
|
-
* - 'compact_failed'
|
|
879
|
-
* - 'aborted'
|
|
888
|
+
* - 'llm_error' LLM 调用失败 → 终结,stop_reason='error'
|
|
889
|
+
* - 'max_turns' 撞最大轮数上限 → 终结,stop_reason='max_turns'
|
|
890
|
+
* - 'compact_failed' 自动压缩失败但 loop 想继续 → 非致命,print.ts 只 stderr 警告不退出
|
|
891
|
+
* - 'aborted' 被中断(signal.aborted)
|
|
892
|
+
* - 'stalled' StallGuard 判定原地打转(同一失败反复 / 整批调用无进展)
|
|
893
|
+
* → 终结,stop_reason='stalled'
|
|
894
|
+
* - 'context_overflow' 即使降级后仍超模型输入上限,主动不发这次请求
|
|
895
|
+
* → 终结,stop_reason='error'
|
|
880
896
|
* 缺省(旧调用方 / 未分类错误)按致命、stop_reason='error' 处理。
|
|
897
|
+
* ⚠️ 增删 code 时同步改 print.ts 的映射:那里只特判 max_turns / stalled,
|
|
898
|
+
* 其余一律落 'error',新增的终结原因不特判就会被并进通用 error。
|
|
881
899
|
*/
|
|
882
900
|
| {
|
|
883
901
|
type: 'error';
|
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));
|