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.
Files changed (208) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +6 -0
  4. package/README.md +90 -9
  5. package/bin/tinker.js +39 -0
  6. package/node_modules/ansi-regex/index.d.ts +33 -0
  7. package/node_modules/ansi-regex/index.js +14 -0
  8. package/node_modules/ansi-regex/license +9 -0
  9. package/node_modules/ansi-regex/package.json +61 -0
  10. package/node_modules/ansi-regex/readme.md +66 -0
  11. package/node_modules/ansi-styles/index.d.ts +236 -0
  12. package/node_modules/ansi-styles/index.js +223 -0
  13. package/node_modules/ansi-styles/license +9 -0
  14. package/node_modules/ansi-styles/package.json +54 -0
  15. package/node_modules/ansi-styles/readme.md +173 -0
  16. package/node_modules/chalk/license +9 -0
  17. package/node_modules/chalk/package.json +83 -0
  18. package/node_modules/chalk/readme.md +297 -0
  19. package/node_modules/chalk/source/index.d.ts +325 -0
  20. package/node_modules/chalk/source/index.js +225 -0
  21. package/node_modules/chalk/source/utilities.js +33 -0
  22. package/node_modules/chalk/source/vendor/ansi-styles/index.d.ts +236 -0
  23. package/node_modules/chalk/source/vendor/ansi-styles/index.js +223 -0
  24. package/node_modules/chalk/source/vendor/supports-color/browser.d.ts +1 -0
  25. package/node_modules/chalk/source/vendor/supports-color/browser.js +34 -0
  26. package/node_modules/chalk/source/vendor/supports-color/index.d.ts +55 -0
  27. package/node_modules/chalk/source/vendor/supports-color/index.js +190 -0
  28. package/node_modules/character-entities/index.d.ts +6 -0
  29. package/node_modules/character-entities/index.js +2132 -0
  30. package/node_modules/character-entities/license +22 -0
  31. package/node_modules/character-entities/package.json +78 -0
  32. package/node_modules/character-entities/readme.md +152 -0
  33. package/node_modules/decode-named-character-reference/index.d.ts +13 -0
  34. package/node_modules/decode-named-character-reference/index.d.ts.map +1 -0
  35. package/node_modules/decode-named-character-reference/index.dom.d.ts +6 -0
  36. package/node_modules/decode-named-character-reference/index.dom.d.ts.map +1 -0
  37. package/node_modules/decode-named-character-reference/index.dom.js +32 -0
  38. package/node_modules/decode-named-character-reference/index.js +19 -0
  39. package/node_modules/decode-named-character-reference/license +22 -0
  40. package/node_modules/decode-named-character-reference/package.json +90 -0
  41. package/node_modules/decode-named-character-reference/readme.md +136 -0
  42. package/node_modules/get-east-asian-width/index.d.ts +60 -0
  43. package/node_modules/get-east-asian-width/index.js +30 -0
  44. package/node_modules/get-east-asian-width/license +9 -0
  45. package/node_modules/get-east-asian-width/lookup-data.js +21 -0
  46. package/node_modules/get-east-asian-width/lookup.js +138 -0
  47. package/node_modules/get-east-asian-width/package.json +71 -0
  48. package/node_modules/get-east-asian-width/readme.md +65 -0
  49. package/node_modules/get-east-asian-width/utilities.js +24 -0
  50. package/node_modules/has-flag/index.d.ts +39 -0
  51. package/node_modules/has-flag/index.js +8 -0
  52. package/node_modules/has-flag/license +9 -0
  53. package/node_modules/has-flag/package.json +49 -0
  54. package/node_modules/has-flag/readme.md +74 -0
  55. package/node_modules/is-fullwidth-code-point/index.d.ts +17 -0
  56. package/node_modules/is-fullwidth-code-point/index.js +12 -0
  57. package/node_modules/is-fullwidth-code-point/license +9 -0
  58. package/node_modules/is-fullwidth-code-point/package.json +53 -0
  59. package/node_modules/is-fullwidth-code-point/readme.md +31 -0
  60. package/node_modules/markdansi/LICENSE +21 -0
  61. package/node_modules/markdansi/README.md +167 -0
  62. package/node_modules/markdansi/dist/ast.d.ts +99 -0
  63. package/node_modules/markdansi/dist/ast.js +1 -0
  64. package/node_modules/markdansi/dist/cli.d.ts +17 -0
  65. package/node_modules/markdansi/dist/cli.js +208 -0
  66. package/node_modules/markdansi/dist/hyperlink.d.ts +9 -0
  67. package/node_modules/markdansi/dist/hyperlink.js +26 -0
  68. package/node_modules/markdansi/dist/index.d.ts +10 -0
  69. package/node_modules/markdansi/dist/index.js +16 -0
  70. package/node_modules/markdansi/dist/parser.d.ts +2 -0
  71. package/node_modules/markdansi/dist/parser.js +205 -0
  72. package/node_modules/markdansi/dist/render.d.ts +9 -0
  73. package/node_modules/markdansi/dist/render.js +680 -0
  74. package/node_modules/markdansi/dist/stream.d.ts +39 -0
  75. package/node_modules/markdansi/dist/stream.js +192 -0
  76. package/node_modules/markdansi/dist/theme.d.ts +18 -0
  77. package/node_modules/markdansi/dist/theme.js +105 -0
  78. package/node_modules/markdansi/dist/types.d.ts +58 -0
  79. package/node_modules/markdansi/dist/types.js +1 -0
  80. package/node_modules/markdansi/dist/wrap.d.ts +10 -0
  81. package/node_modules/markdansi/dist/wrap.js +73 -0
  82. package/node_modules/markdansi/docs/spec.md +110 -0
  83. package/node_modules/markdansi/package.json +78 -0
  84. package/node_modules/markdansi/tsconfig.json +18 -0
  85. package/node_modules/marked/LICENSE +44 -0
  86. package/node_modules/marked/README.md +115 -0
  87. package/node_modules/marked/bin/main.js +284 -0
  88. package/node_modules/marked/bin/marked.js +16 -0
  89. package/node_modules/marked/lib/marked.d.ts +760 -0
  90. package/node_modules/marked/lib/marked.esm.js +77 -0
  91. package/node_modules/marked/lib/marked.esm.js.map +7 -0
  92. package/node_modules/marked/lib/marked.umd.js +79 -0
  93. package/node_modules/marked/lib/marked.umd.js.map +7 -0
  94. package/node_modules/marked/man/marked.1 +113 -0
  95. package/node_modules/marked/man/marked.1.md +93 -0
  96. package/node_modules/marked/package.json +103 -0
  97. package/node_modules/slice-ansi/index.d.ts +19 -0
  98. package/node_modules/slice-ansi/index.js +317 -0
  99. package/node_modules/slice-ansi/license +10 -0
  100. package/node_modules/slice-ansi/package.json +59 -0
  101. package/node_modules/slice-ansi/readme.md +55 -0
  102. package/node_modules/slice-ansi/tokenize-ansi.js +752 -0
  103. package/node_modules/string-width/index.d.ts +39 -0
  104. package/node_modules/string-width/index.js +203 -0
  105. package/node_modules/string-width/license +9 -0
  106. package/node_modules/string-width/package.json +65 -0
  107. package/node_modules/string-width/readme.md +66 -0
  108. package/node_modules/strip-ansi/index.d.ts +15 -0
  109. package/node_modules/strip-ansi/index.js +19 -0
  110. package/node_modules/strip-ansi/license +9 -0
  111. package/node_modules/strip-ansi/package.json +59 -0
  112. package/node_modules/strip-ansi/readme.md +37 -0
  113. package/node_modules/supports-color/browser.d.ts +1 -0
  114. package/node_modules/supports-color/browser.js +35 -0
  115. package/node_modules/supports-color/index.d.ts +55 -0
  116. package/node_modules/supports-color/index.js +202 -0
  117. package/node_modules/supports-color/license +9 -0
  118. package/node_modules/supports-color/package.json +64 -0
  119. package/node_modules/supports-color/readme.md +75 -0
  120. package/node_modules/supports-hyperlinks/browser.js +10 -0
  121. package/node_modules/supports-hyperlinks/index.d.ts +25 -0
  122. package/node_modules/supports-hyperlinks/index.js +163 -0
  123. package/node_modules/supports-hyperlinks/license +10 -0
  124. package/node_modules/supports-hyperlinks/package.json +47 -0
  125. package/node_modules/supports-hyperlinks/readme.md +45 -0
  126. package/package.json +41 -10
  127. package/patches/markdansi@0.3.2.patch +22 -2
  128. package/src/agent/context-builder.ts +24 -10
  129. package/src/agent/context-meter.ts +89 -5
  130. package/src/agent/loop.ts +107 -10
  131. package/src/agent/runtime-session.ts +1437 -82
  132. package/src/agent/session-ledger.ts +169 -29
  133. package/src/agent/types.ts +12 -1
  134. package/src/agent/user-prompt-projection.ts +62 -0
  135. package/src/cli/config.ts +57 -14
  136. package/src/cli/model-profiles.ts +124 -0
  137. package/src/cli/run-runner.ts +4 -1
  138. package/src/cli/tui-runner.tsx +68 -30
  139. package/src/context/compiled-context-hash.ts +1 -1
  140. package/src/context/compiled-context-validator.ts +92 -15
  141. package/src/context/context-automation-policy.ts +121 -0
  142. package/src/context/context-manager.ts +390 -11
  143. package/src/context/context-policy.ts +8 -0
  144. package/src/context/context-protocol-validator.ts +11 -1
  145. package/src/context/context-revision-compiler.ts +190 -27
  146. package/src/context/context-revision.ts +105 -19
  147. package/src/context/context-surface.ts +329 -0
  148. package/src/context/prefix-retirement-planner.ts +570 -0
  149. package/src/context/protocol-frame.ts +30 -3
  150. package/src/context/recall-retirement-contract.ts +27 -0
  151. package/src/context/swap-planner.ts +34 -10
  152. package/src/events/observation-text-log.ts +3 -3
  153. package/src/events/stdout-event-printer.ts +39 -3
  154. package/src/events/types.ts +172 -35
  155. package/src/ids/runtime-id.ts +6 -0
  156. package/src/ids/uuid-v7.ts +7 -0
  157. package/src/image/image-asset-store.ts +319 -0
  158. package/src/image/image-input-policy.ts +14 -0
  159. package/src/image/image-probe.ts +233 -0
  160. package/src/image/image-types.ts +262 -0
  161. package/src/instructions/project-instructions.ts +4 -1
  162. package/src/mcp/mcp-manager.ts +83 -19
  163. package/src/model/fake-model-client.ts +9 -1
  164. package/src/model/input-token-estimator.ts +25 -0
  165. package/src/model/model-client.ts +60 -0
  166. package/src/model/model-request-preflight.ts +1 -0
  167. package/src/model/moonshot-input-token-estimator.ts +107 -0
  168. package/src/model/openai-chat-mapping.ts +77 -1
  169. package/src/model/openai-chat-model-client.ts +389 -59
  170. package/src/model/openai-chat-stream.ts +272 -0
  171. package/src/model/prompt-prefix-hash.ts +2 -4
  172. package/src/model/token-estimator.ts +4 -0
  173. package/src/observation/observation-builder.ts +29 -0
  174. package/src/session/resume-projection.ts +94 -34
  175. package/src/session/session-catalog.ts +4 -4
  176. package/src/session/session-errors.ts +1 -1
  177. package/src/session/session-history-reader.ts +151 -4
  178. package/src/session/session-last-response-reader.ts +189 -0
  179. package/src/session/session-schema.ts +349 -35
  180. package/src/session/session-store.ts +3266 -255
  181. package/src/session/sqlite-session-ledger.ts +9 -4
  182. package/src/skills/skill-catalog.ts +182 -0
  183. package/src/skills/skill-context.ts +364 -0
  184. package/src/skills/skill-loader.ts +779 -0
  185. package/src/skills/skill-tool.ts +297 -0
  186. package/src/tools/edit.ts +51 -64
  187. package/src/tools/ensure-parent-directory.ts +6 -0
  188. package/src/tools/read.ts +2 -2
  189. package/src/tools/recall.ts +43 -40
  190. package/src/tools/registry.ts +24 -3
  191. package/src/tools/ripgrep.ts +3 -2
  192. package/src/tools/types.ts +43 -4
  193. package/src/tools/write.ts +20 -31
  194. package/src/tui/app.tsx +333 -96
  195. package/src/tui/clipboard.ts +5 -0
  196. package/src/tui/components/mcp-panel.tsx +28 -0
  197. package/src/tui/components/prompt-input.tsx +790 -257
  198. package/src/tui/components/skills-panel.tsx +31 -0
  199. package/src/tui/components/timeline.tsx +30 -1
  200. package/src/tui/event-store.ts +85 -2
  201. package/src/tui/file-mention.ts +234 -0
  202. package/src/tui/line-editor.ts +1 -0
  203. package/src/tui/project-slash-commands.ts +229 -0
  204. package/src/tui/prompt-draft.ts +510 -0
  205. package/src/tui/prompt-history.ts +294 -22
  206. package/src/tui/slash-commands.ts +46 -2
  207. package/src/tui/tui-session-controller.ts +110 -48
  208. 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 buildSystemPrompt(input: {
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 {
@@ -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: error instanceof Error ? error.message : String(error),
83
+ error: "connection failed",
62
84
  },
63
85
  });
64
- continue;
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
- throw new AggregateError(
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: error instanceof Error ? error.message : String(error),
109
+ error: "tool discovery failed",
85
110
  },
86
111
  });
87
- continue;
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
- for (const tool of tools) {
95
- if (seenToolNames.has(tool.name)) {
96
- continue;
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
- seenToolNames.add(tool.name);
100
- executors.push(
101
- createMcpToolExecutor({
102
- client: connection.client,
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
- timeoutMs,
106
- maxObservationChars,
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: "fake-v1",
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;
@@ -3,6 +3,7 @@ import type { ModelContextBudget } from "./model-context-profile";
3
3
 
4
4
  export type ContextUsageSource =
5
5
  | "estimated_full"
6
+ | "provider_estimated"
6
7
  | "provider_measured"
7
8
  | "measured_plus_estimated_delta";
8
9