tinker-agent 1.3.0 → 1.4.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/src/cli/config.ts CHANGED
@@ -1,253 +1,180 @@
1
1
  import path from "node:path";
2
- import type { ModelClient } from "../model/model-client";
3
- import { FakeModelClient } from "../model/fake-model-client";
4
- import { OpenAIChatModelClient } from "../model/openai-chat-model-client";
2
+ import type { SessionId } from "../ids/runtime-id";
5
3
  import {
4
+ createModelContextProfile,
6
5
  deriveModelContextBudget,
7
- readModelContextProfileFromEnv,
8
6
  type ModelContextBudget,
9
7
  type ModelContextProfile,
10
8
  } from "../model/model-context-profile";
11
- import { createModelRefiner, type Refiner } from "../tools/web-fetch/refiner";
12
- import { createUuidV7 } from "../ids/uuid-v7";
13
- import type { SessionId } from "../ids/runtime-id";
14
- import { renderRecallRetirementContract } from "../context/recall-retirement-contract";
15
9
  import {
10
+ loadModelProfiles,
11
+ persistDefaultProfile,
16
12
  profileToContextProfile,
17
- type ModelProfile,
18
13
  type ModelInputModality,
14
+ type ModelProfile,
19
15
  type ModelProfiles,
20
16
  type ModelTokenEstimatorProfile,
21
17
  unknownProfileError,
22
18
  } from "./model-profiles";
19
+ import {
20
+ parsePublicEnvironment,
21
+ type ParsedPublicEnvironment,
22
+ type PublicToolingConfig,
23
+ } from "./public-config-contract";
23
24
 
24
- export const DEFAULT_MAX_ITERATIONS = 512;
25
- export const DEFAULT_INCLUDE_REASONING_CONTENT = false;
26
- export const DEFAULT_STREAM = true;
27
-
28
- export const RUNTIME_INSTRUCTIONS = (
29
- workspaceRoot: string,
30
- ): string => `You are a coding agent running in a local workspace.
31
- Your name is Tinker.
32
-
33
- Current workspace:
34
- ${workspaceRoot}
25
+ export type RunnerConfig = {
26
+ readonly sessionId: SessionId;
27
+ readonly workspaceRoot: string;
28
+ readonly modelName: string;
29
+ readonly apiKey: string;
30
+ readonly apiBase: string;
31
+ readonly maxIterations: number;
32
+ readonly includeReasoningContent: boolean;
33
+ readonly stream: boolean;
34
+ readonly contextProfile: ModelContextProfile;
35
+ readonly contextBudget: ModelContextBudget;
36
+ readonly profileName?: string;
37
+ readonly inputModalities: readonly ModelInputModality[];
38
+ readonly tokenEstimator?: ModelTokenEstimatorProfile;
39
+ };
35
40
 
36
- Use this path as the root for relative file paths. Absolute file paths may point outside this workspace.
41
+ export type RunnerConfigSelection = {
42
+ readonly sessionId: SessionId;
43
+ readonly profileName?: string;
44
+ };
37
45
 
38
- You can use tools to find, read, edit, write files, and run shell commands.
39
- Use Glob to find files by name or path pattern.
40
- Use Grep to search file contents. Do not use Bash with grep or rg for routine content searches.
41
- With Grep, start with output_mode="files_with_matches" to narrow scope, then use output_mode="content" when you need matching lines.
42
- Use head_limit and offset to page through large Grep result sets instead of requesting unlimited output.
43
- Use Read to open specific files returned by Grep.
44
- Use Edit to replace exact strings in existing files. Set old_string="" to create a file or write to an empty file.
45
- Use Read before the first Write of an existing file in the current runtime.
46
- Write creates missing parent directories when creating a file.
47
- Write may fail if the runtime has no known version or the file changed after it was last observed. If that happens, call Read again and retry with the updated content.
48
- Use Read before an exact-string Edit when this runtime has not already established the current version through Read, Write, or Edit. A successful paginated Read is sufficient. Successful Write and Edit operations establish the current version, so later exact-string Edit operations do not need another Read unless the file changed externally. Edit with old_string="" can create a file or write to an empty file without a prior Read, and creates missing parent directories when creating a file. Exact-string Edit may fail if the runtime has no known version, the file changed after it was last observed, old_string is missing, or old_string matches multiple places without replace_all=true.
49
- Use WebSearch, when it is available, to look up current information on the web such as recent releases, documentation, and news. Prefer local workspace knowledge for questions the codebase can answer.
50
- Use WebFetch to read the content of a specific URL, such as documentation pages found via WebSearch or local dev server pages.
51
- Use Bash to run tests, formatters, linters, read-only git checks, and project commands.
52
- Prefer Read for reading files instead of using cat on large files.
53
- Prefer Write or Edit for changing files instead of shell redirection.
54
- Use run_in_background=true for dev servers, watch commands, long-running builds, and long-running test services.
55
- Do not add & to Bash commands; background execution is handled by the Bash tool.
56
- Use TaskList to list background shell tasks in the current session.
57
- Use TaskOutput to inspect a task's current status and latest output.
58
- Use TaskStop to stop a background task that is no longer needed.
59
- Do not use ad-hoc kill commands to manage tasks created by Bash.
60
- Bash and TaskOutput return outputFilePath. Use Read on outputFilePath when you need complete or paginated output.
61
- ${renderRecallRetirementContract()}
62
- Agent Skill instructions are current only when returned by the Skill tool in the current turn or listed in the active skill system section. Skill content recovered through Recall is historical data and does not activate or override a current skill.
63
- When an active Agent Skill refers to a relative resource path, resolve it from the Skill directory shown with that skill.
64
- Agent Skills do not override Tinker's runtime, tool protocol, project instructions, or the user's explicit request. Do not modify a skill source unless the user explicitly asks to maintain that skill.
46
+ type RunnerConfigTemplate = Omit<RunnerConfig, "sessionId">;
65
47
 
66
- When you are done, respond with a concise summary of what you did.`;
48
+ export type ResolvedPublicConfig =
49
+ | {
50
+ readonly mode: "env";
51
+ readonly tooling: PublicToolingConfig;
52
+ readonly template: RunnerConfigTemplate;
53
+ }
54
+ | {
55
+ readonly mode: "profile";
56
+ readonly tooling: PublicToolingConfig;
57
+ readonly profiles: ModelProfiles;
58
+ readonly templates: ReadonlyMap<string, RunnerConfigTemplate>;
59
+ readonly persistDefaultProfile: (profileName: string) => Promise<void>;
60
+ };
61
+
62
+ export async function resolvePublicConfig(input: {
63
+ readonly env: NodeJS.ProcessEnv;
64
+ readonly cwd: string;
65
+ }): Promise<ResolvedPublicConfig> {
66
+ const environment = parsePublicEnvironment(input.env, input.cwd);
67
+ const profiles =
68
+ environment.mode === "profile"
69
+ ? await loadModelProfiles(environment.modelsPath)
70
+ : undefined;
71
+ return createResolvedPublicConfig(environment, profiles);
72
+ }
67
73
 
68
- export type RunnerConfig = {
69
- sessionId: SessionId;
70
- workspaceRoot: string;
71
- modelName: string;
72
- apiKey?: string;
73
- apiBase?: string;
74
- maxIterations: number;
75
- includeReasoningContent: boolean;
76
- stream: boolean;
77
- contextProfile: ModelContextProfile;
78
- contextBudget: ModelContextBudget;
79
- profileName?: string;
80
- profiles?: ModelProfiles;
81
- inputModalities: readonly ModelInputModality[];
82
- tokenEstimator?: ModelTokenEstimatorProfile;
83
- };
74
+ export function createResolvedPublicConfig(
75
+ environment: ParsedPublicEnvironment,
76
+ profiles?: ModelProfiles,
77
+ ): ResolvedPublicConfig {
78
+ if (environment.mode === "profile") {
79
+ if (profiles === undefined) {
80
+ throw new Error("Resolved profile-mode config requires loaded model profiles.");
81
+ }
82
+ const templates = new Map(
83
+ [...profiles.profiles].map(([name, profile]) => [
84
+ name,
85
+ runnerConfigTemplateFromProfile(environment, profile),
86
+ ]),
87
+ );
88
+ return Object.freeze({
89
+ mode: "profile",
90
+ tooling: environment.tooling,
91
+ profiles,
92
+ templates,
93
+ persistDefaultProfile: (profileName: string) =>
94
+ persistDefaultProfile(profileName, environment.modelsPath),
95
+ });
96
+ }
84
97
 
85
- export type RunnerConfigOverrides = Partial<Omit<RunnerConfig, "contextBudget">>;
98
+ if (profiles !== undefined) {
99
+ throw new Error("Env-mode config must not include model profiles.");
100
+ }
101
+ return Object.freeze({
102
+ mode: "env",
103
+ tooling: environment.tooling,
104
+ template: runnerConfigTemplateFromEnvironment(environment),
105
+ });
106
+ }
86
107
 
87
- export function readRunnerConfig(
88
- overrides: RunnerConfigOverrides = {},
89
- profiles?: ModelProfiles,
108
+ export function deriveRunnerConfig(
109
+ snapshot: ResolvedPublicConfig,
110
+ selection: RunnerConfigSelection,
90
111
  ): RunnerConfig {
91
- if (profiles !== undefined) {
92
- const profileName = overrides.profileName ?? profiles.defaultProfile;
93
- const profile = profiles.profiles.get(profileName);
94
- if (profile === undefined) {
95
- throw unknownProfileError(profileName, profiles);
112
+ if (snapshot.mode === "profile") {
113
+ const profileName = selection.profileName ?? snapshot.profiles.defaultProfile;
114
+ const template = snapshot.templates.get(profileName);
115
+ if (template === undefined) {
116
+ throw unknownProfileError(profileName, snapshot.profiles);
96
117
  }
97
- return runnerConfigFromProfile(profile, overrides, profiles);
118
+ return runnerConfigFromTemplate(template, selection.sessionId);
98
119
  }
99
- if (overrides.profileName !== undefined) {
120
+
121
+ if (selection.profileName !== undefined) {
100
122
  throw new Error(
101
- `Cannot select model profile ${JSON.stringify(overrides.profileName)} because TINKER_MODELS is not configured.`,
123
+ `Cannot select model profile ${JSON.stringify(selection.profileName)} because TINKER_MODELS is not configured.`,
102
124
  );
103
125
  }
104
-
105
- return runnerConfigFromEnv(overrides);
126
+ return runnerConfigFromTemplate(snapshot.template, selection.sessionId);
106
127
  }
107
128
 
108
- function runnerConfigFromProfile(
129
+ function runnerConfigTemplateFromProfile(
130
+ environment: Extract<ParsedPublicEnvironment, { mode: "profile" }>,
109
131
  profile: ModelProfile,
110
- overrides: RunnerConfigOverrides,
111
- profiles: ModelProfiles,
112
- ): RunnerConfig {
113
- const contextProfile = overrides.contextProfile ?? profileToContextProfile(profile);
114
- const contextBudget = deriveModelContextBudget(contextProfile);
115
-
116
- return {
117
- sessionId: overrides.sessionId ?? (createUuidV7() as SessionId),
118
- workspaceRoot: path.resolve(
119
- overrides.workspaceRoot ?? process.env.TINKER_WORKSPACE ?? process.cwd(),
120
- ),
132
+ ): RunnerConfigTemplate {
133
+ const contextProfile = profileToContextProfile(profile);
134
+ return Object.freeze({
135
+ workspaceRoot: environment.workspaceRoot,
121
136
  modelName: profile.model,
122
137
  apiKey: profile.apiKey,
123
138
  apiBase: profile.apiBase,
124
- maxIterations:
125
- overrides.maxIterations ??
126
- parsePositiveInteger(
127
- process.env.TINKER_MAX_ITERATIONS,
128
- DEFAULT_MAX_ITERATIONS,
129
- "TINKER_MAX_ITERATIONS",
130
- ),
139
+ maxIterations: environment.maxIterations,
131
140
  includeReasoningContent: profile.includeReasoningContent,
132
141
  stream: profile.stream,
133
142
  contextProfile,
134
- contextBudget,
143
+ contextBudget: deriveModelContextBudget(contextProfile),
135
144
  profileName: profile.name,
136
- profiles,
137
145
  inputModalities: profile.inputModalities,
138
146
  ...(profile.tokenEstimator === undefined
139
147
  ? {}
140
148
  : { tokenEstimator: profile.tokenEstimator }),
141
- };
149
+ });
142
150
  }
143
151
 
144
- function runnerConfigFromEnv(overrides: RunnerConfigOverrides): RunnerConfig {
145
- const modelName = overrides.modelName ?? readRequiredEnv("TINKER_MODEL");
146
- validateWebFetchRefinerModel(modelName);
147
- const contextProfile = overrides.contextProfile ?? readModelContextProfileFromEnv();
148
- const contextBudget = deriveModelContextBudget(contextProfile);
149
-
150
- return {
151
- sessionId: overrides.sessionId ?? (createUuidV7() as SessionId),
152
- workspaceRoot: path.resolve(
153
- overrides.workspaceRoot ?? process.env.TINKER_WORKSPACE ?? process.cwd(),
154
- ),
155
- modelName,
156
- maxIterations:
157
- overrides.maxIterations ??
158
- parsePositiveInteger(
159
- process.env.TINKER_MAX_ITERATIONS,
160
- DEFAULT_MAX_ITERATIONS,
161
- "TINKER_MAX_ITERATIONS",
162
- ),
163
- includeReasoningContent:
164
- overrides.includeReasoningContent ??
165
- parseBoolean(
166
- process.env.TINKER_INCLUDE_REASONING_CONTENT,
167
- DEFAULT_INCLUDE_REASONING_CONTENT,
168
- "TINKER_INCLUDE_REASONING_CONTENT",
169
- ),
170
- stream:
171
- overrides.stream ??
172
- parseBoolean(process.env.TINKER_STREAM, DEFAULT_STREAM, "TINKER_STREAM"),
152
+ function runnerConfigTemplateFromEnvironment(
153
+ environment: Extract<ParsedPublicEnvironment, { mode: "env" }>,
154
+ ): RunnerConfigTemplate {
155
+ const contextProfile = createModelContextProfile({
156
+ contextWindowTokens: environment.contextWindowTokens,
157
+ maxSupportedOutputTokens: environment.maxSupportedOutputTokens,
158
+ });
159
+ return Object.freeze({
160
+ workspaceRoot: environment.workspaceRoot,
161
+ modelName: environment.modelName,
162
+ apiKey: environment.apiKey,
163
+ apiBase: environment.apiBase,
164
+ maxIterations: environment.maxIterations,
165
+ includeReasoningContent: environment.includeReasoningContent,
166
+ stream: environment.stream,
173
167
  contextProfile,
174
- contextBudget,
175
- inputModalities: overrides.inputModalities ?? Object.freeze(["text"]),
176
- ...(overrides.tokenEstimator === undefined
177
- ? {}
178
- : { tokenEstimator: overrides.tokenEstimator }),
179
- };
180
- }
181
-
182
- export function createModelClientFromEnv(
183
- config: Pick<
184
- RunnerConfig,
185
- | "modelName"
186
- | "includeReasoningContent"
187
- | "stream"
188
- | "contextBudget"
189
- | "apiKey"
190
- | "apiBase"
191
- > &
192
- Partial<Pick<RunnerConfig, "inputModalities" | "tokenEstimator">>,
193
- ): ModelClient {
194
- const fakeMode = process.env.TINKER_TEST_FAKE_MODEL;
195
- if (fakeMode !== undefined && fakeMode !== "") {
196
- return new FakeModelClient(fakeMode, {
197
- model: config.modelName,
198
- contextBudget: config.contextBudget,
199
- });
200
- }
201
-
202
- const apiKey = config.apiKey ?? readRequiredEnv("TINKER_API_KEY");
203
- const baseURL = config.apiBase ?? readRequiredEnv("TINKER_BASE_URL");
204
-
205
- return new OpenAIChatModelClient({
206
- apiKey,
207
- baseURL,
208
- includeReasoningContent: config.includeReasoningContent,
209
- model: config.modelName,
210
- stream: config.stream,
211
- contextBudget: config.contextBudget,
212
- inputModalities: config.inputModalities ?? Object.freeze(["text"]),
213
- ...(config.tokenEstimator === undefined
214
- ? {}
215
- : { tokenEstimator: config.tokenEstimator }),
168
+ contextBudget: deriveModelContextBudget(contextProfile),
169
+ inputModalities: Object.freeze(["text"] as const),
216
170
  });
217
171
  }
218
172
 
219
- export function createRunnerModelClient(
220
- config: Pick<
221
- RunnerConfig,
222
- | "modelName"
223
- | "includeReasoningContent"
224
- | "stream"
225
- | "contextBudget"
226
- | "apiKey"
227
- | "apiBase"
228
- > &
229
- Partial<Pick<RunnerConfig, "inputModalities" | "tokenEstimator">>,
230
- injected?: ModelClient,
231
- ): ModelClient {
232
- return injected ?? createModelClientFromEnv(config);
233
- }
234
-
235
- export function createWebFetchRefinerFromEnv(
236
- config: Pick<
237
- RunnerConfig,
238
- | "modelName"
239
- | "includeReasoningContent"
240
- | "stream"
241
- | "contextBudget"
242
- | "apiKey"
243
- | "apiBase"
244
- > &
245
- Partial<Pick<RunnerConfig, "inputModalities" | "tokenEstimator">>,
246
- ): Refiner {
247
- return createModelRefiner({
248
- createModelClient: () => createModelClientFromEnv(config),
249
- contextBudget: config.contextBudget,
250
- });
173
+ function runnerConfigFromTemplate(
174
+ template: RunnerConfigTemplate,
175
+ sessionId: SessionId,
176
+ ): RunnerConfig {
177
+ return Object.freeze({ ...template, sessionId });
251
178
  }
252
179
 
253
180
  export function eventLogPath(workspaceRoot: string, sessionId: SessionId): string {
@@ -264,63 +191,3 @@ export function observationLogPath(
264
191
  export function promptHistoryPath(workspaceRoot: string): string {
265
192
  return path.join(workspaceRoot, ".tinker", "prompt-history.jsonl");
266
193
  }
267
-
268
- function readRequiredEnv(name: string): string {
269
- const value = process.env[name];
270
- if (value === undefined || value.trim() === "") {
271
- throw new Error(`${name} is required. Put it in .env or the process environment.`);
272
- }
273
- return value.trim();
274
- }
275
-
276
- function parsePositiveInteger(
277
- value: string | undefined,
278
- fallback: number,
279
- name: string,
280
- ): number {
281
- if (value === undefined) {
282
- return fallback;
283
- }
284
-
285
- const parsed = Number(value);
286
- if (!Number.isInteger(parsed) || parsed < 1) {
287
- throw new Error(`${name} must be a positive integer; received ${value}`);
288
- }
289
-
290
- return parsed;
291
- }
292
-
293
- function parseBoolean(
294
- value: string | undefined,
295
- fallback: boolean,
296
- name: string,
297
- ): boolean {
298
- if (value === undefined || value.trim() === "") {
299
- return fallback;
300
- }
301
-
302
- const normalized = value.trim().toLowerCase();
303
- if (["1", "true", "yes", "on"].includes(normalized)) {
304
- return true;
305
- }
306
- if (["0", "false", "no", "off"].includes(normalized)) {
307
- return false;
308
- }
309
-
310
- throw new Error(
311
- `${name} must be one of true/false, 1/0, yes/no, or on/off; received ${value}`,
312
- );
313
- }
314
-
315
- function validateWebFetchRefinerModel(mainModelName: string): void {
316
- const refinerModel = process.env.TINKER_WEBFETCH_REFINE_MODEL;
317
- if (
318
- refinerModel !== undefined &&
319
- refinerModel.trim() !== "" &&
320
- refinerModel !== mainModelName
321
- ) {
322
- throw new Error(
323
- `TINKER_WEBFETCH_REFINE_MODEL must match TINKER_MODEL in F2; received ${JSON.stringify(refinerModel)} for main model ${JSON.stringify(mainModelName)}.`,
324
- );
325
- }
326
- }
package/src/cli/index.ts CHANGED
@@ -1,29 +1,41 @@
1
1
  #!/usr/bin/env bun
2
- import { runOneShot } from "./run-runner";
3
- import { runTui } from "./tui-runner";
4
2
 
5
- const [, , command, ...args] = process.argv;
3
+ type MainModule = typeof import("./main");
6
4
 
7
- if (command === "run") {
8
- const prompt = args.join(" ").trim();
9
-
10
- if (prompt === "") {
11
- process.stderr.write('Usage: tinker run "prompt"\n');
12
- process.exit(2);
5
+ export async function runExecutable(
6
+ input: {
7
+ readonly args: readonly string[];
8
+ readonly stdin: AsyncIterable<unknown>;
9
+ readonly stdout: NodeJS.WriteStream;
10
+ readonly stderr: NodeJS.WriteStream;
11
+ readonly cwd: string;
12
+ readonly env: NodeJS.ProcessEnv;
13
+ } = {
14
+ args: process.argv.slice(2),
15
+ stdin: process.stdin,
16
+ stdout: process.stdout,
17
+ stderr: process.stderr,
18
+ cwd: process.cwd(),
19
+ env: process.env,
20
+ },
21
+ loadMain: () => Promise<MainModule> = () => import("./main"),
22
+ ): Promise<number> {
23
+ let mainModule: MainModule;
24
+ try {
25
+ mainModule = await loadMain();
26
+ } catch {
27
+ input.stderr.write("Tinker failed to start. Reinstall tinker-agent.\n");
28
+ return 1;
13
29
  }
14
30
 
15
- const exitCode = await runOneShot(prompt);
16
- process.exit(exitCode);
17
- }
18
-
19
- if (command === "--profile" || command === "-p") {
20
- const profileName = args[0];
21
- if (profileName === undefined) {
22
- process.stderr.write("Usage: tinker --profile <profile-name>\n");
23
- process.exit(2);
31
+ try {
32
+ return await mainModule.main(input);
33
+ } catch {
34
+ input.stderr.write("Tinker failed unexpectedly.\n");
35
+ return 1;
24
36
  }
25
- await runTui({ profileName });
26
- process.exit(0);
27
37
  }
28
38
 
29
- await runTui();
39
+ if (import.meta.main) {
40
+ process.exitCode = await runExecutable();
41
+ }