@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.
- package/dist/host/local-agent-host.d.ts +15 -3
- package/dist/host/local-agent-host.js +41 -4
- package/dist/index.d.ts +2 -2
- package/dist/prompts/resources.js +1 -1
- package/dist/sdk/agent.js +14 -1
- package/dist/sdk/types.d.ts +6 -2
- package/dist/subagent/session-controller.d.ts +1 -0
- package/dist/subagent/session-controller.js +29 -6
- package/dist/tools/apply-patch.js +156 -64
- package/dist/tools/runtime.js +4 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
98
|
-
|
|
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
|
-
|
|
101
|
-
if (
|
|
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]
|
|
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
|
};
|
package/dist/sdk/types.d.ts
CHANGED
|
@@ -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>;
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
67
|
-
|
|
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 #
|
|
78
|
-
const
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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 = [];
|
package/dist/tools/runtime.js
CHANGED
|
@@ -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,
|