newmark-agent 0.4.2 → 0.4.4

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.
@@ -93,6 +93,9 @@ async function handle(request) {
93
93
  if (request.method === 'set_mode') {
94
94
  return kernel.setMode(checkedTarget(request.params.target), request.params.mode);
95
95
  }
96
+ if (request.method === 'set_model') {
97
+ return kernel.setModel(checkedTarget(request.params.target), request.params.model);
98
+ }
96
99
  if (request.method === 'set_input_mode') {
97
100
  return kernel.setInputMode(checkedTarget(request.params.target), request.params.mode);
98
101
  }
@@ -314,6 +314,18 @@ export declare class Agent {
314
314
  model: string;
315
315
  fallback: boolean;
316
316
  } | null;
317
+ providerUsageTotals: {
318
+ input: number;
319
+ output: number;
320
+ cacheRead: number;
321
+ cacheWrite: number;
322
+ };
323
+ lastProviderUsage: {
324
+ input: number;
325
+ output: number;
326
+ cacheRead: number;
327
+ cacheWrite: number;
328
+ };
317
329
  private compressionCache;
318
330
  private pendingHistoryRemovals;
319
331
  private branchMailbox;
@@ -631,12 +643,18 @@ export declare class Agent {
631
643
  setConversationPinned(id: string, pinned: boolean): boolean;
632
644
  renameConversation(id: string, title: string): boolean;
633
645
  /**
634
- * 首 Build 命名提示判定:仅当 (1) 当前对话尚无历史 Build(排除当前 run)且
635
- * (2) 其持久化 title 仍是自动生成(含为空)时返回 true。满足条件时运行时会
636
- * 在首个 provider request bootstrap 注入一次性命名指令,让 Agent 调用
637
- * conversation_rename 自行命名。判定本身只读存储、无副作用,保缓存稳定。
646
+ * 首 Build 命名判定:仅当 (1) 当前对话尚无历史 Build(排除当前 run)且
647
+ * (2) 其持久化 title 仍是自动生成(含为空)时返回 true。dev-0.4.3 起不再
648
+ * 用该判定注入首轮 tool-call 指令,而是在首个完成 Build 的最终响应处自动
649
+ * 命名(见 maybeAutoRenameConversationFromRun)。判定本身只读存储、无副作用。
638
650
  */
639
651
  shouldPromptConversationRename(): boolean;
652
+ /**
653
+ * 从首个 Build 的最终响应中提取一个简短对话标题。跳过 Markdown 标题/列表
654
+ * 符号与固定 section 标题,取第一条有意义的摘要句并做保守清洗。
655
+ */
656
+ private deriveConversationTitleFromSummary;
657
+ private maybeAutoRenameConversationFromRun;
640
658
  reorderConversations(ids: string[]): boolean;
641
659
  flushConversationState(): void;
642
660
  private titleFromMessages;
@@ -730,6 +748,19 @@ export declare class Agent {
730
748
  * 已有的 renameConversation 持久化路径。返回简短结果以保缓存友好。
731
749
  */
732
750
  handleConversationRename(args: string): NewmarkToolResult;
751
+ /**
752
+ * task_read:读取当前对话的持久化内联任务清单(conversationPlan)。
753
+ * 只读、无副作用,输出有界(每项 text 截断到 240 字符),保缓存友好。
754
+ * Agent 在需要具体任务状态时调用,替代把动态清单注入每个 provider request。
755
+ */
756
+ handleTaskRead(): NewmarkToolResult;
757
+ /**
758
+ * task_create:把任务项写入当前对话的持久化内联任务清单(conversationPlan)。
759
+ * action=create 追加单项;action=update 按 id/index 改状态或文本;action=clear
760
+ * 移除已完成项。写入走 updateConversationPlan 持久化路径,GUI Task 面板与
761
+ * TUI plan 视图立即反映。返回紧凑确认,避免回显全量清单。
762
+ */
763
+ handleTaskCreate(args: string): NewmarkToolResult;
733
764
  private conversationTree;
734
765
  private currentRuntimeBranchId;
735
766
  handleBranchList(args: string): NewmarkToolResult;
@@ -773,6 +804,12 @@ export declare class Agent {
773
804
  modelLabel(): string;
774
805
  estimateContextTokens(messages?: Array<Record<string, unknown>>): number;
775
806
  private estimateContextTokenComponents;
807
+ recordProviderUsage(input: {
808
+ input: number;
809
+ output: number;
810
+ cacheRead: number;
811
+ cacheWrite: number;
812
+ }): void;
776
813
  contextWindow(modelName?: string): {
777
814
  estimatedTokens: number;
778
815
  maxTokens: number;
@@ -789,6 +826,12 @@ export declare class Agent {
789
826
  compressionEnabled?: boolean;
790
827
  cacheEntries?: number;
791
828
  archiveEntries?: number;
829
+ providerTotalTokens?: number;
830
+ providerInputTokens?: number;
831
+ providerOutputTokens?: number;
832
+ providerCacheReadTokens?: number;
833
+ providerCacheWriteTokens?: number;
834
+ providerCacheReadRatio?: number;
792
835
  };
793
836
  private resolveWindowModel;
794
837
  private contextMaxTokens;
@@ -892,6 +935,11 @@ export declare class Agent {
892
935
  modelValidationStatus(): ModelValidationProgress;
893
936
  private runModelValidation;
894
937
  engineModel(): LLMProvider | null;
938
+ /**
939
+ * dev-0.4.3 模型原生思考强度档位映射表(模型名 → thinking_tier_map)。
940
+ * 未配置映射的模型返回 undefined,provider 侧维持默认透传(不变动映射)。
941
+ */
942
+ private modelThinkingTierMaps;
895
943
  editorModelRequest(input: {
896
944
  path?: string;
897
945
  content?: string;
@@ -96,6 +96,11 @@ function throwIfAgentAborted(signal) {
96
96
  error.name = 'AbortError';
97
97
  throw error;
98
98
  }
99
+ /** task_read 输出的单项文本上限:清单项只作状态索引,不承载长描述。 */
100
+ function compactPlanItemText(value) {
101
+ const clean = String(value || '').replace(/\s+/g, ' ').trim();
102
+ return clean.length <= 240 ? clean : `${clean.slice(0, 237)}...`;
103
+ }
99
104
  let CORE_SYSTEM_PROMPT = `You are Newmark Agent, a powerful AI coding assistant built into a native desktop application.
100
105
 
101
106
  ## Available Tools
@@ -149,8 +154,8 @@ let CORE_SYSTEM_PROMPT = `You are Newmark Agent, a powerful AI coding assistant
149
154
  - When the current instruction is a new task, complete that task without silently appending unrelated historical work.
150
155
 
151
156
  ## Inline Task Management (Mandatory)
152
- - For every multi-step conversation task, maintain a compact inline checklist in the current Build work state with actionable items and one status per item: pending, in_progress, completed, or blocked.
153
- - Update that checklist as work changes and use it to drive tool order and final verification. Keep it bounded to actionable task labels; never expose hidden reasoning.
157
+ - For every multi-step conversation task, persist a compact task checklist through the task_create tool: create actionable items when the work starts, update each item's status (pending|in_progress|done) as work progresses, and mark items done only after verification. These items are the same list the GUI Task panel and TUI plan view render, and they persist across Build Blocks.
158
+ - Call task_read to reload the concrete checklist state whenever you need it; the system prompt intentionally does not inject the live list so the provider prefix cache stays stable. Keep items bounded to actionable task labels; never expose hidden reasoning.
154
159
  - The inline checklist is the per-turn task manager. The durable linked-plan document exists and is available through the linked_plan tool when explicitly needed, but its full contents are not injected into every request.
155
160
 
156
161
  ## Guidelines
@@ -214,6 +219,8 @@ class Agent {
214
219
  continuations = [];
215
220
  activeConversationId = 'default';
216
221
  lastCompression = null;
222
+ providerUsageTotals = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
223
+ lastProviderUsage = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
217
224
  compressionCache = [];
218
225
  pendingHistoryRemovals = [];
219
226
  branchMailbox = [];
@@ -2153,8 +2160,10 @@ class Agent {
2153
2160
  if (run.status !== status)
2154
2161
  return false;
2155
2162
  const terminalAt = run.endedAt || endedAt;
2156
- if (status === 'completed')
2163
+ if (status === 'completed') {
2157
2164
  this.ensureCompletedWorkRunFinalResult(run);
2165
+ this.maybeAutoRenameConversationFromRun(run);
2166
+ }
2158
2167
  const goalAudit = this.auditGoalAtWorkRunEnd(run, status, terminalAt);
2159
2168
  this.enforceGoalTerminalInvariant(status, goalAudit);
2160
2169
  this.persistBuildBlockWorkOverview(run, status, terminalAt, goalAudit);
@@ -2202,6 +2211,8 @@ class Agent {
2202
2211
  run.status = status;
2203
2212
  run.endedAt = endedAt;
2204
2213
  run.expanded = true;
2214
+ if (status === 'completed')
2215
+ this.maybeAutoRenameConversationFromRun(run);
2205
2216
  this.activeWorkRunId = '';
2206
2217
  this.finalizingWorkRunId = '';
2207
2218
  this.managedWorkRunIds.delete(run.runId);
@@ -2320,6 +2331,8 @@ class Agent {
2320
2331
  const goalAudit = this.auditGoalAtWorkRunEnd(activeRun, terminalStatus, terminalAt);
2321
2332
  this.enforceGoalTerminalInvariant(terminalStatus, goalAudit);
2322
2333
  this.persistBuildBlockWorkOverview(activeRun, terminalStatus, terminalAt, goalAudit);
2334
+ if (terminalStatus === 'completed')
2335
+ this.maybeAutoRenameConversationFromRun(activeRun);
2323
2336
  }
2324
2337
  return event;
2325
2338
  }
@@ -3414,10 +3427,10 @@ class Agent {
3414
3427
  return true;
3415
3428
  }
3416
3429
  /**
3417
- * 首 Build 命名提示判定:仅当 (1) 当前对话尚无历史 Build(排除当前 run)且
3418
- * (2) 其持久化 title 仍是自动生成(含为空)时返回 true。满足条件时运行时会
3419
- * 在首个 provider request bootstrap 注入一次性命名指令,让 Agent 调用
3420
- * conversation_rename 自行命名。判定本身只读存储、无副作用,保缓存稳定。
3430
+ * 首 Build 命名判定:仅当 (1) 当前对话尚无历史 Build(排除当前 run)且
3431
+ * (2) 其持久化 title 仍是自动生成(含为空)时返回 true。dev-0.4.3 起不再
3432
+ * 用该判定注入首轮 tool-call 指令,而是在首个完成 Build 的最终响应处自动
3433
+ * 命名(见 maybeAutoRenameConversationFromRun)。判定本身只读存储、无副作用。
3421
3434
  */
3422
3435
  shouldPromptConversationRename() {
3423
3436
  if (this.conversationBuildHistory(1).length > 0)
@@ -3431,6 +3444,46 @@ class Agent {
3431
3444
  const messages = entry?.chatMessages || this.chatMessages;
3432
3445
  return this.isGeneratedConversationTitle(priorTitle, conversationId, messages);
3433
3446
  }
3447
+ /**
3448
+ * 从首个 Build 的最终响应中提取一个简短对话标题。跳过 Markdown 标题/列表
3449
+ * 符号与固定 section 标题,取第一条有意义的摘要句并做保守清洗。
3450
+ */
3451
+ deriveConversationTitleFromSummary(summary) {
3452
+ const clean = this.sanitizeAssistantOutput(summary || '').replace(/\r/g, '');
3453
+ const lines = clean.split('\n').map(line => line.trim()).filter(Boolean);
3454
+ for (const line of lines) {
3455
+ const withoutHeading = line
3456
+ .replace(/^#{1,6}\s*/, '')
3457
+ .replace(/^[-*+>]\s*/, '')
3458
+ .trim();
3459
+ if (!withoutHeading)
3460
+ continue;
3461
+ if (/^(做了什么|验证|文件|问题|下一步|What changed|Verification|Files|Issues|Next)[::]?$/i.test(withoutHeading))
3462
+ continue;
3463
+ const firstSentence = withoutHeading.split(/[。!?!?.;;]/)[0].trim() || withoutHeading;
3464
+ const title = firstSentence
3465
+ .replace(/[{}[\]()<>"'`]/g, '')
3466
+ .replace(/\s+/g, ' ')
3467
+ .trim()
3468
+ .slice(0, 48);
3469
+ if (title.length >= 2)
3470
+ return title;
3471
+ }
3472
+ return '';
3473
+ }
3474
+ maybeAutoRenameConversationFromRun(run) {
3475
+ if (run.status !== 'completed')
3476
+ return;
3477
+ if (!this.shouldPromptConversationRename())
3478
+ return;
3479
+ const finalEvent = [...run.events].reverse().find(event => event.type === 'final_response');
3480
+ const finalMessage = [...this.chatMessages].reverse().find(message => message.role === 'assistant' && message.runId === run.runId);
3481
+ const raw = finalEvent?.content || finalMessage?.content || '';
3482
+ const summary = this.sanitizePublicWorkContent(raw).slice(0, 2000);
3483
+ const title = this.deriveConversationTitleFromSummary(summary);
3484
+ if (title)
3485
+ this.renameConversation(this.activeConversationId || 'default', title);
3486
+ }
3434
3487
  reorderConversations(ids) {
3435
3488
  const prefix = this.workspaceConversationPrefix() || '';
3436
3489
  const normalized = Array.from(new Set((Array.isArray(ids) ? ids : []).map(id => this.safeConversationId(id)).filter(Boolean)));
@@ -4124,8 +4177,8 @@ class Agent {
4124
4177
  const tool = String(input.tool || '').trim();
4125
4178
  if (!tool)
4126
4179
  return { ok: false, output: '[background_tool] tool is required.', error: 'tool is required.' };
4127
- if (tool === 'background_tool' || tool === 'read_tool_result' || tool === 'compress_tool_result' || tool === 'goal_manage' || tool === 'conversation_rename') {
4128
- return { ok: false, output: '[background_tool] cannot background the control tools (background_tool/read_tool_result/compress_tool_result/goal_manage/conversation_rename).', error: 'control-tool-unsupported.' };
4180
+ if (tool === 'background_tool' || tool === 'read_tool_result' || tool === 'compress_tool_result' || tool === 'goal_manage' || tool === 'conversation_rename' || tool === 'task_read' || tool === 'task_create') {
4181
+ return { ok: false, output: '[background_tool] cannot background the control tools (background_tool/read_tool_result/compress_tool_result/goal_manage/conversation_rename/task_read/task_create).', error: 'control-tool-unsupported.' };
4129
4182
  }
4130
4183
  // 禁止后台化子代理/编排类工具——它们有独立的生命周期管理,后台化会破坏其
4131
4184
  // mailbox/abort/persistence 语义。
@@ -4280,6 +4333,106 @@ class Agent {
4280
4333
  metadata: { kind: 'conversation-rename' },
4281
4334
  };
4282
4335
  }
4336
+ /**
4337
+ * task_read:读取当前对话的持久化内联任务清单(conversationPlan)。
4338
+ * 只读、无副作用,输出有界(每项 text 截断到 240 字符),保缓存友好。
4339
+ * Agent 在需要具体任务状态时调用,替代把动态清单注入每个 provider request。
4340
+ */
4341
+ handleTaskRead() {
4342
+ const plan = this.normalizeConversationPlan(this.conversationPlan);
4343
+ const items = plan.items.map((item, index) => ({
4344
+ index,
4345
+ id: item.id,
4346
+ status: item.status,
4347
+ task: compactPlanItemText(item.text),
4348
+ updatedAt: item.updatedAt || '',
4349
+ }));
4350
+ return {
4351
+ ok: true,
4352
+ output: JSON.stringify({
4353
+ ok: true,
4354
+ conversationId: this.activeConversationId || 'default',
4355
+ total: items.length,
4356
+ unfinished: items.filter(item => item.status !== 'done').length,
4357
+ items,
4358
+ }, null, 2),
4359
+ metadata: { kind: 'task-read' },
4360
+ };
4361
+ }
4362
+ /**
4363
+ * task_create:把任务项写入当前对话的持久化内联任务清单(conversationPlan)。
4364
+ * action=create 追加单项;action=update 按 id/index 改状态或文本;action=clear
4365
+ * 移除已完成项。写入走 updateConversationPlan 持久化路径,GUI Task 面板与
4366
+ * TUI plan 视图立即反映。返回紧凑确认,避免回显全量清单。
4367
+ */
4368
+ handleTaskCreate(args) {
4369
+ let input = {};
4370
+ try {
4371
+ input = JSON.parse(args || '{}');
4372
+ }
4373
+ catch { }
4374
+ const action = String(input.action || 'create').trim();
4375
+ const plan = this.normalizeConversationPlan(this.conversationPlan);
4376
+ const now = new Date().toISOString();
4377
+ if (action === 'create') {
4378
+ const text = String(input.task || input.text || '').replace(/\s+/g, ' ').trim();
4379
+ if (!text)
4380
+ return { ok: false, output: '[task_create] task text is required.', error: 'task text is required.' };
4381
+ const item = {
4382
+ id: `plan-${Date.now()}-${Math.random().toString(16).slice(2, 6)}`,
4383
+ text: text.slice(0, 400),
4384
+ status: 'pending',
4385
+ createdAt: now,
4386
+ updatedAt: now,
4387
+ };
4388
+ plan.items.push(item);
4389
+ this.updateConversationPlan(plan);
4390
+ return {
4391
+ ok: true,
4392
+ output: JSON.stringify({ ok: true, action, id: item.id, status: item.status, total: plan.items.length }, null, 2),
4393
+ metadata: { kind: 'task-create' },
4394
+ };
4395
+ }
4396
+ if (action === 'update') {
4397
+ const id = String(input.id || '').trim();
4398
+ const index = Number(input.index);
4399
+ const status = String(input.status || '').trim();
4400
+ const text = String(input.task || input.text || '').replace(/\s+/g, ' ').trim();
4401
+ const target = id
4402
+ ? plan.items.find(item => item.id === id)
4403
+ : Number.isInteger(index) && index >= 0 && index < plan.items.length
4404
+ ? plan.items[index]
4405
+ : undefined;
4406
+ if (!target)
4407
+ return { ok: false, output: '[task_create] no matching task item for update (pass id or valid index).', error: 'task item not found.' };
4408
+ if (status) {
4409
+ if (!['pending', 'in_progress', 'done', 'blocked'].includes(status)) {
4410
+ return { ok: false, output: '[task_create] status must be pending|in_progress|done|blocked.', error: 'invalid status.' };
4411
+ }
4412
+ target.status = status === 'blocked' ? 'pending' : status;
4413
+ }
4414
+ if (text)
4415
+ target.text = text.slice(0, 400);
4416
+ target.updatedAt = now;
4417
+ this.updateConversationPlan(plan);
4418
+ return {
4419
+ ok: true,
4420
+ output: JSON.stringify({ ok: true, action, id: target.id, status: target.status, total: plan.items.length }, null, 2),
4421
+ metadata: { kind: 'task-create' },
4422
+ };
4423
+ }
4424
+ if (action === 'clear') {
4425
+ const remaining = plan.items.filter(item => item.status !== 'done');
4426
+ const removed = plan.items.length - remaining.length;
4427
+ this.updateConversationPlan({ items: remaining });
4428
+ return {
4429
+ ok: true,
4430
+ output: JSON.stringify({ ok: true, action, removed, total: remaining.length }, null, 2),
4431
+ metadata: { kind: 'task-create' },
4432
+ };
4433
+ }
4434
+ return { ok: false, output: '[task_create] action must be create|update|clear.', error: 'invalid action.' };
4435
+ }
4283
4436
  conversationTree() {
4284
4437
  const stateKey = this.workspaceConversationStateKey();
4285
4438
  const stored = this.readStoredConversationState();
@@ -5099,6 +5252,20 @@ class Agent {
5099
5252
  buildBlockTokens: estimate(buildBlockAsciiChars, buildBlockNonAsciiChars, buildBlockStructuralChars, true),
5100
5253
  };
5101
5254
  }
5255
+ recordProviderUsage(input) {
5256
+ const bounded = (value) => Math.max(0, Math.floor(Number(value) || 0));
5257
+ const usage = {
5258
+ input: bounded(input.input),
5259
+ output: bounded(input.output),
5260
+ cacheRead: bounded(input.cacheRead),
5261
+ cacheWrite: bounded(input.cacheWrite),
5262
+ };
5263
+ this.lastProviderUsage = usage;
5264
+ this.providerUsageTotals.input += usage.input;
5265
+ this.providerUsageTotals.output += usage.output;
5266
+ this.providerUsageTotals.cacheRead += usage.cacheRead;
5267
+ this.providerUsageTotals.cacheWrite += usage.cacheWrite;
5268
+ }
5102
5269
  contextWindow(modelName = this.model) {
5103
5270
  const estimatedTokens = this.estimateContextTokens();
5104
5271
  // Display and compression must share one window resolution. Both resolve
@@ -5127,12 +5294,32 @@ class Agent {
5127
5294
  compressionEnabled: this.config.getBool('context', 'auto_compress'),
5128
5295
  cacheEntries: this.compressionCache.length,
5129
5296
  archiveEntries: this.compressionArchiveEntryCount(),
5297
+ providerTotalTokens: this.providerUsageTotals.input + this.providerUsageTotals.output,
5298
+ providerInputTokens: this.providerUsageTotals.input,
5299
+ providerOutputTokens: this.providerUsageTotals.output,
5300
+ providerCacheReadTokens: this.providerUsageTotals.cacheRead,
5301
+ providerCacheWriteTokens: this.providerUsageTotals.cacheWrite,
5302
+ providerCacheReadRatio: this.providerUsageTotals.input > 0
5303
+ ? Math.min(1, this.providerUsageTotals.cacheRead / this.providerUsageTotals.input)
5304
+ : 0,
5130
5305
  };
5131
5306
  }
5132
5307
  resolveWindowModel(modelName) {
5133
- if (modelName !== 'auto')
5134
- return this.config.findModel(modelName);
5135
- return this.activeModelConfig() || this.config.findModel(this.config.getStr('models', 'default_model'));
5308
+ // The context window (display ring, inspector, and compaction trigger) must
5309
+ // resolve the deployment that is actually running. For the active selection
5310
+ // auto or a fixed model — resolve through the active deployment so a
5311
+ // qualified selection or two same-named models across providers never fall
5312
+ // through to the 128000 default. Only a caller-supplied foreign name (for
5313
+ // example a validation probe against another model) resolves by bare name.
5314
+ if (modelName === 'auto' || modelName === this.model || modelName === this.activeModelName()) {
5315
+ const active = this.activeModelConfig();
5316
+ if (active)
5317
+ return active;
5318
+ }
5319
+ const byName = this.config.findModel(modelName);
5320
+ if (byName)
5321
+ return byName;
5322
+ return this.config.findModel(this.config.getStr('models', 'default_model'));
5136
5323
  }
5137
5324
  contextMaxTokens(modelName = this.model) {
5138
5325
  const model = this.resolveWindowModel(modelName);
@@ -6368,7 +6555,7 @@ class Agent {
6368
6555
  this.modelValidationProgress = { ...this.modelValidationProgress, currentModel, currentCheck: 'catalog' };
6369
6556
  const inferredVision = !!m.vision || (0, config_1.inferModelVisionCapability)(m.name, m.display, m.description, m.provider, m.provider_protocol);
6370
6557
  const inferredImageOutput = !!m.image_output || /(?:^|[-_.])(gpt-image|dall-e|imagen|imagegen|image-generation)(?:$|[-_.])/i.test(m.name);
6371
- const p = new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'));
6558
+ const p = new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'), undefined, this.modelThinkingTierMaps(m));
6372
6559
  let catalog = catalogByProvider.get(m.provider_id);
6373
6560
  if (!catalog && m.provider_url && m.api_key) {
6374
6561
  try {
@@ -6484,7 +6671,17 @@ class Agent {
6484
6671
  const m = this.activeModelConfig();
6485
6672
  if (!m)
6486
6673
  return null;
6487
- return new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'));
6674
+ return new provider_1.LLMProvider(m.provider, m.provider_url, m.api_key, m.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'), undefined, this.modelThinkingTierMaps(m));
6675
+ }
6676
+ /**
6677
+ * dev-0.4.3 模型原生思考强度档位映射表(模型名 → thinking_tier_map)。
6678
+ * 未配置映射的模型返回 undefined,provider 侧维持默认透传(不变动映射)。
6679
+ */
6680
+ modelThinkingTierMaps(model) {
6681
+ const map = model?.thinking_tier_map;
6682
+ if (!model?.name || !map || typeof map !== 'object' || !Object.keys(map).length)
6683
+ return undefined;
6684
+ return { [model.name]: map };
6488
6685
  }
6489
6686
  async editorModelRequest(input, signal) {
6490
6687
  const models = this.config.allModels().filter(model => {
@@ -6510,8 +6707,8 @@ class Agent {
6510
6707
  if (!selected?.api_key || !selected.provider_url)
6511
6708
  return { ok: false, text: '', error: 'No available editor prediction model.' };
6512
6709
  const provider = input.completion
6513
- ? new provider_1.LLMProvider(selected.provider, selected.provider_url, selected.api_key, selected.provider_protocol, 'chat_stream', this.config.contextFlag('provider_adapters_v2'), EDITOR_COMPLETION_TIMEOUT_MS)
6514
- : new provider_1.LLMProvider(selected.provider, selected.provider_url, selected.api_key, selected.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'));
6710
+ ? new provider_1.LLMProvider(selected.provider, selected.provider_url, selected.api_key, selected.provider_protocol, 'chat_stream', this.config.contextFlag('provider_adapters_v2'), EDITOR_COMPLETION_TIMEOUT_MS, this.modelThinkingTierMaps(selected))
6711
+ : new provider_1.LLMProvider(selected.provider, selected.provider_url, selected.api_key, selected.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'), undefined, this.modelThinkingTierMaps(selected));
6515
6712
  const language = path.extname(String(input.path || '')).replace(/^\./, '') || 'text';
6516
6713
  const system = input.completion
6517
6714
  ? 'You are an inline code completion engine. Return only the exact text to insert at the cursor. Do not use Markdown fences or explanations.'
@@ -6807,20 +7004,70 @@ class Agent {
6807
7004
  throw new Error(message);
6808
7005
  }
6809
7006
  }
6810
- const text = typeof input === 'string' ? input : String(input.text || '');
7007
+ let text = typeof input === 'string' ? input : String(input.text || '');
6811
7008
  const inputEnvelope = typeof input === 'string' ? null : input;
6812
- const hiddenUserInput = inputEnvelope?.hiddenUserInput === true;
6813
- // An empty selection may be repaired to the configured default, but an
6814
- // explicitly requested fixed model must remain visible to the
6815
- // fail-closed availability check below. Otherwise a typo such as
6816
- // provider-that-does-not-exist/missing-model silently becomes the first
6817
- // configured fixture/default deployment.
7009
+ let hiddenUserInput = inputEnvelope?.hiddenUserInput === true;
6818
7010
  const explicitFixedModel = this.model !== '' && this.model !== 'auto';
6819
7011
  if (!explicitFixedModel)
6820
7012
  this.ensureUsableModelSelection();
6821
- const clientMessageId = String(inputEnvelope?.clientMessageId || '').trim();
7013
+ let clientMessageId = String(inputEnvelope?.clientMessageId || '').trim();
6822
7014
  const inputRunId = String(inputEnvelope?.runId || this.activeWorkRunId || '').trim();
6823
- const rawImages = typeof input === 'string' ? [] : (Array.isArray(input.images) ? input.images : []);
7015
+ let rawImages = typeof input === 'string' ? [] : (Array.isArray(input.images) ? input.images : []);
7016
+ // dev-0.4.3 Guide 批量续接:同一 Build block 内连续到达的多个 Guide
7017
+ // 由 conversation kernel 合并到这里一次注入,不再每来一个 Guide 响应一次。
7018
+ const batchGuides = Array.isArray(inputEnvelope?.batchGuides) ? inputEnvelope.batchGuides : [];
7019
+ if (batchGuides.length) {
7020
+ const batchRunId = inputRunId || this.activeWorkRunId || '';
7021
+ const batchTarget = this.currentConversationTarget();
7022
+ const appliedAt = this.nowIso();
7023
+ const persisted = [];
7024
+ const batchImages = [];
7025
+ for (const guide of batchGuides) {
7026
+ const guideClientMessageId = String(guide.clientMessageId || '').trim();
7027
+ if (!guideClientMessageId)
7028
+ continue;
7029
+ let guideImages = [];
7030
+ let guideAttachments = [];
7031
+ try {
7032
+ const prepared = this.prepareSubmittedConversationImages(guide.images);
7033
+ guideImages = prepared.images;
7034
+ guideAttachments = prepared.attachments;
7035
+ }
7036
+ catch (error) {
7037
+ this.status = 'idle';
7038
+ return [{ type: 'text', text: `[Attachment rejected] ${error instanceof Error ? error.message : String(error)}` }];
7039
+ }
7040
+ const guideDisplay = guideImages.length
7041
+ ? `${guide.text}${guide.text ? '\n\n' : ''}[${guideImages.length} image attachment${guideImages.length === 1 ? '' : 's'}]`
7042
+ : guide.text;
7043
+ batchImages.push(...guideImages);
7044
+ // 图片统一由主 hidden user 消息携带,Guide 自身 history 只保留文本,
7045
+ // 避免同一批图片在上下文中重复计费。
7046
+ this.persistGuideMessage(guideClientMessageId, guideDisplay, batchRunId, guide.text, guideAttachments, String(guide.guideId || ''));
7047
+ this.recordGuideReceipt({
7048
+ clientMessageId: guideClientMessageId,
7049
+ guideId: String(guide.guideId || '') || undefined,
7050
+ target: batchTarget,
7051
+ runId: batchRunId,
7052
+ status: 'applied',
7053
+ content: guideDisplay,
7054
+ createdAt: appliedAt,
7055
+ updatedAt: appliedAt,
7056
+ appliedAt,
7057
+ });
7058
+ this.consumeConversationContinuation({ content: guide.text, queueMode: 'steer', clientMessageId: guideClientMessageId });
7059
+ persisted.push({ text: guide.text, clientMessageId: guideClientMessageId });
7060
+ }
7061
+ if (persisted.length === 1) {
7062
+ text = persisted[0].text;
7063
+ }
7064
+ else {
7065
+ text = `Apply the following intervening Guides in submission order within the current Build Block and continue automatically:\n${persisted.map((guide, index) => `Guide ${index + 1}: ${guide.text}`).join('\n')}`;
7066
+ }
7067
+ hiddenUserInput = true;
7068
+ clientMessageId = '';
7069
+ rawImages = batchImages;
7070
+ }
6824
7071
  let autoRouteEvaluated = false;
6825
7072
  let attachments = [];
6826
7073
  let images = [];
@@ -7639,7 +7886,7 @@ class Agent {
7639
7886
  const activeModel = this.activeModelConfig();
7640
7887
  const activeProvider = this.engineModel();
7641
7888
  const assignedProvider = assignedModel && assignedModel.provider_id !== activeModel?.provider_id
7642
- ? new provider_1.LLMProvider(assignedModel.provider, assignedModel.provider_url, assignedModel.api_key, assignedModel.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'))
7889
+ ? new provider_1.LLMProvider(assignedModel.provider, assignedModel.provider_url, assignedModel.api_key, assignedModel.provider_protocol, this.config.openAIApiMode(), this.config.contextFlag('provider_adapters_v2'), undefined, this.modelThinkingTierMaps(assignedModel))
7643
7890
  : activeProvider;
7644
7891
  if (!assignedProvider || !model) {
7645
7892
  throw new Error('No LLM configured. Add provider in Settings > Models.');
@@ -8623,7 +8870,7 @@ class Agent {
8623
8870
  '- Memory Lab is governed by an explicit Policy chain: pre-think whether memory is needed; prefer bounded memory_lab_query retrieval; then choose ADD/UPDATE/DELETE only when the user authorizes durable memory mutation.',
8624
8871
  '- Before memory_lab_update, inspect the target with memory_lab_query or memory_lab_read. For an existing component pass expectedUpdatedAt so concurrent/stale writes fail closed; preserve established tag parent paths.',
8625
8872
  '- Use memory_lab_delete only for an explicit user request to forget/remove memory. Prior revisions are retained under Memory Lab/archive and mutation decisions are appended to policy.jsonl for replay.',
8626
- '- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. Use build_history_query only when the current user asks what specifically happened in one Build Block; querying history is read-only and never authorizes resuming that work.',
8873
+ '- Build history disclosure is two-layered. The request prompt contains only each historical Build Block user input, final summary, and completion status. When the current task continues, fixes, verifies, or depends on earlier Build Blocks, proactively call build_history_query to read the concrete tool activity and results of the relevant block, and reuse that information instead of re-investigating (re-running commands or re-reading files) from scratch. Querying history is read-only and never authorizes resuming that work; do not query merely to answer completion status already shown in the prompt.',
8627
8874
  '- Linked plan disclosure: a durable conversation-linked Markdown plan exists and can be inspected or updated with linked_plan when explicitly needed or required by Plan mode. Its full Markdown and revision are not injected into every model request.',
8628
8875
  '- A memory_lab_update, memory_lab_delete, or memory_lab_reindex call is unfinished until its awaited tool result contains rebuildReceipt.completed=true. The completion receipt is represented by the tool activity inside the current Build block and should not be repeated as a separate completion message.',
8629
8876
  `- Skills and subagents: skill searches enabled metadata and loads one SKILL.md body on demand; skill_download installs offline skill folders; task creates constrained subagents tracked in agent state.`,