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