tinker-agent 1.0.65

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 (110) hide show
  1. package/README.md +173 -0
  2. package/package.json +78 -0
  3. package/patches/markdansi@0.3.2.patch +37 -0
  4. package/src/agent/context-builder.ts +43 -0
  5. package/src/agent/context-meter.ts +310 -0
  6. package/src/agent/loop.ts +525 -0
  7. package/src/agent/runtime-session.ts +1212 -0
  8. package/src/agent/session-ledger.ts +828 -0
  9. package/src/agent/turn-cancellation.ts +44 -0
  10. package/src/agent/types.ts +77 -0
  11. package/src/cli/config.ts +283 -0
  12. package/src/cli/index.ts +29 -0
  13. package/src/cli/model-profiles.ts +289 -0
  14. package/src/cli/run-runner.ts +107 -0
  15. package/src/cli/tui-runner.tsx +290 -0
  16. package/src/context/compiled-context-hash.ts +138 -0
  17. package/src/context/compiled-context-validator.ts +209 -0
  18. package/src/context/context-manager.ts +362 -0
  19. package/src/context/context-policy.ts +8 -0
  20. package/src/context/context-protocol-validator.ts +463 -0
  21. package/src/context/context-revision-compiler.ts +281 -0
  22. package/src/context/context-revision.ts +111 -0
  23. package/src/context/context-source.ts +30 -0
  24. package/src/context/context-swap-renderer.ts +272 -0
  25. package/src/context/protocol-frame.ts +240 -0
  26. package/src/context/swap-planner.ts +725 -0
  27. package/src/events/append-private-file.ts +16 -0
  28. package/src/events/bash-result-detail.ts +70 -0
  29. package/src/events/composite-event-sink.ts +82 -0
  30. package/src/events/event-sink.ts +16 -0
  31. package/src/events/jsonl-event-log.ts +13 -0
  32. package/src/events/observation-text-log.ts +195 -0
  33. package/src/events/stdout-event-printer.ts +396 -0
  34. package/src/events/types.ts +263 -0
  35. package/src/ids/runtime-id.ts +68 -0
  36. package/src/ids/uuid-v7.ts +5 -0
  37. package/src/instructions/project-instructions.ts +242 -0
  38. package/src/mcp/mcp-config.ts +144 -0
  39. package/src/mcp/mcp-manager.ts +216 -0
  40. package/src/mcp/mcp-tool-executor.ts +178 -0
  41. package/src/model/committed-prefix-auditor.ts +68 -0
  42. package/src/model/fake-model-client.ts +280 -0
  43. package/src/model/model-client.ts +64 -0
  44. package/src/model/model-context-profile.ts +134 -0
  45. package/src/model/model-request-preflight.ts +120 -0
  46. package/src/model/openai-chat-mapping.ts +444 -0
  47. package/src/model/openai-chat-model-client.ts +190 -0
  48. package/src/model/prompt-prefix-hash.ts +47 -0
  49. package/src/model/token-estimator.ts +148 -0
  50. package/src/observation/observation-builder.ts +481 -0
  51. package/src/session/resume-projection.ts +616 -0
  52. package/src/session/session-catalog.ts +270 -0
  53. package/src/session/session-errors.ts +121 -0
  54. package/src/session/session-history-reader.ts +535 -0
  55. package/src/session/session-lock.ts +291 -0
  56. package/src/session/session-schema.ts +741 -0
  57. package/src/session/session-store.ts +3067 -0
  58. package/src/session/sqlite-session-ledger.ts +153 -0
  59. package/src/tools/bash-task.ts +617 -0
  60. package/src/tools/bash.ts +450 -0
  61. package/src/tools/cwd-state.ts +22 -0
  62. package/src/tools/edit.ts +428 -0
  63. package/src/tools/file-diff.ts +116 -0
  64. package/src/tools/glob.ts +202 -0
  65. package/src/tools/grep.ts +550 -0
  66. package/src/tools/hash.ts +9 -0
  67. package/src/tools/path-safety.ts +33 -0
  68. package/src/tools/read.ts +319 -0
  69. package/src/tools/recall.ts +400 -0
  70. package/src/tools/registry.ts +213 -0
  71. package/src/tools/ripgrep.ts +220 -0
  72. package/src/tools/task-list.ts +59 -0
  73. package/src/tools/task-output-snapshot.ts +47 -0
  74. package/src/tools/task-output-tool.ts +62 -0
  75. package/src/tools/task-output.ts +159 -0
  76. package/src/tools/task-stop.ts +59 -0
  77. package/src/tools/task-tool-args.ts +29 -0
  78. package/src/tools/types.ts +330 -0
  79. package/src/tools/web-fetch/backend.ts +27 -0
  80. package/src/tools/web-fetch/browser-backend.ts +126 -0
  81. package/src/tools/web-fetch/exa-backend.ts +172 -0
  82. package/src/tools/web-fetch/index.ts +298 -0
  83. package/src/tools/web-fetch/local-backend.ts +267 -0
  84. package/src/tools/web-fetch/refiner.ts +78 -0
  85. package/src/tools/web-fetch/route.ts +95 -0
  86. package/src/tools/web-search.ts +300 -0
  87. package/src/tools/write.ts +244 -0
  88. package/src/tui/app.tsx +497 -0
  89. package/src/tui/components/assistant-markdown.tsx +47 -0
  90. package/src/tui/components/background-tasks.tsx +92 -0
  91. package/src/tui/components/bash-result-view.tsx +47 -0
  92. package/src/tui/components/context-status.tsx +127 -0
  93. package/src/tui/components/diff-view.tsx +151 -0
  94. package/src/tui/components/file-viewer.tsx +212 -0
  95. package/src/tui/components/footer.tsx +60 -0
  96. package/src/tui/components/header.tsx +21 -0
  97. package/src/tui/components/model-picker.tsx +142 -0
  98. package/src/tui/components/prompt-input.tsx +432 -0
  99. package/src/tui/components/resume-session-picker.tsx +273 -0
  100. package/src/tui/components/timeline.tsx +121 -0
  101. package/src/tui/context-format.ts +24 -0
  102. package/src/tui/event-store.ts +865 -0
  103. package/src/tui/git-branch.ts +23 -0
  104. package/src/tui/line-editor.ts +157 -0
  105. package/src/tui/prompt-history.ts +94 -0
  106. package/src/tui/slash-commands.ts +126 -0
  107. package/src/tui/tui-projection-policy.ts +35 -0
  108. package/src/tui/tui-projection-store.ts +123 -0
  109. package/src/tui/tui-session-controller.ts +170 -0
  110. package/src/tui/view-file.ts +122 -0
@@ -0,0 +1,190 @@
1
+ import OpenAI from "openai";
2
+ import type { ChatCompletionCreateParamsNonStreaming } from "openai/resources/chat/completions";
3
+ import type { AssistantMessage } from "../agent/types";
4
+ import type { ModelContextBudget } from "./model-context-profile";
5
+ import type {
6
+ ModelClient,
7
+ ModelRequestInput,
8
+ ModelRequestOptions,
9
+ ModelRequestOutput,
10
+ PreparedModelRequest,
11
+ PreparedPromptSegment,
12
+ } from "./model-client";
13
+ import {
14
+ fromOpenAIChatCompletion,
15
+ toOpenAIChatMessages,
16
+ toOpenAIChatTools,
17
+ } from "./openai-chat-mapping";
18
+ import {
19
+ canonicalJsonValue,
20
+ sha256,
21
+ stableJsonStringify,
22
+ } from "./model-request-preflight";
23
+
24
+ const OPENAI_CHAT_SERIALIZATION_VERSION = "openai-chat-v1";
25
+
26
+ export class OpenAIChatModelClient implements ModelClient {
27
+ private readonly client: OpenAI;
28
+ private readonly preparedRequests = new WeakSet<object>();
29
+ private readonly provider: string;
30
+
31
+ constructor(
32
+ private readonly options: {
33
+ apiKey: string;
34
+ contextBudget: ModelContextBudget;
35
+ baseURL?: string;
36
+ includeReasoningContent?: boolean;
37
+ model: string;
38
+ providerName?: string;
39
+ timeoutMs?: number;
40
+ fetch?: typeof fetch;
41
+ },
42
+ ) {
43
+ this.provider = options.providerName ?? "openai-compatible";
44
+ this.client = new OpenAI({
45
+ apiKey: options.apiKey,
46
+ baseURL: options.baseURL,
47
+ timeout: options.timeoutMs,
48
+ fetch: options.fetch,
49
+ });
50
+ }
51
+
52
+ prepare(input: ModelRequestInput): PreparedModelRequest {
53
+ const messages = toOpenAIChatMessages(input.messages, {
54
+ includeReasoningContent: this.options.includeReasoningContent,
55
+ });
56
+ const tools = input.tools.length > 0 ? toOpenAIChatTools(input.tools) : undefined;
57
+ const payload = deepFreeze(
58
+ canonicalJsonValue({
59
+ model: this.options.model,
60
+ messages,
61
+ tools,
62
+ tool_choice: tools === undefined ? undefined : "auto",
63
+ max_tokens: this.options.contextBudget.requestMaxOutputTokens,
64
+ }),
65
+ ) as ChatCompletionCreateParamsNonStreaming;
66
+ const toolSegments = (payload.tools ?? []).map(
67
+ (tool): PreparedPromptSegment => ({
68
+ kind: "tool_schema",
69
+ normalizedText: stableJsonStringify(tool),
70
+ }),
71
+ );
72
+ const messageSegments = payload.messages.map(
73
+ (message, index): PreparedPromptSegment => ({
74
+ kind: segmentKind(input.messages[index]?.role),
75
+ normalizedText: stableJsonStringify(message),
76
+ }),
77
+ );
78
+ const requestConfigHash = sha256(
79
+ stableJsonStringify({
80
+ provider: this.provider,
81
+ baseURL: nonSecretBaseUrl(this.options.baseURL),
82
+ model: this.options.model,
83
+ serializationVersion: OPENAI_CHAT_SERIALIZATION_VERSION,
84
+ requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
85
+ includeReasoningContent: this.options.includeReasoningContent === true,
86
+ }),
87
+ );
88
+ const prepared: PreparedModelRequest = {
89
+ provider: this.provider,
90
+ model: this.options.model,
91
+ payload,
92
+ promptSegments: Object.freeze([...toolSegments, ...messageSegments]),
93
+ requestConfigHash,
94
+ toolSchemaHash: sha256(
95
+ toolSegments.map((segment) => segment.normalizedText).join("\n"),
96
+ ),
97
+ requestMaxOutputTokens: this.options.contextBudget.requestMaxOutputTokens,
98
+ assistantReplaySegments: (message) => this.assistantReplaySegments(message),
99
+ };
100
+ Object.freeze(prepared);
101
+ this.preparedRequests.add(prepared);
102
+ return prepared;
103
+ }
104
+
105
+ async request(
106
+ prepared: PreparedModelRequest,
107
+ options: ModelRequestOptions,
108
+ ): Promise<ModelRequestOutput> {
109
+ if (!this.preparedRequests.has(prepared)) {
110
+ throw new Error(
111
+ `OpenAI chat request was not prepared by this client (provider=${this.provider}, model=${this.options.model}).`,
112
+ );
113
+ }
114
+ if (
115
+ prepared.provider !== this.provider ||
116
+ prepared.model !== this.options.model ||
117
+ prepared.requestMaxOutputTokens !==
118
+ this.options.contextBudget.requestMaxOutputTokens
119
+ ) {
120
+ throw new Error(
121
+ "Prepared OpenAI chat request configuration does not match client.",
122
+ );
123
+ }
124
+
125
+ const response = await this.client.chat.completions.create(
126
+ prepared.payload as ChatCompletionCreateParamsNonStreaming,
127
+ { signal: options.signal },
128
+ );
129
+
130
+ return fromOpenAIChatCompletion(response, {
131
+ identity: options.identity,
132
+ provider: this.provider,
133
+ model: this.options.model,
134
+ });
135
+ }
136
+
137
+ private assistantReplaySegments(message: AssistantMessage): PreparedPromptSegment[] {
138
+ const [mapped] = toOpenAIChatMessages([message], {
139
+ includeReasoningContent: this.options.includeReasoningContent,
140
+ });
141
+ if (mapped === undefined) {
142
+ throw new Error("OpenAI assistant replay mapping produced no message.");
143
+ }
144
+ return [
145
+ {
146
+ kind: "assistant",
147
+ normalizedText: stableJsonStringify(canonicalJsonValue(mapped)),
148
+ },
149
+ ];
150
+ }
151
+ }
152
+
153
+ function segmentKind(
154
+ role: ModelRequestInput["messages"][number]["role"] | undefined,
155
+ ): PreparedPromptSegment["kind"] {
156
+ switch (role) {
157
+ case "system":
158
+ return "kernel";
159
+ case "user":
160
+ return "user";
161
+ case "assistant":
162
+ return "assistant";
163
+ case "tool":
164
+ return "tool";
165
+ case undefined:
166
+ throw new Error("OpenAI message mapping changed the message count.");
167
+ }
168
+ }
169
+
170
+ function nonSecretBaseUrl(value: string | undefined): string {
171
+ if (value === undefined) {
172
+ return "https://api.openai.com/v1";
173
+ }
174
+ try {
175
+ const url = new URL(value);
176
+ return `${url.origin}${url.pathname}`;
177
+ } catch {
178
+ return value.split(/[?#]/, 1)[0] ?? value;
179
+ }
180
+ }
181
+
182
+ function deepFreeze<T>(value: T): T {
183
+ if (typeof value !== "object" || value === null || Object.isFrozen(value)) {
184
+ return value;
185
+ }
186
+ for (const child of Object.values(value)) {
187
+ deepFreeze(child);
188
+ }
189
+ return Object.freeze(value);
190
+ }
@@ -0,0 +1,47 @@
1
+ import type { PreparedModelRequest, PreparedPromptSegment } from "./model-client";
2
+ import { sha256 } from "./model-request-preflight";
3
+
4
+ export type PromptPrefixFingerprint = {
5
+ readonly requestConfigHash: string;
6
+ readonly toolSchemaHash: string;
7
+ readonly segmentCount: number;
8
+ readonly prefixHash: string;
9
+ };
10
+
11
+ export function promptPrefixHashes(
12
+ requestConfigHash: string,
13
+ segments: readonly PreparedPromptSegment[],
14
+ ): readonly string[] {
15
+ const hashes = [sha256(`request-config:${requestConfigHash}`)];
16
+ for (const segment of segments) {
17
+ const previous = hashes.at(-1);
18
+ if (previous === undefined) {
19
+ throw new Error("Prompt prefix hash chain has no seed.");
20
+ }
21
+ hashes.push(
22
+ sha256(`${previous}\u0000${segment.kind}\u0000${segment.normalizedText}`),
23
+ );
24
+ }
25
+ return Object.freeze(hashes);
26
+ }
27
+
28
+ export function lastPromptPrefixHash(hashes: readonly string[]): string {
29
+ const value = hashes.at(-1);
30
+ if (value === undefined) {
31
+ throw new Error("Prompt prefix hash chain is empty.");
32
+ }
33
+ return value;
34
+ }
35
+
36
+ export function promptPrefixFingerprint(
37
+ prepared: PreparedModelRequest,
38
+ ): PromptPrefixFingerprint {
39
+ return Object.freeze({
40
+ requestConfigHash: prepared.requestConfigHash,
41
+ toolSchemaHash: prepared.toolSchemaHash,
42
+ segmentCount: prepared.promptSegments.length,
43
+ prefixHash: lastPromptPrefixHash(
44
+ promptPrefixHashes(prepared.requestConfigHash, prepared.promptSegments),
45
+ ),
46
+ });
47
+ }
@@ -0,0 +1,148 @@
1
+ import type { PreparedPromptSegment } from "./model-client";
2
+
3
+ export const INITIAL_CORRECTION_FACTOR = 1.25;
4
+ export const MIN_CORRECTION_FACTOR = 1.1;
5
+ export const OBSERVED_RATIO_PADDING = 1.05;
6
+ export const CALIBRATION_WINDOW_SIZE = 8;
7
+
8
+ export type RawContextBreakdown = {
9
+ kernelTokens: number;
10
+ userTokens: number;
11
+ assistantTokens: number;
12
+ toolTokens: number;
13
+ toolSchemaTokens: number;
14
+ protocolTokens: number;
15
+ totalTokens: number;
16
+ };
17
+
18
+ type BreakdownKey = Exclude<keyof RawContextBreakdown, "totalTokens">;
19
+
20
+ const breakdownKeys: BreakdownKey[] = [
21
+ "kernelTokens",
22
+ "userTokens",
23
+ "assistantTokens",
24
+ "toolTokens",
25
+ "toolSchemaTokens",
26
+ "protocolTokens",
27
+ ];
28
+
29
+ export function estimatePromptSegments(
30
+ segments: readonly PreparedPromptSegment[],
31
+ ): RawContextBreakdown {
32
+ const exact: Record<BreakdownKey, number> = {
33
+ kernelTokens: 0,
34
+ userTokens: 0,
35
+ assistantTokens: 0,
36
+ toolTokens: 0,
37
+ toolSchemaTokens: 0,
38
+ protocolTokens: segments.length * 8,
39
+ };
40
+
41
+ for (const segment of segments) {
42
+ exact[breakdownKey(segment.kind)] += estimateText(segment.normalizedText);
43
+ }
44
+
45
+ const totalTokens = Math.ceil(
46
+ breakdownKeys.reduce((total, key) => total + exact[key], 0),
47
+ );
48
+ const rounded = Object.fromEntries(
49
+ breakdownKeys.map((key) => [key, Math.floor(exact[key])]),
50
+ ) as Record<BreakdownKey, number>;
51
+ let remaining =
52
+ totalTokens - breakdownKeys.reduce((total, key) => total + rounded[key], 0);
53
+ const byFraction = [...breakdownKeys].sort((left, right) => {
54
+ const fractionDifference =
55
+ exact[right] - Math.floor(exact[right]) - (exact[left] - Math.floor(exact[left]));
56
+ return fractionDifference === 0
57
+ ? breakdownKeys.indexOf(left) - breakdownKeys.indexOf(right)
58
+ : fractionDifference;
59
+ });
60
+ for (const key of byFraction) {
61
+ if (remaining === 0) {
62
+ break;
63
+ }
64
+ rounded[key] += 1;
65
+ remaining -= 1;
66
+ }
67
+
68
+ return { ...rounded, totalTokens };
69
+ }
70
+
71
+ export class RollingTokenCalibration {
72
+ private readonly samples: number[] = [];
73
+
74
+ correctionFactor(): number {
75
+ if (this.samples.length === 0) {
76
+ return INITIAL_CORRECTION_FACTOR;
77
+ }
78
+ return Math.max(
79
+ MIN_CORRECTION_FACTOR,
80
+ Math.max(...this.samples) * OBSERVED_RATIO_PADDING,
81
+ );
82
+ }
83
+
84
+ sampleCount(): number {
85
+ return this.samples.length;
86
+ }
87
+
88
+ record(promptTokens: number, rawEstimatedTokens: number): void {
89
+ if (!Number.isSafeInteger(promptTokens) || promptTokens < 0) {
90
+ throw new Error(
91
+ `Calibration promptTokens must be a non-negative safe integer; received ${promptTokens}.`,
92
+ );
93
+ }
94
+ if (!Number.isSafeInteger(rawEstimatedTokens) || rawEstimatedTokens <= 0) {
95
+ throw new Error(
96
+ `Calibration rawEstimatedTokens must be a positive safe integer; received ${rawEstimatedTokens}.`,
97
+ );
98
+ }
99
+
100
+ this.samples.push(promptTokens / rawEstimatedTokens);
101
+ if (this.samples.length > CALIBRATION_WINDOW_SIZE) {
102
+ this.samples.shift();
103
+ }
104
+ }
105
+
106
+ clear(): void {
107
+ this.samples.length = 0;
108
+ }
109
+ }
110
+
111
+ function estimateText(value: string): number {
112
+ let asciiCount = 0;
113
+ let hanCount = 0;
114
+ let otherUtf8Bytes = 0;
115
+
116
+ for (const codePoint of value) {
117
+ const scalar = codePoint.codePointAt(0);
118
+ if (scalar === undefined) {
119
+ throw new Error("Unable to read a Unicode code point during token estimation.");
120
+ }
121
+ if (scalar <= 0x7f) {
122
+ asciiCount += 1;
123
+ } else if (/\p{Script=Han}/u.test(codePoint)) {
124
+ hanCount += 1;
125
+ } else {
126
+ otherUtf8Bytes += Buffer.byteLength(codePoint, "utf8");
127
+ }
128
+ }
129
+
130
+ return asciiCount * 0.3 + hanCount * 0.6 + otherUtf8Bytes * 0.5;
131
+ }
132
+
133
+ function breakdownKey(kind: PreparedPromptSegment["kind"]): BreakdownKey {
134
+ switch (kind) {
135
+ case "kernel":
136
+ return "kernelTokens";
137
+ case "user":
138
+ return "userTokens";
139
+ case "assistant":
140
+ return "assistantTokens";
141
+ case "tool":
142
+ return "toolTokens";
143
+ case "tool_schema":
144
+ return "toolSchemaTokens";
145
+ case "protocol":
146
+ return "protocolTokens";
147
+ }
148
+ }