@myagentroam/agent 0.11.20 → 0.11.22

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.
Files changed (81) hide show
  1. package/dist/agent-core/error.d.ts +1 -1
  2. package/dist/agent-core/error.js +20 -14
  3. package/dist/agent-core/extensions/compaction.d.ts +6 -0
  4. package/dist/agent-core/extensions/compaction.js +16 -2
  5. package/dist/agent-core/extensions/model-request.d.ts +5 -9
  6. package/dist/agent-core/extensions/model-request.js +14 -3
  7. package/dist/agent-core/extensions/prompt-input.js +3 -1
  8. package/dist/agent-core/host/contracts.d.ts +8 -0
  9. package/dist/agent-core/host/local-agent-host.d.ts +2 -0
  10. package/dist/agent-core/host/local-agent-host.js +3 -0
  11. package/dist/agent-core/model/anthropic-messages.js +15 -26
  12. package/dist/agent-core/model/catalog-file.d.ts +2 -0
  13. package/dist/agent-core/model/configuration.d.ts +3 -1
  14. package/dist/agent-core/model/configuration.js +1 -0
  15. package/dist/agent-core/model/contracts.d.ts +5 -8
  16. package/dist/agent-core/model/image.d.ts +17 -0
  17. package/dist/agent-core/model/image.js +53 -0
  18. package/dist/agent-core/model/openai-responses.d.ts +7 -2
  19. package/dist/agent-core/model/openai-responses.js +81 -27
  20. package/dist/agent-core/runtime/compaction/retained-context.js +2 -9
  21. package/dist/agent-core/runtime/context/gc-runtime.d.ts +3 -1
  22. package/dist/agent-core/runtime/context/gc-runtime.js +17 -6
  23. package/dist/agent-core/runtime/context/gc-tokens.js +13 -0
  24. package/dist/agent-core/runtime/context/gc.d.ts +2 -1
  25. package/dist/agent-core/runtime/context/gc.js +75 -12
  26. package/dist/agent-core/runtime/context/projection.js +4 -0
  27. package/dist/agent-core/runtime/context/request-budget.d.ts +2 -4
  28. package/dist/agent-core/runtime/context/restore.d.ts +2 -0
  29. package/dist/agent-core/runtime/context/restore.js +19 -3
  30. package/dist/agent-core/runtime/contracts.d.ts +2 -3
  31. package/dist/agent-core/runtime/execution.d.ts +1 -1
  32. package/dist/agent-core/runtime/execution.js +10 -4
  33. package/dist/agent-core/runtime/resource-publication.d.ts +8 -0
  34. package/dist/agent-core/runtime/resource-publication.js +60 -0
  35. package/dist/agent-core/sdk/agent.js +16 -0
  36. package/dist/agent-core/sdk/model-runtime.js +2 -0
  37. package/dist/agent-core/sdk/resource-input.d.ts +31 -0
  38. package/dist/agent-core/sdk/resource-input.js +342 -0
  39. package/dist/agent-core/sdk/session.js +144 -39
  40. package/dist/agent-core/session/catalog.d.ts +4 -0
  41. package/dist/agent-core/session/catalog.js +4 -0
  42. package/dist/agent-core/session/context-gc-checkpoint.d.ts +6 -4
  43. package/dist/agent-core/session/context-gc-checkpoint.js +41 -16
  44. package/dist/agent-core/session/inline-media.d.ts +45 -0
  45. package/dist/agent-core/session/inline-media.js +163 -0
  46. package/dist/agent-core/session/store/contracts.d.ts +11 -0
  47. package/dist/agent-core/session/store/history.d.ts +1 -1
  48. package/dist/agent-core/session/store/history.js +26 -2
  49. package/dist/agent-core/session/store/images.d.ts +4 -6
  50. package/dist/agent-core/session/store/images.js +18 -111
  51. package/dist/agent-core/session/store/jsonl-store.d.ts +9 -0
  52. package/dist/agent-core/session/store/jsonl-store.js +100 -28
  53. package/dist/agent-core/session/store/resources.d.ts +26 -0
  54. package/dist/agent-core/session/store/resources.js +196 -0
  55. package/dist/agent-core/session/store/types.d.ts +7 -6
  56. package/dist/agent-core/session/transaction-recovery.js +1 -1
  57. package/dist/agent-core/tools/code-mode/cells.js +3 -4
  58. package/dist/agent-core/tools/code-mode/index.js +32 -8
  59. package/dist/agent-core/tools/code-mode/observations.d.ts +5 -16
  60. package/dist/agent-core/tools/code-mode/quickjs-engine.js +23 -26
  61. package/dist/agent-core/tools/contracts.d.ts +15 -5
  62. package/dist/agent-core/tools/execution-limits.d.ts +3 -1
  63. package/dist/agent-core/tools/execution-limits.js +3 -1
  64. package/dist/agent-core/tools/execution.d.ts +2 -1
  65. package/dist/agent-core/tools/execution.js +31 -4
  66. package/dist/agent-core/tools/input-diagnostics/source-preview.d.ts +10 -0
  67. package/dist/agent-core/tools/input-diagnostics/source-preview.js +64 -0
  68. package/dist/agent-core/tools/local/apply-patch-parser.js +2 -53
  69. package/dist/agent-core/tools/local/apply-patch.js +1 -1
  70. package/dist/agent-core/tools/local/image-processing.d.ts +24 -0
  71. package/dist/agent-core/tools/local/image-processing.js +135 -0
  72. package/dist/agent-core/tools/local/read.js +1 -1
  73. package/dist/agent-core/tools/local/view-image.d.ts +4 -0
  74. package/dist/agent-core/tools/local/view-image.js +154 -25
  75. package/dist/agent-core/tools/output-projection.d.ts +2 -4
  76. package/dist/agent-core/tools/output-projection.js +81 -10
  77. package/dist/agent-core/tools/runtime-context.d.ts +1 -0
  78. package/dist/agent-core/tools/runtime.js +20 -1
  79. package/dist/cli/main.js +2 -1
  80. package/dist/index.d.ts +1 -1
  81. package/package.json +2 -1
@@ -1,3 +1,4 @@
1
+ import type { ModelImage } from '../../model/image.js';
1
2
  /** Execution metadata is independent of explicit model observations. */
2
3
  export interface CodeToolCallRecord {
3
4
  callId: string;
@@ -41,10 +42,7 @@ export type CodeObservation = {
41
42
  };
42
43
  export declare function observationContent(value: CodeObservation): string;
43
44
  export declare function observationImages(value: CodeObservation): readonly number[];
44
- export declare function selectObservationImages(values: readonly CodeObservation[], images: readonly {
45
- mimeType: string;
46
- dataBase64: string;
47
- }[], offset?: number): {
45
+ export declare function selectObservationImages(values: readonly CodeObservation[], images: readonly ModelImage[], offset?: number): {
48
46
  observations: ({
49
47
  result: {
50
48
  imageIndices: number[];
@@ -68,10 +66,7 @@ export declare function selectObservationImages(values: readonly CodeObservation
68
66
  value: string;
69
67
  truncated?: boolean;
70
68
  })[];
71
- images: {
72
- mimeType: string;
73
- dataBase64: string;
74
- }[];
69
+ images: ModelImage[];
75
70
  };
76
71
  /** Only the visible projection crosses into model text; program data and inputs stay private. */
77
72
  export declare function visibleObservation(value: CodeObservation): Record<string, unknown>;
@@ -90,19 +85,13 @@ export declare function observationModelParts(message: {
90
85
  readonly text: string;
91
86
  }[];
92
87
  observations?: readonly CodeObservation[];
93
- images?: readonly {
94
- mimeType: string;
95
- dataBase64: string;
96
- }[];
88
+ images?: readonly import('../../model/image.js').ModelImage[];
97
89
  }): Array<{
98
90
  type: 'text';
99
91
  text: string;
100
92
  } | {
101
93
  type: 'image';
102
- image: {
103
- mimeType: string;
104
- dataBase64: string;
105
- };
94
+ image: import('../../model/image.js').ModelImage;
106
95
  }> | undefined;
107
96
  /** Uses the same visible text as both adapters, without pixels or private observation data. */
108
97
  export declare function observationModelText(message: Parameters<typeof observationModelParts>[0]): string | undefined;
@@ -1,4 +1,5 @@
1
1
  import { BoundedTextOutput } from './text-output.js';
2
+ import { sourcePreview } from '../input-diagnostics/source-preview.js';
2
3
  // This trusted module runs only in the Host-managed worker, never in the model's JS context.
3
4
  /** @returns {Promise<import('./engine.js').CodeModeEngine>} */
4
5
  export async function createCodeModeEngine(options) {
@@ -80,19 +81,25 @@ export async function createCodeModeEngine(options) {
80
81
  throw executionTimeout();
81
82
  let message = String(error?.message ?? error).slice(0, 4096);
82
83
  const syntaxError = locateSource && error?.name === 'SyntaxError';
83
- if (syntaxError)
84
- message = `JavaScript compilation failed before tool execution: ${message}`.slice(0, 4096);
84
+ if (syntaxError) {
85
+ const reason = message === 'unexpected end of string' ? 'Unterminated string literal.' : message;
86
+ const location = compilerSourceLocation(error, input.code);
87
+ message = `${reason.slice(0, 256)}\n${location ? `Line ${location.line}${location.column === undefined ? '' : `, column ${location.column}`}. ` : ''}No tools were executed.`;
88
+ if (location?.column !== undefined) {
89
+ const source = input.code.split(/\r\n|[\r\n\u2028\u2029]/u)[location.line - 1];
90
+ const preview = sourcePreview(source, location.column);
91
+ const gutter = String(location.line);
92
+ message += `\n\n${gutter} | ${preview.text}\n${' '.repeat(gutter.length)} | ${' '.repeat(preview.caret)}^`;
93
+ }
94
+ }
85
95
  if (error?.name === 'ReferenceError' &&
86
96
  ["'text' is not defined", "'console' is not defined"].includes(message))
87
97
  message =
88
98
  `${message} Code Mode has no console or standalone text/image helpers; use observe.text(value) or observe.image(value).`.slice(0, 4096);
89
- const location = locateSource ? compilerSourceLocation(error, input.code) : '';
90
- if (location)
91
- message = message.slice(0, 4096 - location.length) + location;
92
- const code = message.includes('out of memory')
93
- ? 'MAR_AGENT_CODE_MEMORY_LIMIT'
94
- : syntaxError
95
- ? 'MAR_AGENT_CODE_SYNTAX_ERROR'
99
+ const code = syntaxError
100
+ ? 'MAR_AGENT_CODE_SYNTAX_ERROR'
101
+ : message.includes('out of memory')
102
+ ? 'MAR_AGENT_CODE_MEMORY_LIMIT'
96
103
  : typeof error?.code === 'string'
97
104
  ? error.code
98
105
  : 'MAR_AGENT_CODE_FAILED';
@@ -711,12 +718,12 @@ function compilerSourceLocation(error, source) {
711
718
  error.fileName !== 'code-mode.js' ||
712
719
  !Number.isSafeInteger(error.lineNumber) ||
713
720
  error.lineNumber <= 1)
714
- return '';
721
+ return undefined;
715
722
  // QuickJS counts LF lines and Unicode code-point columns; the wrapper adds one LF line.
716
723
  const lines = source.split('\n');
717
724
  const line = error.lineNumber - 1;
718
725
  if (line > lines.length)
719
- return compilerCaret(source, source, true);
726
+ return originalSourceLocation(source);
720
727
  const column = Number(typeof error.stack === 'string'
721
728
  ? error.stack.match(/^\s*at code-mode\.js:\d+:(\d+)(?:\n|$)/u)?.[1]
722
729
  : undefined);
@@ -725,9 +732,9 @@ function compilerSourceLocation(error, source) {
725
732
  column > Array.from(lines[line - 1]).length + 1) {
726
733
  // Without a valid column, intra-LF-line separators make the original source line ambiguous.
727
734
  if (/[\r\u2028\u2029]/u.test(lines[line - 1].replace(/\r$/u, '')))
728
- return '';
735
+ return undefined;
729
736
  const preceding = line > 1 ? lines.slice(0, line - 1).join('\n') + '\n' : '';
730
- return ` (code-mode.js:${preceding.split(/\r\n|[\r\n\u2028\u2029]/u).length})`;
737
+ return { line: preceding.split(/\r\n|[\r\n\u2028\u2029]/u).length };
731
738
  }
732
739
  const prefix = [
733
740
  ...lines.slice(0, line - 1),
@@ -735,23 +742,13 @@ function compilerSourceLocation(error, source) {
735
742
  .slice(0, column - 1)
736
743
  .join('')
737
744
  ].join('\n');
738
- return compilerCaret(source, prefix);
745
+ return originalSourceLocation(prefix);
739
746
  }
740
- function compilerCaret(source, prefix, endOfInput = false) {
747
+ function originalSourceLocation(prefix) {
741
748
  const segments = prefix.split(/\r\n|[\r\n\u2028\u2029]/u);
742
749
  const line = segments.length;
743
750
  const column = Array.from(segments.at(-1)).length + 1;
744
- const character = source.slice(prefix.length)[Symbol.iterator]().next().value;
745
- // Source may contain credentials, commands or private code. Show only a punctuation token.
746
- const token = endOfInput
747
- ? '<end of input>'
748
- : character === undefined || /[\r\n\u2028\u2029]/u.test(character)
749
- ? '<end of line>'
750
- : /^[{}[\]():;,.=+*/?!<>|&%\-]$/u.test(character)
751
- ? character
752
- : '<character omitted>';
753
- const preview = '<source omitted>';
754
- return ` (code-mode.js:${line}${endOfInput ? ', end of input' : ''})\nSyntax location: line ${line}, column ${column}\n${line} | ${preview}${token}\n${' '.repeat(String(line).length)} | ${' '.repeat(preview.length)}^`;
751
+ return { line, column };
755
752
  }
756
753
  function isParameterlessTool(tool) {
757
754
  if (tool.parameterless === true)
@@ -98,10 +98,7 @@ export interface ToolExecutionResult {
98
98
  /** Runtime-only definitions used to encode a Tool Search output. */
99
99
  toolSearchTools?: readonly ClientToolDefinition[];
100
100
  truncated?: boolean;
101
- images?: readonly {
102
- mimeType: string;
103
- dataBase64: string;
104
- }[];
101
+ images?: readonly import('../model/image.js').ModelImage[];
105
102
  artifacts?: readonly MarAgentToolArtifact[];
106
103
  /** Host-facing evidence, excluded from model and programmatic guest results. */
107
104
  resultExtensions?: readonly import('./result-extensions.js').ToolResultExtension[];
@@ -164,10 +161,23 @@ export interface ReadEventData {
164
161
  }>;
165
162
  truncated: boolean;
166
163
  }
164
+ export interface ViewImageRegion {
165
+ x: number;
166
+ y: number;
167
+ width: number;
168
+ height: number;
169
+ }
167
170
  export interface ViewImageResult {
168
- path: string;
171
+ /** The input source; transformations may produce a different output resource. */
172
+ path?: string;
173
+ resourceId?: string;
169
174
  mimeType: 'image/png' | 'image/jpeg' | 'image/webp';
170
175
  bytes: number;
176
+ width: number;
177
+ height: number;
178
+ sourceWidth?: number;
179
+ sourceHeight?: number;
180
+ region?: ViewImageRegion;
171
181
  sha256: string;
172
182
  }
173
183
  export interface TodoItem {
@@ -5,7 +5,9 @@ export declare const TOOL_EXECUTION_LIMITS: Readonly<{
5
5
  readMaxBatchItems: 16;
6
6
  readBatchConcurrency: 4;
7
7
  readMaxBatchBytes: 4194304;
8
- viewImageMaxBytes: 20971520;
8
+ viewImageMaxBytes: 3145728;
9
+ viewImageMaxSourceBytes: 20971520;
10
+ viewImageMaxPixels: 16777216;
9
11
  execMaxTimeoutMs: 3600000;
10
12
  execDefaultYieldMs: 10000;
11
13
  execMaxYieldMs: 30000;
@@ -5,7 +5,9 @@ export const TOOL_EXECUTION_LIMITS = Object.freeze({
5
5
  readMaxBatchItems: 16,
6
6
  readBatchConcurrency: 4,
7
7
  readMaxBatchBytes: 4_194_304,
8
- viewImageMaxBytes: 20_971_520,
8
+ viewImageMaxBytes: 3_145_728,
9
+ viewImageMaxSourceBytes: 20_971_520,
10
+ viewImageMaxPixels: 16_777_216,
9
11
  execMaxTimeoutMs: 3_600_000,
10
12
  execDefaultYieldMs: 10_000,
11
13
  execMaxYieldMs: 30_000,
@@ -11,6 +11,7 @@ export type ExecutedToolCall = {
11
11
  call: ClientToolCall;
12
12
  content: string;
13
13
  images?: ToolExecutionResult['images'];
14
+ sourceImages?: ToolExecutionResult['images'];
14
15
  observations?: ToolExecutionResult['observations'];
15
16
  calls?: ToolExecutionResult['calls'];
16
17
  } & ({
@@ -31,7 +32,7 @@ export interface RoundToolScope {
31
32
  readonly assertExecutionLive: () => void;
32
33
  readonly emit: (event: EventPayload, assertLive?: () => void) => Promise<void>;
33
34
  readonly searchMcpTools: NonNullable<ToolRuntimeContext['searchMcpTools']>;
34
- readonly recentImages: () => ToolRuntimeImage[];
35
+ readonly recentImages: () => Promise<ToolRuntimeImage[]>;
35
36
  readonly captureCodeContext: () => Omit<ToolRuntimeContext, 'signal' | 'codeMode' | 'expectedMcpTool' | 'searchMcpTools'>;
36
37
  readonly codeModeContext: (call: ToolCall & {
37
38
  readonly callId: string;
@@ -4,6 +4,7 @@ import { allowsToolCaller } from './callers.js';
4
4
  import { CodeCellExecutionError } from './code-mode/cells.js';
5
5
  import { createCodeToolDispatch } from './code-mode/dispatch.js';
6
6
  import { boundedModelToolOutput, structuredToolArtifacts, structuredToolEventData, structuredToolFailureData } from './output-projection.js';
7
+ import { publishModelResources } from '../runtime/resource-publication.js';
7
8
  import { executeRegisteredTool } from './runtime.js';
8
9
  import { failedToolResultExtensions } from './result-extensions.js';
9
10
  import { permissionStopFromError } from './permissions.js';
@@ -13,6 +14,11 @@ export async function createRoundToolRuntimeContext(scope, call, signal) {
13
14
  scope.assertLive();
14
15
  signal.throwIfAborted();
15
16
  const codeMode = await scope.codeModeContext(call);
17
+ const recentImages = scope.extensionToolNames.has(call.name) ||
18
+ call.name === 'code_exec' ||
19
+ call.name === 'code_exec_wait'
20
+ ? await scope.recentImages()
21
+ : undefined;
16
22
  scope.assertLive();
17
23
  signal.throwIfAborted();
18
24
  const assertSessionLive = scope.assertLive;
@@ -52,7 +58,10 @@ export async function createRoundToolRuntimeContext(scope, call, signal) {
52
58
  }
53
59
  }),
54
60
  invoke: createCodeToolDispatch({
55
- context: scope.captureCodeContext(),
61
+ context: {
62
+ ...scope.captureCodeContext(),
63
+ ...(recentImages === undefined ? {} : { recentImages })
64
+ },
56
65
  tools: scope.tools,
57
66
  expectations: scope.expectations,
58
67
  parentCallId: call.callId,
@@ -61,7 +70,9 @@ export async function createRoundToolRuntimeContext(scope, call, signal) {
61
70
  emit: (event) => emitCodeEvent(event, assertDispatchLive)
62
71
  })
63
72
  },
64
- ...(scope.extensionToolNames.has(call.name) ? { recentImages: scope.recentImages() } : {})
73
+ ...(scope.extensionToolNames.has(call.name) && recentImages !== undefined
74
+ ? { recentImages }
75
+ : {})
65
76
  };
66
77
  }
67
78
  /** Complete every call in model order even if individual execution fails. */
@@ -84,9 +95,19 @@ export async function executeRoundTools(calls, scope) {
84
95
  signal.throwIfAborted();
85
96
  return executeRegisteredTool(call.name, call.arguments, call.callId, context);
86
97
  };
87
- const output = call.name === 'code_exec' || call.name === 'code_exec_wait'
98
+ const rawOutput = call.name === 'code_exec' || call.name === 'code_exec_wait'
88
99
  ? await runTool()
89
100
  : await scope.schedule(scope.tools.find((tool) => tool.name === call.name), runTool);
101
+ const publishedImages = await publishModelResources(scope.context.host, rawOutput.images, {
102
+ sessionId: scope.context.sessionId,
103
+ executionId: scope.context.executionId
104
+ }, scope.context.imageStore);
105
+ const output = publishedImages === rawOutput.images
106
+ ? rawOutput
107
+ : {
108
+ ...rawOutput,
109
+ ...(publishedImages === undefined ? {} : { images: publishedImages })
110
+ };
90
111
  const modelOutput = boundedModelToolOutput(call.name, output.content, output.data);
91
112
  const eventSummary = toolEventSummary(output.content);
92
113
  const structuredData = structuredToolEventData(call.name, output.data);
@@ -106,6 +127,7 @@ export async function executeRoundTools(calls, scope) {
106
127
  call,
107
128
  output,
108
129
  content: modelOutput.content,
130
+ ...(rawOutput.images?.length ? { sourceImages: rawOutput.images } : {}),
109
131
  ...(output.calls
110
132
  ? { calls: output.calls.map(({ resultExtensions: _evidence, ...call }) => call) }
111
133
  : {}),
@@ -116,7 +138,11 @@ export async function executeRoundTools(calls, scope) {
116
138
  }
117
139
  catch (error) {
118
140
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_HOST_OPERATION_FAILED');
119
- const partial = error instanceof CodeCellExecutionError ? error.result : undefined;
141
+ const partialResult = error instanceof CodeCellExecutionError ? error.result : undefined;
142
+ const publishedPartialImages = await publishModelResources(scope.context.host, partialResult?.images, { sessionId: scope.context.sessionId, executionId: scope.context.executionId }, scope.context.imageStore);
143
+ const partial = partialResult !== undefined && publishedPartialImages !== partialResult.images
144
+ ? { ...partialResult, images: publishedPartialImages }
145
+ : partialResult;
120
146
  const failureData = structuredToolFailureData(call.name, error);
121
147
  const eventData = failureData ??
122
148
  (partial?.data === undefined
@@ -151,6 +177,7 @@ export async function executeRoundTools(calls, scope) {
151
177
  }), failureData).content
152
178
  : boundedModelToolOutput(call.name, partial.content, partial.data).content,
153
179
  ...(partial?.images?.length ? { images: partial.images } : {}),
180
+ ...(partialResult?.images?.length ? { sourceImages: partialResult.images } : {}),
154
181
  failed: true,
155
182
  ...(partial?.calls
156
183
  ? { calls: partial.calls.map(({ resultExtensions: _evidence, ...call }) => call) }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Preview only the submitted line around a one-based Unicode character column.
3
+ * Escape controls and measure rendered graphemes, not source columns, for the caret.
4
+ * @param {string} source
5
+ * @param {number} column
6
+ */
7
+ export function sourcePreview(source: string, column: number): {
8
+ text: string;
9
+ caret: number;
10
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Preview only the submitted line around a one-based Unicode character column.
3
+ * Escape controls and measure rendered graphemes, not source columns, for the caret.
4
+ * @param {string} source
5
+ * @param {number} column
6
+ */
7
+ export function sourcePreview(source, column) {
8
+ const characters = Array.from(source);
9
+ const index = column - 1;
10
+ let before = '';
11
+ let start = index;
12
+ let width = 0;
13
+ while (start > 0) {
14
+ const token = displayCharacter(characters[start - 1]);
15
+ const candidate = token + before;
16
+ const size = displayWidth(candidate);
17
+ if (size > 60 || Array.from(candidate).length > 60)
18
+ break;
19
+ before = candidate;
20
+ width = size;
21
+ start--;
22
+ }
23
+ const prefix = start > 0 ? '… ' : '';
24
+ const caret = width + prefix.length;
25
+ let text = before;
26
+ let end = index;
27
+ while (end < characters.length) {
28
+ const token = displayCharacter(characters[end]);
29
+ const candidate = text + token;
30
+ if (displayWidth(candidate) > 156 || Array.from(candidate).length > 156)
31
+ break;
32
+ text = candidate;
33
+ end++;
34
+ }
35
+ return {
36
+ text: `${prefix}${text}${end < characters.length ? ' …' : ''}`,
37
+ caret
38
+ };
39
+ }
40
+ const graphemes = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
41
+ function displayWidth(text) {
42
+ let width = 0;
43
+ for (const { segment } of graphemes.segment(text)) {
44
+ const base = segment.match(/[^\p{Mark}\p{Cf}]/u)?.[0];
45
+ if (base === undefined)
46
+ continue;
47
+ // Match conventional monospace cells: combining marks join their base and emoji occupy two.
48
+ width +=
49
+ /[\p{Emoji_Presentation}\ufe0f\u20e3]/u.test(segment) ||
50
+ /[\u1100-\u115f\u2329\u232a\u2e80-\ua4cf\uac00-\ud7a3\uf900-\ufaff\ufe10-\ufe19\ufe30-\ufe6f\uff01-\uff60\uffe0-\uffe6\u{20000}-\u{3fffd}]/u.test(base)
51
+ ? 2
52
+ : 1;
53
+ }
54
+ return width;
55
+ }
56
+ function displayCharacter(character) {
57
+ if (character === '\t')
58
+ return ' ';
59
+ if (/[\u0000-\u001f\u007f-\u009f\u2028\u2029\p{Bidi_Control}]/u.test(character)) {
60
+ const escaped = JSON.stringify(character).slice(1, -1);
61
+ return escaped === character ? `\\u{${character.codePointAt(0).toString(16)}}` : escaped;
62
+ }
63
+ return character;
64
+ }
@@ -1,24 +1,13 @@
1
1
  import { isAbsolute, resolve } from 'node:path';
2
2
  import { MarAgentError } from '../../error.js';
3
3
  import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
4
+ import { sourcePreview } from '../input-diagnostics/source-preview.js';
4
5
  const beginMarker = '*** Begin Patch';
5
6
  const endMarker = '*** End Patch';
6
7
  const addHeader = '*** Add File: ';
7
8
  const updateHeader = '*** Update File: ';
8
9
  const deleteHeader = '*** Delete File: ';
9
10
  const moveHeader = '*** Move to: ';
10
- const syntaxTokens = [
11
- `${beginMarker} ***`,
12
- `${endMarker} ***`,
13
- addHeader,
14
- updateHeader,
15
- deleteHeader,
16
- moveHeader,
17
- '@@ ',
18
- '+',
19
- '-',
20
- ' '
21
- ];
22
11
  export function parsePatch(patch) {
23
12
  if (typeof patch !== 'string')
24
13
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Patch must be a string.');
@@ -169,7 +158,7 @@ class FreeformPatchParser {
169
158
  const line = index + 1;
170
159
  const column = characterCount(source.slice(0, characterOffset)) + 1;
171
160
  const offset = this.#lineOffsets[index] + characterOffset;
172
- const preview = syntaxPreview(source, characterOffset);
161
+ const preview = sourcePreview(source, column);
173
162
  const gutter = String(line);
174
163
  throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', `Patch syntax error at line ${line}, column ${column}: ${message}\n${gutter} | ${preview.text}\n${' '.repeat(gutter.length)} | ${' '.repeat(preview.caret)}^`, { details: { line, column, offset } });
175
164
  }
@@ -186,43 +175,3 @@ function characterCount(value) {
186
175
  count++;
187
176
  return count;
188
177
  }
189
- function syntaxPreview(source, characterOffset) {
190
- // Only grammar tokens and one offending character are public; paths and file bodies are not.
191
- const trimmed = source.trimStart();
192
- const leading = source.length - trimmed.length;
193
- const tokenLength = Math.max(...syntaxTokens.map((token) => commonPrefixLength(trimmed, token)));
194
- const token = trimmed.slice(0, tokenLength);
195
- if (characterOffset < leading) {
196
- const before = characterOffset > 40 ? '… ' : displaySyntax(source.slice(0, characterOffset));
197
- return {
198
- text: `${before}${displayCharacter(source[characterOffset])}<content omitted>`,
199
- caret: before.length
200
- };
201
- }
202
- const prefix = leading > 40 ? '… ' : displaySyntax(source.slice(0, leading));
203
- const syntaxEnd = leading + tokenLength;
204
- if (characterOffset < syntaxEnd) {
205
- return {
206
- text: `${prefix}${token}${source.length > syntaxEnd ? '<content omitted>' : ''}`,
207
- caret: prefix.length + characterOffset - leading
208
- };
209
- }
210
- const gap = characterOffset > syntaxEnd ? '<content omitted>' : '';
211
- const character = source.slice(characterOffset)[Symbol.iterator]().next().value;
212
- const before = `${prefix}${token}${gap}`;
213
- return {
214
- text: `${before}${character === undefined ? '<end of line>' : displayCharacter(character)}${character !== undefined && source.length > characterOffset + character.length ? '<content omitted>' : ''}`,
215
- caret: before.length
216
- };
217
- }
218
- function displayCharacter(character) {
219
- const point = character.codePointAt(0);
220
- if (point >= 0x7f)
221
- return `\\u{${point.toString(16)}}`;
222
- return JSON.stringify(character).slice(1, -1);
223
- }
224
- function displaySyntax(value) {
225
- return [...value]
226
- .map((character) => (character === '\t' ? ' ' : displayCharacter(character)))
227
- .join('');
228
- }
@@ -11,7 +11,7 @@ export const applyPatchToolDefinition = {
11
11
  allowedCallers: ['direct', 'programmatic'],
12
12
  codeModeExposure: 'both',
13
13
  parallelSafety: 'serial',
14
- 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, each marker on its own line. Surrounding marker whitespace and an optional trailing *** are accepted; incomplete markers or extra text are rejected. Add File content lines, including blank lines, must start with +. Syntax errors include line/column and a bounded caret diagnostic with file content omitted. 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, including trailing hunks, and unchanged file sections are validated in order but are not written or reported as edits. The patch must include a change or move. 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. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. 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.`,
14
+ 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, each marker on its own line. Surrounding marker whitespace and an optional trailing *** are accepted; incomplete markers or extra text are rejected. Add File content lines, including blank lines, must start with +. Syntax errors include line/column and a bounded excerpt of the submitted patch with a caret at the error. 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, including trailing hunks, and unchanged file sections are validated in order but are not written or reported as edits. The patch must include a change or move. 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. In Code Mode, the Promise resolves on success and rejects with code/message on failure; the returned envelope has data.files and data.totals, not an ok or success flag. 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.`,
15
15
  inputMode: 'freeform',
16
16
  outputSchema: {
17
17
  type: 'object',
@@ -0,0 +1,24 @@
1
+ import type { ViewImageRegion, ViewImageResult } from '../contracts.js';
2
+ export interface ImageResizeBounds {
3
+ maxWidth?: number;
4
+ maxHeight?: number;
5
+ }
6
+ export interface ImageMetadata {
7
+ width: number;
8
+ height: number;
9
+ pages: number;
10
+ }
11
+ export interface ProcessedImage {
12
+ bytes: Buffer;
13
+ width: number;
14
+ height: number;
15
+ mimeType: 'image/png';
16
+ sourceWidth?: number;
17
+ sourceHeight?: number;
18
+ region?: ViewImageRegion;
19
+ }
20
+ export declare function readImageMetadata(data: Buffer, mimeType: ViewImageResult['mimeType'], signal?: AbortSignal): Promise<ImageMetadata>;
21
+ export declare function processImage(data: Buffer, metadata: ImageMetadata, options: {
22
+ region?: ViewImageRegion;
23
+ resize?: ImageResizeBounds;
24
+ }, signal?: AbortSignal): Promise<ProcessedImage>;
@@ -0,0 +1,135 @@
1
+ import { MarAgentError } from '../../error.js';
2
+ import { TOOL_EXECUTION_LIMITS } from '../execution-limits.js';
3
+ export async function readImageMetadata(data, mimeType, signal) {
4
+ throwIfAborted(signal);
5
+ try {
6
+ const sharp = await loadSharp();
7
+ const metadata = await sharp(data, {
8
+ failOn: 'warning',
9
+ limitInputPixels: TOOL_EXECUTION_LIMITS.viewImageMaxPixels,
10
+ pages: 1
11
+ }).metadata();
12
+ throwIfAborted(signal);
13
+ if (metadata.format !== expectedSharpFormat(mimeType) ||
14
+ !isPositiveSafeInteger(metadata.width) ||
15
+ !isPositiveSafeInteger(metadata.height))
16
+ return incompleteImage();
17
+ const width = metadata.autoOrient?.width ?? metadata.width;
18
+ const height = metadata.autoOrient?.height ?? metadata.height;
19
+ assertPixelLimit(width, height);
20
+ return { width, height, pages: metadata.pages ?? 1 };
21
+ }
22
+ catch (error) {
23
+ throwIfAborted(signal);
24
+ if (error instanceof MarAgentError)
25
+ throw error;
26
+ if (isPixelLimitError(error))
27
+ return imagePixelLimit();
28
+ return incompleteImage();
29
+ }
30
+ }
31
+ export async function processImage(data, metadata, options, signal) {
32
+ throwIfAborted(signal);
33
+ if (metadata.pages > 1)
34
+ throw new MarAgentError('MAR_AGENT_IMAGE_FORMAT_UNSUPPORTED', 'Animated images cannot be cropped or resized.');
35
+ const region = options.region;
36
+ const regionWidth = region?.width ?? metadata.width;
37
+ const regionHeight = region?.height ?? metadata.height;
38
+ const resize = options.resize;
39
+ try {
40
+ const sharp = await loadSharp();
41
+ const image = sharp(data, {
42
+ failOn: 'warning',
43
+ limitInputPixels: TOOL_EXECUTION_LIMITS.viewImageMaxPixels,
44
+ pages: 1,
45
+ autoOrient: true
46
+ });
47
+ if (region)
48
+ image.extract({
49
+ left: region.x,
50
+ top: region.y,
51
+ width: region.width,
52
+ height: region.height
53
+ });
54
+ if (resize) {
55
+ image.resize({
56
+ ...(resize.maxWidth === undefined ? {} : { width: Math.min(resize.maxWidth, regionWidth) }),
57
+ ...(resize.maxHeight === undefined
58
+ ? {}
59
+ : { height: Math.min(resize.maxHeight, regionHeight) }),
60
+ fit: 'inside',
61
+ withoutEnlargement: true
62
+ });
63
+ }
64
+ const output = await image.png().toBuffer({ resolveWithObject: true });
65
+ throwIfAborted(signal);
66
+ if (output.data.byteLength > TOOL_EXECUTION_LIMITS.viewImageMaxBytes)
67
+ throw imageOutputLimit();
68
+ const changedRegion = region !== undefined &&
69
+ (region.x !== 0 ||
70
+ region.y !== 0 ||
71
+ region.width !== metadata.width ||
72
+ region.height !== metadata.height);
73
+ const changedSize = output.info.width !== regionWidth || output.info.height !== regionHeight;
74
+ return {
75
+ bytes: output.data,
76
+ width: output.info.width,
77
+ height: output.info.height,
78
+ mimeType: 'image/png',
79
+ ...(changedRegion || changedSize
80
+ ? { sourceWidth: metadata.width, sourceHeight: metadata.height }
81
+ : {}),
82
+ ...(changedRegion ? { region } : {})
83
+ };
84
+ }
85
+ catch (error) {
86
+ throwIfAborted(signal);
87
+ if (error instanceof MarAgentError)
88
+ throw error;
89
+ if (isPixelLimitError(error))
90
+ return imagePixelLimit();
91
+ return incompleteImage();
92
+ }
93
+ }
94
+ function expectedSharpFormat(mimeType) {
95
+ switch (mimeType) {
96
+ case 'image/png':
97
+ return 'png';
98
+ case 'image/jpeg':
99
+ return 'jpeg';
100
+ case 'image/webp':
101
+ return 'webp';
102
+ }
103
+ }
104
+ function assertPixelLimit(width, height) {
105
+ if (!isPositiveSafeInteger(width) ||
106
+ !isPositiveSafeInteger(height) ||
107
+ width > Math.floor(TOOL_EXECUTION_LIMITS.viewImageMaxPixels / height))
108
+ imagePixelLimit();
109
+ }
110
+ function isPositiveSafeInteger(value) {
111
+ return Number.isSafeInteger(value) && value > 0;
112
+ }
113
+ function isPixelLimitError(error) {
114
+ return error instanceof Error && /pixel limit/iu.test(error.message);
115
+ }
116
+ function throwIfAborted(signal) {
117
+ signal?.throwIfAborted();
118
+ }
119
+ function imageOutputLimit() {
120
+ return new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', `Image output exceeds ${TOOL_EXECUTION_LIMITS.viewImageMaxBytes} bytes. Use a smaller resize or region.`);
121
+ }
122
+ function imagePixelLimit() {
123
+ throw new MarAgentError('MAR_AGENT_TOOL_OUTPUT_LIMIT', `Image exceeds the source pixel-area limit (equivalent to ${Math.sqrt(TOOL_EXECUTION_LIMITS.viewImageMaxPixels)}×${Math.sqrt(TOOL_EXECUTION_LIMITS.viewImageMaxPixels)} pixels).`);
124
+ }
125
+ function incompleteImage() {
126
+ throw new MarAgentError('MAR_AGENT_IMAGE_INCOMPLETE', 'Image file is incomplete, malformed, or changed while reading. Wait for the writer to finish and read it again.');
127
+ }
128
+ async function loadSharp() {
129
+ try {
130
+ return (await import('sharp')).default;
131
+ }
132
+ catch {
133
+ throw new MarAgentError('MAR_AGENT_IMAGE_PROCESSING_UNAVAILABLE', 'Native image processing is unavailable because the sharp dependency could not be loaded.');
134
+ }
135
+ }
@@ -289,7 +289,7 @@ function readKey(input) {
289
289
  }
290
290
  function readError(path, error) {
291
291
  if (error instanceof MarAgentError)
292
- return { path, error: { code: error.code, message: error.message } };
292
+ return { path, error: { code: error.code, message: error.publicMessage } };
293
293
  const code = typeof error === 'object' && error !== null && 'code' in error && typeof error.code === 'string'
294
294
  ? error.code
295
295
  : 'MAR_AGENT_TOOL_FAILED';