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.
Files changed (181) hide show
  1. package/package.json +5 -1
  2. package/plugins/workflow-runner/plugin.js +27 -15
  3. package/plugins/workflow-runner/plugin.ts +34 -20
  4. package/plugins/workflow-runner/src/agentCommon.js +13 -12
  5. package/plugins/workflow-runner/src/agentCommon.ts +22 -13
  6. package/plugins/workflow-runner/src/expressions.js +29 -8
  7. package/plugins/workflow-runner/src/expressions.ts +28 -10
  8. package/plugins/workflow-runner/src/index.js +50 -20
  9. package/plugins/workflow-runner/src/index.ts +75 -31
  10. package/plugins/workflow-runner/src/multimodal.js +2 -1
  11. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  12. package/plugins/workflow-runner/src/recovery.js +9 -2
  13. package/plugins/workflow-runner/src/recovery.ts +9 -2
  14. package/plugins/workflow-runner/src/runner.js +31 -5
  15. package/plugins/workflow-runner/src/runner.ts +33 -5
  16. package/plugins/workflow-runner/src/stepExecutors/assert.js +2 -1
  17. package/plugins/workflow-runner/src/stepExecutors/assert.ts +3 -1
  18. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  19. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  20. package/plugins/workflow-runner/src/stepExecutors/tool.js +91 -61
  21. package/plugins/workflow-runner/src/stepExecutors/tool.ts +111 -71
  22. package/plugins/workflow-runner/src/stepExecutors/verify.js +6 -3
  23. package/plugins/workflow-runner/src/stepExecutors/verify.ts +7 -3
  24. package/plugins/workflow-runner/src/taskIdentity.js +197 -21
  25. package/plugins/workflow-runner/src/taskIdentity.ts +201 -20
  26. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  27. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  28. package/plugins/workflow-runner/src/types.js +113 -7
  29. package/plugins/workflow-runner/src/types.ts +137 -14
  30. package/plugins/workflow-runner/src/workflowState.js +455 -54
  31. package/plugins/workflow-runner/src/workflowState.ts +508 -63
  32. package/plugins/workflow-runner/test/cli.e2e.test.ts +20 -0
  33. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  34. package/plugins/workflow-runner/test/e2eNewLayout.test.ts +314 -0
  35. package/plugins/workflow-runner/test/engineRobustness.test.ts +9 -1
  36. package/plugins/workflow-runner/test/expressions.test.ts +42 -0
  37. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  38. package/plugins/workflow-runner/test/illustrate.load.test.ts +2 -2
  39. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  40. package/plugins/workflow-runner/test/parseWorkflowArgs.test.ts +19 -0
  41. package/plugins/workflow-runner/test/ppt.load.test.ts +15 -15
  42. package/plugins/workflow-runner/test/ralphLoop.e2e.test.ts +3 -2
  43. package/plugins/workflow-runner/test/research.load.test.ts +328 -0
  44. package/plugins/workflow-runner/test/runnerPreflight.test.ts +68 -11
  45. package/plugins/workflow-runner/test/stateLifecycle.test.ts +60 -12
  46. package/plugins/workflow-runner/test/taskArbitration.test.ts +19 -3
  47. package/plugins/workflow-runner/test/taskLifecycle.test.ts +61 -7
  48. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  49. package/plugins/workflow-runner/test/webppt.load.test.ts +4 -4
  50. package/plugins/workflow-runner/test/word.load.test.ts +9 -9
  51. package/skills/skill-creator/scripts/run_eval.py +3 -2
  52. package/src/cli/print.js +36 -4
  53. package/src/cli/print.ts +42 -4
  54. package/src/cli/streamJson.js +19 -4
  55. package/src/cli/streamJson.ts +17 -4
  56. package/src/config/configDefaults.js +25 -0
  57. package/src/config/configDefaults.ts +33 -0
  58. package/src/config/configFile.js +2 -21
  59. package/src/config/configFile.ts +11 -24
  60. package/src/config/configManagement.js +1 -1
  61. package/src/config/configManagement.ts +2 -2
  62. package/src/config/configService.js +24 -0
  63. package/src/config/configService.ts +23 -0
  64. package/src/context/compact.js +4 -0
  65. package/src/context/compact.ts +4 -0
  66. package/src/context/contextBundle.js +38 -13
  67. package/src/context/contextBundle.ts +47 -13
  68. package/src/harness/stallGuard.js +19 -4
  69. package/src/harness/stallGuard.ts +30 -7
  70. package/src/infra/verification.js +25 -18
  71. package/src/infra/verification.ts +50 -18
  72. package/src/loop.js +95 -11
  73. package/src/loop.ts +125 -16
  74. package/src/main.js +8 -1
  75. package/src/main.tsx +7 -1
  76. package/src/memory/constants.js +10 -0
  77. package/src/memory/constants.ts +13 -0
  78. package/src/memory/episodicMemory.js +274 -2
  79. package/src/memory/episodicMemory.ts +306 -1
  80. package/src/memory/fileStore.js +24 -4
  81. package/src/memory/fileStore.ts +22 -2
  82. package/src/memory/memoryService.js +79 -12
  83. package/src/memory/memoryService.ts +108 -9
  84. package/src/memory/repository.js +415 -85
  85. package/src/memory/repository.ts +437 -86
  86. package/src/memory/turnJournal.js +95 -0
  87. package/src/memory/turnJournal.ts +120 -0
  88. package/src/memory/types.ts +16 -0
  89. package/src/plugins/pluginApi.ts +30 -6
  90. package/src/plugins/pluginRunner.js +23 -8
  91. package/src/plugins/pluginRunner.ts +32 -13
  92. package/src/prompts/policyRegistry.js +55 -1
  93. package/src/prompts/policyRegistry.ts +55 -1
  94. package/src/prompts/promptAssembly.js +39 -7
  95. package/src/prompts/promptAssembly.ts +145 -59
  96. package/src/session/runAgentTurn.js +43 -21
  97. package/src/session/runAgentTurn.ts +68 -26
  98. package/src/session/runTurn.js +11 -46
  99. package/src/session/runTurn.ts +12 -46
  100. package/src/session/runtimeStore.js +149 -0
  101. package/src/session/runtimeStore.ts +197 -0
  102. package/src/session/sessionState.js +5 -0
  103. package/src/session/sessionState.ts +14 -0
  104. package/src/tools/advisor/advisor.js +76 -9
  105. package/src/tools/advisor/advisor.ts +79 -9
  106. package/src/tools/agent/agent.js +143 -14
  107. package/src/tools/agent/agent.ts +149 -14
  108. package/src/tools/agent/agentTaskService.js +60 -0
  109. package/src/tools/agent/agentTaskService.ts +113 -0
  110. package/src/tools/agent/agentTypes.js +5 -0
  111. package/src/tools/agent/agentTypes.ts +5 -0
  112. package/src/tools/bash/bash.js +80 -15
  113. package/src/tools/bash/bash.ts +83 -15
  114. package/src/tools/capabilities.js +5 -0
  115. package/src/tools/capabilities.ts +11 -1
  116. package/src/tools/edit/edit.js +7 -4
  117. package/src/tools/edit/edit.ts +7 -4
  118. package/src/tools/edit/multi-edit.js +4 -2
  119. package/src/tools/edit/multi-edit.ts +4 -2
  120. package/src/tools/glob/glob.js +156 -40
  121. package/src/tools/glob/glob.ts +185 -46
  122. package/src/tools/index.js +35 -17
  123. package/src/tools/index.ts +81 -20
  124. package/src/tools/lsp/lsp.js +165 -22
  125. package/src/tools/lsp/lsp.ts +206 -36
  126. package/src/tools/powershell/powershell.js +134 -27
  127. package/src/tools/powershell/powershell.ts +171 -30
  128. package/src/tools/presentresult/presentresult.js +10 -0
  129. package/src/tools/presentresult/presentresult.ts +10 -0
  130. package/src/tools/read/read.js +249 -29
  131. package/src/tools/read/read.ts +396 -59
  132. package/src/tools/shared/diffSummary.js +44 -0
  133. package/src/tools/shared/diffSummary.ts +63 -0
  134. package/src/tools/shared/fileActivity.js +2 -1
  135. package/src/tools/shared/fileActivity.ts +3 -0
  136. package/src/tools/shared/permanentDeleteGuard.js +57 -0
  137. package/src/tools/shared/permanentDeleteGuard.ts +71 -0
  138. package/src/tools/shared/toolOutputStore.js +97 -0
  139. package/src/tools/shared/toolOutputStore.ts +110 -0
  140. package/src/tools/skill/skill.js +86 -0
  141. package/src/tools/skill/skill.ts +94 -0
  142. package/src/tools/snip/snip.js +67 -0
  143. package/src/tools/snip/snip.ts +80 -0
  144. package/src/tools/tasks/backgroundTaskService.js +79 -28
  145. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  146. package/src/tools/tasks/taskManagement.js +163 -0
  147. package/src/tools/tasks/taskManagement.ts +180 -0
  148. package/src/tools/tasks/taskOutput.js +64 -14
  149. package/src/tools/tasks/taskOutput.ts +102 -23
  150. package/src/tools/tasks/taskStop.js +48 -8
  151. package/src/tools/tasks/taskStop.ts +69 -9
  152. package/src/tools/tasks/taskStore.js +46 -0
  153. package/src/tools/tasks/taskStore.ts +77 -0
  154. package/src/tools/terminal/terminalCapture.js +167 -0
  155. package/src/tools/terminal/terminalCapture.ts +179 -0
  156. package/src/tools/trash/trash.js +233 -0
  157. package/src/tools/trash/trash.ts +283 -0
  158. package/src/tools/webbrowser/browserControl.js +5 -1
  159. package/src/tools/webbrowser/browserControl.ts +7 -1
  160. package/src/tools/webbrowser/webbrowser.js +8 -4
  161. package/src/tools/webbrowser/webbrowser.ts +8 -4
  162. package/src/tools/worktree/sessionWorktree.js +128 -0
  163. package/src/tools/worktree/sessionWorktree.ts +158 -0
  164. package/src/tools/write/write.js +6 -2
  165. package/src/tools/write/write.ts +6 -2
  166. package/src/types.ts +22 -4
  167. package/src/ui/hooks/useChat.js +31 -11
  168. package/src/ui/hooks/useChat.ts +31 -13
  169. package/src/web/protocol.ts +78 -3
  170. package/workflows/adaptive-research.yaml +5 -5
  171. package/workflows/book-review-short.yaml +5 -5
  172. package/workflows/e2e-write-greet.yaml +2 -2
  173. package/workflows/illustrate.yaml +12 -12
  174. package/workflows/plan.yaml +18 -18
  175. package/workflows/ppt.yaml +201 -201
  176. package/workflows/ralph-loop.yaml +3 -1
  177. package/workflows/research.md +449 -0
  178. package/workflows/research.yaml +873 -0
  179. package/workflows/webppt.yaml +47 -47
  180. package/workflows/word.yaml +80 -80
  181. 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
+ };
@@ -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) {
@@ -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' LLM 调用失败 → 终结,stop_reason='error'
877
- * - 'max_turns' 撞最大轮数上限 → 终结,stop_reason='max_turns'
878
- * - 'compact_failed' 自动压缩失败但 loop 想继续 → 非致命,print.ts 只 stderr 警告不退出
879
- * - 'aborted' 被中断(signal.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';
@@ -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: getWorkingDir(),
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(getWorkingDir(), turnTools);
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: getWorkingDir(),
484
+ cwd: effectiveCwd,
471
485
  tools: turnTools,
472
486
  runtime: {
473
487
  surface: 'tui',
474
- cwd: getWorkingDir(),
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: getOrCreateSessionState(sessionStateKey({
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));
@@ -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: getWorkingDir(),
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(getWorkingDir(), turnTools);
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: getWorkingDir(),
724
+ cwd: effectiveCwd,
711
725
  tools: turnTools,
712
726
  runtime: {
713
727
  surface: 'tui',
714
- cwd: getWorkingDir(),
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: getOrCreateSessionState(
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));