minimal-agent 0.11.0 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/package.json +4 -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/index.js +5 -3
  7. package/plugins/workflow-runner/src/index.ts +31 -13
  8. package/plugins/workflow-runner/src/multimodal.js +2 -1
  9. package/plugins/workflow-runner/src/multimodal.ts +2 -1
  10. package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
  11. package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
  12. package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
  13. package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
  14. package/plugins/workflow-runner/src/toolRuntime.js +48 -0
  15. package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
  16. package/plugins/workflow-runner/src/types.ts +26 -7
  17. package/plugins/workflow-runner/test/context-files.test.ts +47 -16
  18. package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
  19. package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
  20. package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
  21. package/src/cli/print.js +19 -4
  22. package/src/cli/print.ts +17 -4
  23. package/src/cli/streamJson.js +19 -4
  24. package/src/cli/streamJson.ts +17 -4
  25. package/src/config/configDefaults.js +25 -0
  26. package/src/config/configDefaults.ts +33 -0
  27. package/src/config/configFile.js +2 -21
  28. package/src/config/configFile.ts +11 -24
  29. package/src/config/configManagement.js +1 -1
  30. package/src/config/configManagement.ts +2 -2
  31. package/src/context/compact.js +4 -0
  32. package/src/context/compact.ts +4 -0
  33. package/src/context/contextBundle.js +38 -13
  34. package/src/context/contextBundle.ts +47 -13
  35. package/src/harness/stallGuard.js +19 -4
  36. package/src/harness/stallGuard.ts +30 -7
  37. package/src/loop.js +95 -11
  38. package/src/loop.ts +125 -16
  39. package/src/main.js +8 -1
  40. package/src/main.tsx +7 -1
  41. package/src/memory/constants.js +10 -0
  42. package/src/memory/constants.ts +13 -0
  43. package/src/memory/episodicMemory.js +274 -2
  44. package/src/memory/episodicMemory.ts +306 -1
  45. package/src/memory/fileStore.js +24 -4
  46. package/src/memory/fileStore.ts +22 -2
  47. package/src/memory/memoryService.js +79 -12
  48. package/src/memory/memoryService.ts +108 -9
  49. package/src/memory/repository.js +415 -85
  50. package/src/memory/repository.ts +437 -86
  51. package/src/memory/turnJournal.js +95 -0
  52. package/src/memory/turnJournal.ts +120 -0
  53. package/src/memory/types.ts +16 -0
  54. package/src/plugins/pluginApi.ts +30 -6
  55. package/src/plugins/pluginRunner.js +6 -2
  56. package/src/plugins/pluginRunner.ts +12 -7
  57. package/src/prompts/policyRegistry.js +27 -0
  58. package/src/prompts/policyRegistry.ts +27 -0
  59. package/src/prompts/promptAssembly.js +39 -7
  60. package/src/prompts/promptAssembly.ts +145 -59
  61. package/src/session/runAgentTurn.js +43 -21
  62. package/src/session/runAgentTurn.ts +68 -26
  63. package/src/session/runTurn.js +11 -46
  64. package/src/session/runTurn.ts +12 -46
  65. package/src/session/runtimeStore.js +149 -0
  66. package/src/session/runtimeStore.ts +197 -0
  67. package/src/session/sessionState.js +5 -0
  68. package/src/session/sessionState.ts +14 -0
  69. package/src/tools/agent/agent.js +143 -14
  70. package/src/tools/agent/agent.ts +149 -14
  71. package/src/tools/agent/agentTaskService.js +60 -0
  72. package/src/tools/agent/agentTaskService.ts +113 -0
  73. package/src/tools/agent/agentTypes.js +5 -0
  74. package/src/tools/agent/agentTypes.ts +5 -0
  75. package/src/tools/bash/bash.js +31 -11
  76. package/src/tools/bash/bash.ts +29 -10
  77. package/src/tools/capabilities.js +5 -0
  78. package/src/tools/capabilities.ts +11 -1
  79. package/src/tools/edit/edit.js +7 -4
  80. package/src/tools/edit/edit.ts +7 -4
  81. package/src/tools/edit/multi-edit.js +4 -2
  82. package/src/tools/edit/multi-edit.ts +4 -2
  83. package/src/tools/glob/glob.js +156 -40
  84. package/src/tools/glob/glob.ts +185 -46
  85. package/src/tools/index.js +35 -17
  86. package/src/tools/index.ts +81 -20
  87. package/src/tools/lsp/lsp.js +165 -22
  88. package/src/tools/lsp/lsp.ts +206 -36
  89. package/src/tools/powershell/powershell.js +116 -26
  90. package/src/tools/powershell/powershell.ts +155 -30
  91. package/src/tools/presentresult/presentresult.js +10 -0
  92. package/src/tools/presentresult/presentresult.ts +10 -0
  93. package/src/tools/read/read.js +244 -29
  94. package/src/tools/read/read.ts +391 -59
  95. package/src/tools/shared/diffSummary.js +44 -0
  96. package/src/tools/shared/diffSummary.ts +63 -0
  97. package/src/tools/shared/fileActivity.js +2 -1
  98. package/src/tools/shared/fileActivity.ts +3 -0
  99. package/src/tools/shared/permanentDeleteGuard.js +26 -0
  100. package/src/tools/shared/permanentDeleteGuard.ts +34 -0
  101. package/src/tools/shared/toolOutputStore.js +97 -0
  102. package/src/tools/shared/toolOutputStore.ts +110 -0
  103. package/src/tools/skill/skill.js +86 -0
  104. package/src/tools/skill/skill.ts +94 -0
  105. package/src/tools/snip/snip.js +67 -0
  106. package/src/tools/snip/snip.ts +80 -0
  107. package/src/tools/tasks/backgroundTaskService.js +79 -28
  108. package/src/tools/tasks/backgroundTaskService.ts +165 -49
  109. package/src/tools/tasks/taskManagement.js +163 -0
  110. package/src/tools/tasks/taskManagement.ts +180 -0
  111. package/src/tools/tasks/taskOutput.js +64 -14
  112. package/src/tools/tasks/taskOutput.ts +102 -23
  113. package/src/tools/tasks/taskStop.js +48 -8
  114. package/src/tools/tasks/taskStop.ts +69 -9
  115. package/src/tools/tasks/taskStore.js +46 -0
  116. package/src/tools/tasks/taskStore.ts +77 -0
  117. package/src/tools/terminal/terminalCapture.js +167 -0
  118. package/src/tools/terminal/terminalCapture.ts +179 -0
  119. package/src/tools/trash/trash.js +233 -0
  120. package/src/tools/trash/trash.ts +283 -0
  121. package/src/tools/worktree/sessionWorktree.js +128 -0
  122. package/src/tools/worktree/sessionWorktree.ts +158 -0
  123. package/src/tools/write/write.js +6 -2
  124. package/src/tools/write/write.ts +6 -2
  125. package/src/types.ts +12 -0
  126. package/src/ui/hooks/useChat.js +31 -11
  127. package/src/ui/hooks/useChat.ts +31 -13
  128. package/src/web/protocol.ts +78 -3
@@ -29,6 +29,10 @@ import { getSkillList, formatSkillHint } from './skillList.js';
29
29
  import { getPluginMetaList, formatPluginHint } from './pluginList.js';
30
30
  import { memoryService } from '../memory/memoryService.js';
31
31
  import { countTextTokens } from '../context/tokenCounter.js';
32
+ import {
33
+ effectiveTaskStatus,
34
+ listManagedTasks,
35
+ } from '../tools/tasks/taskStore.js';
32
36
  import {
33
37
  assembleContextBundle,
34
38
  contextFragment,
@@ -77,6 +81,7 @@ export interface PromptRuntimeContext {
77
81
  surface: PromptSurface;
78
82
  cwd: string;
79
83
  workspaceId?: string;
84
+ sessionId?: string;
80
85
  interactiveAsk: boolean;
81
86
  supervisedBrowser: boolean;
82
87
  /** Trusted host decision. Defaults to denied; never inferred from profile. */
@@ -128,18 +133,22 @@ async function buildAdvisorStatusLine(): Promise<string> {
128
133
  const advisor = await configService.advisor();
129
134
  if (!advisor) return 'Advisor 状态:未配置。';
130
135
  const mode = advisor.mode === 'auto' ? '自动' : '仅手动';
131
- const vision = advisor.supportsVision && advisor.shareSensitive
132
- ? '允许图片'
133
- : advisor.supportsVision
134
- ? '支持图片但敏感共享关闭'
135
- : '不支持图片';
136
+ const vision =
137
+ advisor.supportsVision && advisor.shareSensitive
138
+ ? '允许图片'
139
+ : advisor.supportsVision
140
+ ? '支持图片但敏感共享关闭'
141
+ : '不支持图片';
136
142
  return `Advisor 状态:${advisor.enabled ? '已启用' : '已关闭'},${advisor.provider.name}/${advisor.provider.model},${mode},${vision},超时 ${Math.round(advisor.timeoutMs / 1000)} 秒。`;
137
143
  } catch {
138
144
  return 'Advisor 状态:暂时无法读取。';
139
145
  }
140
146
  }
141
147
 
142
- async function buildToolLayer(tools: Tool[], context: StablePromptContext): Promise<string> {
148
+ async function buildToolLayer(
149
+ tools: Tool[],
150
+ context: StablePromptContext,
151
+ ): Promise<string> {
143
152
  const catalog = tools.map((tool) => tool.name).join(', ');
144
153
  const rules = renderCapabilityPolicyLayer(context);
145
154
 
@@ -162,11 +171,14 @@ export interface AssemblePromptOptions {
162
171
  async function buildMemoryTransient(
163
172
  runtime: PromptRuntimeContext,
164
173
  tools: Tool[],
165
- ): Promise<Awaited<ReturnType<typeof memoryService.buildContextBundle>> | null> {
174
+ ): Promise<Awaited<
175
+ ReturnType<typeof memoryService.buildContextBundle>
176
+ > | null> {
166
177
  const names = new Set(tools.map((tool) => tool.name));
167
- const memoryAllowed = runtime.memoryAccess !== undefined && runtime.memoryAccess !== 'denied'
178
+ const memoryAllowed =
179
+ (runtime.memoryAccess !== undefined && runtime.memoryAccess !== 'denied') ||
168
180
  // Compatibility is explicit-only: omitted ownerLocalSession never grants access.
169
- || (runtime.memoryAccess === undefined && runtime.ownerLocalSession === true);
181
+ (runtime.memoryAccess === undefined && runtime.ownerLocalSession === true);
170
182
  if (!memoryAllowed || !names.has('RecallMemory')) return null;
171
183
  try {
172
184
  // One repository snapshot feeds persona/preferences/relevance/diagnostics.
@@ -180,8 +192,12 @@ async function buildMemoryTransient(
180
192
  }
181
193
  }
182
194
 
183
- async function buildAdvisorTransient(profile: PromptProfile, tools: Tool[]): Promise<string> {
184
- if (profile !== 'main' || !tools.some((tool) => tool.name === 'Advisor')) return '';
195
+ async function buildAdvisorTransient(
196
+ profile: PromptProfile,
197
+ tools: Tool[],
198
+ ): Promise<string> {
199
+ if (profile !== 'main' || !tools.some((tool) => tool.name === 'Advisor'))
200
+ return '';
185
201
  return buildAdvisorStatusLine();
186
202
  }
187
203
 
@@ -280,7 +296,9 @@ export async function assembleStablePrompt(
280
296
  ].join('|');
281
297
  const cached = stablePromptCache.get(key);
282
298
  if (cached) return cached;
283
- const base = (await buildStableBase(options.cwd, options.tools, profile, runtime)).base;
299
+ const base = (
300
+ await buildStableBase(options.cwd, options.tools, profile, runtime)
301
+ ).base;
284
302
  stablePromptCache.set(key, base);
285
303
  if (stablePromptCache.size > 64) {
286
304
  stablePromptCache.delete(stablePromptCache.keys().next().value!);
@@ -310,15 +328,22 @@ export async function buildTransientBundle(
310
328
  runtime: PromptRuntimeContext,
311
329
  profile: PromptProfile,
312
330
  tools: Tool[],
313
- options: Pick<AssemblePromptOptions, 'includeProjectInstructions' | 'includePlugins'> = {},
331
+ options: Pick<
332
+ AssemblePromptOptions,
333
+ 'includeProjectInstructions' | 'includePlugins'
334
+ > = {},
314
335
  ): Promise<ContextBundle> {
315
336
  const runtimeLines = [
316
337
  `【运行时状态】本地日期:${localDate()}。时效敏感信息必须在线核实并使用明确日期或年份。`,
317
338
  runtime.supervisedBrowser
318
339
  ? '宿主提供可监督浏览器:调用 WebBrowser 时用户可看到过程并在验证或登录环节接管。'
319
340
  : '',
320
- runtime.interactiveAsk ? '' : '当前宿主不能交互提问;不要等待用户选择,缺少关键输入时说明阻塞。',
321
- ].filter(Boolean).join('\n');
341
+ runtime.interactiveAsk
342
+ ? ''
343
+ : '当前宿主不能交互提问;不要等待用户选择,缺少关键输入时说明阻塞。',
344
+ ]
345
+ .filter(Boolean)
346
+ .join('\n');
322
347
  const fragments: ContextFragment[] = [
323
348
  contextFragment({
324
349
  id: 'runtime.host-state',
@@ -329,7 +354,14 @@ export async function buildTransientBundle(
329
354
  content: runtimeLines,
330
355
  }),
331
356
  ];
332
- const [memory, advisor, skillHint, pluginHint, projectInstructions] = await Promise.all([
357
+ const [
358
+ memory,
359
+ advisor,
360
+ skillHint,
361
+ pluginHint,
362
+ projectInstructions,
363
+ activeTasks,
364
+ ] = await Promise.all([
333
365
  buildMemoryTransient(runtime, tools),
334
366
  buildAdvisorTransient(profile, tools),
335
367
  getSkillList(runtime.cwd, new Set(tools.map((tool) => tool.name)))
@@ -337,54 +369,102 @@ export async function buildTransientBundle(
337
369
  .catch(() => ''),
338
370
  options.includePlugins === false
339
371
  ? Promise.resolve('')
340
- : getPluginMetaList(runtime.cwd).then(formatPluginHint).catch(() => ''),
372
+ : getPluginMetaList(runtime.cwd)
373
+ .then(formatPluginHint)
374
+ .catch(() => ''),
341
375
  options.includeProjectInstructions === false
342
376
  ? Promise.resolve('')
343
377
  : loadProjectPromptInstructions(runtime.cwd).catch(() => ''),
378
+ runtime.sessionId &&
379
+ tools.some((tool) =>
380
+ ['TaskCreate', 'TaskGet', 'TaskUpdate', 'TaskList'].includes(tool.name),
381
+ )
382
+ ? listManagedTasks({
383
+ workspaceId: runtime.workspaceId,
384
+ sessionId: runtime.sessionId,
385
+ cwd: runtime.cwd,
386
+ }).catch(() => [])
387
+ : Promise.resolve([]),
344
388
  ]);
345
389
  if (memory) {
346
- const priorities = { persona: 82, preferences: 78, relevant: 70, health: 10 } as const;
390
+ const priorities = {
391
+ persona: 82,
392
+ preferences: 78,
393
+ relevant: 70,
394
+ health: 10,
395
+ } as const;
347
396
  for (const section of memory.sections) {
348
- fragments.push(contextFragment({
349
- id: `memory.${section.id}`,
350
- kind: section.id === 'persona' ? 'persona' : 'memory',
351
- authority: section.id === 'health' ? 'host' : 'user-evidence',
352
- priority: priorities[section.id],
353
- estimatedTokens: section.estimatedTokens,
354
- content: section.content,
355
- }));
397
+ fragments.push(
398
+ contextFragment({
399
+ id: `memory.${section.id}`,
400
+ kind: section.id === 'persona' ? 'persona' : 'memory',
401
+ authority: section.id === 'health' ? 'host' : 'user-evidence',
402
+ priority: priorities[section.id],
403
+ estimatedTokens: section.estimatedTokens,
404
+ content: section.content,
405
+ }),
406
+ );
356
407
  }
357
408
  }
358
- if (advisor) fragments.push(contextFragment({
359
- id: 'diagnostic.advisor',
360
- kind: 'diagnostic',
361
- authority: 'host',
362
- priority: 20,
363
- content: advisor,
364
- }));
365
- if (skillHint) fragments.push(contextFragment({
366
- id: 'skills.catalog',
367
- kind: 'skill',
368
- authority: 'constraint',
369
- priority: 45,
370
- clippable: true,
371
- content: skillHint,
372
- }));
373
- if (pluginHint) fragments.push(contextFragment({
374
- id: 'plugins.catalog',
375
- kind: 'plugin',
376
- authority: 'untrusted-data',
377
- priority: 50,
378
- content: pluginHint,
379
- }));
380
- if (projectInstructions) fragments.push(contextFragment({
381
- id: 'project.instructions',
382
- kind: 'project',
383
- authority: 'constraint',
384
- priority: 90,
385
- required: true,
386
- content: `# 项目指令\n以下内容来自当前工作目录的 minimal-agent.md。行为约束和仓库约定必须遵守;架构事实若与当前代码冲突,以实际代码为准。不要擅自重写该文件。\n\n${projectInstructions}`,
387
- }));
409
+ if (advisor)
410
+ fragments.push(
411
+ contextFragment({
412
+ id: 'diagnostic.advisor',
413
+ kind: 'diagnostic',
414
+ authority: 'host',
415
+ priority: 20,
416
+ content: advisor,
417
+ }),
418
+ );
419
+ if (skillHint)
420
+ fragments.push(
421
+ contextFragment({
422
+ id: 'skills.catalog',
423
+ kind: 'skill',
424
+ authority: 'constraint',
425
+ priority: 45,
426
+ clippable: true,
427
+ content: skillHint,
428
+ }),
429
+ );
430
+ if (pluginHint)
431
+ fragments.push(
432
+ contextFragment({
433
+ id: 'plugins.catalog',
434
+ kind: 'plugin',
435
+ authority: 'untrusted-data',
436
+ priority: 50,
437
+ content: pluginHint,
438
+ }),
439
+ );
440
+ if (projectInstructions)
441
+ fragments.push(
442
+ contextFragment({
443
+ id: 'project.instructions',
444
+ kind: 'project',
445
+ authority: 'constraint',
446
+ priority: 90,
447
+ required: true,
448
+ content: `# 项目指令\n以下内容来自当前工作目录的 minimal-agent.md。行为约束和仓库约定必须遵守;架构事实若与当前代码冲突,以实际代码为准。不要擅自重写该文件。\n\n${projectInstructions}`,
449
+ }),
450
+ );
451
+ const unfinished = activeTasks.filter((task) => task.status !== 'completed');
452
+ if (unfinished.length)
453
+ fragments.push(
454
+ contextFragment({
455
+ id: 'tasks.active',
456
+ kind: 'task',
457
+ authority: 'constraint',
458
+ priority: 85,
459
+ content: `# Active tasks\n${unfinished
460
+ .slice(0, 20)
461
+ .map(
462
+ (task) =>
463
+ `- ${task.id} [${effectiveTaskStatus(task, activeTasks)}] ${task.title}`,
464
+ )
465
+ .join('\n')}`,
466
+ }),
467
+ );
388
468
  return assembleContextBundle(
389
469
  fragments,
390
470
  runtime.transientBudgetTokens ?? DEFAULT_TRANSIENT_BUDGET_TOKENS,
@@ -397,12 +477,16 @@ export async function buildTransientBundle(
397
477
  * 直接调 assemblePromptEnvelope 再取 .transient 也能拿到同样结果,但会白算一遍 base
398
478
  * (27 个 SKILL.md + 项目指令共约 40 次文件读)。每回合都白算一遍不划算,故单独开口。
399
479
  */
400
- export async function buildTransientContext(options: AssemblePromptOptions): Promise<string> {
480
+ export async function buildTransientContext(
481
+ options: AssemblePromptOptions,
482
+ ): Promise<string> {
401
483
  return (await buildTransientPromptBundle(options)).text;
402
484
  }
403
485
 
404
486
  /** Structured counterpart used by the Harness for request-time budget reduction. */
405
- export async function buildTransientPromptBundle(options: AssemblePromptOptions): Promise<ContextBundle> {
487
+ export async function buildTransientPromptBundle(
488
+ options: AssemblePromptOptions,
489
+ ): Promise<ContextBundle> {
406
490
  const profile = options.profile ?? 'main';
407
491
  const runtime: PromptRuntimeContext = {
408
492
  surface: profile === 'main' ? 'cli' : 'subagent',
@@ -418,7 +502,9 @@ export async function buildTransientPromptBundle(options: AssemblePromptOptions)
418
502
  });
419
503
  }
420
504
 
421
- export async function assembleBasePrompt(options: AssemblePromptOptions): Promise<string> {
505
+ export async function assembleBasePrompt(
506
+ options: AssemblePromptOptions,
507
+ ): Promise<string> {
422
508
  const envelope = await assemblePromptEnvelope(options);
423
509
  return [envelope.base, envelope.transient].filter(Boolean).join('\n\n');
424
510
  }
@@ -1,10 +1,11 @@
1
- import { buildBaseSystemPrompt, buildTransientPromptBundle } from '../prompts/system.js';
1
+ import { buildBaseSystemPrompt, buildTransientPromptBundle, } from '../prompts/system.js';
2
2
  import { ALL_TOOLS } from '../tools/index.js';
3
3
  import { sessionFileFor } from '../context/sessionPath.js';
4
- import { getOrCreateSessionState, sessionStateKey } from './sessionState.js';
4
+ import { getOrCreateSessionState, sessionStateKey, } from './sessionState.js';
5
+ import { hydrateSessionWorktree } from '../tools/worktree/sessionWorktree.js';
5
6
  import { runTurn } from './runTurn.js';
6
7
  import { withWorkingDir } from '../bootstrap/workingDir.js';
7
- import { listSessionSummaries, updateSessionMetadata } from '../context/sessionRegistry.js';
8
+ import { listSessionSummaries, updateSessionMetadata, } from '../context/sessionRegistry.js';
8
9
  import { workspaceIndexService } from '../context/workspaceIndex.js';
9
10
  import { configService } from '../config/configService.js';
10
11
  import { createPromptCacheContext } from '../llm/promptCache.js';
@@ -17,9 +18,12 @@ deps = {}) {
17
18
  const session = (await listSessionSummaries(opts.cwd)).find((item) => item.id === opts.sessionId);
18
19
  let sessionModelId = session?.modelId;
19
20
  if (!opts.model && !sessionModelId) {
20
- sessionModelId = (await configService.getSnapshot()).config?.baseModelId ?? undefined;
21
+ sessionModelId =
22
+ (await configService.getSnapshot()).config?.baseModelId ?? undefined;
21
23
  if (sessionModelId) {
22
- await updateSessionMetadata(opts.cwd, opts.sessionId, { modelId: sessionModelId }).catch(() => { });
24
+ await updateSessionMetadata(opts.cwd, opts.sessionId, {
25
+ modelId: sessionModelId,
26
+ }).catch(() => { });
23
27
  }
24
28
  }
25
29
  const sessionProvider = !opts.model && sessionModelId
@@ -27,41 +31,62 @@ deps = {}) {
27
31
  : null;
28
32
  const provider = opts.model
29
33
  ? { ...opts.provider, model: opts.model }
30
- : sessionProvider ?? opts.provider;
31
- const workspace = await workspaceIndexService.register(opts.cwd).catch(() => undefined);
34
+ : (sessionProvider ?? opts.provider);
35
+ const workspace = await workspaceIndexService
36
+ .register(opts.cwd)
37
+ .catch(() => undefined);
32
38
  const sessionFile = sessionFileFor(opts.cwd, opts.sessionId);
33
39
  const events = [];
34
40
  let result = '';
35
41
  let error;
36
42
  let numTurns = 0;
37
- await withWorkingDir(opts.cwd, async () => {
43
+ const turnSessionState = opts.sessionState ??
44
+ getOrCreateSessionState(sessionStateKey({
45
+ surface: 'headless',
46
+ workspaceId: workspace?.id,
47
+ cwd: workspace?.id ? undefined : opts.cwd,
48
+ sessionId: opts.sessionId,
49
+ }));
50
+ await hydrateSessionWorktree({
51
+ sessionState: turnSessionState,
52
+ workspaceId: workspace?.id,
53
+ sessionId: opts.sessionId,
54
+ cwd: opts.cwd,
55
+ });
56
+ const effectiveCwd = turnSessionState.activeWorktree?.path ?? opts.cwd;
57
+ await withWorkingDir(effectiveCwd, async () => {
38
58
  // 稳定层进 history[0](runTurn 每轮重写它,必须逐字节可复现,见 runTurn.ts 的缓存不变量);
39
59
  // 易变层每轮单独计算,仅附加到当前用户消息的模型副本。
40
60
  const runtimeContext = createAgentRuntimeContext({
41
61
  surface: 'headless',
42
62
  profile: 'main',
43
- cwd: opts.cwd,
63
+ cwd: effectiveCwd,
44
64
  workspaceId: workspace?.id,
45
65
  sessionId: opts.sessionId,
46
- owner: opts.memoryAccess && opts.memoryAccess !== 'denied' ? 'local-owner' : 'untrusted',
66
+ owner: opts.memoryAccess && opts.memoryAccess !== 'denied'
67
+ ? 'local-owner'
68
+ : 'untrusted',
47
69
  memoryAccess: opts.memoryAccess ?? 'denied',
48
70
  interactiveAsk: false,
49
71
  });
50
72
  const turnTools = filterToolsForRuntime(ALL_TOOLS, runtimeContext);
51
- const systemPrompt = await buildBaseSystemPrompt(opts.cwd, turnTools);
73
+ const systemPrompt = await buildBaseSystemPrompt(effectiveCwd, turnTools);
52
74
  const transientBundle = await buildTransientPromptBundle({
53
- cwd: opts.cwd,
75
+ cwd: effectiveCwd,
54
76
  tools: turnTools,
55
77
  runtime: {
56
78
  surface: 'headless',
57
- cwd: opts.cwd,
79
+ cwd: effectiveCwd,
58
80
  workspaceId: workspace?.id,
81
+ sessionId: opts.sessionId,
59
82
  interactiveAsk: false,
60
83
  memoryAccess: runtimeContext.memoryAccess,
61
84
  currentUserInput: opts.input,
62
85
  },
63
86
  }).catch(() => undefined);
64
- await updateSessionMetadata(opts.cwd, opts.sessionId, { lastMessagePreview: opts.input }).catch(() => { });
87
+ await updateSessionMetadata(opts.cwd, opts.sessionId, {
88
+ lastMessagePreview: opts.input,
89
+ }).catch(() => { });
65
90
  for await (const ev of run({
66
91
  userInput: opts.input,
67
92
  provider,
@@ -69,12 +94,7 @@ deps = {}) {
69
94
  systemPrompt,
70
95
  ...(transientBundle ? { transientBundle } : {}),
71
96
  promptCache: createPromptCacheContext(`workspace:${workspace?.id ?? opts.cwd}:session:${opts.sessionId}`),
72
- sessionState: opts.sessionState ?? getOrCreateSessionState(sessionStateKey({
73
- surface: runtimeContext.surface,
74
- workspaceId: runtimeContext.workspaceId,
75
- cwd: opts.cwd,
76
- sessionId: opts.sessionId,
77
- })),
97
+ sessionState: turnSessionState,
78
98
  signal: opts.signal,
79
99
  maxTurns: opts.maxTurns,
80
100
  runtimeContext,
@@ -103,5 +123,7 @@ deps = {}) {
103
123
  };
104
124
  }
105
125
  function isEventLike(ev) {
106
- return Boolean(ev && typeof ev === 'object' && typeof ev.type === 'string');
126
+ return Boolean(ev &&
127
+ typeof ev === 'object' &&
128
+ typeof ev.type === 'string');
107
129
  }
@@ -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
  }