@myagentroam/agent 0.9.171 → 0.9.173

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 (31) hide show
  1. package/dist/agent-core/extensions/compaction.d.ts +16 -0
  2. package/dist/agent-core/extensions/compaction.js +42 -1
  3. package/dist/agent-core/model/anthropic-messages.js +40 -6
  4. package/dist/agent-core/model/attempt-diagnostics.d.ts +3 -0
  5. package/dist/agent-core/model/attempt-diagnostics.js +9 -0
  6. package/dist/agent-core/model/contracts.d.ts +9 -1
  7. package/dist/agent-core/model/openai-responses.js +4 -11
  8. package/dist/agent-core/prompts/resources.d.ts +0 -1
  9. package/dist/agent-core/prompts/resources.js +4 -14
  10. package/dist/agent-core/runtime/compaction/history.d.ts +3 -2
  11. package/dist/agent-core/runtime/compaction/history.js +42 -12
  12. package/dist/agent-core/runtime/compaction/operation.d.ts +5 -0
  13. package/dist/agent-core/runtime/compaction/operation.js +22 -2
  14. package/dist/agent-core/runtime/context/projection.js +1 -0
  15. package/dist/agent-core/runtime/context/request-budget.js +1 -1
  16. package/dist/agent-core/runtime/context/restore.d.ts +2 -2
  17. package/dist/agent-core/runtime/context/restore.js +24 -8
  18. package/dist/agent-core/runtime/context/world-state.d.ts +2 -5
  19. package/dist/agent-core/runtime/context/world-state.js +14 -14
  20. package/dist/agent-core/sdk/session.js +82 -49
  21. package/dist/agent-core/session/context-gc-replacements.js +14 -3
  22. package/dist/agent-core/session/jsonl-store.d.ts +9 -4
  23. package/dist/agent-core/session/jsonl-store.js +48 -1
  24. package/dist/agent-core/tools/code-mode/observations.js +19 -1
  25. package/dist/agent-core/tools/local/exec.d.ts +3 -1
  26. package/dist/agent-core/tools/local/exec.js +21 -0
  27. package/dist/agent-core/tools/local/registry.js +2 -2
  28. package/dist/index.d.ts +1 -1
  29. package/dist/shared/model-text.d.ts +2 -1
  30. package/dist/shared/model-text.js +1 -0
  31. package/package.json +1 -1
@@ -1,4 +1,16 @@
1
1
  import type { ModelMessage, ModelUsage } from '../model/contracts.js';
2
+ export interface CoreCompactDiagnostic {
3
+ readonly modelId: string;
4
+ readonly providerModelId?: string;
5
+ readonly reasoningEffort: string;
6
+ readonly state: 'STARTED' | 'SUCCEEDED' | 'DECLINED' | 'FAILED';
7
+ readonly inputTokensEstimateBefore: number;
8
+ readonly inputTokensEstimateAfter: number;
9
+ readonly modelRequests: number;
10
+ readonly elapsedMs: number;
11
+ readonly reason?: 'INPUT_BUDGET_EXHAUSTED' | 'MODEL_INCOMPLETE' | 'MODEL_FAILED';
12
+ readonly errorCode?: string;
13
+ }
2
14
  export interface CoreCompactMessage {
3
15
  readonly role: 'user' | 'assistant' | 'tool' | 'tool_call';
4
16
  readonly content: string;
@@ -14,6 +26,8 @@ export interface CoreCompactContext {
14
26
  readonly summaryTokenBudget: number;
15
27
  /** Return the Core compaction contract selected for this operation's main model snapshot. */
16
28
  readonly getPrompt: () => string;
29
+ /** Private, bounded telemetry only; reporting never changes the generation result. */
30
+ readonly reportDiagnostic?: (diagnostic: CoreCompactDiagnostic) => Promise<void>;
17
31
  }
18
32
  export interface CoreCompactResult {
19
33
  readonly summary: string;
@@ -21,5 +35,7 @@ export interface CoreCompactResult {
21
35
  }
22
36
  /** Copy only neutral evidence; a hook never receives provider payloads or history ownership. */
23
37
  export declare function compactExtensionMessages(messages: readonly ModelMessage[]): readonly CoreCompactMessage[];
38
+ /** Whitelist scalar facts; never copy arbitrary plugin payloads into the session log. */
39
+ export declare function compactExtensionDiagnostic(value: unknown): CoreCompactDiagnostic | undefined;
24
40
  /** Invalid plugin results decline generation rather than altering the checkpoint contract. */
25
41
  export declare function compactExtensionResult(value: unknown, budget: number): CoreCompactResult | undefined;
@@ -2,7 +2,8 @@ import { countModelTokens } from '../runtime/token-budget.js';
2
2
  /** Copy only neutral evidence; a hook never receives provider payloads or history ownership. */
3
3
  export function compactExtensionMessages(messages) {
4
4
  return freeze(messages
5
- .filter((message) => message.role !== 'provider' && message.contextKind === undefined)
5
+ .filter((message) => message.role !== 'provider' &&
6
+ !['environment', 'private', 'compact_control'].includes(message.contextKind ?? ''))
6
7
  .map((message) => ({
7
8
  role: message.role,
8
9
  content: message.content,
@@ -23,6 +24,46 @@ const usageKeys = new Set([
23
24
  'hostedSearches',
24
25
  'hostedFetches'
25
26
  ]);
27
+ /** Whitelist scalar facts; never copy arbitrary plugin payloads into the session log. */
28
+ export function compactExtensionDiagnostic(value) {
29
+ if (!value || typeof value !== 'object' || Array.isArray(value))
30
+ return undefined;
31
+ const input = value;
32
+ const label = (value, limit) => typeof value === 'string' &&
33
+ value.length > 0 &&
34
+ value.length <= limit &&
35
+ !/[\r\n]/u.test(value);
36
+ if (!label(input.modelId, 128) ||
37
+ !label(input.reasoningEffort, 32) ||
38
+ typeof input.state !== 'string' ||
39
+ !['STARTED', 'SUCCEEDED', 'DECLINED', 'FAILED'].includes(input.state))
40
+ return undefined;
41
+ for (const key of [
42
+ 'inputTokensEstimateBefore',
43
+ 'inputTokensEstimateAfter',
44
+ 'modelRequests',
45
+ 'elapsedMs'
46
+ ])
47
+ if (typeof input[key] !== 'number' || !Number.isSafeInteger(input[key]) || input[key] < 0)
48
+ return undefined;
49
+ return {
50
+ modelId: input.modelId,
51
+ reasoningEffort: input.reasoningEffort,
52
+ state: input.state,
53
+ inputTokensEstimateBefore: input.inputTokensEstimateBefore,
54
+ inputTokensEstimateAfter: input.inputTokensEstimateAfter,
55
+ modelRequests: input.modelRequests,
56
+ elapsedMs: input.elapsedMs,
57
+ ...(label(input.providerModelId, 256) ? { providerModelId: input.providerModelId } : {}),
58
+ ...(typeof input.reason === 'string' &&
59
+ ['INPUT_BUDGET_EXHAUSTED', 'MODEL_INCOMPLETE', 'MODEL_FAILED'].includes(input.reason)
60
+ ? { reason: input.reason }
61
+ : {}),
62
+ ...(typeof input.errorCode === 'string' && /^[A-Z][A-Z0-9_]{0,127}$/u.test(input.errorCode)
63
+ ? { errorCode: input.errorCode }
64
+ : {})
65
+ };
66
+ }
26
67
  /** Invalid plugin results decline generation rather than altering the checkpoint contract. */
27
68
  export function compactExtensionResult(value, budget) {
28
69
  if (value === null ||
@@ -3,9 +3,10 @@ import { randomUUID } from 'node:crypto';
3
3
  import { MarAgentError } from '../error.js';
4
4
  import { resolveModelDefaultReasoningEffort } from './configuration.js';
5
5
  import { isRetryableUpstreamOverloadCode, number, parseSse, record, text, observeModelStreamEvent, modelStreamEventObservedAt } from './sse.js';
6
- import { fetchModelResponse, modelRetryMaxAttempts, isRetryableModelRequestError, isRetryableModelTransportError, waitForModelRetry } from './http.js';
6
+ import { fetchModelResponse, modelRetryMaxAttempts, isRetryableModelRequestError, isRetryableModelTransportError, modelRetryDelay, waitForModelRetry } from './http.js';
7
7
  import { anthropicThinkingBlock, assertAnthropicThinkingBudget } from './anthropic-thinking.js';
8
8
  import { resolveModelTextBlocks } from './text-blocks.js';
9
+ import { reportModelAttempt } from './attempt-diagnostics.js';
9
10
  export class AnthropicMessagesAdapter {
10
11
  configuration;
11
12
  constructor(configuration) {
@@ -16,13 +17,25 @@ export class AnthropicMessagesAdapter {
16
17
  const maxAttempts = modelRetryMaxAttempts(retryState);
17
18
  let resetOutputTiming = false;
18
19
  while (retryState.attempts < maxAttempts) {
20
+ signal.throwIfAborted();
19
21
  if (resetOutputTiming)
20
22
  yield { type: 'output.reset' };
21
23
  resetOutputTiming = false;
22
24
  const attempt = retryState.attempts++;
25
+ const startedAt = Date.now();
26
+ const attemptDiagnostic = {
27
+ protocol: 'ANTHROPIC_MESSAGES',
28
+ transport: 'HTTP',
29
+ attempt: attempt + 1,
30
+ maxAttempts,
31
+ continuation: false,
32
+ credentialReason: 'REQUEST'
33
+ };
23
34
  // Buffer non-semantic events until a semantic event makes this attempt non-retryable.
24
35
  const prelude = [];
25
36
  let sawSemanticEvent = false;
37
+ let firstSemanticEventType;
38
+ await reportModelAttempt(request, { ...attemptDiagnostic, state: 'STARTED' });
26
39
  try {
27
40
  for await (const event of this.startAttempt(request, signal)) {
28
41
  if (event.type === 'output.delta') {
@@ -36,21 +49,42 @@ export class AnthropicMessagesAdapter {
36
49
  }
37
50
  if (!sawSemanticEvent) {
38
51
  sawSemanticEvent = true;
52
+ firstSemanticEventType = event.type;
39
53
  for (const pending of prelude)
40
54
  yield pending;
41
55
  prelude.length = 0;
42
56
  }
43
57
  yield event;
44
58
  }
59
+ await reportModelAttempt(request, {
60
+ ...attemptDiagnostic,
61
+ state: 'SUCCEEDED',
62
+ semanticEventObserved: sawSemanticEvent,
63
+ ...(firstSemanticEventType === undefined ? {} : { firstSemanticEventType }),
64
+ durationMs: Math.max(0, Date.now() - startedAt)
65
+ });
45
66
  return;
46
67
  }
47
68
  catch (error) {
48
- if (signal.aborted ||
49
- sawSemanticEvent ||
50
- !(isRetryableModelRequestError(error) || isAnthropicRecoverableStreamError(error)) ||
51
- attempt + 1 >= maxAttempts)
69
+ const retryable = isRetryableModelRequestError(error) || isAnthropicRecoverableStreamError(error);
70
+ const willRetry = !signal.aborted && !sawSemanticEvent && retryable && attempt + 1 < maxAttempts;
71
+ const delayMs = willRetry ? modelRetryDelay(retryAfter(error), attempt) : undefined;
72
+ await reportModelAttempt(request, {
73
+ ...attemptDiagnostic,
74
+ state: 'FAILED',
75
+ semanticEventObserved: sawSemanticEvent,
76
+ ...(firstSemanticEventType === undefined ? {} : { firstSemanticEventType }),
77
+ durationMs: Math.max(0, Date.now() - startedAt),
78
+ errorCode: error instanceof MarAgentError ? error.code : 'MAR_AGENT_MODEL_ATTEMPT_FAILED',
79
+ retryable,
80
+ willRetry,
81
+ ...(delayMs === undefined
82
+ ? {}
83
+ : { retryDelayMs: delayMs, retryReason: 'STREAM' })
84
+ });
85
+ if (!willRetry)
52
86
  throw error;
53
- await waitForModelRetry(retryAfter(error), attempt, signal);
87
+ await waitForModelRetry(retryAfter(error), attempt, signal, delayMs);
54
88
  }
55
89
  }
56
90
  }
@@ -0,0 +1,3 @@
1
+ import type { ModelAttemptDiagnostic, ModelRequest } from './contracts.js';
2
+ /** One observer boundary for all adapters; telemetry cannot change request outcomes. */
3
+ export declare function reportModelAttempt(request: ModelRequest, diagnostic: ModelAttemptDiagnostic): Promise<void>;
@@ -0,0 +1,9 @@
1
+ /** One observer boundary for all adapters; telemetry cannot change request outcomes. */
2
+ export async function reportModelAttempt(request, diagnostic) {
3
+ try {
4
+ await request.onAttemptDiagnostic?.(diagnostic);
5
+ }
6
+ catch {
7
+ // Diagnostics must never change the model request result.
8
+ }
9
+ }
@@ -5,6 +5,12 @@ export interface ModelTextBlock {
5
5
  readonly text: string;
6
6
  readonly cache?: true;
7
7
  }
8
+ export interface ModelInputMetadata {
9
+ source: 'user' | 'parent_agent' | 'extension';
10
+ recordId?: string;
11
+ extensionId?: string;
12
+ truncated?: true;
13
+ }
8
14
  export interface ModelMessage {
9
15
  /** Model-facing tool-result status; independent of the actual execution outcome. */
10
16
  isError?: boolean;
@@ -12,7 +18,9 @@ export interface ModelMessage {
12
18
  role: 'user' | 'assistant' | 'tool' | 'tool_call' | 'provider';
13
19
  content: string;
14
20
  textBlocks?: readonly ModelTextBlock[];
15
- contextKind?: 'environment' | 'private';
21
+ contextKind?: 'environment' | 'private' | 'contextual' | 'summary' | 'compact_control';
22
+ /** Input provenance and local truncation state; never serialized to a Provider. */
23
+ metadata?: ModelInputMetadata;
16
24
  callId?: string;
17
25
  name?: string;
18
26
  arguments?: unknown;
@@ -8,6 +8,7 @@ import { isRetryableUpstreamOverloadCode, number, parseSse, record, text, observ
8
8
  import { connectSocks5Target, fetchModelResponse, isRetryableModelHttpStatus, isRetryableModelTransportError, modelRetryMaxAttempts, isRetryableModelRequestError, modelRetryDelay, waitForModelRetry } from './http.js';
9
9
  import { acquireModelCredential, appendModelCredentialHeaders } from './runtime-credential.js';
10
10
  import { resolveModelTextBlocks } from './text-blocks.js';
11
+ import { reportModelAttempt } from './attempt-diagnostics.js';
11
12
  const WEBSOCKET_IDLE_TIMEOUT_MS = 300_000;
12
13
  const WEBSOCKET_SEND_FRAME_BYTES = 1024 * 1024;
13
14
  export const OPENAI_RESPONSES_EARLY_EOF_REASON = 'OPENAI_RESPONSES_EARLY_EOF';
@@ -92,7 +93,7 @@ export class OpenAiResponsesAdapter {
92
93
  let sawSemanticEvent = false;
93
94
  let firstSemanticEventType;
94
95
  turn.beginAttempt();
95
- await reportOpenAiResponsesAttempt(request, {
96
+ await reportModelAttempt(request, {
96
97
  protocol: 'OPENAI_RESPONSES',
97
98
  transport: attemptTransport,
98
99
  state: 'STARTED',
@@ -121,7 +122,7 @@ export class OpenAiResponsesAdapter {
121
122
  }
122
123
  yield event;
123
124
  }
124
- await reportOpenAiResponsesAttempt(request, {
125
+ await reportModelAttempt(request, {
125
126
  protocol: 'OPENAI_RESPONSES',
126
127
  transport: attemptTransport,
127
128
  state: 'SUCCEEDED',
@@ -137,7 +138,7 @@ export class OpenAiResponsesAdapter {
137
138
  return;
138
139
  }
139
140
  catch (error) {
140
- const reportFailure = async (willRetry, options = {}) => reportOpenAiResponsesAttempt(request, {
141
+ const reportFailure = async (willRetry, options = {}) => reportModelAttempt(request, {
141
142
  protocol: 'OPENAI_RESPONSES',
142
143
  transport: attemptTransport,
143
144
  state: 'FAILED',
@@ -1437,14 +1438,6 @@ function retryAfter(error) {
1437
1438
  const value = error.details?.retryAfter;
1438
1439
  return typeof value === 'string' ? value : null;
1439
1440
  }
1440
- async function reportOpenAiResponsesAttempt(request, diagnostic) {
1441
- try {
1442
- await request.onAttemptDiagnostic?.(diagnostic);
1443
- }
1444
- catch {
1445
- // Diagnostics must never change the model request result.
1446
- }
1447
- }
1448
1441
  function openAiResponsesAttemptError(error) {
1449
1442
  const diagnostic = describeMarAgentError(error, 'MAR_AGENT_MODEL_ATTEMPT_FAILED');
1450
1443
  const causeCode = deepestCauseCode(error);
@@ -14,4 +14,3 @@ export interface RetainedSessionResources {
14
14
  }[];
15
15
  }
16
16
  export declare function retainedSessionResourcesSnapshot(input: RetainedSessionResources): string | undefined;
17
- export declare function retainedSessionResourcesUpdate(snapshot: string | undefined, previousSnapshot: string | undefined, previousMayContainResources?: boolean): string | undefined;
@@ -1,3 +1,4 @@
1
+ import { truncateModelText } from '../runtime/token-budget.js';
1
2
  export function retainedSessionResourcesSnapshot(input) {
2
3
  if (input.processes.length === 0 && input.subagents.length === 0 && !input.codeCells?.length)
3
4
  return undefined;
@@ -9,7 +10,7 @@ export function retainedSessionResourcesSnapshot(input) {
9
10
  ...(input.codeCells?.length
10
11
  ? [
11
12
  'Code Exec cells: use direct code_exec_wait with the retained cellId for ongoing work or needed output; cancelling a wait does not terminate the cell.',
12
- ...input.codeCells.map((cell) => `- cellId: ${cell.cellId}; status: ${cell.status}; executionId: ${cell.executionId}`)
13
+ ...input.codeCells.map((cell) => `- cellId: ${cell.cellId}; status: ${cell.status}`)
13
14
  ]
14
15
  : []),
15
16
  processes.length === 0
@@ -25,26 +26,15 @@ export function retainedSessionResourcesSnapshot(input) {
25
26
  ? ''
26
27
  : [
27
28
  'Managed processes:',
28
- ...processes.map((process) => `- processId: ${process.processId}; status: ${process.status}; cwd: ${process.cwd}; command: ${process.command}`)
29
+ ...processes.map((process) => `- processId: ${process.processId}; status: ${process.status}; cwd: ${truncateModelText(process.cwd, 64, 'resource cwd').content}; command: ${truncateModelText(process.command, 128, 'resource command').content}`)
29
30
  ].join('\n'),
30
31
  subagents.length === 0
31
32
  ? ''
32
33
  : [
33
34
  'Subagents:',
34
- ...subagents.map((task) => `- agentId: ${task.agentId}; status: ${task.status}; description: ${task.description}`)
35
+ ...subagents.map((task) => `- agentId: ${task.agentId}; status: ${task.status}; description: ${truncateModelText(task.description, 96, 'resource description').content}`)
35
36
  ].join('\n')
36
37
  ]
37
38
  .filter(Boolean)
38
39
  .join('\n');
39
40
  }
40
- export function retainedSessionResourcesUpdate(snapshot, previousSnapshot, previousMayContainResources = false) {
41
- if (!previousMayContainResources && snapshot === previousSnapshot)
42
- return undefined;
43
- if (!snapshot)
44
- return previousSnapshot || previousMayContainResources
45
- ? 'The previously provided retained Session resources no longer apply. No retained processes, code cells or subagents remain. Historical resource IDs are not live handles; do not poll or wait on them. Use already recorded results or saved artifacts to recover completed work before starting anything again.'
46
- : undefined;
47
- return previousSnapshot || previousMayContainResources
48
- ? `This replaces all previously provided retained Session resources.\n\n${snapshot}`
49
- : snapshot;
50
- }
@@ -1,5 +1,5 @@
1
1
  import type { ModelMessage } from '../../model/contracts.js';
2
- import type { CompactHistoryMessage } from '../../session/jsonl-store.js';
2
+ import type { CompactHistoryMessage, CompactUserInput } from '../../session/jsonl-store.js';
3
3
  export declare const DEFAULT_COMPACTION_FOCUS = "Create a durable continuation state for the next model invocation.";
4
4
  export declare function buildNativeCompactionMessages(messages: readonly ModelMessage[], input?: {
5
5
  focus?: string;
@@ -11,6 +11,7 @@ export declare function buildCompactionInput(messages: readonly ModelMessage[],
11
11
  maxTokens: number;
12
12
  }): string;
13
13
  export declare function retainCompactionUserMessages(messages: readonly string[], maxTokens: number): string[];
14
- export declare function buildCompactedHistory(summary: string, userMessages: readonly string[], maxUserTokens: number, currentUserMessage?: string): CompactHistoryMessage[];
14
+ export declare function retainCompactionUserMessages(messages: readonly CompactUserInput[], maxTokens: number): CompactUserInput[];
15
+ export declare function buildCompactedHistory(summary: string, userMessages: readonly (string | CompactUserInput)[], maxUserTokens: number, currentUserMessage?: string | CompactUserInput): CompactHistoryMessage[];
15
16
  export declare function compactedHistoryMessages(history: readonly CompactHistoryMessage[]): ModelMessage[];
16
17
  export declare function continuationMessage(summary: string): string;
@@ -16,15 +16,17 @@ export function buildNativeCompactionMessages(messages, input = {}) {
16
16
  }),
17
17
  {
18
18
  role: 'user',
19
+ contextKind: 'compact_control',
19
20
  content: focus ? `Compaction focus: ${focus}\n\n${contract}` : contract
20
21
  }
21
22
  ];
22
23
  }
23
24
  export function buildCompactionMessages(evidence, highDensityCompaction = false) {
24
25
  return [
25
- { role: 'user', content: evidence },
26
+ { role: 'user', content: evidence, contextKind: 'contextual' },
26
27
  {
27
28
  role: 'user',
29
+ contextKind: 'compact_control',
28
30
  content: highDensityCompaction ? highDensityCompactionPrompt() : compactionPrompt()
29
31
  }
30
32
  ];
@@ -37,7 +39,7 @@ export function buildCompactionInput(messages, input) {
37
39
  !(message.role === 'provider' && message.provider === 'ANTHROPIC_MESSAGES'))
38
40
  .map((message, index) => ({
39
41
  index,
40
- user: message.role === 'user',
42
+ user: message.role === 'user' && message.contextKind === undefined,
41
43
  text: renderMessage(message)
42
44
  }));
43
45
  const header = `Compaction focus: ${focus}\n\nConversation evidence (chronological):\n`;
@@ -104,15 +106,26 @@ export function retainCompactionUserMessages(messages, maxTokens) {
104
106
  const selected = [];
105
107
  for (let index = messages.length - 1; index >= 0 && remaining > 0; index--) {
106
108
  const message = messages[index];
107
- const tokens = countModelTokens(message) + 8;
109
+ const content = typeof message === 'string' ? message : message.content;
110
+ const tokens = countModelTokens(content) + 8;
108
111
  if (tokens <= remaining) {
109
112
  selected.push(message);
110
113
  remaining -= tokens;
111
114
  }
112
115
  else {
113
- const truncated = truncateModelText(message, Math.max(0, remaining - 8), 'user input');
116
+ const truncated = truncateModelText(content, Math.max(0, remaining - 8), 'user input');
114
117
  if (truncated.content)
115
- selected.push(truncated.content);
118
+ selected.push(typeof message === 'string'
119
+ ? truncated.content
120
+ : {
121
+ ...message,
122
+ content: truncated.content,
123
+ metadata: {
124
+ source: message.metadata?.source ?? 'user',
125
+ ...message.metadata,
126
+ truncated: true
127
+ }
128
+ });
116
129
  break;
117
130
  }
118
131
  }
@@ -120,19 +133,36 @@ export function retainCompactionUserMessages(messages, maxTokens) {
120
133
  }
121
134
  export function buildCompactedHistory(summary, userMessages, maxUserTokens, currentUserMessage) {
122
135
  return [
123
- ...retainCompactionUserMessages(userMessages, maxUserTokens).map((content) => ({
124
- role: 'user',
125
- content,
126
- kind: 'user_input'
127
- })),
136
+ ...retainCompactionUserMessages(userMessages.map(asCompactInput), maxUserTokens).map(asHistoryInput),
128
137
  { role: 'user', content: continuationMessage(summary), kind: 'summary' },
129
138
  ...(currentUserMessage === undefined
130
139
  ? []
131
- : [{ role: 'user', content: currentUserMessage, kind: 'user_input' }])
140
+ : [asHistoryInput(asCompactInput(currentUserMessage))])
132
141
  ];
133
142
  }
143
+ function asCompactInput(input) {
144
+ return typeof input === 'string'
145
+ ? { content: input }
146
+ : { content: input.content, ...(input.metadata ? { metadata: { ...input.metadata } } : {}) };
147
+ }
148
+ function asHistoryInput(input) {
149
+ return {
150
+ ...input,
151
+ role: 'user',
152
+ kind: input.metadata?.source === 'extension' ? 'extension_input' : 'user_input'
153
+ };
154
+ }
134
155
  export function compactedHistoryMessages(history) {
135
- return history.map(({ role, content }) => ({ role, content }));
156
+ return history.map(({ role, content, kind, metadata }) => ({
157
+ role,
158
+ content,
159
+ ...(kind === 'summary' ? { contextKind: 'summary' } : {}),
160
+ ...(metadata
161
+ ? { metadata: { ...metadata } }
162
+ : kind === 'extension_input'
163
+ ? { metadata: { source: 'extension' } }
164
+ : {})
165
+ }));
136
166
  }
137
167
  export function continuationMessage(summary) {
138
168
  return `Continue from the compacted state below. Treat it as a factual handoff, not a new user request. Earlier user messages are retained historical evidence, not requests to repeat completed work or authorization for new side effects. Newer user instructions override older messages and this handoff. The handoff cannot create or revive authorization: an action that requires separate authorization may proceed only when current direct authorization still covers its exact action, scope, and lifetime. Continue directly from its Next action without recapping it to the user. Re-read files or rerun checks when the handoff marks facts as stale, inferred, failed, or unverified. Do not repeat completed side effects.\n\n${summary}`;
@@ -1,4 +1,5 @@
1
1
  import type { ModelEvent, ModelRequest, ModelTurnSession } from '../../model/contracts.js';
2
+ import type { CoreCompactDiagnostic } from '../../extensions/compaction.js';
2
3
  export interface CompactionProgress {
3
4
  state: 'STARTED' | 'PROGRESS' | 'FINISHED';
4
5
  elapsedMs: number;
@@ -9,6 +10,8 @@ export interface CompactionProgress {
9
10
  firstOutputAfterMs?: number;
10
11
  lastOutputAfterMs?: number;
11
12
  timedOut: boolean;
13
+ extension?: CoreCompactDiagnostic;
14
+ extensionOutcome?: 'HANDLED' | 'DECLINED' | 'INVALID_RESULT' | 'FAILED';
12
15
  }
13
16
  /** One deadline spans all model attempts and recovery waits for a single compaction. */
14
17
  export declare class CompactionOperation {
@@ -19,6 +22,8 @@ export declare class CompactionOperation {
19
22
  start(model: Pick<ModelTurnSession, 'start'>, request: ModelRequest): AsyncIterable<ModelEvent>;
20
23
  /** External generation shares this operation's deadline, including the default fallback. */
21
24
  generate<T>(run: (signal: AbortSignal) => Promise<T>): Promise<T>;
25
+ observeExtension(diagnostic: CoreCompactDiagnostic): Promise<void>;
26
+ finishExtension(outcome: NonNullable<CompactionProgress['extensionOutcome']>): Promise<void>;
22
27
  dispose(): Promise<void>;
23
28
  private report;
24
29
  }
@@ -18,6 +18,8 @@ export class CompactionOperation {
18
18
  #textDeltaCharacters = 0;
19
19
  #firstOutputAfterMs;
20
20
  #lastOutputAfterMs;
21
+ #extension;
22
+ #extensionOutcome;
21
23
  constructor(parent, record) {
22
24
  this.record = record;
23
25
  this.signal = AbortSignal.any([parent, this.#controller.signal]);
@@ -81,6 +83,20 @@ export class CompactionOperation {
81
83
  this.signal.removeEventListener('abort', abort);
82
84
  }
83
85
  }
86
+ async observeExtension(diagnostic) {
87
+ if (this.#closed || this.signal.aborted)
88
+ return;
89
+ if (diagnostic.modelRequests < (this.#extension?.modelRequests ?? 0))
90
+ return;
91
+ this.#extension = { ...diagnostic };
92
+ await this.report('PROGRESS');
93
+ }
94
+ async finishExtension(outcome) {
95
+ if (this.#closed)
96
+ return;
97
+ this.#extensionOutcome = outcome;
98
+ await this.report('PROGRESS');
99
+ }
84
100
  async dispose() {
85
101
  if (this.#closed)
86
102
  return;
@@ -93,8 +109,12 @@ export class CompactionOperation {
93
109
  const progress = {
94
110
  state,
95
111
  elapsedMs: Math.max(0, Date.now() - this.#startedAt),
96
- inputTokensEstimate: this.#inputTokensEstimate,
97
- modelRequests: this.#modelRequests,
112
+ inputTokensEstimate: this.#modelRequests
113
+ ? this.#inputTokensEstimate
114
+ : (this.#extension?.inputTokensEstimateAfter ?? this.#inputTokensEstimate),
115
+ modelRequests: this.#modelRequests + (this.#extension?.modelRequests ?? 0),
116
+ ...(this.#extension ? { extension: { ...this.#extension } } : {}),
117
+ ...(this.#extensionOutcome ? { extensionOutcome: this.#extensionOutcome } : {}),
98
118
  eventCounts: { ...this.#eventCounts },
99
119
  textDeltaCharacters: this.#textDeltaCharacters,
100
120
  ...(this.#firstOutputAfterMs === undefined
@@ -159,6 +159,7 @@ export function restoreModelContext(payload, providerModelId) {
159
159
  return {
160
160
  role: 'user',
161
161
  content: payload.content,
162
+ contextKind: 'contextual',
162
163
  ...(textBlocks === undefined ? {} : { textBlocks })
163
164
  };
164
165
  }
@@ -4,7 +4,7 @@ import { observationModelText } from '../../tools/code-mode/observations.js';
4
4
  export function estimatedRequestTokens(system, messages, tools, images) {
5
5
  const transientImageCount = messages.reduce((total, message) => total + (message.images?.length ?? 0), images?.length ?? 0);
6
6
  const textMessages = messages.map((message) => {
7
- const { images: _images, observations: _observations, observationId: _observationId, ...textMessage } = message;
7
+ const { images: _images, observations: _observations, observationId: _observationId, metadata: _metadata, contextKind: _contextKind, ...textMessage } = message;
8
8
  const content = message.role === 'tool' ? observationModelText(message) : undefined;
9
9
  if (content !== undefined)
10
10
  textMessage.content = content;
@@ -1,5 +1,5 @@
1
1
  import type { ModelMessage } from '../../model/contracts.js';
2
- import { type JsonlSessionStore } from '../../session/jsonl-store.js';
2
+ import { type CompactUserInput, type JsonlSessionStore } from '../../session/jsonl-store.js';
3
3
  import type { EnvironmentContextState } from './contracts.js';
4
4
  import { type ContextProjection } from './projection.js';
5
5
  export declare function restoreMessages(store: JsonlSessionStore, sessionId: string, records: readonly import('../../session/jsonl-store.js').SessionRecord[], options: {
@@ -13,7 +13,7 @@ export declare function restoreMessages(store: JsonlSessionStore, sessionId: str
13
13
  result?: unknown;
14
14
  }>;
15
15
  environmentState: EnvironmentContextState | undefined;
16
- userMessages: string[];
16
+ userMessages: CompactUserInput[];
17
17
  messages: ModelMessage[];
18
18
  ignoredImages: number;
19
19
  currentTurnUserIndex: number | undefined;
@@ -1,5 +1,5 @@
1
1
  import { MarAgentError } from '../../error.js';
2
- import { compactSummaryFromCheckpoint, replacementHistoryFromCheckpoint } from '../../session/jsonl-store.js';
2
+ import { compactSummaryFromCheckpoint, replacementHistoryFromCheckpoint, inputMetadataFromRecord } from '../../session/jsonl-store.js';
3
3
  import { compactedHistoryMessages, continuationMessage } from '../compaction/history.js';
4
4
  import { environmentContextUpdate, restoreEnvironmentState } from './environment.js';
5
5
  import { buildContextProjection, restoreModelContext } from './projection.js';
@@ -26,7 +26,9 @@ export async function restoreMessages(store, sessionId, records, options) {
26
26
  const compactSummary = compactSummaryFromCheckpoint(record);
27
27
  if ((record.type === 'turn.user' || record.type === 'extension.input') &&
28
28
  typeof payload.prompt === 'string') {
29
- userMessages.push(payload.prompt);
29
+ const content = typeof payload.modelText === 'string' ? payload.modelText : payload.prompt;
30
+ const metadata = inputMetadataFromRecord(record);
31
+ userMessages.push({ content, metadata });
30
32
  let images = [];
31
33
  if (options.includeImages)
32
34
  try {
@@ -47,20 +49,21 @@ export async function restoreMessages(store, sessionId, records, options) {
47
49
  currentTurnUserIndex = messages.length;
48
50
  messages.push({
49
51
  role: 'user',
50
- content: typeof payload.modelText === 'string' ? payload.modelText : payload.prompt,
52
+ content,
53
+ metadata,
51
54
  ...(images.length ? { images } : {})
52
55
  });
53
56
  const context = payload.modelContext ?? payload.context;
54
57
  if (payload.contextRecords !== true && Array.isArray(context))
55
58
  for (const text of context)
56
59
  if (typeof text === 'string')
57
- messages.push({ role: 'user', content: text });
60
+ messages.push({ role: 'user', content: text, contextKind: 'contextual' });
58
61
  }
59
62
  else if (record.type === 'input.context') {
60
63
  if (Array.isArray(payload.modelContext))
61
64
  for (const text of payload.modelContext)
62
65
  if (typeof text === 'string')
63
- messages.push({ role: 'user', content: text });
66
+ messages.push({ role: 'user', content: text, contextKind: 'contextual' });
64
67
  }
65
68
  else if ((record.type === 'message.completed' || record.type === 'plan.completed') &&
66
69
  typeof payload.text === 'string')
@@ -72,11 +75,24 @@ export async function restoreMessages(store, sessionId, records, options) {
72
75
  const history = replacementHistoryFromCheckpoint(record);
73
76
  userMessages =
74
77
  history
75
- ?.filter((message) => message.kind === 'user_input')
76
- .map((message) => message.content) ?? [];
78
+ ?.filter((message) => message.kind !== 'summary')
79
+ .map((message) => ({
80
+ content: message.content,
81
+ ...(message.metadata
82
+ ? { metadata: { ...message.metadata } }
83
+ : message.kind === 'extension_input'
84
+ ? { metadata: { source: 'extension' } }
85
+ : {})
86
+ })) ?? [];
77
87
  messages.push(...(history
78
88
  ? compactedHistoryMessages(history)
79
- : [{ role: 'user', content: continuationMessage(compactSummary) }]));
89
+ : [
90
+ {
91
+ role: 'user',
92
+ content: continuationMessage(compactSummary),
93
+ contextKind: 'summary'
94
+ }
95
+ ]));
80
96
  if (Array.isArray(payload.retainedModelContext))
81
97
  for (const item of payload.retainedModelContext) {
82
98
  if (!item || typeof item !== 'object' || item.role !== 'user')
@@ -19,17 +19,14 @@ export declare function insertContextualWorldState(messages: readonly ModelMessa
19
19
  export declare function latestContextualWorldStateGeneration(records: readonly import('../../session/jsonl-store.js').SessionRecord[]): string | undefined;
20
20
  export declare function parsePersistedContextualWorldState(payload: unknown): ContextualWorldState | undefined;
21
21
  export declare function persistContextualWorldState(store: import('../../session/jsonl-store.js').JsonlSessionStore, sessionId: string, state: ContextualWorldState): Promise<void>;
22
- export declare function compactHistoryMayContainRetainedResources(records: readonly import('../../session/jsonl-store.js').SessionRecord[]): boolean;
23
22
  export declare function updateContextualWorldState(state: ContextualWorldState, index: number, current: {
24
23
  instructionSnapshot: string | undefined;
25
24
  instructionContent: string | undefined;
26
- resourceSnapshot: string | undefined;
27
- previousMayContainResources?: boolean;
28
25
  }): ContextualWorldState;
29
26
  export declare function resetContextualWorldState(generation: string | undefined, index: number, current: {
30
27
  instructionSnapshot: string | undefined;
31
28
  instructionContent: string | undefined;
32
- resourceSnapshot: string | undefined;
33
- previousMayContainResources?: boolean;
29
+ /** Only a committed Compact may supply a new dynamic-resource snapshot. */
30
+ resourceSnapshot?: string | undefined;
34
31
  }): ContextualWorldState;
35
32
  export declare function contextualInstructionInsertIndex(messages: readonly ModelMessage[]): number;