@myagentroam/agent 0.10.29 → 0.10.31
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.
- package/dist/agent-core/extensions/session-input.d.ts +2 -0
- package/dist/agent-core/extensions/session-input.js +21 -3
- package/dist/agent-core/model/contracts.d.ts +1 -0
- package/dist/agent-core/model/openai-responses.js +6 -2
- package/dist/agent-core/runtime/compaction/coordinator.d.ts +15 -0
- package/dist/agent-core/runtime/compaction/coordinator.js +88 -0
- package/dist/agent-core/runtime/compaction/requests.d.ts +16 -0
- package/dist/agent-core/runtime/compaction/requests.js +114 -0
- package/dist/agent-core/runtime/compaction/retained-context.d.ts +6 -0
- package/dist/agent-core/runtime/compaction/retained-context.js +52 -0
- package/dist/agent-core/runtime/compaction/smart.d.ts +14 -0
- package/dist/agent-core/runtime/compaction/smart.js +59 -0
- package/dist/agent-core/runtime/context/restore.d.ts +1 -0
- package/dist/agent-core/runtime/context/restore.js +14 -4
- package/dist/agent-core/runtime/context/usage.js +3 -1
- package/dist/agent-core/runtime/event-projection.js +1 -1
- package/dist/agent-core/runtime/events.d.ts +21 -0
- package/dist/agent-core/runtime/execution-policy.d.ts +2 -0
- package/dist/agent-core/runtime/execution-policy.js +2 -0
- package/dist/agent-core/runtime/execution.d.ts +1 -0
- package/dist/agent-core/runtime/execution.js +1 -0
- package/dist/agent-core/sdk/extension-session.js +4 -2
- package/dist/agent-core/sdk/session.d.ts +2 -0
- package/dist/agent-core/sdk/session.js +350 -192
- package/dist/agent-core/sdk/types.d.ts +4 -1
- package/dist/agent-core/session/catalog.js +2 -2
- package/dist/agent-core/session/jsonl-store.d.ts +2 -0
- package/dist/agent-core/session/jsonl-store.js +9 -1
- package/dist/agent-core/tools/agent/smart-compact.d.ts +9 -0
- package/dist/agent-core/tools/agent/smart-compact.js +20 -0
- package/dist/agent-core/tools/registry.js +4 -1
- package/dist/agent-core/tools/runtime-context.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- 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
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1771,6 +1771,8 @@ function strictFunctionSchema(value, optional = false) {
|
|
|
1771
1771
|
return value;
|
|
1772
1772
|
const schema = value;
|
|
1773
1773
|
const supportedSchema = Object.fromEntries(Object.entries(schema).filter(([keyword]) => keyword !== 'propertyNames' && keyword !== 'uniqueItems'));
|
|
1774
|
+
if (optional && Array.isArray(schema.enum) && !schema.enum.includes(null))
|
|
1775
|
+
supportedSchema.enum = [...schema.enum, null];
|
|
1774
1776
|
if (schema.type === 'object') {
|
|
1775
1777
|
const properties = record(schema.properties);
|
|
1776
1778
|
const required = new Set(Array.isArray(schema.required) ? schema.required : []);
|
|
@@ -1795,11 +1797,13 @@ function strictFunctionSchema(value, optional = false) {
|
|
|
1795
1797
|
return supportedSchema;
|
|
1796
1798
|
if (typeof schema.type === 'string')
|
|
1797
1799
|
return { ...supportedSchema, type: [schema.type, 'null'] };
|
|
1798
|
-
if (Array.isArray(schema.
|
|
1800
|
+
if (Array.isArray(schema.type))
|
|
1799
1801
|
return {
|
|
1800
1802
|
...supportedSchema,
|
|
1801
|
-
|
|
1803
|
+
type: schema.type.includes('null') ? schema.type : [...schema.type, 'null']
|
|
1802
1804
|
};
|
|
1805
|
+
if (Array.isArray(schema.enum))
|
|
1806
|
+
return supportedSchema;
|
|
1803
1807
|
return { anyOf: [supportedSchema, { type: 'null' }] };
|
|
1804
1808
|
}
|
|
1805
1809
|
function responseCitations(response) {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ModelMessage } from '../../model/contracts.js';
|
|
2
|
+
import { type SessionRecord } from '../../session/jsonl-store.js';
|
|
3
|
+
export declare const SMART_COMPACTION_MARKER = "<mar_runtime>\n<smart_compaction scope=\"this_message\">eligible</smart_compaction>\n</mar_runtime>";
|
|
4
|
+
export interface SmartCompactionInput {
|
|
5
|
+
readonly recordId: string;
|
|
6
|
+
readonly eligible: boolean;
|
|
7
|
+
requested: boolean;
|
|
8
|
+
error?: unknown;
|
|
9
|
+
}
|
|
10
|
+
/** A measured watermark, not the stricter native-request budget calibration. */
|
|
11
|
+
export declare function latestSmartContextTokens(records: readonly SessionRecord[], modelId: string, contextWindowTokens: number): number | undefined;
|
|
12
|
+
/** Only provider views receive the marker; durable message content stays unchanged. */
|
|
13
|
+
export declare function projectSmartCompactionMessage(message: ModelMessage): ModelMessage;
|
|
14
|
+
export declare function requestSmartCompaction(input: SmartCompactionInput | undefined, start: () => void): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { MarAgentError } from '../../error.js';
|
|
2
|
+
import { compactSummaryFromCheckpoint } from '../../session/jsonl-store.js';
|
|
3
|
+
export const SMART_COMPACTION_MARKER = '<mar_runtime>\n<smart_compaction scope="this_message">eligible</smart_compaction>\n</mar_runtime>';
|
|
4
|
+
/** A measured watermark, not the stricter native-request budget calibration. */
|
|
5
|
+
export function latestSmartContextTokens(records, modelId, contextWindowTokens) {
|
|
6
|
+
let tokens;
|
|
7
|
+
let matchingModel = false;
|
|
8
|
+
for (const record of records) {
|
|
9
|
+
const payload = record.payload;
|
|
10
|
+
if (compactSummaryFromCheckpoint(record) !== undefined) {
|
|
11
|
+
tokens = undefined;
|
|
12
|
+
// A sliced checkpoint does not prove the input window used by later usage.
|
|
13
|
+
matchingModel = false;
|
|
14
|
+
}
|
|
15
|
+
if (record.type === 'context.gc.completed')
|
|
16
|
+
tokens = undefined;
|
|
17
|
+
if (record.type === 'execution.header' && payload.mode !== 'compact') {
|
|
18
|
+
matchingModel =
|
|
19
|
+
payload.modelId === modelId && payload.contextWindowTokens === contextWindowTokens;
|
|
20
|
+
if (!matchingModel)
|
|
21
|
+
tokens = undefined;
|
|
22
|
+
}
|
|
23
|
+
if (matchingModel && payload.type === 'usage.updated' && payload.purpose !== 'compact') {
|
|
24
|
+
const value = payload.contextInputTokens ?? payload.inputTokens;
|
|
25
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
|
|
26
|
+
tokens = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return tokens;
|
|
30
|
+
}
|
|
31
|
+
/** Only provider views receive the marker; durable message content stays unchanged. */
|
|
32
|
+
export function projectSmartCompactionMessage(message) {
|
|
33
|
+
if (message.role !== 'user' ||
|
|
34
|
+
message.contextKind !== undefined ||
|
|
35
|
+
!message.metadata?.smartCompaction)
|
|
36
|
+
return message;
|
|
37
|
+
const suffix = '\n\n' + SMART_COMPACTION_MARKER;
|
|
38
|
+
return {
|
|
39
|
+
...message,
|
|
40
|
+
content: message.content + suffix,
|
|
41
|
+
...(message.textBlocks ? { textBlocks: [...message.textBlocks, { text: suffix }] } : {})
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function requestSmartCompaction(input, start) {
|
|
45
|
+
if (!input?.eligible)
|
|
46
|
+
throw new MarAgentError('MAR_AGENT_SMART_COMPACT_DENIED', 'The current user input is not eligible for Smart Compaction.');
|
|
47
|
+
if ('error' in input)
|
|
48
|
+
throw input.error;
|
|
49
|
+
if (input.requested)
|
|
50
|
+
return;
|
|
51
|
+
input.requested = true;
|
|
52
|
+
try {
|
|
53
|
+
start();
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
input.error = error;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -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
|
|
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'
|
|
100
|
+
if (!item || typeof item !== 'object')
|
|
99
101
|
continue;
|
|
100
|
-
const message =
|
|
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' &&
|
|
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,8 @@ export declare const AGENT_EXECUTION_POLICY: Readonly<{
|
|
|
3
3
|
toolEventSummaryCharacters: 2000;
|
|
4
4
|
modelToolOutputTokens: 10000;
|
|
5
5
|
automaticCompactionPercent: 90;
|
|
6
|
+
backgroundCompactionPercent: 85;
|
|
7
|
+
smartCompactionPercent: 60;
|
|
6
8
|
subagentVerificationCharacters: 500;
|
|
7
9
|
estimatedTokensPerImage: 1000;
|
|
8
10
|
}>;
|
|
@@ -3,6 +3,8 @@ export const AGENT_EXECUTION_POLICY = Object.freeze({
|
|
|
3
3
|
toolEventSummaryCharacters: 2_000,
|
|
4
4
|
modelToolOutputTokens: 10_000,
|
|
5
5
|
automaticCompactionPercent: 90,
|
|
6
|
+
backgroundCompactionPercent: 85,
|
|
7
|
+
smartCompactionPercent: 60,
|
|
6
8
|
subagentVerificationCharacters: 500,
|
|
7
9
|
estimatedTokensPerImage: 1_000
|
|
8
10
|
});
|
|
@@ -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
|
-
|
|
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 &&
|
|
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 });
|