tinker-agent 1.2.1 → 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/CHANGELOG.md +26 -1
- package/README.md +217 -72
- package/bin/tinker.js +75 -25
- package/package.json +8 -3
- package/src/agent/loop.ts +115 -11
- package/src/agent/runtime-session.ts +34 -15
- package/src/cli/command-line.ts +291 -0
- package/src/cli/config.ts +131 -264
- package/src/cli/index.ts +33 -21
- package/src/cli/main.ts +213 -0
- package/src/cli/model-profiles.ts +143 -72
- package/src/cli/output.ts +113 -0
- package/src/cli/package-metadata.ts +36 -0
- package/src/cli/prompt-source.ts +229 -0
- package/src/cli/public-cli-contract.ts +69 -0
- package/src/cli/public-config-contract.ts +650 -0
- package/src/cli/run-runner.ts +17 -12
- package/src/cli/runner-dependencies.ts +100 -0
- package/src/cli/tui-runner.tsx +52 -49
- package/src/events/observation-text-log.ts +2 -0
- package/src/events/stdout-event-printer.ts +7 -1
- package/src/events/types.ts +27 -3
- package/src/mcp/mcp-manager.ts +2 -19
- package/src/mcp/mcp-tool-executor.ts +3 -4
- package/src/model/model-client.ts +29 -0
- package/src/model/model-context-profile.ts +0 -30
- package/src/model/openai-chat-mapping.ts +54 -15
- package/src/model/openai-chat-model-client.ts +46 -27
- package/src/model/openai-chat-stream.ts +6 -2
- package/src/tools/bash.ts +8 -25
- package/src/tools/grep.ts +9 -1
- package/src/tools/registry.ts +15 -1
- package/src/tools/ripgrep.ts +24 -27
- package/src/tools/web-fetch/index.ts +2 -15
- package/src/tui/app.tsx +3 -0
- package/src/tui/components/prompt-input.tsx +6 -3
- package/src/tui/event-store.ts +24 -7
- package/src/tui/slash-commands.ts +76 -24
- package/src/tui/workspace-file-search.ts +78 -71
package/src/cli/run-runner.ts
CHANGED
|
@@ -10,27 +10,29 @@ import {
|
|
|
10
10
|
loadProjectInstructions,
|
|
11
11
|
projectInstructionManifest,
|
|
12
12
|
} from "../instructions/project-instructions";
|
|
13
|
+
import { type RunnerConfig } from "./config";
|
|
13
14
|
import {
|
|
14
15
|
createRunnerModelClient,
|
|
15
|
-
|
|
16
|
-
readRunnerConfig,
|
|
16
|
+
createWebFetchRefiner,
|
|
17
17
|
RUNTIME_INSTRUCTIONS,
|
|
18
|
-
|
|
19
|
-
} from "./config";
|
|
20
|
-
import { loadModelProfiles } from "./model-profiles";
|
|
18
|
+
} from "./runner-dependencies";
|
|
19
|
+
import type { PublicToolingConfig } from "./public-config-contract";
|
|
21
20
|
import { realpath } from "node:fs/promises";
|
|
22
21
|
import { loadSkillCatalog } from "../skills/skill-loader";
|
|
23
22
|
|
|
24
|
-
export type RunOneShotOptions =
|
|
23
|
+
export type RunOneShotOptions = {
|
|
24
|
+
config: RunnerConfig;
|
|
25
|
+
tooling: PublicToolingConfig;
|
|
25
26
|
modelClient?: ModelClient;
|
|
26
27
|
stdout?: WritableLike;
|
|
27
28
|
stderr?: WritableLike;
|
|
28
29
|
eventLogPath?: string | false;
|
|
30
|
+
env?: NodeJS.ProcessEnv;
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export async function runOneShot(
|
|
32
34
|
userPrompt: string,
|
|
33
|
-
options: RunOneShotOptions
|
|
35
|
+
options: RunOneShotOptions,
|
|
34
36
|
): Promise<number> {
|
|
35
37
|
const stdout = options.stdout ?? process.stdout;
|
|
36
38
|
const stderr = options.stderr ?? process.stderr;
|
|
@@ -40,9 +42,7 @@ export async function runOneShot(
|
|
|
40
42
|
let exitCode = 1;
|
|
41
43
|
|
|
42
44
|
try {
|
|
43
|
-
const
|
|
44
|
-
options.modelClient === undefined ? await loadModelProfiles() : undefined;
|
|
45
|
-
const config = readRunnerConfig(options, profiles);
|
|
45
|
+
const config = options.config;
|
|
46
46
|
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
47
47
|
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
48
48
|
const skillCatalog = await loadSkillCatalog({ workspaceRoot });
|
|
@@ -51,7 +51,11 @@ export async function runOneShot(
|
|
|
51
51
|
runtimeInstructions: RUNTIME_INSTRUCTIONS(workspaceRoot),
|
|
52
52
|
projectInstructions,
|
|
53
53
|
});
|
|
54
|
-
const modelClient = createRunnerModelClient(
|
|
54
|
+
const modelClient = createRunnerModelClient(
|
|
55
|
+
config,
|
|
56
|
+
options.modelClient,
|
|
57
|
+
options.env,
|
|
58
|
+
);
|
|
55
59
|
session = await createRuntimeSession({
|
|
56
60
|
selection: { mode: "new", sessionId: config.sessionId },
|
|
57
61
|
workspaceRoot,
|
|
@@ -68,7 +72,8 @@ export async function runOneShot(
|
|
|
68
72
|
presentationSinks: [new StdoutEventPrinter(stdout, stderr)],
|
|
69
73
|
persistence:
|
|
70
74
|
options.eventLogPath === false ? false : { eventLogPath: options.eventLogPath },
|
|
71
|
-
webFetchRefiner:
|
|
75
|
+
webFetchRefiner: createWebFetchRefiner(config, options.env),
|
|
76
|
+
toolingConfig: options.tooling,
|
|
72
77
|
});
|
|
73
78
|
|
|
74
79
|
const result = await session.executeTurn({
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { renderRecallRetirementContract } from "../context/recall-retirement-contract";
|
|
2
|
+
import { FakeModelClient } from "../model/fake-model-client";
|
|
3
|
+
import type { ModelClient } from "../model/model-client";
|
|
4
|
+
import { OpenAIChatModelClient } from "../model/openai-chat-model-client";
|
|
5
|
+
import { createModelRefiner, type Refiner } from "../tools/web-fetch/refiner";
|
|
6
|
+
import type { RunnerConfig } from "./config";
|
|
7
|
+
|
|
8
|
+
export const RUNTIME_INSTRUCTIONS = (
|
|
9
|
+
workspaceRoot: string,
|
|
10
|
+
): string => `You are a coding agent running in a local workspace.
|
|
11
|
+
Your name is Tinker.
|
|
12
|
+
|
|
13
|
+
Current workspace:
|
|
14
|
+
${workspaceRoot}
|
|
15
|
+
|
|
16
|
+
Use this path as the root for relative file paths. Absolute file paths may point outside this workspace.
|
|
17
|
+
|
|
18
|
+
You can use tools to find, read, edit, write files, and run shell commands.
|
|
19
|
+
Use Glob to find files by name or path pattern.
|
|
20
|
+
Use Grep to search file contents. Do not use Bash with grep or rg for routine content searches.
|
|
21
|
+
With Grep, start with output_mode="files_with_matches" to narrow scope, then use output_mode="content" when you need matching lines.
|
|
22
|
+
Use head_limit and offset to page through large Grep result sets instead of requesting unlimited output.
|
|
23
|
+
Use Read to open specific files returned by Grep.
|
|
24
|
+
Use Edit to replace exact strings in existing files. Set old_string="" to create a file or write to an empty file.
|
|
25
|
+
Use Read before the first Write of an existing file in the current runtime.
|
|
26
|
+
Write creates missing parent directories when creating a file.
|
|
27
|
+
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.
|
|
28
|
+
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.
|
|
29
|
+
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.
|
|
30
|
+
Use WebFetch to read the content of a specific URL, such as documentation pages found via WebSearch or local dev server pages.
|
|
31
|
+
Use Bash to run tests, formatters, linters, read-only git checks, and project commands.
|
|
32
|
+
Prefer Read for reading files instead of using cat on large files.
|
|
33
|
+
Prefer Write or Edit for changing files instead of shell redirection.
|
|
34
|
+
Use run_in_background=true for dev servers, watch commands, long-running builds, and long-running test services.
|
|
35
|
+
Do not add & to Bash commands; background execution is handled by the Bash tool.
|
|
36
|
+
Use TaskList to list background shell tasks in the current session.
|
|
37
|
+
Use TaskOutput to inspect a task's current status and latest output.
|
|
38
|
+
Use TaskStop to stop a background task that is no longer needed.
|
|
39
|
+
Do not use ad-hoc kill commands to manage tasks created by Bash.
|
|
40
|
+
Bash and TaskOutput return outputFilePath. Use Read on outputFilePath when you need complete or paginated output.
|
|
41
|
+
${renderRecallRetirementContract()}
|
|
42
|
+
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.
|
|
43
|
+
When an active Agent Skill refers to a relative resource path, resolve it from the Skill directory shown with that skill.
|
|
44
|
+
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.
|
|
45
|
+
|
|
46
|
+
When you are done, respond with a concise summary of what you did.`;
|
|
47
|
+
|
|
48
|
+
export function createModelClient(
|
|
49
|
+
config: Pick<
|
|
50
|
+
RunnerConfig,
|
|
51
|
+
| "modelName"
|
|
52
|
+
| "includeReasoningContent"
|
|
53
|
+
| "stream"
|
|
54
|
+
| "contextBudget"
|
|
55
|
+
| "apiKey"
|
|
56
|
+
| "apiBase"
|
|
57
|
+
| "inputModalities"
|
|
58
|
+
| "tokenEstimator"
|
|
59
|
+
>,
|
|
60
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
61
|
+
): ModelClient {
|
|
62
|
+
const fakeMode = env.TINKER_TEST_FAKE_MODEL;
|
|
63
|
+
if (fakeMode !== undefined && fakeMode !== "") {
|
|
64
|
+
return new FakeModelClient(fakeMode, {
|
|
65
|
+
model: config.modelName,
|
|
66
|
+
contextBudget: config.contextBudget,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return new OpenAIChatModelClient({
|
|
71
|
+
apiKey: config.apiKey,
|
|
72
|
+
baseURL: config.apiBase,
|
|
73
|
+
includeReasoningContent: config.includeReasoningContent,
|
|
74
|
+
model: config.modelName,
|
|
75
|
+
stream: config.stream,
|
|
76
|
+
contextBudget: config.contextBudget,
|
|
77
|
+
inputModalities: config.inputModalities,
|
|
78
|
+
...(config.tokenEstimator === undefined
|
|
79
|
+
? {}
|
|
80
|
+
: { tokenEstimator: config.tokenEstimator }),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createRunnerModelClient(
|
|
85
|
+
config: Parameters<typeof createModelClient>[0],
|
|
86
|
+
injected?: ModelClient,
|
|
87
|
+
env?: NodeJS.ProcessEnv,
|
|
88
|
+
): ModelClient {
|
|
89
|
+
return injected ?? createModelClient(config, env);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createWebFetchRefiner(
|
|
93
|
+
config: Parameters<typeof createModelClient>[0],
|
|
94
|
+
env?: NodeJS.ProcessEnv,
|
|
95
|
+
): Refiner {
|
|
96
|
+
return createModelRefiner({
|
|
97
|
+
createModelClient: () => createModelClient(config, env),
|
|
98
|
+
contextBudget: config.contextBudget,
|
|
99
|
+
});
|
|
100
|
+
}
|
package/src/cli/tui-runner.tsx
CHANGED
|
@@ -27,28 +27,31 @@ import {
|
|
|
27
27
|
type TuiSessionBinding,
|
|
28
28
|
} from "../tui/tui-session-controller";
|
|
29
29
|
import {
|
|
30
|
-
createRunnerModelClient,
|
|
31
|
-
createWebFetchRefinerFromEnv,
|
|
32
30
|
promptHistoryPath,
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
deriveRunnerConfig,
|
|
32
|
+
type ResolvedPublicConfig,
|
|
35
33
|
type RunnerConfig,
|
|
36
34
|
} from "./config";
|
|
37
35
|
import {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
} from "./model-profiles";
|
|
36
|
+
createRunnerModelClient,
|
|
37
|
+
createWebFetchRefiner,
|
|
38
|
+
RUNTIME_INSTRUCTIONS,
|
|
39
|
+
} from "./runner-dependencies";
|
|
40
|
+
import { resolveSessionProfileName, type ModelProfile } from "./model-profiles";
|
|
43
41
|
import { loadSkillCatalog } from "../skills/skill-loader";
|
|
44
42
|
import { loadProjectSlashCommands } from "../tui/project-slash-commands";
|
|
43
|
+
import { createWorkspaceFileLister } from "../tui/workspace-file-search";
|
|
45
44
|
|
|
46
|
-
export
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
export type RunTuiOptions = {
|
|
46
|
+
readonly publicConfig: ResolvedPublicConfig;
|
|
47
|
+
readonly initialRunnerConfig: RunnerConfig;
|
|
48
|
+
readonly env: NodeJS.ProcessEnv;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export async function runTui(options: RunTuiOptions): Promise<void> {
|
|
52
|
+
const profiles =
|
|
53
|
+
options.publicConfig.mode === "profile" ? options.publicConfig.profiles : undefined;
|
|
54
|
+
const config = options.initialRunnerConfig;
|
|
52
55
|
const workspaceRoot = await realpath(config.workspaceRoot);
|
|
53
56
|
let controller: DefaultTuiSessionController | undefined;
|
|
54
57
|
let instance: ReturnType<typeof render> | undefined;
|
|
@@ -64,7 +67,11 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
64
67
|
sessionId: SessionId,
|
|
65
68
|
sink: EventSink,
|
|
66
69
|
): Promise<RuntimeSession> => {
|
|
67
|
-
const modelClient = createRunnerModelClient(
|
|
70
|
+
const modelClient = createRunnerModelClient(
|
|
71
|
+
sessionConfig,
|
|
72
|
+
undefined,
|
|
73
|
+
options.env,
|
|
74
|
+
);
|
|
68
75
|
const projectInstructions = await loadProjectInstructions(workspaceRoot);
|
|
69
76
|
const skillCatalog = await loadSkillCatalog({ workspaceRoot });
|
|
70
77
|
const common = {
|
|
@@ -84,7 +91,8 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
84
91
|
projectInstruction: projectInstructionManifest(projectInstructions),
|
|
85
92
|
skillCatalog,
|
|
86
93
|
presentationSinks: [sink],
|
|
87
|
-
webFetchRefiner:
|
|
94
|
+
webFetchRefiner: createWebFetchRefiner(sessionConfig, options.env),
|
|
95
|
+
toolingConfig: options.publicConfig.tooling,
|
|
88
96
|
};
|
|
89
97
|
if (mode === "resume") {
|
|
90
98
|
return createRuntimeSession({
|
|
@@ -118,18 +126,12 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
118
126
|
): Promise<ManagedTuiSessionBinding> => {
|
|
119
127
|
const deferred = new DeferredProjectionSink();
|
|
120
128
|
const summary = await catalog.get(sessionId);
|
|
121
|
-
const resumeConfig =
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
profileName: resolveSessionProfileName(profiles, summary),
|
|
128
|
-
workspaceRoot: config.workspaceRoot,
|
|
129
|
-
maxIterations: config.maxIterations,
|
|
130
|
-
},
|
|
131
|
-
profiles,
|
|
132
|
-
);
|
|
129
|
+
const resumeConfig = deriveRunnerConfig(options.publicConfig, {
|
|
130
|
+
sessionId,
|
|
131
|
+
...(profiles === undefined
|
|
132
|
+
? {}
|
|
133
|
+
: { profileName: resolveSessionProfileName(profiles, summary) }),
|
|
134
|
+
});
|
|
133
135
|
const runtimeSession = await createSessionForConfig(
|
|
134
136
|
resumeConfig,
|
|
135
137
|
"resume",
|
|
@@ -195,15 +197,10 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
195
197
|
throw new Error("Model profiles are not configured.");
|
|
196
198
|
}
|
|
197
199
|
return createNewSessionBinding(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
profileName: profile.name,
|
|
203
|
-
maxIterations: config.maxIterations,
|
|
204
|
-
},
|
|
205
|
-
profiles,
|
|
206
|
-
),
|
|
200
|
+
deriveRunnerConfig(options.publicConfig, {
|
|
201
|
+
sessionId: createUuidV7() as SessionId,
|
|
202
|
+
profileName: profile.name,
|
|
203
|
+
}),
|
|
207
204
|
);
|
|
208
205
|
};
|
|
209
206
|
|
|
@@ -212,21 +209,18 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
212
209
|
): Promise<ManagedTuiSessionBinding> => {
|
|
213
210
|
const sessionId = createUuidV7() as SessionId;
|
|
214
211
|
if (profiles === undefined) {
|
|
215
|
-
return createNewSessionBinding(
|
|
212
|
+
return createNewSessionBinding(
|
|
213
|
+
deriveRunnerConfig(options.publicConfig, { sessionId }),
|
|
214
|
+
);
|
|
216
215
|
}
|
|
217
216
|
if (current.profileName === undefined) {
|
|
218
217
|
throw new Error("Current session does not have a model profile.");
|
|
219
218
|
}
|
|
220
219
|
return createNewSessionBinding(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
profileName: current.profileName,
|
|
226
|
-
maxIterations: config.maxIterations,
|
|
227
|
-
},
|
|
228
|
-
profiles,
|
|
229
|
-
),
|
|
220
|
+
deriveRunnerConfig(options.publicConfig, {
|
|
221
|
+
sessionId,
|
|
222
|
+
profileName: current.profileName,
|
|
223
|
+
}),
|
|
230
224
|
);
|
|
231
225
|
};
|
|
232
226
|
|
|
@@ -251,7 +245,16 @@ export async function runTui(options: { profileName?: string } = {}): Promise<vo
|
|
|
251
245
|
history={promptHistory}
|
|
252
246
|
projectSlashCommands={projectSlashCommands}
|
|
253
247
|
profiles={profiles}
|
|
254
|
-
persistDefaultProfile={
|
|
248
|
+
persistDefaultProfile={
|
|
249
|
+
options.publicConfig.mode === "profile"
|
|
250
|
+
? options.publicConfig.persistDefaultProfile
|
|
251
|
+
: undefined
|
|
252
|
+
}
|
|
253
|
+
fileLister={createWorkspaceFileLister({
|
|
254
|
+
command: options.publicConfig.tooling.ripgrepPath,
|
|
255
|
+
timeoutMs: options.publicConfig.tooling.grepTimeoutMs,
|
|
256
|
+
maxBufferBytes: options.publicConfig.tooling.grepMaxBufferBytes,
|
|
257
|
+
})}
|
|
255
258
|
onQuit={() => {
|
|
256
259
|
quitRequested = true;
|
|
257
260
|
}}
|
|
@@ -45,6 +45,8 @@ export function renderObservationLogEvent(event: AgentEvent): string | undefined
|
|
|
45
45
|
return renderTurnCancelled(event);
|
|
46
46
|
case "turn.failed":
|
|
47
47
|
return renderTurnFailed(event.data.error);
|
|
48
|
+
case "model.request.failed":
|
|
49
|
+
return undefined;
|
|
48
50
|
default:
|
|
49
51
|
return undefined;
|
|
50
52
|
}
|
|
@@ -43,7 +43,13 @@ export class StdoutEventPrinter implements EventSink {
|
|
|
43
43
|
);
|
|
44
44
|
break;
|
|
45
45
|
case "model.request.started":
|
|
46
|
-
|
|
46
|
+
if (event.data.attemptNumber === 1) {
|
|
47
|
+
this.stdout.write(
|
|
48
|
+
`model.request.started iteration=${event.iterationNumber}\n`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
break;
|
|
52
|
+
case "model.request.failed":
|
|
47
53
|
break;
|
|
48
54
|
case "model.request.finished":
|
|
49
55
|
this.stdout.write(
|
package/src/events/types.ts
CHANGED
|
@@ -13,7 +13,11 @@ import type {
|
|
|
13
13
|
ToolCallId,
|
|
14
14
|
TurnId,
|
|
15
15
|
} from "../ids/runtime-id";
|
|
16
|
-
import type {
|
|
16
|
+
import type {
|
|
17
|
+
ModelRequestOutput,
|
|
18
|
+
ProviderResponseDiagnostics,
|
|
19
|
+
ProviderResponseErrorCode,
|
|
20
|
+
} from "../model/model-client";
|
|
17
21
|
import type {
|
|
18
22
|
ModelContextBudget,
|
|
19
23
|
ModelContextProfile,
|
|
@@ -280,6 +284,22 @@ export type TurnFinishedData = {
|
|
|
280
284
|
messageCount: number;
|
|
281
285
|
};
|
|
282
286
|
|
|
287
|
+
export type ModelRequestAttemptData = {
|
|
288
|
+
attemptNumber: 1 | 2;
|
|
289
|
+
maxAttempts: 2;
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
export type ModelRequestFailureCode = ProviderResponseErrorCode;
|
|
293
|
+
|
|
294
|
+
export type ModelRequestFailedData = ModelRequestAttemptData & {
|
|
295
|
+
code: ModelRequestFailureCode;
|
|
296
|
+
retryDisposition: "scheduled" | "not_retryable" | "exhausted";
|
|
297
|
+
provider: string;
|
|
298
|
+
model: string;
|
|
299
|
+
error: string;
|
|
300
|
+
diagnostics?: ProviderResponseDiagnostics;
|
|
301
|
+
};
|
|
302
|
+
|
|
283
303
|
export type AgentEventDataMap = {
|
|
284
304
|
"session.started": SessionStartedData;
|
|
285
305
|
"session.resumed": SessionResumedData;
|
|
@@ -290,8 +310,11 @@ export type AgentEventDataMap = {
|
|
|
290
310
|
"turn.failed": { error: string };
|
|
291
311
|
"turn.cancelled": { cancellation: TurnCancellation };
|
|
292
312
|
"agent.iteration.started": { iterationNumber: number };
|
|
293
|
-
"model.request.started":
|
|
294
|
-
"model.request.
|
|
313
|
+
"model.request.started": ModelRequestAttemptData;
|
|
314
|
+
"model.request.failed": ModelRequestFailedData;
|
|
315
|
+
"model.request.finished": ModelRequestAttemptData & {
|
|
316
|
+
output: ModelRequestOutput;
|
|
317
|
+
};
|
|
295
318
|
"context.usage.updated": ContextUsageUpdatedData;
|
|
296
319
|
"context.revision.started": ContextRevisionStartedData;
|
|
297
320
|
"context.revision.finished": ContextRevisionFinishedData;
|
|
@@ -385,6 +408,7 @@ export type AgentEventInput =
|
|
|
385
408
|
| IterationEventInput<
|
|
386
409
|
| "agent.iteration.started"
|
|
387
410
|
| "model.request.started"
|
|
411
|
+
| "model.request.failed"
|
|
388
412
|
| "model.request.finished"
|
|
389
413
|
| "context.usage.updated"
|
|
390
414
|
| "context.shadow.planned"
|
package/src/mcp/mcp-manager.ts
CHANGED
|
@@ -58,11 +58,8 @@ export async function createMcpManager(
|
|
|
58
58
|
options: CreateMcpManagerOptions,
|
|
59
59
|
): Promise<McpManager> {
|
|
60
60
|
const clientFactory = options.clientFactory ?? stdioClientFactory;
|
|
61
|
-
const timeoutMs =
|
|
62
|
-
|
|
63
|
-
const maxObservationChars =
|
|
64
|
-
options.maxObservationChars ??
|
|
65
|
-
parsePositiveIntegerEnv("TINKER_MCP_MAX_OBSERVATION_CHARS");
|
|
61
|
+
const timeoutMs = options.timeoutMs;
|
|
62
|
+
const maxObservationChars = options.maxObservationChars;
|
|
66
63
|
const connections: McpClientConnection[] = [];
|
|
67
64
|
const executors: ToolExecutor[] = [];
|
|
68
65
|
const servers: McpServerInventory[] = [];
|
|
@@ -227,20 +224,6 @@ async function closeConnections(
|
|
|
227
224
|
}
|
|
228
225
|
}
|
|
229
226
|
|
|
230
|
-
function parsePositiveIntegerEnv(name: string): number | undefined {
|
|
231
|
-
const value = process.env[name];
|
|
232
|
-
if (value === undefined || value.trim() === "") {
|
|
233
|
-
return undefined;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
const parsed = Number(value);
|
|
237
|
-
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
238
|
-
throw new Error(`${name} must be a positive integer; received ${value}`);
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
return parsed;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
227
|
async function stdioClientFactory(
|
|
245
228
|
serverName: string,
|
|
246
229
|
serverConfig: McpServerConfig,
|
|
@@ -8,10 +8,9 @@ import type {
|
|
|
8
8
|
ToolExecutionContext,
|
|
9
9
|
ToolExecutor,
|
|
10
10
|
} from "../tools/types";
|
|
11
|
+
import { DEFAULT_PUBLIC_TOOLING_CONFIG } from "../cli/public-config-contract";
|
|
11
12
|
|
|
12
13
|
export const MCP_TOOL_NAME_PREFIX = "mcp__";
|
|
13
|
-
export const DEFAULT_MCP_TIMEOUT_MS = 60_000;
|
|
14
|
-
export const DEFAULT_MCP_MAX_OBSERVATION_CHARS = 40_000;
|
|
15
14
|
|
|
16
15
|
export function mcpToolName(serverName: string, toolName: string): string {
|
|
17
16
|
return `${MCP_TOOL_NAME_PREFIX}${serverName}__${toolName}`;
|
|
@@ -48,9 +47,9 @@ export function createMcpToolExecutor(
|
|
|
48
47
|
options: CreateMcpToolExecutorOptions,
|
|
49
48
|
): ToolExecutor {
|
|
50
49
|
const toolName = mcpToolName(options.serverName, options.tool.name);
|
|
51
|
-
const timeoutMs = options.timeoutMs ??
|
|
50
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_PUBLIC_TOOLING_CONFIG.mcpTimeoutMs;
|
|
52
51
|
const maxObservationChars =
|
|
53
|
-
options.maxObservationChars ??
|
|
52
|
+
options.maxObservationChars ?? DEFAULT_PUBLIC_TOOLING_CONFIG.mcpMaxObservationChars;
|
|
54
53
|
|
|
55
54
|
const base = {
|
|
56
55
|
toolName,
|
|
@@ -122,3 +122,32 @@ export type ModelUsage = {
|
|
|
122
122
|
promptCacheMissTokens?: number;
|
|
123
123
|
reasoningTokens?: number;
|
|
124
124
|
};
|
|
125
|
+
|
|
126
|
+
export type ProviderResponseErrorCode =
|
|
127
|
+
| "reasoning_only_assistant"
|
|
128
|
+
| "invalid_provider_response"
|
|
129
|
+
| "invalid_provider_stream"
|
|
130
|
+
| "provider_request_error";
|
|
131
|
+
|
|
132
|
+
export type ProviderResponseDiagnostics = {
|
|
133
|
+
provider: string;
|
|
134
|
+
model: string;
|
|
135
|
+
path?: string;
|
|
136
|
+
finishReason?: string;
|
|
137
|
+
contentChars?: number;
|
|
138
|
+
reasoningChars?: number;
|
|
139
|
+
toolCallCount?: number;
|
|
140
|
+
usage?: ModelUsage;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
export class ProviderResponseError extends Error {
|
|
144
|
+
constructor(
|
|
145
|
+
readonly code: ProviderResponseErrorCode,
|
|
146
|
+
message: string,
|
|
147
|
+
readonly diagnostics: ProviderResponseDiagnostics,
|
|
148
|
+
options?: ErrorOptions,
|
|
149
|
+
) {
|
|
150
|
+
super(message, options);
|
|
151
|
+
this.name = "ProviderResponseError";
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -14,21 +14,6 @@ export type ModelContextBudget = ModelContextProfile & {
|
|
|
14
14
|
triggerTokens: number;
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
export function readModelContextProfileFromEnv(
|
|
18
|
-
env: NodeJS.ProcessEnv = process.env,
|
|
19
|
-
): ModelContextProfile {
|
|
20
|
-
return createModelContextProfile({
|
|
21
|
-
contextWindowTokens: parseRequiredTokenCount(
|
|
22
|
-
env.TINKER_CONTEXT_WINDOW_TOKENS,
|
|
23
|
-
"TINKER_CONTEXT_WINDOW_TOKENS",
|
|
24
|
-
),
|
|
25
|
-
maxSupportedOutputTokens: parseRequiredTokenCount(
|
|
26
|
-
env.TINKER_MAX_SUPPORTED_OUTPUT_TOKENS,
|
|
27
|
-
"TINKER_MAX_SUPPORTED_OUTPUT_TOKENS",
|
|
28
|
-
),
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
|
|
32
17
|
export function createModelContextProfile(
|
|
33
18
|
input: ModelContextProfile,
|
|
34
19
|
): ModelContextProfile {
|
|
@@ -102,21 +87,6 @@ export function assertMatchingContextBudget(
|
|
|
102
87
|
}
|
|
103
88
|
}
|
|
104
89
|
|
|
105
|
-
function parseRequiredTokenCount(value: string | undefined, name: string): number {
|
|
106
|
-
if (value === undefined || value.trim() === "") {
|
|
107
|
-
throw new Error(`${name} is required; received ${displayValue(value)}.`);
|
|
108
|
-
}
|
|
109
|
-
if (!/^\d+$/.test(value)) {
|
|
110
|
-
throw new Error(
|
|
111
|
-
`${name} must be a positive safe integer token count; received ${displayValue(value)}.`,
|
|
112
|
-
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const parsed = Number(value);
|
|
116
|
-
requirePositiveSafeInteger(parsed, name, value);
|
|
117
|
-
return parsed;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
90
|
function requirePositiveSafeInteger(
|
|
121
91
|
value: number,
|
|
122
92
|
name: string,
|
|
@@ -5,7 +5,12 @@ import type {
|
|
|
5
5
|
UserMessage,
|
|
6
6
|
} from "../agent/types";
|
|
7
7
|
import type { RuntimeSessionContext } from "../agent/runtime-session";
|
|
8
|
-
import
|
|
8
|
+
import {
|
|
9
|
+
ProviderResponseError,
|
|
10
|
+
type ModelRequestOutput,
|
|
11
|
+
type ModelUsage,
|
|
12
|
+
type ProviderResponseDiagnostics,
|
|
13
|
+
} from "./model-client";
|
|
9
14
|
import type { ToolDefinition } from "../tools/types";
|
|
10
15
|
import type {
|
|
11
16
|
ChatCompletionAssistantMessageParam,
|
|
@@ -178,21 +183,45 @@ export function fromOpenAIChatCompletion(
|
|
|
178
183
|
"choices[0].message.content",
|
|
179
184
|
options,
|
|
180
185
|
);
|
|
186
|
+
const finishReason = optionalString(
|
|
187
|
+
choice.finish_reason,
|
|
188
|
+
"choices[0].finish_reason",
|
|
189
|
+
options,
|
|
190
|
+
);
|
|
191
|
+
const usage = parseUsage(completion.usage, options);
|
|
192
|
+
const reasoningContent = normalizeContent(
|
|
193
|
+
message.reasoning_content,
|
|
194
|
+
"choices[0].message.reasoning_content",
|
|
195
|
+
options,
|
|
196
|
+
);
|
|
197
|
+
const diagnostics = responseDiagnostics(options, {
|
|
198
|
+
path: "choices[0].message",
|
|
199
|
+
finishReason,
|
|
200
|
+
contentChars: content?.length ?? 0,
|
|
201
|
+
reasoningChars: reasoningContent?.length ?? 0,
|
|
202
|
+
toolCallCount: rawToolCalls.length,
|
|
203
|
+
usage,
|
|
204
|
+
});
|
|
181
205
|
if ((content === null || content.trim() === "") && rawToolCalls.length === 0) {
|
|
206
|
+
if (
|
|
207
|
+
finishReason === "stop" &&
|
|
208
|
+
reasoningContent !== null &&
|
|
209
|
+
reasoningContent.trim() !== ""
|
|
210
|
+
) {
|
|
211
|
+
throw new ProviderResponseError(
|
|
212
|
+
"reasoning_only_assistant",
|
|
213
|
+
`Invalid provider response (provider=${options.provider}, model=${options.model}): choices[0].message contains reasoning but neither non-empty final text nor tool calls.`,
|
|
214
|
+
diagnostics,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
182
217
|
throw providerResponseError(
|
|
183
218
|
options,
|
|
184
219
|
"choices[0].message",
|
|
185
220
|
"has neither non-empty text nor tool calls",
|
|
221
|
+
diagnostics,
|
|
186
222
|
);
|
|
187
223
|
}
|
|
188
224
|
|
|
189
|
-
const finishReason = optionalString(
|
|
190
|
-
choice.finish_reason,
|
|
191
|
-
"choices[0].finish_reason",
|
|
192
|
-
options,
|
|
193
|
-
);
|
|
194
|
-
const usage = parseUsage(completion.usage, options);
|
|
195
|
-
|
|
196
225
|
if (rawToolCalls.length > 0 && options.identity === undefined) {
|
|
197
226
|
throw providerResponseError(
|
|
198
227
|
options,
|
|
@@ -208,11 +237,7 @@ export function fromOpenAIChatCompletion(
|
|
|
208
237
|
message: {
|
|
209
238
|
role: "assistant",
|
|
210
239
|
content,
|
|
211
|
-
reasoningContent
|
|
212
|
-
message.reasoning_content,
|
|
213
|
-
"choices[0].message.reasoning_content",
|
|
214
|
-
options,
|
|
215
|
-
),
|
|
240
|
+
reasoningContent,
|
|
216
241
|
toolCalls: toolCalls.length === 0 ? undefined : toolCalls,
|
|
217
242
|
},
|
|
218
243
|
finishReason,
|
|
@@ -513,8 +538,22 @@ function providerResponseError(
|
|
|
513
538
|
options: { provider: string; model: string },
|
|
514
539
|
path: string,
|
|
515
540
|
detail: string,
|
|
516
|
-
|
|
517
|
-
|
|
541
|
+
diagnostics: ProviderResponseDiagnostics = responseDiagnostics(options, { path }),
|
|
542
|
+
): ProviderResponseError {
|
|
543
|
+
return new ProviderResponseError(
|
|
544
|
+
"invalid_provider_response",
|
|
518
545
|
`Invalid provider response (provider=${options.provider}, model=${options.model}): ${path} ${detail}.`,
|
|
546
|
+
{ ...diagnostics, path },
|
|
519
547
|
);
|
|
520
548
|
}
|
|
549
|
+
|
|
550
|
+
function responseDiagnostics(
|
|
551
|
+
options: { provider: string; model: string },
|
|
552
|
+
diagnostics: Omit<ProviderResponseDiagnostics, "provider" | "model">,
|
|
553
|
+
): ProviderResponseDiagnostics {
|
|
554
|
+
return {
|
|
555
|
+
provider: options.provider,
|
|
556
|
+
model: options.model,
|
|
557
|
+
...diagnostics,
|
|
558
|
+
};
|
|
559
|
+
}
|