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
package/package.json CHANGED
@@ -1,45 +1,69 @@
1
1
  {
2
2
  "name": "tinker-agent",
3
- "version": "1.0.65",
3
+ "version": "1.2.0",
4
4
  "description": "A personal coding agent with an interactive TUI and one-shot CLI.",
5
- "license": "UNLICENSED",
5
+ "license": "Apache-2.0",
6
6
  "type": "module",
7
7
  "bin": {
8
- "tinker": "src/cli/index.ts",
9
- "tinker-agent": "src/cli/index.ts"
8
+ "tinker": "bin/tinker.js"
10
9
  },
11
10
  "files": [
11
+ "bin",
12
+ "LICENSE",
13
+ "NOTICE",
14
+ "CHANGELOG.md",
12
15
  "src/agent",
13
16
  "src/cli",
14
17
  "src/context",
15
18
  "src/events",
16
19
  "src/ids",
20
+ "src/image",
17
21
  "src/instructions",
18
22
  "src/mcp",
19
23
  "src/model",
20
24
  "src/observation",
21
25
  "src/session",
26
+ "src/skills",
22
27
  "src/tools",
23
28
  "src/tui",
24
- "patches",
25
- "README.md"
29
+ "patches"
26
30
  ],
27
31
  "engines": {
28
- "bun": ">=1.3.0"
32
+ "node": ">=20.0.0"
29
33
  },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "git+https://github.com/ishowshao/tinker.git"
37
+ },
38
+ "bugs": {
39
+ "url": "https://github.com/ishowshao/tinker/issues"
40
+ },
41
+ "homepage": "https://github.com/ishowshao/tinker#readme",
30
42
  "publishConfig": {
31
43
  "access": "public"
32
44
  },
45
+ "bundledDependencies": [
46
+ "markdansi"
47
+ ],
33
48
  "scripts": {
34
49
  "bench:long-session": "bun scripts/bench-long-session-memory.ts",
50
+ "bench:i3-provider-smoke": "bun scripts/bench-i3-provider-smoke.ts",
51
+ "bench:k3-image-live": "bun scripts/k3-image-live-smoke.ts",
52
+ "bench:i4-active-recall": "bun scripts/bench-i4-active-recall.ts",
53
+ "qualify:i4-active-recall": "bun scripts/qualify-i4-active-recall.ts",
54
+ "release:verify": "bun scripts/verify-release-package.ts",
35
55
  "bench:recall": "bun scripts/bench-recall.ts",
36
56
  "bench:smoke": "bun scripts/bench-smoke.ts",
57
+ "chrome:build": "bun packages/tinker-chrome/scripts/build.ts",
58
+ "chrome:diagnose": "bun packages/tinker-chrome/src/cli.ts diagnose",
59
+ "chrome:install-host": "bun packages/tinker-chrome/src/cli.ts install-host",
60
+ "chrome:mcp": "bun packages/tinker-chrome/src/cli.ts mcp",
37
61
  "tinker": "bun src/cli/index.ts",
38
62
  "check": "bun run typecheck && bun run format:check && bun run lint && bun test && bun run bench:smoke",
39
63
  "format": "biome format --write .",
40
64
  "format:check": "biome format .",
41
- "lint": "eslint \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" --max-warnings=0",
42
- "lint:fix": "eslint \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" --fix",
65
+ "lint": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --max-warnings=0",
66
+ "lint:fix": "eslint \"bin/**/*.js\" \"src/**/*.{ts,tsx}\" \"scripts/**/*.ts\" \"packages/**/*.ts\" --fix",
43
67
  "test": "bun test",
44
68
  "typecheck": "tsc --noEmit"
45
69
  },
@@ -49,20 +73,27 @@
49
73
  "@inkjs/ui": "^2.0.0",
50
74
  "@modelcontextprotocol/sdk": "^1.29.0",
51
75
  "@mozilla/readability": "^0.6.0",
76
+ "@vscode/ripgrep": "1.18.0",
77
+ "bun": "1.3.14",
78
+ "clipboardy": "^5.3.1",
52
79
  "diff": "^9.0.0",
53
80
  "glob": "^13.0.6",
54
81
  "ink": "^7.1.0",
55
82
  "linkedom": "^0.18.13",
83
+ "markdansi": "0.3.2",
56
84
  "openai": "^6.45.0",
57
85
  "react": "^19.2.7",
86
+ "sharp": "^0.35.3",
58
87
  "shiki": "^4.3.1",
59
88
  "turndown": "^7.2.4",
60
- "uuid": "^14.0.1"
89
+ "uuid": "^14.0.1",
90
+ "yaml": "^2.9.0"
61
91
  },
62
92
  "devDependencies": {
63
93
  "@biomejs/biome": "^2.5.2",
64
94
  "@eslint/js": "^10.0.1",
65
95
  "@types/bun": "^1.3.14",
96
+ "@types/chrome": "^0.2.2",
66
97
  "@types/react": "^19.2.17",
67
98
  "@types/turndown": "^5.0.6",
68
99
  "eslint": "^10.6.0",
@@ -1,5 +1,5 @@
1
1
  diff --git a/dist/render.js b/dist/render.js
2
- index 5de3a5d5c1e0c8bfe8dec992ca3d57987f2ba6ee..53237b86315dc56fdb2e6295aea698ee033350ce 100644
2
+ index 5de3a5d5c1e0c8bfe8dec992ca3d57987f2ba6ee..9aff5e7c5553e53c92d3f1b8295e3a5995650767 100644
3
3
  --- a/dist/render.js
4
4
  +++ b/dist/render.js
5
5
  @@ -572,7 +572,7 @@ function renderTable(node, ctx) {
@@ -11,7 +11,27 @@ index 5de3a5d5c1e0c8bfe8dec992ca3d57987f2ba6ee..53237b86315dc56fdb2e6295aea698ee
11
11
  return wrapped.map((l) => {
12
12
  const aligned = padCell(l, target, aligns[idx] ?? "left");
13
13
  const padded = `${padStr}${aligned}${padStr}`;
14
- @@ -627,6 +627,23 @@ function sliceCellContent(text, width) {
14
+ @@ -595,8 +595,8 @@ function renderTable(node, ctx) {
15
+ };
16
+ const headerRows = renderRow(header.children.map((c) => renderInline(c.children, ctx)), true);
17
+ - const bodyRows = rows.flatMap((r) => renderRow(r.children.map((c) => renderInline(c.children, ctx))));
18
+ + const bodyRows = rows.map((r) => renderRow(r.children.map((c) => renderInline(c.children, ctx))));
19
+ if (ctx.options.tableBorder === "none") {
20
+ - const lines = [...headerRows, ...bodyRows].map((row) => row.join(" | ")).join("\n");
21
+ + const lines = [...headerRows, ...bodyRows.flat()].map((row) => row.join(" | ")).join("\n");
22
+ return [`${lines}\n\n`];
23
+ }
24
+ const box = TABLE_BOX[ctx.options.tableBorder] || TABLE_BOX.unicode;
25
+ @@ -607,6 +607,7 @@ function renderTable(node, ctx) {
26
+ const renderFlat = (rowsArr) => rowsArr.map((r) => `${box.vSep}${r.map((c) => c).join(box.vSep)}${box.vSep}\n`).join("");
27
+ const dense = ctx.options.tableDense;
28
+ - const out = [top, renderFlat(headerRows), dense ? "" : mid, renderFlat(bodyRows), bottom, "\n"];
29
+ + const renderedBody = bodyRows.map((row) => renderFlat(row)).join(dense ? "" : mid);
30
+ + const out = [top, renderFlat(headerRows), dense ? "" : mid, renderedBody, bottom, "\n"];
31
+ return out;
32
+ }
33
+ function truncateCell(text, width, ellipsis) {
34
+ @@ -627,6 +628,23 @@ function sliceCellContent(text, width) {
15
35
  }
16
36
  return sliced;
17
37
  }
@@ -2,42 +2,56 @@ import type { ToolDefinition } from "../tools/types";
2
2
  import type {
3
3
  BuiltContextRequest,
4
4
  CompiledRevisionContext,
5
- StoredContextRevisionV5,
5
+ StoredContextRevisionV8,
6
6
  SwapOverride,
7
7
  } from "../context/context-revision";
8
8
  import type { ProtocolContextView } from "../context/protocol-frame";
9
+ import type { StoredContextSurfaceV8 } from "../context/context-surface";
10
+ import { stableJsonStringify } from "../model/model-request-preflight";
11
+ import { validateUserMessage, type UserMessage } from "../image/image-types";
9
12
 
10
13
  export class ContextBuilder {
11
14
  build(input: {
12
15
  canonical: ProtocolContextView;
13
- revision: StoredContextRevisionV5;
16
+ revision: StoredContextRevisionV8;
14
17
  activeOverrides: readonly SwapOverride[];
15
18
  compiled: CompiledRevisionContext;
19
+ surface: StoredContextSurfaceV8;
16
20
  tools: readonly ToolDefinition[];
17
- candidateUserPrompt?: string;
21
+ candidateUserMessage?: UserMessage;
18
22
  }): BuiltContextRequest {
19
23
  if (input.canonical.sessionId !== input.compiled.sessionId) {
20
24
  throw new Error(
21
25
  "Compiled context and canonical history belong to different sessions.",
22
26
  );
23
27
  }
28
+ if (
29
+ input.surface.sessionId !== input.canonical.sessionId ||
30
+ input.surface.surfaceId !== input.revision.surfaceId ||
31
+ input.surface.surfaceSha256 !== input.revision.surfaceSha256 ||
32
+ stableJsonStringify(input.tools) !==
33
+ stableJsonStringify(input.surface.toolDefinitions)
34
+ ) {
35
+ throw new Error(
36
+ "Executable tool definitions do not match the active context surface.",
37
+ );
38
+ }
24
39
  const messages = input.compiled.entries.map((entry) => entry.message);
25
- if (input.candidateUserPrompt !== undefined) {
26
- if (input.candidateUserPrompt.trim() === "") {
27
- throw new Error("Cannot build a candidate context with an empty prompt.");
28
- }
29
- messages.push({ role: "user", content: input.candidateUserPrompt });
40
+ if (input.candidateUserMessage !== undefined) {
41
+ validateUserMessage(input.candidateUserMessage);
42
+ messages.push(input.candidateUserMessage);
30
43
  }
31
44
  return Object.freeze({
32
45
  canonical: input.canonical,
33
46
  revision: input.revision,
47
+ surface: input.surface,
34
48
  activeOverrides: input.activeOverrides,
35
49
  compiled: input.compiled,
36
50
  request: {
37
51
  messages,
38
- tools: [...input.tools],
52
+ tools: [...input.surface.toolDefinitions],
39
53
  },
40
- candidateUserPromptIncluded: input.candidateUserPrompt !== undefined,
54
+ candidateUserPromptIncluded: input.candidateUserMessage !== undefined,
41
55
  });
42
56
  }
43
57
  }
@@ -2,8 +2,13 @@ import type { ModelContextBudget } from "../model/model-context-profile";
2
2
  import type {
3
3
  ModelRequestOutput,
4
4
  ModelUsage,
5
+ MaterializedModelRequest,
5
6
  PreparedModelRequest,
6
7
  } from "../model/model-client";
8
+ import type {
9
+ InputTokenEstimate,
10
+ InputTokenEstimator,
11
+ } from "../model/input-token-estimator";
7
12
  import {
8
13
  assertContextBudget,
9
14
  contextPressure,
@@ -63,6 +68,7 @@ export class ContextMeter {
63
68
  private anchor?: MeasuredContextAnchor;
64
69
  private lastProviderUsage?: ModelUsage;
65
70
  private calibrationIdentity?: string;
71
+ private readonly providerEstimateCache = new Map<string, InputTokenEstimate>();
66
72
 
67
73
  constructor(
68
74
  private readonly budget: ModelContextBudget,
@@ -166,10 +172,12 @@ export class ContextMeter {
166
172
  throw new Error("Cannot record provider usage before measuring the request.");
167
173
  }
168
174
 
169
- this.calibration.record(
170
- output.usage.promptTokens,
171
- measurement.rawFullEstimate.totalTokens,
172
- );
175
+ if (prepared.mediaOccurrenceCount === 0) {
176
+ this.calibration.record(
177
+ output.usage.promptTokens,
178
+ measurement.rawFullEstimate.totalTokens,
179
+ );
180
+ }
173
181
  this.lastProviderUsage = { ...output.usage };
174
182
  const replaySegments = prepared.assistantReplaySegments(output.message);
175
183
  const anchoredSegments = [...prepared.promptSegments, ...replaySegments];
@@ -209,6 +217,62 @@ export class ContextMeter {
209
217
  };
210
218
  }
211
219
 
220
+ applyProviderEstimate(
221
+ prepared: PreparedModelRequest,
222
+ estimate: {
223
+ inputTokens: number;
224
+ coverage: "messages" | "full_request";
225
+ },
226
+ ): ContextUsageSnapshot {
227
+ if (!Number.isSafeInteger(estimate.inputTokens) || estimate.inputTokens < 0) {
228
+ throw new Error("Provider input estimate must be a non-negative safe integer.");
229
+ }
230
+ const local = this.measure(prepared);
231
+ const measurement = this.measurements.get(prepared)!;
232
+ const guardedTools =
233
+ estimate.coverage === "messages"
234
+ ? Math.ceil(
235
+ measurement.rawFullEstimate.toolSchemaTokens * local.correctionFactor,
236
+ )
237
+ : 0;
238
+ const providerGuarded = estimate.inputTokens + guardedTools;
239
+ if (providerGuarded <= local.usedInputTokens) {
240
+ return local;
241
+ }
242
+ const snapshot: ContextUsageSnapshot = {
243
+ ...local,
244
+ usedInputTokens: providerGuarded,
245
+ source: "provider_estimated",
246
+ pressure: contextPressure(providerGuarded, this.budget),
247
+ };
248
+ this.measurements.set(prepared, {
249
+ rawFullEstimate: measurement.rawFullEstimate,
250
+ snapshot,
251
+ });
252
+ return snapshot;
253
+ }
254
+
255
+ async estimateProviderInput(
256
+ prepared: MaterializedModelRequest,
257
+ estimator: InputTokenEstimator,
258
+ options: { signal: AbortSignal },
259
+ ): Promise<InputTokenEstimate> {
260
+ options.signal.throwIfAborted();
261
+ const key = providerEstimateCacheKey(
262
+ prepared,
263
+ estimator.compatibility.coverageVersion,
264
+ );
265
+ const cached = this.providerEstimateCache.get(key);
266
+ if (cached !== undefined) {
267
+ return cached;
268
+ }
269
+ const estimate = await estimator.estimate(prepared, options);
270
+ options.signal.throwIfAborted();
271
+ const frozen = Object.freeze({ ...estimate });
272
+ this.providerEstimateCache.set(key, frozen);
273
+ return frozen;
274
+ }
275
+
212
276
  assertWithinBudget(snapshot: ContextUsageSnapshot): void {
213
277
  assertContextBudget({
214
278
  usedInputTokens: snapshot.usedInputTokens,
@@ -231,6 +295,7 @@ export class ContextMeter {
231
295
  this.lastProviderUsage = undefined;
232
296
  this.measurements = new WeakMap();
233
297
  this.calibration.clear();
298
+ this.providerEstimateCache.clear();
234
299
  this.calibrationIdentity = nextIdentity;
235
300
  throw new Error(
236
301
  "Context revision changed the request configuration or tool schema.",
@@ -239,6 +304,7 @@ export class ContextMeter {
239
304
  this.anchor = undefined;
240
305
  this.lastProviderUsage = undefined;
241
306
  this.measurements = new WeakMap();
307
+ this.providerEstimateCache.clear();
242
308
  this.calibrationIdentity = nextIdentity;
243
309
  }
244
310
 
@@ -249,6 +315,7 @@ export class ContextMeter {
249
315
  this.measurements = new WeakMap();
250
316
  this.calibration.clear();
251
317
  this.calibrationIdentity = undefined;
318
+ this.providerEstimateCache.clear();
252
319
  }
253
320
 
254
321
  private usableAnchor(
@@ -263,7 +330,10 @@ export class ContextMeter {
263
330
  anchor.requestConfigHash !== prepared.requestConfigHash ||
264
331
  anchor.toolSchemaHash !== prepared.toolSchemaHash ||
265
332
  anchor.segmentCount > prepared.promptSegments.length ||
266
- prefixHashes[anchor.segmentCount] !== anchor.prefixHash
333
+ prefixHashes[anchor.segmentCount] !== anchor.prefixHash ||
334
+ prepared.promptSegments
335
+ .slice(anchor.segmentCount)
336
+ .some((segment) => (segment.media?.length ?? 0) > 0)
267
337
  ) {
268
338
  this.anchor = undefined;
269
339
  return undefined;
@@ -289,6 +359,20 @@ export class ContextMeter {
289
359
  }
290
360
  }
291
361
 
362
+ function providerEstimateCacheKey(
363
+ prepared: PreparedModelRequest,
364
+ coverageVersion: string,
365
+ ): string {
366
+ return [
367
+ prepared.requestConfigHash,
368
+ prepared.toolSchemaHash,
369
+ lastPromptPrefixHash(
370
+ promptPrefixHashes(prepared.requestConfigHash, prepared.promptSegments),
371
+ ),
372
+ coverageVersion,
373
+ ].join(":");
374
+ }
375
+
292
376
  function assertMeasuredContextAnchor(anchor: MeasuredContextAnchor): void {
293
377
  for (const [name, value] of [
294
378
  ["promptTokens", anchor.promptTokens],
package/src/agent/loop.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { ModelClient, PreparedModelRequest } from "../model/model-client";
1
+ import {
2
+ materializeModelRequest,
3
+ type MaterializedModelRequest,
4
+ type ModelClient,
5
+ type PreparedModelRequest,
6
+ } from "../model/model-client";
7
+ import type { ImageAssetStore } from "../image/image-asset-store";
2
8
  import {
3
9
  CommittedPrefixAuditError,
4
10
  CommittedPrefixAuditor,
@@ -6,7 +12,7 @@ import {
6
12
  import type { ObservationBuilder } from "../observation/observation-builder";
7
13
  import type { ToolRegistry, ToolRuntime } from "../tools/registry";
8
14
  import { ToolExecutionFatalError } from "../tools/types";
9
- import type { ContextMeter } from "./context-meter";
15
+ import type { ContextMeter, ContextUsageSnapshot } from "./context-meter";
10
16
  import type { RuntimeSessionContext } from "./runtime-session";
11
17
  import type { AgentTurnLedger } from "./session-ledger";
12
18
  import { ContextProtocolError } from "../context/context-protocol-validator";
@@ -21,6 +27,7 @@ import {
21
27
  type SwapPlanner,
22
28
  } from "../context/swap-planner";
23
29
  import { SessionError } from "../session/session-errors";
30
+ import { stableJsonStringify } from "../model/model-request-preflight";
24
31
  import {
25
32
  normalizeSyntheticDetail,
26
33
  type SyntheticToolCompletionInput,
@@ -59,6 +66,11 @@ export type RunAgentInput = {
59
66
  runtimeSession: RuntimeSessionContext;
60
67
  turn: TurnIdentity;
61
68
  signal: AbortSignal;
69
+ assetStore?: ImageAssetStore;
70
+ initialRequest?: {
71
+ prepared: MaterializedModelRequest;
72
+ usage: ContextUsageSnapshot;
73
+ };
62
74
  };
63
75
 
64
76
  export class FatalAgentTurnError extends Error {
@@ -97,14 +109,20 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
97
109
  }
98
110
 
99
111
  let prepared: PreparedModelRequest;
112
+ let request: PreparedModelRequest;
100
113
  let built: BuiltContextRequest;
101
114
  let preflight;
102
115
  try {
103
116
  throwIfTurnCancelled(input.signal);
104
117
  built = input.ledger.buildModelRequest(input.tools.definitions());
105
- prepared = input.model.prepare(built.request);
118
+ const admission = iterationNumber === 1 ? input.initialRequest : undefined;
119
+ const candidate = input.model.prepare(built.request);
120
+ if (admission !== undefined && !samePreparedPlan(candidate, admission.prepared)) {
121
+ throw new Error("Accepted request no longer matches the first dispatch plan.");
122
+ }
123
+ prepared = admission?.prepared ?? candidate;
106
124
  committedPrefixAuditor.audit(built.compiled.revisionId, prepared);
107
- preflight = input.contextMeter.measure(prepared);
125
+ preflight = admission?.usage ?? input.contextMeter.measure(prepared);
108
126
  } catch (error) {
109
127
  if (input.signal.aborted) {
110
128
  return cancelledResult(
@@ -135,6 +153,52 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
135
153
  return failedResult(error, iteration);
136
154
  }
137
155
 
156
+ try {
157
+ if (prepared.mediaOccurrenceCount > 0) {
158
+ if (input.initialRequest?.prepared === prepared) {
159
+ request = prepared;
160
+ } else {
161
+ if (input.assetStore === undefined) {
162
+ throw new Error("Image model request has no asset store.");
163
+ }
164
+ request = await materializeModelRequest(input.model, prepared, {
165
+ assetStore: input.assetStore,
166
+ signal: input.signal,
167
+ });
168
+ if (preflight.source === "measured_plus_estimated_delta") {
169
+ preflight = input.contextMeter.measure(request);
170
+ } else {
171
+ const estimator = input.model.inputTokenEstimator;
172
+ if (estimator === undefined) {
173
+ throw new Error("Image model request has no input token estimator.");
174
+ }
175
+ const estimate = await input.contextMeter.estimateProviderInput(
176
+ request as MaterializedModelRequest,
177
+ estimator,
178
+ { signal: input.signal },
179
+ );
180
+ preflight = input.contextMeter.applyProviderEstimate(request, estimate);
181
+ }
182
+ input.contextMeter.assertWithinBudget(preflight);
183
+ await input.runtimeSession.append({
184
+ type: "context.usage.updated",
185
+ ...iteration,
186
+ data: { phase: "preflight", snapshot: preflight },
187
+ });
188
+ }
189
+ } else {
190
+ request = prepared;
191
+ }
192
+ } catch (error) {
193
+ if (input.signal.aborted) {
194
+ return cancelledResult(
195
+ cancellation(input.signal, iteration, "model_request"),
196
+ iteration,
197
+ );
198
+ }
199
+ return failedResult(error, iteration);
200
+ }
201
+
138
202
  await input.runtimeSession.append({
139
203
  type: "model.request.started",
140
204
  ...iteration,
@@ -144,7 +208,8 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
144
208
  let modelOutput;
145
209
  try {
146
210
  throwIfTurnCancelled(input.signal);
147
- modelOutput = await input.model.request(prepared, {
211
+ input.runtimeSession.prepareModelDispatch?.({ iteration, built });
212
+ modelOutput = await input.model.request(request, {
148
213
  signal: input.signal,
149
214
  identity: {
150
215
  iteration,
@@ -167,8 +232,8 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
167
232
  input.ledger.appendAssistant({
168
233
  iteration,
169
234
  message: modelOutput.message,
170
- provider: prepared.provider,
171
- model: prepared.model,
235
+ provider: request.provider,
236
+ model: request.model,
172
237
  });
173
238
  } catch (error) {
174
239
  if (error instanceof ContextProtocolError) {
@@ -182,7 +247,7 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
182
247
  ...iteration,
183
248
  data: { output: modelOutput },
184
249
  });
185
- const measured = input.contextMeter.recordProviderUsage(prepared, modelOutput);
250
+ const measured = input.contextMeter.recordProviderUsage(request, modelOutput);
186
251
  await input.runtimeSession.append({
187
252
  type: "context.usage.updated",
188
253
  ...iteration,
@@ -259,14 +324,29 @@ export async function runAgent(input: RunAgentInput): Promise<RunAgentResult> {
259
324
  }
260
325
 
261
326
  const observation = input.observationBuilder.build({ call, raw });
262
- input.ledger.commitToolCompletions([
327
+ const completions = [
263
328
  {
264
329
  call,
265
330
  kind: "returned",
266
331
  raw,
267
332
  observation: observation.content,
268
333
  },
269
- ]);
334
+ ] as const;
335
+ const committed = input.ledger.commitToolCompletions(completions);
336
+ if (
337
+ raw.kind === "skill" &&
338
+ raw.ok &&
339
+ raw.status === "loaded" &&
340
+ input.runtimeSession.onToolCompletionsCommitted === undefined
341
+ ) {
342
+ throw new Error(
343
+ "Loaded Agent Skill completion has no runtime activation coordinator.",
344
+ );
345
+ }
346
+ input.runtimeSession.onToolCompletionsCommitted?.({
347
+ completions,
348
+ committed,
349
+ });
270
350
 
271
351
  await input.runtimeSession.append({
272
352
  type: "tool.raw_result",
@@ -342,6 +422,7 @@ async function runShadowPlanning(input: {
342
422
  result = shadowPlanning.planner.plan({
343
423
  active: input.built.compiled,
344
424
  revision: input.built.revision,
425
+ surface: input.built.surface,
345
426
  activeOverrides: input.built.activeOverrides,
346
427
  canonical: input.built.canonical,
347
428
  activePrepared: input.prepared,
@@ -418,6 +499,22 @@ function isCanonicalContextFailure(error: unknown): boolean {
418
499
  );
419
500
  }
420
501
 
502
+ function samePreparedPlan(
503
+ candidate: PreparedModelRequest,
504
+ accepted: PreparedModelRequest,
505
+ ): boolean {
506
+ return (
507
+ candidate.provider === accepted.provider &&
508
+ candidate.model === accepted.model &&
509
+ candidate.requestConfigHash === accepted.requestConfigHash &&
510
+ candidate.toolSchemaHash === accepted.toolSchemaHash &&
511
+ candidate.requestMaxOutputTokens === accepted.requestMaxOutputTokens &&
512
+ candidate.mediaOccurrenceCount === accepted.mediaOccurrenceCount &&
513
+ stableJsonStringify(candidate.promptSegments) ===
514
+ stableJsonStringify(accepted.promptSegments)
515
+ );
516
+ }
517
+
421
518
  function cancelledToolCompletions(
422
519
  toolCalls: readonly ToolCall[],
423
520
  startIndex: number,