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,34 +1,74 @@
1
1
  import { z } from 'zod';
2
2
  import { toToolParameters } from '../../utils/zodToJson.js';
3
3
  import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
4
- import { stopBackgroundTask } from './backgroundTaskService.js';
4
+ import { stopBackgroundTask, } from './backgroundTaskService.js';
5
5
  import { processExecutionMetadata } from './processEvidence.js';
6
- const inputSchema = z.object({ task_id: z.string().uuid() });
7
- function call(input, state) {
6
+ import { getLiveAgentTask, loadAgentRecord, } from '../agent/agentTaskService.js';
7
+ const inputSchema = z.object({
8
+ task_id: z.string().regex(/^(?:proc_|agent_)?[0-9a-f-]{36}$/i),
9
+ });
10
+ async function call(input, state, context) {
11
+ if (input.task_id.startsWith('agent_') && context) {
12
+ const live = getLiveAgentTask(context.sessionState?.agentTasks, input.task_id);
13
+ if (live?.status === 'running') {
14
+ live.abort.abort();
15
+ await Promise.race([
16
+ live.completion,
17
+ new Promise((r) => setTimeout(r, 5000)),
18
+ ]);
19
+ return {
20
+ ok: true,
21
+ content: `已停止 Agent ${input.task_id}`,
22
+ metadata: {
23
+ kind: 'agent',
24
+ agentId: live.id,
25
+ status: 'stopped',
26
+ agentType: live.input.agent_type ?? 'explore',
27
+ description: live.description,
28
+ isolation: live.input.isolation ?? 'none',
29
+ turns: 0,
30
+ toolCalls: 0,
31
+ failedToolCalls: 0,
32
+ durationMs: Date.now() - live.createdAt,
33
+ changedFiles: [],
34
+ },
35
+ };
36
+ }
37
+ const saved = await loadAgentRecord(context, input.task_id);
38
+ return saved
39
+ ? { ok: true, content: `Agent ${input.task_id} 已经是 ${saved.status}` }
40
+ : { ok: false, error: `当前会话中不存在 Agent ${input.task_id}` };
41
+ }
8
42
  const task = stopBackgroundTask(input.task_id, state);
9
43
  if (!task)
10
44
  return { ok: false, error: `当前会话中不存在后台任务 ${input.task_id}` };
11
45
  return {
12
46
  ok: true,
13
- content: task.status === 'stopped' ? `已停止后台任务 ${task.id}` : `后台任务 ${task.id} 已经是 ${task.status}`,
47
+ content: task.status === 'stopped'
48
+ ? `已停止后台任务 ${task.id}`
49
+ : `后台任务 ${task.id} 已经是 ${task.status}`,
14
50
  metadata: processExecutionMetadata({
15
51
  shell: task.shell,
16
52
  mode: 'background',
17
53
  command: task.command,
18
54
  taskId: task.id,
19
- status: task.status === 'completed' ? 'completed' : task.status === 'failed' ? 'failed' : 'stopped',
55
+ status: task.status === 'completed'
56
+ ? 'completed'
57
+ : task.status === 'failed'
58
+ ? 'failed'
59
+ : 'stopped',
20
60
  exitCode: task.exitCode,
21
61
  }),
22
62
  };
23
63
  }
24
64
  export const taskStopTool = {
25
65
  name: 'TaskStop',
26
- description: 'Stops one Bash or PowerShell background task owned by the current session, including its child process tree.',
66
+ description: 'Stops one Bash/PowerShell background process (including its process tree) or one running Agent task owned by the current session.',
27
67
  inputSchema,
28
68
  parameters: toToolParameters(inputSchema),
29
69
  isReadOnly: false,
30
70
  isConcurrencySafe: true,
31
71
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
32
- call: (input) => Promise.resolve(call(input)),
33
- callWithContext: (input, _signal, context) => Promise.resolve(call(input, context.sessionState?.backgroundTasks)),
72
+ call: (input) => call(input),
73
+ callWithContext: (input, _signal, context) => call(input, context.sessionState?.backgroundTasks, context),
34
74
  };
@@ -3,24 +3,82 @@ import { z } from 'zod';
3
3
  import type { Tool, ToolCallContext, ToolResult } from '../../types.js';
4
4
  import { toToolParameters } from '../../utils/zodToJson.js';
5
5
  import { DEFAULT_MAX_RESULT_SIZE_CHARS } from '../types.js';
6
- import { stopBackgroundTask, type BackgroundTaskState } from './backgroundTaskService.js';
6
+ import {
7
+ stopBackgroundTask,
8
+ type BackgroundTaskState,
9
+ } from './backgroundTaskService.js';
7
10
  import { processExecutionMetadata } from './processEvidence.js';
11
+ import {
12
+ getLiveAgentTask,
13
+ loadAgentRecord,
14
+ saveAgentRecord,
15
+ } from '../agent/agentTaskService.js';
8
16
 
9
- const inputSchema = z.object({ task_id: z.string().uuid() });
17
+ const inputSchema = z.object({
18
+ task_id: z.string().regex(/^(?:proc_|agent_)?[0-9a-f-]{36}$/i),
19
+ });
10
20
  type TaskStopInput = z.infer<typeof inputSchema>;
11
21
 
12
- function call(input: TaskStopInput, state?: BackgroundTaskState): ToolResult {
22
+ async function call(
23
+ input: TaskStopInput,
24
+ state?: BackgroundTaskState,
25
+ context?: ToolCallContext,
26
+ ): Promise<ToolResult> {
27
+ if (input.task_id.startsWith('agent_') && context) {
28
+ const live = getLiveAgentTask(
29
+ context.sessionState?.agentTasks,
30
+ input.task_id,
31
+ );
32
+ if (live?.status === 'running') {
33
+ live.abort.abort();
34
+ await Promise.race([
35
+ live.completion,
36
+ new Promise<void>((r) => setTimeout(r, 5000)),
37
+ ]);
38
+ return {
39
+ ok: true,
40
+ content: `已停止 Agent ${input.task_id}`,
41
+ metadata: {
42
+ kind: 'agent',
43
+ agentId: live.id,
44
+ status: 'stopped',
45
+ agentType:
46
+ (live.input.agent_type as 'explore' | 'general') ?? 'explore',
47
+ description: live.description,
48
+ isolation: (live.input.isolation as 'none' | 'worktree') ?? 'none',
49
+ turns: 0,
50
+ toolCalls: 0,
51
+ failedToolCalls: 0,
52
+ durationMs: Date.now() - live.createdAt,
53
+ changedFiles: [],
54
+ },
55
+ };
56
+ }
57
+ const saved = await loadAgentRecord(context, input.task_id);
58
+ return saved
59
+ ? { ok: true, content: `Agent ${input.task_id} 已经是 ${saved.status}` }
60
+ : { ok: false, error: `当前会话中不存在 Agent ${input.task_id}` };
61
+ }
13
62
  const task = stopBackgroundTask(input.task_id, state);
14
- if (!task) return { ok: false, error: `当前会话中不存在后台任务 ${input.task_id}` };
63
+ if (!task)
64
+ return { ok: false, error: `当前会话中不存在后台任务 ${input.task_id}` };
15
65
  return {
16
66
  ok: true,
17
- content: task.status === 'stopped' ? `已停止后台任务 ${task.id}` : `后台任务 ${task.id} 已经是 ${task.status}`,
67
+ content:
68
+ task.status === 'stopped'
69
+ ? `已停止后台任务 ${task.id}`
70
+ : `后台任务 ${task.id} 已经是 ${task.status}`,
18
71
  metadata: processExecutionMetadata({
19
72
  shell: task.shell,
20
73
  mode: 'background',
21
74
  command: task.command,
22
75
  taskId: task.id,
23
- status: task.status === 'completed' ? 'completed' : task.status === 'failed' ? 'failed' : 'stopped',
76
+ status:
77
+ task.status === 'completed'
78
+ ? 'completed'
79
+ : task.status === 'failed'
80
+ ? 'failed'
81
+ : 'stopped',
24
82
  exitCode: task.exitCode,
25
83
  }),
26
84
  };
@@ -28,12 +86,14 @@ function call(input: TaskStopInput, state?: BackgroundTaskState): ToolResult {
28
86
 
29
87
  export const taskStopTool: Tool<TaskStopInput> = {
30
88
  name: 'TaskStop',
31
- description: 'Stops one Bash or PowerShell background task owned by the current session, including its child process tree.',
89
+ description:
90
+ 'Stops one Bash/PowerShell background process (including its process tree) or one running Agent task owned by the current session.',
32
91
  inputSchema,
33
92
  parameters: toToolParameters(inputSchema),
34
93
  isReadOnly: false,
35
94
  isConcurrencySafe: true,
36
95
  maxResultSizeChars: DEFAULT_MAX_RESULT_SIZE_CHARS,
37
- call: (input) => Promise.resolve(call(input)),
38
- callWithContext: (input, _signal, context: ToolCallContext) => Promise.resolve(call(input, context.sessionState?.backgroundTasks)),
96
+ call: (input) => call(input),
97
+ callWithContext: (input, _signal, context: ToolCallContext) =>
98
+ call(input, context.sessionState?.backgroundTasks, context),
39
99
  };
@@ -0,0 +1,46 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+ import { mutateRuntimeJson, readRuntimeJson, sessionRuntimeDir, } from '../../session/runtimeStore.js';
4
+ const EMPTY = { schemaVersion: 1, tasks: [] };
5
+ function pathFor(context) {
6
+ return join(sessionRuntimeDir(context), 'tasks.json');
7
+ }
8
+ export async function listManagedTasks(context) {
9
+ return (await readRuntimeJson(pathFor(context), EMPTY)).tasks;
10
+ }
11
+ export async function mutateManagedTasks(context, fn) {
12
+ return (await mutateRuntimeJson(pathFor(context), EMPTY, (file) => ({
13
+ schemaVersion: 1,
14
+ tasks: fn(file.tasks),
15
+ }))).tasks;
16
+ }
17
+ export function newTaskId() {
18
+ return `task_${randomUUID()}`;
19
+ }
20
+ export function assertNoDependencyCycle(tasks) {
21
+ const map = new Map(tasks.map((task) => [task.id, task]));
22
+ const visiting = new Set();
23
+ const visited = new Set();
24
+ const visit = (id) => {
25
+ if (visiting.has(id))
26
+ throw new Error(`任务依赖形成环:${id}`);
27
+ if (visited.has(id))
28
+ return;
29
+ const task = map.get(id);
30
+ if (!task)
31
+ throw new Error(`依赖任务不存在:${id}`);
32
+ visiting.add(id);
33
+ task.blockedBy.forEach(visit);
34
+ visiting.delete(id);
35
+ visited.add(id);
36
+ };
37
+ tasks.forEach((task) => visit(task.id));
38
+ }
39
+ export function effectiveTaskStatus(task, tasks) {
40
+ if (task.status === 'completed')
41
+ return 'completed';
42
+ const byId = new Map(tasks.map((item) => [item.id, item]));
43
+ return task.blockedBy.some((id) => byId.get(id)?.status !== 'completed')
44
+ ? 'blocked'
45
+ : task.status;
46
+ }
@@ -0,0 +1,77 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { join } from 'node:path';
3
+
4
+ import type { ToolCallContext } from '../../types.js';
5
+ import {
6
+ mutateRuntimeJson,
7
+ readRuntimeJson,
8
+ sessionRuntimeDir,
9
+ } from '../../session/runtimeStore.js';
10
+
11
+ export type ManagedTaskStatus = 'pending' | 'in_progress' | 'completed';
12
+ export interface ManagedTask {
13
+ id: string;
14
+ title: string;
15
+ description?: string;
16
+ activeForm?: string;
17
+ owner?: string;
18
+ status: ManagedTaskStatus;
19
+ blockedBy: string[];
20
+ metadata?: Record<string, unknown>;
21
+ createdAt: number;
22
+ updatedAt: number;
23
+ }
24
+ interface TaskFile {
25
+ schemaVersion: 1;
26
+ tasks: ManagedTask[];
27
+ }
28
+ const EMPTY: TaskFile = { schemaVersion: 1, tasks: [] };
29
+
30
+ function pathFor(context: ToolCallContext): string {
31
+ return join(sessionRuntimeDir(context), 'tasks.json');
32
+ }
33
+ export async function listManagedTasks(
34
+ context: ToolCallContext,
35
+ ): Promise<ManagedTask[]> {
36
+ return (await readRuntimeJson(pathFor(context), EMPTY)).tasks;
37
+ }
38
+ export async function mutateManagedTasks(
39
+ context: ToolCallContext,
40
+ fn: (tasks: ManagedTask[]) => ManagedTask[],
41
+ ): Promise<ManagedTask[]> {
42
+ return (
43
+ await mutateRuntimeJson(pathFor(context), EMPTY, (file) => ({
44
+ schemaVersion: 1 as const,
45
+ tasks: fn(file.tasks),
46
+ }))
47
+ ).tasks;
48
+ }
49
+ export function newTaskId(): string {
50
+ return `task_${randomUUID()}`;
51
+ }
52
+ export function assertNoDependencyCycle(tasks: ManagedTask[]): void {
53
+ const map = new Map(tasks.map((task) => [task.id, task]));
54
+ const visiting = new Set<string>();
55
+ const visited = new Set<string>();
56
+ const visit = (id: string) => {
57
+ if (visiting.has(id)) throw new Error(`任务依赖形成环:${id}`);
58
+ if (visited.has(id)) return;
59
+ const task = map.get(id);
60
+ if (!task) throw new Error(`依赖任务不存在:${id}`);
61
+ visiting.add(id);
62
+ task.blockedBy.forEach(visit);
63
+ visiting.delete(id);
64
+ visited.add(id);
65
+ };
66
+ tasks.forEach((task) => visit(task.id));
67
+ }
68
+ export function effectiveTaskStatus(
69
+ task: ManagedTask,
70
+ tasks: ManagedTask[],
71
+ ): string {
72
+ if (task.status === 'completed') return 'completed';
73
+ const byId = new Map(tasks.map((item) => [item.id, item]));
74
+ return task.blockedBy.some((id) => byId.get(id)?.status !== 'completed')
75
+ ? 'blocked'
76
+ : task.status;
77
+ }
@@ -0,0 +1,167 @@
1
+ import { z } from 'zod';
2
+ import { toToolParameters } from '../../utils/zodToJson.js';
3
+ const schema = z.object({
4
+ lines: z.number().int().min(1).max(200).default(50).optional(),
5
+ });
6
+ export class AnsiScreen {
7
+ columns;
8
+ rows = [''];
9
+ row = 0;
10
+ column = 0;
11
+ escape = '';
12
+ constructor(columns = process.stdout.columns || 120) {
13
+ this.columns = columns;
14
+ }
15
+ resize(columns) {
16
+ this.columns = Math.max(20, columns || 120);
17
+ }
18
+ write(value) {
19
+ const text = Buffer.isBuffer(value)
20
+ ? value.toString('utf8')
21
+ : String(value);
22
+ for (const char of text) {
23
+ if (this.escape) {
24
+ this.escape += char;
25
+ if (/^\x1b\[[0-?]*[ -/]*[@-~]$/.test(this.escape)) {
26
+ this.applyCsi(this.escape);
27
+ this.escape = '';
28
+ }
29
+ else if (this.escape.length > 64 ||
30
+ !/^\x1b(?:\[[0-?]*[ -/]*)?$/.test(this.escape)) {
31
+ this.escape = '';
32
+ }
33
+ continue;
34
+ }
35
+ if (char === '\x1b') {
36
+ this.escape = char;
37
+ continue;
38
+ }
39
+ if (char === '\r') {
40
+ this.column = 0;
41
+ continue;
42
+ }
43
+ if (char === '\n') {
44
+ this.newLine();
45
+ continue;
46
+ }
47
+ if (char === '\b') {
48
+ this.column = Math.max(0, this.column - 1);
49
+ continue;
50
+ }
51
+ if (char < ' ')
52
+ continue;
53
+ this.put(char);
54
+ }
55
+ }
56
+ snapshot(lines) {
57
+ return this.rows.slice(-lines).join('\n') || '<terminal is empty>';
58
+ }
59
+ put(char) {
60
+ const current = this.rows[this.row] ?? '';
61
+ this.rows[this.row] =
62
+ current.length < this.column
63
+ ? `${current}${' '.repeat(this.column - current.length)}${char}`
64
+ : `${current.slice(0, this.column)}${char}${current.slice(this.column + 1)}`;
65
+ this.column += 1;
66
+ if (this.column >= this.columns)
67
+ this.newLine();
68
+ }
69
+ newLine() {
70
+ this.row += 1;
71
+ this.column = 0;
72
+ if (!this.rows[this.row])
73
+ this.rows[this.row] = '';
74
+ if (this.rows.length > 200) {
75
+ this.rows.splice(0, this.rows.length - 200);
76
+ this.row = this.rows.length - 1;
77
+ }
78
+ }
79
+ applyCsi(sequence) {
80
+ const match = /^\x1b\[([0-9;?]*)([A-Za-z])$/.exec(sequence);
81
+ if (!match)
82
+ return;
83
+ const rawValues = match[1].replace(/^\?/, '').split(';');
84
+ const values = rawValues.map((value) => value === '' ? undefined : Number(value));
85
+ const amount = values[0] ?? (match[2] === 'J' || match[2] === 'K' ? 0 : 1);
86
+ switch (match[2]) {
87
+ case 'A':
88
+ this.row = Math.max(0, this.row - amount);
89
+ break;
90
+ case 'B':
91
+ this.row = Math.min(this.rows.length - 1, this.row + amount);
92
+ break;
93
+ case 'C':
94
+ this.column += amount;
95
+ break;
96
+ case 'D':
97
+ this.column = Math.max(0, this.column - amount);
98
+ break;
99
+ case 'G':
100
+ this.column = Math.max(0, amount - 1);
101
+ break;
102
+ case 'H':
103
+ case 'f':
104
+ this.row = Math.max(0, Math.min(this.rows.length - 1, (values[0] ?? 1) - 1));
105
+ this.column = Math.max(0, (values[1] ?? 1) - 1);
106
+ break;
107
+ case 'K': {
108
+ const current = this.rows[this.row] ?? '';
109
+ if (amount === 1)
110
+ this.rows[this.row] =
111
+ `${' '.repeat(this.column)}${current.slice(this.column)}`;
112
+ else if (amount === 2)
113
+ this.rows[this.row] = '';
114
+ else
115
+ this.rows[this.row] = current.slice(0, this.column);
116
+ break;
117
+ }
118
+ case 'J':
119
+ if (amount === 2 || amount === 3) {
120
+ this.rows = [''];
121
+ this.row = 0;
122
+ this.column = 0;
123
+ }
124
+ break;
125
+ default:
126
+ break;
127
+ }
128
+ }
129
+ }
130
+ let uninstall;
131
+ const screen = new AnsiScreen();
132
+ export function installTerminalCapture() {
133
+ if (uninstall)
134
+ return () => undefined;
135
+ const original = process.stdout.write.bind(process.stdout);
136
+ const onResize = () => screen.resize(process.stdout.columns || 120);
137
+ process.stdout.write = ((chunk, ...args) => {
138
+ screen.write(chunk);
139
+ return original(chunk, ...args);
140
+ });
141
+ process.stdout.on('resize', onResize);
142
+ uninstall = () => {
143
+ process.stdout.write = original;
144
+ process.stdout.off('resize', onResize);
145
+ uninstall = undefined;
146
+ };
147
+ return uninstall;
148
+ }
149
+ export function hasTerminalCapture() {
150
+ return Boolean(uninstall);
151
+ }
152
+ export const terminalCaptureTool = {
153
+ name: 'TerminalCapture',
154
+ description: 'Returns a bounded virtual-screen snapshot of the current TUI terminal. Available only when the TUI host installed its capture provider.',
155
+ inputSchema: schema,
156
+ parameters: toToolParameters(schema),
157
+ isReadOnly: true,
158
+ isConcurrencySafe: true,
159
+ resolveAvailability: (runtime) => ({
160
+ available: runtime.surface === 'tui' && hasTerminalCapture(),
161
+ signature: `terminal:${runtime.surface}:${hasTerminalCapture()}`,
162
+ }),
163
+ call: async (input) => ({
164
+ ok: true,
165
+ content: screen.snapshot(input.lines ?? 50),
166
+ }),
167
+ };
@@ -0,0 +1,179 @@
1
+ import { z } from 'zod';
2
+
3
+ import type { Tool, ToolResult } from '../../types.js';
4
+ import { toToolParameters } from '../../utils/zodToJson.js';
5
+
6
+ const schema = z.object({
7
+ lines: z.number().int().min(1).max(200).default(50).optional(),
8
+ });
9
+
10
+ export class AnsiScreen {
11
+ private rows = [''];
12
+ private row = 0;
13
+ private column = 0;
14
+ private escape = '';
15
+
16
+ constructor(private columns = process.stdout.columns || 120) {}
17
+
18
+ resize(columns: number): void {
19
+ this.columns = Math.max(20, columns || 120);
20
+ }
21
+
22
+ write(value: unknown): void {
23
+ const text = Buffer.isBuffer(value)
24
+ ? value.toString('utf8')
25
+ : String(value);
26
+ for (const char of text) {
27
+ if (this.escape) {
28
+ this.escape += char;
29
+ if (/^\x1b\[[0-?]*[ -/]*[@-~]$/.test(this.escape)) {
30
+ this.applyCsi(this.escape);
31
+ this.escape = '';
32
+ } else if (
33
+ this.escape.length > 64 ||
34
+ !/^\x1b(?:\[[0-?]*[ -/]*)?$/.test(this.escape)
35
+ ) {
36
+ this.escape = '';
37
+ }
38
+ continue;
39
+ }
40
+ if (char === '\x1b') {
41
+ this.escape = char;
42
+ continue;
43
+ }
44
+ if (char === '\r') {
45
+ this.column = 0;
46
+ continue;
47
+ }
48
+ if (char === '\n') {
49
+ this.newLine();
50
+ continue;
51
+ }
52
+ if (char === '\b') {
53
+ this.column = Math.max(0, this.column - 1);
54
+ continue;
55
+ }
56
+ if (char < ' ') continue;
57
+ this.put(char);
58
+ }
59
+ }
60
+
61
+ snapshot(lines: number): string {
62
+ return this.rows.slice(-lines).join('\n') || '<terminal is empty>';
63
+ }
64
+
65
+ private put(char: string): void {
66
+ const current = this.rows[this.row] ?? '';
67
+ this.rows[this.row] =
68
+ current.length < this.column
69
+ ? `${current}${' '.repeat(this.column - current.length)}${char}`
70
+ : `${current.slice(0, this.column)}${char}${current.slice(this.column + 1)}`;
71
+ this.column += 1;
72
+ if (this.column >= this.columns) this.newLine();
73
+ }
74
+
75
+ private newLine(): void {
76
+ this.row += 1;
77
+ this.column = 0;
78
+ if (!this.rows[this.row]) this.rows[this.row] = '';
79
+ if (this.rows.length > 200) {
80
+ this.rows.splice(0, this.rows.length - 200);
81
+ this.row = this.rows.length - 1;
82
+ }
83
+ }
84
+
85
+ private applyCsi(sequence: string): void {
86
+ const match = /^\x1b\[([0-9;?]*)([A-Za-z])$/.exec(sequence);
87
+ if (!match) return;
88
+ const rawValues = match[1]!.replace(/^\?/, '').split(';');
89
+ const values = rawValues.map((value) =>
90
+ value === '' ? undefined : Number(value),
91
+ );
92
+ const amount = values[0] ?? (match[2] === 'J' || match[2] === 'K' ? 0 : 1);
93
+ switch (match[2]) {
94
+ case 'A':
95
+ this.row = Math.max(0, this.row - amount);
96
+ break;
97
+ case 'B':
98
+ this.row = Math.min(this.rows.length - 1, this.row + amount);
99
+ break;
100
+ case 'C':
101
+ this.column += amount;
102
+ break;
103
+ case 'D':
104
+ this.column = Math.max(0, this.column - amount);
105
+ break;
106
+ case 'G':
107
+ this.column = Math.max(0, amount - 1);
108
+ break;
109
+ case 'H':
110
+ case 'f':
111
+ this.row = Math.max(
112
+ 0,
113
+ Math.min(this.rows.length - 1, (values[0] ?? 1) - 1),
114
+ );
115
+ this.column = Math.max(0, (values[1] ?? 1) - 1);
116
+ break;
117
+ case 'K': {
118
+ const current = this.rows[this.row] ?? '';
119
+ if (amount === 1)
120
+ this.rows[this.row] =
121
+ `${' '.repeat(this.column)}${current.slice(this.column)}`;
122
+ else if (amount === 2) this.rows[this.row] = '';
123
+ else this.rows[this.row] = current.slice(0, this.column);
124
+ break;
125
+ }
126
+ case 'J':
127
+ if (amount === 2 || amount === 3) {
128
+ this.rows = [''];
129
+ this.row = 0;
130
+ this.column = 0;
131
+ }
132
+ break;
133
+ default:
134
+ break;
135
+ }
136
+ }
137
+ }
138
+
139
+ let uninstall: (() => void) | undefined;
140
+ const screen = new AnsiScreen();
141
+
142
+ export function installTerminalCapture(): () => void {
143
+ if (uninstall) return () => undefined;
144
+ const original = process.stdout.write.bind(process.stdout);
145
+ const onResize = () => screen.resize(process.stdout.columns || 120);
146
+ process.stdout.write = ((chunk: unknown, ...args: unknown[]) => {
147
+ screen.write(chunk);
148
+ return original(chunk as never, ...(args as never[]));
149
+ }) as typeof process.stdout.write;
150
+ process.stdout.on('resize', onResize);
151
+ uninstall = () => {
152
+ process.stdout.write = original;
153
+ process.stdout.off('resize', onResize);
154
+ uninstall = undefined;
155
+ };
156
+ return uninstall;
157
+ }
158
+
159
+ export function hasTerminalCapture(): boolean {
160
+ return Boolean(uninstall);
161
+ }
162
+
163
+ export const terminalCaptureTool: Tool<z.infer<typeof schema>> = {
164
+ name: 'TerminalCapture',
165
+ description:
166
+ 'Returns a bounded virtual-screen snapshot of the current TUI terminal. Available only when the TUI host installed its capture provider.',
167
+ inputSchema: schema,
168
+ parameters: toToolParameters(schema),
169
+ isReadOnly: true,
170
+ isConcurrencySafe: true,
171
+ resolveAvailability: (runtime) => ({
172
+ available: runtime.surface === 'tui' && hasTerminalCapture(),
173
+ signature: `terminal:${runtime.surface}:${hasTerminalCapture()}`,
174
+ }),
175
+ call: async (input): Promise<ToolResult> => ({
176
+ ok: true,
177
+ content: screen.snapshot(input.lines ?? 50),
178
+ }),
179
+ };