newmark-agent 0.3.6 → 0.3.7

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.
@@ -327011,6 +327011,8 @@ var NATIVE_TOOL_CATALOG = [
327011
327011
  { name: "subagent_close", label: "Subagent close", description: "Close a same-conversation peer agent.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327012
327012
  { name: "linked_plan", label: "Linked plan", description: "Read or conservatively update the conversation-linked Markdown plan.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327013
327013
  { name: "build_history_query", label: "Build history query", description: "Read concrete public work details for one historical Build Block.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327014
+ { name: "context_compress", label: "Context compress", description: "Actively compress the LLM context history, leaving the displayed conversation history unchanged.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327015
+ { name: "context_history_manage", label: "Context history manage", description: "List, remove, or summarize entries in the LLM context history without touching the displayed conversation history.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327014
327016
  { name: "question", label: "Ask question", description: "Ask the user for structured option feedback.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327015
327017
  { name: "skill_download", label: "Skill download", description: "Download and install a skill.", category: "agent", defaultEnabled: true },
327016
327018
  { name: "skill", label: "Skill", description: "Search enabled skill metadata or load one skill body on demand.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
@@ -334501,6 +334503,8 @@ var MODE_SCOPED_TOOLS = /* @__PURE__ */ new Set([
334501
334503
  "pdf_read",
334502
334504
  "linked_plan",
334503
334505
  "build_history_query",
334506
+ "context_compress",
334507
+ "context_history_manage",
334504
334508
  "question",
334505
334509
  "task",
334506
334510
  "subagent_list",
@@ -335522,14 +335526,21 @@ var ToolExecutor = class {
335522
335526
  remote_root: { type: "string" },
335523
335527
  remote_path: { type: "string" }
335524
335528
  }, ["action"]),
335525
- t3("task", "Create a same-conversation peer agent and return immediately. The peer has a nature slug, short id, and canonical UUID-qualified name. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent's currently resolved model deployment. Plan mode peers are forced to Plan.", { nature: { type: "string" }, name: { type: "string", description: "Legacy alias for nature." }, prompt: { type: "string" }, preset: { type: "string" }, agent: { type: "string" }, model: { type: "string", description: "Optional exact model deployment. Omit to inherit the parent Agent resolved model." }, mode: { type: "string" }, input_mode: { type: "string" }, flow: { type: "string" } }, ["prompt"]),
335526
- t3("subagent_list", "List flat same-conversation peer agents, optionally filtered by status.", { status: { type: "string", enum: ["idle", "queued", "working", "completed", "error", "closed"] } }, []),
335527
- t3("subagent_read", "Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers.", { id: { type: "string" }, name: { type: "string" }, max_chars: { type: "number", description: "Bounded result size from 2000 to 32000 characters." } }, []),
335528
- t3("subagent_send", "Persist a mailbox message to a same-conversation peer agent.", { id: { type: "string" }, name: { type: "string" }, message: { type: "string" }, prompt: { type: "string", description: "Legacy alias for message." }, kind: { type: "string", enum: ["directive", "question", "result", "handoff"] }, reply_to: { type: "string" }, correlation_id: { type: "string" } }, []),
335529
- t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent.", { id: { type: "string" }, name: { type: "string" } }, []),
335530
- t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself.", { id: { type: "string" }, name: { type: "string" } }, []),
335529
+ t3("task", "Create a same-conversation peer agent and return immediately. The peer has a stable human-readable name, a short id, and a canonical UUID-qualified identity. The peer name is decoupled from its id: pass name for readable references and id for exact targeting. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent's currently resolved model deployment. Plan mode peers are forced to Plan.", { nature: { type: "string" }, name: { type: "string", description: "Legacy alias for nature." }, prompt: { type: "string" }, preset: { type: "string" }, agent: { type: "string" }, model: { type: "string", description: "Optional exact model deployment. Omit to inherit the parent Agent resolved model." }, mode: { type: "string" }, input_mode: { type: "string" }, flow: { type: "string" } }, ["prompt"]),
335530
+ t3("subagent_list", "List flat same-conversation peer agents, optionally filtered by status. Each entry exposes both the stable name and the exact id; use the id for any subsequent targeting.", { status: { type: "string", enum: ["idle", "queued", "working", "completed", "error", "closed"] } }, []),
335531
+ t3("subagent_read", "Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers. Pass the exact id returned by subagent_list, or a name for convenience lookup.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name; ambiguous names resolve to the first match." }, max_chars: { type: "number", description: "Bounded result size from 2000 to 32000 characters." } }, []),
335532
+ t3("subagent_send", "Persist a mailbox message to a same-conversation peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." }, message: { type: "string" }, prompt: { type: "string", description: "Legacy alias for message." }, kind: { type: "string", enum: ["directive", "question", "result", "handoff"] }, reply_to: { type: "string" }, correlation_id: { type: "string" } }, []),
335533
+ t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
335534
+ t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
335531
335535
  t3("linked_plan", "Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.", { action: { type: "string", enum: ["get", "update"] }, markdown: { type: "string" }, expected_revision: { type: "number" } }, ["action"]),
335532
335536
  t3("build_history_query", "Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." } }, []),
335537
+ t3("context_compress", "Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.", { keep_recent: { type: "number", minimum: 2, maximum: 60, description: "Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages." }, force: { type: "boolean", description: "Compress even if the context is not yet over the automatic threshold. Defaults to false." } }, []),
335538
+ t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry. The displayed conversation history (what the user sees) is never modified by any action.", {
335539
+ action: { type: "string", enum: ["list", "remove", "summarize"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry." },
335540
+ position: { type: "number", minimum: 0, description: "0-based context entry index for remove, or the start of the range for summarize." },
335541
+ to: { type: "number", minimum: 0, description: "0-based inclusive end of the range for summarize. Defaults to position." },
335542
+ limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list; defaults to 200." }
335543
+ }, ["action"]),
335533
335544
  t3("question", "Ask user a multiple-choice question", { questions: { type: "array" } }, ["questions"]),
335534
335545
  t3("skill_download", "Download a skill", { name: { type: "string" }, source: { type: "string" } }, ["name", "source"]),
335535
335546
  t3("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 } }, []),
@@ -337615,7 +337626,7 @@ var SubagentManager = class {
337615
337626
  natureSlug: slug,
337616
337627
  displayName,
337617
337628
  qualifiedName,
337618
- name: qualifiedName,
337629
+ name: slug,
337619
337630
  conversationId: this.conversationId,
337620
337631
  createdByAgentId,
337621
337632
  prompt,
@@ -337626,7 +337637,7 @@ var SubagentManager = class {
337626
337637
  flowName: flowName || void 0,
337627
337638
  flowPc: Math.max(0, Math.floor(Number(flowPc) || 0)),
337628
337639
  status: "queued",
337629
- messages: [{ role: "system", content: `Peer agent '${qualifiedName}': ${prompt}` }, { role: "user", content: prompt, hidden_user_input: true }],
337640
+ messages: [{ role: "system", content: `Peer agent '${slug}' (${id}): ${prompt}` }, { role: "user", content: prompt, hidden_user_input: true }],
337630
337641
  result: null,
337631
337642
  createdAt: stamp,
337632
337643
  updatedAt: stamp
@@ -337636,7 +337647,10 @@ var SubagentManager = class {
337636
337647
  return id;
337637
337648
  }
337638
337649
  get(id) {
337639
- return this.subs.get(id) || [...this.subs.values()].find((item) => item.name === id || item.qualifiedName === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
337650
+ if (this.subs.has(id)) return this.subs.get(id);
337651
+ const exact = [...this.subs.values()].find((item) => item.id === id || item.qualifiedName === id);
337652
+ if (exact) return exact;
337653
+ return [...this.subs.values()].find((item) => item.name === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
337640
337654
  }
337641
337655
  send(id, prompt) {
337642
337656
  const target = this.get(id);
@@ -337784,6 +337798,7 @@ var SubagentManager = class {
337784
337798
  natureSlug: record.natureSlug,
337785
337799
  displayName: record.displayName,
337786
337800
  qualifiedName: record.qualifiedName,
337801
+ name: record.name,
337787
337802
  createdByAgentId: record.createdByAgentId,
337788
337803
  status: record.status,
337789
337804
  active: record.status !== "closed",
@@ -340412,6 +340427,8 @@ async function executeNewmarkTool(agent, name50, args, inputSchema, signal) {
340412
340427
  if (name50 === "subagent_close") return agent.handleSubagentCloseEnvelope(args).output;
340413
340428
  if (name50 === "linked_plan") return agent.handleLinkedPlanTool(args);
340414
340429
  if (name50 === "build_history_query") return agent.handleBuildHistoryQuery(args);
340430
+ if (name50 === "context_compress") return (await agent.handleContextCompress(args, signal)).output;
340431
+ if (name50 === "context_history_manage") return agent.handleContextHistoryManage(args).output;
340415
340432
  if (name50 === "question") {
340416
340433
  if (agent.config.getStr("agent", "option_feedback") === "fully_autonomous") return "[question] Disabled by fully_autonomous option feedback.";
340417
340434
  if (!agent.handleQuestion(args)) return "[Question rejected: at least one question with two labeled options is required.]";
@@ -346148,6 +346165,132 @@ Review this persisted peer result and summarize or continue the parent task as n
346148
346165
  truncatedActivities: Math.max(0, publicEvents.length - activities.length)
346149
346166
  });
346150
346167
  }
346168
+ async handleContextCompress(args, signal) {
346169
+ let input = {};
346170
+ try {
346171
+ input = JSON.parse(args || "{}");
346172
+ } catch {
346173
+ }
346174
+ if (this.history.length <= 1) return { ok: false, output: "[context_compress] No context to compress.", error: "No context to compress." };
346175
+ const previousKeepLast = this.config.getNum("context", "keep_recent_messages");
346176
+ const keepRecent = Math.max(2, Math.min(60, Math.floor(Number(input.keep_recent) || previousKeepLast || 10)));
346177
+ const force = Boolean(input.force);
346178
+ try {
346179
+ this.config.set("context", "keep_recent_messages", keepRecent);
346180
+ const msgs = this.history.map((message) => ({ ...message }));
346181
+ const provider = this.engineModel();
346182
+ await this.maybeCompress(msgs, provider, signal, this.activeModelName(), force);
346183
+ if (this.history.length <= 1) return { ok: false, output: "[context_compress] Compression skipped: context unchanged.", error: "Compression skipped." };
346184
+ return {
346185
+ ok: true,
346186
+ output: JSON.stringify({
346187
+ ok: true,
346188
+ compressed: true,
346189
+ at: this.lastCompression?.at,
346190
+ originalMessages: this.lastCompression?.originalMessages,
346191
+ compressedMessages: this.lastCompression?.compressedMessages,
346192
+ originalChars: this.lastCompression?.originalChars,
346193
+ compressedChars: this.lastCompression?.compressedChars,
346194
+ estimatedTokens: this.lastCompression?.compressedTokens,
346195
+ summary: this.lastCompression?.summary?.slice(0, 2e3),
346196
+ model: this.lastCompression?.model,
346197
+ fallback: this.lastCompression?.fallback,
346198
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346199
+ }, null, 2),
346200
+ metadata: { kind: "context-compress" }
346201
+ };
346202
+ } finally {
346203
+ this.config.set("context", "keep_recent_messages", previousKeepLast);
346204
+ }
346205
+ }
346206
+ handleContextHistoryManage(args) {
346207
+ let input = {};
346208
+ try {
346209
+ input = JSON.parse(args || "{}");
346210
+ } catch {
346211
+ }
346212
+ const action = String(input.action || "").trim();
346213
+ if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize).", error: "action is required." };
346214
+ if (action === "list") {
346215
+ const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
346216
+ const entries = this.history.slice(0, limit).map((message, index) => ({
346217
+ position: index,
346218
+ role: String(message.role || ""),
346219
+ name: String(message.name || ""),
346220
+ chars: typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length,
346221
+ preview: this.compressionHistoryContent(message.content || "").slice(0, 160)
346222
+ }));
346223
+ return {
346224
+ ok: true,
346225
+ output: JSON.stringify({
346226
+ ok: true,
346227
+ action: "list",
346228
+ entryCount: this.history.length,
346229
+ listed: entries.length,
346230
+ entries,
346231
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346232
+ }, null, 2),
346233
+ metadata: { kind: "context-history-list" }
346234
+ };
346235
+ }
346236
+ if (action === "remove") {
346237
+ const position = Math.floor(Number(input.position));
346238
+ if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
346239
+ return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: "remove position out of range." };
346240
+ }
346241
+ const removed = this.history.splice(position, 1)[0];
346242
+ this.saveWorkspaceConversationState(true);
346243
+ return {
346244
+ ok: true,
346245
+ output: JSON.stringify({
346246
+ ok: true,
346247
+ action: "remove",
346248
+ removedPosition: position,
346249
+ removedRole: String(removed?.role || ""),
346250
+ remaining: this.history.length,
346251
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346252
+ }, null, 2),
346253
+ metadata: { kind: "context-history-remove" }
346254
+ };
346255
+ }
346256
+ if (action === "summarize") {
346257
+ const from = Math.max(0, Math.floor(Number(input.position || 0)));
346258
+ const toRaw = Math.floor(Number(input.to ?? input.position));
346259
+ const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
346260
+ if (from >= this.history.length) return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: "summarize position out of range." };
346261
+ if (to - from < 1) return { ok: false, output: "[context_history_manage] summarize requires at least two entries in range.", error: "summarize requires a range of at least two entries." };
346262
+ const segment = this.history.slice(from, to + 1);
346263
+ const chars = segment.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
346264
+ const summary = this.localCompressionSummary(
346265
+ `Workspace: ${this.workspace.current?.path || this.rootPath}
346266
+ Mode: ${this.modeName()}`,
346267
+ segment.map((message, i4) => `#${i4 + 1} [${String(message.role || "unknown")}${message.name ? ` ${String(message.name)}` : ""}]
346268
+ ${this.compressionHistoryContent(message.content || "")}`).join("\n\n").slice(0, 2e4),
346269
+ segment.length,
346270
+ chars
346271
+ );
346272
+ const replacement = { role: "system", content: `[Context History Summary]
346273
+ ${summary}` };
346274
+ this.history.splice(from, to - from + 1, replacement);
346275
+ this.saveWorkspaceConversationState(true);
346276
+ return {
346277
+ ok: true,
346278
+ output: JSON.stringify({
346279
+ ok: true,
346280
+ action: "summarize",
346281
+ foldedFrom: from,
346282
+ foldedTo: to,
346283
+ foldedEntries: to - from + 1,
346284
+ foldedChars: chars,
346285
+ remaining: this.history.length,
346286
+ summary: summary.slice(0, 2e3),
346287
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346288
+ }, null, 2),
346289
+ metadata: { kind: "context-history-summarize" }
346290
+ };
346291
+ }
346292
+ return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
346293
+ }
346151
346294
  recordContextCompressionStep() {
346152
346295
  const runId = this.currentWorkRunId();
346153
346296
  if (!runId) return;
@@ -348015,7 +348158,7 @@ ${settled?.result || settled?.error || ""}`.trim();
348015
348158
  const transcript = sa.messages.map((m2) => `[${m2.role}] ${m2.content}`).join("\n");
348016
348159
  return this.subagents.toToolResult(
348017
348160
  sa.id,
348018
- `get.subagent("${sa.name}")
348161
+ `get.subagent("${sa.name}", id="${sa.id}")
348019
348162
  Status: ${sa.status}
348020
348163
  Model: ${sa.model}
348021
348164
  Mode: ${sa.agentMode}
@@ -348807,7 +348950,7 @@ Falling back to built-in engine.` }];
348807
348950
  if (!this.config.getBool("context", "auto_compress")) return;
348808
348951
  const total = msgs.reduce((sum, m2) => sum + (typeof m2.content === "string" ? m2.content.length : JSON.stringify(m2.content || "").length), 0);
348809
348952
  const budget = this.compressionBudget(msgs);
348810
- if (budget.estimatedTokens < budget.triggerTokens) return;
348953
+ if (budget.estimatedTokens < budget.triggerTokens && !force) return;
348811
348954
  if (!force && this.lastCompression && String(msgs[0]?.content || "").includes(this.lastCompression.summary)) {
348812
348955
  const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
348813
348956
  const baselineTokens = Math.max(0, Number(this.lastCompression.compressedTokens || 0));
@@ -590,6 +590,8 @@ export declare class Agent {
590
590
  endedAt?: string;
591
591
  }>;
592
592
  handleBuildHistoryQuery(args: string): string;
593
+ handleContextCompress(args: string, signal?: AbortSignal): Promise<NewmarkToolResult>;
594
+ handleContextHistoryManage(args: string): NewmarkToolResult;
593
595
  recordContextCompressionStep(): void;
594
596
  compressionContinuationPrompt(): string;
595
597
  mirrorConversationStateFrom(id: string, source: Pick<Agent, 'chatMessages' | 'history' | 'conversationPlan'> & Partial<Pick<Agent, 'linkedPlan' | 'subagents' | 'workRuns' | 'continuations'>> & {
@@ -3417,6 +3417,130 @@ class Agent {
3417
3417
  truncatedActivities: Math.max(0, publicEvents.length - activities.length),
3418
3418
  });
3419
3419
  }
3420
+ async handleContextCompress(args, signal) {
3421
+ let input = {};
3422
+ try {
3423
+ input = JSON.parse(args || '{}');
3424
+ }
3425
+ catch { }
3426
+ if (this.history.length <= 1)
3427
+ return { ok: false, output: '[context_compress] No context to compress.', error: 'No context to compress.' };
3428
+ const previousKeepLast = this.config.getNum('context', 'keep_recent_messages');
3429
+ const keepRecent = Math.max(2, Math.min(60, Math.floor(Number(input.keep_recent) || previousKeepLast || 10)));
3430
+ const force = Boolean(input.force);
3431
+ try {
3432
+ this.config.set('context', 'keep_recent_messages', keepRecent);
3433
+ const msgs = this.history.map(message => ({ ...message }));
3434
+ const provider = this.engineModel();
3435
+ await this.maybeCompress(msgs, provider, signal, this.activeModelName(), force);
3436
+ if (this.history.length <= 1)
3437
+ return { ok: false, output: '[context_compress] Compression skipped: context unchanged.', error: 'Compression skipped.' };
3438
+ return {
3439
+ ok: true,
3440
+ output: JSON.stringify({
3441
+ ok: true,
3442
+ compressed: true,
3443
+ at: this.lastCompression?.at,
3444
+ originalMessages: this.lastCompression?.originalMessages,
3445
+ compressedMessages: this.lastCompression?.compressedMessages,
3446
+ originalChars: this.lastCompression?.originalChars,
3447
+ compressedChars: this.lastCompression?.compressedChars,
3448
+ estimatedTokens: this.lastCompression?.compressedTokens,
3449
+ summary: this.lastCompression?.summary?.slice(0, 2000),
3450
+ model: this.lastCompression?.model,
3451
+ fallback: this.lastCompression?.fallback,
3452
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3453
+ }, null, 2),
3454
+ metadata: { kind: 'context-compress' },
3455
+ };
3456
+ }
3457
+ finally {
3458
+ this.config.set('context', 'keep_recent_messages', previousKeepLast);
3459
+ }
3460
+ }
3461
+ handleContextHistoryManage(args) {
3462
+ let input = {};
3463
+ try {
3464
+ input = JSON.parse(args || '{}');
3465
+ }
3466
+ catch { }
3467
+ const action = String(input.action || '').trim();
3468
+ if (!action)
3469
+ return { ok: false, output: '[context_history_manage] action is required (list|remove|summarize).', error: 'action is required.' };
3470
+ if (action === 'list') {
3471
+ const limit = Math.max(5, Math.min(400, Math.floor(Number(input.limit || 200))));
3472
+ const entries = this.history.slice(0, limit).map((message, index) => ({
3473
+ position: index,
3474
+ role: String(message.role || ''),
3475
+ name: String(message.name || ''),
3476
+ chars: typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content || '').length,
3477
+ preview: this.compressionHistoryContent(message.content || '').slice(0, 160),
3478
+ }));
3479
+ return {
3480
+ ok: true,
3481
+ output: JSON.stringify({
3482
+ ok: true,
3483
+ action: 'list',
3484
+ entryCount: this.history.length,
3485
+ listed: entries.length,
3486
+ entries,
3487
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3488
+ }, null, 2),
3489
+ metadata: { kind: 'context-history-list' },
3490
+ };
3491
+ }
3492
+ if (action === 'remove') {
3493
+ const position = Math.floor(Number(input.position));
3494
+ if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
3495
+ return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: 'remove position out of range.' };
3496
+ }
3497
+ const removed = this.history.splice(position, 1)[0];
3498
+ this.saveWorkspaceConversationState(true);
3499
+ return {
3500
+ ok: true,
3501
+ output: JSON.stringify({
3502
+ ok: true,
3503
+ action: 'remove',
3504
+ removedPosition: position,
3505
+ removedRole: String(removed?.role || ''),
3506
+ remaining: this.history.length,
3507
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3508
+ }, null, 2),
3509
+ metadata: { kind: 'context-history-remove' },
3510
+ };
3511
+ }
3512
+ if (action === 'summarize') {
3513
+ const from = Math.max(0, Math.floor(Number(input.position || 0)));
3514
+ const toRaw = Math.floor(Number(input.to ?? input.position));
3515
+ const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
3516
+ if (from >= this.history.length)
3517
+ return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: 'summarize position out of range.' };
3518
+ if (to - from < 1)
3519
+ return { ok: false, output: '[context_history_manage] summarize requires at least two entries in range.', error: 'summarize requires a range of at least two entries.' };
3520
+ const segment = this.history.slice(from, to + 1);
3521
+ const chars = segment.reduce((sum, message) => sum + (typeof message.content === 'string' ? message.content.length : JSON.stringify(message.content || '').length), 0);
3522
+ const summary = this.localCompressionSummary(`Workspace: ${this.workspace.current?.path || this.rootPath}\nMode: ${this.modeName()}`, segment.map((message, i) => `#${i + 1} [${String(message.role || 'unknown')}${message.name ? ` ${String(message.name)}` : ''}]\n${this.compressionHistoryContent(message.content || '')}`).join('\n\n').slice(0, 20000), segment.length, chars);
3523
+ const replacement = { role: 'system', content: `[Context History Summary]\n${summary}` };
3524
+ this.history.splice(from, to - from + 1, replacement);
3525
+ this.saveWorkspaceConversationState(true);
3526
+ return {
3527
+ ok: true,
3528
+ output: JSON.stringify({
3529
+ ok: true,
3530
+ action: 'summarize',
3531
+ foldedFrom: from,
3532
+ foldedTo: to,
3533
+ foldedEntries: to - from + 1,
3534
+ foldedChars: chars,
3535
+ remaining: this.history.length,
3536
+ summary: summary.slice(0, 2000),
3537
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length },
3538
+ }, null, 2),
3539
+ metadata: { kind: 'context-history-summarize' },
3540
+ };
3541
+ }
3542
+ return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
3543
+ }
3420
3544
  recordContextCompressionStep() {
3421
3545
  const runId = this.currentWorkRunId();
3422
3546
  if (!runId)
@@ -5496,7 +5620,7 @@ class Agent {
5496
5620
  if (!sa)
5497
5621
  return { ok: false, output: `[Subagent] Not found: ${name}`, error: `Not found: ${name}` };
5498
5622
  const transcript = sa.messages.map(m => `[${m.role}] ${m.content}`).join('\n');
5499
- return this.subagents.toToolResult(sa.id, `get.subagent("${sa.name}")\nStatus: ${sa.status}\nModel: ${sa.model}\nMode: ${sa.agentMode}\n\nResult:\n${sa.result || ''}\n\nConversation:\n${transcript}`, true);
5623
+ return this.subagents.toToolResult(sa.id, `get.subagent("${sa.name}", id="${sa.id}")\nStatus: ${sa.status}\nModel: ${sa.model}\nMode: ${sa.agentMode}\n\nResult:\n${sa.result || ''}\n\nConversation:\n${transcript}`, true);
5500
5624
  }
5501
5625
  catch {
5502
5626
  return { ok: false, output: '[Subagent] Invalid result arguments.', error: 'Invalid result arguments.' };
@@ -6393,7 +6517,7 @@ class Agent {
6393
6517
  return;
6394
6518
  const total = msgs.reduce((sum, m) => sum + (typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content || '').length), 0);
6395
6519
  const budget = this.compressionBudget(msgs);
6396
- if (budget.estimatedTokens < budget.triggerTokens)
6520
+ if (budget.estimatedTokens < budget.triggerTokens && !force)
6397
6521
  return;
6398
6522
  if (!force && this.lastCompression && String(msgs[0]?.content || '').includes(this.lastCompression.summary)) {
6399
6523
  const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
@@ -1470,6 +1470,10 @@ async function executeNewmarkTool(agent, name, args, inputSchema, signal) {
1470
1470
  return agent.handleLinkedPlanTool(args);
1471
1471
  if (name === 'build_history_query')
1472
1472
  return agent.handleBuildHistoryQuery(args);
1473
+ if (name === 'context_compress')
1474
+ return (await agent.handleContextCompress(args, signal)).output;
1475
+ if (name === 'context_history_manage')
1476
+ return agent.handleContextHistoryManage(args).output;
1473
1477
  if (name === 'question') {
1474
1478
  if (agent.config.getStr('agent', 'option_feedback') === 'fully_autonomous')
1475
1479
  return '[question] Disabled by fully_autonomous option feedback.';
@@ -111,6 +111,7 @@ export interface SubagentReadSnapshot {
111
111
  natureSlug: string;
112
112
  displayName: string;
113
113
  qualifiedName: string;
114
+ name: string;
114
115
  createdByAgentId: string;
115
116
  status: SubagentStatus;
116
117
  active: boolean;
@@ -119,6 +119,10 @@ class SubagentManager {
119
119
  const id = (0, crypto_1.randomUUID)();
120
120
  const shortId = id.replace(/-/g, '').slice(0, 8);
121
121
  const slug = natureSlug(name);
122
+ // The Agent-facing name is the caller's stable, human-readable label and is
123
+ // deliberately decoupled from the identity. The UUID and its short form are
124
+ // the only identity-bearing fields; the UI renders the short-id-qualified
125
+ // display name while Agent tool interactions accept both name and id.
122
126
  const displayName = `${slug}-${shortId}`;
123
127
  const qualifiedName = `${displayName}--${id}`;
124
128
  const stamp = now();
@@ -128,7 +132,7 @@ class SubagentManager {
128
132
  natureSlug: slug,
129
133
  displayName,
130
134
  qualifiedName,
131
- name: qualifiedName,
135
+ name: slug,
132
136
  conversationId: this.conversationId,
133
137
  createdByAgentId,
134
138
  prompt,
@@ -139,7 +143,7 @@ class SubagentManager {
139
143
  flowName: flowName || undefined,
140
144
  flowPc: Math.max(0, Math.floor(Number(flowPc) || 0)),
141
145
  status: 'queued',
142
- messages: [{ role: 'system', content: `Peer agent '${qualifiedName}': ${prompt}` }, { role: 'user', content: prompt, hidden_user_input: true }],
146
+ messages: [{ role: 'system', content: `Peer agent '${slug}' (${id}): ${prompt}` }, { role: 'user', content: prompt, hidden_user_input: true }],
143
147
  result: null,
144
148
  createdAt: stamp,
145
149
  updatedAt: stamp,
@@ -152,7 +156,14 @@ class SubagentManager {
152
156
  return id;
153
157
  }
154
158
  get(id) {
155
- return this.subs.get(id) || [...this.subs.values()].find(item => item.name === id || item.qualifiedName === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
159
+ if (this.subs.has(id))
160
+ return this.subs.get(id);
161
+ const exact = [...this.subs.values()].find(item => item.id === id || item.qualifiedName === id);
162
+ if (exact)
163
+ return exact;
164
+ // name is now caller-supplied and not identity-bearing, so it is a
165
+ // convenience lookup only; the id/shortId/displayName paths stay exact.
166
+ return [...this.subs.values()].find(item => item.name === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
156
167
  }
157
168
  send(id, prompt) {
158
169
  const target = this.get(id);
@@ -321,6 +332,7 @@ class SubagentManager {
321
332
  natureSlug: record.natureSlug,
322
333
  displayName: record.displayName,
323
334
  qualifiedName: record.qualifiedName,
335
+ name: record.name,
324
336
  createdByAgentId: record.createdByAgentId,
325
337
  status: record.status,
326
338
  active: record.status !== 'closed',
@@ -14,6 +14,8 @@ const MODE_SCOPED_TOOLS = new Set([
14
14
  'pdf_read',
15
15
  'linked_plan',
16
16
  'build_history_query',
17
+ 'context_compress',
18
+ 'context_history_manage',
17
19
  'question',
18
20
  'task',
19
21
  'subagent_list',
@@ -365,14 +365,21 @@ class ToolExecutor {
365
365
  remote_root: { type: 'string' },
366
366
  remote_path: { type: 'string' },
367
367
  }, ['action']),
368
- t('task', 'Create a same-conversation peer agent and return immediately. The peer has a nature slug, short id, and canonical UUID-qualified name. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent\'s currently resolved model deployment. Plan mode peers are forced to Plan.', { nature: { type: 'string' }, name: { type: 'string', description: 'Legacy alias for nature.' }, prompt: { type: 'string' }, preset: { type: 'string' }, agent: { type: 'string' }, model: { type: 'string', description: 'Optional exact model deployment. Omit to inherit the parent Agent resolved model.' }, mode: { type: 'string' }, input_mode: { type: 'string' }, flow: { type: 'string' } }, ['prompt']),
369
- t('subagent_list', 'List flat same-conversation peer agents, optionally filtered by status.', { status: { type: 'string', enum: ['idle', 'queued', 'working', 'completed', 'error', 'closed'] } }, []),
370
- t('subagent_read', 'Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers.', { id: { type: 'string' }, name: { type: 'string' }, max_chars: { type: 'number', description: 'Bounded result size from 2000 to 32000 characters.' } }, []),
371
- t('subagent_send', 'Persist a mailbox message to a same-conversation peer agent.', { id: { type: 'string' }, name: { type: 'string' }, message: { type: 'string' }, prompt: { type: 'string', description: 'Legacy alias for message.' }, kind: { type: 'string', enum: ['directive', 'question', 'result', 'handoff'] }, reply_to: { type: 'string' }, correlation_id: { type: 'string' } }, []),
372
- t('subagent_result', 'Return the persisted transcript, mailbox summary, status, and latest result for a peer agent.', { id: { type: 'string' }, name: { type: 'string' } }, []),
373
- t('subagent_close', 'Close a same-conversation peer. Root can close any peer; a peer can close only itself.', { id: { type: 'string' }, name: { type: 'string' } }, []),
368
+ t('task', 'Create a same-conversation peer agent and return immediately. The peer has a stable human-readable name, a short id, and a canonical UUID-qualified identity. The peer name is decoupled from its id: pass name for readable references and id for exact targeting. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent\'s currently resolved model deployment. Plan mode peers are forced to Plan.', { nature: { type: 'string' }, name: { type: 'string', description: 'Legacy alias for nature.' }, prompt: { type: 'string' }, preset: { type: 'string' }, agent: { type: 'string' }, model: { type: 'string', description: 'Optional exact model deployment. Omit to inherit the parent Agent resolved model.' }, mode: { type: 'string' }, input_mode: { type: 'string' }, flow: { type: 'string' } }, ['prompt']),
369
+ t('subagent_list', 'List flat same-conversation peer agents, optionally filtered by status. Each entry exposes both the stable name and the exact id; use the id for any subsequent targeting.', { status: { type: 'string', enum: ['idle', 'queued', 'working', 'completed', 'error', 'closed'] } }, []),
370
+ t('subagent_read', 'Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers. Pass the exact id returned by subagent_list, or a name for convenience lookup.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name; ambiguous names resolve to the first match.' }, max_chars: { type: 'number', description: 'Bounded result size from 2000 to 32000 characters.' } }, []),
371
+ t('subagent_send', 'Persist a mailbox message to a same-conversation peer agent. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' }, message: { type: 'string' }, prompt: { type: 'string', description: 'Legacy alias for message.' }, kind: { type: 'string', enum: ['directive', 'question', 'result', 'handoff'] }, reply_to: { type: 'string' }, correlation_id: { type: 'string' } }, []),
372
+ t('subagent_result', 'Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
373
+ t('subagent_close', 'Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.', { id: { type: 'string', description: 'Exact peer id from subagent_list.' }, name: { type: 'string', description: 'Convenience peer name.' } }, []),
374
374
  t('linked_plan', 'Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.', { action: { type: 'string', enum: ['get', 'update'] }, markdown: { type: 'string' }, expected_revision: { type: 'number' } }, ['action']),
375
375
  t('build_history_query', 'Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.', { history_index: { type: 'number', minimum: 1, description: '1-based historical Build Block index from the request ledger; 1 is the newest previous task.' }, run_id: { type: 'string', description: 'Exact run id returned by an earlier build_history_query result.' }, max_events: { type: 'number', minimum: 1, maximum: 200, description: 'Maximum trailing public work events; defaults to 80.' } }, []),
376
+ t('context_compress', 'Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.', { keep_recent: { type: 'number', minimum: 2, maximum: 60, description: 'Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages.' }, force: { type: 'boolean', description: 'Compress even if the context is not yet over the automatic threshold. Defaults to false.' } }, []),
377
+ t('context_history_manage', 'Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry. The displayed conversation history (what the user sees) is never modified by any action.', {
378
+ action: { type: 'string', enum: ['list', 'remove', 'summarize'], description: 'list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry.' },
379
+ position: { type: 'number', minimum: 0, description: '0-based context entry index for remove, or the start of the range for summarize.' },
380
+ to: { type: 'number', minimum: 0, description: '0-based inclusive end of the range for summarize. Defaults to position.' },
381
+ limit: { type: 'number', minimum: 5, maximum: 400, description: 'Maximum context entries to list; defaults to 200.' },
382
+ }, ['action']),
376
383
  t('question', 'Ask user a multiple-choice question', { questions: { type: 'array' } }, ['questions']),
377
384
  t('skill_download', 'Download a skill', { name: { type: 'string' }, source: { type: 'string' } }, ['name', 'source']),
378
385
  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 } }, []),
@@ -40,6 +40,8 @@ exports.NATIVE_TOOL_CATALOG = [
40
40
  { name: 'subagent_close', label: 'Subagent close', description: 'Close a same-conversation peer agent.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
41
41
  { name: 'linked_plan', label: 'Linked plan', description: 'Read or conservatively update the conversation-linked Markdown plan.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
42
42
  { name: 'build_history_query', label: 'Build history query', description: 'Read concrete public work details for one historical Build Block.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
43
+ { name: 'context_compress', label: 'Context compress', description: 'Actively compress the LLM context history, leaving the displayed conversation history unchanged.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
+ { name: 'context_history_manage', label: 'Context history manage', description: 'List, remove, or summarize entries in the LLM context history without touching the displayed conversation history.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
43
45
  { name: 'question', label: 'Ask question', description: 'Ask the user for structured option feedback.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
44
46
  { name: 'skill_download', label: 'Skill download', description: 'Download and install a skill.', category: 'agent', defaultEnabled: true },
45
47
  { name: 'skill', label: 'Skill', description: 'Search enabled skill metadata or load one skill body on demand.', category: 'agent', defaultEnabled: true, protected: true, availability: 'mode-scoped' },
@@ -9112,7 +9112,7 @@ function workToolActivity(event) {
9112
9112
  if (name === 'task' || name.indexOf('subagent_') === 0) return { key: 'subagents', type: 'tool_subagent' };
9113
9113
  if (name === 'skill' || name === 'skill_load' || name === 'skill_read' || name === 'skill_download') return { key: 'skills', type: 'tool_skill' };
9114
9114
  if (name === 'mcp' || name.indexOf('mcp__') === 0 || /^mcp[:._-]/.test(name) || /[:._-]mcp[:._-]/.test(name)) return { key: 'mcp', type: 'tool_mcp' };
9115
- if (name === 'context_compression') return { key: 'context_compression', type: 'tool_context_compression' };
9115
+ if (name === 'context_compression' || name === 'context_compress' || name === 'context_history_manage') return { key: 'context_compression', type: 'tool_context_compression' };
9116
9116
  if (name === 'memory_lab_update' || name === 'memory_lab_reindex') return { key: 'memory_lab', type: 'tool_memory_lab' };
9117
9117
  if (name === 'image_inspect' || name === 'computer_use') return { key: 'images', type: 'tool_images' };
9118
9118
  if (name === 'write' || name === 'edit') return { key: 'files', type: 'tool_files' };
@@ -327015,6 +327015,8 @@ var NATIVE_TOOL_CATALOG = [
327015
327015
  { name: "subagent_close", label: "Subagent close", description: "Close a same-conversation peer agent.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327016
327016
  { name: "linked_plan", label: "Linked plan", description: "Read or conservatively update the conversation-linked Markdown plan.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327017
327017
  { name: "build_history_query", label: "Build history query", description: "Read concrete public work details for one historical Build Block.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327018
+ { name: "context_compress", label: "Context compress", description: "Actively compress the LLM context history, leaving the displayed conversation history unchanged.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327019
+ { name: "context_history_manage", label: "Context history manage", description: "List, remove, or summarize entries in the LLM context history without touching the displayed conversation history.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327018
327020
  { name: "question", label: "Ask question", description: "Ask the user for structured option feedback.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
327019
327021
  { name: "skill_download", label: "Skill download", description: "Download and install a skill.", category: "agent", defaultEnabled: true },
327020
327022
  { name: "skill", label: "Skill", description: "Search enabled skill metadata or load one skill body on demand.", category: "agent", defaultEnabled: true, protected: true, availability: "mode-scoped" },
@@ -334509,6 +334511,8 @@ var MODE_SCOPED_TOOLS = /* @__PURE__ */ new Set([
334509
334511
  "pdf_read",
334510
334512
  "linked_plan",
334511
334513
  "build_history_query",
334514
+ "context_compress",
334515
+ "context_history_manage",
334512
334516
  "question",
334513
334517
  "task",
334514
334518
  "subagent_list",
@@ -335526,14 +335530,21 @@ var ToolExecutor = class {
335526
335530
  remote_root: { type: "string" },
335527
335531
  remote_path: { type: "string" }
335528
335532
  }, ["action"]),
335529
- t3("task", "Create a same-conversation peer agent and return immediately. The peer has a nature slug, short id, and canonical UUID-qualified name. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent's currently resolved model deployment. Plan mode peers are forced to Plan.", { nature: { type: "string" }, name: { type: "string", description: "Legacy alias for nature." }, prompt: { type: "string" }, preset: { type: "string" }, agent: { type: "string" }, model: { type: "string", description: "Optional exact model deployment. Omit to inherit the parent Agent resolved model." }, mode: { type: "string" }, input_mode: { type: "string" }, flow: { type: "string" } }, ["prompt"]),
335530
- t3("subagent_list", "List flat same-conversation peer agents, optionally filtered by status.", { status: { type: "string", enum: ["idle", "queued", "working", "completed", "error", "closed"] } }, []),
335531
- t3("subagent_read", "Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers.", { id: { type: "string" }, name: { type: "string" }, max_chars: { type: "number", description: "Bounded result size from 2000 to 32000 characters." } }, []),
335532
- t3("subagent_send", "Persist a mailbox message to a same-conversation peer agent.", { id: { type: "string" }, name: { type: "string" }, message: { type: "string" }, prompt: { type: "string", description: "Legacy alias for message." }, kind: { type: "string", enum: ["directive", "question", "result", "handoff"] }, reply_to: { type: "string" }, correlation_id: { type: "string" } }, []),
335533
- t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent.", { id: { type: "string" }, name: { type: "string" } }, []),
335534
- t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself.", { id: { type: "string" }, name: { type: "string" } }, []),
335533
+ t3("task", "Create a same-conversation peer agent and return immediately. The peer has a stable human-readable name, a short id, and a canonical UUID-qualified identity. The peer name is decoupled from its id: pass name for readable references and id for exact targeting. Pass model to select an exact configured model deployment (deployment:providerId:modelId or an unambiguous provider/model name). When model is omitted, the peer inherits the parent Agent's currently resolved model deployment. Plan mode peers are forced to Plan.", { nature: { type: "string" }, name: { type: "string", description: "Legacy alias for nature." }, prompt: { type: "string" }, preset: { type: "string" }, agent: { type: "string" }, model: { type: "string", description: "Optional exact model deployment. Omit to inherit the parent Agent resolved model." }, mode: { type: "string" }, input_mode: { type: "string" }, flow: { type: "string" } }, ["prompt"]),
335534
+ t3("subagent_list", "List flat same-conversation peer agents, optionally filtered by status. Each entry exposes both the stable name and the exact id; use the id for any subsequent targeting.", { status: { type: "string", enum: ["idle", "queued", "working", "completed", "error", "closed"] } }, []),
335535
+ t3("subagent_read", "Read one same-conversation peer status, queue/mailbox summary, latest bounded feedback, and result. Available for running, queued, completed, error, and closed peers. Pass the exact id returned by subagent_list, or a name for convenience lookup.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name; ambiguous names resolve to the first match." }, max_chars: { type: "number", description: "Bounded result size from 2000 to 32000 characters." } }, []),
335536
+ t3("subagent_send", "Persist a mailbox message to a same-conversation peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." }, message: { type: "string" }, prompt: { type: "string", description: "Legacy alias for message." }, kind: { type: "string", enum: ["directive", "question", "result", "handoff"] }, reply_to: { type: "string" }, correlation_id: { type: "string" } }, []),
335537
+ t3("subagent_result", "Return the persisted transcript, mailbox summary, status, and latest result for a peer agent. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
335538
+ t3("subagent_close", "Close a same-conversation peer. Root can close any peer; a peer can close only itself. Target by exact id (preferred) or name.", { id: { type: "string", description: "Exact peer id from subagent_list." }, name: { type: "string", description: "Convenience peer name." } }, []),
335535
335539
  t3("linked_plan", "Read or update the current conversation linked Markdown plan. Update requires the current expected_revision.", { action: { type: "string", enum: ["get", "update"] }, markdown: { type: "string" }, expected_revision: { type: "number" } }, ["action"]),
335536
335540
  t3("build_history_query", "Read the concrete public work details of one historical Build Block. The prompt already exposes user input, final summary, and completion status; call this read-only tool only when the user asks what specifically happened. Select by newest-to-oldest history_index, or by run_id returned from an earlier query.", { history_index: { type: "number", minimum: 1, description: "1-based historical Build Block index from the request ledger; 1 is the newest previous task." }, run_id: { type: "string", description: "Exact run id returned by an earlier build_history_query result." }, max_events: { type: "number", minimum: 1, maximum: 200, description: "Maximum trailing public work events; defaults to 80." } }, []),
335541
+ t3("context_compress", "Actively compress the LLM context history for this conversation. This collapses older history entries into a concise summary while preserving the recent tail, which reduces context tokens and cost. IMPORTANT: it affects only the LLM context (what the model sees); the displayed conversation history shown to the user is never altered. Call this when the conversation is long, token pressure is high, or you judge that older turns are no longer needed in full. Idempotent and safe: repeated calls produce incremental summaries.", { keep_recent: { type: "number", minimum: 2, maximum: 60, description: "Recent message count to keep uncompressed at the tail. Defaults to the configured keep_recent_messages." }, force: { type: "boolean", description: "Compress even if the context is not yet over the automatic threshold. Defaults to false." } }, []),
335542
+ t3("context_history_manage", "Manage the LLM context history for this conversation without affecting the displayed conversation history. This is the active context-management surface. Actions: list returns a bounded index of context entries (position, role, name, length, first-line preview); remove deletes the entry at a given position; summarize replaces a contiguous range of context entries with a concise local summary entry. The displayed conversation history (what the user sees) is never modified by any action.", {
335543
+ action: { type: "string", enum: ["list", "remove", "summarize"], description: "list: index context entries. remove: delete the entry at position. summarize: fold entries [from, to] into one summary entry." },
335544
+ position: { type: "number", minimum: 0, description: "0-based context entry index for remove, or the start of the range for summarize." },
335545
+ to: { type: "number", minimum: 0, description: "0-based inclusive end of the range for summarize. Defaults to position." },
335546
+ limit: { type: "number", minimum: 5, maximum: 400, description: "Maximum context entries to list; defaults to 200." }
335547
+ }, ["action"]),
335537
335548
  t3("question", "Ask user a multiple-choice question", { questions: { type: "array" } }, ["questions"]),
335538
335549
  t3("skill_download", "Download a skill", { name: { type: "string" }, source: { type: "string" } }, ["name", "source"]),
335539
335550
  t3("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 } }, []),
@@ -337619,7 +337630,7 @@ var SubagentManager = class {
337619
337630
  natureSlug: slug,
337620
337631
  displayName,
337621
337632
  qualifiedName,
337622
- name: qualifiedName,
337633
+ name: slug,
337623
337634
  conversationId: this.conversationId,
337624
337635
  createdByAgentId,
337625
337636
  prompt,
@@ -337630,7 +337641,7 @@ var SubagentManager = class {
337630
337641
  flowName: flowName || void 0,
337631
337642
  flowPc: Math.max(0, Math.floor(Number(flowPc) || 0)),
337632
337643
  status: "queued",
337633
- messages: [{ role: "system", content: `Peer agent '${qualifiedName}': ${prompt}` }, { role: "user", content: prompt, hidden_user_input: true }],
337644
+ messages: [{ role: "system", content: `Peer agent '${slug}' (${id}): ${prompt}` }, { role: "user", content: prompt, hidden_user_input: true }],
337634
337645
  result: null,
337635
337646
  createdAt: stamp,
337636
337647
  updatedAt: stamp
@@ -337640,7 +337651,10 @@ var SubagentManager = class {
337640
337651
  return id;
337641
337652
  }
337642
337653
  get(id) {
337643
- return this.subs.get(id) || [...this.subs.values()].find((item) => item.name === id || item.qualifiedName === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
337654
+ if (this.subs.has(id)) return this.subs.get(id);
337655
+ const exact = [...this.subs.values()].find((item) => item.id === id || item.qualifiedName === id);
337656
+ if (exact) return exact;
337657
+ return [...this.subs.values()].find((item) => item.name === id || item.displayName === id || item.shortId === id || item.natureSlug === natureSlug(id));
337644
337658
  }
337645
337659
  send(id, prompt) {
337646
337660
  const target = this.get(id);
@@ -337788,6 +337802,7 @@ var SubagentManager = class {
337788
337802
  natureSlug: record.natureSlug,
337789
337803
  displayName: record.displayName,
337790
337804
  qualifiedName: record.qualifiedName,
337805
+ name: record.name,
337791
337806
  createdByAgentId: record.createdByAgentId,
337792
337807
  status: record.status,
337793
337808
  active: record.status !== "closed",
@@ -340416,6 +340431,8 @@ async function executeNewmarkTool(agent, name50, args, inputSchema, signal) {
340416
340431
  if (name50 === "subagent_close") return agent.handleSubagentCloseEnvelope(args).output;
340417
340432
  if (name50 === "linked_plan") return agent.handleLinkedPlanTool(args);
340418
340433
  if (name50 === "build_history_query") return agent.handleBuildHistoryQuery(args);
340434
+ if (name50 === "context_compress") return (await agent.handleContextCompress(args, signal)).output;
340435
+ if (name50 === "context_history_manage") return agent.handleContextHistoryManage(args).output;
340419
340436
  if (name50 === "question") {
340420
340437
  if (agent.config.getStr("agent", "option_feedback") === "fully_autonomous") return "[question] Disabled by fully_autonomous option feedback.";
340421
340438
  if (!agent.handleQuestion(args)) return "[Question rejected: at least one question with two labeled options is required.]";
@@ -346152,6 +346169,132 @@ Review this persisted peer result and summarize or continue the parent task as n
346152
346169
  truncatedActivities: Math.max(0, publicEvents.length - activities.length)
346153
346170
  });
346154
346171
  }
346172
+ async handleContextCompress(args, signal) {
346173
+ let input2 = {};
346174
+ try {
346175
+ input2 = JSON.parse(args || "{}");
346176
+ } catch {
346177
+ }
346178
+ if (this.history.length <= 1) return { ok: false, output: "[context_compress] No context to compress.", error: "No context to compress." };
346179
+ const previousKeepLast = this.config.getNum("context", "keep_recent_messages");
346180
+ const keepRecent = Math.max(2, Math.min(60, Math.floor(Number(input2.keep_recent) || previousKeepLast || 10)));
346181
+ const force = Boolean(input2.force);
346182
+ try {
346183
+ this.config.set("context", "keep_recent_messages", keepRecent);
346184
+ const msgs = this.history.map((message) => ({ ...message }));
346185
+ const provider = this.engineModel();
346186
+ await this.maybeCompress(msgs, provider, signal, this.activeModelName(), force);
346187
+ if (this.history.length <= 1) return { ok: false, output: "[context_compress] Compression skipped: context unchanged.", error: "Compression skipped." };
346188
+ return {
346189
+ ok: true,
346190
+ output: JSON.stringify({
346191
+ ok: true,
346192
+ compressed: true,
346193
+ at: this.lastCompression?.at,
346194
+ originalMessages: this.lastCompression?.originalMessages,
346195
+ compressedMessages: this.lastCompression?.compressedMessages,
346196
+ originalChars: this.lastCompression?.originalChars,
346197
+ compressedChars: this.lastCompression?.compressedChars,
346198
+ estimatedTokens: this.lastCompression?.compressedTokens,
346199
+ summary: this.lastCompression?.summary?.slice(0, 2e3),
346200
+ model: this.lastCompression?.model,
346201
+ fallback: this.lastCompression?.fallback,
346202
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346203
+ }, null, 2),
346204
+ metadata: { kind: "context-compress" }
346205
+ };
346206
+ } finally {
346207
+ this.config.set("context", "keep_recent_messages", previousKeepLast);
346208
+ }
346209
+ }
346210
+ handleContextHistoryManage(args) {
346211
+ let input2 = {};
346212
+ try {
346213
+ input2 = JSON.parse(args || "{}");
346214
+ } catch {
346215
+ }
346216
+ const action = String(input2.action || "").trim();
346217
+ if (!action) return { ok: false, output: "[context_history_manage] action is required (list|remove|summarize).", error: "action is required." };
346218
+ if (action === "list") {
346219
+ const limit = Math.max(5, Math.min(400, Math.floor(Number(input2.limit || 200))));
346220
+ const entries = this.history.slice(0, limit).map((message, index) => ({
346221
+ position: index,
346222
+ role: String(message.role || ""),
346223
+ name: String(message.name || ""),
346224
+ chars: typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length,
346225
+ preview: this.compressionHistoryContent(message.content || "").slice(0, 160)
346226
+ }));
346227
+ return {
346228
+ ok: true,
346229
+ output: JSON.stringify({
346230
+ ok: true,
346231
+ action: "list",
346232
+ entryCount: this.history.length,
346233
+ listed: entries.length,
346234
+ entries,
346235
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346236
+ }, null, 2),
346237
+ metadata: { kind: "context-history-list" }
346238
+ };
346239
+ }
346240
+ if (action === "remove") {
346241
+ const position = Math.floor(Number(input2.position));
346242
+ if (!Number.isFinite(position) || position < 0 || position >= this.history.length) {
346243
+ return { ok: false, output: `[context_history_manage] remove position ${position} out of range (0..${this.history.length - 1}).`, error: "remove position out of range." };
346244
+ }
346245
+ const removed = this.history.splice(position, 1)[0];
346246
+ this.saveWorkspaceConversationState(true);
346247
+ return {
346248
+ ok: true,
346249
+ output: JSON.stringify({
346250
+ ok: true,
346251
+ action: "remove",
346252
+ removedPosition: position,
346253
+ removedRole: String(removed?.role || ""),
346254
+ remaining: this.history.length,
346255
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346256
+ }, null, 2),
346257
+ metadata: { kind: "context-history-remove" }
346258
+ };
346259
+ }
346260
+ if (action === "summarize") {
346261
+ const from = Math.max(0, Math.floor(Number(input2.position || 0)));
346262
+ const toRaw = Math.floor(Number(input2.to ?? input2.position));
346263
+ const to = Number.isFinite(toRaw) ? Math.min(this.history.length - 1, Math.max(from, toRaw)) : from;
346264
+ if (from >= this.history.length) return { ok: false, output: `[context_history_manage] summarize position ${from} out of range (0..${this.history.length - 1}).`, error: "summarize position out of range." };
346265
+ if (to - from < 1) return { ok: false, output: "[context_history_manage] summarize requires at least two entries in range.", error: "summarize requires a range of at least two entries." };
346266
+ const segment = this.history.slice(from, to + 1);
346267
+ const chars = segment.reduce((sum, message) => sum + (typeof message.content === "string" ? message.content.length : JSON.stringify(message.content || "").length), 0);
346268
+ const summary = this.localCompressionSummary(
346269
+ `Workspace: ${this.workspace.current?.path || this.rootPath}
346270
+ Mode: ${this.modeName()}`,
346271
+ segment.map((message, i4) => `#${i4 + 1} [${String(message.role || "unknown")}${message.name ? ` ${String(message.name)}` : ""}]
346272
+ ${this.compressionHistoryContent(message.content || "")}`).join("\n\n").slice(0, 2e4),
346273
+ segment.length,
346274
+ chars
346275
+ );
346276
+ const replacement = { role: "system", content: `[Context History Summary]
346277
+ ${summary}` };
346278
+ this.history.splice(from, to - from + 1, replacement);
346279
+ this.saveWorkspaceConversationState(true);
346280
+ return {
346281
+ ok: true,
346282
+ output: JSON.stringify({
346283
+ ok: true,
346284
+ action: "summarize",
346285
+ foldedFrom: from,
346286
+ foldedTo: to,
346287
+ foldedEntries: to - from + 1,
346288
+ foldedChars: chars,
346289
+ remaining: this.history.length,
346290
+ summary: summary.slice(0, 2e3),
346291
+ displayHistory: { untouched: true, messageCount: this.chatMessages.length }
346292
+ }, null, 2),
346293
+ metadata: { kind: "context-history-summarize" }
346294
+ };
346295
+ }
346296
+ return { ok: false, output: `[context_history_manage] Unknown action: ${action}`, error: `Unknown action: ${action}` };
346297
+ }
346155
346298
  recordContextCompressionStep() {
346156
346299
  const runId = this.currentWorkRunId();
346157
346300
  if (!runId) return;
@@ -348019,7 +348162,7 @@ ${settled?.result || settled?.error || ""}`.trim();
348019
348162
  const transcript = sa.messages.map((m2) => `[${m2.role}] ${m2.content}`).join("\n");
348020
348163
  return this.subagents.toToolResult(
348021
348164
  sa.id,
348022
- `get.subagent("${sa.name}")
348165
+ `get.subagent("${sa.name}", id="${sa.id}")
348023
348166
  Status: ${sa.status}
348024
348167
  Model: ${sa.model}
348025
348168
  Mode: ${sa.agentMode}
@@ -348811,7 +348954,7 @@ Falling back to built-in engine.` }];
348811
348954
  if (!this.config.getBool("context", "auto_compress")) return;
348812
348955
  const total = msgs.reduce((sum, m2) => sum + (typeof m2.content === "string" ? m2.content.length : JSON.stringify(m2.content || "").length), 0);
348813
348956
  const budget = this.compressionBudget(msgs);
348814
- if (budget.estimatedTokens < budget.triggerTokens) return;
348957
+ if (budget.estimatedTokens < budget.triggerTokens && !force) return;
348815
348958
  if (!force && this.lastCompression && String(msgs[0]?.content || "").includes(this.lastCompression.summary)) {
348816
348959
  const baselineChars = Math.max(0, Number(this.lastCompression.compressedChars || 0));
348817
348960
  const baselineTokens = Math.max(0, Number(this.lastCompression.compressedTokens || 0));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "newmark-agent",
3
3
  "productName": "Newmark Agent",
4
- "version": "0.3.6",
4
+ "version": "0.3.7",
5
5
  "description": "Newmark Agent — Portable AI coding agent with rich GUI and CLI (TypeScript)",
6
6
  "homepage": "https://github.com/positer/Newmark-Agent",
7
7
  "repository": {