newmark-agent 0.5.7 → 0.5.9

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.
@@ -464,7 +464,7 @@ export declare class Agent {
464
464
  markRouteToolExecuted(name: string, rawArgs?: string): void;
465
465
  routeTransitionKind(): PlannedRouteAttempt['kind'] | '';
466
466
  beginRouteAttempt(): void;
467
- waitForPlannedRouteRetry(): Promise<void>;
467
+ waitForPlannedRouteRetry(explicitDelayMs?: number): Promise<void>;
468
468
  recordRouteSuccess(latencyMs?: number, throughput?: number): void;
469
469
  recordRouteToolOutcome(valid: boolean): void;
470
470
  updateProviders(value: unknown): void;
@@ -355,7 +355,7 @@ class Agent {
355
355
  this.config.set('skills', 'auto_download', 'disabled');
356
356
  }
357
357
  const modeStr = this.config.getStr('agent', 'default_mode');
358
- this.mode = (['plan', 'goal', 'flow'].includes(modeStr) ? modeStr : 'build');
358
+ this.mode = (['plan', 'chat', 'goal', 'flow'].includes(modeStr) ? modeStr : 'build');
359
359
  const inputStr = this.config.getStr('general', 'default_input');
360
360
  this.inputMode = inputStr === 'next' ? 'next' : 'guide';
361
361
  const configuredModel = this.config.getStr('models', 'default_model');
@@ -399,6 +399,8 @@ class Agent {
399
399
  }
400
400
  }
401
401
  setMode(m) {
402
+ if (!['build', 'plan', 'chat', 'goal', 'flow'].includes(m))
403
+ m = 'build';
402
404
  if (m === 'goal' && !this.goal) {
403
405
  this.goal = new GoalStateImpl('Set your objective');
404
406
  }
@@ -1016,7 +1018,13 @@ class Agent {
1016
1018
  beginRouteAttempt() {
1017
1019
  this.routeAttemptStartedAt = Date.now();
1018
1020
  }
1019
- async waitForPlannedRouteRetry() {
1021
+ async waitForPlannedRouteRetry(explicitDelayMs) {
1022
+ if (explicitDelayMs !== undefined) {
1023
+ if (explicitDelayMs <= 0)
1024
+ return;
1025
+ await new Promise(resolve => setTimeout(resolve, explicitDelayMs));
1026
+ return;
1027
+ }
1020
1028
  const waitBudgetMs = Math.max(0, Math.min(15_000, this.lastRouteDecision?.retryBudgetMs ?? 5_000));
1021
1029
  const delay = Math.max(0, Math.min(waitBudgetMs, this.lastRouteRetryDelayMs));
1022
1030
  this.lastRouteRetryDelayMs = 0;
@@ -5356,7 +5364,24 @@ class Agent {
5356
5364
  if (action !== 'update')
5357
5365
  return JSON.stringify({ ok: false, error: `Unknown linked_plan action: ${action}` });
5358
5366
  const expectedRevision = Number(input.expected_revision ?? input.expectedRevision);
5359
- return JSON.stringify({ ok: true, linkedPlan: this.updateLinkedPlan(String(input.markdown || ''), expectedRevision) }, null, 2);
5367
+ const current = this.getLinkedPlan();
5368
+ let markdown = input.markdown === undefined ? current.markdown : String(input.markdown);
5369
+ if (input.append !== undefined)
5370
+ markdown = `${current.markdown}${String(input.append)}`;
5371
+ if (input.old_text !== undefined || input.oldText !== undefined) {
5372
+ const oldText = String(input.old_text ?? input.oldText ?? '');
5373
+ if (!oldText)
5374
+ throw new Error('linked_plan old_text must not be empty.');
5375
+ const matches = current.markdown.split(oldText).length - 1;
5376
+ if (!matches)
5377
+ throw new Error('linked_plan old_text was not found.');
5378
+ const replaceAll = input.replace_all === true || input.replaceAll === true;
5379
+ if (matches > 1 && !replaceAll)
5380
+ throw new Error(`linked_plan old_text matched ${matches} places; pass replace_all=true or a unique fragment.`);
5381
+ const newText = String(input.new_text ?? input.newText ?? '');
5382
+ markdown = replaceAll ? current.markdown.split(oldText).join(newText) : current.markdown.replace(oldText, newText);
5383
+ }
5384
+ return JSON.stringify({ ok: true, linkedPlan: this.updateLinkedPlan(markdown, expectedRevision) }, null, 2);
5360
5385
  }
5361
5386
  catch (error) {
5362
5387
  return JSON.stringify({ ok: false, error: error instanceof Error ? error.message : String(error) });
@@ -7959,7 +7984,24 @@ class Agent {
7959
7984
  }));
7960
7985
  }
7961
7986
  case 'memory_lab_update': {
7962
- const result = await this.updateMemoryLab({
7987
+ const selector = String(params.component || params.slug || '').trim();
7988
+ const prepared = selector ? this.memoryLab.preparePatch({
7989
+ component: selector,
7990
+ name: params.name === undefined ? undefined : String(params.name),
7991
+ description: params.description === undefined ? undefined : String(params.description),
7992
+ tags: params.tags === undefined ? undefined : (Array.isArray(params.tags) ? params.tags.map(String) : String(params.tags).split(/[,,\n]+/)),
7993
+ tagPaths: params.tagPaths === undefined ? undefined : (Array.isArray(params.tagPaths) ? params.tagPaths.filter(Array.isArray).map(pathValue => pathValue.map(String)) : []),
7994
+ content: params.content === undefined ? undefined : String(params.content),
7995
+ contentAppend: params.contentAppend === undefined && params.content_append === undefined ? undefined : String(params.contentAppend ?? params.content_append),
7996
+ oldText: params.oldText === undefined && params.old_text === undefined ? undefined : String(params.oldText ?? params.old_text),
7997
+ newText: String(params.newText ?? params.new_text ?? ''),
7998
+ replaceAll: params.replaceAll === true || params.replace_all === true,
7999
+ kind: params.kind === undefined ? undefined : (params.kind === 'folder' ? 'folder' : 'file'),
8000
+ expectedUpdatedAt: String(params.expectedUpdatedAt || params.expected_updated_at || ''),
8001
+ reason: String(params.reason || ''),
8002
+ source: String(params.source || ''),
8003
+ }) : undefined;
8004
+ const result = await this.updateMemoryLab(prepared || {
7963
8005
  name: String(params.name || ''),
7964
8006
  description: String(params.description || ''),
7965
8007
  tags: Array.isArray(params.tags) ? params.tags.map(String) : String(params.tags || '').split(/[,,\n]+/),
@@ -9121,6 +9163,8 @@ class Agent {
9121
9163
  parts.push(this.buildFeatureDisclosurePrompt());
9122
9164
  if (this.mode === 'plan')
9123
9165
  parts.push(`[Plan Tool Policy]\n${(0, toolPolicy_1.planModePolicyPrompt)()}`);
9166
+ if (this.mode === 'chat')
9167
+ parts.push(`[Chat Tool Policy]\n${(0, toolPolicy_1.chatModePolicyPrompt)()}`);
9124
9168
  const pm = this.config.getStr('workspace', 'prompt_mode') || 'both';
9125
9169
  const injectedPrompts = new Set();
9126
9170
  if ((pm === 'global_only' || pm === 'both') && globalPrompt) {
@@ -9241,7 +9285,7 @@ class Agent {
9241
9285
  `- Language policy: general.language=${language}; the UI can switch this at runtime and each turn must obey the current value. auto follows the user's dominant input language, en replies in English, zh replies in Simplified Chinese. Keep code, commands, file paths, JSON keys, model/provider names, tool names, quoted source text, and user-provided literals exactly as required by their source language.`,
9242
9286
  `- Workspace permissions: access_permission=${permission}; file tools are checked before execution and blocked when they exceed the configured workspace boundary.`,
9243
9287
  `- Remote repository safety: when the active workspace or any target path is inside a GitHub/remote-backed repository, proactively use repo_security_audit and file_audit before git_push, gh_pr_create, release packaging, public reporting, or cloud-side audit. Treat public remotes as public disclosure surfaces and keep private URLs, secrets, privacy addresses (credential URLs, private network addresses, local user paths), local runtime state, archives, Memory Lab, Work, config, and release outputs out of commits and summaries. git_push/gh_pr_create hard-block on detected high-risk findings until a second review resolves them and the action is retried with security_review_confirmed=true.`,
9244
- `- Mode engine: current mode=${this.modeName()}; Build works autonomously, Plan is fully read-only with no file modifications, Goal continues until completion unless paused, Flow follows saved workflow components.`,
9288
+ `- Mode engine: current mode=${this.modeName()}; Build works autonomously, Plan is fully read-only, Chat only performs web search/fetch evidence gathering and prompt synthesis, Goal continues until completion unless paused, Flow follows saved workflow components.`,
9245
9289
  `- Input mode: ${input}; Guide injects immediately, Next queues user intent for the following build turn.`,
9246
9290
  `- Option feedback: ${this.buildQuestionPolicyPrompt(optionFeedback)}`,
9247
9291
  `- Model policy: current model=${this.model || '(unset)'}, intelligence=${this.intelligence}, auto-switch=${modelSwitch}.`,
@@ -9312,6 +9356,14 @@ class Agent {
9312
9356
  'Only after the durable linked plan has actually been updated and the plan is complete, expose the fixed mode handoff asking whether execution should begin. Offer exactly these two choices in the user language: "是,执行此计划" / "否,请补充____" (or "Yes, execute this plan" / "No, please supplement _____"). This fixed handoff remains required when discretionary questions are disabled.',
9313
9357
  'A positive choice starts a new Build-mode input. A negative choice remains in Plan mode so the user can supply the missing details.',
9314
9358
  ]).join('\n');
9359
+ case 'chat':
9360
+ return withLanguage([
9361
+ 'CHAT MODE.',
9362
+ 'Use only web_search and web_fetch. You have no workspace, host, application, memory, task, browser-control, or write permissions.',
9363
+ 'Perform an online search to gather evidence before answering. Fetch primary or authoritative pages when the search snippets are insufficient.',
9364
+ 'After sufficient evidence is collected, summarize and answer the user as soon as possible. Stay concise and do not turn the request into a long-running Build, Plan, Goal, or Flow task.',
9365
+ 'Distinguish sourced facts from uncertainty and include useful source links in the final answer.',
9366
+ ]).join('\n');
9315
9367
  case 'goal': {
9316
9368
  const g = this.goal?.history() || '';
9317
9369
  const paused = this.goal?.paused ? '\n[GOAL PAUSED by user. Wait for resume.]' : '\n[Continue working until the goal is achieved.]';
@@ -46,6 +46,7 @@ const toolPolicy_1 = require("./toolPolicy");
46
46
  const performanceDiagnostics_1 = require("./performanceDiagnostics");
47
47
  const agentKernelDiagnostics_1 = require("./agentKernelDiagnostics");
48
48
  const toolchain_1 = require("../toolchain");
49
+ const emptyResponseRetry_1 = require("./emptyResponseRetry");
49
50
  const publicStreamFilters = new WeakMap();
50
51
  const brokerOnlyAssistantBuffers = new WeakMap();
51
52
  const BROKER_PREFACE_BUFFER_CHARS = 96;
@@ -206,7 +207,7 @@ function kernelTurnFailed(agent, turn) {
206
207
  return turn.stopReason === 'error' || agent.isLlmErrorText(turn.text);
207
208
  }
208
209
  function providerTurnIsEmpty(turn) {
209
- return /provider returned an empty response/i.test(`${turn.errorMessage}\n${turn.text}`);
210
+ return !turn.activity && /provider returned an empty response/i.test(`${turn.errorMessage}\n${turn.text}`);
210
211
  }
211
212
  function removeTrailingFailedAssistant(agent, messages) {
212
213
  const last = messages[messages.length - 1];
@@ -216,6 +217,14 @@ function removeTrailingFailedAssistant(agent, messages) {
216
217
  if (last.stopReason === 'error' || agent.isLlmErrorText(text))
217
218
  messages.pop();
218
219
  }
220
+ function removeTrailingThoughtOnlyAssistant(messages) {
221
+ const last = messages[messages.length - 1];
222
+ if (last?.role !== 'assistant')
223
+ return;
224
+ const hasToolCall = last.content.some(content => content.type === 'toolCall');
225
+ if (!KernelMessageText(last).trim() && !hasToolCall)
226
+ messages.pop();
227
+ }
219
228
  function normalizePublicProviderError(error, secrets = []) {
220
229
  let raw = '';
221
230
  if (error instanceof Error) {
@@ -369,8 +378,22 @@ async function runAgentKernel(agent) {
369
378
  const tokens = [];
370
379
  const runOnce = async (promptMessages, appendPromptToAgentHistory) => {
371
380
  let lastAssistant = null;
381
+ let observedActivity = false;
382
+ let observedThought = false;
372
383
  const unsubscribe = kernel.subscribe(async (event) => {
373
384
  await handleKernelEvent(agent, event, tokens);
385
+ if (event.type === 'message_update') {
386
+ const delta = event.assistantMessageEvent;
387
+ const deltaText = typeof delta.delta === 'string'
388
+ ? delta.delta
389
+ : '';
390
+ const thoughtDelta = delta.type === 'thinking_delta' && !!deltaText.trim();
391
+ observedThought = observedThought || thoughtDelta;
392
+ observedActivity = observedActivity ||
393
+ thoughtDelta ||
394
+ (delta.type === 'text_delta' && !!deltaText.trim()) ||
395
+ (delta.type === 'toolcall_end');
396
+ }
374
397
  if (event.type === 'message_end' && event.message.role === 'assistant') {
375
398
  lastAssistant = event.message;
376
399
  }
@@ -390,11 +413,14 @@ async function runAgentKernel(agent) {
390
413
  const text = assistant ? KernelMessageText(assistant) : '';
391
414
  const hasToolCall = !!assistant?.content?.some(content => content.type === 'toolCall');
392
415
  const emptyResponse = !assistant
393
- || (!text.trim() && !hasToolCall && String(assistant?.stopReason || '') !== 'aborted');
416
+ || (!text.trim() && !hasToolCall && !observedActivity && String(assistant?.stopReason || '') !== 'aborted');
394
417
  return {
395
418
  text: emptyResponse ? '[Error] Provider returned an empty response.' : text,
396
419
  stopReason: String(assistant?.stopReason || ''),
397
420
  errorMessage: String(assistant?.errorMessage || (emptyResponse ? 'Provider returned an empty response.' : '')),
421
+ activity: observedActivity || !!text.trim() || hasToolCall,
422
+ thoughtOnly: observedThought && !text.trim() && !hasToolCall
423
+ && !['error', 'aborted'].includes(String(assistant?.stopReason || '')),
398
424
  };
399
425
  }
400
426
  finally {
@@ -448,14 +474,23 @@ async function runAgentKernel(agent) {
448
474
  fallback: { from: modelBeforeKernelRun, to: agent.model, providerId: agent.activeDeployment()?.providerId },
449
475
  });
450
476
  }
451
- let emptyResponseRetries = 0;
452
- while (providerTurnIsEmpty(lastTurn) && emptyResponseRetries < 2) {
477
+ let consecutiveEmptyResponses = 0;
478
+ for (;;) {
479
+ const emptyResponseState = (0, emptyResponseRetry_1.observeEmptyResponseOutcome)(consecutiveEmptyResponses, providerTurnIsEmpty(lastTurn));
480
+ consecutiveEmptyResponses = emptyResponseState.consecutiveEmptyResponses;
481
+ if (lastTurn.thoughtOnly) {
482
+ removeTrailingThoughtOnlyAssistant(kernel.state.messages);
483
+ lastTurn = await runWithCompressionResume([], false);
484
+ continue;
485
+ }
486
+ if (!emptyResponseState.retry)
487
+ break;
453
488
  removeTrailingFailedAssistant(agent, kernel.state.messages);
454
- emptyResponseRetries += 1;
455
- const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${emptyResponseRetries}/2).`;
489
+ const retryNumber = consecutiveEmptyResponses;
490
+ const notice = `[Model retry] Provider returned an empty response; retrying the same deployment (${retryNumber}/${emptyResponseRetry_1.MAX_EMPTY_RESPONSE_RETRIES}) after ${(0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses)}ms.`;
456
491
  tokens.push({ type: 'text', text: notice });
457
492
  agent.recordWorkStatus(notice);
458
- await agent.waitForPlannedRouteRetry();
493
+ await agent.waitForPlannedRouteRetry((0, emptyResponseRetry_1.emptyResponseRetryDelayMs)(consecutiveEmptyResponses));
459
494
  lastTurn = await runWithCompressionResume([], false);
460
495
  }
461
496
  let routeRetries = 0;
@@ -667,7 +702,7 @@ async function runAgentKernel(agent) {
667
702
  }
668
703
  if (textStarted)
669
704
  finalContent.push({ type: 'text', text });
670
- if (!finalContent.length) {
705
+ if (!finalContent.length && !thinking.trim()) {
671
706
  text = '[Error] Provider returned an empty response.';
672
707
  finalContent.push({ type: 'text', text });
673
708
  }
@@ -0,0 +1,16 @@
1
+ export declare const EMPTY_RESPONSE_RETRY_DELAYS_MS: readonly [200, 800, 2000, 10000, 60000];
2
+ /**
3
+ * A retry is scheduled only after an explicit provider empty-response
4
+ * failure. The initial failed request is not called a retry, so five retries
5
+ * means six consecutive explicit failures before termination.
6
+ */
7
+ export declare const MAX_EMPTY_RESPONSE_RETRIES: 5;
8
+ export declare const MAX_CONSECUTIVE_EMPTY_RESPONSES: number;
9
+ export declare function emptyResponseRetryDelayMs(consecutiveEmptyResponses: number): number;
10
+ export interface EmptyResponseRetryState {
11
+ consecutiveEmptyResponses: number;
12
+ retry: boolean;
13
+ terminate: boolean;
14
+ }
15
+ export declare function observeEmptyResponseOutcome(consecutiveEmptyResponses: number, emptyResponse: boolean): EmptyResponseRetryState;
16
+ //# sourceMappingURL=emptyResponseRetry.d.ts.map
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MAX_CONSECUTIVE_EMPTY_RESPONSES = exports.MAX_EMPTY_RESPONSE_RETRIES = exports.EMPTY_RESPONSE_RETRY_DELAYS_MS = void 0;
4
+ exports.emptyResponseRetryDelayMs = emptyResponseRetryDelayMs;
5
+ exports.observeEmptyResponseOutcome = observeEmptyResponseOutcome;
6
+ exports.EMPTY_RESPONSE_RETRY_DELAYS_MS = [200, 800, 2_000, 10_000, 60_000];
7
+ /**
8
+ * A retry is scheduled only after an explicit provider empty-response
9
+ * failure. The initial failed request is not called a retry, so five retries
10
+ * means six consecutive explicit failures before termination.
11
+ */
12
+ exports.MAX_EMPTY_RESPONSE_RETRIES = exports.EMPTY_RESPONSE_RETRY_DELAYS_MS.length;
13
+ exports.MAX_CONSECUTIVE_EMPTY_RESPONSES = exports.MAX_EMPTY_RESPONSE_RETRIES + 1;
14
+ function emptyResponseRetryDelayMs(consecutiveEmptyResponses) {
15
+ return exports.EMPTY_RESPONSE_RETRY_DELAYS_MS[Math.max(0, consecutiveEmptyResponses - 1)] ?? 0;
16
+ }
17
+ function observeEmptyResponseOutcome(consecutiveEmptyResponses, emptyResponse) {
18
+ const nextCount = emptyResponse ? consecutiveEmptyResponses + 1 : 0;
19
+ return {
20
+ consecutiveEmptyResponses: nextCount,
21
+ retry: emptyResponse && nextCount <= exports.MAX_EMPTY_RESPONSE_RETRIES,
22
+ terminate: emptyResponse && nextCount > exports.MAX_EMPTY_RESPONSE_RETRIES,
23
+ };
24
+ }
25
+ //# sourceMappingURL=emptyResponseRetry.js.map
@@ -35,6 +35,22 @@ export interface MemoryLabUpdateInput {
35
35
  reason?: string;
36
36
  source?: string;
37
37
  }
38
+ export interface MemoryLabPatchInput {
39
+ component: string;
40
+ name?: string;
41
+ description?: string;
42
+ tags?: string[];
43
+ tagPaths?: string[][];
44
+ content?: string;
45
+ contentAppend?: string;
46
+ oldText?: string;
47
+ newText?: string;
48
+ replaceAll?: boolean;
49
+ kind?: MemoryLabComponentKind;
50
+ expectedUpdatedAt?: string;
51
+ reason?: string;
52
+ source?: string;
53
+ }
38
54
  export interface MemoryLabPreparedUpdate extends MemoryLabUpdateInput {
39
55
  slug: string;
40
56
  description: string;
@@ -120,6 +136,7 @@ export declare class MemoryLabManager {
120
136
  read(componentSelector?: string): MemoryLabReadResult;
121
137
  visualizationSnapshot(): MemoryLabVisualizationResult;
122
138
  prepareUpdate(input: MemoryLabUpdateInput): MemoryLabPreparedUpdate;
139
+ preparePatch(input: MemoryLabPatchInput): MemoryLabPreparedUpdate;
123
140
  update(prepared: MemoryLabPreparedUpdate): MemoryLabWriteResult;
124
141
  query(input: {
125
142
  query: string;
@@ -91,7 +91,8 @@ class MemoryLabManager {
91
91
  'Use memory_lab_read to inspect index.json before deciding what memory is relevant.',
92
92
  'Use memory_lab_query for bounded task-relevant retrieval; do not inject the complete index when a focused query is sufficient.',
93
93
  'Use memory_lab_read with component/name/slug to read a component core markdown file.',
94
- 'Use memory_lab_update only when the user asks to create or update durable memory, passing name, description, tags, optional tagPaths, content, and optional kind=file|folder.',
94
+ 'Use memory_lab_update only when the user asks to create or update durable memory. Create with name, tags, and content; patch an existing component with component plus only changed fields.',
95
+ 'For small body edits prefer contentAppend or oldText/newText over resending the complete content.',
95
96
  'For an existing component, pass expectedUpdatedAt from the latest read/query result. A stale update is rejected instead of overwriting newer memory.',
96
97
  'Use memory_lab_delete only when the user explicitly asks to forget/remove durable memory. Delete moves the prior revision to Memory Lab/archive and records a policy event.',
97
98
  'Every mutation should include a concise reason and source. ADD, UPDATE, and DELETE decisions are append-only in policy.jsonl and are recoverable from archive.',
@@ -206,6 +207,47 @@ class MemoryLabManager {
206
207
  source: String(input.source || '').trim(),
207
208
  };
208
209
  }
210
+ preparePatch(input) {
211
+ const selector = String(input.component || '').trim();
212
+ if (!selector)
213
+ throw new Error('Memory component is required for a patch.');
214
+ const current = this.read(selector);
215
+ if (!current.ok || !current.component)
216
+ throw new Error(current.error || `Memory component not found: ${selector}`);
217
+ const existing = current.component.meta;
218
+ const oldContent = current.component.content;
219
+ let content = input.content !== undefined ? String(input.content) : oldContent;
220
+ if (input.contentAppend !== undefined)
221
+ content = `${oldContent}${String(input.contentAppend)}`;
222
+ if (input.oldText !== undefined) {
223
+ const oldText = String(input.oldText);
224
+ if (!oldText)
225
+ throw new Error('oldText must not be empty.');
226
+ const matches = oldContent.split(oldText).length - 1;
227
+ if (!matches)
228
+ throw new Error('oldText was not found in the Memory Lab component.');
229
+ if (matches > 1 && input.replaceAll !== true)
230
+ throw new Error(`oldText matched ${matches} places; pass replaceAll=true or a unique fragment.`);
231
+ content = input.replaceAll === true
232
+ ? oldContent.split(oldText).join(String(input.newText || ''))
233
+ : oldContent.replace(oldText, String(input.newText || ''));
234
+ }
235
+ const name = input.name === undefined ? existing.name : String(input.name);
236
+ if (this.slugify(name) !== current.component.slug) {
237
+ throw new Error('Renaming a Memory Lab component is not supported by incremental patch; create the new component then delete the old one.');
238
+ }
239
+ return this.prepareUpdate({
240
+ name,
241
+ description: input.description === undefined ? existing.description : String(input.description),
242
+ tags: input.tags === undefined ? existing.tags : input.tags,
243
+ tagPaths: input.tagPaths === undefined ? existing.tagPaths : input.tagPaths,
244
+ content,
245
+ kind: input.kind === undefined ? existing.kind : input.kind,
246
+ expectedUpdatedAt: String(input.expectedUpdatedAt || existing.updatedAt),
247
+ reason: input.reason,
248
+ source: input.source,
249
+ });
250
+ }
209
251
  update(prepared) {
210
252
  this.ensure();
211
253
  const index = this.loadIndex();
@@ -0,0 +1,24 @@
1
+ export declare const TERMINAL_HISTORY_LIMIT: number;
2
+ export interface TerminalOutputBufferOptions {
3
+ flushIntervalMs?: number;
4
+ historyLimit?: number;
5
+ }
6
+ export declare class TerminalOutputBuffer {
7
+ private readonly send;
8
+ private readonly sessions;
9
+ private timer;
10
+ private readonly flushIntervalMs;
11
+ private readonly historyLimit;
12
+ constructor(send: (sessionId: string, text: string) => void, options?: TerminalOutputBufferOptions);
13
+ push(sessionId: string, text: string): void;
14
+ flush(sessionId: string): void;
15
+ flushAll(): void;
16
+ close(sessionId: string): string;
17
+ history(sessionId: string): string;
18
+ pendingChunkCount(): number;
19
+ hasScheduledFlush(): boolean;
20
+ private bound;
21
+ private schedule;
22
+ private clearTimerWhenIdle;
23
+ }
24
+ //# sourceMappingURL=terminalOutputBuffer.d.ts.map
@@ -0,0 +1,92 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.TerminalOutputBuffer = exports.TERMINAL_HISTORY_LIMIT = void 0;
4
+ exports.TERMINAL_HISTORY_LIMIT = 256 * 1024;
5
+ class TerminalOutputBuffer {
6
+ send;
7
+ sessions = new Map();
8
+ timer = null;
9
+ flushIntervalMs;
10
+ historyLimit;
11
+ constructor(send, options = {}) {
12
+ this.send = send;
13
+ this.flushIntervalMs = Math.max(1, options.flushIntervalMs ?? 20);
14
+ this.historyLimit = Math.max(1, options.historyLimit ?? exports.TERMINAL_HISTORY_LIMIT);
15
+ }
16
+ push(sessionId, text) {
17
+ if (!text)
18
+ return;
19
+ let state = this.sessions.get(sessionId);
20
+ if (!state) {
21
+ state = { chunks: [], length: 0, history: '' };
22
+ this.sessions.set(sessionId, state);
23
+ }
24
+ state.chunks.push(text);
25
+ state.length += text.length;
26
+ this.schedule();
27
+ }
28
+ flush(sessionId) {
29
+ const state = this.sessions.get(sessionId);
30
+ if (!state?.length)
31
+ return;
32
+ const text = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
33
+ state.chunks = [];
34
+ state.length = 0;
35
+ state.history = this.bound(`${state.history}${text}`);
36
+ this.send(sessionId, text);
37
+ this.clearTimerWhenIdle();
38
+ }
39
+ flushAll() {
40
+ for (const sessionId of this.sessions.keys())
41
+ this.flush(sessionId);
42
+ this.clearTimerWhenIdle(true);
43
+ }
44
+ close(sessionId) {
45
+ this.flush(sessionId);
46
+ const history = this.sessions.get(sessionId)?.history ?? '';
47
+ this.sessions.delete(sessionId);
48
+ this.clearTimerWhenIdle();
49
+ return history;
50
+ }
51
+ history(sessionId) {
52
+ const state = this.sessions.get(sessionId);
53
+ if (!state)
54
+ return '';
55
+ if (!state.length)
56
+ return state.history;
57
+ const pending = state.chunks.length === 1 ? state.chunks[0] : state.chunks.join('');
58
+ return this.bound(`${state.history}${pending}`);
59
+ }
60
+ pendingChunkCount() {
61
+ let count = 0;
62
+ for (const state of this.sessions.values())
63
+ count += state.chunks.length;
64
+ return count;
65
+ }
66
+ hasScheduledFlush() {
67
+ return this.timer !== null;
68
+ }
69
+ bound(text) {
70
+ return text.length > this.historyLimit ? text.slice(-this.historyLimit) : text;
71
+ }
72
+ schedule() {
73
+ if (this.timer)
74
+ return;
75
+ this.timer = setTimeout(() => {
76
+ this.timer = null;
77
+ this.flushAll();
78
+ }, this.flushIntervalMs);
79
+ this.timer.unref?.();
80
+ }
81
+ clearTimerWhenIdle(force = false) {
82
+ if (!this.timer)
83
+ return;
84
+ const hasPending = !force && Array.from(this.sessions.values()).some(state => state.length > 0);
85
+ if (hasPending)
86
+ return;
87
+ clearTimeout(this.timer);
88
+ this.timer = null;
89
+ }
90
+ }
91
+ exports.TerminalOutputBuffer = TerminalOutputBuffer;
92
+ //# sourceMappingURL=terminalOutputBuffer.js.map
@@ -25,6 +25,7 @@ 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 declare function chatModePolicyPrompt(): string;
28
29
  export interface DeletionGuardDecision {
29
30
  blocked: boolean;
30
31
  reason?: string;
@@ -7,6 +7,7 @@ exports.toolAvailability = toolAvailability;
7
7
  exports.evaluateToolPolicy = evaluateToolPolicy;
8
8
  exports.filterToolDefinitions = filterToolDefinitions;
9
9
  exports.planModePolicyPrompt = planModePolicyPrompt;
10
+ exports.chatModePolicyPrompt = chatModePolicyPrompt;
10
11
  exports.evaluateDeletionGuard = evaluateDeletionGuard;
11
12
  const REQUIRED_TOOLS = new Set(['pwd', 'read', 'glob', 'grep']);
12
13
  const MODE_SCOPED_TOOLS = new Set([
@@ -81,6 +82,7 @@ exports.PLAN_COMPUTER_USE_ACTIONS = ['observe', 'app_list', 'app_observe'];
81
82
  exports.PLAN_BROWSER_USE_ACTIONS = ['observe', 'navigate', 'wait', 'extract'];
82
83
  const PLAN_COMPUTER_USE_ACTION_SET = new Set(exports.PLAN_COMPUTER_USE_ACTIONS);
83
84
  const PLAN_BROWSER_USE_ACTION_SET = new Set(exports.PLAN_BROWSER_USE_ACTIONS);
85
+ const CHAT_WEB_TOOLS = new Set(['web_search', 'web_fetch']);
84
86
  /**
85
87
  * 并发安全工具集合(DSH isConcurrencySafe 语义的 Newmark 落地)。
86
88
  *
@@ -140,6 +142,13 @@ function evaluateToolPolicy(request) {
140
142
  const base = { availability, settingsVisible: availability === 'configurable' };
141
143
  if (!name)
142
144
  return { ...base, allowed: false, reason: '[permission] Tool name is required.' };
145
+ if (request.mode === 'chat' && !CHAT_WEB_TOOLS.has(name)) {
146
+ return {
147
+ ...base,
148
+ allowed: false,
149
+ reason: `[permission] Chat mode only allows web_search and web_fetch. It has no workspace, host, application, memory, task, or other write access. Blocked: ${name}`,
150
+ };
151
+ }
143
152
  if (request.mode === 'plan') {
144
153
  if (name === 'computer_use') {
145
154
  const action = String(request.args?.action || '').trim();
@@ -182,6 +191,13 @@ function planModePolicyPrompt() {
182
191
  'Runtime policy rejects stale or hidden mutating tool calls even if a prompt asks for them.',
183
192
  ].join(' ');
184
193
  }
194
+ function chatModePolicyPrompt() {
195
+ return [
196
+ 'Chat mode is a narrow web-evidence mode.',
197
+ 'Only web_search and web_fetch are available; every workspace, host, application, memory, task, browser-control, and write capability is denied at runtime.',
198
+ 'Search the web for relevant evidence, fetch primary or authoritative sources when useful, then summarize and answer promptly instead of expanding into a long-running task.',
199
+ ].join(' ');
200
+ }
185
201
  /** 删除命令动词(跨 POSIX / PowerShell / cmd)。注意:不含单独 "remove"(避免匹配普通英文)。 */
186
202
  const DELETE_VERB_SOURCE = '(?:remove-item|rmdir|unlink|erase|del|rm|rd|ri)';
187
203
  const DELETE_VERB_BOUNDARY = new RegExp(`(?:^|[\\s;&|()\\n])${DELETE_VERB_SOURCE}(?:\\s|$)`, 'i');
@@ -1,4 +1,4 @@
1
- export type AgentMode = 'build' | 'plan' | 'goal' | 'flow';
1
+ export type AgentMode = 'build' | 'plan' | 'chat' | 'goal' | 'flow';
2
2
  export type InputMode = 'guide' | 'next';
3
3
  export type AgentStatus = 'idle' | 'working' | 'error' | 'goal_paused';
4
4
  /** Public routing identity used by renderer/main/runtime envelopes. */
@@ -96,7 +96,7 @@ export declare class LLMProvider {
96
96
  * `provider_adapters_v2` context flag. Request serialization and SSE
97
97
  * normalization are delegated to the shared provider adapters while the
98
98
  * transport orchestration (loopback node-http, fetch -> node-http fallback,
99
- * 120s/30s timeouts) and the 4xx Chat -> Responses downgrade stay here.
99
+ * cancellation-only streaming and the 4xx Chat -> Responses downgrade) stay here.
100
100
  * The emitted request body and StreamToken stream are byte-equivalent to
101
101
  * the legacy inlined path.
102
102
  */
@@ -106,9 +106,9 @@ export declare class LLMProvider {
106
106
  private shouldDowngradeToResponses;
107
107
  /**
108
108
  * Loopback-aware transport injected into adapter `execute`. Streaming
109
- * requests retain the fetch-to-node fallback for transport failures, while
110
- * a local deadline is returned directly so one request cannot become a
111
- * second Windows fallback request.
109
+ * requests retain the fetch-to-node fallback for transport failures. They
110
+ * have no response deadline; only caller cancellation or a concrete
111
+ * transport/provider failure may end the request.
112
112
  */
113
113
  private buildProviderAdapterTransport;
114
114
  private toTransportResponse;