@myagentroam/agent 0.9.88 → 0.9.90

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.
@@ -1,7 +1,14 @@
1
1
  import type { HostDescription } from './contracts.js';
2
2
  import type { LocalProcessExecutor } from './local-process-executor.js';
3
3
  import type { WorkspacePathMount } from '../tools/shared/path-resolver.js';
4
- import type { MarAgentEvent, MarAgentHost, ToolCall, ToolExecutionContext, ToolExecutionResult } from '../sdk/types.js';
4
+ import type { MarAgentEvent, MarAgentHost, McpToolCatalogContext, ToolCall, ToolExecutionContext, ToolExecutionResult } from '../sdk/types.js';
5
+ export interface LocalAgentMcpBinding {
6
+ readonly tools: readonly import('../model/contracts.js').ClientToolDefinition[];
7
+ call(call: ToolCall, signal: AbortSignal): Promise<ToolExecutionResult>;
8
+ }
9
+ export interface LocalAgentExecutionMcpBinding extends LocalAgentMcpBinding {
10
+ active: boolean;
11
+ }
5
12
  export declare class LocalAgentHost implements MarAgentHost {
6
13
  #private;
7
14
  readonly events: MarAgentEvent[];
@@ -37,8 +44,13 @@ export declare class LocalAgentHost implements MarAgentHost {
37
44
  requestId: string;
38
45
  questions: unknown[];
39
46
  }, signal: AbortSignal): Promise<Record<string, unknown>>;
40
- listMcpTools(): Promise<readonly import("../model/contracts.js").ClientToolDefinition[]>;
41
- callMcpTool(call: ToolCall, signal: AbortSignal): Promise<ToolExecutionResult>;
47
+ bindExecutionMcp(binding?: LocalAgentMcpBinding): LocalAgentExecutionMcpBinding | undefined;
48
+ unbindExecutionMcp(binding: LocalAgentExecutionMcpBinding | undefined): void;
49
+ listMcpTools(context?: McpToolCatalogContext): Promise<import("../model/contracts.js").ClientToolDefinition[]>;
50
+ callMcpTool(call: ToolCall, signal: AbortSignal, context?: {
51
+ sessionId: string;
52
+ executionId: string;
53
+ }): Promise<ToolExecutionResult>;
42
54
  listSessionResources(sessionId: string): Promise<{
43
55
  processes: {
44
56
  processId: string;
@@ -13,6 +13,8 @@ export class LocalAgentHost {
13
13
  #question;
14
14
  #mcpTools;
15
15
  #callMcp;
16
+ #executionMcp = new Map();
17
+ #currentExecutionMcp;
16
18
  events = [];
17
19
  #mode;
18
20
  #environment;
@@ -94,11 +96,46 @@ export class LocalAgentHost {
94
96
  throw new MarAgentError('MAR_AGENT_USER_INPUT_UNAVAILABLE', 'User input is unavailable.');
95
97
  return this.#question(input, signal);
96
98
  }
97
- listMcpTools() {
98
- return Promise.resolve(this.#mcpTools);
99
+ bindExecutionMcp(binding) {
100
+ const current = binding === undefined ? undefined : { ...binding, active: true };
101
+ if (current === undefined)
102
+ this.#currentExecutionMcp = undefined;
103
+ else
104
+ this.#currentExecutionMcp = current;
105
+ return current;
99
106
  }
100
- async callMcpTool(call, signal) {
101
- if (!this.#callMcp)
107
+ unbindExecutionMcp(binding) {
108
+ if (binding !== undefined)
109
+ binding.active = false;
110
+ if (this.#currentExecutionMcp === binding)
111
+ this.#currentExecutionMcp = undefined;
112
+ for (const [executionId, candidate] of this.#executionMcp)
113
+ if (candidate === binding)
114
+ this.#executionMcp.delete(executionId);
115
+ }
116
+ listMcpTools(context) {
117
+ const execution = context?.claimExecutionScope === true
118
+ ? this.#currentExecutionMcp
119
+ : context?.executionScopeId === undefined
120
+ ? undefined
121
+ : this.#executionMcp.get(context.executionScopeId);
122
+ if (context !== undefined && execution !== undefined)
123
+ this.#executionMcp.set(context.executionId, execution);
124
+ const tools = [...this.#mcpTools, ...(execution?.tools ?? [])];
125
+ if (new Set(tools.map((tool) => tool.name)).size !== tools.length)
126
+ throw new MarAgentError('MAR_AGENT_MCP_FAILED', 'MCP tool names conflict.');
127
+ return Promise.resolve(tools);
128
+ }
129
+ async callMcpTool(call, signal, context) {
130
+ const execution = context === undefined
131
+ ? this.#currentExecutionMcp
132
+ : this.#executionMcp.get(context.executionId);
133
+ if (execution?.tools.some((tool) => tool.name === call.name)) {
134
+ if (!execution.active)
135
+ throw new MarAgentError('MAR_AGENT_MCP_FAILED', 'Execution MCP is no longer available.');
136
+ return execution.call(call, signal);
137
+ }
138
+ if (!this.#callMcp || !this.#mcpTools.some((tool) => tool.name === call.name))
102
139
  throw new MarAgentError('MAR_AGENT_MCP_FAILED', 'MCP tool execution is unavailable.');
103
140
  return this.#callMcp(call, signal);
104
141
  }
package/dist/index.d.ts CHANGED
@@ -7,11 +7,11 @@ export type { ClientToolDefinition, ModelAdapter, ModelEvent, ModelMessage, Mode
7
7
  export { LocalWorkspaceTools, type ExecInput, type ExecResult } from './tools/local-workspace-tools.js';
8
8
  export type { ApplyPatchFileResult, ApplyPatchResult } from './tools/apply-patch.js';
9
9
  export { openMarAgentSessionCatalog, type MarAgentContextUsage, type MarAgentSessionDeleteResult, type MarAgentHistoryEntry, type MarAgentHistoryPage, type MarAgentSessionCatalog, type MarAgentSessionImage, type MarAgentSessionPage, type MarAgentSessionSummary } from './session/catalog.js';
10
- export { LocalAgentHost, selectExecutionShell } from './host/local-agent-host.js';
10
+ export { LocalAgentHost, selectExecutionShell, type LocalAgentExecutionMcpBinding, type LocalAgentMcpBinding } from './host/local-agent-host.js';
11
11
  export type { LocalProcessExecutor, LocalProcessSpawnRequest } from './host/local-process-executor.js';
12
12
  export type { WorkspacePathMount } from './tools/shared/path-resolver.js';
13
13
  export { createMarAgent } from './sdk/agent.js';
14
14
  export { MAR_AGENT_PROMPT_VERSION, buildSystemPrompt } from './prompts/index.js';
15
15
  export { TodoStore, type TodoItem } from './tools/todo.js';
16
16
  export { SubagentScheduler, type SubagentOutputOptions, type SubagentMessageOptions, type SubagentRunOptions, type SubagentTaskInput, type SubagentTaskResult, type SubagentTaskSnapshot } from './subagent/scheduler.js';
17
- export type { AccessMode, CreateMarAgentOptions, CreateSessionInput, ExecutionHandle, ExecutionMode, ExecutionResult, HostSessionResourceSnapshot, MarAgent, MarAgentEvent, MarAgentHost, MarAgentImageMetadata, MarAgentSession, MarAgentToolArtifact, MarAgentToolImageArtifact, RolloutBudgetOptions, RunInput, RunInputImage, ToolCall, ToolExecutionContext, ToolExecutionResult } from './sdk/types.js';
17
+ export type { AccessMode, CreateMarAgentOptions, CreateSessionInput, ExecutionHandle, ExecutionMode, ExecutionResult, HostSessionResourceSnapshot, McpToolCatalogContext, MarAgent, MarAgentEvent, MarAgentHost, MarAgentImageMetadata, MarAgentSession, MarAgentToolArtifact, MarAgentToolImageArtifact, RolloutBudgetOptions, RunInput, RunInputImage, ToolCall, ToolExecutionContext, ToolExecutionResult } from './sdk/types.js';
@@ -2,7 +2,7 @@ export function retainedSessionResourcesSnapshot(input) {
2
2
  if (input.processes.length === 0 && input.subagents.length === 0)
3
3
  return undefined;
4
4
  const processes = [...input.processes].sort((left, right) => left.processId.localeCompare(right.processId));
5
- const subagents = [...input.subagents].sort((left, right) => left.agentId.localeCompare(right.agentId));
5
+ const subagents = [...input.subagents];
6
6
  return [
7
7
  '# Retained Session resources',
8
8
  'These resources belong to this Session and survived the previous Execution. Reuse their stable IDs instead of starting duplicate processes or subagents.',
package/dist/sdk/agent.js CHANGED
@@ -421,6 +421,7 @@ export async function createMarAgent(options) {
421
421
  access: input.access,
422
422
  initialMailbox: turn.initialMailbox,
423
423
  turnId: turn.turnId,
424
+ mcpExecutionScopeId: executionId,
424
425
  ...(turn.delivery ? { delivery: turn.delivery } : {}),
425
426
  ...(rolloutBudget === undefined ? {} : { sharedRolloutBudget: rolloutBudget })
426
427
  });
@@ -430,7 +431,15 @@ export async function createMarAgent(options) {
430
431
  (input.images?.length || hasRestoredUserImages) &&
431
432
  !selectedModel.inputCapabilities.includes('IMAGE'))
432
433
  throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'Selected model does not support image input.');
433
- const mcpTools = (await options.host.listMcpTools?.()) ?? [];
434
+ const mcpTools = (await options.host.listMcpTools?.({
435
+ sessionId,
436
+ executionId,
437
+ ...(sessionSource.type === 'subagent'
438
+ ? internal.mcpExecutionScopeId === undefined
439
+ ? {}
440
+ : { executionScopeId: internal.mcpExecutionScopeId }
441
+ : { claimExecutionScope: true })
442
+ })) ?? [];
434
443
  const selectedModelForTools = executionModels.get(input.modelId ?? executionDefaultModelId);
435
444
  const availableTools = [...BUILTIN_TOOL_DEFINITIONS, ...mcpTools].filter((tool) => {
436
445
  if (sessionSource.type === 'subagent' &&
@@ -642,6 +651,7 @@ export async function createMarAgent(options) {
642
651
  tools: executionTools,
643
652
  toolChoice: 'none',
644
653
  promptCacheKey: sessionId,
654
+ reasoningEffort,
645
655
  onAttemptDiagnostic: recordModelAttempt('COMPACTION')
646
656
  })) {
647
657
  if (event.type === 'text.completed' &&
@@ -1296,6 +1306,7 @@ export async function createMarAgent(options) {
1296
1306
  inputSource: 'parent_agent',
1297
1307
  initialMailbox: turn.initialMailbox,
1298
1308
  turnId: turn.turnId,
1309
+ mcpExecutionScopeId: turn.mcpExecutionScopeId,
1299
1310
  ...(turn.delivery ? { delivery: turn.delivery } : {}),
1300
1311
  ...(turn.sharedRolloutBudget === undefined
1301
1312
  ? {}
@@ -1449,6 +1460,7 @@ function createSubagentBackend(store, workspace) {
1449
1460
  return Promise.all(sessions.map(async (session) => ({
1450
1461
  id: session.id,
1451
1462
  createdAt: session.createdAt,
1463
+ lastActivityAt: session.lastActivityAt,
1452
1464
  source: session.source,
1453
1465
  records: await store.read(session.id)
1454
1466
  })));
@@ -1468,6 +1480,7 @@ function createSubagentBackend(store, workspace) {
1468
1480
  return {
1469
1481
  id: session.id,
1470
1482
  createdAt: session.createdAt,
1483
+ lastActivityAt: session.lastActivityAt,
1471
1484
  source: session.source,
1472
1485
  records: await store.read(session.id)
1473
1486
  };
@@ -22,6 +22,10 @@ export interface ToolExecutionContext {
22
22
  invoke(call: ToolCall, signal: AbortSignal): Promise<ToolExecutionResult>;
23
23
  };
24
24
  }
25
+ export interface McpToolCatalogContext extends Pick<ToolExecutionContext, 'sessionId' | 'executionId'> {
26
+ executionScopeId?: string;
27
+ claimExecutionScope?: boolean;
28
+ }
25
29
  export interface MarAgentToolImageArtifact {
26
30
  kind: 'image';
27
31
  name: string;
@@ -58,8 +62,8 @@ export interface MarAgentHost {
58
62
  requestId: string;
59
63
  questions: unknown[];
60
64
  }, signal: AbortSignal): Promise<Record<string, unknown>>;
61
- listMcpTools?(): Promise<readonly import('../model/contracts.js').ClientToolDefinition[]>;
62
- callMcpTool?(call: ToolCall, signal: AbortSignal): Promise<ToolExecutionResult>;
65
+ listMcpTools?(context?: McpToolCatalogContext): Promise<readonly import('../model/contracts.js').ClientToolDefinition[]>;
66
+ callMcpTool?(call: ToolCall, signal: AbortSignal, context?: Pick<ToolExecutionContext, 'sessionId' | 'executionId'>): Promise<ToolExecutionResult>;
63
67
  listSessionResources?(sessionId: string): Promise<HostSessionResourceSnapshot>;
64
68
  disposeSessionResources?(sessionId: string): Promise<void>;
65
69
  dispose(): Promise<void>;
@@ -43,6 +43,7 @@ export interface SubagentParentExecution {
43
43
  export interface PersistedSubagentSession {
44
44
  readonly id: string;
45
45
  readonly createdAt: string;
46
+ readonly lastActivityAt: string;
46
47
  readonly source: Extract<SessionSource, {
47
48
  type: 'subagent';
48
49
  }>;
@@ -11,6 +11,7 @@ export class SubagentSessionController {
11
11
  #completionWaiters = new Set();
12
12
  #activityWaiters = new Set();
13
13
  #activityRevision = 0;
14
+ #resourceRevision = 0;
14
15
  #running = 0;
15
16
  #disposed = false;
16
17
  #createdInExecution = 0;
@@ -23,9 +24,12 @@ export class SubagentSessionController {
23
24
  }
24
25
  static async open(input) {
25
26
  const controller = new SubagentSessionController(input.parentSessionId, input.rootSessionId, input.backend, input.limits);
26
- const sessions = await input.backend.listChildren(input.parentSessionId);
27
- for (const session of sessions)
28
- controller.#agents.set(session.id, restoreAgentState(session));
27
+ const sessions = [...(await input.backend.listChildren(input.parentSessionId))].sort((a, b) => a.lastActivityAt.localeCompare(b.lastActivityAt));
28
+ for (const session of sessions) {
29
+ const state = restoreAgentState(session);
30
+ state.resourceRevision = ++controller.#resourceRevision;
31
+ controller.#agents.set(session.id, state);
32
+ }
29
33
  return controller;
30
34
  }
31
35
  async beginParentExecution(execution) {
@@ -78,7 +82,8 @@ export class SubagentSessionController {
78
82
  verification: [],
79
83
  evidence: [],
80
84
  terminalPending: false,
81
- terminalObserved: false
85
+ terminalObserved: false,
86
+ resourceRevision: ++this.#resourceRevision
82
87
  };
83
88
  this.#createdInExecution++;
84
89
  this.#agents.set(state.agentId, state);
@@ -335,6 +340,7 @@ export class SubagentSessionController {
335
340
  if (index >= 0)
336
341
  this.#queue.splice(index, 1);
337
342
  state.status = 'cancelled';
343
+ this.#touchResource(state);
338
344
  this.#closeReceipt(state);
339
345
  state.terminalPending = true;
340
346
  await this.#publishTerminal(state);
@@ -355,7 +361,15 @@ export class SubagentSessionController {
355
361
  }
356
362
  }
357
363
  listSessionResources() {
358
- return [...this.#agents.values()].map((state) => ({
364
+ const states = [...this.#agents.values()];
365
+ const active = states
366
+ .filter((state) => state.status === 'queued' || state.status === 'running')
367
+ .sort((left, right) => right.resourceRevision - left.resourceRevision);
368
+ const terminal = states
369
+ .filter((state) => state.status !== 'queued' && state.status !== 'running')
370
+ .sort((left, right) => right.resourceRevision - left.resourceRevision)
371
+ .slice(0, 8);
372
+ return [...active, ...terminal].map((state) => ({
359
373
  agentId: state.agentId,
360
374
  status: state.status,
361
375
  description: state.description
@@ -389,6 +403,7 @@ export class SubagentSessionController {
389
403
  const completion = new Promise((done) => (resolve = done));
390
404
  const handleReady = new Promise((done) => (resolveHandleReady = done));
391
405
  state.status = 'queued';
406
+ this.#touchResource(state);
392
407
  delete state.summary;
393
408
  delete state.errorCode;
394
409
  delete state.errorMessage;
@@ -417,6 +432,7 @@ export class SubagentSessionController {
417
432
  if (!turn || state.status !== 'queued')
418
433
  continue;
419
434
  state.status = 'running';
435
+ this.#touchResource(state);
420
436
  this.#running++;
421
437
  void this.#startAndConsumeTurn(state, turn);
422
438
  }
@@ -452,6 +468,7 @@ export class SubagentSessionController {
452
468
  continue;
453
469
  state.messageRevision++;
454
470
  state.latestMessageActivityRevision = ++this.#activityRevision;
471
+ this.#touchResource(state);
455
472
  this.#notifyActivity({ state, kind: 'message' });
456
473
  }
457
474
  })();
@@ -469,6 +486,7 @@ export class SubagentSessionController {
469
486
  state.errorMessage = result.errorMessage ?? 'Subagent execution failed.';
470
487
  }
471
488
  state.terminalPending = true;
489
+ this.#touchResource(state);
472
490
  this.#closeReceipt(state);
473
491
  await this.#publishTerminal(state);
474
492
  }
@@ -504,6 +522,10 @@ export class SubagentSessionController {
504
522
  state.errorCode = diagnostic.code;
505
523
  state.errorMessage = diagnostic.message;
506
524
  state.terminalPending = true;
525
+ this.#touchResource(state);
526
+ }
527
+ #touchResource(state) {
528
+ state.resourceRevision = ++this.#resourceRevision;
507
529
  }
508
530
  #waitForAnyCompletion(candidates, waitMs, signal) {
509
531
  return waitForCompletionNotification(this.#completionWaiters, (state) => candidates.includes(state), waitMs, signal);
@@ -738,7 +760,8 @@ function restoreAgentState(session) {
738
760
  verification: [],
739
761
  evidence: [],
740
762
  terminalPending: false,
741
- terminalObserved: false
763
+ terminalObserved: false,
764
+ resourceRevision: 0
742
765
  };
743
766
  for (const record of currentRecords) {
744
767
  if (!isRecord(record.payload) ||
@@ -7,7 +7,7 @@ import { TOOL_EXECUTION_LIMITS } from './execution-limits.js';
7
7
  export const applyPatchToolDefinition = {
8
8
  name: 'apply_patch',
9
9
  parallelSafety: 'serial',
10
- description: `Preferred for precise, reviewable local edits. Group small, related implementation and test changes in one patch when they are already understood. Split large or independent changes, and wait for intermediate results only when the next edit depends on them. Submit a raw *** Begin Patch / *** End Patch patch. Supports Add File, Update File, Delete File, and Move to. Update hunks accept @@ or @@ <change context>; context-only hunks may also be used as ordered anchors before a later changing hunk. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. The complete patch is validated and committed atomically. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts. Limits: ${TOOL_EXECUTION_LIMITS.patchMaxBytes / 1_048_576} MiB patch input, ${TOOL_EXECUTION_LIMITS.patchMaxFiles} files, ${TOOL_EXECUTION_LIMITS.patchMaxFileBytes / 1_048_576} MiB per file, and ${TOOL_EXECUTION_LIMITS.patchMaxTotalBytes / 1_048_576} MiB working set. MAR_AGENT_PATCH_CONFLICT means no partial edit was applied; hunk match conflicts identify the affected path and hunk so you can reread that file before retrying.`,
10
+ description: `Preferred for precise, reviewable local edits. Do not batch all task files merely to reduce tool calls. Group only a few tightly coupled edits whose combined patch is quick to generate and review; split broad or independent work into successive bounded patches before composing them. Submit a raw *** Begin Patch / *** End Patch patch. Supports Add File, Update File, Delete File, and Move to. Add File may replace an existing regular file, and Move to may replace an existing regular destination; duplicate operations on the same resolved path remain invalid. Update hunks accept @@ or @@ <change context>; context-only hunks may also be used as ordered anchors before a later changing hunk. Context is searched forward with Codex-style exact, whitespace-tolerant, then common Unicode punctuation-tolerant matching. Syntax and resolved paths are validated before writing, then file units are committed in order. Each file unit is atomic, and a Move keeps its source and destination atomic. On MAR_AGENT_PATCH_PARTIAL, earlier files remain applied; the error lists applied, failed, and pending paths, so retry only failed and pending paths. Existing file permissions, line endings, and final-newline state are preserved. Returns changed file paths and operations with per-file and total line counts when all units succeed. Oversized patches are rejected without advertising defensive limits as recommended patch sizes. A first-unit MAR_AGENT_PATCH_CONFLICT applies nothing; hunk match conflicts identify the affected path and hunk so you can reread only that file before retrying.`,
11
11
  inputMode: 'freeform',
12
12
  outputSchema: {
13
13
  type: 'object',
@@ -63,8 +63,23 @@ export class ApplyPatchTool {
63
63
  }
64
64
  async execute(patch) {
65
65
  const changes = parsePatch(patch);
66
- const { mutations, results } = await this.#preflight(changes);
67
- await this.#commit(mutations);
66
+ const resolvedChanges = await this.#resolveChanges(changes);
67
+ const results = [];
68
+ let totalBytes = 0;
69
+ for (const [index, resolvedChange] of resolvedChanges.entries()) {
70
+ try {
71
+ const preflight = await this.#preflight(resolvedChange, totalBytes);
72
+ await this.#commit(preflight.mutations);
73
+ results.push(preflight.result);
74
+ totalBytes = preflight.totalBytes;
75
+ }
76
+ catch (error) {
77
+ if (!results.length ||
78
+ (error instanceof MarAgentError && error.code === 'MAR_AGENT_PATCH_ROLLBACK_FAILED'))
79
+ throw error;
80
+ throw partialPatchError(error, results, resolvedChanges.slice(index));
81
+ }
82
+ }
68
83
  return {
69
84
  files: results,
70
85
  totals: {
@@ -74,9 +89,8 @@ export class ApplyPatchTool {
74
89
  }
75
90
  };
76
91
  }
77
- async #preflight(changes) {
78
- const mutations = [];
79
- const results = [];
92
+ async #resolveChanges(changes) {
93
+ const resolvedChanges = [];
80
94
  const claimedTargets = new Set();
81
95
  const claimTarget = (target) => {
82
96
  const key = resolve(target);
@@ -84,83 +98,130 @@ export class ApplyPatchTool {
84
98
  invalid('Patch contains duplicate or overlapping paths.');
85
99
  claimedTargets.add(key);
86
100
  };
87
- let totalBytes = 0;
88
101
  for (const change of changes) {
89
102
  if (change.kind === 'add') {
90
103
  const target = await this.paths.resolve(change.path, true, 'write');
91
104
  claimTarget(target);
92
- await assertMissing(target);
93
- const content = Buffer.from(`${change.lines.join('\n')}\n`);
94
- totalBytes = enforceContentLimits(content, totalBytes);
95
- mutations.push({ path: change.path, target, content, expectedExisting: false });
96
- results.push({
97
- path: change.path,
98
- operation: 'added',
99
- addedLines: change.lines.length,
100
- deletedLines: 0
101
- });
105
+ resolvedChanges.push({ kind: 'add', change, target });
102
106
  continue;
103
107
  }
104
- const source = await this.paths.resolve(change.path, false, 'write');
108
+ const source = await this.paths.resolve(change.path, true, 'write');
105
109
  claimTarget(source);
106
- const sourceStat = await lstat(source);
107
- if (!sourceStat.isFile())
108
- invalid('Patch sources must be regular files.');
109
- const original = await readBoundedFile(source);
110
- totalBytes = enforceContentLimits(original, totalBytes);
111
110
  if (change.kind === 'delete') {
112
- mutations.push({ path: change.path, target: source, expectedExisting: true });
113
- results.push({
114
- path: change.path,
115
- operation: 'deleted',
116
- addedLines: 0,
117
- deletedLines: logicalLineCount(original)
118
- });
111
+ resolvedChanges.push({ kind: 'delete', change, source });
119
112
  continue;
120
113
  }
121
- const content = change.hunks.length
122
- ? applyHunks(original, change.hunks, change.path)
123
- : original;
124
- totalBytes = enforceContentLimits(content, totalBytes);
125
- const addedLines = change.hunks.reduce((sum, hunk) => sum + hunk.addedLines, 0);
126
- const deletedLines = change.hunks.reduce((sum, hunk) => sum + hunk.deletedLines, 0);
127
114
  if (change.targetPath) {
128
115
  const target = await this.paths.resolve(change.targetPath, true, 'write');
129
116
  claimTarget(target);
130
- await assertMissing(target);
131
- mutations.push({ path: change.path, target: source, expectedExisting: true });
132
- mutations.push({
133
- path: change.targetPath,
134
- target,
135
- content,
136
- mode: sourceStat.mode & 0o7777,
137
- expectedExisting: false
117
+ resolvedChanges.push({
118
+ kind: 'move',
119
+ change: { ...change, targetPath: change.targetPath },
120
+ source,
121
+ target
138
122
  });
139
- results.push({
123
+ }
124
+ else
125
+ resolvedChanges.push({ kind: 'update', change, source });
126
+ }
127
+ return resolvedChanges;
128
+ }
129
+ async #preflight(resolvedChange, currentTotalBytes) {
130
+ if (resolvedChange.kind === 'add') {
131
+ const { change } = resolvedChange;
132
+ const targetStat = await optionalLstat(resolvedChange.target);
133
+ if (targetStat && !targetStat.isFile())
134
+ invalid('Patch targets must be regular files.');
135
+ const content = Buffer.from(`${change.lines.join('\n')}\n`);
136
+ const totalBytes = enforceContentLimits(content, currentTotalBytes);
137
+ return {
138
+ mutations: [
139
+ {
140
+ path: change.path,
141
+ target: resolvedChange.target,
142
+ content,
143
+ ...(targetStat ? { mode: targetStat.mode & 0o7777 } : {}),
144
+ expectedExisting: targetStat !== undefined
145
+ }
146
+ ],
147
+ result: {
140
148
  path: change.path,
141
- targetPath: change.targetPath,
149
+ operation: 'added',
150
+ addedLines: change.lines.length,
151
+ deletedLines: 0
152
+ },
153
+ totalBytes
154
+ };
155
+ }
156
+ const sourceStat = await lstat(resolvedChange.source);
157
+ if (!sourceStat.isFile())
158
+ invalid('Patch sources must be regular files.');
159
+ const original = await readBoundedFile(resolvedChange.source);
160
+ let totalBytes = enforceContentLimits(original, currentTotalBytes);
161
+ if (resolvedChange.kind === 'delete') {
162
+ const { change } = resolvedChange;
163
+ return {
164
+ mutations: [{ path: change.path, target: resolvedChange.source, expectedExisting: true }],
165
+ result: {
166
+ path: change.path,
167
+ operation: 'deleted',
168
+ addedLines: 0,
169
+ deletedLines: logicalLineCount(original)
170
+ },
171
+ totalBytes
172
+ };
173
+ }
174
+ const { change } = resolvedChange;
175
+ const content = change.hunks.length
176
+ ? applyHunks(original, change.hunks, change.path)
177
+ : original;
178
+ totalBytes = enforceContentLimits(content, totalBytes);
179
+ const addedLines = change.hunks.reduce((sum, hunk) => sum + hunk.addedLines, 0);
180
+ const deletedLines = change.hunks.reduce((sum, hunk) => sum + hunk.deletedLines, 0);
181
+ if (resolvedChange.kind === 'move') {
182
+ const moveChange = resolvedChange.change;
183
+ const targetStat = await optionalLstat(resolvedChange.target);
184
+ if (targetStat && !targetStat.isFile())
185
+ invalid('Patch targets must be regular files.');
186
+ return {
187
+ mutations: [
188
+ { path: change.path, target: resolvedChange.source, expectedExisting: true },
189
+ {
190
+ path: moveChange.targetPath,
191
+ target: resolvedChange.target,
192
+ content,
193
+ mode: (targetStat ?? sourceStat).mode & 0o7777,
194
+ expectedExisting: targetStat !== undefined
195
+ }
196
+ ],
197
+ result: {
198
+ path: change.path,
199
+ targetPath: moveChange.targetPath,
142
200
  operation: 'moved',
143
201
  addedLines,
144
202
  deletedLines
145
- });
146
- }
147
- else {
148
- mutations.push({
203
+ },
204
+ totalBytes
205
+ };
206
+ }
207
+ return {
208
+ mutations: [
209
+ {
149
210
  path: change.path,
150
- target: source,
211
+ target: resolvedChange.source,
151
212
  content,
152
213
  mode: sourceStat.mode & 0o7777,
153
214
  expectedExisting: true
154
- });
155
- results.push({
156
- path: change.path,
157
- operation: 'modified',
158
- addedLines,
159
- deletedLines
160
- });
161
- }
162
- }
163
- return { mutations, results };
215
+ }
216
+ ],
217
+ result: {
218
+ path: change.path,
219
+ operation: 'modified',
220
+ addedLines,
221
+ deletedLines
222
+ },
223
+ totalBytes
224
+ };
164
225
  }
165
226
  async #commit(mutations) {
166
227
  const staged = [];
@@ -447,9 +508,40 @@ function enforceContentLimits(content, current) {
447
508
  throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', 'Patch working set exceeds the byte limit.');
448
509
  return total;
449
510
  }
450
- async function assertMissing(path) {
451
- if (await exists(path))
452
- throw new MarAgentError('MAR_AGENT_PATCH_CONFLICT', 'Patch target already exists.');
511
+ function partialPatchError(cause, appliedResults, pendingChanges) {
512
+ const appliedFiles = appliedResults.map(resultPath);
513
+ const failedFile = changePath(pendingChanges[0].change);
514
+ const pendingFiles = pendingChanges.slice(1).map(({ change }) => changePath(change));
515
+ return new MarAgentError('MAR_AGENT_PATCH_PARTIAL', `Patch partially applied. Applied: ${summarizePaths(appliedFiles)}. Failed: ${failedFile}. Pending: ${summarizePaths(pendingFiles)}. Retry only failed and pending paths.`, {
516
+ cause,
517
+ retryable: true,
518
+ details: { appliedFiles, failedFile, pendingFiles }
519
+ });
520
+ }
521
+ function resultPath(result) {
522
+ return result.targetPath === undefined ? result.path : `${result.path} -> ${result.targetPath}`;
523
+ }
524
+ function changePath(change) {
525
+ return change.kind === 'update' && change.targetPath !== undefined
526
+ ? `${change.path} -> ${change.targetPath}`
527
+ : change.path;
528
+ }
529
+ function summarizePaths(paths) {
530
+ if (!paths.length)
531
+ return '(none)';
532
+ const visible = paths.slice(0, 12);
533
+ const suffix = paths.length > visible.length ? `, … (${paths.length} total)` : '';
534
+ return `${visible.join(', ')}${suffix}`;
535
+ }
536
+ async function optionalLstat(path) {
537
+ try {
538
+ return await lstat(path);
539
+ }
540
+ catch (error) {
541
+ if (error.code === 'ENOENT')
542
+ return undefined;
543
+ throw error;
544
+ }
453
545
  }
454
546
  async function ensureDirectory(path, created) {
455
547
  const missing = [];
@@ -8,7 +8,10 @@ export async function executeRegisteredTool(name, arguments_, callId, context) {
8
8
  if (name.startsWith('mcp__')) {
9
9
  if (!context.host.callMcpTool)
10
10
  throw new MarAgentError('MAR_AGENT_MCP_FAILED', 'MCP execution is unavailable.');
11
- return context.host.callMcpTool(call, context.signal);
11
+ return context.host.callMcpTool(call, context.signal, {
12
+ sessionId: context.sessionId,
13
+ executionId: context.executionId
14
+ });
12
15
  }
13
16
  return context.host.executeTool(call, {
14
17
  access: context.access,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.9.88",
3
+ "version": "0.9.90",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",