tinker-agent 1.11.0 → 2.1.0
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/CHANGELOG.md +44 -1
- package/README.md +33 -24
- package/package.json +2 -1
- package/src/agent/context-meter.ts +12 -85
- package/src/agent/loop.ts +1 -14
- package/src/agent/runtime-session.ts +9 -25
- package/src/agent/session-ledger.ts +12 -5
- package/src/agent/tool-result-content.ts +76 -0
- package/src/agent/types.ts +14 -2
- package/src/cli/config.ts +4 -5
- package/src/cli/model-profiles.ts +38 -97
- package/src/cli/public-config-contract.ts +24 -88
- package/src/cli/runner-dependencies.ts +5 -7
- package/src/cli/tui-memory.ts +1 -3
- package/src/cli/tui-runner.tsx +4 -0
- package/src/context/compiled-context-hash.ts +2 -1
- package/src/context/compiled-context-validator.ts +13 -4
- package/src/context/context-protocol-validator.ts +33 -2
- package/src/context/context-revision-compiler.ts +2 -1
- package/src/context/context-revision.ts +8 -2
- package/src/context/context-swap-renderer.ts +46 -12
- package/src/context/prefix-retirement-planner.ts +13 -9
- package/src/context/protocol-frame.ts +74 -7
- package/src/context/swap-planner.ts +19 -14
- package/src/events/observation-text-log.ts +1 -1
- package/src/events/stdout-event-printer.ts +6 -0
- package/src/image/image-asset-store.ts +32 -3
- package/src/image/image-input-policy.ts +53 -2
- package/src/image/image-probe.ts +8 -2
- package/src/image/provider-image.ts +99 -0
- package/src/memory/contracts.ts +61 -3
- package/src/memory/memory-coordinator.ts +313 -49
- package/src/memory/memory-extractor.ts +48 -48
- package/src/memory/memory-get-tool.ts +86 -0
- package/src/memory/memory-search-tool.ts +122 -33
- package/src/memory/memory-store.ts +227 -20
- package/src/model/fake-model-client.ts +177 -124
- package/src/model/model-client.ts +62 -11
- package/src/model/model-request-preflight.ts +0 -1
- package/src/model/openai-chat-mapping.ts +2 -1
- package/src/model/openai-chat-model-client.ts +26 -38
- package/src/model/openai-model-utils.ts +109 -40
- package/src/model/openai-responses-mapping.ts +25 -1
- package/src/model/openai-responses-model-client.ts +27 -40
- package/src/model/token-estimator.ts +26 -3
- package/src/observation/observation-builder.ts +100 -25
- package/src/session/session-history-reader.ts +128 -5
- package/src/session/session-schema.ts +59 -9
- package/src/session/session-store.ts +342 -205
- package/src/tools/registry.ts +18 -0
- package/src/tools/types.ts +46 -0
- package/src/tools/view-image.ts +89 -0
- package/src/tools/wait.ts +85 -0
- package/src/tui/components/memory-browser.tsx +3 -0
- package/src/tui/event-store.ts +61 -2
- package/src/model/input-token-estimator.ts +0 -25
- package/src/model/moonshot-input-token-estimator.ts +0 -111
- package/src/model/openai-responses-token-estimator.ts +0 -155
|
@@ -2,14 +2,13 @@ import type { AgentMessage, AssistantMessage, IterationIdentity } from "../agent
|
|
|
2
2
|
import type { RuntimeSessionContext } from "../agent/runtime-session";
|
|
3
3
|
import type { ToolDefinition } from "../tools/types";
|
|
4
4
|
import type { ImageAssetStore } from "../image/image-asset-store";
|
|
5
|
-
import type {
|
|
6
|
-
import type { InputTokenEstimator } from "./input-token-estimator";
|
|
5
|
+
import type { ImageAssetRef } from "../image/image-types";
|
|
7
6
|
import type { ReasoningEffortController } from "./reasoning-effort";
|
|
8
7
|
|
|
9
8
|
export interface ModelClient {
|
|
10
9
|
readonly messageProtocol: ModelMessageProtocol;
|
|
11
|
-
readonly
|
|
12
|
-
readonly
|
|
10
|
+
readonly inputModalities: readonly ModelInputModality[];
|
|
11
|
+
readonly toolResultModalities: readonly ToolResultModality[];
|
|
13
12
|
readonly reasoningEffort?: ReasoningEffortController;
|
|
14
13
|
prepare(input: ModelRequestInput): PreparedModelRequest;
|
|
15
14
|
materialize?(
|
|
@@ -22,6 +21,59 @@ export interface ModelClient {
|
|
|
22
21
|
): Promise<ModelRequestOutput>;
|
|
23
22
|
}
|
|
24
23
|
|
|
24
|
+
export type ModelInputModality = "text" | "image";
|
|
25
|
+
export type ToolResultModality = "text" | "image";
|
|
26
|
+
|
|
27
|
+
export function validateModelModalities(input: {
|
|
28
|
+
readonly profileName?: string;
|
|
29
|
+
readonly adapter: ModelMessageProtocol["adapter"];
|
|
30
|
+
readonly inputModalities: readonly ModelInputModality[];
|
|
31
|
+
readonly toolResultModalities: readonly ToolResultModality[];
|
|
32
|
+
readonly adapterToolResultModalities: readonly ToolResultModality[];
|
|
33
|
+
}): {
|
|
34
|
+
readonly inputModalities: readonly ModelInputModality[];
|
|
35
|
+
readonly toolResultModalities: readonly ToolResultModality[];
|
|
36
|
+
} {
|
|
37
|
+
const inputModalities = normalizeModalities(input.inputModalities, "model input");
|
|
38
|
+
const toolResultModalities = normalizeModalities(
|
|
39
|
+
input.toolResultModalities,
|
|
40
|
+
"tool result",
|
|
41
|
+
);
|
|
42
|
+
if (toolResultModalities.includes("image") && !inputModalities.includes("image")) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
'Image tool results require "image" in the model input modalities.',
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const unsupported = toolResultModalities.find(
|
|
48
|
+
(modality) => !input.adapterToolResultModalities.includes(modality),
|
|
49
|
+
);
|
|
50
|
+
if (unsupported !== undefined) {
|
|
51
|
+
const subject =
|
|
52
|
+
input.profileName === undefined
|
|
53
|
+
? "Model configuration"
|
|
54
|
+
: `Profile ${JSON.stringify(input.profileName)}`;
|
|
55
|
+
throw new Error(
|
|
56
|
+
`${subject} declares ${unsupported} tool results, but adapter ${JSON.stringify(input.adapter)} does not support them.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
return Object.freeze({ inputModalities, toolResultModalities });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeModalities(
|
|
63
|
+
modalities: readonly (ModelInputModality | ToolResultModality)[],
|
|
64
|
+
label: string,
|
|
65
|
+
): readonly ("text" | "image")[] {
|
|
66
|
+
if (
|
|
67
|
+
modalities.length === 0 ||
|
|
68
|
+
modalities.some((modality) => modality !== "text" && modality !== "image") ||
|
|
69
|
+
new Set(modalities).size !== modalities.length ||
|
|
70
|
+
!modalities.includes("text")
|
|
71
|
+
) {
|
|
72
|
+
throw new Error(`${label} modalities must be unique and include "text".`);
|
|
73
|
+
}
|
|
74
|
+
return Object.freeze(modalities.includes("image") ? ["text", "image"] : ["text"]);
|
|
75
|
+
}
|
|
76
|
+
|
|
25
77
|
export class ModelRequestMediaAggregateError extends Error {
|
|
26
78
|
readonly code = "MODEL_REQUEST_MEDIA_AGGREGATE_LIMIT";
|
|
27
79
|
|
|
@@ -67,15 +119,14 @@ export type PreparedPromptSegmentKind =
|
|
|
67
119
|
export type PreparedPromptSegment = {
|
|
68
120
|
kind: PreparedPromptSegmentKind;
|
|
69
121
|
normalizedText: string;
|
|
70
|
-
media?: readonly
|
|
122
|
+
media?: readonly PreparedMediaOccurrence[];
|
|
71
123
|
};
|
|
72
124
|
|
|
73
|
-
export type
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
byteLength: number;
|
|
125
|
+
export type PreparedMediaOccurrence = {
|
|
126
|
+
readonly asset: ImageAssetRef;
|
|
127
|
+
readonly source: "user_attachment" | "tool_result";
|
|
128
|
+
readonly messageOrdinal: number;
|
|
129
|
+
readonly blockPosition: number;
|
|
79
130
|
width: number;
|
|
80
131
|
height: number;
|
|
81
132
|
planningTokens: number;
|
|
@@ -21,6 +21,7 @@ import type {
|
|
|
21
21
|
} from "openai/resources/chat/completions";
|
|
22
22
|
import { validateUserMessage, type ImageAssetId } from "../image/image-types";
|
|
23
23
|
import { imageAssetUrlMarker } from "./openai-image-mapping";
|
|
24
|
+
import { toolResultText } from "../agent/tool-result-content";
|
|
24
25
|
|
|
25
26
|
type DeepSeekAssistantMessageParam = ChatCompletionAssistantMessageParam & {
|
|
26
27
|
reasoning_content?: string | null;
|
|
@@ -57,7 +58,7 @@ export function toOpenAIChatMessages(
|
|
|
57
58
|
return {
|
|
58
59
|
role: "tool",
|
|
59
60
|
tool_call_id: message.providerToolCallId,
|
|
60
|
-
content: message.content,
|
|
61
|
+
content: toolResultText(message.content),
|
|
61
62
|
};
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -9,8 +9,7 @@ import {
|
|
|
9
9
|
IMAGE_INPUT_POLICY_VERSION,
|
|
10
10
|
} from "../image/image-input-policy";
|
|
11
11
|
import type { ModelContextBudget } from "./model-context-profile";
|
|
12
|
-
import
|
|
13
|
-
import { ProviderResponseError } from "./model-client";
|
|
12
|
+
import { ProviderResponseError, validateModelModalities } from "./model-client";
|
|
14
13
|
import type {
|
|
15
14
|
MaterializedModelRequest,
|
|
16
15
|
ModelClient,
|
|
@@ -30,13 +29,13 @@ import {
|
|
|
30
29
|
import { OpenAIChatCompletionStreamAccumulator } from "./openai-chat-stream";
|
|
31
30
|
import {
|
|
32
31
|
deepFreeze,
|
|
32
|
+
imageToolSegment,
|
|
33
33
|
imageUserSegment,
|
|
34
34
|
materializeOpenAIRequest,
|
|
35
35
|
normalizedEndpointPolicy,
|
|
36
36
|
sanitizedProviderError,
|
|
37
37
|
segmentKind,
|
|
38
38
|
} from "./openai-model-utils";
|
|
39
|
-
import { MoonshotInputTokenEstimator } from "./moonshot-input-token-estimator";
|
|
40
39
|
import type { ReasoningEffortController } from "./reasoning-effort";
|
|
41
40
|
import { sha256, stableJsonStringify } from "./model-request-preflight";
|
|
42
41
|
|
|
@@ -48,7 +47,6 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
48
47
|
adapter: "openai-chat",
|
|
49
48
|
serializationVersion: OPENAI_CHAT_SERIALIZATION_VERSION,
|
|
50
49
|
});
|
|
51
|
-
readonly inputTokenEstimator?: InputTokenEstimator;
|
|
52
50
|
readonly reasoningEffort?: ReasoningEffortController;
|
|
53
51
|
private readonly client: OpenAI;
|
|
54
52
|
private readonly preparedRequests = new WeakSet<object>();
|
|
@@ -56,6 +54,7 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
56
54
|
private readonly provider: string;
|
|
57
55
|
private readonly stream: boolean;
|
|
58
56
|
readonly inputModalities: readonly ("text" | "image")[];
|
|
57
|
+
readonly toolResultModalities: readonly ("text" | "image")[];
|
|
59
58
|
|
|
60
59
|
constructor(
|
|
61
60
|
private readonly options: {
|
|
@@ -64,14 +63,8 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
64
63
|
baseURL?: string;
|
|
65
64
|
includeReasoningContent?: boolean;
|
|
66
65
|
inputModalities?: readonly ("text" | "image")[];
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
model: string;
|
|
70
|
-
apiBase: string;
|
|
71
|
-
apiKey: string;
|
|
72
|
-
timeoutMs: number;
|
|
73
|
-
maxRetries: 0;
|
|
74
|
-
};
|
|
66
|
+
toolResultModalities?: readonly ("text" | "image")[];
|
|
67
|
+
profileName?: string;
|
|
75
68
|
model: string;
|
|
76
69
|
providerName?: string;
|
|
77
70
|
reasoningEffort?: ReasoningEffortController;
|
|
@@ -83,14 +76,15 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
83
76
|
this.provider = options.providerName ?? "openai-compatible";
|
|
84
77
|
this.stream = options.stream ?? true;
|
|
85
78
|
this.reasoningEffort = options.reasoningEffort;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
const modalities = validateModelModalities({
|
|
80
|
+
profileName: options.profileName,
|
|
81
|
+
adapter: this.messageProtocol.adapter,
|
|
82
|
+
inputModalities: options.inputModalities ?? ["text"],
|
|
83
|
+
toolResultModalities: options.toolResultModalities ?? ["text"],
|
|
84
|
+
adapterToolResultModalities: ["text"],
|
|
85
|
+
});
|
|
86
|
+
this.inputModalities = modalities.inputModalities;
|
|
87
|
+
this.toolResultModalities = modalities.toolResultModalities;
|
|
94
88
|
this.client = new OpenAI({
|
|
95
89
|
apiKey: options.apiKey,
|
|
96
90
|
baseURL: options.baseURL,
|
|
@@ -100,15 +94,6 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
100
94
|
maxRetries: 0,
|
|
101
95
|
fetch: options.fetch,
|
|
102
96
|
});
|
|
103
|
-
if (options.tokenEstimator !== undefined) {
|
|
104
|
-
this.inputTokenEstimator = new MoonshotInputTokenEstimator({
|
|
105
|
-
apiKey: options.tokenEstimator.apiKey,
|
|
106
|
-
baseURL: options.tokenEstimator.apiBase,
|
|
107
|
-
model: options.tokenEstimator.model,
|
|
108
|
-
timeoutMs: options.tokenEstimator.timeoutMs,
|
|
109
|
-
fetch: options.fetch,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
97
|
}
|
|
113
98
|
|
|
114
99
|
prepare(input: ModelRequestInput): PreparedModelRequest {
|
|
@@ -139,11 +124,13 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
139
124
|
const messageSegments = input.messages.map(
|
|
140
125
|
(message, index): PreparedPromptSegment =>
|
|
141
126
|
message.role === "user" && message.attachments !== undefined
|
|
142
|
-
? imageUserSegment(message)
|
|
143
|
-
:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
127
|
+
? imageUserSegment(message, index + 1)
|
|
128
|
+
: message.role === "tool"
|
|
129
|
+
? imageToolSegment(message, index + 1, stableJsonStringify(messages[index]))
|
|
130
|
+
: {
|
|
131
|
+
kind: segmentKind(message.role),
|
|
132
|
+
normalizedText: stableJsonStringify(messages[index]),
|
|
133
|
+
},
|
|
147
134
|
);
|
|
148
135
|
const mediaOccurrenceCount = messageSegments.reduce(
|
|
149
136
|
(total, segment) => total + (segment.media?.length ?? 0),
|
|
@@ -159,14 +146,12 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
159
146
|
includeReasoningContent: this.options.includeReasoningContent === true,
|
|
160
147
|
stream: this.stream,
|
|
161
148
|
inputModalities: this.inputModalities,
|
|
149
|
+
toolResultModalities: this.toolResultModalities,
|
|
162
150
|
requestPolicy: { toolChoice: "auto" },
|
|
163
151
|
imagePolicy: {
|
|
164
152
|
version: IMAGE_INPUT_POLICY_VERSION,
|
|
165
153
|
...IMAGE_INPUT_POLICY,
|
|
166
154
|
},
|
|
167
|
-
...(this.inputTokenEstimator === undefined
|
|
168
|
-
? {}
|
|
169
|
-
: { tokenEstimator: this.inputTokenEstimator.compatibility }),
|
|
170
155
|
}),
|
|
171
156
|
);
|
|
172
157
|
const prepared: PreparedModelRequest = {
|
|
@@ -299,4 +284,7 @@ export class OpenAIChatModelClient implements ModelClient {
|
|
|
299
284
|
}
|
|
300
285
|
}
|
|
301
286
|
|
|
302
|
-
export {
|
|
287
|
+
export {
|
|
288
|
+
assertOpenAIRequestBodyLimit,
|
|
289
|
+
exactJsonBodyBytes,
|
|
290
|
+
} from "./openai-model-utils";
|
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import type { UserMessage } from "../agent/types";
|
|
3
|
-
import {
|
|
2
|
+
import type { AgentMessage, UserMessage } from "../agent/types";
|
|
3
|
+
import {
|
|
4
|
+
IMAGE_INPUT_POLICY,
|
|
5
|
+
imagePlanningTokens,
|
|
6
|
+
providerImageDimensions,
|
|
7
|
+
} from "../image/image-input-policy";
|
|
4
8
|
import type { ImageAssetId, ImageAssetRef } from "../image/image-types";
|
|
9
|
+
import { materializeProviderImage, type ProviderImage } from "../image/provider-image";
|
|
5
10
|
import {
|
|
6
11
|
ModelRequestMediaAggregateError,
|
|
7
12
|
ProviderResponseError,
|
|
8
13
|
type MaterializedModelRequest,
|
|
9
14
|
type ModelMaterializeOptions,
|
|
10
15
|
type ModelRequestInput,
|
|
11
|
-
type
|
|
16
|
+
type PreparedMediaOccurrence,
|
|
12
17
|
type PreparedModelRequest,
|
|
13
18
|
type PreparedPromptSegment,
|
|
14
19
|
type ProviderResponseErrorCode,
|
|
@@ -28,26 +33,22 @@ export async function materializeOpenAIRequest(
|
|
|
28
33
|
}
|
|
29
34
|
|
|
30
35
|
const assets = distinctPreparedAssets(prepared.promptSegments);
|
|
31
|
-
const lowerLengths = new Map<ImageAssetId, number>();
|
|
32
|
-
for (const asset of assets.values()) {
|
|
33
|
-
lowerLengths.set(asset.assetId, dataUrlLength(asset));
|
|
34
|
-
}
|
|
35
36
|
const markerCount = countImageMarkers(prepared.payload);
|
|
36
37
|
if (markerCount !== prepared.mediaOccurrenceCount) {
|
|
37
38
|
throw new Error("Prepared image marker count does not match media descriptors.");
|
|
38
39
|
}
|
|
39
|
-
const lowerBodyBytes = exactJsonBodyBytes(prepared.payload, lowerLengths);
|
|
40
|
-
assertBodyLimit(lowerBodyBytes, prepared.mediaOccurrenceCount);
|
|
41
|
-
|
|
42
40
|
const dataUrls = new Map<ImageAssetId, string>();
|
|
41
|
+
const providerImages = new Map<ImageAssetId, ProviderImage>();
|
|
43
42
|
for (const asset of assets.values()) {
|
|
44
43
|
options.signal.throwIfAborted();
|
|
45
44
|
const bytes = await options.assetStore.readVerified(asset, {
|
|
46
45
|
signal: options.signal,
|
|
47
46
|
});
|
|
47
|
+
const image = await materializeProviderImage(bytes, asset.mimeType);
|
|
48
|
+
providerImages.set(asset.assetId, image);
|
|
48
49
|
dataUrls.set(
|
|
49
50
|
asset.assetId,
|
|
50
|
-
`data:${
|
|
51
|
+
`data:${image.mimeType};base64,${image.bytes.toString("base64")}`,
|
|
51
52
|
);
|
|
52
53
|
await yieldToEventLoop();
|
|
53
54
|
}
|
|
@@ -56,9 +57,14 @@ export async function materializeOpenAIRequest(
|
|
|
56
57
|
[...dataUrls].map(([assetId, value]) => [assetId, value.length] as const),
|
|
57
58
|
);
|
|
58
59
|
const bodyBytes = exactJsonBodyBytes(prepared.payload, exactLengths);
|
|
59
|
-
|
|
60
|
+
assertOpenAIRequestBodyLimit(bodyBytes, prepared.mediaOccurrenceCount);
|
|
60
61
|
const payload = deepFreeze(materializePayload(prepared.payload, dataUrls));
|
|
61
|
-
return Object.freeze({
|
|
62
|
+
return Object.freeze({
|
|
63
|
+
...prepared,
|
|
64
|
+
payload,
|
|
65
|
+
promptSegments: materializedPromptSegments(prepared.promptSegments, providerImages),
|
|
66
|
+
bodyBytes,
|
|
67
|
+
});
|
|
62
68
|
}
|
|
63
69
|
|
|
64
70
|
export function exactJsonBodyBytes(
|
|
@@ -118,19 +124,29 @@ export function exactJsonBodyBytes(
|
|
|
118
124
|
);
|
|
119
125
|
}
|
|
120
126
|
|
|
121
|
-
export function imageUserSegment(
|
|
127
|
+
export function imageUserSegment(
|
|
128
|
+
message: UserMessage,
|
|
129
|
+
messageOrdinal: number,
|
|
130
|
+
): PreparedPromptSegment {
|
|
122
131
|
const media = message.attachments!.map(
|
|
123
|
-
(attachment):
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
(attachment, blockPosition): PreparedMediaOccurrence => {
|
|
133
|
+
const dimensions = providerImageDimensions(attachment.width, attachment.height);
|
|
134
|
+
return Object.freeze({
|
|
135
|
+
asset: Object.freeze({
|
|
136
|
+
assetId: attachment.assetId,
|
|
137
|
+
mimeType: attachment.mimeType,
|
|
138
|
+
byteLength: attachment.byteLength,
|
|
139
|
+
width: attachment.width,
|
|
140
|
+
height: attachment.height,
|
|
141
|
+
}),
|
|
142
|
+
source: "user_attachment",
|
|
143
|
+
messageOrdinal,
|
|
144
|
+
blockPosition,
|
|
145
|
+
width: dimensions.width,
|
|
146
|
+
height: dimensions.height,
|
|
147
|
+
planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
|
|
148
|
+
});
|
|
149
|
+
},
|
|
134
150
|
);
|
|
135
151
|
return Object.freeze({
|
|
136
152
|
kind: "user",
|
|
@@ -139,6 +155,35 @@ export function imageUserSegment(message: UserMessage): PreparedPromptSegment {
|
|
|
139
155
|
});
|
|
140
156
|
}
|
|
141
157
|
|
|
158
|
+
export function imageToolSegment(
|
|
159
|
+
message: Extract<AgentMessage, { role: "tool" }>,
|
|
160
|
+
messageOrdinal: number,
|
|
161
|
+
normalizedText: string,
|
|
162
|
+
): PreparedPromptSegment {
|
|
163
|
+
const media = message.content.flatMap((block, blockPosition) => {
|
|
164
|
+
if (block.type !== "image") {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
const dimensions = providerImageDimensions(block.asset.width, block.asset.height);
|
|
168
|
+
return [
|
|
169
|
+
Object.freeze<PreparedMediaOccurrence>({
|
|
170
|
+
asset: Object.freeze({ ...block.asset }),
|
|
171
|
+
source: "tool_result",
|
|
172
|
+
messageOrdinal,
|
|
173
|
+
blockPosition,
|
|
174
|
+
width: dimensions.width,
|
|
175
|
+
height: dimensions.height,
|
|
176
|
+
planningTokens: imagePlanningTokens(dimensions.width, dimensions.height),
|
|
177
|
+
}),
|
|
178
|
+
];
|
|
179
|
+
});
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
kind: "tool",
|
|
182
|
+
normalizedText,
|
|
183
|
+
...(media.length === 0 ? {} : { media: Object.freeze(media) }),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
142
187
|
export function normalizedEndpointPolicy(baseURL: string | undefined): string {
|
|
143
188
|
const url = new URL(baseURL ?? "https://api.openai.com/v1");
|
|
144
189
|
url.username = "";
|
|
@@ -200,21 +245,15 @@ function distinctPreparedAssets(
|
|
|
200
245
|
const assets = new Map<ImageAssetId, ImageAssetRef>();
|
|
201
246
|
for (const segment of segments) {
|
|
202
247
|
for (const media of segment.media ?? []) {
|
|
203
|
-
const asset =
|
|
204
|
-
|
|
205
|
-
mimeType: media.mimeType,
|
|
206
|
-
byteLength: media.byteLength,
|
|
207
|
-
width: media.width,
|
|
208
|
-
height: media.height,
|
|
209
|
-
});
|
|
210
|
-
const existing = assets.get(media.assetId);
|
|
248
|
+
const asset = media.asset;
|
|
249
|
+
const existing = assets.get(asset.assetId);
|
|
211
250
|
if (
|
|
212
251
|
existing !== undefined &&
|
|
213
252
|
stableJsonStringify(existing) !== stableJsonStringify(asset)
|
|
214
253
|
) {
|
|
215
|
-
throw new Error(`Conflicting descriptors for image ${
|
|
254
|
+
throw new Error(`Conflicting descriptors for image ${asset.assetId}.`);
|
|
216
255
|
}
|
|
217
|
-
assets.set(
|
|
256
|
+
assets.set(asset.assetId, asset);
|
|
218
257
|
}
|
|
219
258
|
}
|
|
220
259
|
return assets;
|
|
@@ -265,11 +304,10 @@ function countImageMarkers(value: unknown): number {
|
|
|
265
304
|
return 0;
|
|
266
305
|
}
|
|
267
306
|
|
|
268
|
-
function
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
function assertBodyLimit(bodyBytes: number, imageCount: number): void {
|
|
307
|
+
export function assertOpenAIRequestBodyLimit(
|
|
308
|
+
bodyBytes: number,
|
|
309
|
+
imageCount: number,
|
|
310
|
+
): void {
|
|
273
311
|
if (bodyBytes > IMAGE_INPUT_POLICY.maxRequestBodyBytes) {
|
|
274
312
|
throw new ModelRequestMediaAggregateError(
|
|
275
313
|
`Model request is ${bodyBytes} UTF-8 bytes with ${imageCount} images; maximum is ${IMAGE_INPUT_POLICY.maxRequestBodyBytes}.`,
|
|
@@ -277,6 +315,37 @@ function assertBodyLimit(bodyBytes: number, imageCount: number): void {
|
|
|
277
315
|
}
|
|
278
316
|
}
|
|
279
317
|
|
|
318
|
+
function materializedPromptSegments(
|
|
319
|
+
segments: readonly PreparedPromptSegment[],
|
|
320
|
+
images: ReadonlyMap<ImageAssetId, ProviderImage>,
|
|
321
|
+
): readonly PreparedPromptSegment[] {
|
|
322
|
+
return Object.freeze(
|
|
323
|
+
segments.map((segment) =>
|
|
324
|
+
segment.media === undefined
|
|
325
|
+
? segment
|
|
326
|
+
: Object.freeze({
|
|
327
|
+
...segment,
|
|
328
|
+
media: Object.freeze(
|
|
329
|
+
segment.media.map((media) => {
|
|
330
|
+
const image = images.get(media.asset.assetId);
|
|
331
|
+
if (image === undefined) {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`Image ${media.asset.assetId.slice(0, 12)}… was not materialized.`,
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
return Object.freeze({
|
|
337
|
+
...media,
|
|
338
|
+
width: image.width,
|
|
339
|
+
height: image.height,
|
|
340
|
+
planningTokens: image.planningTokens,
|
|
341
|
+
});
|
|
342
|
+
}),
|
|
343
|
+
),
|
|
344
|
+
}),
|
|
345
|
+
),
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
280
349
|
function yieldToEventLoop(): Promise<void> {
|
|
281
350
|
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
282
351
|
}
|
|
@@ -10,6 +10,10 @@ import type {
|
|
|
10
10
|
ToolCall,
|
|
11
11
|
UserMessage,
|
|
12
12
|
} from "../agent/types";
|
|
13
|
+
import {
|
|
14
|
+
toolResultText,
|
|
15
|
+
validateToolResultContent,
|
|
16
|
+
} from "../agent/tool-result-content";
|
|
13
17
|
import type { RuntimeSessionContext } from "../agent/runtime-session";
|
|
14
18
|
import {
|
|
15
19
|
parseImageAssetId,
|
|
@@ -61,11 +65,31 @@ export function toOpenAIResponsesItems(
|
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
if (message.role === "tool") {
|
|
68
|
+
validateToolResultContent(message.content);
|
|
69
|
+
const hasImage = message.content.some((block) => block.type === "image");
|
|
64
70
|
return [
|
|
65
71
|
{
|
|
66
72
|
type: "function_call_output",
|
|
67
73
|
call_id: message.providerToolCallId,
|
|
68
|
-
output:
|
|
74
|
+
output: hasImage
|
|
75
|
+
? message.content.map((block) =>
|
|
76
|
+
block.type === "text"
|
|
77
|
+
? ({ type: "input_text", text: block.text } as const)
|
|
78
|
+
: ({
|
|
79
|
+
type: "input_image",
|
|
80
|
+
detail: "auto" as const,
|
|
81
|
+
image_url:
|
|
82
|
+
options.materializedImages === undefined
|
|
83
|
+
? (imageAssetUrlMarker(
|
|
84
|
+
block.asset.assetId,
|
|
85
|
+
) as unknown as string)
|
|
86
|
+
: requireMaterializedImage(
|
|
87
|
+
options.materializedImages,
|
|
88
|
+
block.asset.assetId,
|
|
89
|
+
),
|
|
90
|
+
} as const),
|
|
91
|
+
)
|
|
92
|
+
: toolResultText(message.content),
|
|
69
93
|
},
|
|
70
94
|
];
|
|
71
95
|
}
|
|
@@ -8,9 +8,8 @@ import {
|
|
|
8
8
|
IMAGE_INPUT_POLICY,
|
|
9
9
|
IMAGE_INPUT_POLICY_VERSION,
|
|
10
10
|
} from "../image/image-input-policy";
|
|
11
|
-
import type { InputTokenEstimator } from "./input-token-estimator";
|
|
12
11
|
import type { ModelContextBudget } from "./model-context-profile";
|
|
13
|
-
import { ProviderResponseError } from "./model-client";
|
|
12
|
+
import { ProviderResponseError, validateModelModalities } from "./model-client";
|
|
14
13
|
import type {
|
|
15
14
|
MaterializedModelRequest,
|
|
16
15
|
ModelClient,
|
|
@@ -22,9 +21,9 @@ import type {
|
|
|
22
21
|
PreparedModelRequest,
|
|
23
22
|
PreparedPromptSegment,
|
|
24
23
|
} from "./model-client";
|
|
25
|
-
import { MoonshotInputTokenEstimator } from "./moonshot-input-token-estimator";
|
|
26
24
|
import {
|
|
27
25
|
deepFreeze,
|
|
26
|
+
imageToolSegment,
|
|
28
27
|
imageUserSegment,
|
|
29
28
|
materializeOpenAIRequest,
|
|
30
29
|
normalizedEndpointPolicy,
|
|
@@ -38,11 +37,10 @@ import {
|
|
|
38
37
|
toOpenAIResponsesTools,
|
|
39
38
|
} from "./openai-responses-mapping";
|
|
40
39
|
import { OpenAIResponsesStreamAccumulator } from "./openai-responses-stream";
|
|
41
|
-
import { responsesPayloadForChatTokenEstimator } from "./openai-responses-token-estimator";
|
|
42
40
|
import type { ReasoningEffortController } from "./reasoning-effort";
|
|
43
41
|
import { sha256, stableJsonStringify } from "./model-request-preflight";
|
|
44
42
|
|
|
45
|
-
const OPENAI_RESPONSES_SERIALIZATION_VERSION = "openai-responses-
|
|
43
|
+
const OPENAI_RESPONSES_SERIALIZATION_VERSION = "openai-responses-v2";
|
|
46
44
|
const OPENAI_RESPONSES_TIMEOUT_MS = 30 * 60 * 1_000;
|
|
47
45
|
|
|
48
46
|
export class OpenAIResponsesModelClient implements ModelClient {
|
|
@@ -50,8 +48,8 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
50
48
|
adapter: "openai-responses",
|
|
51
49
|
serializationVersion: OPENAI_RESPONSES_SERIALIZATION_VERSION,
|
|
52
50
|
});
|
|
53
|
-
readonly inputTokenEstimator?: InputTokenEstimator;
|
|
54
51
|
readonly inputModalities: readonly ("text" | "image")[];
|
|
52
|
+
readonly toolResultModalities: readonly ("text" | "image")[];
|
|
55
53
|
readonly reasoningEffort?: ReasoningEffortController;
|
|
56
54
|
private readonly client: OpenAI;
|
|
57
55
|
private readonly preparedRequests = new WeakSet<object>();
|
|
@@ -65,14 +63,8 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
65
63
|
contextBudget: ModelContextBudget;
|
|
66
64
|
baseURL?: string;
|
|
67
65
|
inputModalities?: readonly ("text" | "image")[];
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
model: string;
|
|
71
|
-
apiBase: string;
|
|
72
|
-
apiKey: string;
|
|
73
|
-
timeoutMs: number;
|
|
74
|
-
maxRetries: 0;
|
|
75
|
-
};
|
|
66
|
+
toolResultModalities?: readonly ("text" | "image")[];
|
|
67
|
+
profileName?: string;
|
|
76
68
|
model: string;
|
|
77
69
|
providerName?: string;
|
|
78
70
|
reasoningEffort?: ReasoningEffortController;
|
|
@@ -84,14 +76,15 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
84
76
|
this.provider = options.providerName ?? "responses-compatible";
|
|
85
77
|
this.stream = options.stream ?? true;
|
|
86
78
|
this.reasoningEffort = options.reasoningEffort;
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
const modalities = validateModelModalities({
|
|
80
|
+
profileName: options.profileName,
|
|
81
|
+
adapter: this.messageProtocol.adapter,
|
|
82
|
+
inputModalities: options.inputModalities ?? ["text"],
|
|
83
|
+
toolResultModalities: options.toolResultModalities ?? ["text"],
|
|
84
|
+
adapterToolResultModalities: ["text", "image"],
|
|
85
|
+
});
|
|
86
|
+
this.inputModalities = modalities.inputModalities;
|
|
87
|
+
this.toolResultModalities = modalities.toolResultModalities;
|
|
95
88
|
this.client = new OpenAI({
|
|
96
89
|
apiKey: options.apiKey,
|
|
97
90
|
baseURL: options.baseURL,
|
|
@@ -99,16 +92,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
99
92
|
maxRetries: 0,
|
|
100
93
|
fetch: options.fetch,
|
|
101
94
|
});
|
|
102
|
-
if (options.tokenEstimator !== undefined) {
|
|
103
|
-
this.inputTokenEstimator = new MoonshotInputTokenEstimator({
|
|
104
|
-
apiKey: options.tokenEstimator.apiKey,
|
|
105
|
-
baseURL: options.tokenEstimator.apiBase,
|
|
106
|
-
model: options.tokenEstimator.model,
|
|
107
|
-
timeoutMs: options.tokenEstimator.timeoutMs,
|
|
108
|
-
fetch: options.fetch,
|
|
109
|
-
payloadMapper: responsesPayloadForChatTokenEstimator,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
95
|
}
|
|
113
96
|
|
|
114
97
|
prepare(input: ModelRequestInput): PreparedModelRequest {
|
|
@@ -139,11 +122,17 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
139
122
|
const messageSegments = input.messages.map(
|
|
140
123
|
(message, index): PreparedPromptSegment =>
|
|
141
124
|
message.role === "user" && message.attachments !== undefined
|
|
142
|
-
? imageUserSegment(message)
|
|
143
|
-
:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
125
|
+
? imageUserSegment(message, index + 1)
|
|
126
|
+
: message.role === "tool"
|
|
127
|
+
? imageToolSegment(
|
|
128
|
+
message,
|
|
129
|
+
index + 1,
|
|
130
|
+
stableJsonStringify(itemsByMessage[index]),
|
|
131
|
+
)
|
|
132
|
+
: {
|
|
133
|
+
kind: segmentKind(message.role),
|
|
134
|
+
normalizedText: stableJsonStringify(itemsByMessage[index]),
|
|
135
|
+
},
|
|
147
136
|
);
|
|
148
137
|
const mediaOccurrenceCount = messageSegments.reduce(
|
|
149
138
|
(total, segment) => total + (segment.media?.length ?? 0),
|
|
@@ -158,14 +147,12 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
158
147
|
requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
159
148
|
stream: this.stream,
|
|
160
149
|
inputModalities: this.inputModalities,
|
|
150
|
+
toolResultModalities: this.toolResultModalities,
|
|
161
151
|
requestPolicy: { store: false, toolChoice: "auto" },
|
|
162
152
|
imagePolicy: {
|
|
163
153
|
version: IMAGE_INPUT_POLICY_VERSION,
|
|
164
154
|
...IMAGE_INPUT_POLICY,
|
|
165
155
|
},
|
|
166
|
-
...(this.inputTokenEstimator === undefined
|
|
167
|
-
? {}
|
|
168
|
-
: { tokenEstimator: this.inputTokenEstimator.compatibility }),
|
|
169
156
|
}),
|
|
170
157
|
);
|
|
171
158
|
const prepared: PreparedModelRequest = {
|