tinker-agent 1.0.65 → 1.2.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 +36 -0
- package/LICENSE +201 -0
- package/NOTICE +6 -0
- package/README.md +90 -9
- package/bin/tinker.js +39 -0
- package/node_modules/ansi-regex/index.d.ts +33 -0
- package/node_modules/ansi-regex/index.js +14 -0
- package/node_modules/ansi-regex/license +9 -0
- package/node_modules/ansi-regex/package.json +61 -0
- package/node_modules/ansi-regex/readme.md +66 -0
- package/node_modules/ansi-styles/index.d.ts +236 -0
- package/node_modules/ansi-styles/index.js +223 -0
- package/node_modules/ansi-styles/license +9 -0
- package/node_modules/ansi-styles/package.json +54 -0
- package/node_modules/ansi-styles/readme.md +173 -0
- package/node_modules/chalk/license +9 -0
- package/node_modules/chalk/package.json +83 -0
- package/node_modules/chalk/readme.md +297 -0
- package/node_modules/chalk/source/index.d.ts +325 -0
- package/node_modules/chalk/source/index.js +225 -0
- package/node_modules/chalk/source/utilities.js +33 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
- package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
- package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
- package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
- package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
- package/node_modules/character-entities/index.d.ts +6 -0
- package/node_modules/character-entities/index.js +2132 -0
- package/node_modules/character-entities/license +22 -0
- package/node_modules/character-entities/package.json +78 -0
- package/node_modules/character-entities/readme.md +152 -0
- package/node_modules/decode-named-character-reference/index.d.ts +13 -0
- package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
- package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
- package/node_modules/decode-named-character-reference/index.dom.js +32 -0
- package/node_modules/decode-named-character-reference/index.js +19 -0
- package/node_modules/decode-named-character-reference/license +22 -0
- package/node_modules/decode-named-character-reference/package.json +90 -0
- package/node_modules/decode-named-character-reference/readme.md +136 -0
- package/node_modules/get-east-asian-width/index.d.ts +60 -0
- package/node_modules/get-east-asian-width/index.js +30 -0
- package/node_modules/get-east-asian-width/license +9 -0
- package/node_modules/get-east-asian-width/lookup-data.js +21 -0
- package/node_modules/get-east-asian-width/lookup.js +138 -0
- package/node_modules/get-east-asian-width/package.json +71 -0
- package/node_modules/get-east-asian-width/readme.md +65 -0
- package/node_modules/get-east-asian-width/utilities.js +24 -0
- package/node_modules/has-flag/index.d.ts +39 -0
- package/node_modules/has-flag/index.js +8 -0
- package/node_modules/has-flag/license +9 -0
- package/node_modules/has-flag/package.json +49 -0
- package/node_modules/has-flag/readme.md +74 -0
- package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
- package/node_modules/is-fullwidth-code-point/index.js +12 -0
- package/node_modules/is-fullwidth-code-point/license +9 -0
- package/node_modules/is-fullwidth-code-point/package.json +53 -0
- package/node_modules/is-fullwidth-code-point/readme.md +31 -0
- package/node_modules/markdansi/LICENSE +21 -0
- package/node_modules/markdansi/README.md +167 -0
- package/node_modules/markdansi/dist/ast.d.ts +99 -0
- package/node_modules/markdansi/dist/ast.js +1 -0
- package/node_modules/markdansi/dist/cli.d.ts +17 -0
- package/node_modules/markdansi/dist/cli.js +208 -0
- package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
- package/node_modules/markdansi/dist/hyperlink.js +26 -0
- package/node_modules/markdansi/dist/index.d.ts +10 -0
- package/node_modules/markdansi/dist/index.js +16 -0
- package/node_modules/markdansi/dist/parser.d.ts +2 -0
- package/node_modules/markdansi/dist/parser.js +205 -0
- package/node_modules/markdansi/dist/render.d.ts +9 -0
- package/node_modules/markdansi/dist/render.js +680 -0
- package/node_modules/markdansi/dist/stream.d.ts +39 -0
- package/node_modules/markdansi/dist/stream.js +192 -0
- package/node_modules/markdansi/dist/theme.d.ts +18 -0
- package/node_modules/markdansi/dist/theme.js +105 -0
- package/node_modules/markdansi/dist/types.d.ts +58 -0
- package/node_modules/markdansi/dist/types.js +1 -0
- package/node_modules/markdansi/dist/wrap.d.ts +10 -0
- package/node_modules/markdansi/dist/wrap.js +73 -0
- package/node_modules/markdansi/docs/spec.md +110 -0
- package/node_modules/markdansi/package.json +78 -0
- package/node_modules/markdansi/tsconfig.json +18 -0
- package/node_modules/marked/LICENSE +44 -0
- package/node_modules/marked/README.md +115 -0
- package/node_modules/marked/bin/main.js +284 -0
- package/node_modules/marked/bin/marked.js +16 -0
- package/node_modules/marked/lib/marked.d.ts +760 -0
- package/node_modules/marked/lib/marked.esm.js +77 -0
- package/node_modules/marked/lib/marked.esm.js.map +7 -0
- package/node_modules/marked/lib/marked.umd.js +79 -0
- package/node_modules/marked/lib/marked.umd.js.map +7 -0
- package/node_modules/marked/man/marked.1 +113 -0
- package/node_modules/marked/man/marked.1.md +93 -0
- package/node_modules/marked/package.json +103 -0
- package/node_modules/slice-ansi/index.d.ts +19 -0
- package/node_modules/slice-ansi/index.js +317 -0
- package/node_modules/slice-ansi/license +10 -0
- package/node_modules/slice-ansi/package.json +59 -0
- package/node_modules/slice-ansi/readme.md +55 -0
- package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
- package/node_modules/string-width/index.d.ts +39 -0
- package/node_modules/string-width/index.js +203 -0
- package/node_modules/string-width/license +9 -0
- package/node_modules/string-width/package.json +65 -0
- package/node_modules/string-width/readme.md +66 -0
- package/node_modules/strip-ansi/index.d.ts +15 -0
- package/node_modules/strip-ansi/index.js +19 -0
- package/node_modules/strip-ansi/license +9 -0
- package/node_modules/strip-ansi/package.json +59 -0
- package/node_modules/strip-ansi/readme.md +37 -0
- package/node_modules/supports-color/browser.d.ts +1 -0
- package/node_modules/supports-color/browser.js +35 -0
- package/node_modules/supports-color/index.d.ts +55 -0
- package/node_modules/supports-color/index.js +202 -0
- package/node_modules/supports-color/license +9 -0
- package/node_modules/supports-color/package.json +64 -0
- package/node_modules/supports-color/readme.md +75 -0
- package/node_modules/supports-hyperlinks/browser.js +10 -0
- package/node_modules/supports-hyperlinks/index.d.ts +25 -0
- package/node_modules/supports-hyperlinks/index.js +163 -0
- package/node_modules/supports-hyperlinks/license +10 -0
- package/node_modules/supports-hyperlinks/package.json +47 -0
- package/node_modules/supports-hyperlinks/readme.md +45 -0
- package/package.json +41 -10
- package/patches/markdansi@0.3.2.patch +22 -2
- package/src/agent/context-builder.ts +24 -10
- package/src/agent/context-meter.ts +89 -5
- package/src/agent/loop.ts +107 -10
- package/src/agent/runtime-session.ts +1437 -82
- package/src/agent/session-ledger.ts +169 -29
- package/src/agent/types.ts +12 -1
- package/src/agent/user-prompt-projection.ts +62 -0
- package/src/cli/config.ts +57 -14
- package/src/cli/model-profiles.ts +124 -0
- package/src/cli/run-runner.ts +4 -1
- package/src/cli/tui-runner.tsx +68 -30
- package/src/context/compiled-context-hash.ts +1 -1
- package/src/context/compiled-context-validator.ts +92 -15
- package/src/context/context-automation-policy.ts +121 -0
- package/src/context/context-manager.ts +390 -11
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +11 -1
- package/src/context/context-revision-compiler.ts +190 -27
- package/src/context/context-revision.ts +105 -19
- package/src/context/context-surface.ts +329 -0
- package/src/context/prefix-retirement-planner.ts +570 -0
- package/src/context/protocol-frame.ts +30 -3
- package/src/context/recall-retirement-contract.ts +27 -0
- package/src/context/swap-planner.ts +34 -10
- package/src/events/observation-text-log.ts +3 -3
- package/src/events/stdout-event-printer.ts +39 -3
- package/src/events/types.ts +172 -35
- package/src/ids/runtime-id.ts +6 -0
- package/src/ids/uuid-v7.ts +7 -0
- package/src/image/image-asset-store.ts +319 -0
- package/src/image/image-input-policy.ts +14 -0
- package/src/image/image-probe.ts +233 -0
- package/src/image/image-types.ts +262 -0
- package/src/instructions/project-instructions.ts +4 -1
- package/src/mcp/mcp-manager.ts +83 -19
- package/src/model/fake-model-client.ts +9 -1
- package/src/model/input-token-estimator.ts +25 -0
- package/src/model/model-client.ts +60 -0
- package/src/model/model-request-preflight.ts +1 -0
- package/src/model/moonshot-input-token-estimator.ts +107 -0
- package/src/model/openai-chat-mapping.ts +77 -1
- package/src/model/openai-chat-model-client.ts +389 -59
- package/src/model/openai-chat-stream.ts +272 -0
- package/src/model/prompt-prefix-hash.ts +2 -4
- package/src/model/token-estimator.ts +4 -0
- package/src/observation/observation-builder.ts +29 -0
- package/src/session/resume-projection.ts +94 -34
- package/src/session/session-catalog.ts +4 -4
- package/src/session/session-errors.ts +1 -1
- package/src/session/session-history-reader.ts +151 -4
- package/src/session/session-last-response-reader.ts +189 -0
- package/src/session/session-schema.ts +349 -35
- package/src/session/session-store.ts +3266 -255
- package/src/session/sqlite-session-ledger.ts +9 -4
- package/src/skills/skill-catalog.ts +182 -0
- package/src/skills/skill-context.ts +364 -0
- package/src/skills/skill-loader.ts +779 -0
- package/src/skills/skill-tool.ts +297 -0
- package/src/tools/edit.ts +51 -64
- package/src/tools/ensure-parent-directory.ts +6 -0
- package/src/tools/read.ts +2 -2
- package/src/tools/recall.ts +43 -40
- package/src/tools/registry.ts +24 -3
- package/src/tools/ripgrep.ts +3 -2
- package/src/tools/types.ts +43 -4
- package/src/tools/write.ts +20 -31
- package/src/tui/app.tsx +333 -96
- package/src/tui/clipboard.ts +5 -0
- package/src/tui/components/mcp-panel.tsx +28 -0
- package/src/tui/components/prompt-input.tsx +790 -257
- package/src/tui/components/skills-panel.tsx +31 -0
- package/src/tui/components/timeline.tsx +30 -1
- package/src/tui/event-store.ts +85 -2
- package/src/tui/file-mention.ts +234 -0
- package/src/tui/line-editor.ts +1 -0
- package/src/tui/project-slash-commands.ts +229 -0
- package/src/tui/prompt-draft.ts +510 -0
- package/src/tui/prompt-history.ts +294 -22
- package/src/tui/slash-commands.ts +46 -2
- package/src/tui/tui-session-controller.ts +110 -48
- package/src/tui/workspace-file-search.ts +94 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isCanonicalUuidV7 } from "../ids/uuid-v7";
|
|
3
|
+
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
4
|
+
import { IMAGE_INPUT_POLICY } from "./image-input-policy";
|
|
5
|
+
|
|
6
|
+
export type ImageMimeType = (typeof IMAGE_INPUT_POLICY.allowedMimeTypes)[number];
|
|
7
|
+
|
|
8
|
+
export type ImageAttachmentId = string & {
|
|
9
|
+
readonly __imageAttachmentId: "image-attachment";
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ImageAssetId = string & {
|
|
13
|
+
readonly __imageAssetId: "image-asset";
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type CodePointRange = {
|
|
17
|
+
readonly start: number;
|
|
18
|
+
readonly end: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type ImageAssetRef = {
|
|
22
|
+
readonly assetId: ImageAssetId;
|
|
23
|
+
readonly mimeType: ImageMimeType;
|
|
24
|
+
readonly byteLength: number;
|
|
25
|
+
readonly width: number;
|
|
26
|
+
readonly height: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type UserImageAttachment = ImageAssetRef & {
|
|
30
|
+
readonly attachmentId: ImageAttachmentId;
|
|
31
|
+
readonly label: string;
|
|
32
|
+
readonly range: CodePointRange;
|
|
33
|
+
readonly originalName: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type UserMessage = {
|
|
37
|
+
readonly role: "user";
|
|
38
|
+
readonly content: string;
|
|
39
|
+
readonly attachments?: readonly UserImageAttachment[];
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function imageAssetIdForBytes(bytes: Uint8Array): ImageAssetId {
|
|
43
|
+
return createHash("sha256").update(bytes).digest("hex") as ImageAssetId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function parseImageAssetId(value: string): ImageAssetId {
|
|
47
|
+
if (!/^[0-9a-f]{64}$/.test(value)) {
|
|
48
|
+
throw new Error(`Invalid image asset ID: ${JSON.stringify(value)}.`);
|
|
49
|
+
}
|
|
50
|
+
return value as ImageAssetId;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseImageAttachmentId(value: string): ImageAttachmentId {
|
|
54
|
+
if (!isCanonicalUuidV7(value)) {
|
|
55
|
+
throw new Error(`Invalid image attachment ID: ${JSON.stringify(value)}.`);
|
|
56
|
+
}
|
|
57
|
+
return value as ImageAttachmentId;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function normalizeOriginalImageName(value: string): string {
|
|
61
|
+
const normalized = value.normalize("NFC");
|
|
62
|
+
validateOriginalImageName(normalized);
|
|
63
|
+
return normalized;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function validateOriginalImageName(value: string): void {
|
|
67
|
+
if (
|
|
68
|
+
value === "" ||
|
|
69
|
+
value !== value.normalize("NFC") ||
|
|
70
|
+
value === "." ||
|
|
71
|
+
value === ".." ||
|
|
72
|
+
value.includes("/") ||
|
|
73
|
+
value.includes("\\") ||
|
|
74
|
+
hasControlCharacter(value) ||
|
|
75
|
+
Buffer.byteLength(value, "utf8") > 255
|
|
76
|
+
) {
|
|
77
|
+
throw new Error(`Invalid image original name: ${JSON.stringify(value)}.`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function validateImageAssetRef(asset: ImageAssetRef): void {
|
|
82
|
+
parseImageAssetId(asset.assetId);
|
|
83
|
+
if (!IMAGE_INPUT_POLICY.allowedMimeTypes.includes(asset.mimeType)) {
|
|
84
|
+
throw new Error(`Unsupported image MIME type: ${JSON.stringify(asset.mimeType)}.`);
|
|
85
|
+
}
|
|
86
|
+
requirePositiveSafeInteger(asset.byteLength, "image byteLength");
|
|
87
|
+
requirePositiveSafeInteger(asset.width, "image width");
|
|
88
|
+
requirePositiveSafeInteger(asset.height, "image height");
|
|
89
|
+
if (asset.byteLength > IMAGE_INPUT_POLICY.maxBytesPerImage) {
|
|
90
|
+
throw new Error(
|
|
91
|
+
`Image byte length ${asset.byteLength} exceeds ${IMAGE_INPUT_POLICY.maxBytesPerImage}.`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
if (
|
|
95
|
+
asset.width > IMAGE_INPUT_POLICY.maxLongEdge ||
|
|
96
|
+
asset.height > IMAGE_INPUT_POLICY.maxLongEdge
|
|
97
|
+
) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Image dimensions ${asset.width}x${asset.height} exceed ${IMAGE_INPUT_POLICY.maxLongEdge}px.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const pixels = asset.width * asset.height;
|
|
103
|
+
if (!Number.isSafeInteger(pixels) || pixels > IMAGE_INPUT_POLICY.maxPixels) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`Image pixel count ${pixels} exceeds ${IMAGE_INPUT_POLICY.maxPixels}.`,
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function validateUserMessage(message: UserMessage): void {
|
|
111
|
+
if (message.role !== "user" || typeof message.content !== "string") {
|
|
112
|
+
throw new Error("Invalid canonical user message.");
|
|
113
|
+
}
|
|
114
|
+
const attachments = message.attachments;
|
|
115
|
+
if (attachments !== undefined && attachments.length === 0) {
|
|
116
|
+
throw new Error("Canonical user message attachments must be omitted when empty.");
|
|
117
|
+
}
|
|
118
|
+
if (attachments === undefined) {
|
|
119
|
+
if (message.content.trim() === "") {
|
|
120
|
+
throw new Error("Canonical user message must not be empty.");
|
|
121
|
+
}
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (attachments.length > IMAGE_INPUT_POLICY.maxImagesPerMessage) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`User message has ${attachments.length} images; maximum is ${IMAGE_INPUT_POLICY.maxImagesPerMessage}.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
if (message.content.trim() === "" && attachments.length === 0) {
|
|
130
|
+
throw new Error("Canonical user message must contain text or an image.");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const chars = [...message.content];
|
|
134
|
+
const attachmentIds = new Set<string>();
|
|
135
|
+
const labels = new Set<string>();
|
|
136
|
+
let previousEnd = -1;
|
|
137
|
+
for (let index = 0; index < attachments.length; index += 1) {
|
|
138
|
+
const attachment = attachments[index];
|
|
139
|
+
parseImageAttachmentId(attachment.attachmentId);
|
|
140
|
+
validateImageAssetRef(attachment);
|
|
141
|
+
validateOriginalImageName(attachment.originalName);
|
|
142
|
+
const expectedLabel = imageLabel(index + 1);
|
|
143
|
+
if (attachment.label !== expectedLabel) {
|
|
144
|
+
throw new Error(
|
|
145
|
+
`Image attachment label must be ${expectedLabel}; received ${attachment.label}.`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
validateCodePointRange(attachment.range, chars.length);
|
|
149
|
+
if (attachment.range.start < previousEnd) {
|
|
150
|
+
throw new Error("Image attachment ranges must be ordered and non-overlapping.");
|
|
151
|
+
}
|
|
152
|
+
const selected = chars.slice(attachment.range.start, attachment.range.end).join("");
|
|
153
|
+
if (selected !== attachment.label) {
|
|
154
|
+
throw new Error("Image attachment range does not match its label.");
|
|
155
|
+
}
|
|
156
|
+
if (attachmentIds.has(attachment.attachmentId)) {
|
|
157
|
+
throw new Error("Image attachment IDs must be unique within a message.");
|
|
158
|
+
}
|
|
159
|
+
if (labels.has(attachment.label)) {
|
|
160
|
+
throw new Error("Image attachment labels must be unique within a message.");
|
|
161
|
+
}
|
|
162
|
+
attachmentIds.add(attachment.attachmentId);
|
|
163
|
+
labels.add(attachment.label);
|
|
164
|
+
previousEnd = attachment.range.end;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
assertNoUnboundImageLabels(message.content, attachments);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function canonicalUserMessageHash(message: UserMessage): string {
|
|
171
|
+
validateUserMessage(message);
|
|
172
|
+
return sha256(
|
|
173
|
+
stableJsonStringify({
|
|
174
|
+
content: message.content,
|
|
175
|
+
...(message.attachments === undefined
|
|
176
|
+
? {}
|
|
177
|
+
: {
|
|
178
|
+
attachments: message.attachments.map((attachment) => ({
|
|
179
|
+
attachmentId: attachment.attachmentId,
|
|
180
|
+
assetId: attachment.assetId,
|
|
181
|
+
label: attachment.label,
|
|
182
|
+
range: attachment.range,
|
|
183
|
+
mimeType: attachment.mimeType,
|
|
184
|
+
byteLength: attachment.byteLength,
|
|
185
|
+
width: attachment.width,
|
|
186
|
+
height: attachment.height,
|
|
187
|
+
originalName: attachment.originalName,
|
|
188
|
+
})),
|
|
189
|
+
}),
|
|
190
|
+
}),
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function imageLabel(index: number): string {
|
|
195
|
+
if (
|
|
196
|
+
!Number.isSafeInteger(index) ||
|
|
197
|
+
index < 1 ||
|
|
198
|
+
index > IMAGE_INPUT_POLICY.maxImagesPerMessage
|
|
199
|
+
) {
|
|
200
|
+
throw new Error(`Image label index is outside the MVP range: ${index}.`);
|
|
201
|
+
}
|
|
202
|
+
return `[Image #${index}]`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function codePointSlice(value: string, range: CodePointRange): string {
|
|
206
|
+
const chars = [...value];
|
|
207
|
+
validateCodePointRange(range, chars.length);
|
|
208
|
+
return chars.slice(range.start, range.end).join("");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateCodePointRange(range: CodePointRange, length: number): void {
|
|
212
|
+
if (
|
|
213
|
+
!Number.isSafeInteger(range.start) ||
|
|
214
|
+
!Number.isSafeInteger(range.end) ||
|
|
215
|
+
range.start < 0 ||
|
|
216
|
+
range.end <= range.start ||
|
|
217
|
+
range.end > length
|
|
218
|
+
) {
|
|
219
|
+
throw new Error(
|
|
220
|
+
`Invalid image code-point range [${range.start}, ${range.end}) for length ${length}.`,
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function assertNoUnboundImageLabels(
|
|
226
|
+
content: string,
|
|
227
|
+
attachments: readonly UserImageAttachment[],
|
|
228
|
+
): void {
|
|
229
|
+
const chars = [...content];
|
|
230
|
+
for (const attachment of attachments) {
|
|
231
|
+
const label = [...attachment.label];
|
|
232
|
+
for (let start = 0; start <= chars.length - label.length; start += 1) {
|
|
233
|
+
if (!label.every((char, offset) => chars[start + offset] === char)) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (
|
|
237
|
+
start !== attachment.range.start ||
|
|
238
|
+
start + label.length !== attachment.range.end
|
|
239
|
+
) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`Prompt contains an unbound literal ${attachment.label}; rewrite it before submitting.`,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function requirePositiveSafeInteger(value: number, name: string): void {
|
|
249
|
+
if (!Number.isSafeInteger(value) || value < 1) {
|
|
250
|
+
throw new Error(`${name} must be a positive safe integer.`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function hasControlCharacter(value: string): boolean {
|
|
255
|
+
return [...value].some((character) => {
|
|
256
|
+
const codePoint = character.codePointAt(0);
|
|
257
|
+
return (
|
|
258
|
+
codePoint !== undefined &&
|
|
259
|
+
(codePoint <= 0x1f || (codePoint >= 0x7f && codePoint <= 0x9f))
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
@@ -44,7 +44,7 @@ export async function loadProjectInstructions(
|
|
|
44
44
|
return { workspaceRoot: canonicalRoot };
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
export function
|
|
47
|
+
export function buildBaseSystemPrompt(input: {
|
|
48
48
|
workspaceRoot: string;
|
|
49
49
|
runtimeInstructions: string;
|
|
50
50
|
projectInstructions: ProjectInstructionsSnapshot;
|
|
@@ -79,6 +79,9 @@ ${content}</instruction_file>
|
|
|
79
79
|
return `${runtime}\n\n${project}`;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
export const buildSystemPrompt = buildBaseSystemPrompt;
|
|
83
|
+
export { buildActiveSystemPrompt } from "../skills/skill-context";
|
|
84
|
+
|
|
82
85
|
export function projectInstructionManifest(
|
|
83
86
|
snapshot: ProjectInstructionsSnapshot,
|
|
84
87
|
): ProjectInstructionManifest | undefined {
|
package/src/mcp/mcp-manager.ts
CHANGED
|
@@ -21,10 +21,20 @@ export type McpClientFactory = (
|
|
|
21
21
|
) => Promise<McpClientConnection>;
|
|
22
22
|
|
|
23
23
|
export type McpManager = {
|
|
24
|
-
executors: ToolExecutor[];
|
|
24
|
+
readonly executors: ToolExecutor[];
|
|
25
|
+
readonly inventory: McpInventorySnapshot;
|
|
25
26
|
dispose(): Promise<void>;
|
|
26
27
|
};
|
|
27
28
|
|
|
29
|
+
export type McpInventorySnapshot = {
|
|
30
|
+
readonly servers: readonly McpServerInventory[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type McpServerInventory = {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly tools: readonly string[];
|
|
36
|
+
};
|
|
37
|
+
|
|
28
38
|
export type CreateMcpManagerOptions = {
|
|
29
39
|
config: McpConfig;
|
|
30
40
|
runtimeSession: RuntimeSessionContext;
|
|
@@ -33,6 +43,17 @@ export type CreateMcpManagerOptions = {
|
|
|
33
43
|
maxObservationChars?: number;
|
|
34
44
|
};
|
|
35
45
|
|
|
46
|
+
export class McpInitializationError extends Error {
|
|
47
|
+
constructor(
|
|
48
|
+
readonly serverName: string,
|
|
49
|
+
readonly stage: "connect" | "list_tools" | "validate_tools",
|
|
50
|
+
options?: ErrorOptions,
|
|
51
|
+
) {
|
|
52
|
+
super(`MCP server ${serverName} failed during ${stage}.`, options);
|
|
53
|
+
this.name = "McpInitializationError";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
36
57
|
export async function createMcpManager(
|
|
37
58
|
options: CreateMcpManagerOptions,
|
|
38
59
|
): Promise<McpManager> {
|
|
@@ -44,6 +65,7 @@ export async function createMcpManager(
|
|
|
44
65
|
parsePositiveIntegerEnv("TINKER_MCP_MAX_OBSERVATION_CHARS");
|
|
45
66
|
const connections: McpClientConnection[] = [];
|
|
46
67
|
const executors: ToolExecutor[] = [];
|
|
68
|
+
const servers: McpServerInventory[] = [];
|
|
47
69
|
|
|
48
70
|
try {
|
|
49
71
|
for (const [serverName, serverConfig] of options.config.servers) {
|
|
@@ -58,19 +80,22 @@ export async function createMcpManager(
|
|
|
58
80
|
sessionId: options.runtimeSession.sessionId,
|
|
59
81
|
data: {
|
|
60
82
|
serverName,
|
|
61
|
-
error:
|
|
83
|
+
error: "connection failed",
|
|
62
84
|
},
|
|
63
85
|
});
|
|
64
|
-
|
|
86
|
+
throw new McpInitializationError(serverName, "connect", {
|
|
87
|
+
cause: error,
|
|
88
|
+
});
|
|
65
89
|
}
|
|
66
90
|
|
|
67
91
|
try {
|
|
68
92
|
tools = (await connection.client.listTools()).tools;
|
|
69
93
|
} catch (error) {
|
|
94
|
+
let cause: unknown = error;
|
|
70
95
|
try {
|
|
71
96
|
await connection.close();
|
|
72
97
|
} catch (closeError) {
|
|
73
|
-
|
|
98
|
+
cause = new AggregateError(
|
|
74
99
|
[error, closeError],
|
|
75
100
|
`Failed to inspect and close MCP server ${serverName}.`,
|
|
76
101
|
{ cause: closeError },
|
|
@@ -81,31 +106,55 @@ export async function createMcpManager(
|
|
|
81
106
|
sessionId: options.runtimeSession.sessionId,
|
|
82
107
|
data: {
|
|
83
108
|
serverName,
|
|
84
|
-
error:
|
|
109
|
+
error: "tool discovery failed",
|
|
85
110
|
},
|
|
86
111
|
});
|
|
87
|
-
|
|
112
|
+
throw new McpInitializationError(serverName, "list_tools", {
|
|
113
|
+
cause,
|
|
114
|
+
});
|
|
88
115
|
}
|
|
89
116
|
|
|
90
117
|
connections.push(connection);
|
|
91
118
|
|
|
92
119
|
const seenToolNames = new Set<string>();
|
|
120
|
+
try {
|
|
121
|
+
for (const tool of tools) {
|
|
122
|
+
if (
|
|
123
|
+
tool.name.trim() === "" ||
|
|
124
|
+
typeof tool.inputSchema !== "object" ||
|
|
125
|
+
tool.inputSchema === null ||
|
|
126
|
+
Array.isArray(tool.inputSchema) ||
|
|
127
|
+
tool.inputSchema.type !== "object"
|
|
128
|
+
) {
|
|
129
|
+
throw new Error("MCP tool name or input schema is invalid.");
|
|
130
|
+
}
|
|
131
|
+
if (seenToolNames.has(tool.name)) {
|
|
132
|
+
throw new Error(`Duplicate MCP tool name ${tool.name}.`);
|
|
133
|
+
}
|
|
93
134
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
135
|
+
seenToolNames.add(tool.name);
|
|
136
|
+
executors.push(
|
|
137
|
+
createMcpToolExecutor({
|
|
138
|
+
client: connection.client,
|
|
139
|
+
serverName,
|
|
140
|
+
tool,
|
|
141
|
+
timeoutMs,
|
|
142
|
+
maxObservationChars,
|
|
143
|
+
}),
|
|
144
|
+
);
|
|
97
145
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
146
|
+
} catch (error) {
|
|
147
|
+
await options.runtimeSession.append({
|
|
148
|
+
type: "mcp.server.failed",
|
|
149
|
+
sessionId: options.runtimeSession.sessionId,
|
|
150
|
+
data: {
|
|
103
151
|
serverName,
|
|
104
|
-
tool,
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
152
|
+
error: "tool validation failed",
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
throw new McpInitializationError(serverName, "validate_tools", {
|
|
156
|
+
cause: error,
|
|
157
|
+
});
|
|
109
158
|
}
|
|
110
159
|
|
|
111
160
|
await options.runtimeSession.append({
|
|
@@ -113,7 +162,17 @@ export async function createMcpManager(
|
|
|
113
162
|
sessionId: options.runtimeSession.sessionId,
|
|
114
163
|
data: { serverName, toolCount: seenToolNames.size },
|
|
115
164
|
});
|
|
165
|
+
servers.push(
|
|
166
|
+
Object.freeze({
|
|
167
|
+
name: serverName,
|
|
168
|
+
tools: Object.freeze([...seenToolNames].sort(compareText)),
|
|
169
|
+
}),
|
|
170
|
+
);
|
|
116
171
|
}
|
|
172
|
+
executors.sort((left, right) =>
|
|
173
|
+
compareText(left.definition.name, right.definition.name),
|
|
174
|
+
);
|
|
175
|
+
servers.sort((left, right) => compareText(left.name, right.name));
|
|
117
176
|
} catch (error) {
|
|
118
177
|
const errors = [error];
|
|
119
178
|
await closeConnections(connections, errors);
|
|
@@ -128,6 +187,7 @@ export async function createMcpManager(
|
|
|
128
187
|
let disposePromise: Promise<void> | undefined;
|
|
129
188
|
return {
|
|
130
189
|
executors,
|
|
190
|
+
inventory: Object.freeze({ servers: Object.freeze(servers) }),
|
|
131
191
|
dispose(): Promise<void> {
|
|
132
192
|
disposePromise ??= disposeConnections(connections);
|
|
133
193
|
return disposePromise;
|
|
@@ -135,6 +195,10 @@ export async function createMcpManager(
|
|
|
135
195
|
};
|
|
136
196
|
}
|
|
137
197
|
|
|
198
|
+
function compareText(left: string, right: string): number {
|
|
199
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
138
202
|
async function disposeConnections(connections: McpClientConnection[]): Promise<void> {
|
|
139
203
|
const errors: unknown[] = [];
|
|
140
204
|
await closeConnections(connections, errors);
|
|
@@ -3,6 +3,7 @@ import { cancellationError } from "../agent/turn-cancellation";
|
|
|
3
3
|
import type { ModelContextBudget } from "./model-context-profile";
|
|
4
4
|
import type {
|
|
5
5
|
ModelClient,
|
|
6
|
+
ModelMessageProtocol,
|
|
6
7
|
ModelRequestInput,
|
|
7
8
|
ModelRequestOptions,
|
|
8
9
|
ModelRequestOutput,
|
|
@@ -13,6 +14,11 @@ import { sha256, stableJsonStringify } from "./model-request-preflight";
|
|
|
13
14
|
import { estimatePromptSegments } from "./token-estimator";
|
|
14
15
|
|
|
15
16
|
export class FakeModelClient implements ModelClient {
|
|
17
|
+
readonly inputModalities = Object.freeze(["text"] as const);
|
|
18
|
+
readonly messageProtocol: ModelMessageProtocol = Object.freeze({
|
|
19
|
+
adapter: "fake",
|
|
20
|
+
serializationVersion: "fake-v1",
|
|
21
|
+
});
|
|
16
22
|
private steps = 0;
|
|
17
23
|
private readonly preparedInputs = new WeakMap<object, ModelRequestInput>();
|
|
18
24
|
|
|
@@ -34,7 +40,8 @@ export class FakeModelClient implements ModelClient {
|
|
|
34
40
|
const messageSegments = input.messages.map(toPromptSegment);
|
|
35
41
|
const requestConfigHash = sha256(
|
|
36
42
|
stableJsonStringify({
|
|
37
|
-
adapter:
|
|
43
|
+
adapter: this.messageProtocol.adapter,
|
|
44
|
+
serializationVersion: this.messageProtocol.serializationVersion,
|
|
38
45
|
mode: this.mode,
|
|
39
46
|
model: this.options.model,
|
|
40
47
|
requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
@@ -54,6 +61,7 @@ export class FakeModelClient implements ModelClient {
|
|
|
54
61
|
toolSegments.map((segment) => segment.normalizedText).join("\n"),
|
|
55
62
|
),
|
|
56
63
|
requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
|
|
64
|
+
mediaOccurrenceCount: 0,
|
|
57
65
|
assistantReplaySegments: (message: AssistantMessage) => [
|
|
58
66
|
toPromptSegment(message),
|
|
59
67
|
],
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { MaterializedModelRequest } from "./model-client";
|
|
2
|
+
|
|
3
|
+
export type InputTokenEstimate = {
|
|
4
|
+
readonly inputTokens: number;
|
|
5
|
+
readonly source: "provider_estimated";
|
|
6
|
+
readonly coverage: "messages" | "full_request";
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type InputTokenEstimatorCompatibility = {
|
|
10
|
+
readonly kind: "moonshot-estimate-token-count-v1";
|
|
11
|
+
readonly coverageVersion: "full-request-v1";
|
|
12
|
+
readonly model: string;
|
|
13
|
+
readonly endpoint: string;
|
|
14
|
+
readonly timeoutMs: number;
|
|
15
|
+
readonly maxRetries: 0;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export interface InputTokenEstimator {
|
|
19
|
+
readonly kind: string;
|
|
20
|
+
readonly compatibility: InputTokenEstimatorCompatibility;
|
|
21
|
+
estimate(
|
|
22
|
+
request: MaterializedModelRequest,
|
|
23
|
+
options: { signal: AbortSignal },
|
|
24
|
+
): Promise<InputTokenEstimate>;
|
|
25
|
+
}
|
|
@@ -1,15 +1,39 @@
|
|
|
1
1
|
import type { AgentMessage, AssistantMessage, IterationIdentity } from "../agent/types";
|
|
2
2
|
import type { RuntimeSessionContext } from "../agent/runtime-session";
|
|
3
3
|
import type { ToolDefinition } from "../tools/types";
|
|
4
|
+
import type { ImageAssetStore } from "../image/image-asset-store";
|
|
5
|
+
import type { CodePointRange, ImageAssetId, ImageMimeType } from "../image/image-types";
|
|
6
|
+
import type { InputTokenEstimator } from "./input-token-estimator";
|
|
4
7
|
|
|
5
8
|
export interface ModelClient {
|
|
9
|
+
readonly messageProtocol: ModelMessageProtocol;
|
|
10
|
+
readonly inputTokenEstimator?: InputTokenEstimator;
|
|
11
|
+
readonly inputModalities?: readonly ("text" | "image")[];
|
|
6
12
|
prepare(input: ModelRequestInput): PreparedModelRequest;
|
|
13
|
+
materialize?(
|
|
14
|
+
prepared: PreparedModelRequest,
|
|
15
|
+
options: ModelMaterializeOptions,
|
|
16
|
+
): Promise<MaterializedModelRequest>;
|
|
7
17
|
request(
|
|
8
18
|
prepared: PreparedModelRequest,
|
|
9
19
|
options: ModelRequestOptions,
|
|
10
20
|
): Promise<ModelRequestOutput>;
|
|
11
21
|
}
|
|
12
22
|
|
|
23
|
+
export class ModelRequestMediaAggregateError extends Error {
|
|
24
|
+
readonly code = "MODEL_REQUEST_MEDIA_AGGREGATE_LIMIT";
|
|
25
|
+
|
|
26
|
+
constructor(message: string) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "ModelRequestMediaAggregateError";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type ModelMessageProtocol = {
|
|
33
|
+
adapter: "openai-chat" | "fake";
|
|
34
|
+
serializationVersion: string;
|
|
35
|
+
};
|
|
36
|
+
|
|
13
37
|
export type ModelRequestOptions = {
|
|
14
38
|
signal: AbortSignal;
|
|
15
39
|
identity?: {
|
|
@@ -34,6 +58,18 @@ export type PreparedPromptSegmentKind =
|
|
|
34
58
|
export type PreparedPromptSegment = {
|
|
35
59
|
kind: PreparedPromptSegmentKind;
|
|
36
60
|
normalizedText: string;
|
|
61
|
+
media?: readonly PreparedMediaDescriptor[];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type PreparedMediaDescriptor = {
|
|
65
|
+
assetId: ImageAssetId;
|
|
66
|
+
label: string;
|
|
67
|
+
range: CodePointRange;
|
|
68
|
+
mimeType: ImageMimeType;
|
|
69
|
+
byteLength: number;
|
|
70
|
+
width: number;
|
|
71
|
+
height: number;
|
|
72
|
+
planningTokens: number;
|
|
37
73
|
};
|
|
38
74
|
|
|
39
75
|
export type PreparedModelRequest = {
|
|
@@ -44,9 +80,33 @@ export type PreparedModelRequest = {
|
|
|
44
80
|
requestConfigHash: string;
|
|
45
81
|
toolSchemaHash: string;
|
|
46
82
|
requestMaxOutputTokens: number;
|
|
83
|
+
mediaOccurrenceCount: number;
|
|
47
84
|
assistantReplaySegments(message: AssistantMessage): PreparedPromptSegment[];
|
|
48
85
|
};
|
|
49
86
|
|
|
87
|
+
export type ModelMaterializeOptions = {
|
|
88
|
+
assetStore: ImageAssetStore;
|
|
89
|
+
signal: AbortSignal;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export type MaterializedModelRequest = PreparedModelRequest & {
|
|
93
|
+
readonly bodyBytes: number;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export async function materializeModelRequest(
|
|
97
|
+
model: ModelClient,
|
|
98
|
+
prepared: PreparedModelRequest,
|
|
99
|
+
options: ModelMaterializeOptions,
|
|
100
|
+
): Promise<MaterializedModelRequest> {
|
|
101
|
+
if (model.materialize !== undefined) {
|
|
102
|
+
return model.materialize(prepared, options);
|
|
103
|
+
}
|
|
104
|
+
if (prepared.mediaOccurrenceCount !== 0) {
|
|
105
|
+
throw new Error("Model client does not implement image request materialization.");
|
|
106
|
+
}
|
|
107
|
+
return prepared as MaterializedModelRequest;
|
|
108
|
+
}
|
|
109
|
+
|
|
50
110
|
export type ModelRequestOutput = {
|
|
51
111
|
message: AssistantMessage;
|
|
52
112
|
finishReason?: string;
|