@myagentroam/agent 0.10.6 → 0.10.8

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 (42) hide show
  1. package/dist/agent-core/extensions/compaction.d.ts +1 -0
  2. package/dist/agent-core/extensions/model-request.js +1 -1
  3. package/dist/agent-core/extensions/session-messages.d.ts +1 -1
  4. package/dist/agent-core/host/contracts.d.ts +2 -0
  5. package/dist/agent-core/host/local-agent-host.d.ts +2 -0
  6. package/dist/agent-core/host/local-agent-host.js +2 -0
  7. package/dist/agent-core/model/anthropic-messages.js +10 -2
  8. package/dist/agent-core/model/contracts.d.ts +8 -0
  9. package/dist/agent-core/model/openai-responses.js +8 -2
  10. package/dist/agent-core/model/usage-observer.d.ts +31 -0
  11. package/dist/agent-core/model/usage-observer.js +82 -0
  12. package/dist/agent-core/runtime/events.d.ts +2 -0
  13. package/dist/agent-core/sdk/agent.js +9 -1
  14. package/dist/agent-core/sdk/extension-models.d.ts +1 -1
  15. package/dist/agent-core/sdk/extension-models.js +9 -1
  16. package/dist/agent-core/sdk/session.js +31 -3
  17. package/dist/agent-core/session/catalog.d.ts +1 -0
  18. package/dist/agent-core/session/catalog.js +1 -0
  19. package/dist/agent-core/session/jsonl-store.d.ts +9 -0
  20. package/dist/agent-core/session/jsonl-store.js +24 -0
  21. package/dist/agent-core/session/tool-result-resources.d.ts +26 -0
  22. package/dist/agent-core/session/tool-result-resources.js +285 -0
  23. package/dist/agent-core/session/transaction-recovery.js +1 -1
  24. package/dist/agent-core/tools/code-mode/dispatch.js +5 -0
  25. package/dist/agent-core/tools/code-mode/index.js +7 -1
  26. package/dist/agent-core/tools/code-mode/observations.d.ts +1 -0
  27. package/dist/agent-core/tools/contracts.d.ts +2 -0
  28. package/dist/agent-core/tools/execution.js +11 -2
  29. package/dist/agent-core/tools/file-change-diff.d.ts +10 -0
  30. package/dist/agent-core/tools/file-change-diff.js +139 -0
  31. package/dist/agent-core/tools/local/apply-patch.js +19 -8
  32. package/dist/agent-core/tools/result-extensions.d.ts +46 -0
  33. package/dist/agent-core/tools/result-extensions.js +134 -0
  34. package/dist/agent-core/tools/runtime-context.d.ts +1 -0
  35. package/dist/agent-core/tools/runtime.js +6 -3
  36. package/dist/index.d.ts +4 -0
  37. package/dist/index.js +1 -0
  38. package/dist/shared/auto-title.d.ts +2 -0
  39. package/dist/shared/auto-title.js +6 -0
  40. package/dist/shared/model-text.d.ts +2 -0
  41. package/dist/shared/model-text.js +6 -0
  42. package/package.json +1 -1
@@ -19,6 +19,7 @@ export interface CoreCompactMessage {
19
19
  readonly arguments?: unknown;
20
20
  }
21
21
  export interface CoreCompactContext {
22
+ readonly reportModelUsage?: ((usage: import('../model/usage-observer.js').ModelRequestUsage) => Promise<void> | void) | undefined;
22
23
  readonly operationId: string;
23
24
  readonly reason: 'manual' | 'automatic';
24
25
  readonly messages: readonly CoreCompactMessage[];
@@ -9,7 +9,7 @@ export class ExtensionRequestSnapshots {
9
9
  #current;
10
10
  #completed;
11
11
  record(modelId, request) {
12
- const { onAttemptDiagnostic: _diagnostic, ...serializable } = request;
12
+ const { onAttemptDiagnostic: _diagnostic, onModelUsage: _usage, onModelUsageFailure: _usageFailure, usageRequestId: _usageRequestId, ...serializable } = request;
13
13
  const retained = structuredClone(serializable);
14
14
  let view;
15
15
  // Preserve dispatch-time isolation without projecting unused public views.
@@ -29,7 +29,7 @@ export interface CoreSessionMessages {
29
29
  readonly nextCursor?: string;
30
30
  }
31
31
  export declare function normalizeSessionMessagesInput(input: CoreSessionMessagesInput): Readonly<{
32
- view: "model" | "request" | "conversation";
32
+ view: "model" | "conversation" | "request";
33
33
  agentId: string | undefined;
34
34
  includeImages: boolean;
35
35
  cursor: number;
@@ -53,6 +53,8 @@ export interface HostSessionResourceSnapshot {
53
53
  }
54
54
  /** The Host owns workspace/tool effects; the SDK owns JSONL storage beneath its supplied HOME. */
55
55
  export interface MarAgentHost {
56
+ /** Independent model consumption facts, including child and auxiliary calls. */
57
+ modelUsage?: import('../model/usage-observer.js').ModelUsageObserver | undefined;
56
58
  readonly extensionPorts?: CoreHostExtensionPorts;
57
59
  describe(): Promise<HostDescription>;
58
60
  executeTool(call: ToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
@@ -23,6 +23,7 @@ export interface LocalAgentExecutionExtensions extends HostExecutionExtensions {
23
23
  }
24
24
  export declare class LocalAgentHost implements MarAgentHost {
25
25
  #private;
26
+ readonly modelUsage: import('../model/usage-observer.js').ModelUsageObserver | undefined;
26
27
  readonly extensionPorts: import('./contracts.js').CoreHostExtensionPorts;
27
28
  readonly events: MarAgentEvent[];
28
29
  constructor(input: {
@@ -31,6 +32,7 @@ export declare class LocalAgentHost implements MarAgentHost {
31
32
  beforeOperationToolExecute?: (input: ToolCall, call: CoreOperationContext) => Promise<void> | void;
32
33
  callMcpOperation?: (input: ToolCall, call: CoreOperationContext, expected: NonNullable<McpToolCallContext['expectedTool']>) => Promise<ToolExecutionResult>;
33
34
  network?: CoreNetworkBackend | false;
35
+ modelUsage?: import('../model/usage-observer.js').ModelUsageObserver;
34
36
  homeDirectory: string;
35
37
  workspace: string;
36
38
  mode?: 'normal' | 'node';
@@ -12,6 +12,7 @@ import { directLocalProcessExecutor } from './local-process-executor.js';
12
12
  import { BUILTIN_TOOL_DEFINITIONS } from '../tools/registry.js';
13
13
  import { assertMcpToolCatalog } from '../tools/mcp/catalog.js';
14
14
  export class LocalAgentHost {
15
+ modelUsage;
15
16
  extensionPorts;
16
17
  #home;
17
18
  #workspace;
@@ -37,6 +38,7 @@ export class LocalAgentHost {
37
38
  #shell;
38
39
  #resourceIdleTtlMs;
39
40
  constructor(input) {
41
+ this.modelUsage = input.modelUsage;
40
42
  this.#home = resolve(input.homeDirectory);
41
43
  this.#workspace = resolve(input.workspace);
42
44
  this.#environment = input.environment ?? process.env;
@@ -1,3 +1,4 @@
1
+ import { observeModelRequestUsage } from './usage-observer.js';
1
2
  import { observationModelParts } from '../tools/code-mode/observations.js';
2
3
  import { stableToolSchema } from '../tools/definition-projection.js';
3
4
  import { randomUUID } from 'node:crypto';
@@ -38,7 +39,7 @@ export class AnthropicMessagesAdapter {
38
39
  let firstSemanticEventType;
39
40
  await reportModelAttempt(request, { ...attemptDiagnostic, state: 'STARTED' });
40
41
  try {
41
- for await (const event of this.startAttempt(request, signal)) {
42
+ for await (const event of observeModelRequestUsage(() => this.startAttempt(request, signal), request, this.configuration, signal)) {
42
43
  if (event.type.startsWith('output.')) {
43
44
  resetOutputTiming = true;
44
45
  yield event;
@@ -150,7 +151,11 @@ export class AnthropicMessagesAdapter {
150
151
  if (type === 'message_start') {
151
152
  const message = record(event.message);
152
153
  responseId = text(message.id) ?? responseId;
153
- yield observeModelStreamEvent({ type: 'response.started', responseId }, modelStreamEventObservedAt(raw));
154
+ yield observeModelStreamEvent({
155
+ type: 'response.started',
156
+ responseId,
157
+ ...(text(message.model) ? { modelId: text(message.model) } : {})
158
+ }, modelStreamEventObservedAt(raw));
154
159
  const usage = record(message.usage);
155
160
  const inputTokens = inputTokenCount(usage.input_tokens);
156
161
  const cacheReadTokens = inputTokenCount(usage.cache_read_input_tokens);
@@ -491,6 +496,9 @@ function inputUsage(inputTokens, cacheReadTokens, cacheWriteTokens) {
491
496
  return {
492
497
  inputTokens,
493
498
  contextInputTokens: tokenSum(inputTokens, cacheReadTokens, cacheWriteTokens),
499
+ ...(inputTokens === undefined
500
+ ? {}
501
+ : { totalInputTokens: tokenSum(inputTokens, cacheReadTokens, cacheWriteTokens) }),
494
502
  cacheReadTokens,
495
503
  cacheWriteTokens
496
504
  };
@@ -82,6 +82,10 @@ export interface ModelRetryState {
82
82
  retryDelayMs?: number;
83
83
  }
84
84
  export interface ModelRequest {
85
+ /** Metering identity and sink are local only, never serialized upstream. */
86
+ usageRequestId?: string;
87
+ onModelUsage?: (usage: import('./usage-observer.js').ModelRequestUsage) => Promise<void> | void;
88
+ onModelUsageFailure?: () => void;
85
89
  /** Shared local attempt accounting across Adapter retries and Runtime continuation. Never sent upstream. */
86
90
  retryState?: ModelRetryState;
87
91
  system: string;
@@ -106,6 +110,9 @@ export interface ModelRequest {
106
110
  onAttemptDiagnostic?: (diagnostic: ModelAttemptDiagnostic) => Promise<void> | void;
107
111
  }
108
112
  export interface ModelUsage {
113
+ /** Normalized total input including cache; distinct from protocol-native inputTokens. */
114
+ totalInputTokens?: number | undefined;
115
+ reasoningTokens?: number | undefined;
109
116
  inputTokens?: number | undefined;
110
117
  contextInputTokens?: number | undefined;
111
118
  outputTokens?: number | undefined;
@@ -119,6 +126,7 @@ export type ModelStopReason = 'completed' | 'tool_use' | 'pause' | 'max_output'
119
126
  export type ModelEvent = {
120
127
  type: 'response.started';
121
128
  responseId: string;
129
+ modelId?: string;
122
130
  } | {
123
131
  type: 'output.started';
124
132
  } | {
@@ -1,3 +1,4 @@
1
+ import { observeModelRequestUsage } from './usage-observer.js';
1
2
  import { observationModelParts } from '../tools/code-mode/observations.js';
2
3
  import { stableToolSchema } from '../tools/definition-projection.js';
3
4
  import { isDeepStrictEqual } from 'node:util';
@@ -114,7 +115,7 @@ export class OpenAiResponsesAdapter {
114
115
  ...requestPrefixDiagnostic
115
116
  });
116
117
  try {
117
- for await (const event of this.startAttempt(request, signal, requestBody, useContinuation, credentialReason, turn)) {
118
+ for await (const event of observeModelRequestUsage(() => this.startAttempt(request, signal, requestBody, useContinuation, credentialReason, turn), request, this.configuration, signal)) {
118
119
  if (event.type.startsWith('output.')) {
119
120
  resetOutputTiming = true;
120
121
  yield event;
@@ -383,7 +384,10 @@ export class OpenAiResponsesAdapter {
383
384
  else if (type === 'response.created')
384
385
  yield observeModelStreamEvent({
385
386
  type: 'response.started',
386
- responseId: responseEventId(event) ?? 'unknown'
387
+ responseId: responseEventId(event) ?? 'unknown',
388
+ ...(text(record(event.response).model)
389
+ ? { modelId: text(record(event.response).model) }
390
+ : {})
387
391
  }, modelStreamEventObservedAt(raw));
388
392
  else if (type === 'response.output_text.delta')
389
393
  yield observeModelStreamEvent({
@@ -564,6 +568,8 @@ export class OpenAiResponsesAdapter {
564
568
  final: true,
565
569
  usage: {
566
570
  inputTokens: number(usage.input_tokens),
571
+ totalInputTokens: number(usage.input_tokens),
572
+ reasoningTokens: number(record(usage.output_tokens_details).reasoning_tokens),
567
573
  contextInputTokens: number(usage.input_tokens),
568
574
  outputTokens: number(usage.output_tokens),
569
575
  cacheReadTokens: number(inputDetails.cached_tokens),
@@ -0,0 +1,31 @@
1
+ import type { MarAgentModelConfiguration } from './configuration.js';
2
+ import type { ModelEvent, ModelRequest } from './contracts.js';
3
+ /** Safe request facts, independent of conversation events and provider payloads. */
4
+ export interface ModelRequestUsage {
5
+ readonly recordId: string;
6
+ readonly requestId: string;
7
+ readonly revision: number;
8
+ readonly modelConfigurationId: string;
9
+ readonly modelName: string;
10
+ readonly provider: string;
11
+ readonly requestedModel: string;
12
+ readonly actualModel?: string;
13
+ readonly responseId?: string;
14
+ readonly startedAt: number;
15
+ readonly endedAt?: number;
16
+ readonly state: 'running' | 'succeeded' | 'failed' | 'cancelled' | 'interrupted';
17
+ readonly inputTokens?: string;
18
+ readonly outputTokens?: string;
19
+ readonly cacheReadTokens?: string;
20
+ readonly cacheWriteTokens?: string;
21
+ readonly reasoningTokens?: string;
22
+ }
23
+ export interface ModelUsageObservation extends ModelRequestUsage {
24
+ readonly sessionId: string;
25
+ readonly executionId?: string;
26
+ readonly purpose: 'conversation' | 'compact' | 'title' | 'other';
27
+ readonly agentRole: 'root' | 'subagent';
28
+ }
29
+ export type ModelUsageObserver = (usage: ModelUsageObservation) => Promise<void> | void;
30
+ /** Observe before retry prelude buffering so failed attempts retain reported usage. */
31
+ export declare function observeModelRequestUsage(start: () => AsyncIterable<ModelEvent>, request: ModelRequest, configuration: MarAgentModelConfiguration, signal: AbortSignal, now?: () => number): AsyncIterable<ModelEvent>;
@@ -0,0 +1,82 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ function count(value) {
3
+ return value !== undefined && Number.isSafeInteger(value) && value >= 0
4
+ ? String(value)
5
+ : undefined;
6
+ }
7
+ /** Observe before retry prelude buffering so failed attempts retain reported usage. */
8
+ export async function* observeModelRequestUsage(start, request, configuration, signal, now = Date.now) {
9
+ if (!request.onModelUsage) {
10
+ yield* start();
11
+ return;
12
+ }
13
+ const base = {
14
+ recordId: randomUUID(),
15
+ requestId: request.usageRequestId ?? randomUUID(),
16
+ modelConfigurationId: configuration.id,
17
+ modelName: configuration.name,
18
+ provider: configuration.protocol,
19
+ requestedModel: configuration.modelId,
20
+ startedAt: now()
21
+ };
22
+ let usage = {};
23
+ let responseId;
24
+ let actualModel;
25
+ let state = 'interrupted';
26
+ const publish = async (value) => {
27
+ try {
28
+ await request.onModelUsage(value);
29
+ }
30
+ catch {
31
+ // Metering must not break the model stream or expose callback error details.
32
+ try {
33
+ request.onModelUsageFailure?.();
34
+ }
35
+ catch {
36
+ // Host diagnostics are also best effort.
37
+ }
38
+ }
39
+ };
40
+ await publish({ ...base, revision: 1, state: 'running' });
41
+ try {
42
+ for await (const event of start()) {
43
+ if (event.type === 'response.started') {
44
+ responseId = event.responseId;
45
+ actualModel = event.modelId;
46
+ }
47
+ else if (event.type === 'usage') {
48
+ for (const [key, value] of Object.entries(event.usage)) {
49
+ if (value !== undefined)
50
+ usage = { ...usage, [key]: value };
51
+ }
52
+ }
53
+ else if (event.type === 'completed') {
54
+ state = event.reason === 'cancelled' ? 'cancelled' : 'succeeded';
55
+ }
56
+ yield event;
57
+ }
58
+ }
59
+ catch (error) {
60
+ state = signal.aborted ? 'cancelled' : 'failed';
61
+ throw error;
62
+ }
63
+ finally {
64
+ await publish({
65
+ ...base,
66
+ revision: 2,
67
+ state: signal.aborted && state !== 'succeeded' ? 'cancelled' : state,
68
+ endedAt: now(),
69
+ ...(responseId === undefined ? {} : { responseId }),
70
+ ...(actualModel === undefined ? {} : { actualModel }),
71
+ ...optionalCount('inputTokens', usage.totalInputTokens),
72
+ ...optionalCount('outputTokens', usage.outputTokens),
73
+ ...optionalCount('cacheReadTokens', usage.cacheReadTokens),
74
+ ...optionalCount('cacheWriteTokens', usage.cacheWriteTokens),
75
+ ...optionalCount('reasoningTokens', usage.reasoningTokens)
76
+ });
77
+ }
78
+ }
79
+ function optionalCount(key, value) {
80
+ const normalized = count(value);
81
+ return normalized === undefined ? {} : { [key]: normalized };
82
+ }
@@ -75,6 +75,7 @@ export type MarAgentEvent = MarAgentEventBase & ({
75
75
  truncated?: boolean;
76
76
  } | {
77
77
  type: 'tool.completed';
78
+ resultExtensions?: readonly import('../tools/result-extensions.js').ToolResultExtension[];
78
79
  toolName: string;
79
80
  callId: string;
80
81
  parentCallId?: string;
@@ -84,6 +85,7 @@ export type MarAgentEvent = MarAgentEventBase & ({
84
85
  data?: CodeCellEventData | import('../tools/contracts.js').ExecResult | Omit<import('../tools/contracts.js').ExecResult, 'stdout' | 'stderr'> | import('../tools/contracts.js').ApplyPatchResult | import('../tools/contracts.js').ReadEventData | import('../tools/contracts.js').ViewImageResult | import('../subagent/contracts.js').SubagentTaskSnapshot;
85
86
  } | {
86
87
  type: 'tool.failed';
88
+ resultExtensions?: readonly import('../tools/result-extensions.js').ToolResultExtension[];
87
89
  data?: CodeCellEventData | import('../tools/contracts.js').ApplyPatchPartialResult;
88
90
  toolName: string;
89
91
  callId: string;
@@ -84,7 +84,15 @@ export async function createMarAgentWithAdapter(options) {
84
84
  if (!snapshots)
85
85
  throw new MarAgentError('MAR_AGENT_MODEL_SNAPSHOT_INVALID', 'Request snapshot is unavailable.');
86
86
  return snapshots.resolve(snapshot);
87
- }),
87
+ }, options.host.modelUsage === undefined
88
+ ? undefined
89
+ : (usage, call) => options.host.modelUsage({
90
+ ...usage,
91
+ sessionId: call.sessionId,
92
+ ...(call.executionId === undefined ? {} : { executionId: call.executionId }),
93
+ purpose: 'other',
94
+ agentRole: sessionRuntimes.get(call.sessionId)?.sourceType === 'subagent' ? 'subagent' : 'root'
95
+ })),
88
96
  sessionBackend: (id) => sessionRuntimes.get(id)?.sessionBackend,
89
97
  agentsBackend: (id) => sessionRuntimes.get(id)?.agentsBackend,
90
98
  scheduleTool: (id) => (definition, run) => {
@@ -10,4 +10,4 @@ export declare function createExtensionModels(state: () => {
10
10
  modelId: string;
11
11
  request: ModelRequest;
12
12
  view: CoreModelRequest;
13
- }): CoreModelsBackend;
13
+ }, observeUsage?: (usage: import('../model/usage-observer.js').ModelRequestUsage, call: CoreOperationContext) => Promise<void> | void): CoreModelsBackend;
@@ -1,8 +1,9 @@
1
+ import { randomUUID } from 'node:crypto';
1
2
  import { MarAgentError } from '../error.js';
2
3
  import { ExtensionRequestBridge } from '../extensions/model-request.js';
3
4
  import { transformCoreModelStream } from '../extensions/model-stream.js';
4
5
  import { modelTextInput } from '../model/text-blocks.js';
5
- export function createExtensionModels(state, createAdapter, resolveSnapshot) {
6
+ export function createExtensionModels(state, createAdapter, resolveSnapshot, observeUsage) {
6
7
  return {
7
8
  list: () => {
8
9
  const { models, defaultModelId } = state();
@@ -67,6 +68,13 @@ export function createExtensionModels(state, createAdapter, resolveSnapshot) {
67
68
  throw new MarAgentError('MAR_AGENT_MODEL_REQUEST_INVALID', 'Independent generation does not execute tools.');
68
69
  const request = {
69
70
  ...lowered,
71
+ usageRequestId: randomUUID(),
72
+ ...(observeUsage === undefined
73
+ ? {}
74
+ : {
75
+ onModelUsage: (usage) => observeUsage(usage, call),
76
+ onModelUsageFailure: () => diagnoseCleanup?.('MAR_AGENT_MODEL_USAGE_FAILED')
77
+ }),
70
78
  ...(input.retainToolCatalog &&
71
79
  snapshot.request.systemBlocks === undefined &&
72
80
  snapshot.request.messages.every((message) => message.textBlocks === undefined)
@@ -667,7 +667,7 @@ export function createSessionOpener(services) {
667
667
  code: 'MAR_AGENT_MODEL_STREAM_CLEANUP_FAILED',
668
668
  sessionId
669
669
  }));
670
- const transformModelRequest = async (candidate, bindings = captureTools(), modelId = selectedModel.id) => {
670
+ const transformModelRequest = async (candidate, bindings = captureTools(), modelId = selectedModel.id, purpose = mode === 'compact' ? 'compact' : 'conversation') => {
671
671
  let lowered = candidate;
672
672
  if (coreExtensions.hooks.hasInterceptors('model.request')) {
673
673
  const bridge = new ExtensionRequestBridge(modelId, candidate);
@@ -681,7 +681,25 @@ export function createSessionOpener(services) {
681
681
  if (!binding || !isDeepStrictEqual(contract, capturedContract))
682
682
  throw new MarAgentError('MAR_AGENT_EXTENSION_MODEL_REQUEST_INVALID', 'Request tools do not match captured sources.');
683
683
  }
684
- return lowered;
684
+ return {
685
+ ...lowered,
686
+ usageRequestId: randomUUID(),
687
+ ...(host.modelUsage === undefined
688
+ ? {}
689
+ : {
690
+ onModelUsage: (usage) => host.modelUsage({
691
+ ...usage,
692
+ sessionId,
693
+ executionId,
694
+ purpose,
695
+ agentRole: sessionSource.type === 'subagent' ? 'subagent' : 'root'
696
+ }),
697
+ onModelUsageFailure: () => coreExtensions.diagnose({
698
+ code: 'MAR_AGENT_MODEL_USAGE_FAILED',
699
+ sessionId
700
+ })
701
+ })
702
+ };
685
703
  };
686
704
  const toolEligible = (tool) => {
687
705
  toolCallers(tool);
@@ -825,6 +843,7 @@ export function createSessionOpener(services) {
825
843
  : subagents.captureParentFrame(executionId);
826
844
  const toolContext = {
827
845
  host: host,
846
+ writeToolResultResource: (text) => store.writeToolResultResource(sessionId, text),
828
847
  todo,
829
848
  ...(subagents === undefined || mode === 'compact'
830
849
  ? {}
@@ -1038,6 +1057,15 @@ export function createSessionOpener(services) {
1038
1057
  ...(focus === undefined ? {} : { focus }),
1039
1058
  summaryTokenBudget,
1040
1059
  getPrompt: () => buildCompactionMessages('')[1].content,
1060
+ reportModelUsage: host.modelUsage === undefined
1061
+ ? undefined
1062
+ : (usage) => host.modelUsage({
1063
+ ...usage,
1064
+ sessionId,
1065
+ executionId,
1066
+ purpose: 'compact',
1067
+ agentRole: sessionSource.type === 'subagent' ? 'subagent' : 'root'
1068
+ }),
1041
1069
  reportDiagnostic: async (value) => {
1042
1070
  if (!acceptsDiagnostics || ++diagnosticReports > 8)
1043
1071
  return;
@@ -1179,7 +1207,7 @@ export function createSessionOpener(services) {
1179
1207
  promptCacheKey: sessionId,
1180
1208
  reasoningEffort: requestReasoningEffort ?? reasoningEffort,
1181
1209
  onAttemptDiagnostic: recordModelAttempt('COMPACTION')
1182
- });
1210
+ }, undefined, selectedModel.id, 'compact');
1183
1211
  for await (const event of streamModel((signal) => compaction.start({ start: (request) => selected.start(request, signal) }, request), compaction.signal)) {
1184
1212
  if (event.type === 'text.completed' &&
1185
1213
  (event.phase ?? 'final_answer') === 'final_answer')
@@ -71,6 +71,7 @@ export interface MarAgentSessionCatalog {
71
71
  readonly imageIndex: number;
72
72
  }): Promise<MarAgentSessionImage | undefined>;
73
73
  contextUsage(sessionId: string): Promise<MarAgentContextUsage | undefined>;
74
+ readToolResultResource(sessionId: string, input: import('./tool-result-resources.js').ToolResultResourceInput): Promise<import('./tool-result-resources.js').ToolResultResourcePage>;
74
75
  fork(sessionId: string, input: {
75
76
  readonly boundaryTurnId: string | null;
76
77
  readonly title: string;
@@ -79,6 +79,7 @@ export async function openMarAgentSessionCatalog(input) {
79
79
  dataBase64: image.dataBase64
80
80
  };
81
81
  },
82
+ readToolResultResource: (sessionId, input) => store.readToolResultResource(sessionId, input),
82
83
  contextUsage: async (sessionId) => {
83
84
  const page = await store.readPage(sessionId, { limit: 1000 });
84
85
  for (const record of page.records.toReversed()) {
@@ -1,3 +1,4 @@
1
+ import { type ToolResultResourceInput } from './tool-result-resources.js';
1
2
  import { type CoreCheckpoint, type CoreJsonValue } from '../extensions/data.js';
2
3
  import type { ModelInputMetadata, ModelMessage, ModelUsage } from '../model/contracts.js';
3
4
  import type { EnvironmentContextState } from '../runtime/context/contracts.js';
@@ -104,6 +105,14 @@ export declare class JsonlSessionStore {
104
105
  writeContextualWorldState(sessionId: string, state: unknown): Promise<void>;
105
106
  /** Mark each image pending before publication; turn.user or lease recovery resolves ownership. */
106
107
  persistInputImages(sessionId: string, images: readonly SessionInputImage[]): Promise<SessionInputImageMetadata[]>;
108
+ writeToolResultResource(sessionId: string, text: string): Promise<{
109
+ status: "available";
110
+ resourceRef: string;
111
+ format: "unified-diff";
112
+ byteSize: number;
113
+ sha256: string;
114
+ }>;
115
+ readToolResultResource(sessionId: string, input: ToolResultResourceInput): Promise<import("./tool-result-resources.js").ToolResultResourcePage>;
107
116
  readInputImages(sessionId: string, value: unknown): Promise<RestoredSessionInputImage[]>;
108
117
  readInputImage(sessionId: string, input: {
109
118
  readonly sequence: number;
@@ -4,6 +4,7 @@ import { copyFile, lstat, mkdir, open, readFile, readdir, rename, rm, stat, writ
4
4
  import { basename, join } from 'node:path';
5
5
  import { isDeepStrictEqual } from 'node:util';
6
6
  import { MarAgentError } from '../error.js';
7
+ import { completeToolResultResources, cleanupUnreferencedToolResources, bindToolResultResources, copyToolResultResources, readToolResultResource, toolResultResourceFiles, writeToolResultResource } from './tool-result-resources.js';
7
8
  import { acquireFileLock } from '../storage/file-lock.js';
8
9
  import { cloneCoreJson } from '../extensions/data.js';
9
10
  import { applyCodeModeStateMutations, parseCodeModeStateMutations, restoreCodeModeState } from '../tools/code-mode/state.js';
@@ -484,6 +485,22 @@ export class JsonlSessionStore {
484
485
  throw error;
485
486
  }
486
487
  }
488
+ async writeToolResultResource(sessionId, text) {
489
+ this.#assertOpen();
490
+ if (!this.#shared.index.sessions.some((item) => item.id === sessionId))
491
+ throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
492
+ return writeToolResultResource(this.sessionDirectory(sessionId), text);
493
+ }
494
+ async readToolResultResource(sessionId, input) {
495
+ this.#assertOpen();
496
+ await this.#refreshIndex();
497
+ if (!this.#shared.index.sessions.some((item) => item.id === sessionId))
498
+ throw new MarAgentError('MAR_AGENT_SESSION_NOT_FOUND', 'Session was not found.');
499
+ return readToolResultResource(this.sessionDirectory(sessionId), sessionId, input, async (sequence) => {
500
+ await this.#refreshIndex();
501
+ return (await this.readPage(sessionId, { beforeSequence: sequence + 1, limit: 1 })).records.find((record) => record.sequence === sequence);
502
+ });
503
+ }
487
504
  async readInputImages(sessionId, value) {
488
505
  this.#assertOpen();
489
506
  const references = inputImageReferences(value);
@@ -606,6 +623,7 @@ export class JsonlSessionStore {
606
623
  this.#shared.segments.set(target.id, staged.segments.length);
607
624
  for (const record of copied)
608
625
  this.#applyRecordToResumeIndex(target, record);
626
+ await copyToolResultResources(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), copied);
609
627
  await copyGeneratedImages(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), generatedImageFileNames(retained));
610
628
  await copyInputImages(this.sessionDirectory(sourceSessionId), this.sessionDirectory(target.id), inputImageReferencesFromRecords(retained));
611
629
  target.lastActivityAt = target.createdAt;
@@ -719,6 +737,7 @@ export class JsonlSessionStore {
719
737
  contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
720
738
  generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(generatedImageFileNames(retained)), transactionId);
721
739
  inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(inputImageReferencesFromRecords(retained).map((image) => image.fileName)), transactionId);
740
+ inputTrash.push(...(await stageUnreferencedImages(this.sessionDirectory(sessionId), 'tool_results', toolResultResourceFiles(retained), transactionId)));
722
741
  for (const item of backups)
723
742
  await rename(item.path, item.backup);
724
743
  for (const item of staged.files) {
@@ -880,6 +899,7 @@ export class JsonlSessionStore {
880
899
  contextualWorldStateStaged = await renameIfExists(contextualWorldStatePath, contextualWorldStateBackup);
881
900
  generatedTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'generated_images', new Set(), transactionId);
882
901
  inputTrash = await stageUnreferencedImages(this.sessionDirectory(sessionId), 'input_images', new Set(), transactionId);
902
+ inputTrash.push(...(await stageUnreferencedImages(this.sessionDirectory(sessionId), 'tool_results', new Set(), transactionId)));
883
903
  for (const item of backups)
884
904
  await rename(item.path, item.backup);
885
905
  for (const item of staged.files) {
@@ -998,6 +1018,8 @@ export class JsonlSessionStore {
998
1018
  type: input.type,
999
1019
  payload: input.payload
1000
1020
  };
1021
+ JSON.stringify(record); // Reject malformed payloads before binding resources.
1022
+ await bindToolResultResources(this.sessionDirectory(sessionId), record, (sequence) => this.#readRecordAtSequence(meta, sequence), true);
1001
1023
  const serialized = `${JSON.stringify(record)}\n`;
1002
1024
  let segment = await this.#currentSegment(sessionId);
1003
1025
  let path = this.#segmentPath(sessionId, segment);
@@ -1028,6 +1050,7 @@ export class JsonlSessionStore {
1028
1050
  await handle.close();
1029
1051
  }
1030
1052
  meta.nextSequence++;
1053
+ await completeToolResultResources(this.sessionDirectory(sessionId), record);
1031
1054
  if (record.type === 'turn.user' &&
1032
1055
  isObject(record.payload) &&
1033
1056
  Array.isArray(record.payload.images))
@@ -1798,6 +1821,7 @@ export class JsonlSessionStore {
1798
1821
  await repairSegmentTail(latestPath);
1799
1822
  const records = await readRecords(latestPath);
1800
1823
  await recoverPendingInputImages(this.sessionDirectory(sessionId), () => this.#readRecords(sessionId));
1824
+ await cleanupUnreferencedToolResources(this.sessionDirectory(sessionId), () => this.#readRecords(sessionId));
1801
1825
  const last = records.at(-1);
1802
1826
  const match = /events-(\d{6})\.jsonl$/.exec(latestPath);
1803
1827
  const segment = Number(match?.[1] ?? 1);
@@ -0,0 +1,26 @@
1
+ import { type AvailableToolResultResource } from '../tools/result-extensions.js';
2
+ import type { SessionRecord } from './jsonl-store.js';
3
+ export interface ToolResultResourceInput {
4
+ resourceRef: string;
5
+ cursor?: string;
6
+ maxBytes?: number;
7
+ }
8
+ export interface ToolResultResourcePage {
9
+ type: 'file_changes';
10
+ version: 1;
11
+ format: 'unified-diff';
12
+ resourceRef: string;
13
+ byteSize: number;
14
+ sha256: string;
15
+ content: string;
16
+ nextCursor?: string;
17
+ }
18
+ export declare function writeToolResultResource(directory: string, text: string): Promise<AvailableToolResultResource>;
19
+ export declare function recordToolResources(record: Pick<SessionRecord, 'type' | 'payload'>): AvailableToolResultResource[];
20
+ /** A locator is only a hint: reads must verify its exact committed record identity. */
21
+ export declare function bindToolResultResources(directory: string, record: SessionRecord, readRecord?: (sequence: number) => Promise<SessionRecord | undefined>, degrade?: boolean): Promise<void>;
22
+ export declare function toolResultResourceFiles(records: readonly SessionRecord[]): Set<string>;
23
+ export declare function copyToolResultResources(source: string, target: string, records: readonly SessionRecord[]): Promise<void>;
24
+ export declare function completeToolResultResources(directory: string, record: SessionRecord): Promise<void>;
25
+ export declare function cleanupUnreferencedToolResources(directory: string, readRecords: () => Promise<readonly SessionRecord[]>): Promise<void>;
26
+ export declare function readToolResultResource(directory: string, sessionId: string, input: ToolResultResourceInput, readRecord: (sequence: number) => Promise<SessionRecord | undefined>): Promise<ToolResultResourcePage>;