@myagentroam/agent 0.9.135 → 0.9.136

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 (69) hide show
  1. package/dist/cli/main.js +1 -1
  2. package/dist/host/local-agent-host.d.ts +9 -7
  3. package/dist/host/local-agent-host.js +77 -5
  4. package/dist/host/local-process-executor.d.ts +2 -0
  5. package/dist/index.d.ts +4 -2
  6. package/dist/index.js +1 -0
  7. package/dist/model/catalog-file.d.ts +2 -0
  8. package/dist/model/configuration.d.ts +5 -0
  9. package/dist/model/configuration.js +8 -0
  10. package/dist/model/contracts.d.ts +7 -0
  11. package/dist/model/openai-responses.d.ts +1 -1
  12. package/dist/model/openai-responses.js +85 -20
  13. package/dist/prompts/workflow.js +2 -0
  14. package/dist/runtime/context-gc-runtime.d.ts +1 -0
  15. package/dist/runtime/context-gc-runtime.js +3 -0
  16. package/dist/runtime/context-gc-tokens.d.ts +1 -0
  17. package/dist/runtime/context-gc-tokens.js +1 -1
  18. package/dist/runtime/context-gc.d.ts +1 -0
  19. package/dist/runtime/context-gc.js +85 -8
  20. package/dist/runtime/context-projection.js +19 -2
  21. package/dist/sandbox/linux/bubblewrap-sandbox-executor.js +29 -2
  22. package/dist/sdk/agent.js +208 -25
  23. package/dist/sdk/mcp-catalog.d.ts +13 -0
  24. package/dist/sdk/mcp-catalog.js +219 -0
  25. package/dist/sdk/types.d.ts +50 -1
  26. package/dist/session/context-gc-checkpoint.d.ts +7 -1
  27. package/dist/session/context-gc-checkpoint.js +28 -2
  28. package/dist/session/context-gc-replacements.d.ts +7 -0
  29. package/dist/session/context-gc-replacements.js +26 -0
  30. package/dist/session/jsonl-store.d.ts +6 -0
  31. package/dist/session/jsonl-store.js +92 -54
  32. package/dist/subagent/scheduler.d.ts +3 -3
  33. package/dist/subagent/session-controller.d.ts +1 -1
  34. package/dist/tools/agent-cancel.js +1 -0
  35. package/dist/tools/agent-message.js +1 -0
  36. package/dist/tools/agent-start.js +1 -0
  37. package/dist/tools/agent-wait.js +1 -0
  38. package/dist/tools/apply-patch.js +2 -1
  39. package/dist/tools/callers.d.ts +4 -0
  40. package/dist/tools/callers.js +18 -0
  41. package/dist/tools/code-mode/engine.d.ts +43 -0
  42. package/dist/tools/code-mode/engine.js +1 -0
  43. package/dist/tools/code-mode/quickjs-engine.d.ts +2 -0
  44. package/dist/tools/code-mode/quickjs-engine.js +454 -0
  45. package/dist/tools/code-mode/state.d.ts +20 -0
  46. package/dist/tools/code-mode/state.js +82 -0
  47. package/dist/tools/code-mode/worker.d.ts +1 -0
  48. package/dist/tools/code-mode/worker.js +65 -0
  49. package/dist/tools/code-mode.d.ts +5 -3
  50. package/dist/tools/code-mode.js +129 -120
  51. package/dist/tools/exec.js +1 -0
  52. package/dist/tools/execution-limits.d.ts +8 -0
  53. package/dist/tools/execution-limits.js +8 -0
  54. package/dist/tools/local-registry.d.ts +2 -0
  55. package/dist/tools/local-registry.js +1 -1
  56. package/dist/tools/question.js +1 -0
  57. package/dist/tools/read.js +1 -0
  58. package/dist/tools/registry.js +3 -0
  59. package/dist/tools/runtime-context.d.ts +2 -0
  60. package/dist/tools/runtime.js +3 -1
  61. package/dist/tools/skill.js +1 -0
  62. package/dist/tools/todo-read.js +1 -0
  63. package/dist/tools/todo-write.js +1 -0
  64. package/dist/tools/tool-search.d.ts +43 -0
  65. package/dist/tools/tool-search.js +278 -0
  66. package/dist/tools/view-image.js +1 -0
  67. package/dist/tools/web-fetch.js +1 -0
  68. package/dist/tools/web-search.js +1 -0
  69. package/package.json +3 -1
@@ -0,0 +1,219 @@
1
+ import { MarAgentError } from '../error.js';
2
+ export const MCP_DEFINITION_LIMITS = Object.freeze({
3
+ depth: 64,
4
+ nodes: 32768,
5
+ definitionBytes: 256 * 1024,
6
+ catalogBytes: 16 * 1024 * 1024,
7
+ tools: 10000,
8
+ servers: 256
9
+ });
10
+ function invalid() {
11
+ throw new MarAgentError('MAR_AGENT_MCP_CATALOG_INVALID', 'MCP catalog contains invalid or oversized definitions.');
12
+ }
13
+ const record = (value) => value !== null &&
14
+ typeof value === 'object' &&
15
+ !Array.isArray(value) &&
16
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
17
+ const text = (value, maximum) => typeof value === 'string' && value.length > 0 && value.length <= maximum;
18
+ const onlyKeys = (value, keys) => Object.keys(value).every((key) => keys.includes(key));
19
+ /** Bound all JSON, including unknown extension keywords, before recursive lowering. */
20
+ function assertJson(value) {
21
+ const stack = [{ value, depth: 0 }];
22
+ const seen = new Set();
23
+ let nodes = 0;
24
+ while (stack.length) {
25
+ const current = stack.pop();
26
+ if (current.exit) {
27
+ seen.delete(current.value);
28
+ continue;
29
+ }
30
+ if (++nodes > MCP_DEFINITION_LIMITS.nodes || current.depth > MCP_DEFINITION_LIMITS.depth)
31
+ invalid();
32
+ const item = current.value;
33
+ if (item === null ||
34
+ typeof item === 'string' ||
35
+ typeof item === 'boolean' ||
36
+ (typeof item === 'number' && Number.isFinite(item)))
37
+ continue;
38
+ if (!Array.isArray(item) && !record(item))
39
+ invalid();
40
+ if (seen.has(item))
41
+ invalid();
42
+ seen.add(item);
43
+ stack.push({ value: item, depth: current.depth, exit: true });
44
+ for (const child of Object.values(item))
45
+ stack.push({ value: child, depth: current.depth + 1 });
46
+ }
47
+ }
48
+ function assertSchema(schema) {
49
+ if (!record(schema) || schema.type !== 'object')
50
+ invalid();
51
+ assertJson(schema);
52
+ const stack = [schema];
53
+ const types = new Set(['null', 'boolean', 'object', 'array', 'number', 'integer', 'string']);
54
+ while (stack.length) {
55
+ const value = stack.pop();
56
+ if (typeof value === 'boolean')
57
+ continue;
58
+ if (!record(value))
59
+ invalid();
60
+ if (value.type !== undefined) {
61
+ const entries = Array.isArray(value.type) ? value.type : [value.type];
62
+ if (!entries.length ||
63
+ new Set(entries).size !== entries.length ||
64
+ entries.some((type) => typeof type !== 'string' || !types.has(type)))
65
+ invalid();
66
+ }
67
+ if (value.required !== undefined &&
68
+ (!Array.isArray(value.required) ||
69
+ value.required.some((name) => typeof name !== 'string') ||
70
+ new Set(value.required).size !== value.required.length))
71
+ invalid();
72
+ for (const key of [
73
+ 'properties',
74
+ 'patternProperties',
75
+ '$defs',
76
+ 'definitions',
77
+ 'dependentSchemas'
78
+ ]) {
79
+ const children = value[key];
80
+ if (children === undefined)
81
+ continue;
82
+ if (!record(children))
83
+ invalid();
84
+ stack.push(...Object.values(children));
85
+ }
86
+ for (const key of ['allOf', 'anyOf', 'oneOf', 'prefixItems']) {
87
+ const children = value[key];
88
+ if (children === undefined)
89
+ continue;
90
+ if (!Array.isArray(children) || !children.length)
91
+ invalid();
92
+ stack.push(...children);
93
+ }
94
+ for (const key of [
95
+ 'additionalProperties',
96
+ 'unevaluatedProperties',
97
+ 'additionalItems',
98
+ 'unevaluatedItems',
99
+ 'contains',
100
+ 'propertyNames',
101
+ 'not',
102
+ 'if',
103
+ 'then',
104
+ 'else',
105
+ 'items'
106
+ ]) {
107
+ const child = value[key];
108
+ if (child !== undefined) {
109
+ if (key === 'items' && Array.isArray(child))
110
+ stack.push(...child);
111
+ else
112
+ stack.push(child);
113
+ }
114
+ }
115
+ for (const key of ['$ref', '$id', '$schema', 'title', 'description', 'format', 'pattern'])
116
+ if (value[key] !== undefined && typeof value[key] !== 'string')
117
+ invalid();
118
+ for (const key of ['minimum', 'maximum', 'exclusiveMinimum', 'exclusiveMaximum', 'multipleOf'])
119
+ if (value[key] !== undefined &&
120
+ (typeof value[key] !== 'number' ||
121
+ !Number.isFinite(value[key]) ||
122
+ (key === 'multipleOf' && Number(value[key]) <= 0)))
123
+ invalid();
124
+ for (const key of [
125
+ 'minLength',
126
+ 'maxLength',
127
+ 'minItems',
128
+ 'maxItems',
129
+ 'minProperties',
130
+ 'maxProperties'
131
+ ])
132
+ if (value[key] !== undefined && (!Number.isSafeInteger(value[key]) || Number(value[key]) < 0))
133
+ invalid();
134
+ if (value.enum !== undefined && (!Array.isArray(value.enum) || !value.enum.length))
135
+ invalid();
136
+ }
137
+ }
138
+ /** Shared Host and Node publication boundary, not a provider-specific conversion. */
139
+ export function assertMcpToolDefinition(value) {
140
+ if (!record(value) ||
141
+ !text(value.name, 256) ||
142
+ !/^mcp__[A-Za-z0-9_-]+__[A-Za-z0-9_-]+$/u.test(value.name) ||
143
+ typeof value.description !== 'string' ||
144
+ value.description.length > 65536 ||
145
+ (value.parallelSafety !== undefined &&
146
+ !['safe', 'serial'].includes(String(value.parallelSafety))) ||
147
+ (value.agentScope !== undefined &&
148
+ !['ROOT_AND_SUBAGENTS', 'ROOT_ONLY'].includes(String(value.agentScope))) ||
149
+ (value.allowedCallers !== undefined &&
150
+ (!Array.isArray(value.allowedCallers) ||
151
+ !value.allowedCallers.length ||
152
+ value.allowedCallers.some((caller) => caller !== 'direct' && caller !== 'programmatic'))) ||
153
+ (value.inputMode !== undefined && value.inputMode !== 'json'))
154
+ invalid();
155
+ assertJson(value);
156
+ assertSchema(value.inputSchema);
157
+ if (value.outputSchema !== undefined)
158
+ assertSchema(value.outputSchema);
159
+ if (Buffer.byteLength(JSON.stringify(value)) > MCP_DEFINITION_LIMITS.definitionBytes)
160
+ invalid();
161
+ }
162
+ export function assertMcpToolCatalog(value) {
163
+ if (!record(value) ||
164
+ typeof value.revision !== 'string' ||
165
+ value.revision.length > 512 ||
166
+ !onlyKeys(value, ['revision', 'servers', 'tools']) ||
167
+ !Array.isArray(value.servers) ||
168
+ value.servers.length > MCP_DEFINITION_LIMITS.servers ||
169
+ !Array.isArray(value.tools) ||
170
+ value.tools.length > MCP_DEFINITION_LIMITS.tools)
171
+ invalid();
172
+ const servers = new Set();
173
+ for (const server of value.servers) {
174
+ if (!record(server) ||
175
+ !text(server.id, 512) ||
176
+ !text(server.name, 256) ||
177
+ servers.has(server.id) ||
178
+ !onlyKeys(server, ['id', 'name', 'description', 'instructions']) ||
179
+ (server.description !== undefined &&
180
+ (typeof server.description !== 'string' || server.description.length > 65536)) ||
181
+ (server.instructions !== undefined &&
182
+ (typeof server.instructions !== 'string' || server.instructions.length > 65536)))
183
+ invalid();
184
+ servers.add(server.id);
185
+ }
186
+ const names = new Set();
187
+ for (const tool of value.tools) {
188
+ if (!record(tool) ||
189
+ !text(tool.serverId, 512) ||
190
+ !servers.has(tool.serverId) ||
191
+ !onlyKeys(tool, [
192
+ 'definition',
193
+ 'serverId',
194
+ 'rawName',
195
+ 'sourceIdentity',
196
+ 'definitionRevision',
197
+ 'lifetime',
198
+ 'exposure',
199
+ 'annotations'
200
+ ]) ||
201
+ !text(tool.rawName, 256) ||
202
+ !text(tool.sourceIdentity, 512) ||
203
+ !text(tool.definitionRevision, 512) ||
204
+ !['session', 'execution'].includes(String(tool.lifetime)) ||
205
+ !['direct', 'deferred'].includes(String(tool.exposure)))
206
+ invalid();
207
+ assertMcpToolDefinition(tool.definition);
208
+ if (tool.annotations !== undefined) {
209
+ if (!record(tool.annotations))
210
+ invalid();
211
+ assertJson(tool.annotations);
212
+ }
213
+ if (names.has(tool.definition.name))
214
+ invalid();
215
+ names.add(tool.definition.name);
216
+ }
217
+ if (Buffer.byteLength(JSON.stringify(value)) > MCP_DEFINITION_LIMITS.catalogBytes)
218
+ invalid();
219
+ }
@@ -1,7 +1,9 @@
1
1
  import type { HostDescription } from '../host/contracts.js';
2
2
  import type { MarAgentModelConfiguration, MarAgentReasoningEffort } from '../model/configuration.js';
3
+ import type { ClientToolDefinition } from '../model/contracts.js';
3
4
  import type { MarAgentSessionPage, MarAgentSessionSummary } from '../session/catalog.js';
4
5
  import type { SessionListInput, SessionSource } from '../session/types.js';
6
+ import type { CodeModeStateEntry, CodeModeStateMutation } from '../tools/code-mode/state.js';
5
7
  /** @deprecated MAR Agent always executes tools directly through the Host. */
6
8
  export type AccessMode = 'full-access';
7
9
  export type ExecutionMode = 'run' | 'plan' | 'compact';
@@ -22,6 +24,8 @@ export interface ToolExecutionContext {
22
24
  codeMode?: {
23
25
  supportsImages?: boolean;
24
26
  tools: readonly import('../model/contracts.js').ClientToolDefinition[];
27
+ state?: readonly CodeModeStateEntry[];
28
+ commitState?(mutations: readonly CodeModeStateMutation[]): Promise<readonly CodeModeStateEntry[]>;
25
29
  invoke(call: ToolCall, signal: AbortSignal): Promise<ToolExecutionResult>;
26
30
  };
27
31
  }
@@ -29,6 +33,34 @@ export interface McpToolCatalogContext extends Pick<ToolExecutionContext, 'sessi
29
33
  executionScopeId?: string;
30
34
  claimExecutionScope?: boolean;
31
35
  }
36
+ export interface McpToolCatalog {
37
+ readonly revision: string;
38
+ readonly servers: readonly {
39
+ readonly id: string;
40
+ readonly name: string;
41
+ readonly description?: string;
42
+ readonly instructions?: string;
43
+ }[];
44
+ readonly tools: readonly McpCatalogTool[];
45
+ }
46
+ export interface McpCatalogTool {
47
+ readonly definition: import('../model/contracts.js').ClientToolDefinition;
48
+ readonly serverId: string;
49
+ readonly rawName: string;
50
+ readonly sourceIdentity: string;
51
+ readonly definitionRevision: string;
52
+ readonly lifetime: 'session' | 'execution';
53
+ readonly exposure: 'direct' | 'deferred';
54
+ /** Untrusted MCP hints; never an authorization or parallel-safety decision. */
55
+ readonly annotations?: Readonly<Record<string, unknown>>;
56
+ }
57
+ export interface McpToolCallContext extends Pick<ToolExecutionContext, 'sessionId' | 'executionId'> {
58
+ readonly mode?: ExecutionMode;
59
+ readonly expectedTool?: {
60
+ readonly sourceIdentity: string;
61
+ readonly definitionRevision: string;
62
+ };
63
+ }
32
64
  export interface HostSkill {
33
65
  readonly name: string;
34
66
  readonly description: string;
@@ -51,6 +83,8 @@ export type MarAgentToolArtifact = MarAgentToolImageArtifact;
51
83
  export interface ToolExecutionResult {
52
84
  content: string;
53
85
  data?: unknown;
86
+ /** Runtime-only definitions used to encode a Tool Search output. */
87
+ toolSearchTools?: readonly ClientToolDefinition[];
54
88
  truncated?: boolean;
55
89
  images?: readonly {
56
90
  mimeType: string;
@@ -78,7 +112,8 @@ export interface MarAgentHost {
78
112
  questions: unknown[];
79
113
  }, signal: AbortSignal): Promise<Record<string, unknown>>;
80
114
  listMcpTools?(context?: McpToolCatalogContext): Promise<readonly import('../model/contracts.js').ClientToolDefinition[]>;
81
- callMcpTool?(call: ToolCall, signal: AbortSignal, context?: Pick<ToolExecutionContext, 'sessionId' | 'executionId'>): Promise<ToolExecutionResult>;
115
+ listMcpToolCatalog?(context: McpToolCatalogContext): Promise<McpToolCatalog>;
116
+ callMcpTool?(call: ToolCall, signal: AbortSignal, context?: McpToolCallContext): Promise<ToolExecutionResult>;
82
117
  listSessionResources?(sessionId: string): Promise<HostSessionResourceSnapshot>;
83
118
  disposeSessionResources?(sessionId: string): Promise<void>;
84
119
  dispose(): Promise<void>;
@@ -106,6 +141,20 @@ export type MarAgentEvent = MarAgentEventBase & ({
106
141
  text: string;
107
142
  modelId: string;
108
143
  modelName: string;
144
+ } | {
145
+ type: 'model.performance';
146
+ ttftMs: number;
147
+ outputTokens?: number | undefined;
148
+ outputDurationMs?: number | undefined;
149
+ tokensPerSecond?: number | undefined;
150
+ } | {
151
+ /** Legacy history event; new executions publish model.performance instead. */
152
+ type: 'message.performance';
153
+ itemId: string;
154
+ ttftMs: number;
155
+ outputTokens?: number | undefined;
156
+ outputDurationMs?: number | undefined;
157
+ tokensPerSecond?: number | undefined;
109
158
  } | {
110
159
  type: 'plan.completed';
111
160
  itemId: string;
@@ -50,7 +50,7 @@ export type ContextGcReplacement = {
50
50
  readonly evidenceSequences: readonly number[];
51
51
  } | {
52
52
  readonly kind: 'TOOL_CALL_INPUT';
53
- readonly toolName: 'apply_patch' | 'exec';
53
+ readonly toolName: 'apply_patch' | 'exec' | 'code_mode';
54
54
  readonly callId: string;
55
55
  readonly target: ContextGcTarget;
56
56
  readonly replacementArguments: unknown;
@@ -62,6 +62,12 @@ export type ContextGcReplacement = {
62
62
  readonly target: ContextGcTarget;
63
63
  readonly replacementContent: string;
64
64
  readonly evidenceSequence: number;
65
+ } | {
66
+ readonly kind: 'CODE_MODE_OUTPUT';
67
+ readonly callId: string;
68
+ readonly target: ContextGcTarget;
69
+ readonly replacementContent: string;
70
+ readonly evidenceSequence: number;
65
71
  } | {
66
72
  readonly kind: 'SUBAGENT_OUTPUT';
67
73
  readonly toolName: 'agent_wait' | 'agent_message' | 'agent_cancel';
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { posix, win32 } from 'node:path';
3
3
  import { isDeepStrictEqual } from 'node:util';
4
4
  import { parseReadContextOutput } from '../tools/read-context.js';
5
- import { contextGcApplyPatchInputReplacement, contextGcWebOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcStaleReadReplacement, normalizeContextGcExecResult } from './context-gc-replacements.js';
5
+ import { contextGcApplyPatchInputReplacement, contextGcCodeModeInputReplacement, contextGcCodeModeOutputReplacement, contextGcExecInputReplacement, contextGcExecOutputReplacement, contextGcPatchResultFromCompletedPayload, contextGcStaleReadReplacement, contextGcWebOutputReplacement, normalizeContextGcCodeModeInput, normalizeContextGcExecResult } from './context-gc-replacements.js';
6
6
  import { contextGcSubagentNeedsRetainedPriorFinal, contextGcSubagentOutputReplacement, contextGcSubagentSnapshotIsPreciselyCovered, contextGcSubagentSnapshotIsSuperseded, normalizeContextGcSubagentSnapshot } from './context-gc-subagent.js';
7
7
  export const CONTEXT_GC_CHECKPOINT_VERSION = 1;
8
8
  export const CONTEXT_GC_MAX_TARGETS = 4_096;
@@ -266,6 +266,25 @@ function replacementMatchesRecords(replacement, records, recordsBySequence, work
266
266
  result !== undefined &&
267
267
  isDeepStrictEqual(replacement.replacementArguments, contextGcApplyPatchInputReplacement(source.payload.arguments, result, replacement.evidenceSequence, applyPatchReplacementFormatVersion(replacement.replacementArguments))));
268
268
  }
269
+ if (replacement.kind === 'TOOL_CALL_INPUT' && replacement.toolName === 'code_mode') {
270
+ const evidence = successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, 'code_mode');
271
+ const input = normalizeContextGcCodeModeInput(source.payload.arguments);
272
+ return (evidence?.callId === replacement.callId &&
273
+ evidence.call.sequence === replacement.target.sequence &&
274
+ input !== undefined &&
275
+ isDeepStrictEqual(replacement.replacementArguments, contextGcCodeModeInputReplacement(input)));
276
+ }
277
+ if (replacement.kind === 'CODE_MODE_OUTPUT') {
278
+ const evidence = successfulToolEvidence(records, recordsBySequence, replacement.evidenceSequence, 'code_mode');
279
+ return (evidence?.callId === replacement.callId &&
280
+ evidence.result.sequence === replacement.target.sequence &&
281
+ isRecord(evidence.result.payload) &&
282
+ typeof evidence.result.payload.content === 'string' &&
283
+ isRecord(evidence.completed.payload) &&
284
+ typeof evidence.completed.payload.truncated === 'boolean' &&
285
+ replacement.replacementContent ===
286
+ contextGcCodeModeOutputReplacement(evidence.result.payload.content, evidence.completed.payload.truncated));
287
+ }
269
288
  if (replacement.kind === 'SUBAGENT_OUTPUT') {
270
289
  const retainedPriorFinal = replacement.retainedPriorFinal;
271
290
  const sourceEvidence = successfulToolEvidence(records, recordsBySequence, source.sequence, replacement.toolName);
@@ -605,7 +624,9 @@ function isContextGcReplacement(value) {
605
624
  'evidenceSequence'
606
625
  ]) &&
607
626
  value.target.role === 'tool_call' &&
608
- (value.toolName === 'apply_patch' || value.toolName === 'exec') &&
627
+ (value.toolName === 'apply_patch' ||
628
+ value.toolName === 'exec' ||
629
+ value.toolName === 'code_mode') &&
609
630
  value.target.toolName === value.toolName &&
610
631
  Array.isArray(value.providerAliasTargets) &&
611
632
  value.providerAliasTargets.every((target) => isContextGcTarget(target) &&
@@ -661,6 +682,11 @@ function isContextGcReplacement(value) {
661
682
  (value.target.toolName === undefined || value.target.toolName === value.toolName) &&
662
683
  isNonemptyString(value.replacementContent) &&
663
684
  isPositiveInteger(value.evidenceSequence));
685
+ if (value.kind === 'CODE_MODE_OUTPUT')
686
+ return (hasOnlyKeys(value, ['kind', 'callId', 'target', 'replacementContent', 'evidenceSequence']) &&
687
+ value.target.role === 'tool' &&
688
+ isNonemptyString(value.replacementContent) &&
689
+ isPositiveInteger(value.evidenceSequence));
664
690
  return (value.kind === 'EXEC_OUTPUT' &&
665
691
  hasOnlyKeys(value, ['kind', 'callId', 'target', 'replacementContent', 'evidenceSequence']) &&
666
692
  value.target.role === 'tool' &&
@@ -1,4 +1,11 @@
1
1
  export type ContextGcWebToolName = 'web_search' | 'web_fetch';
2
+ export interface ContextGcCodeModeInput {
3
+ readonly code: string;
4
+ readonly inputMode: 'freeform' | 'json';
5
+ }
6
+ export declare function normalizeContextGcCodeModeInput(arguments_: unknown): ContextGcCodeModeInput | undefined;
7
+ export declare function contextGcCodeModeInputReplacement(input: ContextGcCodeModeInput): unknown;
8
+ export declare function contextGcCodeModeOutputReplacement(originalContent: string, truncated: boolean): string;
2
9
  export interface ContextGcPatchFile {
3
10
  readonly path: string;
4
11
  readonly targetPath?: string;
@@ -3,6 +3,32 @@ const RAW_MODEL_CONTEXT_PREVIEW_CHARACTERS = 256;
3
3
  const EXEC_INPUT_COMPLETE_CHARACTERS = 512;
4
4
  const EXEC_INPUT_PREVIEW_CHARACTERS = 256;
5
5
  const EXEC_OUTPUT_PREVIEW_CHARACTERS = 512;
6
+ const CODE_MODE_OUTPUT_PREVIEW_CHARACTERS = 512;
7
+ export function normalizeContextGcCodeModeInput(arguments_) {
8
+ if (typeof arguments_ === 'string' && arguments_.length > 0)
9
+ return { code: arguments_, inputMode: 'freeform' };
10
+ if (isRecord(arguments_) &&
11
+ Object.keys(arguments_).length === 1 &&
12
+ typeof arguments_.code === 'string' &&
13
+ arguments_.code.length > 0)
14
+ return { code: arguments_.code, inputMode: 'json' };
15
+ return undefined;
16
+ }
17
+ export function contextGcCodeModeInputReplacement(input) {
18
+ const replacement = `<context_gc kind="completed_code_mode_input" originalBytes="${Buffer.byteLength(input.code)}" lines="${lineCount(input.code)}" inputMode="${input.inputMode}">Completed Code Mode source was reclaimed after six successful rollouts. The paired output, images, completion evidence, and nested tool side effects remain in history.</context_gc>`;
19
+ return input.inputMode === 'freeform' ? replacement : { code: replacement };
20
+ }
21
+ export function contextGcCodeModeOutputReplacement(originalContent, truncated) {
22
+ const preview = unicodeHeadTail(originalContent, CODE_MODE_OUTPUT_PREVIEW_CHARACTERS);
23
+ return JSON.stringify({
24
+ originalBytes: Buffer.byteLength(originalContent),
25
+ lines: lineCount(originalContent),
26
+ truncated,
27
+ preview,
28
+ previewComplete: !truncated && preview === originalContent,
29
+ contextGc: 'Detailed Code Mode output was reclaimed after six successful rollouts. An incomplete preview is not the complete cell output. The source was not replayed and nested tool side effects were not changed.'
30
+ });
31
+ }
6
32
  export function contextGcPatchResultFromCompletedPayload(payload) {
7
33
  if (!isRecord(payload) || payload.type !== 'tool.completed')
8
34
  return undefined;
@@ -1,5 +1,6 @@
1
1
  import type { ModelUsage } from '../model/contracts.js';
2
2
  import type { EnvironmentContextState } from '../prompts/core.js';
3
+ import { type CodeModeStateEntry } from '../tools/code-mode/state.js';
3
4
  import { type TodoSnapshot } from '../tools/todo.js';
4
5
  import { type ContextGcCheckpointDraft } from './context-gc-checkpoint.js';
5
6
  import type { SessionListInput, SessionSource } from './types.js';
@@ -146,6 +147,11 @@ export declare class JsonlSessionStore {
146
147
  timestamp?: string;
147
148
  }): Promise<SessionRecord>;
148
149
  read(sessionId: string): Promise<SessionRecord[]>;
150
+ readCodeModeState(sessionId: string): Promise<CodeModeStateEntry[]>;
151
+ commitCodeModeState(sessionId: string, mutations: unknown, input: {
152
+ readonly executionId: string;
153
+ readonly turnId: string;
154
+ }): Promise<CodeModeStateEntry[]>;
149
155
  readExecutionState(sessionId: string): Promise<SessionExecutionState>;
150
156
  readContext(sessionId: string): Promise<SessionRecord[]>;
151
157
  contextGcProjectionHash(sessionId: string, boundarySequence: number): Promise<string>;
@@ -5,6 +5,7 @@ import { basename, join } from 'node:path';
5
5
  import { setTimeout as delay } from 'node:timers/promises';
6
6
  import { isDeepStrictEqual } from 'node:util';
7
7
  import { MarAgentError } from '../error.js';
8
+ import { applyCodeModeStateMutations, parseCodeModeStateMutations, restoreCodeModeState } from '../tools/code-mode/state.js';
8
9
  import { todoItemsFromSnapshot } from '../tools/todo.js';
9
10
  import { contextGcBaseFromRecords, contextGcBaseSequence, contextGcMinimumUsageGrowth, contextGcSourceProjectionHash, latestValidContextGcCheckpoint, normalizeContextGcReplacements, parseContextGcCheckpoint, sameContextGcBase, validateContextGcCheckpoint } from './context-gc-checkpoint.js';
10
11
  import { beginSessionStorageTransaction, isUnjournaledTransactionFile, recoverSessionStorageTransaction } from './transaction-recovery.js';
@@ -521,20 +522,31 @@ export class JsonlSessionStore {
521
522
  this.#shared.pendingSessions.set(target.id, target);
522
523
  let discard = false;
523
524
  try {
524
- await this.append(target.id, {
525
- type: 'session.created',
526
- payload: {
527
- title: target.title,
528
- workspace: target.workspace,
529
- automaticTitle: false,
530
- source: target.source
525
+ const copied = [
526
+ {
527
+ schemaVersion: 1,
528
+ recordId: randomUUID(),
529
+ sequence: 1,
530
+ timestamp: createdAt,
531
+ sessionId: target.id,
532
+ type: 'session.created',
533
+ payload: {
534
+ title: target.title,
535
+ workspace: target.workspace,
536
+ automaticTitle: false,
537
+ source: target.source
538
+ }
531
539
  }
532
- });
540
+ ];
533
541
  const retained = input.boundaryTurnId === null ? [] : records.slice(1, boundaryIndex + 1);
534
542
  for (const record of retained) {
535
543
  if (record.type === 'context.gc.completed' || record.type === 'responses.continuation')
536
544
  continue;
537
- await this.append(target.id, {
545
+ copied.push({
546
+ schemaVersion: 1,
547
+ recordId: randomUUID(),
548
+ sequence: copied.length + 1,
549
+ sessionId: target.id,
538
550
  type: record.type,
539
551
  payload: forkedPayload(record.type, record.payload, sourceSessionId, target.id, input.title),
540
552
  ...(record.executionId === undefined ? {} : { executionId: record.executionId }),
@@ -542,6 +554,15 @@ export class JsonlSessionStore {
542
554
  timestamp: record.timestamp
543
555
  });
544
556
  }
557
+ // A fork is unpublished until every segment and referenced image is durable.
558
+ const staged = await stageSessionSegments(this.sessionDirectory(target.id), copied, 'fork');
559
+ for (const file of staged.files)
560
+ await rename(file.temporaryPath, file.path);
561
+ target.segments = staged.segments;
562
+ target.nextSequence = copied.length + 1;
563
+ this.#shared.segments.set(target.id, staged.segments.length);
564
+ for (const record of copied)
565
+ this.#applyRecordToResumeIndex(target, record);
545
566
  await copyGeneratedImages(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), generatedImageFileNames(retained));
546
567
  await copyInputImages(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), inputImageReferencesFromRecords(retained));
547
568
  target.lastActivityAt = target.createdAt;
@@ -621,7 +642,7 @@ export class JsonlSessionStore {
621
642
  const retained = records
622
643
  .slice(0, boundaryIndex + 1)
623
644
  .filter((record) => record.type !== 'responses.continuation');
624
- const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
645
+ const staged = await stageSessionSegments(this.sessionDirectory(sessionId), retained);
625
646
  const transactionId = randomUUID();
626
647
  const targetNextSequence = (retained.at(-1)?.sequence ?? 0) + 1;
627
648
  const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
@@ -765,7 +786,7 @@ export class JsonlSessionStore {
765
786
  }
766
787
  }
767
788
  ];
768
- const staged = await stageRollbackSegments(this.sessionDirectory(sessionId), retained);
789
+ const staged = await stageSessionSegments(this.sessionDirectory(sessionId), retained);
769
790
  const transactionId = randomUUID();
770
791
  const contextualWorldStatePath = join(this.sessionDirectory(sessionId), CONTEXTUAL_WORLD_STATE_FILE);
771
792
  const contextualWorldStateBackup = `${contextualWorldStatePath}.2.${transactionId}.clear-backup`;
@@ -966,6 +987,22 @@ export class JsonlSessionStore {
966
987
  throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.', { cause });
967
988
  }
968
989
  }
990
+ async readCodeModeState(sessionId) {
991
+ return restoreCodeModeState(await this.read(sessionId));
992
+ }
993
+ async commitCodeModeState(sessionId, mutations, input) {
994
+ const parsed = parseCodeModeStateMutations(mutations);
995
+ if (parsed.length === 0)
996
+ return this.readCodeModeState(sessionId);
997
+ const state = applyCodeModeStateMutations(await this.readCodeModeState(sessionId), parsed);
998
+ await this.append(sessionId, {
999
+ type: 'code.state.updated',
1000
+ payload: { mutations: parsed },
1001
+ executionId: input.executionId,
1002
+ turnId: input.turnId
1003
+ });
1004
+ return state;
1005
+ }
969
1006
  async readExecutionState(sessionId) {
970
1007
  this.#assertOpen();
971
1008
  const meta = this.#shared.index.sessions.find((item) => item.id === sessionId);
@@ -1637,55 +1674,56 @@ function generatedImageFileNames(records) {
1637
1674
  }
1638
1675
  return [...names];
1639
1676
  }
1640
- async function stageRollbackSegments(sessionDirectory, records) {
1641
- const groups = [];
1642
- for (const record of records) {
1643
- const line = `${JSON.stringify(record)}\n`;
1644
- const bytes = Buffer.byteLength(line);
1645
- let group = groups.at(-1);
1646
- if (group !== undefined && group.bytes > 0 && group.bytes + bytes > MAX_SEGMENT_BYTES)
1647
- group = undefined;
1648
- if (group === undefined) {
1649
- group = {
1650
- lines: [],
1651
- bytes: 0,
1652
- firstSequence: record.sequence,
1653
- lastSequence: record.sequence
1654
- };
1655
- groups.push(group);
1656
- }
1657
- group.lines.push(line);
1658
- group.bytes += bytes;
1659
- group.lastSequence = record.sequence;
1660
- }
1661
- if (groups.length === 0)
1677
+ async function stageSessionSegments(sessionDirectory, records, operation = 'rollback') {
1678
+ let group;
1679
+ if (records.length === 0)
1662
1680
  throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Session history is corrupt.');
1663
1681
  const transactionId = randomUUID();
1664
1682
  const files = [];
1683
+ const segments = [];
1684
+ const flush = async () => {
1685
+ if (group === undefined)
1686
+ return;
1687
+ const number = segments.length + 1;
1688
+ const path = join(sessionDirectory, `events-${String(number).padStart(6, '0')}.jsonl`);
1689
+ const temporaryPath = `${path}.${transactionId}.${operation}-tmp`;
1690
+ const handle = await open(temporaryPath, 'wx', 0o600);
1691
+ files.push({ path, temporaryPath });
1692
+ try {
1693
+ await handle.writeFile(group.lines.join(''));
1694
+ await handle.sync();
1695
+ }
1696
+ finally {
1697
+ await handle.close();
1698
+ }
1699
+ segments.push({
1700
+ number,
1701
+ firstSequence: group.firstSequence,
1702
+ lastSequence: group.lastSequence,
1703
+ bytes: group.bytes
1704
+ });
1705
+ group = undefined;
1706
+ };
1665
1707
  try {
1666
- for (const [index, group] of groups.entries()) {
1667
- const number = index + 1;
1668
- const path = join(sessionDirectory, `events-${String(number).padStart(6, '0')}.jsonl`);
1669
- const temporaryPath = `${path}.${transactionId}.rollback-tmp`;
1670
- const handle = await open(temporaryPath, 'wx', 0o600);
1671
- try {
1672
- await handle.writeFile(group.lines.join(''));
1673
- await handle.sync();
1674
- }
1675
- finally {
1676
- await handle.close();
1708
+ for (const record of records) {
1709
+ const line = `${JSON.stringify(record)}\n`;
1710
+ const bytes = Buffer.byteLength(line);
1711
+ if (group !== undefined && group.bytes > 0 && group.bytes + bytes > MAX_SEGMENT_BYTES)
1712
+ await flush();
1713
+ if (group === undefined) {
1714
+ group = {
1715
+ lines: [],
1716
+ bytes: 0,
1717
+ firstSequence: record.sequence,
1718
+ lastSequence: record.sequence
1719
+ };
1677
1720
  }
1678
- files.push({ path, temporaryPath });
1721
+ group.lines.push(line);
1722
+ group.bytes += bytes;
1723
+ group.lastSequence = record.sequence;
1679
1724
  }
1680
- return {
1681
- files,
1682
- segments: groups.map((group, index) => ({
1683
- number: index + 1,
1684
- firstSequence: group.firstSequence,
1685
- lastSequence: group.lastSequence,
1686
- bytes: group.bytes
1687
- }))
1688
- };
1725
+ await flush();
1726
+ return { files, segments };
1689
1727
  }
1690
1728
  catch (error) {
1691
1729
  await Promise.all(files.map((item) => rm(item.temporaryPath, { force: true }))).catch(() => undefined);