newmark-agent 0.4.0 → 0.4.3

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.
@@ -21,6 +21,22 @@ export interface AgentPromptMessage {
21
21
  clientMessageId?: string;
22
22
  guideId?: string;
23
23
  runId?: string;
24
+ /**
25
+ * dev-0.4.3: 同一 Build block 内连续到达的多个 Guide 会被 conversation
26
+ * kernel 合并为一次 provider 续接,而不是每来一个 Guide 就响应一次。
27
+ * 数组顺序即用户提交顺序(顺序执行且自动接续)。
28
+ */
29
+ batchGuides?: Array<{
30
+ clientMessageId: string;
31
+ guideId?: string;
32
+ text: string;
33
+ images?: Array<{
34
+ dataUrl: string;
35
+ name?: string;
36
+ type?: string;
37
+ }>;
38
+ attachments?: ConversationImageAttachment[];
39
+ }>;
24
40
  routePolicy?: {
25
41
  mode?: 'quality' | 'balanced' | 'cost' | 'speed';
26
42
  maxQualityLoss?: number;
@@ -586,7 +586,40 @@ class ConversationKernel {
586
586
  if (runtime.stopRequestedRunId === runtime.runId)
587
587
  return this.result(runtime, lastTokens);
588
588
  const next = runtime.pendingNextTurn.shift();
589
- lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
589
+ if (next.queueMode === 'steer' && typeof next.message !== 'string' && !!next.message.clientMessageId) {
590
+ const batchGuides = [];
591
+ const pushGuide = (message) => {
592
+ batchGuides.push({
593
+ clientMessageId: String(message.clientMessageId || ''),
594
+ guideId: message.guideId,
595
+ text: message.text,
596
+ images: message.images?.map(image => ({ ...image })),
597
+ attachments: message.attachments?.map(attachment => ({ ...attachment })),
598
+ });
599
+ };
600
+ pushGuide(next.message);
601
+ while (runtime.pendingNextTurn.length > 0
602
+ && runtime.pendingNextTurn[0].queueMode === 'steer'
603
+ && typeof runtime.pendingNextTurn[0].message !== 'string'
604
+ && !!runtime.pendingNextTurn[0].message.clientMessageId) {
605
+ const guide = runtime.pendingNextTurn.shift();
606
+ pushGuide(guide.message);
607
+ }
608
+ if (batchGuides.length === 1) {
609
+ lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
610
+ continue;
611
+ }
612
+ const batchText = batchGuides.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join('\n');
613
+ const batchMessage = {
614
+ text: `Apply the following intervening Guides in submission order within the current Build Block and continue automatically:\n${batchText}`,
615
+ hiddenUserInput: true,
616
+ batchGuides,
617
+ };
618
+ lastTokens = await this.runSingle(runtime, batchMessage, 'steer');
619
+ }
620
+ else {
621
+ lastTokens = await this.runSingle(runtime, next.message, next.queueMode);
622
+ }
590
623
  }
591
624
  const rootMessage = runtime.runner.subagents.readRootInbox()[0];
592
625
  if (!rootMessage) {
@@ -25,4 +25,13 @@ export declare function toolAvailability(name: string): ToolAvailability;
25
25
  export declare function evaluateToolPolicy(request: ToolPolicyRequest): ToolPolicyDecision;
26
26
  export declare function filterToolDefinitions<T>(definitions: T[], request: Omit<ToolPolicyRequest, 'name' | 'args'>): T[];
27
27
  export declare function planModePolicyPrompt(): string;
28
+ export interface DeletionGuardDecision {
29
+ blocked: boolean;
30
+ reason?: string;
31
+ }
32
+ /**
33
+ * 硬性删除命令审查。blocked=true 表示该命令构成脚本/命令批量删除,必须拒绝并
34
+ * 引导 Agent 使用受监管的 delete_file 工具逐个删除。
35
+ */
36
+ export declare function evaluateDeletionGuard(command: string): DeletionGuardDecision;
28
37
  //# sourceMappingURL=toolPolicy.d.ts.map
@@ -7,6 +7,7 @@ exports.toolAvailability = toolAvailability;
7
7
  exports.evaluateToolPolicy = evaluateToolPolicy;
8
8
  exports.filterToolDefinitions = filterToolDefinitions;
9
9
  exports.planModePolicyPrompt = planModePolicyPrompt;
10
+ exports.evaluateDeletionGuard = evaluateDeletionGuard;
10
11
  const REQUIRED_TOOLS = new Set(['pwd', 'read', 'glob', 'grep']);
11
12
  const MODE_SCOPED_TOOLS = new Set([
12
13
  'image_inspect',
@@ -22,6 +23,8 @@ const MODE_SCOPED_TOOLS = new Set([
22
23
  'read_tool_result',
23
24
  'goal_manage',
24
25
  'conversation_rename',
26
+ 'task_read',
27
+ 'task_create',
25
28
  'question',
26
29
  'task',
27
30
  'subagent_list',
@@ -35,6 +38,7 @@ const MODE_SCOPED_TOOLS = new Set([
35
38
  'branch_create',
36
39
  ]);
37
40
  const PLAN_READ_ONLY_TOOLS = new Set([
41
+ 'task_read',
38
42
  'pwd',
39
43
  'read',
40
44
  'glob',
@@ -167,4 +171,128 @@ function planModePolicyPrompt() {
167
171
  'Runtime policy rejects stale or hidden mutating tool calls even if a prompt asks for them.',
168
172
  ].join(' ');
169
173
  }
174
+ /** 删除命令动词(跨 POSIX / PowerShell / cmd)。注意:不含单独 "remove"(避免匹配普通英文)。 */
175
+ const DELETE_VERB_SOURCE = '(?:remove-item|rmdir|unlink|erase|del|rm|rd|ri)';
176
+ const DELETE_VERB_BOUNDARY = new RegExp(`(?:^|[\\s;&|()\\n])${DELETE_VERB_SOURCE}(?:\\s|$)`, 'i');
177
+ function hasDeletionVerb(text) {
178
+ return DELETE_VERB_BOUNDARY.test(text);
179
+ }
180
+ function deletionVerbCount(text) {
181
+ const matches = text.match(new RegExp(DELETE_VERB_BOUNDARY.source, 'gi'));
182
+ return matches ? matches.length : 0;
183
+ }
184
+ /** 循环结构批量删除:foreach / for…in / for( / while( / bash do…done。 */
185
+ function hasLoopDeletion(text) {
186
+ const lower = text.toLowerCase();
187
+ if (/\bforeach\b/.test(lower))
188
+ return true; // PowerShell foreach
189
+ if (/\bfor\b\s*[$({]/.test(lower))
190
+ return true; // PowerShell/C for(...)
191
+ if (/\bfor\b\s+\S+\s+in\b/.test(lower))
192
+ return true; // bash for f in ...
193
+ if (/\bwhile\b\s*[({]/.test(lower))
194
+ return true; // while(...)
195
+ if (/\bwhile\b\s+\S/.test(lower) && /\bdo\b/.test(lower))
196
+ return true; // bash while ... do
197
+ if (/\bdone\b/.test(lower))
198
+ return true; // bash 循环结束标记
199
+ return false;
200
+ }
201
+ /** find -delete / find -exec rm / xargs rm 批量删除。 */
202
+ function hasFindXargsDeletion(text) {
203
+ if (/\bfind\b[^\n;&|]*-(?:delete\b|exec(?:dir)?\s+(?:rm|del|erase)\b)/i.test(text))
204
+ return true;
205
+ if (/\bxargs\b[^\n;&|]*\b(?:rm|del|erase|remove-item)\b/i.test(text))
206
+ return true;
207
+ return false;
208
+ }
209
+ /** 按 shell 语义切分参数:引号内的空格不拆分,返回去引号后的 token。 */
210
+ function splitCommandArgs(args) {
211
+ const tokens = [];
212
+ const re = /"([^"]*)"|'([^']*)'|(\S+)/g;
213
+ let m;
214
+ while ((m = re.exec(args)) !== null) {
215
+ const token = m[1] ?? m[2] ?? m[3] ?? '';
216
+ if (token)
217
+ tokens.push(token);
218
+ }
219
+ return tokens;
220
+ }
221
+ /** 管道接收端删除:上游产出多项,删除动词作为接收端即批量删除。 */
222
+ function hasPipeDeletion(text) {
223
+ return new RegExp(`\\|\\s*${DELETE_VERB_SOURCE}\\b`, 'i').test(text);
224
+ }
225
+ /** 递归删除标志:rm -r/-R/--recursive、Remove-Item -Recurse、rmdir/rd /s、del /s。 */
226
+ function hasRecursiveDeletionFlag(text) {
227
+ const lower = text.toLowerCase();
228
+ if (/\brm\b\s+(-[a-z]*r[a-z]*|--recursive)\b/.test(lower))
229
+ return true;
230
+ if (/\bremove-item\b[^\n;&|]*\s+-(?:recurse|r)\b/.test(lower))
231
+ return true;
232
+ if (/\b(?:rmdir|rd)\b\s+(-r\b|\/[s]\b)/.test(lower))
233
+ return true;
234
+ if (/\bdel\b\s+\/[s]\b/.test(lower))
235
+ return true;
236
+ return false;
237
+ }
238
+ /** 删除命令后跟含通配符的目标 token。 */
239
+ function hasWildcardDeletionTarget(text) {
240
+ const segmentRe = new RegExp(`(?:^|[\\s;&|()\\n])${DELETE_VERB_SOURCE}([\\s][^;&|\\n]*)?`, 'gi');
241
+ let m;
242
+ while ((m = segmentRe.exec(text)) !== null) {
243
+ const args = m[1] || '';
244
+ for (const token of splitCommandArgs(args)) {
245
+ if (!token || token.startsWith('-') || /^\/[A-Za-z]/.test(token))
246
+ continue;
247
+ if (/[*?]/.test(token))
248
+ return true;
249
+ }
250
+ }
251
+ return false;
252
+ }
253
+ /** 单条删除命令后跟 >= 2 个明确目标(非 flag、非 shell 开关)。 */
254
+ function hasMultipleDeleteTargets(text) {
255
+ const segmentRe = new RegExp(`(?:^|[\\s;&|()\\n])${DELETE_VERB_SOURCE}([\\s][^;&|\\n]*)?`, 'gi');
256
+ let m;
257
+ while ((m = segmentRe.exec(text)) !== null) {
258
+ const args = m[1] || '';
259
+ const targets = splitCommandArgs(args)
260
+ .filter(t => t && !t.startsWith('-') && !/^\/[A-Za-z]/.test(t) && !/^(&&|\|\||;|\||&|>|>>|<|2>&1)$/.test(t));
261
+ if (targets.length >= 2)
262
+ return true;
263
+ }
264
+ return false;
265
+ }
266
+ /**
267
+ * 硬性删除命令审查。blocked=true 表示该命令构成脚本/命令批量删除,必须拒绝并
268
+ * 引导 Agent 使用受监管的 delete_file 工具逐个删除。
269
+ */
270
+ function evaluateDeletionGuard(command) {
271
+ const text = String(command || '');
272
+ if (!text.trim())
273
+ return { blocked: false };
274
+ // find -delete / find -exec rm 中,-delete 不含标准删除动词,需在入口单独识别为批量删除意图。
275
+ const findXargs = hasFindXargsDeletion(text);
276
+ if (!hasDeletionVerb(text) && !findXargs)
277
+ return { blocked: false };
278
+ const refuse = (kind) => ({
279
+ blocked: true,
280
+ reason: `[deletion guard] ${kind} batch deletion is not allowed. Delete files one by one with the delete_file tool under Agent supervision.`,
281
+ });
282
+ if (hasLoopDeletion(text))
283
+ return refuse('Loop-based');
284
+ if (findXargs)
285
+ return refuse('find/xargs');
286
+ if (hasPipeDeletion(text))
287
+ return refuse('Pipe-fed');
288
+ if (hasRecursiveDeletionFlag(text))
289
+ return refuse('Recursive');
290
+ if (hasWildcardDeletionTarget(text))
291
+ return refuse('Wildcard');
292
+ if (hasMultipleDeleteTargets(text))
293
+ return refuse('Multiple-target');
294
+ if (deletionVerbCount(text) >= 2)
295
+ return refuse('Multiple-statement');
296
+ return { blocked: false };
297
+ }
170
298
  //# sourceMappingURL=toolPolicy.js.map
@@ -29,13 +29,25 @@ export declare class LLMProvider {
29
29
  openAIMode: OpenAITransportMode | boolean;
30
30
  useProviderAdaptersV2: boolean;
31
31
  requestTimeoutMs: number;
32
+ /** dev-0.4.3 模型原生思考强度档位映射(模型名 → { 模型原生档位: Newmark 档位 })。 */
33
+ thinkingTierMaps?: Record<string, Record<string, string>> | undefined;
32
34
  static nodeHttpTransport: ((method: 'GET' | 'POST', url: string, headers: Record<string, string>, body?: string) => Promise<NodeHttpResult>) | null;
33
35
  static powershellTransport: ((method: 'GET' | 'POST', url: string, headers: Record<string, string>, body?: string) => Promise<NodeHttpResult>) | null;
34
- constructor(name: string, baseUrl: string, apiKey: string, explicitProtocol?: ProviderProtocol | undefined, openAIMode?: OpenAITransportMode | boolean, useProviderAdaptersV2?: boolean, requestTimeoutMs?: number);
36
+ constructor(name: string, baseUrl: string, apiKey: string, explicitProtocol?: ProviderProtocol | undefined, openAIMode?: OpenAITransportMode | boolean, useProviderAdaptersV2?: boolean, requestTimeoutMs?: number,
37
+ /** dev-0.4.3 模型原生思考强度档位映射(模型名 → { 模型原生档位: Newmark 档位 })。 */
38
+ thinkingTierMaps?: Record<string, Record<string, string>> | undefined);
35
39
  private effectiveRequestTimeout;
36
40
  private withRequestTimeout;
37
41
  intelligenceConfig(tier: string): IntelligenceConfig;
38
42
  private reasoningEffort;
43
+ /**
44
+ * dev-0.4.3 模型原生思考强度档位映射。不同模型的原生 reasoning_effort
45
+ * 档位配置可能不同(档位数量或档位命名不同)。模型配置 `thinking_tier_map`
46
+ * 以「模型原生档位名 → Newmark 档位」声明映射,这里把 Newmark 档位反查为
47
+ * 模型原生档位名;未配置映射(或映射为空)时返回 undefined,由调用方
48
+ * 维持默认透传行为(默认不变动映射)。
49
+ */
50
+ private mappedNativeEffort;
39
51
  private applyChatReasoningEffort;
40
52
  private protocol;
41
53
  private openAITransportMode;
@@ -98,9 +98,12 @@ class LLMProvider {
98
98
  openAIMode;
99
99
  useProviderAdaptersV2;
100
100
  requestTimeoutMs;
101
+ thinkingTierMaps;
101
102
  static nodeHttpTransport = null;
102
103
  static powershellTransport = null;
103
- constructor(name, baseUrl, apiKey, explicitProtocol, openAIMode = 'chat_stream', useProviderAdaptersV2 = false, requestTimeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS) {
104
+ constructor(name, baseUrl, apiKey, explicitProtocol, openAIMode = 'chat_stream', useProviderAdaptersV2 = false, requestTimeoutMs = DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS,
105
+ /** dev-0.4.3 模型原生思考强度档位映射(模型名 → { 模型原生档位: Newmark 档位 })。 */
106
+ thinkingTierMaps) {
104
107
  this.name = name;
105
108
  this.baseUrl = baseUrl;
106
109
  this.apiKey = apiKey;
@@ -108,6 +111,7 @@ class LLMProvider {
108
111
  this.openAIMode = openAIMode;
109
112
  this.useProviderAdaptersV2 = useProviderAdaptersV2;
110
113
  this.requestTimeoutMs = requestTimeoutMs;
114
+ this.thinkingTierMaps = thinkingTierMaps;
111
115
  }
112
116
  effectiveRequestTimeout(timeoutMs) {
113
117
  const requested = Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : DEFAULT_PROVIDER_REQUEST_TIMEOUT_MS;
@@ -140,6 +144,9 @@ class LLMProvider {
140
144
  }
141
145
  }
142
146
  reasoningEffort(model, tier) {
147
+ const mapped = this.mappedNativeEffort(model, tier);
148
+ if (mapped !== undefined)
149
+ return mapped;
143
150
  if (!/^(?:gpt-5|o[134](?:-|$)|codex)|(?:reasoner|reasoning|deepseek-r1|deepseek-reasoner|\br1\b)/i.test(model))
144
151
  return undefined;
145
152
  const effort = tier === 'low' || tier === 'high' || tier === 'xhigh' || tier === 'max'
@@ -150,6 +157,39 @@ class LLMProvider {
150
157
  // OpenAI-compatible/Codex gateways may expose the user-facing max tier.
151
158
  return effort === 'max' && /^https:\/\/(?:api\.)?openai\.com(?:\/|$)/i.test(this.cleanBaseUrl()) ? 'xhigh' : effort;
152
159
  }
160
+ /**
161
+ * dev-0.4.3 模型原生思考强度档位映射。不同模型的原生 reasoning_effort
162
+ * 档位配置可能不同(档位数量或档位命名不同)。模型配置 `thinking_tier_map`
163
+ * 以「模型原生档位名 → Newmark 档位」声明映射,这里把 Newmark 档位反查为
164
+ * 模型原生档位名;未配置映射(或映射为空)时返回 undefined,由调用方
165
+ * 维持默认透传行为(默认不变动映射)。
166
+ */
167
+ mappedNativeEffort(model, tier) {
168
+ const map = this.thinkingTierMaps?.[model];
169
+ if (!map || typeof map !== 'object')
170
+ return undefined;
171
+ const order = ['low', 'medium', 'high', 'xhigh', 'max'];
172
+ const entries = Object.entries(map)
173
+ .filter((entry) => order.includes(entry[1]))
174
+ .sort((a, b) => order.indexOf(a[1]) - order.indexOf(b[1]));
175
+ if (!entries.length)
176
+ return undefined;
177
+ const normalized = tier === 'ultra'
178
+ ? 'max'
179
+ : (order.includes(tier) ? tier : 'medium');
180
+ const exact = entries.find(([, newmark]) => newmark === normalized);
181
+ if (exact)
182
+ return exact[0];
183
+ // 就近降级:取强度不超过目标档位的最高已映射档位
184
+ const targetIndex = order.indexOf(normalized);
185
+ for (let i = targetIndex; i >= 0; i--) {
186
+ const candidate = entries.find(([, newmark]) => newmark === order[i]);
187
+ if (candidate)
188
+ return candidate[0];
189
+ }
190
+ // 全部高于目标档位:取最低档位
191
+ return entries[0]?.[0];
192
+ }
153
193
  applyChatReasoningEffort(body, model, tier) {
154
194
  const effort = this.reasoningEffort(model, tier);
155
195
  if (effort)
@@ -859,6 +899,7 @@ class LLMProvider {
859
899
  tools: this.toNormalizedTools(tools),
860
900
  temperature,
861
901
  maxOutputTokens: maxTokens,
902
+ reasoningEffort: this.reasoningEffort(model, reasoningTier),
862
903
  apiKey: this.apiKey,
863
904
  baseUrl: this.cleanBaseUrl(),
864
905
  ...(sessionId ? { sessionId } : {}),
@@ -52,7 +52,9 @@ export interface NormalizedAgentRequest {
52
52
  tools: NormalizedTool[];
53
53
  temperature: number;
54
54
  maxOutputTokens: number;
55
- reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
55
+ /** dev-0.4.3:模型原生思考强度档位名。Newmark 档位经模型配置
56
+ * `thinking_tier_map` 映射后可能不再是 Newmark 档位名,故为自由字符串。 */
57
+ reasoningEffort?: string;
56
58
  apiKey: string;
57
59
  baseUrl: string;
58
60
  /** 可选的会话标识。仅当目标 provider 显式支持 session_id 语义的上下文缓存
@@ -15,7 +15,7 @@ const DOMAIN_PREFIXES = [
15
15
  [/^web_/, 'web'],
16
16
  [/^computer_use$/, 'computer'],
17
17
  [/^(image_|ocr_|pdf_)/, 'media'],
18
- [/^(bash|pwd|read|write|edit|glob|grep)$/, 'core'],
18
+ [/^(bash|pwd|read|write|edit|delete_file|glob|grep)$/, 'core'],
19
19
  ];
20
20
  const READ_TOOL_PATTERN = /^(pwd|read|glob|grep|git_status|git_log|git_diff|git_branch|git_show|memory_lab_read|memory_lab_query|skill|linked_plan|build_history_query|subagent_read|subagent_result|subagent_list|subagent_progress|question|image_inspect|image_display|ocr_read|pdf_read|automation_list|automation_status)$/;
21
21
  /**
@@ -59,6 +59,7 @@ export declare class ToolExecutor {
59
59
  private fread;
60
60
  private fwrite;
61
61
  private fedit;
62
+ private fdelete;
62
63
  private glob;
63
64
  private grep;
64
65
  private createProxyAgent;
@@ -227,6 +227,7 @@ class ToolExecutor {
227
227
  t('read', 'Read file contents. Use ABSOLUTE paths. The working directory is given in system prompt.', { path: { type: 'string' } }, ['path']),
228
228
  t('write', 'Write/create a file. Use ABSOLUTE paths.', { path: { type: 'string' }, content: { type: 'string' } }, ['path', 'content']),
229
229
  t('edit', 'Edit file with find-and-replace. Use ABSOLUTE paths.', { path: { type: 'string' }, old_str: { type: 'string' }, new_str: { type: 'string' } }, ['path', 'old_str', 'new_str']),
230
+ t('delete_file', 'Delete ONE file under Agent supervision. Use ABSOLUTE paths. This tool refuses directory deletion and wildcard paths; delete files one by one. Never use bash rm/del/Remove-Item for batch (recursive/wildcard/loop/pipe/multi-target) deletion — the runtime hard-blocks such commands.', { path: { type: 'string' } }, ['path']),
230
231
  t('glob', 'Find files by glob pattern (e.g. **/*.ts, src/**/*.html)', { pattern: { type: 'string' } }, ['pattern']),
231
232
  t('grep', 'Search file content with regex', { pattern: { type: 'string' }, path: { type: 'string' } }, ['pattern', 'path']),
232
233
  t('web_search', 'Search the web', { query: { type: 'string' } }, ['query']),
@@ -371,7 +372,9 @@ class ToolExecutor {
371
372
  t('background_tool', 'Run a tool call in the background WITHOUT blocking the conversation turn. Pass the target tool name and its arguments; this tool returns a background_id IMMEDIATELY, and the real tool keeps running in the background. The result is persisted and can be retrieved later with read_tool_result. Use this for long-running or non-critical tools (bash, web_fetch, long read/grep) so the conversation continues without waiting. The background result stays OUT of context until you explicitly read it, preserving prompt-cache hit rate. Orchestration/flow/subagent/question tools cannot be backgrounded.', { tool: { type: 'string', description: 'The tool name to run in the background (e.g. bash, web_fetch, read, grep).' }, args: { type: 'object', description: 'The arguments object for the target tool, matching its normal schema.' } }, ['tool']),
372
373
  t('read_tool_result', 'Read the result of a background tool. Pass the background_id returned by background_tool. When status is running, returns a running marker; when done, returns the persisted result (optionally release it from storage after reading); when error, returns the failure. Background results are released from storage only when you set release=true.', { background_id: { type: 'string', description: 'The background_id returned by background_tool.' }, release: { type: 'boolean', description: 'Set true to release the persisted result from storage after reading it.' } }, ['background_id']),
373
374
  t('goal_manage', 'Actively manage the persistent Goal state for this conversation. You may enter Goal mode, update (edit) its objective, mark it complete, or exit Goal mode yourself. Call this when the user asks you to pursue a persistent objective, when the objective changes, when you have verified the objective is genuinely achieved, or when you judge the Goal is no longer needed and should be cleared. This is the agent-side state control that mirrors the GUI goal panel controls. enter/update require objective; complete marks the objective verified and exits Goal mode; exit clears the Goal (and returns to Build mode) without claiming completion.', { action: { type: 'string', enum: ['enter', 'update', 'complete', 'exit'], description: 'enter=enter Goal mode and set the objective; update=edit the objective (records a change); complete=mark the objective verified-achieved and exit Goal mode; exit=clear the Goal and return to Build mode without claiming completion.' }, objective: { type: 'string', description: 'The Goal objective text. Required for enter and update.' }, reason: { type: 'string', description: 'Optional one-line reason for the state change, recorded for audit.' } }, ['action']),
374
- t('conversation_rename', 'Rename the CURRENT conversation to a concise, descriptive title you choose. On the FIRST Build Block of a NEW conversation the runtime asks you to call this once so the conversation list shows a meaningful name instead of an auto-generated one. Keep the title short (a few words) and cache-friendly: a concrete noun phrase describing the task, never a sentence or quoted prompt.', { title: { type: 'string', description: 'The new conversation title (a short noun phrase, e.g. "Fix TUI color leak", "Add goal_manage tool").' } }, ['title']),
375
+ t('task_read', 'Read the persistent inline task checklist of the CURRENT conversation (the same list the GUI Task panel and TUI plan view render). Returns bounded items (id, status, task text <=240 chars) plus unfinished count. Read-only and side-effect free; call this whenever you need concrete task-list state instead of assuming it. Kept out of the system prompt so provider prefix caching stays stable.', {}, []),
376
+ t('task_create', 'Maintain the persistent inline task checklist of the CURRENT conversation. This is the durable replacement for ephemeral in-reply checklists: items you create here appear in the GUI Task panel and TUI plan view immediately and persist across Build Blocks. Actions: create appends one task item; update changes status (pending|in_progress|done) or text of one item by id or index; clear removes completed items. Use create when starting a multi-step task, update as each item progresses, and update status=done when verified. Returns a compact confirmation, never the full list.', { action: { type: 'string', enum: ['create', 'update', 'clear'], description: 'create=append a task item; update=change one item status/text; clear=remove completed items.' }, task: { type: 'string', description: 'Task text for create, or new text for update. Short actionable label (<=400 chars).' }, text: { type: 'string', description: 'Alias of task.' }, id: { type: 'string', description: 'Item id from task_read for update.' }, index: { type: 'number', description: '0-based item index for update (alternative to id).' }, status: { type: 'string', enum: ['pending', 'in_progress', 'done', 'blocked'], description: 'New status for update; blocked is stored as pending.' } }, ['action']),
377
+ t('conversation_rename', 'Rename the CURRENT conversation to a concise, descriptive title you choose. Use this when the user asks to rename the conversation or when the current auto-generated title no longer describes the work. Keep the title short (a few words) and cache-friendly: a concrete noun phrase describing the task, never a sentence or quoted prompt.', { title: { type: 'string', description: 'The new conversation title (a short noun phrase, e.g. "Fix TUI color leak", "Add goal_manage tool").' } }, ['title']),
375
378
  t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
376
379
  t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
377
380
  t('skill', 'Search enabled skill metadata or load one exact skill body on demand. Use query when unsure, then name to load the selected skill.', { query: { type: 'string', maxLength: 200 }, name: { type: 'string', maxLength: 200 } }, []),
@@ -578,6 +581,7 @@ class ToolExecutor {
578
581
  case 'read':
579
582
  case 'write':
580
583
  case 'edit':
584
+ case 'delete_file':
581
585
  case 'grep':
582
586
  case 'file_audit':
583
587
  case 'pdf_read':
@@ -598,6 +602,15 @@ class ToolExecutor {
598
602
  : null;
599
603
  if (bashGuard)
600
604
  return bashGuard;
605
+ // 硬性删除审查:允许单文件删除,拒绝脚本/命令批量删除。
606
+ const deletionGuardTarget = tool === 'bash' || (tool === 'terminal_takeover' && g('action') === 'write')
607
+ ? g('command')
608
+ : null;
609
+ if (deletionGuardTarget !== null) {
610
+ const deletionGuard = (0, toolPolicy_1.evaluateDeletionGuard)(deletionGuardTarget);
611
+ if (deletionGuard.blocked)
612
+ return deletionGuard.reason || '[deletion guard] Batch deletion is not allowed.';
613
+ }
601
614
  try {
602
615
  switch (tool) {
603
616
  case 'bash': return await this.bash(g('command'), wsPath, args.timeout_ms, context.signal);
@@ -605,6 +618,7 @@ class ToolExecutor {
605
618
  case 'read': return this.fread(resolve(g('path')));
606
619
  case 'write': return this.fwrite(resolve(g('path')), g('content'));
607
620
  case 'edit': return this.fedit(resolve(g('path')), g('old_str'), g('new_str'));
621
+ case 'delete_file': return this.fdelete(resolve(g('path')));
608
622
  case 'glob': return this.glob(g('pattern'), wsPath);
609
623
  case 'grep': return this.grep(g('pattern'), resolve(g('path')));
610
624
  case 'web_search': return await this.wsearch(g('query'), context.signal);
@@ -1136,6 +1150,22 @@ class ToolExecutor {
1136
1150
  return `[edit] ${e}`;
1137
1151
  }
1138
1152
  }
1153
+ fdelete(p) {
1154
+ try {
1155
+ if (/[*?]/.test(p))
1156
+ return '[delete_file] Refused: wildcard paths are not allowed. Delete one file per call.';
1157
+ const resolved = path.resolve(p);
1158
+ const stat = fs.lstatSync(resolved);
1159
+ if (stat.isDirectory()) {
1160
+ return '[delete_file] Refused: deleting a directory is not allowed. Delete files one by one under Agent supervision.';
1161
+ }
1162
+ fs.unlinkSync(resolved);
1163
+ return `[delete_file] OK: ${resolved}`;
1164
+ }
1165
+ catch (e) {
1166
+ return `[delete_file] ${e instanceof Error ? e.message : String(e)}`;
1167
+ }
1168
+ }
1139
1169
  glob(pattern, ws) {
1140
1170
  try {
1141
1171
  const results = globSync(pattern, {
@@ -11,6 +11,7 @@ exports.NATIVE_TOOL_CATALOG = [
11
11
  { name: 'read', label: 'Read file', description: 'Read workspace file contents.', category: 'core', defaultEnabled: true, protected: true, availability: 'required' },
12
12
  { name: 'write', label: 'Write file', description: 'Create or overwrite workspace files.', category: 'core', defaultEnabled: true },
13
13
  { name: 'edit', label: 'Edit file', description: 'Patch workspace files through exact find and replace.', category: 'core', defaultEnabled: true },
14
+ { name: 'delete_file', label: 'Delete file', description: 'Delete one file at a time under Agent supervision; refuses directory and wildcard deletion.', category: 'core', defaultEnabled: true },
14
15
  { name: 'glob', label: 'Glob files', description: 'Find files by glob pattern.', category: 'core', defaultEnabled: true, protected: true, availability: 'required' },
15
16
  { name: 'grep', label: 'Search files', description: 'Search workspace text by regex.', category: 'core', defaultEnabled: true, protected: true, availability: 'required' },
16
17
  { name: 'web_search', label: 'Web search', description: 'Search the web from the Agent.', category: 'web', defaultEnabled: true },
@@ -17,9 +17,12 @@ const {
17
17
  createState,
18
18
  cycleConversationMode,
19
19
  enterConversation,
20
+ enterHistoryEventFocus,
21
+ exitHistoryEventFocus,
20
22
  filteredCommands,
21
23
  moveFocusHorizontal,
22
24
  moveConversationHistoryCursor,
25
+ moveHistoryEventCursor,
23
26
  moveInputCursorVertical,
24
27
  moveMenuSelection,
25
28
  moveSettingChoiceSelection,
@@ -37,6 +40,7 @@ const {
37
40
  toggleAgentHistory,
38
41
  toggleConversationPinned,
39
42
  toggleSelectedBuildBlock,
43
+ toggleSelectedBuildEvent,
40
44
  toggleSelected,
41
45
  toggleWorkflowDetails,
42
46
  validateSelectedModel
@@ -330,7 +334,21 @@ function start(options = {}) {
330
334
  Promise.resolve(requestConversationStop(state)).finally(paint);
331
335
  } else if (key.name === "tab") {
332
336
  state.conversationHistoryFocus = false;
337
+ state.historyEventFocus = false;
338
+ state.historyEventIndex = -1;
333
339
  returnToConversationSelection(state);
340
+ } else if (state.historyEventFocus) {
341
+ if (key.name === "left") {
342
+ exitHistoryEventFocus(state);
343
+ } else if (key.name === "up") {
344
+ moveHistoryEventCursor(state, -1);
345
+ } else if (key.name === "down") {
346
+ moveHistoryEventCursor(state, 1);
347
+ } else if (key.name === "return" || key.name === "space") {
348
+ toggleSelectedBuildEvent(state);
349
+ }
350
+ } else if (key.name === "right") {
351
+ enterHistoryEventFocus(state);
334
352
  } else if (key.name === "up") {
335
353
  moveConversationHistoryCursor(state, -1);
336
354
  } else if (key.name === "down") {
@@ -490,6 +508,12 @@ function start(options = {}) {
490
508
  state.focusRegion = "content";
491
509
  state.contentColumn = 0;
492
510
  state.notice = "Search Memory Lab tags";
511
+ } else if (state.view === "memory" && !state.inputMode && (str === "o" || str === "O")) {
512
+ state.notice = "Opening Memory Lab Overview";
513
+ var overviewResult = state.adapter.openMemoryOverview();
514
+ if (overviewResult && typeof overviewResult.then === "function") {
515
+ overviewResult.catch(function(error) { state.notice = "Overview failed: " + error.message; }).finally(paint);
516
+ }
493
517
  } else if (key.name === "tab") {
494
518
  state.focusRegion = "menu";
495
519
  state.contentColumn = 0;