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
@@ -1,11 +1,22 @@
1
- import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../prompts/system.js';
1
+ import {
2
+ buildBaseSystemPrompt,
3
+ buildTransientPromptBundle,
4
+ } from '../prompts/system.js';
2
5
  import { ALL_TOOLS } from '../tools/index.js';
3
6
  import type { LoopEvent, Provider } from '../types.js';
4
7
  import { sessionFileFor } from '../context/sessionPath.js';
5
- import { getOrCreateSessionState, sessionStateKey, type SessionState } from './sessionState.js';
8
+ import {
9
+ getOrCreateSessionState,
10
+ sessionStateKey,
11
+ type SessionState,
12
+ } from './sessionState.js';
13
+ import { hydrateSessionWorktree } from '../tools/worktree/sessionWorktree.js';
6
14
  import { runTurn } from './runTurn.js';
7
15
  import { withWorkingDir } from '../bootstrap/workingDir.js';
8
- import { listSessionSummaries, updateSessionMetadata } from '../context/sessionRegistry.js';
16
+ import {
17
+ listSessionSummaries,
18
+ updateSessionMetadata,
19
+ } from '../context/sessionRegistry.js';
9
20
  import { workspaceIndexService } from '../context/workspaceIndex.js';
10
21
  import { configService } from '../config/configService.js';
11
22
  import { createPromptCacheContext } from '../llm/promptCache.js';
@@ -44,55 +55,86 @@ export async function runAgentTurn(
44
55
  deps: { runTurn?: typeof runTurn } = {},
45
56
  ): Promise<RunAgentTurnResult> {
46
57
  const run = deps.runTurn ?? runTurn;
47
- const session = (await listSessionSummaries(opts.cwd)).find((item) => item.id === opts.sessionId);
58
+ const session = (await listSessionSummaries(opts.cwd)).find(
59
+ (item) => item.id === opts.sessionId,
60
+ );
48
61
  let sessionModelId = session?.modelId;
49
62
  if (!opts.model && !sessionModelId) {
50
- sessionModelId = (await configService.getSnapshot()).config?.baseModelId ?? undefined;
63
+ sessionModelId =
64
+ (await configService.getSnapshot()).config?.baseModelId ?? undefined;
51
65
  if (sessionModelId) {
52
- await updateSessionMetadata(opts.cwd, opts.sessionId, { modelId: sessionModelId }).catch(() => {});
66
+ await updateSessionMetadata(opts.cwd, opts.sessionId, {
67
+ modelId: sessionModelId,
68
+ }).catch(() => {});
53
69
  }
54
70
  }
55
- const sessionProvider = !opts.model && sessionModelId
56
- ? await configService.providerForModel(sessionModelId)
57
- : null;
71
+ const sessionProvider =
72
+ !opts.model && sessionModelId
73
+ ? await configService.providerForModel(sessionModelId)
74
+ : null;
58
75
  const provider = opts.model
59
76
  ? { ...opts.provider, model: opts.model }
60
- : sessionProvider ?? opts.provider;
61
- const workspace = await workspaceIndexService.register(opts.cwd).catch(() => undefined);
77
+ : (sessionProvider ?? opts.provider);
78
+ const workspace = await workspaceIndexService
79
+ .register(opts.cwd)
80
+ .catch(() => undefined);
62
81
  const sessionFile = sessionFileFor(opts.cwd, opts.sessionId);
63
82
  const events: LoopEvent[] = [];
64
83
  let result = '';
65
84
  let error: string | undefined;
66
85
  let numTurns = 0;
86
+ const turnSessionState =
87
+ opts.sessionState ??
88
+ getOrCreateSessionState(
89
+ sessionStateKey({
90
+ surface: 'headless',
91
+ workspaceId: workspace?.id,
92
+ cwd: workspace?.id ? undefined : opts.cwd,
93
+ sessionId: opts.sessionId,
94
+ }),
95
+ );
96
+ await hydrateSessionWorktree({
97
+ sessionState: turnSessionState,
98
+ workspaceId: workspace?.id,
99
+ sessionId: opts.sessionId,
100
+ cwd: opts.cwd,
101
+ });
102
+ const effectiveCwd = turnSessionState.activeWorktree?.path ?? opts.cwd;
67
103
 
68
- await withWorkingDir(opts.cwd, async () => {
104
+ await withWorkingDir(effectiveCwd, async () => {
69
105
  // 稳定层进 history[0](runTurn 每轮重写它,必须逐字节可复现,见 runTurn.ts 的缓存不变量);
70
106
  // 易变层每轮单独计算,仅附加到当前用户消息的模型副本。
71
107
  const runtimeContext = createAgentRuntimeContext({
72
108
  surface: 'headless',
73
109
  profile: 'main',
74
- cwd: opts.cwd,
110
+ cwd: effectiveCwd,
75
111
  workspaceId: workspace?.id,
76
112
  sessionId: opts.sessionId,
77
- owner: opts.memoryAccess && opts.memoryAccess !== 'denied' ? 'local-owner' : 'untrusted',
113
+ owner:
114
+ opts.memoryAccess && opts.memoryAccess !== 'denied'
115
+ ? 'local-owner'
116
+ : 'untrusted',
78
117
  memoryAccess: opts.memoryAccess ?? 'denied',
79
118
  interactiveAsk: false,
80
119
  });
81
120
  const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
82
- const systemPrompt = await buildBaseSystemPrompt(opts.cwd, turnTools);
121
+ const systemPrompt = await buildBaseSystemPrompt(effectiveCwd, turnTools);
83
122
  const transientBundle = await buildTransientPromptBundle({
84
- cwd: opts.cwd,
123
+ cwd: effectiveCwd,
85
124
  tools: turnTools,
86
125
  runtime: {
87
126
  surface: 'headless',
88
- cwd: opts.cwd,
127
+ cwd: effectiveCwd,
89
128
  workspaceId: workspace?.id,
129
+ sessionId: opts.sessionId,
90
130
  interactiveAsk: false,
91
131
  memoryAccess: runtimeContext.memoryAccess,
92
132
  currentUserInput: opts.input,
93
133
  },
94
134
  }).catch(() => undefined);
95
- await updateSessionMetadata(opts.cwd, opts.sessionId, { lastMessagePreview: opts.input }).catch(() => {});
135
+ await updateSessionMetadata(opts.cwd, opts.sessionId, {
136
+ lastMessagePreview: opts.input,
137
+ }).catch(() => {});
96
138
  for await (const ev of run({
97
139
  userInput: opts.input,
98
140
  provider,
@@ -102,12 +144,7 @@ export async function runAgentTurn(
102
144
  promptCache: createPromptCacheContext(
103
145
  `workspace:${workspace?.id ?? opts.cwd}:session:${opts.sessionId}`,
104
146
  ),
105
- sessionState: opts.sessionState ?? getOrCreateSessionState(sessionStateKey({
106
- surface: runtimeContext.surface,
107
- workspaceId: runtimeContext.workspaceId,
108
- cwd: opts.cwd,
109
- sessionId: opts.sessionId,
110
- })),
147
+ sessionState: turnSessionState,
111
148
  signal: opts.signal,
112
149
  maxTurns: opts.maxTurns,
113
150
  runtimeContext,
@@ -120,7 +157,8 @@ export async function runAgentTurn(
120
157
  if (ev.type === 'assistant_message') numTurns++;
121
158
  // compact_failed 是非致命(loop 压缩失败后 snip 兜底继续跑完,types.ts 有约定;
122
159
  // print.ts 同样特判)——不置 error,否则 cron 会把自愈轮误判失败。
123
- if (ev.type === 'error' && ev.code !== 'compact_failed') error = ev.error;
160
+ if (ev.type === 'error' && ev.code !== 'compact_failed')
161
+ error = ev.error;
124
162
  }
125
163
  }
126
164
  });
@@ -135,5 +173,9 @@ export async function runAgentTurn(
135
173
  }
136
174
 
137
175
  function isEventLike(ev: unknown): ev is LoopEvent {
138
- return Boolean(ev && typeof ev === 'object' && typeof (ev as { type?: unknown }).type === 'string');
176
+ return Boolean(
177
+ ev &&
178
+ typeof ev === 'object' &&
179
+ typeof (ev as { type?: unknown }).type === 'string',
180
+ );
139
181
  }
@@ -20,7 +20,7 @@
20
20
  */
21
21
  import { loadContext, saveContext } from '../context/persistContext.js';
22
22
  import { acquireContextLock } from '../context/contextLock.js';
23
- import { episodicMemoryService } from '../memory/episodicMemory.js';
23
+ import { startTurnJournal } from '../memory/turnJournal.js';
24
24
  import { runWithPlugins } from '../plugins/pluginRunner.js';
25
25
  /**
26
26
  * 已有历史时,用调用方新算好的 systemPrompt(含当前日期/实时扫描的 skills·plugins 清单/
@@ -55,23 +55,14 @@ export async function* runTurn(opts) {
55
55
  const loaded = opts.initialHistory ?? (persist ? await loadContext(opts.sessionFile) : null);
56
56
  const history = buildInitialHistory(loaded, opts.systemPrompt);
57
57
  const historyStart = history.length;
58
- const memoryContext = opts.memoryContext;
59
- const journalContext = memoryContext?.access !== 'denied'
60
- && memoryContext?.workspaceId
61
- && memoryContext?.sessionId
62
- ? {
63
- workspaceId: memoryContext.workspaceId,
64
- sessionId: memoryContext.sessionId,
65
- }
66
- : null;
67
- if (journalContext) {
68
- await episodicMemoryService.append({
69
- ...journalContext,
70
- role: 'user',
71
- content: opts.userInput,
72
- messageId: opts.userMessageId,
73
- }).catch(() => undefined);
74
- }
58
+ // 会话原文记账:实现在 src/memory/turnJournal.ts(TUI 的 useChat 用同一份),
59
+ // 刻意不下沉到 runQuery —— 子 Agent 与 workflow 步也调 runQuery,记在那里会把
60
+ // 子任务提示词当成"用户说的话"污染历史检索。
61
+ const journal = await startTurnJournal({
62
+ memoryContext: opts.memoryContext,
63
+ userInput: opts.userInput,
64
+ ...(opts.userMessageId ? { userMessageId: opts.userMessageId } : {}),
65
+ });
75
66
  try {
76
67
  yield* runWithPlugins(opts.userInput, {
77
68
  provider: opts.provider,
@@ -83,6 +74,7 @@ export async function* runTurn(opts) {
83
74
  sessionState: opts.sessionState,
84
75
  chat: opts.chat,
85
76
  executeTool: opts.executeTool,
77
+ commitFormalDelivery: opts.commitFormalDelivery,
86
78
  toolGate: opts.toolGate,
87
79
  transientContext: opts.transientContext,
88
80
  transientBundle: opts.transientBundle,
@@ -102,17 +94,7 @@ export async function* runTurn(opts) {
102
94
  }
103
95
  finally {
104
96
  try {
105
- if (journalContext) {
106
- const assistant = lastAssistantMessage(history, historyStart);
107
- const content = assistant ? messageText(assistant) : '';
108
- if (content) {
109
- await episodicMemoryService.append({
110
- ...journalContext,
111
- role: 'assistant',
112
- content,
113
- }).catch(() => undefined);
114
- }
115
- }
97
+ await journal?.finish(history, historyStart);
116
98
  if (persist)
117
99
  await saveContext(history, opts.sessionFile);
118
100
  }
@@ -121,20 +103,3 @@ export async function* runTurn(opts) {
121
103
  }
122
104
  }
123
105
  }
124
- function messageText(message) {
125
- if (typeof message.content === 'string')
126
- return message.content;
127
- if (!Array.isArray(message.content))
128
- return '';
129
- return message.content
130
- .filter((part) => part.type === 'text')
131
- .map((part) => part.text)
132
- .join('\n');
133
- }
134
- function lastAssistantMessage(messages, start) {
135
- for (let index = messages.length - 1; index >= start; index -= 1) {
136
- if (messages[index]?.role === 'assistant')
137
- return messages[index];
138
- }
139
- return undefined;
140
- }
@@ -21,7 +21,7 @@
21
21
 
22
22
  import { loadContext, saveContext } from '../context/persistContext.js';
23
23
  import { acquireContextLock } from '../context/contextLock.js';
24
- import { episodicMemoryService } from '../memory/episodicMemory.js';
24
+ import { startTurnJournal } from '../memory/turnJournal.js';
25
25
  import type { PluginEvent } from '../plugins/pluginApi.js';
26
26
  import { runWithPlugins } from '../plugins/pluginRunner.js';
27
27
  import type {
@@ -49,6 +49,7 @@ export interface RunTurnOptions {
49
49
  * 让测试用显式注入替代 mock.module(loop.js)——后者会全局泄漏污染其它测试文件。 */
50
50
  chat?: import('../plugins/pluginRunner.js').PluginRunnerOptions['chat'];
51
51
  executeTool?: import('../plugins/pluginRunner.js').PluginRunnerOptions['executeTool'];
52
+ commitFormalDelivery?: import('../plugins/pluginRunner.js').PluginRunnerOptions['commitFormalDelivery'];
52
53
  /** 工具执行闸门(生产用):透传给 runWithPlugins→runQuery。见 src/toolGate.ts。 */
53
54
  toolGate?: import('../plugins/pluginRunner.js').PluginRunnerOptions['toolGate'];
54
55
  /** 只对本次模型回合可见,不写入 session。 */
@@ -111,24 +112,14 @@ export async function* runTurn(
111
112
  const loaded = opts.initialHistory ?? (persist ? await loadContext(opts.sessionFile) : null);
112
113
  const history: Message[] = buildInitialHistory(loaded, opts.systemPrompt);
113
114
  const historyStart = history.length;
114
- const memoryContext = opts.memoryContext;
115
- const journalContext = memoryContext?.access !== 'denied'
116
- && memoryContext?.workspaceId
117
- && memoryContext?.sessionId
118
- ? {
119
- workspaceId: memoryContext.workspaceId,
120
- sessionId: memoryContext.sessionId,
121
- }
122
- : null;
123
-
124
- if (journalContext) {
125
- await episodicMemoryService.append({
126
- ...journalContext,
127
- role: 'user',
128
- content: opts.userInput,
129
- messageId: opts.userMessageId,
130
- }).catch(() => undefined);
131
- }
115
+ // 会话原文记账:实现在 src/memory/turnJournal.ts(TUI 的 useChat 用同一份),
116
+ // 刻意不下沉到 runQuery —— 子 Agent 与 workflow 步也调 runQuery,记在那里会把
117
+ // 子任务提示词当成"用户说的话"污染历史检索。
118
+ const journal = await startTurnJournal({
119
+ memoryContext: opts.memoryContext,
120
+ userInput: opts.userInput,
121
+ ...(opts.userMessageId ? { userMessageId: opts.userMessageId } : {}),
122
+ });
132
123
 
133
124
  try {
134
125
  yield* runWithPlugins(opts.userInput, {
@@ -141,6 +132,7 @@ export async function* runTurn(
141
132
  sessionState: opts.sessionState,
142
133
  chat: opts.chat,
143
134
  executeTool: opts.executeTool,
135
+ commitFormalDelivery: opts.commitFormalDelivery,
144
136
  toolGate: opts.toolGate,
145
137
  transientContext: opts.transientContext,
146
138
  transientBundle: opts.transientBundle,
@@ -159,36 +151,10 @@ export async function* runTurn(
159
151
  });
160
152
  } finally {
161
153
  try {
162
- if (journalContext) {
163
- const assistant = lastAssistantMessage(history, historyStart);
164
- const content = assistant ? messageText(assistant) : '';
165
- if (content) {
166
- await episodicMemoryService.append({
167
- ...journalContext,
168
- role: 'assistant',
169
- content,
170
- }).catch(() => undefined);
171
- }
172
- }
154
+ await journal?.finish(history, historyStart);
173
155
  if (persist) await saveContext(history, opts.sessionFile);
174
156
  } finally {
175
157
  await release();
176
158
  }
177
159
  }
178
160
  }
179
-
180
- function messageText(message: Message): string {
181
- if (typeof message.content === 'string') return message.content;
182
- if (!Array.isArray(message.content)) return '';
183
- return message.content
184
- .filter((part): part is Extract<typeof part, { type: 'text' }> => part.type === 'text')
185
- .map((part) => part.text)
186
- .join('\n');
187
- }
188
-
189
- function lastAssistantMessage(messages: Message[], start: number): Message | undefined {
190
- for (let index = messages.length - 1; index >= start; index -= 1) {
191
- if (messages[index]?.role === 'assistant') return messages[index];
192
- }
193
- return undefined;
194
- }
@@ -0,0 +1,149 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { mkdir, open, readFile, readdir, rename, rm, stat, } from 'node:fs/promises';
3
+ import { dirname, join, resolve } from 'node:path';
4
+ import { agentStateDir } from '../context/agentHome.js';
5
+ function segment(value, fallback) {
6
+ return ((value ?? fallback).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) ||
7
+ fallback);
8
+ }
9
+ export function sessionRuntimeDir(identity) {
10
+ return join(agentStateDir(), 'runtime', segment(identity.workspaceId, 'local'), segment(identity.sessionId, 'local'));
11
+ }
12
+ const queues = new Map();
13
+ export async function withRuntimeFileLock(path, work) {
14
+ const key = resolve(path);
15
+ const previous = queues.get(key) ?? Promise.resolve();
16
+ let release;
17
+ const current = new Promise((done) => {
18
+ release = done;
19
+ });
20
+ const tail = previous.then(() => current);
21
+ queues.set(key, tail);
22
+ await previous.catch(() => undefined);
23
+ let lockHandle;
24
+ const lockPath = `${path}.lock`;
25
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
26
+ try {
27
+ const deadline = Date.now() + 10_000;
28
+ while (!lockHandle) {
29
+ try {
30
+ lockHandle = await open(lockPath, 'wx', 0o600);
31
+ await lockHandle.writeFile(`${process.pid}\n${Date.now()}\n`);
32
+ }
33
+ catch (error) {
34
+ if (error.code !== 'EEXIST')
35
+ throw error;
36
+ const age = Date.now() -
37
+ (await stat(lockPath).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
38
+ if (age > 30_000)
39
+ await rm(lockPath, { force: true });
40
+ else if (Date.now() >= deadline)
41
+ throw new Error(`runtime lock timeout: ${path}`);
42
+ else
43
+ await new Promise((r) => setTimeout(r, 25));
44
+ }
45
+ }
46
+ return await work();
47
+ }
48
+ finally {
49
+ await lockHandle?.close().catch(() => undefined);
50
+ if (lockHandle)
51
+ await rm(lockPath, { force: true }).catch(() => undefined);
52
+ release();
53
+ if (queues.get(key) === tail)
54
+ queues.delete(key);
55
+ }
56
+ }
57
+ export async function readRuntimeJson(path, fallback) {
58
+ try {
59
+ return JSON.parse(await readFile(path, 'utf8'));
60
+ }
61
+ catch {
62
+ return fallback;
63
+ }
64
+ }
65
+ export async function writeRuntimeJson(path, value) {
66
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
67
+ const temp = `${path}.${randomUUID()}.tmp`;
68
+ try {
69
+ const handle = await open(temp, 'wx', 0o600);
70
+ try {
71
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
72
+ await handle.sync();
73
+ }
74
+ finally {
75
+ await handle.close();
76
+ }
77
+ await rename(temp, path);
78
+ }
79
+ finally {
80
+ await rm(temp, { force: true }).catch(() => undefined);
81
+ }
82
+ }
83
+ export async function mutateRuntimeJson(path, fallback, mutate) {
84
+ return withRuntimeFileLock(path, async () => {
85
+ const next = await mutate(await readRuntimeJson(path, fallback));
86
+ await writeRuntimeJson(path, next);
87
+ return next;
88
+ });
89
+ }
90
+ export async function removeManagedPath(path, expectedRoot) {
91
+ const target = resolve(path);
92
+ const root = resolve(expectedRoot);
93
+ const rel = target.slice(root.length);
94
+ if (target === root || !target.startsWith(`${root}/`) || rel.includes('\0')) {
95
+ throw new Error(`拒绝清理托管根目录之外的路径:${target}`);
96
+ }
97
+ await rm(target, { recursive: true, force: true });
98
+ }
99
+ export async function runtimeFileVersion(path) {
100
+ const info = await stat(path);
101
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}`;
102
+ }
103
+ const activeOutputs = new Set();
104
+ const OUTPUT_RETENTION_MS = 7 * 24 * 60 * 60_000;
105
+ const OUTPUT_GLOBAL_LIMIT = 5 * 1024 * 1024 * 1024;
106
+ let pruneOutputsPromise;
107
+ export function registerRuntimeOutput(path) {
108
+ activeOutputs.add(resolve(path));
109
+ }
110
+ export function unregisterRuntimeOutput(path) {
111
+ activeOutputs.delete(resolve(path));
112
+ }
113
+ export function pruneRuntimeOutputs() {
114
+ pruneOutputsPromise ??= (async () => {
115
+ const root = join(agentStateDir(), 'runtime');
116
+ const files = [];
117
+ for (const workspace of await readdir(root).catch(() => [])) {
118
+ const workspaceDir = join(root, workspace);
119
+ for (const session of await readdir(workspaceDir).catch(() => [])) {
120
+ const outputDir = join(workspaceDir, session, 'outputs');
121
+ const sessionFiles = [];
122
+ for (const name of await readdir(outputDir).catch(() => [])) {
123
+ const path = join(outputDir, name);
124
+ const info = await stat(path).catch(() => undefined);
125
+ if (info?.isFile() && !activeOutputs.has(resolve(path))) {
126
+ sessionFiles.push({ path, size: info.size, mtimeMs: info.mtimeMs });
127
+ }
128
+ }
129
+ sessionFiles.sort((left, right) => right.mtimeMs - left.mtimeMs);
130
+ await Promise.all(sessionFiles
131
+ .slice(50)
132
+ .map((file) => rm(file.path, { force: true }).catch(() => undefined)));
133
+ files.push(...sessionFiles.slice(0, 50));
134
+ }
135
+ }
136
+ files.sort((left, right) => right.mtimeMs - left.mtimeMs);
137
+ let retainedBytes = 0;
138
+ const cutoff = Date.now() - OUTPUT_RETENTION_MS;
139
+ for (const file of files) {
140
+ retainedBytes += file.size;
141
+ if (file.mtimeMs < cutoff || retainedBytes > OUTPUT_GLOBAL_LIMIT) {
142
+ await rm(file.path, { force: true }).catch(() => undefined);
143
+ }
144
+ }
145
+ })().finally(() => {
146
+ pruneOutputsPromise = undefined;
147
+ });
148
+ return pruneOutputsPromise;
149
+ }
@@ -0,0 +1,197 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import {
3
+ mkdir,
4
+ open,
5
+ readFile,
6
+ readdir,
7
+ rename,
8
+ rm,
9
+ stat,
10
+ } from 'node:fs/promises';
11
+ import { dirname, join, resolve } from 'node:path';
12
+
13
+ import { agentStateDir } from '../context/agentHome.js';
14
+
15
+ export interface RuntimeIdentity {
16
+ workspaceId?: string;
17
+ sessionId?: string;
18
+ }
19
+
20
+ function segment(value: string | undefined, fallback: string): string {
21
+ return (
22
+ (value ?? fallback).replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 120) ||
23
+ fallback
24
+ );
25
+ }
26
+
27
+ export function sessionRuntimeDir(identity: RuntimeIdentity): string {
28
+ return join(
29
+ agentStateDir(),
30
+ 'runtime',
31
+ segment(identity.workspaceId, 'local'),
32
+ segment(identity.sessionId, 'local'),
33
+ );
34
+ }
35
+
36
+ const queues = new Map<string, Promise<unknown>>();
37
+ export async function withRuntimeFileLock<T>(
38
+ path: string,
39
+ work: () => Promise<T>,
40
+ ): Promise<T> {
41
+ const key = resolve(path);
42
+ const previous = queues.get(key) ?? Promise.resolve();
43
+ let release!: () => void;
44
+ const current = new Promise<void>((done) => {
45
+ release = done;
46
+ });
47
+ const tail = previous.then(() => current);
48
+ queues.set(key, tail);
49
+ await previous.catch(() => undefined);
50
+ let lockHandle: Awaited<ReturnType<typeof open>> | undefined;
51
+ const lockPath = `${path}.lock`;
52
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
53
+ try {
54
+ const deadline = Date.now() + 10_000;
55
+ while (!lockHandle) {
56
+ try {
57
+ lockHandle = await open(lockPath, 'wx', 0o600);
58
+ await lockHandle.writeFile(`${process.pid}\n${Date.now()}\n`);
59
+ } catch (error) {
60
+ if ((error as NodeJS.ErrnoException).code !== 'EEXIST') throw error;
61
+ const age =
62
+ Date.now() -
63
+ (await stat(lockPath).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
64
+ if (age > 30_000) await rm(lockPath, { force: true });
65
+ else if (Date.now() >= deadline)
66
+ throw new Error(`runtime lock timeout: ${path}`);
67
+ else await new Promise((r) => setTimeout(r, 25));
68
+ }
69
+ }
70
+ return await work();
71
+ } finally {
72
+ await lockHandle?.close().catch(() => undefined);
73
+ if (lockHandle) await rm(lockPath, { force: true }).catch(() => undefined);
74
+ release();
75
+ if (queues.get(key) === tail) queues.delete(key);
76
+ }
77
+ }
78
+
79
+ export async function readRuntimeJson<T>(
80
+ path: string,
81
+ fallback: T,
82
+ ): Promise<T> {
83
+ try {
84
+ return JSON.parse(await readFile(path, 'utf8')) as T;
85
+ } catch {
86
+ return fallback;
87
+ }
88
+ }
89
+
90
+ export async function writeRuntimeJson(
91
+ path: string,
92
+ value: unknown,
93
+ ): Promise<void> {
94
+ await mkdir(dirname(path), { recursive: true, mode: 0o700 });
95
+ const temp = `${path}.${randomUUID()}.tmp`;
96
+ try {
97
+ const handle = await open(temp, 'wx', 0o600);
98
+ try {
99
+ await handle.writeFile(`${JSON.stringify(value, null, 2)}\n`, 'utf8');
100
+ await handle.sync();
101
+ } finally {
102
+ await handle.close();
103
+ }
104
+ await rename(temp, path);
105
+ } finally {
106
+ await rm(temp, { force: true }).catch(() => undefined);
107
+ }
108
+ }
109
+
110
+ export async function mutateRuntimeJson<T>(
111
+ path: string,
112
+ fallback: T,
113
+ mutate: (value: T) => T | Promise<T>,
114
+ ): Promise<T> {
115
+ return withRuntimeFileLock(path, async () => {
116
+ const next = await mutate(await readRuntimeJson(path, fallback));
117
+ await writeRuntimeJson(path, next);
118
+ return next;
119
+ });
120
+ }
121
+
122
+ export async function removeManagedPath(
123
+ path: string,
124
+ expectedRoot: string,
125
+ ): Promise<void> {
126
+ const target = resolve(path);
127
+ const root = resolve(expectedRoot);
128
+ const rel = target.slice(root.length);
129
+ if (target === root || !target.startsWith(`${root}/`) || rel.includes('\0')) {
130
+ throw new Error(`拒绝清理托管根目录之外的路径:${target}`);
131
+ }
132
+ await rm(target, { recursive: true, force: true });
133
+ }
134
+
135
+ export async function runtimeFileVersion(path: string): Promise<string> {
136
+ const info = await stat(path);
137
+ return `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}`;
138
+ }
139
+
140
+ const activeOutputs = new Set<string>();
141
+ const OUTPUT_RETENTION_MS = 7 * 24 * 60 * 60_000;
142
+ const OUTPUT_GLOBAL_LIMIT = 5 * 1024 * 1024 * 1024;
143
+ let pruneOutputsPromise: Promise<void> | undefined;
144
+
145
+ export function registerRuntimeOutput(path: string): void {
146
+ activeOutputs.add(resolve(path));
147
+ }
148
+
149
+ export function unregisterRuntimeOutput(path: string): void {
150
+ activeOutputs.delete(resolve(path));
151
+ }
152
+
153
+ export function pruneRuntimeOutputs(): Promise<void> {
154
+ pruneOutputsPromise ??= (async () => {
155
+ const root = join(agentStateDir(), 'runtime');
156
+ const files: Array<{ path: string; size: number; mtimeMs: number }> = [];
157
+ for (const workspace of await readdir(root).catch(() => [])) {
158
+ const workspaceDir = join(root, workspace);
159
+ for (const session of await readdir(workspaceDir).catch(() => [])) {
160
+ const outputDir = join(workspaceDir, session, 'outputs');
161
+ const sessionFiles: Array<{
162
+ path: string;
163
+ size: number;
164
+ mtimeMs: number;
165
+ }> = [];
166
+ for (const name of await readdir(outputDir).catch(() => [])) {
167
+ const path = join(outputDir, name);
168
+ const info = await stat(path).catch(() => undefined);
169
+ if (info?.isFile() && !activeOutputs.has(resolve(path))) {
170
+ sessionFiles.push({ path, size: info.size, mtimeMs: info.mtimeMs });
171
+ }
172
+ }
173
+ sessionFiles.sort((left, right) => right.mtimeMs - left.mtimeMs);
174
+ await Promise.all(
175
+ sessionFiles
176
+ .slice(50)
177
+ .map((file) =>
178
+ rm(file.path, { force: true }).catch(() => undefined),
179
+ ),
180
+ );
181
+ files.push(...sessionFiles.slice(0, 50));
182
+ }
183
+ }
184
+ files.sort((left, right) => right.mtimeMs - left.mtimeMs);
185
+ let retainedBytes = 0;
186
+ const cutoff = Date.now() - OUTPUT_RETENTION_MS;
187
+ for (const file of files) {
188
+ retainedBytes += file.size;
189
+ if (file.mtimeMs < cutoff || retainedBytes > OUTPUT_GLOBAL_LIMIT) {
190
+ await rm(file.path, { force: true }).catch(() => undefined);
191
+ }
192
+ }
193
+ })().finally(() => {
194
+ pruneOutputsPromise = undefined;
195
+ });
196
+ return pruneOutputsPromise;
197
+ }