@myagentroam/agent 0.10.28 → 0.10.30

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 (28) hide show
  1. package/dist/agent-core/extensions/session-input.d.ts +2 -0
  2. package/dist/agent-core/extensions/session-input.js +21 -3
  3. package/dist/agent-core/model/openai-responses.js +1 -1
  4. package/dist/agent-core/runtime/compaction/coordinator.d.ts +15 -0
  5. package/dist/agent-core/runtime/compaction/coordinator.js +88 -0
  6. package/dist/agent-core/runtime/compaction/requests.d.ts +16 -0
  7. package/dist/agent-core/runtime/compaction/requests.js +114 -0
  8. package/dist/agent-core/runtime/compaction/retained-context.d.ts +6 -0
  9. package/dist/agent-core/runtime/compaction/retained-context.js +52 -0
  10. package/dist/agent-core/runtime/context/restore.d.ts +1 -0
  11. package/dist/agent-core/runtime/context/restore.js +14 -4
  12. package/dist/agent-core/runtime/context/usage.js +3 -1
  13. package/dist/agent-core/runtime/event-projection.js +1 -1
  14. package/dist/agent-core/runtime/events.d.ts +21 -0
  15. package/dist/agent-core/runtime/execution-policy.d.ts +1 -0
  16. package/dist/agent-core/runtime/execution-policy.js +1 -0
  17. package/dist/agent-core/runtime/execution.d.ts +1 -0
  18. package/dist/agent-core/runtime/execution.js +1 -0
  19. package/dist/agent-core/sdk/extension-session.js +4 -2
  20. package/dist/agent-core/sdk/session.d.ts +2 -0
  21. package/dist/agent-core/sdk/session.js +307 -191
  22. package/dist/agent-core/sdk/types.d.ts +4 -1
  23. package/dist/agent-core/session/catalog.js +2 -2
  24. package/dist/agent-core/session/jsonl-store.d.ts +2 -0
  25. package/dist/agent-core/session/jsonl-store.js +6 -0
  26. package/dist/agent-core/tools/local/exec.js +9 -1
  27. package/dist/index.d.ts +1 -1
  28. package/package.json +1 -1
@@ -34,6 +34,7 @@ export interface CoreSubmitInput extends CorePromptInput {
34
34
  }
35
35
  export interface CoreCompactInput {
36
36
  readonly focus?: string;
37
+ readonly async?: boolean;
37
38
  }
38
39
  export type CoreSessionRequest = {
39
40
  readonly kind: 'append';
@@ -130,6 +131,7 @@ export declare class SessionInputCoordinator {
130
131
  get hasPending(): boolean;
131
132
  get hasBoundaryPending(): boolean;
132
133
  get executing(): boolean;
134
+ get hasBoundaryCompactPending(): boolean;
133
135
  currentCompact(): CoreSessionReceipt | undefined;
134
136
  inspect(): readonly CoreSessionReceipt[];
135
137
  performAppend(input: CoreAppendInput, call: CoreOperationContext, action: CoreSessionAction, leaf: (entry: SessionInputEntry, request: CoreSessionRequest, call: CoreOperationContext) => Promise<CoreSessionActionResult>): Promise<CoreSessionReceipt>;
@@ -43,6 +43,17 @@ export class SessionInputCoordinator {
43
43
  get executing() {
44
44
  return this.#current.getStore()?.receipt.status === 'in-progress';
45
45
  }
46
+ get hasBoundaryCompactPending() {
47
+ for (const entry of this.#pending) {
48
+ if (entry.receipt.status === 'cancelled' || entry.source.signal.aborted)
49
+ continue;
50
+ if (entry.request.kind === 'submit' && entry.request.input.delivery === 'idle')
51
+ return false;
52
+ if (entry.request.kind === 'compact')
53
+ return true;
54
+ }
55
+ return false;
56
+ }
46
57
  currentCompact() {
47
58
  const entry = this.#current.getStore();
48
59
  return entry?.request.kind === 'compact' && entry.receipt.status === 'in-progress'
@@ -186,7 +197,14 @@ export class SessionInputCoordinator {
186
197
  : entry.source.signal,
187
198
  ...(execution ? { executionId: execution.executionId, turnId: execution.turnId } : {})
188
199
  });
189
- const applied = await this.#execute(entry, call, leaf);
200
+ const executing = this.#execute(entry, call, leaf);
201
+ if (execution && entry.request.kind === 'compact' && entry.request.input.async) {
202
+ // Keep the receipt in progress while the main loop reaches adoption checkpoints.
203
+ void executing.catch(() => undefined);
204
+ await Promise.race([entry.started, executing]);
205
+ continue;
206
+ }
207
+ const applied = await executing;
190
208
  changed ||= applied;
191
209
  }
192
210
  return changed;
@@ -345,7 +363,7 @@ export function validateCoreSessionRequest(request) {
345
363
  (request.input.modelAttribution !== undefined &&
346
364
  request.input.modelAttribution !== 'none'))) ||
347
365
  (request.kind === 'compact' &&
348
- request.input.focus !== undefined &&
349
- typeof request.input.focus !== 'string'))
366
+ ((request.input.focus !== undefined && typeof request.input.focus !== 'string') ||
367
+ (request.input.async !== undefined && typeof request.input.async !== 'boolean'))))
350
368
  throw new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'Session input is invalid.');
351
369
  }
@@ -1770,7 +1770,7 @@ function strictFunctionSchema(value, optional = false) {
1770
1770
  if (value === null || typeof value !== 'object' || Array.isArray(value))
1771
1771
  return value;
1772
1772
  const schema = value;
1773
- const supportedSchema = Object.fromEntries(Object.entries(schema).filter(([keyword]) => keyword !== 'propertyNames'));
1773
+ const supportedSchema = Object.fromEntries(Object.entries(schema).filter(([keyword]) => keyword !== 'propertyNames' && keyword !== 'uniqueItems'));
1774
1774
  if (schema.type === 'object') {
1775
1775
  const properties = record(schema.properties);
1776
1776
  const required = new Set(Array.isArray(schema.required) ? schema.required : []);
@@ -0,0 +1,15 @@
1
+ export interface CompactFlight {
2
+ readonly id: string;
3
+ readonly focus: string | undefined;
4
+ readonly result: Promise<boolean>;
5
+ }
6
+ /** One generation and adoption lane, with installation released only by a safe checkpoint. */
7
+ export declare class CompactCoordinator {
8
+ #private;
9
+ get pending(): boolean;
10
+ get waiting(): boolean;
11
+ assertNotReentrant(): void;
12
+ start(id: string, focus: string | undefined, run: (prepare: () => Promise<void>, signal: AbortSignal) => Promise<boolean>): CompactFlight;
13
+ checkpoint(wait: boolean): Promise<boolean | undefined>;
14
+ close(reason: unknown): Promise<void>;
15
+ }
@@ -0,0 +1,88 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { MarAgentError } from '../../error.js';
3
+ /** One generation and adoption lane, with installation released only by a safe checkpoint. */
4
+ export class CompactCoordinator {
5
+ #owner = new AsyncLocalStorage();
6
+ #flight;
7
+ get pending() {
8
+ return this.#flight !== undefined;
9
+ }
10
+ get waiting() {
11
+ return this.#flight?.waiting === true;
12
+ }
13
+ assertNotReentrant() {
14
+ if (this.#owner.getStore())
15
+ throw new MarAgentError('MAR_AGENT_COMPACT_REENTRANT', 'Recursive compaction is not allowed.');
16
+ }
17
+ start(id, focus, run) {
18
+ this.assertNotReentrant();
19
+ if (this.#flight) {
20
+ if (focus !== undefined && focus !== this.#flight.focus)
21
+ throw new MarAgentError('MAR_AGENT_COMPACT_CONFLICT', 'A different compaction is already active.');
22
+ return this.#flight;
23
+ }
24
+ let ready;
25
+ let release;
26
+ const gate = new Promise((resolve) => {
27
+ release = resolve;
28
+ });
29
+ const controller = new AbortController();
30
+ const flight = {
31
+ id,
32
+ focus,
33
+ controller,
34
+ prepared: false,
35
+ settled: false,
36
+ waiting: false,
37
+ release,
38
+ ready: new Promise((resolve) => {
39
+ ready = resolve;
40
+ }),
41
+ result: undefined
42
+ };
43
+ this.#flight = flight;
44
+ const abort = () => release();
45
+ controller.signal.addEventListener('abort', abort, { once: true });
46
+ flight.result = this.#owner.run(id, async () => {
47
+ try {
48
+ return await run(async () => {
49
+ controller.signal.throwIfAborted();
50
+ flight.prepared = true;
51
+ ready();
52
+ await gate;
53
+ controller.signal.throwIfAborted();
54
+ }, controller.signal);
55
+ }
56
+ finally {
57
+ flight.settled = true;
58
+ ready();
59
+ controller.signal.removeEventListener('abort', abort);
60
+ }
61
+ });
62
+ // The result remains observable at the next checkpoint, including asynchronous failures.
63
+ void flight.result.catch(() => undefined);
64
+ return flight;
65
+ }
66
+ async checkpoint(wait) {
67
+ const flight = this.#flight;
68
+ if (!flight || (!wait && !flight.prepared && !flight.settled))
69
+ return undefined;
70
+ flight.waiting ||= wait;
71
+ try {
72
+ await flight.ready;
73
+ flight.release();
74
+ return await flight.result;
75
+ }
76
+ finally {
77
+ if (this.#flight === flight)
78
+ this.#flight = undefined;
79
+ }
80
+ }
81
+ async close(reason) {
82
+ const flight = this.#flight;
83
+ if (!flight)
84
+ return;
85
+ flight.controller.abort(reason);
86
+ await this.checkpoint(true).catch(() => undefined);
87
+ }
88
+ }
@@ -0,0 +1,16 @@
1
+ import type { CompactInput, ExecutionHandle } from '../../sdk/types.js';
2
+ import type { MarAgentEvent } from '../events.js';
3
+ import type { CompactFlight } from './coordinator.js';
4
+ /** External handles represent an operation inside its owning main Execution, not another loop. */
5
+ export declare class CompactRequests {
6
+ #private;
7
+ readonly executionId: string;
8
+ readonly assertAdmission: () => void;
9
+ constructor(executionId: string, assertAdmission: () => void);
10
+ request(input: CompactInput): ExecutionHandle;
11
+ publish(event: MarAgentEvent): void;
12
+ bind(join: (input: CompactInput) => CompactFlight | undefined): void;
13
+ drain(start: (input: CompactInput) => CompactFlight): boolean;
14
+ seal(): void;
15
+ close(error: unknown): void;
16
+ }
@@ -0,0 +1,114 @@
1
+ import { AsyncQueue } from '../async-queue.js';
2
+ import { MarAgentError, describeMarAgentError } from '../../error.js';
3
+ /** External handles represent an operation inside its owning main Execution, not another loop. */
4
+ export class CompactRequests {
5
+ executionId;
6
+ assertAdmission;
7
+ #closed = false;
8
+ #blocking = false;
9
+ #join;
10
+ #pending = [];
11
+ #observers = new Set();
12
+ constructor(executionId, assertAdmission) {
13
+ this.executionId = executionId;
14
+ this.assertAdmission = assertAdmission;
15
+ }
16
+ request(input) {
17
+ this.assertAdmission();
18
+ if (this.#closed)
19
+ throw new MarAgentError('MAR_AGENT_SESSION_INPUT_CLOSED', 'Execution is finishing.');
20
+ if (Object.entries(input).some(([key, value]) => value !== undefined && !['prompt', 'async'].includes(key)))
21
+ throw new MarAgentError('MAR_AGENT_COMPACT_CONFLICT', 'An active compaction uses its owning execution configuration.');
22
+ if (input.async !== undefined && typeof input.async !== 'boolean')
23
+ throw new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'Compaction async mode is invalid.');
24
+ const queue = new AsyncQueue();
25
+ let resolve;
26
+ const result = new Promise((settle) => {
27
+ resolve = settle;
28
+ });
29
+ let settled = false;
30
+ let operationId;
31
+ const observe = (event) => {
32
+ if ('compactionId' in event && event.compactionId === operationId)
33
+ queue.push(event);
34
+ };
35
+ this.#observers.add(observe);
36
+ const finish = (status, error) => {
37
+ if (settled)
38
+ return;
39
+ settled = true;
40
+ const diagnostic = error === undefined ? undefined : describeMarAgentError(error, 'MAR_AGENT_COMPACT_FAILED');
41
+ resolve({
42
+ executionId: this.executionId,
43
+ status,
44
+ finalAnswer: '',
45
+ ...(diagnostic ? { errorCode: diagnostic.code, errorMessage: diagnostic.message } : {})
46
+ });
47
+ this.#observers.delete(observe);
48
+ queue.close();
49
+ };
50
+ const pending = {
51
+ input: { ...input },
52
+ cancelled: () => settled,
53
+ settle: (flight) => {
54
+ operationId = flight.id;
55
+ void flight.result.then((changed) => finish(changed ? 'completed' : 'failed', changed
56
+ ? undefined
57
+ : new MarAgentError('MAR_AGENT_CONTEXT_LIMIT', 'No history can be compacted.')), (error) => finish('failed', error));
58
+ },
59
+ fail: (error) => finish('failed', error)
60
+ };
61
+ try {
62
+ const flight = this.#join?.(input);
63
+ if (flight) {
64
+ pending.settle(flight);
65
+ this.#blocking ||= input.async !== true;
66
+ }
67
+ else
68
+ this.#pending.push(pending);
69
+ }
70
+ catch (error) {
71
+ pending.fail(error);
72
+ }
73
+ return {
74
+ executionId: this.executionId,
75
+ [Symbol.asyncIterator]: () => queue[Symbol.asyncIterator](),
76
+ result: () => {
77
+ queue.discardIfUnobserved();
78
+ return result;
79
+ },
80
+ cancel: () => finish('cancelled')
81
+ };
82
+ }
83
+ publish(event) {
84
+ for (const observer of this.#observers)
85
+ observer(event);
86
+ }
87
+ bind(join) {
88
+ this.#join = join;
89
+ }
90
+ drain(start) {
91
+ let blocking = this.#blocking;
92
+ this.#blocking = false;
93
+ for (const pending of this.#pending.splice(0)) {
94
+ if (pending.cancelled())
95
+ continue;
96
+ try {
97
+ pending.settle(start(pending.input));
98
+ blocking ||= pending.input.async !== true;
99
+ }
100
+ catch (error) {
101
+ pending.fail(error);
102
+ }
103
+ }
104
+ return blocking;
105
+ }
106
+ seal() {
107
+ this.#closed = true;
108
+ }
109
+ close(error) {
110
+ this.seal();
111
+ for (const pending of this.#pending.splice(0))
112
+ pending.fail(error);
113
+ }
114
+ }
@@ -0,0 +1,6 @@
1
+ import type { ModelMessage } from '../../model/contracts.js';
2
+ import type { CompactSnapshotAnchor } from '../events.js';
3
+ /** Use a public native identity, never private context text, as the visible boundary. */
4
+ export declare function compactSnapshotAnchor(messages: readonly ModelMessage[], sources: WeakMap<ModelMessage, import('../../session/jsonl-store.js').SessionRecord>): CompactSnapshotAnchor | undefined;
5
+ /** Decode the SDK-owned checkpoint suffix without promoting contextual text to real input. */
6
+ export declare function restoreCompactTail(value: unknown): ModelMessage | undefined;
@@ -0,0 +1,52 @@
1
+ import { parseModelInputMetadata } from '../../session/jsonl-store.js';
2
+ import { restoreModelContext } from '../context/projection.js';
3
+ /** Use a public native identity, never private context text, as the visible boundary. */
4
+ export function compactSnapshotAnchor(messages, sources) {
5
+ for (let index = messages.length - 1; index >= 0; index--) {
6
+ const message = messages[index];
7
+ if (message.role === 'tool' && message.callId)
8
+ return { kind: 'tool', id: message.callId };
9
+ const record = sources.get(message);
10
+ const payload = record?.payload;
11
+ if (record?.type === 'message.completed' && typeof payload?.itemId === 'string')
12
+ return { kind: 'message', id: payload.itemId };
13
+ }
14
+ return undefined;
15
+ }
16
+ /** Decode the SDK-owned checkpoint suffix without promoting contextual text to real input. */
17
+ export function restoreCompactTail(value) {
18
+ if (!value || typeof value !== 'object' || Array.isArray(value))
19
+ return undefined;
20
+ const input = value;
21
+ if ((input.contextKind === 'private' && input.role !== 'provider') ||
22
+ input.contextKind === 'compact_control')
23
+ return undefined;
24
+ if ((input.role === 'assistant' || input.role === 'user') && typeof input.content === 'string') {
25
+ const projected = restoreModelContext(input);
26
+ const metadata = parseModelInputMetadata(input.metadata);
27
+ const contextKind = ['environment', 'contextual', 'summary'].includes(String(input.contextKind))
28
+ ? input.contextKind
29
+ : undefined;
30
+ return {
31
+ role: input.role,
32
+ content: input.content,
33
+ ...(contextKind
34
+ ? { contextKind }
35
+ : input.role === 'user' && !metadata
36
+ ? { contextKind: 'contextual' }
37
+ : {}),
38
+ ...(metadata ? { metadata } : {}),
39
+ ...(projected?.textBlocks ? { textBlocks: projected.textBlocks } : {}),
40
+ ...(Array.isArray(input.images) &&
41
+ input.images.every((image) => image && typeof image.mimeType === 'string' && typeof image.dataBase64 === 'string')
42
+ ? {
43
+ images: input.images.map((image) => ({
44
+ mimeType: image.mimeType,
45
+ dataBase64: image.dataBase64
46
+ }))
47
+ }
48
+ : {})
49
+ };
50
+ }
51
+ return restoreModelContext(input);
52
+ }
@@ -7,6 +7,7 @@ export declare function restoreMessages(store: JsonlSessionStore, sessionId: str
7
7
  strictImages?: boolean;
8
8
  currentTurnId?: string;
9
9
  projection?: ContextProjection;
10
+ messageSources?: WeakMap<ModelMessage, import('../../session/jsonl-store.js').SessionRecord>;
10
11
  }): Promise<{
11
12
  toolOutcomes: WeakMap<ModelMessage, {
12
13
  isError: boolean;
@@ -1,8 +1,9 @@
1
1
  import { MarAgentError } from '../../error.js';
2
2
  import { compactSummaryFromCheckpoint, replacementHistoryFromCheckpoint, inputMetadataFromRecord } from '../../session/jsonl-store.js';
3
3
  import { compactedHistoryMessages, continuationMessage } from '../compaction/history.js';
4
+ import { restoreCompactTail } from '../compaction/retained-context.js';
4
5
  import { environmentContextUpdate, restoreEnvironmentState } from './environment.js';
5
- import { buildContextProjection, restoreModelContext } from './projection.js';
6
+ import { buildContextProjection } from './projection.js';
6
7
  export async function restoreMessages(store, sessionId, records, options) {
7
8
  const messages = [];
8
9
  const outcomes = new Map();
@@ -14,6 +15,7 @@ export async function restoreMessages(store, sessionId, records, options) {
14
15
  let currentTurnUserIndex;
15
16
  let environmentState;
16
17
  for (const record of records) {
18
+ const messageStart = messages.length;
17
19
  const payload = record.payload;
18
20
  if ((record.type === 'tool.completed' || record.type === 'tool.failed') &&
19
21
  typeof payload.callId === 'string') {
@@ -95,11 +97,17 @@ export async function restoreMessages(store, sessionId, records, options) {
95
97
  ]));
96
98
  if (Array.isArray(payload.retainedModelContext))
97
99
  for (const item of payload.retainedModelContext) {
98
- if (!item || typeof item !== 'object' || item.role !== 'user')
100
+ if (!item || typeof item !== 'object')
99
101
  continue;
100
- const message = restoreModelContext(item);
101
- if (message)
102
+ const message = restoreCompactTail(item);
103
+ if (message) {
102
104
  messages.push(message);
105
+ if (message.role === 'user' && message.contextKind === undefined)
106
+ userMessages.push({
107
+ content: message.content,
108
+ ...(message.metadata ? { metadata: message.metadata } : {})
109
+ });
110
+ }
103
111
  }
104
112
  }
105
113
  else if (record.type === 'context.environment') {
@@ -122,6 +130,8 @@ export async function restoreMessages(store, sessionId, records, options) {
122
130
  }
123
131
  }
124
132
  }
133
+ for (const message of messages.slice(messageStart))
134
+ options.messageSources?.set(message, record);
125
135
  }
126
136
  const completedToolCalls = new Set(messages.filter((message) => message.role === 'tool').map((message) => message.callId));
127
137
  return {
@@ -48,7 +48,9 @@ export function latestModelUsage(records, selectedModelId) {
48
48
  else if (invalidatesUsageCalibration(record.type, payload.type))
49
49
  usage = undefined;
50
50
  }
51
- if (payload.type === 'usage.updated' && activeModelId === selectedModelId) {
51
+ if (payload.type === 'usage.updated' &&
52
+ payload.purpose !== 'compact' &&
53
+ activeModelId === selectedModelId) {
52
54
  const value = payload.contextInputTokens ?? payload.inputTokens;
53
55
  if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
54
56
  usage = {
@@ -11,7 +11,7 @@ export function toolEventSummary(text) {
11
11
  /** Keep streaming deltas live-only; exec output has one durable copy in model context records. */
12
12
  export function eventForPersistence(event) {
13
13
  if (event.type === 'message.delta' ||
14
- event.type === 'context.compacting' ||
14
+ (event.type === 'context.compacting' && event.async !== true && event.stage === undefined) ||
15
15
  event.type === 'model.performance' ||
16
16
  event.type === 'message.performance')
17
17
  return undefined;
@@ -9,6 +9,10 @@ export interface MarAgentEventBase {
9
9
  executionId: string;
10
10
  turnId?: string;
11
11
  }
12
+ export interface CompactSnapshotAnchor {
13
+ readonly kind: 'message' | 'tool';
14
+ readonly id: string;
15
+ }
12
16
  export type MarAgentEvent = MarAgentEventBase & ({
13
17
  type: 'execution.started';
14
18
  mode: ExecutionMode;
@@ -97,6 +101,7 @@ export type MarAgentEvent = MarAgentEventBase & ({
97
101
  truncated?: boolean;
98
102
  } | {
99
103
  type: 'usage.updated';
104
+ purpose?: 'compact';
100
105
  inputTokens?: number | undefined;
101
106
  contextInputTokens?: number | undefined;
102
107
  outputTokens?: number | undefined;
@@ -145,8 +150,24 @@ export type MarAgentEvent = MarAgentEventBase & ({
145
150
  } | {
146
151
  type: 'context.compacting';
147
152
  compactionId: string;
153
+ async?: boolean;
154
+ stage?: 'GENERATING' | 'READY' | 'WAITING';
155
+ anchorRecordId?: string;
156
+ anchor?: CompactSnapshotAnchor;
148
157
  } | {
149
158
  type: 'context.compacted';
150
159
  compactionId: string;
160
+ async?: boolean;
161
+ retainedContext?: boolean;
162
+ anchorRecordId?: string;
163
+ anchor?: CompactSnapshotAnchor;
164
+ } | {
165
+ type: 'context.compact_failed';
166
+ compactionId: string;
167
+ code: string;
168
+ async?: boolean;
169
+ cancelled?: boolean;
170
+ anchorRecordId?: string;
171
+ anchor?: CompactSnapshotAnchor;
151
172
  });
152
173
  export type EventPayload<T = MarAgentEvent> = T extends MarAgentEvent ? Omit<T, 'eventId' | 'sequence' | 'timestamp' | 'sessionId' | 'executionId' | 'turnId'> : never;
@@ -3,6 +3,7 @@ export declare const AGENT_EXECUTION_POLICY: Readonly<{
3
3
  toolEventSummaryCharacters: 2000;
4
4
  modelToolOutputTokens: 10000;
5
5
  automaticCompactionPercent: 90;
6
+ backgroundCompactionPercent: 85;
6
7
  subagentVerificationCharacters: 500;
7
8
  estimatedTokensPerImage: 1000;
8
9
  }>;
@@ -3,6 +3,7 @@ export const AGENT_EXECUTION_POLICY = Object.freeze({
3
3
  toolEventSummaryCharacters: 2_000,
4
4
  modelToolOutputTokens: 10_000,
5
5
  automaticCompactionPercent: 90,
6
+ backgroundCompactionPercent: 85,
6
7
  subagentVerificationCharacters: 500,
7
8
  estimatedTokensPerImage: 1_000
8
9
  });
@@ -27,6 +27,7 @@ export interface ExecutionLoopInput {
27
27
  readonly compactionFallback?: (() => boolean) | undefined;
28
28
  readonly observeUsage: (usage: Omit<AuthoritativeModelUsage, 'historyMessages'> | undefined) => void;
29
29
  readonly observeContextTokens: (tokens: number) => void;
30
+ readonly afterModel?: (closedPrefixLength: number) => Promise<void>;
30
31
  readonly checkpoint: (round: PreparedExecutionRound, usage: ModelUsage | undefined) => Promise<void>;
31
32
  readonly mailbox: {
32
33
  hasPending(): boolean;
@@ -299,6 +299,7 @@ export class ExecutionLoop {
299
299
  }
300
300
  recoveringFromContextOverflow = false;
301
301
  input.observeUsage(roundServerUsage);
302
+ await input.afterModel?.(pendingTools.length ? roundMessageCount : messages.length);
302
303
  const toolResults = await executeRoundTools(pendingTools, round.toolScope);
303
304
  for (const result of toolResults) {
304
305
  if ('images' in result && result.images?.length)
@@ -88,13 +88,15 @@ export function createExtensionSessionBackend(ports) {
88
88
  }
89
89
  const current = request.kind === 'compact' ? coordinator.currentCompact() : undefined;
90
90
  if (current)
91
- return current;
91
+ throw new MarAgentError('MAR_AGENT_COMPACT_REENTRANT', 'Recursive compaction is not allowed.');
92
92
  const idle = !ports.activeExecutionId() && !coordinator.executing;
93
93
  const entry = coordinator.accept(request, call, action, request.kind === 'submit' ? 'accepted' : 'queued');
94
94
  kick();
95
95
  if (idle && request.kind === 'submit' && request.input.waitFor === 'start')
96
96
  await entry.started;
97
- else if ((idle && request.kind !== 'submit') ||
97
+ else if ((idle &&
98
+ request.kind !== 'submit' &&
99
+ !(request.kind === 'compact' && request.input.async)) ||
98
100
  (request.kind === 'submit' && request.input.waitFor === 'commit'))
99
101
  await entry.terminal;
100
102
  return Object.freeze({ ...entry.receipt });
@@ -9,6 +9,7 @@ import type { CoreAgentsBackend } from '../extensions/agents.js';
9
9
  import type { HostDescription, MarAgentHost } from '../host/contracts.js';
10
10
  import { type MarAgentReasoningEffort } from '../model/configuration.js';
11
11
  import type { ModelAdapter, ModelMessage } from '../model/contracts.js';
12
+ import { CompactRequests } from '../runtime/compaction/requests.js';
12
13
  import { JsonlSessionStore } from '../session/jsonl-store.js';
13
14
  import { SubagentSessionController, type SubagentMailboxMessage, type SubagentMessageDelivery, type SubagentTurnHandle } from '../subagent/session-controller.js';
14
15
  import { createSessionToolScheduler } from '../tools/scheduler.js';
@@ -24,6 +25,7 @@ export interface SessionRuntime {
24
25
  subagents?: SubagentSessionController;
25
26
  subagentsOpening?: Promise<SubagentSessionController>;
26
27
  inputs?: SessionInputCoordinator;
28
+ compactRequests?: CompactRequests;
27
29
  requestSnapshots?: ExtensionRequestSnapshots;
28
30
  requestMessages?: {
29
31
  readonly executionId: string;