@myagentroam/agent 0.10.48 → 0.10.49

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.
@@ -111,6 +111,11 @@ export interface CoreSessionBackend {
111
111
  handled(input: CoreSubmitInput, call: CoreOperationContext, value?: unknown): CoreSessionReceipt;
112
112
  }
113
113
  export interface SessionInputEntry {
114
+ /** SDK-owned admission barrier, never accepted from Extension input. */
115
+ nativeSteer?: {
116
+ readonly ready: Promise<void>;
117
+ recordId?: string;
118
+ };
114
119
  readonly request: CoreSessionRequest;
115
120
  readonly source: CoreOperationContext;
116
121
  readonly action: CoreSessionAction;
@@ -129,6 +134,9 @@ export declare class SessionInputCoordinator {
129
134
  readonly diagnose: (receipt: CoreSessionReceipt) => void;
130
135
  constructor(diagnose?: (receipt: CoreSessionReceipt) => void);
131
136
  get hasPending(): boolean;
137
+ get boundarySize(): number;
138
+ get hasIdleBarrier(): boolean;
139
+ restoreSteer(recordId: string, text: string, source: CoreOperationContext): void;
132
140
  get hasBoundaryPending(): boolean;
133
141
  get executing(): boolean;
134
142
  get hasBoundaryCompactPending(): boolean;
@@ -144,6 +152,8 @@ export declare class SessionInputCoordinator {
144
152
  readonly executionId: string;
145
153
  readonly turnId: string;
146
154
  readonly signal: AbortSignal;
155
+ /** An already authorized idle launch restores older durable input before its own prompt. */
156
+ readonly restoring?: true;
147
157
  }, limit?: number): Promise<boolean>;
148
158
  idle(): Promise<void>;
149
159
  retire(instanceId?: string): Promise<void>;
@@ -36,6 +36,29 @@ export class SessionInputCoordinator {
36
36
  get hasPending() {
37
37
  return this.#pending.length > 0;
38
38
  }
39
+ get boundarySize() {
40
+ return this.#pending.length;
41
+ }
42
+ get hasIdleBarrier() {
43
+ return this.#pending.some((entry) => entry.receipt.status !== 'cancelled' &&
44
+ !entry.source.signal.aborted &&
45
+ entry.request.kind === 'submit' &&
46
+ entry.request.input.delivery === 'idle');
47
+ }
48
+ restoreSteer(recordId, text, source) {
49
+ const index = this.#pending.findIndex((entry) => entry.nativeSteer?.recordId === recordId);
50
+ const previous = this.#pending[index];
51
+ if (previous &&
52
+ !previous.source.signal.aborted &&
53
+ !['cancelled', 'failed'].includes(previous.receipt.status))
54
+ return;
55
+ const entry = this.#record({ kind: 'submit', input: { text } }, source, (request, call, next) => next(request, call), 'accepted');
56
+ entry.nativeSteer = { recordId, ready: Promise.resolve() };
57
+ if (index >= 0)
58
+ this.#pending[index] = entry;
59
+ else
60
+ this.#pending.unshift(entry);
61
+ }
39
62
  get hasBoundaryPending() {
40
63
  const head = this.#pending.find((entry) => entry.receipt.status !== 'cancelled' && !entry.source.signal.aborted);
41
64
  return !!head && (head.request.kind !== 'submit' || head.request.input.delivery !== 'idle');
@@ -169,13 +192,19 @@ export class SessionInputCoordinator {
169
192
  return entry;
170
193
  }
171
194
  drain(leaf, execution, limit = Number.POSITIVE_INFINITY) {
172
- if (this.#draining)
195
+ if (this.#draining && (!this.executing || !execution?.restoring))
173
196
  return this.executing ? Promise.resolve(false) : this.#draining;
174
- this.#draining = (async () => {
197
+ const drain = async () => {
175
198
  let changed = false;
176
199
  let consumed = 0;
177
200
  while (this.#pending.length && consumed++ < limit) {
178
201
  const head = this.#pending[0];
202
+ // Accepted guidance is never permission to launch a new idle execution.
203
+ if (!execution && head.nativeSteer) {
204
+ this.#pending.shift();
205
+ this.#finish(head, 'cancelled');
206
+ continue;
207
+ }
179
208
  // An idle prompt is a new Turn, not a mailbox delivery into the active Turn.
180
209
  // Preserve the single FIFO: entries behind it cannot overtake it.
181
210
  if (execution &&
@@ -208,7 +237,10 @@ export class SessionInputCoordinator {
208
237
  changed ||= applied;
209
238
  }
210
239
  return changed;
211
- })().finally(() => {
240
+ };
241
+ if (this.#draining)
242
+ return drain();
243
+ this.#draining = drain().finally(() => {
212
244
  this.#draining = undefined;
213
245
  });
214
246
  return this.#draining;
@@ -223,6 +255,14 @@ export class SessionInputCoordinator {
223
255
  let effects = 0;
224
256
  let committed;
225
257
  try {
258
+ if (entry.nativeSteer) {
259
+ await entry.nativeSteer.ready;
260
+ call.signal.throwIfAborted();
261
+ if (!entry.nativeSteer.recordId) {
262
+ this.#finish(entry, 'handled', { changed: false });
263
+ return false;
264
+ }
265
+ }
226
266
  const outcome = await this.#current.run(entry, () => entry.action(entry.request, call, async (request, effective) => {
227
267
  validateCoreSessionRequest(request);
228
268
  effective.signal.throwIfAborted();
@@ -254,6 +294,8 @@ export class SessionInputCoordinator {
254
294
  committed = physicalCommits.get(entry) ?? committed;
255
295
  changed ||= (committed?.modelChanged ?? committed?.changed) || !!entry.receipt.changed;
256
296
  this.#finish(entry, call.signal.aborted ? 'cancelled' : 'failed', committed, error instanceof MarAgentError ? error.code : 'MAR_AGENT_SESSION_INPUT_FAILED');
297
+ if (entry.nativeSteer?.recordId)
298
+ throw error;
257
299
  }
258
300
  finally {
259
301
  physicalCommits.delete(entry);
@@ -22,6 +22,8 @@ export function restoreCompactTail(value) {
22
22
  input.contextKind === 'compact_control')
23
23
  return undefined;
24
24
  if ((input.role === 'assistant' || input.role === 'user') && typeof input.content === 'string') {
25
+ if (input.inputRef !== undefined)
26
+ return restoreModelContext(input);
25
27
  const projected = restoreModelContext(input);
26
28
  const metadata = parseModelInputMetadata(input.metadata);
27
29
  const contextKind = ['environment', 'contextual', 'summary'].includes(String(input.contextKind))
@@ -1,4 +1,6 @@
1
1
  import { anthropicThinkingBlock } from '../../model/anthropic-thinking.js';
2
+ import { appliedInputRecord } from '../../session/deferred-input.js';
3
+ import { inputMetadataFromRecord } from '../../session/jsonl-store.js';
2
4
  import { contextGcRecordSourceHash, contextGcTargetKey, latestValidContextGcCheckpoint } from '../../session/context-gc-checkpoint.js';
3
5
  import { TOOL_EXECUTION_LIMITS } from '../../tools/execution-limits.js';
4
6
  import { replaceReadContextItems } from '../../tools/local/read-context.js';
@@ -19,7 +21,14 @@ export function buildContextProjection(records, workspace, checkpointCache) {
19
21
  }
20
22
  if (record.type !== 'model.context' || !isRecord(record.payload))
21
23
  continue;
22
- const message = restoreModelContext(record.payload, activeModelId);
24
+ const applied = appliedInputRecord(record);
25
+ const message = applied
26
+ ? {
27
+ role: 'user',
28
+ content: record.payload.content,
29
+ metadata: inputMetadataFromRecord(applied)
30
+ }
31
+ : restoreModelContext(record.payload, activeModelId);
23
32
  if (message)
24
33
  rawEntries.push({ record, message });
25
34
  }
@@ -133,6 +142,14 @@ export function filterPairedContext(messages, messageSources) {
133
142
  };
134
143
  }
135
144
  export function restoreModelContext(payload, providerModelId) {
145
+ if (payload.inputRef !== undefined) {
146
+ const input = appliedInputRecord({ type: 'model.context', payload });
147
+ return {
148
+ role: 'user',
149
+ content: payload.content,
150
+ metadata: inputMetadataFromRecord(input)
151
+ };
152
+ }
136
153
  const nativeItem = payload.role === 'provider' && payload.provider === 'OPENAI_RESPONSES' && isRecord(payload.item)
137
154
  ? payload.item
138
155
  : undefined;
@@ -13,6 +13,7 @@ export declare function restoreMessages(store: JsonlSessionStore, sessionId: str
13
13
  isError: boolean;
14
14
  result?: unknown;
15
15
  }>;
16
+ pendingInputs: Map<string, import("../../session/jsonl-store.js").SessionRecord>;
16
17
  environmentState: EnvironmentContextState | undefined;
17
18
  userMessages: CompactUserInput[];
18
19
  messages: ModelMessage[];
@@ -1,4 +1,5 @@
1
1
  import { MarAgentError } from '../../error.js';
2
+ import { appliedInputRecord, pendingDeferredInputs } from '../../session/deferred-input.js';
2
3
  import { compactSummaryFromCheckpoint, replacementHistoryFromCheckpoint, inputMetadataFromRecord } from '../../session/jsonl-store.js';
3
4
  import { compactedHistoryMessages, continuationMessage } from '../compaction/history.js';
4
5
  import { restoreCompactTail } from '../compaction/retained-context.js';
@@ -14,9 +15,12 @@ export async function restoreMessages(store, sessionId, records, options) {
14
15
  let ignoredImages = 0;
15
16
  let currentTurnUserIndex;
16
17
  let environmentState;
18
+ const pendingInputs = pendingDeferredInputs(records);
17
19
  for (const record of records) {
18
20
  const messageStart = messages.length;
19
- const payload = record.payload;
21
+ const applied = appliedInputRecord(record);
22
+ const inputRecord = applied ?? record;
23
+ const payload = inputRecord.payload;
20
24
  if ((record.type === 'tool.completed' || record.type === 'tool.failed') &&
21
25
  typeof payload.callId === 'string') {
22
26
  const result = record.type === 'tool.failed' ? payload.message : payload.data;
@@ -26,10 +30,12 @@ export async function restoreMessages(store, sessionId, records, options) {
26
30
  });
27
31
  }
28
32
  const compactSummary = compactSummaryFromCheckpoint(record);
29
- if ((record.type === 'turn.user' || record.type === 'extension.input') &&
33
+ if ((applied ||
34
+ ((record.type === 'turn.user' || record.type === 'extension.input') &&
35
+ payload.deferred !== true)) &&
30
36
  typeof payload.prompt === 'string') {
31
37
  const content = typeof payload.modelText === 'string' ? payload.modelText : payload.prompt;
32
- const metadata = inputMetadataFromRecord(record);
38
+ const metadata = inputMetadataFromRecord(inputRecord);
33
39
  userMessages.push({ content, metadata });
34
40
  let images = [];
35
41
  if (options.includeImages)
@@ -101,6 +107,16 @@ export async function restoreMessages(store, sessionId, records, options) {
101
107
  continue;
102
108
  const message = restoreCompactTail(item);
103
109
  if (message) {
110
+ if (options.includeImages && item.inputRef !== undefined) {
111
+ const input = appliedInputRecord({
112
+ ...record,
113
+ type: 'model.context',
114
+ payload: item
115
+ });
116
+ const images = await store.readInputImages(sessionId, input.payload.images);
117
+ if (images.length)
118
+ message.images = images;
119
+ }
104
120
  messages.push(message);
105
121
  if (message.role === 'user' && message.contextKind === undefined)
106
122
  userMessages.push({
@@ -136,6 +152,7 @@ export async function restoreMessages(store, sessionId, records, options) {
136
152
  const completedToolCalls = new Set(messages.filter((message) => message.role === 'tool').map((message) => message.callId));
137
153
  return {
138
154
  toolOutcomes,
155
+ pendingInputs,
139
156
  environmentState,
140
157
  userMessages,
141
158
  messages: messages.filter((message) => {
@@ -11,6 +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 === 'input.received' ||
14
15
  (event.type === 'context.compacting' && event.async !== true && event.stage === undefined) ||
15
16
  event.type === 'model.performance' ||
16
17
  event.type === 'message.performance')
@@ -14,6 +14,11 @@ export interface CompactSnapshotAnchor {
14
14
  readonly id: string;
15
15
  }
16
16
  export type MarAgentEvent = MarAgentEventBase & ({
17
+ type: 'input.received';
18
+ entry: Extract<import('../session/catalog.js').MarAgentHistoryEntry, {
19
+ kind: 'user';
20
+ }>;
21
+ } | {
17
22
  type: 'execution.started';
18
23
  mode: ExecutionMode;
19
24
  permissionMode?: import('../tools/permissions.js').PermissionMode;
@@ -0,0 +1,19 @@
1
+ import type { CoreOperationContext } from '../extensions/operation.js';
2
+ import { type CorePromptInput, type CorePromptResult } from '../extensions/prompt-input.js';
3
+ import type { SessionInputCoordinator } from '../extensions/session-input.js';
4
+ import type { SessionRecord } from '../session/jsonl-store.js';
5
+ import type { SteerInput, SteerResult } from './types.js';
6
+ /** Admission reserves the existing FIFO before any asynchronous Hook or writer work. */
7
+ export declare function createNativeSteer(input: {
8
+ readonly executionId: string;
9
+ readonly inputs: SessionInputCoordinator;
10
+ readonly signal: AbortSignal;
11
+ isOpen(): boolean;
12
+ operation<T>(action: (call: CoreOperationContext) => T): T;
13
+ admit(content: CorePromptInput, call: CoreOperationContext, write: (content: CorePromptInput, call: CoreOperationContext) => Promise<CorePromptResult>): Promise<unknown>;
14
+ receive(content: CorePromptInput, clientMessageId: string, call: CoreOperationContext): Promise<SessionRecord>;
15
+ notify(entry: Extract<SteerResult, {
16
+ status: 'accepted';
17
+ }>['entry']): Promise<unknown>;
18
+ diagnose(): void;
19
+ }): (request: SteerInput) => Promise<SteerResult>;
@@ -0,0 +1,83 @@
1
+ import { MarAgentError } from '../error.js';
2
+ import { captureCorePromptInput } from '../extensions/prompt-input.js';
3
+ import { imageMetadata } from '../session/catalog.js';
4
+ /** Admission reserves the existing FIFO before any asynchronous Hook or writer work. */
5
+ export function createNativeSteer(input) {
6
+ const retries = new Map();
7
+ let admissionTail = Promise.resolve();
8
+ return async (request) => {
9
+ if (request.expectedExecutionId !== input.executionId)
10
+ return Promise.resolve({ status: 'notAccepted' });
11
+ const previous = retries.get(request.clientMessageId);
12
+ if (previous)
13
+ return previous;
14
+ if (!input.isOpen() || input.signal.aborted || input.inputs.hasIdleBarrier)
15
+ return Promise.resolve({ status: 'notAccepted' });
16
+ if (typeof request.clientMessageId !== 'string' ||
17
+ !request.clientMessageId.trim() ||
18
+ request.clientMessageId.length > 512)
19
+ return Promise.reject(new MarAgentError('MAR_AGENT_SESSION_INPUT_INVALID', 'A stable client message ID is required.'));
20
+ const content = captureCorePromptInput({
21
+ text: request.prompt,
22
+ ...(request.modelText === undefined ? {} : { modelText: request.modelText }),
23
+ ...(request.images === undefined ? {} : { images: request.images })
24
+ });
25
+ const result = Promise.withResolvers();
26
+ const ready = Promise.withResolvers();
27
+ const entry = input.operation((call) => input.inputs.accept({ kind: 'submit', input: content }, call, (request, effective, next) => next(request, effective), 'accepted'));
28
+ entry.nativeSteer = { ready: ready.promise };
29
+ const aborted = () => ready.resolve();
30
+ input.signal.addEventListener('abort', aborted, { once: true });
31
+ let committed = false;
32
+ const admission = admissionTail
33
+ .catch(() => undefined)
34
+ .then(() => input.operation((call) => input.admit(content, call, async (actual, effective) => {
35
+ if (committed)
36
+ return {};
37
+ effective.signal.throwIfAborted();
38
+ if (!input.isOpen()) {
39
+ result.resolve({ status: 'notAccepted' });
40
+ return {};
41
+ }
42
+ const record = await input.receive(actual, request.clientMessageId, effective);
43
+ committed = true;
44
+ entry.nativeSteer.recordId = record.recordId;
45
+ const payload = record.payload;
46
+ const images = imageMetadata(payload.images);
47
+ const historyEntry = {
48
+ kind: 'user',
49
+ recordId: record.recordId,
50
+ sequence: record.sequence,
51
+ timestamp: record.timestamp,
52
+ sessionId: record.sessionId,
53
+ executionId: record.executionId,
54
+ turnId: record.turnId,
55
+ text: payload.prompt,
56
+ clientMessageId: request.clientMessageId,
57
+ ...(images.length ? { images } : {})
58
+ };
59
+ // Neither Hook post-processing nor a failed observer can revoke the writer's receipt.
60
+ void input.notify(historyEntry).catch(() => input.diagnose());
61
+ ready.resolve();
62
+ result.resolve({ status: 'accepted', entry: historyEntry });
63
+ return {};
64
+ })));
65
+ void admission
66
+ .then(() => {
67
+ if (!committed)
68
+ result.resolve({ status: 'handled' });
69
+ }, (error) => {
70
+ if (!committed)
71
+ result.reject(error);
72
+ else
73
+ input.diagnose();
74
+ })
75
+ .finally(() => {
76
+ ready.resolve();
77
+ input.signal.removeEventListener('abort', aborted);
78
+ });
79
+ admissionTail = result.promise;
80
+ retries.set(request.clientMessageId, result.promise);
81
+ return result.promise;
82
+ };
83
+ }
@@ -16,6 +16,7 @@ import { createSessionToolScheduler } from '../tools/scheduler.js';
16
16
  import { TodoStore } from '../tools/agent/todo.js';
17
17
  import type { CreateMarAgentOptions, ExecutionResult, MarAgentSession } from './types.js';
18
18
  export interface SessionRuntime {
19
+ steer?: (input: import('./types.js').SteerInput) => Promise<import('./types.js').SteerResult>;
19
20
  todo?: TodoStore;
20
21
  readonly scheduleTool: ReturnType<typeof createSessionToolScheduler>;
21
22
  readonly sourceType: 'user' | 'subagent';
@@ -1,6 +1,7 @@
1
1
  import { isProviderContextOverflow, recoverModelStream } from '../model/recovery.js';
2
2
  import { estimatedRequestTokens } from '../runtime/context/request-budget.js';
3
3
  import { restoreMessages } from '../runtime/context/restore.js';
4
+ import { createNativeSteer } from './native-steer.js';
4
5
  import { lastConfigurationEffort, latestModelUsage, reasoningEffortBaseline, shouldUseNativeCompaction } from '../runtime/context/usage.js';
5
6
  import { splitContextualPrompt } from '../runtime/context/contextual-blocks.js';
6
7
  import { contextualInstructionInsertIndex, insertContextualWorldState, latestContextualWorldStateGeneration, parsePersistedContextualWorldState, persistContextualWorldState, resetContextualWorldState, updateContextualWorldState, updatePromptWorldState } from '../runtime/context/world-state.js';
@@ -274,6 +275,7 @@ export function createSessionOpener(services) {
274
275
  for (const message of internal.initialMailbox ?? [])
275
276
  acceptParent(message);
276
277
  let acceptsMailbox = sessionSource.type === 'subagent' && mode !== 'compact';
278
+ let acceptsSteer = false;
277
279
  let sequence = 0;
278
280
  let emitTail = Promise.resolve();
279
281
  let releaseSession;
@@ -411,6 +413,7 @@ export function createSessionOpener(services) {
411
413
  includeImages: mode !== 'compact'
412
414
  });
413
415
  const messages = restored.messages;
416
+ const pendingInputs = restored.pendingInputs;
414
417
  liveModelMessages = messages;
415
418
  const restoredRecordIds = new Set(contextRecords.map((record) => record.recordId));
416
419
  for (const pending of pendingAppends)
@@ -472,7 +475,7 @@ export function createSessionOpener(services) {
472
475
  messages.push({ role: 'user', content: text, contextKind: 'contextual' });
473
476
  });
474
477
  };
475
- const appendMailboxMessages = async () => inputs.drain(async (entry, request, call) => {
478
+ const appendMailboxMessages = async (restoring = false) => inputs.drain(async (entry, request, call) => {
476
479
  call.signal.throwIfAborted();
477
480
  if (request.kind === 'compact') {
478
481
  if (request.input.async) {
@@ -486,6 +489,63 @@ export function createSessionOpener(services) {
486
489
  const result = await appendExtensionMessage(entry, request, call);
487
490
  return { ...result, executionId };
488
491
  }
492
+ if (entry.nativeSteer?.recordId) {
493
+ const received = pendingInputs.get(entry.nativeSteer.recordId);
494
+ if (!received)
495
+ return { changed: false, modelChanged: false };
496
+ return withRecordProjection(async () => {
497
+ call.signal.throwIfAborted();
498
+ const payload = received.payload;
499
+ const images = await store.readInputImages(sessionId, payload.images);
500
+ if (images.length && !selectedModel.inputCapabilities.includes('IMAGE'))
501
+ throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'Selected model does not support image input.');
502
+ const percent = smartContextTokens === undefined
503
+ ? undefined
504
+ : (smartContextTokens * 100) / selectedModel.contextWindowTokens;
505
+ const eligible = sessionSource.type === 'user' &&
506
+ percent !== undefined &&
507
+ percent >= AGENT_EXECUTION_POLICY.smartCompactionPercent &&
508
+ percent < AGENT_EXECUTION_POLICY.automaticCompactionPercent;
509
+ const actual = {
510
+ ...payload,
511
+ ...(eligible ? { smartCompaction: true } : {})
512
+ };
513
+ const content = (actual.modelText ?? actual.prompt);
514
+ const applied = await store.append(sessionId, {
515
+ type: 'model.context',
516
+ payload: {
517
+ role: 'user',
518
+ content,
519
+ inputRef: received.recordId,
520
+ input: actual
521
+ },
522
+ executionId,
523
+ turnId
524
+ }, { signal: call.signal });
525
+ const outcome = {
526
+ changed: true,
527
+ modelChanged: true,
528
+ executionId,
529
+ recordId: received.recordId
530
+ };
531
+ recordSessionInputCommit(entry, outcome);
532
+ pendingInputs.delete(received.recordId);
533
+ const metadata = inputMetadataFromRecord({ ...received, payload: actual });
534
+ const message = {
535
+ role: 'user',
536
+ content,
537
+ metadata,
538
+ ...(images.length ? { images } : {})
539
+ };
540
+ messages.push(message);
541
+ messageSources.set(message, applied);
542
+ compactionUserMessages.push({ content, metadata });
543
+ smartInput = { recordId: received.recordId, eligible, requested: false };
544
+ for (const text of (payload.modelContext ?? payload.context ?? []))
545
+ messages.push({ role: 'user', content: text, contextKind: 'contextual' });
546
+ return outcome;
547
+ });
548
+ }
489
549
  const parent = entry.parentMessage;
490
550
  let outcome;
491
551
  const content = {
@@ -528,7 +588,12 @@ export function createSessionOpener(services) {
528
588
  parent?.delivery?.applied();
529
589
  });
530
590
  return outcome;
531
- }, { executionId, turnId, signal: controller.signal });
591
+ }, {
592
+ executionId,
593
+ turnId,
594
+ signal: controller.signal,
595
+ ...(restoring ? { restoring: true } : {})
596
+ }, inputs.boundarySize);
532
597
  const recordEnvironment = async () => {
533
598
  const message = environmentContextUpdate(executionEnvironment, recordedEnvironment);
534
599
  if (!message)
@@ -698,6 +763,12 @@ export function createSessionOpener(services) {
698
763
  entry: 'native-api',
699
764
  signal: controller.signal
700
765
  }, run);
766
+ if (mode !== 'compact') {
767
+ // Missing entries prepend in reverse order; warm entries retain their FIFO position.
768
+ for (const record of [...pendingInputs.values()].reverse()) {
769
+ operation((call) => inputs.restoreSteer(record.recordId, record.payload.prompt, call));
770
+ }
771
+ }
701
772
  await operation((call) => coreExtensions.ready(call));
702
773
  const captureTools = () => operation((call) => {
703
774
  return coreExtensions.capture(call, false, sourceBindings.filter((binding) => !supplementalBindings.includes(binding) || binding.available?.(call) !== false));
@@ -1061,6 +1132,8 @@ export function createSessionOpener(services) {
1061
1132
  : message), mode === 'compact' ? [] : contextualWorldState.entries, mode !== 'compact' && selectedModel.inputCapabilities.includes('IMAGE'), baseTools().some((tool) => tool.name === 'code_exec'), permissionPolicy.mode === 'auto')
1062
1133
  };
1063
1134
  if (mode !== 'compact') {
1135
+ if (pendingInputs.size)
1136
+ await appendMailboxMessages(true);
1064
1137
  let entered = false;
1065
1138
  const writeInput = async (content, call) => {
1066
1139
  call.signal.throwIfAborted();
@@ -1113,6 +1186,45 @@ export function createSessionOpener(services) {
1113
1186
  await emit({ type: 'execution.completed' });
1114
1187
  return { executionId, status: 'completed', finalAnswer: '', inputHandled: true };
1115
1188
  }
1189
+ acceptsSteer = true;
1190
+ sessionRuntime.steer = createNativeSteer({
1191
+ executionId,
1192
+ inputs,
1193
+ signal: controller.signal,
1194
+ isOpen: () => acceptsSteer,
1195
+ operation,
1196
+ admit: (content, call, write) => invokeCorePromptSubmission(coreExtensions.hooks, content, call, write),
1197
+ receive: (content, clientMessageId, call) => withRecordProjection(async () => {
1198
+ call.signal.throwIfAborted();
1199
+ if (content.images?.length && !selectedModel.inputCapabilities.includes('IMAGE'))
1200
+ throw new MarAgentError('MAR_AGENT_INPUT_CAPABILITY_UNSUPPORTED', 'Selected model does not support image input.');
1201
+ const images = content.images?.length
1202
+ ? await store.persistInputImages(sessionId, content.images)
1203
+ : [];
1204
+ const record = await store.append(sessionId, {
1205
+ type: 'turn.user',
1206
+ payload: {
1207
+ deferred: true,
1208
+ prompt: content.text,
1209
+ ...(content.modelText === undefined
1210
+ ? {}
1211
+ : { modelText: content.modelText }),
1212
+ ...(content.context === undefined ? {} : { context: content.context }),
1213
+ ...(content.modelContext === undefined
1214
+ ? {}
1215
+ : { modelContext: content.modelContext }),
1216
+ clientMessageId,
1217
+ ...(images.length ? { images } : {})
1218
+ },
1219
+ executionId,
1220
+ turnId
1221
+ }, { signal: call.signal });
1222
+ pendingInputs.set(record.recordId, record);
1223
+ return record;
1224
+ }),
1225
+ notify: (entry) => emit({ type: 'input.received', entry }),
1226
+ diagnose: () => coreExtensions.diagnose({ code: 'MAR_AGENT_STEER_POST_COMMIT_FAILED', sessionId })
1227
+ });
1116
1228
  }
1117
1229
  if (mode !== 'compact' &&
1118
1230
  useEffortUpdates &&
@@ -1360,7 +1472,22 @@ export function createSessionOpener(services) {
1360
1472
  const record = await store.compact(sessionId, {
1361
1473
  summary: actual,
1362
1474
  replacementHistory,
1363
- retainedModelContext,
1475
+ retainedModelContext: retainedModelContext.map((message) => {
1476
+ const source = messageSources.get(message);
1477
+ const saved = source?.type === 'model.context' &&
1478
+ source.payload.inputRef
1479
+ ? source.payload
1480
+ : source?.type === 'compact.completed'
1481
+ ? (source.payload.retainedModelContext ?? []).find((item) => item.inputRef === message.metadata?.recordId)
1482
+ : undefined;
1483
+ return saved
1484
+ ? {
1485
+ ...saved,
1486
+ ...(message.metadata ? { metadata: message.metadata } : {})
1487
+ }
1488
+ : message;
1489
+ }),
1490
+ pendingInputs: [...pendingInputs.values()],
1364
1491
  snapshotMessageCount: prefix.length,
1365
1492
  ...(anchorRecordId ? { snapshotRecordId: anchorRecordId } : {}),
1366
1493
  automatic: mode !== 'compact',
@@ -1714,6 +1841,7 @@ export function createSessionOpener(services) {
1714
1841
  append: appendMailboxMessages,
1715
1842
  close: () => {
1716
1843
  acceptsMailbox = false;
1844
+ acceptsSteer = false;
1717
1845
  }
1718
1846
  }
1719
1847
  });
@@ -1750,6 +1878,9 @@ export function createSessionOpener(services) {
1750
1878
  catch (error) {
1751
1879
  finalAnswer = executionLoop?.finalAnswer ?? finalAnswer;
1752
1880
  acceptsMailbox = false;
1881
+ acceptsSteer = false;
1882
+ delete sessionRuntime.steer;
1883
+ await sessionRuntime.recordProjection?.catch(() => undefined);
1753
1884
  await compactCoordinator.close(error);
1754
1885
  compactRequests.close(new MarAgentError('MAR_AGENT_CANCELLED', 'Execution ended.'));
1755
1886
  if (sessionRuntime.compactRequests === compactRequests)
@@ -2078,6 +2209,7 @@ export function createSessionOpener(services) {
2078
2209
  id: sessionId,
2079
2210
  run: (input) => execution('run', input),
2080
2211
  plan: (input) => execution('plan', input),
2212
+ steer: (input) => sessionRuntime.steer?.(input) ?? Promise.resolve({ status: 'notAccepted' }),
2081
2213
  compact: (input = {}) => sessionRuntime.compactRequests?.request(input) ??
2082
2214
  execution('compact', {
2083
2215
  prompt: input.prompt ?? DEFAULT_COMPACTION_FOCUS,
@@ -24,11 +24,29 @@ export interface ExecutionHandle extends AsyncIterable<MarAgentEvent> {
24
24
  export type CompactInput = Partial<RunInput> & {
25
25
  readonly async?: boolean;
26
26
  };
27
+ export interface SteerInput {
28
+ readonly expectedExecutionId: string;
29
+ readonly prompt: string;
30
+ readonly modelText?: string;
31
+ readonly clientMessageId: string;
32
+ readonly images?: RunInput['images'];
33
+ }
34
+ export type SteerResult = {
35
+ readonly status: 'accepted';
36
+ readonly entry: Extract<import('../session/catalog.js').MarAgentHistoryEntry, {
37
+ kind: 'user';
38
+ }>;
39
+ } | {
40
+ readonly status: 'handled';
41
+ } | {
42
+ readonly status: 'notAccepted';
43
+ };
27
44
  /** A persisted Session permits one active main execution at a time. */
28
45
  export interface MarAgentSession {
29
46
  readonly id: string;
30
47
  run(input: RunInput): ExecutionHandle;
31
48
  plan(input: RunInput): ExecutionHandle;
49
+ steer(input: SteerInput): Promise<SteerResult>;
32
50
  compact(input?: CompactInput): ExecutionHandle;
33
51
  cancel(executionId: string): Promise<void>;
34
52
  }
@@ -23,6 +23,7 @@ export interface MarAgentHistoryImageMetadata extends MarAgentImageMetadata {
23
23
  }
24
24
  export type MarAgentHistoryEntry = {
25
25
  kind: 'user';
26
+ recordId?: string;
26
27
  sequence: number;
27
28
  timestamp: string;
28
29
  sessionId: string;
@@ -86,3 +87,4 @@ export interface MarAgentSessionCatalog {
86
87
  export declare function openMarAgentSessionCatalog(input: {
87
88
  homeDirectory: string;
88
89
  }): Promise<MarAgentSessionCatalog>;
90
+ export declare function imageMetadata(value: unknown): MarAgentHistoryImageMetadata[];
@@ -25,6 +25,7 @@ export async function openMarAgentSessionCatalog(input) {
25
25
  return [
26
26
  {
27
27
  kind: 'user',
28
+ ...(record.payload.deferred === true ? { recordId: record.recordId } : {}),
28
29
  sequence: record.sequence,
29
30
  timestamp: record.timestamp,
30
31
  sessionId: record.sessionId,
@@ -118,7 +119,7 @@ function parseCursor(cursor) {
118
119
  function isRecord(value) {
119
120
  return value !== null && typeof value === 'object' && !Array.isArray(value);
120
121
  }
121
- function imageMetadata(value) {
122
+ export function imageMetadata(value) {
122
123
  if (!Array.isArray(value))
123
124
  return [];
124
125
  return value.flatMap((image) => isRecord(image) &&
@@ -0,0 +1,5 @@
1
+ import type { SessionRecord } from './jsonl-store.js';
2
+ /** The application is self-contained; the receive record may precede the Compact window. */
3
+ export declare function appliedInputRecord(record: SessionRecord): SessionRecord | undefined;
4
+ /** Only the last checkpoint and its tail are needed, never a historical consume index. */
5
+ export declare function pendingDeferredInputs(records: readonly SessionRecord[]): Map<string, SessionRecord>;
@@ -0,0 +1,74 @@
1
+ import { MarAgentError } from '../error.js';
2
+ /** The application is self-contained; the receive record may precede the Compact window. */
3
+ export function appliedInputRecord(record) {
4
+ if (record.type !== 'model.context' || !record.payload || typeof record.payload !== 'object')
5
+ return undefined;
6
+ const payload = record.payload;
7
+ if (payload.inputRef === undefined)
8
+ return undefined;
9
+ const input = payload.input;
10
+ if (payload.role !== 'user' ||
11
+ typeof payload.content !== 'string' ||
12
+ typeof payload.inputRef !== 'string' ||
13
+ !payload.inputRef ||
14
+ !input ||
15
+ typeof input !== 'object' ||
16
+ Array.isArray(input) ||
17
+ typeof input.prompt !== 'string' ||
18
+ input.deferred !== true ||
19
+ payload.content !==
20
+ (input.modelText ?? input.prompt))
21
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Deferred input application is invalid.');
22
+ return { ...record, type: 'turn.user', recordId: payload.inputRef, payload: input };
23
+ }
24
+ /** Only the last checkpoint and its tail are needed, never a historical consume index. */
25
+ export function pendingDeferredInputs(records) {
26
+ const pending = new Map();
27
+ const applied = new Set();
28
+ for (const record of records) {
29
+ const payload = (record.payload ?? {});
30
+ if (record.type === 'compact.completed' && typeof payload.summary === 'string') {
31
+ pending.clear();
32
+ applied.clear();
33
+ if (payload.pendingInputs !== undefined) {
34
+ if (!Array.isArray(payload.pendingInputs))
35
+ corrupt();
36
+ for (const item of payload.pendingInputs) {
37
+ if (!item ||
38
+ item.type !== 'turn.user' ||
39
+ item.sessionId !== record.sessionId ||
40
+ typeof item.recordId !== 'string' ||
41
+ !Number.isSafeInteger(item.sequence) ||
42
+ item.sequence < 1 ||
43
+ item.sequence >= record.sequence ||
44
+ !item.payload ||
45
+ item.payload.deferred !== true ||
46
+ typeof item.payload.prompt !== 'string' ||
47
+ pending.has(item.recordId))
48
+ corrupt();
49
+ pending.set(item.recordId, item);
50
+ }
51
+ }
52
+ }
53
+ else if (record.type === 'turn.user' && payload.deferred === true) {
54
+ if (typeof payload.prompt !== 'string' ||
55
+ pending.has(record.recordId) ||
56
+ applied.has(record.recordId))
57
+ corrupt();
58
+ pending.set(record.recordId, record);
59
+ }
60
+ else {
61
+ const input = appliedInputRecord(record);
62
+ if (!input)
63
+ continue;
64
+ if (applied.has(input.recordId))
65
+ corrupt();
66
+ applied.add(input.recordId);
67
+ pending.delete(input.recordId);
68
+ }
69
+ }
70
+ return new Map([...pending].sort(([, a], [, b]) => a.sequence - b.sequence));
71
+ }
72
+ function corrupt() {
73
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Deferred input state is invalid.');
74
+ }
@@ -193,6 +193,7 @@ export declare class JsonlSessionStore {
193
193
  summary: string;
194
194
  replacementHistory?: readonly CompactHistoryMessage[];
195
195
  retainedModelContext?: readonly ModelMessage[];
196
+ pendingInputs?: readonly SessionRecord[];
196
197
  snapshotRecordId?: string;
197
198
  snapshotMessageCount?: number;
198
199
  environment?: EnvironmentContextState;
@@ -599,16 +599,18 @@ export class JsonlSessionStore {
599
599
  }
600
600
  ];
601
601
  const retained = input.boundaryTurnId === null ? [] : records.slice(1, boundaryIndex + 1);
602
+ const forkIdentities = new Map();
602
603
  for (const record of retained) {
603
604
  if (record.type === 'context.gc.completed' || record.type === 'responses.continuation')
604
605
  continue;
606
+ const identity = { recordId: randomUUID(), sequence: copied.length + 1 };
607
+ forkIdentities.set(record.recordId, identity);
605
608
  copied.push({
606
609
  schemaVersion: 1,
607
- recordId: randomUUID(),
608
- sequence: copied.length + 1,
610
+ ...identity,
609
611
  sessionId: target.id,
610
612
  type: record.type,
611
- payload: forkedPayload(record.type, record.payload, sourceSessionId, target.id, input.title),
613
+ payload: forkedPayload(record.type, record.payload, sourceSessionId, target.id, input.title, forkIdentities),
612
614
  ...(record.executionId === undefined ? {} : { executionId: record.executionId }),
613
615
  ...(record.turnId === undefined ? {} : { turnId: record.turnId }),
614
616
  timestamp: record.timestamp
@@ -1306,6 +1308,7 @@ export class JsonlSessionStore {
1306
1308
  type: 'compact.completed',
1307
1309
  payload: {
1308
1310
  summary: input.summary,
1311
+ ...(input.pendingInputs?.length ? { pendingInputs: input.pendingInputs } : {}),
1309
1312
  ...(input.snapshotRecordId === undefined
1310
1313
  ? {}
1311
1314
  : { snapshotRecordId: input.snapshotRecordId }),
@@ -2380,12 +2383,39 @@ function sessionTreeIds(sessions, sessionId) {
2380
2383
  }
2381
2384
  return ids;
2382
2385
  }
2383
- function forkedPayload(type, value, sourceSessionId, targetSessionId, title) {
2386
+ function forkedPayload(type, value, sourceSessionId, targetSessionId, title, identities) {
2384
2387
  if (value === null || typeof value !== 'object' || Array.isArray(value))
2385
2388
  return value;
2386
2389
  const payload = { ...value };
2387
2390
  if (payload.sessionId === sourceSessionId)
2388
2391
  payload.sessionId = targetSessionId;
2392
+ const application = (input) => {
2393
+ if (typeof input.inputRef !== 'string')
2394
+ return input;
2395
+ const identity = identities.get(input.inputRef);
2396
+ if (!identity)
2397
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork input reference is missing.');
2398
+ return {
2399
+ ...input,
2400
+ inputRef: identity.recordId,
2401
+ ...(isObject(input.metadata)
2402
+ ? { metadata: { ...input.metadata, recordId: identity.recordId } }
2403
+ : {})
2404
+ };
2405
+ };
2406
+ if (type === 'model.context')
2407
+ return application(payload);
2408
+ if (type === 'compact.completed') {
2409
+ if (Array.isArray(payload.retainedModelContext))
2410
+ payload.retainedModelContext = payload.retainedModelContext.map((item) => isObject(item) ? application(item) : item);
2411
+ if (Array.isArray(payload.pendingInputs))
2412
+ payload.pendingInputs = payload.pendingInputs.map((record) => {
2413
+ const identity = identities.get(record.recordId);
2414
+ if (!identity)
2415
+ throw new MarAgentError('MAR_AGENT_SESSION_CORRUPT', 'Fork pending input reference is missing.');
2416
+ return { ...record, ...identity, sessionId: targetSessionId };
2417
+ });
2418
+ }
2389
2419
  if (type === 'session.title.updated') {
2390
2420
  payload.title = title;
2391
2421
  payload.automatic = false;
@@ -123,7 +123,7 @@ export declare class SubagentSessionController implements SubagentController {
123
123
  cancel(agentId: string): Promise<SubagentTaskSnapshot>;
124
124
  listSessionResources(): {
125
125
  agentId: string;
126
- status: "completed" | "failed" | "cancelled" | "running" | "queued" | "interrupted";
126
+ status: "completed" | "failed" | "cancelled" | "running" | "interrupted" | "queued";
127
127
  description: string;
128
128
  }[];
129
129
  dispose(): Promise<void>;
package/dist/index.d.ts CHANGED
@@ -38,7 +38,7 @@ export type { CoreModelsApi, CoreModelDescription, CoreModelTextBlock, CoreGener
38
38
  export type { CoreDataApi, CoreJsonValue, CoreCheckpoint, CoreCheckpointApi, CoreStateApi, CoreStateChange, CoreStoreApi, CoreStoreCommit } from './agent-core/extensions/data.js';
39
39
  export { generateSessionTitle, type GenerateSessionTitleInput } from './shared/auto-title.js';
40
40
  export type { CoreProcessesApi, CoreProcessesBackend, CoreProcessInput, CoreProcessRunInput, CoreProcessHandle, CoreProcessExit, CoreProcessResult, CoreProcessCompletion } from './agent-core/extensions/processes.js';
41
- export type { CreateMarAgentOptions, CreateSessionInput, ExecutionHandle, CompactInput, MarAgent, MarAgentSession, SessionTitleUpdateInput } from './agent-core/sdk/types.js';
41
+ export type { CreateMarAgentOptions, CreateSessionInput, ExecutionHandle, CompactInput, SteerInput, SteerResult, MarAgent, MarAgentSession, SessionTitleUpdateInput } from './agent-core/sdk/types.js';
42
42
  export { openMarAgentSessionCatalog, type MarAgentContextUsage, type MarAgentHistoryEntry, type MarAgentHistoryPage, type MarAgentSessionCatalog, type MarAgentSessionDeleteResult, type MarAgentSessionImage, type MarAgentSessionPage, type MarAgentSessionSummary } from './agent-core/session/catalog.js';
43
43
  export type { SessionListInput, SessionSource } from './agent-core/session/types.js';
44
44
  export type { ApplyPatchFileResult, ApplyPatchResult, ApplyPatchPartialResult, ClientToolDefinition, MarAgentToolArtifact, MarAgentToolImageArtifact, McpCatalogTool, McpToolCallContext, McpToolCatalog, McpToolCatalogContext, ToolCall, ToolCaller, ToolExecutionContext, ToolExecutionResult } from './agent-core/tools/contracts.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myagentroam/agent",
3
- "version": "0.10.48",
3
+ "version": "0.10.49",
4
4
  "description": "Embeddable MAR coding agent SDK and CLI.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",