@myagentroam/agent 0.10.68 → 0.11.1

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.
@@ -880,6 +880,9 @@ export class JsonlSessionStore {
880
880
  }
881
881
  async #appendRecord(sessionId, input) {
882
882
  this.#assertOpen();
883
+ const writer = this.#shared.writerLeases.get(sessionId);
884
+ if (writer?.appendFailure)
885
+ throw writer.appendFailure.error;
883
886
  const before = this.#shared.index.sessions.find((item) => item.id === sessionId) ??
884
887
  this.#shared.pendingSessions.get(sessionId);
885
888
  if (!before)
@@ -913,7 +916,10 @@ export class JsonlSessionStore {
913
916
  // The first append creates the first segment.
914
917
  }
915
918
  try {
916
- await appendRecordBytes(path, serialized);
919
+ await appendRecordBytes(path, serialized, (error) => {
920
+ if (writer)
921
+ writer.appendFailure = { error };
922
+ });
917
923
  }
918
924
  catch (error) {
919
925
  // A failed write/sync/close can leave a complete record on disk.
@@ -1232,6 +1238,8 @@ export class JsonlSessionStore {
1232
1238
  if (lease) {
1233
1239
  if (lease.executing || owner === undefined || lease.owner !== owner)
1234
1240
  throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session already has a writer.');
1241
+ if (lease.appendFailure)
1242
+ throw lease.appendFailure.error;
1235
1243
  lease.executing = true;
1236
1244
  }
1237
1245
  else {
@@ -1261,6 +1269,8 @@ export class JsonlSessionStore {
1261
1269
  let lease = this.#shared.writerLeases.get(sessionId);
1262
1270
  if (lease && lease.owner !== owner)
1263
1271
  throw new MarAgentError('MAR_AGENT_SESSION_LOCKED', 'Session already has a writer.');
1272
+ if (lease?.appendFailure)
1273
+ throw lease.appendFailure.error;
1264
1274
  if (!lease) {
1265
1275
  const release = await this.#acquireSessionLease(sessionId);
1266
1276
  lease = { owner, executing: false, retained: 0, release };
@@ -20,4 +20,4 @@ export declare function readRecordsFromTail(path: string, page?: {
20
20
  readonly positions: Map<string, HistoryPagePositions>;
21
21
  }): AsyncGenerator<SessionRecord>;
22
22
  export declare function segmentNumberFromPath(path: string): number | undefined;
23
- export declare function appendRecordBytes(path: string, serialized: string): Promise<void>;
23
+ export declare function appendRecordBytes(path: string, serialized: string, onWriteFailure: (error: unknown) => void): Promise<void>;
@@ -230,10 +230,11 @@ export function segmentNumberFromPath(path) {
230
230
  const match = /events-(\d{6})\.jsonl$/.exec(path);
231
231
  return match === null ? undefined : Number(match[1]);
232
232
  }
233
- export async function appendRecordBytes(path, serialized) {
233
+ export async function appendRecordBytes(path, serialized, onWriteFailure) {
234
+ const bytes = Buffer.from(serialized);
234
235
  const handle = await open(path, 'a', 0o600);
236
+ let failure;
235
237
  try {
236
- const bytes = Buffer.from(serialized);
237
238
  let offset = 0;
238
239
  while (offset < bytes.length) {
239
240
  const { bytesWritten } = await handle.write(bytes, offset, bytes.length - offset);
@@ -243,7 +244,18 @@ export async function appendRecordBytes(path, serialized) {
243
244
  }
244
245
  await handle.sync();
245
246
  }
246
- finally {
247
+ catch (error) {
248
+ failure = { error };
249
+ }
250
+ try {
247
251
  await handle.close();
248
252
  }
253
+ catch (error) {
254
+ failure ??= { error };
255
+ }
256
+ if (failure) {
257
+ // Opening/encoding failures cannot have appended bytes; write/sync/close failures can.
258
+ onWriteFailure(failure.error);
259
+ throw failure.error;
260
+ }
249
261
  }
@@ -1,6 +1,8 @@
1
1
  import { MarAgentError } from '../error.js';
2
2
  /** Normalize trusted Host policy independently of discovery, Agent scope and parallel safety. */
3
3
  export function toolCallers(tool) {
4
+ if (tool.codeModeExposure !== undefined && tool.codeModeExposure !== 'both')
5
+ throw new MarAgentError('MAR_AGENT_TOOL_INPUT_INVALID', 'Code Mode exposure is invalid.');
4
6
  const value = tool.allowedCallers;
5
7
  if (value === undefined)
6
8
  return tool.name.startsWith('mcp__') ? ['direct', 'programmatic'] : ['direct'];
@@ -31,5 +33,7 @@ export function allowsToolCaller(tool, caller) {
31
33
  /** Code Mode Only changes model exposure, never the Host's underlying caller permissions. */
32
34
  export function allowsDirectTool(tool, codeModeEnabled) {
33
35
  return (allowsToolCaller(tool, 'direct') &&
34
- (!codeModeEnabled || !allowsToolCaller(tool, 'programmatic')));
36
+ (!codeModeEnabled ||
37
+ tool.codeModeExposure === 'both' ||
38
+ !allowsToolCaller(tool, 'programmatic')));
35
39
  }
@@ -21,14 +21,14 @@ import { failedToolResultExtensions } from '../result-extensions.js';
21
21
  export const codeModeToolDefinition = {
22
22
  name: 'code_exec',
23
23
  description: `Run JavaScript to orchestrate or compose tool calls.
24
- - Code Mode Only: call the programmatic tools listed below through tools.NAME inside this tool, including single-tool operations. They are omitted from the top-level tool list. Direct-only tools remain separate model tools and cannot be called here.
24
+ - Code Mode Only by default: call the programmatic tools listed below through tools.NAME, including single-tool operations. Tools explicitly exposed in both places may also be called directly; follow each tool's calling guidance. Direct-only tools remain separate model tools and cannot be called here.
25
25
  - Run independent calls concurrently with Promise.all; await sequentially when data, side effects, or failure handling require ordering.
26
26
  - Complete known, bounded steps and deterministic result processing in code when this avoids a model round trip. Return to the model when interpretation or a new decision is needed; do not invent extra work to fill a cell.
27
27
  - Observe only evidence needed for the next decision or verification: use observe.result for original results, observe.text for derived values, and observe.image for image values. Preserve source and failure details needed for reasoning; keep other intermediate data in the cell.
28
28
  - Single-tool cells are valid. Keep loops and retries bounded, and do not blindly replay operations with side effects.
29
29
  - Evaluates source in a fresh restricted ECMAScript runtime with top-level await and Promise.all. It is not Node.js or a browser. JavaScript globals do not survive between cells; bounded JSON state is available through state.
30
30
  - Accepts JavaScript source text. When exposed as a freeform tool, send the source directly; when exposed with a JSON input schema, put the source in its code field. Do not add another JSON wrapper, quoted string, or markdown code fence inside the source.
31
- - Keep embedded commands, patches and source in JSON-escaped double-quoted strings when they contain backticks or literal \${...}; JavaScript template literals would interpolate them before the tool runs. Do not paste Python literals such as None into JavaScript expressions.
31
+ - Keep embedded commands, patches and source in JSON-escaped double-quoted strings when they contain backticks or literal \${...}; escape newlines as \\n, never put literal line breaks inside single- or double-quoted JS strings. JavaScript template literals would interpolate their contents before the tool runs. Do not paste Python literals such as None into JavaScript expressions.
32
32
  - Use only standard ECMAScript built-ins and the explicitly documented tools, observe, state, catalog and cell namespaces. Do not use Node.js, browser or Web APIs. Module imports, filesystem and network APIs are unavailable. Obtain external capabilities only through available tools. Use tools.exec to start managed processes when permitted; use tools.exec_wait afterward to wait, write stdin or cancel.
33
33
 
34
34
  Tool calls:
@@ -41,7 +41,7 @@ Built-in namespaces:
41
41
  - observe.result(result): void registers an unmodified complete tools.NAME result or caught tool error as a trusted tool_result, using the original text and all images without repeating program data. Non-original, copied or modified values are rejected. This selects evidence for the next model response, not a log and not a new model call. Prefer observe.result for inspecting tool results.
42
42
  - observe.text(value): void registers text/JSON values. observe.image(value): void registers one image value when image input is supported. Both accept original tool results for compatibility and use the same complete tool_result observation path; they do not select part of an original result. Extracted single images, copies and computed summaries are value observations, never trusted tool results. Observations are returned in registration order. Only explicitly observed evidence reaches the model; registration does not prove inspection. Sources and types are runtime-assigned. No out or callable observe(value) alias exists.
43
43
  - state.get(key), state.has(key), state.set(key, value), state.delete(key) access bounded session JSON state. Mutations commit atomically only when the cell succeeds; failed or cancelled cells discard them.
44
- - catalog.tools() returns a read-only snapshot of the tools callable in this cell, including name, description and inputSchema. It does not discover or authorize additional tools.
44
+ - catalog.tools() returns a read-only array of the tools callable in this cell, including name, description and inputSchema. It does not discover or authorize additional tools.
45
45
  - await cell.sleep(ms) performs a cancellable delay up to ${limits.codeSleepMaxMs} ms.
46
46
  - await cell.yield_control() returns accumulated output to the model immediately while this cell continues running.
47
47
  - Optionally start source with // @options: {"yieldMs": ${limits.codeDefaultYieldMs}}. yieldMs defaults to ${limits.codeDefaultYieldMs} ms and controls this call's wait window (0..${limits.codeMaxYieldMs}), not nested tool timeouts or wait windows. A running result includes cellId; use direct code_exec_wait for new output, completion or termination.
@@ -106,7 +106,7 @@ export const codeExecWaitToolDefinition = {
106
106
  function projectProgrammaticTool(tool) {
107
107
  return {
108
108
  name: tool.name,
109
- description: `Call only as tools.${tool.name} inside code_exec, never as a direct model tool.\nPass a JavaScript value matching inputSchema.\n${tool.description}`,
109
+ description: `${allowsDirectTool(tool, true) ? `Call directly or as tools.${tool.name} inside code_exec.` : `Call only as tools.${tool.name} inside code_exec, never as a direct model tool.`}\nInside code_exec, pass a JavaScript value matching inputSchema.\n${tool.description}`,
110
110
  inputSchema: stableToolSchema(tool.inputSchema)
111
111
  };
112
112
  }
@@ -121,14 +121,14 @@ export function projectCodeModeTools(tools, residentTools = tools) {
121
121
  const programmatic = orderToolDefinitions(residentTools.filter((tool) => allowsToolCaller(tool, 'programmatic')));
122
122
  const directory = JSON.stringify({
123
123
  callableNames: programmatic.map((tool) => tool.name),
124
- programmaticOnly: programmatic.map(projectProgrammaticTool)
124
+ tools: programmatic.map(projectProgrammaticTool)
125
125
  });
126
126
  if (Buffer.byteLength(directory) > 32 * 1024)
127
127
  throw new MarAgentError('MAR_AGENT_TOOL_EXPOSURE_LIMIT', 'Programmatic tool directory is too large.');
128
128
  return direct.map((tool) => tool.name === 'code_exec'
129
129
  ? {
130
130
  ...tool,
131
- description: `${codeModeToolDefinition.description}\nDeferred MCP definitions are returned by direct tool_search and become callable from the next model response, not later in the same response; they are not appended to this description.\nProgrammatic tools (call only inside code_exec): ${directory}`
131
+ description: `${codeModeToolDefinition.description}\nDeferred MCP definitions are returned by direct tool_search and become callable from the next model response, not later in the same response; they are not appended to this description.\nProgrammatic tools: ${directory}`
132
132
  }
133
133
  : tool);
134
134
  }
@@ -145,14 +145,23 @@ export function projectCodeModeSearchResult(message, codeModeEnabled) {
145
145
  };
146
146
  const tools = message.toolSearchTools.map((tool) => ({
147
147
  ...(allowsToolCaller(tool, 'programmatic')
148
- ? projectProgrammaticTool(tool)
148
+ ? {
149
+ ...projectProgrammaticTool(tool),
150
+ ...(allowsDirectTool(tool, true)
151
+ ? { inputMode: tool.inputMode ?? 'json' }
152
+ : {})
153
+ }
149
154
  : {
150
155
  name: tool.name,
151
156
  description: tool.description,
152
157
  inputSchema: stableToolSchema(tool.inputSchema),
153
158
  inputMode: tool.inputMode ?? 'json'
154
159
  }),
155
- caller: allowsToolCaller(tool, 'programmatic') ? 'programmatic' : 'direct'
160
+ caller: allowsToolCaller(tool, 'programmatic')
161
+ ? allowsDirectTool(tool, true)
162
+ ? 'both'
163
+ : 'programmatic'
164
+ : 'direct'
156
165
  }));
157
166
  return {
158
167
  ...message,
@@ -10,6 +10,8 @@ export interface ClientToolDefinition {
10
10
  outputSchema?: Record<string, unknown>;
11
11
  /** Runtime-only policy. MCP defaults to both callers; other tools default to direct only. */
12
12
  allowedCallers?: readonly ToolCaller[];
13
+ /** Runtime-only exposure override; retains both permitted entry points with Code Mode enabled. */
14
+ codeModeExposure?: 'both';
13
15
  inputMode?: 'json' | 'freeform';
14
16
  parallelSafety?: 'safe' | 'serial';
15
17
  /** Preserved business-schema eligibility after framework metadata projection. */
@@ -9,6 +9,7 @@ import { parsePatch } from './apply-patch-parser.js';
9
9
  export const applyPatchToolDefinition = {
10
10
  name: 'apply_patch',
11
11
  allowedCallers: ['direct', 'programmatic'],
12
+ codeModeExposure: 'both',
12
13
  parallelSafety: 'serial',
13
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
15
  inputMode: 'freeform',
@@ -150,6 +150,7 @@ export function assertMcpToolDefinition(value, options = {}) {
150
150
  (!Array.isArray(value.allowedCallers) ||
151
151
  !value.allowedCallers.length ||
152
152
  value.allowedCallers.some((caller) => caller !== 'direct' && caller !== 'programmatic'))) ||
153
+ (value.codeModeExposure !== undefined && value.codeModeExposure !== 'both') ||
153
154
  (value.inputMode !== undefined && value.inputMode !== 'json'))
154
155
  invalid();
155
156
  assertJson(value);
@@ -14,7 +14,7 @@ export const TOOL_SEARCH_LIMITS = Object.freeze({
14
14
  });
15
15
  export const toolSearchToolDefinition = {
16
16
  name: 'tool_search',
17
- description: 'Search deferred MCP metadata and return matching tool definitions for the next model call. Use the full callable name for exact selection, or keywords for BM25 search. Never executes the discovered tools. With Code Mode Only, programmatic tools must be called through code_exec, not directly. Already exposed tools need not be searched again. The selected definitions must fit the exposure budget or the search fails atomically.',
17
+ description: 'Search deferred MCP metadata and return matching tool definitions for the next model call. Use the full callable name for exact selection, or keywords for BM25 search. Never executes the discovered tools. With Code Mode Only, programmatic tools use code_exec unless explicitly exposed for both direct and programmatic calls. Already exposed tools need not be searched again. The selected definitions must fit the exposure budget or the search fails atomically.',
18
18
  inputSchema: {
19
19
  type: 'object',
20
20
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.10.68",
3
+ "version": "0.11.1",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",